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> accidentList = accidentEsService.getAllAccidentsFromES(); + sendToClients(accidentList); + + return emitter; + } + + public void sendToClients(List> accidentList) { + for (SseEmitter emitter : emitters) { + try { + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); + } catch (IOException e) { + emitter.completeWithError(e); + emitters.remove(emitter); + } + } + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java deleted file mode 100644 index 1ec6d57..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.ParkService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/parking") -@RequiredArgsConstructor -public class ParkingController { - - private final ParkService parkService; - - @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) - public String getParking(@PathVariable String areaId) { - return parkService.getParkingData(areaId); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java deleted file mode 100644 index 44bfd26..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.RoadService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/traffic") -@RequiredArgsConstructor -public class RoadController { - private final RoadService roadService; - - @GetMapping("/{area-id}") - public String getRoadData(@PathVariable("area-id") String id) { - // TrafficService 호출하여 OpenAPI 데이터를 JSON 형식으로 반환 - return roadService.getTrafficData(id); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java new file mode 100644 index 0000000..6beda5b --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -0,0 +1,107 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.AccidentEsService; +import com.example.externalinfoservice.service.ESRoadService; +import com.example.externalinfoservice.service.ParkEsService; +import com.example.externalinfoservice.service.WeatherEsService; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +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") +@Slf4j +public class StreamController { + private final List emitters = new CopyOnWriteArrayList<>(); + + private final WeatherEsService weatherEsService; + private final ESRoadService roadService; + private final ParkEsService parkEsService; + private final AccidentEsService accidentEsService; + + @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter stream() { + SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 + emitters.add(emitter); + + emitter.onCompletion(() -> emitters.remove(emitter)); + emitter.onTimeout(() -> emitters.remove(emitter)); + emitter.onError((e) -> emitters.remove(emitter)); + + // 구독시, 초기 데이터 주입 + try { + log.info("초기 데이터 푸시 중..."); + var weatherList = weatherEsService.getAllWeatherFromES(); + var trafficList = roadService.getTrafficData(); + var parkList = parkEsService.getAllParkFromES(); + var accidentList = accidentEsService.getAllAccidentsFromES(); + + emitter.send(SseEmitter.event() + .name("weather-update") + .data(weatherList)); + emitter.send(SseEmitter.event() + .name("traffic-update") + .data(trafficList)); + emitter.send(SseEmitter.event() + .name("park-update") + .data(parkList)); + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); + + log.info(".. 초기 데이터 푸시 완료"); + + } catch (IOException | IllegalStateException e) { + log.error("SSE 오류 발생"); + emitter.completeWithError(e); + emitters.remove(emitter); + } + + return emitter; + } + + // 주기적으로 클라이언트에게 push + public void sendToClients(List> weatherList, JsonNode trafficList, List> parkList, List> accidentList) { + for (SseEmitter emitter : emitters) { + try { + log.info("external 데이터 푸시 중..."); + + emitter.send(SseEmitter.event() + .name("weather-update") + .data(weatherList)); + log.info(".. 날씨 데이터 푸시 완료"); + + emitter.send(SseEmitter.event() + .name("traffic-update") + .data(trafficList)); + log.info(".. 도로현황 데이터 푸시 완료"); + + emitter.send(SseEmitter.event() + .name("park-update") + .data(parkList)); + log.info(".. 주차 데이터 푸시 완료"); + + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); + log.info(".. 사고 데이터 푸시 완료"); + + } catch (IOException | IllegalStateException e) { + log.error("SSE 오류 발생"); + emitter.completeWithError(e); + emitters.remove(emitter); + } + } + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java deleted file mode 100644 index 5b43eb9..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.WeatherService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/weather") -@RequiredArgsConstructor -public class WeatherController { - private final WeatherService weatherService; - - @GetMapping("/{place-code}") - public String getWeatherData(@PathVariable("place-code") String placeCode) { - return weatherService.getWeatherData(placeCode); - } - - @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { - String jsonData = weatherService.getWeatherData(placeCode); - return ResponseEntity.ok(jsonData); - } -} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java new file mode 100644 index 0000000..b35df16 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java @@ -0,0 +1,4 @@ +//package com.example.externalinfoservice.dto; +// +//public class AccidentDto { +//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java index 1deb47b..d8d0690 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -1,74 +1,74 @@ -package com.example.externalinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.xml.bind.annotation.*; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "SeoulRtd.citydata") -@XmlAccessorType(XmlAccessType.FIELD) -@JsonIgnoreProperties(ignoreUnknown = true) -public class ParkDto { - - @XmlElement(name = "CITYDATA") - private CityData cityData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class CityData { - - @XmlElement(name = "PRK_STTS") - private PrkSttsWrapper prktts; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class PrkSttsWrapper { - - @XmlElement(name = "PRK_STTS") - private List prkStts; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class ParkingStatus { - - @XmlElement(name = "PRK_NM") - private String prk_nm; - - @XmlElement(name = "PRK_CD") - private String prk_cd; - - @XmlElement(name = "PRK_TYPE") - private String prk_type; - - @XmlElement(name = "CPCTY") - private int cpcty; - - @XmlElement(name = "CUR_PRK_CNT") - private String cur_prk_cnt; - - @XmlElement(name = "CUR_PRK_TIME") - private String cur_prk_time; - - @XmlElement(name = "CUR_PRK_YN") - private String cur_prk_yn; - - @XmlElement(name = "PAY_YN") - private String pay_yn; - - @XmlElement(name = "RATES") - private int rates; - - @XmlElement(name = "TIME_RATES") - private int time_rates; - - @XmlElement(name = "ADD_RATES") - private int add_rates; - - @XmlElement(name = "ADD_TIME_RATES") - private int add_time_rates; - } -} +//package com.example.externalinfoservice.dto; +// +//import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +//import jakarta.xml.bind.annotation.*; +//import lombok.Data; +// +//import java.util.List; +// +//@Data +//@XmlRootElement(name = "SeoulRtd.citydata") +//@XmlAccessorType(XmlAccessType.FIELD) +//@JsonIgnoreProperties(ignoreUnknown = true) +//public class ParkDto { +// +// @XmlElement(name = "CITYDATA") +// private CityData cityData; +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class CityData { +// +// @XmlElement(name = "PRK_STTS") +// private PrkSttsWrapper prktts; +// } +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class PrkSttsWrapper { +// +// @XmlElement(name = "PRK_STTS") +// private List prkStts; +// } +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class ParkingStatus { +// +// @XmlElement(name = "PRK_NM") +// private String prk_nm; +// +// @XmlElement(name = "PRK_CD") +// private String prk_cd; +// +// @XmlElement(name = "PRK_TYPE") +// private String prk_type; +// +// @XmlElement(name = "CPCTY") +// private int cpcty; +// +// @XmlElement(name = "CUR_PRK_CNT") +// private String cur_prk_cnt; +// +// @XmlElement(name = "CUR_PRK_TIME") +// private String cur_prk_time; +// +// @XmlElement(name = "CUR_PRK_YN") +// private String cur_prk_yn; +// +// @XmlElement(name = "PAY_YN") +// private String pay_yn; +// +// @XmlElement(name = "RATES") +// private int rates; +// +// @XmlElement(name = "TIME_RATES") +// private int time_rates; +// +// @XmlElement(name = "ADD_RATES") +// private int add_rates; +// +// @XmlElement(name = "ADD_TIME_RATES") +// private int add_time_rates; +// } +//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java deleted file mode 100644 index ae24bad..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.example.externalinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "SeoulRtd.citydata") -@XmlAccessorType(XmlAccessType.FIELD) -public class RoadDto { - - @XmlElement(name = "CITYDATA") - @JsonProperty("city_data") - private CityData cityData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class CityData { - @XmlElement(name = "ROAD_TRAFFIC_STTS") - @JsonProperty("road_traffic_stts") - private List roadTrafficSttsList; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficStts { - @XmlElement(name = "AVG_ROAD_DATA") - @JsonProperty("avg_road_data") - private AvgRoadData avgRoadData; - - @XmlElement(name = "ROAD_TRAFFIC_STTS") - @JsonProperty("road_traffic_stts") - private RoadTrafficDetail roadTrafficDetail; - } - - - @XmlAccessorType(XmlAccessType.FIELD) - public static class AvgRoadData { - @XmlElement(name = "ROAD_MSG") - @JsonProperty("road_msg") - private String roadMsg; - - @XmlElement(name = "ROAD_TRAFFIC_IDX") - @JsonProperty("road_traffic_idx") - private String trafficIdx; - - @XmlElement(name = "ROAD_TRAFFIC_SPD") - @JsonProperty("road_traffic_spd") - private int trafficSpeed; - - @XmlElement(name = "ROAD_TRAFFIC_TIME") - @JsonProperty("road_traffic_time") - private String trafficTime; - - // Getters & Setters - } - - @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficDetail { - @XmlElement(name = "LINK_ID") - @JsonProperty("link_id") - private String linkId; - - @XmlElement(name = "ROAD_NM") - @JsonProperty("road_nm") - private String roadName; - - @XmlElement(name = "START_ND_CD") - @JsonProperty("start_nd_cd") - private String startNodeCode; - - @XmlElement(name = "START_ND_NM") - @JsonProperty("start_nd_nm") - private String startNodeName; - - @XmlElement(name = "START_ND_XY") - @JsonProperty("start_nd_xy") - private String startCoordinates; - - @XmlElement(name = "END_ND_CD") - @JsonProperty("end_nd_cd") - private String endNodeCode; - - @XmlElement(name = "END_ND_NM") - @JsonProperty("end_nd_nm") - private String endNodeName; - - @XmlElement(name = "END_ND_XY") - @JsonProperty("end_nd_xy") - private String endCoordinates; - - @XmlElement(name = "DIST") - @JsonProperty("dist") - private double distance; - - @XmlElement(name = "SPD") - @JsonProperty("spd") - private double speed; - - @XmlElement(name = "IDX") - @JsonProperty("idx") - private String congestionIndex; - - @XmlElement(name = "XYLIST") - @JsonProperty("xylist") - private String xyList; - - // Getters & Setters - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java deleted file mode 100644 index 3570ae6..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.externalinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -@Data -@XmlRootElement(name = "SeoulRtd.citydata") -@XmlAccessorType(XmlAccessType.FIELD) -public class WeatherDto { - @XmlElement(name = "WEATHER_STTS") - @JsonProperty("weather_status") - private WeatherStatus weatherStatus; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class WeatherStatus { - @XmlElement(name = "TEMP") - @JsonProperty("temperature") - private Double temperature; - - - @XmlElement(name = "PRECIPITATION") - @JsonProperty("precipitation") - private Double precipitation; - - @XmlElement(name = "PRECPT_TYPE") - @JsonProperty("precipitation_type") - private String precipitationType; - - @XmlElement(name = "PM10") - @JsonProperty("pm10") - private Integer pm10; - - @XmlElement(name = "FCST24HOURS") - @JsonProperty("forecast24hours") - private String forecast24hours; - - @XmlElement(name = "SKY_STTS") - @JsonProperty("sky_status") - private String skyStatus; - } -} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java new file mode 100644 index 0000000..c483e8b --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java @@ -0,0 +1,46 @@ +//package com.example.externalinfoservice.dto; +// +//import com.fasterxml.jackson.annotation.JsonProperty; +//import jakarta.xml.bind.annotation.XmlAccessType; +//import jakarta.xml.bind.annotation.XmlAccessorType; +//import jakarta.xml.bind.annotation.XmlElement; +//import jakarta.xml.bind.annotation.XmlRootElement; +//import lombok.Data; +// +//@Data +//@XmlRootElement(name = "SeoulRtd.citydata") +//@XmlAccessorType(XmlAccessType.FIELD) +//public class WeatherDtoOriginal { +// @XmlElement(name = "WEATHER_STTS") +// @JsonProperty("weather_status") +// private WeatherStatus weatherStatus; +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class WeatherStatus { +// @XmlElement(name = "TEMP") +// @JsonProperty("temperature") +// private Double temperature; +// +// +// @XmlElement(name = "PRECIPITATION") +// @JsonProperty("precipitation") +// private Double precipitation; +// +// @XmlElement(name = "PRECPT_TYPE") +// @JsonProperty("precipitation_type") +// private String precipitationType; +// +// @XmlElement(name = "PM10") +// @JsonProperty("pm10") +// private Integer pm10; +// +// @XmlElement(name = "FCST24HOURS") +// @JsonProperty("forecast24hours") +// private String forecast24hours; +// +// @XmlElement(name = "SKY_STTS") +// @JsonProperty("sky_status") +// private String skyStatus; +// } +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java new file mode 100644 index 0000000..23c7ac6 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java @@ -0,0 +1,8 @@ +//package com.example.externalinfoservice.dto; +// +//import lombok.Data; +// +//@Data +//public class WeatherRequest { +// private String area; +//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java new file mode 100644 index 0000000..2669175 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java @@ -0,0 +1,36 @@ +//package com.example.externalinfoservice.entity; +// +//import jakarta.persistence.Column; +//import jakarta.persistence.Entity; +//import jakarta.persistence.Id; +//import jakarta.persistence.Table; +//import lombok.Getter; +//import lombok.Setter; +// +//@Entity +//@Table(name = "area") +//@Getter +//@Setter +//public class Area { +// @Id +// @Column(name = "area_id") +// private Long areaId; +// +// @Column(name = "seoul_id") +// private String seoulId; +// +// @Column(name = "name") +// private String name; +// +// @Column(name = "name_eng") +// private String nameEng; +// +// @Column(name = "category") +// private String category; +// +// @Column(name = "lat") +// private Double latitude; +// +// @Column(name = "lon") +// private Double longitude; +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java new file mode 100644 index 0000000..223cfa2 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java @@ -0,0 +1,12 @@ +//package com.example.externalinfoservice.repository; +// +//import com.example.externalinfoservice.entity.Area; +//import org.springframework.data.jpa.repository.JpaRepository; +//import org.springframework.stereotype.Repository; +// +//import java.util.Optional; +// +//@Repository +//public interface AreaRepository extends JpaRepository { +// Optional findBySeoulId(String seoulId); +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java new file mode 100644 index 0000000..6588156 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java @@ -0,0 +1,33 @@ +package com.example.externalinfoservice.scheduler; + +import com.example.externalinfoservice.controller.StreamController; +import com.example.externalinfoservice.service.AccidentEsService; +import com.example.externalinfoservice.service.ESRoadService; +import com.example.externalinfoservice.service.ParkEsService; +import com.example.externalinfoservice.service.WeatherEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PushScheduler { + + private final WeatherEsService weatherEsService; + private final ESRoadService roadService; + private final ParkEsService parkEsService; + private final StreamController streamController; + private final AccidentEsService accidentEsService; + + + // 5분마다 날씨 데이터를 SSE 클라이언트에 push + @Scheduled(fixedRate = 300_000) // 5분 = 300초 + public void pushWeatherToClients() { + var weatherList = weatherEsService.getAllWeatherFromES(); + var trafficList = roadService.getTrafficData(); + var parkList = parkEsService.getAllParkFromES(); + var accidentList = accidentEsService.getAllAccidentsFromES(); + streamController.sendToClients(weatherList, trafficList, parkList,accidentList); + } + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java new file mode 100644 index 0000000..7d3e9c6 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -0,0 +1,101 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AccidentEsService { + + private final RestTemplate restTemplate = new RestTemplate(); // 주입 방식으로 대체해도 OK + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Value("${elastic_url}") + private String elastic_url; + + public JsonNode getAccidentData() { + try { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String apiUrl = String.format(elastic_url + "/seoul_citydata_accident_%s/_search", today); + + String jsonBody = """ + { + "query": { + "bool": { + "must": [ + { + "range": { + "accident.exp_clr_dt": { + "gt": "now" + } + } + } + ] + } + }, + "collapse": { + "field": "accident.search" + }, + "size": 100 + } + """; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); + + ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class); + String body = response.getBody(); + + JsonNode root = objectMapper.readTree(body); + JsonNode hits = root.path("hits").path("hits"); + + List accidentList = new ArrayList<>(); + for (JsonNode hit : hits) { + JsonNode accident = hit.path("_source").path("accident"); + accidentList.add(accident); + } + + return objectMapper.valueToTree(accidentList); + + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch (Exception e) { + log.error(String.valueOf(e)); + throw new RuntimeException("사고 현황 : 예상치 못한 오류", e); + } + } + + public List> getAllAccidentsFromES() { + try { + JsonNode accidentArray = getAccidentData(); + List> list = new ArrayList<>(); + + for (JsonNode node : accidentArray) { + Map map = objectMapper.convertValue(node, Map.class); + list.add(map); + } + + return list; + } catch (Exception e) { + throw new RuntimeException("사고 데이터 변환 실패", e); + } + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java new file mode 100644 index 0000000..60c32c4 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -0,0 +1,100 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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 ESRoadService { + + private static final RestTemplate restTemplate = new RestTemplate(); + + @Value("${elastic_url}") + private String elastic_url; + + public JsonNode getTrafficData() { + try { + // 오늘 날짜 구하기 + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String apiUrl = String.format(elastic_url + "/seoul_citydata_road_%s/_search", today); + + // JSON Body 생성 + String jsonBody = String.format("{\n" + + " \"size\": 0,\n" + + " \"aggs\": {\n" + + " \"by_area\": {\n" + + " \"terms\": {\n" + + " \"field\": \"road_traffic.area_nm\",\n" + + " \"size\": 1000 // area_nm 종류 수만큼 충분히\n" + + " },\n" + + " \"aggs\": {\n" + + " \"latest_hit\": {\n" + + " \"top_hits\": {\n" + + " \"size\": 1,\n" + + " \"sort\": [\n" + + " {\n" + + " \"road_traffic.road_traffic_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(); + // road_traffic 데이터만 가져오기 + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(String.valueOf(body)); // response는 JSON 문자열 + + JsonNode buckets = root + .path("aggregations") + .path("by_area") + .path("buckets"); + + List roadTrafficList = new ArrayList<>(); + for (JsonNode bucket : buckets) { + JsonNode latestHit = bucket.path("latest_hit").path("hits").path("hits").get(0); // 첫 번째 hit 선택 + JsonNode roadTraffic = latestHit.path("_source").path("road_traffic"); + roadTrafficList.add(roadTraffic); + } + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode result = objectMapper.valueToTree(roadTrafficList); + + return result; + + }catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + }catch (Exception e){ + log.error(String.valueOf(e)); + throw new RuntimeException("도로 현황 데이터 : 예상치 못한 오류",e); + } + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java new file mode 100644 index 0000000..a89003f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java @@ -0,0 +1,123 @@ +package com.example.externalinfoservice.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ParkEsService { + + private final RestTemplate restTemplate = new RestTemplate(); + + @Value("${elastic_url}") + private String elastic_url; + + // 오늘 날짜 기반 인덱스 URL 생성 + private String getTodayIndexUrl() { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + return String.format(elastic_url + "/seoul_citydata_parking_%s/_search", today); + } + +// // 특정 지역 주차장 정보 +// public Map getParkFromES(String areaId) { +// String url = getTodayIndexUrl(); +// log.info("Requesting park info for areaId: {}", areaId); +// +// Map term = Map.of("parking.area_nm", areaId); +// Map query = Map.of("term", term); +// Map body = Map.of("size", 100, "query", query); +// +// HttpHeaders headers = new HttpHeaders(); +// headers.setContentType(MediaType.APPLICATION_JSON); +// +// HttpEntity> entity = new HttpEntity<>(body, headers); +// log.info("Request body: {}", body); +// +// ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); +// log.info("Response status: {}", response.getStatusCode()); +// +// List> hits = (List>) +// ((Map) response.getBody().get("hits")).get("hits"); +// +// if (hits.isEmpty()) { +// log.warn("No results found for areaId: {}", areaId); +// return null; +// } +// +// Map source = (Map) hits.get(0).get("_source"); +// Map parking = (Map) source.get("parking"); +// +// log.info("Parking data: {}", parking); +// return parking; +// } + + // 전체 지역 주차장 정보 + public List> getAllParkFromES() { + String url = getTodayIndexUrl(); + // log.info("Requesting all park info"); + + Map body = Map.of( + "size", 0, + "aggs", Map.of( + "by_area", Map.of( + "terms", Map.of("field", "parking.area_nm", "size", 1000), + "aggs", Map.of( + "latest_hit", Map.of( + "top_hits", Map.of( + "size", 1, + "sort", List.of(Map.of("_score", Map.of("order", "desc"))) + ) + ) + ) + ) + ) + ); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + log.info("Response status: {}", response.getStatusCode()); + + if (!response.getBody().containsKey("aggregations")) { + log.error("Response does not contain 'aggregations' key"); + return Collections.emptyList(); + } + + Map aggregations = (Map) response.getBody().get("aggregations"); + Map byArea = (Map) aggregations.get("by_area"); + List> buckets = (List>) byArea.get("buckets"); + + List> results = new ArrayList<>(); + for (Map bucket : buckets) { + Map latestHitWrapper = (Map) bucket.get("latest_hit"); + Map hitsWrapper = (Map) latestHitWrapper.get("hits"); + List> hits = (List>) hitsWrapper.get("hits"); + if (hits.isEmpty()) continue; + + Map latestHit = hits.get(0); + Map source = (Map) latestHit.get("_source"); + Map parkData = (Map) source.get("parking"); + + if (parkData != null) { + results.add(parkData); +// log.info("Added park data for area: {}", parkData.get("area_nm")); + } else { + log.warn("Parking data is null for a hit"); + } + } + + log.info("주차 데이터 : {} park results", results.size()); + return results; + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java deleted file mode 100644 index 8507da0..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.ParkDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.StringReader; - -@Service -public class ParkService { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - - public ParkService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getParkingData(String areaId) { - try { - String url = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaId - ); - - // XML 요청 - String xmlResponse = restTemplate.getForObject(url, String.class); - - // XML -> DTO - ParkDto parkDto = xmlToDto(xmlResponse); - - // DTO -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); - - } catch (Exception e) { - throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); - } - } - - private ParkDto xmlToDto(String xml) throws Exception { - JAXBContext context = JAXBContext.newInstance(ParkDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java deleted file mode 100644 index 40ea77f..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.RoadDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.JAXBException; -import jakarta.xml.bind.Unmarshaller; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.StringReader; - -@Service -public class RoadService { - - @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata/1/5/") - private String openApiUrl; - - private final RestTemplate restTemplate; - - public RoadService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getTrafficData(String id) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 - try { - // DTO -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); - - return json; - } catch (Exception e) { - throw new RuntimeException("XML 파싱 실패", e); - } - } - - private RoadDto xmlToDto(String xml) throws Exception { - // JAXB를 사용해서 XML을 DTO로 변환 - JAXBContext context = JAXBContext.newInstance(RoadDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (RoadDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java new file mode 100644 index 0000000..febc0ac --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -0,0 +1,96 @@ +package com.example.externalinfoservice.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class WeatherEsService { + + private final RestTemplate restTemplate = new RestTemplate(); + + @Value("${elastic_url}") + private String elastic_url; + + // 오늘 날짜 기반 인덱스 이름 생성 + private String getTodayIndex() { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + return String.format(elastic_url + "/seoul_citydata_weather_%s/_search", today); + } + + // 특정 지역 날씨 조회 + public Map getWeatherFromES(String area) { + String url = getTodayIndex(); + + Map term = Map.of("weather.area_nm", area); + Map query = Map.of("term", term); + Map body = Map.of("size", 100, "query", query); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + + List> hits = (List>) + ((Map) response.getBody().get("hits")).get("hits"); + + if (hits.isEmpty()) return null; + + Map source = (Map) hits.get(0).get("_source"); + return (Map) source.get("weather"); + } + + // 전체 지역 날씨 조회 + public List> getAllWeatherFromES() { + String url = getTodayIndex(); + + Map body = Map.of( + "size", 0, + "aggs", Map.of( + "by_area", Map.of( + "terms", Map.of("field", "weather.area_nm", "size", 1000), + "aggs", Map.of( + "latest_hit", Map.of( + "top_hits", Map.of( + "size", 1, + "sort", List.of(Map.of("weather.weather_time", Map.of("order", "desc"))) + ) + ) + ) + ) + ) + ); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + + Map aggregations = (Map) response.getBody().get("aggregations"); + Map byArea = (Map) aggregations.get("by_area"); + List> buckets = (List>) byArea.get("buckets"); + + List> results = new ArrayList<>(); + for (Map bucket : buckets) { + Map latestHitWrapper = (Map) bucket.get("latest_hit"); + Map hitsWrapper = (Map) latestHitWrapper.get("hits"); + List> hits = (List>) hitsWrapper.get("hits"); + if (hits.isEmpty()) continue; + + Map latestHit = hits.get(0); + Map source = (Map) latestHit.get("_source"); + results.add((Map) source.get("weather")); + } + + return results; + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java deleted file mode 100644 index 7686729..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayInputStream; -import java.util.Set; -import java.util.HashSet; - -@Service -public class WeatherService { - - private final String weatherApiUrl; - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - - // 날씨 관련 필드 목록 - private static final Set WEATHER_FIELDS = new HashSet<>() {{ - add("TEMP"); // 기온 - add("PRECIPITATION"); // 강수량 - add("PRECPT_TYPE"); // 강수 형태 - add("PM10"); // 미세먼지 - add("FCST24HOURS"); // 24시간 예보 - add("SKY_STTS"); // 하늘 상태 - }}; - - public WeatherService(RestTemplate restTemplate, ObjectMapper objectMapper) { - this.weatherApiUrl = "http://openapi.seoul.go.kr:8088/594b4a6559796f683930466466666d/xml/citydata/1/5/"; - this.restTemplate = restTemplate; - this.objectMapper = objectMapper; - } - - public String getWeatherData(String placeCode) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(weatherApiUrl + placeCode, String.class); - - // 전체 XML 응답 출력 - System.out.println("XML Response: " + xmlResponse); - - try { - // XML 파싱을 위한 DocumentBuilder 생성 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // 모든 노드 이름 출력 - NodeList allNodes = document.getElementsByTagName("*"); - System.out.println("Total nodes: " + allNodes.getLength()); - for (int i = 0; i < allNodes.getLength(); i++) { - System.out.println("Node " + i + ": " + allNodes.item(i).getNodeName()); - } - - // JSON 객체 생성 - ObjectNode weatherJson = objectMapper.createObjectNode(); - ObjectNode weatherSttsJson = objectMapper.createObjectNode(); - - // 원하는 필드들 - String[] desiredFields = { - "TEMP", "PRECIPITATION", "PRECPT_TYPE", - "PM10", "FCST24HOURS", "SKY_STTS" - }; - - // 모든 WEATHER_STTS 하위 노드 탐색 - NodeList weatherSttsNodes = document.getElementsByTagName("WEATHER_STTS"); - System.out.println("WEATHER_STTS nodes count: " + weatherSttsNodes.getLength()); - - // 각 필드별로 특별 처리 - for (String field : desiredFields) { - NodeList fieldNodes = document.getElementsByTagName(field); - System.out.println(field + " nodes count: " + fieldNodes.getLength()); - - // 각 필드에 대해 값 추출 - if (fieldNodes.getLength() > 0) { - Node fieldNode = fieldNodes.item(0); - String nodeValue = fieldNode.getTextContent().trim(); - System.out.println(field + " value: " + nodeValue); - - if (field.equals("FCST24HOURS")) { - // FCST24HOURS는 배열로 처리 - ArrayNode forecastArray = objectMapper.createArrayNode(); - NodeList fcstChildren = fieldNode.getChildNodes(); - - for (int j = 0; j < fcstChildren.getLength(); j++) { - Node fcstChild = fcstChildren.item(j); - if (fcstChild.getNodeType() == Node.ELEMENT_NODE) { - Element fcstElement = (Element) fcstChild; - ObjectNode forecastItem = objectMapper.createObjectNode(); - NodeList itemChildren = fcstElement.getChildNodes(); - - for (int k = 0; k < itemChildren.getLength(); k++) { - Node itemChild = itemChildren.item(k); - if (itemChild.getNodeType() == Node.ELEMENT_NODE) { - Element itemElement = (Element) itemChild; - String itemNodeName = itemElement.getNodeName(); - String itemNodeValue = itemElement.getTextContent().trim(); - - if (!itemNodeValue.isEmpty()) { - forecastItem.put(convertToCamelCase(itemNodeName), itemNodeValue); - } - } - } - - if (!forecastItem.isEmpty()) { - forecastArray.add(forecastItem); - } - } - } - - weatherSttsJson.set(convertToCamelCase(field), forecastArray); - } else if (!nodeValue.isEmpty()) { - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } - } - } - - // weatherStts 필드에 추출된 정보 저장 - weatherJson.set("weatherStts", weatherSttsJson); - - // JSON을 예쁘게 출력 (들여쓰기 2칸) - String jsonString = objectMapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(weatherJson); - // JSON 문자열에 줄바꿈 문자가 제대로 유지되는지 확인 - System.out.println("Formatted JSON: " + jsonString); - return jsonString; - - } catch (Exception e) { - e.printStackTrace(); // 전체 스택 트레이스 출력 - throw new RuntimeException("날씨 데이터 변환 실패", e); - } - } - - // 언더스코어 이름을 camelCase로 변환 - private String convertToCamelCase(String input) { - if (input == null || input.isEmpty()) return input; - - StringBuilder result = new StringBuilder(); - boolean capitalizeNext = false; - - for (char c : input.toLowerCase().toCharArray()) { - if (c == '_') { - capitalizeNext = true; - } else if (capitalizeNext) { - result.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - result.append(c); - } - } - - return result.toString(); - } -} \ No newline at end of file diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties deleted file mode 100644 index ee75722..0000000 --- a/external-info-service/src/main/resources/application.properties +++ /dev/null @@ -1,13 +0,0 @@ -spring.application.name=external-info-service - - -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin -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 - -seoul.api.key=505a6255746b696d34344b46566174 diff --git a/external-info-service/src/main/resources/static/weather-stream-test.html b/external-info-service/src/main/resources/static/weather-stream-test.html new file mode 100644 index 0000000..ff848ac --- /dev/null +++ b/external-info-service/src/main/resources/static/weather-stream-test.html @@ -0,0 +1,26 @@ + + + + + SSE Test + + +

실시간 날씨 스트림

+

+
+
+
+
diff --git a/gateway/.gitattributes b/gateway/.gitattributes
new file mode 100644
index 0000000..8af972c
--- /dev/null
+++ b/gateway/.gitattributes
@@ -0,0 +1,3 @@
+/gradlew text eol=lf
+*.bat text eol=crlf
+*.jar binary
diff --git a/gateway/.gitignore b/gateway/.gitignore
new file mode 100644
index 0000000..15c7cd6
--- /dev/null
+++ b/gateway/.gitignore
@@ -0,0 +1,42 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+src/main/resources/application.properties
+src/main/resources/application.yml
+src/main/resources/application.txt
+
+.env
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/gateway/Dockerfile b/gateway/Dockerfile
new file mode 100644
index 0000000..568e28d
--- /dev/null
+++ b/gateway/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 ./gateway.jar
+# 포트지정
+
+# env 파일 추가
+#COPY .env .env
+
+EXPOSE 8080
+
+# 서버가동
+CMD ["java", "-jar", "gateway.jar"]
\ No newline at end of file
diff --git a/gateway/build.gradle b/gateway/build.gradle
new file mode 100644
index 0000000..bb43c52
--- /dev/null
+++ b/gateway/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+    id 'java'
+    id 'org.springframework.boot' version '3.4.4'
+    id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'com.example'
+version = '0.0.1-SNAPSHOT'
+
+java {
+    toolchain {
+        languageVersion = JavaLanguageVersion.of(17)
+    }
+}
+
+repositories {
+    mavenCentral()
+}
+
+dependencyManagement {
+    imports {
+        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2024.0.1'
+    }
+}
+
+dependencies {
+    implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
+
+    implementation 'org.springframework.boot:spring-boot-starter'
+    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
+    implementation 'org.springframework.boot:spring-boot-starter-webflux'
+    testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+     // 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'
+
+}
+
+tasks.named('test') {
+    useJUnitPlatform()
+}
diff --git a/gateway/gradle/wrapper/gradle-wrapper.jar b/gateway/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..9bbc975
Binary files /dev/null and b/gateway/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gateway/gradle/wrapper/gradle-wrapper.properties b/gateway/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/gateway/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gateway/gradlew b/gateway/gradlew
new file mode 100644
index 0000000..faf9300
--- /dev/null
+++ b/gateway/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gateway/gradlew.bat b/gateway/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/gateway/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/gateway/settings.gradle b/gateway/settings.gradle
new file mode 100644
index 0000000..a239fa0
--- /dev/null
+++ b/gateway/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'gateway'
diff --git a/gateway/src/main/java/com/example/gateway/GatewayApplication.java b/gateway/src/main/java/com/example/gateway/GatewayApplication.java
new file mode 100644
index 0000000..b92f26a
--- /dev/null
+++ b/gateway/src/main/java/com/example/gateway/GatewayApplication.java
@@ -0,0 +1,13 @@
+package com.example.gateway;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class GatewayApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(GatewayApplication.class, args);
+    }
+
+}
diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java
new file mode 100644
index 0000000..7564b9d
--- /dev/null
+++ b/gateway/src/main/java/com/example/gateway/config/Cors.java
@@ -0,0 +1,41 @@
+package com.example.gateway.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.reactive.CorsWebFilter;
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
+
+import java.util.List;
+
+@Configuration
+public class Cors {
+
+    @Bean
+    public CorsWebFilter corsWebFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true); // 인증 정보 포함 X
+        config.setAllowedOrigins(List.of(
+                "https://localhost:5173",
+                "https://www.seoultravel.life",
+                "http://localhost:5173",
+                "http://192.168.0.186:5173",
+                "http://58.127.241.84:5173",
+                "http://map.seoultravel.life",
+                "http://www.seoultravel.life"
+        ));
+        config.setAllowedHeaders(List.of("Origin", "Content-Type", "Accept", "Authorization"));
+        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+
+        // expose Content-Type so it can be sent with the response
+        config.setExposedHeaders(List.of("Content-Type", "Cache-Control"));
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config); // 모든 경로에 적용
+
+        return new CorsWebFilter(source);
+    }
+
+
+
+}
diff --git a/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java b/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java
new file mode 100644
index 0000000..0c9e48c
--- /dev/null
+++ b/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java
@@ -0,0 +1,13 @@
+package com.example.gateway;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class GatewayApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}
diff --git a/place-service/.gitignore b/place-service/.gitignore
index c2065bc..8778a85 100644
--- a/place-service/.gitignore
+++ b/place-service/.gitignore
@@ -4,6 +4,9 @@ build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
 !**/src/test/**/build/
