diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
new file mode 100644
index 00000000..043551db
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -0,0 +1,18 @@
+---
+name: Bug
+about: BUG 발생 시 작성해주세요
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+## 버그설명
+버그 발생 상황을 적어주세요
+
+## 발생한 위치
+버그가 발생한 위치를 알려주세요
+
+## 참고 사항
+공유할 내용, 레퍼런스, 추가로 발생할 것으로 예상되는 이슈, 스크린샷 등을 넣어 주세요.
+- 추가적으로 필요한 내용은 comment로 남겨주세요.
diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md
new file mode 100644
index 00000000..071cea4c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature.md
@@ -0,0 +1,20 @@
+---
+name: Feature
+about: 프로젝트에 관한 아이디어, 기능추가
+title: ''
+labels: "✨ feat"
+assignees: ''
+
+---
+
+## 기능 설명
+개발할 기능에 대해서 말씀해주세요.
+
+## 세부 기능
+어떤 세부 기능을 구현할지 말씀해주세요.
+- [ ] feature_detail 1
+- [ ] feature_detail 2
+
+## 참고 사항
+공유할 내용, 레퍼런스, 추가로 발생할 것으로 예상되는 이슈, 스크린샷 등을 넣어 주세요.
+- 추가적으로 필요한 내용은 comment로 남겨주세요.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..4831d940
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,48 @@
+---
+(확인 후 지워주세요)
+---
+제목 양식 - 이모지 작업타입 [#이슈번호] : 제목
+
+|작업 타입|작업내용 |
+| --- | --- |
+| ✨ feat | 새로운 기능을 추가 |
+| 🐛 bugfix | 버그 수정 |
+| ♻️ refactor | 코드 리팩토링 |
+| 🩹 fix | 코드 수정 |
+| 🚚 move | 파일 옮김/정리 |
+| 🔥 del | 기능/파일을 삭제 |
+| 🍻 test | 테스트 코드를 작성 |
+| 🎨 readme | readme 수정 |
+| 🙈 gitfix | gitignore 수정 |
+| 🔨script | package.json 변경(npm 설치 등) |
+
+---
+
+## ✨ 변경 타입
+* [ ] 신규 기능 추가/수정
+* [ ] 버그 수정
+* [ ] 리팩토링
+* [ ] 설정
+* [ ] 비기능 (주석 등 기능에 영향을 주지 않음)
+
+## ✅ 체크리스트
+
+* [ ] 코드 작성 가이드라인을 준수했는가?
+* [ ] 변경 사항에 대한 테스트를 완료했는가?
+* [ ] 문서 변경이 필요한 경우, 해당 내용을 반영했는가?
+
+## 🚀 변경 내용
+* **as-is**
+ * (변경 전 설명을 여기에 작성)
+
+* **to-be**
+ * (변경 후 설명을 여기에 작성)
+
+## 💡 추가 설명
+
+* 코드 리뷰 시 특별히 참고해야 할 부분이 있다면 언급해주세요.
+* 궁금한 점이나 논의하고 싶은 내용이 있다면 작성해주세요.
+
+## 📚 참고 자료 (선택 사항)
+
+* 관련 자료 링크
diff --git a/.gitignore b/.gitignore
index 870b44ec..8a778213 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,259 +1,89 @@
-# Created by https://www.toptal.com/developers/gitignore/api/java,intellij,maven,kotlin,gradle,windows,macos
-# Edit at https://www.toptal.com/developers/gitignore?templates=java,intellij,maven,kotlin,gradle,windows,macos
+# -------------------- IDEA --------------------
+.idea/
+*.iml
+*.iws
-### Intellij ###
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+# 허용할 IntelliJ 설정
+!.idea/codeStyles/
+!.idea/inspectionProfiles/
-# User-specific stuff
+# IDEA 자동 생성 불필요 파일
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
-.idea/**/shelf
-
-# AWS User-specific
-.idea/**/aws.xml
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
+.idea/**/shelf/
+.idea/**/dataSources*
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
-
-# Gradle
+.idea/httpRequests/
+.idea/caches/
.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# SonarLint plugin
-.idea/sonarlint/
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-### Intellij Patch ###
-# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
-
-# *.iml
-# modules.xml
-# .idea/misc.xml
-# *.ipr
-
-# Sonarlint plugin
-# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/libraries/
.idea/**/sonarlint/
-
-# SonarQube Plugin
-# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
-
-# Markdown Navigator plugin
-# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
-.idea/**/markdown-navigator.xml
-.idea/**/markdown-navigator-enh.xml
-.idea/**/markdown-navigator/
-
-# Cache file creation bug
-# See https://youtrack.jetbrains.com/issue/JBR-2257
-.idea/$CACHE_FILE$
-
-# CodeStream plugin
-# https://plugins.jetbrains.com/plugin/12206-codestream
-.idea/codestream.xml
-
-# Azure Toolkit for IntelliJ plugin
-# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
+.idea/**/markdown-navigator*
+.idea/**/aws.xml
.idea/**/azureSettings.xml
-### Java ###
-# Compiled class file
-*.class
-# Log file
+# -------------------- LOGS / DB --------------------
*.log
+log/
+logs/
+*.db
+*.sqlite
+*.h2.db
+*.mv.db
+*.pgdata/
+
+# -------------------- BUILD --------------------
+# build 전체는 무시하되, 필요한 파일은 다시 포함
+**/build/
+!**/build/resources/main/application.yml
+!**/build/resources/test/http/*.http
+!**/build/generated/**/*.java
+!**/build/tmp/compiled_classes/**/resolvedMainClassName
+!**/build/tmp/**/*.bin
+!src/**/build/
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
+# -------------------- GRADLE / MAVEN --------------------
+.gradle/
+out/
+target/
+*.class
*.jar
*.war
-*.nar
*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-replay_pid*
-
-### Kotlin ###
-# Compiled class file
-
-# Log file
-# BlueJ files
-
-# Mobile Tools for Java (J2ME)
-
-# Package Files #
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+.mvn/
+!gradle-wrapper.jar
+!gradle-wrapper.properties
-### macOS ###
-# General
+# -------------------- SYSTEM --------------------
+# macOS
.DS_Store
.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon
-
-
-# Thumbnails
-._*
-
-# Files that might appear in the root of a volume
-.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
-.VolumeIcon.icns
-.com.apple.timemachine.donotpresent
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
.apdisk
-
-### macOS Patch ###
-# iCloud generated files
*.icloud
-### Maven ###
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-# https://github.com/takari/maven-wrapper#usage-without-binary-jar
-.mvn/wrapper/maven-wrapper.jar
-
-# Eclipse m2e generated files
-# Eclipse Core
-.project
-# JDT-specific (Eclipse Java Development Tools)
-.classpath
-
-### Windows ###
-# Windows thumbnail cache files
+# Windows
Thumbs.db
-Thumbs.db:encryptable
ehthumbs.db
-ehthumbs_vista.db
-
-# Dump file
-*.stackdump
-
-# Folder config file
-[Dd]esktop.ini
-
-# Recycle Bin used on file shares
+Desktop.ini
$RECYCLE.BIN/
-
-# Windows Installer files
-*.cab
-*.msi
-*.msix
-*.msm
-*.msp
-
-# Windows shortcuts
*.lnk
-### Gradle ###
-.gradle
-**/build/
-!src/**/build/
-
-# Ignore Gradle GUI config
-gradle-app.setting
-
-# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
-!gradle-wrapper.jar
-
-# Avoid ignore Gradle wrappper properties
-!gradle-wrapper.properties
-
-# Cache of project
-.gradletasknamecache
-
-# Eclipse Gradle plugin generated files
-# Eclipse Core
-# JDT-specific (Eclipse Java Development Tools)
-
-### Gradle Patch ###
-# Java heap dump
-*.hprof
+# -------------------- SECRETS --------------------
+application-secret.yml
+*.key
+*.pem
+*.crt
+*.pfx
+.env
-# End of https://www.toptal.com/developers/gitignore/api/java,intellij,maven,kotlin,gradle,windows,macos
\ No newline at end of file
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 00000000..5397ff51
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+logistics-delivery
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index 4099c43c..00000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 00000000..fdc392fe
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index fe0b0dab..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 5ab07c06..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/eureka/logistics-delivery.eureka.main.iml b/.idea/modules/eureka/logistics-delivery.eureka.main.iml
deleted file mode 100644
index 421c3736..00000000
--- a/.idea/modules/eureka/logistics-delivery.eureka.main.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/gateway/logistics-delivery.gateway.main.iml b/.idea/modules/gateway/logistics-delivery.gateway.main.iml
deleted file mode 100644
index f96a0520..00000000
--- a/.idea/modules/gateway/logistics-delivery.gateway.main.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7f..35eb1ddf 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..c7b5f942
--- /dev/null
+++ b/README.md
@@ -0,0 +1,168 @@
+Logistics-delivery
+ ===
+
+ # 프로젝트 소개
+**- 각 기능에 따른 개별 애플리케이션을 생성하여 서비스가 독립적으로 존재할 수 있도록 구현한 MSA 기반 물류 관리 및 배송시스템 프로젝트입니다.**
+
+**- 서로다른 애플리케이션 간의 데이터 연동과 기능요청을 경험하며 실무에서 발생할 수 있는 다양한 상황에 대한 협업경험을 쌓는것을 목표로합니다.**
+ ## 핵심기술 목표
+
+### 1. 각 기능별 서비스 분리(MSA 기반 시스템 설계)
+- 기능에 따른 개별 애플리케이션을 생성하여 서비스가 독립적으로 존재할 수 있도록 구현.
+- 한 서비스에서 이슈가 발생하더라도 다른서비스에는 영향을 미치지않아 시스템의 가용성 향상.
+- 서비스 추가에 대한 확장성을 고려한 설계.
+
+### 2.서비스 간 데이터 공유 및 동기화
+- 독립적인 서비스로 구현되어있지만, 서로 정보를 주고받을 수 있도록 서비스간 통신 설계.
+- 트랜젝션 관리를 구현하여 하나라도 실패 시 전체롤백처리.
+- 데이터 일관성을 유지하기위한 로직 구현.
+
+### 3. 인증 및 보안
+- 데이터를 보호하기위해 인증된 사용자만이 접근할 수 있도록 구현.
+- 각 서비스 별 접근가능한 권한을 설정하여 보안을 강화.
+- JWT(Json Web Token) 를 활용한 인증 및 인가체계를 구현하여 서비스의 보안을 유지
+
+### 4. 배포 및 확장성
+- 시스템은 클라우드 기반에서 배포되며, 각 서비스는 필요에 따라 확장이 가능하도록 구현되어야함.
+- 컨테이너화(Docker)를 활용한 시스템 배포 및 관리기능 구현.
+
+ ## 구현 목표
+
+### 1. MSA 기반 아키텍처 & 커뮤니케이션
+- 멀티모듈 프로젝트 구조로 각 기능을 독립적인 마이크로서비스로 분리하여 개발진행.
+- 서비스 내 통신은 REST API를 통해 이루어지며, 다른 서브모듈 간의 통신은 FeignClient 사용.
+- Spring Cloud Eureka를 이용한 서비스 디스커버리 및 Spring Cloud Gateway를 통한 API 라우팅.
+
+### 2. 서비스 확장성 및 유연성
+- 각 마이크로서비스는 수평 확장이 가능하며, 필요한 경우 독립적인 배포 및 유지보수.
+- Layered Architecture로 클린 코드 유지 및 DDD를 통한 도메인 기반 설계.
+
+### 3. 권한관리 및 보안
+- JWT 인증: 사용자 인증 및 권한 관리를 위해 JWT를 사용하며, 각 요청에서 JWT 토큰을 검증하여 인증된 사용자만 접근하도록 처리.
+- GateWay의 WebFluxSecurity: API Gateway에서 WebFluxSecurity를 이용해 JWT 토큰을 검증하고, 권한에 따라 요청에 대한 접근을 인가 처리.
+- AOP를 통한 권한 인가: 서브모듈에서 세부 API별로 AOP를 활용해 권한 인가를 처리, 각 API에 맞는 권한 검사를 자동화
+- 비밀번호 암호화: BCrypt 해시 알고리즘을 사용하여 비밀번호 입력 시 암호화 하여 저장.
+- 데이터 유효성 검사: Spring Validator로 서버 측에서 유효성 검사 진행.
+
+### 4. 사용자 경험 개선
+- 슬랙 API 연동: 슬랙 API와 연동하여 메시지 작성 후 발송 시 실시간으로 전달되도록 구현.
+
+### 5. API 문서화
+- Swagger를 사용하여 API 문서 자동화 지원.
+
+ # 개발 환경 소개
+ ### 개발환경
+
+ - spring boot 3.4.3
+ - Gradle
+ - java 17
+ - Docker
+
+
+ ### 기술스택
+ | 분류 | 상세 |
+ |---------------|:-----------------------------------------------------------------------------------------------------------------|
+ | Backend | Java & Spring Boot
Spring Cloud (Eureka)
Spring Gateway
JPA
Feign Client
Spring AOP
QueryDSL |
+ | Database | PostgreSQL |
+ | Infra | Docker, Docker Compose |
+| Auth | Spring Security
JWT |
+ | Documentation | Swagger |
+ | Test | Spring Boot Test
JUnit |
+
+ # 프로젝트 실행 방법
+
+ - Docker Desktop 필요(데이터베이스 연동, 서비스 실행)
+```sh
+# 프로젝트 클론
+git clone https://github.com/logistics-delivery/Back-end.git
+cd Back-end
+
+# 빌드 및 실행
+# 1. 데이터베이스만 docker container로 실행한 뒤, 각 애플리케이션을 개별실행
+$ docker compose up -d postgres
+
+# 2. 데이터베이스와 개별 서비스를 함께 docker container로 업로드하여 실행하는경우
+$ docker compose up -d
+
+```
+
+ # 설계 산출물
+
+
+ - [도메인 다이어그램](https://github.com/logistics-delivery/Back-end/wiki/%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8-&-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%8B%B4%EB%8B%B9%EC%9E%90-%EB%AA%A9%EB%A1%9D)
+ - [테이블 설계서](https://github.com/logistics-delivery/Back-end/wiki/%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%AA%85%EC%84%B8%EC%84%9C)
+ - [ERD](https://github.com/logistics-delivery/Back-end/wiki/ERD-%EB%AA%85%EC%84%B8%EC%84%9C)
+ - [API 명세서](https://github.com/logistics-delivery/Back-end/wiki/API-%EB%AA%85%EC%84%B8%EC%84%9C)
+ - [인프라 설계서](https://github.com/logistics-delivery/Back-end/wiki/%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%84%A4%EA%B3%84%EC%84%9C)
+ - [Conventions] : 우리 조의 개발 규칙
+ - [[Commit Message Conventions]]
+ - [[Java Code Style]]
+ - [[Git-flow]]
+ - [[Package Structure]]
+
+
+ # 개발 산출물
+
+
+ **트러블 슈팅**
+
+- [게이트웨이에서의 인가처리오류](https://github.com/logistics-delivery/Back-end/wiki/%EA%B2%8C%EC%9D%B4%ED%8A%B8%EC%9B%A8%EC%9D%B4%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%B8%EA%B0%80%EC%B2%98%EB%A6%AC%28ReactiveSecurityContextHolder%29)
+- [QueryDSL Q파일 문제 트러블슈팅](https://github.com/logistics-delivery/Back-end/wiki/QueryDSL-Q%ED%8C%8C%EC%9D%BC-%EB%AC%B8%EC%A0%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85)
+- [도메인형 3계층 vs 4계층 구조 설계](https://github.com/logistics-delivery/Back-end/wiki/%EB%8F%84%EB%A9%94%EC%9D%B8%ED%98%95-3%EA%B3%84%EC%B8%B5-vs-4%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0-%EC%84%A4%EA%B3%84)
+
+**공통 관심 사항**
+
+ - [AOP @Rolecheck 사용방법](https://github.com/logistics-delivery/Back-end/wiki/%EA%B3%B5%ED%86%B5%EB%AA%A8%EB%93%88-AOP-@RoleCheck-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95)
+ - [AuditorAwareImpl 구현 및 @SQLRestriction 설정](https://github.com/logistics-delivery/Back-end/wiki/AuditorAwareImpl-%EA%B5%AC%ED%98%84-%EB%B0%8F-@SQLRestriction-%EC%84%A4%EC%A0%95)
+- [Git 시크릿 키 보호 방법](https://github.com/logistics-delivery/Back-end/wiki/Git-%EC%8B%9C%ED%81%AC%EB%A6%BF-%ED%82%A4-%EB%B3%B4%ED%98%B8-%EB%B0%A9%EB%B2%95)
+- [Docker 명령어](https://github.com/logistics-delivery/Back-end/wiki/docker-%EB%AA%85%EB%A0%B9%EC%96%B4)
+- [설계 대비 API 구현률](https://github.com/logistics-delivery/Back-end/wiki/%EC%84%A4%EA%B3%84%EB%8C%80%EB%B9%84-API-%EA%B5%AC%ED%98%84%EB%A5%A0)
+
+
+ # 시스템을 발전 시키기 위해 더 해본다면?
+- ai 자동생성 배송메세지 구현
+- 매일 6시 슬랙 알림 발송기능 구현
+- 슬랙 이름이 Null값인 관리자 권한 사용자의 다른 사용자와의 테이블 분리
+- Zipkin을 통한 분산 추적 기능 구현
+- 비동기 이벤트 통신이 필요한 서비스 간의 호출을 Feign client 가 아닌 메세징 시스템으로 전환
+- 페이징 처리 공통모듈에서 관리
+- 예외처리 도메인 책임으로 분리
+
+
+ # 협업 시 우리조가 잘한 것들
+### 기술적 측면
+ - **서비스 디스커버리 도입**
+ - Eureka 를 활용해 서비스의 동적 등록과 검색이 가능해짐.
+- **API 게이트웨이 활용**
+ - Gateway를 통해 JWT 토큰 검증 및 권한정보 인가처리를 진행하여 보안이 강화됨.
+- **DDD 설계를 위한 4계층 구조 활용**
+ - MSA환경에서 4계층 구조를 사용함으로써 인프라스트럭처 코드와 순수한 자바 도메인 코드를 명확히 분리함.
+ - 이를 통해 서로 다른 서비스 간에 기술적인 의존도를 줄일 수 있게됨.
+- **AOP를 도입**
+ - 서비스간 권한 체크를 하여 코드 중복을 줄이고 공통 관심사를 효율적으로 처리할 수 있게함.
+
+### 소통적 측면
+- **Pull Request 를 통한 코드 리뷰**
+ - dev에 두 명 이상의 승인이 이루어지게 Pull Request를 열어놓아 코드 변경 사항에 대해 팀원들과 공유하고 검토하는 과정을 거쳐 코드 품질을 높임.
+- **GitHub Issues를 통한 이슈 관리**
+ - 이슈 트래킹을 통해 진행사항을 파악하는데 도움이 됨.
+- **데일리 스크럼을 통한 원활한 커뮤니케이션**
+ - 매일 스크럼을 진행하여 모든 팀원의 진행 상황을 공유하고, 이슈를 공유 및 논의 함으로써 신속히 처리할 수 있었음.
+
+ # 협업 간 발생한 문제와 해결 방안
+- MSA 방식의 프로젝트를 처음 진행하며 레이어드 계층 구성에 대한 어려움을 겪음
+ → MSA 에서의 4계층 구조의 필요성을 팀원들과 공유하고, 도입하여 각 계층의 역할을 명확히 정의하여 코드의 가독성과 유지보수성을 높임.
+
+- 개발 초기 서버간 통신에 대한 규약을 정하지 않고, 각자 기능 구현에 집중하여 서비스 간 통신시 데이터 포맷 및 명세 불일치 문제 발생
+ → 연관된 서비스 담당자들 간의 주기적인 커뮤니케이션을 강화하고, 공통 API 명세서를 보완하여 협업 효율을 높임
+
+ # 팀원 소개 및 담당역할
+| **역할** | **담당자** | **세부 업무** |
+|-------------------------|:---------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **인증인가,
사용자API** | 신다은
(팀장) | - 회원가입,로그인 등의 사용자 정보CRUD 구현
- 로그인 성공 시 JWT 토큰을 생성하여 사용자에게 전달되도록 함
- JWT 토큰을 사용하여 회원정보를 인증, 내부payload값을 추출하여 서브모듈에서 사용가능하도록 구현
- 인증된 정보를 바탕으로 사용자의 권한이 요청 url에 접근가능한지 gateway에서 우선적으로 인가처리를 할 수 있도록 구현
- gateway에서 기본 인가 처리 후, AOP와 Custom Annotation을 사용하여 각 API의 세부 기능별로 권한을 추가로 체크하는 접근 인가 기능을 구현. |
+|**슬랙API**|신다은
(팀장) | - 슬랙 메세지 관리 CRUD 구현
- 슬랙 외부 API를 연동하여 메세지 발송 시 실제 슬랙 사이트로 알림메세지가 전송되도록 함.
- Base Entity를 사용한 생성,수정,삭제 기록 저장 및 SoftDelete 처리
- QueryDSL을 이용한 슬랙 메세지 검색기능 구현 |
+|**허브API**|이소현
(테크리드) | - Naver API 를 이용한 주소→위경도 변환 및 허브 생성 기능 구현
- 허브 조회 및 수정, 삭제 기능 구현
- QueryDSL을 활용한 허브 검색 기능 구현
- 위경도 계산을 통한 허브 경로 생성 구현
- 허브 경로 조회 및 삭제 구현
- 다익스트라 알고리즘을 이용한 최단 경로 기반 체크포인트 생성 구현
- 허브 경로 체크포인트 조회 및 삭제 기능 구현
- Feign client를 이용한 허브 내 입출고 관리 기능 구현
- 공통 모듈 : BaseEntity 구현
- 공통 모듈 : 전역 예외 처리(Exception Handling) 구현
- 배송 서비스 내에 허브 입출고 정보를 수신 및 저장하는 기능 구현
- 모든 도메인이 직접 구현한 BaseEntity를 상속받아 생성일, 수정일, 삭제일을 자동으로 기록하며, Soft Delete를 지원하도록 설계 |
+|**상품API**|서진영
(테크리드) | - 상품 CRUD 및 QueryDSL 기반 검색 기능 구현
- Swagger 설정 및 Auditor Aware 등 공통 모듈 구축
- FeignClient를 통한 마이크로서비스 간 통신 처리
- DDD 기반 4계층 아키텍처 설계 및 적용
- 시스템 흐름에 대한 플로우 차트 작성 및 공유
- 프로젝트 내 미숙한 기술에 대한 문서화 및 설명을 통해 팀 기술 이해도 향상 기여 |
+|**배송API**|권길남 | - 배송, 배송 로그, 배송 담당자 CRUD 구현
- 배송 담당자 배정 알고리즘 구현
- QueryDSL을 사용한 배송정보 검색기능 구현
- Docker 개발 환경세팅 |
+|**업체API**|원지윤 | - Company-service CRUD 개발 및 Spring Boot 기반 4계층 아키텍처 적용
- QueryDSL 기반 동적 검색 조건 및 페이징 기능 구현
- HTTP API 테스트, 도메인 및 서비스 계층 테스트 코드 작성 |
+|**주문API**|이용재 | - 주문 CRUD 구현
- 주문 생성 시 Product 서비스에 재고 차감 요청 기능 연동 (FeignClient 사용)
- 주문 생성 시 Shipping 서비스에 배송 생성 요청 기능 연동 (FeignClient 사용)
- Slack 도메인 연동을 위한 주문 + 배송 정보 응답 API 제공
- QueryDSL을 활용한 주문 검색 기능 구현 (주문명 + 상태 검색) |
diff --git a/api-http/hub/create-hub-route-checkpoints.http b/api-http/hub/create-hub-route-checkpoints.http
new file mode 100644
index 00000000..e7e64196
--- /dev/null
+++ b/api-http/hub/create-hub-route-checkpoints.http
@@ -0,0 +1,31 @@
+
+@seoul_hub = d25f4291-d134-4f73-885a-0eb188a72649
+@busan_hub = 12bca245-3e19-400d-a421-6e3e42c56b1e
+@daejeon_hub = 9d23ac68-441f-4122-ac91-98c99896fcc4
+@Gyeongsangbukdo_hub = ec87005e-5160-4515-90ce-dcbbfb392678
+@host = http://localhost:8080
+
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+
+### 👩🏻💻 Hub Route Checkpoint 테이블 더미데이터에 필요한 코드 -> 중복 있는 상태
+###
+
+### 서울 -> 부산
+POST{{host}}/api/v1/hubs/hub-routes/{{seoul_hub}}/{{busan_hub}}/path
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzk2MzcsImV4cCI6MTc0Mjg0MzIzN30.QKV6EjSQn5-E9ok_o_teaxK12cIhF3OUsnrPKim4WY16BvO9H97lLIgZru8o3PCKAjoqXHsQhntk3sz8mUU0Cg
+Content-Type: application/json
+
+### 대전 -> 경상북도
+POST {{host}}/api/v1/hubs/hub-routes/{{daejeon_hub}}/{{Gyeongsangbukdo_hub}}/path
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzk2MzcsImV4cCI6MTc0Mjg0MzIzN30.QKV6EjSQn5-E9ok_o_teaxK12cIhF3OUsnrPKim4WY16BvO9H97lLIgZru8o3PCKAjoqXHsQhntk3sz8mUU0Cg
+Content-Type: application/json
+
diff --git a/api-http/hub/create-hub-routes.http b/api-http/hub/create-hub-routes.http
new file mode 100644
index 00000000..7a172652
--- /dev/null
+++ b/api-http/hub/create-hub-routes.http
@@ -0,0 +1,201 @@
+@host = http://localhost:8080
+
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+
+### 👩🏻💻 Hub Route 테이블의 더미데이터에 필요한 코드
+###
+
+### 경기 남부 → 경기 북부
+POST http://localhost:8080/api/v1/hubs/hub-routes/8b3f2668-49cb-4885-8855-e20fd4bf10aa/d2587e4e-b2d4-4c9b-b2fd-37f035596a0d/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경기 북부 → 경기 남부
+POST {{host}}/api/v1/hubs/hub-routes/d2587e4e-b2d4-4c9b-b2fd-37f035596a0d/8b3f2668-49cb-4885-8855-e20fd4bf10aa/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경기 남부 → 서울특별시
+POST {{host}}/api/v1/hubs/hub-routes/8b3f2668-49cb-4885-8855-e20fd4bf10aa/d25f4291-d134-4f73-885a-0eb188a72649/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 서울특별시 → 경기 남부
+POST {{host}}/api/v1/hubs/hub-routes/d25f4291-d134-4f73-885a-0eb188a72649/8b3f2668-49cb-4885-8855-e20fd4bf10aa/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경기 남부 → 인천광역시
+POST {{host}}/api/v1/hubs/hub-routes/8b3f2668-49cb-4885-8855-e20fd4bf10aa/4410d46d-27a2-4267-824b-4305a6731624/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 인천광역시 → 경기 남부
+POST {{host}}/api/v1/hubs/hub-routes/4410d46d-27a2-4267-824b-4305a6731624/8b3f2668-49cb-4885-8855-e20fd4bf10aa/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경기 남부 → 강원특별자치도
+POST {{host}}/api/v1/hubs/hub-routes/8b3f2668-49cb-4885-8855-e20fd4bf10aa/3e734ae7-1030-45c8-b19d-b7ee7601cf2f/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 강원특별자치도 → 경기 남부
+POST {{host}}/api/v1/hubs/hub-routes/3e734ae7-1030-45c8-b19d-b7ee7601cf2f/8b3f2668-49cb-4885-8855-e20fd4bf10aa/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경기 남부 → 경상북도
+POST {{host}}/api/v1/hubs/hub-routes/8b3f2668-49cb-4885-8855-e20fd4bf10aa/ec87005e-5160-4515-90ce-dcbbfb392678/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경상북도 → 경기 남부
+POST {{host}}/api/v1/hubs/hub-routes/ec87005e-5160-4515-90ce-dcbbfb392678/8b3f2668-49cb-4885-8855-e20fd4bf10aa/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+
+
+
+
+
+### 대전광역시 → 충청남도
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/a8d36f04-6312-4acb-aac1-1b5e82e0995d/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 충청남도 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/a8d36f04-6312-4acb-aac1-1b5e82e0995d/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대전광역시 → 충청북도
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/2cddf4cb-07fc-487a-bceb-9613b884447f/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 충청북도 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/2cddf4cb-07fc-487a-bceb-9613b884447f/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대전광역시 → 세종특별자치시
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/0b1fa666-ebaa-4da8-a880-3bd40181ec0c/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 세종특별자치시 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/0b1fa666-ebaa-4da8-a880-3bd40181ec0c/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대전광역시 → 전북특별자치도
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/eb42b568-480c-4515-ac25-6d2dc6532110/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 전북특별자치도 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/eb42b568-480c-4515-ac25-6d2dc6532110/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대전광역시 → 광주광역시
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/b38d1ab0-2d3e-424c-b39d-814f10b294a5/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 광주광역시 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/b38d1ab0-2d3e-424c-b39d-814f10b294a5/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대전광역시 → 전라남도
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/972869be-9b58-4db2-9477-67a986c42d5b/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 전라남도 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/972869be-9b58-4db2-9477-67a986c42d5b/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대전광역시 → 경기 남부
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/8b3f2668-49cb-4885-8855-e20fd4bf10aa/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경기 남부 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/8b3f2668-49cb-4885-8855-e20fd4bf10aa/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대전광역시 → 대구광역시
+POST {{host}}/api/v1/hubs/hub-routes/9d23ac68-441f-4122-ac91-98c99896fcc4/73c63196-a79c-4b05-912e-d3bbd8244cda/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대구광역시 → 대전광역시
+POST {{host}}/api/v1/hubs/hub-routes/73c63196-a79c-4b05-912e-d3bbd8244cda/9d23ac68-441f-4122-ac91-98c99896fcc4/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대구광역시 → 경상북도
+POST {{host}}/api/v1/hubs/hub-routes/73c63196-a79c-4b05-912e-d3bbd8244cda/ec87005e-5160-4515-90ce-dcbbfb392678/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경상북도 → 대구광역시
+POST {{host}}/api/v1/hubs/hub-routes/ec87005e-5160-4515-90ce-dcbbfb392678/73c63196-a79c-4b05-912e-d3bbd8244cda/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대구광역시 → 경상남도
+POST {{host}}/api/v1/hubs/hub-routes/73c63196-a79c-4b05-912e-d3bbd8244cda/6ad0e2b5-6a15-437b-be2d-db5329b6cff6/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경상남도 → 대구광역시
+POST {{host}}/api/v1/hubs/hub-routes/6ad0e2b5-6a15-437b-be2d-db5329b6cff6/73c63196-a79c-4b05-912e-d3bbd8244cda/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대구광역시 → 부산광역시
+POST {{host}}/api/v1/hubs/hub-routes/73c63196-a79c-4b05-912e-d3bbd8244cda/12bca245-3e19-400d-a421-6e3e42c56b1e/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 부산광역시 → 대구광역시
+POST {{host}}/api/v1/hubs/hub-routes/12bca245-3e19-400d-a421-6e3e42c56b1e/73c63196-a79c-4b05-912e-d3bbd8244cda/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대구광역시 → 울산광역시
+POST {{host}}/api/v1/hubs/hub-routes/73c63196-a79c-4b05-912e-d3bbd8244cda/b97f9c00-f8bb-4ff4-99d0-624da28a2be7/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 울산광역시 → 대구광역시
+POST {{host}}/api/v1/hubs/hub-routes/b97f9c00-f8bb-4ff4-99d0-624da28a2be7/73c63196-a79c-4b05-912e-d3bbd8244cda/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 대구광역시 → 경기 남부
+POST {{host}}/api/v1/hubs/hub-routes/73c63196-a79c-4b05-912e-d3bbd8244cda/8b3f2668-49cb-4885-8855-e20fd4bf10aa/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
+### 경기 남부 → 대구광역시
+POST {{host}}/api/v1/hubs/hub-routes/8b3f2668-49cb-4885-8855-e20fd4bf10aa/73c63196-a79c-4b05-912e-d3bbd8244cda/direct
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI4Mzg2MDEsImV4cCI6MTc0Mjg0MjIwMX0.nz_C1XrfRH0FYkP75wiFCzWdMn10TmDs8pUThBvAHaVwotpWtOZmT_vqHNWwIvGAqA1QCvbiSN4ib_E1E3Iy7w
+Content-Type: application/json
+
diff --git a/api-http/hub/create-hub.http b/api-http/hub/create-hub.http
new file mode 100644
index 00000000..e5b30877
--- /dev/null
+++ b/api-http/hub/create-hub.http
@@ -0,0 +1,185 @@
+
+@host = http://localhost:8080
+
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+
+### 👩🏻💻 Hub 테이블 더미데이터에 필요한 코드
+###
+### - 서울특별시 센터 : 서울특별시 송파구 송파대로 55
+POST http://localhost:8080/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "서울특별시 센터",
+ "address": "서울특별시 송파구 송파대로 55"
+}
+
+### - 경기 북부 센터 : 경기도 고양시 덕양구 권율대로 570
+POST http://localhost:8080/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "경기 북부 센터",
+ "address": "경기도 고양시 덕양구 권율대로 570"
+}
+
+### - 경기 남부 센터 : 경기도 이천시 덕평로 257-21
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "경기 남부 센터",
+ "address": "경기도 이천시 덕평로 257-21"
+}
+
+### - 부산광역시 센터 : 부산 동구 중앙대로 206
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "부산광역시 센터",
+ "address": "부산 동구 중앙대로 206"
+}
+
+### - 대구광역시 센터 : 대구 북구 태평로 161
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "대구광역시 센터",
+ "address": "대구 북구 태평로 161"
+}
+
+### - 인천광역시 센터 : 인천 남동구 정각로 29
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "인천광역시 센터",
+ "address": "인천 남동구 정각로 29"
+}
+
+### - 광주광역시 센터 : 광주 서구 내방로 111
+POST{{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "광주광역시 센터",
+ "address": "광주 서구 내방로 111"
+}
+
+### - 대전광역시 센터 : 대전 서구 둔산로 100
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "대전광역시 센터",
+ "address": "대전 서구 둔산로 100"
+}
+
+### - 울산광역시 센터 : 울산 남구 중앙로 201
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "울산광역시 센터",
+ "address": "울산 남구 중앙로 201"
+}
+
+### - 세종특별자치시 센터 : 세종특별자치시 한누리대로 2130
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "세종특별자치시 센터",
+ "address": "세종특별자치시 한누리대로 2130"
+}
+
+### - 강원특별자치도 센터 : 강원특별자치도 춘천시 중앙로 1
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "강원특별자치도 센터",
+ "address": "강원특별자치도 춘천시 중앙로 1"
+}
+
+### - 충청북도 센터 : 충북 청주시 상당구 상당로 82
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "충청북도 센터",
+ "address": "충북 청주시 상당구 상당로 82"
+}
+
+### - 충청남도 센터 : 충남 홍성군 홍북읍 충남대로 21
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "충청남도 센터",
+ "address": "충남 홍성군 홍북읍 충남대로 21"
+}
+
+### - 전북특별자치도 센터 : 전북특별자치도 전주시 완산구 효자로 225
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "전북특별자치도 센터",
+ "address": "전북특별자치도 전주시 완산구 효자로 225"
+}
+
+### - 전라남도 센터 : 전남 무안군 삼향읍 오룡길 1
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "전라남도 센터",
+ "address": "전남 무안군 삼향읍 오룡길 1"
+}
+
+### - 경상북도 센터 : 경북 안동시 풍천면 도청대로 455
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "경상북도 센터",
+ "address": "경북 안동시 풍천면 도청대로 455"
+}
+
+### - 경상남도 센터 : 경남 창원시 의창구 중앙대로 300
+POST {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "경상남도 센터",
+ "address": "경남 창원시 의창구 중앙대로 300"
+}
\ No newline at end of file
diff --git a/api-http/hub/hub-route-total-test.http b/api-http/hub/hub-route-total-test.http
new file mode 100644
index 00000000..bfbaab1e
--- /dev/null
+++ b/api-http/hub/hub-route-total-test.http
@@ -0,0 +1,39 @@
+
+@hub_route_id = e0ea93fb-8d7c-42c2-ba06-684646745c1e
+@seoul_hub = d25f4291-d134-4f73-885a-0eb188a72649
+@busan_hub = 12bca245-3e19-400d-a421-6e3e42c56b1e
+@south_gyeonggi_hub = 8b3f2668-49cb-4885-8855-e20fd4bf10aa
+@host = http://localhost:8080
+
+
+### 🔹 전체 허브 경로 목록 조회 (페이징)
+GET {{host}}/api/v1/hubs/hub-routes
+Content-Type: application/json
+Authorization: {{access_token}}
+
+
+### 🔹 특정 경로 ID 조회
+GET {{host}}/api/v1/hubs/hub-routes/{{hub_route_id}}
+Content-Type: application/json
+Authorization: {{access_token}}
+
+### 🔹 특정 허브 간 다이렉트 경로 조회
+GET {{host}}/api/v1/hubs/hub-routes/{{seoul_hub}}/{{south_gyeonggi_hub}}/direct
+Content-Type: application/json
+Authorization: {{access_token}}
+
+### 🔹 허브 간 다이렉트 경로 생성 -> ✨ create-hub-routes.http 에서 테스트 및 더미데이터 생성
+
+
+### 🔹 허브 간 경로 삭제
+DELETE {{host}}/api/v1/hubs/hub-routes/{{hub_route_id}}
+Content-Type: application/json
+Authorization: {{access_token}}
+
+### 🔹 허브 간 최단 경로 생성 (Path 기반) -> ✨create-hub-route-checkpoint.http 에서 테스트 및 더미데이터 생성
+
+
+### 🔹 허브 간 최단 경로 조회 (Path 기반) *
+GET {{host}}/api/v1/hubs/hub-routes/{{seoul_hub}}/{{busan_hub}}/path
+Content-Type: application/json
+Authorization: {{access_token}}
diff --git a/api-http/hub/hub-shipping-feignclient.http b/api-http/hub/hub-shipping-feignclient.http
new file mode 100644
index 00000000..8db3e66d
--- /dev/null
+++ b/api-http/hub/hub-shipping-feignclient.http
@@ -0,0 +1,18 @@
+### 내부 로직에 feign client 호출 존재 : Shipping-service 함께 실행중이여야 가능
+
+@hub_id =
+@shipping_id =
+@host = http://localhost:8080
+
+
+### Hub - 상품 입고 처리
+POST {{host}}/api/v1/hubs/hub-shipping-scan/{{hub_id}}/{{shipping_id}}/inbound-log
+Content-Type: application/json
+Authorization: {{access_token}}
+
+###
+
+### Hub - 상품 출고 처리
+POST {{host}}/api/v1/hubs/hub-shipping-scan/{{hub_id}}/{{shipping_id}}/outbound-log
+Content-Type: application/json
+Authorization: {{access_token}}
diff --git a/api-http/hub/hub-total-test.http b/api-http/hub/hub-total-test.http
new file mode 100644
index 00000000..29d8e1d4
--- /dev/null
+++ b/api-http/hub/hub-total-test.http
@@ -0,0 +1,32 @@
+
+@hub_id = d25f4291-d134-4f73-885a-0eb188a72649
+@host = http://localhost:8080
+
+
+### 🔹 허브 목록 조회 (페이징)
+GET {{host}}/api/v1/hubs
+Content-Type: application/json
+Authorization: {{access_token}}
+
+### 🔹 특정 허브 조회 (GET, UUID 사용)
+GET {{host}}/api/v1/hubs/{{hub_id}}
+Content-Type: application/json
+Authorization: {{access_token}}
+
+### 🔹 허브 생성 (POST) -> ✨ create-hub.http 에서 테스트 및 더미데이터 생성
+
+### 🔹 허브 검색 (해당 단어 포함하는 모든 주소, 이름 검색 가능)
+GET {{host}}/api/v1/hubs/search?address=서울특별시 송파구
+Content-Type: application/json
+Authorization: {{access_token}}
+
+### 🔹 허브 업데이트 (PUT, UUID 사용)
+PUT {{host}}/api/v1/hubs/{{hub_id}}?address=충남 홍성군 홍북읍 충남대로 21
+Content-Type: application/json
+Authorization: {{access_token}}
+
+
+### 🔹 허브 삭제 (DELETE, UUID 사용)
+DELETE {{host}}/api/v1/hubs/{{hub_id}}
+Content-Type: application/json
+Authorization: {{access_token}}
diff --git a/api-http/shipping/http-request.http b/api-http/shipping/http-request.http
new file mode 100644
index 00000000..9ce8f92e
--- /dev/null
+++ b/api-http/shipping/http-request.http
@@ -0,0 +1,15 @@
+###
+
+POST http://localhost:8088/api/v1/shippings
+Content-Type: application/json
+Authorization: {{access_token}}
+X-User-Id: 12345
+
+{
+ "orderId": "550e8400-e29b-41d4-a716-446655440000",
+ "shippingAddress": "서울특별시 강남구 테헤란로 427",
+ "receiverName": "홍길동",
+ "status": "PENDING"
+}
+
+###
\ No newline at end of file
diff --git a/api-http/shipping/shipping-service.http b/api-http/shipping/shipping-service.http
new file mode 100644
index 00000000..59be16be
--- /dev/null
+++ b/api-http/shipping/shipping-service.http
@@ -0,0 +1,103 @@
+POST http://localhost:8080/api/v1/users/sign-up
+Content-Type: application/json
+
+{
+ "username":"asdf123",
+ "password":"asdfgA12@",
+ "email":"asdf@naver.com",
+ "slackName": "asdggg",
+ "tokenValue": "mgbE4vogtrMGufz6PXkQNTV-KZtU4-Mz7_wcKf7r40kKTu8z4BD9l_kacdd4MzU3pQV6y3LB-yrmMvvXFKep2Q"
+}
+
+
+
+###
+
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+
+###
+GET http://localhost:8080/api/v1/users
+Authorization: {{access_token}}
+
+###
+POST http://localhost:8080/api/v1/shippings
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "orderId": "550e8400-e29b-41d4-a716-446655440000",
+ "shippingAddress": "서울특별시 강남구 테헤란로 427",
+ "receiverName": "홍길동",
+ "status": "PENDING"
+}
+
+
+###
+
+GET http://localhost:8080/api/v1/shipping/9ae52019-9b37-4bc0-b171-96da397337f3
+Authorization: {{access_token}}
+
+###
+GET http://localhost:8080/api/v1/shippings
+Authorization: {{access_token}}
+
+###
+PATCH http://localhost:8080/api/v1/shippings/f271488e-a691-42c6-ac21-e4f85f5af6a7
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "shipping": {
+ "orderId": "e0a8018e-7ecf-11ee-b962-0242ac120002",
+ "shippingAddress": "서울시 강남구 역삼동 123",
+ "receiverName": "홍이",
+ "status": "PENDING"
+ },
+ "routeLog": {
+ "startHubId": "11111111-aaaa-bbbb-cccc-111111111111",
+ "endHubId": "22222222-aaaa-bbbb-cccc-222222222222",
+ "sequence": 1,
+ "estimatedDistance": 12.34,
+ "estimatedTime": 25,
+ "actualDistance": 3.0,
+ "actualTime": 2,
+ "shippingManagerId": "d3e4e889-7134-4db7-bcf6-7cb8b527a1c1"
+ }
+}
+
+###
+
+DELETE http://localhost:8080/api/v1/shippings/cbc196b9-28e4-4d59-99ef-6191e753a38e
+Authorization: {{access_token}}
+###
+
+GET http://localhost:8080/api/v1/shippings/cbc196b9-28e4-4d59-99ef-6191e753a38e/286e62b7-e1e0-4474-89ba-17418508d1eb
+Authorization: {{access_token}}
+
+###
+GET http://localhost:8080/api/v1/shippings/log
+Authorization: {{access_token}}
+
+###
+POST http://localhost:8080/api/shipping-managers
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "shippingManagerId": "2f20c1b6-d3cb-4b1d-9c24-23b5d6e5b9af",
+ "managerType": "CARRIER",
+ "isActive": true,
+ "count": 0
+}
+
+
+
diff --git a/api-http/slack/slack-service.http b/api-http/slack/slack-service.http
new file mode 100644
index 00000000..ebac9453
--- /dev/null
+++ b/api-http/slack/slack-service.http
@@ -0,0 +1,58 @@
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+###
+
+POST http://localhost:8080/api/v1/slacks/create
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "message":"반가워요!",
+ "receiverId":3
+}
+
+###
+
+POST http://localhost:8080/api/v1/slacks/send/9ae52019-9b37-4bc0-b171-96da397337f3
+Authorization: {{access_token}}
+
+####
+
+###
+
+GET http://localhost:8080/api/v1/slacks/9ae52019-9b37-4bc0-b171-96da397337f3
+Authorization: {{access_token}}
+
+####
+
+POST http://localhost:8080/api/v1/slacks/search
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "receiverId": 3
+}
+
+###
+
+PUT http://localhost:8080/api/v1/slacks/modify/b08bd42a-6b0e-497d-831f-a9f5b4fa63db
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "message": "변경메세지",
+ "receiverId": 1
+}
+
+###
+
+DELETE http://localhost:8080/api/v1/slacks/b08bd42a-6b0e-497d-831f-a9f5b4fa63db
+Authorization: {{access_token}}
\ No newline at end of file
diff --git a/api-http/stock/stock.http b/api-http/stock/stock.http
new file mode 100644
index 00000000..7d08bed0
--- /dev/null
+++ b/api-http/stock/stock.http
@@ -0,0 +1,47 @@
+### 유저 로그인
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+
+
+### 재고 생성
+POST http://localhost:8080/api/v1/stocks
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "productId": "ddf9aa8c-1e24-4a08-b1c3-5dfe14ec46a8",
+ "hubId": "123e4567-e89b-12d3-a456-426614174000",
+ "quantity": 50
+}
+
+
+
+### 재고 감소
+PUT http://localhost:8080/api/v1/stocks/ddf9aa8c-1e24-4a08-b1c3-5dfe14ec46a8/decrease
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "hubId": "123e4567-e89b-12d3-a456-426614174000",
+ "quantity": 20
+}
+
+
+
+### 재고 증가
+PUT http://localhost:8080/api/v1/stocks/ddf9aa8c-1e24-4a08-b1c3-5dfe14ec46a8/increase
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "hubId": "123e4567-e89b-12d3-a456-426614174000",
+ "quantity": 50
+}
\ No newline at end of file
diff --git a/api-http/user/user-service.http b/api-http/user/user-service.http
new file mode 100644
index 00000000..f4206cb3
--- /dev/null
+++ b/api-http/user/user-service.http
@@ -0,0 +1,45 @@
+POST http://localhost:8080/api/v1/users/sign-up
+Content-Type: application/json
+
+{
+ "username":"asdf123",
+ "password":"asdfgA12@",
+ "email":"asdf@naver.com",
+ "slackName": "asdggg",
+ "tokenValue": "mgbE4vogtrMGufz6PXkQNTV-KZtU4-Mz7_wcKf7r40kKTu8z4BD9l_kacdd4MzU3pQV6y3LB-yrmMvvXFKep2Q"
+}
+
+###
+
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+###
+
+GET http://localhost:8080/api/v1/users
+Authorization: {{access_token}}
+
+###
+
+PUT http://localhost:8080/api/v1/users/update
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "username": "afdsjhsd24",
+ "password": "asdfgA12gg@",
+ "email":"asdfg3@naver.com",
+ "slackName": "asdgggh"
+}
+
+###
+
+DELETE http://localhost:8080/api/v1/users
+Authorization: {{access_token}}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index c89de2c1..a1df3ce0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,11 +37,13 @@ subprojects {
apply plugin: 'io.spring.dependency-management'
dependencies {
- implementation 'org.springframework.boot:spring-boot-starter-web'
+
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //jpa 추가
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
}
tasks.named('test') {
diff --git a/common-module/build.gradle b/common-module/build.gradle
new file mode 100644
index 00000000..f51b4bb1
--- /dev/null
+++ b/common-module/build.gradle
@@ -0,0 +1,27 @@
+dependencies {
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ compileOnly 'org.springframework:spring-tx:5.3.30'
+ // QueryDSL : (hub-service에서 필요)
+ implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
+ annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
+ annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
+ annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
+ implementation 'org.springframework.boot:spring-boot-starter-aop'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+}
+
+// ✅ QueryDSL 자동 생성 디렉토리 설정
+tasks.withType(JavaCompile).configureEach {
+ options.annotationProcessorGeneratedSourcesDirectory = file("$buildDir/generated/querydsl")
+}
+
+// ✅ 자동 생성된 QueryDSL 클래스 경로 추가
+sourceSets {
+ main {
+ java {
+ srcDirs += "$buildDir/generated/querydsl"
+ }
+ }
+}
+
+
diff --git a/common-module/src/main/java/com/sparta/commonmodule/aop/AuthorizationAspect.java b/common-module/src/main/java/com/sparta/commonmodule/aop/AuthorizationAspect.java
new file mode 100644
index 00000000..c2ef605f
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/aop/AuthorizationAspect.java
@@ -0,0 +1,39 @@
+package com.sparta.commonmodule.aop;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+@Aspect
+@Component
+@Slf4j
+public class AuthorizationAspect {
+
+
+ @Before("@annotation(roleCheck)") // @RoleCheck 어노테이션을 가진 메서드에 AOP 적용
+ public void checkPermission(JoinPoint joinPoint, RoleCheck roleCheck) throws Throwable {
+ // 요구되는 권한 (예: ROLE_MASTER)
+ String[] requiredRole = roleCheck.value().split(",");
+
+ ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ HttpServletRequest request = requestAttributes.getRequest();
+ String currentUserRole = request.getHeader("role");//현재 로그인한 사용자의 권한
+ boolean check = false;
+ for (String requiredRoleName : requiredRole) {
+ if (requiredRoleName.equals(currentUserRole)) {
+ check = true;
+ }
+ }
+ if (!check) {
+ log.error("CurrentUserRole [{}] is not matched requiredRole [{}]", currentUserRole, requiredRole);
+ throw new RuntimeException("권한이 다릅니다.");
+ }
+
+ }
+
+}
diff --git a/common-module/src/main/java/com/sparta/commonmodule/aop/RoleCheck.java b/common-module/src/main/java/com/sparta/commonmodule/aop/RoleCheck.java
new file mode 100644
index 00000000..de3a153c
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/aop/RoleCheck.java
@@ -0,0 +1,12 @@
+package com.sparta.commonmodule.aop;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD) //생성한 어노테이션을 사용할 수 있는곳(적용대상)
+@Retention(RetentionPolicy.RUNTIME) //어노테이션 적용 및 유지범위
+public @interface RoleCheck {
+ String value(); //@RoleCheck("ROLE_MASTER")형식으로 값을 입력할수있게 구현
+}
diff --git a/common-module/src/main/java/com/sparta/commonmodule/config/JpaAuditingConfig.java b/common-module/src/main/java/com/sparta/commonmodule/config/JpaAuditingConfig.java
new file mode 100644
index 00000000..92578965
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/config/JpaAuditingConfig.java
@@ -0,0 +1,10 @@
+package com.sparta.commonmodule.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@Configuration
+@EnableJpaAuditing
+public class JpaAuditingConfig {
+
+}
diff --git a/common-module/src/main/java/com/sparta/commonmodule/config/SwaggerConfig.java b/common-module/src/main/java/com/sparta/commonmodule/config/SwaggerConfig.java
new file mode 100644
index 00000000..9c146f37
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/config/SwaggerConfig.java
@@ -0,0 +1,21 @@
+package com.sparta.commonmodule.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.servers.Server;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI openAPI() {
+ return new OpenAPI()
+ .servers(List.of(
+ new Server().url("http://localhost:8080")
+ ));
+ }
+
+}
diff --git a/common-module/src/main/java/com/sparta/commonmodule/entity/AuditorAwareImpl.java b/common-module/src/main/java/com/sparta/commonmodule/entity/AuditorAwareImpl.java
new file mode 100644
index 00000000..8eb45da3
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/entity/AuditorAwareImpl.java
@@ -0,0 +1,51 @@
+package com.sparta.commonmodule.entity;
+
+import org.springframework.data.domain.AuditorAware;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.Optional;
+
+/**
+ * Spring Data JPA의 AuditorAware 구현체
+ * HTTP 요청의 `X-User-Id` 헤더 값을 기반으로 @CreatedBy, @LastModifiedBy 값을 자동 설정
+ */
+@Component
+public class AuditorAwareImpl implements AuditorAware {
+
+ /**
+ * 현재 요청을 보낸 사용자의 ID를 반환 (Auditing 기능에서 호출됨)
+ * @return Optional - 요청에서 추출한 사용자 ID (없으면 null)
+ */
+ @Override
+ public Optional getCurrentAuditor() {
+ Long userId = getUserIdFromHeader();
+ return Optional.ofNullable(userId);
+ }
+
+ /**
+ * HTTP 요청에서 'X-User-Id' 헤더 값을 가져와 Long 타입으로 변환
+ * @return Long - 변환된 사용자 ID (없으면 null)
+ */
+ private Long getUserIdFromHeader() {
+ // 현재 요청의 정보를 가져옴
+ ServletRequestAttributes attributes =
+ (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+
+ if (attributes != null) {
+ HttpServletRequest request = attributes.getRequest();
+ String userIdHeader = request.getHeader("user_id"); // 헤더에서 사용자 ID 가져오기
+
+ if (userIdHeader != null) {
+ try {
+ return Long.parseLong(userIdHeader); // String → Long 변환
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/common-module/src/main/java/com/sparta/commonmodule/entity/BaseEntity.java b/common-module/src/main/java/com/sparta/commonmodule/entity/BaseEntity.java
new file mode 100644
index 00000000..eb8eed6c
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/entity/BaseEntity.java
@@ -0,0 +1,70 @@
+package com.sparta.commonmodule.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import java.time.LocalDateTime;
+import lombok.Data;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.hibernate.annotations.ColumnDefault;
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+@Data
+@Getter
+@RequiredArgsConstructor
+@EntityListeners(AuditingEntityListener.class)
+@MappedSuperclass
+public class BaseEntity {
+
+ @CreatedDate
+ @Column(name = "created_at", updatable = false)
+ private LocalDateTime createdAt;
+
+ @CreatedBy
+ @Column(name = "created_by")
+ private Long createdBy;
+
+ @LastModifiedDate
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+
+ @LastModifiedBy
+ @Column(name = "updated_by")
+ private Long updatedBy;
+
+
+ @Column(name = "is_deleted")
+ @ColumnDefault("FALSE")
+ private Boolean isDeleted = false;
+
+
+ @Column(name = "deleted_at", nullable = true)
+ private LocalDateTime deletedAt;
+
+ @Column(name = "deleted_by", nullable = true)
+ private Long deletedBy;
+
+ // 생성을 위한 method
+ public BaseEntity(Long userId) {
+ this.createdBy = userId;
+ this.updatedBy = userId;
+ this.isDeleted = false;
+ }
+
+ // update를 위한 method
+ public void update(Long userId) {
+ this.updatedBy = userId;
+ }
+
+ // 소프트 delete를 위한 method
+ public void delete(Long userId) {
+ this.isDeleted = true;
+ this.deletedAt = LocalDateTime.now();
+ this.deletedBy = userId;
+ }
+}
\ No newline at end of file
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/DeletedDataAccessException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/DeletedDataAccessException.java
new file mode 100644
index 00000000..00caf67a
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/DeletedDataAccessException.java
@@ -0,0 +1,21 @@
+package com.sparta.commonmodule.exception;
+
+import org.springframework.dao.DataAccessException;
+
+/**
+ * 삭제된 데이터에 접근할 때 발생하는 예외입니다. 보안상 상세 정보를 노출하지 않고, "삭제된 리소스" 혹은 "잘못된 요청"으로 처리할 수 있습니다.
+ */
+public class DeletedDataAccessException extends DataAccessException {
+
+ public DeletedDataAccessException() {
+ super("삭제된 리소스에 접근했습니다.");
+ }
+
+ public DeletedDataAccessException(String msg) {
+ super(msg);
+ }
+
+ public DeletedDataAccessException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
\ No newline at end of file
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/DuplicateResourceException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/DuplicateResourceException.java
new file mode 100644
index 00000000..45bd769c
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/DuplicateResourceException.java
@@ -0,0 +1,22 @@
+package com.sparta.commonmodule.exception;
+
+import org.springframework.dao.DataAccessException;
+
+/**
+ * 이미 존재하는 데이터를 생성하려 할 때 발생하는 예외입니다. 주로 HTTP 409 (Conflict) 응답과 매핑하여 처리합니다.
+ */
+public class DuplicateResourceException extends DataAccessException {
+
+ public DuplicateResourceException() {
+ super("이미 존재하는 리소스입니다.");
+ }
+
+ public DuplicateResourceException(String message) {
+ super(message);
+ }
+
+ public DuplicateResourceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/GlobalExceptionHandler.java b/common-module/src/main/java/com/sparta/commonmodule/exception/GlobalExceptionHandler.java
new file mode 100644
index 00000000..ff0e073f
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/GlobalExceptionHandler.java
@@ -0,0 +1,59 @@
+package com.sparta.commonmodule.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ // ❌ 삭제된 데이터 접근
+ @ExceptionHandler(DeletedDataAccessException.class)
+ public ResponseEntity handleDeletedDataAccess(DeletedDataAccessException ex) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
+ }
+
+ // ❌ 존재하지 않는 데이터 접근
+ @ExceptionHandler(ResourceNotFoundException.class)
+ public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
+ }
+
+ // ❌ 이미 존재하는 데이터 생성 (409)
+ @ExceptionHandler(DuplicateResourceException.class)
+ public ResponseEntity handleDuplicateResource(DuplicateResourceException ex) {
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(ex.getMessage());
+ }
+
+ // ❌ 권한 없음 (401)
+ @ExceptionHandler(UnauthorizedAccessException.class)
+ public ResponseEntity handleUnauthorizedAccess(UnauthorizedAccessException ex) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage());
+ }
+
+ // ❌ 소유자가 아닌 사용자의 접근 (403)
+ @ExceptionHandler(OwnershipMismatchException.class)
+ public ResponseEntity handleOwnershipMismatch(OwnershipMismatchException ex) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage());
+ }
+
+ // ❌ 잘못된 요청 파라미터 (400)
+ @ExceptionHandler(InvalidParameterException.class)
+ public ResponseEntity handleInvalidParameter(InvalidParameterException ex) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
+ }
+
+ // ❌ 수행할 수 없는 작업 요청 (400 또는 403)
+ @ExceptionHandler(OperationNotAllowedException.class)
+ public ResponseEntity handleOperationNotAllowed(OperationNotAllowedException ex) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
+ }
+
+ // ❌ 서버 내부 오류 (500)
+ @ExceptionHandler(InternalServerException.class)
+ public ResponseEntity handleInternalServerError(InternalServerException ex) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
+ }
+}
+
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/InternalServerException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/InternalServerException.java
new file mode 100644
index 00000000..1f742470
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/InternalServerException.java
@@ -0,0 +1,16 @@
+package com.sparta.commonmodule.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // HTTP 500
+public class InternalServerException extends RuntimeException {
+
+ public InternalServerException() {
+ super("서버 내부 오류가 발생했습니다.");
+ }
+
+ public InternalServerException(String message) {
+ super(message);
+ }
+}
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/InvalidParameterException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/InvalidParameterException.java
new file mode 100644
index 00000000..97ffeae5
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/InvalidParameterException.java
@@ -0,0 +1,23 @@
+package com.sparta.commonmodule.exception;
+
+/**
+ * 메서드나 API 호출 시 잘못된 파라미터가 전달되었을 때 발생하는 예외입니다. 주로 HTTP 400 (Bad Request) 응답과 매핑하여 처리합니다.
+ */
+public class InvalidParameterException extends RuntimeException {
+
+ public InvalidParameterException() {
+ super("잘못된 파라미터가 전달되었습니다.");
+ }
+
+ public InvalidParameterException(String message) {
+ super(message);
+ }
+
+ public InvalidParameterException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public InvalidParameterException(Throwable cause) {
+ super(cause);
+ }
+}
\ No newline at end of file
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/OperationNotAllowedException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/OperationNotAllowedException.java
new file mode 100644
index 00000000..73ff6bd3
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/OperationNotAllowedException.java
@@ -0,0 +1,16 @@
+package com.sparta.commonmodule.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.BAD_REQUEST) // 또는 상황에 따라 FORBIDDEN (403)
+public class OperationNotAllowedException extends RuntimeException {
+
+ public OperationNotAllowedException() {
+ super("현재 상태에서 수행할 수 없는 작업입니다.");
+ }
+
+ public OperationNotAllowedException(String message) {
+ super(message);
+ }
+}
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/OwnershipMismatchException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/OwnershipMismatchException.java
new file mode 100644
index 00000000..ffe11824
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/OwnershipMismatchException.java
@@ -0,0 +1,23 @@
+package com.sparta.commonmodule.exception;
+
+/**
+ * 리소스의 소유자와 다른 ID로 접근하려 할 때 발생하는 예외입니다. 주로 HTTP 403 (Forbidden) 응답과 매핑하여 처리합니다.
+ */
+public class OwnershipMismatchException extends RuntimeException {
+
+ public OwnershipMismatchException() {
+ super("소유자와 일치하지 않는 사용자입니다.");
+ }
+
+ public OwnershipMismatchException(String message) {
+ super(message);
+ }
+
+ public OwnershipMismatchException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public OwnershipMismatchException(Throwable cause) {
+ super(cause);
+ }
+}
\ No newline at end of file
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/ResourceNotFoundException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/ResourceNotFoundException.java
new file mode 100644
index 00000000..6249ad82
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/ResourceNotFoundException.java
@@ -0,0 +1,21 @@
+package com.sparta.commonmodule.exception;
+
+import org.springframework.dao.DataAccessException;
+
+/**
+ * 존재하지 않는 ID에 접근할 때 발생하는 예외입니다. 주로 HTTP 404 (Not Found) 응답과 매핑하여 처리합니다.
+ */
+public class ResourceNotFoundException extends DataAccessException {
+
+ public ResourceNotFoundException() {
+ super("요청한 리소스를 찾을 수 없습니다.");
+ }
+
+ public ResourceNotFoundException(String message) {
+ super(message);
+ }
+
+ public ResourceNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
\ No newline at end of file
diff --git a/common-module/src/main/java/com/sparta/commonmodule/exception/UnauthorizedAccessException.java b/common-module/src/main/java/com/sparta/commonmodule/exception/UnauthorizedAccessException.java
new file mode 100644
index 00000000..bb1c8a1d
--- /dev/null
+++ b/common-module/src/main/java/com/sparta/commonmodule/exception/UnauthorizedAccessException.java
@@ -0,0 +1,23 @@
+package com.sparta.commonmodule.exception;
+
+/**
+ * 사용자가 필요한 권한을 보유하지 않은 경우 발생하는 예외입니다. HTTP 401 (Unauthorized) 또는 403 (Forbidden)으로 매핑할 수 있습니다.
+ */
+public class UnauthorizedAccessException extends RuntimeException {
+
+ public UnauthorizedAccessException() {
+ super("권한이 없습니다.");
+ }
+
+ public UnauthorizedAccessException(String message) {
+ super(message);
+ }
+
+ public UnauthorizedAccessException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public UnauthorizedAccessException(Throwable cause) {
+ super(cause);
+ }
+}
\ No newline at end of file
diff --git a/company-service/Dockerfile.dev b/company-service/Dockerfile.dev
new file mode 100644
index 00000000..ac4cbeec
--- /dev/null
+++ b/company-service/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", ":company-service:bootRun"]
diff --git a/company-service/build.gradle b/company-service/build.gradle
new file mode 100644
index 00000000..91943fec
--- /dev/null
+++ b/company-service/build.gradle
@@ -0,0 +1,55 @@
+ext {
+ set('springCloudVersion', "2024.0.0")
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation project(':common-module')
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
+ implementation 'org.postgresql:postgresql:42.7.3'
+ implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
+ implementation 'org.hibernate.validator:hibernate-validator:8.0.0.Final'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+
+ // QueryDSL
+ implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
+ annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
+ implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
+ annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
+ annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
+
+// QueryDSL 자동 생성 디렉토리 설정
+def querydslDir = "$buildDir/generated/querydsl"
+
+tasks.withType(JavaCompile).configureEach {
+ options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
+}
+
+// 자동 생성된 QueryDSL 클래스 경로 추가
+sourceSets {
+ main {
+ java {
+ srcDirs += querydslDir
+ }
+ }
+}
+
+// clean 시 Q파일 삭제
+clean {
+ delete file(querydslDir)
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/CompanyServiceApplication.java b/company-service/src/main/java/com/sparta/companyservice/CompanyServiceApplication.java
new file mode 100644
index 00000000..7ac5fc70
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/CompanyServiceApplication.java
@@ -0,0 +1,19 @@
+package com.sparta.companyservice;
+
+import com.sparta.commonmodule.config.JpaAuditingConfig;
+import com.sparta.commonmodule.config.SwaggerConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Import;
+
+@SpringBootApplication(scanBasePackages = "com.sparta")
+@EnableFeignClients(basePackages = "com.sparta")
+@Import({SwaggerConfig.class, JpaAuditingConfig.class})
+public class CompanyServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(CompanyServiceApplication.class, args);
+ }
+
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyCreateDto.java b/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyCreateDto.java
new file mode 100644
index 00000000..c0dd91e7
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyCreateDto.java
@@ -0,0 +1,13 @@
+package com.sparta.companyservice.application.dto;
+
+import com.sparta.companyservice.domain.model.CompanyType;
+
+import java.util.UUID;
+
+// Controller로부터 받은 요청 데이터를 내부 비즈니스 로직에서 사용할 수 있게 가공한 객체
+public record CompanyCreateDto (
+ String name,
+ CompanyType type,
+ UUID hubId,
+ String address
+) {}
diff --git a/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyDto.java b/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyDto.java
new file mode 100644
index 00000000..52033266
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyDto.java
@@ -0,0 +1,32 @@
+package com.sparta.companyservice.application.dto;
+
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.domain.model.CompanyType;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+// 서비스 <-> 도메인 간 내부 데이터 전달용 DTO
+
+public record CompanyDto(
+ UUID id,
+ String name,
+ CompanyType type,
+ UUID hubId,
+ String address,
+ LocalDateTime createdAt,
+ long createdBy
+) {
+ public static CompanyDto fromEntity(Company company) {
+ return new CompanyDto(
+ company.getId(),
+ company.getName(),
+ company.getType(),
+ company.getHubId(),
+ company.getAddress(),
+ company.getCreatedAt(),
+ company.getCreatedBy()
+ );
+ }
+}
+
diff --git a/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyUpdateDto.java b/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyUpdateDto.java
new file mode 100644
index 00000000..f8b24599
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/application/dto/CompanyUpdateDto.java
@@ -0,0 +1,9 @@
+package com.sparta.companyservice.application.dto;
+
+import java.util.UUID;
+
+public record CompanyUpdateDto(
+ String name,
+ UUID hubId,
+ String address
+) {}
diff --git a/company-service/src/main/java/com/sparta/companyservice/application/service/CompanyService.java b/company-service/src/main/java/com/sparta/companyservice/application/service/CompanyService.java
new file mode 100644
index 00000000..d76ddbaf
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/application/service/CompanyService.java
@@ -0,0 +1,96 @@
+package com.sparta.companyservice.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.companyservice.application.dto.CompanyCreateDto;
+import com.sparta.companyservice.application.dto.CompanyDto;
+import com.sparta.companyservice.application.dto.CompanyUpdateDto;
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.domain.model.CompanyType;
+import com.sparta.companyservice.domain.repository.CompanyRepository;
+import com.sparta.companyservice.infrastructure.client.HubClient;
+import com.sparta.companyservice.infrastructure.querydsl.CompanyQueryRepository;
+import com.sparta.companyservice.presentation.response.CompanyDeleteResponse;
+import feign.FeignException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class CompanyService {
+ private final CompanyRepository companyRepository;
+ private final HubClient hubClient;
+
+ @Transactional(readOnly = true)
+ public boolean existsById(UUID id) { // 업체 존재 확인
+ return companyRepository.existsByIdAndDeletedAtIsNull(id);
+ }
+
+ @Transactional // 생성
+ public CompanyDto createCompany(CompanyCreateDto dto, long userId) {
+ validateHubExists(dto.hubId());
+ Company company = Company.create(
+ dto.name(),
+ dto.address(),
+ dto.hubId(),
+ dto.type(),
+ userId
+ );
+ Company createdCompany = companyRepository.save(company);
+ return CompanyDto.fromEntity(createdCompany);
+ }
+
+ @Transactional(readOnly = true) // 전체 조회
+ public Page searchCompanies(String name, String address, CompanyType type, Pageable pageable) {
+ return companyRepository.searchCompanies(name, address, type, pageable)
+ .map(CompanyDto::fromEntity);
+ }
+
+ @Transactional(readOnly = true) // 단일 조회
+ public CompanyDto getCompanyById(UUID id) {
+ return CompanyDto.fromEntity(findCompany(id));
+ }
+
+ @Transactional // 수정
+ public CompanyDto updateCompany(UUID id, CompanyUpdateDto dto, long userId) {
+ Company company = findCompany(id);
+
+ // hubId가 변경된 경우 유효한 허브Id인지 유효성 검사
+ if (dto.hubId() != null && !dto.hubId().equals(company.getHubId())) {
+ validateHubExists(dto.hubId());
+ }
+
+ company.applyUpdate(dto, userId);
+ return CompanyDto.fromEntity(company);
+ }
+
+ @Transactional // 삭제
+ public CompanyDeleteResponse deleteCompany(UUID id, long userId) {
+ Company company = findCompany(id);
+ company.delete(userId);
+ return CompanyDeleteResponse.of(id);
+ }
+
+ /// //////////////////////////////////////////////////////////////////////////////////
+
+ private void validateHubExists(UUID hubId) {
+ try {
+ hubClient.getHubById(hubId);
+ } catch (FeignException.NotFound e) {
+ throw new ResourceNotFoundException("해당 허브가 존재하지 않습니다.");
+ }
+ }
+
+// public void validateHubExists(UUID hubId) {
+// // http 테스트 시 이 메서드 사용
+// }
+
+
+ private Company findCompany(UUID id) {
+ return companyRepository.findByIdAndDeletedAtIsNull(id).orElseThrow(() -> new ResourceNotFoundException("해당 업체를 찾을 수 없습니다."));
+ }
+}
\ No newline at end of file
diff --git a/company-service/src/main/java/com/sparta/companyservice/config/QuerydslConfig.java b/company-service/src/main/java/com/sparta/companyservice/config/QuerydslConfig.java
new file mode 100644
index 00000000..684fd973
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/config/QuerydslConfig.java
@@ -0,0 +1,18 @@
+package com.sparta.companyservice.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class QuerydslConfig {
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Bean
+ public JPAQueryFactory jpaQueryFactory() {
+ return new JPAQueryFactory(entityManager);
+ }
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/domain/model/Company.java b/company-service/src/main/java/com/sparta/companyservice/domain/model/Company.java
new file mode 100644
index 00000000..001aa76b
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/domain/model/Company.java
@@ -0,0 +1,97 @@
+package com.sparta.companyservice.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import com.sparta.companyservice.application.dto.CompanyUpdateDto;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.util.UUID;
+
+@Entity
+@Table(name = "p_company")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Company extends BaseEntity {
+ @Id
+ @Column(name = "company_id")
+ private UUID id;
+
+ @Column(nullable = false, length = 100, unique = true)
+ private String name;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private CompanyType type;
+
+ @Column(name = "hub_id", nullable = false)
+ private UUID hubId;
+
+ @Column(nullable = false, columnDefinition = "TEXT")
+ private String address;
+
+ @Builder
+ // 도메인 객체 생성 책임은 create()가 지고, 그 내부에서 builder를 통해 객체 생성
+ private Company(UUID id, String name, CompanyType type, UUID hubId, String address, long userId) {
+ super(userId);
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ this.hubId = hubId;
+ this.address = address;
+ }
+
+ public static Company create(String name, String address, UUID hubId, CompanyType type, long userId) {
+ validateCompany(name, address, hubId, type);
+ return Company.builder()
+ .id(UUID.randomUUID())
+ .name(name)
+ .address(address)
+ .hubId(hubId)
+ .type(type)
+ .userId(userId)
+ .build();
+ }
+
+ public void update(String newName, String newAddress, UUID newHubId, long userId) {
+ validateCompany(newName, newAddress, newHubId);
+ this.name = newName;
+ this.address = newAddress;
+ this.hubId = newHubId;
+ super.update(userId);
+ }
+
+ public void applyUpdate(CompanyUpdateDto dto, long userId) {
+ // 수정 시 사용자가 입력하지 않은 필드는 기존 값으로 씌움
+ String newName = dto.name() != null ? dto.name() : this.name;
+ String newAddress = dto.address() != null ? dto.address() : this.address;
+ UUID newHubId = dto.hubId() != null ? dto.hubId() : this.hubId;
+
+ update(newName, newAddress, newHubId, userId);
+ }
+
+ public void delete(long userId) {
+ super.delete(userId);
+ }
+
+ /// ///////////////////////////////////////////////////////////////////////////////////////
+
+ // 업체 수정 시 검증
+ private static void validateCompany(String newName, String newAddress, UUID newHubId) {
+ validateNotNull(newName, "업체명");
+ validateNotNull(newAddress, "주소");
+ validateNotNull(newHubId, "소속 Hub");
+ }
+
+ // 업체 생성 시 검증
+ private static void validateCompany(String newName, String newAddress, UUID newHubId, CompanyType newType) {
+ validateCompany(newName, newAddress, newHubId);
+ validateNotNull(newType, "업체 type");
+ }
+
+ private static void validateNotNull(Object value, String fieldName) {
+ if (value == null) {
+ throw new IllegalArgumentException(fieldName + "은(는) null일 수 없습니다.");
+ }
+ }
+}
+
diff --git a/company-service/src/main/java/com/sparta/companyservice/domain/model/CompanyType.java b/company-service/src/main/java/com/sparta/companyservice/domain/model/CompanyType.java
new file mode 100644
index 00000000..bb8a9632
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/domain/model/CompanyType.java
@@ -0,0 +1,6 @@
+package com.sparta.companyservice.domain.model;
+
+public enum CompanyType {
+ PRODUCER,
+ RECEIVER
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/domain/repository/CompanyRepository.java b/company-service/src/main/java/com/sparta/companyservice/domain/repository/CompanyRepository.java
new file mode 100644
index 00000000..0a5cc8c8
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/domain/repository/CompanyRepository.java
@@ -0,0 +1,19 @@
+package com.sparta.companyservice.domain.repository;
+
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.domain.model.CompanyType;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+import java.util.UUID;
+
+public interface CompanyRepository {
+ Company save(Company company);
+
+ Optional findByIdAndDeletedAtIsNull(UUID id);
+
+ Page searchCompanies(String name, String address, CompanyType type, Pageable pageable);
+
+ boolean existsByIdAndDeletedAtIsNull(UUID id);
+}
\ No newline at end of file
diff --git a/company-service/src/main/java/com/sparta/companyservice/infrastructure/client/HubClient.java b/company-service/src/main/java/com/sparta/companyservice/infrastructure/client/HubClient.java
new file mode 100644
index 00000000..f91c40d6
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/infrastructure/client/HubClient.java
@@ -0,0 +1,15 @@
+package com.sparta.companyservice.infrastructure.client;
+
+import com.sparta.companyservice.infrastructure.client.dto.HubClientDto;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.UUID;
+
+@FeignClient(name = "hub-service")
+public interface HubClient {
+
+ @GetMapping("/api/v1/hubs/{hub_id}")
+ HubClientDto getHubById(@PathVariable("hub_id") UUID hubId);
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/infrastructure/client/dto/HubClientDto.java b/company-service/src/main/java/com/sparta/companyservice/infrastructure/client/dto/HubClientDto.java
new file mode 100644
index 00000000..739daff4
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/infrastructure/client/dto/HubClientDto.java
@@ -0,0 +1,6 @@
+package com.sparta.companyservice.infrastructure.client.dto;
+
+import java.util.UUID;
+
+public record HubClientDto(UUID hubId, String name, String address) {
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/infrastructure/querydsl/CompanyQueryRepository.java b/company-service/src/main/java/com/sparta/companyservice/infrastructure/querydsl/CompanyQueryRepository.java
new file mode 100644
index 00000000..5add2fa7
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/infrastructure/querydsl/CompanyQueryRepository.java
@@ -0,0 +1,10 @@
+package com.sparta.companyservice.infrastructure.querydsl;
+
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.domain.model.CompanyType;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface CompanyQueryRepository {
+ Page searchCompanies(String name, String address, CompanyType companyType, Pageable pageable);
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/infrastructure/querydsl/CompanyQueryRepositoryImpl.java b/company-service/src/main/java/com/sparta/companyservice/infrastructure/querydsl/CompanyQueryRepositoryImpl.java
new file mode 100644
index 00000000..b4da4aac
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/infrastructure/querydsl/CompanyQueryRepositoryImpl.java
@@ -0,0 +1,113 @@
+package com.sparta.companyservice.infrastructure.querydsl;
+
+import com.querydsl.core.BooleanBuilder;
+import com.querydsl.core.types.Order;
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.dsl.PathBuilder;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.domain.model.CompanyType;
+import com.sparta.companyservice.domain.model.QCompany;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+public class CompanyQueryRepositoryImpl implements CompanyQueryRepository {
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public Page searchCompanies(String name, String address, CompanyType companyType, Pageable pageable) {
+ QCompany company = QCompany.company;
+
+ try {
+ // 조건 빌더 생성
+ BooleanBuilder conditions = createConditions(company, name, address, companyType);
+
+ // 결과 조회
+ List results = queryFactory
+ .selectFrom(company)
+ .where(conditions)
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize())
+ .orderBy(getSortedColumn(pageable.getSort()))
+ .fetch();
+
+ // 전체 카운트 조회
+ Long countResult = queryFactory
+ .select(company.count())
+ .from(company)
+ .where(conditions)
+ .fetchOne();
+
+ long total = countResult != null ? countResult : 0L;
+
+ return new PageImpl<>(results, pageable, total);
+ } catch (Exception e) {
+ log.error("업체 검색 중 오류 발생: {}", e.getMessage(), e);
+ throw new RuntimeException("업체 검색 도중 오류가 발생했습니다.");
+ }
+ }
+
+ /**
+ * 검색 조건 생성
+ */
+ private BooleanBuilder createConditions(QCompany company, String name, String address, CompanyType companyType) {
+ BooleanBuilder builder = new BooleanBuilder();
+
+ // 소프트 삭제 제외
+ builder.and(company.deletedAt.isNull());
+
+ if (name != null && !name.isBlank()) {
+ builder.and(company.name.containsIgnoreCase(name));
+ }
+
+ if (address != null && !address.isBlank()) {
+ builder.and(company.address.containsIgnoreCase(address));
+ }
+
+ if (companyType != null) {
+ builder.and(company.type.eq(companyType));
+ }
+
+ return builder;
+ }
+
+ /**
+ * 정렬 지정자 생성
+ */
+ private OrderSpecifier>[] getSortedColumn(Sort sort) {
+ List> orderSpecifiers = new ArrayList<>();
+ PathBuilder entityPath = new PathBuilder<>(Company.class, "company");
+
+ // Sort가 비어있지 않은 경우에만 처리
+ if (sort != null && sort.isSorted()) {
+ for (Sort.Order order : sort) {
+ Order direction = order.isAscending() ? Order.ASC : Order.DESC;
+ String property = order.getProperty();
+
+ // 제네릭 타입의 OrderSpecifier 생성
+ orderSpecifiers.add(
+ new OrderSpecifier(direction, entityPath.get(property))
+ );
+ }
+ }
+
+ // 기본 정렬(ID 오름차순) 추가
+ if (orderSpecifiers.isEmpty()) {
+ orderSpecifiers.add(
+ new OrderSpecifier(Order.DESC, entityPath.get("createdAt"))
+ );
+
+ }
+
+ return orderSpecifiers.toArray(new OrderSpecifier[0]);
+ }
+}
\ No newline at end of file
diff --git a/company-service/src/main/java/com/sparta/companyservice/infrastructure/repository/CompanyRepositoryImpl.java b/company-service/src/main/java/com/sparta/companyservice/infrastructure/repository/CompanyRepositoryImpl.java
new file mode 100644
index 00000000..3cf99fe2
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/infrastructure/repository/CompanyRepositoryImpl.java
@@ -0,0 +1,43 @@
+package com.sparta.companyservice.infrastructure.repository;
+
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.domain.model.CompanyType;
+import com.sparta.companyservice.domain.repository.CompanyRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+@Repository
+@RequiredArgsConstructor
+public class CompanyRepositoryImpl implements CompanyRepository {
+
+ private final JpaCompanyRepository jpaCompanyRepository;
+
+ @Override
+ public Company save(Company company) {
+ return jpaCompanyRepository.save(company);
+ }
+
+ @Override
+ public Optional findByIdAndDeletedAtIsNull(UUID id) {
+ return jpaCompanyRepository.findById(id)
+ .filter(c -> c.getDeletedAt() == null);
+ }
+
+ @Override
+ public Page searchCompanies(String name, String address, CompanyType type, Pageable pageable) {
+ return jpaCompanyRepository.searchCompanies(name, address, type, pageable);
+ }
+
+ @Override
+ public boolean existsByIdAndDeletedAtIsNull(UUID id) {
+ return jpaCompanyRepository.existsByIdAndDeletedAtIsNull(id);
+ }
+
+
+}
+
diff --git a/company-service/src/main/java/com/sparta/companyservice/infrastructure/repository/JpaCompanyRepository.java b/company-service/src/main/java/com/sparta/companyservice/infrastructure/repository/JpaCompanyRepository.java
new file mode 100644
index 00000000..499461a2
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/infrastructure/repository/JpaCompanyRepository.java
@@ -0,0 +1,13 @@
+package com.sparta.companyservice.infrastructure.repository;
+
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.infrastructure.querydsl.CompanyQueryRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+@Repository
+public interface JpaCompanyRepository extends JpaRepository, CompanyQueryRepository {
+ boolean existsByIdAndDeletedAtIsNull(UUID id);
+}
\ No newline at end of file
diff --git a/company-service/src/main/java/com/sparta/companyservice/presentation/controller/CompanyController.java b/company-service/src/main/java/com/sparta/companyservice/presentation/controller/CompanyController.java
new file mode 100644
index 00000000..f858fd0a
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/presentation/controller/CompanyController.java
@@ -0,0 +1,97 @@
+package com.sparta.companyservice.presentation.controller;
+
+import com.sparta.commonmodule.aop.RoleCheck;
+import com.sparta.companyservice.application.dto.CompanyDto;
+import com.sparta.companyservice.application.service.CompanyService;
+import com.sparta.companyservice.domain.model.CompanyType;
+import com.sparta.companyservice.presentation.request.CompanyCreateRequest;
+import com.sparta.companyservice.presentation.request.CompanyUpdateRequest;
+import com.sparta.companyservice.presentation.response.CompanyDeleteResponse;
+import com.sparta.companyservice.presentation.response.CompanyResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/v1/companies")
+@RequiredArgsConstructor
+@Tag(name = "Company Service", description = "Company Service API")
+public class CompanyController {
+
+ private final CompanyService companyService;
+
+ // 업체 존재 확인
+ @GetMapping("/{id}/exists")
+ public boolean existsById(@PathVariable("id") UUID id) {
+ return companyService.existsById(id);
+ }
+
+ // 생성
+ @Operation(summary = "Company 등록", description = "Company 생성 api 입니다.")
+ @RoleCheck("ROLE_COMPANY")
+ @PostMapping
+ public ResponseEntity createCompany(@Valid @RequestBody CompanyCreateRequest request, @RequestHeader("user_id") Long userId) {
+ CompanyDto createdCompany = companyService.createCompany(request.toDto(), userId);
+ return ResponseEntity.ok(CompanyResponse.fromDto(createdCompany));
+ }
+
+ // 전체 목록 조회 & 검색
+ @Operation(summary = "Company 조회", description = "Company 조회 api 입니다.")
+ @GetMapping
+ public ResponseEntity> searchCompanies(
+ @RequestParam(name = "name", required = false) String name,
+ @RequestParam(name = "address", required = false) String address,
+ @RequestParam(name = "type", required = false) CompanyType type,
+ Pageable pageable
+ ) {
+ Pageable validatedPageable = validatePageSize(pageable); // 페이지 사이즈 검증
+
+ Page companies = companyService.searchCompanies(name, address, type, validatedPageable);
+ Page responses = companies.map(CompanyResponse::fromDto);
+ return ResponseEntity.ok(responses);
+ }
+
+ // 단일 조회
+ @Operation(summary = "Company 조회", description = "Company 단건 조회 api 입니다.")
+ @GetMapping("/{companyId}")
+ public ResponseEntity getCompanyById(@PathVariable("companyId") UUID companyId
+ ) {
+ CompanyDto oneCompany = companyService.getCompanyById(companyId);
+ return ResponseEntity.ok(CompanyResponse.fromDto(oneCompany));
+ }
+
+ // 수정
+ @Operation(summary = "Company 수정", description = "Company 수정 api 입니다.")
+ @RoleCheck("ROLE_MASTER, ROLE_HUB, ROLE_COMPANY")
+ @PatchMapping("/{companyId}")
+ public ResponseEntity updateCompany(@PathVariable("companyId") UUID companyId, @Valid @RequestBody CompanyUpdateRequest request, @RequestHeader("user_id") Long userId) {
+ CompanyDto updatedCompany = companyService.updateCompany(companyId, request.toDto(), userId);
+ return ResponseEntity.ok(CompanyResponse.fromDto(updatedCompany));
+ }
+
+ // 삭제
+ @Operation(summary = "Company 삭제", description = "Company 삭제 api 입니다.")
+ @RoleCheck("ROLE_MASTER, ROLE_HUB")
+ @DeleteMapping("/{companyId}")
+ public ResponseEntity deleteCompany(@PathVariable("companyId") UUID companyId, @RequestHeader("user_id") Long userId) {
+ CompanyDeleteResponse deletedCompany = companyService.deleteCompany(companyId, userId);
+ return ResponseEntity.ok(deletedCompany);
+ }
+
+ private Pageable validatePageSize(Pageable pageable) { // 페이지 사이즈 검증
+ List allowedSizes = List.of(10, 30, 50);
+ int size = allowedSizes.contains(pageable.getPageSize()) ? pageable.getPageSize() : 10;
+ return PageRequest.of(pageable.getPageNumber(), size, pageable.getSort());
+ }
+
+}
+
diff --git a/company-service/src/main/java/com/sparta/companyservice/presentation/request/CompanyCreateRequest.java b/company-service/src/main/java/com/sparta/companyservice/presentation/request/CompanyCreateRequest.java
new file mode 100644
index 00000000..3959cea4
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/presentation/request/CompanyCreateRequest.java
@@ -0,0 +1,36 @@
+package com.sparta.companyservice.presentation.request;
+
+import com.sparta.companyservice.application.dto.CompanyCreateDto;
+import com.sparta.companyservice.domain.model.CompanyType;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+import java.util.UUID;
+
+public record CompanyCreateRequest(
+ @NotBlank @Size(min = 3) String name,
+ @NotNull CompanyType type,
+ @NotNull UUID hubId,
+ @NotBlank String address
+) {
+ public CompanyCreateDto toDto() {
+ return new CompanyCreateDto(name, type, hubId, address);
+ }
+}
+
+// 레이어드 아키텍처 설계 헷갈리는 부분: CompanyCreateRequest가 application 계층의 dto가 아니라 왜 presentation 계층?
+// ㄴ CompanyCreateRequest는 클라이언트에서 컨트롤러로 들어오는 HTTP 요청을 담는 객체이기 때문에 presentation 계층에 있어야 한다.
+// ㄴ Controller 외부와 맞닿은 영역이라 presentation 계층에 두는 게 맞다고 함
+
+
+/// 표: 계층 / 역할 / DTO 예시
+/// presentation / HTTP 요청,응답 처리 (Request/Response) / CompanyCreateRequest
+/// application / 서비스 로직, UseCase 정의 / CompanyDto (비즈니스 응답 DTO)
+/// domain / 비즈니스 도메인 모델, Entity / 여긴 DTO 없음
+
+
+// application 계층의 dto는:
+// ㄴ Service 계층이 처리 결과를 반환할 때 사용하는 응답 DTO
+// ㄴ 내부 도메인 객체(Company)를 변환해서 API 응답 포맷에 맞게 구성
+// ㄴ 클라이언트에게 전달할 구조로 정제된 정보
diff --git a/company-service/src/main/java/com/sparta/companyservice/presentation/request/CompanyUpdateRequest.java b/company-service/src/main/java/com/sparta/companyservice/presentation/request/CompanyUpdateRequest.java
new file mode 100644
index 00000000..a4f80f94
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/presentation/request/CompanyUpdateRequest.java
@@ -0,0 +1,15 @@
+package com.sparta.companyservice.presentation.request;
+
+import com.sparta.companyservice.application.dto.CompanyUpdateDto;
+
+import java.util.UUID;
+
+public record CompanyUpdateRequest (
+ String name,
+ UUID hubId,
+ String address
+ ) {
+ public CompanyUpdateDto toDto() {
+ return new CompanyUpdateDto(name, hubId, address);
+ }
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/presentation/response/CompanyDeleteResponse.java b/company-service/src/main/java/com/sparta/companyservice/presentation/response/CompanyDeleteResponse.java
new file mode 100644
index 00000000..460e45e0
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/presentation/response/CompanyDeleteResponse.java
@@ -0,0 +1,12 @@
+package com.sparta.companyservice.presentation.response;
+
+import java.util.UUID;
+
+public record CompanyDeleteResponse (
+ UUID id,
+ String Message
+) {
+ public static CompanyDeleteResponse of(UUID id) {
+ return new CompanyDeleteResponse(id, "업체가 성공적으로 삭제되었습니다.");
+ }
+}
diff --git a/company-service/src/main/java/com/sparta/companyservice/presentation/response/CompanyResponse.java b/company-service/src/main/java/com/sparta/companyservice/presentation/response/CompanyResponse.java
new file mode 100644
index 00000000..67d7d55e
--- /dev/null
+++ b/company-service/src/main/java/com/sparta/companyservice/presentation/response/CompanyResponse.java
@@ -0,0 +1,29 @@
+package com.sparta.companyservice.presentation.response;
+
+import com.sparta.companyservice.application.dto.CompanyDto;
+import com.sparta.companyservice.domain.model.CompanyType;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+public record CompanyResponse(
+ UUID id,
+ String name,
+ CompanyType type,
+ UUID hub_id,
+ String address,
+ LocalDateTime created_at,
+ long created_by
+) {
+ public static CompanyResponse fromDto(CompanyDto dto) {
+ return new CompanyResponse(
+ dto.id(),
+ dto.name(),
+ dto.type(),
+ dto.hubId(),
+ dto.address(),
+ dto.createdAt(),
+ dto.createdBy()
+ );
+ }
+}
diff --git a/company-service/src/main/resources/application.yml b/company-service/src/main/resources/application.yml
new file mode 100644
index 00000000..d6ffbae3
--- /dev/null
+++ b/company-service/src/main/resources/application.yml
@@ -0,0 +1,45 @@
+spring:
+ application:
+ name: company-service
+ datasource:
+ url: jdbc:postgresql://localhost:5001/maindb
+ username: admin
+ password: 1234
+ driver-class-name: org.postgresql.Driver
+ jpa:
+ properties:
+ hibernate:
+ default_schema: company_service
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ hibernate:
+ ddl-auto: update
+ show-sql: true
+# datasource:
+# url: jdbc:postgresql://localhost:5432/company-service
+# username: myuser
+# password: mypassword
+# driver-class-name: org.postgresql.Driver
+# jpa:
+# hibernate:
+# ddl-auto: update
+# show-sql: true # 실행되는 SQL 쿼리 출력
+# database-platform: org.hibernate.dialect.PostgreSQLDialect
+
+springdoc:
+ api-docs:
+ version: openapi_3_1
+ enabled: true
+ path: /company-service/v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ service-url:
+ defaultZone: http://localhost:8761/eureka/
+
+server:
+ port: 8085
diff --git a/company-service/src/test/http/company.http b/company-service/src/test/http/company.http
new file mode 100644
index 00000000..8b7ff01f
--- /dev/null
+++ b/company-service/src/test/http/company.http
@@ -0,0 +1,67 @@
+### 업체 존재 여부 확인
+GET http://localhost:8085/api/v1/companies/550e8400-e29b-41d4-a716-446655440000/exists
+Content-Type: application/json
+
+###
+
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username":"company123",
+ "password":"Company123@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+
+
+### 업체 생성
+POST http://localhost:8080/api/v1/companies
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "원두 로스팅 업체",
+ "type": "PRODUCER",
+ "hubId": "d25f4291-d134-4f73-885a-0eb188a72649",
+ "address": "경기도 과천시"
+}
+
+###
+
+### 업체 단일 조회
+GET http://localhost:8085/api/v1/companies/14613578-fd1b-4576-9d33-9601dc79c3ff
+Content-Type: application/json
+
+###
+
+### 업체 목록 조회 (전체 조회)
+GET http://localhost:8085/api/v1/companies?page=0&size=10
+Content-Type: application/json
+
+###
+
+### 업체 목록 조회 (검색 포함)
+GET http://localhost:8085/api/v1/companies?name=스파르타
+Content-Type: application/json
+
+###
+
+### 업체 수정
+PATCH http://localhost:8085/api/v1/companies/b3f77c99-ae2c-426a-835b-b9f2c23ed112
+Content-Type: application/json
+user_id: 12345
+role: ROLE_MASTER
+
+{
+ "name": "스파르타 가공식품",
+ "address": "서울특별시 강남구"
+}
+
+###
+
+### 업체 삭제
+DELETE http://localhost:8085/api/v1/companies/767c333e-b7cc-4be7-a860-14adebbc962a
+user_id: 12345
+role: ROLE_MASTER
diff --git a/company-service/src/test/java/com/sparta/companyservice/CompanyServiceApplicationTests.java b/company-service/src/test/java/com/sparta/companyservice/CompanyServiceApplicationTests.java
new file mode 100644
index 00000000..f00e15ad
--- /dev/null
+++ b/company-service/src/test/java/com/sparta/companyservice/CompanyServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.sparta.companyservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class CompanyServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/company-service/src/test/java/com/sparta/companyservice/application/service/CompanyServiceTest.java b/company-service/src/test/java/com/sparta/companyservice/application/service/CompanyServiceTest.java
new file mode 100644
index 00000000..0ffdf7ae
--- /dev/null
+++ b/company-service/src/test/java/com/sparta/companyservice/application/service/CompanyServiceTest.java
@@ -0,0 +1,170 @@
+package com.sparta.companyservice.application.service;
+
+import com.sparta.companyservice.application.dto.CompanyCreateDto;
+import com.sparta.companyservice.application.dto.CompanyDto;
+import com.sparta.companyservice.application.dto.CompanyUpdateDto;
+import com.sparta.companyservice.domain.model.Company;
+import com.sparta.companyservice.domain.model.CompanyType;
+import com.sparta.companyservice.domain.repository.CompanyRepository;
+import com.sparta.companyservice.infrastructure.client.HubClient;
+import com.sparta.companyservice.infrastructure.client.dto.HubClientDto;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class CompanyServiceTest {
+
+ @Mock
+ private HubClient hubClient;
+
+ @Mock
+ private CompanyRepository companyRepository;
+
+ @InjectMocks
+ private CompanyService companyService;
+
+ @Test
+ void 회사_생성_정상작동() {
+ // given
+ UUID hubId = UUID.randomUUID();
+ long userId = 1L;
+
+ CompanyCreateDto 요청 = new CompanyCreateDto(
+ "새로운회사",
+ CompanyType.PRODUCER,
+ hubId,
+ "서울시 강남구"
+ );
+
+ HubClientDto 가짜허브 = new HubClientDto(hubId, "서울허브", "서울시 중구");
+
+ Company 저장예정회사 = Company.create(
+ 요청.name(),
+ 요청.address(),
+ 요청.hubId(),
+ 요청.type(),
+ userId
+ );
+
+ when(hubClient.getHubById(hubId)).thenReturn(가짜허브);
+ when(companyRepository.save(any(Company.class))).thenReturn(저장예정회사);
+
+ // when
+ CompanyDto 결과 = companyService.createCompany(요청, userId);
+
+ // then
+ assertEquals("새로운회사", 결과.name());
+ assertEquals(CompanyType.PRODUCER, 결과.type());
+ assertEquals("서울시 강남구", 결과.address());
+ assertEquals(hubId, 결과.hubId());
+ assertEquals(userId, 결과.createdBy());
+
+ verify(hubClient, times(1)).getHubById(hubId);
+ verify(companyRepository, times(1)).save(any(Company.class));
+ }
+
+ @Test
+ void 허브가_없으면_예외처리() {
+ // given
+ UUID hubId = UUID.randomUUID();
+ long userId = 1L;
+
+ CompanyCreateDto 요청 = new CompanyCreateDto(
+ "없는허브회사",
+ CompanyType.RECEIVER,
+ hubId,
+ "대전 중구"
+ );
+
+ when(hubClient.getHubById(hubId)).thenThrow(new IllegalArgumentException("존재하지 않는 허브입니다."));
+
+ // when & then
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> {
+ companyService.createCompany(요청, userId);
+ });
+
+ assertEquals("존재하지 않는 허브입니다.", ex.getMessage());
+ verify(hubClient, times(1)).getHubById(hubId);
+ verify(companyRepository, never()).save(any());
+ }
+
+ @Test
+ void 회사_정보_수정_정상작동() {
+ // given
+ UUID companyId = UUID.randomUUID();
+ UUID newHubId = UUID.randomUUID();
+ long userId = 99L;
+
+ Company 기존회사 = Company.create(
+ "기존이름", "기존주소", UUID.randomUUID(), CompanyType.PRODUCER, 1L
+ );
+
+ CompanyUpdateDto 수정요청 = new CompanyUpdateDto("수정된이름", newHubId, "수정된주소");
+
+ when(companyRepository.findByIdAndDeletedAtIsNull(companyId)).thenReturn(Optional.of(기존회사));
+ when(hubClient.getHubById(newHubId)).thenReturn(new HubClientDto(newHubId, "부산허브", "부산시"));
+
+ // when
+ CompanyDto 결과 = companyService.updateCompany(companyId, 수정요청, userId);
+
+ // then
+ assertEquals("수정된이름", 결과.name());
+ assertEquals("수정된주소", 결과.address());
+ assertEquals(newHubId, 결과.hubId());
+
+ verify(companyRepository).save(any(Company.class));
+ }
+
+ @Test
+ void 회사_정보_일부수정_정상작동() {
+ // given
+ UUID companyId = UUID.randomUUID();
+ UUID 기존HubId = UUID.randomUUID();
+ long userId = 42L;
+
+ Company 기존회사 = Company.create("오리지널", "강남구", 기존HubId, CompanyType.RECEIVER, 1L);
+ CompanyUpdateDto 수정요청 = new CompanyUpdateDto("수정된이름", null, null); // 일부만 수정
+
+ when(companyRepository.findByIdAndDeletedAtIsNull(companyId)).thenReturn(Optional.of(기존회사));
+
+ // when
+ CompanyDto 결과 = companyService.updateCompany(companyId, 수정요청, userId);
+
+ // then
+ assertEquals("수정된이름", 결과.name());
+ assertEquals("강남구", 결과.address()); // 그대로 유지
+ assertEquals(기존HubId, 결과.hubId()); // 그대로 유지
+ }
+
+ @Test
+ void 회사_삭제_정상작동() {
+ // given
+ UUID companyId = UUID.randomUUID();
+ long userId = 88L;
+
+ Company 기존회사 = Company.create("삭제대상", "용산구", UUID.randomUUID(), CompanyType.RECEIVER, 2L);
+
+ when(companyRepository.findByIdAndDeletedAtIsNull(companyId)).thenReturn(Optional.of(기존회사));
+
+ // when
+ companyService.deleteCompany(companyId, userId);
+
+ // then
+ assertNotNull(기존회사.getDeletedAt());
+ assertEquals(userId, 기존회사.getDeletedBy());
+
+ verify(companyRepository).save(any(Company.class));
+ }
+
+}
diff --git a/company-service/src/test/java/com/sparta/companyservice/domain/model/CompanyTest.java b/company-service/src/test/java/com/sparta/companyservice/domain/model/CompanyTest.java
new file mode 100644
index 00000000..8d74368d
--- /dev/null
+++ b/company-service/src/test/java/com/sparta/companyservice/domain/model/CompanyTest.java
@@ -0,0 +1,119 @@
+package com.sparta.companyservice.domain.model;
+
+import com.sparta.companyservice.application.dto.CompanyUpdateDto;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CompanyTest {
+
+ @Test
+ void 회사_정상_생성됨() {
+ // given
+ String name = "테스트회사";
+ String address = "서울시 강남구";
+ UUID hubId = UUID.randomUUID();
+ CompanyType type = CompanyType.PRODUCER;
+ long userId = 1L;
+
+ // when
+ Company company = Company.create(name, address, hubId, type, userId);
+
+ // then
+ assertEquals(name, company.getName());
+ assertEquals(address, company.getAddress());
+ assertEquals(hubId, company.getHubId());
+ assertEquals(type, company.getType());
+ assertEquals(userId, company.getCreatedBy());
+ }
+
+ @Test
+ void 회사명_null_이면_예외발생() {
+ // given
+ String name = null;
+ String address = "서울시 중구";
+ UUID hubId = UUID.randomUUID();
+ CompanyType type = CompanyType.RECEIVER;
+ long userId = 1L;
+
+ // when & then
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
+ Company.create(name, address, hubId, type, userId)
+ );
+ assertTrue(ex.getMessage().contains("업체명"));
+ }
+
+ @Test
+ void 회사정보_업데이트_정상작동() {
+ // given
+ Company company = Company.create(
+ "기존회사",
+ "기존주소",
+ UUID.randomUUID(),
+ CompanyType.RECEIVER,
+ 1L
+ );
+
+ String newName = "새로운회사";
+ String newAddress = "새로운주소";
+ UUID newHubId = UUID.randomUUID();
+ long updatedBy = 2L;
+
+ // when
+ company.update(newName, newAddress, newHubId, updatedBy);
+
+ // then
+ assertEquals(newName, company.getName());
+ assertEquals(newAddress, company.getAddress());
+ assertEquals(newHubId, company.getHubId());
+ assertEquals(updatedBy, company.getUpdatedBy());
+ }
+
+ @Test
+ void 회사정보_일부만_업데이트() {
+ // given
+ UUID hubId = UUID.randomUUID();
+ Company company = Company.create(
+ "기존상호",
+ "기존주소",
+ hubId,
+ CompanyType.PRODUCER,
+ 1L
+ );
+
+ CompanyUpdateDto dto = new CompanyUpdateDto("바뀐상호", null, null);
+ long updatedBy = 3L;
+
+ // when
+ company.applyUpdate(dto, updatedBy);
+
+ // then
+ assertEquals("바뀐상호", company.getName());
+ assertEquals("기존주소", company.getAddress());
+ assertEquals(hubId, company.getHubId());
+ assertEquals(updatedBy, company.getUpdatedBy());
+ }
+
+ @Test
+ void 회사정보_소프트삭제_정상작동() {
+ // given
+ Company company = Company.create(
+ "삭제할회사",
+ "삭제주소",
+ UUID.randomUUID(),
+ CompanyType.RECEIVER,
+ 1L
+ );
+
+ long deletedBy = 5L;
+
+ // when
+ company.delete(deletedBy);
+
+ // then
+ assertNotNull(company.getDeletedAt());
+ assertEquals(deletedBy, company.getDeletedBy());
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..7fb95fb1
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,154 @@
+services:
+ postgres:
+ image: postgres:15
+ container_name: my_postgres
+ environment:
+ POSTGRES_USER: admin
+ POSTGRES_PASSWORD: 1234
+ POSTGRES_DB: maindb
+ ports:
+ - "5001:5432"
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
+
+
+ eureka-server:
+ build:
+ context: . # 루트에서 빌드 (Gradle 파일 접근 가능)
+ dockerfile: eureka/Dockerfile.dev
+ container_name: eureka-server
+ ports:
+ - "8761:8761"
+ networks:
+ - msa-network
+
+ gateway-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: gateway/Dockerfile.dev
+ container_name: gateway-service
+ ports:
+ - "8080:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+
+ company-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: company-service/Dockerfile.dev
+ container_name: company-service
+ ports:
+ - "8085:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+
+ hub-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: hub-service/Dockerfile.dev
+ container_name: hub-service
+ ports:
+ - "8082:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+
+ order-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: order-service/Dockerfile.dev
+ container_name: order-service
+ ports:
+ - "8084:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+
+ payment-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: payment-service/Dockerfile.dev
+ container_name: payment-service
+ ports:
+ - "8086:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+ product-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: product-service/Dockerfile.dev
+ container_name: product-service
+ ports:
+ - "8083:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+ shipping-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: shipping-service/Dockerfile.dev
+ container_name: shipping-service
+ ports:
+ - "8088:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+
+ slack-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: slack-service/Dockerfile.dev
+ container_name: slack-service
+ ports:
+ - "8087:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+ user-service:
+ build:
+ context: . # 루트에서 빌드
+ dockerfile: user-service/Dockerfile.dev
+ container_name: user-service
+ ports:
+ - "8081:8080"
+ depends_on:
+ - eureka-server
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka
+ networks:
+ - msa-network
+
+networks:
+ msa-network:
+ driver: bridge
+
+volumes:
+ pgdata:
\ No newline at end of file
diff --git a/eureka/Dockerfile b/eureka/Dockerfile
new file mode 100644
index 00000000..c3126ffc
--- /dev/null
+++ b/eureka/Dockerfile
@@ -0,0 +1,16 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", "bootRun"]
+
diff --git a/eureka/Dockerfile.dev b/eureka/Dockerfile.dev
new file mode 100644
index 00000000..85af58c4
--- /dev/null
+++ b/eureka/Dockerfile.dev
@@ -0,0 +1,19 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# 루트 프로젝트에서 Gradle 관련 파일 복사
+COPY ../gradlew ../build.gradle ../settings.gradle ./
+COPY ../gradle gradle/
+
+# 실행 권한 추가
+RUN chmod +x gradlew
+
+# Gradle 의존성 다운로드
+RUN ./gradlew dependencies --no-daemon
+
+# Eureka 서버 코드 복사
+COPY . .
+
+# Eureka 서버 실행
+ENTRYPOINT ["./gradlew", ":eureka:bootRun"]
diff --git a/eureka/build.gradle b/eureka/build.gradle
new file mode 100644
index 00000000..818a8bc4
--- /dev/null
+++ b/eureka/build.gradle
@@ -0,0 +1,14 @@
+ext {
+ set('springCloudVersion', "2024.0.0")
+}
+
+dependencies {
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
+
diff --git a/eureka/src/main/java/com/sparta/eureka/EurekaApplication.java b/eureka/src/main/java/com/sparta/eureka/EurekaApplication.java
new file mode 100644
index 00000000..40c26fcc
--- /dev/null
+++ b/eureka/src/main/java/com/sparta/eureka/EurekaApplication.java
@@ -0,0 +1,15 @@
+package com.sparta.eureka;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
+
+@EnableEurekaServer
+@SpringBootApplication
+public class EurekaApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(EurekaApplication.class, args);
+ }
+
+}
diff --git a/eureka/src/main/resources/application.yml b/eureka/src/main/resources/application.yml
new file mode 100644
index 00000000..026ddae7
--- /dev/null
+++ b/eureka/src/main/resources/application.yml
@@ -0,0 +1,13 @@
+spring:
+ application:
+ name: eureka
+ autoconfigure:
+ exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
+
+server:
+ port: 8761
+
+eureka:
+ client:
+ register-with-eureka: false
+ fetch-registry: false
diff --git a/eureka/src/test/java/com/sparta/eureka/EurekaApplicationTests.java b/eureka/src/test/java/com/sparta/eureka/EurekaApplicationTests.java
new file mode 100644
index 00000000..0905de50
--- /dev/null
+++ b/eureka/src/test/java/com/sparta/eureka/EurekaApplicationTests.java
@@ -0,0 +1,13 @@
+package com.sparta.eureka;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class EurekaApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/gateway/Dockerfile b/gateway/Dockerfile
new file mode 100644
index 00000000..3e8680f8
--- /dev/null
+++ b/gateway/Dockerfile
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화 (빠른 빌드를 위해 종속성 미리 다운로드)
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 Gateway 실행 (소스 코드 변경 즉시 반영)
+ENTRYPOINT ["./gradlew", ":gateway:bootRun"]
diff --git a/gateway/Dockerfile.dev b/gateway/Dockerfile.dev
new file mode 100644
index 00000000..6d75ec7c
--- /dev/null
+++ b/gateway/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", ":gateway:bootRun"]
diff --git a/gateway/build.gradle b/gateway/build.gradle
index 9550d4f8..8af85ef2 100644
--- a/gateway/build.gradle
+++ b/gateway/build.gradle
@@ -3,8 +3,17 @@ ext {
}
dependencies {
+ implementation 'io.jsonwebtoken:jjwt:0.12.6'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+
+
+ implementation 'org.springframework.boot:spring-boot-starter-webflux'
+ implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.3.0'
}
dependencyManagement {
diff --git a/gateway/src/main/java/com/sparta/gateway/JwtAuthenticationFilter.java b/gateway/src/main/java/com/sparta/gateway/JwtAuthenticationFilter.java
new file mode 100644
index 00000000..e395b93d
--- /dev/null
+++ b/gateway/src/main/java/com/sparta/gateway/JwtAuthenticationFilter.java
@@ -0,0 +1,145 @@
+package com.sparta.gateway;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.SecretKey;
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+public class JwtAuthenticationFilter implements WebFilter {
+ private String secretKey;
+
+ public JwtAuthenticationFilter(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+
+ private static final List EXCLUDED_PATHS = List.of(
+ "/api/v1/users/sign-in",
+ "/api/v1/users/sign-up",
+ "/v3/api-docs",
+ "/swagger-ui.html",
+ "/swagger-ui",
+ "/webjars/swagger-ui"
+ );
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
+ String path = exchange.getRequest().getURI().getPath();
+
+ if (isExcludedPath(path)) {
+ log.info("***********" + path);
+ return chain.filter(exchange);
+ }
+
+ ServerHttpRequest request = exchange.getRequest();
+ ServerHttpResponse response = exchange.getResponse();
+
+ String token = extractToken(request); //토큰값을 Bearer 떼고 가져옴
+
+
+ if (token == null || !validateToken(token)) {
+ response.setStatusCode(HttpStatus.UNAUTHORIZED);
+ return exchange.getResponse().setComplete();
+ }
+
+ Claims claims = extractClaims(token); //사용자 정보 추출
+
+ //응답 헤더에 사용자 정보 반환처리
+ setAuthenticationHeader(claims,request);
+
+ Authentication authentication = setAuthenticationContext(claims);
+
+ return chain.filter(exchange).contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
+ }
+
+
+ private void setAuthenticationHeader(Claims claims, ServerHttpRequest request) {
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ String user_id = claims.get("user_id", String.class);
+ String role = claims.get("role", String.class);
+ String slack_name = claims.get("slack_name",String.class);
+
+ HttpHeaders headers = request.getHeaders();
+ headers.add("user_id", user_id);
+ headers.add("role", role);
+ headers.add("slack_name", slack_name);
+ }
+
+
+
+ private Claims extractClaims(String token) {
+ SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
+ //SecretKey 형태로 변환
+
+ return Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+
+
+ private String extractToken(ServerHttpRequest request) {
+ String authHeader = request.getHeaders().getFirst("Authorization");
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ return authHeader.substring(7);
+ }
+ return null;
+ }
+
+ private boolean validateToken(String token) {
+ try {
+ SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
+ Jws claimsJws = Jwts.parser()
+ .setSigningKey(key)
+ .build()
+ .parseClaimsJws(token);
+ log.info("#####payload :: " + claimsJws.getPayload().toString());
+
+ // 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
+ return true;
+ } catch (Exception e) {
+ log.error("Error extracting claims: " + e.getMessage());
+ return false;
+ }
+ }
+
+ private Authentication setAuthenticationContext(Claims claims) {
+ // JWT에서 사용자 정보와 권한 추출
+ String userId = claims.get("user_id", String.class);
+ String role = claims.get("role", String.class);
+
+ // 권한 설정
+ List authorities = Collections.singletonList(new SimpleGrantedAuthority(role));
+
+ // 인증 객체 생성 후 SecurityContext에 설정
+ return new UsernamePasswordAuthenticationToken(userId, null, authorities);
+ }
+
+
+ private boolean isExcludedPath(String path) {
+ return EXCLUDED_PATHS.stream().anyMatch(path::contains);
+ }
+}
diff --git a/gateway/src/main/java/com/sparta/gateway/config/SecurityConfig.java b/gateway/src/main/java/com/sparta/gateway/config/SecurityConfig.java
new file mode 100644
index 00000000..c49056e9
--- /dev/null
+++ b/gateway/src/main/java/com/sparta/gateway/config/SecurityConfig.java
@@ -0,0 +1,46 @@
+package com.sparta.gateway.config;
+
+import com.sparta.gateway.JwtAuthenticationFilter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
+
+@EnableWebFluxSecurity
+@Configuration
+public class SecurityConfig {
+
+ @Value("${service.jwt.secret-key}")
+ private String secretKey;
+
+ @Bean
+ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+ return http
+ .csrf(csrf -> csrf.disable())
+ .authorizeExchange(exchange -> exchange.anyExchange().permitAll() // 모든 요청 허용
+ /* .pathMatchers("/api/v1/users/**").permitAll()
+ .pathMatchers("/api/v1/companys/**").hasAnyRole("MASTER","COMPANY","SHIPPING")
+ .pathMatchers("/api/v1/shippings/**").hasAnyRole("MASTER","SHIPPING")
+ .pathMatchers("/api/v1/hubs/**").hasAnyRole("MASTER","HUB","SHIPPING")
+ .pathMatchers("/api/v1/products/**").hasAnyRole("MASTER","COMPANY","HUB")
+ .pathMatchers("/api/v1/orders/**").permitAll()
+ .pathMatchers("/api/v1/payments/**").hasAnyRole("MASTER","COMPANY","SHIPPING")
+ .pathMatchers("/api/v1/slacks/**").hasRole("COMPANY") // 권한 기반 접근
+ .anyExchange().authenticated()
+ )*/
+ )
+ .addFilterBefore(jwtAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
+ .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
+ .build();
+ }
+
+ private JwtAuthenticationFilter jwtAuthenticationFilter() {
+ return new JwtAuthenticationFilter(secretKey);
+ }
+
+
+}
diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml
index cb429342..e2eedee6 100644
--- a/gateway/src/main/resources/application.yml
+++ b/gateway/src/main/resources/application.yml
@@ -6,24 +6,137 @@ spring:
web-application-type: reactive
application:
name: gateway-service
+ autoconfigure:
+ exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
cloud:
gateway:
- routes: # Spring Cloud Gateway? ??? ??
- - id: user-service # ??? ???
- uri: lb://user-service # 'user-service'?? ???? ?? ???? ???? ???
- predicates:
- - Path=/api/v1/users/** # /api/v1/users/** ??? ???? ??? ? ???? ??
-# - id: product-service
-# uri: lb://product-service
-# predicates:
-# - Path=/api/v1/products/**
-# - id: auth-service
-# uri: lb://auth-service
-# predicates:
-# - Path=/api/v1/auth/**
+ routes: # Spring Cloud Gateway
+ # User
+ - id: user-service
+ uri: lb://user-service
+ predicates:
+ - Path=/api/v1/users/**
+ - id: user-docs
+ uri: lb://user-service
+ predicates:
+ - Path=/user-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/user-service/api/v1/users/(?.*), /api/v1/users/$\{segment}
+
+
+ # Order
+ - id: order-service
+ uri: lb://order-service
+ predicates:
+ - Path=/api/v1/orders/**
+ - id: order-docs
+ uri: lb://order-service
+ predicates:
+ - Path=/order-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/order-service/api/v1/orders/(?.*), /api/v1/orders/$\{segment}
+
+
+ # Company
+ - id: company-service
+ uri: lb://company-service
+ predicates:
+ - Path=/api/v1/companies/**
+ - id: company-docs
+ uri: lb://company-service
+ predicates:
+ - Path=/company-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/company-service/api/v1/companies/(?.*), /api/v1/companies/$\{segment}
+
+
+ # Hub
+ - id: hub-service
+ uri: lb://hub-service
+ predicates:
+ - Path=/api/v1/hubs/**
+ - id: hub-docs
+ uri: lb://hub-service
+ predicates:
+ - Path=/hub-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/hub-service/api/v1/hubs/(?.*), /api/v1/hubs/$\{segment}
+
+
+ # Product
+ - id: product-service
+ uri: lb://product-service
+ predicates:
+ - Path=/api/v1/products/**
+ - id: product-docs
+ uri: lb://product-service
+ predicates:
+ - Path=/product-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/product-service/api/v1/products/(?.*), /api/v1/products/$\{segment}
+
+
+ # Stock
+ - id: stock-service
+ uri: lb://stock-service
+ predicates:
+ - Path=/api/v1/stocks/**
+ - id: stock-docs
+ uri: lb://stock-service
+ predicates:
+ - Path=/stock-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/stock-service/api/v1/stocks/(?.*), /api/v1/stocks/$\{segment}
+
+
+ # Slack
+ - id: slack-service
+ uri: lb://slack-service
+ predicates:
+ - Path=/api/v1/slacks/**
+ - id: slack-docs
+ uri: lb://slack-service
+ predicates:
+ - Path=/slack-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/slack-service/api/v1/slacks/(?.*), /api/v1/slacks/$\{segment}
+
+
+ # Shipping
+ - id: shipping-service
+ uri: lb://shipping-service
+ predicates:
+ - Path=/api/v1/shippings/**
+ - id: shipping-docs
+ uri: lb://shipping-service
+ predicates:
+ - Path=/shipping-service/**
+ filters:
+ - RemoveRequestHeader=Cookie
+ - RewritePath=/shipping-service/api/v1/shippings/(?.*), /api/v1/shippings/$\{segment}
+
+
+ # Payment
+ - id: payment-service
+ uri: lb://payment-service
+ predicates:
+ - Path=/api/v1/payments/**
+ # - id: auth-service
+ # uri: lb://auth-service
+ # predicates:
+ # - Path=/api/v1/auth/**
discovery:
locator:
- enabled: true # ??? ?????? ?? ???? ???? ????? ??
+ enabled: true
+ loadbalancer:
+ enable: true
eureka:
client:
@@ -31,6 +144,41 @@ eureka:
defaultZone: http://localhost:8761/eureka/
-#service:
-# jwt:
-# secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
+service:
+ jwt:
+ secret-key: "50OsBNAaYDd8KJkGL8DCy8l8GFeJY--lzgxXrQJA-vUl1ZfivKtLNuwR_qNn2LJ6NkXpg8AAa2fe2CVUtN4UcQ"
+
+
+springdoc:
+ enable-native-support: true
+ api-docs:
+ enabled: true
+ path: /v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
+ use-root-path: true
+ urls[0]:
+ name: Order Service
+ url: /order-service/v3/api-docs
+ urls[1]:
+ name: Shipping Service
+ url: /shipping-service/v3/api-docs
+ urls[2]:
+ name: Hub Service
+ url: /hub-service/v3/api-docs
+ urls[3]:
+ name: Company Service
+ url: /company-service/v3/api-docs
+ urls[4]:
+ name: Slack Service
+ url: /slack-service/v3/api-docs
+ urls[5]:
+ name: User Service
+ url: /user-service/v3/api-docs
+ urls[6]:
+ name: Product Service
+ url: /product-service/v3/api-docs
+ urls[7]:
+ name: Stock Service
+ url: /stock-service/v3/api-docs
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/hub-service/Dockerfile.dev b/hub-service/Dockerfile.dev
new file mode 100644
index 00000000..d116b206
--- /dev/null
+++ b/hub-service/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", ":hub-service:bootRun"]
diff --git a/hub-service/build.gradle b/hub-service/build.gradle
new file mode 100644
index 00000000..6b82eaf8
--- /dev/null
+++ b/hub-service/build.gradle
@@ -0,0 +1,56 @@
+
+ext {
+ set('springCloudVersion', "2024.0.0")
+}
+
+dependencies {
+ // 공통 모듈
+ implementation project(':common-module')
+
+ // Swagger 에 필요한 의존성
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+
+ // eureka client
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ // feign client
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
+ // 유효성 검사
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ // Spring Boot 및 JPA 관련 의존성
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
+
+ // JGraphT : 그래프 자료구조 및 알고리즘 제공
+ implementation 'org.jgrapht:jgrapht-core:1.5.1'
+
+ // QueryDSL
+ implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
+ annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
+ annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
+ annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
+
+ implementation 'org.hibernate:hibernate-core:6.3.1.Final' // Hibernate ORM 라이브러리
+ implementation("org.postgresql:postgresql:42.7.2") // PostgreSQL JDBC 드라이버
+ runtimeOnly 'com.h2database:h2' // 개발/테스트 환경에서 H2 사용
+}
+
+// ✅ QueryDSL 자동 생성 디렉토리 설정
+tasks.withType(JavaCompile).configureEach {
+ options.annotationProcessorGeneratedSourcesDirectory = file("$buildDir/generated/querydsl")
+}
+
+// ✅ 자동 생성된 QueryDSL 클래스 경로 추가
+sourceSets {
+ main {
+ java {
+ srcDirs += "$buildDir/generated/querydsl"
+ }
+ }
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
\ No newline at end of file
diff --git a/hub-service/src/main/java/com/sparta/hubservice/HubServiceApplication.java b/hub-service/src/main/java/com/sparta/hubservice/HubServiceApplication.java
new file mode 100644
index 00000000..a31b7a13
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/HubServiceApplication.java
@@ -0,0 +1,18 @@
+package com.sparta.hubservice;
+
+import com.sparta.commonmodule.config.SwaggerConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Import;
+
+@SpringBootApplication(scanBasePackages = "com.sparta")
+@EnableFeignClients
+@Import(SwaggerConfig.class)
+public class HubServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(HubServiceApplication.class, args);
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/request/HubRequestDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/request/HubRequestDto.java
new file mode 100644
index 00000000..3b904b91
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/request/HubRequestDto.java
@@ -0,0 +1,21 @@
+package com.sparta.hubservice.hub.application.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class HubRequestDto {
+
+ @NotBlank(message = "허브 이름을 입력해주세요.")
+ private String name;
+
+ @NotBlank(message = "주소를 입력해주세요.")
+ private String address;
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/GeocodeResponse.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/GeocodeResponse.java
new file mode 100644
index 00000000..b265ad84
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/GeocodeResponse.java
@@ -0,0 +1,21 @@
+package com.sparta.hubservice.hub.application.dto.response;
+
+import java.util.List;
+import lombok.Data;
+import org.springframework.data.jpa.repository.query.Meta;
+
+@Data
+public class GeocodeResponse {
+ private String status;
+ private Meta meta;
+ private List addresses;
+ private String errorMessage;
+
+ @Data
+ public static class Address {
+ private String x; // longitude
+ private String y; // latitude
+ }
+
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubCreateResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubCreateResponseDto.java
new file mode 100644
index 00000000..0700de6a
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubCreateResponseDto.java
@@ -0,0 +1,36 @@
+package com.sparta.hubservice.hub.application.dto.response;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+@AllArgsConstructor
+public class HubCreateResponseDto {
+
+ private final UUID hubId;
+ private final String message;
+ private final String name;
+ private final String address;
+ private final BigDecimal latitud;
+ private final BigDecimal longitud;
+ private final LocalDateTime createdAt;
+ private final long createdBy;
+
+ public HubCreateResponseDto(Hub hub, String message) {
+ this.hubId = hub.getHubId();
+ this.message = message;
+ this.name = hub.getName();
+ this.address = hub.getAddress();
+ this.latitud = hub.getLatitude();
+ this.longitud = hub.getLongitude();
+ this.createdAt = LocalDateTime.now();
+ this.createdBy = hub.getCreatedBy();
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubDeleteResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubDeleteResponseDto.java
new file mode 100644
index 00000000..4650d0aa
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubDeleteResponseDto.java
@@ -0,0 +1,19 @@
+package com.sparta.hubservice.hub.application.dto.response;
+
+import java.util.UUID;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class HubDeleteResponseDto {
+
+ private final UUID hubId;
+ private final String message;
+
+ public HubDeleteResponseDto(UUID hubId, String message) {
+ this.hubId = hubId;
+ this.message = message;
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubResponseDto.java
new file mode 100644
index 00000000..9250b641
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubResponseDto.java
@@ -0,0 +1,38 @@
+package com.sparta.hubservice.hub.application.dto.response;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+@AllArgsConstructor
+public class HubResponseDto {
+
+ private final UUID hubId;
+ private final String name;
+ private final String address;
+ private final BigDecimal latitude;
+ private final BigDecimal longitude;
+ private final LocalDateTime createdAt;
+ private final long createdBy;
+ private final LocalDateTime updatedAt;
+ private final long updatedBy;
+
+ public HubResponseDto(Hub hub) {
+ this.hubId = hub.getHubId();
+ this.name = hub.getName();
+ this.address = hub.getAddress();
+ this.latitude = hub.getLatitude();
+ this.longitude = hub.getLongitude();
+ this.createdAt = hub.getCreatedAt();
+ this.createdBy = hub.getCreatedBy();
+ this.updatedAt = hub.getUpdatedAt();
+ this.updatedBy = hub.getUpdatedBy();
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubUpdateResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubUpdateResponseDto.java
new file mode 100644
index 00000000..7b248915
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/dto/response/HubUpdateResponseDto.java
@@ -0,0 +1,34 @@
+package com.sparta.hubservice.hub.application.dto.response;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+@AllArgsConstructor
+public class HubUpdateResponseDto {
+
+ private final UUID hubId;
+ private final String message;
+ private final String address;
+ private final BigDecimal latitude;
+ private final BigDecimal longitude;
+ private final LocalDateTime updatedAt;
+ private final long updatedBy;
+
+ public HubUpdateResponseDto(Hub hub, String message) {
+ this.hubId = hub.getHubId();
+ this.message = message;
+ this.address = hub.getAddress();
+ this.latitude = hub.getLatitude();
+ this.longitude = hub.getLongitude();
+ this.updatedAt = LocalDateTime.now();
+ this.updatedBy = hub.getCreatedBy();
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/GeocodeApiService.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/GeocodeApiService.java
new file mode 100644
index 00000000..1d434d71
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/GeocodeApiService.java
@@ -0,0 +1,62 @@
+package com.sparta.hubservice.hub.application.service;
+
+
+import com.sparta.commonmodule.exception.OperationNotAllowedException;
+import com.sparta.hubservice.hub.application.dto.response.GeocodeResponse;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+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;
+
+@Service
+@Slf4j(topic = "GeocodeApiService : naver geocode api 호출")
+public class GeocodeApiService {
+
+ @Value("${naver.api.geocode.url}")
+ private String geocodeUrl;
+
+ @Value("${naver.api.geocode.client-id}")
+ private String clientId;
+
+ @Value("${naver.api.geocode.client-secret}")
+ private String clientSecret;
+
+
+ private final RestTemplate restTemplate = new RestTemplate();
+
+ public Map getGeocodeAddress(String address) {
+ // URL 설정
+ String uri = geocodeUrl + "?query=" + address;
+
+ // Http 요청 헤더 설정
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("X-NCP-APIGW-API-KEY-ID", clientId);
+ headers.set("X-NCP-APIGW-API-KEY", clientSecret);
+ headers.set("Content-Type", "application/json; charset=UTF-8");
+ headers.set("Accept", "application/json");
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ // API 호출 및 응답 받기
+ ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, entity, GeocodeResponse.class);
+
+ if(response.getBody() == null || response.getBody().getAddresses() == null) {
+ throw new OperationNotAllowedException();
+ }
+
+ // 위도, 경도 값 추출
+ Map result = new HashMap<>();
+
+ result.put("latitude", new BigDecimal(response.getBody().getAddresses().get(0).getY()));
+ result.put("longitude", new BigDecimal(response.getBody().getAddresses().get(0).getX()));
+
+ return result;
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/HubService.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/HubService.java
new file mode 100644
index 00000000..bcff0c7f
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/HubService.java
@@ -0,0 +1,92 @@
+package com.sparta.hubservice.hub.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.hubservice.hub.application.dto.request.HubRequestDto;
+import com.sparta.hubservice.hub.application.dto.response.HubCreateResponseDto;
+import com.sparta.hubservice.hub.application.dto.response.HubDeleteResponseDto;
+import com.sparta.hubservice.hub.application.dto.response.HubResponseDto;
+import com.sparta.hubservice.hub.application.dto.response.HubUpdateResponseDto;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub.domain.repository.HubRepository;
+import java.math.BigDecimal;
+import java.util.Map;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class HubService {
+
+ private final GeocodeApiService geocodeApiService;
+ private final HubRepository hubRepository;
+
+ // 허브 목록 조회
+ @Transactional(readOnly = true)
+ public Page getHubs(Pageable pageable) {
+ Page hubPages = hubRepository.findByIsDeletedFalse(pageable);
+
+ return hubPages.map(HubResponseDto::new);
+ }
+
+ // 특정 허브 조회
+ @Transactional(readOnly = true)
+ public HubResponseDto getHub(UUID hubId) {
+ Hub hubDetail = hubRepository.findById(hubId).orElseThrow(ResourceNotFoundException::new);
+ return new HubResponseDto(hubDetail);
+ }
+
+ // 허브 검색
+ @Transactional(readOnly = true)
+ public Page getSearchHubs(String name, String address, Pageable pageable) {
+ Page searchHubs = hubRepository.searchByKeyword(name, address, pageable)
+ .orElseThrow(ResourceNotFoundException::new);
+
+ return searchHubs.map(HubResponseDto::new);
+ }
+
+ // 허브 생성
+ @Transactional
+ public HubCreateResponseDto createHub(HubRequestDto hubRequestDto, Long userId) {
+
+ // 주소 -> 위,경도값 변환
+ Map map = geocodeApiService.getGeocodeAddress(hubRequestDto.getAddress());
+
+ Hub createHub = Hub.builder()
+ .name(hubRequestDto.getName())
+ .address(hubRequestDto.getAddress())
+ .latitude(map.get("latitude"))
+ .longitude(map.get("longitude"))
+ .userId(userId)
+ .build();
+
+ hubRepository.save(createHub);
+ return new HubCreateResponseDto(createHub, "Hub successfully created.");
+ }
+
+ // 허브 수정
+ @Transactional
+ public HubUpdateResponseDto updateHub(UUID hubId, String address, Long userId) {
+ Hub hub = hubRepository.findById(hubId).orElseThrow(ResourceNotFoundException::new);
+
+ // 주소 -> 위, 경도값 변환
+ Map map = geocodeApiService.getGeocodeAddress(hub.getAddress());
+
+ hub.updateHub(address, map.get("latitude"), map.get("longitude"), userId);
+ return new HubUpdateResponseDto(hub,"Hub successfully updated.");
+ }
+
+ // 허브삭제 (Soft Delete)
+ @Transactional
+ public HubDeleteResponseDto deleteHub(UUID hubId, Long userId) {
+ Hub hub = hubRepository.findById(hubId).orElseThrow(ResourceNotFoundException::new);
+
+ hub.delete(userId);
+ return new HubDeleteResponseDto(hub.getHubId(), "Hub successfully deleted.");
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/HubShippingScanService.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/HubShippingScanService.java
new file mode 100644
index 00000000..e90671a5
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/HubShippingScanService.java
@@ -0,0 +1,60 @@
+package com.sparta.hubservice.hub.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.hubservice.hub.application.service.shipping.ShippingService;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog;
+import com.sparta.hubservice.hub.domain.repository.HubRepository;
+import com.sparta.hubservice.hub.domain.repository.HubShippingScanRepository;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.InboundStatusResponseDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.OutboundStatusResponseDto;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@RequiredArgsConstructor
+@Service
+@Slf4j(topic = "HubShippingScanService")
+public class HubShippingScanService {
+
+ private final HubShippingScanRepository hubShippingScanRepository;
+ private final HubRepository hubRepository;
+ private final ShippingService shippingService;
+
+ // 입고 처리 로그 저장 feign client 호출
+ @Transactional
+ public InboundStatusResponseDto createInbound(UUID hubId, UUID shippingId, Long userId) {
+ Hub hub = hubRepository.findById(hubId).orElseThrow(ResourceNotFoundException::new);
+
+ HubShippingScanLog hubShippingScanLog =
+ HubShippingScanLog.createInboundLog(hub, shippingId, userId);
+
+ hubShippingScanRepository.save(hubShippingScanLog);
+ log.info("Success Save InboundLog - HubShippingScanLog: {}", hubShippingScanLog);
+
+ return shippingService.inboundStatus(hubShippingScanLog, userId);
+ }
+
+ // 출고 처리 로그 저장 후 feign client 호출
+ @Transactional
+ public OutboundStatusResponseDto createOutbound(UUID hubId, UUID shippingId, Long userId) {
+ Hub hub = hubRepository.findById(hubId).orElseThrow(ResourceNotFoundException::new);
+
+ HubShippingScanLog hubShippingScanLog =
+ HubShippingScanLog.createOutboundLog(hub, shippingId, userId);
+
+ hubShippingScanRepository.save(hubShippingScanLog);
+ log.info("Success Save OutboundLog - HubShippingScanLog: {}", hubShippingScanLog);
+
+ // feign client
+ OutboundStatusResponseDto responseDto = shippingService.outboundStatus(hubShippingScanLog, userId);
+
+ // 받아온 정보로 nextHub 값 update
+ Hub nextHub = hubRepository.findById(responseDto.getNextHubId()).orElseThrow(ResourceNotFoundException::new);
+ hubShippingScanLog.updateNextHub(nextHub);
+
+ return responseDto;
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/shipping/ShippingService.java b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/shipping/ShippingService.java
new file mode 100644
index 00000000..ce0b7692
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/application/service/shipping/ShippingService.java
@@ -0,0 +1,27 @@
+package com.sparta.hubservice.hub.application.service.shipping;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog;
+import com.sparta.hubservice.hub.infrastructure.feignclient.ShippingFeignClient;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.InboundStatusRequestDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.InboundStatusResponseDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.OutboundStatusRequestDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.OutboundStatusResponseDto;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j(topic = "ShippingService : call shippingFeignClient")
+public class ShippingService {
+
+ private final ShippingFeignClient shippingFeignClient;
+
+ public InboundStatusResponseDto inboundStatus(HubShippingScanLog log, Long userId) {
+ return shippingFeignClient.inboundStatus(log.getShippingId(), userId, new InboundStatusRequestDto(log));
+ }
+
+ public OutboundStatusResponseDto outboundStatus(HubShippingScanLog log, Long userId) {
+ return shippingFeignClient.outboundStatus(log.getShippingId(),userId, new OutboundStatusRequestDto(log));
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/config/JpaConfig.java b/hub-service/src/main/java/com/sparta/hubservice/hub/config/JpaConfig.java
new file mode 100644
index 00000000..eafbfc1a
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/config/JpaConfig.java
@@ -0,0 +1,19 @@
+package com.sparta.hubservice.hub.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class JpaConfig {
+
+ @PersistenceContext // ✅ EntityManager가 자동으로 주입됨
+ private EntityManager entityManager;
+
+ @Bean
+ public JPAQueryFactory queryFactory() {
+ return new JPAQueryFactory(entityManager); // ✅ QueryDSL이 사용할 JPAQueryFactory Bean 등록
+ }
+}
\ No newline at end of file
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/domain/model/Hub.java b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/model/Hub.java
new file mode 100644
index 00000000..3474230b
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/model/Hub.java
@@ -0,0 +1,69 @@
+package com.sparta.hubservice.hub.domain.model;
+
+import static org.springframework.data.jpa.domain.AbstractPersistable_.id;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.*;
+import jakarta.validation.constraints.Digits;
+import java.math.BigDecimal;
+import java.util.Objects;
+import java.util.UUID;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor
+@Table(name = "p_hub")
+public class Hub extends BaseEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "hub_id", nullable = false)
+ private UUID hubId;
+
+ @Column(unique = true, nullable = false, length = 255)
+ private String name;
+
+ @Column(nullable = false, length = 255)
+ private String address;
+
+ @Column(nullable = false)
+ @Digits(integer = 10, fraction = 8)
+ private BigDecimal latitude;
+
+ @Column(nullable = false)
+ @Digits(integer = 10, fraction = 8)
+ private BigDecimal longitude;
+
+ @Builder
+ public Hub(String name, String address, BigDecimal latitude, BigDecimal longitude, long userId) {
+ super(userId);
+ this.name = name;
+ this.address = address;
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+
+ public void updateHub(String address, BigDecimal latitude, BigDecimal longitude, long userId){
+ super.update(userId);
+ this.address = address;
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Hub)) return false;
+ Hub other = (Hub) o;
+ return Objects.equals(this.hubId, other.hubId); // 또는 비즈니스 키
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hubId); // id 기반으로
+ }
+
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/domain/model/HubShippingScanLog.java b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/model/HubShippingScanLog.java
new file mode 100644
index 00000000..826d2011
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/model/HubShippingScanLog.java
@@ -0,0 +1,79 @@
+package com.sparta.hubservice.hub.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+
+@Entity
+@Getter
+@NoArgsConstructor
+@Table(name = "p_hub_shipping_scan_log")
+public class HubShippingScanLog extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "hub_shipping_scan_log_id")
+ private UUID hubShippingScanLogId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name ="hub_id", nullable = false)
+ private Hub hub;
+
+ @Column(name = "shipping_id", nullable = false)
+ private UUID shippingId;
+
+ @CreatedDate
+ @Column(nullable = false, updatable = false)
+ private LocalDateTime timestamp;
+
+ @Column(nullable = false)
+ private ShippingStatus status;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "next_hub_id", nullable = true)
+ private Hub nextHub;
+
+ public HubShippingScanLog(Hub hub,UUID shippingId, ShippingStatus status, Long userId) {
+ super(userId);
+ this.hub = hub;
+ this.shippingId = shippingId;
+ this.status = status;
+ }
+
+ public HubShippingScanLog(Hub hub,UUID shippingId, ShippingStatus status, Hub nextHub, Long userId) {
+ super(userId);
+ this.hub = hub;
+ this.shippingId = shippingId;
+ this.status = status;
+ this.nextHub = nextHub;
+ }
+
+ public static HubShippingScanLog createInboundLog(Hub hub, UUID shippingId, Long userId) {
+ return new HubShippingScanLog(hub,shippingId, ShippingStatus.INBOUND, userId);
+ }
+
+ public static HubShippingScanLog createOutboundLog(Hub hub, UUID shippingId, Long userId) {
+ return new HubShippingScanLog(hub, shippingId, ShippingStatus.OUTBOUND, userId);
+ }
+
+ public void updateNextHub(Hub nextHub) {
+ this.nextHub = nextHub;
+ }
+
+ public enum ShippingStatus {
+ INBOUND, OUTBOUND
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/domain/repository/HubRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/repository/HubRepository.java
new file mode 100644
index 00000000..8355332e
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/repository/HubRepository.java
@@ -0,0 +1,20 @@
+package com.sparta.hubservice.hub.domain.repository;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface HubRepository {
+ Page findByIsDeletedFalse(Pageable pageable);
+
+ Optional findById(UUID hubId);
+
+ S save(S hub);
+
+ List findAll();
+
+ Optional> searchByKeyword(String name, String address, Pageable pageable);
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/domain/repository/HubShippingScanRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/repository/HubShippingScanRepository.java
new file mode 100644
index 00000000..3b86fcdc
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/domain/repository/HubShippingScanRepository.java
@@ -0,0 +1,8 @@
+package com.sparta.hubservice.hub.domain.repository;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog;
+
+public interface HubShippingScanRepository {
+
+ void save(HubShippingScanLog hubShippingScanLog);
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/ShippingFeignClient.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/ShippingFeignClient.java
new file mode 100644
index 00000000..b2159946
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/ShippingFeignClient.java
@@ -0,0 +1,30 @@
+package com.sparta.hubservice.hub.infrastructure.feignclient;
+
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.InboundStatusRequestDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.InboundStatusResponseDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.OutboundStatusRequestDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.OutboundStatusResponseDto;
+import java.util.UUID;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+
+@FeignClient(name ="shipping-service", url="${shipping.service.url}")
+public interface ShippingFeignClient {
+
+ // 입고 처리 내용 전달
+ @PostMapping("/{shipping_id}/inbound")
+ InboundStatusResponseDto inboundStatus(
+ @PathVariable("shipping_id") UUID shippingId,
+ @RequestHeader("user_id") Long userId,
+ @RequestBody InboundStatusRequestDto inboundStatusRequestDto);
+
+ // 출고 처리 내용 전달
+ @PostMapping("/{shipping_id}/outbound")
+ OutboundStatusResponseDto outboundStatus(
+ @PathVariable("shipping_id") UUID shippingId,
+ @RequestHeader("user_id") Long userId,
+ @RequestBody OutboundStatusRequestDto outboundStatusRequestDto);
+}
\ No newline at end of file
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/InboundStatusRequestDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/InboundStatusRequestDto.java
new file mode 100644
index 00000000..236009c2
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/InboundStatusRequestDto.java
@@ -0,0 +1,28 @@
+package com.sparta.hubservice.hub.infrastructure.feignclient.dto;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog;
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog.ShippingStatus;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class InboundStatusRequestDto {
+
+ private UUID hubId;
+ private ShippingStatus status;
+ private LocalDateTime timestamp;
+
+ public InboundStatusRequestDto(HubShippingScanLog hubShippingScanLog) {
+ this.hubId = hubShippingScanLog.getHub().getHubId();
+ this.status = hubShippingScanLog.getStatus();
+ this.timestamp = hubShippingScanLog.getTimestamp();
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/InboundStatusResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/InboundStatusResponseDto.java
new file mode 100644
index 00000000..9c39c6c5
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/InboundStatusResponseDto.java
@@ -0,0 +1,24 @@
+package com.sparta.hubservice.hub.infrastructure.feignclient.dto;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog.ShippingStatus;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class InboundStatusResponseDto {
+
+ private String message;
+ private UUID hubId;
+ private UUID shippingId;
+ private ShippingStatus shippingStatus;
+ private LocalDateTime timestamp;
+
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/OutboundStatusRequestDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/OutboundStatusRequestDto.java
new file mode 100644
index 00000000..467613ae
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/OutboundStatusRequestDto.java
@@ -0,0 +1,29 @@
+package com.sparta.hubservice.hub.infrastructure.feignclient.dto;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog;
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog.ShippingStatus;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OutboundStatusRequestDto {
+
+ private UUID hubId;
+ private ShippingStatus shippingStatus;
+ private LocalDateTime timestamp;
+ private UUID nextHubId;
+
+ public OutboundStatusRequestDto(HubShippingScanLog log) {
+ this.hubId = log.getHub().getHubId();
+ this.shippingStatus = log.getStatus();
+ this.timestamp = log.getTimestamp();
+ this.nextHubId = log.getNextHub().getHubId();
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/OutboundStatusResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/OutboundStatusResponseDto.java
new file mode 100644
index 00000000..ca36dbcf
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/feignclient/dto/OutboundStatusResponseDto.java
@@ -0,0 +1,24 @@
+package com.sparta.hubservice.hub.infrastructure.feignclient.dto;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog.ShippingStatus;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OutboundStatusResponseDto {
+
+ private String message;
+ private UUID hubId;
+ private UUID shippingId;
+ private ShippingStatus shippingStatus;
+ private LocalDateTime timestamp;
+ private UUID nextHubId;
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/JPAHubRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/JPAHubRepository.java
new file mode 100644
index 00000000..e19ee2c7
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/JPAHubRepository.java
@@ -0,0 +1,14 @@
+package com.sparta.hubservice.hub.infrastructure.persistence;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub.domain.repository.HubRepository;
+import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface JPAHubRepository extends JpaRepository {
+ Page findByIsDeletedFalse(Pageable pageable);
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/JPAHubShippingScanRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/JPAHubShippingScanRepository.java
new file mode 100644
index 00000000..2e3e5162
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/JPAHubShippingScanRepository.java
@@ -0,0 +1,11 @@
+package com.sparta.hubservice.hub.infrastructure.persistence;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface JPAHubShippingScanRepository extends JpaRepository {
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/QueryDSLHubRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/QueryDSLHubRepository.java
new file mode 100644
index 00000000..19327c83
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/persistence/QueryDSLHubRepository.java
@@ -0,0 +1,50 @@
+package com.sparta.hubservice.hub.infrastructure.persistence;
+
+import com.querydsl.core.BooleanBuilder;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub.domain.model.QHub;
+import java.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class QueryDSLHubRepository{
+
+ private final JPAQueryFactory queryFactory;
+
+ public Optional> searchByKeyword(String name, String address, Pageable pageable) {
+ QHub hub = QHub.hub;
+
+ BooleanBuilder predicate = new BooleanBuilder();
+
+ if(name != null && !name.isEmpty()) {
+ predicate.or(hub.name.containsIgnoreCase(name));
+ }
+ if(address != null && !address.isEmpty()) {
+ predicate.or(hub.address.containsIgnoreCase(address));
+ }
+
+ List hubList = queryFactory
+ .selectFrom(hub)
+ .where(predicate
+ .and(hub.isDeleted.eq(false)))
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize())
+ .fetch();
+
+ // 페이징 처리
+ long total = queryFactory
+ .selectFrom(hub)
+ .where(predicate
+ .and(hub.isDeleted.eq(false)))
+ .fetchCount();
+
+ return Optional.of(new PageImpl<>(hubList, pageable, total));
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/repository/HubRepositoryImpl.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/repository/HubRepositoryImpl.java
new file mode 100644
index 00000000..9d7afdb5
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/repository/HubRepositoryImpl.java
@@ -0,0 +1,47 @@
+package com.sparta.hubservice.hub.infrastructure.repository;
+
+import com.sparta.hubservice.hub.application.service.HubService;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub.domain.repository.HubRepository;
+import com.sparta.hubservice.hub.infrastructure.persistence.JPAHubRepository;
+import com.sparta.hubservice.hub.infrastructure.persistence.QueryDSLHubRepository;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class HubRepositoryImpl implements HubRepository {
+
+ private final JPAHubRepository jpaHubRepository;
+ private final QueryDSLHubRepository queryDSLHubRepository;
+
+ @Override
+ public Page findByIsDeletedFalse(Pageable pageable) {
+ return jpaHubRepository.findByIsDeletedFalse(pageable);
+ }
+
+ @Override
+ public Optional findById(UUID hubId) {
+ return jpaHubRepository.findById(hubId);
+ }
+
+ @Override
+ public Hub save(Hub hub) {
+ return jpaHubRepository.save(hub);
+ }
+
+ @Override
+ public List findAll() {
+ return jpaHubRepository.findAll();
+ }
+
+ @Override
+ public Optional> searchByKeyword(String name, String address, Pageable pageable) {
+ return queryDSLHubRepository.searchByKeyword(name, address, pageable);
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/repository/HubShippingScanRepositoryImpl.java b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/repository/HubShippingScanRepositoryImpl.java
new file mode 100644
index 00000000..bea36db6
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/infrastructure/repository/HubShippingScanRepositoryImpl.java
@@ -0,0 +1,21 @@
+package com.sparta.hubservice.hub.infrastructure.repository;
+
+import com.sparta.hubservice.hub.domain.model.HubShippingScanLog;
+import com.sparta.hubservice.hub.domain.repository.HubShippingScanRepository;
+import com.sparta.hubservice.hub.infrastructure.persistence.JPAHubShippingScanRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class HubShippingScanRepositoryImpl implements HubShippingScanRepository {
+
+ private final JPAHubShippingScanRepository jpaHubShippingScanRepository;
+
+ @Override
+ public void save(HubShippingScanLog hubShippingScanLog) {
+ jpaHubShippingScanRepository.save(hubShippingScanLog);
+ }
+
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/presentation/controller/HubController.java b/hub-service/src/main/java/com/sparta/hubservice/hub/presentation/controller/HubController.java
new file mode 100644
index 00000000..772a052e
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/presentation/controller/HubController.java
@@ -0,0 +1,102 @@
+package com.sparta.hubservice.hub.presentation.controller;
+
+import com.sparta.commonmodule.aop.RoleCheck;
+import com.sparta.hubservice.hub.application.dto.request.HubRequestDto;
+import com.sparta.hubservice.hub.application.dto.response.HubCreateResponseDto;
+import com.sparta.hubservice.hub.application.dto.response.HubDeleteResponseDto;
+import com.sparta.hubservice.hub.application.dto.response.HubResponseDto;
+import com.sparta.hubservice.hub.application.dto.response.HubUpdateResponseDto;
+import com.sparta.hubservice.hub.application.service.HubService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/hubs")
+@RequiredArgsConstructor
+@Tag(name = "Hub Service", description = "허브 서비스 API")
+public class HubController {
+
+ private final HubService hubService;
+
+ // 허브 전체 조회
+ @Operation(summary = "Hub 전체 조회", description = "허브 전체 조회 api")
+ @GetMapping
+ public ResponseEntity> getHubs(
+ @PageableDefault(page = 0, size = 30, sort = "createdAt") Pageable pageable) {
+ Page responseDtos = hubService.getHubs(pageable);
+ return ResponseEntity.ok(responseDtos);
+ }
+
+ // 특정 허브 조회
+ @Operation(summary = "Hub 단일 조회", description = "허브 단일 조회 api")
+ @GetMapping("/{hub_id}")
+ public ResponseEntity getHubById(@PathVariable("hub_id") UUID hubId) {
+ HubResponseDto responseDto = hubService.getHub(hubId);
+ return ResponseEntity.ok(responseDto);
+ }
+
+ // 허브 검색
+ @Operation(summary = "Hub 검색", description = "허브 검색 api")
+ @GetMapping("/search")
+ public ResponseEntity> getSearchHubs(
+ @RequestParam(required = false) String name,
+ @RequestParam(required = false) String address,
+ @PageableDefault(page = 0, size = 30, sort = "createdAt") Pageable pageable){
+ Page responseDto = hubService.getSearchHubs(name, address, pageable);
+ return ResponseEntity.ok(responseDto);
+ }
+
+ // 허브 생성
+ @Operation(summary = "Hub 생성", description = "허브 생성 api")
+ @RoleCheck("ROLE_MASTER")
+ @PostMapping
+ public ResponseEntity createHub(
+ @RequestBody @Valid HubRequestDto requestDto,
+ @RequestHeader("user_id") Long userId) {
+ HubCreateResponseDto responseDto = hubService.createHub(requestDto, userId);
+ return ResponseEntity.ok(responseDto);
+ }
+
+ // 허브 수정
+ @Operation(summary = "Hub 수정", description = "허브 수정 api")
+
+ @RoleCheck("ROLE_MASTER")
+ @PutMapping("{hub_id}")
+ public ResponseEntity updateHub(
+ @PathVariable("hub_id") UUID hubId,
+ @RequestParam String address,
+ @RequestHeader("user_id") Long userId) {
+
+ HubUpdateResponseDto responseDto = hubService.updateHub(hubId, address, userId);
+ return ResponseEntity.ok(responseDto);
+ }
+
+ // 허브 삭제
+ @Operation(summary = "Hub 삭제", description = "허브 삭제 api")
+ @RoleCheck("ROLE_MASTER")
+ @DeleteMapping("{hub_id}")
+ public ResponseEntity deleteHub(@PathVariable("hub_id") UUID hubId, @RequestHeader("user_id") Long userId) {
+ HubDeleteResponseDto responseDto = hubService.deleteHub(hubId, userId);
+ return ResponseEntity.ok(responseDto);
+ }
+
+
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub/presentation/controller/HubShippingScanController.java b/hub-service/src/main/java/com/sparta/hubservice/hub/presentation/controller/HubShippingScanController.java
new file mode 100644
index 00000000..8ff8356a
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub/presentation/controller/HubShippingScanController.java
@@ -0,0 +1,53 @@
+package com.sparta.hubservice.hub.presentation.controller;
+
+import com.sparta.hubservice.hub.application.service.HubShippingScanService;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.InboundStatusResponseDto;
+import com.sparta.hubservice.hub.infrastructure.feignclient.dto.OutboundStatusResponseDto;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/hubs/hub-shipping-scan")
+@Tag(name = "Hub Service", description = "허브 서비스 API")
+public class HubShippingScanController {
+
+ private final HubShippingScanService hubShippingScanService;
+
+ // 입고 처리 저장
+ @Operation(summary = "Hub - 상품 입고처리", description = "허브 내 상품 입고처리 api")
+ @PostMapping("/{hub_id}/{shipping_id}/inbound-log")
+ public ResponseEntity inboundStatus(
+ @PathVariable("hub_id") UUID hubId,
+ @PathVariable("shipping_id") UUID shippingId,
+ @RequestHeader("user_id") Long userId){
+
+ InboundStatusResponseDto responseDto =
+ hubShippingScanService.createInbound(hubId, shippingId, userId);
+
+ return ResponseEntity.ok(responseDto);
+ }
+
+ // 출고 처리 저장
+ @Operation(summary = "Hub - 상품 출고처리", description = "허브 내 상품 출고처리 api")
+ @PostMapping("/{hub_id}/{shipping_id}/outbound-log")
+ public ResponseEntity outboundStatus(
+ @PathVariable("hub_id") UUID hubId,
+ @PathVariable("shipping_id") UUID shippingId,
+ @RequestHeader("user_id") Long userId
+ ){
+ OutboundStatusResponseDto responseDto =
+ hubShippingScanService.createOutbound(hubId, shippingId, userId);
+
+ return ResponseEntity.ok(responseDto);
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/DijkstraPathFinder.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/DijkstraPathFinder.java
new file mode 100644
index 00000000..6a0c4d2f
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/DijkstraPathFinder.java
@@ -0,0 +1,91 @@
+package com.sparta.hubservice.hub_route.application.dijkstra;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+import lombok.Getter;
+
+@Getter
+class HubNode implements Comparable {
+ private final Hub hub;
+ private final BigDecimal distance;
+ private final Hub beforeHub;
+
+ public HubNode(Hub hub, BigDecimal distance, Hub beforeHub) {
+ this.hub = hub;
+ this.distance = distance;
+ this.beforeHub = beforeHub;
+ }
+
+ @Override
+ public int compareTo(HubNode other) {
+ return this.distance.compareTo(other.distance);
+ }
+}
+
+public class DijkstraPathFinder implements PathFinder {
+
+ private final Map> graph;
+
+ public DijkstraPathFinder(Map> graph) {
+ this.graph = graph;
+ }
+
+ @Override
+ public List getShortPath(Hub start, Hub end) {
+
+ Map distances = new HashMap<>();
+ Map beforeHub = new HashMap<>();
+ Set visited = new HashSet<>();
+ PriorityQueue queue = new PriorityQueue<>();
+
+ distances.put(start, BigDecimal.ZERO);
+ queue.offer(new HubNode(start, BigDecimal.ZERO, null));
+
+ while (!queue.isEmpty()) {
+ HubNode currentNode = queue.poll();
+ Hub currentHub = currentNode.getHub();
+
+ if (currentHub.equals(end)) break;
+
+ if (visited.contains(currentHub)) continue;
+ visited.add(currentHub);
+
+ // 현재 허브에서 연결된 모든 허브 경로 가져오기
+ List routes = graph.getOrDefault(currentHub, List.of());
+
+
+ for (HubRoute route : routes) {
+ Hub nextHub = route.getToHub();
+ BigDecimal newDistance = distances.get(currentHub).add(route.getDistance());
+
+ // nextHub의 거리정보가 없거나, 기존 경로보다 짧으면 값 변경
+ if(!distances.containsKey(nextHub) ||
+ newDistance.compareTo(distances.get(nextHub)) < 0){
+
+ distances.put(nextHub, newDistance);
+ beforeHub.put(nextHub, currentHub);
+ queue.offer(new HubNode(nextHub, newDistance, currentHub));
+
+ }
+ }
+ }
+ // 최단 경로 시퀀스
+ List sequence = new LinkedList<>();
+ Hub current = end;
+ while (current != null) {
+ sequence.add(0, current);
+ current = beforeHub.get(current);
+ }
+
+ return sequence;
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/JGraphTPathFinder.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/JGraphTPathFinder.java
new file mode 100644
index 00000000..9253d9b7
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/JGraphTPathFinder.java
@@ -0,0 +1,52 @@
+package com.sparta.hubservice.hub_route.application.dijkstra;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import java.util.List;
+import java.util.Map;
+import org.jgrapht.Graph;
+import org.jgrapht.GraphPath;
+import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
+import org.jgrapht.graph.DefaultDirectedWeightedGraph;
+
+public class JGraphTPathFinder implements PathFinder {
+
+ private final Graph graph; // Graph
+
+ public JGraphTPathFinder(Map> graphMap) {
+
+ Graph graph = new DefaultDirectedWeightedGraph<>(HubRoute.class);
+
+ for(Hub fromHub : graphMap.keySet()) {
+ // from 꼭짓점 추가
+ graph.addVertex(fromHub);
+ List routes = graphMap.get(fromHub);
+
+ for(HubRoute route : routes) {
+ // to 꼭짓점 추가
+ graph.addVertex(route.getToHub());
+ // graph(fromV, toV, e)
+ graph.addEdge(fromHub, route.getToHub(), route);
+ // e 에 대한 가중치 설정
+ graph.setEdgeWeight(route, route.getDistance().doubleValue());
+ }
+ }
+
+ this.graph = graph;
+ }
+
+ // 경로 시퀀스
+ @Override
+ public List getShortPath(Hub start, Hub end){
+ GraphPath path = new DijkstraShortestPath<>(graph).getPath(start, end);
+
+ if (path == null) {
+ throw new IllegalArgumentException(String.format(
+ "허브 간 최단 경로가 존재하지 않습니다 : %s → %s", start.getName(), end.getName()
+ ));
+ }
+
+ return path.getVertexList();
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/PathCalculate.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/PathCalculate.java
new file mode 100644
index 00000000..ee164291
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/PathCalculate.java
@@ -0,0 +1,83 @@
+package com.sparta.hubservice.hub_route.application.dijkstra;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub.domain.repository.HubRepository;
+import com.sparta.hubservice.hub_route.application.dto.serviceDto.PathValueDto;
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import com.sparta.hubservice.hub_route.domain.repository.HubRouteRepository;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class PathCalculate {
+
+ private final HubRouteRepository hubRouteRepository;
+ private final HubRepository hubRepository;
+
+ public List getShortPath(Hub fromHub, Hub toHub) {
+
+ // 그래프 정의
+ Map> graph = new HashMap<>();
+ List hubs = hubRepository.findAll();
+ for (Hub hub : hubs) {
+ List routes = hubRouteRepository.findByFromHub(hub)
+ .orElse(Collections.emptyList());
+
+ graph.putIfAbsent(hub, new ArrayList<>());
+ graph.get(hub).addAll(routes);
+
+ for (HubRoute route : routes) {
+ Hub to = route.getToHub();
+ graph.putIfAbsent(to, new ArrayList<>());
+
+ HubRoute reversed = hubRouteRepository.findShortestRouteByFromAndTo(to, route.getFromHub()).orElse(null);
+ graph.get(to).add(reversed);
+ }
+ }
+
+ // 직접 정의한 dijkstra를 이용한 체크포인트 리스트 생성
+ DijkstraPathFinder dijkstraPathFinder = new DijkstraPathFinder(graph);
+ List sequencePathByDijkstra = dijkstraPathFinder.getShortPath(fromHub, toHub);
+
+ /*
+ // JGraphT 라이브러리를 사용한 체크포인트 리스트 생성
+ JGraphTPathFinder jgraphT = new JGraphTPathFinder(graph);
+ List sequencePathByJGraphT = jgraphT.getShortPath(fromHub, toHub);
+
+ if(sequencePathByDijkstra.equals(sequencePathByJGraphT)) {
+ log.info("Checking checkpoint path validity : dijkstraPath same JGraphT");
+ }
+ */
+
+ return sequencePathByDijkstra;
+ }
+
+ public PathValueDto getValue(List shortPath){
+
+ BigDecimal totalDistance = BigDecimal.ZERO;
+ int totalDuration = 0;
+
+ for(int i = 0; i < shortPath.size()-1; i++){
+ Hub h1 = shortPath.get(i);
+ Hub h2 = shortPath.get(i+1);
+ HubRoute route = hubRouteRepository.findShortestRouteByFromAndTo(h1, h2)
+ .orElseThrow(ResourceNotFoundException::new);
+ totalDistance = totalDistance.add(route.getDistance());
+ totalDuration += route.getDuration();
+ }
+
+ return new PathValueDto(totalDistance, totalDuration);
+ }
+
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/PathFinder.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/PathFinder.java
new file mode 100644
index 00000000..f1674264
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dijkstra/PathFinder.java
@@ -0,0 +1,9 @@
+package com.sparta.hubservice.hub_route.application.dijkstra;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import java.util.List;
+
+public interface PathFinder {
+
+ List getShortPath(Hub from, Hub to);
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/CheckpointResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/CheckpointResponseDto.java
new file mode 100644
index 00000000..671184f0
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/CheckpointResponseDto.java
@@ -0,0 +1,25 @@
+package com.sparta.hubservice.hub_route.application.dto.response;
+
+import com.sparta.hubservice.hub_route.domain.model.HubRouteCheckpoint;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+@Builder
+public class CheckpointResponseDto {
+
+ private final UUID hubRouteCheckpointId;
+ private final UUID hubRouteId;
+ private final UUID checkpointHubId;
+ private final int sequence;
+
+ public CheckpointResponseDto(HubRouteCheckpoint checkpoint) {
+ hubRouteCheckpointId = checkpoint.getHubRouteCheckpointId();
+ hubRouteId = checkpoint.getHubRoute().getHubRouteId();
+ sequence = checkpoint.getSequence();
+ checkpointHubId = checkpoint.getCheckpointHubId().getHubId();
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteCreateResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteCreateResponseDto.java
new file mode 100644
index 00000000..ca011bda
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteCreateResponseDto.java
@@ -0,0 +1,29 @@
+package com.sparta.hubservice.hub_route.application.dto.response;
+
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import java.math.BigDecimal;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+@AllArgsConstructor
+public class HubRouteCreateResponseDto {
+
+ private final UUID hubRouteId;
+ private final UUID fromHubId;
+ private final UUID toHubId;
+ private final int duration;
+ private final BigDecimal distance;
+
+ public HubRouteCreateResponseDto(HubRoute hubRoute) {
+ this.hubRouteId = hubRoute.getHubRouteId();
+ this.fromHubId = hubRoute.getFromHub().getHubId();
+ this.toHubId = hubRoute.getToHub().getHubId();
+ this.duration = hubRoute.getDuration();
+ this.distance = hubRoute.getDistance();
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteDeleteResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteDeleteResponseDto.java
new file mode 100644
index 00000000..7d3290c9
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteDeleteResponseDto.java
@@ -0,0 +1,20 @@
+package com.sparta.hubservice.hub_route.application.dto.response;
+
+import java.util.UUID;
+import lombok.Builder;
+import lombok.Getter;
+
+
+@Builder
+@Getter
+public class HubRouteDeleteResponseDto {
+
+ private final UUID hub_route_id;
+ private final String message;
+
+ public HubRouteDeleteResponseDto(UUID hub_route_id, String message) {
+ this.hub_route_id = hub_route_id;
+ this.message = message;
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteDetailsResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteDetailsResponseDto.java
new file mode 100644
index 00000000..a5d27604
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteDetailsResponseDto.java
@@ -0,0 +1,31 @@
+package com.sparta.hubservice.hub_route.application.dto.response;
+
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+@AllArgsConstructor
+public class HubRouteDetailsResponseDto {
+
+ private final UUID hubRouteId;
+ private final UUID fromHubId;
+ private final UUID toHubId;
+ private final int duration;
+ private final BigDecimal distance;
+ private final List checkpoints;
+
+ public HubRouteDetailsResponseDto(HubRoute hubRoute, List checkpoints) {
+ this.hubRouteId = hubRoute.getHubRouteId();
+ this.fromHubId = hubRoute.getFromHub().getHubId();
+ this.toHubId = hubRoute.getToHub().getHubId();
+ this.duration = hubRoute.getDuration();
+ this.distance = hubRoute.getDistance();
+ this.checkpoints = checkpoints;
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteResponseDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteResponseDto.java
new file mode 100644
index 00000000..1db952ae
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/response/HubRouteResponseDto.java
@@ -0,0 +1,29 @@
+package com.sparta.hubservice.hub_route.application.dto.response;
+
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import java.math.BigDecimal;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+@AllArgsConstructor
+public class HubRouteResponseDto {
+
+ private final UUID routeId;
+ private final UUID fromHubId;
+ private final UUID toHubId;
+ private final int duration;
+ private final BigDecimal distance;
+
+ public HubRouteResponseDto(HubRoute hubRoute) {
+ this.routeId = hubRoute.getHubRouteId();
+ this.fromHubId = hubRoute.getFromHub().getHubId();
+ this.toHubId = hubRoute.getToHub().getHubId();
+ this.duration = hubRoute.getDuration();
+ this.distance = hubRoute.getDistance();
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/serviceDto/PathValueDto.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/serviceDto/PathValueDto.java
new file mode 100644
index 00000000..47b446af
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/dto/serviceDto/PathValueDto.java
@@ -0,0 +1,14 @@
+package com.sparta.hubservice.hub_route.application.dto.serviceDto;
+
+import java.math.BigDecimal;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class PathValueDto {
+
+ private BigDecimal totalDistance;
+ private int totalDuration;
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/service/HubRouteService.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/service/HubRouteService.java
new file mode 100644
index 00000000..066b5f30
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/application/service/HubRouteService.java
@@ -0,0 +1,140 @@
+package com.sparta.hubservice.hub_route.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub.domain.repository.HubRepository;
+import com.sparta.hubservice.hub_route.application.dijkstra.PathCalculate;
+import com.sparta.hubservice.hub_route.application.dto.response.CheckpointResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteCreateResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteDeleteResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteDetailsResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.serviceDto.PathValueDto;
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import com.sparta.hubservice.hub_route.domain.model.HubRouteCheckpoint;
+import com.sparta.hubservice.hub_route.domain.repository.HubRouteCheckpointRepository;
+import com.sparta.hubservice.hub_route.domain.repository.HubRouteRepository;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j(topic = "HubRoutesService ")
+public class HubRouteService {
+
+ private final HubRouteRepository hubRouteRepository;
+ private final HubRepository hubRepository;
+ private final HubRouteCheckpointRepository checkpointRepository;
+ private final PathCalculate pathCalculate;
+
+ // 전체 허브 간 경로 목록 조회
+ @Transactional(readOnly = true)
+ public Page getHubRoutes(Pageable pageable) {
+ Page hubRoutes = hubRouteRepository.findAllByIsDeletedFalse(pageable)
+ .orElseThrow(ResourceNotFoundException::new);
+
+ return hubRoutes.map(HubRouteResponseDto::new);
+ }
+
+ // 특정 경로 ID 조회
+ public HubRouteResponseDto getHubRoute(UUID hubRouteId) {
+ HubRoute hubRoute = hubRouteRepository.findById(hubRouteId).orElseThrow(ResourceNotFoundException::new);
+ return new HubRouteResponseDto(hubRoute);
+ }
+
+ // 출발 허브 -> 도착 허브 : 특정 경로 조회 (direct)
+ @Transactional(readOnly = true)
+ public HubRouteResponseDto getDirectHubRoute(UUID fromHubId, UUID toHubId) {
+
+ Optional hubRoute = hubRouteRepository.findByFromHub_HubIdAndToHub_HubIdAndIsDeletedFalse(fromHubId, toHubId);
+
+ if(hubRoute.isEmpty()){
+ throw new ResourceNotFoundException("Hub route not found");
+ }
+ return new HubRouteResponseDto(hubRoute.get());
+ }
+
+ // 허브 간 경로 생성 (direct 연결)
+ @Transactional
+ public HubRouteCreateResponseDto createDirectHubRoute(Long userId, UUID fromHubId, UUID toHubId) {
+ Hub fromHub = hubRepository.findById(fromHubId).orElseThrow(ResourceNotFoundException::new);
+ Hub toHub = hubRepository.findById(toHubId).orElseThrow(ResourceNotFoundException::new);
+
+ HubRoute saveHubRoute = new HubRoute(fromHub, toHub, userId);
+
+ HubRoute hubRoute = hubRouteRepository.save(saveHubRoute);
+ return new HubRouteCreateResponseDto(hubRoute);
+ }
+
+ // 허브 간 경로 정보 삭제 (soft deleted)
+ @Transactional
+ public HubRouteDeleteResponseDto deleteHubRoute(UUID hubRouteId, Long userId) {
+ HubRoute hubRoute = hubRouteRepository.findByHubRouteIdAndIsDeletedFalse(hubRouteId)
+ .orElseThrow(ResourceNotFoundException::new);
+
+ // hubRouteId와 관련있는 hub_route_checkpoint 정보 삭제
+ checkpointRepository.findAllByHubRoute(hubRoute)
+ .forEach(checkpoint -> checkpoint.delete(userId));
+
+ hubRoute.delete(userId);
+ return new HubRouteDeleteResponseDto(hubRouteId, "Hub successfully deleted.");
+ }
+
+ // fromHub -> toHub 최단 경로 생성
+ @Transactional
+ public HubRouteDetailsResponseDto createPathHubRoute(UUID fromHubId, UUID toHubId, Long userId) {
+ Hub fromHub = hubRepository.findById(fromHubId).orElseThrow(ResourceNotFoundException::new);
+ Hub toHub = hubRepository.findById(toHubId).orElseThrow(ResourceNotFoundException::new);
+
+ List shortPath = pathCalculate.getShortPath(fromHub, toHub);
+ PathValueDto vlaues = pathCalculate.getValue(shortPath);
+
+ // 다이렉트는 이미 최단 경로 -> 이미 route 정보가 있다면 패스
+ //hubRouteRepository.findShortestRouteByFromAndTo(fromHub, toHub).orElseThrow(EntityExistsException::new);
+
+ HubRoute route = new HubRoute(fromHub, toHub, vlaues.getTotalDuration(), vlaues.getTotalDistance(), userId);
+ hubRouteRepository.save(route);
+
+ List checkpointList = new ArrayList<>();
+ for(int i = 1; i < shortPath.size()-1; i++) {
+ HubRouteCheckpoint result = HubRouteCheckpoint.builder()
+ .checkpointHub(shortPath.get(i))
+ .hubRoute(route)
+ .userId(userId)
+ .sequence(i)
+ .build();
+ checkpointList.add(result);
+ log.info("Creating checkpoint for hub route id " + route.getHubRouteId() + ", checkpoint : " + result);
+ checkpointRepository.save(result);
+ }
+
+ List checkpoints =
+ checkpointList.stream().map(CheckpointResponseDto::new).toList();
+
+ return new HubRouteDetailsResponseDto(route, checkpoints);
+ }
+
+ // fromHub -> toHub 최단 경로 정보 조회
+ @Transactional(readOnly = true)
+ public HubRouteDetailsResponseDto getPathHubRoute(UUID fromHubId, UUID toHubId) {
+ Hub fromHub = hubRepository.findById(fromHubId).orElseThrow(ResourceNotFoundException::new);
+ Hub toHub = hubRepository.findById(toHubId).orElseThrow(ResourceNotFoundException::new);
+
+ HubRoute route = hubRouteRepository.findShortestRouteByFromAndTo(fromHub, toHub)
+ .orElseThrow(ResourceNotFoundException::new);
+
+ List checkpoints =
+ checkpointRepository.findAllByHubRoute_OrderBySequenceAsc(route)
+ .stream().map(CheckpointResponseDto::new).toList();
+
+ return new HubRouteDetailsResponseDto(route, checkpoints);
+ }
+}
\ No newline at end of file
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/common/HaversineCalculator.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/common/HaversineCalculator.java
new file mode 100644
index 00000000..691d3ca8
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/common/HaversineCalculator.java
@@ -0,0 +1,28 @@
+package com.sparta.hubservice.hub_route.domain.common;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+
+public class HaversineCalculator {
+
+ private static final double R = 6371; // 지구 반지름 (km)
+
+ public static double haversineDistance(Hub hub1, Hub hub2) {
+
+ // 삼각함수 계산을 하기 위해 radian 단위로 변경
+ double latRad1 = Math.toRadians(hub1.getLatitude().doubleValue());
+ double latRad2 = Math.toRadians(hub2.getLatitude().doubleValue());
+ double lonRad1 = Math.toRadians(hub1.getLongitude().doubleValue());
+ double lonRad2 = Math.toRadians(hub2.getLongitude().doubleValue());
+
+ double diffLat = latRad2 - latRad1;
+ double diffLon = lonRad2 - lonRad1;
+
+ // haversine 공식
+ double a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2)
+ + Math.cos(latRad1) * Math.cos(latRad2) * Math.sin(diffLon / 2) * Math.sin(diffLon / 2);
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return R * c;
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/model/HubRoute.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/model/HubRoute.java
new file mode 100644
index 00000000..2df3af0f
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/model/HubRoute.java
@@ -0,0 +1,78 @@
+package com.sparta.hubservice.hub_route.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub_route.domain.common.HaversineCalculator;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.Digits;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor
+@Table(name = "p_hub_route")
+public class HubRoute extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "hub_route_id", nullable = false)
+ private UUID hubRouteId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name ="from_hub", nullable = false)
+ private Hub fromHub;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name ="to_hub", nullable = false)
+ private Hub toHub;
+
+ @Column(nullable = false)
+ private int duration;
+
+ @Column(nullable = false)
+ @Digits(integer = 8, fraction = 2)
+ private BigDecimal distance;
+
+ // 다이렉트로 가는 경로 생성시
+ public HubRoute(Hub fromHub, Hub toHub, Long userId) {
+ super(userId);
+ this.fromHub = fromHub;
+ this.toHub = toHub;
+ this.distance = this.calculateDistance();
+ this.duration = this.calculateDuration();
+ }
+
+ // 체크포인트가 존재하는 최단 거리 생성시
+ public HubRoute(Hub fromHub, Hub toHub, int duration, BigDecimal distance, Long userId) {
+ super(userId);
+ this.fromHub = fromHub;
+ this.toHub = toHub;
+ this.duration = duration;
+ this.distance = distance.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ // 거리 계산 (km)
+ private BigDecimal calculateDistance() {
+ return BigDecimal.valueOf(HaversineCalculator.haversineDistance(fromHub, toHub))
+ .setScale(2, RoundingMode.HALF_UP);
+ }
+
+ // 시간 계산 (시)
+ private int calculateDuration(){
+ double speed = 60.0;
+ return (int) Math.round(this.distance.doubleValue() / speed);
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/model/HubRouteCheckpoint.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/model/HubRouteCheckpoint.java
new file mode 100644
index 00000000..071f4aae
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/model/HubRouteCheckpoint.java
@@ -0,0 +1,49 @@
+package com.sparta.hubservice.hub_route.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import com.sparta.hubservice.hub.domain.model.Hub;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.util.UUID;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor
+@Table(name = "p_hub_route_checkpoint")
+public class HubRouteCheckpoint extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "hub_route_checkpoint_id", nullable = false)
+ private UUID hubRouteCheckpointId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name ="hub_route_id", nullable = false)
+ private HubRoute hubRoute;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name ="checkpoint_hub_id", nullable = false)
+ private Hub checkpointHubId;
+
+ @Column(nullable = false)
+ private int sequence;
+
+ @Builder
+ public HubRouteCheckpoint(HubRoute hubRoute, Hub checkpointHub, int sequence, long userId) {
+ super(userId);
+ this.hubRoute = hubRoute;
+ this.checkpointHubId = checkpointHub;
+ this.sequence = sequence;
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/repository/HubRouteCheckpointRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/repository/HubRouteCheckpointRepository.java
new file mode 100644
index 00000000..be67d08f
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/repository/HubRouteCheckpointRepository.java
@@ -0,0 +1,16 @@
+package com.sparta.hubservice.hub_route.domain.repository;
+
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import com.sparta.hubservice.hub_route.domain.model.HubRouteCheckpoint;
+import java.util.List;
+
+public interface HubRouteCheckpointRepository {
+
+ List findAllByHubRoute(HubRoute hubRoute);
+
+ void saveAll(List checkpointList);
+
+ List findAllByHubRoute_OrderBySequenceAsc(HubRoute hubRoute);
+
+ void save(HubRouteCheckpoint result);
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/repository/HubRouteRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/repository/HubRouteRepository.java
new file mode 100644
index 00000000..b2c2546f
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/domain/repository/HubRouteRepository.java
@@ -0,0 +1,26 @@
+package com.sparta.hubservice.hub_route.domain.repository;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface HubRouteRepository {
+
+ Optional findByFromHub_HubIdAndToHub_HubIdAndIsDeletedFalse(UUID fromHubId, UUID toHubId);
+
+ Optional findByHubRouteIdAndIsDeletedFalse(UUID hubRouteId);
+
+ S save(S hubRoute);
+
+ Optional> findAllByIsDeletedFalse(Pageable pageable);
+
+ Optional findById(java.util.UUID hubRouteId);
+
+ Optional> findByFromHub(Hub hub);
+
+ Optional findShortestRouteByFromAndTo(Hub fromHub, Hub toHub);
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/persistence/JPAHubRouteCheckpointRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/persistence/JPAHubRouteCheckpointRepository.java
new file mode 100644
index 00000000..207ef063
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/persistence/JPAHubRouteCheckpointRepository.java
@@ -0,0 +1,16 @@
+package com.sparta.hubservice.hub_route.infrastructure.persistence;
+
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import com.sparta.hubservice.hub_route.domain.model.HubRouteCheckpoint;
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface JPAHubRouteCheckpointRepository extends JpaRepository{
+
+ List findAllByHubRoute(HubRoute hubRouteId);
+
+ List findAllByHubRoute_OrderBySequenceAsc(HubRoute hubRoute);
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/persistence/JPAHubRouteRepository.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/persistence/JPAHubRouteRepository.java
new file mode 100644
index 00000000..d3333597
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/persistence/JPAHubRouteRepository.java
@@ -0,0 +1,28 @@
+package com.sparta.hubservice.hub_route.infrastructure.persistence;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import feign.Param;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface JPAHubRouteRepository extends JpaRepository {
+
+ Optional findByFromHub_HubIdAndToHub_HubIdAndIsDeletedFalse(UUID fromHubId, UUID toHubId);
+
+ Optional findByHubRouteIdAndIsDeletedFalse(UUID hubRouteId);
+
+ Optional> findAllByIsDeletedFalse(Pageable pageable);
+
+ Optional> findByFromHub(Hub hub);
+
+ @Query("SELECT h FROM HubRoute h WHERE h.fromHub = :from AND h.toHub = :to AND h.isDeleted = false ORDER BY h.distance ASC LIMIT 1")
+ Optional findShortestRouteByFromAndTo(@Param("from") Hub from, @Param("to") Hub to);
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/repository/HubRouteCheckpointRepositoryImpl.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/repository/HubRouteCheckpointRepositoryImpl.java
new file mode 100644
index 00000000..2d85ed17
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/repository/HubRouteCheckpointRepositoryImpl.java
@@ -0,0 +1,37 @@
+package com.sparta.hubservice.hub_route.infrastructure.repository;
+
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import com.sparta.hubservice.hub_route.domain.model.HubRouteCheckpoint;
+import com.sparta.hubservice.hub_route.domain.repository.HubRouteCheckpointRepository;
+import com.sparta.hubservice.hub_route.infrastructure.persistence.JPAHubRouteCheckpointRepository;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class HubRouteCheckpointRepositoryImpl implements HubRouteCheckpointRepository {
+
+ private final JPAHubRouteCheckpointRepository jpaHubRouteCheckpointRepository;
+
+ @Override
+ public List findAllByHubRoute(HubRoute hubRoute) {
+ return jpaHubRouteCheckpointRepository.findAllByHubRoute(hubRoute);
+ }
+
+ @Override
+ public void saveAll(List checkpointList) {
+ jpaHubRouteCheckpointRepository.saveAll(checkpointList);
+ }
+
+ @Override
+ public List findAllByHubRoute_OrderBySequenceAsc(HubRoute hubRoute) {
+ return jpaHubRouteCheckpointRepository.findAllByHubRoute_OrderBySequenceAsc(hubRoute);
+ }
+
+ @Override
+ public void save(HubRouteCheckpoint result) {
+ jpaHubRouteCheckpointRepository.save(result);
+ }
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/repository/HubRouteRepositoryImpl.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/repository/HubRouteRepositoryImpl.java
new file mode 100644
index 00000000..caf2b236
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/infrastructure/repository/HubRouteRepositoryImpl.java
@@ -0,0 +1,56 @@
+package com.sparta.hubservice.hub_route.infrastructure.repository;
+
+import com.sparta.hubservice.hub.domain.model.Hub;
+import com.sparta.hubservice.hub_route.domain.model.HubRoute;
+import com.sparta.hubservice.hub_route.domain.repository.HubRouteRepository;
+import com.sparta.hubservice.hub_route.infrastructure.persistence.JPAHubRouteRepository;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class HubRouteRepositoryImpl implements HubRouteRepository {
+
+ private final JPAHubRouteRepository jpaHubRouteRepository;
+
+ @Override
+ public Optional findByFromHub_HubIdAndToHub_HubIdAndIsDeletedFalse(UUID fromHubId,
+ UUID toHubId) {
+ return jpaHubRouteRepository.findByFromHub_HubIdAndToHub_HubIdAndIsDeletedFalse(fromHubId, toHubId);
+ }
+
+ @Override
+ public Optional findByHubRouteIdAndIsDeletedFalse(UUID hubRouteId) {
+ return jpaHubRouteRepository.findByHubRouteIdAndIsDeletedFalse(hubRouteId);
+ }
+
+ @Override
+ public HubRoute save(HubRoute hubRoute) {
+ return jpaHubRouteRepository.save(hubRoute);
+ }
+
+ @Override
+ public Optional> findAllByIsDeletedFalse(Pageable pageable) {
+ return jpaHubRouteRepository.findAllByIsDeletedFalse(pageable);
+ }
+
+ @Override
+ public Optional findById(UUID hubRouteId) {
+ return jpaHubRouteRepository.findById(hubRouteId);
+ }
+
+ @Override
+ public Optional> findByFromHub(Hub hub) {
+ return jpaHubRouteRepository.findByFromHub(hub);
+ }
+
+ @Override
+ public Optional findShortestRouteByFromAndTo(Hub fromHub, Hub toHub) {
+ return jpaHubRouteRepository.findShortestRouteByFromAndTo(fromHub, toHub);
+ }
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/presentation/controller/HubRouteController.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/presentation/controller/HubRouteController.java
new file mode 100644
index 00000000..18d46d31
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/presentation/controller/HubRouteController.java
@@ -0,0 +1,110 @@
+package com.sparta.hubservice.hub_route.presentation.controller;
+
+
+import com.sparta.commonmodule.aop.RoleCheck;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteCreateResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteDeleteResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteDetailsResponseDto;
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteResponseDto;
+import com.sparta.hubservice.hub_route.application.service.HubRouteService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/hubs/hub-routes")
+@RequiredArgsConstructor
+@Tag(name = "Hub Service", description = "허브 서비스 API")
+public class HubRouteController {
+
+ private final HubRouteService hubRouteService;
+
+ // 전체 허브 간 경로 목록 조회 (direct)
+ @Operation(summary = "Hub Route", description = "전체 허브 간 경로 조회 api")
+ @GetMapping
+ public ResponseEntity> getHubRoutes(
+ @PageableDefault(page = 0, size = 30, sort = "createdAt") Pageable pageable){
+ Page responses = hubRouteService.getHubRoutes(pageable);
+ return ResponseEntity.ok(responses);
+ }
+
+ // 특정 경로 ID 조회
+ @Operation(summary = "Hub Route", description = "id기반 허브 간 경로 조회 api")
+ @GetMapping("/{hub_route_id}")
+ public ResponseEntity getHubRoute(@PathVariable("hub_route_id") UUID hubRouteId) {
+ HubRouteResponseDto response = hubRouteService.getHubRoute(hubRouteId);
+ return ResponseEntity.ok(response);
+ }
+
+ // 특정 출발 허브 → 도착 허브 경로 조회 (direct)
+ @Operation(summary = "Hub Route", description = "from->to 허브 간 경로 조회 api")
+ @GetMapping("/{from_hub_id}/{to_hub_id}/direct")
+ public ResponseEntity getDirectHubRoute(
+ @PathVariable("from_hub_id") UUID fromHubId,
+ @PathVariable("to_hub_id") UUID toHubId) {
+ HubRouteResponseDto response = hubRouteService.getDirectHubRoute(fromHubId, toHubId);
+ return ResponseEntity.ok(response);
+ }
+
+ // (정해진) 허브 간 경로 생성 (direct)
+ @Operation(summary = "Hub Route", description = "from->to 허브 간 경로 생성 api")
+ @PostMapping("/{from_hub_id}/{to_hub_id}/direct")
+ public ResponseEntity createDirectHubRoute(
+ @PathVariable("from_hub_id") UUID fromHubId,
+ @PathVariable("to_hub_id") UUID toHubId,
+ @RequestHeader("user_id") Long userId) {
+ HubRouteCreateResponseDto response =
+ hubRouteService.createDirectHubRoute(userId, fromHubId, toHubId);
+ return ResponseEntity.ok(response);
+ }
+
+ // 허브 간 경로 정보 삭제
+ @Operation(summary = "Hub Route", description = "허브 간 경로 삭제 api")
+ @RoleCheck("ROLE_MASTER")
+ @DeleteMapping("/{hub_route_id}")
+ public ResponseEntity deleteHubRoute(
+ @PathVariable("hub_route_id") UUID hubRouteId,
+ @RequestHeader("user_id") Long userId) {
+ HubRouteDeleteResponseDto response = hubRouteService.deleteHubRoute(hubRouteId, userId);
+ return ResponseEntity.ok(response);
+ }
+
+ // form -> to 최단경로 생성 (다이렉트는 항상 최단경로)
+ @Operation(summary = "Hub Route - Checkpoint", description = "허브 간 최단 경로 생성 api")
+ @PostMapping("/{from_hub_id}/{to_hub_id}/path")
+ public ResponseEntity createPathHubRoute(
+ @PathVariable("from_hub_id") UUID fromHubId,
+ @PathVariable("to_hub_id") UUID toHubId,
+ @RequestHeader("user_id") Long userId
+ ){
+ HubRouteDetailsResponseDto response =
+ hubRouteService.createPathHubRoute(fromHubId, toHubId, userId);
+ return ResponseEntity.ok(response);
+ }
+
+ // fromHub -> toHub 최단 경로 정보 조회
+ @Operation(summary = "Hub Route - Checkpoint", description = "허브 간 최단 경로 조회 api")
+ @GetMapping("/{from_hub_id}/{to_hub_id}/path")
+ public ResponseEntity getPathHubRoute(
+ @PathVariable("from_hub_id") UUID fromHubId,
+ @PathVariable("to_hub_id") UUID toHubId
+ ){
+ HubRouteDetailsResponseDto responseDto =
+ hubRouteService.getPathHubRoute(fromHubId, toHubId);
+ return ResponseEntity.ok(responseDto);
+ }
+
+
+}
diff --git a/hub-service/src/main/java/com/sparta/hubservice/hub_route/presentation/controller/HubRouteFeignController.java b/hub-service/src/main/java/com/sparta/hubservice/hub_route/presentation/controller/HubRouteFeignController.java
new file mode 100644
index 00000000..eaa20b48
--- /dev/null
+++ b/hub-service/src/main/java/com/sparta/hubservice/hub_route/presentation/controller/HubRouteFeignController.java
@@ -0,0 +1,32 @@
+package com.sparta.hubservice.hub_route.presentation.controller;
+
+import com.sparta.hubservice.hub_route.application.dto.response.HubRouteDetailsResponseDto;
+import com.sparta.hubservice.hub_route.application.service.HubRouteService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+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
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/hubs/hub-route/feign")
+@Tag(name = "Hub Service", description = "허브 서비스 API")
+public class HubRouteFeignController {
+
+ private final HubRouteService hubRouteService;
+
+ @Operation(summary = "Hub Route - Checkpoint : Feign", description = "허브 간 최단 경로 조회 api")
+ @GetMapping("/{from_id}/{to_id}/path")
+ public ResponseEntity getHubRouteDetails(
+ @PathVariable("from_id") UUID fromId,
+ @PathVariable("to_id") UUID toId
+ ){
+ HubRouteDetailsResponseDto responseDto = hubRouteService.getPathHubRoute(fromId, toId);
+ return ResponseEntity.ok(responseDto);
+ }
+}
diff --git a/hub-service/src/main/resources/application.yml b/hub-service/src/main/resources/application.yml
new file mode 100644
index 00000000..107e0350
--- /dev/null
+++ b/hub-service/src/main/resources/application.yml
@@ -0,0 +1,43 @@
+server:
+ port: 8082
+
+spring:
+ application:
+ name: hub-service
+ config:
+ import: "application-secret.yml"
+ datasource:
+ url: jdbc:postgresql://localhost:5001/maindb
+ username: admin
+ password: 1234
+ driver-class-name: org.postgresql.Driver
+ jpa:
+ properties:
+ hibernate:
+ default_schema: hub_service
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ hibernate:
+ ddl-auto: update
+ show-sql: true
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ service-url:
+ defaultZone: http://localhost:8761/eureka/
+ instance:
+ prefer-ip-address: true
+
+springdoc:
+ api-docs:
+ version: openapi_3_1
+ enabled: true
+ path: /hub-service/v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
+
+shipping:
+ service:
+ url: http://localhost:8081/api/v1/shippings
diff --git a/hub-service/src/main/resources/sql/p_hub.sql b/hub-service/src/main/resources/sql/p_hub.sql
new file mode 100644
index 00000000..34d0786c
--- /dev/null
+++ b/hub-service/src/main/resources/sql/p_hub.sql
@@ -0,0 +1,17 @@
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('d25f4291-d134-4f73-885a-0eb188a72649', '2025-03-25 01:51:28.958692', 1, null, null, false, '2025-03-25 01:51:28.958692', 1, '서울특별시 송파구 송파대로 55', 37.47415400, 127.12390620, '서울특별시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('d2587e4e-b2d4-4c9b-b2fd-37f035596a0d', '2025-03-25 01:51:29.072057', 1, null, null, false, '2025-03-25 01:51:29.072057', 1, '경기도 고양시 덕양구 권율대로 570', 37.64037710, 126.87379550, '경기 북부 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('8b3f2668-49cb-4885-8855-e20fd4bf10aa', '2025-03-25 01:51:29.179759', 1, null, null, false, '2025-03-25 01:51:29.179759', 1, '경기도 이천시 덕평로 257-21', 37.18962130, 127.37505010, '경기 남부 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('12bca245-3e19-400d-a421-6e3e42c56b1e', '2025-03-25 01:51:29.285968', 1, null, null, false, '2025-03-25 01:51:29.285968', 1, '부산 동구 중앙대로 206', 35.11760120, 129.04505790, '부산광역시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('73c63196-a79c-4b05-912e-d3bbd8244cda', '2025-03-25 01:51:29.395185', 1, null, null, false, '2025-03-25 01:51:29.395185', 1, '대구 북구 태평로 161', 35.87586320, 128.59613850, '대구광역시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('4410d46d-27a2-4267-824b-4305a6731624', '2025-03-25 01:51:29.499141', 1, null, null, false, '2025-03-25 01:51:29.499141', 1, '인천 남동구 정각로 29', 37.45594180, 126.70515050, '인천광역시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('b38d1ab0-2d3e-424c-b39d-814f10b294a5', '2025-03-25 01:51:29.608871', 1, null, null, false, '2025-03-25 01:51:29.608871', 1, '광주 서구 내방로 111', 35.16003200, 126.85133800, '광주광역시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('9d23ac68-441f-4122-ac91-98c99896fcc4', '2025-03-25 01:51:29.711015', 1, null, null, false, '2025-03-25 01:51:29.711015', 1, '대전 서구 둔산로 100', 36.35050010, 127.38483340, '대전광역시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('b97f9c00-f8bb-4ff4-99d0-624da28a2be7', '2025-03-25 01:51:29.815896', 1, null, null, false, '2025-03-25 01:51:29.815896', 1, '울산 남구 중앙로 201', 35.53947730, 129.31129940, '울산광역시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('0b1fa666-ebaa-4da8-a880-3bd40181ec0c', '2025-03-25 01:51:29.926910', 1, null, null, false, '2025-03-25 01:51:29.926910', 1, '세종특별자치시 한누리대로 2130', 36.48001210, 127.28906910, '세종특별자치시 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('3e734ae7-1030-45c8-b19d-b7ee7601cf2f', '2025-03-25 01:51:30.049931', 1, null, null, false, '2025-03-25 01:51:30.049931', 1, '강원특별자치도 춘천시 중앙로 1', 37.88539900, 127.72975000, '강원특별자치도 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('2cddf4cb-07fc-487a-bceb-9613b884447f', '2025-03-25 01:51:30.155681', 1, null, null, false, '2025-03-25 01:51:30.155681', 1, '충북 청주시 상당구 상당로 82', 36.63609950, 127.49136050, '충청북도 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('a8d36f04-6312-4acb-aac1-1b5e82e0995d', '2025-03-25 01:51:30.267675', 1, null, null, false, '2025-03-25 01:51:30.267675', 1, '충남 홍성군 홍북읍 충남대로 21', 36.65924900, 126.67290800, '충청남도 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('eb42b568-480c-4515-ac25-6d2dc6532110', '2025-03-25 01:51:30.367681', 1, null, null, false, '2025-03-25 01:51:30.367681', 1, '전북특별자치도 전주시 완산구 효자로 225', 35.82032940, 127.10878400, '전북특별자치도 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('972869be-9b58-4db2-9477-67a986c42d5b', '2025-03-25 01:51:30.469500', 1, null, null, false, '2025-03-25 01:51:30.469500', 1, '전남 무안군 삼향읍 오룡길 1', 34.81611020, 126.46317140, '전라남도 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('ec87005e-5160-4515-90ce-dcbbfb392678', '2025-03-25 01:51:30.562934', 1, null, null, false, '2025-03-25 01:51:30.562934', 1, '경북 안동시 풍천면 도청대로 455', 36.57594770, 128.50564620, '경상북도 센터');
+INSERT INTO hub_service.p_hub (hub_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, address, latitude, longitude, name) VALUES ('6ad0e2b5-6a15-437b-be2d-db5329b6cff6', '2025-03-25 01:51:30.665512', 1, null, null, false, '2025-03-25 01:51:30.665512', 1, '경남 창원시 의창구 중앙대로 300', 35.23779740, 128.69194030, '경상남도 센터');
diff --git a/hub-service/src/main/resources/sql/p_hub_route.sql b/hub-service/src/main/resources/sql/p_hub_route.sql
new file mode 100644
index 00000000..913379f9
--- /dev/null
+++ b/hub-service/src/main/resources/sql/p_hub_route.sql
@@ -0,0 +1,38 @@
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('41f4f9f6-4528-465d-a92b-46caee54fd7e', '2025-03-25 02:52:21.400934', 1, null, null, false, '2025-03-25 02:52:21.400934', 1, 66.87, 1, 'd2587e4e-b2d4-4c9b-b2fd-37f035596a0d', '8b3f2668-49cb-4885-8855-e20fd4bf10aa');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('20f474c1-f262-47f2-95db-cb991e613e01', '2025-03-25 02:52:23.575234', 1, null, null, false, '2025-03-25 02:52:23.575234', 1, 38.65, 1, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', 'd25f4291-d134-4f73-885a-0eb188a72649');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('68293329-979f-450e-b625-14c10e6ffc24', '2025-03-25 02:52:36.041411', 1, null, null, false, '2025-03-25 02:52:36.041411', 1, 38.65, 1, 'd25f4291-d134-4f73-885a-0eb188a72649', '8b3f2668-49cb-4885-8855-e20fd4bf10aa');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('64e0c1e8-09f4-4171-8bdd-343da708d0b0', '2025-03-25 02:52:40.494551', 1, null, null, false, '2025-03-25 02:52:40.494551', 1, 66.23, 1, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', '4410d46d-27a2-4267-824b-4305a6731624');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('1b36e421-a0bd-4711-abfc-dfcf6adb7767', '2025-03-25 02:52:44.921514', 1, null, null, false, '2025-03-25 02:52:44.921514', 1, 66.23, 1, '4410d46d-27a2-4267-824b-4305a6731624', '8b3f2668-49cb-4885-8855-e20fd4bf10aa');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('006ab37f-cd90-434e-8ab1-b6ffe6743864', '2025-03-25 02:52:49.610882', 1, null, null, false, '2025-03-25 02:52:49.610882', 1, 83.45, 1, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', '3e734ae7-1030-45c8-b19d-b7ee7601cf2f');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('2794961b-ee17-4eb1-bf15-d9036676845f', '2025-03-25 02:52:54.076497', 1, null, null, false, '2025-03-25 02:52:54.076497', 1, 83.45, 1, '3e734ae7-1030-45c8-b19d-b7ee7601cf2f', '8b3f2668-49cb-4885-8855-e20fd4bf10aa');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('3ebac11c-f1c6-4053-a0a7-2f72d42a1473', '2025-03-25 02:52:58.551441', 1, null, null, false, '2025-03-25 02:52:58.551441', 1, 121.52, 2, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', 'ec87005e-5160-4515-90ce-dcbbfb392678');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('5d5836e9-8fab-4986-9d71-edabdaf2722b', '2025-03-25 02:53:03.202529', 1, null, null, false, '2025-03-25 02:53:03.202529', 1, 121.52, 2, 'ec87005e-5160-4515-90ce-dcbbfb392678', '8b3f2668-49cb-4885-8855-e20fd4bf10aa');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('301ab043-5906-4729-9907-7cf6bd8cb9b4', '2025-03-25 02:53:15.608909', 1, null, null, false, '2025-03-25 02:53:15.608909', 1, 72.30, 1, '9d23ac68-441f-4122-ac91-98c99896fcc4', 'a8d36f04-6312-4acb-aac1-1b5e82e0995d');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('5ef095ff-4db5-4dec-af00-1dcef0582f57', '2025-03-25 02:53:23.883033', 1, null, null, false, '2025-03-25 02:53:23.883033', 1, 72.30, 1, 'a8d36f04-6312-4acb-aac1-1b5e82e0995d', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('1f14d362-1167-40d9-a819-e1e164683b54', '2025-03-25 02:54:48.663139', 1, null, null, false, '2025-03-25 02:54:48.663139', 1, 33.15, 1, '2cddf4cb-07fc-487a-bceb-9613b884447f', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('67e5c1f6-2c42-4ef4-9f8f-abed23552dd0', '2025-03-25 02:54:54.531925', 1, null, null, false, '2025-03-25 02:54:54.531925', 1, 33.15, 1, '9d23ac68-441f-4122-ac91-98c99896fcc4', '2cddf4cb-07fc-487a-bceb-9613b884447f');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('120dea07-2fd9-4ffb-85b1-306c31f8ad45', '2025-03-25 02:55:01.370964', 1, null, null, false, '2025-03-25 02:55:01.370964', 1, 16.76, 0, '9d23ac68-441f-4122-ac91-98c99896fcc4', '0b1fa666-ebaa-4da8-a880-3bd40181ec0c');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('659e2dfc-3cc5-4205-baea-a12dec20dbaa', '2025-03-25 02:59:24.345632', 1, null, null, false, '2025-03-25 02:59:24.345632', 1, 16.76, 0, '0b1fa666-ebaa-4da8-a880-3bd40181ec0c', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('fe8ebb35-7cba-4484-b9e9-0ed9ed40af87', '2025-03-25 02:59:29.953806', 1, null, null, false, '2025-03-25 02:59:29.953806', 1, 63.96, 1, '9d23ac68-441f-4122-ac91-98c99896fcc4', 'eb42b568-480c-4515-ac25-6d2dc6532110');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('f6e46825-a663-43af-be4e-0c9277ff8bf5', '2025-03-25 02:59:37.912100', 1, null, null, false, '2025-03-25 02:59:37.912100', 1, 63.96, 1, 'eb42b568-480c-4515-ac25-6d2dc6532110', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('4ef5b377-84ac-4a5a-8a65-037e4006996d', '2025-03-25 02:59:48.033759', 1, null, null, false, '2025-03-25 02:59:48.033759', 1, 140.86, 2, '9d23ac68-441f-4122-ac91-98c99896fcc4', 'b38d1ab0-2d3e-424c-b39d-814f10b294a5');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('09cf5179-f01b-4780-a84c-eb20b0d60bba', '2025-03-25 02:59:52.636079', 1, null, null, false, '2025-03-25 02:59:52.636079', 1, 140.86, 2, 'b38d1ab0-2d3e-424c-b39d-814f10b294a5', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('5e08df2c-4c8f-4274-be98-1fbf6da44437', '2025-03-25 02:59:57.357067', 1, null, null, false, '2025-03-25 02:59:57.357067', 1, 189.88, 3, '9d23ac68-441f-4122-ac91-98c99896fcc4', '972869be-9b58-4db2-9477-67a986c42d5b');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('6f88c55e-279a-4582-9da9-c2aeda1c9c87', '2025-03-25 03:00:07.000388', 1, null, null, false, '2025-03-25 03:00:07.000388', 1, 189.88, 3, '972869be-9b58-4db2-9477-67a986c42d5b', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('048a196d-72a3-4964-acb7-e799544300fb', '2025-03-25 03:00:11.549404', 1, null, null, false, '2025-03-25 03:00:11.549404', 1, 93.31, 2, '9d23ac68-441f-4122-ac91-98c99896fcc4', '8b3f2668-49cb-4885-8855-e20fd4bf10aa');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('d3651716-5b68-4fb4-899d-dec54d5c3f61', '2025-03-25 03:00:16.788193', 1, null, null, false, '2025-03-25 03:00:16.788193', 1, 93.31, 2, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('2f56d3a1-9e12-4de9-a402-cef2cd03134c', '2025-03-25 03:00:23.828329', 1, null, null, false, '2025-03-25 03:00:23.828329', 1, 120.93, 2, '9d23ac68-441f-4122-ac91-98c99896fcc4', '73c63196-a79c-4b05-912e-d3bbd8244cda');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('9f11ba56-8a4e-4bf5-a61c-4eb03cc1af6b', '2025-03-25 03:00:32.135024', 1, null, null, false, '2025-03-25 03:00:32.135024', 1, 120.93, 2, '73c63196-a79c-4b05-912e-d3bbd8244cda', '9d23ac68-441f-4122-ac91-98c99896fcc4');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('de92eaea-1b5f-49ff-b413-682f366a68bb', '2025-03-25 03:00:37.068232', 1, null, null, false, '2025-03-25 03:00:37.068232', 1, 78.27, 1, '73c63196-a79c-4b05-912e-d3bbd8244cda', 'ec87005e-5160-4515-90ce-dcbbfb392678');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('fa5fd99e-d03b-48bf-bfd0-98ebd7d256d8', '2025-03-25 03:00:41.817325', 1, null, null, false, '2025-03-25 03:00:41.817325', 1, 78.27, 1, 'ec87005e-5160-4515-90ce-dcbbfb392678', '73c63196-a79c-4b05-912e-d3bbd8244cda');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('a0d96dfe-3dee-4909-8652-553fae7e57c9', '2025-03-25 03:00:49.210659', 1, null, null, false, '2025-03-25 03:00:49.210659', 1, 71.48, 1, '73c63196-a79c-4b05-912e-d3bbd8244cda', '6ad0e2b5-6a15-437b-be2d-db5329b6cff6');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('b76608da-82a8-40bb-a08c-2e6c70667cb2', '2025-03-25 03:00:53.560388', 1, null, null, false, '2025-03-25 03:00:53.560388', 1, 71.48, 1, '6ad0e2b5-6a15-437b-be2d-db5329b6cff6', '73c63196-a79c-4b05-912e-d3bbd8244cda');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('96d7be7b-e45b-4b98-b05d-77ee1d39717f', '2025-03-25 03:00:57.411149', 1, null, null, false, '2025-03-25 03:00:57.411149', 1, 93.60, 2, '73c63196-a79c-4b05-912e-d3bbd8244cda', '12bca245-3e19-400d-a421-6e3e42c56b1e');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('aada000f-3269-4c52-b947-f8d9ac970908', '2025-03-25 03:01:01.050435', 1, null, null, false, '2025-03-25 03:01:01.050435', 1, 93.60, 2, '12bca245-3e19-400d-a421-6e3e42c56b1e', '73c63196-a79c-4b05-912e-d3bbd8244cda');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('440486c1-4e9f-4a32-a455-d53da83b06a8', '2025-03-25 03:01:05.670636', 1, null, null, false, '2025-03-25 03:01:05.670636', 1, 74.62, 1, '73c63196-a79c-4b05-912e-d3bbd8244cda', 'b97f9c00-f8bb-4ff4-99d0-624da28a2be7');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('a9c08ef8-4015-48f7-a669-8549791e230e', '2025-03-25 03:01:10.395417', 1, null, null, false, '2025-03-25 03:01:10.395417', 1, 74.62, 1, 'b97f9c00-f8bb-4ff4-99d0-624da28a2be7', '73c63196-a79c-4b05-912e-d3bbd8244cda');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('45497ae7-78c9-4e4b-b430-e44de0a13403', '2025-03-25 03:01:16.714435', 1, null, null, false, '2025-03-25 03:01:16.714435', 1, 182.32, 3, '73c63196-a79c-4b05-912e-d3bbd8244cda', '8b3f2668-49cb-4885-8855-e20fd4bf10aa');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('623c82db-1f7e-4178-ab73-8980e367a0c8', '2025-03-25 03:01:21.660722', 1, null, null, false, '2025-03-25 03:01:21.660722', 1, 182.32, 3, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', '73c63196-a79c-4b05-912e-d3bbd8244cda');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('b2d6e643-6f6a-40ef-a0c1-fb5908a9bce8', '2025-03-25 03:08:04.567230', 1, null, null, false, '2025-03-25 03:08:04.567230', 1, 314.57, 6, 'd25f4291-d134-4f73-885a-0eb188a72649', '12bca245-3e19-400d-a421-6e3e42c56b1e');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('50f45bd6-fed3-4478-82b8-c815b86577f1', '2025-03-25 03:08:07.358910', 1, null, null, false, '2025-03-25 03:08:07.358910', 1, 199.20, 3, '9d23ac68-441f-4122-ac91-98c99896fcc4', 'ec87005e-5160-4515-90ce-dcbbfb392678');
+INSERT INTO hub_service.p_hub_route (hub_route_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, distance, duration, from_hub, to_hub) VALUES ('e0ea93fb-8d7c-42c2-ba06-684646745c1e', '2025-03-25 02:52:18.708099', 1, '2025-03-25 03:12:41.743152', 1, true, '2025-03-25 03:12:41.743472', 1, 66.87, 1, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', 'd2587e4e-b2d4-4c9b-b2fd-37f035596a0d');
diff --git a/hub-service/src/main/resources/sql/p_hub_route_checkpoint.sql b/hub-service/src/main/resources/sql/p_hub_route_checkpoint.sql
new file mode 100644
index 00000000..9701e346
--- /dev/null
+++ b/hub-service/src/main/resources/sql/p_hub_route_checkpoint.sql
@@ -0,0 +1,3 @@
+INSERT INTO hub_service.p_hub_route_checkpoint (hub_route_checkpoint_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, sequence, checkpoint_hub_id, hub_route_id) VALUES ('b90d35cf-a8e5-43d0-8892-902a48b0c534', '2025-03-25 03:08:04.569632', 1, null, null, false, '2025-03-25 03:08:04.569632', 1, 1, '8b3f2668-49cb-4885-8855-e20fd4bf10aa', 'b2d6e643-6f6a-40ef-a0c1-fb5908a9bce8');
+INSERT INTO hub_service.p_hub_route_checkpoint (hub_route_checkpoint_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, sequence, checkpoint_hub_id, hub_route_id) VALUES ('89fdb248-938b-4e7c-a77f-969e4dbe2783', '2025-03-25 03:08:04.569818', 1, null, null, false, '2025-03-25 03:08:04.569818', 1, 2, '73c63196-a79c-4b05-912e-d3bbd8244cda', 'b2d6e643-6f6a-40ef-a0c1-fb5908a9bce8');
+INSERT INTO hub_service.p_hub_route_checkpoint (hub_route_checkpoint_id, created_at, created_by, deleted_at, deleted_by, is_deleted, updated_at, updated_by, sequence, checkpoint_hub_id, hub_route_id) VALUES ('1e5438da-508e-486a-9e49-86177eec6916', '2025-03-25 03:08:07.359160', 1, null, null, false, '2025-03-25 03:08:07.359160', 1, 1, '73c63196-a79c-4b05-912e-d3bbd8244cda', '50f45bd6-fed3-4478-82b8-c815b86577f1');
diff --git a/hub-service/src/test/java/com/sparta/hubservice/HubServiceApplicationTests.java b/hub-service/src/test/java/com/sparta/hubservice/HubServiceApplicationTests.java
new file mode 100644
index 00000000..e8f52df5
--- /dev/null
+++ b/hub-service/src/test/java/com/sparta/hubservice/HubServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.sparta.hubservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class HubServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/hub-service/src/test/resources/http/create-hub-route-checkpoints.http b/hub-service/src/test/resources/http/create-hub-route-checkpoints.http
new file mode 100644
index 00000000..057f4a0f
--- /dev/null
+++ b/hub-service/src/test/resources/http/create-hub-route-checkpoints.http
@@ -0,0 +1,8 @@
+### 👩🏻💻 Hub Route Checkpoint 테이블 더미데이터에 필요한 코드 -> 중복 있는 상태
+###
+
+### 서울 -> 부산
+POST http://localhost:8082/api/v1/hub-routes/eff77800-604d-441d-a736-63f1f7d25e51/ac20c293-6951-4916-bd0f-d32f6b3b815b/path?userId=123
+
+### 대전 -> 경상북도
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/c27c2ba8-4011-4d68-991e-19315c19a192/path?userId=123
diff --git a/hub-service/src/test/resources/http/create-hub-routes.http b/hub-service/src/test/resources/http/create-hub-routes.http
new file mode 100644
index 00000000..6ad88418
--- /dev/null
+++ b/hub-service/src/test/resources/http/create-hub-routes.http
@@ -0,0 +1,88 @@
+### 👩🏻💻 Hub Route 테이블의 더미데이터에 필요한 코드
+
+###
+
+### 경기 남부 센터 연결 (경기북부, 서울, 인천, 강원도, 경상북도, 대전, 대구)
+POST http://localhost:8082/api/v1/hub-routes/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/c1984014-c71d-4c55-a3b6-8568d7d1f897/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/eff77800-604d-441d-a736-63f1f7d25e51/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/f95653a0-edb1-4bb4-8d2e-814e4638639a/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/d6a865bd-b172-433c-b49f-dcd5ff9ee428/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/c27c2ba8-4011-4d68-991e-19315c19a192/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/c1f6e270-bc15-4816-bc25-a1b71070b751/direct?userId=123
+
+### 경기 남부 센터 연결 역방향
+POST http://localhost:8082/api/v1/hub-routes/c1984014-c71d-4c55-a3b6-8568d7d1f897/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/eff77800-604d-441d-a736-63f1f7d25e51/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/f95653a0-edb1-4bb4-8d2e-814e4638639a/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/d6a865bd-b172-433c-b49f-dcd5ff9ee428/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/c27c2ba8-4011-4d68-991e-19315c19a192/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/c1f6e270-bc15-4816-bc25-a1b71070b751/3b4279fa-ec57-4a03-a97e-aa8428f3ab0d/direct?userId=123
+
+
+
+### 대전 센터 연결 (충남, 충북, 세종, 전북, 광주, 전남, 경기남부, 대구)
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/b252fddc-a4b1-420e-94c2-9a91bebee41a/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/031973c0-944d-4c53-803b-27ad45fbfb38/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/854d3f6d-c150-4471-a75c-06248c64daef/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/d073a70d-e200-4301-af35-4c1a87798427/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/9984a080-0ecb-45d6-a92f-0a67cfb63592/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/dfba0f7b-ce3b-4d11-aa8f-40ac398028f9/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/c1f6e270-bc15-4816-bc25-a1b71070b751/direct?userId=123
+
+### 대전 센터 연결 역방향
+POST http://localhost:8082/api/v1/hub-routes/b252fddc-a4b1-420e-94c2-9a91bebee41a/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/031973c0-944d-4c53-803b-27ad45fbfb38/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/854d3f6d-c150-4471-a75c-06248c64daef/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/d073a70d-e200-4301-af35-4c1a87798427/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/9984a080-0ecb-45d6-a92f-0a67cfb63592/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/dfba0f7b-ce3b-4d11-aa8f-40ac398028f9/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/c1f6e270-bc15-4816-bc25-a1b71070b751/984feadc-b1db-43c5-81bc-a24ec5001ddc/direct?userId=123
+
+
+
+### 대구 센터 연결 (경북, 경남, 부산, 울산, 경기남부, 대전)
+POST http://localhost:8082/api/v1/hub-routes/c1f6e270-bc15-4816-bc25-a1b71070b751/c27c2ba8-4011-4d68-991e-19315c19a192/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/c1f6e270-bc15-4816-bc25-a1b71070b751/d57d3022-46a7-4997-902f-0fc122cacd92/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/c1f6e270-bc15-4816-bc25-a1b71070b751/ac20c293-6951-4916-bd0f-d32f6b3b815b/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/c1f6e270-bc15-4816-bc25-a1b71070b751/d03faa5d-afdc-4ed0-b61c-aa7d1b97ba48/direct?userId=123
+
+
+### 대구 센터 연결 역방향
+POST http://localhost:8082/api/v1/hub-routes/c27c2ba8-4011-4d68-991e-19315c19a192/c1f6e270-bc15-4816-bc25-a1b71070b751/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/d57d3022-46a7-4997-902f-0fc122cacd92/c1f6e270-bc15-4816-bc25-a1b71070b751/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/ac20c293-6951-4916-bd0f-d32f6b3b815b/c1f6e270-bc15-4816-bc25-a1b71070b751/direct?userId=123
+###
+POST http://localhost:8082/api/v1/hub-routes/d03faa5d-afdc-4ed0-b61c-aa7d1b97ba48/c1f6e270-bc15-4816-bc25-a1b71070b751/direct?userId=123
+
+### 나머지는 위에서 중복되는 데이터임
\ No newline at end of file
diff --git a/hub-service/src/test/resources/http/create-hub.http b/hub-service/src/test/resources/http/create-hub.http
new file mode 100644
index 00000000..6611e24c
--- /dev/null
+++ b/hub-service/src/test/resources/http/create-hub.http
@@ -0,0 +1,154 @@
+### 👩🏻💻 Hub 테이블 더미데이터에 필요한 코드
+
+### - 서울특별시 센터 : 서울특별시 송파구 송파대로 55
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "서울특별시 센터",
+ "address": "서울특별시 송파구 송파대로 55"
+}
+
+### - 경기 북부 센터 : 경기도 고양시 덕양구 권율대로 570
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "경기 북부 센터",
+ "address": "경기도 고양시 덕양구 권율대로 570"
+}
+
+### - 경기 남부 센터 : 경기도 이천시 덕평로 257-21
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "경기 남부 센터",
+ "address": "경기도 이천시 덕평로 257-21"
+}
+
+### - 부산광역시 센터 : 부산 동구 중앙대로 206
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "부산광역시 센터",
+ "address": "부산 동구 중앙대로 206"
+}
+
+### - 대구광역시 센터 : 대구 북구 태평로 161
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "대구광역시 센터",
+ "address": "대구 북구 태평로 161"
+}
+
+### - 인천광역시 센터 : 인천 남동구 정각로 29
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "인천광역시 센터",
+ "address": "인천 남동구 정각로 29"
+}
+
+### - 광주광역시 센터 : 광주 서구 내방로 111
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "광주광역시 센터",
+ "address": "광주 서구 내방로 111"
+}
+
+### - 대전광역시 센터 : 대전 서구 둔산로 100
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "대전광역시 센터",
+ "address": "대전 서구 둔산로 100"
+}
+
+### - 울산광역시 센터 : 울산 남구 중앙로 201
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "울산광역시 센터",
+ "address": "울산 남구 중앙로 201"
+}
+
+### - 세종특별자치시 센터 : 세종특별자치시 한누리대로 2130
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "세종특별자치시 센터",
+ "address": "세종특별자치시 한누리대로 2130"
+}
+
+### - 강원특별자치도 센터 : 강원특별자치도 춘천시 중앙로 1
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "강원특별자치도 센터",
+ "address": "강원특별자치도 춘천시 중앙로 1"
+}
+
+### - 충청북도 센터 : 충북 청주시 상당구 상당로 82
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "충청북도 센터",
+ "address": "충북 청주시 상당구 상당로 82"
+}
+
+### - 충청남도 센터 : 충남 홍성군 홍북읍 충남대로 21
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "충청남도 센터",
+ "address": "충남 홍성군 홍북읍 충남대로 21"
+}
+
+### - 전북특별자치도 센터 : 전북특별자치도 전주시 완산구 효자로 225
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "전북특별자치도 센터",
+ "address": "전북특별자치도 전주시 완산구 효자로 225"
+}
+
+### - 전라남도 센터 : 전남 무안군 삼향읍 오룡길 1
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "전라남도 센터",
+ "address": "전남 무안군 삼향읍 오룡길 1"
+}
+
+### - 경상북도 센터 : 경북 안동시 풍천면 도청대로 455
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "경상북도 센터",
+ "address": "경북 안동시 풍천면 도청대로 455"
+}
+
+### - 경상남도 센터 : 경남 창원시 의창구 중앙대로 300
+POST http://localhost:8082/api/v1/hubs?userId=123
+Content-Type: application/json
+
+{
+ "name": "경상남도 센터",
+ "address": "경남 창원시 의창구 중앙대로 300"
+}
\ No newline at end of file
diff --git a/hub-service/src/test/resources/http/hub-route-total-test.http b/hub-service/src/test/resources/http/hub-route-total-test.http
new file mode 100644
index 00000000..9635068d
--- /dev/null
+++ b/hub-service/src/test/resources/http/hub-route-total-test.http
@@ -0,0 +1,30 @@
+### 🔹 전체 허브 경로 목록 조회 (페이징)
+GET http://localhost:8082/api/v1/hub-routes
+Accept: application/json
+
+
+### 🔹 특정 경로 ID 조회
+GET http://localhost:8082/api/v1/hub-routes/898a53ef-21f4-4a8f-ae1d-5d9c4db9bdf5
+Accept: application/json
+
+
+### 🔹 특정 허브 간 다이렉트 경로 조회
+GET http://localhost:8082/api/v1/hub-routes/984feadc-b1db-43c5-81bc-a24ec5001ddc/d073a70d-e200-4301-af35-4c1a87798427/direct
+Accept: application/json
+
+
+### 🔹 허브 간 다이렉트 경로 생성 -> ✨ create-hub-routes.http 에서 테스트 및 더미데이터 생성
+
+
+### 🔹 허브 간 경로 삭제
+DELETE http://localhost:8082/api/v1/hub-routes/898a53ef-21f4-4a8f-ae1d-5d9c4db9bdf5?userId=123
+Accept: application/json
+
+
+### 🔹 허브 간 최단 경로 생성 (Path 기반) -> ✨create-hub-route-checkpoint.http 에서 테스트 및 더미데이터 생성
+
+
+### 🔹 허브 간 최단 경로 조회 (Path 기반) *
+GET http://localhost:8082/api/v1/hub-routes/eff77800-604d-441d-a736-63f1f7d25e51/ac20c293-6951-4916-bd0f-d32f6b3b815b/path
+Accept: application/json
+
diff --git a/hub-service/src/test/resources/http/hub-total-test.http b/hub-service/src/test/resources/http/hub-total-test.http
new file mode 100644
index 00000000..ff01742b
--- /dev/null
+++ b/hub-service/src/test/resources/http/hub-total-test.http
@@ -0,0 +1,23 @@
+
+### 🔹 허브 목록 조회 (페이징)
+GET http://localhost:8082/api/v1/hubs
+Accept: application/json
+
+### 🔹 특정 허브 조회 (GET, UUID 사용)
+GET http://localhost:8082/api/v1/hubs/b252fddc-a4b1-420e-94c2-9a91bebee41a
+Accept: application/json
+
+### 🔹 허브 생성 (POST) -> ✨ create-hub.http 에서 테스트 및 더미데이터 생성
+
+### 🔹 허브 검색 (해당 단어 포함하는 모든 주소, 이름 검색 가능)
+GET http://localhost:8082/api/v1/hubs/search?address=서울특별시 송파구
+Accept: application/json
+
+### 🔹 허브 업데이트 (PUT, UUID 사용)
+PUT http://localhost:8082/api/v1/hubs/b252fddc-a4b1-420e-94c2-9a91bebee41a?userId=123&address=충남 홍성군 홍북읍 충남대로 21
+Accept: application/json
+
+
+### 🔹 허브 삭제 (DELETE, UUID 사용)
+DELETE http://localhost:8082/api/v1/hubs/984feadc-b1db-43c5-81bc-a24ec5001ddc?userId=123
+Accept: application/json
diff --git a/init.sql b/init.sql
new file mode 100644
index 00000000..0fc73bbf
--- /dev/null
+++ b/init.sql
@@ -0,0 +1,9 @@
+-- init.sql
+CREATE SCHEMA IF NOT EXISTS user_service;
+CREATE SCHEMA IF NOT EXISTS order_service;
+CREATE SCHEMA IF NOT EXISTS hub_service;
+CREATE SCHEMA IF NOT EXISTS product_service;
+CREATE SCHEMA IF NOT EXISTS company_service;
+CREATE SCHEMA IF NOT EXISTS shipping_service;
+CREATE SCHEMA IF NOT EXISTS slack_service;
+CREATE SCHEMA IF NOT EXISTS stock_service;
diff --git a/order-service/Dockerfile.dev b/order-service/Dockerfile.dev
new file mode 100644
index 00000000..ad0907e8
--- /dev/null
+++ b/order-service/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", ":order-service:bootRun"]
diff --git a/order-service/build.gradle b/order-service/build.gradle
new file mode 100644
index 00000000..d6bc9770
--- /dev/null
+++ b/order-service/build.gradle
@@ -0,0 +1,43 @@
+ext {
+ set('springCloudVersion', "2024.0.0")
+}
+
+dependencies {
+ implementation project(':common-module')
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.postgresql:postgresql:42.6.0'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+
+ // QueryDSL
+ implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
+ annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
+ annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
+ annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
+
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
+
+
+// querydsl 설정
+def querydslDir = "$buildDir/generated/querydsl"
+
+tasks.withType(JavaCompile).configureEach {
+ options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
+}
+
+sourceSets {
+ main {
+ java {
+ srcDirs += querydslDir
+ }
+ }
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/OrderServiceApplication.java b/order-service/src/main/java/com/sparta/orderservice/OrderServiceApplication.java
new file mode 100644
index 00000000..4745301c
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/OrderServiceApplication.java
@@ -0,0 +1,19 @@
+package com.sparta.orderservice;
+
+import com.sparta.commonmodule.config.JpaAuditingConfig;
+import com.sparta.commonmodule.config.SwaggerConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Import;
+
+@EnableFeignClients(basePackages = "com.sparta.orderservice.infrastructure.client")
+@SpringBootApplication(scanBasePackages = "com.sparta")
+@Import({SwaggerConfig.class, JpaAuditingConfig.class})
+public class OrderServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrderServiceApplication.class, args);
+ }
+
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/application/dto/OrderRequestDto.java b/order-service/src/main/java/com/sparta/orderservice/application/dto/OrderRequestDto.java
new file mode 100644
index 00000000..59cba32d
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/application/dto/OrderRequestDto.java
@@ -0,0 +1,37 @@
+package com.sparta.orderservice.application.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import jakarta.validation.constraints.NotNull;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Setter
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderRequestDto {
+
+ @NotNull(message = "name 필드는 필수입니다.")
+ private String name;
+
+ @NotNull(message = "supplierId 필드는 필수입니다.")
+ private UUID supplierId;
+
+ @NotNull(message = "receiverId 필드는 필수입니다.")
+ private UUID receiverId;
+
+ @NotNull(message = "productId 필드는 필수입니다.")
+ private UUID productId;
+
+ @NotNull(message = "totalPrice 필드는 필수입니다.")
+ private BigDecimal totalPrice;
+
+ private String requestDetail;
+
+ @NotNull(message = "quantity는 필수입니다.") //
+ private Integer quantity;
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/application/dto/OrderResponseDto.java b/order-service/src/main/java/com/sparta/orderservice/application/dto/OrderResponseDto.java
new file mode 100644
index 00000000..6a2f4f56
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/application/dto/OrderResponseDto.java
@@ -0,0 +1,57 @@
+package com.sparta.orderservice.application.dto;
+
+import com.sparta.orderservice.domain.model.Order;
+import com.sparta.orderservice.domain.model.OrderStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OrderResponseDto {
+ private UUID orderId;
+ private String name;
+ private UUID supplierId;
+ private UUID receiverId;
+ private UUID productId;
+ private BigDecimal totalPrice;
+ private OrderStatus status;
+ private String requestDetail;
+ private LocalDateTime createdAt;
+
+ public OrderResponseDto(Order order) {
+ this.orderId = order.getOrderId();
+ this.name = order.getName();
+ this.supplierId = order.getSupplierId();
+ this.receiverId = order.getReceiverId();
+ this.productId = order.getProductId();
+ this.totalPrice = order.getTotalPrice();
+ this.status = order.getStatus();
+ this.requestDetail = order.getRequestDetail();
+ this.createdAt = order.getCreatedAt();
+ }
+
+ // 더 직관적인 DTO 변환
+ public static OrderResponseDto fromEntity(Order order) {
+ return OrderResponseDto.builder()
+ .orderId(order.getOrderId())
+ .name(order.getName())
+ .supplierId(order.getSupplierId())
+ .receiverId(order.getReceiverId())
+ .productId(order.getProductId())
+ .totalPrice(order.getTotalPrice())
+ .status(order.getStatus())
+ .requestDetail(order.getRequestDetail())
+ .createdAt(order.getCreatedAt())
+ .build();
+ }
+}
+
+
diff --git a/order-service/src/main/java/com/sparta/orderservice/application/dto/UpdateOrderRequestDto.java b/order-service/src/main/java/com/sparta/orderservice/application/dto/UpdateOrderRequestDto.java
new file mode 100644
index 00000000..f37898a5
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/application/dto/UpdateOrderRequestDto.java
@@ -0,0 +1,18 @@
+package com.sparta.orderservice.application.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Getter
+@NoArgsConstructor
+public class UpdateOrderRequestDto {
+ private String name;
+ private UUID supplierId;
+ private UUID receiverID;
+ private UUID productId;
+ private BigDecimal totalPrice;
+ private String requestDetail;
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/application/service/OrderService.java b/order-service/src/main/java/com/sparta/orderservice/application/service/OrderService.java
new file mode 100644
index 00000000..add829d5
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/application/service/OrderService.java
@@ -0,0 +1,280 @@
+package com.sparta.orderservice.application.service;
+
+import com.sparta.commonmodule.exception.OperationNotAllowedException;
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.orderservice.application.dto.OrderRequestDto;
+import com.sparta.orderservice.application.dto.OrderResponseDto;
+import com.sparta.orderservice.domain.model.Order;
+import com.sparta.orderservice.domain.model.OrderStatus;
+import com.sparta.orderservice.domain.repository.OrderQueryDSLRepository;
+import com.sparta.orderservice.domain.repository.OrderRepository;
+import com.sparta.orderservice.infrastructure.client.ProductClient;
+import com.sparta.orderservice.infrastructure.client.ShippingClient;
+import com.sparta.orderservice.infrastructure.client.dto.request.CreateShippingRequestDto;
+import com.sparta.orderservice.infrastructure.client.dto.response.CreateShippingResponseDto;
+import com.sparta.orderservice.infrastructure.client.dto.response.DecreaseProductQuantityResponseDto;
+import com.sparta.orderservice.infrastructure.client.dto.request.DecreaseProductQuantityRequestDto;
+import com.sparta.orderservice.infrastructure.client.dto.response.SlackNotificationDto;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class OrderService {
+
+ private final OrderRepository orderRepository;
+
+ @Qualifier("orderQueryDSLRepositoryImpl")
+ private final OrderQueryDSLRepository orderQueryDSLRepository;
+
+ private final ProductClient productClient;
+ private final ShippingClient shippingClient;
+
+ //주문 생성
+ @Transactional
+ public OrderResponseDto createOrder(OrderRequestDto requestDto) {
+
+ // 1. 재고 차감 요청 DTO 생성
+ DecreaseProductQuantityRequestDto reduceRequest =
+ DecreaseProductQuantityRequestDto.builder()
+ .companyId(requestDto.getSupplierId()) // supplierId → companyId
+ .hubId(requestDto.getReceiverId()) // receiverId → hubId
+ .quantity(50) // 기본 수량
+ .build();
+
+ // 2. FeignClient로 재고 차감 요청
+ DecreaseProductQuantityResponseDto response =
+ productClient.decreaseProductQuantity(requestDto.getProductId(), reduceRequest);
+
+ // 3. 실패 시 예외 발생
+ if (!response.getIsSuccess()) {
+ throw new OperationNotAllowedException("재고 차감에 실패하여 주문을 생성할 수 없습니다.");
+ }
+
+ // 4. 주문 저장
+ Order order = Order.builder()
+ .name(requestDto.getName())
+ .supplierId(requestDto.getSupplierId())
+ .receiverId(requestDto.getReceiverId())
+ .productId(requestDto.getProductId())
+ .totalPrice(requestDto.getTotalPrice())
+ .requestDetail(requestDto.getRequestDetail())
+ .status(OrderStatus.CREATED)
+ .build();
+
+ orderRepository.save(order);
+
+ // 5. 배송 요청 DTO 생성
+ CreateShippingRequestDto shippingRequest = CreateShippingRequestDto.builder()
+ .orderId(order.getOrderId())
+ .productId(order.getProductId())
+ .supplierId(order.getSupplierId()) // supplierId → companyId
+ .receiverId(order.getReceiverId()) // receiverId → hubId
+ .quantity(50)
+ .build();
+
+ // 6. FeignClient로 배송 요청
+ CreateShippingResponseDto shippingResponse = shippingClient.createShipping(shippingRequest);
+
+ // 7. 배송 실패 시 예외
+ if (!"READY".equals(shippingResponse.getStatus())) {
+ throw new OperationNotAllowedException("배송 생성 실패로 주문 생성 중단");
+ }
+
+ return new OrderResponseDto(order);
+ }
+
+
+
+
+
+/*
+ @Transactional
+ public OrderResponseDto createOrder2(OrderRequestDto requestDto) {
+
+ Order order = Order.builder()
+ .name(requestDto.getName())
+ .supplierId(requestDto.getSupplierId())
+ .receiverId(requestDto.getReceiverId())
+ .productId(requestDto.getProductId())
+ .totalPrice(requestDto.getTotalPrice())
+ .requestDetail(requestDto.getRequestDetail())
+ .status(OrderStatus.CREATED)
+ .build();
+
+ orderRepository.save(order);
+
+
+ // 1. 재고 차감 요청 DTO 생성
+ DecreaseProductQuantityRequestDto reduceRequest =
+ DecreaseProductQuantityRequestDto.builder()
+ .companyId(requestDto.getSupplierId()) // supplierId → companyId
+ .hubId(requestDto.getReceiverId()) // receiverId → hubId
+ .quantity(50) // 기본 수량
+ .build();
+
+ // 재고 감소 성공 여부
+ boolean stockDecreased = false;
+
+ try {
+ // 2. FeignClient 재고 차감 요청
+ DecreaseProductQuantityResponseDto response =
+ productClient.decreaseProductQuantity(requestDto.getProductId(), reduceRequest);
+ stockDecreased = true;
+
+ // 3. FeignClient 배송 생성 요청
+ CreateShippingResponseDto shippingResponse = shippingClient.create(shippingRequest);
+ UUID shippingId = shippingResponse.getShippingId();
+
+ // 4. 슬랙 생성 요청
+
+
+ } catch (Exception e) {
+
+ // 주문 재고 보상 트랜잭션 - 재고 복구
+ if (stockDecreased) {
+ compensateProduct(requestDto.getProductId(), reduceRequest.getQuantity());
+ }
+
+ // 배송 보상 트랜잭션 - 배송 삭제
+ if (shippingId != null) {
+ compensateShipping(shippingId);
+ }
+
+ throw new RuntimeException("주문 실패 및 보상 처리 완료", e);
+ }
+
+
+
+
+ return new OrderResponseDto(order);
+ }
+
+
+ // 주문 보상 트랜잭션 - 주문 삭제
+ private void compensateOrder(Order order) {
+ try {
+ orderRepository.delete(order);
+ log.info("주문 삭제 보상 완료");
+ } catch (Exception e) {
+ log.warn("주문 삭제 보상 실패", e);
+ }
+ }
+
+
+ // 주문 재고 보상 트랜잭션 - 재고 복구
+ private void compensateProduct(UUID productId, int quantity) {
+ try {
+ productClient.increaseProductQuantity(productId, quantity);
+ log.info("재고 복구 보상 완료");
+ } catch (Exception e) {
+ log.warn("재고 복구 보상 실패", e);
+ }
+ }
+
+ // 배송 보상 트랜잭션 - 배송 삭제
+ private void compensateShipping(UUID shippingId) {
+ try {
+ shippingClient.deleteShipping(shippingId);
+ log.info("배송 삭제 보상 완료");
+ } catch (Exception e) {
+ log.warn("배송 삭제 보상 실패", e);
+ }
+ }
+
+*/
+
+
+ // 주문 전체 조회
+ @Transactional(readOnly = true)
+ public List getALlOrders() {
+ List orders = orderRepository.findAll();
+ return orders.stream()
+ .map(OrderResponseDto::fromEntity)
+ .collect(Collectors.toList());
+ }
+
+ // 주문 단일 조회
+ @Transactional(readOnly = true)
+ public OrderResponseDto getOrderById(UUID orderId) {
+ Order order = orderRepository.findById(orderId)
+ .orElseThrow(() -> new ResourceNotFoundException("해당 주문을 찾을 수 없습니다."));
+ return OrderResponseDto.fromEntity(order);
+ }
+
+ // 주문 수정
+ @Transactional
+ public OrderResponseDto updateOrder(UUID orderId, OrderRequestDto requestDto) {
+ Order order = orderRepository.findById(orderId)
+ .orElseThrow(() -> new ResourceNotFoundException("해당 주문을 찾을 수 없습니다."));
+
+ if (order.getStatus() != OrderStatus.CREATED) {
+ throw new OperationNotAllowedException("CREATED 상태의 주문만 수정할 수 있습니다.");
+ }
+ order.updateOrderDetails(
+ requestDto.getName(),
+ requestDto.getSupplierId(),
+ requestDto.getReceiverId(),
+ requestDto.getProductId(),
+ requestDto.getTotalPrice(),
+ requestDto.getRequestDetail()
+ );
+ return new OrderResponseDto(order);
+ }
+
+ // 주문 삭제
+ @Transactional
+ public void deleteOrder(UUID orderId) {
+ Order order = orderRepository.findById(orderId)
+ .orElseThrow(() -> new ResourceNotFoundException("해당 주문을 찾을 수 없습니다."));
+
+ order.softDelete();
+ }
+
+ // 주문 취소
+ @Transactional
+ public OrderResponseDto cancelOrder(UUID orderId, String cancelReason) {
+ Order order = orderRepository.findById(orderId)
+ .orElseThrow(() -> new ResourceNotFoundException("해당 주문을 찾을 수 없습니다."));
+
+ order.cancel(cancelReason);
+ return OrderResponseDto.fromEntity(order);
+ }
+
+ // 주문 검색
+ @Transactional(readOnly = true)
+ public List searchOrders(String name, OrderStatus status) {
+ List result = orderQueryDSLRepository.searchOrders(name, status);
+ return result.stream()
+ .map(OrderResponseDto::fromEntity)
+ .collect(Collectors.toList());
+ }
+
+ // 슬랙 알림용 DTO 생성 메서드
+ @Transactional(readOnly = true)
+ public SlackNotificationDto getSlackNotificationDto(UUID orderId) {
+ Order order = orderRepository.findById(orderId)
+ .orElseThrow(() -> new ResourceNotFoundException("해당 주문을 찾을 수 없습니다."));
+
+ CreateShippingResponseDto shipping = shippingClient.getShippingInfo(orderId);
+
+ return SlackNotificationDto.builder()
+ .orderId(order.getOrderId())
+ .shippingId(shipping.getShippingId())
+ .shippingStatus(shipping.getStatus())
+ .route(shipping.getRoute())
+ .hubName(shipping.getHubName())
+ .hubManagerName(shipping.getHubManagerName())
+ .message(shipping.getMessage())
+ .build();
+ }
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/config/QueryDslConfig.java b/order-service/src/main/java/com/sparta/orderservice/config/QueryDslConfig.java
new file mode 100644
index 00000000..4589559e
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/config/QueryDslConfig.java
@@ -0,0 +1,19 @@
+package com.sparta.orderservice.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class QueryDslConfig {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Bean
+ public JPAQueryFactory jpaQueryFactory() {
+ return new JPAQueryFactory(entityManager);
+ }
+}
\ No newline at end of file
diff --git a/order-service/src/main/java/com/sparta/orderservice/domain/model/Order.java b/order-service/src/main/java/com/sparta/orderservice/domain/model/Order.java
new file mode 100644
index 00000000..bca4b714
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/domain/model/Order.java
@@ -0,0 +1,99 @@
+package com.sparta.orderservice.domain.model;
+
+import com.querydsl.core.annotations.QueryEntity;
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+import org.hibernate.annotations.SQLRestriction;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@QueryEntity
+@Entity
+@Table(name = "p_order")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Builder
+@SQLRestriction("is_deleted IS FALSE")
+public class Order extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private UUID orderId; // 주문 ID
+
+ @Column(nullable = false, length = 100)
+ private String name; // 주문명
+
+ @Column(nullable = false)
+ private UUID supplierId; // 공급업체 ID
+
+ @Column(nullable = false)
+ private UUID receiverId; // 수령업체 ID
+
+ @Column(nullable = false)
+ private UUID productId; // 상품 ID
+
+
+ @Column(nullable = false, precision = 10, scale = 2)
+ private BigDecimal totalPrice; // 주문 총 금액
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private OrderStatus status = OrderStatus.CREATED; // 주문 상태
+
+ @Lob
+ private String requestDetail; // 요청 사항
+
+ @Lob
+ private String cancelReason; // 주문 취소 사유
+
+
+ @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List orderItems = new ArrayList<>(); // 주문 아이템 1:N 관계 설정
+
+ public void setCreatedBy(Long createdBy) {
+ super.update(createdBy);
+ }
+
+
+ // 주문 생성자 (BaseEntity의 createdBy 강제 설정)
+ public Order(String name, UUID supplierId, UUID receiverId, UUID productId, BigDecimal totalPrice, String requestDetail) {
+ super(0L); // createdBy 기본값 0L (임시 사용자)
+ this.name = name;
+ this.supplierId = supplierId;
+ this.receiverId = receiverId;
+ this.productId = productId;
+ this.totalPrice = totalPrice;
+ this.requestDetail = requestDetail;
+ }
+
+
+ // 주문 update 메서드
+ public void updateOrderDetails(String name, UUID supplierId, UUID receiverId, UUID productId, BigDecimal totalPrice, String requestDetail) {
+ this.name = name;
+ this.supplierId = supplierId;
+ this.receiverId = receiverId;
+ this.productId = productId;
+ this.totalPrice = totalPrice;
+ this.requestDetail = requestDetail;
+ }
+ // 주문 soft 삭제
+ public void softDelete() {
+ super.delete(0L); // 사용자 인증 시스템 없으므로 임시로 0L 사용
+ }
+
+ // 주문 cancel
+ public void cancel(String cancelReason) {
+ if (this.status != OrderStatus.CREATED) {
+ throw new IllegalStateException("CREATED 상태의 주문만 취소할 수 있습니다.");
+ }
+ this.status = OrderStatus.CANCELED;
+ this.cancelReason = cancelReason;
+ }
+
+}
+
diff --git a/order-service/src/main/java/com/sparta/orderservice/domain/model/OrderItem.java b/order-service/src/main/java/com/sparta/orderservice/domain/model/OrderItem.java
new file mode 100644
index 00000000..fde15c34
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/domain/model/OrderItem.java
@@ -0,0 +1,34 @@
+package com.sparta.orderservice.domain.model;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Entity
+@Table(name = "order_items")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Builder
+public class OrderItem {
+
+ @Id
+ @GeneratedValue
+ private UUID orderItemId; // 주문 상세 ID
+
+ @ManyToOne
+ @JoinColumn(name = "order_id", nullable = false)
+ private Order order; // 주문 ID
+
+ @Column(nullable = false)
+ private UUID productId; // 상품 ID
+
+ @Column(nullable = false)
+ private int quantity; // 주문 수량
+
+ @Column(nullable = false, precision = 10, scale = 2)
+ private BigDecimal productPrice; // 삼품 금액
+}
+
diff --git a/order-service/src/main/java/com/sparta/orderservice/domain/model/OrderStatus.java b/order-service/src/main/java/com/sparta/orderservice/domain/model/OrderStatus.java
new file mode 100644
index 00000000..cf55d617
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/domain/model/OrderStatus.java
@@ -0,0 +1,6 @@
+package com.sparta.orderservice.domain.model;
+
+public enum OrderStatus {
+ CREATED, // 주문 생성됨
+ CANCELED // 주문 취소됨
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/domain/repository/OrderQueryDSLRepository.java b/order-service/src/main/java/com/sparta/orderservice/domain/repository/OrderQueryDSLRepository.java
new file mode 100644
index 00000000..c350f69c
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/domain/repository/OrderQueryDSLRepository.java
@@ -0,0 +1,10 @@
+package com.sparta.orderservice.domain.repository;
+
+import com.sparta.orderservice.domain.model.Order;
+import com.sparta.orderservice.domain.model.OrderStatus;
+
+import java.util.List;
+
+public interface OrderQueryDSLRepository {
+ List searchOrders(String name, OrderStatus status);
+}
\ No newline at end of file
diff --git a/order-service/src/main/java/com/sparta/orderservice/domain/repository/OrderRepository.java b/order-service/src/main/java/com/sparta/orderservice/domain/repository/OrderRepository.java
new file mode 100644
index 00000000..0efc45b3
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/domain/repository/OrderRepository.java
@@ -0,0 +1,14 @@
+package com.sparta.orderservice.domain.repository;
+
+import com.sparta.orderservice.domain.model.Order;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface OrderRepository {
+ Order save(Order order);
+ Optional findById(UUID orderId);
+ List findAll();
+ void delete(Order order);
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/ProductClient.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/ProductClient.java
new file mode 100644
index 00000000..32eeced0
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/ProductClient.java
@@ -0,0 +1,20 @@
+package com.sparta.orderservice.infrastructure.client;
+
+import com.sparta.orderservice.infrastructure.client.dto.response.DecreaseProductQuantityResponseDto;
+import com.sparta.orderservice.infrastructure.client.dto.request.DecreaseProductQuantityRequestDto;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.UUID;
+
+ @FeignClient(name = "product-service", path = "/api/v1/products")
+ public interface ProductClient {
+
+ @PutMapping("/{productId}/decrease")
+ DecreaseProductQuantityResponseDto decreaseProductQuantity(
+ @PathVariable("productId") UUID productId,
+ @RequestBody DecreaseProductQuantityRequestDto requestDto
+ );
+ }
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/ShippingClient.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/ShippingClient.java
new file mode 100644
index 00000000..c4ce3787
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/ShippingClient.java
@@ -0,0 +1,23 @@
+package com.sparta.orderservice.infrastructure.client;
+
+import com.sparta.orderservice.infrastructure.client.dto.request.CreateShippingRequestDto;
+import com.sparta.orderservice.infrastructure.client.dto.response.CreateShippingResponseDto;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.UUID;
+
+@FeignClient(name = "shipping-service", path = "/api/v1/shippings")
+public interface ShippingClient {
+
+ // 배송 생성
+ @PostMapping
+ CreateShippingResponseDto createShipping(@RequestBody CreateShippingRequestDto request);
+
+ // 주문 ID로 배송 조회
+ @GetMapping("/{orderId}")
+ CreateShippingResponseDto getShippingInfo(@PathVariable("orderId") UUID orderId);
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/request/CreateShippingRequestDto.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/request/CreateShippingRequestDto.java
new file mode 100644
index 00000000..f0a76c46
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/request/CreateShippingRequestDto.java
@@ -0,0 +1,22 @@
+package com.sparta.orderservice.infrastructure.client.dto.request;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+// 배송 생성 요청 DTO
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateShippingRequestDto {
+
+ private UUID orderId; // 주문 ID
+ private UUID productId; // 상품 ID
+ private UUID supplierId; // 출발지 (공급업체 ID)
+ private UUID receiverId; // 도착지 (수령업체 ID)
+ private Integer quantity; // 수량
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/request/DecreaseProductQuantityRequestDto.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/request/DecreaseProductQuantityRequestDto.java
new file mode 100644
index 00000000..f73456bf
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/request/DecreaseProductQuantityRequestDto.java
@@ -0,0 +1,14 @@
+package com.sparta.orderservice.infrastructure.client.dto.request;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.UUID;
+
+@Getter
+@Builder
+public class DecreaseProductQuantityRequestDto {
+ private UUID companyId;
+ private UUID hubId;
+ private Integer quantity;
+}
\ No newline at end of file
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/CreateShippingResponseDto.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/CreateShippingResponseDto.java
new file mode 100644
index 00000000..461b0104
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/CreateShippingResponseDto.java
@@ -0,0 +1,24 @@
+package com.sparta.orderservice.infrastructure.client.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+//배송 생성 응답 DTO
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateShippingResponseDto {
+
+ private UUID shippingId; // 생성된 배송 ID
+ private String status; // 배송 상태 (예: READY, SHIPPING 등)
+ private String message; // 성공/실패 메시지
+ private String route; // 배송 경로
+ private String hubName; // 허브 이름
+ private String hubManagerName; // 허브 담당자 이름
+
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/DecreaseProductQuantityResponseDto.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/DecreaseProductQuantityResponseDto.java
new file mode 100644
index 00000000..acd5747d
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/DecreaseProductQuantityResponseDto.java
@@ -0,0 +1,22 @@
+package com.sparta.orderservice.infrastructure.client.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+//Product 서비스에서 재고 차감 요청 후 반환되는 응답 DTO
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DecreaseProductQuantityResponseDto {
+
+ private UUID productId; // 재고 감소 상품 ID
+ private Boolean isSuccess; // 재고 감소 성공 여부
+ private Integer decreasedQuantity; // 실제 차감된 수량
+ private String message; // 성공/실패 메시지
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/SlackNotificationDto.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/SlackNotificationDto.java
new file mode 100644
index 00000000..94cf5b4f
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/client/dto/response/SlackNotificationDto.java
@@ -0,0 +1,24 @@
+package com.sparta.orderservice.infrastructure.client.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+//Slack 알림 관련해서 응답 DTO
+public class SlackNotificationDto {
+
+ private UUID orderId;
+ private UUID shippingId;
+ private String shippingStatus;
+ private String route;
+ private String hubName;
+ private String hubManagerName;
+ private String message;
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/repository/JpaOrderRepository.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/repository/JpaOrderRepository.java
new file mode 100644
index 00000000..df77e873
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/repository/JpaOrderRepository.java
@@ -0,0 +1,14 @@
+package com.sparta.orderservice.infrastructure.repository;
+
+import com.sparta.orderservice.domain.model.Order;
+import com.sparta.orderservice.domain.repository.OrderQueryDSLRepository;
+import com.sparta.orderservice.domain.repository.OrderRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+@Repository
+public interface JpaOrderRepository extends OrderRepository, JpaRepository{
+
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/infrastructure/repository/OrderQueryDSLRepositoryImpl.java b/order-service/src/main/java/com/sparta/orderservice/infrastructure/repository/OrderQueryDSLRepositoryImpl.java
new file mode 100644
index 00000000..536fe7a4
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/infrastructure/repository/OrderQueryDSLRepositoryImpl.java
@@ -0,0 +1,39 @@
+package com.sparta.orderservice.infrastructure.repository;
+
+
+import com.querydsl.core.BooleanBuilder;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.orderservice.domain.model.Order;
+import com.sparta.orderservice.domain.model.OrderStatus;
+import com.sparta.orderservice.domain.repository.OrderQueryDSLRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+import com.sparta.orderservice.domain.model.QOrder;
+
+
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class OrderQueryDSLRepositoryImpl implements OrderQueryDSLRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public List searchOrders(String name, OrderStatus status) {
+ QOrder order = QOrder.order;
+ BooleanBuilder builder = new BooleanBuilder();
+
+ if (name != null && !name.isBlank()) {
+ builder.and(order.name.containsIgnoreCase(name));
+ }
+
+ if (status != null) {
+ builder.and(order.status.eq(status));
+ }
+
+ return queryFactory.selectFrom(order)
+ .where(builder)
+ .fetch();
+ }
+}
diff --git a/order-service/src/main/java/com/sparta/orderservice/presentation/controller/OrderController.java b/order-service/src/main/java/com/sparta/orderservice/presentation/controller/OrderController.java
new file mode 100644
index 00000000..42453a9e
--- /dev/null
+++ b/order-service/src/main/java/com/sparta/orderservice/presentation/controller/OrderController.java
@@ -0,0 +1,95 @@
+package com.sparta.orderservice.presentation.controller;
+
+import com.sparta.orderservice.application.dto.OrderRequestDto;
+import com.sparta.orderservice.application.dto.OrderResponseDto;
+import com.sparta.orderservice.application.service.OrderService;
+import com.sparta.orderservice.domain.model.OrderStatus;
+import com.sparta.orderservice.infrastructure.client.dto.response.SlackNotificationDto;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+
+@Tag(name = "Order Service", description = "주문 서비스 API")
+@RestController
+@RequestMapping("/api/v1/orders")
+public class OrderController {
+
+ private final OrderService orderService;
+
+ public OrderController(OrderService orderService) {
+
+ this.orderService = orderService;
+ }
+
+ // 주문 생성
+ @Operation(summary = "Order 등록 ", description = "Order 생성 api 입니다.")
+ @PostMapping
+ public ResponseEntity createOrder(
+ @Valid @RequestBody OrderRequestDto requestDto) {
+ OrderResponseDto responseDto = orderService.createOrder(requestDto);
+ return ResponseEntity.ok(responseDto);
+ }
+ // 주문 전체 조회
+ @Operation(summary = "Order 전체 조회 ", description = "Order 전체 조회 api 입니다.")
+ @GetMapping
+ public ResponseEntity> getAllOrders(){
+ return ResponseEntity.ok(orderService.getALlOrders());
+ }
+ // 주문 단일 조회
+ @Operation(summary = "Order 단일 조회 ", description = "Order 단일 조회 api 입니다.")
+ @GetMapping("/{id}")
+ public ResponseEntity getOrderById(@PathVariable("id") UUID id){
+ return ResponseEntity.ok(orderService.getOrderById(id));
+ }
+
+ // 주문 수정
+ @Operation(summary = "Order 수정 ", description = "Order 수정 입니다.")
+ @PutMapping("/{id}")
+ public ResponseEntity updateOrder(
+ @PathVariable("id") UUID id,
+ @RequestBody OrderRequestDto orderRequestDto) {
+ return ResponseEntity.ok(orderService.updateOrder(id, orderRequestDto));
+ }
+
+ // 주문 삭제
+ @Operation(summary = "Order 삭제 ", description = "Order 삭제 api 입니다.")
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteOrder(@PathVariable("id") UUID id) {
+ orderService.deleteOrder(id);
+ return ResponseEntity.ok("주문이 성공적으로 삭제되었습니다.");
+ }
+
+ // 주문 취소
+ @Operation(summary = "Order 취소 ", description = "Order 생성 api 입니다.")
+ @PatchMapping("/{id}/cancel")
+ public ResponseEntity cancelOrder(
+ @PathVariable("id") UUID id,
+ @RequestParam("reason") String cancelReason) {
+ return ResponseEntity.ok(orderService.cancelOrder(id, cancelReason));
+ }
+
+ // 주문 검색
+ @GetMapping("/search")
+ @Operation(summary = "Order 검색", description = "Order 검색 api 입니다.")
+ public ResponseEntity> searchOrders(
+ @RequestParam(required = false) String name,
+ @RequestParam(required = false) OrderStatus status
+ ) {
+ return ResponseEntity.ok(orderService.searchOrders(name, status));
+ }
+
+ // slack 응답
+ @Operation(summary = "Slack 알림 응답", description = "Slack 도메인에서 주문 정보를 가져가기 위한 api입니다.")
+ @GetMapping("/{id}/slack-info")
+ public ResponseEntity getSlackNotificationInfo(@PathVariable UUID id) {
+ SlackNotificationDto slackDto = orderService.getSlackNotificationDto(id);
+ return ResponseEntity.ok(slackDto);
+ }
+
+
+}
diff --git a/order-service/src/main/resources/application.yml b/order-service/src/main/resources/application.yml
new file mode 100644
index 00000000..5d435504
--- /dev/null
+++ b/order-service/src/main/resources/application.yml
@@ -0,0 +1,37 @@
+spring:
+ application:
+ name: order-service
+
+ datasource:
+ url: jdbc:postgresql://localhost:5001/maindb
+ username: admin
+ password: 1234
+ driver-class-name: org.postgresql.Driver
+
+ jpa:
+ hibernate:
+ ddl-auto: update
+ properties:
+ hibernate:
+ default_schema: order_service
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ show-sql: true
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ service-url:
+ defaultZone: http://localhost:8761/eureka/
+
+springdoc:
+ api-docs:
+ version: openapi_3_1
+ enabled: true
+ path: /order-service/v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
+
+server:
+ port: 8084
diff --git a/order-service/src/test/http/order.http b/order-service/src/test/http/order.http
new file mode 100644
index 00000000..120637bd
--- /dev/null
+++ b/order-service/src/test/http/order.http
@@ -0,0 +1,33 @@
+### Order 생성 API 테스트
+POST http://localhost:8084/api/v1/orders?userId=123
+Content-Type: application/json
+
+{
+ "name": "테스트 주문",
+ "supplierId": "550e8400-e29b-41d4-a716-446655440000",
+ "receiverId": "550e8400-e29b-41d4-a716-446655440001",
+ "productId": "550e8400-e29b-41d4-a716-446655440002",
+ "totalPrice": 25000.00,
+ "requestDetail": "빠른 배송 부탁드립니다."
+}
+
+### 모든 주문 조회
+GET http://localhost:8084/api/v1/orders
+
+### 특정 주문 조회 (실제 UUID로 바꿔서 사용)
+GET http://localhost:8084/api/v1/orders/fac5ba71-5844-4928-a2be-7e477517324c
+
+### 주문 수정 (실제 UUID로 바꿔서 사용)
+PUT http://localhost:8084/api/v1/orders/fac5ba71-5844-4928-a2be-7e477517324c
+Content-Type: application/json
+
+{
+ "name": "수정된 주문명",
+ "supplierId": "550e8400-e29b-41d4-a716-446655440000",
+ "receiverId": "550e8400-e29b-41d4-a716-446655440001",
+ "productId": "550e8400-e29b-41d4-a716-446655440002",
+ "totalPrice": 30000.00,
+ "requestDetail": "수정된 요청사항"
+}
+### 주문 삭제 (실제 UUID로 바꿔서 사용)
+DELETE http://localhost:8084/api/v1/orders/fac5ba71-5844-4928-a2be-7e477517324c
diff --git a/order-service/src/test/java/com/sparta/orderservice/OrderServiceApplicationTests.java b/order-service/src/test/java/com/sparta/orderservice/OrderServiceApplicationTests.java
new file mode 100644
index 00000000..8f759590
--- /dev/null
+++ b/order-service/src/test/java/com/sparta/orderservice/OrderServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.sparta.orderservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class OrderServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/payment-service/Dockerfile.dev b/payment-service/Dockerfile.dev
new file mode 100644
index 00000000..96d10d4a
--- /dev/null
+++ b/payment-service/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", ":payment-service:bootRun"]
diff --git a/payment-service/build.gradle b/payment-service/build.gradle
new file mode 100644
index 00000000..deb0a7e5
--- /dev/null
+++ b/payment-service/build.gradle
@@ -0,0 +1,15 @@
+ext {
+ set('springCloudVersion', "2024.0.0")
+}
+
+dependencies {
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
diff --git a/payment-service/src/main/java/com/sparta/paymentservice/PaymentServiceApplication.java b/payment-service/src/main/java/com/sparta/paymentservice/PaymentServiceApplication.java
new file mode 100644
index 00000000..e4589a0c
--- /dev/null
+++ b/payment-service/src/main/java/com/sparta/paymentservice/PaymentServiceApplication.java
@@ -0,0 +1,13 @@
+package com.sparta.paymentservice;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication(scanBasePackages = "com.sparta")
+public class PaymentServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(PaymentServiceApplication.class, args);
+ }
+
+}
diff --git a/payment-service/src/main/java/com/sparta/paymentservice/presentation/PaymentController.java b/payment-service/src/main/java/com/sparta/paymentservice/presentation/PaymentController.java
new file mode 100644
index 00000000..ddc82169
--- /dev/null
+++ b/payment-service/src/main/java/com/sparta/paymentservice/presentation/PaymentController.java
@@ -0,0 +1,15 @@
+package com.sparta.paymentservice.presentation;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/payments")
+public class PaymentController {
+
+ @GetMapping("/test")
+ public String getPayment() {
+ return "Hello World";
+ }
+}
diff --git a/payment-service/src/main/resources/application.yml b/payment-service/src/main/resources/application.yml
new file mode 100644
index 00000000..a66fae6a
--- /dev/null
+++ b/payment-service/src/main/resources/application.yml
@@ -0,0 +1,13 @@
+spring:
+ application:
+ name: payment-service
+
+server:
+ port: 8086
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ service-url:
+ defaultZone: http://localhost:8761/eureka/
\ No newline at end of file
diff --git a/payment-service/src/test/java/com/sparta/paymentservice/PaymentServiceApplicationTests.java b/payment-service/src/test/java/com/sparta/paymentservice/PaymentServiceApplicationTests.java
new file mode 100644
index 00000000..48c7c5ca
--- /dev/null
+++ b/payment-service/src/test/java/com/sparta/paymentservice/PaymentServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.sparta.paymentservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class PaymentServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/product-service/Dockerfile.dev b/product-service/Dockerfile.dev
new file mode 100644
index 00000000..eb04bfb3
--- /dev/null
+++ b/product-service/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", ":product-service:bootRun"]
diff --git a/product-service/build.gradle b/product-service/build.gradle
new file mode 100644
index 00000000..cd87726d
--- /dev/null
+++ b/product-service/build.gradle
@@ -0,0 +1,41 @@
+ext {
+ set('springCloudVersion', "2024.0.0")
+}
+
+dependencies {
+ implementation project(':common-module')
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
+ implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
+ runtimeOnly 'org.postgresql:postgresql'
+
+ // QueryDSL
+ implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
+ annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
+ annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
+ annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
+
+
+// ✅ QueryDSL 자동 생성 디렉토리 설정
+tasks.withType(JavaCompile).configureEach {
+ options.annotationProcessorGeneratedSourcesDirectory = file("$buildDir/generated/querydsl")
+}
+
+// ✅ 자동 생성된 QueryDSL 클래스 경로 추가
+sourceSets {
+ main {
+ java {
+ srcDirs += "$buildDir/generated/querydsl"
+ }
+ }
+}
\ No newline at end of file
diff --git a/product-service/src/main/java/com/sparta/product/ProductServiceApplication.java b/product-service/src/main/java/com/sparta/product/ProductServiceApplication.java
new file mode 100644
index 00000000..8fbdd84f
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/ProductServiceApplication.java
@@ -0,0 +1,19 @@
+package com.sparta.product;
+
+import com.sparta.commonmodule.config.JpaAuditingConfig;
+import com.sparta.commonmodule.config.SwaggerConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Import;
+
+@SpringBootApplication(scanBasePackages = "com.sparta")
+@EnableFeignClients
+@Import({SwaggerConfig.class, JpaAuditingConfig.class})
+public class ProductServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ProductServiceApplication.class, args);
+ }
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/application/dto/DeleteProductServiceRequestDto.java b/product-service/src/main/java/com/sparta/product/application/dto/DeleteProductServiceRequestDto.java
new file mode 100644
index 00000000..12eb24e8
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/application/dto/DeleteProductServiceRequestDto.java
@@ -0,0 +1,19 @@
+package com.sparta.product.application.dto;
+
+import lombok.Builder;
+
+import java.util.UUID;
+
+@Builder
+public record DeleteProductServiceRequestDto(Long userId,
+ UUID productId) {
+
+ // 요청 값 -> 서비스 DTO 변환 메서드
+ public static DeleteProductServiceRequestDto of(Long userId,
+ UUID productId) {
+ return DeleteProductServiceRequestDto.builder()
+ .userId(userId)
+ .productId(productId)
+ .build();
+ }
+}
diff --git a/product-service/src/main/java/com/sparta/product/application/dto/UpdateProductServiceRequestDto.java b/product-service/src/main/java/com/sparta/product/application/dto/UpdateProductServiceRequestDto.java
new file mode 100644
index 00000000..9379b0db
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/application/dto/UpdateProductServiceRequestDto.java
@@ -0,0 +1,28 @@
+package com.sparta.product.application.dto;
+
+
+import com.sparta.product.presentation.dto.request.UpdateProductRequestDto;
+import lombok.Builder;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Builder
+public record UpdateProductServiceRequestDto(UUID id,
+ String name,
+ String description,
+ BigDecimal price,
+ boolean isDisplay
+) {
+
+ // 요청 DTO -> 서비스 DTO 변환 메서드
+ public static UpdateProductServiceRequestDto of(UpdateProductRequestDto request, UUID productId) {
+ return UpdateProductServiceRequestDto.builder()
+ .id(productId)
+ .name(request.name())
+ .description(request.description())
+ .price(request.price())
+ .isDisplay(request.isDisplay())
+ .build();
+ }
+}
diff --git a/product-service/src/main/java/com/sparta/product/application/service/ProductService.java b/product-service/src/main/java/com/sparta/product/application/service/ProductService.java
new file mode 100644
index 00000000..4688d0a9
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/application/service/ProductService.java
@@ -0,0 +1,28 @@
+package com.sparta.product.application.service;
+
+import com.sparta.product.application.dto.DeleteProductServiceRequestDto;
+import com.sparta.product.application.dto.UpdateProductServiceRequestDto;
+import com.sparta.product.presentation.dto.request.CreateProductRequestDto;
+import com.sparta.product.presentation.dto.request.SearchProductRequestDto;
+import com.sparta.product.presentation.dto.response.*;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+import java.util.UUID;
+
+
+public interface ProductService {
+
+ CreateProductResponseDto createProduct(CreateProductRequestDto requestDto);
+
+ ReadProductResponseDto readProduct(UUID productId);
+
+ List readAllProduct();
+
+ UpdateProductResponseDto updateProduct(UpdateProductServiceRequestDto serviceDto);
+
+ void deleteProduct(DeleteProductServiceRequestDto serviceDto);
+
+ Page searchProducts(SearchProductRequestDto requestDto, Pageable pageable);
+}
diff --git a/product-service/src/main/java/com/sparta/product/application/service/ProductServiceImpl.java b/product-service/src/main/java/com/sparta/product/application/service/ProductServiceImpl.java
new file mode 100644
index 00000000..f5158d66
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/application/service/ProductServiceImpl.java
@@ -0,0 +1,121 @@
+package com.sparta.product.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.product.application.dto.DeleteProductServiceRequestDto;
+import com.sparta.product.application.dto.UpdateProductServiceRequestDto;
+import com.sparta.product.domain.model.Product;
+import com.sparta.product.domain.repository.ProductRepository;
+import com.sparta.product.infrastructure.client.CompanyClient;
+import com.sparta.product.presentation.dto.request.CreateProductRequestDto;
+import com.sparta.product.presentation.dto.request.SearchProductRequestDto;
+import com.sparta.product.presentation.dto.response.*;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class ProductServiceImpl implements ProductService {
+
+ private final ProductRepository productRepository;
+ private final CompanyClient companyClient;
+
+
+ /**
+ * 상품 생성
+ */
+ @Override
+ public CreateProductResponseDto createProduct(CreateProductRequestDto requestDto) {
+ validateCompanyExists(requestDto.companyId());
+ Product product = productRepository
+ .save(Product.createProduct(
+ requestDto.name(),
+ requestDto.description(),
+ requestDto.price(),
+ requestDto.isDisplay(),
+ requestDto.companyId()
+ ));
+
+ return CreateProductResponseDto.from(product);
+ }
+
+
+ /**
+ * 상품 단일 조회
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public ReadProductResponseDto readProduct(UUID productId) {
+ Product product = productRepository.findById(productId)
+ .orElseThrow(() -> new ResourceNotFoundException("찾을 수 없는 상품 입니다."));
+ return ReadProductResponseDto.from(product);
+ }
+
+
+ /**
+ * 상품 목록 조회
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public List readAllProduct() {
+ return productRepository.findAll()
+ .stream()
+ .map(ReadProductResponseDto::from)
+ .toList();
+ }
+
+
+ /**
+ * 상품 수정
+ */
+ @Override
+ public UpdateProductResponseDto updateProduct(UpdateProductServiceRequestDto serviceDto) {
+ Product product = productRepository.findById(serviceDto.id())
+ .orElseThrow(() -> new ResourceNotFoundException("찾을 수 없는 상품 입니다."));
+ product.updateProduct(
+ serviceDto.name(),
+ serviceDto.description(),
+ serviceDto.price(),
+ serviceDto.isDisplay()
+ );
+
+ return UpdateProductResponseDto.from(product);
+ }
+
+
+ /**
+ * 상품 삭제
+ */
+ @Override
+ public void deleteProduct(DeleteProductServiceRequestDto serviceDto) {
+ Product product = productRepository.findById(serviceDto.productId())
+ .orElseThrow(() -> new ResourceNotFoundException("찾을 수 없는 상품 입니다."));
+ product.delete(serviceDto.userId());
+ }
+
+
+ /**
+ * 상품 검색
+ */
+ @Override
+ @Transactional(readOnly = true)
+ public Page searchProducts(SearchProductRequestDto requestDto, Pageable pageable) {
+ return productRepository.searchProducts(requestDto, pageable);
+ }
+
+
+ // 업체 존재 검증 메서드
+ private void validateCompanyExists(UUID companyId) {
+ if (!companyClient.existsById(companyId)) {
+ throw new ResourceNotFoundException("해당 업체가 존재하지 않습니다.");
+ }
+ }
+
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/domain/model/Product.java b/product-service/src/main/java/com/sparta/product/domain/model/Product.java
new file mode 100644
index 00000000..71e274ae
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/domain/model/Product.java
@@ -0,0 +1,69 @@
+package com.sparta.product.domain.model;
+
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+import org.hibernate.annotations.SQLRestriction;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+
+@Entity
+@Table(name = "p_product")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@SQLRestriction("is_deleted IS FALSE")
+@Builder(access = AccessLevel.PRIVATE)
+public class Product extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "product_id", updatable = false, nullable = false)
+ private UUID id;
+
+ @Column(name = "product_name", nullable = false, length = 100)
+ private String name;
+
+ @Column(name = "product_description", nullable = false, length = 255)
+ private String description;
+
+ @Column(name = "product_price", nullable = false)
+ private BigDecimal price;
+
+ @Column(name = "is_display", nullable = false)
+ private boolean isDisplay;
+
+ @Column(name = "company_id", nullable = false)
+ private UUID companyId;
+
+
+ /**
+ * 상품 생성
+ */
+ public static Product createProduct(String name, String description, BigDecimal price, boolean isDisplay,UUID companyId) {
+ return Product.builder()
+ .name(name)
+ .description(description)
+ .price(price)
+ .isDisplay(isDisplay)
+ .companyId(companyId)
+ .build();
+ }
+
+
+ /**
+ * 상품 수정
+ */
+ public Product updateProduct(String name, String description, BigDecimal price, boolean isDisplay) {
+ this.name = name;
+ this.description = description;
+ this.price = price;
+ this.isDisplay = isDisplay;
+ return this;
+ }
+
+
+}
\ No newline at end of file
diff --git a/product-service/src/main/java/com/sparta/product/domain/repository/ProductRepository.java b/product-service/src/main/java/com/sparta/product/domain/repository/ProductRepository.java
new file mode 100644
index 00000000..a00f5617
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/domain/repository/ProductRepository.java
@@ -0,0 +1,22 @@
+package com.sparta.product.domain.repository;
+
+import com.sparta.product.domain.model.Product;
+import com.sparta.product.presentation.dto.request.SearchProductRequestDto;
+import com.sparta.product.presentation.dto.response.SearchProductResponseDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface ProductRepository {
+
+ Product save(Product product);
+
+ Optional findById(UUID productId);
+
+ List findAll();
+
+ Page searchProducts(SearchProductRequestDto requestDto, Pageable pageable);
+}
diff --git a/product-service/src/main/java/com/sparta/product/infrastructure/client/CompanyClient.java b/product-service/src/main/java/com/sparta/product/infrastructure/client/CompanyClient.java
new file mode 100644
index 00000000..5a33dfe4
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/infrastructure/client/CompanyClient.java
@@ -0,0 +1,17 @@
+package com.sparta.product.infrastructure.client;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.UUID;
+
+@FeignClient(name = "company-service")
+public interface CompanyClient {
+
+ /**
+ * 업체 존재 확인
+ */
+ @GetMapping("/api/v1/companies/{id}/exists")
+ boolean existsById(@PathVariable("id") UUID id);
+}
diff --git a/product-service/src/main/java/com/sparta/product/infrastructure/client/HubClient.java b/product-service/src/main/java/com/sparta/product/infrastructure/client/HubClient.java
new file mode 100644
index 00000000..e07a1dab
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/infrastructure/client/HubClient.java
@@ -0,0 +1,18 @@
+package com.sparta.product.infrastructure.client;
+
+import com.sparta.product.infrastructure.client.dto.response.HubResponseDto;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.UUID;
+
+@FeignClient(name = "hub-service")
+public interface HubClient {
+
+ /**
+ * 허브 단일 조회 (허브 존재 확인)
+ */
+ @GetMapping("/api/v1/hubs/{hub_id}")
+ HubResponseDto getHubById(@PathVariable("hub_id") UUID hubId);
+}
diff --git a/product-service/src/main/java/com/sparta/product/infrastructure/client/dto/response/HubResponseDto.java b/product-service/src/main/java/com/sparta/product/infrastructure/client/dto/response/HubResponseDto.java
new file mode 100644
index 00000000..23cb8c4a
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/infrastructure/client/dto/response/HubResponseDto.java
@@ -0,0 +1,10 @@
+package com.sparta.product.infrastructure.client.dto.response;
+
+
+import java.util.UUID;
+
+public class HubResponseDto {
+ private UUID id;
+ private String name;
+}
+
diff --git a/product-service/src/main/java/com/sparta/product/infrastructure/repository/JpaProductRepository.java b/product-service/src/main/java/com/sparta/product/infrastructure/repository/JpaProductRepository.java
new file mode 100644
index 00000000..f93c3c73
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/infrastructure/repository/JpaProductRepository.java
@@ -0,0 +1,11 @@
+package com.sparta.product.infrastructure.repository;
+
+import com.sparta.product.domain.model.Product;
+import com.sparta.product.domain.repository.ProductRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.UUID;
+
+public interface JpaProductRepository extends ProductRepository, JpaRepository, ProductQueryDSLRepository {
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/infrastructure/repository/ProductQueryDSLRepository.java b/product-service/src/main/java/com/sparta/product/infrastructure/repository/ProductQueryDSLRepository.java
new file mode 100644
index 00000000..1c1facf8
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/infrastructure/repository/ProductQueryDSLRepository.java
@@ -0,0 +1,12 @@
+package com.sparta.product.infrastructure.repository;
+
+import com.sparta.product.presentation.dto.request.SearchProductRequestDto;
+import com.sparta.product.presentation.dto.response.SearchProductResponseDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface ProductQueryDSLRepository {
+
+ Page searchProducts(SearchProductRequestDto requestDto, Pageable pageable);
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/infrastructure/repository/config/QueryDslConfig.java b/product-service/src/main/java/com/sparta/product/infrastructure/repository/config/QueryDslConfig.java
new file mode 100644
index 00000000..f3c1cde6
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/infrastructure/repository/config/QueryDslConfig.java
@@ -0,0 +1,21 @@
+package com.sparta.product.infrastructure.repository.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+
+@Configuration
+@RequiredArgsConstructor
+public class QueryDslConfig {
+
+ private final EntityManager em;
+
+ @Bean
+ public JPAQueryFactory jpaQueryFactory() {
+ return new JPAQueryFactory(em);
+ }
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/infrastructure/repository/impl/ProductQueryDSLRepositoryImpl.java b/product-service/src/main/java/com/sparta/product/infrastructure/repository/impl/ProductQueryDSLRepositoryImpl.java
new file mode 100644
index 00000000..906b92e6
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/infrastructure/repository/impl/ProductQueryDSLRepositoryImpl.java
@@ -0,0 +1,140 @@
+package com.sparta.product.infrastructure.repository.impl;
+
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.product.domain.model.Product;
+import com.sparta.product.infrastructure.repository.ProductQueryDSLRepository;
+import com.sparta.product.presentation.dto.request.SearchProductRequestDto;
+import com.sparta.product.presentation.dto.response.SearchProductResponseDto;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static com.sparta.product.domain.model.QProduct.product;
+
+@RequiredArgsConstructor
+public class ProductQueryDSLRepositoryImpl implements ProductQueryDSLRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+
+ @Override
+ public Page searchProducts(SearchProductRequestDto requestDto, Pageable pageable) {
+
+ // 동적 정렬 조건 생성
+ List> orderSpecifierList = dynamicOrder(pageable);
+
+ // 유효한 페이지 크기 설정
+ int pageSize = validatePageSize(pageable.getPageSize());
+
+
+ // 동적 쿼리 생성 및 조회
+ List resultList = queryFactory
+ .selectFrom(product)
+ .where(
+ nameContains(requestDto.name()),
+ descriptionContains(requestDto.description()),
+ companyIdEq(requestDto.companyId())
+ )
+ .orderBy(orderSpecifierList.toArray(new OrderSpecifier[0])) // 동적 정렬
+ .offset(pageable.getOffset()) // 페이징 - 시작 인덱스
+ .limit(pageSize) // 페이징 - 페이지 크기
+ .distinct() // 중복 제거
+ .fetch();
+
+
+ // 총 개수 조회 (count 쿼리 따로 실행)
+ Long totalCount = queryFactory
+ .select(product.count())
+ .from(product)
+ .where(
+ nameContains(requestDto.name()),
+ descriptionContains(requestDto.description()),
+ companyIdEq(requestDto.companyId())
+ )
+ .fetchOne();
+
+ if (totalCount == null) {
+ totalCount = 0L;
+ }
+
+ // Product -> ProductSearchResponseDto 변환
+ List content = resultList.stream()
+ .map(SearchProductResponseDto::from)
+ .collect(Collectors.toList());
+
+ return new PageImpl<>(content, pageable, totalCount);
+ }
+
+
+
+ private BooleanExpression nameContains(String name) {
+ return name != null ? product.name.containsIgnoreCase(name) : null;
+ }
+
+
+ private BooleanExpression descriptionContains(String description) {
+ return description != null ? product.description.containsIgnoreCase(description) : null;
+ }
+
+
+ private BooleanExpression companyIdEq(UUID companyId) {
+ return companyId != null ? product.companyId.eq(companyId) : null;
+ }
+
+
+
+ /**
+ * 동적 정렬 조건 생성 메서드
+ * @param pageable
+ * @return
+ */
+ private List> dynamicOrder(Pageable pageable) {
+
+ List> orderSpecifierList = new ArrayList<>();
+
+ if (pageable.getSort() != null) {
+ for (Sort.Order sortOrder : pageable.getSort()) {
+ com.querydsl.core.types.Order direction
+ = sortOrder.isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC;
+
+ switch (sortOrder.getProperty()) {
+ case "createdAt": // 상품 생성일 기준 정렬
+ orderSpecifierList.add(new OrderSpecifier<>(direction, product.createdAt));
+ break;
+ case "updatedAt": // 상품 업데이트 기준 정렬
+ orderSpecifierList.add(new OrderSpecifier<>(direction, product.updatedAt));
+ break;
+ default: // 잘못된 정렬 필드 처리
+ throw new IllegalArgumentException(
+ "잘못된 정렬 필드입니다. : " + sortOrder.getProperty());
+ }
+ }
+ } else {
+ // 기본 정렬: 오름차순, createdAt
+ orderSpecifierList.add(new OrderSpecifier<>(com.querydsl.core.types.Order.ASC, product.createdAt));
+ }
+ return orderSpecifierList;
+ }
+
+
+ /**
+ * 유효한 페이지 크기 검증 및 설정
+ * @param pageSize : 요청된 페이지 크기
+ * @return int : 유효한 페이지 크기
+ */
+ private int validatePageSize(int pageSize) {
+ return Set.of(10, 30, 50).contains(pageSize) ? pageSize : 10;
+ }
+
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/presentation/ProductController.java b/product-service/src/main/java/com/sparta/product/presentation/ProductController.java
new file mode 100644
index 00000000..cf13fa5b
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/ProductController.java
@@ -0,0 +1,99 @@
+package com.sparta.product.presentation;
+
+import com.sparta.commonmodule.aop.RoleCheck;
+import com.sparta.product.application.dto.DeleteProductServiceRequestDto;
+import com.sparta.product.application.dto.UpdateProductServiceRequestDto;
+import com.sparta.product.application.service.ProductService;
+import com.sparta.product.presentation.dto.request.*;
+import com.sparta.product.presentation.dto.response.*;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/v1/products")
+@RequiredArgsConstructor
+@Tag(name = "Product Service", description = "상품 서비스 API")
+public class ProductController {
+
+ private final ProductService productService;
+
+ /**
+ * 상품 생성
+ */
+ @Operation(summary = "Product 등록", description = "Product 생성 api 입니다.")
+ @RoleCheck("ROLE_MASTER, ROLE_HUB, ROLE_COMPANY")
+ @PostMapping
+ public ResponseEntity createProduct(@RequestBody CreateProductRequestDto requestDto,
+ @RequestHeader(value = "user_id", required = true) Long userId) {
+ return ResponseEntity.ok(productService.createProduct(requestDto));
+ }
+
+
+ /**
+ * 상품 단일 조회
+ */
+ @Operation(summary = "Product 단일 조회", description = "Product 단일 조회 api 입니다.")
+ @RoleCheck("ROLE_MASTER, ROLE_HUB, ROLE_COMPANY, ROLE_SHIPPING")
+ @GetMapping("/{productId}")
+ public ResponseEntity readProduct(@PathVariable UUID productId) {
+ return ResponseEntity.ok(productService.readProduct(productId));
+ }
+
+
+ /**
+ * 상품 목록 조회
+ */
+ @RoleCheck("ROLE_MASTER, ROLE_HUB, ROLE_COMPANY, ROLE_SHIPPING")
+ @GetMapping
+ public ResponseEntity> readAllProduct() {
+ return ResponseEntity.ok(productService.readAllProduct());
+ }
+
+
+ /**
+ * 상품 수정
+ */
+ @RoleCheck("ROLE_MASTER, ROLE_HUB, ROLE_COMPANY")
+ @PutMapping("/{productId}")
+ public ResponseEntity updateProduct(@PathVariable UUID productId,
+ @RequestBody UpdateProductRequestDto requestDto) {
+ return ResponseEntity.ok(productService.updateProduct(
+ UpdateProductServiceRequestDto.of(requestDto, productId)));
+ }
+
+
+ /**
+ * 상품 삭제
+ */
+ @RoleCheck("ROLE_MASTER, ROLE_HUB")
+ @DeleteMapping("/{productId}")
+ public ResponseEntity deleteProduct(@PathVariable UUID productId,
+ @RequestHeader(value = "user_id", required = true) Long userId) {
+ productService.deleteProduct(
+ DeleteProductServiceRequestDto.of(userId, productId));
+ return ResponseEntity.noContent().build();
+ }
+
+
+ /**
+ * 상품 검색
+ */
+ @RoleCheck("ROLE_MASTER, ROLE_HUB, ROLE_COMPANY, ROLE_SHIPPING")
+ @GetMapping("/search")
+ public ResponseEntity> searchProducts(@ModelAttribute SearchProductRequestDto requestDto,
+ Pageable pageable) {
+ return ResponseEntity.ok(productService.searchProducts(requestDto, pageable));
+ }
+
+
+
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/presentation/dto/request/CreateProductRequestDto.java b/product-service/src/main/java/com/sparta/product/presentation/dto/request/CreateProductRequestDto.java
new file mode 100644
index 00000000..89d8b1ca
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/dto/request/CreateProductRequestDto.java
@@ -0,0 +1,13 @@
+package com.sparta.product.presentation.dto.request;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+
+public record CreateProductRequestDto(String name,
+ String description,
+ BigDecimal price,
+ boolean isDisplay,
+ UUID companyId) {
+
+}
diff --git a/product-service/src/main/java/com/sparta/product/presentation/dto/request/SearchProductRequestDto.java b/product-service/src/main/java/com/sparta/product/presentation/dto/request/SearchProductRequestDto.java
new file mode 100644
index 00000000..c75954bc
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/dto/request/SearchProductRequestDto.java
@@ -0,0 +1,14 @@
+package com.sparta.product.presentation.dto.request;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+public record SearchProductRequestDto(String name,
+ String description,
+ BigDecimal price,
+ Integer quantity,
+ Boolean isDisplay,
+ UUID companyId,
+ UUID hubId) {
+}
+
diff --git a/product-service/src/main/java/com/sparta/product/presentation/dto/request/UpdateProductRequestDto.java b/product-service/src/main/java/com/sparta/product/presentation/dto/request/UpdateProductRequestDto.java
new file mode 100644
index 00000000..e56a329a
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/dto/request/UpdateProductRequestDto.java
@@ -0,0 +1,9 @@
+package com.sparta.product.presentation.dto.request;
+
+import java.math.BigDecimal;
+
+public record UpdateProductRequestDto(String name,
+ String description,
+ BigDecimal price,
+ boolean isDisplay) {
+}
diff --git a/product-service/src/main/java/com/sparta/product/presentation/dto/response/CreateProductResponseDto.java b/product-service/src/main/java/com/sparta/product/presentation/dto/response/CreateProductResponseDto.java
new file mode 100644
index 00000000..db56eaf5
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/dto/response/CreateProductResponseDto.java
@@ -0,0 +1,29 @@
+package com.sparta.product.presentation.dto.response;
+
+
+import com.sparta.product.domain.model.Product;
+import lombok.Builder;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Builder
+public record CreateProductResponseDto(UUID id,
+ String name,
+ String description,
+ BigDecimal price,
+ boolean isDisplay,
+ UUID companyId) {
+
+ // Entity -> DTO 변환 메서드
+ public static CreateProductResponseDto from(Product product) {
+ return CreateProductResponseDto.builder()
+ .id(product.getId())
+ .name(product.getName())
+ .description(product.getDescription())
+ .price(product.getPrice())
+ .isDisplay(product.isDisplay())
+ .companyId(product.getCompanyId())
+ .build();
+ }
+}
diff --git a/product-service/src/main/java/com/sparta/product/presentation/dto/response/ReadProductResponseDto.java b/product-service/src/main/java/com/sparta/product/presentation/dto/response/ReadProductResponseDto.java
new file mode 100644
index 00000000..52c27f6b
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/dto/response/ReadProductResponseDto.java
@@ -0,0 +1,30 @@
+package com.sparta.product.presentation.dto.response;
+
+
+import com.sparta.product.domain.model.Product;
+import lombok.Builder;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Builder
+public record ReadProductResponseDto(UUID id,
+ String name,
+ String description,
+ BigDecimal price,
+ boolean isDisplay,
+ UUID companyId) {
+
+ // Entity -> DTO 변환 메서드
+ public static ReadProductResponseDto from(Product product) {
+ return ReadProductResponseDto.builder()
+ .id(product.getId())
+ .name(product.getName())
+ .description(product.getDescription())
+ .price(product.getPrice())
+ .isDisplay(product.isDisplay())
+ .companyId(product.getCompanyId())
+ .build();
+ }
+}
+
diff --git a/product-service/src/main/java/com/sparta/product/presentation/dto/response/SearchProductResponseDto.java b/product-service/src/main/java/com/sparta/product/presentation/dto/response/SearchProductResponseDto.java
new file mode 100644
index 00000000..ec77df28
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/dto/response/SearchProductResponseDto.java
@@ -0,0 +1,27 @@
+package com.sparta.product.presentation.dto.response;
+
+import com.sparta.product.domain.model.Product;
+import lombok.Builder;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Builder
+public record SearchProductResponseDto(UUID id,
+ String name,
+ String description,
+ BigDecimal price,
+ Boolean isDisplay,
+ UUID companyId) {
+ // Entity -> DTO 변환 메서드
+ public static SearchProductResponseDto from(Product product) {
+ return SearchProductResponseDto.builder()
+ .id(product.getId())
+ .name(product.getName())
+ .description(product.getDescription())
+ .price(product.getPrice())
+ .isDisplay(product.isDisplay())
+ .companyId(product.getCompanyId())
+ .build();
+ }
+}
diff --git a/product-service/src/main/java/com/sparta/product/presentation/dto/response/UpdateProductResponseDto.java b/product-service/src/main/java/com/sparta/product/presentation/dto/response/UpdateProductResponseDto.java
new file mode 100644
index 00000000..294e35c4
--- /dev/null
+++ b/product-service/src/main/java/com/sparta/product/presentation/dto/response/UpdateProductResponseDto.java
@@ -0,0 +1,26 @@
+package com.sparta.product.presentation.dto.response;
+
+import com.sparta.product.domain.model.Product;
+import lombok.Builder;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Builder
+public record UpdateProductResponseDto(UUID id,
+ String name,
+ String description,
+ BigDecimal price,
+ boolean isDisplay) {
+
+ // Entity -> DTO 변환 메서드
+ public static UpdateProductResponseDto from(Product product) {
+ return UpdateProductResponseDto.builder()
+ .id(product.getId())
+ .name(product.getName())
+ .description(product.getDescription())
+ .price(product.getPrice())
+ .isDisplay(product.isDisplay())
+ .build();
+ }
+}
diff --git a/product-service/src/main/resources/application.yml b/product-service/src/main/resources/application.yml
new file mode 100644
index 00000000..ddcde5bf
--- /dev/null
+++ b/product-service/src/main/resources/application.yml
@@ -0,0 +1,39 @@
+spring:
+ application:
+ name: product-service
+ datasource:
+ url: jdbc:postgresql://localhost:5001/maindb
+ username: admin
+ password: 1234
+ driver-class-name: org.postgresql.Driver
+ jpa:
+ database: postgresql
+ properties:
+ hibernate:
+ spring.jpa.open-in-view: false
+ format_sql: true
+ default_batch_fetch_size: 10
+ default_schema: product_service
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ hibernate:
+ ddl-auto: update
+ show-sql: true
+
+server:
+ port: 8083
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ service-url:
+ defaultZone: http://localhost:8761/eureka/
+
+springdoc:
+ api-docs:
+ version: openapi_3_1
+ enabled: true
+ path: /product-service/v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
\ No newline at end of file
diff --git a/product-service/src/test/http/product.http b/product-service/src/test/http/product.http
new file mode 100644
index 00000000..6d21df9a
--- /dev/null
+++ b/product-service/src/test/http/product.http
@@ -0,0 +1,116 @@
+### 유저 로그인
+POST http://localhost:8080/api/v1/users/sign-in
+Content-Type: application/json
+
+{
+ "username": "asdf123",
+ "password": "asdfgA12@"
+}
+> {%
+ client.global.set("access_token", response.headers.valueOf("Authorization"))
+%}
+
+
+### 상품 생성
+POST http://localhost:8080/api/v1/products
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "마른오징어3",
+ "description": "마른오징어 입니다3.",
+ "price": 1000,
+ "isDisplay": true,
+ "companyId": "b8166e5f-b844-441c-ae46-75130d4b9fb0"
+}
+
+
+### 상품 단일 조회
+GET http://localhost:8080/api/v1/products/238d66b2-f8de-4de9-b0e5-6b32fd620788
+Content-Type: application/json
+Authorization: {{access_token}}
+
+
+### 상품 목록 조회
+GET http://localhost:8080/api/v1/products
+Content-Type: application/json
+Authorization: {{access_token}}
+
+
+### 상품 수정
+PUT http://localhost:8080/api/v1/products/238d66b2-f8de-4de9-b0e5-6b32fd620788
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "name": "마른오징어 수정",
+ "description": "마른오징어 입니다 수정.",
+ "price": 3000,
+ "isDisplay": false
+}
+
+
+### 상품 수정(재고 감소 성공)
+PUT http://localhost:8080/api/v1/products/238d66b2-f8de-4de9-b0e5-6b32fd620788/decrease
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI3ODg3OTEsImV4cCI6MTc0Mjc5MjM5MX0.GTMZCuZ7-jfzYshDEfBdCAfelNFAexIogXxU6rzX3fNTaeQ0GPFilKyylO_E90PiX5DFUWBQ29oIChuMpUU-gg
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "companyId": "550e8400-e29b-41d4-a716-446655440000",
+ "hubId": "123e4567-e89b-12d3-a456-426614174000",
+ "quantity": 30
+}
+
+
+### 상품 수정(재고 감소 실패)
+PUT http://localhost:8080/api/v1/products/238d66b2-f8de-4de9-b0e5-6b32fd620788/decrease
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "companyId": "550e8400-e29b-41d4-a716-446655440000",
+ "hubId": "123e4567-e89b-12d3-a456-426614174000",
+ "quantity": 100
+}
+
+
+### 상품 수정(재고 증가)
+PUT http://localhost:8080/api/v1/products/238d66b2-f8de-4de9-b0e5-6b32fd620788/increase
+Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoiMSIsInJvbGUiOiJST0xFX01BU1RFUiIsInNsYWNrX25hbWUiOiJhc2RnZ2ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJpYXQiOjE3NDI3ODg3OTEsImV4cCI6MTc0Mjc5MjM5MX0.GTMZCuZ7-jfzYshDEfBdCAfelNFAexIogXxU6rzX3fNTaeQ0GPFilKyylO_E90PiX5DFUWBQ29oIChuMpUU-gg
+Content-Type: application/json
+Authorization: {{access_token}}
+
+{
+ "companyId": "550e8400-e29b-41d4-a716-446655440000",
+ "hubId": "123e4567-e89b-12d3-a456-426614174000",
+ "quantity": 30
+}
+
+
+### 상품 삭제
+DELETE http://localhost:8080/api/v1/products/225dc81a-7b96-4905-83b9-16945fef9a7d
+Authorization: {{access_token}}
+
+
+
+
+### 상품 검색 (상품 이름으로 검색)
+GET http://localhost:8080/api/v1/products/search?name=치킨2&page=0&size=10&sort=createdAt,asc
+Authorization: {{access_token}}
+
+### 상품 검색 (상품 설명으로 검색)
+GET http://localhost:8080/api/v1/products/search?description=마른오징어&page=0&size=10&sort=createdAt,asc
+Authorization: {{access_token}}
+
+### 상품 검색 (허브 ID로 검색)
+GET http://localhost:8080/api/v1/products/search?hubId=db2e0d31-18fb-4856-8d7a-5da08fbf6845&page=0&size=10&sort=createdAt,asc
+Authorization: {{access_token}}
+
+### 상품 검색 (업체 ID로 검색)
+GET http://localhost:8080/api/v1/products/search?companyId=b8166e5f-b844-441c-ae46-75130d4b9fb0&page=0&size=10&sort=createdAt,asc
+Authorization: {{access_token}}
+
+
+
+
diff --git a/product-service/src/test/java/com/sparta/product/ProductServiceApplicationTests.java b/product-service/src/test/java/com/sparta/product/ProductServiceApplicationTests.java
new file mode 100644
index 00000000..27576f1f
--- /dev/null
+++ b/product-service/src/test/java/com/sparta/product/ProductServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.sparta.product;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class ProductServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/settings.gradle b/settings.gradle
index cf68ea9d..767a7376 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -2,4 +2,13 @@ rootProject.name = 'logistics-delivery'
include('eureka')
include('gateway')
-include('user')
+include('common-module')
+include('user-service')
+include('order-service')
+include('company-service')
+include('hub-service')
+include('product-service')
+include('slack-service')
+include('payment-service')
+include('shipping-service')
+include('stock-service')
diff --git a/shipping-service/build.gradle b/shipping-service/build.gradle
new file mode 100644
index 00000000..bab2915d
--- /dev/null
+++ b/shipping-service/build.gradle
@@ -0,0 +1,48 @@
+ext {
+ set('springCloudVersion', "2024.0.0")
+}
+dependencies {
+
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+
+ implementation "com.querydsl:querydsl-jpa:5.1.0:jakarta"
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
+ annotationProcessor "jakarta.persistence:jakarta.persistence-api:3.1.0"
+ annotationProcessor "jakarta.annotation:jakarta.annotation-api:2.1.1"
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
+ implementation 'org.hibernate:hibernate-core:6.3.1.Final'
+ implementation "org.postgresql:postgresql:42.7.2"
+ implementation project(':common-module')
+ runtimeOnly 'com.h2database:h2'
+}
+
+
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ }
+}
+
+// ✅ QueryDSL Q클래스 생성 디렉토리 설정
+def querydslDir = "$buildDir/generated/querydsl"
+
+tasks.withType(JavaCompile).configureEach {
+ options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
+}
+
+sourceSets {
+ main {
+ java {
+ srcDirs += querydslDir
+ }
+ }
+}
+
+clean {
+ delete file(querydslDir)
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/request/ShippingManagerCreateRequestDto.java b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/request/ShippingManagerCreateRequestDto.java
new file mode 100644
index 00000000..5082daee
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/request/ShippingManagerCreateRequestDto.java
@@ -0,0 +1,25 @@
+package com.sparta.shippingmanager.application.dto.request;
+
+import com.sparta.shippingmanager.domain.model.ManagerType;
+import com.sparta.shippingmanager.domain.model.ShippingManager;
+import com.sparta.shippingmanager.domain.model.trnas.ShippingManagerSelf;
+
+import java.util.UUID;
+
+public record ShippingManagerCreateRequestDto(
+ UUID shippingManagerId,
+ ManagerType managerType,
+ Boolean isActive,
+ Integer count
+) {
+ public ShippingManagerSelf of (){
+ return new ShippingManagerSelf(
+ this.shippingManagerId(),
+ this.managerType(),
+ this.isActive(),
+ this.count()
+
+ );
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/request/ShippingManagerSearchCondition.java b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/request/ShippingManagerSearchCondition.java
new file mode 100644
index 00000000..44f1631e
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/request/ShippingManagerSearchCondition.java
@@ -0,0 +1,21 @@
+package com.sparta.shippingmanager.application.dto.request;
+
+import com.sparta.shippingmanager.domain.model.ManagerType;
+import lombok.*;
+
+import java.util.UUID;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ShippingManagerSearchCondition {
+
+ private UUID shippingManagerId;
+ private ManagerType managerType;
+ private Integer shippingOrder;
+ private String sortBy;
+ private int pageSize; // 허용 값: 10, 30, 50
+ private int page;
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/response/ShippingManagerResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/response/ShippingManagerResponseDto.java
new file mode 100644
index 00000000..85e0be3b
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/response/ShippingManagerResponseDto.java
@@ -0,0 +1,20 @@
+package com.sparta.shippingmanager.application.dto.response;
+
+import com.sparta.shippingmanager.domain.model.ManagerType;
+import com.sparta.shippingmanager.domain.model.ShippingManager;
+
+import java.util.UUID;
+
+public record ShippingManagerResponseDto(
+ ManagerType managerType,
+ UUID shippingManagerId,
+ Integer shippingOrder
+) {
+ public static ShippingManagerResponseDto from(ShippingManager manager) {
+ return new ShippingManagerResponseDto(
+ manager.getManagerType(),
+ manager.getShippingManagerId(),
+ manager.getShippingOrder()
+ );
+ }
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/response/ShippingManagerSearchResult.java b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/response/ShippingManagerSearchResult.java
new file mode 100644
index 00000000..5808230d
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/application/dto/response/ShippingManagerSearchResult.java
@@ -0,0 +1,19 @@
+package com.sparta.shippingmanager.application.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ShippingManagerSearchResult {
+ private List content;
+ private int page; // 현재 페이지 번호
+ private int pageSize; // 한 페이지에 몇 건
+ private long totalCount; // 전체 개수
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/application/service/ShippingManagerService.java b/shipping-service/src/main/java/com/sparta/shippingmanager/application/service/ShippingManagerService.java
new file mode 100644
index 00000000..c99b5227
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/application/service/ShippingManagerService.java
@@ -0,0 +1,79 @@
+package com.sparta.shippingmanager.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.shippingmanager.application.dto.request.ShippingManagerCreateRequestDto;
+import com.sparta.shippingmanager.application.dto.request.ShippingManagerSearchCondition;
+import com.sparta.shippingmanager.application.dto.response.ShippingManagerSearchResult;
+import com.sparta.shippingmanager.domain.model.ShippingManager;
+import com.sparta.shippingmanager.domain.repository.ShippingManagerRepository;
+import com.sparta.shippingmanager.application.dto.response.ShippingManagerResponseDto;
+import com.sparta.shippingmanager.infrastructure.ShippingManagerSearchRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.UUID;
+
+
+@Service
+@RequiredArgsConstructor
+public class ShippingManagerService {
+
+ private final ShippingManagerRepository repository;
+ private final ShippingManagerSearchRepository searchRepository;
+
+ private static final int MAX_ORDER = 10;
+
+
+ @Transactional
+ public ShippingManager assign() {
+ ShippingManager manager = repository.findNextManagerWithLock()
+ .stream().
+ findFirst().
+ orElseThrow(() -> new ResourceNotFoundException(" 배정 가능한 담당자가 없습니다."));
+
+
+ nextManagerOrder(manager.getShippingOrder());
+
+ return manager;
+
+ }
+ @Transactional
+ public ShippingManagerResponseDto create(ShippingManagerCreateRequestDto request,Long userId){
+ ShippingManager shippingManager = request.of().toShippingManager(userId);
+ repository.save(shippingManager);
+ return ShippingManagerResponseDto.from(shippingManager);
+
+ }
+ @Transactional(readOnly = true)
+ public ShippingManagerResponseDto getById(UUID managerId){
+ ShippingManager manager = repository.findById(managerId).orElseThrow(() -> new ResourceNotFoundException("해당 ID의 배송자는 존재하지 않습니다."));
+ return ShippingManagerResponseDto.from(manager);
+ }
+
+
+ public Page search(ShippingManagerSearchCondition condition) {
+ ShippingManagerSearchResult result = searchRepository.search(condition);
+ return new PageImpl<>(
+ result.getContent(),
+ PageRequest.of(result.getPage(), result.getPageSize()),
+ result.getTotalCount()
+ );
+ }
+
+
+ private void nextManagerOrder(int currentOrder) {
+ int nextOrder;
+ if (currentOrder + 1 > MAX_ORDER) {
+ nextOrder = 1;
+ } else {
+ nextOrder = currentOrder + 1;
+ }
+ repository.updateAllManagerOrders(nextOrder);
+
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/ManagerType.java b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/ManagerType.java
new file mode 100644
index 00000000..9f013d48
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/ManagerType.java
@@ -0,0 +1,6 @@
+package com.sparta.shippingmanager.domain.model;
+
+public enum ManagerType {
+ HUB, // 허브 배송 담당자
+ CARRIER // 업체 배송 담당자
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/ShippingManager.java b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/ShippingManager.java
new file mode 100644
index 00000000..54cec73c
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/ShippingManager.java
@@ -0,0 +1,69 @@
+package com.sparta.shippingmanager.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.util.UUID;
+@Entity
+@Table(name = "p_shipping_managers")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ShippingManager extends BaseEntity {
+
+ @Id
+ @Column(name = "shipping_managers_id", updatable = false, nullable = false)
+ private UUID id;
+
+ @Column(nullable = false)
+ private UUID shippingManagerId;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 20)
+ private ManagerType managerType;
+
+ @Column(nullable = false)
+ private Integer shippingOrder;
+
+ @Column(nullable = false)
+ private Boolean isActive;
+
+ private Integer count =0;
+
+
+ public ShippingManager(Long userId, UUID shippingManagerId, ManagerType managerType, Boolean isActive, Integer count) {
+ super(userId);
+ this.shippingManagerId = shippingManagerId;
+ this.managerType = managerType;
+ this.isActive = isActive;
+ this.count = count;
+ }
+
+ @PrePersist
+ public void prePersist(){
+ if(this.id == null){
+ this.id = UUID.randomUUID();
+ }
+ }
+
+ public Integer increaseCount(){
+ if (count <10) {
+ count++;
+ }else{
+ count = 0;
+ }
+ return this.shippingOrder = count;
+ }
+
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/trnas/ShippingManagerSelf.java b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/trnas/ShippingManagerSelf.java
new file mode 100644
index 00000000..03a335c2
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/model/trnas/ShippingManagerSelf.java
@@ -0,0 +1,27 @@
+package com.sparta.shippingmanager.domain.model.trnas;
+
+import com.sparta.shippingmanager.domain.model.ManagerType;
+import com.sparta.shippingmanager.domain.model.ShippingManager;
+
+import java.util.UUID;
+
+public record ShippingManagerSelf(
+ UUID shippingManagerId,
+ ManagerType managerType,
+ Boolean isActive,
+ Integer count
+)
+{
+ public ShippingManager toShippingManager(Long userId){
+ ShippingManager manager = new ShippingManager(
+ userId,
+ this.shippingManagerId,
+ this.managerType,
+ this.isActive,
+ this.count
+
+ );
+ // manager.increaseCount();
+ return manager;
+ }
+ }
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/domain/repository/ShippingManagerRepository.java b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/repository/ShippingManagerRepository.java
new file mode 100644
index 00000000..ebbf4bfc
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/domain/repository/ShippingManagerRepository.java
@@ -0,0 +1,20 @@
+package com.sparta.shippingmanager.domain.repository;
+import com.sparta.shippingmanager.domain.model.ShippingManager;
+
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+
+public interface ShippingManagerRepository {
+
+ // 가장 낮은 순서의 활성화된 담당자 찾기
+ void updateAllManagerOrders(int max);
+
+ List findNextManagerWithLock();
+ ShippingManager save(ShippingManager shippingManager);
+
+ Optional findById(UUID id);
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/infrastructure/JpaManagerRepository.java b/shipping-service/src/main/java/com/sparta/shippingmanager/infrastructure/JpaManagerRepository.java
new file mode 100644
index 00000000..2655eb25
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/infrastructure/JpaManagerRepository.java
@@ -0,0 +1,27 @@
+package com.sparta.shippingmanager.infrastructure;
+
+import com.sparta.shippingmanager.domain.model.ShippingManager;
+import com.sparta.shippingmanager.domain.repository.ShippingManagerRepository;
+import jakarta.persistence.LockModeType;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.UUID;
+
+@Repository
+public interface JpaManagerRepository extends JpaRepository, ShippingManagerRepository {
+
+ @Modifying
+ @Query("UPDATE ShippingManager m SET m.shippingOrder = CASE" +
+ " WHEN m.shippingOrder =:max THEN 1 ELSE m.shippingOrder + 1 END")
+ void updateAllManagerOrders(@Param("max") int max);
+
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @Query("SELECT m FROM ShippingManager m WHERE m.isActive = true ORDER BY m.shippingOrder ASC")
+ List findNextManagerWithLock();
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/infrastructure/ShippingManagerSearchRepository.java b/shipping-service/src/main/java/com/sparta/shippingmanager/infrastructure/ShippingManagerSearchRepository.java
new file mode 100644
index 00000000..05713d4e
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/infrastructure/ShippingManagerSearchRepository.java
@@ -0,0 +1,92 @@
+package com.sparta.shippingmanager.infrastructure;
+
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.shippingmanager.application.dto.request.ShippingManagerSearchCondition;
+import com.sparta.shippingmanager.application.dto.response.ShippingManagerResponseDto;
+import com.sparta.shippingmanager.application.dto.response.ShippingManagerSearchResult;
+import com.sparta.shippingmanager.domain.model.ManagerType;
+import com.sparta.shippingmanager.domain.model.QShippingManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@Repository
+public class ShippingManagerSearchRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+ public ShippingManagerSearchResult search(ShippingManagerSearchCondition condition) {
+ QShippingManager sm = QShippingManager.shippingManager;
+ int validPageSize = switch (condition.getPageSize()) {
+ case 10, 30, 50 -> condition.getPageSize();
+ default -> 10;
+ };
+ int page = Math.max(condition.getPage(), 0);
+ // 1. 리스트 조회
+ List content = queryFactory
+ .select(Projections.constructor(ShippingManagerResponseDto.class,
+ sm.managerType,
+ sm.shippingManagerId,
+ sm.shippingOrder
+ ))
+ .from(sm)
+ .where(
+ eqShippingManagerId(condition.getShippingManagerId()),
+ eqManagerType(condition.getManagerType()),
+ eqShippingOrder(condition.getShippingOrder())
+ )
+ .orderBy(resolveSort(condition.getSortBy(), sm))
+ .offset((long) page * validPageSize)
+ .limit(validPageSize)
+ .fetch();
+ // 2. 카운트 조회
+ Long total = queryFactory
+ .select(sm.count())
+ .from(sm)
+ .where(
+ eqShippingManagerId(condition.getShippingManagerId()),
+ eqManagerType(condition.getManagerType()),
+ eqShippingOrder(condition.getShippingOrder())
+ )
+ .fetchOne();
+ return ShippingManagerSearchResult.builder()
+ .content(content)
+ .page(page)
+ .pageSize(validPageSize)
+ .totalCount(Optional.ofNullable(total).orElse(0L))
+ .build();
+ }
+ private BooleanExpression eqShippingManagerId(UUID id) {
+ return id != null ? QShippingManager.shippingManager.shippingManagerId.eq(id) : null;
+ }
+ private BooleanExpression eqManagerType(ManagerType type) {
+ return type != null ? QShippingManager.shippingManager.managerType.eq(type) : null;
+ }
+ private BooleanExpression eqShippingOrder(Integer order) {
+ return order != null ? QShippingManager.shippingManager.shippingOrder.eq(order) : null;
+ }
+ private OrderSpecifier> resolveSort(String sortBy, QShippingManager sm) {
+ if ("modifiedAt".equalsIgnoreCase(sortBy)) {
+ return sm.updatedAt.desc();
+ }
+ return sm.createdAt.desc();
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingmanager/presentation/ShippingManagerController.java b/shipping-service/src/main/java/com/sparta/shippingmanager/presentation/ShippingManagerController.java
new file mode 100644
index 00000000..a7390837
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingmanager/presentation/ShippingManagerController.java
@@ -0,0 +1,62 @@
+package com.sparta.shippingmanager.presentation;
+
+import com.sparta.commonmodule.aop.RoleCheck;
+import com.sparta.shippingmanager.application.dto.request.ShippingManagerCreateRequestDto;
+import com.sparta.shippingmanager.application.dto.request.ShippingManagerSearchCondition;
+import com.sparta.shippingmanager.application.service.ShippingManagerService;
+import com.sparta.shippingmanager.domain.model.ShippingManager;
+import com.sparta.shippingmanager.application.dto.response.ShippingManagerResponseDto;
+import com.sparta.shippingmanager.infrastructure.ShippingManagerSearchRepository;
+import io.swagger.v3.oas.annotations.Operation;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/shipping-managers")
+@RequiredArgsConstructor
+public class ShippingManagerController {
+
+ private final ShippingManagerService shippingManagerService;
+
+ private final ShippingManagerSearchRepository shippingManagerRepository;
+
+
+ @Operation(summary = "업체 배송 담당자 지정",description = "업체 배송 담당자 지정 API 입니다")
+
+ @GetMapping("/assign") // 배송 담당자 할당
+ public ShippingManager assignManager(){
+ return shippingManagerService.assign();
+ }
+
+
+ @Operation(summary = "배송 담당자 등록",description = "배송 담당자 등록 API 입니다")
+ @RoleCheck("ROLE_HUB,ROLE_MASTER")
+ @PostMapping()
+ public ResponseEntity create(@Valid @RequestBody ShippingManagerCreateRequestDto request, @RequestHeader("userId") Long userId){
+ ShippingManagerResponseDto response = shippingManagerService.create(request, userId);
+ return ResponseEntity.ok(response);
+ }
+
+
+ @Operation(summary = "특정 배송 담당자 조회",description = "특정 배송 담당자 조회 API 입니다")
+ @RoleCheck("ROLE_HUB,ROLE_MASTER")
+ @GetMapping("/{id}")
+ public ResponseEntity getById(@PathVariable("id") UUID id){
+ ShippingManagerResponseDto response = shippingManagerService.getById(id);
+ return ResponseEntity.ok(response);
+ }
+ @Operation(summary = "배송 담당자 검색",description = "배송 담당자 검색 API 입니다")
+ @RoleCheck("ROLE_HUB,ROLE_MASTER,ROLE_SHIPPING")
+ @GetMapping("/search")
+ public ResponseEntity> search(@ModelAttribute ShippingManagerSearchCondition condition) {
+ Page result = shippingManagerService.search(condition);
+ return ResponseEntity.ok(result);
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/ShippingServiceApplication.java b/shipping-service/src/main/java/com/sparta/shippingservice/ShippingServiceApplication.java
new file mode 100644
index 00000000..22e1e74f
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/ShippingServiceApplication.java
@@ -0,0 +1,24 @@
+package com.sparta.shippingservice;
+
+import com.sparta.commonmodule.config.JpaAuditingConfig;
+import com.sparta.commonmodule.config.SwaggerConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@SpringBootApplication(scanBasePackages = "com.sparta")
+@EnableFeignClients(basePackages = "com.sparta")
+@EnableJpaRepositories(basePackages = "com.sparta")
+@EntityScan(basePackages = "com.sparta")
+
+@Import({JpaAuditingConfig.class, SwaggerConfig.class})
+public class ShippingServiceApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(ShippingServiceApplication.class, args);
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/client/HubRouteInfoResponse.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/client/HubRouteInfoResponse.java
new file mode 100644
index 00000000..b76b4197
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/client/HubRouteInfoResponse.java
@@ -0,0 +1,11 @@
+package com.sparta.shippingservice.application.dto.client;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+public record HubRouteInfoResponse(
+ UUID startHubId,
+ UUID endHubId,
+ BigDecimal estimatedDistance,
+ Integer estimatedTime
+) {}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/client/ShippingManagerResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/client/ShippingManagerResponseDto.java
new file mode 100644
index 00000000..08237e71
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/client/ShippingManagerResponseDto.java
@@ -0,0 +1,11 @@
+package com.sparta.shippingservice.application.dto.client;
+
+import com.sparta.shippingmanager.domain.model.ManagerType;
+
+import java.util.UUID;
+
+public record ShippingManagerResponseDto(
+ UUID id,
+ String name,
+ ManagerType managerType
+) {}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateRouteLogRequestDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateRouteLogRequestDto.java
new file mode 100644
index 00000000..01035aaf
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateRouteLogRequestDto.java
@@ -0,0 +1,70 @@
+package com.sparta.shippingservice.application.dto.request;
+
+import com.sparta.shippingservice.domain.model.ShippingCheckpoint;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+@Setter
+@Getter
+@Builder
+@AllArgsConstructor
+public class CreateRouteLogRequestDto {
+ private UUID hubRouteId;
+ private UUID fromHubId;
+ private UUID toHubId;
+ private int duration;
+ private BigDecimal distance;
+ private List checkpoints;
+
+ public ShippingRouteLog toEntity() {
+ ShippingRouteLog route = ShippingRouteLog.builder()
+ .hubRouteId(hubRouteId)
+ .fromHubId(fromHubId)
+ .toHubId(toHubId)
+ .duration(duration)
+ .distance(distance)
+ .build();
+ // 체크포인트 추가
+ if (checkpoints != null) {
+ for (CheckpointSaveDto cp : checkpoints) {
+ route.addCheckpoint(cp.toEntity());
+ }
+ }
+ return route;
+ }
+
+ @Getter
+ @Builder
+ @AllArgsConstructor
+ public static class CheckpointSaveDto {
+ private int orderIndex;
+ private UUID hubId;
+ private String hubName;
+ public ShippingCheckpoint toEntity() {
+ return ShippingCheckpoint.builder()
+ .orderIndex(orderIndex)
+ .hubId(hubId)
+ .hubName(hubName)
+ .build();
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateShippingRequestDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateShippingRequestDto.java
new file mode 100644
index 00000000..613ede20
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateShippingRequestDto.java
@@ -0,0 +1,40 @@
+package com.sparta.shippingservice.application.dto.request;
+
+import com.sparta.shippingservice.domain.model.trans.ShippingSelf;
+import com.sparta.shippingservice.domain.model.ShippingStatus;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+
+import java.util.UUID;
+
+public record CreateShippingRequestDto(
+ // TODO CREATE 요청은 언제 발생 ? 주문이 등록될떄 ? ORDER - SERVICE 에게 해당 정보 요청
+ @NotNull(message = "주문 ID는 필수입니다.")
+ UUID orderId,
+
+ @NotNull(message = "배송 주소는 필수입니다.")
+ @Size(min = 5, max = 255, message = "배송 주소는 5~255자 사이여야 합니다.")
+ String shippingAddress,
+
+ @NotNull(message = "수령인 이름은 필수입니다.")
+ @Size(min = 2, max = 100, message = "수령인 이름은 2~100자 사이여야 합니다.")
+ String receiverName,
+
+
+ @NotNull(message = "배송 상태는 필수입니다.")
+ ShippingStatus status
+
+) {
+ public ShippingSelf of( UUID managerId){
+ return new ShippingSelf(
+ this.orderId(),
+ this.shippingAddress(),
+ this.receiverName(),
+ managerId, //외부에서 받아온 값 주입
+ this.status()
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateShippingWithRouteRequestDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateShippingWithRouteRequestDto.java
new file mode 100644
index 00000000..dbe54c58
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/CreateShippingWithRouteRequestDto.java
@@ -0,0 +1,7 @@
+package com.sparta.shippingservice.application.dto.request;
+
+import jakarta.validation.Valid;
+
+public record CreateShippingWithRouteRequestDto(
+ @Valid CreateShippingRequestDto shipping,
+ @Valid CreateRouteLogRequestDto routeLog){}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/ShippingRouteSearchCondition.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/ShippingRouteSearchCondition.java
new file mode 100644
index 00000000..33bf06b1
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/ShippingRouteSearchCondition.java
@@ -0,0 +1,20 @@
+package com.sparta.shippingservice.application.dto.request;
+
+import lombok.*;
+
+import java.util.UUID;
+
+@Setter
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ShippingRouteSearchCondition {
+ private UUID shippingId;
+ private UUID fromHubId;
+ private UUID toHubId;
+ private UUID hubRouteId;
+ private String sortBy; // "createdAt", "modifiedAt"
+ private int pageSize; // 10, 30, 50만 허용
+ private int page; // 0부터 시작
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/ShippingSearchCondition.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/ShippingSearchCondition.java
new file mode 100644
index 00000000..891a13cd
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/ShippingSearchCondition.java
@@ -0,0 +1,31 @@
+package com.sparta.shippingservice.application.dto.request;
+
+import com.sparta.shippingservice.domain.model.ShippingStatus;
+import lombok.*;
+
+import java.util.UUID;
+
+@Setter
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ShippingSearchCondition {
+ private String shippingAddress;
+ private String receiverName;
+ private ShippingStatus status;
+ private String sortBy; // "createdAt", "modifiedAt"
+ private int pageSize; // 10, 30, 50만 허용
+ private int page; // 0부터 시작
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/UpdateShippingRequestDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/UpdateShippingRequestDto.java
new file mode 100644
index 00000000..90724497
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/request/UpdateShippingRequestDto.java
@@ -0,0 +1,27 @@
+package com.sparta.shippingservice.application.dto.request;
+
+import com.sparta.shippingservice.domain.model.trans.ShippingSelf;
+import com.sparta.shippingservice.domain.model.ShippingStatus;
+
+import java.util.UUID;
+
+
+public record UpdateShippingRequestDto(
+ UUID orderId,
+ String shippingAddress,
+ String receiverName,
+ UUID shippingManagerId,
+ ShippingStatus status
+) {
+ public ShippingSelf of(){
+ return new ShippingSelf(
+ this.orderId(),
+ this.shippingAddress(),
+ this.receiverName(),
+ this.shippingManagerId(),
+ this.status()
+
+ );
+ }
+}
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingLogSearchResult.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingLogSearchResult.java
new file mode 100644
index 00000000..3c8b9eb0
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingLogSearchResult.java
@@ -0,0 +1,22 @@
+package com.sparta.shippingservice.application.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ShippingLogSearchResult {
+
+ private List content;
+ private int page; // 현재 페이지 번호
+ private int pageSize; // 한 페이지에 몇 건
+ private long totalCount; // 전체 개수
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingResponseDto.java
new file mode 100644
index 00000000..89f396dd
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingResponseDto.java
@@ -0,0 +1,28 @@
+package com.sparta.shippingservice.application.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.querydsl.core.annotations.QueryProjection;
+import com.sparta.shippingservice.domain.model.Shipping;
+import com.sparta.shippingservice.domain.model.ShippingStatus;
+
+import java.util.UUID;
+
+
+
+
+public record ShippingResponseDto(
+ UUID shippingId,
+ String receiverName,
+ String shippingAddress,
+ ShippingStatus status
+){
+ public static ShippingResponseDto from (Shipping shipping) {
+ return new ShippingResponseDto(
+ shipping.getId(),
+ shipping.getReceiverName(),
+ shipping.getShippingAddress(),
+ shipping.getStatus());
+ }
+
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingRouteResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingRouteResponseDto.java
new file mode 100644
index 00000000..b294a385
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingRouteResponseDto.java
@@ -0,0 +1,61 @@
+package com.sparta.shippingservice.application.dto.response;
+import com.sparta.shippingservice.domain.model.ShippingCheckpoint;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+import lombok.Builder;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+@Builder
+public record ShippingRouteResponseDto(
+ UUID id,
+ UUID hubRouteId,
+ UUID fromHubId,
+ UUID toHubId,
+ int duration,
+ BigDecimal distance,
+ List checkpoints
+) {
+ public static ShippingRouteResponseDto from(ShippingRouteLog routeLog) {
+ return ShippingRouteResponseDto.builder()
+ .id(routeLog.getId())
+ .hubRouteId(routeLog.getHubRouteId())
+ .fromHubId(routeLog.getFromHubId())
+ .toHubId(routeLog.getToHubId())
+ .duration(routeLog.getDuration())
+ .distance(routeLog.getDistance())
+ .checkpoints(
+ routeLog.getCheckpoints().stream()
+ .map(CheckpointResponse::from)
+ .toList()
+ )
+ .build();
+ }
+ @Builder
+ public record CheckpointResponse(
+ int orderIndex,
+ UUID hubId,
+ String hubName
+ ) {
+ public static CheckpointResponse from(ShippingCheckpoint cp) {
+ return CheckpointResponse.builder()
+ .orderIndex(cp.getOrderIndex())
+ .hubId(cp.getHubId())
+ .hubName(cp.getHubName())
+ .build();
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingSearchResult.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingSearchResult.java
new file mode 100644
index 00000000..652ca876
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingSearchResult.java
@@ -0,0 +1,17 @@
+package com.sparta.shippingservice.application.dto.response;
+
+import com.sparta.shippingservice.domain.model.Shipping;
+import lombok.*;
+
+import java.util.List;
+
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ShippingSearchResult {
+ private List content;
+ private int page; // 현재 페이지 번호
+ private int pageSize; // 한 페이지에 몇 건
+ private long totalCount; // 전체 개수
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingWithRouteResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingWithRouteResponseDto.java
new file mode 100644
index 00000000..d44423da
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/dto/response/ShippingWithRouteResponseDto.java
@@ -0,0 +1,24 @@
+package com.sparta.shippingservice.application.dto.response;
+import com.sparta.shippingservice.domain.model.Shipping;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+import java.util.UUID;
+public record ShippingWithRouteResponseDto(
+ UUID shippingId,
+ String shippingAddress,
+ String receiverName,
+ UUID shippingManagerId,
+ String status,
+ ShippingRouteResponseDto route
+) {
+ public static ShippingWithRouteResponseDto from(Shipping shipping, ShippingRouteLog routeLog) {
+ ShippingRouteResponseDto routeDto = ShippingRouteResponseDto.from(routeLog);
+ return new ShippingWithRouteResponseDto(
+ shipping.getId(),
+ shipping.getShippingAddress(),
+ shipping.getReceiverName(),
+ shipping.getShippingManagerId(),
+ shipping.getStatus().name(),
+ routeDto
+ );
+ }
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/service/ShippingHubScanService.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/service/ShippingHubScanService.java
new file mode 100644
index 00000000..d363ef4a
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/service/ShippingHubScanService.java
@@ -0,0 +1,52 @@
+package com.sparta.shippingservice.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.shippingservice.domain.model.Shipping;
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog;
+import com.sparta.shippingservice.domain.repository.ShippingHubScanLogRepository;
+import com.sparta.shippingservice.domain.repository.ShippingRepository;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.InboundStatusRequestDto;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.InboundStatusResponseDto;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.OutboundStatusRequestDto;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.OutboundStatusResponseDto;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class ShippingHubScanService {
+
+ private final ShippingRepository shippingRepository;
+ private final ShippingHubScanLogRepository shippingHubScanLogRepository;
+
+ // 입고 처리
+ @Transactional
+ public InboundStatusResponseDto createInboundLog(UUID shippingId, Long userId, InboundStatusRequestDto requestDto) {
+
+ Shipping shipping = shippingRepository.findById(shippingId)
+ .orElseThrow(ResourceNotFoundException::new);
+
+ ShippingHubScanLog log =
+ ShippingHubScanLog.createInboundLog(requestDto.getHubId(), shipping, userId);
+
+ shippingHubScanLogRepository.save(log);
+
+ return new InboundStatusResponseDto(log, "Success Save Inbound Log");
+ }
+
+ @Transactional
+ public OutboundStatusResponseDto createOutboundLog(UUID shippingId, Long userId, OutboundStatusRequestDto outboundStatusRequestDto) {
+
+ Shipping shipping = shippingRepository.findById(shippingId)
+ .orElseThrow(ResourceNotFoundException::new);
+
+ ShippingHubScanLog log =
+ ShippingHubScanLog.createOutboundLog(outboundStatusRequestDto.getHubId(), shipping, userId);
+
+ shippingHubScanLogRepository.save(log);
+
+ return new OutboundStatusResponseDto(log, "Success Save Outbound Log");
+ }
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/application/service/ShippingService.java b/shipping-service/src/main/java/com/sparta/shippingservice/application/service/ShippingService.java
new file mode 100644
index 00000000..baf76b50
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/application/service/ShippingService.java
@@ -0,0 +1,159 @@
+package com.sparta.shippingservice.application.service;
+
+import com.sparta.shippingmanager.domain.model.ManagerType;
+import com.sparta.shippingservice.application.dto.client.ShippingManagerResponseDto;
+import com.sparta.shippingservice.application.dto.request.*;
+import com.sparta.shippingservice.application.dto.response.*;
+import com.sparta.shippingservice.domain.model.*;
+import com.sparta.shippingservice.domain.repository.ShippingRepository;
+import com.sparta.commonmodule.exception.*;
+
+import com.sparta.shippingservice.domain.repository.ShippingRouteRepository;
+import com.sparta.shippingservice.infrastructure.client.HubClient;
+import com.sparta.shippingservice.infrastructure.client.HubRouteDetailsResponseDto;
+import com.sparta.shippingservice.infrastructure.client.ShippingManagerClient;
+import com.sparta.shippingservice.infrastructure.repository.ShippingLogSearchRepository;
+import com.sparta.shippingservice.infrastructure.repository.ShippingSearchRepository;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+
+@Service
+@RequiredArgsConstructor
+public class ShippingService {
+ private final ShippingRepository shippingRepository;
+ private final ShippingRouteRepository shippingRouteRepository;
+ private final ShippingManagerClient shippingManagerClient;
+ private final ShippingSearchRepository searchRepository;
+ private final HubClient hubClient;
+ private final ShippingLogSearchRepository shippingLogSearchRepository;
+
+//각 허브에 10명 / 업체에 10명
+
+ @Transactional
+ public ShippingWithRouteResponseDto create(@Valid CreateShippingRequestDto request ,Long userId) {
+
+ ShippingManagerResponseDto manager = shippingManagerClient.assignManager();
+ if(manager.managerType() != ManagerType.CARRIER){
+ throw new InvalidParameterException("배송 담당자는 업체 소속이어야 합니다.");
+ }
+
+ Shipping shipping = request.of(manager.id()).toShipping(userId);
+
+ HubRouteDetailsResponseDto pathHubRoute = hubClient.createPathHubRoute(shipping.getRouteLog().getFromHubId(),shipping.getRouteLog().getToHubId());
+ ShippingRouteLog routeLog = pathHubRoute.toShippingRouteLog();
+
+ // 양방향 연관관계 설정
+ routeLog.setShipping(shipping);
+ shipping.add(routeLog);
+
+ shippingRepository.save(shipping);
+ return ShippingWithRouteResponseDto.from(shipping,routeLog);
+
+ }
+
+ @Transactional(readOnly = true)
+ public ShippingResponseDto getShippingById(UUID shippingId) {
+ Shipping shipping = findShipping(shippingId);
+ return ShippingResponseDto.from(shipping);
+
+ }
+
+ @Transactional(readOnly = true)
+ public List getAllShipping() {
+ List result = shippingRepository.findAll();
+ return result.stream()
+ .map(shipping -> new ShippingResponseDto(
+ shipping.getId(),
+ shipping.getShippingAddress(),
+ shipping.getReceiverName(),
+ shipping.getStatus()
+ ))
+ .collect(Collectors.toList());
+ }
+
+ @Transactional
+ public ShippingResponseDto updateShipping(UUID shippingId, @Valid UpdateShippingRequestDto request ,Long userId) {
+ Shipping shipping = findShipping(shippingId);
+ shipping.updateShipping(request.of().toShipping(userId),userId);
+ return ShippingResponseDto.from(shipping);
+
+ }
+
+ @Transactional
+ public ShippingResponseDto deleteShipping(UUID shippingId, long userId) {
+ Shipping shipping = findShipping(shippingId);
+ shipping.delete(userId);
+ shipping.setStatus(ShippingStatus.CANCELED);
+ shippingRepository.save(shipping);
+ return ShippingResponseDto.from(shipping);
+ }
+
+ public Page searchShipping(ShippingSearchCondition condition) {
+ ShippingSearchResult result = searchRepository.search(condition);
+ return new PageImpl<>(
+ result.getContent(),
+ PageRequest.of(result.getPage(), result.getPageSize()),
+ result.getTotalCount()
+ );
+ }
+
+ @Transactional(readOnly = true)
+ public ShippingRouteResponseDto getLogById(UUID shippingId, UUID shippingLogId){
+ ShippingRouteLog routeLog = shippingRouteRepository.findByIdAndShippingId(shippingLogId, shippingId)
+ .orElseThrow(() -> new ResourceNotFoundException("해당 배송에 속하지 않는 배송 경로 로그입니다."));
+ return ShippingRouteResponseDto.from(routeLog); // 예외 컨트롤러 단에서 잡기
+
+ }
+
+ @Transactional(readOnly = true)
+ public List getLogAll() {
+ List result = shippingRouteRepository.findAll();
+ return result.stream()
+ .map(ShippingRouteResponseDto::from)
+ .collect(Collectors.toList());
+ }
+
+
+ @Transactional
+ public ShippingRouteResponseDto deleteShippingLog(UUID shippingId,UUID shippingLogId, long userId) {
+ ShippingRouteLog routeLog = shippingRouteRepository.findByIdAndShippingId(shippingLogId, shippingId)
+ .orElseThrow(() -> new ResourceNotFoundException("해당 배송에 속하지 않는 배송 경로 로그입니다."));
+
+ routeLog.delete(userId);
+ shippingRouteRepository.save(routeLog);
+ return ShippingRouteResponseDto.from(routeLog);
+ }
+
+ @Transactional(readOnly = true)
+ public Page searchRoutes(ShippingRouteSearchCondition condition) {
+ ShippingLogSearchResult result = shippingLogSearchRepository.search(condition);
+ return new PageImpl<>(
+ result.getContent(),
+ PageRequest.of(result.getPage(), result.getPageSize()),
+ result.getTotalCount()
+ );
+ }
+
+
+
+
+ private Shipping findShipping(UUID shippingId) {
+ Shipping shipping = shippingRepository.findById(shippingId).orElseThrow(
+ () -> new ResourceNotFoundException("찾을 수 없는 배송 정보 입니다."));
+ return shipping;
+ }
+
+
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/Shipping.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/Shipping.java
new file mode 100644
index 00000000..2949d136
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/Shipping.java
@@ -0,0 +1,78 @@
+package com.sparta.shippingservice.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+
+
+import java.util.UUID;
+
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(name = "p_shipping") // 테이블 명 지정
+public class Shipping extends BaseEntity {
+
+ @Id
+ @Column(name = "shipping_id", updatable = false, nullable = false)
+ private UUID id;
+
+ @Column(name = "order_id", nullable = false)
+ private UUID orderId;
+
+ @Column(name = "shipping_address", nullable = false, length = 255)
+ private String shippingAddress;
+
+ @Column(name = "receiver_name", nullable = false, length = 100)
+ private String receiverName;
+
+ @Column(name = "shipping_manager_id", nullable = false)
+ private UUID shippingManagerId;
+
+ @OneToOne(mappedBy = "shipping",cascade = CascadeType.PERSIST)
+ private ShippingRouteLog routeLog;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "status", nullable = false, length = 50)
+ private ShippingStatus status = ShippingStatus.PENDING; // 기본값 설정
+
+ public Shipping(Long userId, UUID id, UUID orderId, String shippingAddress, String receiverName, UUID shippingManagerId, ShippingRouteLog routeLog, ShippingStatus status) {
+ super(userId);
+ this.id = id;
+ this.orderId = orderId;
+ this.shippingAddress = shippingAddress;
+ this.receiverName = receiverName;
+ this.shippingManagerId = shippingManagerId;
+ this.routeLog = routeLog;
+ this.status = status;
+ }
+
+ public Shipping updateShipping(Shipping shipping, Long userId) {
+ super.update(userId);
+ if (this.status == ShippingStatus.DELIVERED) {
+ throw new IllegalStateException("배송이 완료된 후에는 정보를 변경할 수 없습니다.");
+ }
+ if(shipping.getStatus() !=null) this.status=shipping.getStatus();
+ if(shipping.getOrderId()!=null) this.orderId = shipping.getOrderId();
+ if(shipping.getShippingAddress()!=null) this.shippingAddress=shipping.getShippingAddress();
+ if(shipping.getReceiverName()!=null) this.receiverName = shipping.getReceiverName();
+ return this;
+ }
+
+ public void add(ShippingRouteLog routeLog) {
+ this.routeLog = routeLog;
+ }
+
+
+
+ @PrePersist
+ public void prePersist(){
+ if(this.id == null){
+ this.id = UUID.randomUUID();
+ }
+ // this.getCreatedAt() =LocalDateTime.now();
+ }
+
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingCheckpoint.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingCheckpoint.java
new file mode 100644
index 00000000..a4ee9671
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingCheckpoint.java
@@ -0,0 +1,36 @@
+package com.sparta.shippingservice.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.util.UUID;
+
+@Setter
+@Entity
+@Table(name = "p_shipping_checkpoint")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Builder
+public class ShippingCheckpoint extends BaseEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private int orderIndex;
+
+ @Column(nullable = false)
+ private UUID hubId;
+
+ @Column(nullable = false)
+ private String hubName;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "shipping_route_id")
+ private ShippingRouteLog shippingRouteLog;
+
+ public void setShippingRoute(ShippingRouteLog route) {
+ this.shippingRouteLog = route;
+ }
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingHubScanLog.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingHubScanLog.java
new file mode 100644
index 00000000..ae634966
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingHubScanLog.java
@@ -0,0 +1,74 @@
+package com.sparta.shippingservice.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+
+@Entity
+@Getter
+@NoArgsConstructor
+@Table(name = "p_shipping_hub_scan_log")
+public class ShippingHubScanLog extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "shipping_hub_scan_log_id")
+ private UUID hubShippingScanLogId;
+
+ @Column(name = "hub_id", nullable = false)
+ private UUID hubId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name ="shipping_id", nullable = false)
+ private Shipping shipping;
+
+ @CreatedDate
+ @Column(nullable = false, updatable = false)
+ private LocalDateTime timestamp;
+
+ @Column(nullable = false)
+ private ShippingStatus status;
+
+ @Column(name = "next_hub_id", nullable = true)
+ private UUID nextHubId;
+
+ public ShippingHubScanLog(UUID hubId,Shipping shipping, ShippingStatus status, Long userId) {
+ super(userId);
+ this.hubId = hubId;
+ this.shipping = shipping;
+ this.status = status;
+ }
+
+ public ShippingHubScanLog(UUID hubId,Shipping shipping, ShippingStatus status, UUID nextHubId, Long userId) {
+ super(userId);
+ this.hubId = hubId;
+ this.shipping = shipping;
+ this.status = status;
+ this.nextHubId = nextHubId;
+ }
+
+ public static ShippingHubScanLog createInboundLog(UUID hubId, Shipping shipping, Long userId) {
+ return new ShippingHubScanLog(hubId,shipping, ShippingStatus.INBOUND, userId);
+ }
+
+ public static ShippingHubScanLog createOutboundLog(UUID hubId, Shipping shipping, Long userId) {
+ return new ShippingHubScanLog(hubId, shipping, ShippingStatus.OUTBOUND, userId);
+ }
+
+ public enum ShippingStatus {
+ INBOUND, OUTBOUND
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingRouteLog.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingRouteLog.java
new file mode 100644
index 00000000..0c536f6a
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingRouteLog.java
@@ -0,0 +1,61 @@
+package com.sparta.shippingservice.domain.model;
+
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.*;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@Table(name = "p_shipping_route")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Builder
+public class ShippingRouteLog extends BaseEntity {
+ @Id
+ private UUID id;
+
+ @Column(nullable = false)
+ private UUID hubRouteId;
+
+ @Column(nullable = false)
+ private UUID fromHubId;
+
+ @Column(nullable = false)
+ private UUID toHubId;
+
+ @Column(nullable = false)
+ private int duration; // 총 소요 시간 (분)
+
+ @Column(nullable = false, precision = 10, scale = 2)
+ private BigDecimal distance; // 총 거리 (km 단위)
+ // 체크포인트들 - OneToMany
+
+ @OneToMany(mappedBy = "shippingRouteLog", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
+ private List checkpoints = new ArrayList<>();
+
+ @OneToOne
+ @JoinColumn(name = "shipping_shipping_id")
+ private Shipping shipping;
+
+ // 연관관계 편의 메서드
+ public void addCheckpoint(ShippingCheckpoint checkpoint) {
+ checkpoints.add(checkpoint);
+ checkpoint.setShippingRoute(this);
+
+ }
+
+ public void setShipping(Shipping shipping) {
+ this.shipping = shipping;
+ }
+
+
+ @PrePersist
+ public void prePersist() {
+ if (this.id == null) this.id = UUID.randomUUID();
+ }
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingStatus.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingStatus.java
new file mode 100644
index 00000000..31d9033b
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/ShippingStatus.java
@@ -0,0 +1,7 @@
+package com.sparta.shippingservice.domain.model;
+
+public enum ShippingStatus {
+ PENDING, // 배송 대기중
+ DELIVERED, // 배송 완료
+ CANCELED // 배송 취소
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/trans/RouteLogSelf.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/trans/RouteLogSelf.java
new file mode 100644
index 00000000..ff3c06bb
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/trans/RouteLogSelf.java
@@ -0,0 +1,46 @@
+//package com.sparta.shippingservice.domain.model.trans;
+//
+//import com.sparta.shippingservice.domain.model.Shipping;
+//import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+//
+//import java.math.BigDecimal;
+//import java.util.UUID;
+//
+//public record RouteLogSelf(
+//
+// UUID startHubId,
+// UUID endHubId,
+// Integer sequence,
+// BigDecimal estimatedDistance,
+// Integer estimatedTime,
+// BigDecimal actualDistance,
+// Integer actualTime,
+// UUID shippingManagerId
+//) {
+// public ShippingRouteLog toShippingRouteLog() {
+// return new ShippingRouteLog(
+// this.startHubId,
+// this.endHubId,
+// this.sequence,
+// this.estimatedDistance,
+// this.estimatedTime,
+// this.actualDistance,
+// this.actualTime,
+// this.shippingManagerId
+// );
+// }
+//}
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/trans/ShippingSelf.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/trans/ShippingSelf.java
new file mode 100644
index 00000000..a115852a
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/model/trans/ShippingSelf.java
@@ -0,0 +1,26 @@
+package com.sparta.shippingservice.domain.model.trans;
+import com.sparta.shippingservice.domain.model.Shipping;
+import com.sparta.shippingservice.domain.model.ShippingStatus;
+import lombok.Builder;
+import java.util.UUID;
+@Builder
+public record ShippingSelf(
+ UUID orderId,
+ String shippingAddress,
+ String receiverName,
+ UUID shippingManagerId,
+ ShippingStatus status
+) {
+ public Shipping toShipping(Long userId) {
+ return new Shipping(
+ userId,
+ null, // 배송 ID는 생성 시 자동 UUID 설정
+ this.orderId,
+ this.shippingAddress,
+ this.receiverName,
+ this.shippingManagerId,
+ null, // routeLog는 추후 연결
+ this.status != null ? this.status : ShippingStatus.PENDING // 기본값 설정
+ );
+ }
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingHubScanLogRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingHubScanLogRepository.java
new file mode 100644
index 00000000..d4cf34ed
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingHubScanLogRepository.java
@@ -0,0 +1,8 @@
+package com.sparta.shippingservice.domain.repository;
+
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog;
+
+public interface ShippingHubScanLogRepository {
+
+ void save(ShippingHubScanLog log);
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRepository.java
new file mode 100644
index 00000000..4aa5d35f
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRepository.java
@@ -0,0 +1,17 @@
+package com.sparta.shippingservice.domain.repository;
+
+
+import com.sparta.shippingservice.application.dto.response.ShippingResponseDto;
+import com.sparta.shippingservice.domain.model.Shipping;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+
+public interface ShippingRepository {
+ Shipping save(Shipping shipping);
+ Optional findById(UUID id);
+ List findAll();
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRouteQueryRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRouteQueryRepository.java
new file mode 100644
index 00000000..e640ed5a
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRouteQueryRepository.java
@@ -0,0 +1,10 @@
+package com.sparta.shippingservice.domain.repository;
+
+import com.sparta.shippingservice.application.dto.request.ShippingRouteSearchCondition;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface ShippingRouteQueryRepository {
+ Page search(ShippingRouteSearchCondition condition, Pageable pageable);
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRouteRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRouteRepository.java
new file mode 100644
index 00000000..bc8d89f5
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/domain/repository/ShippingRouteRepository.java
@@ -0,0 +1,14 @@
+package com.sparta.shippingservice.domain.repository;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface ShippingRouteRepository {
+ ShippingRouteLog save(ShippingRouteLog shippingRouteLog);
+ Optional findById(UUID id);
+ List findAll();
+ Optional findByIdAndShippingId(UUID id, UUID shippingId);
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/HubClient.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/HubClient.java
new file mode 100644
index 00000000..b91213b8
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/HubClient.java
@@ -0,0 +1,19 @@
+package com.sparta.shippingservice.infrastructure.client;
+
+
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+
+import java.util.UUID;
+
+@FeignClient(name = "hub-service")
+public interface HubClient {
+ @GetMapping("/api/v1/hub-route/feign/{from_id}/{to_id}/path")
+ HubRouteDetailsResponseDto createPathHubRoute(
+ @PathVariable("from_id") UUID fromHubId,
+ @PathVariable("to_id") UUID toHubId
+ );
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/HubRouteDetailsResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/HubRouteDetailsResponseDto.java
new file mode 100644
index 00000000..83b31055
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/HubRouteDetailsResponseDto.java
@@ -0,0 +1,94 @@
+package com.sparta.shippingservice.infrastructure.client;
+
+
+import com.sparta.shippingservice.domain.model.ShippingCheckpoint;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+@Setter
+@Getter
+@Builder
+@AllArgsConstructor
+@RequiredArgsConstructor
+public class HubRouteDetailsResponseDto {
+ private UUID hubRouteId;
+ private UUID fromHubId;
+ private UUID toHubId;
+ private int duration;
+ private BigDecimal distance;
+ private List checkpoints;
+ public HubRouteDetailsResponseDto(HubRoute hubRoute, List checkpoints) {
+ this.hubRouteId = hubRoute.getHubRouteId();
+ this.fromHubId = hubRoute.getFromHub().getHubId();
+ this.toHubId = hubRoute.getToHub().getHubId();
+ this.duration = hubRoute.getDuration();
+ this.distance = hubRoute.getDistance();
+ this.checkpoints = checkpoints;
+ }
+
+ public ShippingRouteLog toShippingRouteLog() {
+ ShippingRouteLog routeLog = ShippingRouteLog.builder()
+ .hubRouteId(hubRouteId)
+ .fromHubId(fromHubId)
+ .toHubId(toHubId)
+ .duration(duration)
+ .distance(distance)
+ .build();
+ if (checkpoints != null) {
+ for (CheckpointResponseDto cp : checkpoints) {
+ ShippingCheckpoint checkpoint = ShippingCheckpoint.builder()
+ .orderIndex(cp.getOrder())
+ .hubId(cp.getHubId())
+ .hubName(cp.getHubName())
+ .build();
+ routeLog.addCheckpoint(checkpoint);
+ }
+ }
+ return routeLog;
+ }
+
+ @Setter
+ @Getter
+ @Builder
+ @AllArgsConstructor
+ public static class CheckpointResponseDto {
+ private final int order;
+ private final UUID hubId;
+ private final String hubName;
+ }
+ @Setter
+ @Getter
+ @Builder
+ @AllArgsConstructor
+ public static class HubRoute {
+ private final UUID hubRouteId;
+ private final Hub fromHub;
+ private final Hub toHub;
+ private final int duration;
+ private final BigDecimal distance;
+ }
+ @Setter
+ @Getter
+ @Builder
+ @AllArgsConstructor
+ public static class Hub {
+ private final UUID hubId;
+ private final String name;
+ private final String address;
+ private final BigDecimal latitude;
+ private final BigDecimal longitude;
+ }
+}
+
+
+
+
+
+
+
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/ShippingManagerClient.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/ShippingManagerClient.java
new file mode 100644
index 00000000..d5111447
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/client/ShippingManagerClient.java
@@ -0,0 +1,17 @@
+package com.sparta.shippingservice.infrastructure.client;
+
+import com.sparta.shippingservice.application.dto.client.ShippingManagerResponseDto;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.UUID;
+
+@FeignClient(name="shipping-manager-service",url ="${feign.client.config.shipping-manager-service.url}")
+public interface ShippingManagerClient {
+
+ @GetMapping("/api/shipping-managers/assign")
+ ShippingManagerResponseDto assignManager();
+
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/config/QueryDslConfig.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/config/QueryDslConfig.java
new file mode 100644
index 00000000..e2842748
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/config/QueryDslConfig.java
@@ -0,0 +1,23 @@
+package com.sparta.shippingservice.infrastructure.config;
+
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@RequiredArgsConstructor
+public class QueryDslConfig {
+
+ // @PersistenceContext
+ private final EntityManager em;
+
+
+ @Bean
+ public JPAQueryFactory jpaQueryFactory() {
+ return new JPAQueryFactory(em);
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/InboundStatusRequestDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/InboundStatusRequestDto.java
new file mode 100644
index 00000000..983d5db9
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/InboundStatusRequestDto.java
@@ -0,0 +1,22 @@
+package com.sparta.shippingservice.infrastructure.hub_feign.dto;
+
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog.ShippingStatus;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class InboundStatusRequestDto {
+
+ private UUID hubId;
+ private ShippingStatus status;
+ private LocalDateTime timestamp;
+
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/InboundStatusResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/InboundStatusResponseDto.java
new file mode 100644
index 00000000..13522fbc
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/InboundStatusResponseDto.java
@@ -0,0 +1,32 @@
+package com.sparta.shippingservice.infrastructure.hub_feign.dto;
+
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog;
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog.ShippingStatus;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class InboundStatusResponseDto {
+
+ private String message;
+ private UUID hubId;
+ private UUID shippingId;
+ private ShippingStatus shippingStatus;
+ private LocalDateTime timestamp;
+
+ public InboundStatusResponseDto(ShippingHubScanLog log, String message) {
+ this.shippingStatus = ShippingStatus.INBOUND;
+ this.hubId = log.getHubId();
+ this.shippingId = log.getShipping().getId();
+ this.message = message;
+ }
+
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/OutboundStatusRequestDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/OutboundStatusRequestDto.java
new file mode 100644
index 00000000..062cbd3b
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/OutboundStatusRequestDto.java
@@ -0,0 +1,24 @@
+package com.sparta.shippingservice.infrastructure.hub_feign.dto;
+
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OutboundStatusRequestDto {
+
+ private UUID hubId;
+ private ShippingHubScanLog.ShippingStatus status;
+ private LocalDateTime timestamp;
+ private UUID nextHubId;
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/OutboundStatusResponseDto.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/OutboundStatusResponseDto.java
new file mode 100644
index 00000000..67686188
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/hub_feign/dto/OutboundStatusResponseDto.java
@@ -0,0 +1,33 @@
+package com.sparta.shippingservice.infrastructure.hub_feign.dto;
+
+
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OutboundStatusResponseDto {
+
+ private String message;
+ private UUID hubId;
+ private UUID shippingId;
+ private ShippingHubScanLog.ShippingStatus status;
+ private LocalDateTime timestamp;
+ private UUID nextHubId;
+
+ public OutboundStatusResponseDto(ShippingHubScanLog log, String message) {
+ this.message = message;
+ this.hubId = log.getHubId();
+ this.shippingId = log.getShipping().getId();
+ this.status = ShippingHubScanLog.ShippingStatus.OUTBOUND;
+ this.nextHubId = log.getNextHubId();
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingHubScanLogRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingHubScanLogRepository.java
new file mode 100644
index 00000000..7737d1df
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingHubScanLogRepository.java
@@ -0,0 +1,11 @@
+package com.sparta.shippingservice.infrastructure.repository;
+
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface JpaShippingHubScanLogRepository extends JpaRepository {
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingRepository.java
new file mode 100644
index 00000000..dc4f88d2
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingRepository.java
@@ -0,0 +1,12 @@
+package com.sparta.shippingservice.infrastructure.repository;
+
+import com.sparta.shippingservice.domain.model.Shipping;
+import com.sparta.shippingservice.domain.repository.ShippingRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+@Repository
+public interface JpaShippingRepository extends JpaRepository, ShippingRepository {
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingRouteRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingRouteRepository.java
new file mode 100644
index 00000000..c2a73bf6
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/JpaShippingRouteRepository.java
@@ -0,0 +1,12 @@
+package com.sparta.shippingservice.infrastructure.repository;
+
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+import com.sparta.shippingservice.domain.repository.ShippingRouteRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+@Repository
+public interface JpaShippingRouteRepository extends JpaRepository, ShippingRouteRepository {
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingHubScanLogRepositoryImpl.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingHubScanLogRepositoryImpl.java
new file mode 100644
index 00000000..b623bb74
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingHubScanLogRepositoryImpl.java
@@ -0,0 +1,18 @@
+package com.sparta.shippingservice.infrastructure.repository;
+
+import com.sparta.shippingservice.domain.model.ShippingHubScanLog;
+import com.sparta.shippingservice.domain.repository.ShippingHubScanLogRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class ShippingHubScanLogRepositoryImpl implements ShippingHubScanLogRepository {
+
+ private final JpaShippingHubScanLogRepository jpaRepository;
+
+ @Override
+ public void save(ShippingHubScanLog log) {
+ jpaRepository.save(log);
+ }
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingLogSearchRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingLogSearchRepository.java
new file mode 100644
index 00000000..b8a11262
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingLogSearchRepository.java
@@ -0,0 +1,83 @@
+package com.sparta.shippingservice.infrastructure.repository;
+
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.shippingservice.application.dto.request.ShippingRouteSearchCondition;
+import com.sparta.shippingservice.application.dto.response.ShippingLogSearchResult;
+import com.sparta.shippingservice.application.dto.response.ShippingRouteResponseDto;
+import com.sparta.shippingservice.domain.model.QShippingRouteLog;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@Repository
+@RequiredArgsConstructor
+public class ShippingLogSearchRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+
+ public ShippingLogSearchResult search(ShippingRouteSearchCondition condition) {
+ QShippingRouteLog log = QShippingRouteLog.shippingRouteLog;
+ int validPageSize = switch (condition.getPageSize()) {
+ case 10, 30, 50 -> condition.getPageSize();
+ default -> 10;
+ };
+ int page = Math.max(condition.getPage(), 0);
+ List logs = queryFactory
+ .selectFrom(log)
+ .where(
+ eqShippingId(condition.getShippingId()),
+ eqStartHubId(condition.getFromHubId()),
+ eqEndHubId(condition.getToHubId()),
+ eqHubRouteId(condition.getHubRouteId())
+ )
+ .orderBy(resolveSort(condition.getSortBy(), log))
+ .offset((long) page * validPageSize)
+ .limit(validPageSize)
+ .fetch();
+ Long total = queryFactory
+ .select(log.count())
+ .from(log)
+ .where(
+ eqShippingId(condition.getShippingId()),
+ eqStartHubId(condition.getFromHubId()),
+ eqEndHubId(condition.getToHubId()),
+ eqHubRouteId(condition.getHubRouteId())
+ )
+ .fetchOne();
+ // 3. DTO 매핑
+ List content = logs.stream()
+ .map(ShippingRouteResponseDto::from)
+ .toList();
+ return ShippingLogSearchResult.builder()
+ .content(content)
+ .page(page)
+ .pageSize(validPageSize)
+ .totalCount(Optional.ofNullable(total).orElse(0L))
+ .build();
+ }
+ private BooleanExpression eqShippingId(UUID id) {
+ return id != null ? QShippingRouteLog.shippingRouteLog.shipping.id.eq(id) : null;
+ }
+ private BooleanExpression eqStartHubId(UUID id) {
+ return id != null ? QShippingRouteLog.shippingRouteLog.fromHubId.eq(id) : null;
+ }
+ private BooleanExpression eqEndHubId(UUID id) {
+ return id != null ? QShippingRouteLog.shippingRouteLog.toHubId.eq(id) : null;
+ }
+ private BooleanExpression eqHubRouteId(UUID id) {
+ return id != null ? QShippingRouteLog.shippingRouteLog.hubRouteId.eq(id) : null;
+ }
+ private OrderSpecifier> resolveSort(String sortBy, QShippingRouteLog log) {
+ if ("modifiedAt".equalsIgnoreCase(sortBy)) {
+ return log.updatedAt.desc();
+ }
+ return log.createdAt.desc();
+ }
+}
\ No newline at end of file
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingRouteQueryRepositoryImpl.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingRouteQueryRepositoryImpl.java
new file mode 100644
index 00000000..f11de998
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingRouteQueryRepositoryImpl.java
@@ -0,0 +1,83 @@
+package com.sparta.shippingservice.infrastructure.repository;
+
+import com.querydsl.core.BooleanBuilder;
+import com.querydsl.core.types.Order;
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.dsl.PathBuilder;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.shippingservice.application.dto.request.ShippingRouteSearchCondition;
+import com.sparta.shippingservice.domain.model.QShippingRouteLog;
+import com.sparta.shippingservice.domain.model.ShippingRouteLog;
+import com.sparta.shippingservice.domain.repository.ShippingRouteQueryRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.*;
+import org.springframework.stereotype.Repository;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class ShippingRouteQueryRepositoryImpl implements ShippingRouteQueryRepository {
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public Page search(ShippingRouteSearchCondition condition, Pageable pageable) {
+ QShippingRouteLog log = QShippingRouteLog.shippingRouteLog;
+
+ BooleanBuilder builder = new BooleanBuilder();
+ builder.and(log.deletedAt.isNull());
+
+ if (condition.getShippingId() != null) {
+ builder.and(log.shipping.id.eq(condition.getShippingId()));
+ }
+
+ if (condition.getFromHubId() != null) {
+ builder.and(log.fromHubId.eq(condition.getFromHubId()));
+ }
+
+ if (condition.getToHubId() != null) {
+ builder.and(log.toHubId.eq(condition.getToHubId()));
+ }
+
+ if (condition.getHubRouteId() != null) {
+ builder.and(log.hubRouteId.eq(condition.getHubRouteId()));
+ }
+
+ List results = queryFactory
+ .selectFrom(log)
+ .where(builder)
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize())
+ .orderBy(getSortedColumn(pageable.getSort()))
+ .fetch();
+
+ long total = queryFactory
+ .select(log.id.count())
+ .from(log)
+ .where(builder)
+ .fetchOne();
+
+ return new PageImpl<>(results, pageable, total);
+ }
+
+ private OrderSpecifier>[] getSortedColumn(Sort sort) {
+ List> orderSpecifiers = new ArrayList<>();
+ PathBuilder entityPath = new PathBuilder<>(ShippingRouteLog.class, "shippingRouteLog");
+
+ if (sort != null && sort.isSorted()) {
+ for (Sort.Order order : sort) {
+ Order direction = order.isAscending() ? Order.ASC : Order.DESC;
+ String property = order.getProperty();
+ orderSpecifiers.add(new OrderSpecifier(direction, entityPath.get(property)));
+ }
+ }
+
+ // 생성일 내림차순으로 기본 정렬
+ if (orderSpecifiers.isEmpty()) {
+ orderSpecifiers.add(new OrderSpecifier(Order.DESC, entityPath.get("createdAt")));
+ }
+
+ return orderSpecifiers.toArray(new OrderSpecifier[0]);
+ }
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingSearchRepository.java b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingSearchRepository.java
new file mode 100644
index 00000000..006ccfc7
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/infrastructure/repository/ShippingSearchRepository.java
@@ -0,0 +1,113 @@
+package com.sparta.shippingservice.infrastructure.repository;
+
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.shippingservice.application.dto.request.ShippingSearchCondition;
+import com.sparta.shippingservice.application.dto.response.ShippingResponseDto;
+import com.sparta.shippingservice.application.dto.response.ShippingSearchResult;
+import com.sparta.shippingservice.domain.model.ShippingStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+import static com.sparta.shippingservice.domain.model.QShipping.shipping;
+
+
+
+@Repository
+@RequiredArgsConstructor
+public class ShippingSearchRepository {
+ private final JPAQueryFactory queryFactory;
+
+
+ public ShippingSearchResult search(ShippingSearchCondition condition) {
+ int validPageSize = switch (condition.getPageSize()) {
+ case 10, 30, 50 -> condition.getPageSize();
+ default -> 10;
+ };
+ int page = condition.getPage();
+ List content = queryFactory
+ .select(Projections.constructor(ShippingResponseDto.class,
+ shipping.id,
+ shipping.receiverName,
+ shipping.shippingAddress,
+ shipping.status
+ ))
+ .from(shipping)
+ .where(
+ containsShippingAddress(condition.getShippingAddress()),
+ containsReceiverName(condition.getReceiverName()),
+ eqStatus(condition.getStatus())
+ )
+ .orderBy(resolveSort(condition.getSortBy()))
+ .offset((long) page * validPageSize)
+ .limit(validPageSize)
+ .fetch();
+ Long total = queryFactory
+ .select(shipping.count())
+ .from(shipping)
+ .where(
+ containsShippingAddress(condition.getShippingAddress()),
+ containsReceiverName(condition.getReceiverName()),
+ eqStatus(condition.getStatus())
+ )
+ .fetchOne();
+ return ShippingSearchResult.builder()
+ .content(content)
+ .page(page)
+ .pageSize(validPageSize)
+ .totalCount(total)
+ .build();
+ }
+
+
+
+ private BooleanExpression containsShippingAddress(String address) {
+ return StringUtils.hasText(address) ? shipping.shippingAddress.containsIgnoreCase(address) : null;
+ }
+ private BooleanExpression containsReceiverName(String name) {
+ return StringUtils.hasText(name) ? shipping.receiverName.containsIgnoreCase(name) : null;
+ }
+
+ private BooleanExpression eqStatus(ShippingStatus status) {
+ return status != null ? shipping.status.eq(status) : null;
+ }
+ private OrderSpecifier> resolveSort(String sortBy) {
+ if ("modifiedAt".equalsIgnoreCase(sortBy)) {
+ return shipping.updatedAt.desc();
+ }
+ return shipping.createdAt.desc(); // default
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/presentation/ShippingController.java b/shipping-service/src/main/java/com/sparta/shippingservice/presentation/ShippingController.java
new file mode 100644
index 00000000..71833451
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/presentation/ShippingController.java
@@ -0,0 +1,102 @@
+package com.sparta.shippingservice.presentation;
+
+import com.sparta.commonmodule.aop.RoleCheck;
+import com.sparta.shippingservice.application.dto.request.*;
+import com.sparta.shippingservice.application.dto.response.ShippingResponseDto;
+import com.sparta.shippingservice.application.dto.response.ShippingRouteResponseDto;
+import com.sparta.shippingservice.application.dto.response.ShippingWithRouteResponseDto;
+import com.sparta.shippingservice.application.service.ShippingService;
+
+import com.sparta.shippingservice.domain.model.Shipping;
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/shippings")
+@RequiredArgsConstructor
+public class ShippingController {
+ private final ShippingService shippingService;
+
+ @Operation(summary = "배송 등록",description = "배송 등록 API 입니다")
+ @RoleCheck("ROLE_MASTER")
+ @PostMapping // 배송 생성
+ public ResponseEntity create (@Valid @RequestBody CreateShippingRequestDto request, @RequestHeader("user_id")Long userId) {
+ ShippingWithRouteResponseDto responseDto = shippingService.create(request,userId);
+ return ResponseEntity.ok(responseDto);
+
+ }
+ @Operation(summary = "특정 배송 조회",description = "특정 배송 조회API 입니다")
+ @GetMapping("/{shippingId}") // 특정 배송 정보 조회
+ public ResponseEntity getShippingById(@PathVariable("shippingId") UUID id) {
+ ShippingResponseDto responseDto = shippingService.getShippingById(id);
+ return ResponseEntity.ok(responseDto);
+ }
+ @Operation(summary = "모든 배송 조회",description = "모든 배송 조회API 입니다")
+ @GetMapping() //모든 배송 내역 조회
+ public ResponseEntity> getAllShipping() {
+ List allShipping = shippingService.getAllShipping();
+ return ResponseEntity.ok(allShipping);
+ }
+
+ @Operation(summary = "배송 수정",description = "배송 수정 API 입니다")
+ @RoleCheck("ROLE_MASTER")
+ @PatchMapping("/{shippingId}") // 배송 내역 수정
+ public ResponseEntity updateShipping(@PathVariable("shippingId") UUID id, @Valid @RequestBody UpdateShippingRequestDto request, @RequestHeader("userId") Long userId ) {
+ ShippingResponseDto ResponseDto = shippingService.updateShipping(id, request,userId);
+ return ResponseEntity.ok(ResponseDto);
+ }
+
+ @GetMapping("/search")
+ public ResponseEntity> searchShippings(@ModelAttribute ShippingSearchCondition condition, HttpServletRequest request) {
+ Page result = shippingService.searchShipping(condition);
+ return ResponseEntity.ok(result);
+ }
+ @Operation(summary = "배송 삭제",description = "배송 삭제 API 입니다")
+ @RoleCheck("ROLE_MASTER")
+ @DeleteMapping("/{shippingId}")
+ public ResponseEntity deleteShipping(@PathVariable("shippingId") UUID id, @RequestHeader("userId") long userId) {
+ ShippingResponseDto responseDto = shippingService.deleteShipping(id, userId);
+ return ResponseEntity.ok(responseDto);
+ }
+
+ @Operation(summary = "특정 배송 로그 조회",description = "특정 배송 로그 조회 API 입니다")
+ @GetMapping("/{shippingId}/{shippingLogId}") // 특정 배송 로그 조회
+ public ResponseEntity getLogById(@PathVariable("shippingId") UUID id, @PathVariable("shippingLogId") UUID logId) {
+ ShippingRouteResponseDto responseDto = shippingService.getLogById(id, logId);
+ return ResponseEntity.ok(responseDto);
+ }
+ @Operation(summary = "모든 배송 로그 조회",description = "모든 배송 로그 조회 API 입니다")
+ @GetMapping("/log")
+ public ResponseEntity> getAllLog() {
+ List allLog = shippingService.getLogAll();
+ return ResponseEntity.ok(allLog);
+
+ }
+
+ @Operation(summary = "배송 로그 삭제",description = "배송 로그 삭제 API 입니다")
+ @RoleCheck("ROLE_MASTER")
+ @DeleteMapping("/{shippingId}/{shippingLogId}")
+ public ResponseEntitydeleteShippingLog(@PathVariable("shippingId") UUID id, @PathVariable("shippingLogId") UUID logId ,@RequestParam Long userId){
+ ShippingRouteResponseDto responseDto = shippingService.deleteShippingLog(id, logId, userId);
+ return ResponseEntity.ok(responseDto);
+
+ }
+ @Operation(summary = "모든 배송 로그 검색",description = "모든 배송 로그 검색 API 입니다")
+ @GetMapping("/log/search")
+ public ResponseEntity> searchShippingRoutes(
+ @ModelAttribute ShippingRouteSearchCondition condition
+ ) {
+ Page result = shippingService.searchRoutes(condition);
+ return ResponseEntity.ok(result);
+ }
+
+}
diff --git a/shipping-service/src/main/java/com/sparta/shippingservice/presentation/ShippingHubScanController.java b/shipping-service/src/main/java/com/sparta/shippingservice/presentation/ShippingHubScanController.java
new file mode 100644
index 00000000..66081b5f
--- /dev/null
+++ b/shipping-service/src/main/java/com/sparta/shippingservice/presentation/ShippingHubScanController.java
@@ -0,0 +1,49 @@
+package com.sparta.shippingservice.presentation;
+
+import com.sparta.shippingservice.application.service.ShippingHubScanService;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.InboundStatusRequestDto;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.InboundStatusResponseDto;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.OutboundStatusRequestDto;
+import com.sparta.shippingservice.infrastructure.hub_feign.dto.OutboundStatusResponseDto;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/shippings")
+public class ShippingHubScanController {
+
+ private final ShippingHubScanService shippingHubScanService;
+
+ // feign client : 배송건에 대한 허브 입고 기록 저장
+ @PostMapping("/{shipping_id}/inbound")
+ public ResponseEntity inboundStatus(
+ @PathVariable("shipping_id") UUID shippingId,
+ @RequestHeader("user_id") Long userId,
+ @RequestBody InboundStatusRequestDto inboundStatusRequestDto) {
+
+ InboundStatusResponseDto responseDto =
+ shippingHubScanService.createInboundLog(shippingId,userId, inboundStatusRequestDto);
+ return ResponseEntity.ok(responseDto);
+ }
+
+ // feign client : 배송건에 대한 허브 출고 기록 저장
+ @PostMapping("/{shipping_id}/outbound")
+ public ResponseEntity outboundStatus(
+ @PathVariable("shipping_id") UUID shippingId,
+ @RequestHeader("user_id") Long userId,
+ @RequestBody OutboundStatusRequestDto outboundStatusRequestDto){
+
+ OutboundStatusResponseDto responseDto =
+ shippingHubScanService.createOutboundLog(shippingId, userId, outboundStatusRequestDto);
+ return ResponseEntity.ok(responseDto);
+ }
+
+}
diff --git a/shipping-service/src/main/resources/application.yaml b/shipping-service/src/main/resources/application.yaml
new file mode 100644
index 00000000..9e6a8033
--- /dev/null
+++ b/shipping-service/src/main/resources/application.yaml
@@ -0,0 +1,43 @@
+spring:
+ application:
+ name: shipping-service
+ datasource:
+ url: jdbc:postgresql://localhost:5001/maindb
+ username: admin
+ password: 1234
+ driver-class-name: org.postgresql.Driver
+
+ jpa:
+ properties:
+ hibernate:
+ default_schema: shipping_service
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ hibernate:
+ ddl-auto: update
+ show-sql: true
+server:
+ port: 8088
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ service-url:
+ defaultZone: http://localhost:8761/eureka/
+
+feign:
+ client:
+ config:
+ shipping-manager-service:
+ url: http://localhost:8088
+ hub-service:
+ url: http://localhost:8082
+
+springdoc:
+ api-docs:
+ version: openapi_3_1
+ enabled: true
+ path: /product-service/v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
\ No newline at end of file
diff --git a/shipping-service/src/test/java/com/sparta/shippingservice/ShippingServiceApplicationTests.java b/shipping-service/src/test/java/com/sparta/shippingservice/ShippingServiceApplicationTests.java
new file mode 100644
index 00000000..03437d39
--- /dev/null
+++ b/shipping-service/src/test/java/com/sparta/shippingservice/ShippingServiceApplicationTests.java
@@ -0,0 +1,12 @@
+package com.sparta.shippingservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class ShippingServiceApplicationTests {
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/slack-service/Dockerfile.dev b/slack-service/Dockerfile.dev
new file mode 100644
index 00000000..55b33b3d
--- /dev/null
+++ b/slack-service/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:17-jdk
+
+WORKDIR /app
+
+# Gradle 캐시 최적화
+COPY gradle gradle
+COPY gradlew build.gradle settings.gradle ./
+RUN chmod +x gradlew
+RUN ./gradlew dependencies --no-daemon
+
+# 전체 프로젝트 복사
+COPY . .
+
+# 개발 모드에서 실행 (빌드 없이 바로 실행)
+ENTRYPOINT ["./gradlew", ":slack-service:bootRun"]
diff --git a/slack-service/build.gradle b/slack-service/build.gradle
new file mode 100644
index 00000000..26a936da
--- /dev/null
+++ b/slack-service/build.gradle
@@ -0,0 +1,33 @@
+
+ext {
+ set('springCloudVersion', "2024.0.0")
+ set('springAiVersion', "1.0.0-M6")
+
+}
+
+dependencies {
+ implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'com.slack.api:slack-api-client:1.43.0'
+
+ implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
+ annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
+ annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
+ annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
+
+ runtimeOnly 'org.postgresql:postgresql'
+ implementation project(':common-module')
+
+ implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
+
+ implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+ mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
+ }
+}
\ No newline at end of file
diff --git a/slack-service/src/main/java/com/sparta/slackservice/SlackServiceApplication.java b/slack-service/src/main/java/com/sparta/slackservice/SlackServiceApplication.java
new file mode 100644
index 00000000..403feec3
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/SlackServiceApplication.java
@@ -0,0 +1,18 @@
+package com.sparta.slackservice;
+
+import com.sparta.commonmodule.config.JpaAuditingConfig;
+import com.sparta.commonmodule.config.SwaggerConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@SpringBootApplication(scanBasePackages = "com.sparta")
+@Import({SwaggerConfig.class, JpaAuditingConfig.class})
+public class SlackServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SlackServiceApplication.class, args);
+ }
+
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/application/config/RestConfig.java b/slack-service/src/main/java/com/sparta/slackservice/application/config/RestConfig.java
new file mode 100644
index 00000000..640e3a3d
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/application/config/RestConfig.java
@@ -0,0 +1,14 @@
+package com.sparta.slackservice.application.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestConfig {
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+}
\ No newline at end of file
diff --git a/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackRequestDto.java b/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackRequestDto.java
new file mode 100644
index 00000000..7fe3ceec
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackRequestDto.java
@@ -0,0 +1,21 @@
+package com.sparta.slackservice.application.dto;
+
+import com.sparta.slackservice.domain.model.Slack;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class SlackRequestDto {
+ private String message;
+ private Long receiverId;
+
+
+ public Slack createSlack(String slack_name,Long userId) {
+ return Slack.builder()
+ .slackName(slack_name)
+ .message(message)
+ .receiverId(receiverId)
+ .build();
+ }
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackResponseDto.java b/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackResponseDto.java
new file mode 100644
index 00000000..af5d801f
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackResponseDto.java
@@ -0,0 +1,29 @@
+package com.sparta.slackservice.application.dto;
+
+import com.sparta.slackservice.domain.model.Slack;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Getter
+@AllArgsConstructor
+public class SlackResponseDto {
+ private UUID slackId;
+ private Long receiverId;
+ private String slackName;
+ private String message;
+ private LocalDateTime sendedAt;
+ private boolean sendingStatus;
+
+
+ public SlackResponseDto(Slack slack) {
+ this.slackId = slack.getId();
+ this.slackName = slack.getSlackName();
+ this.receiverId = slack.getReceiverId();
+ this.message = slack.getMessage();
+ this.sendedAt = slack.getSendedAt();
+ this.sendingStatus = slack.isSendingStatus();
+ }
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackSearchRequestDto.java b/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackSearchRequestDto.java
new file mode 100644
index 00000000..2ccbab4e
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/application/dto/SlackSearchRequestDto.java
@@ -0,0 +1,13 @@
+package com.sparta.slackservice.application.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public class SlackSearchRequestDto {
+ private String message;
+ private Long receiverId;
+ private String slackName;
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/application/service/SlackService.java b/slack-service/src/main/java/com/sparta/slackservice/application/service/SlackService.java
new file mode 100644
index 00000000..55b6a93f
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/application/service/SlackService.java
@@ -0,0 +1,71 @@
+package com.sparta.slackservice.application.service;
+
+import com.sparta.commonmodule.exception.ResourceNotFoundException;
+import com.sparta.slackservice.application.dto.SlackRequestDto;
+import com.sparta.slackservice.application.dto.SlackResponseDto;
+import com.sparta.slackservice.application.dto.SlackSearchRequestDto;
+import com.sparta.slackservice.domain.model.Slack;
+import com.sparta.slackservice.domain.repository.SlackRepository;
+import com.sparta.slackservice.infastructure.JpaSlackRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class SlackService {
+
+ private final SlackRepository slackRepository;
+ private final SlackWebhookService slackWebhookService;
+
+ //슬랙 메세지 생성
+ public SlackResponseDto createSlack(String slack_name, SlackRequestDto requestDto, Long userId) {
+
+ Slack slack = slackRepository.save(requestDto.createSlack(slack_name, userId));
+ return new SlackResponseDto(slack);
+ }
+
+ //메세지 전송
+ public void sendSlack(UUID slackId) {
+ Slack slack = findingSlack(slackId);
+ slack.changeStatus();
+ slackWebhookService.sendMessage(slack.getMessage());
+ }
+
+ //메세지 조회(단건)
+ @Transactional(readOnly = true)
+ public Slack getSlack(UUID slackId) {
+ return findingSlack(slackId);
+ }
+
+ //메세지 조회(전체)
+ @Transactional(readOnly = true)
+ public Page searchSlack(SlackSearchRequestDto requestDto, Pageable pageable) {
+ return slackRepository.searchSlack(requestDto,pageable);
+ }
+
+ //메세지 수정
+ public SlackResponseDto modifySlack(UUID slackId, SlackRequestDto requestDto, Long userId) {
+ Slack slack = findingSlack(slackId);
+ slack.modifySlack(requestDto);
+ return new SlackResponseDto(slack);
+ }
+
+ //메세지 삭제
+ public void deleteSlack(UUID slackId, Long userId) {
+ Slack slack = findingSlack(slackId);
+ slack.delete(userId);
+ }
+
+ private Slack findingSlack(UUID slackId) {
+ return slackRepository.findById(slackId).orElseThrow(() -> new RuntimeException("Slack not found"));
+ }
+
+
+
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/application/service/SlackWebhookService.java b/slack-service/src/main/java/com/sparta/slackservice/application/service/SlackWebhookService.java
new file mode 100644
index 00000000..13c22ccc
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/application/service/SlackWebhookService.java
@@ -0,0 +1,36 @@
+package com.sparta.slackservice.application.service;
+
+
+import com.slack.api.Slack;
+import com.slack.api.webhook.Payload;
+import com.slack.api.webhook.WebhookResponse;
+import com.sparta.slackservice.infastructure.JpaSlackRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+
+@Service
+@RequiredArgsConstructor
+public class SlackWebhookService {//슬랙 api 메세지 전송 서비스
+ @Value("${slack.webhook.url}")
+ private String slackWebhookUrl; //알림을 전송할 url
+
+ private final Slack slack = Slack.getInstance();
+
+ private final JpaSlackRepository slackRepository;
+
+ public WebhookResponse sendMessage(String message) {
+ Payload payload = Payload.builder().text(message).build();
+ // WebhookResponse(code=200, message=OK, body=ok)
+ WebhookResponse response;
+ try {
+ response = slack.send(slackWebhookUrl, payload);
+ System.out.println(response);
+ return response;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/domain/model/Slack.java b/slack-service/src/main/java/com/sparta/slackservice/domain/model/Slack.java
new file mode 100644
index 00000000..db02507e
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/domain/model/Slack.java
@@ -0,0 +1,59 @@
+package com.sparta.slackservice.domain.model;
+
+import com.sparta.commonmodule.entity.BaseEntity;
+import com.sparta.slackservice.application.dto.SlackRequestDto;
+import jakarta.persistence.*;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.SQLRestriction;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+import java.util.UUID;
+
+@Entity
+@Getter
+@Table(name = "p_slacks")
+@NoArgsConstructor
+@SQLRestriction("is_deleted IS FALSE")
+public class Slack extends BaseEntity {
+
+ @Id
+ @Column(name = "slack_id", nullable = false)
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private UUID id;
+
+ @Column(name = "slack_name", nullable = false)
+ private String slackName;
+
+ @Column(nullable = false)
+ private String message;
+
+ @Column(name = "receiver_id", nullable = false)
+ private Long receiverId;
+
+ @Column(name = "sended_at")
+ private LocalDateTime sendedAt;
+
+ @Column(name = "sending_status")
+ private boolean sendingStatus = false; //기본 true? false?
+
+ @Builder
+ public Slack(String slackName, String message, Long receiverId) {
+ this.slackName = slackName;
+ this.message = message;
+ this.receiverId = receiverId;
+ }
+
+ public void changeStatus() {
+ this.sendingStatus = true;
+ this.sendedAt = LocalDateTime.now();
+ }
+
+ public void modifySlack(SlackRequestDto requestDto) {
+ Optional.ofNullable(requestDto.getMessage()).ifPresent(message -> this.message = message);
+ Optional.ofNullable(requestDto.getReceiverId()).ifPresent(receiverId -> this.receiverId = receiverId);
+ }
+
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/domain/repository/SlackRepository.java b/slack-service/src/main/java/com/sparta/slackservice/domain/repository/SlackRepository.java
new file mode 100644
index 00000000..ade11d05
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/domain/repository/SlackRepository.java
@@ -0,0 +1,18 @@
+package com.sparta.slackservice.domain.repository;
+
+import com.sparta.slackservice.application.dto.SlackResponseDto;
+import com.sparta.slackservice.application.dto.SlackSearchRequestDto;
+import com.sparta.slackservice.domain.model.Slack;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+import java.util.UUID;
+
+public interface SlackRepository {
+ Page searchSlack(SlackSearchRequestDto requestDto, Pageable pageable);
+
+ Slack save(Slack slack);
+
+ Optional findById(UUID slackId);
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/infastructure/JpaSlackRepository.java b/slack-service/src/main/java/com/sparta/slackservice/infastructure/JpaSlackRepository.java
new file mode 100644
index 00000000..6876a17e
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/infastructure/JpaSlackRepository.java
@@ -0,0 +1,11 @@
+package com.sparta.slackservice.infastructure;
+import com.sparta.slackservice.domain.model.Slack;
+import com.sparta.slackservice.domain.repository.SlackRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+@Repository
+public interface JpaSlackRepository extends JpaRepository, SlackRepository, SlackQueryDSLRepository {
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/infastructure/SlackQueryDSLRepository.java b/slack-service/src/main/java/com/sparta/slackservice/infastructure/SlackQueryDSLRepository.java
new file mode 100644
index 00000000..f9614e92
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/infastructure/SlackQueryDSLRepository.java
@@ -0,0 +1,11 @@
+package com.sparta.slackservice.infastructure;
+
+import com.sparta.slackservice.application.dto.SlackRequestDto;
+import com.sparta.slackservice.application.dto.SlackResponseDto;
+import com.sparta.slackservice.application.dto.SlackSearchRequestDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface SlackQueryDSLRepository {
+ Page searchSlack(SlackSearchRequestDto requestDto, Pageable pageable);
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/infastructure/SlackQueryDSLRepositoryImpl.java b/slack-service/src/main/java/com/sparta/slackservice/infastructure/SlackQueryDSLRepositoryImpl.java
new file mode 100644
index 00000000..390cbe03
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/infastructure/SlackQueryDSLRepositoryImpl.java
@@ -0,0 +1,67 @@
+package com.sparta.slackservice.infastructure;
+
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.sparta.slackservice.application.dto.SlackRequestDto;
+import com.sparta.slackservice.application.dto.SlackResponseDto;
+import com.sparta.slackservice.application.dto.SlackSearchRequestDto;
+import com.sparta.slackservice.domain.model.QSlack;
+import com.sparta.slackservice.domain.model.Slack;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+
+public class SlackQueryDSLRepositoryImpl implements SlackQueryDSLRepository {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Override
+ public Page searchSlack(SlackSearchRequestDto requestDto, Pageable pageable) {
+ QSlack qSlack = QSlack.slack; // Querydsl의 Q 클래스를 사용
+
+ // 검색 조건을 작성할 BooleanExpression을 초기화
+ BooleanExpression predicate = qSlack.isNotNull(); // 기본 조건 (모든 레코드를 가져오는 기본 조건)
+
+ // 요청된 조건을 바탕으로 동적으로 조건을 추가
+ if (requestDto.getSlackName() != null && !requestDto.getSlackName().isEmpty()) {
+ predicate = predicate.and(qSlack.slackName.containsIgnoreCase(requestDto.getSlackName()));
+ }
+
+ if (requestDto.getMessage() != null && !requestDto.getMessage().isEmpty()) {
+ predicate = predicate.and(qSlack.message.containsIgnoreCase(requestDto.getMessage()));
+ }
+
+ if (requestDto.getReceiverId() != null && !requestDto.getReceiverId().describeConstable().isEmpty()) {
+ predicate = predicate.and(qSlack.receiverId.stringValue().containsIgnoreCase(String.valueOf(requestDto.getReceiverId())));
+ }
+
+ // JPAQueryFactory를 통해 쿼리 실행
+ JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
+
+ // 실제 쿼리 실행 (페이징 적용)
+ List result = queryFactory
+ .selectFrom(qSlack)
+ .where(predicate)
+ .offset(pageable.getOffset()) // 페이지네이션 처리
+ .limit(pageable.getPageSize()) // 페이지네이션 처리
+ .fetch();
+
+ // 전체 데이터 개수 가져오기 (페이징 처리를 위한 전체 개수)
+ long totalCount = queryFactory
+ .selectFrom(qSlack)
+ .where(predicate)
+ .fetchCount();
+
+ // 결과를 Page 객체로 반환 (SlackResponseDto로 변환)
+ List responseDtos = result.stream()
+ .map(SlackResponseDto::new)
+ .toList();
+
+ return new PageImpl<>(responseDtos, pageable, totalCount);
+ }
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/infastructure/client/OrderClient.java b/slack-service/src/main/java/com/sparta/slackservice/infastructure/client/OrderClient.java
new file mode 100644
index 00000000..55543295
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/infastructure/client/OrderClient.java
@@ -0,0 +1,16 @@
+package com.sparta.slackservice.infastructure.client;
+
+import com.sparta.slackservice.infastructure.client.dto.SlackNotificationDto;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.UUID;
+
+@FeignClient(name = "order-service", path = "/api/v1/orders")
+public interface OrderClient {
+
+ @GetMapping("/{id}/slack-info")
+ SlackNotificationDto getSlackNotificationInfo(@PathVariable UUID id);
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/infastructure/client/dto/SlackNotificationDto.java b/slack-service/src/main/java/com/sparta/slackservice/infastructure/client/dto/SlackNotificationDto.java
new file mode 100644
index 00000000..ccb59e06
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/infastructure/client/dto/SlackNotificationDto.java
@@ -0,0 +1,24 @@
+package com.sparta.slackservice.infastructure.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+//Slack 알림 관련해서 응답 DTO
+public class SlackNotificationDto {
+
+ private UUID orderId;
+ private UUID shippingId;
+ private String shippingStatus;
+ private String route;
+ private String hubName;
+ private String hubManagerName;
+ private String message;
+}
diff --git a/slack-service/src/main/java/com/sparta/slackservice/presentation/SlackController.java b/slack-service/src/main/java/com/sparta/slackservice/presentation/SlackController.java
new file mode 100644
index 00000000..563a9763
--- /dev/null
+++ b/slack-service/src/main/java/com/sparta/slackservice/presentation/SlackController.java
@@ -0,0 +1,75 @@
+package com.sparta.slackservice.presentation;
+
+import com.sparta.commonmodule.aop.RoleCheck;
+import com.sparta.slackservice.application.dto.SlackRequestDto;
+import com.sparta.slackservice.application.dto.SlackResponseDto;
+import com.sparta.slackservice.application.dto.SlackSearchRequestDto;
+import com.sparta.slackservice.application.service.SlackService;
+import com.sparta.slackservice.application.service.SlackWebhookService;
+import com.sparta.slackservice.domain.model.Slack;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/v1/slacks")
+@RequiredArgsConstructor
+@Tag(name = "Slack Service", description = "슬랙 메세지 서비스 API")
+public class SlackController {
+
+ private final SlackService slackService;
+ private final SlackWebhookService slackWebhookService;
+
+ @Operation(summary = "메세지 생성", description = "메세지 생성 api입니다.")
+ @PostMapping("/create")
+ public ResponseEntity createSlack(@RequestHeader("user_id") Long userId, @RequestHeader("slack_name") String slack_name, @RequestBody SlackRequestDto requestDto) {
+ return ResponseEntity.ok(slackService.createSlack(slack_name, requestDto, userId));
+ }
+
+ @Operation(summary = "메세지 전송", description = "메세지 전송 api입니다.")
+ @PostMapping("/send/{id}")
+ public ResponseEntity