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 sendSlack(@PathVariable("id") UUID slackId) { + slackService.sendSlack(slackId); + return ResponseEntity.ok("전송성공"); + } + + @RoleCheck("ROLE_MASTER") + @Operation(summary = "메세지 조회(단건)", description = "메세지 조회(단건) api입니다.") + @GetMapping("/{id}") + public ResponseEntity getSlack(@PathVariable("id") UUID slackId) { + return ResponseEntity.ok(slackService.getSlack(slackId)); + } + + @RoleCheck("ROLE_MASTER") + @Operation(summary = "메세지 조회(전체)", description = "메세지 조회(전체) api입니다.") + @PostMapping("/search") + public ResponseEntity> searchSlack( + @RequestBody SlackSearchRequestDto requestDto, + @PageableDefault(page = 0, size = 10, sort = "createdAt") Pageable pageable) { + Page responseDto= slackService.searchSlack(requestDto,pageable); + return ResponseEntity.ok(responseDto); + } + + @RoleCheck("ROLE_MASTER") + @Operation(summary = "메세지 수정", description = "메세지 수정 api입니다.") + @PutMapping("/modify/{id}") + public ResponseEntity modifySlack(@RequestHeader("user_id") Long userId,@PathVariable("id") UUID slackId, @RequestBody SlackRequestDto requestDto) { + return ResponseEntity.ok(slackService.modifySlack(slackId, requestDto, userId)); + } + + @RoleCheck("ROLE_MASTER") + @Operation(summary = "메세지 삭제", description = "메세지 삭제 api입니다.") + @DeleteMapping("/{id}") + public ResponseEntity deleteSlack(@PathVariable("id") UUID slackId, @RequestHeader("user_id") Long userId) { + slackService.deleteSlack(slackId, userId); + return ResponseEntity.noContent().build();//삭제 id, 메세지 출력 + } + +} diff --git a/slack-service/src/main/resources/application.yml b/slack-service/src/main/resources/application.yml new file mode 100644 index 00000000..58083407 --- /dev/null +++ b/slack-service/src/main/resources/application.yml @@ -0,0 +1,44 @@ +spring: + application: + name: slack-service + datasource: + url: jdbc:postgresql://localhost:5001/maindb + username: admin + password: 1234 + driver-class-name: org.postgresql.Driver + jpa: + properties: + hibernate: + default_schema: slack_service + dialect: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: update + show-sql: true + h2: + console: + enabled: false # H2 콘솔 비활성화 (PostgreSQL 사용 시) + ai: + openai: + api-key: ${OPEN_AI_KEY} +server: + port: 8087 + +eureka: + client: + register-with-eureka: true # Eureka? ??? ?? + fetch-registry: true # Eureka?? ??? ?? ???? + service-url: + defaultZone: http://localhost:8761/eureka/ + +springdoc: + api-docs: + version: openapi_3_1 + enabled: true + path: /slack-service/v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html +slack: + webhook: + url: ${SLACK_URL} + \ No newline at end of file diff --git a/slack-service/src/test/java/com/sparta/slackservice/SlackServiceApplicationTests.java b/slack-service/src/test/java/com/sparta/slackservice/SlackServiceApplicationTests.java new file mode 100644 index 00000000..5351bf23 --- /dev/null +++ b/slack-service/src/test/java/com/sparta/slackservice/SlackServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.sparta.slackservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SlackServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/slack-service/src/test/java/com/sparta/slackservice/presentation/SlackControllerTest.java b/slack-service/src/test/java/com/sparta/slackservice/presentation/SlackControllerTest.java new file mode 100644 index 00000000..45644d85 --- /dev/null +++ b/slack-service/src/test/java/com/sparta/slackservice/presentation/SlackControllerTest.java @@ -0,0 +1,95 @@ +package com.sparta.slackservice.presentation; + +import com.sparta.slackservice.application.service.SlackService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class SlackControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private SlackService slackService; + + @Test + void createSlack() throws Exception{ + mockMvc.perform(post("/api/v1/slacks/create") + .contentType(MediaType.APPLICATION_JSON) + .content("{" + + "\"message\":\"안녕하세요!\"," + + "\"receiverId\":3" + + "}") + .header("user_id", 2) + .header("slack_name","sampleName") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.slackName").value("sampleName")) + .andExpect(jsonPath("$.message").value("안녕하세요!")) + .andExpect(jsonPath("$.receiverId").value(3)); + } + + @Test + void sendSlack() throws Exception { + mockMvc.perform(get("/api/v1/slacks/send/{id}", "3dce1425-958c-4c07-bbbb-785a2c9e81d4") + ).andExpect(status().isOk()) + .andExpect(content().string("전송성공")); + } + + @Test + void getSlack() throws Exception{ + mockMvc.perform(get("/api/v1/slacks/{id}", "4acbaa09-473f-4737-a107-5b085a0dbb23") + ).andExpect(status().isOk()) + .andExpect(jsonPath("$.slackName").value("sampleName")) + .andExpect(jsonPath("$.message").value("안녕하세요!")) + .andExpect(jsonPath("$.receiverId").value(3)); + + } + + @Test + void searchSlack() throws Exception { + mockMvc.perform(get("/api/v1/slacks/search") + .contentType(MediaType.APPLICATION_JSON) + .content("{" + + "\"receiverId\":3"+ + "}") + ).andExpect(status().isOk()) + .andExpect(jsonPath("$.slackName").value("sampleName")) + .andExpect(jsonPath("$.message").value("안녕하세요!")) + .andExpect(jsonPath("$.receiverId").value(3)); + } + + @Test + void modifySlack() throws Exception { + mockMvc.perform(put("/api/v1/slacks/modify/{id}", "4acbaa09-473f-4737-a107-5b085a0dbb23") + .contentType(MediaType.APPLICATION_JSON) + .content("{" + + "\"message\":\"감사합니다!\"," + + "\"receiverId\":3" + + "}") + .header("user_id", 2) + .header("slack_name","sampleName") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.slackName").value("sampleName")) + .andExpect(jsonPath("$.message").value("감사합니다!")) + .andExpect(jsonPath("$.receiverId").value(3)); + } + + @Test + void deleteSlack() throws Exception { + mockMvc.perform(delete("/api/v1/slacks/{id}", "4acbaa09-473f-4737-a107-5b085a0dbb23") + .contentType(MediaType.APPLICATION_JSON) + .header("user_id", 2) + ).andExpect(status().isNoContent()); + } +} \ No newline at end of file diff --git a/stock-service/build.gradle b/stock-service/build.gradle new file mode 100644 index 00000000..45cdf64a --- /dev/null +++ b/stock-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/stock-service/src/main/java/com/sparta/stockservice/StockServiceApplication.java b/stock-service/src/main/java/com/sparta/stockservice/StockServiceApplication.java new file mode 100644 index 00000000..591e5953 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/StockServiceApplication.java @@ -0,0 +1,13 @@ +package com.sparta.stockservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StockServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(StockServiceApplication.class, args); + } + +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/application/dto/DecreaseStockServiceRequestDto.java b/stock-service/src/main/java/com/sparta/stockservice/application/dto/DecreaseStockServiceRequestDto.java new file mode 100644 index 00000000..9ff1ff93 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/application/dto/DecreaseStockServiceRequestDto.java @@ -0,0 +1,23 @@ +package com.sparta.stockservice.application.dto; + +import com.sparta.stockservice.presentation.dto.request.DecreaseStockRequestDto; +import lombok.Builder; + +import java.util.UUID; + +@Builder +public record DecreaseStockServiceRequestDto(UUID productId, + UUID companyId, + UUID hubId, + Integer quantity) { + + // 요청 DTO -> 서비스 DTO 변환 메서드 + public static DecreaseStockServiceRequestDto of(DecreaseStockRequestDto request, UUID productId) { + return DecreaseStockServiceRequestDto.builder() + .productId(productId) + .companyId(request.companyId()) + .hubId(request.hubId()) + .quantity(request.quantity()) + .build(); + } +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/application/dto/IncreaseStockServiceRequestDto.java b/stock-service/src/main/java/com/sparta/stockservice/application/dto/IncreaseStockServiceRequestDto.java new file mode 100644 index 00000000..0b8cda4b --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/application/dto/IncreaseStockServiceRequestDto.java @@ -0,0 +1,23 @@ +package com.sparta.stockservice.application.dto; + +import com.sparta.stockservice.presentation.dto.request.IncreaseStockRequestDto; +import lombok.Builder; + +import java.util.UUID; + +@Builder +public record IncreaseStockServiceRequestDto(UUID productId, + UUID companyId, + UUID hubId, + Integer quantity) { + + // 요청 DTO -> 서비스 DTO 변환 메서드 + public static IncreaseStockServiceRequestDto of(IncreaseStockRequestDto request, UUID productId) { + return IncreaseStockServiceRequestDto.builder() + .productId(productId) + .companyId(request.companyId()) + .hubId(request.hubId()) + .quantity(request.quantity()) + .build(); + } +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/application/service/StockService.java b/stock-service/src/main/java/com/sparta/stockservice/application/service/StockService.java new file mode 100644 index 00000000..19e87adc --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/application/service/StockService.java @@ -0,0 +1,18 @@ +package com.sparta.stockservice.application.service; + + +import com.sparta.stockservice.application.dto.DecreaseStockServiceRequestDto; +import com.sparta.stockservice.application.dto.IncreaseStockServiceRequestDto; +import com.sparta.stockservice.presentation.dto.request.CreateStockRequestDto; +import com.sparta.stockservice.presentation.dto.response.CreateStockResponseDto; +import com.sparta.stockservice.presentation.dto.response.DecreaseStockResponseDto; +import com.sparta.stockservice.presentation.dto.response.IncreaseStockResponseDto; + +public interface StockService { + + CreateStockResponseDto createStock(CreateStockRequestDto requestDto); + + DecreaseStockResponseDto decreaseStock(DecreaseStockServiceRequestDto serviceDto); + + IncreaseStockResponseDto increaseStock(IncreaseStockServiceRequestDto serviceDto); +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/application/service/StockServiceImpl.java b/stock-service/src/main/java/com/sparta/stockservice/application/service/StockServiceImpl.java new file mode 100644 index 00000000..c01abfa3 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/application/service/StockServiceImpl.java @@ -0,0 +1,67 @@ +package com.sparta.stockservice.application.service; + +import com.sparta.commonmodule.exception.ResourceNotFoundException; +import com.sparta.stockservice.application.dto.DecreaseStockServiceRequestDto; +import com.sparta.stockservice.application.dto.IncreaseStockServiceRequestDto; +import com.sparta.stockservice.domain.model.Stock; +import com.sparta.stockservice.domain.repository.StockRepository; +import com.sparta.stockservice.presentation.dto.request.CreateStockRequestDto; +import com.sparta.stockservice.presentation.dto.response.CreateStockResponseDto; +import com.sparta.stockservice.presentation.dto.response.DecreaseStockResponseDto; +import com.sparta.stockservice.presentation.dto.response.IncreaseStockResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class StockServiceImpl implements StockService { + + private final StockRepository stockRepository; + + + /** + * 재고 생성 + */ + @Override + public CreateStockResponseDto createStock(CreateStockRequestDto requestDto) { + Stock stock = stockRepository.save(Stock.createStock(requestDto.productId(), requestDto.hubId(), requestDto.quantity())); + return CreateStockResponseDto.from(stock); + } + + + + /** + * 재고 감소 + */ + @Override + public DecreaseStockResponseDto decreaseStock(DecreaseStockServiceRequestDto serviceDto) { + Stock stock = stockRepository.findByProductIdAndHubId(serviceDto.productId(), serviceDto.hubId()) + .orElseThrow(() -> new ResourceNotFoundException("해당 허브에 상품이 존재하지 않습니다.")); + + try { + // 재고 감소 성공 + stock.decreaseStock(serviceDto.quantity()); + return DecreaseStockResponseDto.success(serviceDto.productId(), serviceDto.quantity()); + } catch (IllegalArgumentException | IllegalStateException e) { + // 재고 감소 실패 (재고 부족, 최소 수량 미만 등) + return DecreaseStockResponseDto.failure(serviceDto.productId()); + } + } + + + /** + * 재고 증가 + */ + @Override + public IncreaseStockResponseDto increaseStock(IncreaseStockServiceRequestDto serviceDto) { + Stock stock = stockRepository.findByProductIdAndHubId(serviceDto.productId(), serviceDto.hubId()) + .orElseThrow(() -> new ResourceNotFoundException("해당 허브에 상품이 존재하지 않습니다.")); + + stock.increaseStock(serviceDto.quantity()); + + return IncreaseStockResponseDto.success(serviceDto.productId(), serviceDto.quantity()); + } + +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/domain/model/Stock.java b/stock-service/src/main/java/com/sparta/stockservice/domain/model/Stock.java new file mode 100644 index 00000000..66d23cee --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/domain/model/Stock.java @@ -0,0 +1,76 @@ +package com.sparta.stockservice.domain.model; + +import com.sparta.commonmodule.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLRestriction; + +import java.util.UUID; + +@Entity +@Table( + name="p_stock", + uniqueConstraints = @UniqueConstraint(columnNames = {"product_id", "hub_id"}) +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLRestriction("is_deleted IS FALSE") +@Builder(access = AccessLevel.PRIVATE) +public class Stock extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name="stock_id", updatable = false, nullable = false) + private UUID id; + + @Column(name = "product_id", nullable = false) + private UUID productId; + + @Column(name = "hub_id", nullable = false) + private UUID hubId; + + @Column(name = "quantity", nullable = false) + private Integer quantity; + + + /** + * 재고 생성 + */ + public static Stock createStock(UUID productId, UUID hubId, Integer quantity) { + return Stock.builder() + .productId(productId) + .hubId(hubId) + .quantity(quantity) + .build(); + } + + + /** + * 재고 감소 + */ + public void decreaseStock(Integer quantity) { + validateDecreaseQuantity(quantity); + this.quantity = this.quantity - quantity; + } + + + + /** + * 재고 증가 + */ + public void increaseStock(Integer quantity) { + this.quantity = this.quantity + quantity; + } + + + private void validateDecreaseQuantity(Integer quantity) { + if (quantity == null || quantity < 30) { + throw new IllegalArgumentException("최소 30개 이상 요청해야 합니다."); + } + if (this.quantity < quantity) { + throw new IllegalStateException("재고가 부족합니다."); + } + } + +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/domain/repository/StockRepository.java b/stock-service/src/main/java/com/sparta/stockservice/domain/repository/StockRepository.java new file mode 100644 index 00000000..a3ac60c2 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/domain/repository/StockRepository.java @@ -0,0 +1,13 @@ +package com.sparta.stockservice.domain.repository; + +import com.sparta.stockservice.domain.model.Stock; + +import java.util.Optional; +import java.util.UUID; + +public interface StockRepository { + + Stock save(Stock stock); + + Optional findByProductIdAndHubId(UUID productId, UUID hubId); +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/infrastructure/repository/JpaStockRepository.java b/stock-service/src/main/java/com/sparta/stockservice/infrastructure/repository/JpaStockRepository.java new file mode 100644 index 00000000..3a7eccf1 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/infrastructure/repository/JpaStockRepository.java @@ -0,0 +1,10 @@ +package com.sparta.stockservice.infrastructure.repository; + +import com.sparta.stockservice.domain.model.Stock; +import com.sparta.stockservice.domain.repository.StockRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface JpaStockRepository extends StockRepository, JpaRepository { +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/presentation/StockController.java b/stock-service/src/main/java/com/sparta/stockservice/presentation/StockController.java new file mode 100644 index 00000000..f65a3a92 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/presentation/StockController.java @@ -0,0 +1,56 @@ +package com.sparta.stockservice.presentation; + +import com.sparta.stockservice.presentation.dto.request.CreateStockRequestDto; +import com.sparta.stockservice.presentation.dto.response.CreateStockResponseDto; +import com.sparta.stockservice.presentation.dto.response.DecreaseStockResponseDto; +import com.sparta.stockservice.presentation.dto.response.IncreaseStockResponseDto; +import com.sparta.stockservice.application.dto.DecreaseStockServiceRequestDto; +import com.sparta.stockservice.application.dto.IncreaseStockServiceRequestDto; +import com.sparta.stockservice.application.service.StockService; +import com.sparta.stockservice.presentation.dto.request.DecreaseStockRequestDto; +import com.sparta.stockservice.presentation.dto.request.IncreaseStockRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/stocks") +@RequiredArgsConstructor +public class StockController { + + private final StockService stockService; + + /** + * 재고 생성 + */ + @PostMapping + public ResponseEntity createStock(@RequestBody CreateStockRequestDto requestDto, + @RequestHeader(value = "user_id", required = true) Long user_id){ + return ResponseEntity.ok(stockService.createStock(requestDto)); + } + + + /** + * 재고 감소 + */ + @PutMapping("/{productId}/decrease") + public ResponseEntity decreaseStock(@PathVariable UUID productId, + @RequestBody DecreaseStockRequestDto requestDto) { + return ResponseEntity.ok(stockService.decreaseStock( + DecreaseStockServiceRequestDto.of(requestDto, productId))); + } + + + /** + * 재고 증가 + */ + @PutMapping("/{productId}/increase") + public ResponseEntity increaseStock(@PathVariable UUID productId, + @RequestBody IncreaseStockRequestDto requestDto) { + return ResponseEntity.ok(stockService.increaseStock( + IncreaseStockServiceRequestDto.of(requestDto, productId))); + } + +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/CreateStockRequestDto.java b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/CreateStockRequestDto.java new file mode 100644 index 00000000..b2c76b1e --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/CreateStockRequestDto.java @@ -0,0 +1,8 @@ +package com.sparta.stockservice.presentation.dto.request; + +import java.util.UUID; + +public record CreateStockRequestDto(UUID productId, + UUID hubId, + Integer quantity) { +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/DecreaseStockRequestDto.java b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/DecreaseStockRequestDto.java new file mode 100644 index 00000000..975863f9 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/DecreaseStockRequestDto.java @@ -0,0 +1,11 @@ +package com.sparta.stockservice.presentation.dto.request; + +import java.util.UUID; + +public record DecreaseStockRequestDto(UUID companyId, + UUID hubId, + Integer quantity +) { + + +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/IncreaseStockRequestDto.java b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/IncreaseStockRequestDto.java new file mode 100644 index 00000000..735fb3d1 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/request/IncreaseStockRequestDto.java @@ -0,0 +1,11 @@ +package com.sparta.stockservice.presentation.dto.request; + +import java.util.UUID; + +public record IncreaseStockRequestDto(UUID companyId, + UUID hubId, + Integer quantity +) { + + +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/CreateStockResponseDto.java b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/CreateStockResponseDto.java new file mode 100644 index 00000000..5c354347 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/CreateStockResponseDto.java @@ -0,0 +1,24 @@ +package com.sparta.stockservice.presentation.dto.response; + + +import com.sparta.stockservice.domain.model.Stock; +import lombok.Builder; + +import java.util.UUID; + +@Builder +public record CreateStockResponseDto(UUID id, + UUID productId, + UUID hubId, + Integer quantity) { + + // Entity -> DTO 변환 메서드 + public static CreateStockResponseDto from(Stock stock) { + return CreateStockResponseDto.builder() + .id(stock.getId()) + .productId(stock.getProductId()) + .hubId(stock.getHubId()) + .quantity(stock.getQuantity()) + .build(); + } +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/DecreaseStockResponseDto.java b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/DecreaseStockResponseDto.java new file mode 100644 index 00000000..a15f1cf4 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/DecreaseStockResponseDto.java @@ -0,0 +1,32 @@ +package com.sparta.stockservice.presentation.dto.response; + +import lombok.Builder; + +import java.util.UUID; + +@Builder +public record DecreaseStockResponseDto(UUID productId, // 재고 감소 상품 + Boolean isSuccess, // 재고 감소 성공 여부 + Integer decreasedQuantity, // 실제 차감된 수량 + String message) { // 성공 or 실패 메세지 + // Entity -> DTO 변환 메서드 + // 성공 응답 + public static DecreaseStockResponseDto success(UUID productId, int decreasedQuantity) { + return DecreaseStockResponseDto.builder() + .productId(productId) + .isSuccess(true) + .decreasedQuantity(decreasedQuantity) + .message("재고 감소 성공.") + .build(); + } + + // 실패 응답 + public static DecreaseStockResponseDto failure(UUID productId) { + return DecreaseStockResponseDto.builder() + .productId(productId) + .isSuccess(false) + .decreasedQuantity(0) + .message("재고 감소 실패.") + .build(); + } +} diff --git a/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/IncreaseStockResponseDto.java b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/IncreaseStockResponseDto.java new file mode 100644 index 00000000..410b1744 --- /dev/null +++ b/stock-service/src/main/java/com/sparta/stockservice/presentation/dto/response/IncreaseStockResponseDto.java @@ -0,0 +1,32 @@ +package com.sparta.stockservice.presentation.dto.response; + +import lombok.Builder; + +import java.util.UUID; + +@Builder +public record IncreaseStockResponseDto(UUID productId, // 재고 증가 상품 + Boolean isSuccess, // 재고 증가 성공 여부 + Integer increasedQuantity, // 실제 증가된 수량 + String message) { // 성공 or 실패 메세지 + // Entity -> DTO 변환 메서드 + // 성공 응답 + public static IncreaseStockResponseDto success(UUID productId, int increasedQuantity) { + return IncreaseStockResponseDto.builder() + .productId(productId) + .isSuccess(true) + .increasedQuantity(increasedQuantity) + .message("재고 증가 성공.") + .build(); + } + + // 실패 응답 + public static IncreaseStockResponseDto failure(UUID productId) { + return IncreaseStockResponseDto.builder() + .productId(productId) + .isSuccess(false) + .increasedQuantity(0) + .message("재고 증가 실패.") + .build(); + } +} diff --git a/stock-service/src/main/resources/application.yml b/stock-service/src/main/resources/application.yml new file mode 100644 index 00000000..8903c3c2 --- /dev/null +++ b/stock-service/src/main/resources/application.yml @@ -0,0 +1,39 @@ +spring: + application: + name: stock-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: stock_service + dialect: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: update + show-sql: true + +server: + port: 8089 + +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: /stock-service/v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html diff --git a/stock-service/src/test/java/com/sparta/stockservice/StockServiceApplicationTests.java b/stock-service/src/test/java/com/sparta/stockservice/StockServiceApplicationTests.java new file mode 100644 index 00000000..5e480733 --- /dev/null +++ b/stock-service/src/test/java/com/sparta/stockservice/StockServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.sparta.stockservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StockServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/user-service/Dockerfile.dev b/user-service/Dockerfile.dev new file mode 100644 index 00000000..a33494a1 --- /dev/null +++ b/user-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", ":user-service:bootRun"] diff --git a/user-service/build.gradle b/user-service/build.gradle new file mode 100644 index 00000000..b800c01f --- /dev/null +++ b/user-service/build.gradle @@ -0,0 +1,25 @@ +ext { + set('springCloudVersion', "2024.0.0") +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'io.jsonwebtoken:jjwt:0.12.6' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'io.projectreactor:reactor-test' + runtimeOnly 'org.postgresql:postgresql' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation project(':common-module') +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} +test { + useJUnitPlatform() // JUnit 5 사용 시 필요 +} diff --git a/user-service/src/main/java/com/sparta/user/UserServiceApplication.java b/user-service/src/main/java/com/sparta/user/UserServiceApplication.java new file mode 100644 index 00000000..973fce6e --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/UserServiceApplication.java @@ -0,0 +1,17 @@ +package com.sparta.user; + +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; + +@SpringBootApplication(scanBasePackages = "com.sparta") +@Import({SwaggerConfig.class, JpaAuditingConfig.class}) +public class UserServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(UserServiceApplication.class, args); + } + +} diff --git a/user-service/src/main/java/com/sparta/user/application/dto/request/UserSigninReqeustDto.java b/user-service/src/main/java/com/sparta/user/application/dto/request/UserSigninReqeustDto.java new file mode 100644 index 00000000..8200cd98 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/application/dto/request/UserSigninReqeustDto.java @@ -0,0 +1,23 @@ +package com.sparta.user.application.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class UserSigninReqeustDto { + + //최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9) + @NotBlank + @Pattern(regexp = "^(?=.*[a-z])(?=.*[0-9])[a-z0-9^\\s]{4,10}$", + message = "회원이름은 알파벳 소문자와 숫자로 이루어진 4자 이상 10자 이하로 입력해주세요.") + private String username; + + //최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9), 특수문자 + @NotBlank + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d\\s])[^\\s]{8,15}$", + message = "비밀번호는 알파벳 대소문자, 숫자, 특수문자를 포함한 8자 이상 15자 이하입니다.") + private String password; + +} diff --git a/user-service/src/main/java/com/sparta/user/application/dto/request/UserSignupRequestDto.java b/user-service/src/main/java/com/sparta/user/application/dto/request/UserSignupRequestDto.java new file mode 100644 index 00000000..e34392b2 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/application/dto/request/UserSignupRequestDto.java @@ -0,0 +1,49 @@ +package com.sparta.user.application.dto.request; + +import com.sparta.user.domain.model.User; +import com.sparta.user.domain.model.UserRoleEnum; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor +public class UserSignupRequestDto { + + //최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9) + @NotBlank + @Pattern(regexp = "^(?=.*[a-z])(?=.*[0-9])[a-z0-9^\\s]{4,10}$", + message = "회원이름은 알파벳 소문자와 숫자로 이루어진 4자 이상 10자 이하로 입력해주세요.") + private String username; + + //최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9), 특수문자 + @NotBlank + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d\\s])[^\\s]{8,15}$", + message = "비밀번호는 알파벳 대소문자, 숫자, 특수문자를 포함한 8자 이상 15자 이하입니다.") + private String password; + + @Email(message = "유효한 이메일 주소를 입력하세요.") + @NotBlank(message = "이메일은 필수 입력값입니다.") + @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,25}$", message = "유효한 이메일 주소를 입력하세요.") + private String email; + + @NotBlank(message = "슬랙이름은 필수 입력값입니다.") + private String slackName; + + private String tokenValue; + + public User createUser(String password, UserRoleEnum role){ + return User.builder() + .username(username) + .password(password) + .email(email) + .slackName(slackName) + .role(role.getAuthority()) + .build(); + } + + +} + diff --git a/user-service/src/main/java/com/sparta/user/application/dto/request/UserUpdateRequestDto.java b/user-service/src/main/java/com/sparta/user/application/dto/request/UserUpdateRequestDto.java new file mode 100644 index 00000000..4c545b60 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/application/dto/request/UserUpdateRequestDto.java @@ -0,0 +1,37 @@ +package com.sparta.user.application.dto.request; + +import com.sparta.user.domain.model.User; +import com.sparta.user.domain.model.UserRoleEnum; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor +public class UserUpdateRequestDto { + + //최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9) + @Pattern(regexp = "^(?=.*[a-z])(?=.*[0-9])[a-z0-9^\\s]{4,10}$", + message = "회원이름은 알파벳 소문자와 숫자로 이루어진 4자 이상 10자 이하로 입력해주세요.") + private String username; + + //최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9), 특수문자 + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d\\s])[^\\s]{8,15}$", + message = "비밀번호는 알파벳 대소문자, 숫자, 특수문자를 포함한 8자 이상 15자 이하입니다.") + private String password; + + @Email(message = "유효한 이메일 주소를 입력하세요.") + @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,25}$", message = "유효한 이메일 주소를 입력하세요.") + private String email; + + private String slackName; + + private String tokenValue; + + +} + diff --git a/user-service/src/main/java/com/sparta/user/application/dto/response/UserInfoResponseDto.java b/user-service/src/main/java/com/sparta/user/application/dto/response/UserInfoResponseDto.java new file mode 100644 index 00000000..ee4c62fa --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/application/dto/response/UserInfoResponseDto.java @@ -0,0 +1,23 @@ +package com.sparta.user.application.dto.response; + +import com.sparta.user.domain.model.User; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class UserInfoResponseDto { + private String username; + private String email; + private String slackName; + private String role; + + public UserInfoResponseDto(User user) { + this.username = user.getUsername(); + this.email = user.getEmail(); + this.slackName = user.getSlackName(); + this.role = user.getRole(); + } +} diff --git a/user-service/src/main/java/com/sparta/user/application/dto/response/UserSigninResponseDto.java b/user-service/src/main/java/com/sparta/user/application/dto/response/UserSigninResponseDto.java new file mode 100644 index 00000000..cf0db502 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/application/dto/response/UserSigninResponseDto.java @@ -0,0 +1,18 @@ +package com.sparta.user.application.dto.response; + +import com.sparta.user.domain.model.User; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class UserSigninResponseDto { + private Long id; + private String role; + private String slackName; + + public UserSigninResponseDto(User user) { + this.id = user.getId(); + this.role = user.getRole(); + this.slackName = user.getSlackName(); + } +} diff --git a/user-service/src/main/java/com/sparta/user/application/service/AuthService.java b/user-service/src/main/java/com/sparta/user/application/service/AuthService.java new file mode 100644 index 00000000..cf56406b --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/application/service/AuthService.java @@ -0,0 +1,46 @@ +package com.sparta.user.application.service; + +import com.sparta.user.application.dto.response.UserSigninResponseDto; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.sql.Date; + +@Service +public class AuthService { + @Value("${spring.application.name}") + private String issuer; + + @Value("${service.jwt.access-expiration}") + private Long accessExpiration; + + private final SecretKey secretKey; + + public AuthService(@Value("${service.jwt.secret-key}") String secretKey) { + this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey)); + } + + public String createAccessToken(UserSigninResponseDto responseDto) { + return Jwts.builder() + // 사용자 ID를 클레임으로 설정 + .claim("user_id", responseDto.getId().toString()) + .claim("role", responseDto.getRole()) + .claim("slack_name", responseDto.getSlackName()) + // JWT 발행자를 설정(모듈명) + .issuer(issuer) + // JWT 발행 시간을 현재 시간으로 설정 + .issuedAt(new Date(System.currentTimeMillis())) + // JWT 만료 시간을 설정 + .expiration(new Date(System.currentTimeMillis() + accessExpiration)) + // SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명 + .signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512) + // JWT 문자열로 컴팩트하게 변환 + .compact(); + } + + +} diff --git a/user-service/src/main/java/com/sparta/user/application/service/UserService.java b/user-service/src/main/java/com/sparta/user/application/service/UserService.java new file mode 100644 index 00000000..24cfde4f --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/application/service/UserService.java @@ -0,0 +1,108 @@ +package com.sparta.user.application.service; + +import com.sparta.user.application.dto.request.UserSigninReqeustDto; +import com.sparta.user.application.dto.request.UserUpdateRequestDto; +import com.sparta.user.application.dto.response.UserInfoResponseDto; +import com.sparta.user.application.dto.response.UserSigninResponseDto; +import com.sparta.user.application.dto.request.UserSignupRequestDto; +import com.sparta.user.domain.model.User; +import com.sparta.user.domain.model.UserRoleEnum; +import com.sparta.user.infastructure.configuration.AuthConfig; +import com.sparta.user.infastructure.repository.JpaUserRepository; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang.NullArgumentException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.naming.AuthenticationException; + +@Service +@RequiredArgsConstructor +@Transactional +public class UserService { + + private final JpaUserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final AuthConfig authConfig; + private final AuthService authService; + + //회원가입 + public Long signUp(UserSignupRequestDto requestDto) throws IllegalAccessException { + validDuplicatedNames(requestDto); + UserRoleEnum role = checkUserRole(requestDto.getTokenValue()); + User user = requestDto.createUser(encryptPassword(requestDto.getPassword()), role); + user.setting(userRepository.save(user).getId()); + return userRepository.save(user).getId(); + } + //로그인 + public String signIn(UserSigninReqeustDto reqeustDto) throws AuthenticationException { + User user = userRepository.findByUsername(reqeustDto.getUsername()) + .orElseThrow(()-> new AuthenticationException("아이디가 존재하지않습니다.")); + passwordMatchChecker(reqeustDto,user); + UserSigninResponseDto responseDto = new UserSigninResponseDto(user); + return authService.createAccessToken(responseDto); + } + //회원정보 조회(관리자) + + //회원정보 조회(본인) + @Transactional(readOnly = true) + public UserInfoResponseDto getUserInfo(Long userId) { + User user = findUserInfo(userId); + return new UserInfoResponseDto(user); + } + + //회원정보 수정 + public void updateUser(UserUpdateRequestDto requestDto, Long userId) { + User user = findUserInfo(userId); + UserRoleEnum userRole = checkUserRole(requestDto.getTokenValue()); + user.updateUser(encryptPassword(requestDto.getPassword()), requestDto, userRole.getAuthority()); + } + //회원정보 삭제 + public void deleteUser(Long userId) { + User user = findUserInfo(userId); + user.delete(userId); + } + + //비밀번호 인증 + private void passwordMatchChecker(UserSigninReqeustDto reqeustDto, User user) throws AuthenticationException { + boolean pwcheck = passwordEncoder.matches(reqeustDto.getPassword(), user.getPassword()); + if(!pwcheck){ + throw new AuthenticationException("아이디나 비밀번호가 일치하지않습니다.");// 일치하지않는 부분 특정방지 + } + } + //권한 키 체크 + private UserRoleEnum checkUserRole(String tokenValue) { + if (authConfig.getMasterKey().equals(tokenValue)) { + return UserRoleEnum.MASTER; + } else if (authConfig.getHubKey().equals(tokenValue)) { + return UserRoleEnum.HUB; + } else if (authConfig.getShippingKey().equals(tokenValue)) { + return UserRoleEnum.SHIPPING; + } else { + return UserRoleEnum.COMPANY; + } + } + //비밀번호 암호화 + private String encryptPassword (String password) { + if(password == null){ + throw new NullArgumentException("비밀번호를 입력해주세요"); + } + return passwordEncoder.encode(password); + } + //중복이름방지 + private void validDuplicatedNames(UserSignupRequestDto requestDto) throws IllegalAccessException { + boolean exsist = userRepository.existsByUsername(requestDto.getUsername()); + if(exsist){ + throw new IllegalAccessException("Username or Email or SlackName is already taken."); + } + } + //회원 존재여부 + private User findUserInfo(Long userId) { + return userRepository.findById(userId) + .orElseThrow(()->new IllegalArgumentException("존재하지않는 회원입니다.")); + } + + +} diff --git a/user-service/src/main/java/com/sparta/user/domain/model/User.java b/user-service/src/main/java/com/sparta/user/domain/model/User.java new file mode 100644 index 00000000..e9669502 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/domain/model/User.java @@ -0,0 +1,74 @@ +package com.sparta.user.domain.model; + +import com.sparta.commonmodule.entity.BaseEntity; +import com.sparta.user.application.dto.request.UserSignupRequestDto; +import com.sparta.user.application.dto.request.UserUpdateRequestDto; +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; + +@Getter +@NoArgsConstructor +@Table(name = "p_users") +@Entity +@SQLRestriction("is_deleted IS FALSE") +public class User extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id", nullable = false) + private Long id; + + @Column(length = 10, nullable = false) + private String username; + + @Column(nullable = false) + private String password; + + @Column(length = 30, nullable = false) + private String email; + + @Column(name = "slack_name", length = 30) + private String slackName; + + @Column(nullable = false) + private String role; + + + @Builder + public User(String username, String password, String email, String slackName, String role) { + this.username = username; + this.password = password; + this.email = email; + this.slackName = slackName; + this.role = role; + } + + @Builder + public User(String username, String password, String email, String slackName,Long userId) { + this.username = username; + this.password = password; + this.email = email; + this.slackName = slackName; + setCreatedBy(userId); + } + + + public void updateUser(String encryptedpassword, UserUpdateRequestDto requestDto, String userRole) { + Optional.ofNullable(requestDto.getUsername()).ifPresent(username -> this.username = username); + Optional.ofNullable(encryptedpassword).ifPresent(password -> this.password = encryptedpassword); + Optional.ofNullable(requestDto.getEmail()).ifPresent(email -> this.email = email); + Optional.ofNullable(requestDto.getSlackName()).ifPresent(slackName -> this.slackName = slackName); + Optional.ofNullable(userRole).ifPresent(role -> this.role = userRole); + } + + public void setting(Long userId) { + setCreatedBy(userId); + setUpdatedBy(userId); + } +} diff --git a/user-service/src/main/java/com/sparta/user/domain/model/UserRoleEnum.java b/user-service/src/main/java/com/sparta/user/domain/model/UserRoleEnum.java new file mode 100644 index 00000000..93eb235d --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/domain/model/UserRoleEnum.java @@ -0,0 +1,16 @@ +package com.sparta.user.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UserRoleEnum { + MASTER("ROLE_MASTER"), // 마스터 권한 + HUB("ROLE_HUB"), // 허브 권한 + SHIPPING("ROLE_SHIPPING"), // 배송 권한 + COMPANY("ROLE_COMPANY"); // 업체 권한(디폴트) + + + private final String authority; +} diff --git a/user-service/src/main/java/com/sparta/user/domain/repository/UserRepository.java b/user-service/src/main/java/com/sparta/user/domain/repository/UserRepository.java new file mode 100644 index 00000000..c7120040 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/domain/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.sparta.user.domain.repository; + +import com.sparta.user.domain.model.User; + +import java.util.Optional; + +public interface UserRepository { + Optional findByUsername(String username); + + boolean existsByUsername(String username); +} diff --git a/user-service/src/main/java/com/sparta/user/infastructure/configuration/AuthConfig.java b/user-service/src/main/java/com/sparta/user/infastructure/configuration/AuthConfig.java new file mode 100644 index 00000000..e0797fec --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/infastructure/configuration/AuthConfig.java @@ -0,0 +1,16 @@ +package com.sparta.user.infastructure.configuration; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class AuthConfig { + @Value("${service.master.signup-key}") + private String MasterKey; + @Value("${service.hub.signup-key}") + private String HubKey; + @Value("${service.shipping.signup-key}") + private String ShippingKey; +} diff --git a/user-service/src/main/java/com/sparta/user/infastructure/configuration/UserConfig.java b/user-service/src/main/java/com/sparta/user/infastructure/configuration/UserConfig.java new file mode 100644 index 00000000..c2858372 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/infastructure/configuration/UserConfig.java @@ -0,0 +1,14 @@ +package com.sparta.user.infastructure.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class UserConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/user-service/src/main/java/com/sparta/user/infastructure/repository/JpaUserRepository.java b/user-service/src/main/java/com/sparta/user/infastructure/repository/JpaUserRepository.java new file mode 100644 index 00000000..fae1b169 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/infastructure/repository/JpaUserRepository.java @@ -0,0 +1,10 @@ +package com.sparta.user.infastructure.repository; + +import com.sparta.user.domain.model.User; +import com.sparta.user.domain.repository.UserRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface JpaUserRepository extends JpaRepository , UserRepository { +} diff --git a/user-service/src/main/java/com/sparta/user/presentation/UserController.java b/user-service/src/main/java/com/sparta/user/presentation/UserController.java new file mode 100644 index 00000000..760addb3 --- /dev/null +++ b/user-service/src/main/java/com/sparta/user/presentation/UserController.java @@ -0,0 +1,75 @@ +package com.sparta.user.presentation; + + +import com.sparta.user.application.dto.request.UserSigninReqeustDto; +import com.sparta.user.application.dto.request.UserSignupRequestDto; +import com.sparta.user.application.dto.request.UserUpdateRequestDto; +import com.sparta.user.application.dto.response.UserInfoResponseDto; +import com.sparta.user.application.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.naming.AuthenticationException; +import java.net.URI; + +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +@Tag(name = "User Service", description = "사용자 서비스 API") +public class UserController { + private final UserService userService; + + @Operation(summary = "회원가입", description = "화원가입 api입니다.") + @PostMapping("/sign-up") + public ResponseEntity signUp(@Valid @RequestBody UserSignupRequestDto requestDto, BindingResult bindingResult) throws IllegalAccessException { + if (bindingResult.hasErrors()) { + return ResponseEntity.badRequest().build(); + } + Long userId = userService.signUp(requestDto); + URI createdUserUri = UriComponentsBuilder + .fromUriString("/api/v1/users/{id}") + .buildAndExpand(userId) + .toUri(); + + return ResponseEntity.created(createdUserUri).build(); + } + + @PostMapping("/sign-in") + public ResponseEntity signIn(@Valid @RequestBody UserSigninReqeustDto requestDto, + HttpServletResponse httpServletResponse) throws AuthenticationException { + String accessToken= userService.signIn(requestDto); + //HttpHeaders(중요) + httpServletResponse.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + + return ResponseEntity.ok().build(); + } + + @GetMapping + public ResponseEntity getUserInfo(@RequestHeader("user_id") Long userId) { + UserInfoResponseDto userInfoResponseDto = userService.getUserInfo(userId); + return ResponseEntity.ok(userInfoResponseDto); + } + + @PutMapping("/update") + public ResponseEntity updateUser(@Valid @RequestBody UserUpdateRequestDto requestDto, @RequestHeader("user_id") Long userId) { + userService.updateUser(requestDto, userId); + + return ResponseEntity.ok().build(); + } + + @DeleteMapping + public ResponseEntity deleteUser(@RequestHeader("user_id") Long userId) { + userService.deleteUser(userId); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml new file mode 100644 index 00000000..b1e66f93 --- /dev/null +++ b/user-service/src/main/resources/application.yml @@ -0,0 +1,49 @@ +spring: + application: + name: user-service + datasource: + url: jdbc:postgresql://localhost:5001/maindb + username: admin + password: 1234 + driver-class-name: org.postgresql.Driver + jpa: + properties: + hibernate: + default_schema: user_service + dialect: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: create + show-sql: true + h2: + console: + enabled: false # H2 콘솔 비활성화 (PostgreSQL 사용 시) +server: + port: 8081 + +eureka: + client: + register-with-eureka: true # Eureka? ??? ?? + fetch-registry: true # Eureka?? ??? ?? ???? + service-url: + defaultZone: http://localhost:8761/eureka/ + +service: + jwt: + access-expiration: 3600000 # 1시간 뒤 만료 + secret-key: "50OsBNAaYDd8KJkGL8DCy8l8GFeJY--lzgxXrQJA-vUl1ZfivKtLNuwR_qNn2LJ6NkXpg8AAa2fe2CVUtN4UcQ" + master: + signup-key: "mgbE4vogtrMGufz6PXkQNTV-KZtU4-Mz7_wcKf7r40kKTu8z4BD9l_kacdd4MzU3pQV6y3LB-yrmMvvXFKep2Q" + hub: + signup-key: "WS4LLkUhglcrzhFaqWhGXMX70V-ntoQrJdsbT8997aR32veCI2Y35-yyjdHzUzVc4TybaGSdRJ6WYVVcY9K6Uw" + shipping: + signup-key: "QqjxcIpIisVqc2evLWPRyjMya8nCyWiheO5L8SbxwKbJFRjlIkxFaX1l8ShI8GlE1hxw_oOoyrfefEE7ZrUkMQ" + + +springdoc: + api-docs: + version: openapi_3_1 + enabled: true + path: /user-service/v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html \ No newline at end of file diff --git a/user-service/src/test/java/com/sparta/user/UserServiceApplicationTests.java b/user-service/src/test/java/com/sparta/user/UserServiceApplicationTests.java new file mode 100644 index 00000000..11a417b6 --- /dev/null +++ b/user-service/src/test/java/com/sparta/user/UserServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.sparta.user; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/user-service/src/test/java/com/sparta/user/presentation/UserControllerTest.java b/user-service/src/test/java/com/sparta/user/presentation/UserControllerTest.java new file mode 100644 index 00000000..92771988 --- /dev/null +++ b/user-service/src/test/java/com/sparta/user/presentation/UserControllerTest.java @@ -0,0 +1,85 @@ +package com.sparta.user.presentation; + +import com.sparta.user.application.service.UserService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class UserControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserService userService; + + @Test + void signUp() throws Exception { + mockMvc.perform(post("/api/v1/users/sign-up") + .contentType(MediaType.APPLICATION_JSON) + .content("{" + + "\"username\":\"asdfg123\"," + + "\"password\":\"asdf3gA12@\"," + + "\"email\":\"asdafv@naver.com\"," + + "\"slackName\":\"as3dgggv\"" + + "}") + ) + .andExpect(status().isCreated()); + } + + @Test + void signIn() throws Exception { + mockMvc.perform(post("/api/v1/users/sign-in") + .contentType(MediaType.APPLICATION_JSON) + .content("{" + + "\"username\":\"asdfg123\"," + + "\"password\":\"asdf3gA12@\"" + + "}") + ) + .andExpect(status().isOk()) + .andReturn().getResponse().getHeader("Authorization"); + } + + @Test + void getUserInfo() throws Exception { + mockMvc.perform(get("/api/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .header("user_id", 1) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("asdfg123")) + .andExpect(jsonPath("$.email").value("asdafv@naver.com")) + .andExpect(jsonPath("$.slackName").value("as3dgggv")) + .andExpect(jsonPath("$.role").value("ROLE_COMPANY")); + } + + @Test + void updateUser() throws Exception{ + mockMvc.perform(put("/api/v1/users/update") + .contentType(MediaType.APPLICATION_JSON) + .header("user_id", 1) + .content("{" + + "\"username\":\"asdf225s3\"," + + "\"password\":\"asdhdgA12@\"" + + "}") + ) + .andExpect(status().isOk()); + } + + @Test + void deleteUser() throws Exception{ + mockMvc.perform(delete("/api/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .header("user_id", 1) + ) + .andExpect(status().isNoContent()); + } +} \ No newline at end of file