+src/main/resources/application.properties
+.env
+.dockerignore
 
 ### STS ###
 .apt_generated
diff --git a/place-service/Dockerfile b/place-service/Dockerfile
new file mode 100644
index 0000000..dfd3b8c
--- /dev/null
+++ b/place-service/Dockerfile
@@ -0,0 +1,63 @@
+## Gradle
+#FROM gradle:8.13.0-jdk17 AS build
+#WORKDIR /app
+#COPY . .
+#RUN gradle build -x test
+#
+#FROM eclipse-temurin:17-jdk
+#WORKDIR /app
+#COPY --from=build /app/build/libs/*.jar app.jar
+#EXPOSE 8080
+#ENTRYPOINT ["java", "-jar", "app.jar"]
+#
+#ARG TOUR_API_KEY
+#
+#COPY .env .env
+
+
+# 테스트용
+
+# 멀티스테이징 도커 파일 구성
+
+# 소스코드를 빌드
+# 빌드의 결과물 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 ./place.jar
+# 포트지정
+
+# env 파일 추가
+COPY .env .env
+
+EXPOSE 8080
+
+# 서버가동
+CMD ["java", "-jar", "place.jar"]
\ No newline at end of file
diff --git a/place-service/build.gradle b/place-service/build.gradle
index e094fb2..edd0d65 100644
--- a/place-service/build.gradle
+++ b/place-service/build.gradle
@@ -24,22 +24,49 @@ repositories {
 }
 
 dependencies {
+    implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
+
+    // redis 추가
+    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    implementation 'com.fasterxml.jackson.core:jackson-databind'
+
+
+    // JAXB (XML 파싱)
+    implementation 'javax.xml.bind:jaxb-api:2.3.1'
+    // JAXB Runtime (XML 파싱을 위해 필요)
+    implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
+    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
+
+    // 캐싱
+    implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
+    implementation 'org.springframework.boot:spring-boot-starter-cache'
+
+
+     // 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'
+
     // 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'
 
-
     implementation 'org.postgresql:postgresql:42.7.2'
     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'
     implementation 'org.springframework.boot:spring-boot-starter-security'
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'org.springframework.boot:spring-boot-starter-webflux'
+    //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전
+    implementation 'io.github.cdimascio:dotenv-java:2.2.4'
     implementation 'org.apache.kafka:kafka-streams'
     implementation 'org.springframework.kafka:spring-kafka'
     implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
+    implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0'
+    implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
+
     compileOnly 'org.projectlombok:lombok'
     developmentOnly 'org.springframework.boot:spring-boot-devtools'
     runtimeOnly 'org.postgresql:postgresql'
@@ -56,4 +83,4 @@ tasks.named('bootBuildImage') {
 
 tasks.named('test') {
     useJUnitPlatform()
-}
+}
\ No newline at end of file
diff --git a/place-service/docker-compose.yml b/place-service/docker-compose.yml
new file mode 100644
index 0000000..b4dc6bd
--- /dev/null
+++ b/place-service/docker-compose.yml
@@ -0,0 +1,30 @@
+version: '3'
+services:
+  db:
+    image: postgres:16.8
+    container_name: my_postgres
+    environment:
+      POSTGRES_DB: stars_db
+      POSTGRES_USER: root
+      POSTGRES_PASSWORD: admin
+    ports:
+      - "5432:5432"
+    volumes:
+      - pgdata:/var/lib/postgresql/data
+  app:
+    build: .
+    container_name: my_spring_app
+    environment:
+      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/stars_db
+      SPRING_DATASOURCE_USERNAME: root
+      SPRING_DATASOURCE_PASSWORD: admin
+
+    env_file: # .env 파일 로드
+      - .env
+    ports:
+      - "8080:8080"
+    depends_on:
+      - db
+      -
+volumes:
+  pgdata:
diff --git a/place-service/gradlew b/place-service/gradlew
old mode 100755
new mode 100644
index faf9300..48117b8
--- a/place-service/gradlew
+++ b/place-service/gradlew
@@ -1,4 +1,4 @@
-#!/bin/sh
+ #!/bin/sh
 
 #
 # Copyright © 2015-2021 the original authors.
diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index 7aaef28..f2689a8 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -2,12 +2,45 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
 
 @SpringBootApplication
+@EnableAsync
 public class PlaceServiceApplication {
 
     public static void main(String[] args) {
+        // .env 로드하고 시스템 환경변수로 설정
+        io.github.cdimascio.dotenv.Dotenv dotenv = io.github.cdimascio.dotenv.Dotenv.load();
+        System.setProperty("KAKAO_API_KEY", dotenv.get("KAKAO_API_KEY"));
+
         SpringApplication.run(PlaceServiceApplication.class, args);
     }
 
-}
+    /*
+    @Bean
+
+    public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository, CafeService cafeService, CafeRepository cafeRepository) {
+        return args -> {
+            // 음식점 데이터 초기화
+            if (restaurantRepository.count() == 0) {
+                System.out.println("음식점 데이터 저장 시작");
+                restaurantService.fetchAndSaveRestaurants();
+                System.out.println("음식점 데이터 저장 완료");
+            } else {
+                System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
+            }
+
+            // 카페 데이터 초기화
+            if (cafeRepository.count() == 0) {
+                System.out.println("카페 데이터 저장 시작");
+                cafeService.processAllAreas();
+                System.out.println("카페 데이터 저장 완료");
+            } else {
+                System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
+            }
+
+
+        };
+    }
+    */
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/config/AppConfig.java b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java
new file mode 100644
index 0000000..4b97730
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java
@@ -0,0 +1,14 @@
+package com.example.placeservice.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/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java
new file mode 100644
index 0000000..0eafb25
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java
@@ -0,0 +1,15 @@
+package com.example.placeservice.config;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class DotenvConfig {
+
+    @Bean
+    public Dotenv dotenv() {
+        return Dotenv.load(); // .env 파일 자동 로딩
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java
new file mode 100644
index 0000000..7010ad6
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java
@@ -0,0 +1,27 @@
+package com.example.placeservice.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 {
+
+    // spring-security 임시 block
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf(csrf -> csrf.disable())
+                .authorizeHttpRequests(auth -> auth
+                        .requestMatchers(
+                                "/v3/api-docs/**",
+                                "/swagger-ui/**",
+                                "/swagger-ui.html"
+                        ).permitAll()
+                        .anyRequest().permitAll()
+                );
+
+        return http.build();
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/config/SwaggerConfig.java b/place-service/src/main/java/com/example/placeservice/config/SwaggerConfig.java
new file mode 100644
index 0000000..fba4e37
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/config/SwaggerConfig.java
@@ -0,0 +1,26 @@
+package com.example.placeservice.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SwaggerConfig {
+
+    @Bean
+    public OpenAPI openAPI() {
+        return new OpenAPI()
+                .components(new Components())
+                .info(apiInfo());
+    }
+
+    private Info apiInfo() {
+        return new Info()
+                .title("API Title") // API의 제목
+                .description("This is my Swagger UI") // API에 대한 설명
+                .version("1.0.0"); // API의 버전
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
new file mode 100644
index 0000000..afc337f
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
@@ -0,0 +1,48 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.dto.accommodation.AccommodationDto;
+import com.example.placeservice.service.AccommodationService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/main")
+public class AccommodationController {
+    private final AccommodationService accommodationService;
+
+    // 생성자 주입
+    public AccommodationController(AccommodationService accommodationService) {
+        this.accommodationService = accommodationService;
+    }
+
+    @GetMapping("/accommodation/list")
+    public List getAccommodations() throws IOException {
+        return accommodationService.showAccommodations();
+    }
+
+    @GetMapping("/accommodation/list/{areaId}")
+    public Map getAccommodationByAreaId(@PathVariable Long areaId) throws IOException {
+        return accommodationService.getAccommodationByAreaId(areaId);
+    }
+
+    @GetMapping("/info/accommodation/{accommodation_id}")
+    public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id) throws IOException {
+        return accommodationService.getAccommodationById(accommodation_id);
+    }
+
+    @GetMapping("/accommodation/gu/{gu}")
+    public List getAccommodationByGu(@PathVariable String gu) throws IOException {
+        return accommodationService.getAccommodationByGu(gu);
+    }
+
+    @GetMapping("/accommodation/type/{type}")
+    public List getAccommodationByType(@PathVariable String type) throws IOException {
+        return accommodationService.getAccommodationByType(type);
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AiSummaryController.java b/place-service/src/main/java/com/example/placeservice/controller/AiSummaryController.java
new file mode 100644
index 0000000..f318229
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/AiSummaryController.java
@@ -0,0 +1,24 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.dto.AiSummaryParsedResponse;
+import com.example.placeservice.service.AiSummaryService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/summary")
+@RequiredArgsConstructor
+public class AiSummaryController {
+
+    private final AiSummaryService aiSummaryService;
+
+    @GetMapping("/{targetType}/{targetId}")
+    public ResponseEntity getSummaryParsed(
+            @PathVariable String targetType,
+            @PathVariable String targetId) {
+
+        AiSummaryParsedResponse response = aiSummaryService.getSummaryParsed(targetType, targetId);
+        return ResponseEntity.ok(response);
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaController.java
new file mode 100644
index 0000000..61cf37c
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/AreaController.java
@@ -0,0 +1,26 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.dto.AreaDto;
+import com.example.placeservice.service.AreaService;
+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 java.util.List;
+
+@RestController
+@RequestMapping("/main")
+@RequiredArgsConstructor
+public class AreaController {
+
+    private final AreaService areaService;
+
+    // 지역 목록 조회
+    @GetMapping("/area/list")
+    public List getAreaList() {
+
+        return areaService.getAreaData();
+
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java
new file mode 100644
index 0000000..f721010
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java
@@ -0,0 +1,174 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.entity.*;
+import com.example.placeservice.repository.AccommodationRepository;
+import com.example.placeservice.repository.AreaRepository;
+import com.example.placeservice.repository.AttractionRepository;
+import com.example.placeservice.repository.CafeRepository;
+import com.example.placeservice.repository.CulturalEventRepository;
+import com.example.placeservice.repository.RestaurantRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/main")
+@RequiredArgsConstructor
+public class AreaPlaceController {
+
+    private final AreaRepository areaRepository;
+    private final CafeRepository cafeRepository;
+    private final RestaurantRepository restaurantRepository;
+    private final AttractionRepository attractionRepository;
+    private final AccommodationRepository accommodationRepository;
+    private final CulturalEventRepository culturalEventRepository;
+
+    /**
+     * 지역 ID를 기준으로 모든 장소 정보(카페, 음식점, 관광지, 숙박, 행사)를 한번에 조회
+     */
+    @GetMapping("/place/list/{areaId}")
+    public ResponseEntity>> getAllPlacesByAreaId(@PathVariable Long areaId) {
+        log.info("지역 ID {}의 모든 장소 정보 통합 조회", areaId);
+
+        // 지역 정보 조회
+        Optional areaOptional = areaRepository.findById(areaId);
+        if (!areaOptional.isPresent()) {
+            return ResponseEntity.notFound().build();
+        }
+
+        // 배열 형태로 응답 변경
+        List> response = new ArrayList<>();
+
+        // 카페 정보
+        Map cafeResponse = new LinkedHashMap<>();
+        cafeResponse.put("type", "cafe");
+
+        List> cafeList = new ArrayList<>();
+        List cafes = cafeRepository.findByAreaId(areaId);
+
+        for (Cafe cafe : cafes) {
+            Map cafeMap = new LinkedHashMap<>();
+            cafeMap.put("id", cafe.getId());
+            cafeMap.put("name", cafe.getName());
+            cafeMap.put("address", cafe.getAddress());
+            cafeMap.put("phone", cafe.getPhone());
+            cafeMap.put("kakaomap_url", cafe.getKakaomapUrl());
+            cafeMap.put("lat", cafe.getLat());
+            cafeMap.put("lon", cafe.getLon());
+            cafeList.add(cafeMap);
+        }
+
+        cafeResponse.put("content", cafeList);
+        response.add(cafeResponse); // 배열에 추가
+
+        // 음식점 정보
+        Map restaurantResponse = new LinkedHashMap<>();
+        restaurantResponse.put("type", "restaurant");
+
+        List> restaurantList = new ArrayList<>();
+        List restaurants = restaurantRepository.findByAreaAreaId(areaId);
+
+        for (Restaurant restaurant : restaurants) {
+            Map restaurantMap = new LinkedHashMap<>();
+            restaurantMap.put("id", restaurant.getRestaurantId());
+            restaurantMap.put("name", restaurant.getName());
+            restaurantMap.put("address", restaurant.getAddress());
+            restaurantMap.put("phone", restaurant.getPhone());
+            restaurantMap.put("category_code", restaurant.getCategory_code());
+            restaurantMap.put("category_group_name", restaurant.getCategoryGroupName());
+            restaurantMap.put("category_name", restaurant.getCategoryName());
+            restaurantMap.put("kakaomap_url", restaurant.getKakaomap_url());
+            restaurantMap.put("lat", restaurant.getLat());
+            restaurantMap.put("lon", restaurant.getLon());
+            restaurantList.add(restaurantMap);
+        }
+
+        restaurantResponse.put("content", restaurantList);
+        response.add(restaurantResponse); // 배열에 추가
+
+        // 관광지 정보
+        Map attractionResponse = new LinkedHashMap<>();
+        attractionResponse.put("type", "attraction");
+
+        List> attractionList = new ArrayList<>();
+        List attractions = attractionRepository.findByAreaAreaId(areaId);
+
+        for (Attraction attraction : attractions) {
+            Map attractionMap = new LinkedHashMap<>();
+            attractionMap.put("id", attraction.getAttractionId());
+            attractionMap.put("seoul_attraction_id", attraction.getSeoulAttractionId());
+            attractionMap.put("name", attraction.getName());
+            attractionMap.put("address", attraction.getAddress());
+            attractionMap.put("phone", attraction.getPhone());
+            attractionMap.put("homepage_url", attraction.getHomepageUrl());
+            attractionMap.put("close_day", attraction.getCloseDay());
+            attractionMap.put("use_time", attraction.getUseTime());
+            attractionMap.put("kakaomap_url", attraction.getKakaomapUrl());
+            attractionMap.put("lat", attraction.getLat());
+            attractionMap.put("lon", attraction.getLon());
+            attractionList.add(attractionMap);
+        }
+
+        attractionResponse.put("content", attractionList);
+        response.add(attractionResponse); // 배열에 추가
+
+        // 숙박시설 정보
+        Map accommodationResponse = new LinkedHashMap<>();
+        accommodationResponse.put("type", "accommodation");
+
+        List> accommodationList = new ArrayList<>();
+        List accommodations = accommodationRepository.findByAreaId(areaId);
+
+        for (Accommodation accommodation : accommodations) {
+            Map accommodationMap = new LinkedHashMap<>();
+            accommodationMap.put("id", accommodation.getAccommodationId());
+            accommodationMap.put("name", accommodation.getName());
+            accommodationMap.put("address", accommodation.getAddress());
+            accommodationMap.put("phone", accommodation.getPhone());
+            accommodationMap.put("gu", accommodation.getGu());
+            accommodationMap.put("type", accommodation.getType());
+            accommodationMap.put("kakaomap_url", accommodation.getKakaomapUrl());
+            accommodationMap.put("lat", accommodation.getLat());
+            accommodationMap.put("lon", accommodation.getLon());
+            accommodationList.add(accommodationMap);
+        }
+
+        accommodationResponse.put("content", accommodationList);
+        response.add(accommodationResponse); // 배열에 추가
+
+        // 문화행사 정보
+        Map eventResponse = new LinkedHashMap<>();
+        eventResponse.put("type", "cultural_event");
+
+        List> eventList = new ArrayList<>();
+        List events = culturalEventRepository.findByAreaAreaId(areaId);
+
+        for (CulturalEvent event : events) {
+            Map eventMap = new LinkedHashMap<>();
+            eventMap.put("id", event.getEventId());
+            eventMap.put("name", event.getTitle());
+            eventMap.put("address", event.getAddress());
+            eventMap.put("category", event.getCategory());
+            eventMap.put("target", event.getTarget());
+            eventMap.put("event_fee", event.getEventFee());
+            eventMap.put("event_img", event.getEventImg());
+            eventMap.put("start_date", event.getStartDate());
+            eventMap.put("end_date", event.getEndDate());
+            eventMap.put("lat", event.getLat());
+            eventMap.put("lon", event.getLon());
+            eventList.add(eventMap);
+        }
+
+        eventResponse.put("content", eventList);
+        response.add(eventResponse); // 배열에 추가
+
+        return ResponseEntity.ok(response);
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java
new file mode 100644
index 0000000..2a2d072
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java
@@ -0,0 +1,32 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.dto.attraction.AreaAttractionsDto;
+import com.example.placeservice.dto.attraction.AttractionInfoDto;
+import com.example.placeservice.service.AttractionService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/main")
+@RequiredArgsConstructor
+public class AttractionController {
+
+    private final AttractionService attractionService;
+
+    // 관광지 목록 조회
+    @GetMapping("/attraction/list")
+    public List getAttraction() {
+        return attractionService.getAttractionData();
+    }
+
+    // 관광지 정보 조회(place-code)
+    @GetMapping("info/attraction/{place-code}")
+    public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long attractionId) {
+        return attractionService.getAttractionInfoData(attractionId);
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
new file mode 100644
index 0000000..bf49fc1
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
@@ -0,0 +1,44 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.dto.cafe.CafeListDto;
+import com.example.placeservice.service.CafeService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/main")
+@RequiredArgsConstructor
+public class CafeController {
+
+    private final CafeService cafeService;
+
+    /**
+     * 전체 카페 목록 조회 - 레스토랑 스타일로 단순화 (캐싱 없음)
+     */
+    @GetMapping("/cafe/list")
+    public ResponseEntity> getCafeList() {
+        log.info("카페 목록 조회");
+        return ResponseEntity.ok(cafeService.getSimpleCafeList());
+    }
+
+    @GetMapping("/info/cafe/{cafeId}")
+    public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) {
+        log.info("카페 ID {}의 상세 정보 조회", cafeId);
+        Map cafeInfo = cafeService.getCafeInfo(cafeId);
+
+        if (cafeInfo != null) {
+            return ResponseEntity.ok(cafeInfo);
+        }
+
+        return ResponseEntity.notFound().build();
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
new file mode 100644
index 0000000..d149dc0
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
@@ -0,0 +1,32 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.dto.culturalevent.CulturalEventDto;
+import com.example.placeservice.entity.CulturalEvent;
+import com.example.placeservice.repository.CulturalEventRepository;
+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 java.util.List;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/main")
+@RequiredArgsConstructor
+@Slf4j
+public class CulturalEventController {
+
+    private final CulturalEventRepository culturalEventRepository;
+
+    // /main/events: 저장된 모든 이벤트 조회
+    @GetMapping("/events")
+    public List getAllEvents() {
+        List events = culturalEventRepository.findAll();
+        return events.stream()
+                .map(CulturalEventDto::new)  // 생성자 이용해서 CulturalEvent -> DTO 변환
+                .collect(Collectors.toList());
+
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java b/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java
new file mode 100644
index 0000000..a358571
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java
@@ -0,0 +1,29 @@
+package com.example.placeservice.controller;
+
+import com.example.placeservice.dto.PlaceDocument;
+import com.example.placeservice.service.PlaceDocumentService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/search")
+public class PlaceSearchController {
+
+    private final PlaceDocumentService placeSearchService;
+
+    @GetMapping("/{keyword}")
+    public List searchByName(@PathVariable String keyword) {
+        return placeSearchService.searchPlacesByName(keyword);
+    }
+
+    @GetMapping("/address/{keyword}")
+    public List searchByAddress(@PathVariable String keyword) {
+        return placeSearchService.searchPlacesByAddress(keyword);
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
new file mode 100644
index 0000000..7456e8e
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
@@ -0,0 +1,42 @@
+package com.example.placeservice.controller;
+
+
+import com.example.placeservice.dto.restaurant.RestaurantDetailResponse;
+import com.example.placeservice.dto.restaurant.RestaurantListResponse;
+import com.example.placeservice.service.RestaurantListService;
+import com.example.placeservice.service.RestaurantService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/main")
+@RequiredArgsConstructor
+public class RestaurantController {
+
+    private final RestaurantService restaurantService;
+    private final RestaurantListService restaurantListService;
+
+    // 전체 지역 기준으로 음식점 정보 저장
+    @PostMapping("/fetch")
+    public ResponseEntity fetchRestaurants() {
+        restaurantService.fetchAndSaveRestaurants();
+        return ResponseEntity.ok("음식점 데이터 저장 완료!");
+    }
+
+    //음식점 목록 제공
+    @GetMapping("/restaurant/list")
+    public ResponseEntity> getRestaurantList() {
+        List restaurantList = restaurantListService.getRestaurantList();
+        return ResponseEntity.ok(restaurantList);
+    }
+
+    //음식점 상세 정보 제공
+    @GetMapping("/info/restaurant/{restaurantId}")
+    public ResponseEntity getRestaurantDetail(@PathVariable Long restaurantId) {
+        RestaurantDetailResponse restaurantDetail = restaurantListService.getRestaurantDetail(restaurantId);
+        return ResponseEntity.ok(restaurantDetail);
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/AiSummaryParsedResponse.java b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryParsedResponse.java
new file mode 100644
index 0000000..4df31e7
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryParsedResponse.java
@@ -0,0 +1,14 @@
+package com.example.placeservice.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+import java.util.List;
+
+@Getter
+@Builder
+public class AiSummaryParsedResponse {
+    private List positiveKeywords;
+    private List negativeKeywords;
+    private int positiveCount;
+    private int negativeCount;
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/AiSummaryResponse.java b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryResponse.java
new file mode 100644
index 0000000..df58fd2
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryResponse.java
@@ -0,0 +1,11 @@
+package com.example.placeservice.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class AiSummaryResponse {
+    private String content;
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java b/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java
new file mode 100644
index 0000000..79fc41e
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java
@@ -0,0 +1,27 @@
+package com.example.placeservice.dto;
+
+import com.example.placeservice.entity.Area;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class AreaDto {
+    private Long area_id;
+    private String seoul_id;
+    private String area_name;
+    private String name_eng;
+    private String category;
+    private BigDecimal lat;
+    private BigDecimal lon;
+
+    public AreaDto(Area area) {
+        this.area_id = area.getAreaId();
+        this.seoul_id = area.getSeoulId();
+        this.area_name = area.getName();
+        this.name_eng = area.getNameEng();
+        this.category = area.getCategory();
+        this.lat = area.getLat();
+        this.lon = area.getLon();
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java
new file mode 100644
index 0000000..f742c49
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java
@@ -0,0 +1,22 @@
+package com.example.placeservice.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.math.BigDecimal;
+
+@Builder
+@Getter
+public class PlaceDocument {
+
+    // 공통
+    private String name;
+    private String type;
+    private String address;
+    private String place_id;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String phone;
+    private String kakaomap_url;
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/Result.java b/place-service/src/main/java/com/example/placeservice/dto/Result.java
new file mode 100644
index 0000000..efd696a
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/Result.java
@@ -0,0 +1,9 @@
+package com.example.placeservice.dto;
+
+import lombok.Data;
+
+@Data
+public class Result {
+    private String CODE;
+    private String MESSAGE;
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
new file mode 100644
index 0000000..e923d11
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
@@ -0,0 +1,59 @@
+package com.example.placeservice.dto.accommodation;
+
+import com.example.placeservice.entity.Accommodation;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AccommodationDto {
+
+    private Long accommodation_id;
+    private Long area_id;
+    private String area_name;
+    private String accommodation_name;
+    private String address;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String phone;
+    private String gu;
+    private String type;
+    private String kakaomap_url;
+
+    /**
+     * Accommodation 엔티티를 AccommodationDto로 변환하는 정적 팩토리 메소드
+     * @param entity 변환할 Accommodation 엔티티
+     * @return 변환된 AccommodationDto 객체
+     */
+    public static AccommodationDto fromEntity(Accommodation entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        // Area 정보 추출 (N+1 문제를 방지하기 위해 Fetch Join 고려)
+        Long areaIdValue = null;
+        String areaNameValue = null;
+        if (entity.getArea() != null) {
+            areaIdValue = entity.getArea().getAreaId();
+            areaNameValue = entity.getArea().getName(); // Area 엔티티에 getName() 메소드가 있다고 가정
+        }
+
+        return new AccommodationDto(
+                entity.getAccommodationId(),
+                areaIdValue,
+                areaNameValue,
+                entity.getName(),
+                entity.getAddress(),
+                entity.getLat(),
+                entity.getLon(),
+                entity.getPhone(),
+                entity.getGu(),
+                entity.getType(),
+                entity.getKakaomapUrl()
+        );
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
new file mode 100644
index 0000000..dbea9ef
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
@@ -0,0 +1,55 @@
+package com.example.placeservice.dto.accommodation; // 적절한 패키지 경로로 변경
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true) // API 응답에 모르는 필드가 있어도 무시
+public class KakaoApiResponse {
+    private Meta meta;
+    private List documents;
+
+    @Data
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Meta {
+        @JsonProperty("total_count")
+        private Integer totalCount;
+        @JsonProperty("pageable_count")
+        private Integer pageableCount;
+        @JsonProperty("is_end")
+        private Boolean isEnd;
+        // 필요시 다른 meta 필드 추가
+    }
+
+    @Data
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Document {
+        private String id; // 카카오에서 부여하는 장소 ID (String이지만 숫자 형태)
+        @JsonProperty("place_name")
+        private String placeName; // 장소명, 업체명
+        @JsonProperty("category_name")
+        private String categoryName; // 카테고리 이름 (예: 여행 > 숙박 > 호텔)
+        @JsonProperty("category_group_code")
+        private String categoryGroupCode; // 카테고리 그룹 코드
+        @JsonProperty("category_group_name")
+        private String categoryGroupName; // 카테고리 그룹명
+        private String phone; // 전화번호
+        @JsonProperty("address_name")
+        private String addressName; // 전체 지번 주소
+        @JsonProperty("road_address_name")
+        private String roadAddressName; // 전체 도로명 주소
+        private String x; // 경도(longitude) - String 형태
+        private String y; // 위도(latitude) - String 형태
+        @JsonProperty("place_url")
+        private String placeUrl; // 장소 상세페이지 URL
+        @JsonProperty("distance")
+        private String distance; // 중심좌표와의 거리 (단위: m) - radius 파라미터 사용 시
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java
new file mode 100644
index 0000000..62d5963
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java
@@ -0,0 +1,12 @@
+package com.example.placeservice.dto.attraction;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AreaAttractionsDto {
+    private String area_name; // Area 이름
+    private List attraction_list;
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java
new file mode 100644
index 0000000..0323622
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java
@@ -0,0 +1,53 @@
+package com.example.placeservice.dto.attraction;
+
+import jakarta.xml.bind.annotation.XmlAccessType;
+import jakarta.xml.bind.annotation.XmlAccessorType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@XmlRootElement(name = "NewDataSet")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class AttractionDto {
+
+    @XmlElement(name = "Table1")
+    private List attractions;
+
+    @Data
+    @XmlAccessorType(XmlAccessType.FIELD)
+    public static class AttractionTable {
+
+        @XmlElement(name = "COT_ART_ID")
+        private String id;
+
+        @XmlElement(name = "TITLE")
+        private String title;
+
+        @XmlElement(name = "COT_ADDR_NEW")
+        private String newAddress;
+
+        @XmlElement(name = "COT_MAP_POINTX")
+        private String mapX;
+
+        @XmlElement(name = "COT_MAP_POINTY")
+        private String mapY;
+
+        @XmlElement(name = "COT_TEL", nillable = true)
+        private String tel;
+
+        @XmlElement(name = "COT_HOMEPAGE", nillable = true)
+        private String homepage;
+
+        @XmlElement(name = "COT_CLOSE_DAY", nillable = true)
+        private String closeDay;
+
+        @XmlElement(name = "COT_USE_TIME_DESC", nillable = true)
+        private String useTime;
+
+        // 필요하면 나머지도 추가
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
new file mode 100644
index 0000000..dcb5546
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
@@ -0,0 +1,39 @@
+package com.example.placeservice.dto.attraction;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class AttractionInfoDto {
+    private Long attraction_id;
+    private String seoul_attraction_id;
+    private String attraction_name;
+    private String address;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String phone;
+    private String homepage_url;
+    private String close_day;
+    private String use_time;
+    private String kakaomap_url;
+    private Long area_id;
+    private String area_name;
+
+    public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl, Long area_id, String area_name) {
+        this.attraction_id = attractionId;
+        this.seoul_attraction_id = seoul_attraction_id;
+        this.attraction_name = name;
+        this.address = address;
+        this.lat = lat;
+        this.lon = lon;
+        this.phone = phone;
+        this.homepage_url = homepage_url;
+        this.close_day = close_day;
+        this.use_time = use_time;
+        this.kakaomap_url = kakaomapUrl;
+        this.area_id = area_id;
+        this.area_name = area_name;
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
new file mode 100644
index 0000000..b3a262e
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
@@ -0,0 +1,27 @@
+package com.example.placeservice.dto.attraction;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class AttractionListDto {
+    private Long attraction_id;
+    private String seoul_attraction_id;
+    private String attraction_name;
+    private String address;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String kakaomap_url;
+
+
+    public AttractionListDto(Long attraction_id, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String kakaomapUrl) {
+        this.attraction_id = attraction_id;
+        this.seoul_attraction_id = seoul_attraction_id;
+        this.attraction_name = name;
+        this.address = address;
+        this.lat = lat;
+        this.lon = lon;
+        this.kakaomap_url = kakaomapUrl;
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
new file mode 100644
index 0000000..3b4279c
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
@@ -0,0 +1,55 @@
+package com.example.placeservice.dto.cafe;
+
+import com.example.placeservice.entity.Cafe;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CafeDto {
+    private Long id;
+    private Long areaId;
+    private String areaName;
+    private String name;
+    private String address;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String phone;
+    private String kakaomapUrl;
+    private String categoryCode;
+
+    /**
+     * Cafe 엔티티를 CafeDto로 변환하는 정적 팩토리 메소드
+     */
+    public static CafeDto fromEntity(Cafe entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        Long areaIdValue = null;
+        String areaNameValue = null;
+        if (entity.getArea() != null) {
+            areaIdValue = entity.getArea().getAreaId();
+            areaNameValue = entity.getArea().getName();
+        }
+
+        return new CafeDto(
+                entity.getId(),
+                areaIdValue,
+                areaNameValue,
+                entity.getName(),
+                entity.getAddress(),
+                entity.getLat(),
+                entity.getLon(),
+                entity.getPhone(),
+                entity.getKakaomapUrl(),
+                entity.getCategoryCode()
+        );
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeListDto.java b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeListDto.java
new file mode 100644
index 0000000..c2cc8aa
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeListDto.java
@@ -0,0 +1,17 @@
+package com.example.placeservice.dto.cafe;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CafeListDto {
+    private Long id;
+    private String name;
+    private BigDecimal lat;
+    private BigDecimal lon;
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
new file mode 100644
index 0000000..26bb657
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
@@ -0,0 +1,39 @@
+package com.example.placeservice.dto.culturalevent;
+
+import com.example.placeservice.entity.CulturalEvent;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class CulturalEventDto {
+
+    private Long event_id;
+    private String category;
+    private String event_name;
+    private String address;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String target;
+    private String event_fee;
+    private String event_img;
+    private LocalDateTime start_date;
+    private LocalDateTime end_date;
+
+    public CulturalEventDto(CulturalEvent culturalEvent) {
+        this.event_id = culturalEvent.getEventId();
+        this.category = culturalEvent.getCategory();
+        this.event_name = culturalEvent.getTitle();
+        this.address = culturalEvent.getAddress();
+        this.lat = culturalEvent.getLat();
+        this.lon = culturalEvent.getLon();
+        this.target = culturalEvent.getTarget();
+        this.event_fee = culturalEvent.getEventFee();
+        this.event_img = culturalEvent.getEventImg();
+        this.start_date = culturalEvent.getStartDate();
+        this.end_date = culturalEvent.getEndDate();
+
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventInfo.java b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventInfo.java
new file mode 100644
index 0000000..02fa788
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventInfo.java
@@ -0,0 +1,17 @@
+package com.example.placeservice.dto.culturalevent;
+
+import com.example.placeservice.dto.Result;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CulturalEventInfo {
+    private int list_total_count;
+    private Result RESULT;
+    @JsonProperty("row")
+    private List row;
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventItem.java b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventItem.java
new file mode 100644
index 0000000..ae968e2
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventItem.java
@@ -0,0 +1,59 @@
+package com.example.placeservice.dto.culturalevent;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class CulturalEventItem {
+
+    @JsonProperty("CODENAME")
+    private String codename;
+
+    @JsonProperty("GUNAME")
+    private String guname;
+
+    @JsonProperty("TITLE")
+    private String title;
+
+    @JsonProperty("PLACE")
+    private String place;
+
+    @JsonProperty("ORG_NAME")
+    private String orgName;
+
+    @JsonProperty("USE_TRGT")
+    private String useTrgt;
+
+    @JsonProperty("USE_FEE")
+    private String useFee;
+
+    @JsonProperty("ORG_LINK")
+    private String orgLink;
+
+    @JsonProperty("MAIN_IMG")
+    private String mainImg;
+
+    @JsonProperty("STRTDATE")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S")  // 날짜 포맷 설정
+    private LocalDateTime strtdate;
+
+    @JsonProperty("END_DATE")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S")  // 날짜 포맷 설정
+    private LocalDateTime endDate;
+
+    @JsonProperty("LOT")
+    private BigDecimal lot;
+
+    @JsonProperty("LAT")
+    private BigDecimal lat;
+
+    @JsonProperty("IS_FREE")
+    private String isFree;
+
+    @JsonProperty("HMPG_ADDR")
+    private String hmpgAddr;
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventResponse.java b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventResponse.java
new file mode 100644
index 0000000..380da1a
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventResponse.java
@@ -0,0 +1,10 @@
+package com.example.placeservice.dto.culturalevent;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class CulturalEventResponse {
+    @JsonProperty("culturalEventInfo")
+    private CulturalEventInfo culturalEventInfo;
+}
diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java
new file mode 100644
index 0000000..86b30ca
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java
@@ -0,0 +1,49 @@
+//추가
+package com.example.placeservice.dto.restaurant;
+
+import com.example.placeservice.entity.Restaurant;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class RestaurantDetailResponse {
+
+    private Long   area_id;
+    private String area_name;
+    private String address;
+    private String category_group_code;
+    private String category_group_name;
+    private String category_name;
+    private String restaurant_id;
+    private String phone;
+    private String restaurant_name;
+    private String place_url;
+    private String road_address_name;
+    private String lat;
+    private String lon;
+    private String categoryGroupName;
+    private String categoryName;
+    private String kakao_id;
+
+    public static RestaurantDetailResponse fromEntity(Restaurant restaurant) {
+        return RestaurantDetailResponse.builder()
+                .area_id(restaurant.getArea().getAreaId())
+                .area_name(restaurant.getArea().getName())
+                .address(restaurant.getAddress())
+                .category_group_code(restaurant.getCategory_code())
+                .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB)
+                .category_name(restaurant.getCategoryName()) // 고정X (DB)
+                .restaurant_id(String.valueOf(restaurant.getRestaurantId()))
+                .kakao_id(restaurant.getKakao_id())
+                .phone(restaurant.getPhone())
+                .restaurant_name(restaurant.getName())
+                .place_url(restaurant.getKakaomap_url())
+                .categoryGroupName(restaurant.getCategoryGroupName()) //  추가
+                .categoryName(restaurant.getCategoryName()) //  추가
+                .road_address_name(restaurant.getAddress()) // 현재 Address랑 Road Address가 같게 저장된 경우
+                .lat(restaurant.getLat().toString())
+                .lon(restaurant.getLon().toString())
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantListResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantListResponse.java
new file mode 100644
index 0000000..b7a2b73
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantListResponse.java
@@ -0,0 +1,23 @@
+package com.example.placeservice.dto.restaurant;
+//추가
+import com.example.placeservice.entity.Restaurant;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class RestaurantListResponse {
+    private Long restaurant_id;
+    private String restaurant_name;
+    private String lat;
+    private String lon;
+
+    public static RestaurantListResponse fromEntity(Restaurant restaurant) {
+        return RestaurantListResponse.builder()
+                .restaurant_id(restaurant.getRestaurantId())
+                .restaurant_name(restaurant.getName())
+                .lat(restaurant.getLat().toString())
+                .lon(restaurant.getLon().toString())
+                .build();
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
new file mode 100644
index 0000000..9c1bc03
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
@@ -0,0 +1,32 @@
+package com.example.placeservice.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.Getter;
+
+import java.math.BigDecimal;
+
+@Data
+@Entity
+@Getter
+public class Accommodation {
+    @Id
+    @Column(name = "accommodation_id")
+    private Long accommodationId;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "area_id", referencedColumnName = "area_id", nullable = false)
+    private Area area;
+
+    private String name;
+    private String address;
+    @Column(precision = 10, scale = 6)
+    private BigDecimal lat;
+    @Column(precision = 10, scale = 6)
+    private BigDecimal lon;
+    private String phone;
+    private String gu;
+    private String type;
+    @Column(name = "kakaomap_url")
+    private String kakaomapUrl;
+}
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java
index faa2087..5c04beb 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Area.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java
@@ -1,27 +1,41 @@
 package com.example.placeservice.entity;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import jakarta.persistence.*;
 
+import lombok.Data;
+
 import java.math.BigDecimal;
+import java.util.List;
 
 @Entity
+@Data
 public class Area {
     @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "area_id")
-    private Long area_id;
+    private Long areaId;
+
+    @Column(name = "seoul_id", length = 10, nullable = false, unique = true)
+    private String seoulId;
 
     @Column(name = "name", length = 50, nullable = false)
     private String name;  // 지역명
 
+    @Column(name = "name_eng", length = 100, nullable = false)
+    private String nameEng;
+
     @Column(name = "category", length = 20, nullable = false)
     private String category;  // 구분
 
-    @Column(name = "x", precision = 10, scale = 6, nullable = false)
-    private BigDecimal x; // 위도
+    @Column(name = "lat", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lat; // 위도
+
+    @Column(name = "lon", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lon; // 경도
 
-    @Column(name = "y", precision = 10, scale = 6, nullable = false)
-    private BigDecimal y; // 경도
+    @OneToMany(mappedBy = "area", fetch = FetchType.LAZY)
+    @JsonIgnore
+    private List attractions;
 
 
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
new file mode 100644
index 0000000..c5b3194
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
@@ -0,0 +1,50 @@
+package com.example.placeservice.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@Entity
+public class Attraction {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "attraction_id")
+    private Long attractionId;
+
+    @Column(name = "seoul_attraction_id", length = 10, nullable = false, unique = true)
+    private String seoulAttractionId;
+
+    @Column(name="name", length = 200, nullable = false)
+    private String name;
+
+    @Column(name="address", length = 200, nullable = false)
+    private String address;
+
+    @Column(name = "lat", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lat; // 위도
+
+    @Column(name = "lon", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lon; // 경도
+
+    @Column(name="phone", length = 30)
+    private String phone;
+
+    @Column(name="homepage_url", columnDefinition = "TEXT")
+    private String homepageUrl;
+
+    @Column(name="close_day", columnDefinition = "TEXT")
+    private String closeDay;
+
+    @Column(name="use_time", columnDefinition = "TEXT")
+    private String useTime;
+
+    @Column(name="kakaomap_url",columnDefinition = "TEXT")
+    private String kakaomapUrl;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "area_id", nullable = false)
+    private Area area;
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
new file mode 100644
index 0000000..7cefe05
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
@@ -0,0 +1,49 @@
+package com.example.placeservice.entity;
+
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "cafe")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Cafe {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "cafe_id")
+    private Long id;
+
+    @Column(name = "name")
+    private String name;
+
+    @Column(name = "address")
+    private String address;
+
+    @Column(name = "lat", precision = 10, scale = 6)
+    private BigDecimal lat;  // 위도
+
+    @Column(name = "lon", precision = 10, scale = 6)
+    private BigDecimal lon;  // 경도
+
+    @Column(name = "phone")
+    private String phone;
+
+    @Column(name = "kakaomap_url")
+    private String kakaomapUrl;
+
+    @Column(name = "category_code")
+    private String categoryCode;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "area_id")
+    private Area area;
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java
new file mode 100644
index 0000000..e6be07f
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java
@@ -0,0 +1,57 @@
+package com.example.placeservice.entity;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Entity
+@Data
+@NoArgsConstructor
+public class CulturalEvent {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "event_id")
+    private Long eventId;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "area_id")  // 외래키 컬럼
+    @JsonIgnore
+    private Area area;
+
+    @Column(name = "category", length = 200)
+    private String category;
+
+    @Column(name = "title", length = 200)
+    private String title;
+
+    @Column(name = "address", length = 200)
+    private String address;  // PLACE → address
+
+    @Column(name = "lat", precision = 10, scale = 6)
+    private BigDecimal lat;
+
+    @Column(name = "lon", precision = 10, scale = 6)
+    private BigDecimal lon;
+
+    @Column(name = "target", length = 200)
+    private String target;
+
+    @Column(name = "event_fee", columnDefinition = "TEXT")
+    private String eventFee;
+
+    @Column(name = "event_img", length = 200)
+    private String eventImg;
+
+    @Column(name = "start_date")
+    private LocalDateTime startDate;
+
+    @Column(name = "end_date")
+    private LocalDateTime endDate;
+}
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java
new file mode 100644
index 0000000..8207738
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java
@@ -0,0 +1,67 @@
+package com.example.placeservice.entity;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "restaurant") // 테이블명 명시
+public class Restaurant {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "restaurant_id")
+    private Long restaurantId; // PK
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "area_id", nullable = false)
+    private Area area; // Area 엔티티와 다대일 관계
+
+    @Column(length = 50, nullable = false)
+    private String name; // 음식점 이름
+
+    @Column(length = 200)
+    private String address; // 음식점 주소
+
+    @Column(precision = 10, scale = 6, nullable = false)
+    private BigDecimal lat; // 위도
+
+    @Column(precision = 10, scale = 6, nullable = false)
+    private BigDecimal lon; // 경도
+
+    @Column(length = 20)
+    private String phone; // 전화번호
+
+    @Column(length = 200)
+    private String kakaomap_url; // 카카오맵 URL
+
+    @Column(length = 20)
+    private String category_code; // 카테고리 코드 (FD6: 음식점)
+
+//    @Column(length = 30)
+//    private String kakao_id;  // 카카오 API가 주는 id 저장 (String)
+
+    //추가
+    @Column(length = 100)
+    private String categoryGroupName; // (예시) 음식점
+
+    @Column(length = 200)
+    private String categoryName; // (예시) 음식점 > 한식 > 육류,고기
+
+    @Column(name = "kakao_id", length = 30)
+    private String kakao_id;
+
+    public String getKakao_id() {
+        return kakao_id;
+    }
+
+    public void setKakao_id(String kakao_id) {
+        this.kakao_id = kakao_id;
+    }
+
+}
+
diff --git a/place-service/src/main/java/com/example/placeservice/init/DataInit.java b/place-service/src/main/java/com/example/placeservice/init/DataInit.java
new file mode 100644
index 0000000..99e7b33
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/init/DataInit.java
@@ -0,0 +1,75 @@
+package com.example.placeservice.init;
+
+import com.example.placeservice.repository.CafeRepository;
+import com.example.placeservice.repository.RestaurantRepository;
+import com.example.placeservice.service.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함)
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DataInit implements CommandLineRunner {
+
+    private final AccommodationService accommodationService;
+    private final AttractionService attractionService;
+    private final CulturalEventService culturalEventService;
+    private final RestaurantRepository restaurantRepository;
+    private final RestaurantService restaurantService;
+    private final CafeRepository cafeRepository;
+    private final CafeService cafeService;
+
+    private final JdbcTemplate jdbcTemplate;
+
+
+    @Override
+    public void run(String... args) throws Exception {
+        // 1. data.sql 파일 실행(지역 데이터)
+        try {
+            ClassPathResource resource = new ClassPathResource("data.sql");
+
+            // 파일 내용을 문자열로 읽기
+            String sql = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
+
+            // SQL 실행
+            jdbcTemplate.execute(sql);
+            log.info("data.sql 실행 완료");
+
+        } catch (IOException e) {
+            log.error("area 데이터 초기화 시 오류 발생 : {}",e.getMessage());
+        }
+
+        // 2. 장소 데이터 삽입
+        accommodationService.saveAccommodations();
+        attractionService.fetchDataFromVisitSeoul();
+        culturalEventService.fetchAndSaveAllEvents();
+
+        if (restaurantRepository.count() == 0) {
+            log.info("음식점 데이터 저장 시작");
+            restaurantService.fetchAndSaveRestaurants();
+            log.info("음식점 데이터 저장 완료");
+        } else {
+            log.info("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
+        }
+
+        // 카페 데이터 초기화
+        if (cafeRepository.count() == 0) {
+            log.info("카페 데이터 저장 시작");
+            cafeService.processAllAreas();
+            log.info("카페 데이터 저장 완료");
+        } else {
+            log.info("카페 데이터가 이미 존재합니다. 건너뜁니다.");
+        }
+
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
new file mode 100644
index 0000000..02bae26
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
@@ -0,0 +1,21 @@
+package com.example.placeservice.repository;
+
+import com.example.placeservice.entity.Accommodation;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+
+public interface AccommodationRepository extends JpaRepository {
+    boolean existsByAccommodationId(Long accommodationId);
+
+    List findByGu(String gu);
+
+    List findByType(String type);
+
+    @Query(value = "SELECT * FROM Accommodation WHERE area_id = ?1", nativeQuery = true)
+    List findByAreaId(Long areaId);
+
+    List findByNameContainingIgnoreCase(String keyword);
+    List findByAddressContainingIgnoreCase(String keyword);
+}
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
index 13d7a2b..d33237f 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
@@ -2,6 +2,13 @@
 
 import com.example.placeservice.entity.Area;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
 
 public interface AreaRepository extends JpaRepository {
-}
+
+
+    @Query("SELECT a.name FROM Area a")
+    List findAllAreaNames();
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java
new file mode 100644
index 0000000..68a2159
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java
@@ -0,0 +1,24 @@
+package com.example.placeservice.repository;
+
+import com.example.placeservice.entity.Accommodation;
+import com.example.placeservice.entity.Attraction;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+import java.util.Optional;
+
+
+public interface AttractionRepository extends JpaRepository {
+    boolean existsBySeoulAttractionId(String seoul_attraction_id);
+    Optional findByAttractionId(Long attraction_id);
+
+    List findByNameContainingIgnoreCase(String keyword);
+
+    List findByAddressContainingIgnoreCase(String keyword);
+
+    // 지역 ID로 관광지 목록 조회
+    @Query("SELECT a FROM Attraction a WHERE a.area.areaId = :areaId")
+    List findByAreaAreaId(@Param("areaId") Long areaId);
+}
diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
new file mode 100644
index 0000000..ae67f5f
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
@@ -0,0 +1,32 @@
+package com.example.placeservice.repository;
+
+import com.example.placeservice.entity.Accommodation;
+import com.example.placeservice.entity.Cafe;
+import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+import java.util.Optional;
+
+public interface CafeRepository extends JpaRepository {
+    @Query("SELECT c FROM Cafe c WHERE c.area.areaId = :areaId")
+    List findByAreaId(@org.springframework.data.repository.query.Param("areaId") Long areaId);
+
+    @Transactional
+    @Modifying
+    @Query("DELETE FROM Cafe c WHERE c.area.areaId = :areaId")
+    void deleteByAreaId(Long areaId);
+
+    Optional findByKakaomapUrl(String placeCode);
+    List findByNameContainingIgnoreCase(String keyword);
+    List findByAddressContainingIgnoreCase(String keyword);
+
+    // 성능 개선을 위한 추가 메소드
+    @EntityGraph(attributePaths = {"area"})
+    List findAll();
+
+    @EntityGraph(attributePaths = {"area"})
+    Optional findById(Long id);
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java
new file mode 100644
index 0000000..89d30a9
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java
@@ -0,0 +1,27 @@
+// CulturalEventRepository.java
+package com.example.placeservice.repository;
+
+import com.example.placeservice.entity.Accommodation;
+import com.example.placeservice.entity.CulturalEvent;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Repository
+public interface CulturalEventRepository extends JpaRepository {
+    boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate);
+
+
+    List findBytitleContainingIgnoreCase(String keyword);
+    List findByAddressContainingIgnoreCase(String keyword);
+
+    // 지역 ID로 문화 행사 목록 조회
+    @Query("SELECT e FROM CulturalEvent e WHERE e.area.areaId = :areaId")
+    List findByAreaAreaId(@Param("areaId") Long areaId);
+
+    List findByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate);
+}
diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
new file mode 100644
index 0000000..bd968cc
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
@@ -0,0 +1,24 @@
+package com.example.placeservice.repository;
+
+import com.example.placeservice.entity.Accommodation;
+import com.example.placeservice.entity.Restaurant;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+import java.util.List;
+
+public interface RestaurantRepository extends JpaRepository {
+    // 기본 CRUD 사용 (findAll, findById, save 등)
+    List findByNameContainingIgnoreCase(String keyword);
+
+
+    List findByAddressContainingIgnoreCase(String keyword);
+
+    // 지역 ID로 음식점 목록 조회
+    @Query("SELECT r FROM Restaurant r WHERE r.area.areaId = :areaId")
+    List findByAreaAreaId(@Param("areaId") Long areaId);
+
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
new file mode 100644
index 0000000..572a7fe
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
@@ -0,0 +1,229 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.accommodation.AccommodationDto;
+import com.example.placeservice.dto.accommodation.KakaoApiResponse;
+import com.example.placeservice.entity.Accommodation;
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.repository.AccommodationRepository;
+import com.example.placeservice.repository.AreaRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.cdimascio.dotenv.Dotenv;
+import jakarta.transaction.Transactional;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.math.BigDecimal;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class AccommodationService {
+    private final AreaRepository areaRepository;
+    private final AccommodationRepository accommodationRepository; // AccommodationRepository 주입
+    private final ObjectMapper objectMapper;
+
+    private final String baseUrl = "https://dapi.kakao.com/v2/local/search/category.json";
+    private final RestTemplate restTemplate;
+    private final Dotenv dotenv;
+
+    public AccommodationService(AreaRepository areaRepository, AccommodationRepository accommodationRepository, ObjectMapper objectMapper, RestTemplate restTemplate, Dotenv dotenv) {
+        this.areaRepository = areaRepository;
+        this.accommodationRepository = accommodationRepository;
+        this.objectMapper = objectMapper;
+        this.restTemplate = restTemplate;
+        this.dotenv = dotenv;
+    }
+
+    public String searchAccommodations(BigDecimal x, BigDecimal y) {
+        HttpHeaders headers = new HttpHeaders();
+        String kakaoApiKey = dotenv.get("KAKAO_API_KEY");
+        headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+        HttpEntity entity = new HttpEntity<>(headers);
+
+        URI uri = UriComponentsBuilder
+                .fromUriString(baseUrl)
+                .queryParam("category_group_code", "AD5")
+                .queryParam("radius", 1000)
+                .queryParam("x", x.toString()) // BigDecimal을 문자열로 변환
+                .queryParam("y", y.toString()) // BigDecimal을 문자열로 변환
+                .build()
+                .encode(StandardCharsets.UTF_8) // UTF-8 인코딩 명시
+                .toUri();
+
+        try {
+            ResponseEntity response = restTemplate.exchange(
+                    uri,
+                    HttpMethod.GET,
+                    entity,
+                    String.class // 응답 본문을 문자열(JSON)로 받음
+            );
+
+            // 응답 상태 코드가 OK(200) 인지 확인
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return response.getBody(); // 성공 시 응답 본문(JSON 문자열) 반환 [1]
+            } else {
+                // API 호출은 성공했으나, 응답 코드가 200 OK가 아닌 경우
+                log.error("Error calling Kakao API. Status code: {} Response: {}", response.getStatusCode(), response.getBody());
+                return null; // 실패 시 null 반환 [1]
+            }
+        } catch (RestClientException e) {
+            // RestTemplate 사용 중 네트워크 오류 등 예외 발생 시
+            log.error("Exception during Kakao Api call to {} : {}", uri, e.getMessage());
+            return null; // 예외 발생 시 null 반환
+        }
+    }
+
+    @Transactional
+    public void saveAccommodations() throws JsonProcessingException {
+        List areas = areaRepository.findAll();
+
+        List accommodationsToSave = new ArrayList<>();
+
+        for (Area area : areas) {
+            BigDecimal lon = area.getLon(); // Area에서 경도(lon) 가져오기
+            BigDecimal lat = area.getLat(); // Area에서 위도(lat) 가져오기
+
+            String jsonResponse = searchAccommodations(lon, lat); // API 호출 함수 분리
+
+            if (jsonResponse != null && !jsonResponse.isEmpty()) {
+                try {
+                    KakaoApiResponse apiResponse = objectMapper.readValue(jsonResponse, KakaoApiResponse.class);
+
+                    if (apiResponse != null && apiResponse.getDocuments() != null) {
+                        for (KakaoApiResponse.Document doc : apiResponse.getDocuments()) {
+                            Long kakaoId = Long.parseLong(doc.getId());
+
+                            if (!accommodationRepository.existsByAccommodationId(kakaoId)) { // 중복 여부 확인
+                                Accommodation accommodation = mapDocumentToAccommodation(doc, area); // 매핑 함수 사용
+                                accommodationsToSave.add(accommodation);
+
+                            }
+//                            else {
+//                                log.info("새롭게 저장할 숙박업소가 없습니다.");
+//                            }
+                        }
+                    }
+                } catch (JsonProcessingException e) {
+                    log.error("Json 파싱 실패 : {}", e.getMessage());
+                } catch (NumberFormatException e) {
+                    log.error("숙박업소 Id 오류 : {}", e.getMessage());
+                }
+            } else {
+                log.error("유효한 응답이 없습니다.");
+            }
+            // try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
+        }
+
+        // 5. 수집된 숙소 정보 일괄 저장
+        if (!accommodationsToSave.isEmpty()) {
+            accommodationRepository.saveAll(accommodationsToSave); // saveAll로 성능 향상
+            log.info("숙박업소 저장 성공");
+        } else {
+            log.info("신규 저장 숙박업소가 없습니다.");
+        }
+    }
+
+    private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc, Area area) {
+        Accommodation accommodation = new Accommodation();
+        try {
+            accommodation.setAccommodationId(Long.parseLong(doc.getId())); // String ID를 Long으로 변환
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("Invalid accommodation ID format: " + doc.getId(), e);
+        }
+        accommodation.setArea(area);
+        accommodation.setName(doc.getPlaceName());
+        accommodation.setAddress(doc.getRoadAddressName());
+        try {
+            accommodation.setLat(new BigDecimal(doc.getY())); // String 좌표를 BigDecimal로 변환
+            accommodation.setLon(new BigDecimal(doc.getX())); // String 좌표를 BigDecimal로 변환
+        } catch (NumberFormatException e) {
+            log.warn("좌표 설정 실패 : {}", e.getMessage());
+            accommodation.setLat(null);
+            accommodation.setLon(null);
+        }
+        accommodation.setPhone(doc.getPhone());
+
+        String[] parts = doc.getRoadAddressName().split(" ");
+        for (String part : parts) {
+            if (part.endsWith("구")) {
+                accommodation.setGu(part);
+            }
+        }
+
+        parts = doc.getCategoryName().split(" > ");
+        if (parts.length > 2) {
+            accommodation.setType(parts[2]);
+        }
+        else {
+            accommodation.setType("구분없음");
+        }
+        accommodation.setKakaomapUrl(doc.getPlaceUrl());
+
+        return accommodation;
+    }
+
+    public List showAccommodations() {
+        List accommodations = accommodationRepository.findAll();
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+        return accommodationDtos;
+    }
+
+    public AccommodationDto getAccommodationById(Long id) {
+        return AccommodationDto.fromEntity(accommodationRepository.findById(id).get());
+    }
+
+    public List getAccommodationByGu(String gu) {
+        List accommodations = accommodationRepository.findByGu(gu);
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+        return accommodationDtos;
+    }
+
+    public List getAccommodationByType(String type) {
+        List accommodations = accommodationRepository.findByType(type);
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+        return accommodationDtos;
+    }
+
+    public Map getAccommodationByAreaId(Long areaId) {
+        List accommodations = accommodationRepository.findByAreaId(areaId);
+        List accommodationDtos = new ArrayList<>();
+
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+
+        Map response = new HashMap<>();
+
+        response.put("type", "accommodation"); // 고정된 타입 문자열
+        response.put("content", accommodationDtos); // 위에서 만든 DTO 리스트
+
+        return response;
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java
new file mode 100644
index 0000000..20b6911
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java
@@ -0,0 +1,82 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.AiSummaryResponse;
+import com.example.placeservice.dto.AiSummaryParsedResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Slf4j
+@Service
+public class AiSummaryService {
+
+    private final WebClient webClient;
+
+    // fastapi-svc 값을 application.properties에서 주입
+    public AiSummaryService(@Value("${fastapi-svc}") String fastapiUrl) {
+        this.webClient = WebClient.builder()
+                .baseUrl(fastapiUrl)
+                .build();
+    }
+
+    public AiSummaryParsedResponse getSummaryParsed(String targetType, String targetId) {
+        AiSummaryResponse raw = webClient.get()
+                .uri(uriBuilder -> uriBuilder
+                        .path("/summary/{target_type}/{target_id}")
+                        .build(targetType, targetId))
+                .retrieve()
+                .bodyToMono(AiSummaryResponse.class)
+                .block();
+        return parseSummaryContent(Objects.requireNonNull(raw));
+    }
+
+    private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) {
+        String content = raw.getContent();
+        try {
+            List posKeywords = extractKeywords(content, "\\[긍정 키워드\\](.*)");
+            List negKeywords = extractKeywords(content, "\\[부정 키워드\\](.*)");
+            int posCount = extractInt(content, "\\[긍정 라벨 수\\]\\s*(\\d+)");
+            int negCount = extractInt(content, "\\[부정 라벨 수\\]\\s*(\\d+)");
+
+            return AiSummaryParsedResponse.builder()
+                    .positiveKeywords(posKeywords)
+                    .negativeKeywords(negKeywords)
+                    .positiveCount(posCount)
+                    .negativeCount(negCount)
+                    .build();
+        }catch (Exception e){
+            log.debug(content);
+            log.error(e.getMessage());
+            return AiSummaryParsedResponse.builder()
+                    .positiveKeywords(Collections.singletonList(""))
+                    .negativeKeywords(Collections.singletonList(""))
+                    .positiveCount(0)
+                    .negativeCount(0)
+                    .build();
+        }
+    }
+
+    private List extractKeywords(String content, String pattern) {
+        Matcher matcher = Pattern.compile(pattern).matcher(content);
+        if (matcher.find()) {
+            String[] keywords = matcher.group(1).trim().split("\\s*,\\s*");
+            return Arrays.stream(keywords)
+                    .filter(k -> !k.isBlank())
+                    .toList();
+        }
+        return List.of();
+    }
+
+    private int extractInt(String content, String pattern) {
+        Matcher matcher = Pattern.compile(pattern).matcher(content);
+        return matcher.find() ? Integer.parseInt(matcher.group(1)) : 0;
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/AreaService.java b/place-service/src/main/java/com/example/placeservice/service/AreaService.java
new file mode 100644
index 0000000..70a134c
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/AreaService.java
@@ -0,0 +1,34 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.AreaDto;
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.repository.AreaRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class AreaService {
+    private final AreaRepository areaRepository;
+
+
+    public List getAreaData() {
+        try{
+            List areas = areaRepository.findAll();
+
+            return areas.stream()
+                    .map(AreaDto::new)
+                    .collect(Collectors.toList());
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            throw new RuntimeException("예상치 못한 오류",e);
+        }
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java
new file mode 100644
index 0000000..64ec8e3
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java
@@ -0,0 +1,58 @@
+package com.example.placeservice.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AttractionKakaoMapClient {
+
+    @Value("${kakao.api.key}")
+    private String kakaoApiKey;
+
+
+
+    public JsonNode searchAttractionByKeyword(String keyword) {
+        try {
+            RestTemplate restTemplate = new RestTemplate();
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+
+            // Kakao API 호출 URL 세팅
+            String builder = UriComponentsBuilder
+                    .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json")
+                    .queryParam("query", keyword)
+                    .queryParam("sort", "accuracy") // 정확도순 정렬
+                    .queryParam("page", 1) // 요청 페이지
+                    .queryParam("size", 15)
+                    .build(false).toString(); // 한 페이지에 최대 15개 결과
+
+            HttpEntity entity = new HttpEntity<>(headers);
+
+            // REST API 호출
+            ResponseEntity response = restTemplate.exchange(
+                    builder.toString(),
+                    HttpMethod.GET,
+                    entity,
+                    JsonNode.class
+            );
+
+            return response.getBody(); // JSON 응답 반환
+        } catch (Exception e) {
+            log.error("관광지 카카오맵 url 조회 중 오류 발생 : {}",e.getMessage());
+            return null;
+        }
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
new file mode 100644
index 0000000..bdb140e
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
@@ -0,0 +1,238 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.attraction.AreaAttractionsDto;
+import com.example.placeservice.dto.attraction.AttractionDto;
+import com.example.placeservice.dto.attraction.AttractionInfoDto;
+import com.example.placeservice.dto.attraction.AttractionListDto;
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.entity.Attraction;
+import com.example.placeservice.repository.AreaRepository;
+import com.example.placeservice.repository.AttractionRepository;
+import com.fasterxml.jackson.databind.JsonNode;
+import jakarta.xml.bind.JAXBContext;
+import jakarta.xml.bind.Unmarshaller;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Objects;
+
+import static com.example.placeservice.util.GeoUtils.calculateDistanceKm;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AttractionService {
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final AttractionRepository attractionRepository;
+    private final AreaRepository areaRepository;
+    private final AttractionKakaoMapClient attractionKakaoMapClient;
+
+    // visitSeoul 관광지 데이터 로드 후 attraction 테이블 저장
+    public void fetchDataFromVisitSeoul() {
+        String url="https://www.visitseoul.net/file_save/OPENAPI/OPEN_API_kr.xml";
+        List areaList = areaRepository.findAll();
+
+        log.info("관광지 데이터 저장 시작");
+
+        try {
+            // 1. XML 받아오기
+            ResponseEntity response = restTemplate.getForEntity(url, String.class);
+            String xml = new String(Objects.requireNonNull(response.getBody()).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
+            xml = xml.trim().replace("\uFEFF", "");
+
+            // 2. JAXB로 XML → DTO 변환
+            JAXBContext jaxbContext = JAXBContext.newInstance(AttractionDto.class);
+            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
+            StringReader reader = new StringReader(xml);
+            AttractionDto dto = (AttractionDto) unmarshaller.unmarshal(reader);
+
+            // 3. DTO → Entity로 변환 (매핑)
+            List entities = dto.getAttractions().stream()
+                .map(table -> {
+
+                    // seoul_attraction_id 중복 체크
+                    if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null;
+                    if (Integer.parseInt(table.getId()) >= 31345 && Integer.parseInt(table.getId()) <= 53595) return null;
+
+                    // 👇: 조건에 따라 Area 객체를 지정
+                    Area area = findAreaByCondition(table, areaList);  // 예: 주소나 지역코드 등으로 판단
+                    if (area == null) return null;
+
+                    Attraction attraction = new Attraction();
+                    attraction.setSeoulAttractionId(table.getId());
+                    attraction.setName(table.getTitle());
+                    attraction.setAddress(table.getNewAddress());
+                    attraction.setLat(new BigDecimal(table.getMapY()));
+                    attraction.setLon(new BigDecimal(table.getMapX()));
+                    attraction.setPhone(table.getTel());
+
+
+                    if(normalizeAndValidateUrl(table.getHomepage()) != null){
+                        attraction.setHomepageUrl(normalizeAndValidateUrl(table.getHomepage()));
+                    }
+
+//                    attraction.setHomepageUrl(table.getHomepage());
+                    attraction.setCloseDay(table.getCloseDay());
+                    attraction.setUseTime(table.getUseTime());
+                    attraction.setArea(area);
+
+                    // kakaomap_url 추가
+                    String title = table.getTitle();
+                    if (title.length() <= 100){ // 키워드 최대 100글자까지 검색가능
+                        JsonNode kakao_response = attractionKakaoMapClient.searchAttractionByKeyword(title);
+                        if (kakao_response.has("documents")) {
+                            for (JsonNode doc : kakao_response.get("documents")) {
+                                String categoryName = doc.get("category_name").asText();
+                                if (categoryName.contains("여행 > 관광") || categoryName.contains("여행 > 명소")) {
+//                                    System.out.println("🎯 관광 관련 장소: " +table.getTitle()+"는 바로바로 :::"+ doc.get("place_name").asText());
+                                    attraction.setKakaomapUrl(doc.get("place_url").asText());
+                                    break;
+                                }
+                            }
+                        }else{
+                            log.debug("카카오 관광지 검색 결과가 없습니다 : {}", title);}
+                    }
+                return attraction;
+            })
+            .filter(Objects::nonNull) // null 필터링
+            .toList();
+
+            log.info("관광지 데이터 저장 완료 : {}",entities.size());
+            // 4. DB에 저장
+            attractionRepository.saveAll(entities);
+
+        } catch (Exception e) {
+            log.error("관광지 데이터 저장 중 오류 발생 : {}",e.getMessage());
+            throw new RuntimeException("외부 XML 데이터 파싱 실패", e);
+        }
+    }
+
+    private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) {
+        double lat = Double.parseDouble(table.getMapY());
+        double lon = Double.parseDouble(table.getMapX());
+
+        Area closestArea = null;
+        double minDistance = Double.MAX_VALUE;
+
+        for (Area area : areaList) {
+            double distance = calculateDistanceKm(
+                    lat, lon,
+                    area.getLat().doubleValue(), area.getLon().doubleValue()
+            );
+
+            if (distance < minDistance) {
+                minDistance = distance;
+                closestArea = area;
+            }
+        }
+        if (minDistance > 2.0) { // 2. 거리가 2km를 넘어가면 제외한다.
+            log.debug("{} - 가까운 지역 없음 (거리: {}km)", table.getTitle(), minDistance);
+            return null;
+        }
+        return closestArea;
+    }
+
+
+
+    // attraction 목록 DB 불러오기
+    public List getAttractionData() {
+        try {
+                List areas = areaRepository.findAll();
+
+                return areas.stream()
+                        .map(area -> {
+                            List attractions = area.getAttractions().stream()
+                                    .map(attraction -> new AttractionListDto(
+                                            attraction.getAttractionId(),
+                                            attraction.getSeoulAttractionId(),
+                                            attraction.getName(),
+                                            attraction.getAddress(),
+                                            attraction.getLat(),
+                                            attraction.getLon(),
+                                            attraction.getKakaomapUrl()
+                                    )).toList();
+
+                            AreaAttractionsDto dto = new AreaAttractionsDto();
+                            dto.setArea_name(area.getName());
+                            dto.setAttraction_list(attractions);
+                            return dto;
+                        })
+                        .toList();
+
+        } catch (RuntimeException e) {
+            log.error("관광지 목록 조회 오류 발생 : {}", e.getMessage());
+            throw new RuntimeException("예상치 못한 오류",e);
+        }
+    }
+
+    public AttractionInfoDto getAttractionInfoData(long attractionId) {
+        try{
+            Attraction attraction = attractionRepository.findByAttractionId(attractionId)
+                    .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다."));
+
+            // 연결된 Area 정보 가져오기
+            Area area = attraction.getArea(); // 여기에서 바로 가져올 수 있음
+            String areaName = (area != null) ? area.getName() : null;
+            Long areaId = (area != null) ? area.getAreaId() : null;
+
+            return new AttractionInfoDto(
+                    attraction.getAttractionId(),
+                    attraction.getSeoulAttractionId(),
+                    attraction.getName(),
+                    attraction.getAddress(),
+                    attraction.getLat(),
+                    attraction.getLon(),
+                    attraction.getPhone(),
+                    attraction.getHomepageUrl(),
+                    attraction.getCloseDay(),
+                    attraction.getUseTime(),
+                    attraction.getKakaomapUrl(),
+                    areaId,
+                    areaName
+            );
+
+        } catch (RuntimeException e) {
+            log.error("관광지 정보 조회 오류 발생 : {}", e.getMessage());
+            throw new RuntimeException("예상치 못한 오류",e);
+        }
+    }
+
+    // 접속이 가능한 홈페이지인지 확인
+    public String normalizeAndValidateUrl(String rawUrl) {
+        if (rawUrl == null || rawUrl.isBlank()) return null;
+
+        // 기본적인 정제
+        String url = rawUrl.trim();
+        if (!url.startsWith("http")) {
+            url = "https://" + url;
+        }
+
+        try {
+            HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+            conn.setRequestMethod("HEAD");
+            conn.setConnectTimeout(1000);
+            conn.setReadTimeout(1000);
+            int code = conn.getResponseCode();
+
+            if (code >= 200 && code < 400) {
+                return url;
+            }else{
+                return null;
+            }
+        } catch (Exception e) {
+            return null; // 실패 시 null 반환
+        }
+
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java
new file mode 100644
index 0000000..9f3652c
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java
@@ -0,0 +1,329 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.cafe.CafeListDto;
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.entity.Cafe;
+import com.example.placeservice.repository.AreaRepository;
+import com.example.placeservice.repository.CafeRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CafeService {
+
+    private final AreaRepository areaRepository;
+    private final CafeRepository cafeRepository;
+    private final KakaoApiService kakaoApiService;
+
+    // 모든 지역에 대한 카페 처리가 완료되었는지 확인하는 플래그
+    private boolean processingCompleted = false;
+
+    /**
+     * 레스토랑 스타일로 단순화된 카페 목록 조회 (캐싱 없음)
+     */
+    @Transactional(readOnly = true)
+    public List getSimpleCafeList() {
+        log.info("심플한 카페 목록 조회 - 레스토랑 스타일 (캐싱 없음)");
+        return cafeRepository.findAll().stream()
+                .map(cafe -> new CafeListDto(
+                        cafe.getId(),
+                        cafe.getName(),
+                        cafe.getLat(),
+                        cafe.getLon()
+                ))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 카페 상세 정보 조회 (캐싱 없음)
+     */
+    @Transactional(readOnly = true)
+    public Map getCafeInfo(Long cafeId) {
+        log.info("카페 상세 정보 조회 (캐싱 없음): {}", cafeId);
+        Optional cafeOptional = cafeRepository.findById(cafeId);
+
+        if (cafeOptional.isPresent()) {
+            Cafe cafe = cafeOptional.get();
+            Map cafeInfo = new LinkedHashMap<>();
+            cafeInfo.put("cafe_id", cafe.getId());
+            cafeInfo.put("cafe_name", cafe.getName());
+            cafeInfo.put("address", cafe.getAddress());
+            cafeInfo.put("phone", cafe.getPhone());
+            cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl());
+            cafeInfo.put("lat", cafe.getLat());
+            cafeInfo.put("lon", cafe.getLon());
+
+            // 지역(Area) 정보 추가
+            if (cafe.getArea() != null) {
+                cafeInfo.put("area_id", cafe.getArea().getAreaId());
+                cafeInfo.put("area_name", cafe.getArea().getName());
+            }
+
+            return cafeInfo;
+        }
+
+        return null;
+    }
+
+    /**
+     * 모든 지역에 대한 카페 데이터 처리
+     */
+    @Async
+    @Transactional
+    public void processAllAreas() {
+        log.info("processAllAreas() 메서드 시작");
+
+        // 이미 처리가 완료되었거나 이미 데이터가 있으면 다시 실행하지 않음
+        if (processingCompleted) {
+            log.info("이미 처리가 완료되었습니다. 중복 실행 방지를 위해 종료합니다.");
+            return;
+        }
+
+        long cafeCount = cafeRepository.count();
+        log.info("현재 DB에 저장된 카페 수: {}", cafeCount);
+
+        if (cafeCount > 0) {
+            log.info("카페 데이터가 이미 존재합니다. 처리를 건너뜁니다.");
+            processingCompleted = true;
+            return;
+        }
+
+        try {
+            List areas = areaRepository.findAll();
+
+            // 다른 처리 중 중복 실행 방지
+            processingCompleted = true;
+
+            log.info("지역별 카페 정보 처리 시작");
+            for (int i = 0; i < areas.size(); i++) {
+                Area area = areas.get(i);
+                log.info("[{}/{}] '{}' (ID: {}) 처리 중...",
+                        i + 1, areas.size(), area.getName(), area.getAreaId());
+
+                processSingleArea(area);
+
+                // API 호출 간격 조절
+                try {
+                    Thread.sleep(300);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    log.error("스레드 인터럽트 발생", e);
+                }
+            }
+
+            // 최종 카페 수 확인
+            log.info("최종 저장된 카페 수: {}", cafeRepository.count());
+
+        } catch (Exception e) {
+            processingCompleted = false; // 오류 발생 시 플래그 재설정
+            log.error("카페 데이터 처리 중 오류 발생: {}", e.getMessage());
+            throw e;
+        }
+    }
+
+    /**
+     * 단일 장소의 주변 카페 정보 처리 - 벌크 저장으로 최적화
+     */
+    @Transactional
+    public void processSingleArea(Area area) {
+//        log.info("processSingleArea() 시작 - 지역: '{}' (ID: {})", area.getName(), area.getAreaId());
+
+        try {
+            // 해당 지역의 기존 카페 데이터 삭제
+            log.info("지역 ID {}의 기존 카페 데이터 삭제 시도", area.getAreaId());
+            cafeRepository.deleteByAreaId(area.getAreaId());
+            log.info("지역 ID {}의 기존 카페 데이터 삭제 완료", area.getAreaId());
+
+            // 주변 카페 검색
+            double latitude = area.getLat().doubleValue();
+            double longitude = area.getLon().doubleValue();
+
+            if (latitude == 0 || longitude == 0) {
+                log.warn("장소 ID {}의 위도/경도 정보가 없습니다. 위도: {}, 경도: {}",
+                        area.getAreaId(), latitude, longitude);
+                return;
+            }
+
+//            log.info("카카오 API 호출 - 위도: {}, 경도: {}, 반경: 1000m", latitude, longitude);
+            List cafes =
+                    kakaoApiService.findCafesNearby(latitude, longitude, 1000);
+            log.info("카카오 API 응답 - 총 {}개의 장소 정보 수신", cafes.size());
+
+            // "음식점 > 카페"가 포함된 카페만 필터링하고 "(휴업중)" 카페는 제외
+            List filteredCafes = cafes.stream()
+                    .filter(cafe -> {
+//                        if (!categoryMatch) {
+//                            log.debug("카테고리 불일치로 제외: {}, 카테고리: {}",
+//                                    cafe.getPlaceName(), cafe.getCategoryName());;
+//                        }
+                        return cafe.getCategoryName() != null &&
+                                cafe.getCategoryName().contains("음식점 > 카페");
+                    })
+                    .filter(cafe -> {
+//                        if (!notClosed) {
+//                            log.debug("휴업중으로 제외: {}", cafe.getPlaceName());
+//                        }
+                        return cafe.getPlaceName() == null ||
+                                !cafe.getPlaceName().contains("(휴업중)");
+                    })
+                    .toList();
+
+            log.info("'{}' 주변 총 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리(휴업중 제외)를 찾았습니다.",
+                    area.getName(), cafes.size(), filteredCafes.size());
+
+            // 최대 15개의 카페만 저장 (음식점과 동일한 로직 적용)
+            int maxCafesToSave = 15;
+
+            // 벌크 저장을 위한 리스트
+            List cafesToSave = new ArrayList<>();
+
+            // 필터링된 카페 정보 저장
+            for (KakaoApiService.CafeResponse.Document cafeDoc : filteredCafes) {
+                // 최대 카페 저장 개수 제한
+                if (cafesToSave.size() >= maxCafesToSave) {
+                    log.info("최대 저장 개수({})에 도달하여 저장 중단", maxCafesToSave);
+                    break;
+                }
+
+                log.info("카페 저장 시도: {}, URL: {}", cafeDoc.getPlaceName(), cafeDoc.getPlaceUrl());
+
+                // 이미 동일한 카페가 있는지 확인 (kakaomapUrl로 중복 체크)
+                Optional existingCafe = cafeRepository.findByKakaomapUrl(cafeDoc.getPlaceUrl());
+                if (existingCafe.isPresent()) {
+                    log.info("이미 존재하는 카페이므로 건너뜀: {}, ID: {}",
+                            existingCafe.get().getName(), existingCafe.get().getId());
+                    continue; // 이미 존재하는 카페는 건너뜀
+                }
+
+                try {
+                    Cafe cafe = Cafe.builder()
+                            .name(cafeDoc.getPlaceName())
+                            .address(cafeDoc.getAddressName())
+                            .lat(new BigDecimal(cafeDoc.getY()))
+                            .lon(new BigDecimal(cafeDoc.getX()))
+                            .phone(cafeDoc.getPhone())
+                            .kakaomapUrl(cafeDoc.getPlaceUrl())
+                            .categoryCode(cafeDoc.getCategoryGroupCode())
+                            .area(area)
+                            .build();
+
+//                    log.info("카페 객체 생성 완료: {}", cafe.getName());
+                    cafesToSave.add(cafe); // 리스트에 추가
+                } catch (Exception e) {
+                    log.error("카페 저장 중 오류 발생: {}, 오류: {}", cafeDoc.getPlaceName(), e.getMessage(), e);
+                }
+            }
+
+            // 벌크 저장 수행
+            if (!cafesToSave.isEmpty()) {
+                List savedCafes = cafeRepository.saveAll(cafesToSave);
+                log.info("지역 '{}' (ID: {})에 총 {}개의 카페 일괄 저장 완료",
+                        area.getName(), area.getAreaId(), savedCafes.size());
+            }
+
+        } catch (Exception e) {
+            log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e);
+            throw e; // 상위 메서드에서 처리할 수 있도록 예외 전파
+        }
+    }
+
+    /**
+     * 특정 지역의 카페 목록 조회
+     */
+//    @Transactional(readOnly = true)
+//    public List getCafesByAreaId(Long areaId) {
+//        log.info("지역 ID {}의 카페 목록 조회", areaId);
+//        List cafes = cafeRepository.findByAreaId(areaId);
+//        log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size());
+//
+//        return cafes.stream()
+//                .map(this::convertToDto)
+//                .collect(Collectors.toList());
+//    }
+
+    /**
+     * 모든 카페 목록 조회
+     */
+//    @Transactional(readOnly = true)
+//    public List getAllCafes() {
+//        log.info("모든 카페 목록 조회");
+//        List cafes = cafeRepository.findAll();
+//        log.info("총 {}개의 카페 조회됨", cafes.size());
+//
+//        return cafes.stream()
+//                .map(this::convertToDto)
+//                .collect(Collectors.toList());
+//    }
+
+    /**
+     * Cafe 엔티티를 DTO로 변환
+     */
+//    private CafeDto convertToDto(Cafe cafe) {
+//        return CafeDto.builder()
+//                .id(cafe.getId())
+//                .name(cafe.getName())
+//                .address(cafe.getAddress())
+//                .lat(cafe.getLat())
+//                .lon(cafe.getLon())
+//                .phone(cafe.getPhone())
+//                .kakaomapUrl(cafe.getKakaomapUrl())
+//                .categoryCode(cafe.getCategoryCode())
+//                .build();
+//    }
+
+    /**
+     * 카카오맵 URL로 카페 상세 정보 조회
+     */
+//    @Transactional(readOnly = true)
+//    public Map getCafeDetailByPlaceCode(String placeCode) {
+//        log.info("장소 코드로 카페 상세 정보 조회: {}", placeCode);
+//        Optional cafeOptional = cafeRepository.findByKakaomapUrl(placeCode);
+//
+//        if (cafeOptional.isPresent()) {
+//            Cafe cafe = cafeOptional.get();
+//            log.info("카페 찾음: {}, ID: {}", cafe.getName(), cafe.getId());
+//
+//            Map cafeInfo = new HashMap<>();
+//            cafeInfo.put("name", cafe.getName());
+//            cafeInfo.put("address", cafe.getAddress());
+//            cafeInfo.put("phone", cafe.getPhone());
+//            cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl());
+//            cafeInfo.put("lat", cafe.getLat());
+//            cafeInfo.put("lon", cafe.getLon());
+//            cafeInfo.put("category_code", cafe.getCategoryCode());
+//
+//            return cafeInfo;
+//        }
+//
+//        log.info("장소 코드 {}에 해당하는 카페를 찾을 수 없음", placeCode);
+//        return null;
+//    }
+
+    /**
+     * 지역 ID로 카페 목록 조회 (응답 형식 맞춤)
+     */
+//    @Transactional(readOnly = true)
+//    public Map getCafesByAreaIdFormatted(Long areaId) {
+//        log.info("지역 ID {}의 카페 목록 조회 (응답 형식 맞춤)", areaId);
+//
+//        List cafes = cafeRepository.findByAreaId(areaId);
+//        List cafeDtos = cafes.stream()
+//                .map(this::convertToDto)
+//                .collect(Collectors.toList());
+//
+//        Map response = new HashMap<>();
+//        response.put("type", "cafe");
+//        response.put("content", cafeDtos);
+//
+//        return response;
+//    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
new file mode 100644
index 0000000..82260ca
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
@@ -0,0 +1,118 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.culturalevent.CulturalEventItem;
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.entity.CulturalEvent;
+import com.example.placeservice.repository.AreaRepository;
+import com.example.placeservice.repository.CulturalEventRepository;
+import com.example.placeservice.util.GeoUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CulturalEventService {
+
+    private final EventParserService eventParserService;
+    private final CulturalEventRepository culturalEventRepository;
+    private final AreaRepository areaRepository;
+
+    private final RestTemplate restTemplate;
+
+    @Transactional
+    public void fetchAndSaveAllEvents() {
+        log.info("행사 데이터 저장 시작");
+
+        int[] ranges = {1, 1001, 2001, 3001, 4001, 5001};
+        for (int startIndex : ranges) {
+            int endIndex = startIndex + 999;
+
+            String jsonData = fetchEventData(startIndex, endIndex);
+            List eventItems = eventParserService.parse(jsonData);
+
+            // 행사 종료일이 오늘 이후 또는 오늘인 것만 추가
+            List filteredList = new ArrayList<>();
+            for (CulturalEventItem item : eventItems) {
+                String endDateStr = String.valueOf(item.getEndDate());
+                LocalDate endDate = LocalDate.parse(endDateStr.substring(0, 10));
+
+                if (!endDate.isBefore(LocalDate.now())) {
+                    filteredList.add(item);
+                }
+            }
+
+            if (!eventItems.isEmpty()) {
+                saveEvents(filteredList);
+            }
+        }
+        log.info("행사 데이터 저장 완료");
+    }
+
+    private void saveEvents(List eventItems) {
+        List events = eventParserService.toEntityList(eventItems);
+        List areas = areaRepository.findAll();
+
+            for (CulturalEvent event : events) {
+                if (!culturalEventRepository.existsByTitleAndAddressAndStartDate(
+                        event.getTitle(), event.getAddress(), event.getStartDate())) {
+                    try{
+                        // 행사 위치 기준 가장 가까운 지역 찾기
+                        Area nearestArea = findNearestArea(event.getLat().doubleValue(), event.getLon().doubleValue(), areas);
+                        event.setArea(nearestArea); // 🔹 지역 설정
+
+                        culturalEventRepository.save(event);
+                    }catch (Exception e){
+                        log.error(e.getMessage()+event.getTitle());
+                    }
+                }else{
+                    // 이미 있는 데이터인데 종료일이 지났으면 삭제
+                    List existing = culturalEventRepository.findByTitleAndAddressAndStartDate(
+                            event.getTitle(), event.getAddress(), event.getStartDate());
+
+                    if (existing != null){
+                        for (CulturalEvent existingEvent : existing) {
+                            if(existingEvent.getEndDate() != null && existingEvent.getEndDate().isBefore(LocalDate.now().atStartOfDay())){
+                                culturalEventRepository.delete(existingEvent);
+                            }
+                        }
+                    }
+                }
+            }
+
+    }
+
+    private Area findNearestArea(double lat, double lon, List areas) {
+        double minDistance = Double.MAX_VALUE;
+        Area nearestArea = null;
+
+        for (Area area : areas) {
+            double areaLat = area.getLat().doubleValue();
+            double areaLon = area.getLon().doubleValue();
+            double distance = GeoUtils.calculateDistanceKm(lat, lon, areaLat, areaLon);
+
+            if (distance < minDistance) {
+                minDistance = distance;
+                nearestArea = area;
+            }
+        }
+        return nearestArea;
+    }
+
+    private String fetchEventData(int startIndex, int endIndex) {
+        try {
+            String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex);
+            return restTemplate.getForObject(url, String.class);
+        }catch (Exception e){
+            log.error("행사데이터 API 조회 중 오류 발생 :{}",e.getMessage());
+        }
+        return null;
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java
new file mode 100644
index 0000000..d101067
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java
@@ -0,0 +1,61 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.culturalevent.CulturalEventItem;
+import com.example.placeservice.dto.culturalevent.CulturalEventResponse;
+import com.example.placeservice.entity.CulturalEvent;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import lombok.extern.slf4j.Slf4j;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.List;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor // final 필드에 대한 생성자 주입
+public class EventParserService {
+
+    private final ObjectMapper objectMapper;
+
+    public List parse(String jsonResponse) {
+        try {
+            CulturalEventResponse response = objectMapper.readValue(jsonResponse, CulturalEventResponse.class);
+            return response.getCulturalEventInfo().getRow();
+        } catch (Exception e) {
+            log.error("파싱 중 오류 발생: {}", e.getMessage());
+            return List.of(); // 빈 리스트 반환
+        }
+    }
+
+    public CulturalEvent toEntity(CulturalEventItem item) {
+        CulturalEvent event = new CulturalEvent();
+
+
+        // 2. 필드 매핑 (컬럼명에 맞춰 정확히 매핑)
+        event.setCategory(item.getCodename());           // category
+        event.setTitle(item.getTitle());                 // title
+        event.setAddress(item.getPlace());             // address
+        event.setLat(item.getLot());                     // lat
+        event.setLon(item.getLat());                     // lon
+        event.setTarget(item.getUseTrgt());               // target
+        event.setEventFee(item.getUseFee());                // event_fee
+        event.setStartDate(item.getStrtdate());         // start_date
+        event.setEndDate(item.getEndDate());             // end_date
+
+        // 3. 이미지 URL 정리
+        String imageUrl = item.getMainImg();
+        if (imageUrl != null) {
+            imageUrl = imageUrl.replace("https://culture.seoul.go.ko?",
+                    "https://culture.seoul.go.kr/cmmn/file/getImage.do?");
+        }
+        event.setEventImg(imageUrl);                               // event_img
+
+        return event;
+    }
+
+
+    public List toEntityList(List items) {
+        return items.stream()
+                .map(this::toEntity)
+                .toList();
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java
new file mode 100644
index 0000000..7f028a2
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java
@@ -0,0 +1,144 @@
+package com.example.placeservice.service;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class KakaoApiService {
+
+    private final RestTemplate restTemplate;
+
+    @Value("${kakao.api.key}")
+    private String kakaoApiKey;
+
+    private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local";
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class CafeResponse {
+        private Meta meta;
+        private List documents;
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Meta {
+            @JsonProperty("total_count")
+            private int totalCount;
+            @JsonProperty("pageable_count")
+            private int pageableCount;
+            @JsonProperty("is_end")
+            private boolean isEnd;
+        }
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Document {
+            @JsonProperty("id")
+            private String id;
+            @JsonProperty("place_name")
+            private String placeName;
+            @JsonProperty("category_name")
+            private String categoryName;
+            @JsonProperty("category_group_code")
+            private String categoryGroupCode;
+            @JsonProperty("category_group_name")
+            private String categoryGroupName;
+            @JsonProperty("phone")
+            private String phone;
+            @JsonProperty("address_name")
+            private String addressName;
+            @JsonProperty("road_address_name")
+            private String roadAddressName;
+            private String x;  // 경도
+            private String y;  // 위도
+            @JsonProperty("place_url")
+            private String placeUrl;
+            private String distance;
+        }
+    }
+
+    // 위도/경도 기준으로 주변 카페 검색 (최대 15개만)
+    public List findCafesNearby(double latitude, double longitude, int radius) {
+        List allCafes = new ArrayList<>();
+        boolean isEnd = false;
+        int page = 1;
+        int maxResults = 15; // 최대 15개만 가져오기
+
+        try {
+            // 페이지별로 반복 요청하되, 총 15개가 모이면 종료
+            while (!isEnd && allCafes.size() < maxResults) {
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+
+                // 페이지당 결과 개수는 한번에 최대한 많이 가져오도록 15로 설정
+                String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_BASE_URL + "/search/category.json")
+                        .queryParam("category_group_code", "CE7")  // 카페 카테고리 코드
+                        .queryParam("x", longitude)
+                        .queryParam("y", latitude)
+                        .queryParam("radius", radius)  // 반경(미터)
+                        .queryParam("page", page)      // 페이지 번호
+                        .queryParam("size", 15)        // 페이지당 15개 결과
+                        .queryParam("sort", "accuracy") // 정확도 순 정렬
+                        .build()
+                        .toUriString();
+
+                ResponseEntity response = restTemplate.exchange(
+                        url,
+                        HttpMethod.GET,
+                        new HttpEntity<>(headers),
+                        CafeResponse.class
+                );
+
+                CafeResponse result = response.getBody();
+                if (result != null && result.getDocuments() != null && !result.getDocuments().isEmpty()) {
+                    // 최대 15개까지만 추가
+                    int remainingSpace = maxResults - allCafes.size();
+                    int toAdd = Math.min(remainingSpace, result.getDocuments().size());
+
+                    allCafes.addAll(result.getDocuments().subList(0, toAdd));
+
+                    log.info("카페 검색 - 페이지 {}: {}개 데이터 추가 (현재 총 {}개/최대 {}개)",
+                            page, toAdd, allCafes.size(), maxResults);
+
+                    // 다음 페이지 존재 여부 확인 또는 이미 15개를 채웠는지 확인
+                    isEnd = result.getMeta().isEnd() || allCafes.size() >= maxResults;
+                    page++;
+
+                    // API 호출 간격 조절
+                    try {
+                        Thread.sleep(200);
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                    }
+                } else {
+                    // 결과가 없으면 종료
+                    isEnd = true;
+                }
+            }
+
+            log.info("총 {}개의 카페 데이터 조회 완료", allCafes.size());
+            return allCafes;
+
+        } catch (Exception e) {
+            log.error("카페 검색 중 오류: {}", e.getMessage(), e);
+            return Collections.emptyList();
+        }
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java
new file mode 100644
index 0000000..c63429e
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java
@@ -0,0 +1,51 @@
+package com.example.placeservice.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.math.BigDecimal;
+
+@Service
+@RequiredArgsConstructor
+public class KakaoMapClient {
+
+    @Value("${kakao.api.key}")
+    private String kakaoApiKey;
+
+
+
+    public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int page) {
+        RestTemplate restTemplate = new RestTemplate();
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+
+        // Kakao API 호출 URL 세팅
+        UriComponentsBuilder builder = UriComponentsBuilder
+                .fromHttpUrl("https://dapi.kakao.com/v2/local/search/category.json")
+                .queryParam("category_group_code", "FD6") // 음식점 카테고리
+                .queryParam("x", lon) // x: 경도
+                .queryParam("y", lat) // y: 위도
+                .queryParam("sort", "accuracy") // 정확도순 정렬
+                .queryParam("page", page) // 요청 페이지
+                .queryParam("size", 15); // 한 페이지에 최대 15개 결과
+
+        HttpEntity entity = new HttpEntity<>(headers);
+
+        // REST API 호출
+        ResponseEntity response = restTemplate.exchange(
+                builder.toUriString(),
+                HttpMethod.GET,
+                entity,
+                JsonNode.class
+        );
+
+        return response.getBody(); // JSON 응답 반환
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java
new file mode 100644
index 0000000..bb76391
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java
@@ -0,0 +1,73 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.PlaceDocument;
+
+import com.example.placeservice.entity.*;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PlaceDocumentMapper {
+
+    public PlaceDocument toPlaceDocument(Accommodation item) {
+        return PlaceDocument.builder()
+                .name(item.getName())
+                .address(item.getAddress())
+                .type("accommodation")
+                .place_id(String.valueOf(item.getAccommodationId()))
+                .lat(item.getLat())
+                .lon(item.getLon())
+                .phone(item.getPhone())
+                .kakaomap_url(item.getKakaomapUrl())
+                .build();
+    }
+
+    public PlaceDocument toPlaceDocument(Cafe item) {
+        return PlaceDocument.builder()
+                .name(item.getName())
+                .address(item.getAddress())
+                .type("cafe")
+                .place_id(String.valueOf(item.getId()))
+                .lat(item.getLat())
+                .lon(item.getLon())
+                .phone(item.getPhone())
+                .kakaomap_url(item.getKakaomapUrl())
+                .build();
+    }
+
+    public PlaceDocument toPlaceDocument(CulturalEvent item) {
+        return PlaceDocument.builder()
+                .name(item.getTitle())
+                .address(item.getAddress())
+                .type("culturalevent")
+                .place_id(String.valueOf(item.getEventId()))
+                .lat(item.getLat())
+                .lon(item.getLon())
+                .build();
+    }
+
+    public PlaceDocument toPlaceDocument(Restaurant item) {
+        return PlaceDocument.builder()
+                .name(item.getName())
+                .address(item.getAddress())
+                .type("restaurant")
+                .place_id(String.valueOf(item.getRestaurantId()))
+                .lat(item.getLat())
+                .lon(item.getLon())
+                .phone(item.getPhone())
+                .kakaomap_url(item.getKakaomap_url())
+                .build();
+    }
+
+    public PlaceDocument toPlaceDocument(Attraction item) {
+        return PlaceDocument.builder()
+                .name(item.getName())
+                .address(item.getAddress())
+                .type("attraction")
+                .place_id(String.valueOf(item.getAttractionId()))
+                .lat(item.getLat())
+                .lon(item.getLon())
+                .phone(item.getPhone())
+                .kakaomap_url(item.getKakaomapUrl())
+                .build();
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java
new file mode 100644
index 0000000..412bc42
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java
@@ -0,0 +1,83 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.accommodation.AccommodationDto;
+import com.example.placeservice.dto.PlaceDocument;
+import com.example.placeservice.service.PlaceDocumentMapper;
+import com.example.placeservice.entity.*;
+import com.example.placeservice.repository.*;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import lombok.extern.slf4j.Slf4j;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class PlaceDocumentService {
+
+    private final AccommodationRepository accommodationRepository;
+    private final RestaurantRepository restaurantRepository;
+    private final AttractionRepository attractionRepository;
+    private final CulturalEventRepository culturalEventRepository;
+    private final CafeRepository cafeRepository;
+
+    private final PlaceDocumentMapper placeDocumentMapper;
+
+    public List searchPlacesByName(String keyword) {
+        List results = new ArrayList<>();
+
+        // Accommodation, Attraction, Cafe, Restaurant, CulturalEvent에서 이름이 포함된 객체만 필터링
+        results.addAll(accommodationRepository.findByNameContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(attractionRepository.findByNameContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(cafeRepository.findByNameContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(restaurantRepository.findByNameContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(culturalEventRepository.findBytitleContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        // 이름에만 keyword가 포함된 것만 남긴다.
+        return results.stream()
+                .filter(place -> place.getName().toLowerCase().contains(keyword.toLowerCase()))
+                .collect(Collectors.toList());
+    }
+
+    public List searchPlacesByAddress(String keyword) {
+        List results = new ArrayList<>();
+
+        // Accommodation, Attraction, Cafe, Restaurant, CulturalEvent에서 주소가 포함된 객체만 필터링
+        results.addAll(accommodationRepository.findByAddressContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(attractionRepository.findByAddressContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(cafeRepository.findByAddressContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(restaurantRepository.findByAddressContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        results.addAll(culturalEventRepository.findByAddressContainingIgnoreCase(keyword)
+                .stream().map(placeDocumentMapper::toPlaceDocument).toList());
+
+        // 주소에만 keyword가 포함된 것만 남긴다.
+        return results.stream()
+                .filter(place -> place.getAddress().toLowerCase().contains(keyword.toLowerCase()))
+                .collect(Collectors.toList());
+    }
+
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
new file mode 100644
index 0000000..dc33e3e
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
@@ -0,0 +1,34 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.dto.restaurant.RestaurantDetailResponse;
+import com.example.placeservice.dto.restaurant.RestaurantListResponse;
+import com.example.placeservice.entity.Restaurant;
+import com.example.placeservice.repository.RestaurantRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class RestaurantListService {
+
+    private final RestaurantRepository restaurantRepository;
+
+    @Transactional(readOnly = true)
+    public List getRestaurantList() {
+        return restaurantRepository.findAll()
+                .stream()
+                .map(RestaurantListResponse::fromEntity)
+                .collect(Collectors.toList());
+    }
+
+    @Transactional(readOnly = true)
+    public RestaurantDetailResponse getRestaurantDetail(Long id) {
+        Restaurant restaurant = restaurantRepository.findById(id)
+                .orElseThrow(() -> new IllegalArgumentException("해당 음식점이 존재하지 않습니다. id=" + id));
+        return RestaurantDetailResponse.fromEntity(restaurant);
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
new file mode 100644
index 0000000..f91931e
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
@@ -0,0 +1,111 @@
+package com.example.placeservice.service;
+
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.entity.Restaurant;
+import com.example.placeservice.repository.AreaRepository;
+import com.example.placeservice.repository.RestaurantRepository;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class RestaurantService {
+
+    private final AreaRepository areaRepository;
+    private final RestaurantRepository restaurantRepository;
+    private final KakaoMapClient kakaoMapClient;
+
+    // 특정 Area 기준으로 주변 음식점 저장
+    public void saveRestaurantsNearbyArea(Area area) {
+        List savedRestaurants = new ArrayList<>();
+        int page = 1;
+        boolean isEnd = false;
+        double searchRadius = 1000.0; // 반경 1km
+        int savedCount = 0; // 저장한 음식점 수
+
+        while (!isEnd) {
+            // Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달
+            JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page);
+
+            if (response.has("documents")) {
+                for (JsonNode doc : response.get("documents")) {
+                    if (savedCount >= 15) { // 15개 저장했으면 종료
+                        isEnd = true;
+                        break;
+                    }
+
+                    double restaurantLon = Double.parseDouble(doc.get("x").asText());
+                    double restaurantLat = Double.parseDouble(doc.get("y").asText());
+
+                    double distance = calculateDistance(
+                            area.getLat().doubleValue(),  // 위도
+                            area.getLon().doubleValue(),  // 경도
+                            restaurantLat,                // 위도
+                            restaurantLon                 // 경도
+                    );
+
+                    // 1km 이내 음식점만 저장
+                    if (distance <= searchRadius) {
+                        Restaurant restaurant = new Restaurant();
+                        restaurant.setArea(area);
+                        restaurant.setName(doc.get("place_name").asText());
+                        restaurant.setAddress(doc.get("road_address_name").asText());
+                        restaurant.setLat(new BigDecimal(doc.get("y").asText())); // 위도
+                        restaurant.setLon(new BigDecimal(doc.get("x").asText())); // 경도
+                        restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null);
+                        restaurant.setKakaomap_url(doc.get("place_url").asText());
+                        restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null);
+                        restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null);
+                        restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null);
+                        restaurant.setKakao_id(doc.get("id").asText());
+
+                        savedRestaurants.add(restaurant);
+                        restaurantRepository.save(restaurant);
+                        savedCount++;
+                    }
+                }
+
+                if (!isEnd) {
+                    isEnd = response.get("meta").get("is_end").asBoolean();
+                    page++;
+                }
+            } else {
+                log.info("레스토랑 카카오 데이터 조회 실패");
+                isEnd = true;
+            }
+        }
+    }
+
+    // 전체 Area 대상 음식점 저장
+    @Transactional
+    public void fetchAndSaveRestaurants() {
+        List areas = areaRepository.findAll();
+        for (Area area : areas) {
+            saveRestaurantsNearbyArea(area);
+        }
+    }
+
+    // Haversine 거리 계산 공식 (meter 단위)
+    private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
+        final int EARTH_RADIUS = 6371; // km
+
+        double dLat = Math.toRadians(lat2 - lat1);
+        double dLon = Math.toRadians(lon2 - lon1);
+
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
+                * Math.sin(dLon / 2) * Math.sin(dLon / 2);
+
+        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+        return EARTH_RADIUS * c * 1000; // meter 반환
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java
new file mode 100644
index 0000000..7c71636
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java
@@ -0,0 +1,20 @@
+package com.example.placeservice.util;
+
+
+public class GeoUtils {
+
+    private static final double EARTH_RADIUS_KM = 6371.0;
+
+    public static double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) {
+        double dLat = Math.toRadians(lat2 - lat1);
+        double dLon = Math.toRadians(lon2 - lon1);
+
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+                Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
+                        Math.sin(dLon / 2) * Math.sin(dLon / 2);
+
+        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+        return EARTH_RADIUS_KM * c;
+    }
+
+}
\ No newline at end of file
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
deleted file mode 100644
index 0c9366c..0000000
--- a/place-service/src/main/resources/application.properties
+++ /dev/null
@@ -1,16 +0,0 @@
-spring.application.name=place-service
-
-
-# postgreSQL ??
-
-spring.datasource.url=jdbc:postgresql://localhost:5432/star
-spring.datasource.username=admin
-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
-
-
-spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=update
diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql
new file mode 100644
index 0000000..8e1d998
--- /dev/null
+++ b/place-service/src/main/resources/data.sql
@@ -0,0 +1,130 @@
+INSERT INTO area (area_id, seoul_id, name, name_eng, category, lat, lon) VALUES
+(1, 'POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705),
+(2, 'POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576),
+(3, 'POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864),
+(4, 'POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342),
+(5, 'POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114),
+(6, 'POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675),
+(7, 'POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915),
+(8, 'POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209),
+(9, 'POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834),
+(10, 'POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172),
+(11, 'POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499),
+(12, 'POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715),
+(13, 'POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826),
+(14, 'POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981),
+(15, 'POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415),
+(16, 'POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486),
+(17, 'POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697),
+(18, 'POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736),
+(19, 'POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915),
+(20, 'POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136),
+(21, 'POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267),
+/*(22, 'POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),*/
+(23, 'POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546),
+(24, 'POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538),
+(25, 'POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764),
+(26, 'POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811),
+(27, 'POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551),
+/*(28, 'POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),*/
+(29, 'POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415),
+(30, 'POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508),
+(31, 'POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334),
+(32, 'POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689),
+(33, 'POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233),
+(34, 'POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203),
+(35, 'POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346),
+(36, 'POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451),
+(37, 'POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254),
+(38, 'POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083),
+(39, 'POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233),
+(40, 'POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075),
+(41, 'POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439),
+(42, 'POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585),
+(43, 'POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034),
+(44, 'POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015),
+(45, 'POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515),
+(46, 'POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128),
+(47, 'POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051),
+(48, 'POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641),
+(49, 'POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185),
+(50, 'POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884),
+(51, 'POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491),
+(52, 'POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645),
+(53, 'POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917),
+(54, 'POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915),
+(55, 'POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786),
+(56, 'POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965),
+/*(57, 'POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),*/
+(58, 'POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454),
+(59, 'POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263),
+(60, 'POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192),
+(61, 'POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135),
+/*(62, 'POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),*/
+(63, 'POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423),
+(64, 'POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493),
+/*(65, 'POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),*/
+(66, 'POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175),
+(67, 'POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548),
+(68, 'POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015),
+/*(69, 'POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),*/
+(70, 'POI070', '쌍문역', 'Ssangmun-dong station', '인구밀집지역', 37.6477285443038, 127.03242528481013),
+(71, 'POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626),
+(72, 'POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487),
+(73, 'POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905),
+(74, 'POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634),
+/*(75, 'POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),*/
+(76, 'POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237),
+(77, 'POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602),
+(78, 'POI078', '인사동', 'Insa-dong', '발달상권', 37.5741577489609, 126.98736374162822),
+(79, 'POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348),
+(80, 'POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364),
+(81, 'POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348),
+(82, 'POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178),
+(83, 'POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622),
+(84, 'POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541),
+(85, 'POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644),
+(86, 'POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083),
+(87, 'POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764),
+(88, 'POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502),
+(89, 'POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178),
+(90, 'POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329),
+(91, 'POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628),
+(92, 'POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296),
+(93, 'POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332),
+(94, 'POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892),
+(95, 'POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858),
+(96, 'POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815),
+/*(97, 'POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),*/
+(98, 'POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917),
+(99, 'POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284),
+(100, 'POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076),
+(101, 'POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584),
+(102, 'POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643),
+(103, 'POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624),
+(104, 'POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458),
+(105, 'POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478),
+(106, 'POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091),
+(107, 'POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303),
+(108, 'POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725),
+(109, 'POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283),
+(110, 'POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517),
+(111, 'POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632),
+(112, 'POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313),
+(113, 'POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
+(114, 'POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
+(115, 'POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821),
+(116, 'POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286),
+(117, 'POI117', '신정네거리역', 'Shinjeongnegeori Station', '인구밀집지역', 37.521121, 126.855010),
+(118, 'POI118', '잠실새내역', 'Jamsil Saenae Station', '인구밀집지역', 37.510417, 127.083309),
+(119, 'POI119', '잠실역', 'Jamsil Station', '인구밀집지역', 37.512077, 127.101158),
+(120, 'POI120', '잠실롯데타워 일대', 'Jamsil Lotte Tower area', '발달상권', 37.511679, 127.104408),
+(121, 'POI121', '송리단길·호수단길', 'Songridan-gil·Hosudan-gil', '발달상권', 37.508292, 127.106789),
+(122, 'POI122', '신촌 스타광장', 'Shinchon Star Plaza', '발달상권', 37.556476, 126.936840),
+(123, 'POI123', '보라매공원', 'Boramae Park', '공원', 37.492817, 126.919840),
+(124, 'POI124', '서대문독립공원', 'Seodaemun Independence Park', '공원', 37.574265, 126.956435),
+(125, 'POI125', '안양천', 'Anyangcheon River', '공원', 37.519953, 126.879723),
+(126, 'POI126', '여의서로', 'Yeouiseoro', '공원', 37.533417, 126.911992),
+(127, 'POI127', '올림픽공원', 'Olympic Park', '공원', 37.519754, 127.123176),
+(128, 'POI128', '홍제폭포', 'Hongje Waterfall', '공원', 37.580613, 126.937246)
+ON CONFLICT (seoul_id) DO NOTHING;
\ No newline at end of file
diff --git a/user-service/.gitignore b/user-service/.gitignore
index c2065bc..6964208 100644
--- a/user-service/.gitignore
+++ b/user-service/.gitignore
@@ -3,7 +3,9 @@ HELP.md
 build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
-!**/src/test/**/build/
+!**/src/test/**/build
+.env
+src/main/resources/application.properties
 
 ### STS ###
 .apt_generated
@@ -35,3 +37,5 @@ out/
 
 ### VS Code ###
 .vscode/
+
+
diff --git a/user-service/Dockerfile b/user-service/Dockerfile
new file mode 100644
index 0000000..54d2456
--- /dev/null
+++ b/user-service/Dockerfile
@@ -0,0 +1,43 @@
+# 멀티스테이징 도커 파일 구성
+
+# 소스코드를 빌드
+# 빌드의 결과물 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 ./user.jar
+# 포트지정
+
+COPY .env .env
+
+EXPOSE 8083
+
+# 서버가동
+CMD ["java", "-jar", "user.jar"]
\ No newline at end of file
diff --git a/user-service/build.gradle b/user-service/build.gradle
index 6329d25..321be19 100644
--- a/user-service/build.gradle
+++ b/user-service/build.gradle
@@ -13,6 +13,15 @@ java {
     }
 }
 
+
+
+//dependencyManagement {
+//    imports {
+//        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.1"
+//    }
+//}
+
+
 configurations {
     compileOnly {
         extendsFrom annotationProcessor
@@ -24,29 +33,53 @@ repositories {
 }
 
 dependencies {
+    implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
+
     // 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'
+
+    // Spring Boot 기본
     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'
     implementation 'org.springframework.boot:spring-boot-starter-security'
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
-    implementation 'org.apache.kafka:kafka-streams'
+    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    implementation 'org.springframework.session:spring-session-data-redis'
     implementation 'org.springframework.kafka:spring-kafka'
+    implementation 'org.apache.kafka:kafka-streams'
     implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
-    compileOnly 'org.projectlombok:lombok'
-    developmentOnly 'org.springframework.boot:spring-boot-devtools'
+    implementation 'io.github.cdimascio:java-dotenv:5.2.2'
+    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
+
+    implementation 'org.json:json:20231013'
+
+    // DB
     runtimeOnly 'org.postgresql:postgresql'
+
+    // Lombok
+    compileOnly 'org.projectlombok:lombok'
     annotationProcessor 'org.projectlombok:lombok'
+
+    // 개발 환경
+    developmentOnly 'org.springframework.boot:spring-boot-devtools'
+
+    // 테스트
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testImplementation 'org.springframework.kafka:spring-kafka-test'
     testImplementation 'org.springframework.security:spring-security-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+    //implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:2.2.4.RELEASE'
+
 }
 
 tasks.named('bootBuildImage') {
diff --git a/user-service/gradlew b/user-service/gradlew
old mode 100755
new mode 100644
diff --git a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java
index 55a5c33..a94c590 100644
--- a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java
+++ b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java
@@ -2,8 +2,10 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
 @SpringBootApplication
+@EnableJpaRepositories(basePackages = "com.example.userservice.repository")
 public class UserServiceApplication {
 
     public static void main(String[] args) {
diff --git a/user-service/src/main/java/com/example/userservice/config/AppConfig.java b/user-service/src/main/java/com/example/userservice/config/AppConfig.java
new file mode 100644
index 0000000..7ee7c5a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/AppConfig.java
@@ -0,0 +1,14 @@
+package com.example.userservice.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/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
new file mode 100644
index 0000000..27f8287
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
@@ -0,0 +1,60 @@
+package com.example.userservice.config;
+
+import com.example.userservice.repository.jpa.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Configuration
+@RequiredArgsConstructor
+public class ApplicationConfig {
+
+    private final MemberRepository memberRepository;
+
+    @Bean
+    public UserDetailsService userDetailsService() {
+        return username -> memberRepository.findByUserId(username)
+                .map(member -> {
+                    List authorities = new ArrayList<>();
+                    // 사용자 역할에 따른 권한 추가
+                    authorities.add(new SimpleGrantedAuthority(member.getRole()));
+
+                    return new org.springframework.security.core.userdetails.User(
+                            member.getUserId(), // userId를 사용자 이름으로 사용
+                            member.getPassword(), // 암호화된 비밀번호
+                            authorities // 권한 목록
+                    );
+                })
+                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다"));
+    }
+
+    @Bean
+    public AuthenticationProvider authenticationProvider() {
+        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+        authProvider.setUserDetailsService(userDetailsService());
+        authProvider.setPasswordEncoder(passwordEncoder());
+        return authProvider;
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+        return config.getAuthenticationManager();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder(); // 비밀번호 암호화에 BCrypt 알고리즘 사용
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java b/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
new file mode 100644
index 0000000..3e5d089
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
@@ -0,0 +1,38 @@
+package com.example.userservice.config;
+
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.TokenService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+    private final JwtUtil jwtUtil;
+    private final TokenService tokenService;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        // 1. 인증 성공했을 때 authentication 객체를 이용해서
+        // 2. accessToken, refreshToken 생성
+        String accessToken = jwtUtil.generateToken((UserDetails) authentication.getPrincipal());
+        String refreshToken = jwtUtil.generateRefreshToken((UserDetails) authentication.getPrincipal());
+
+        // 3. Redis에 Refresh Token 저장
+        String userId = authentication.getName();  // OAuth2 로그인 시 기본적으로 userId 같은 값
+        tokenService.saveRefreshToken(Long.parseLong(userId), refreshToken); // userId가 Long 타입이면
+
+        // 4. 응답에 AccessToken, RefreshToken 보내주기 (예시)
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().write("{\"accessToken\": \"" + accessToken + "\", \"refreshToken\": \"" + refreshToken + "\"}");
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/RedisConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java
new file mode 100644
index 0000000..1086b58
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java
@@ -0,0 +1,10 @@
+package com.example.userservice.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+
+@Configuration
+@EnableRedisRepositories(basePackages = "com.example.userservice.repository.redis")
+public class    RedisConfig {
+    // 추가 설정 있으면 작성
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
new file mode 100644
index 0000000..f952331
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
@@ -0,0 +1,28 @@
+package com.example.userservice.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+
+public class RedisRepositoryConfig {
+    @Value("${spring.data.redis.host}")
+    private String host;
+
+    @Value("${spring.data.redis.port}")
+    private int port;
+
+    @Value("${spring.data.redis.password}")
+    private String password;
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
+        redisStandaloneConfiguration.setHostName(host);
+        redisStandaloneConfiguration.setPassword(password);
+        redisStandaloneConfiguration.setPort(port);
+
+        return new LettuceConnectionFactory(redisStandaloneConfiguration);
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
new file mode 100644
index 0000000..1f69f38
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -0,0 +1,61 @@
+package com.example.userservice.config;
+
+import com.example.userservice.exception.CustomAccessDeniedHandler;
+import com.example.userservice.exception.CustomAuthenticationEntryPoint;
+import com.example.userservice.security.JwtAuthenticationFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+    private final JwtAuthenticationFilter jwtAuthenticationFilter;
+    private final AuthenticationProvider authenticationProvider;
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(csrf -> csrf.disable())
+                .authorizeHttpRequests(auth -> auth
+                        // Swagger 접근 허용
+                        .requestMatchers(
+                                "/swagger-ui.html",
+                                "/swagger-ui/**",
+                                "/v3/api-docs/**",
+                                "/user/swagger-ui/index.html"
+                        ).permitAll()
+
+                        // 공개 엔드포인트 - 인증 불필요
+                        .requestMatchers("/auth/**").permitAll()
+
+                        // 관리자 전용 엔드포인트
+                        .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
+
+                        // 사용자/관리자 접근 가능 엔드포인트
+                        .requestMatchers("/mypage/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
+
+                        // 그 외 모든 요청은 인증 필요
+                        .anyRequest().authenticated()
+                )
+                .sessionManagement(session -> session
+                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                )
+                .authenticationProvider(authenticationProvider)
+                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+                .exceptionHandling(exception -> exception
+                        .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
+                        .accessDeniedHandler(new CustomAccessDeniedHandler())
+                );
+
+        return http.build();
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java b/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java
new file mode 100644
index 0000000..cbccd19
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java
@@ -0,0 +1,27 @@
+package com.example.userservice.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+@Configuration
+public class SwaggerConfig {
+
+    @Bean
+    public OpenAPI openAPI() {
+        return new OpenAPI()
+                .components(new Components())
+                .info(apiInfo());
+    }
+
+    private Info apiInfo() {
+        return new Info()
+                .title("API Title") // API의 제목
+                .description("This is my Swagger UI") // API에 대한 설명
+                .version("1.0.0"); // API의 버전
+    }
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminController.java b/user-service/src/main/java/com/example/userservice/controller/AdminController.java
new file mode 100644
index 0000000..8dfe416
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AdminController.java
@@ -0,0 +1,42 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.FavoriteDto;
+import com.example.userservice.dto.MemberDto;
+import com.example.userservice.service.FavoriteService;
+import com.example.userservice.service.MemberService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/admin")
+public class AdminController {
+
+    private final MemberService memberService;
+    private final FavoriteService favoriteService;
+
+    /**
+     * 전체 회원 목록 조회 API
+     * @return 모든 회원 목록
+     */
+    @GetMapping("/user/list")
+    @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능
+    public ResponseEntity> getAllMembers() {
+        List members = memberService.getAllMembers();
+        return ResponseEntity.ok(members);
+    }
+
+    @GetMapping("/favorite/list")
+    @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능
+    public ResponseEntity>> getAllFavorites() {
+        List> favorites = favoriteService.getAllFavoriteData();
+        return ResponseEntity.ok(favorites);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
new file mode 100644
index 0000000..6c71697
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
@@ -0,0 +1,128 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.AuthService;
+import com.example.userservice.service.CookieUtil;
+import com.example.userservice.service.TokenService;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/auth")
+@RequiredArgsConstructor
+@Slf4j
+public class AuthController {
+
+    private final AuthService authService;
+    private final CookieUtil cookieUtil;
+    private final JwtUtil jwtUtil;
+    private final MemberRepository memberRepository;
+    private final TokenService tokenService;
+
+    /**
+     * 로그인 API
+     * @param request 로그인 요청 (user_id, password)
+     * @param response HTTP 응답 (쿠키 설정용)
+     * @return 로그인 성공 시 토큰 및 사용자 정보
+     */
+    @PostMapping("/login")
+    public ResponseEntity login(
+            @RequestBody AuthDto.LoginRequest request,
+            HttpServletResponse response) {
+
+        // 디버깅 로그 출력
+        log.debug("로그인 컨트롤러 호출: {}", request.getUser_id());
+
+        AuthDto.LoginResponse loginResponse = authService.login(request);
+
+        // 쿠키에 토큰 저장
+        cookieUtil.addCookie(response, "access_token", loginResponse.getAccessToken(), cookieUtil.getDaysInSeconds(1));
+        cookieUtil.addCookie(response, "refresh_token", loginResponse.getRefreshToken(), cookieUtil.getDaysInSeconds(30));
+
+        return ResponseEntity.ok(loginResponse);
+    }
+
+
+    /*
+    * 토큰 재발급 api 추가
+    * */
+
+    @PostMapping("/refresh")
+    public ResponseEntity> refreshAccessToken(@RequestBody AuthDto.RefreshTokenRequest request) {
+        String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());
+
+        Map response = new HashMap<>();
+        response.put("accessToken", newAccessToken);
+
+        return ResponseEntity.ok(response);
+    }
+
+    /**
+     * 로그아웃 API
+     * @param request HTTP 요청 (토큰 추출용)
+     * @param response HTTP 응답 (쿠키 삭제용)
+     * @return 로그아웃 성공 여부 및 메시지
+     */
+    @PostMapping("/logout")
+    public ResponseEntity logout(
+            HttpServletRequest request,
+            HttpServletResponse response) {
+
+        // 쿠키 삭제
+        cookieUtil.addCookie(response, "access_token", "", 0);
+        cookieUtil.addCookie(response, "refresh_token", "", 0);
+
+        // 헤더에서 토큰 추출
+        String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        } else {
+            // 쿠키에서 토큰 찾기 (선택적)
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if ("access_token".equals(cookie.getName())) {
+                        jwt = cookie.getValue();
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (jwt != null) {
+            // 토큰에서 사용자 정보 추출
+            String userId = jwtUtil.extractUsername(jwt);
+            // 사용자 ID로 Member 조회
+            Member member = memberRepository.findByUserId(userId)
+                    .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다"));
+
+            // 로그아웃 처리
+            return ResponseEntity.ok(authService.logout(
+                    AuthDto.LogoutRequest.builder()
+                            .memberId(String.valueOf(member.getMemberId()))
+                            .build()));
+        }
+
+        return ResponseEntity.badRequest().body(
+                AuthDto.LogoutResponse.builder()
+                        .success(false)
+                        .message("유효한 토큰이 없습니다.")
+                        .build());
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java
new file mode 100644
index 0000000..d59fd59
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java
@@ -0,0 +1,49 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.FavoriteDto;
+import com.example.userservice.service.FavoriteService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/mypage/favorite")
+@RequiredArgsConstructor
+public class FavoriteController {
+
+    private final FavoriteService favoriteService;
+
+    //즐겨찾기 리스트 조회
+    @GetMapping("/list")
+    public List getList() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        String userId = authentication.getName();
+
+        return favoriteService.getListData(userId);
+    }
+
+    //즐겨찾기 추가
+    @PostMapping("/add")
+    public ResponseEntity> addFavorite(@RequestBody FavoriteDto favoriteDto) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        String userId = authentication.getName();
+
+        return favoriteService.addFavoriteData(userId,favoriteDto);
+    }
+
+    //즐겨찾기 삭제
+    @DeleteMapping("/delete/{type}/{id}")
+    public ResponseEntity> deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        String userId = authentication.getName();
+
+        return favoriteService.deleteFavorite(userId, type, id);
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java
new file mode 100644
index 0000000..ca70991
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java
@@ -0,0 +1,96 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.MemberDto;
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.MemberService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+public class MemberController {
+
+    private final MemberService memberService;
+    private final JwtUtil jwtUtil;
+
+    /**
+     * 사용자 프로필 조회 API
+     * @param request HTTP 요청 (토큰에서 사용자 ID 추출용)
+     * @return 사용자 프로필 정보
+     */
+    @GetMapping("/mypage/profile")
+    public ResponseEntity getProfile(HttpServletRequest request) {
+        // 현재 인증된 사용자의 userId 추출
+        String userId = extractUserIdFromRequest(request);
+
+        // 서비스를 통해 프로필 정보 조회
+        MemberDto.ProfileResponse profile = memberService.getProfile(userId);
+
+        return ResponseEntity.ok(profile);
+    }
+
+    /**
+     * 사용자 프로필 수정 API
+     * @param request HTTP 요청 (토큰에서 사용자 ID 추출용)
+     * @param updateRequest 프로필 수정 정보 (닉네임, 생년월일, MBTI, 성별, 비밀번호 등)
+     * @return 업데이트된 사용자 정보
+     */
+    @PostMapping("/mypage/profile/edit")
+    public ResponseEntity updateProfile(
+            HttpServletRequest request,
+            @RequestBody MemberDto.UpdateRequest updateRequest) {
+
+        // 현재 인증된 사용자의 userId 추출
+        String userId = extractUserIdFromRequest(request);
+
+        // 서비스를 통해 프로필 정보 업데이트
+        MemberDto.MemberResponse updatedProfile = memberService.updateProfile(userId, updateRequest);
+
+        return ResponseEntity.ok(updatedProfile);
+    }
+
+    /**
+     * JWT 토큰에서 userId 추출 헬퍼 메서드
+     * @param request HTTP 요청
+     * @return 추출된 사용자 ID
+     */
+    private String extractUserIdFromRequest(HttpServletRequest request) {
+        // 요청 디버깅 로그
+        log.debug("요청 URI: {}", request.getRequestURI());
+        log.debug("Authorization 헤더: {}", request.getHeader("Authorization"));
+        log.debug("accessToken 헤더: {}", request.getHeader("accessToken"));
+
+        String authHeader = request.getHeader("Authorization");
+        String accessTokenHeader = request.getHeader("accessToken");
+        String jwt = null;
+
+        // Authorization 헤더 확인
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+            log.debug("Authorization 헤더에서 토큰 추출: {}", jwt);
+        }
+        // accessToken 헤더 확인
+        else if (accessTokenHeader != null) {
+            jwt = accessTokenHeader;
+            log.debug("accessToken 헤더에서 토큰 추출: {}", jwt);
+        }
+
+        // 토큰 유효성 검사 및 사용자 ID 추출
+        if (jwt != null) {
+            try {
+                String userId = jwtUtil.extractUsername(jwt);
+                log.debug("추출된 사용자 ID: {}", userId);
+                return userId;
+            } catch (Exception e) {
+                log.error("토큰 처리 중 오류 발생: {}", e.getMessage());
+                e.printStackTrace();
+            }
+        }
+
+        throw new RuntimeException("인증 토큰이 유효하지 않습니다. 'Authorization: Bearer [token]' 또는 'accessToken' 헤더가 필요합니다.");
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/SignupController.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java
new file mode 100644
index 0000000..fe6dd1b
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java
@@ -0,0 +1,110 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.MemberSign;
+import com.example.userservice.dto.SuggestFastRequestDto;
+import com.example.userservice.dto.SuggestFastResponseDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.MemberService;
+import com.example.userservice.service.TokenService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/auth")
+@Slf4j
+public class SignupController {
+    private final MemberService memberService;
+    private final JwtUtil jwtUtil;
+    private final TokenService tokenService;
+    private final RestTemplate restTemplate;
+
+    @Value("${fastapi-svc}")
+    private String fastApiUrl;
+
+    /**
+     * 일반 사용자 회원가입 API
+     */
+    @PostMapping("/signup")
+    public ResponseEntity> signup(@RequestBody MemberSign dto) {
+        try {
+            log.debug("회원가입 요청: userId={}", dto.getUserId());
+
+            Member member = memberService.registerUser(dto);
+
+            List authorities = Collections.singletonList(
+                    new SimpleGrantedAuthority(member.getRole())
+            );
+
+            UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
+            String accessToken = jwtUtil.generateToken(userDetails);
+            String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+            tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+            Map response = new HashMap<>();
+            response.put("accessToken", accessToken);
+            response.put("refreshToken", refreshToken);
+            response.put("member_id", String.valueOf(member.getMemberId()));
+            response.put("user_id", member.getUserId());
+            response.put("nickname", member.getNickname());
+            response.put("role", member.getRole());
+
+            return ResponseEntity.ok(response);
+        } catch (IllegalStateException e) {
+            Map errorResponse = new HashMap<>();
+            errorResponse.put("error", "회원가입 실패: " + e.getMessage());
+            return ResponseEntity.badRequest().body(errorResponse);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 회원 탈퇴 (본인 인증 포함)
+     */
+    @DeleteMapping("/signout/{memberId}")
+    public ResponseEntity signout(
+            @PathVariable Long memberId,
+            @AuthenticationPrincipal UserDetails userDetails) {
+
+        // 현재 로그인한 사용자 정보 조회
+        String userId = userDetails.getUsername();
+        Member loginMember = memberService.findByUserId(userId);
+
+        // 본인인지 확인
+        if (!loginMember.getMemberId().equals(memberId)) {
+            return ResponseEntity.status(403).body("본인만 탈퇴할 수 있습니다.");
+        }
+
+        // fast-api 서버에 데이터 삭제 요청
+        try {
+            ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "/suggest/" + userId + "/delete", Void.class);
+        } catch (Exception e) {
+            return ResponseEntity.badRequest().body("탈퇴 전 AI 여행지 추천 내역 삭제 실패 : " + e.getMessage());
+        }
+        log.debug("AI 여행지 추천 내역 삭제 완료");
+
+        try {
+            memberService.deleteMemberById(memberId);
+            log.debug("회원탈퇴 완료: memberId={}", memberId);
+            return ResponseEntity.ok("회원 탈퇴가 완료되었습니다.");
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.badRequest().body("회원 탈퇴 실패: " + e.getMessage());
+        }
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java
new file mode 100644
index 0000000..ab4a6cd
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java
@@ -0,0 +1,151 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.SuggestError;
+import com.example.userservice.dto.SuggestFastResponseDto;
+import com.example.userservice.dto.SuggestRequestDto;
+import com.example.userservice.dto.SuggestFastRequestDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.service.MemberService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+@RestController
+@RequestMapping("/suggest")
+@Slf4j
+public class SuggestController {
+    private final ObjectMapper objectMapper;
+    @Value("${fastapi-svc}")
+    private String fastApiUrl;
+
+    private final MemberService memberService;
+    private final RestTemplate restTemplate;
+
+    public SuggestController(MemberService memberService, ObjectMapper objectMapper) {
+        this.memberService = memberService;
+        restTemplate = new RestTemplate();
+        this.objectMapper = objectMapper;
+    }
+
+    @PostMapping("/{user_id}")
+    public ResponseEntity suggestTourPlan(@PathVariable String user_id, @RequestBody SuggestRequestDto suggestRequestDto) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+        if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인이 필요합니다.");
+        }
+
+        // 2. 권한 확인 (요청한 user_id와 인증된 사용자가 동일한지)
+        if (!authentication.getName().equals(user_id)) {
+            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("자신의 정보에 대해서만 요청할 수 있습니다.");
+        }
+
+        Member member;
+        try {
+            member = memberService.findByUserId(user_id);
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("해당 ID의 회원을 찾을 수 없습니다: " + user_id);
+        }
+
+        SuggestFastRequestDto suggestFastRequestDto = new SuggestFastRequestDto();
+
+        suggestFastRequestDto.setQuestionType(suggestRequestDto.getQuestionType());
+        suggestFastRequestDto.setStartTime(suggestRequestDto.getStartTime());
+        suggestFastRequestDto.setFinishTime(suggestRequestDto.getFinishTime());
+        suggestFastRequestDto.setStartPlace(suggestRequestDto.getStartPlace());
+        suggestFastRequestDto.setOptionalRequest(suggestRequestDto.getOptionalRequest());
+        suggestFastRequestDto.setBirthYear(member.getBirthYear());
+        suggestFastRequestDto.setGender(member.getGender());
+        suggestFastRequestDto.setMbti(member.getMbti());
+
+        // FastApi 서버로 POST 요청을 보내기
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        HttpEntity entity = new HttpEntity<>(suggestFastRequestDto, headers);
+
+        try {
+            ResponseEntity response = restTemplate.postForEntity(fastApiUrl + "/suggest/" + user_id, entity, SuggestFastResponseDto.class);
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                SuggestFastResponseDto suggestFastResponseDto = response.getBody();
+                log.debug("FastAPI 응답: {}", suggestFastResponseDto);
+                return ResponseEntity.ok(suggestFastResponseDto);
+            }
+            else {
+                log.error("FastAPI 요청 실패: {}", response.getStatusCode());
+                return ResponseEntity.status(response.getStatusCode()).body("FastAPI 서버로부터 오류 응답: " + response.getStatusCode());
+            }
+        } catch (HttpClientErrorException e) {
+            log.error("클라이언트 오류: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
+            return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 클라이언트 오류: " + e.getResponseBodyAsString());
+        } catch (HttpServerErrorException e) {
+            log.error("서버 오류: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
+            return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 서버 오류: " + e.getResponseBodyAsString());
+        } catch (RestClientException e) {
+            log.error("RestClientException: {}", e.getMessage());
+            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("FastAPI 서비스 통신 오류: " + e.getMessage());
+        }
+    }
+
+    @GetMapping("/{user_id}")
+    public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인이 필요합니다.");
+        }
+
+        // 2. 권한 확인 (요청한 user_id와 인증된 사용자가 동일한지)
+        if (!authentication.getName().equals(user_id)) {
+            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("자신의 정보에 대해서만 요청할 수 있습니다.");
+        }
+
+        Member member;
+        try {
+            member = memberService.findByUserId(user_id);
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("해당 ID의 회원을 찾을 수 없습니다: " + user_id);
+        }
+
+
+        ResponseEntity response =  restTemplate.getForEntity(fastApiUrl + "/suggest/" + user_id, String.class);
+
+        try {
+//            ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "/suggest/" + user_id, SuggestFastResponseDto[].class);
+//            SuggestFastResponseDto[] responseArray = responseEntity.getBody();
+            SuggestFastResponseDto[] responseArray = objectMapper.readValue(response.getBody(), SuggestFastResponseDto[].class);
+            
+            if (responseArray == null) {
+                // FastAPI 응답 본문이 null인 경우 (예: FastAPI가 204 No Content를 반환했거나, 문제가 있는 경우)
+                // 빈 배열 또는 적절한 오류 응답을 반환할 수 있습니다.
+                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("FastAPI로부터 응답 데이터를 받지 못했습니다.");
+            }
+
+            return ResponseEntity.ok(responseArray);
+        } catch (HttpClientErrorException | HttpServerErrorException e) {
+            return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 오류 발생: " + e.getMessage());
+        } catch (Exception e) {
+            try{
+                SuggestError errorResponse = objectMapper.readValue(response.getBody(), SuggestError.class);
+                log.warn("응답 메시지: {}", errorResponse.getMessage());
+                if(errorResponse.getMessage().contains("No chat history found for this user")){
+                    return ResponseEntity.ok("[]");
+                }else{
+                    return ResponseEntity.ok(errorResponse.getMessage());
+                }
+
+            } catch (JsonProcessingException ex) {
+//                throw new RuntimeException(ex);
+                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("내부 서버 오류가 발생했습니다: " + e.getMessage());
+            }
+        }
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
new file mode 100644
index 0000000..cbff3ed
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
@@ -0,0 +1,9 @@
+package com.example.userservice.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminLogin {
+    private Long memberId;
+    private String password;
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
new file mode 100644
index 0000000..54cac85
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
@@ -0,0 +1,75 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+public class AuthDto {
+
+    /**
+     * 로그인 요청 DTO
+     */
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginRequest {
+        private String user_id;  // 로그인 시 사용자가 입력하는 ID
+        private String password; // 비밀번호
+    }
+
+    /**
+     * 로그인 응답 DTO
+     */
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginResponse {
+        private String accessToken;   // 액세스 토큰
+        private String refreshToken;  // 리프레시 토큰
+        private String member_id;     // 내부 관리용 ID
+        private String user_id;       // 로그인용 ID
+        private String nickname;      // 닉네임
+        private String role;          // 사용자 역할 (ROLE_USER or ROLE_ADMIN)
+    }
+
+    /**
+     * 로그아웃 요청 DTO
+     */
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutRequest {
+        private String memberId;      // 회원 ID
+
+        public String getMemberId() {
+            return memberId;
+        }
+    }
+
+    /**
+     * 로그아웃 응답 DTO
+     */
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutResponse {
+        private boolean success;      // 성공 여부
+        private String message;       // 메시지
+    }
+
+    /**
+     * 토큰 갱신 요청 DTO
+     */
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RefreshTokenRequest {
+        private String refreshToken;  // 리프레시 토큰
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java
new file mode 100644
index 0000000..fd469c2
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java
@@ -0,0 +1,24 @@
+package com.example.userservice.dto;
+
+import lombok.Data;
+
+@Data
+public class FavoriteDto {
+    private Long favorite_id;
+    private String type;
+    private String place_id;
+    private String user_id;
+    private String name;
+    private String address;
+
+    public FavoriteDto(Long favorite_id ,String type, String placeId, String userId, String name, String address) {
+        this.favorite_id = favorite_id;
+        this.type = type;
+        this.place_id = placeId;
+        this.user_id = userId;
+        this.name = name;
+        this.address = address;
+    }
+
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
new file mode 100644
index 0000000..7295b95
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
@@ -0,0 +1,72 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+public class MemberDto {
+
+    // 회원 정보 응답 DTO
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MemberResponse {
+        private Long member_id;         // 회원 고유 ID (시스템 내부용)
+        private String user_id;       // 로그인 ID
+        private String nickname;      // 닉네임
+        private Short birth_year;     // 출생연도
+        private String mbti;          // MBTI 유형
+        private String gender;        // 성별
+        private String role;          // 역할 (ROLE_USER, ROLE_ADMIN)
+        private LocalDateTime created_at; // 계정 생성일
+    }
+
+    // 프로필 조회 응답 DTO
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class ProfileResponse {
+        private String member_id;
+        private String user_id;
+        private String nickname;      // 닉네임
+        private Short birth_year;     // 출생연도
+        private String mbti;          // MBTI 유형
+        private String gender;        // 성별
+        private String role;          // 역할 (ROLE_USER, ROLE_ADMIN)
+        private LocalDateTime created_at; // 계정 생성일
+    }
+
+    // 프로필 업데이트 요청 DTO
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class UpdateRequest {
+        private String nickname;          // 변경할 닉네임
+        private Short birth_year;         // 변경할 출생연도
+        private String mbti;              // 변경할 MBTI
+        private String gender;            // 변경할 성별
+        private String current_password;  // 현재 비밀번호 (비밀번호 변경 시 필요)
+        private String new_password;      // 새 비밀번호 (비밀번호 변경 시 필요)
+    }
+
+    // 회원 등록 요청 DTO
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RegisterRequest {
+        private String user_id;           // 로그인 ID
+        private String password;          // 비밀번호
+        private String nickname;          // 닉네임
+        private Short birth_year;         // 출생연도
+        private String mbti;              // MBTI 유형
+        private String gender;            // 성별
+        private String role;              // 역할 (옵션)
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
new file mode 100644
index 0000000..c9f4e94
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -0,0 +1,26 @@
+package com.example.userservice.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@Getter
+@Setter
+public class MemberSign {
+    @JsonProperty("user_id") // JSON 필드와 매핑
+    private String userId; // 로그인에 사용할 ID
+
+    private String password; // 비밀번호
+
+    private String nickname; // 닉네임
+
+    @JsonProperty("birth_year") // JSON 필드와 매핑
+    private Short birthYear; // 출생연도
+
+    private String mbti; // MBTI 유형
+
+    private String gender; // 성별
+
+    // 관리자 회원가입용 옵션 필드
+    private String role; // 사용자 권한 (기본값 ROLE_USER, ROLE_ADMIN 가능)
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestError.java b/user-service/src/main/java/com/example/userservice/dto/SuggestError.java
new file mode 100644
index 0000000..9c5a656
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/SuggestError.java
@@ -0,0 +1,8 @@
+package com.example.userservice.dto;
+
+import lombok.Data;
+
+@Data
+public class SuggestError {
+    private String message;
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestFastRequestDto.java b/user-service/src/main/java/com/example/userservice/dto/SuggestFastRequestDto.java
new file mode 100644
index 0000000..9e5dff7
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/SuggestFastRequestDto.java
@@ -0,0 +1,31 @@
+package com.example.userservice.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class SuggestFastRequestDto {
+    @JsonProperty("question_type")
+    private int questionType;
+
+    @JsonProperty("start_time")
+    private String startTime;
+
+    @JsonProperty("finish_time")
+    private String finishTime;
+
+    @JsonProperty("start_place")
+    private String startPlace;
+
+    @JsonProperty("optional_request")
+    private String optionalRequest;
+
+    @JsonProperty("birth_year")
+    private int birthYear;
+
+    @JsonProperty("gender")
+    private String gender;
+
+    @JsonProperty("mbti")
+    private String mbti;
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestFastResponseDto.java b/user-service/src/main/java/com/example/userservice/dto/SuggestFastResponseDto.java
new file mode 100644
index 0000000..4d0d298
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/SuggestFastResponseDto.java
@@ -0,0 +1,32 @@
+package com.example.userservice.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SuggestFastResponseDto {
+    @JsonProperty("start_time")
+    private String startTime;
+
+    @JsonProperty("finish_time")
+    private String finishTime;
+
+    @JsonProperty("start_place")
+    private String startPlace;
+
+    @JsonProperty("optional_request")
+    private String optionalRequest;
+
+    @JsonProperty("birth_year")
+    private int birthYear;
+
+    @JsonProperty("gender")
+    private String gender;
+
+    @JsonProperty("mbti")
+    private String mbti;
+
+    @JsonProperty("answer")
+    private String answer;
+
+    @JsonProperty("created_at")
+    private String createdAt;
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java b/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java
new file mode 100644
index 0000000..c0eff8c
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java
@@ -0,0 +1,22 @@
+package com.example.userservice.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class SuggestRequestDto {
+    @JsonProperty("question_type")
+    private int questionType;
+
+    @JsonProperty("start_time")
+    private String startTime;
+
+    @JsonProperty("finish_time")
+    private String finishTime;
+
+    @JsonProperty("start_place")
+    private String startPlace;
+
+    @JsonProperty("optional_request")
+    private String optionalRequest = "";
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/AccessToken.java b/user-service/src/main/java/com/example/userservice/entity/AccessToken.java
new file mode 100644
index 0000000..3b63010
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/AccessToken.java
@@ -0,0 +1,26 @@
+package com.example.userservice.entity;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+@Getter
+@NoArgsConstructor
+@RedisHash(value = "accessToken", timeToLive = 2700) // 45분(2700초) 후 자동 삭제
+public class AccessToken {
+
+    @Id
+    private String id; // 사용자 ID를 저장
+
+    @Indexed // 인덱싱하여 검색 가능하도록
+    private String accessToken;
+
+    @Builder
+    public AccessToken(String id, String accessToken) {
+        this.id = id;
+        this.accessToken = accessToken;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/Favorite.java b/user-service/src/main/java/com/example/userservice/entity/Favorite.java
new file mode 100644
index 0000000..4cad270
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/Favorite.java
@@ -0,0 +1,27 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Data
+@NoArgsConstructor
+@Table(name = "favorite")
+public class Favorite {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "favorite_id", nullable = false)
+    private Long favoriteId;
+
+    @Column(name = "type", nullable = false)
+    private String type;
+
+    @Column(name = "place_id", nullable = false)
+    private String placeId; // 사용자 로그인 ID
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "user_id", nullable = false)
+    private Member member;
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
new file mode 100644
index 0000000..5df9b7c
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -0,0 +1,47 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "member")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Member {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "member_id", nullable = false)
+    private Long memberId; // 회원 고유 ID (시스템 내부용)
+
+    @Column(name = "user_id", nullable = false, unique = true)
+    private String userId; // 사용자 로그인 ID
+
+    @Column(nullable = false)
+    private String password; // 비밀번호
+
+    @Column
+    private String nickname; // 닉네임
+
+    @Column(name = "birth_year")
+    private Short birthYear; // 출생연도
+
+    @Column(name = "mbti")
+    private String mbti; // MBTI 유형
+
+    @Column(name = "gender")
+    private String gender; // 성별
+
+    @Column(name = "created_at")
+    private LocalDateTime createdAt; // 계정 생성일
+
+    @Column(name = "role", nullable = false)
+    private String role; // 사용자 권한 ("ROLE_ADMIN" 또는 "ROLE_USER")
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
new file mode 100644
index 0000000..78a7d5a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
@@ -0,0 +1,26 @@
+package com.example.userservice.entity;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+@Getter
+@NoArgsConstructor
+@RedisHash(value = "refreshToken", timeToLive = 21600) // 6시간 후 자동 삭제
+public class RefreshToken {
+
+    @Id
+    private String id; // 사용자 ID를 저장
+
+    @Indexed // 인덱싱하여 검색 가능하도록
+    private String refreshToken;
+
+    @Builder
+    public RefreshToken(String id, String refreshToken) {
+        this.id = id;
+        this.refreshToken = refreshToken;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
new file mode 100644
index 0000000..f2abb4c
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
@@ -0,0 +1,21 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.Id;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+@Getter
+@AllArgsConstructor
+@RedisHash(value = "token", timeToLive = 21600) // 리프레시토큰 유효기간 6시간
+public class TokenRedis {
+
+    @Id
+    private String id;
+
+    @Indexed
+    private String accessToken;
+
+    private String refreshToken;
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Accommodation.java b/user-service/src/main/java/com/example/userservice/entity/place/Accommodation.java
new file mode 100644
index 0000000..4bbf6a6
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/place/Accommodation.java
@@ -0,0 +1,32 @@
+package com.example.userservice.entity.place;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.Getter;
+
+import java.math.BigDecimal;
+
+@Data
+@Entity
+@Getter
+public class Accommodation {
+    @Id
+    @Column(name = "accommodation_id")
+    private Long accommodationId;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "area_id", referencedColumnName = "area_id", nullable = false)
+    private Area area;
+
+    private String name;
+    private String address;
+    @Column(precision = 10, scale = 6)
+    private BigDecimal lat;
+    @Column(precision = 10, scale = 6)
+    private BigDecimal lon;
+    private String phone;
+    private String gu;
+    private String type;
+    @Column(name = "kakaomap_url")
+    private String kakaomapUrl;
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Area.java b/user-service/src/main/java/com/example/userservice/entity/place/Area.java
new file mode 100644
index 0000000..afb808b
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/place/Area.java
@@ -0,0 +1,40 @@
+package com.example.userservice.entity.place;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import jakarta.persistence.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Entity
+@Data
+public class Area {
+    @Id
+    @Column(name = "area_id")
+    private Long areaId;
+
+    @Column(name = "seoul_id", length = 10, nullable = false, unique = true)
+    private String seoulId;
+
+    @Column(name = "name", length = 50, nullable = false)
+    private String name;  // 지역명
+
+    @Column(name = "name_eng", length = 100, nullable = false)
+    private String nameEng;
+
+    @Column(name = "category", length = 20, nullable = false)
+    private String category;  // 구분
+
+    @Column(name = "lat", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lat; // 위도
+
+    @Column(name = "lon", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lon; // 경도
+
+    @OneToMany(mappedBy = "area", fetch = FetchType.LAZY)
+    @JsonIgnore
+    private List attractions;
+
+
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Attraction.java b/user-service/src/main/java/com/example/userservice/entity/place/Attraction.java
new file mode 100644
index 0000000..7ae8e1a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/place/Attraction.java
@@ -0,0 +1,50 @@
+package com.example.userservice.entity.place;
+
+import jakarta.persistence.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@Entity
+public class Attraction {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "attraction_id")
+    private Long attractionId;
+
+    @Column(name = "seoul_attraction_id", length = 10, nullable = false, unique = true)
+    private String seoulAttractionId;
+
+    @Column(name="name", length = 200, nullable = false)
+    private String name;
+
+    @Column(name="address", length = 200, nullable = false)
+    private String address;
+
+    @Column(name = "lat", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lat; // 위도
+
+    @Column(name = "lon", precision = 10, scale = 6, nullable = false)
+    private BigDecimal lon; // 경도
+
+    @Column(name="phone", length = 30)
+    private String phone;
+
+    @Column(name="homepage_url", columnDefinition = "TEXT")
+    private String homepageUrl;
+
+    @Column(name="close_day", columnDefinition = "TEXT")
+    private String closeDay;
+
+    @Column(name="use_time", columnDefinition = "TEXT")
+    private String useTime;
+
+    @Column(name="kakaomap_url",columnDefinition = "TEXT")
+    private String kakaomapUrl;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "area_id", nullable = false)
+    private Area area;
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Cafe.java b/user-service/src/main/java/com/example/userservice/entity/place/Cafe.java
new file mode 100644
index 0000000..d60cba7
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/place/Cafe.java
@@ -0,0 +1,49 @@
+package com.example.userservice.entity.place;
+
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "cafe")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Cafe {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "cafe_id")
+    private Long id;
+
+    @Column(name = "name")
+    private String name;
+
+    @Column(name = "address")
+    private String address;
+
+    @Column(name = "lat", precision = 10, scale = 6)
+    private BigDecimal lat;  // 위도
+
+    @Column(name = "lon", precision = 10, scale = 6)
+    private BigDecimal lon;  // 경도
+
+    @Column(name = "phone")
+    private String phone;
+
+    @Column(name = "kakaomap_url")
+    private String kakaomapUrl;
+
+    @Column(name = "category_code")
+    private String categoryCode;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "area_id")
+    private Area area;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Restaurant.java b/user-service/src/main/java/com/example/userservice/entity/place/Restaurant.java
new file mode 100644
index 0000000..63f1a61
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/place/Restaurant.java
@@ -0,0 +1,67 @@
+package com.example.userservice.entity.place;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "restaurant") // 테이블명 명시
+public class Restaurant {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "restaurant_id")
+    private Long restaurantId; // PK
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "area_id", nullable = false)
+    private Area area; // Area 엔티티와 다대일 관계
+
+    @Column(length = 50, nullable = false)
+    private String name; // 음식점 이름
+
+    @Column(length = 200)
+    private String address; // 음식점 주소
+
+    @Column(precision = 10, scale = 6, nullable = false)
+    private BigDecimal lat; // 위도
+
+    @Column(precision = 10, scale = 6, nullable = false)
+    private BigDecimal lon; // 경도
+
+    @Column(length = 20)
+    private String phone; // 전화번호
+
+    @Column(length = 200)
+    private String kakaomap_url; // 카카오맵 URL
+
+    @Column(length = 20)
+    private String category_code; // 카테고리 코드 (FD6: 음식점)
+
+//    @Column(length = 30)
+//    private String kakao_id;  // 카카오 API가 주는 id 저장 (String)
+
+    //추가
+    @Column(length = 100)
+    private String categoryGroupName; // (예시) 음식점
+
+    @Column(length = 200)
+    private String categoryName; // (예시) 음식점 > 한식 > 육류,고기
+
+    @Column(name = "kakao_id", length = 30)
+    private String kakao_id;
+
+    public String getKakao_id() {
+        return kakao_id;
+    }
+
+    public void setKakao_id(String kakao_id) {
+        this.kakao_id = kakao_id;
+    }
+
+}
+
diff --git a/user-service/src/main/java/com/example/userservice/exception/CustomAccessDeniedHandler.java b/user-service/src/main/java/com/example/userservice/exception/CustomAccessDeniedHandler.java
new file mode 100644
index 0000000..823433b
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/exception/CustomAccessDeniedHandler.java
@@ -0,0 +1,22 @@
+package com.example.userservice.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+public class CustomAccessDeniedHandler implements AccessDeniedHandler {
+    @Override
+    public void handle(HttpServletRequest request,
+                       HttpServletResponse response,
+                       AccessDeniedException accessDeniedException) throws IOException {
+        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().write("{\"error\": \"접근이 거부되었습니다: 권한이 없습니다.\"}");
+    }
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java b/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java
new file mode 100644
index 0000000..cd15374
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java
@@ -0,0 +1,51 @@
+package com.example.userservice.exception;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Slf4j
+public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
+        throws IOException {
+
+        // 원인 예외 메시지 얻기
+        Throwable cause = authException.getCause();
+        String detailedMessage = authException.getMessage();
+        if (cause != null) {
+            detailedMessage += " | 원인: " + cause.getMessage();
+        }
+        // 로그에 예외 내용 남기기
+        log.error("인증 실패 발생:{}, 요청 URI: {} {}", detailedMessage, request.getRequestURI(), authException.getMessage());
+
+        sendErrorResponse(response, HttpStatus.UNAUTHORIZED, detailedMessage, request.getRequestURI());
+    }
+
+
+    private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message, String path)
+            throws IOException {
+        response.setStatus(status.value());
+        response.setContentType("application/json;charset=UTF-8");
+
+        Map body = new LinkedHashMap<>();
+        body.put("timestamp", LocalDateTime.now().toString());
+        body.put("status", status.value());
+        body.put("error", status.getReasonPhrase());
+        body.put("message", message);
+        body.put("path", path);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        response.getWriter().write(objectMapper.writeValueAsString(body));
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java
new file mode 100644
index 0000000..06ca7b0
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java
@@ -0,0 +1,19 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.Favorite;
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface FavoriteRepository extends JpaRepository {
+    List findAllByMember_UserId(String userId);
+
+    Optional findByMemberAndTypeAndPlaceId(Member member, String type, String id);
+
+    void deleteAllByMember(Member member);
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java b/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
new file mode 100644
index 0000000..30e8fe1
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
@@ -0,0 +1,11 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.TokenRedis;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Optional;
+
+public interface TokenRedisRepository extends CrudRepository {
+
+    Optional findByAccessToken(String accessToken); // AccessToken으로 찾아내기
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
new file mode 100644
index 0000000..6d98e36
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
@@ -0,0 +1,16 @@
+package com.example.userservice.repository.jpa;
+
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface MemberRepository extends JpaRepository {
+    boolean existsByMemberId(Long memberId);
+    boolean existsByUserId(String memberId);
+    boolean existsByNickname(String nickname);
+    Optional findByNickname(String nickname);
+    Optional findByUserId(String memberId);
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/place/AccommodationRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/AccommodationRepository.java
new file mode 100644
index 0000000..b449b5a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/place/AccommodationRepository.java
@@ -0,0 +1,9 @@
+package com.example.userservice.repository.place;
+
+import com.example.userservice.entity.place.Accommodation;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface AccommodationRepository extends JpaRepository {
+    boolean existsById(Long id);
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/place/AttractionRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/AttractionRepository.java
new file mode 100644
index 0000000..cbf1493
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/place/AttractionRepository.java
@@ -0,0 +1,9 @@
+package com.example.userservice.repository.place;
+
+import com.example.userservice.entity.place.Attraction;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+
+public interface AttractionRepository extends JpaRepository {
+    boolean existsById(Long id);
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/place/CafeRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/CafeRepository.java
new file mode 100644
index 0000000..554c044
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/place/CafeRepository.java
@@ -0,0 +1,8 @@
+package com.example.userservice.repository.place;
+
+import com.example.userservice.entity.place.Cafe;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CafeRepository extends JpaRepository {
+    boolean existsById(Long id);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/place/RestaurantRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/RestaurantRepository.java
new file mode 100644
index 0000000..584243e
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/place/RestaurantRepository.java
@@ -0,0 +1,9 @@
+package com.example.userservice.repository.place;
+
+import com.example.userservice.entity.place.Restaurant;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface RestaurantRepository extends JpaRepository {
+    boolean existsById(Long id);
+
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
new file mode 100644
index 0000000..a680309
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
@@ -0,0 +1,12 @@
+package com.example.userservice.repository.redis;
+
+import com.example.userservice.entity.RefreshToken;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface RefreshTokenRepository extends CrudRepository {
+    Optional findByRefreshToken(String refreshToken);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..7036246
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
@@ -0,0 +1,138 @@
+package com.example.userservice.security;
+
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.service.TokenService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    private final JwtUtil jwtUtil;
+    private final UserDetailsService userDetailsService;
+    private final MemberRepository memberRepository;
+    private final TokenService tokenService;
+
+    // 완전 일치 제외 경로 목록
+    private final List excludedExactPaths = Arrays.asList(
+            "/auth/signup",
+            "/auth/admin/signup",
+            "/auth/login",
+            "/auth/logout",
+            "/swagger-ui.html",
+            "/swagger-ui/index.html",
+            "/v3/api-docs"
+    );
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain
+    ) throws ServletException, IOException {
+
+        if (shouldNotFilter(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        logger.info("요청 URI: " + request.getRequestURI());
+
+        String jwt = extractTokenFromRequest(request);
+
+        if (jwt == null) {
+            logger.info("토큰이 없음, 인증 스킵");
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            String username = jwtUtil.extractUsername(jwt);
+            logger.info("토큰에서 사용자 ID 추출: " + username);
+
+            String tokenId = jwtUtil.extractTokenId(jwt);
+
+            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
+
+                if (jwtUtil.validateToken(jwt, userDetails)) {
+                    Member member = memberRepository.findByUserId(username)
+                            .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다"));
+                    Long userId = member.getMemberId();
+
+                    if (tokenService.isLatestToken(userId, tokenId)) {
+                        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                                userDetails,
+                                null,
+                                userDetails.getAuthorities()
+                        );
+                        authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+                        SecurityContextHolder.getContext().setAuthentication(authToken);
+                        logger.info("사용자 인증 성공: " + username);
+                    } else {
+                        logger.warn("토큰 검증 실패: 이전 버전의 토큰");
+                    }
+                } else {
+                    logger.warn("토큰 검증 실패: 유효하지 않은 토큰");
+                }
+            }
+        } catch (Exception e) {
+            logger.error("JWT 토큰 검증 실패: " + e.getMessage());
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    // Swagger 및 인증 제외 경로 처리
+    public boolean shouldNotFilter(HttpServletRequest request) {
+        String path = request.getRequestURI();
+        return excludedExactPaths.contains(path)
+                || path.startsWith("/swagger-ui")
+                || path.startsWith("/v3/api-docs");
+    }
+
+    // 토큰 추출 메서드
+    private String extractTokenFromRequest(HttpServletRequest request) {
+        final String authHeader = request.getHeader("Authorization");
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            logger.info("Authorization 헤더에서 토큰 추출됨");
+            return authHeader.substring(7);
+        }
+
+        final String accessTokenHeader = request.getHeader("accessToken");
+        if (accessTokenHeader != null) {
+            logger.info("accessToken 헤더에서 토큰 추출됨");
+            return accessTokenHeader;
+        }
+
+        Cookie[] cookies = request.getCookies();
+        if (cookies != null) {
+            for (Cookie cookie : cookies) {
+                if ("access_token".equals(cookie.getName())) {
+                    logger.info("쿠키에서 토큰 추출됨");
+                    return cookie.getValue();
+                }
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
new file mode 100644
index 0000000..412d6d7
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
@@ -0,0 +1,133 @@
+package com.example.userservice.security;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+@Slf4j
+public class JwtUtil {
+
+    private SecretKey secretKey;
+    private Long accessTokenExpiration;     // 액세스 토큰 유효 시간
+    private Long refreshTokenExpiration;    // 리프레시 토큰 유효 시간
+
+    @PostConstruct
+    public void init() {
+        Dotenv dotenv = Dotenv.configure()
+                .ignoreIfMalformed()
+                .ignoreIfMissing()
+                .load();
+
+        String jwtSecret = dotenv.get("JWT_SECRET_KEY");
+        if (jwtSecret == null || jwtSecret.isEmpty()) {
+            throw new IllegalStateException("JWT_SECRET_KEY 환경변수가 설정되지 않았습니다!");
+        }
+
+        this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
+        this.accessTokenExpiration = 1000L * 60 * 45;         // 45분
+        this.refreshTokenExpiration = 1000L * 60 * 60 * 6;    // 6시간
+
+        log.debug("JWT 설정 완료: 액세스 토큰 = {}분, 리프레시 토큰 = {}분", accessTokenExpiration / 60000, refreshTokenExpiration / 60000);
+    }
+
+    // 사용자 이름 추출
+    public String extractUsername(String token) {
+        return extractClaim(token, Claims::getSubject);
+    }
+
+    // 만료 시간 추출
+    public Date extractExpiration(String token) {
+        return extractClaim(token, Claims::getExpiration);
+    }
+
+    // 토큰 ID 추출
+    public String extractTokenId(String token) {
+        return extractClaim(token, claims -> claims.get("tokenId", String.class));
+    }
+
+    // 클레임 추출
+    public  T extractClaim(String token, Function claimsResolver) {
+        final Claims claims = extractAllClaims(token);
+        return claimsResolver.apply(claims);
+    }
+
+    // 모든 클레임 추출
+    private Claims extractAllClaims(String token) {
+        return Jwts.parser()
+                .verifyWith(secretKey)
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    // 토큰 만료 여부
+    private Boolean isTokenExpired(String token) {
+        return extractExpiration(token).before(new Date());
+    }
+
+    // 액세스 토큰 생성 (UserDetails 기반)
+    public String generateToken(UserDetails userDetails) {
+        return createToken(new HashMap<>(), userDetails.getUsername(), accessTokenExpiration);
+    }
+
+    // 액세스 토큰 생성 (닉네임 기반)
+    public String generateAccessToken(String nickname) {
+        return createToken(new HashMap<>(), nickname, accessTokenExpiration);
+    }
+
+    // 리프레시 토큰 생성 (UserDetails 기반)
+    public String generateRefreshToken(UserDetails userDetails) {
+        return createToken(new HashMap<>(), userDetails.getUsername(), refreshTokenExpiration);
+    }
+
+    // 리프레시 토큰 생성 (닉네임 기반)
+    public String generateRefreshToken(String nickname) {
+        return createToken(new HashMap<>(), nickname, refreshTokenExpiration);
+    }
+
+    // JWT 생성 로직
+    private String createToken(Map claims, String subject, Long expirationTime) {
+        String tokenId = String.valueOf(System.currentTimeMillis());
+
+        return Jwts.builder()
+                .claims(claims)
+                .subject(subject)
+                .claim("tokenId", tokenId)
+                .issuedAt(new Date(System.currentTimeMillis()))
+                .expiration(new Date(System.currentTimeMillis() + expirationTime))
+                .signWith(secretKey, SignatureAlgorithm.HS512)
+                .compact();
+    }
+
+    // 토큰 유효성 검사 (UserDetails와 비교)
+    public Boolean validateToken(String token, UserDetails userDetails) {
+        final String username = extractUsername(token);
+        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+    }
+
+    // 토큰 유효성 검사 (단순 검증)
+    public Boolean validateToken(String token) {
+        try {
+            Jwts.parser()
+                    .verifyWith(secretKey)
+                    .build()
+                    .parseSignedClaims(token);
+            return !isTokenExpired(token);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java
new file mode 100644
index 0000000..f7eb6b9
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java
@@ -0,0 +1,228 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.security.JwtUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    private final MemberRepository memberRepository;
+    private final JwtUtil jwtUtil;
+    private final AuthenticationManager authenticationManager;
+    private final TokenService tokenService;
+    private final RedisTemplate redisTemplate;
+
+    private static final String TOKEN_VERSION_PREFIX = "token:version:";
+    private static final long TOKEN_VERSION_TTL = 2700; // 45분(초 단위)
+
+    /**
+     * 로그인 처리
+     * @param request 로그인 요청 정보 (user_id, password)
+     * @return 토큰과 사용자 정보가 포함된 응답
+     */
+    public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
+        log.debug("로그인 시도: {}", request.getUser_id());
+
+        // 1. 사용자 조회 (userId로 조회)
+        Member member = memberRepository.findByUserId(request.getUser_id())
+                .orElseThrow(() -> {
+                    log.error("사용자를 찾을 수 없음: {}", request.getUser_id());
+                    return new RuntimeException("사용자를 찾을 수 없습니다");
+                });
+
+        log.debug("사용자 찾음: ID={}, UserID={}", member.getMemberId(), member.getUserId());
+
+        // 2. 인증 시도
+        try {
+            authenticationManager.authenticate(
+                    new UsernamePasswordAuthenticationToken(
+                            request.getUser_id(),
+                            request.getPassword()
+                    )
+            );
+            log.debug("인증 성공");
+        } catch (Exception e) {
+            log.error("인증 실패: {}", e.getMessage());
+            throw e;
+        }
+
+        // 3. 기존 토큰이 있으면 삭제
+        Optional existingToken = tokenService.findRefreshTokenByMemberId(member.getMemberId());
+        if (existingToken.isPresent()) {
+            log.debug("기존 리프레시 토큰 발견: 사용자 ID = {}, 삭제 진행", member.getMemberId());
+            tokenService.deleteRefreshToken(member.getMemberId());
+        }
+
+        // 기존 토큰 버전이 있으면 삭제 (이전 토큰 무효화)
+        String versionKey = TOKEN_VERSION_PREFIX + member.getMemberId();
+        if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) {
+            log.debug("기존 토큰 버전 발견: 사용자 ID = {}, 삭제 진행", member.getMemberId());
+            redisTemplate.delete(versionKey);
+        }
+
+        // 사용자 권한 설정
+        List authorities = Collections.singletonList(
+                new SimpleGrantedAuthority(member.getRole())
+        );
+
+        // 4. JWT 토큰 발급 (유효 시간 45분)
+        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
+        String accessToken = jwtUtil.generateToken(userDetails);
+        String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+        // 5. 토큰 저장 및 버전 관리
+        tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+        // 토큰 ID 저장 (버전 관리)
+        String tokenId = jwtUtil.extractTokenId(accessToken);
+        saveTokenVersion(member.getMemberId(), tokenId);
+
+        log.debug("새 토큰 발급 완료: 액세스 토큰 유효 시간 = 45분, 사용자 ID = {}, 토큰 ID = {}", member.getMemberId(), tokenId);
+
+        // 6. 결과 반환
+        return AuthDto.LoginResponse.builder()
+                .accessToken(accessToken)
+                .refreshToken(refreshToken)
+                .member_id(String.valueOf(member.getMemberId()))
+                .user_id(member.getUserId())
+                .nickname(member.getNickname())
+                .role(member.getRole())
+                .build();
+    }
+
+    /**
+     * 로그아웃 처리
+     * @param request 로그아웃 요청 정보 (memberId)
+     * @return 로그아웃 성공 여부와 메시지
+     */
+    public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
+        // 사용자 ID 파싱
+        Long memberId = Long.parseLong(request.getMemberId());
+
+        // 1. 토큰 버전 삭제 (토큰 무효화)
+        String versionKey = TOKEN_VERSION_PREFIX + memberId;
+        if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) {
+            redisTemplate.delete(versionKey);
+            log.debug("토큰 버전 삭제 완료: 사용자 ID = {}", memberId);
+        }
+
+        // 2. 리프레시 토큰 삭제
+        tokenService.deleteRefreshToken(memberId);
+        log.debug("리프레시 토큰 삭제 완료: 사용자 ID = {}", memberId);
+
+        log.debug("로그아웃 완료: 사용자 ID = {}", memberId);
+
+        // 3. 성공 리턴
+        return AuthDto.LogoutResponse.builder()
+                .success(true)
+                .message("로그아웃 완료: 모든 토큰 무효화 완료")
+                .build();
+    }
+
+    /**
+     * 토큰 재발급
+     * @param refreshToken 리프레시 토큰
+     * @return 새로운 액세스 토큰
+     */
+    public String refreshAccessToken(String refreshToken) {
+        // 1. 리프레시 토큰 유효성 검사
+        if (!jwtUtil.validateToken(refreshToken)) {
+            throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
+        }
+
+        // 2. 데이터베이스(레디스)에서 리프레시 토큰 확인
+        Optional storedToken = tokenService.findRefreshTokenByRefreshToken(refreshToken);
+        if (storedToken.isEmpty()) {
+            throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다.");
+        }
+
+        // 3. 사용자 ID로 사용자 조회
+        Long userId = Long.valueOf(storedToken.get().getId()); // id는 String으로 저장되니까 Long으로 변환
+        Member member = memberRepository.findById(userId)
+                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
+
+        // 4. 현재 버전 삭제 (이전 토큰 무효화)
+        String versionKey = TOKEN_VERSION_PREFIX + userId;
+        if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) {
+            redisTemplate.delete(versionKey);
+        }
+
+        // 5. 새 액세스 토큰 생성
+        List authorities = Collections.singletonList(
+                new SimpleGrantedAuthority(member.getRole())
+        );
+
+        UserDetails userDetails = new User(
+                member.getUserId(),
+                member.getPassword(),
+                authorities
+        );
+
+        String newAccessToken = jwtUtil.generateToken(userDetails);
+
+        // 6. 새 토큰 버전 저장
+        String tokenId = jwtUtil.extractTokenId(newAccessToken);
+        saveTokenVersion(userId, tokenId);
+
+        log.debug("액세스 토큰 재발급 완료: 유효 시간 = 45분, 사용자 ID = {}, 토큰 ID = {}", userId, tokenId);
+
+        return newAccessToken;
+    }
+
+    /**
+     * 토큰 버전 저장 (현재 유효한 토큰 ID)
+     * @param userId 사용자 ID
+     * @param tokenId 토큰 ID
+     */
+    private void saveTokenVersion(Long userId, String tokenId) {
+        String key = TOKEN_VERSION_PREFIX + userId;
+        redisTemplate.opsForValue().set(key, tokenId, TOKEN_VERSION_TTL, TimeUnit.SECONDS);
+        log.debug("토큰 버전 저장: 사용자 ID = {}, 토큰 ID = {}", userId, tokenId);
+    }
+
+    /**
+     * 토큰이 최신 버전인지 확인
+     * @param userId 사용자 ID
+     * @param tokenId 토큰 ID
+     * @return 최신 토큰이면 true, 아니면 false
+     */
+    public boolean isLatestToken(Long userId, String tokenId) {
+        String key = TOKEN_VERSION_PREFIX + userId;
+        String latestTokenId = redisTemplate.opsForValue().get(key);
+
+        if (latestTokenId == null) {
+            return false; // 저장된 토큰 ID가 없으면 유효하지 않음 (로그아웃 상태)
+        }
+
+        boolean isLatest = latestTokenId.equals(tokenId);
+        if (!isLatest) {
+            log.debug("토큰 버전 불일치: 사용자 ID = {}, 요청 토큰 ID = {}, 최신 토큰 ID = {}", userId, tokenId, latestTokenId);
+        }
+
+        return isLatest;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/CookieUtil.java b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
new file mode 100644
index 0000000..2a1ab1f
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
@@ -0,0 +1,21 @@
+package com.example.userservice.service;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CookieUtil {
+
+    public void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
+        Cookie cookie = new Cookie(name, value);
+        cookie.setPath("/");
+        cookie.setHttpOnly(true);
+        cookie.setMaxAge(maxAge);
+        response.addCookie(cookie);
+    }
+
+    public int getDaysInSeconds(int days) {
+        return days * 24 * 60 * 60;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java
new file mode 100644
index 0000000..b415d49
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java
@@ -0,0 +1,197 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.FavoriteDto;
+import com.example.userservice.entity.Favorite;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.FavoriteRepository;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.repository.place.AccommodationRepository;
+import com.example.userservice.repository.place.AttractionRepository;
+import com.example.userservice.repository.place.CafeRepository;
+import com.example.userservice.repository.place.RestaurantRepository;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class FavoriteService {
+
+    private final FavoriteRepository favoriteRepository;
+    private final MemberRepository memberRepository;
+
+    private final AccommodationRepository accommodationRepository;
+    private final AttractionRepository attractionRepository;
+    private final CafeRepository cafeRepository;
+    private final RestaurantRepository restaurantRepository;
+
+//    @Value("${gateway-url}")
+//    private String gatewayUrl;
+
+    public FavoriteDto FindPlace(Favorite item){
+        Long placeId = Long.valueOf(item.getPlaceId());
+        String type = item.getType();
+        return switch (type) {
+            case "cafe" -> cafeRepository.findById(placeId)
+                    .map(cafe -> buildFavoriteDto(item, cafe.getName(), cafe.getAddress()))
+                    .orElseThrow(() -> new NoSuchElementException("Cafe not found"));
+            case "restaurant" -> restaurantRepository.findById(placeId)
+                    .map(restaurant -> buildFavoriteDto(item, restaurant.getName(), restaurant.getAddress()))
+                    .orElseThrow(() -> new NoSuchElementException("Restaurant not found"));
+            case "attraction" -> attractionRepository.findById(placeId)
+                    .map(attraction -> buildFavoriteDto(item, attraction.getName(), attraction.getAddress()))
+                    .orElseThrow(() -> new NoSuchElementException("Attraction not found"));
+            case "accommodation" -> accommodationRepository.findById(placeId)
+                    .map(acc -> buildFavoriteDto(item, acc.getName(), acc.getAddress()))
+                    .orElseThrow(() -> new NoSuchElementException("Accommodation not found"));
+            default -> throw new IllegalArgumentException("장소 type 오류: " + type);
+        };
+    }
+
+    private FavoriteDto buildFavoriteDto(Favorite item, String name, String address) {
+        return new FavoriteDto(
+                item.getFavoriteId(),
+                item.getType(),
+                item.getPlaceId(),
+                item.getMember().getUserId(),
+                name,
+                address
+        );
+    }
+
+    public List getListData(String userId) {
+        try{
+            List items = favoriteRepository.findAllByMember_UserId(userId);
+
+            return items.stream()
+                    .map(this::FindPlace).toList();
+
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            throw new BadCredentialsException("즐겨찾기 장소 조회 실패",e);
+        }
+    }
+
+    public ResponseEntity> addFavoriteData(String userId, FavoriteDto favoriteDto) {
+        Map response = new HashMap<>();
+
+        // 1. 사용자 조회
+        Member member = memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new RuntimeException("사용자 없음"));
+
+        // 2. 이미 존재하는 즐겨찾기인지 확인
+        Optional existingFavorite = favoriteRepository.findByMemberAndTypeAndPlaceId(
+                member,
+                favoriteDto.getType(),
+                favoriteDto.getPlace_id()
+        );
+        if (existingFavorite.isPresent()) {
+            response.put("message", "이미 등록된 즐겨찾기입니다.");
+            return ResponseEntity.status(400).body(response);
+        }
+
+        // 3. 존재하는 장소인지 type/id 확인
+        if(Objects.equals(favoriteDto.getType(), "cafe")){
+            if(!cafeRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){
+                response.put("message", "잘못된 장소 type/id 입니다 ");
+                return ResponseEntity.status(400).body(response);
+            }
+        }else if(Objects.equals(favoriteDto.getType(), "restaurant")){
+            if(!restaurantRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){
+                response.put("message", "잘못된 장소 type/id 입니다 ");
+                return ResponseEntity.status(400).body(response);
+            }
+        }else if(Objects.equals(favoriteDto.getType(), "attraction")){
+            if(!attractionRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){
+                response.put("message", "잘못된 장소 type/id 입니다 ");
+                return ResponseEntity.status(400).body(response);
+            }
+        }else if(Objects.equals(favoriteDto.getType(), "accommodation")){
+            if(!accommodationRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){
+                response.put("message", "잘못된 장소 type/id 입니다 ");
+                return ResponseEntity.status(400).body(response);
+            }
+        }
+
+//        //place-service와 REST 통신 방식
+//        ResponseEntity checkPlace = restTemplate.exchange(
+//                gatewayUrl + "/place/main/info/" + favoriteDto.getType() + "/" + favoriteDto.getPlace_id(),
+//                HttpMethod.GET,
+//                null,
+//                JsonNode.class
+//        );
+//
+
+        // 4. Favorite 엔티티 생성
+        Favorite favorite = new Favorite();
+        favorite.setType(favoriteDto.getType());
+        favorite.setPlaceId(favoriteDto.getPlace_id()); // id는 placeId 역할
+        favorite.setMember(member);
+
+        // 5. 저장
+        Favorite saved = favoriteRepository.save(favorite);
+
+        response.put("message", "즐겨찾기에 추가되었습니다.");
+        return ResponseEntity.ok(response);  // 200 OK와 함께 메세지 전송
+    }
+
+    @Transactional
+    public ResponseEntity> deleteFavorite(String userId, String type, String id) {
+
+        Map response = new HashMap<>();
+
+        Member member = memberRepository.findByUserId(userId).orElse(null);
+        if (member == null) {
+            response.put("message", "사용자 없음");
+            return ResponseEntity.status(400).body(response);
+        }
+
+        Favorite favorite = favoriteRepository.findByMemberAndTypeAndPlaceId(member, type, id).orElse(null);
+        if (favorite == null) {
+            response.put("message", "즐겨찾기 없음");
+            return ResponseEntity.status(400).body(response);
+        }
+
+        favoriteRepository.delete(favorite);
+        response.put("message", "즐겨찾기 삭제 완료");
+        return ResponseEntity.ok(response);
+    }
+
+    public List> getAllFavoriteData() {
+        try{
+            List items = favoriteRepository.findAll();
+
+            List favoriteDtoList = items.stream()
+                    .map(this::FindPlace).toList();
+
+            Map> grouped = favoriteDtoList.stream()
+                    .collect(Collectors.groupingBy(FavoriteDto::getUser_id));
+
+            // 원하는 형태로 변환
+
+            return grouped.entrySet().stream()
+                    .map(entry -> {
+                        Map map = new HashMap<>();
+                        map.put("user_id", entry.getKey());
+                        map.put("content", entry.getValue());
+                        return map;
+                    })
+                    .toList();
+
+        } catch (Exception e) {
+//            throw new RuntimeException(e);
+            log.error(e.getMessage());
+            throw new BadCredentialsException("즐겨찾기 장소 조회 실패",e);
+        }
+
+    }
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
new file mode 100644
index 0000000..e1a3608
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -0,0 +1,180 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.MemberDto;
+import com.example.userservice.dto.MemberSign;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.FavoriteRepository;
+import com.example.userservice.repository.jpa.MemberRepository;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class MemberService {
+    private final MemberRepository memberRepository;
+    private final PasswordEncoder passwordEncoder;
+    private final FavoriteRepository favoriteRepository;
+
+    // 일반 사용자 회원가입 (기본 권한은 ROLE_USER)
+    public Member registerUser(MemberSign dto) {
+        return registerMember(dto, "ROLE_USER");
+    }
+
+
+    // 관리자 회원가입
+    public Member registerAdmin(MemberSign dto) {
+        return registerMember(dto, "ROLE_ADMIN");
+    }
+
+
+    // 공통 회원가입 로직
+    private Member registerMember(MemberSign dto, String defaultRole) {
+        // userId 중복 체크
+        if (memberRepository.existsByUserId(dto.getUserId())) {
+            throw new IllegalStateException("이미 존재하는 사용자 ID입니다.");
+        }
+
+        // 닉네임 중복 체크
+        if (memberRepository.existsByNickname(dto.getNickname())) {
+            throw new IllegalStateException("이미 존재하는 닉네임입니다.");
+        }
+
+        // DTO에서 역할이 지정되었고 관리자로 등록하는 경우 해당 역할 사용, 아니면 기본 역할 사용
+        String role = dto.getRole() != null && defaultRole.equals("ROLE_ADMIN")
+                ? dto.getRole()
+                : defaultRole;
+
+        // 회원 엔티티 생성
+        Member member = Member.builder()
+                .userId(dto.getUserId())
+                .password(passwordEncoder.encode(dto.getPassword())) // 비밀번호 암호화
+                .nickname(dto.getNickname())
+                .birthYear(dto.getBirthYear())
+                .mbti(dto.getMbti())
+                .gender(dto.getGender())
+                .role(role)
+                .createdAt(LocalDateTime.now())
+                .build();
+
+        // 저장 후 반환
+        return memberRepository.save(member);
+    }
+
+    // 회원 정보 조회
+    public MemberDto.ProfileResponse getProfile(String userId) {
+        Member member = memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
+
+        return MemberDto.ProfileResponse.builder()
+                .member_id(String.valueOf(member.getMemberId()))
+                .user_id(member.getUserId())
+                .nickname(member.getNickname())
+                .birth_year(member.getBirthYear())
+                .mbti(member.getMbti())
+                .gender(member.getGender())
+                .role(member.getRole())
+                .created_at(member.getCreatedAt())
+                .build();
+    }
+
+    // 회원 정보 수정
+    @Transactional
+    public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateRequest request) {
+        Member member = memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
+
+        // 비밀번호 변경이 요청된 경우
+        if (request.getCurrent_password() != null && request.getNew_password() != null) {
+            // 현재 비밀번호 확인
+            if (!passwordEncoder.matches(request.getCurrent_password(), member.getPassword())) {
+                throw new RuntimeException("현재 비밀번호가 일치하지 않습니다.");
+            }
+
+            // 새 비밀번호로 업데이트 (암호화)
+            member.setPassword(passwordEncoder.encode(request.getNew_password()));
+        }
+
+        // 기타 정보 업데이트
+        if (request.getNickname() != null) {
+            member.setNickname(request.getNickname());
+        }
+
+        if (request.getBirth_year() != null) {
+            member.setBirthYear(request.getBirth_year());
+        }
+
+        if (request.getMbti() != null) {
+            member.setMbti(request.getMbti());
+        }
+
+        if (request.getGender() != null) {
+            member.setGender(request.getGender());
+        }
+
+        // 저장
+        Member updatedMember = memberRepository.save(member);
+
+        // 응답 DTO로 변환
+        return MemberDto.MemberResponse.builder()
+                .member_id(updatedMember.getMemberId())
+                .user_id(updatedMember.getUserId())
+                .nickname(updatedMember.getNickname())
+                .birth_year(updatedMember.getBirthYear())
+                .mbti(updatedMember.getMbti())
+                .gender(updatedMember.getGender())
+                .role(updatedMember.getRole())
+                .created_at(updatedMember.getCreatedAt())
+                .build();
+    }
+
+
+    // 로그인된 userId로 Member 엔티티 조회
+    public Member findByUserId(String userId) {
+        return memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다."));
+    }
+
+    // memberId로 회원 삭제
+    @Transactional
+    public void deleteMemberById(Long memberId) {
+        Member member = memberRepository.findById(memberId)
+                .orElseThrow(() -> new IllegalArgumentException("회원이 존재하지 않습니다."));
+
+        //즐겨찾기 먼저 삭제
+        favoriteRepository.deleteAllByMember(member);
+        // 회원 삭제
+        memberRepository.delete(member);
+    }
+
+
+
+
+
+    /**
+     * 모든 회원 목록 조회
+     * @return 모든 회원 정보 목록
+     */
+    public List getAllMembers() {
+        List members = memberRepository.findAll();
+
+        return members.stream()
+                .map(member -> MemberDto.MemberResponse.builder()
+                        .member_id(member.getMemberId())
+                        .user_id(member.getUserId())
+                        .nickname(member.getNickname())
+                        .birth_year(member.getBirthYear())
+                        .mbti(member.getMbti())
+                        .gender(member.getGender())
+                        .role(member.getRole())
+                        .created_at(member.getCreatedAt())
+                        .build())
+                .collect(Collectors.toList());
+    }
+
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
new file mode 100644
index 0000000..8d215f7
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -0,0 +1,165 @@
+package com.example.userservice.service;
+
+import com.example.userservice.entity.Member;
+import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.repository.redis.RefreshTokenRepository;
+import com.example.userservice.security.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class TokenService {
+
+    private final JwtUtil jwtUtil;
+    private final MemberRepository memberRepository;
+    private final RefreshTokenRepository refreshTokenRepository;
+    private final RedisTemplate redisTemplate;
+
+    private static final String TOKEN_VERSION_PREFIX = "token:version:";
+    private static final long TOKEN_VERSION_TTL = 2700; // 45분
+
+    /**
+     * 리프레시 토큰을 사용하여 새 액세스 토큰 생성
+     * @param refreshToken 리프레시 토큰
+     * @return 새 액세스 토큰
+     */
+    public String createNewAccessToken(String refreshToken) {
+        // 리프레시 토큰 유효성 검사
+        if (!jwtUtil.validateToken(refreshToken)) {
+            throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
+        }
+
+        // 데이터베이스(레디스)에서 리프레시 토큰 확인
+        Optional storedToken = refreshTokenRepository.findByRefreshToken(refreshToken);
+        if (storedToken.isEmpty()) {
+            throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다.");
+        }
+
+        // 사용자 ID로 사용자 조회
+        Long userId = Long.valueOf(storedToken.get().getId());
+        Member member = memberRepository.findById(userId)
+                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
+
+        // 기존 토큰 버전 삭제 (이전 액세스 토큰 무효화)
+        String key = TOKEN_VERSION_PREFIX + userId;
+        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
+            redisTemplate.delete(key);
+            log.debug("기존 토큰 버전 삭제: 사용자 ID = {}", userId);
+        }
+
+        // 사용자 권한 설정
+        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole());
+
+        // 새 액세스 토큰 생성
+        UserDetails userDetails = new User(
+                member.getUserId(),
+                member.getPassword(),
+                Collections.singletonList(authority)
+        );
+
+        String accessToken = jwtUtil.generateToken(userDetails);
+
+        // 토큰 버전 업데이트
+        String tokenId = jwtUtil.extractTokenId(accessToken);
+        saveTokenVersion(userId, tokenId);
+
+        log.debug("새 액세스 토큰 생성 완료: 사용자 ID = {}, 토큰 ID = {}", userId, tokenId);
+
+        return accessToken;
+    }
+
+    /**
+     * 토큰 버전 저장 (현재 유효한 토큰 ID)
+     */
+    public void saveTokenVersion(Long userId, String tokenId) {
+        String key = TOKEN_VERSION_PREFIX + userId;
+        redisTemplate.opsForValue().set(key, tokenId, TOKEN_VERSION_TTL, TimeUnit.SECONDS);
+        log.debug("토큰 버전 업데이트: 사용자 ID = {}, 토큰 ID = {}", userId, tokenId);
+    }
+
+    /**
+     * 토큰이 최신 버전인지 확인
+     */
+    public boolean isLatestToken(Long userId, String tokenId) {
+        String key = TOKEN_VERSION_PREFIX + userId;
+        String latestTokenId = redisTemplate.opsForValue().get(key);
+
+        if (latestTokenId == null) {
+            log.debug("저장된 토큰 버전 없음: 사용자 ID = {}", userId);
+            return false; // 저장된 토큰이 없으면 로그아웃된 상태로 간주하고 인증 실패
+        }
+
+        boolean isLatest = latestTokenId.equals(tokenId);
+        if (!isLatest) {
+            log.debug("토큰 버전 불일치: 사용자 ID = {}, 요청 토큰 ID = {}, 최신 토큰 ID = {}", userId, tokenId, latestTokenId);
+        }
+
+        return isLatest;
+    }
+
+    /**
+     * 토큰 무효화 (토큰 버전 삭제)
+     */
+    public void invalidateToken(Long userId) {
+        String key = TOKEN_VERSION_PREFIX + userId;
+        redisTemplate.delete(key);
+        log.debug("토큰 무효화 완료: 사용자 ID = {}", userId);
+    }
+
+    /**
+     * 리프레시 토큰 저장
+     * @param memberId 회원 ID
+     * @param refreshToken 리프레시 토큰
+     */
+    public void saveRefreshToken(Long memberId, String refreshToken) {
+        // 기존 토큰이 있는지 확인하고 있으면 삭제
+        Optional existingToken = refreshTokenRepository.findById(String.valueOf(memberId));
+        if (existingToken.isPresent()) {
+            refreshTokenRepository.deleteById(String.valueOf(memberId));
+            log.debug("기존 리프레시 토큰 삭제: 사용자 ID = {}", memberId);
+        }
+
+        // 새 토큰 저장
+        RefreshToken token = RefreshToken.builder()
+                .id(String.valueOf(memberId))
+                .refreshToken(refreshToken)
+                .build();
+
+        refreshTokenRepository.save(token);
+        log.debug("새 리프레시 토큰 저장: 사용자 ID = {}", memberId);
+    }
+
+    /**
+     * 리프레시 토큰 삭제
+     */
+    public void deleteRefreshToken(Long memberId) {
+        refreshTokenRepository.deleteById(String.valueOf(memberId));
+        log.debug("리프레시 토큰 삭제 완료: 사용자 ID = {}", memberId);
+    }
+
+    /**
+     * 사용자 ID로 리프레시 토큰 조회
+     */
+    public Optional findRefreshTokenByMemberId(Long memberId) {
+        return refreshTokenRepository.findById(String.valueOf(memberId));
+    }
+
+    /**
+     * 리프레시 토큰 값으로 리프레시 토큰 엔티티 조회
+     */
+    public Optional findRefreshTokenByRefreshToken(String refreshToken) {
+        return refreshTokenRepository.findByRefreshToken(refreshToken);
+    }
+}
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
deleted file mode 100644
index 79b5bda..0000000
--- a/user-service/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=user-service