From ec3ba548179f0befdfe1549eee82506566fdac6b Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 11 Feb 2025 12:05:00 +0900 Subject: [PATCH 001/163] =?UTF-8?q?=F0=9F=94=A7config:=20add=20gitignore?= =?UTF-8?q?=20element?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 321 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) diff --git a/.gitignore b/.gitignore index 5788fcae..96855f23 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,324 @@ gradlew.bat # Build artifacts /build/ + +# Created by https://www.toptal.com/developers/gitignore/api/windows,macos,intellij,eclipse,visualstudiocode,redis,gradle,java +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,intellij,eclipse,visualstudiocode,redis,gradle,java + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +### 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 + +# User-specific stuff +.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/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.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/**/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/**/azureSettings.xml + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.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* + +### macOS ### +# General +.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 + +### Redis ### +# Ignore redis binary dump (dump.rdb) files + +*.rdb + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +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 +$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 +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/windows,macos,intellij,eclipse,visualstudiocode,redis,gradle,java + From e4c4d290cd608a5bc3b0ada2c908185fabf08a2c Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 11 Feb 2025 15:24:44 +0900 Subject: [PATCH 002/163] =?UTF-8?q?=F0=9F=91=B7config:=20init=20CI/CD=20pi?= =?UTF-8?q?peline=20with=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/README.md | 92 +++++++++++++++++++++++++++++++ .github/workflows/kok-CI.yml | 72 ++++++++++++++++++++++++ .github/workflows/kok-dev-CD.yml | 36 ++++++++++++ .github/workflows/kok-prod-CD.yml | 79 ++++++++++++++++++++++++++ Dockerfile | 14 +++++ infra/docker-compose-blue.yml | 17 ++++++ infra/docker-compose-dev.yml | 17 ++++++ infra/docker-compose-green.yml | 17 ++++++ infra/docker-compose-nginx.yml | 17 ++++++ infra/nginx/nginx.conf | 23 ++++++++ 10 files changed, 384 insertions(+) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/kok-CI.yml create mode 100644 .github/workflows/kok-dev-CD.yml create mode 100644 .github/workflows/kok-prod-CD.yml create mode 100644 Dockerfile create mode 100644 infra/docker-compose-blue.yml create mode 100644 infra/docker-compose-dev.yml create mode 100644 infra/docker-compose-green.yml create mode 100644 infra/docker-compose-nginx.yml create mode 100644 infra/nginx/nginx.conf diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..115913c7 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,92 @@ +## ๐Ÿ“Œ CI/CD ๊ฐœ์š” +์ด ํ”„๋กœ์ ํŠธ๋Š” **GitHub Actions + Docker + Nginx + Blue-Green Deployment**๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ **์ž๋™ํ™”๋œ CI/CD**๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + +### โœ… CI (Continuous Integration) +- `kock-api`๋ฅผ ๋นŒ๋“œํ•˜๊ณ  Docker ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜์—ฌ **Docker Hub์— ํ‘ธ์‹œ**ํ•ฉ๋‹ˆ๋‹ค. + +### โœ… CD (Continuous Deployment) +| ๋ธŒ๋žœ์น˜ | ๋™์ž‘ | +|--------|-----------------------------------------------| +| `develop` | **๊ฐœ๋ฐœ ์„œ๋ฒ„** ๋ฐฐํฌ (`kok-dev-CD.yml`) | +| `main` | **์šด์˜ ์„œ๋ฒ„ (Blue-Green ๋ฐฐํฌ)** (`kok-prod-CD.yml`) | +| `kok-api` | **API ๋นŒ๋“œ & Docker Push** (`kok-CI.yml`) | + +--- + +## ๐Ÿ“‚ CI/CD ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ ์„ค๋ช… + +| ํŒŒ์ผ๋ช… | ์„ค๋ช… | +|-------------------------|--------------------------| +| `kok-CI.yml` | `kok-api` ๋นŒ๋“œ ๋ฐ Docker ๋ฐฐํฌ | +| `kok-dev-CD.yml` | ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐฐํฌ | +| `kok-prod-CD.yml` | ์šด์˜ ์„œ๋ฒ„ (Blue-Green) ๋ฐฐํฌ | +| `blue-green-Nginx.conf` | Nginx ์„ค์ • (ํŠธ๋ž˜ํ”ฝ ์Šค์œ„์นญ) | + +--- + +## ๐Ÿ”‘ **ํ•„์š”ํ•œ GitHub Secrets ๋ชฉ๋ก** +| ์ด๋ฆ„ | ์„ค๋ช… | +|------|------| +| `DOCKERHUB_USERNAME` | Docker Hub ๋กœ๊ทธ์ธ ID | +| `DOCKERHUB_PASSWORD` | Docker Hub ๋กœ๊ทธ์ธ ๋น„๋ฐ€๋ฒˆํ˜ธ | +| `NCP_HOST` | NCP ์„œ๋ฒ„ ์ฃผ์†Œ (๋ฐฐํฌ ์„œ๋ฒ„ IP) | +| `NCP_USER` | NCP ์„œ๋ฒ„ ๋กœ๊ทธ์ธ ๊ณ„์ • (๋ณดํ†ต `ubuntu` ๋˜๋Š” `root`) | +| `NCP_KEY` | NCP ์„œ๋ฒ„ ์ ‘๊ทผ์„ ์œ„ํ•œ SSH Key (pem ํŒŒ์ผ ๋‚ด์šฉ) | +| `COMPOSE_FILE_PATH` | Docker Compose๊ฐ€ ์‹คํ–‰๋  ์„œ๋ฒ„ ๊ฒฝ๋กœ | +| `IMAGE_NAME` | Docker Hub์— ์—…๋กœ๋“œํ•  ์ด๋ฏธ์ง€๋ช… | + +--- + +## ๐Ÿ› ๏ธ GitHub Actions ์‹คํ–‰ ๋ฐฉ๋ฒ• + +### 1๏ธโƒฃ **์ˆ˜๋™ ์‹คํ–‰ (`workflow_dispatch`)** +GitHub Actions์—์„œ `Run Workflow` ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### 2๏ธโƒฃ **์ž๋™ ์‹คํ–‰ (๋ธŒ๋žœ์น˜ Push)** +| ๋ธŒ๋žœ์น˜ | ์‹คํ–‰๋˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ | +|-----------|------------------------------| +| `*` | `kok-CI.yml` (๋นŒ๋“œ & Docker ๋ฐฐํฌ) | +| `develop` | `kok-dev-CD.yml` (๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐฐํฌ) | +| `main` | `kok-prod-CD.yml` (์šด์˜ ์„œ๋ฒ„ ๋ฐฐํฌ) | + +--- + +## ๐Ÿš€ ์šด์˜(Prod) ๋ฐฐํฌ ํ”„๋กœ์„ธ์Šค (Blue-Green Deployment) +์šด์˜ ๋ฐฐํฌ๋Š” **Blue-Green Deployment** ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ **๋ฌด์ค‘๋‹จ ๋ฐฐํฌ**๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + +### **1๏ธโƒฃ ๋ฐฐํฌ ํŠธ๋ฆฌ๊ฑฐ** +- `main` ๋ธŒ๋žœ์น˜์— ์ฝ”๋“œ๊ฐ€ ํ‘ธ์‹œ๋˜๋ฉด `kok-prod-CD.yml`์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. +- GitHub Actions๊ฐ€ `docker-compose-prod.yml`์„ ์„œ๋ฒ„์— ์—…๋กœ๋“œํ•˜๊ณ  ๋ฐฐํฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + +### **2๏ธโƒฃ ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์„œ๋น„์Šค ํ™•์ธ** +- `nginx.conf`์—์„œ ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ์ปจํ…Œ์ด๋„ˆ(`kok-blue` ๋˜๋Š” `kok-green`์„ ํ™•์ธ)ํ•ฉ๋‹ˆ๋‹ค. + +### **3๏ธโƒฃ ์ƒˆ๋กœ์šด ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰** +- ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํ™˜๊ฒฝ์ด `kok-blue`์ด๋ฉด `kok-green`์„ ์‹คํ–‰ (`docker-compose-green.yml` ์‚ฌ์šฉ). +- ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํ™˜๊ฒฝ์ด `kok-green`์ด๋ฉด `kok-blue`๋ฅผ ์‹คํ–‰ (`docker-compose-blue.yml` ์‚ฌ์šฉ). + +### **4๏ธโƒฃ Health Check** +- ์ƒˆ๋กญ๊ฒŒ ์‹คํ–‰๋œ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ `/health_check` ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ†ตํ•ด ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. +- Health Check ์‹คํŒจ ์‹œ ๋กค๋ฐฑํ•˜์—ฌ ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. + +### **5๏ธโƒฃ ํŠธ๋ž˜ํ”ฝ ์ „ํ™˜** +- `nginx.conf`์˜ `upstream` ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜์—ฌ, ์ƒˆ๋กœ์šด ์ปจํ…Œ์ด๋„ˆ๋กœ ํŠธ๋ž˜ํ”ฝ์„ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. +- `nginx -s reload` ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜์—ฌ Nginx์„ค์ •์„ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. + +### **6๏ธโƒฃ ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ** +- ์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์ด ์ •์ƒ์ ์œผ๋กœ ๋ฐฐํฌ๋œ ํ›„, ์ด์ „ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ค‘์ง€ํ•˜๊ณ  ์‚ญ์ œํ•˜์—ฌ ๋ฆฌ์†Œ์Šค๋ฅผ ์ ˆ์•ฝํ•ฉ๋‹ˆ๋‹ค. + +--- + +## ๐Ÿš€ ๊ฐœ๋ฐœ(Dev) ๋ฐฐํฌ ํ”„๋กœ์„ธ์Šค +๊ฐœ๋ฐœ ๋ฐฐํฌ๋Š” ์šด์˜ ํ™˜๊ฒฝ๋ณด๋‹ค **๊ฐ„๋‹จํ•œ ๋‹จ์ผ ์ปจํ…Œ์ด๋„ˆ ๋ฐฐํฌ** ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค. + +### **1๏ธโƒฃ ๋ฐฐํฌ ํŠธ๋ฆฌ๊ฑฐ** +- `develop` ๋ธŒ๋žœ์น˜์— ์ฝ”๋“œ๊ฐ€ ํ‘ธ์‹œ๋˜๋ฉด `kok-dev-CD.yml`์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. +- GitHub Actions๊ฐ€ `docker-compose-dev.yml`์„ ์„œ๋ฒ„์— ์—…๋กœ๋“œํ•˜๊ณ  ๋ฐฐํฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + +### **2๏ธโƒฃ ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ข…๋ฃŒ** +- ๊ธฐ์กด์— ์‹คํ–‰ ์ค‘์ธ `kok-dev` ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ค‘์ง€ (`docker compose down` ์‹คํ–‰). + +### **3๏ธโƒฃ ์ƒˆ๋กœ์šด ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰** +- `docker compose -f docker-compose-dev.yml up -d` ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ƒˆ๋กœ์šด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml new file mode 100644 index 00000000..1c6d0882 --- /dev/null +++ b/.github/workflows/kok-CI.yml @@ -0,0 +1,72 @@ +name: kok-CI (Build & Push for kok) + +on: + push: + branches: + - main + - develop + - kok-api + - kok-core + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: '21' + + - name: Determine Image Tag (prod/dev) + id: determine-tag + run: | + if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then + echo "IMAGE_TAG=_prod" >> $GITHUB_ENV + else + echo "IMAGE_TAG=_dev" >> $GITHUB_ENV + fi + + - name: Build with Gradle (๋ฉ€ํ‹ฐ๋ชจ๋“ˆ) + run: | + ./gradlew clean build -x test + + - name: Save Build Artifacts + uses: actions/upload-artifact@v3 + with: + name: build-artifact + path: kok-api/build/libs/*.jar + + docker: + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Download Build Artifacts + uses: actions/download-artifact@v3 + with: + name: build-artifact + path: kok-api/build/libs/ + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build & Push Docker Image + run: | + IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok${{ env.IMAGE_TAG }}" + + echo "Building Image: $IMAGE_NAME" + docker build -t $IMAGE_NAME:${{ github.sha }} -f Dockerfile . + docker push $IMAGE_NAME:${{ github.sha }} + + - name: Notify Deployment Trigger + run: | + echo "๐Ÿš€ Docker ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ! Image: $IMAGE_NAME" diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml new file mode 100644 index 00000000..3e61afcf --- /dev/null +++ b/.github/workflows/kok-dev-CD.yml @@ -0,0 +1,36 @@ +name: kok-dev-CD (Deploy to Dev) + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Create SSH key file + run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem + + - name: Set permissions for SSH key file + run: chmod 600 /tmp/NCP_KEY.pem + + - name: Upload `docker-compose.yml` to NCP + run: | + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-dev.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + + - name: Deploy to Dev Server + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.NCP_HOST }} + username: ${{ secrets.NCP_USER }} + key: ${{ secrets.NCP_KEY }} + script: | + cd ${{ secrets.COMPOSE_FILE_PATH }} + sudo docker compose pull + sudo docker compose down + sudo docker compose -f docker-compose-dev.yml up -d \ No newline at end of file diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml new file mode 100644 index 00000000..a374a80d --- /dev/null +++ b/.github/workflows/kok-prod-CD.yml @@ -0,0 +1,79 @@ +name: kok-prod-CD (Deploy to Prod - Blue-Green Auto) + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Create SSH key file + run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem + + - name: Set permissions for SSH key file + run: chmod 600 /tmp/NCP_KEY.pem + + - name: Upload `docker-compose.yml` & `nginx.conf` to NCP + run: | + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-prod.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/nginx/nginx.conf ${{ secrets.NCP_USER }}@${{ secrets.NCP_HOST }}:${{ secrets.COMPOSE_FILE_PATH }}/nginx.conf + + - name: Deploy to NCP (Blue-Green Auto Switch) + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.NCP_HOST }} + username: ${{ secrets.NCP_USER }} + key: ${{ secrets.NCP_KEY }} + script: | + cd ${{ secrets.COMPOSE_FILE_PATH }} + + # ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Blue/Green ํ™˜๊ฒฝ ๊ฐ์ง€ + CURRENT_ENV=$(grep -o 'server kok-[a-z]\+' nginx.conf | awk '{print $2}') + echo "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํ™˜๊ฒฝ: $CURRENT_ENV" + + if [[ "$CURRENT_ENV" == "kok-blue" ]]; then + NEW_ENV="kok-green" + OLD_ENV="kok-blue" + COMPOSE_FILE="docker-compose-green.yml" + NEW_PORT=8082 + else + NEW_ENV="kok-blue" + OLD_ENV="kok-green" + COMPOSE_FILE="docker-compose-blue.yml" + NEW_PORT=8081 + fi + + echo "์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์œผ๋กœ ๋ฐฐํฌ: $NEW_ENV" + + # ์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์— ์ปจํ…Œ์ด๋„ˆ ๋ฐฐํฌ + sudo docker compose -f $COMPOSE_FILE pull + sudo docker compose -f $COMPOSE_FILE up -d + + # Health Check ์‹คํ–‰ + echo "๐Ÿš€ Health Check ์‹คํ–‰ ์ค‘..." + sleep 30 + if ! curl -s http://localhost:$NEW_PORT/health_check | grep "Success"; then + echo "๐Ÿšจ Health Check ์‹คํŒจ! ๋กค๋ฐฑ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค." + sudo docker compose stop $NEW_ENV + sudo docker compose rm -f $NEW_ENV + exit 1 + fi + + # โœ… Nginx ์„ค์ • ํŒŒ์ผ ์ž๋™ ์ˆ˜์ • (ํŠธ๋ž˜ํ”ฝ์„ ์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์œผ๋กœ ๋ณ€๊ฒฝ) + echo "โœ… ํŠธ๋ž˜ํ”ฝ์„ $NEW_ENV๋กœ ๋ณ€๊ฒฝ ์ค‘..." + sudo sed -i "s/server $OLD_ENV:8080/server $NEW_ENV:8080/" nginx.conf + + # โœ… ๋ฌด์ค‘๋‹จ์œผ๋กœ Nginx ์„ค์ • ๋ฐ˜์˜ + echo "๐Ÿ”„ Nginx ์„ค์ •์„ ๋ฌด์ค‘๋‹จ์œผ๋กœ ์ ์šฉ..." + sudo docker exec kok-nginx nginx -s reload + + # โœ… ๊ธฐ์กด ํ™˜๊ฒฝ ์ค‘์ง€ ๋ฐ ์ •๋ฆฌ + echo "๐Ÿ›‘ ๊ธฐ์กด ํ™˜๊ฒฝ($OLD_ENV) ์ค‘์ง€..." + sudo docker compose stop $OLD_ENV + sudo docker compose rm -f $OLD_ENV \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..37a35c93 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# 1. Base Image ์„ค์ • +FROM openjdk:21-jdk + +# 2. ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • +WORKDIR /app + +# 3. JAR ํŒŒ์ผ ๋ณต์‚ฌ (GitHub Actions์—์„œ ๋นŒ๋“œ๋œ ๊ฒฐ๊ณผ๋ฌผ ์‚ฌ์šฉ) +COPY kok-api/build/libs/*.jar app.jar + +# 4. ๊ธฐ๋ณธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • (์™ธ๋ถ€์—์„œ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ) +ENV SPRING_PROFILES_ACTIVE=dev + +# 5. ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ๋ช…๋ น์–ด +CMD ["java", "-jar", "app.jar"] diff --git a/infra/docker-compose-blue.yml b/infra/docker-compose-blue.yml new file mode 100644 index 00000000..cfe4a707 --- /dev/null +++ b/infra/docker-compose-blue.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + kok-blue: + container_name: kok-blue + image: ${DOCKERHUB_USERNAME}/kok_prod:latest + restart: always + environment: + - SPRING_PROFILES_ACTIVE=prod + ports: + - "8081:8080" + networks: + - kok-network + +networks: + kok-network: + driver: bridge diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml new file mode 100644 index 00000000..842f4f10 --- /dev/null +++ b/infra/docker-compose-dev.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + kok-dev: + container_name: kok-dev + image: ${DOCKERHUB_USERNAME}/kok_dev:latest + restart: always + environment: + - SPRING_PROFILES_ACTIVE=dev + ports: + - "8080:8080" + networks: + - kok-network + +networks: + kok-network: + driver: bridge diff --git a/infra/docker-compose-green.yml b/infra/docker-compose-green.yml new file mode 100644 index 00000000..9e8890d7 --- /dev/null +++ b/infra/docker-compose-green.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + kok-green: + container_name: kok-green + image: ${DOCKERHUB_USERNAME}/kok_prod:latest + restart: always + environment: + - SPRING_PROFILES_ACTIVE=prod + ports: + - "8082:8080" + networks: + - kok-network + +networks: + kok-network: + driver: bridge diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml new file mode 100644 index 00000000..065b1c59 --- /dev/null +++ b/infra/docker-compose-nginx.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + nginx: + container_name: kok-nginx + image: nginx:latest + restart: always + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "80:80" + networks: + - kok-network + +networks: + kok-network: + driver: bridge diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf new file mode 100644 index 00000000..ae67e087 --- /dev/null +++ b/infra/nginx/nginx.conf @@ -0,0 +1,23 @@ +events { + worker_connections 1024; +} + +http { + upstream backend { + server kok-blue:8080; + # ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ์—์„œ 'server kok-green:8080'์œผ๋กœ ๋ณ€๊ฒฝ๋จ + } + + server { + listen 80; + + location / { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} + From e74827449bebdbc6c131c1fa9becc0eeab69fda3 Mon Sep 17 00:00:00 2001 From: yunyoung1819 Date: Tue, 11 Feb 2025 23:54:26 +0900 Subject: [PATCH 003/163] :wrench: config: "add missing gradle file" --- gradlew | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++ gradlew.bat | 94 ++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100755 gradlew create mode 100644 gradlew.bat diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright ยฉ 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป, +# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป; +# * compound commands having a testable exit status, especially ยซcaseยป; +# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..9d21a218 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From ea4d3bf3e416a31583c47b561c3c45d658246336 Mon Sep 17 00:00:00 2001 From: yunyoung1819 Date: Wed, 12 Feb 2025 00:06:27 +0900 Subject: [PATCH 004/163] :wrench: config: "include gradle-wrapper.properties" --- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..a4b76b9530d66f5e68d973ea569d8e19de379189 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..9355b415 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists From 7ed1b7926608ba56334d177ae9956e65f8869e55 Mon Sep 17 00:00:00 2001 From: yunyoung1819 Date: Thu, 13 Feb 2025 18:14:24 +0900 Subject: [PATCH 005/163] :sparkles: feat: add common api response code and common error code --- .../common/response/ApiResponseDto.java | 26 +++++++++++++ .../java/com/kok/kokcore/error/ErrorCode.java | 39 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 22 +++++++++++ 3 files changed, 87 insertions(+) create mode 100644 kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/exception/GlobalExceptionHandler.java diff --git a/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java b/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java new file mode 100644 index 00000000..33c330b2 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java @@ -0,0 +1,26 @@ +package com.kok.kokcore.common.response; + +import com.kok.kokcore.error.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public class ApiResponseDto { + private final int code; + private final String message; + private final T data; + + public static ApiResponseDto success(T data) { + return new ApiResponseDto<>(HttpStatus.OK.value(), "Success", data); + } + + public static ApiResponseDto error(ErrorCode errorCode) { + return new ApiResponseDto<>(errorCode.getStatus().value(), errorCode.getMessage(), null); + } + + public static ApiResponseDto error(ErrorCode errorCode, String customMessage) { + return new ApiResponseDto<>(errorCode.getStatus().value(), customMessage, null); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java b/kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java new file mode 100644 index 00000000..74605ca0 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java @@ -0,0 +1,39 @@ +package com.kok.kokcore.error; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + // 400 BAD REQUEST + BAD_REQUEST(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."), + INVALID_INPUT(HttpStatus.BAD_REQUEST, "์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + EXCEEDED_USAGE_LIMIT(HttpStatus.BAD_REQUEST, "ํ˜ธ์ถœ ๊ฐ€๋Šฅ ํšŸ์ˆ˜๊ฐ€ ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + REQUEST_EXPIRED(HttpStatus.BAD_REQUEST, "์š”์ฒญ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + UNSUPPORTED_OPERATION(HttpStatus.BAD_REQUEST, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค."), + + // 401 UNAUTHORIZED + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + + // 403 FORBIDDEN + FORBIDDEN(HttpStatus.FORBIDDEN, "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."), + + // 404 NOT FOUND + RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + + // 409 CONFLICT + CONFLICT(HttpStatus.CONFLICT, "๋ฐ์ดํ„ฐ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + + // 500 INTERNAL SERVER ERROR + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "ํ˜„์žฌ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + + private final HttpStatus status; + private final String message; +} diff --git a/kok-core/src/main/java/com/kok/kokcore/exception/GlobalExceptionHandler.java b/kok-core/src/main/java/com/kok/kokcore/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..ea4878a6 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/exception/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.kok.kokcore.exception; + +import com.kok.kokcore.common.response.ApiResponseDto; +import com.kok.kokcore.error.ErrorCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleBadRequestException(IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, ex.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGlobalException(Exception ex) { + return ResponseEntity.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + .body(ApiResponseDto.error(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage())); + } +} From f304651b76568295b22f414e7673f53d806c3c9b Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 19:45:40 +0900 Subject: [PATCH 006/163] =?UTF-8?q?=F0=9F=93=9Ddocs:=20update=20worflow=20?= =?UTF-8?q?readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/README.md | 45 ++++++++++++++++++++----------------- .gitignore | 1 + 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 115913c7..242da7a3 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -2,25 +2,26 @@ ์ด ํ”„๋กœ์ ํŠธ๋Š” **GitHub Actions + Docker + Nginx + Blue-Green Deployment**๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ **์ž๋™ํ™”๋œ CI/CD**๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ### โœ… CI (Continuous Integration) -- `kock-api`๋ฅผ ๋นŒ๋“œํ•˜๊ณ  Docker ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜์—ฌ **Docker Hub์— ํ‘ธ์‹œ**ํ•ฉ๋‹ˆ๋‹ค. +| ๋ธŒ๋žœ์น˜ | ๋™์ž‘ | +|----------------------------------|-------------------------------------------| +| `feature/*, bugfix/*, hotfix/*` | **๋นŒ๋“œ & Docker Push** (`kok-CI.yml`) | ### โœ… CD (Continuous Deployment) -| ๋ธŒ๋žœ์น˜ | ๋™์ž‘ | -|--------|-----------------------------------------------| -| `develop` | **๊ฐœ๋ฐœ ์„œ๋ฒ„** ๋ฐฐํฌ (`kok-dev-CD.yml`) | -| `main` | **์šด์˜ ์„œ๋ฒ„ (Blue-Green ๋ฐฐํฌ)** (`kok-prod-CD.yml`) | -| `kok-api` | **API ๋นŒ๋“œ & Docker Push** (`kok-CI.yml`) | +| ๋ธŒ๋žœ์น˜ | ๋™์ž‘ | +|-----------|-------------------------------------------| +| `develop` | **๊ฐœ๋ฐœ ์„œ๋ฒ„** ๋ฐฐํฌ (`kok-dev-CD.yml`) | +| `main` | **์šด์˜ ์„œ๋ฒ„ (Blue-Green ๋ฐฐํฌ)** (`kok-prod-CD.yml`) | --- ## ๐Ÿ“‚ CI/CD ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ ์„ค๋ช… -| ํŒŒ์ผ๋ช… | ์„ค๋ช… | -|-------------------------|--------------------------| -| `kok-CI.yml` | `kok-api` ๋นŒ๋“œ ๋ฐ Docker ๋ฐฐํฌ | -| `kok-dev-CD.yml` | ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐฐํฌ | -| `kok-prod-CD.yml` | ์šด์˜ ์„œ๋ฒ„ (Blue-Green) ๋ฐฐํฌ | -| `blue-green-Nginx.conf` | Nginx ์„ค์ • (ํŠธ๋ž˜ํ”ฝ ์Šค์œ„์นญ) | +| ํŒŒ์ผ๋ช… | ์„ค๋ช… | +|-------------------------|--------------------| +| `kok-CI.yml` | ๋นŒ๋“œ ๋ฐ Docker ๋ฐฐํฌ | +| `kok-dev-CD.yml` | ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐฐํฌ | +| `kok-prod-CD.yml` | ์šด์˜ ์„œ๋ฒ„ (Blue-Green) ๋ฐฐํฌ | +| `blue-green-Nginx.conf` | Nginx ์„ค์ • (ํŠธ๋ž˜ํ”ฝ ์Šค์œ„์นญ) | --- @@ -39,15 +40,19 @@ ## ๐Ÿ› ๏ธ GitHub Actions ์‹คํ–‰ ๋ฐฉ๋ฒ• -### 1๏ธโƒฃ **์ˆ˜๋™ ์‹คํ–‰ (`workflow_dispatch`)** +### 1๏ธโƒฃ **์ˆ˜๋™ ์‹คํ–‰ for Prod(`workflow_dispatch`)** GitHub Actions์—์„œ `Run Workflow` ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -### 2๏ธโƒฃ **์ž๋™ ์‹คํ–‰ (๋ธŒ๋žœ์น˜ Push)** -| ๋ธŒ๋žœ์น˜ | ์‹คํ–‰๋˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ | -|-----------|------------------------------| -| `*` | `kok-CI.yml` (๋นŒ๋“œ & Docker ๋ฐฐํฌ) | -| `develop` | `kok-dev-CD.yml` (๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐฐํฌ) | -| `main` | `kok-prod-CD.yml` (์šด์˜ ์„œ๋ฒ„ ๋ฐฐํฌ) | +| ๋ธŒ๋žœ์น˜ | ์‹คํ–‰ ์›Œํฌํ”Œ๋กœ์šฐ| +|---------|-----------------------------| +| `main` | `kok-prod-CD.yml` (์šด์˜ ์„œ๋ฒ„ ๋ฐฐํฌ) | + +### 2๏ธโƒฃ **์ž๋™ ์‹คํ–‰ for Dev & CI** +| ๋ธŒ๋žœ์น˜ | ํŠธ๋ฆฌ๊ฑฐ | ์‹คํ–‰๋˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ | +|-----|--------|------------------------------| +| `feature/*, bugfix/*, hotfix/*` | `PR` | `kok-CI.yml` (๋นŒ๋“œ & Docker ๋ฐฐํฌ) | +| `develop` | `Push` | `kok-dev-CD.yml` (๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐฐํฌ) | +| `main` | `Push` | `kok-prod-CD.yml` (์šด์˜ ์„œ๋ฒ„ ๋ฐฐํฌ) | --- @@ -55,7 +60,7 @@ GitHub Actions์—์„œ `Run Workflow` ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์šด์˜ ๋ฐฐํฌ๋Š” **Blue-Green Deployment** ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ **๋ฌด์ค‘๋‹จ ๋ฐฐํฌ**๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ### **1๏ธโƒฃ ๋ฐฐํฌ ํŠธ๋ฆฌ๊ฑฐ** -- `main` ๋ธŒ๋žœ์น˜์— ์ฝ”๋“œ๊ฐ€ ํ‘ธ์‹œ๋˜๋ฉด `kok-prod-CD.yml`์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. +- `main` ๋ธŒ๋žœ์น˜์— ์ฝ”๋“œ๋ฅผ ํ‘ธ์‹œํ•˜๊ณ  ๋‚œ ํ›„, ์ˆ˜๋™์œผ๋กœ (`workflow_dispatch`) ๋ฐฐํฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. - GitHub Actions๊ฐ€ `docker-compose-prod.yml`์„ ์„œ๋ฒ„์— ์—…๋กœ๋“œํ•˜๊ณ  ๋ฐฐํฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ### **2๏ธโƒฃ ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์„œ๋น„์Šค ํ™•์ธ** diff --git a/.gitignore b/.gitignore index 03b51071..33b15727 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,7 @@ cmake-build-*/ # IntelliJ out/ +.idea/ # mpeltonen/sbt-idea plugin .idea_modules/ From d020e8387d9c34d9bb9dea0128e93d8dd9ff3c84 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 19:53:17 +0900 Subject: [PATCH 007/163] =?UTF-8?q?=F0=9F=8E=A8style:=20add=20EOL=20to=20C?= =?UTF-8?q?D=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 2 +- .github/workflows/kok-prod-CD.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index 3e61afcf..60f56935 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -33,4 +33,4 @@ jobs: cd ${{ secrets.COMPOSE_FILE_PATH }} sudo docker compose pull sudo docker compose down - sudo docker compose -f docker-compose-dev.yml up -d \ No newline at end of file + sudo docker compose -f docker-compose-dev.yml up -d diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index a374a80d..d61f5aa3 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -76,4 +76,4 @@ jobs: # โœ… ๊ธฐ์กด ํ™˜๊ฒฝ ์ค‘์ง€ ๋ฐ ์ •๋ฆฌ echo "๐Ÿ›‘ ๊ธฐ์กด ํ™˜๊ฒฝ($OLD_ENV) ์ค‘์ง€..." sudo docker compose stop $OLD_ENV - sudo docker compose rm -f $OLD_ENV \ No newline at end of file + sudo docker compose rm -f $OLD_ENV From 17a6241507ca36bba1c1afcbad89ea4a8faf5bae Mon Sep 17 00:00:00 2001 From: yunyoung1819 Date: Thu, 13 Feb 2025 20:10:34 +0900 Subject: [PATCH 008/163] :sparkles: feat: add common api response code and common error code --- kok-api/build.gradle | 1 + .../kokapi/adapter/in/web/BaseController.java | 7 +++ .../adapter/in/web/HealthCheckController.java | 19 +++++++ .../service/HealthCheckService.java | 13 +++++ .../usecase/HealthCheckUseCase.java | 5 ++ .../kok/kokcore/common/error/ErrorCode.java | 52 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 10 ++-- .../common/response/ApiResponseDto.java | 12 ++--- .../java/com/kok/kokcore/error/ErrorCode.java | 39 -------------- 9 files changed, 106 insertions(+), 52 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/common/error/ErrorCode.java rename kok-core/src/main/java/com/kok/kokcore/{ => common}/exception/GlobalExceptionHandler.java (68%) delete mode 100644 kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java diff --git a/kok-api/build.gradle b/kok-api/build.gradle index 84a5056a..45458d0d 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -24,6 +24,7 @@ repositories { } dependencies { + implementation project(':kok-core') implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java new file mode 100644 index 00000000..2e79e87c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java @@ -0,0 +1,7 @@ +package com.kok.kokapi.adapter.in.web; + +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping("/v1/api") +public class BaseController { +} diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java new file mode 100644 index 00000000..e225dfd4 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java @@ -0,0 +1,19 @@ +package com.kok.kokapi.adapter.in.web; + +import com.kok.kokapi.application.usecase.HealthCheckUseCase; +import com.kok.kokcore.common.response.ApiResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class HealthCheckController extends BaseController { + + private final HealthCheckUseCase healthCheckUseCase; + + @GetMapping("/health") + public ApiResponseDto checkHealth() { + return ApiResponseDto.success(healthCheckUseCase.checkHealth()); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java b/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java new file mode 100644 index 00000000..f53596d9 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java @@ -0,0 +1,13 @@ +package com.kok.kokapi.application.service; + +import com.kok.kokapi.application.usecase.HealthCheckUseCase; +import org.springframework.stereotype.Service; + +@Service +public class HealthCheckService implements HealthCheckUseCase { + + @Override + public String checkHealth() { + return "OK"; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java b/kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java new file mode 100644 index 00000000..e63a9fa6 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java @@ -0,0 +1,5 @@ +package com.kok.kokapi.application.usecase; + +public interface HealthCheckUseCase { + String checkHealth(); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/common/error/ErrorCode.java b/kok-core/src/main/java/com/kok/kokcore/common/error/ErrorCode.java new file mode 100644 index 00000000..878ca3cc --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/common/error/ErrorCode.java @@ -0,0 +1,52 @@ +package com.kok.kokcore.common.error; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + // 400 BAD REQUEST + BAD_REQUEST(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."), + INVALID_INPUT(HttpStatus.BAD_REQUEST, "์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + INVALID_FORMAT(HttpStatus.BAD_REQUEST, "์ž…๋ ฅ๊ฐ’์˜ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + EXCEEDED_USAGE_LIMIT(HttpStatus.BAD_REQUEST, "ํ˜ธ์ถœ ๊ฐ€๋Šฅ ํšŸ์ˆ˜๊ฐ€ ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + REQUEST_EXPIRED(HttpStatus.BAD_REQUEST, "์š”์ฒญ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + UNSUPPORTED_OPERATION(HttpStatus.BAD_REQUEST, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค."), + + // 401 UNAUTHORIZED + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + TOKEN_MALFORMED(HttpStatus.UNAUTHORIZED, "ํ† ํฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + SESSION_EXPIRED(HttpStatus.UNAUTHORIZED, "์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + + // 403 FORBIDDEN + FORBIDDEN(HttpStatus.FORBIDDEN, "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."), + IP_BLOCKED(HttpStatus.FORBIDDEN, "ํ•ด๋‹น IP๋Š” ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), + INSUFFICIENT_PERMISSIONS(HttpStatus.FORBIDDEN, "ํ•ด๋‹น ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."), + + + // 404 NOT FOUND + RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "์•ฝ์†๋ฐฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + + // 409 CONFLICT + CONFLICT(HttpStatus.CONFLICT, "์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + VERSION_CONFLICT(HttpStatus.CONFLICT, "๋ฒ„์ „ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."), + + + // 500 INTERNAL SERVER ERROR + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "ํ˜„์žฌ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์™ธ๋ถ€ API ํ˜ธ์ถœ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), + TIMEOUT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + + private final HttpStatus status; + private final String message; +} diff --git a/kok-core/src/main/java/com/kok/kokcore/exception/GlobalExceptionHandler.java b/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java similarity index 68% rename from kok-core/src/main/java/com/kok/kokcore/exception/GlobalExceptionHandler.java rename to kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java index ea4878a6..8e015add 100644 --- a/kok-core/src/main/java/com/kok/kokcore/exception/GlobalExceptionHandler.java +++ b/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ -package com.kok.kokcore.exception; +package com.kok.kokcore.common.exception; import com.kok.kokcore.common.response.ApiResponseDto; -import com.kok.kokcore.error.ErrorCode; +import com.kok.kokcore.common.error.ErrorCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestController; @@ -11,12 +11,14 @@ public class GlobalExceptionHandler { @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleBadRequestException(IllegalArgumentException ex) { - return ResponseEntity.badRequest().body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, ex.getMessage())); + return ResponseEntity.badRequest() + .body(ApiResponseDto.error(ErrorCode.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity> handleGlobalException(Exception ex) { - return ResponseEntity.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + return ResponseEntity + .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) .body(ApiResponseDto.error(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage())); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java b/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java index 33c330b2..2a05d169 100644 --- a/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java +++ b/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java @@ -1,16 +1,10 @@ package com.kok.kokcore.common.response; -import com.kok.kokcore.error.ErrorCode; -import lombok.AllArgsConstructor; -import lombok.Getter; + +import com.kok.kokcore.common.error.ErrorCode; import org.springframework.http.HttpStatus; -@Getter -@AllArgsConstructor -public class ApiResponseDto { - private final int code; - private final String message; - private final T data; +public record ApiResponseDto(int code, String message, T data) { public static ApiResponseDto success(T data) { return new ApiResponseDto<>(HttpStatus.OK.value(), "Success", data); diff --git a/kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java b/kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java deleted file mode 100644 index 74605ca0..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/error/ErrorCode.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.kok.kokcore.error; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@Getter -@RequiredArgsConstructor -public enum ErrorCode { - - // 400 BAD REQUEST - BAD_REQUEST(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."), - INVALID_INPUT(HttpStatus.BAD_REQUEST, "์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), - EXCEEDED_USAGE_LIMIT(HttpStatus.BAD_REQUEST, "ํ˜ธ์ถœ ๊ฐ€๋Šฅ ํšŸ์ˆ˜๊ฐ€ ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), - REQUEST_EXPIRED(HttpStatus.BAD_REQUEST, "์š”์ฒญ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), - UNSUPPORTED_OPERATION(HttpStatus.BAD_REQUEST, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค."), - - // 401 UNAUTHORIZED - UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), - - // 403 FORBIDDEN - FORBIDDEN(HttpStatus.FORBIDDEN, "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."), - - // 404 NOT FOUND - RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), - - // 409 CONFLICT - CONFLICT(HttpStatus.CONFLICT, "๋ฐ์ดํ„ฐ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), - - // 500 INTERNAL SERVER ERROR - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), - SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "ํ˜„์žฌ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - - - private final HttpStatus status; - private final String message; -} From 8295ccdae7030ce7d6fb07aaddce053412cedb1e Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 20:25:03 +0900 Subject: [PATCH 009/163] =?UTF-8?q?=F0=9F=91=B7build:=20modify=20CI/CD=20t?= =?UTF-8?q?riggers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/README.md | 24 ++++++++++++++---------- .github/workflows/kok-CI.yml | 8 +++++--- .github/workflows/kok-dev-CD.yml | 1 - .github/workflows/kok-prod-CD.yml | 3 --- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 242da7a3..0bd28cc8 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,16 +1,19 @@ ## ๐Ÿ“Œ CI/CD ๊ฐœ์š” ์ด ํ”„๋กœ์ ํŠธ๋Š” **GitHub Actions + Docker + Nginx + Blue-Green Deployment**๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ **์ž๋™ํ™”๋œ CI/CD**๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. -### โœ… CI (Continuous Integration) -| ๋ธŒ๋žœ์น˜ | ๋™์ž‘ | -|----------------------------------|-------------------------------------------| -| `feature/*, bugfix/*, hotfix/*` | **๋นŒ๋“œ & Docker Push** (`kok-CI.yml`) | +## โœ… CI (Continuous Integration) +| ๋ธŒ๋žœ์น˜ | ๋™์ž‘ | +|---------------------------------|-------------------------------------------| +| `feature/*, bugfix/*, hotfix/*` | **๋นŒ๋“œ & Docker Push** (`kok-CI.yml`) | +| `develop` | **๋นŒ๋“œ & Docker Push** (`kok-CI.yml`) | -### โœ… CD (Continuous Deployment) +--- + +## โœ… CD (Continuous Deployment) | ๋ธŒ๋žœ์น˜ | ๋™์ž‘ | |-----------|-------------------------------------------| | `develop` | **๊ฐœ๋ฐœ ์„œ๋ฒ„** ๋ฐฐํฌ (`kok-dev-CD.yml`) | -| `main` | **์šด์˜ ์„œ๋ฒ„ (Blue-Green ๋ฐฐํฌ)** (`kok-prod-CD.yml`) | +| `main` | **์šด์˜ ์„œ๋ฒ„ (Blue-Green ๋ฐฐํฌ, ์ˆ˜๋™ ์‹คํ–‰)** (`kok-prod-CD.yml`) | --- @@ -48,11 +51,12 @@ GitHub Actions์—์„œ `Run Workflow` ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. | `main` | `kok-prod-CD.yml` (์šด์˜ ์„œ๋ฒ„ ๋ฐฐํฌ) | ### 2๏ธโƒฃ **์ž๋™ ์‹คํ–‰ for Dev & CI** -| ๋ธŒ๋žœ์น˜ | ํŠธ๋ฆฌ๊ฑฐ | ์‹คํ–‰๋˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ | -|-----|--------|------------------------------| -| `feature/*, bugfix/*, hotfix/*` | `PR` | `kok-CI.yml` (๋นŒ๋“œ & Docker ๋ฐฐํฌ) | +| ๋ธŒ๋žœ์น˜ | ํŠธ๋ฆฌ๊ฑฐ | ์‹คํ–‰๋˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ | +|-----------|--------|------------------------------| +| `feature/*, bugfix/*, hotfix/*` | `PR` | `kok-CI.yml` (๋นŒ๋“œ & Docker ๋ฐฐํฌ) | +| `develop` | `PR` | `kok-CI.yml` (๋นŒ๋“œ & Docker ๋ฐฐํฌ) | +| `develop` | `Push` | `kok-CI.yml` (๋นŒ๋“œ & Docker ๋ฐฐํฌ) | | `develop` | `Push` | `kok-dev-CD.yml` (๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋ฐฐํฌ) | -| `main` | `Push` | `kok-prod-CD.yml` (์šด์˜ ์„œ๋ฒ„ ๋ฐฐํฌ) | --- diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index 1c6d0882..b70f1f40 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -1,12 +1,14 @@ name: kok-CI (Build & Push for kok) on: - push: + pull_request: branches: + - develop - main + push: + branches: - develop - - kok-api - - kok-core + - main jobs: build: diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index 60f56935..dad1c19c 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -4,7 +4,6 @@ on: push: branches: - develop - workflow_dispatch: jobs: deploy: diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index d61f5aa3..2ab54041 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -1,9 +1,6 @@ name: kok-prod-CD (Deploy to Prod - Blue-Green Auto) on: - push: - branches: - - main workflow_dispatch: jobs: From 7aca33c8dbe2e8e4cee9999513e85035537ec521 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 20:28:11 +0900 Subject: [PATCH 010/163] =?UTF-8?q?=F0=9F=91=B7build:=20update=20upload-ar?= =?UTF-8?q?tifact=20action=20to=20v4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index b70f1f40..f9b4e03c 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -37,7 +37,7 @@ jobs: ./gradlew clean build -x test - name: Save Build Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-artifact path: kok-api/build/libs/*.jar From fb4a170d0192d2c10a58b540e54067abb915fe25 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 20:29:49 +0900 Subject: [PATCH 011/163] =?UTF-8?q?=F0=9F=91=B7build:=20update=20upload-ar?= =?UTF-8?q?tifact=20action=20to=20v4=20on=20step=20docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index f9b4e03c..2e36a8ef 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -50,7 +50,7 @@ jobs: uses: actions/checkout@v2 - name: Download Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build-artifact path: kok-api/build/libs/ From d861f7c403df1322f8d6d6e430c39820aef3a3c8 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 21:08:05 +0900 Subject: [PATCH 012/163] =?UTF-8?q?=F0=9F=91=B7build:=20fix=20image=20tag?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index 2e36a8ef..fe59aa46 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -27,9 +27,9 @@ jobs: id: determine-tag run: | if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then - echo "IMAGE_TAG=_prod" >> $GITHUB_ENV + echo "IMAGE_TAG=prod" >> $GITHUB_ENV else - echo "IMAGE_TAG=_dev" >> $GITHUB_ENV + echo "IMAGE_TAG=dev" >> $GITHUB_ENV fi - name: Build with Gradle (๋ฉ€ํ‹ฐ๋ชจ๋“ˆ) @@ -63,11 +63,15 @@ jobs: - name: Build & Push Docker Image run: | - IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok${{ env.IMAGE_TAG }}" - - echo "Building Image: $IMAGE_NAME" - docker build -t $IMAGE_NAME:${{ github.sha }} -f Dockerfile . - docker push $IMAGE_NAME:${{ github.sha }} + # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ + IMAGE_TAG="${{ env.IMAGE_TAG }}" + IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok_${IMAGE_TAG}" + + echo "๐Ÿš€ Building Docker Image: $IMAGE_NAME:$GITHUB_SHA" + + # Docker Build & Push + docker build -t $IMAGE_NAME:$GITHUB_SHA -f Dockerfile . + docker push $IMAGE_NAME:$GITHUB_SHA - name: Notify Deployment Trigger run: | From 9f8b597bdffad1a54912f6c8adbbfd6c84acba79 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 21:14:06 +0900 Subject: [PATCH 013/163] =?UTF-8?q?=F0=9F=91=B7build:=20test=20image=20tag?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index fe59aa46..1efc7b03 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -26,7 +26,9 @@ jobs: - name: Determine Image Tag (prod/dev) id: determine-tag run: | - if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then + echo "ํ˜„์žฌ GITHUB_REF_NAME ๊ฐ’: $GITHUB_REF_NAME" + + if [[ "$GITHUB_REF_NAME" == "main" ]]; then echo "IMAGE_TAG=prod" >> $GITHUB_ENV else echo "IMAGE_TAG=dev" >> $GITHUB_ENV @@ -65,7 +67,7 @@ jobs: run: | # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ IMAGE_TAG="${{ env.IMAGE_TAG }}" - IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok_${IMAGE_TAG}" + IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok-${IMAGE_TAG}" echo "๐Ÿš€ Building Docker Image: $IMAGE_NAME:$GITHUB_SHA" From 32c61f9dbfeb782f767c51c1abc71ba8061ac61f Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 21:25:10 +0900 Subject: [PATCH 014/163] =?UTF-8?q?=F0=9F=91=B7build:=20test=20image=20tag?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index 1efc7b03..3d0b1850 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -26,9 +26,16 @@ jobs: - name: Determine Image Tag (prod/dev) id: determine-tag run: | - echo "ํ˜„์žฌ GITHUB_REF_NAME ๊ฐ’: $GITHUB_REF_NAME" - - if [[ "$GITHUB_REF_NAME" == "main" ]]; then + # PR์ธ์ง€ ์ผ๋ฐ˜ Push์ธ์ง€ ํ™•์ธ + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + BRANCH_NAME="$GITHUB_BASE_REF" + else + BRANCH_NAME="$GITHUB_REF_NAME" + fi + + echo "๐Ÿ” ํ˜„์žฌ ๋ธŒ๋žœ์น˜: $BRANCH_NAME" + + if [[ "$BRANCH_NAME" == "main" ]]; then echo "IMAGE_TAG=prod" >> $GITHUB_ENV else echo "IMAGE_TAG=dev" >> $GITHUB_ENV @@ -65,12 +72,16 @@ jobs: - name: Build & Push Docker Image run: | - # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ - IMAGE_TAG="${{ env.IMAGE_TAG }}" + source $GITHUB_ENV # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฐ•์ œ ๋กœ๋“œ IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok-${IMAGE_TAG}" - + echo "๐Ÿš€ Building Docker Image: $IMAGE_NAME:$GITHUB_SHA" + if [[ -z "$IMAGE_TAG" ]]; then + echo "๐Ÿšจ ERROR: IMAGE_TAG ๊ฐ’์ด ๋น„์–ด ์žˆ์Œ!" + exit 1 + fi + # Docker Build & Push docker build -t $IMAGE_NAME:$GITHUB_SHA -f Dockerfile . docker push $IMAGE_NAME:$GITHUB_SHA From 9e481b13bb2f32bcd699bc0bb41b6d65e26ac482 Mon Sep 17 00:00:00 2001 From: yunyoung1819 Date: Thu, 13 Feb 2025 21:28:43 +0900 Subject: [PATCH 015/163] =?UTF-8?q?:sparkles:=20fix:=20annotation=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kok/kokcore/common/exception/GlobalExceptionHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java b/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java index 8e015add..fc34c54f 100644 --- a/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java +++ b/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java @@ -4,9 +4,9 @@ import com.kok.kokcore.common.error.ErrorCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; -@RestController +@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(IllegalArgumentException.class) From a8751e04a9df1b760e8000137295d37635f3c438 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 21:31:39 +0900 Subject: [PATCH 016/163] =?UTF-8?q?=F0=9F=91=B7build:=20test=20image=20tag?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index 3d0b1850..b5268a64 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -40,6 +40,8 @@ jobs: else echo "IMAGE_TAG=dev" >> $GITHUB_ENV fi + + echo "๐Ÿท IMAGE_TAG: $IMAGE_TAG" - name: Build with Gradle (๋ฉ€ํ‹ฐ๋ชจ๋“ˆ) run: | @@ -72,9 +74,9 @@ jobs: - name: Build & Push Docker Image run: | - source $GITHUB_ENV # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฐ•์ œ ๋กœ๋“œ + export $(grep -v '^#' $GITHUB_ENV | xargs) + echo "โœ… ๋กœ๋“œ๋œ IMAGE_TAG ๊ฐ’: $IMAGE_TAG" IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok-${IMAGE_TAG}" - echo "๐Ÿš€ Building Docker Image: $IMAGE_NAME:$GITHUB_SHA" if [[ -z "$IMAGE_TAG" ]]; then From 2e0b2fdefcd3844f0bdb799a2eb4b8162e322fa8 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 21:39:12 +0900 Subject: [PATCH 017/163] =?UTF-8?q?=F0=9F=91=B7build:=20test=20image=20tag?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index b5268a64..55f055a3 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -26,22 +26,23 @@ jobs: - name: Determine Image Tag (prod/dev) id: determine-tag run: | - # PR์ธ์ง€ ์ผ๋ฐ˜ Push์ธ์ง€ ํ™•์ธ if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then BRANCH_NAME="$GITHUB_BASE_REF" else BRANCH_NAME="$GITHUB_REF_NAME" fi - + echo "๐Ÿ” ํ˜„์žฌ ๋ธŒ๋žœ์น˜: $BRANCH_NAME" - + if [[ "$BRANCH_NAME" == "main" ]]; then - echo "IMAGE_TAG=prod" >> $GITHUB_ENV + echo "IMAGE_TAG=prod" | tee -a $GITHUB_ENV else - echo "IMAGE_TAG=dev" >> $GITHUB_ENV + echo "IMAGE_TAG=dev" | tee -a $GITHUB_ENV fi - echo "๐Ÿท IMAGE_TAG: $IMAGE_TAG" + # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ™•์ธ + echo "โœ… ํ˜„์žฌ GITHUB_ENV ๊ฐ’:" + cat $GITHUB_ENV - name: Build with Gradle (๋ฉ€ํ‹ฐ๋ชจ๋“ˆ) run: | @@ -74,20 +75,25 @@ jobs: - name: Build & Push Docker Image run: | + # ๐Ÿ”ฅ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฐ•์ œ ๋กœ๋“œ export $(grep -v '^#' $GITHUB_ENV | xargs) - echo "โœ… ๋กœ๋“œ๋œ IMAGE_TAG ๊ฐ’: $IMAGE_TAG" - IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok-${IMAGE_TAG}" - echo "๐Ÿš€ Building Docker Image: $IMAGE_NAME:$GITHUB_SHA" + + echo "๐Ÿท IMAGE_TAG ๊ฐ’ ํ™•์ธ: '$IMAGE_TAG'" if [[ -z "$IMAGE_TAG" ]]; then echo "๐Ÿšจ ERROR: IMAGE_TAG ๊ฐ’์ด ๋น„์–ด ์žˆ์Œ!" exit 1 fi + IMAGE_NAME="${{ secrets.DOCKERHUB_USERNAME }}/kok-${IMAGE_TAG}" + + echo "๐Ÿš€ Building Docker Image: $IMAGE_NAME:$GITHUB_SHA" + # Docker Build & Push docker build -t $IMAGE_NAME:$GITHUB_SHA -f Dockerfile . docker push $IMAGE_NAME:$GITHUB_SHA + - name: Notify Deployment Trigger run: | echo "๐Ÿš€ Docker ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ! Image: $IMAGE_NAME" From 86a9900884ca8cb6e29abfa42f6fc3b458f89dae Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 21:47:41 +0900 Subject: [PATCH 018/163] =?UTF-8?q?=F0=9F=91=B7build:=20correct=20Image=5F?= =?UTF-8?q?Tag=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index 55f055a3..a89a52a8 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -74,10 +74,9 @@ jobs: password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build & Push Docker Image + env: + IMAGE_TAG: ${{ env.IMAGE_TAG }} run: | - # ๐Ÿ”ฅ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฐ•์ œ ๋กœ๋“œ - export $(grep -v '^#' $GITHUB_ENV | xargs) - echo "๐Ÿท IMAGE_TAG ๊ฐ’ ํ™•์ธ: '$IMAGE_TAG'" if [[ -z "$IMAGE_TAG" ]]; then @@ -94,6 +93,7 @@ jobs: docker push $IMAGE_NAME:$GITHUB_SHA + - name: Notify Deployment Trigger run: | echo "๐Ÿš€ Docker ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ! Image: $IMAGE_NAME" From 6baca9f4ef289f733d295bd24e9b46c9c47edf47 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Feb 2025 22:00:45 +0900 Subject: [PATCH 019/163] =?UTF-8?q?=F0=9F=91=B7build:=20correct=20Image=5F?= =?UTF-8?q?Tag=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-CI.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index a89a52a8..7de15e9d 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -13,6 +13,8 @@ on: jobs: build: runs-on: ubuntu-latest + outputs: + IMAGE_TAG: ${{ steps.determine-tag.outputs.IMAGE_TAG }} steps: - name: Checkout Repository uses: actions/checkout@v2 @@ -36,11 +38,12 @@ jobs: if [[ "$BRANCH_NAME" == "main" ]]; then echo "IMAGE_TAG=prod" | tee -a $GITHUB_ENV + echo "::set-output name=IMAGE_TAG::prod" else echo "IMAGE_TAG=dev" | tee -a $GITHUB_ENV + echo "::set-output name=IMAGE_TAG::dev" fi - # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ™•์ธ echo "โœ… ํ˜„์žฌ GITHUB_ENV ๊ฐ’:" cat $GITHUB_ENV @@ -57,6 +60,9 @@ jobs: docker: runs-on: ubuntu-latest needs: build + env: + IMAGE_TAG: ${{ needs.build.outputs.IMAGE_TAG }} + steps: - name: Checkout Repository uses: actions/checkout@v2 @@ -74,8 +80,6 @@ jobs: password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build & Push Docker Image - env: - IMAGE_TAG: ${{ env.IMAGE_TAG }} run: | echo "๐Ÿท IMAGE_TAG ๊ฐ’ ํ™•์ธ: '$IMAGE_TAG'" @@ -92,8 +96,6 @@ jobs: docker build -t $IMAGE_NAME:$GITHUB_SHA -f Dockerfile . docker push $IMAGE_NAME:$GITHUB_SHA - - - name: Notify Deployment Trigger run: | echo "๐Ÿš€ Docker ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ! Image: $IMAGE_NAME" From 30ac1d3b27290a6336f69271da553a6e04d891cc Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 13 Feb 2025 23:29:16 +0900 Subject: [PATCH 020/163] :wrench: config: set up jacoco (#27) --- build.gradle | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ec9fce93..2c4f6e7b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.2' id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' } bootJar.enabled = false @@ -40,6 +41,16 @@ subprojects { } } +jacocoTestReport { + reports { + html.required.set(true) + xml.required.set(true) + csv.required.set(false) + xml.outputLocation.set(layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml")) + } +} + tasks.named('test') { useJUnitPlatform() -} \ No newline at end of file + finalizedBy jacocoTestReport +} From 2e080f8ff1716f12f0e67562283546d14547c392 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 13 Feb 2025 23:39:00 +0900 Subject: [PATCH 021/163] =?UTF-8?q?=F0=9F=94=A7=20config:=20set=20up=20app?= =?UTF-8?q?lication=20config=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :wrench: config: set up application file for dev environment * :wrench: config: add spring doc dependency * :wrench: config: add db dependency * :wrench: config: set up application file for prod environment * :wrench: config: remove profile activation * :wrench: config: move spring docs dependency to kok-api --- build.gradle | 7 +++++- kok-api/build.gradle | 3 +++ .../src/main/resources/application-dev.yml | 23 ++++++++++++++++++ .../src/main/resources/application-prod.yml | 24 +++++++++++++++++++ .../src/main/resources/application.properties | 1 - 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 kok-api/src/main/resources/application-dev.yml create mode 100644 kok-api/src/main/resources/application-prod.yml delete mode 100644 kok-api/src/main/resources/application.properties diff --git a/build.gradle b/build.gradle index 2c4f6e7b..cd57cbe5 100644 --- a/build.gradle +++ b/build.gradle @@ -34,10 +34,15 @@ subprojects { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok:1.18.34' + + runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + testImplementation 'junit:junit:4.13.1' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } } diff --git a/kok-api/build.gradle b/kok-api/build.gradle index 84a5056a..fa183d40 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -26,8 +26,11 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml new file mode 100644 index 00000000..77c8d0cb --- /dev/null +++ b/kok-api/src/main/resources/application-dev.yml @@ -0,0 +1,23 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + show_sql: true + format_sql: true + open-in-view: false + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} +springdoc: + default-consumes-media-type: application/json;charset=UTF-8 + default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + path: /swagger diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml new file mode 100644 index 00000000..4e40ddeb --- /dev/null +++ b/kok-api/src/main/resources/application-prod.yml @@ -0,0 +1,24 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + show_sql: true + format_sql: true + open-in-view: false + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} +springdoc: + default-consumes-media-type: application/json;charset=UTF-8 + default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + enabled: false + path: /swagger diff --git a/kok-api/src/main/resources/application.properties b/kok-api/src/main/resources/application.properties deleted file mode 100644 index f185fb9d..00000000 --- a/kok-api/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=kok-api From 96895957612fe99bd4c69432f9fc57be9fbfe83b Mon Sep 17 00:00:00 2001 From: yunyoung1819 Date: Sat, 15 Feb 2025 15:15:51 +0900 Subject: [PATCH 022/163] :recycle: refactor: refactoring code review feedback --- .../java/com/kok/kokapi/adapter/in/web/BaseController.java | 2 +- .../kok/kokapi/adapter/in/web/HealthCheckController.java | 2 +- .../main/java/com/kok/kokapi}/common/error/ErrorCode.java | 2 +- .../kokapi}/common/exception/GlobalExceptionHandler.java | 6 +++--- .../com/kok/kokapi}/common/response/ApiResponseDto.java | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename {kok-core/src/main/java/com/kok/kokcore => kok-api/src/main/java/com/kok/kokapi}/common/error/ErrorCode.java (98%) rename {kok-core/src/main/java/com/kok/kokcore => kok-api/src/main/java/com/kok/kokapi}/common/exception/GlobalExceptionHandler.java (85%) rename {kok-core/src/main/java/com/kok/kokcore => kok-api/src/main/java/com/kok/kokapi}/common/response/ApiResponseDto.java (87%) diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java index 2e79e87c..27231369 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java @@ -3,5 +3,5 @@ import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("/v1/api") -public class BaseController { +public abstract class BaseController { } diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java index e225dfd4..8cf1f23c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java @@ -1,7 +1,7 @@ package com.kok.kokapi.adapter.in.web; import com.kok.kokapi.application.usecase.HealthCheckUseCase; -import com.kok.kokcore.common.response.ApiResponseDto; +import com.kok.kokapi.common.response.ApiResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/kok-core/src/main/java/com/kok/kokcore/common/error/ErrorCode.java b/kok-api/src/main/java/com/kok/kokapi/common/error/ErrorCode.java similarity index 98% rename from kok-core/src/main/java/com/kok/kokcore/common/error/ErrorCode.java rename to kok-api/src/main/java/com/kok/kokapi/common/error/ErrorCode.java index 878ca3cc..07428313 100644 --- a/kok-core/src/main/java/com/kok/kokcore/common/error/ErrorCode.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/error/ErrorCode.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.common.error; +package com.kok.kokapi.common.error; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java similarity index 85% rename from kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java rename to kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java index fc34c54f..8cab5128 100644 --- a/kok-core/src/main/java/com/kok/kokcore/common/exception/GlobalExceptionHandler.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ -package com.kok.kokcore.common.exception; +package com.kok.kokapi.common.exception; -import com.kok.kokcore.common.response.ApiResponseDto; -import com.kok.kokcore.common.error.ErrorCode; +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.common.error.ErrorCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; diff --git a/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java b/kok-api/src/main/java/com/kok/kokapi/common/response/ApiResponseDto.java similarity index 87% rename from kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java rename to kok-api/src/main/java/com/kok/kokapi/common/response/ApiResponseDto.java index 2a05d169..63b1497b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/common/response/ApiResponseDto.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/response/ApiResponseDto.java @@ -1,7 +1,7 @@ -package com.kok.kokcore.common.response; +package com.kok.kokapi.common.response; -import com.kok.kokcore.common.error.ErrorCode; +import com.kok.kokapi.common.error.ErrorCode; import org.springframework.http.HttpStatus; public record ApiResponseDto(int code, String message, T data) { From 90230938beb084d8efc1fba6d659b391c25e9095 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 18 Feb 2025 18:29:30 +0900 Subject: [PATCH 023/163] =?UTF-8?q?=F0=9F=94=A7config:=20redis=20setting?= =?UTF-8?q?=20for=20standalone=20and=20sentinel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/config/redis/RedisConfig.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java b/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java new file mode 100644 index 00000000..b57a5508 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java @@ -0,0 +1,58 @@ +package com.kok.kokapi.adapter.config.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import java.util.UUID; + +@Configuration +@RequiredArgsConstructor +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisStandaloneConnectionFactory() { + RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(redisHost, redisPort); + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + // TODO: ์ถ”๊ฐ€์‚ฌํ•ญ ์ถ”๊ฐ€. (๋ช…๋ น์–ด ํƒ€์ž„์•„์›ƒ, ์ข…๋ฃŒ ํƒ€์ž„์•„์›ƒ) + .build(); + return new LettuceConnectionFactory(redisConfig, clientConfig); + } + +// // ํŽ˜์ผ์˜ค๋ฒ„ ๊ฐ€๋Šฅํ•œ Redis Sentinel ์„ค์ • +// @Bean +// public RedisConnectionFactory redisSentinelConnectionFactory() { +// RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() +// .master("") // Sentinel์—์„œ ์„ค์ •ํ•œ master ์ด๋ฆ„ +// .sentinel("host1", 1234) //Sentinel1 ์„œ๋ฒ„ ์ •๋ณด +// .sentinel("host2", 1234); // Sentinel2 ์„œ๋ฒ„ ์ •๋ณด +// return new LettuceConnectionFactory(sentinelConfig); +// } + + + @Bean(name = "redisTemplate") + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisStandaloneConnectionFactory()); + + // Key Serializer ์„ค์ • (UUID -> String ๋ณ€ํ™˜) + redisTemplate.setKeySerializer(new StringRedisSerializer()); + + // Value Serializer ์„ค์ • (๊ฐ์ฒด -> JSON ๋ณ€ํ™˜) + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return redisTemplate; + } +} From 155e16dd8856627228dbe8f14d2affc67c05cf31 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 18 Feb 2025 18:43:48 +0900 Subject: [PATCH 024/163] =?UTF-8?q?=F0=9F=94=A7config:=20use=20default=20s?= =?UTF-8?q?pring=20redis=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/config/redis/RedisConfig.java | 34 ++----------------- .../src/main/resources/application-prod.yml | 7 ++++ 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java b/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java index b57a5508..1d90ba47 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java @@ -1,13 +1,9 @@ package com.kok.kokapi.adapter.config.redis; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -17,36 +13,10 @@ @RequiredArgsConstructor public class RedisConfig { - @Value("${spring.data.redis.host}") - private String redisHost; - - @Value("${spring.data.redis.port}") - private int redisPort; - @Bean - public RedisConnectionFactory redisStandaloneConnectionFactory() { - RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(redisHost, redisPort); - LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() - // TODO: ์ถ”๊ฐ€์‚ฌํ•ญ ์ถ”๊ฐ€. (๋ช…๋ น์–ด ํƒ€์ž„์•„์›ƒ, ์ข…๋ฃŒ ํƒ€์ž„์•„์›ƒ) - .build(); - return new LettuceConnectionFactory(redisConfig, clientConfig); - } - -// // ํŽ˜์ผ์˜ค๋ฒ„ ๊ฐ€๋Šฅํ•œ Redis Sentinel ์„ค์ • -// @Bean -// public RedisConnectionFactory redisSentinelConnectionFactory() { -// RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() -// .master("") // Sentinel์—์„œ ์„ค์ •ํ•œ master ์ด๋ฆ„ -// .sentinel("host1", 1234) //Sentinel1 ์„œ๋ฒ„ ์ •๋ณด -// .sentinel("host2", 1234); // Sentinel2 ์„œ๋ฒ„ ์ •๋ณด -// return new LettuceConnectionFactory(sentinelConfig); -// } - - - @Bean(name = "redisTemplate") - public RedisTemplate redisTemplate() { + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisStandaloneConnectionFactory()); + redisTemplate.setConnectionFactory(redisConnectionFactory); // Key Serializer ์„ค์ • (UUID -> String ๋ณ€ํ™˜) redisTemplate.setKeySerializer(new StringRedisSerializer()); diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 4e40ddeb..3a53d77f 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -16,6 +16,13 @@ spring: redis: host: ${REDIS_HOST} port: ${REDIS_PORT} +# sentinel: +# master: ${REDIS_SENTINEL_MASTER} +# nodes: +# - ${REDIS_SENTINEL_HOST1}:${REDIS_SENTINEL_PORT1} +# - ${REDIS_SENTINEL_HOST2}:${REDIS_SENTINEL_PORT2} +# - ${REDIS_SENTINEL_HOST3}:${REDIS_SENTINEL_PORT3} + springdoc: default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 From 6cdf399042677d1b25519e56d77cbb88d0c5f967 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 18 Feb 2025 19:42:32 +0900 Subject: [PATCH 025/163] =?UTF-8?q?=F0=9F=94=A7config:=20setup=20swagger?= =?UTF-8?q?=20config=20with=20GroupedOpenApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/config/swagger/SwaggerConfig.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java b/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java new file mode 100644 index 00000000..af82fe99 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.adapter.config.swagger; + +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("KOK") + .version("1.0") + .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.") + .contact(new Contact().name("๋””ํ”„๋งŒ16๊ธฐ 4์กฐ").url("https://www.depromeet.com/"))); + } + + @Bean + public GroupedOpenApi kokApi() { + return GroupedOpenApi.builder() + .group("KOK api") + .pathsToMatch("/v1/api/**") + .build(); + } +} + From a7866b61e9e545a134883f2b0730f62d820abc73 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 18 Feb 2025 21:29:51 +0900 Subject: [PATCH 026/163] =?UTF-8?q?=F0=9F=94=A7config:=20change=20uuid=20t?= =?UTF-8?q?o=20string?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kok/kokapi/adapter/config/redis/RedisConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java b/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java index 1d90ba47..67bd08b5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java @@ -13,9 +13,10 @@ @RequiredArgsConstructor public class RedisConfig { + // ์ถ”ํ›„ ConnectionFactory์„ค์ • ๋ณ€๊ฒฝ์„ ๊ณ ๋ ค. (Sentinel, Cluster, etc...) @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // Key Serializer ์„ค์ • (UUID -> String ๋ณ€ํ™˜) From 215328ccb8b6f2908ad81d2a0ea1ca88ed34ce54 Mon Sep 17 00:00:00 2001 From: minseokey Date: Wed, 19 Feb 2025 20:03:21 +0900 Subject: [PATCH 027/163] =?UTF-8?q?=F0=9F=94=A7config:=20change=20grouped?= =?UTF-8?q?=20api=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kokapi/adapter/config/swagger/SwaggerConfig.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java b/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java index af82fe99..8158ad20 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java @@ -3,7 +3,6 @@ import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import org.springframework.context.annotation.Configuration; @@ -16,15 +15,14 @@ public OpenAPI customOpenAPI() { .info(new Info() .title("KOK") .version("1.0") - .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.") - .contact(new Contact().name("๋””ํ”„๋งŒ16๊ธฐ 4์กฐ").url("https://www.depromeet.com/"))); + .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")); } @Bean - public GroupedOpenApi kokApi() { + public GroupedOpenApi v1Api() { return GroupedOpenApi.builder() - .group("KOK api") - .pathsToMatch("/v1/api/**") + .group("V1 API") // Swagger UI์—์„œ "V1 API" ๊ทธ๋ฃน์œผ๋กœ ํ‘œ์‹œ + .pathsToMatch("/v1/api/**") // v1 ๊ด€๋ จ API๋งŒ ํฌํ•จ .build(); } } From 949e43c463efb5dcc323b3c8d820102dc6b37e21 Mon Sep 17 00:00:00 2001 From: minseokey Date: Wed, 19 Feb 2025 22:51:46 +0900 Subject: [PATCH 028/163] =?UTF-8?q?=F0=9F=94=A7config:=20restructure=20con?= =?UTF-8?q?fig=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kok/kokapi/{adapter => }/config/swagger/SwaggerConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename kok-api/src/main/java/com/kok/kokapi/{adapter => }/config/swagger/SwaggerConfig.java (94%) diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java similarity index 94% rename from kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java rename to kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java index 8158ad20..27f9b423 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/config/swagger/SwaggerConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.adapter.config.swagger; +package com.kok.kokapi.config.swagger; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; From 1c8e27ad513fbd9f5d188b44111415f91a797677 Mon Sep 17 00:00:00 2001 From: minseokey Date: Wed, 19 Feb 2025 22:52:39 +0900 Subject: [PATCH 029/163] =?UTF-8?q?=F0=9F=94=A7config:=20restructure=20con?= =?UTF-8?q?fig=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kok/kokapi/{adapter => }/config/redis/RedisConfig.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/{adapter => }/config/redis/RedisConfig.java (94%) diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java similarity index 94% rename from kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java rename to kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java index 67bd08b5..a28fee9d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/config/redis/RedisConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.adapter.config.redis; +package com.kok.kokapi.config.redis; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -7,7 +7,6 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; -import java.util.UUID; @Configuration @RequiredArgsConstructor From f3425db692c024b4376ac0bb29e94fc90b6a659b Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 20 Feb 2025 20:13:11 +0900 Subject: [PATCH 030/163] =?UTF-8?q?=E2=9C=A8feature:=20initial=20commit=20?= =?UTF-8?q?for=20location=20and=20centroid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + build.gradle | 5 + .../com/kok/kokapi/KokApiApplication.java | 4 + .../adapter/inbound/dto/LocationRequest.java | 11 ++ .../inbound/rest/LocationController.java | 117 ++++++++++++++++++ .../outbound/dto/CentroidResponse.java | 13 ++ .../outbound/dto/LocationResponse.java | 14 +++ .../LocationPersistenceAdapter.java | 50 ++++++++ .../persistence/LocationRepository.java | 27 ++++ .../application/service/LocationService.java | 51 ++++++++ .../config/geometry/GeometryConfig.java | 47 +++++++ .../src/main/resources/application-dev.yml | 1 + .../src/main/resources/application-prod.yml | 1 + .../application/domain/entity/Location.java | 44 +++++++ .../port/outbound/ReadCentroidPort.java | 9 ++ .../port/outbound/ReadLocationPort.java | 11 ++ .../port/outbound/SaveLocationPort.java | 8 ++ .../usecase/CreateLocationUsecase.java | 10 ++ .../usecase/ReadCentroidUsecase.java | 7 ++ .../usecase/ReadLocationUsecase.java | 10 ++ 20 files changed, 443 insertions(+) create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/dto/LocationRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/rest/LocationController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/CentroidResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/LocationResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationPersistenceAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationRepository.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/domain/entity/Location.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadLocationPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/port/outbound/SaveLocationPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadLocationUsecase.java diff --git a/.gitignore b/.gitignore index 33b15727..a0feda54 100644 --- a/.gitignore +++ b/.gitignore @@ -329,3 +329,6 @@ out/ *.DS_Store *.class *.jar + +**/application-local.yml +/infra/docker-compose-local.yml diff --git a/build.gradle b/build.gradle index cd57cbe5..b91b2731 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,11 @@ subprojects { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + implementation 'org.hibernate:hibernate-spatial:6.4.4.Final' + implementation 'org.locationtech.jts:jts-core:1.18.2' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok:1.18.34' diff --git a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java index 3e93cf1e..bfb35c65 100644 --- a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java +++ b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java @@ -2,8 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication +@EnableJpaRepositories(basePackages = "com.kok.kokapi.*") +@EntityScan(basePackages = "com.kok.kokcore.application.domain.entity") public class KokApiApplication { public static void main(String[] args) { diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/dto/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/dto/LocationRequest.java new file mode 100644 index 00000000..df5b4e71 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/dto/LocationRequest.java @@ -0,0 +1,11 @@ +package com.kok.kokapi.centroid.adapter.inbound.dto; + +import java.math.BigDecimal; + +public record LocationRequest( + String uuid, + Integer memberId, + BigDecimal latitude, + BigDecimal longitude +) { +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/rest/LocationController.java new file mode 100644 index 00000000..06c77f7c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/rest/LocationController.java @@ -0,0 +1,117 @@ +package com.kok.kokapi.centroid.adapter.inbound.rest; + +import com.kok.kokapi.centroid.adapter.inbound.dto.LocationRequest; +import com.kok.kokapi.centroid.adapter.outbound.dto.CentroidResponse; +import com.kok.kokapi.centroid.adapter.outbound.dto.LocationResponse; +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.geometry.GeometryConfig; +import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.application.usecase.CreateLocationUsecase; +import com.kok.kokcore.application.usecase.ReadCentroidUsecase; +import com.kok.kokcore.application.usecase.ReadLocationUsecase; +import lombok.RequiredArgsConstructor; +import org.springframework.data.util.Pair; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/location") +public class LocationController { + + private final CreateLocationUsecase createLocationUsecase; + private final ReadCentroidUsecase readCentroidUsecase; + private final ReadLocationUsecase readLocationUsecase; + private final GeometryConfig.PointConverter pointConverter; + + @PostMapping("/create") + public ResponseEntity> createLocation(@RequestBody LocationRequest locationRequest) { + Location location = createLocationUsecase.createLocation( + locationRequest.uuid(), + locationRequest.memberId(), + pointConverter.fromCoordinates( + locationRequest.latitude(), + locationRequest.longitude() + ) + ); + if (location == null) { + throw new IllegalArgumentException("์œ„์น˜ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + + Pair centroid = pointConverter.toCoordinates(readCentroidUsecase.readCentroid(locationRequest.uuid())); + + return ResponseEntity.ok(ApiResponseDto.success( + CentroidResponse.of(locationRequest.uuid(), centroid.getFirst(), centroid.getSecond()) + )); + } + + @GetMapping("/centroid/{uuid}") + public ResponseEntity> getCentroid(@PathVariable String uuid) { + Pair centroid = pointConverter.toCoordinates(readCentroidUsecase.readCentroid(uuid)); + + if (centroid == null) { + throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + return ResponseEntity.ok(ApiResponseDto.success( + CentroidResponse.of(uuid, centroid.getFirst(), centroid.getSecond()) + )); + } + + @GetMapping("/{uuid}/{memberId}") + public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { + Location location = readLocationUsecase.readLocation(uuid, memberId); + if (location == null) { + throw new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + Pair locationPair = pointConverter.toCoordinates(location.getPoint()); + + return ResponseEntity.ok(ApiResponseDto.success( + LocationResponse.of(uuid, memberId, locationPair.getFirst(), locationPair.getSecond()) + )); + } + + @GetMapping("/{uuid}") + public ResponseEntity>> getLocations(@PathVariable String uuid) { + List locations = readLocationUsecase.readLocations(uuid); + + if (locations.isEmpty()) { + throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์œ„์น˜๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + List responses = locations.stream() + .map(location -> { + Pair coordinates = pointConverter.toCoordinates(location.getPoint()); + return LocationResponse.of( + uuid, + location.getMemberId(), + coordinates.getFirst(), + coordinates.getSecond() + ); + }) + .toList(); + + return ResponseEntity.ok(ApiResponseDto.success(responses)); + } + + @PutMapping("/update") + public ResponseEntity> updateLocation(@RequestBody LocationRequest locationRequest) { + Location location = createLocationUsecase.UpdateLocation( + locationRequest.uuid(), + locationRequest.memberId(), + pointConverter.fromCoordinates( + locationRequest.latitude(), + locationRequest.longitude() + ) + ); + Pair locationPair = pointConverter.toCoordinates(location.getPoint()); + + return ResponseEntity.ok(ApiResponseDto.success( + LocationResponse.of(locationRequest.uuid(), locationRequest.memberId(), locationPair.getFirst(), locationPair.getSecond()) + )); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/CentroidResponse.java new file mode 100644 index 00000000..d8565f00 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/CentroidResponse.java @@ -0,0 +1,13 @@ +package com.kok.kokapi.centroid.adapter.outbound.dto; + +import java.math.BigDecimal; + +public record CentroidResponse( + String uuid, + BigDecimal latitude, + BigDecimal longitude +) { + public static CentroidResponse of(String uuid, BigDecimal latitude, BigDecimal longitude) { + return new CentroidResponse(uuid, latitude, longitude); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/LocationResponse.java new file mode 100644 index 00000000..d45539e4 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/LocationResponse.java @@ -0,0 +1,14 @@ +package com.kok.kokapi.centroid.adapter.outbound.dto; + +import java.math.BigDecimal; + +public record LocationResponse ( + String uuid, + Integer memberId, + BigDecimal latitude, + BigDecimal longitude +){ + public static LocationResponse of(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { + return new LocationResponse(uuid, memberId, latitude, longitude); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationPersistenceAdapter.java new file mode 100644 index 00000000..d79fdb98 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationPersistenceAdapter.java @@ -0,0 +1,50 @@ +package com.kok.kokapi.centroid.adapter.outbound.persistence; + + +import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.application.port.outbound.ReadCentroidPort; +import com.kok.kokcore.application.port.outbound.ReadLocationPort; +import com.kok.kokcore.application.port.outbound.SaveLocationPort; +import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; +import org.springframework.stereotype.Repository; +import org.locationtech.jts.io.WKTReader; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocationPort, ReadLocationPort { + private final LocationRepository locationRepository; + + @Override + public Optional findLocationByUuidAndMemberId(String uuid, Integer memberId) { + return locationRepository.findLocationByUuidAndMemberId(uuid, memberId); + } + + @Override + public List findLocationsByUuid(String uuid) { + return locationRepository.findLocationsByUuid(uuid); + } + + @Override + public Point findCentroidByUuid(String uuid) { + String centroidWKT = locationRepository.findCentroidByUuid(uuid); // WKT ํ˜•์‹์œผ๋กœ ๋ฐ›์Œ + if (centroidWKT == null) { + return null; + } + + try { + WKTReader reader = new WKTReader(); + return (Point) reader.read(centroidWKT); + } catch (Exception e) { + throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ..", e); + } + } + + @Override + public Location saveLocation(String uuid, Integer memberId, Point point) { + return locationRepository.save(new Location(uuid, memberId, point)); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationRepository.java new file mode 100644 index 00000000..50148613 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationRepository.java @@ -0,0 +1,27 @@ +package com.kok.kokapi.centroid.adapter.outbound.persistence; + + +import com.kok.kokcore.application.domain.entity.Location; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.util.List; +import java.util.Optional; + + +public interface LocationRepository extends JpaRepository { + + /* ํ•ด๋‹น ์ฟผ๋ฆฌ๋Š” ํŠน์ • UUID๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ„์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ํ•ด๋‹น ์œ„์น˜๋“ค์˜ ์ค‘์‹ฌ์ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + ๋จผ์ €, SRID 4326 (์œ„๋„/๊ฒฝ๋„) ์ขŒํ‘œ๊ณ„๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ SRID 3857 (ํ‰๋ฉด ์ขŒํ‘œ๊ณ„)๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + ๋ณ€ํ™˜๋œ ์ขŒํ‘œ๋“ค์„ ST_Collect๋ฅผ ์ด์šฉํ•ด ํ•˜๋‚˜์˜ MULTIPOINT ํ˜•ํƒœ๋กœ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + ๋ณ‘ํ•ฉ๋œ MULTIPOINT์—์„œ ST_Centroid๋ฅผ ์‚ฌ์šฉํ•ด ์ค‘์‹ฌ์ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + ๊ณ„์‚ฐ๋œ ์ค‘์‹ฌ์ ์„ ๋‹ค์‹œ SRID 4326 (์œ„๋„/๊ฒฝ๋„)์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (์ง€๋„ API์™€ ํ˜ธํ™˜์„ฑ ์œ ์ง€) + ์ตœ์ข…์ ์œผ๋กœ ST_AsText๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ WKT(Well-Known Text) ํฌ๋งท์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. */ + + @Query(value = "SELECT ST_AsText(ST_Transform(ST_Centroid(ST_Collect(ST_Transform(point, 3857))), 4326)) FROM location WHERE uuid = :uuid", nativeQuery = true) + String findCentroidByUuid(@Param("uuid") String uuid); + + Optional findLocationByUuidAndMemberId(String uuid, Integer memberId); + + List findLocationsByUuid(String uuid); +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java new file mode 100644 index 00000000..f700c75c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java @@ -0,0 +1,51 @@ +package com.kok.kokapi.centroid.application.service; + +import com.kok.kokapi.centroid.adapter.outbound.persistence.LocationPersistenceAdapter; +import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.application.usecase.CreateLocationUsecase; +import com.kok.kokcore.application.usecase.ReadCentroidUsecase; +import com.kok.kokcore.application.usecase.ReadLocationUsecase; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class LocationService implements CreateLocationUsecase, ReadCentroidUsecase, ReadLocationUsecase { + + private final LocationPersistenceAdapter locationRepository; + + @Override + public Location createLocation(String uuid, Integer memberId, Point point) { + return locationRepository.saveLocation(uuid, memberId, point); + } + + @Override + @Transactional + public Location UpdateLocation(String uuid, Integer memberId, Point point) { + Location location = locationRepository.findLocationByUuidAndMemberId(uuid, memberId).orElseThrow( + () -> new RuntimeException("ํ•ด๋‹น Id์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") + ); + location.changePoint(point); + return location; + } + + @Override + public Point readCentroid(String uuid) { + return locationRepository.findCentroidByUuid(uuid); + } + + @Override + public Location readLocation(String uuid, Integer memberId) { + return locationRepository.findLocationByUuidAndMemberId(uuid, memberId).orElse(null); + } + + @Override + public List readLocations(String uuid) { + return locationRepository.findLocationsByUuid(uuid); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java new file mode 100644 index 00000000..a0e77924 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java @@ -0,0 +1,47 @@ +package com.kok.kokapi.config.geometry; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.locationtech.jts.geom.Point; +import org.springframework.data.util.Pair; + +import java.math.BigDecimal; +import java.util.List; + +@Configuration +public class GeometryConfig { + + @Bean + public GeometryFactory geometryFactory() { + return new GeometryFactory(); + } + + @Bean + public PointConverter pointConverter(GeometryFactory geometryFactory) { + return new PointConverter(geometryFactory); + } + + // Converter ํด๋ž˜์Šค + public static class PointConverter { + + private final GeometryFactory geometryFactory; + + public PointConverter(GeometryFactory geometryFactory) { + this.geometryFactory = geometryFactory; + } + + public Point fromCoordinates(BigDecimal latitude, BigDecimal longitude) { + Coordinate coordinate = new Coordinate(longitude.doubleValue(), latitude.doubleValue()); + Point point = geometryFactory.createPoint(coordinate); + point.setSRID(4326); // WGS84 ์ขŒํ‘œ๊ณ„ -> Kakao, Naver ๋“ฑ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ขŒํ‘œ๊ณ„ + return point; + } + + public Pair toCoordinates(Point point) { + return Pair.of(BigDecimal.valueOf(point.getY()), BigDecimal.valueOf(point.getX())); + } + } + +} diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 77c8d0cb..ac269390 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -11,6 +11,7 @@ spring: hibernate: show_sql: true format_sql: true + dialect: org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect open-in-view: false data: redis: diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 3a53d77f..a92fb368 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -11,6 +11,7 @@ spring: hibernate: show_sql: true format_sql: true + dialect: org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect open-in-view: false data: redis: diff --git a/kok-core/src/main/java/com/kok/kokcore/application/domain/entity/Location.java b/kok-core/src/main/java/com/kok/kokcore/application/domain/entity/Location.java new file mode 100644 index 00000000..f3ef7d49 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/domain/entity/Location.java @@ -0,0 +1,44 @@ +package com.kok.kokcore.application.domain.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.locationtech.jts.geom.Point; + + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "location" , + uniqueConstraints = { + @UniqueConstraint(columnNames = {"uuid", "member_id"}) + }) + +public class Location { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String uuid; + + @Column(nullable = false) + private Integer memberId; + + @Column(nullable = false, columnDefinition = "POINT SRID 4326") + private Point point; + + + public Location(String uuid, Integer memberId, Point point) { + this.uuid = uuid; + this.memberId = memberId; + this.point = point; + } + + // ๋”ํ‹ฐ์ฒดํ‚น + public void changePoint(Point point) { + this.point = point; + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java new file mode 100644 index 00000000..2db6486a --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.application.port.outbound; + +import com.kok.kokcore.application.domain.entity.Location; +import org.locationtech.jts.geom.Point; +import java.util.List; + +public interface ReadCentroidPort { + Point findCentroidByUuid(String uuid); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadLocationPort.java new file mode 100644 index 00000000..6e332e09 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadLocationPort.java @@ -0,0 +1,11 @@ +package com.kok.kokcore.application.port.outbound; + +import com.kok.kokcore.application.domain.entity.Location; + +import java.util.List; +import java.util.Optional; + +public interface ReadLocationPort { + Optional findLocationByUuidAndMemberId(String uuid, Integer memberId); + List findLocationsByUuid(String uuid); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/SaveLocationPort.java new file mode 100644 index 00000000..1c959797 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/SaveLocationPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.application.port.outbound; + +import com.kok.kokcore.application.domain.entity.Location; +import org.locationtech.jts.geom.Point; + +public interface SaveLocationPort { + Location saveLocation(String uuid, Integer memberId, Point point); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java new file mode 100644 index 00000000..f471da37 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.application.usecase; + +import com.kok.kokcore.application.domain.entity.Location; +import org.locationtech.jts.geom.Point; + + +public interface CreateLocationUsecase { + Location createLocation(String uuid, Integer memberId, Point point); + Location UpdateLocation(String uuid, Integer memberId, Point point); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java b/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java new file mode 100644 index 00000000..5e3ac8ab --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.application.usecase; + +import org.locationtech.jts.geom.Point; + +public interface ReadCentroidUsecase { + Point readCentroid(String uuid); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadLocationUsecase.java new file mode 100644 index 00000000..b8111748 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadLocationUsecase.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.application.usecase; + +import com.kok.kokcore.application.domain.entity.Location; + +import java.util.List; + +public interface ReadLocationUsecase { + Location readLocation(String uuid, Integer memberId); + List readLocations(String uuid); +} From d4737483a74dc525193441cc913282fa87a89baf Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Fri, 21 Feb 2025 00:17:56 +0900 Subject: [PATCH 031/163] :sparkles: [Feature/room] implement create room (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: implement room create feature - Added Room domain model and factory method - Added CreateRoomUseCase with persistence adapter. - Added RoomController to handle room creation request * :sparkles: feat: added RoomRepository and RedisRoomPersistenceAdapter * ๐Ÿ”งconfig: redis setting for standalone and sentinel * ๐Ÿ”งconfig: use default spring redis setting * ๐Ÿ”งconfig: change uuid to string * ๐Ÿ”งconfig: restructure config file * :sparkles: feat: implement room create feature - Added Room domain model and factory method - Added CreateRoomUseCase with persistence adapter. - Added RoomController to handle room creation request * :sparkles: feat: added RoomRepository and RedisRoomPersistenceAdapter * :fire: fire: Removed duplicate Redis config (Using develop config) * :recycle: refactor: Rename packages - inbound -> in - outbound -> out - rest -> web * :recycle: refactor: add request validation * :recycle: refactor: reactoring code * :recycle: refactor: reactoring code * :recycle: refactor: reactoring code * :recycle: refactor: modify GlobalExceptionHandler --------- Co-authored-by: minseokey Co-authored-by: MINSEOK LEE <91869302+minseokey@users.noreply.github.com> --- kok-api/build.gradle | 1 + .../adapter/in/web/HealthCheckController.java | 6 +-- .../service/HealthCheckService.java | 4 +- .../usecase/HealthCheckUseCase.java | 5 -- .../exception/GlobalExceptionHandler.java | 24 +++++++++ .../kokapi/config/usecase/UseCaseConfig.java | 15 ++++++ .../in/dto/request/CreateRoomRequest.java | 20 ++++++++ .../adapter/in/dto/response/RoomResponse.java | 21 ++++++++ .../room/adapter/in/web/RoomController.java | 35 +++++++++++++ .../RedisSaveRoomPersistenceAdapter.java | 31 ++++++++++++ .../room/application/service/RoomService.java | 20 ++++++++ kok-core/build.gradle | 2 + .../application/port/out/SaveRoomPort.java | 7 +++ .../java/com/kok/kokcore/domain/Room.java | 49 +++++++++++++++++++ .../kokcore/usecase/CreateRoomUseCase.java | 7 +++ 15 files changed, 236 insertions(+), 11 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/domain/Room.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java diff --git a/kok-api/build.gradle b/kok-api/build.gradle index 7f66ada7..c96a7e84 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation project(':kok-core') implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' compileOnly 'org.projectlombok:lombok' diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java index 8cf1f23c..3cb96136 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java +++ b/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java @@ -1,6 +1,6 @@ package com.kok.kokapi.adapter.in.web; -import com.kok.kokapi.application.usecase.HealthCheckUseCase; +import com.kok.kokapi.application.service.HealthCheckService; import com.kok.kokapi.common.response.ApiResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -10,10 +10,10 @@ @RequiredArgsConstructor public class HealthCheckController extends BaseController { - private final HealthCheckUseCase healthCheckUseCase; + private final HealthCheckService healthCheckService; @GetMapping("/health") public ApiResponseDto checkHealth() { - return ApiResponseDto.success(healthCheckUseCase.checkHealth()); + return ApiResponseDto.success(healthCheckService.checkHealth()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java b/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java index f53596d9..06adbde4 100644 --- a/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java +++ b/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java @@ -1,12 +1,10 @@ package com.kok.kokapi.application.service; -import com.kok.kokapi.application.usecase.HealthCheckUseCase; import org.springframework.stereotype.Service; @Service -public class HealthCheckService implements HealthCheckUseCase { +public class HealthCheckService { - @Override public String checkHealth() { return "OK"; } diff --git a/kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java b/kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java deleted file mode 100644 index e63a9fa6..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/application/usecase/HealthCheckUseCase.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.kok.kokapi.application.usecase; - -public interface HealthCheckUseCase { - String checkHealth(); -} diff --git a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java index 8cab5128..9951a59c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java @@ -2,13 +2,37 @@ import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.common.error.ErrorCode; +import jakarta.validation.ConstraintViolationException; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.util.stream.Collectors; + @RestControllerAdvice public class GlobalExceptionHandler { + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolationException(ConstraintViolationException ex) { + String message = ex.getConstraintViolations() + .stream() + .map(cv -> cv.getPropertyPath() + " " + cv.getMessage()) + .collect(Collectors.joining(", ")); + return ResponseEntity.badRequest() + .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors() + .stream() + .map(fe -> fe.getField() + " " + fe.getDefaultMessage()) + .collect(Collectors.joining(", ")); + return ResponseEntity.badRequest() + .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); + } + @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleBadRequestException(IllegalArgumentException ex) { return ResponseEntity.badRequest() diff --git a/kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java new file mode 100644 index 00000000..3e1889e0 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.config.usecase; + +import com.kok.kokapi.room.application.service.RoomService; +import com.kok.kokcore.application.port.out.SaveRoomPort; +import com.kok.kokcore.usecase.CreateRoomUseCase; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class UseCaseConfig { + @Bean + public CreateRoomUseCase createRoomUseCase(SaveRoomPort saveRoomPort) { + return new RoomService(saveRoomPort); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java new file mode 100644 index 00000000..66f37a7d --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java @@ -0,0 +1,20 @@ +package com.kok.kokapi.room.adapter.in.dto.request; + + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CreateRoomRequest( + @NotBlank(message = "๋ฐฉ ์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + @Size(max = 30, message = "๋ฐฉ ์ด๋ฆ„์€ ์ตœ๋Œ€ 30์ž๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") + String roomName, + + @Min(value = 2, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + Integer capacity, + + String hostProfile, + + @NotBlank(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + String password +) {} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomResponse.java new file mode 100644 index 00000000..65fe2f4c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomResponse.java @@ -0,0 +1,21 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.domain.Room; + +public record RoomResponse( + String id, + String roomName, + int capacity, + String hostProfile, + String roomLinkUrl +) { + public static RoomResponse from(Room room) { + return new RoomResponse( + room.getId(), + room.getRoomName(), + room.getCapacity(), + room.getHostProfile(), + room.getRoomLinkUrl() + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java new file mode 100644 index 00000000..1612213c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -0,0 +1,35 @@ +package com.kok.kokapi.room.adapter.in.web; + +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; +import com.kok.kokapi.room.adapter.in.dto.response.RoomResponse; +import com.kok.kokcore.domain.Room; +import com.kok.kokcore.usecase.CreateRoomUseCase; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/rooms") +@RequiredArgsConstructor +public class RoomController { + + private final CreateRoomUseCase createRoomUseCase; + + @PostMapping("/create") + public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { + Room room = createRoomUseCase.createRoom( + request.roomName(), + request.capacity(), + request.hostProfile(), + request.password() + ); + + var response = RoomResponse.from(room); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java new file mode 100644 index 00000000..ec60cd0b --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import com.kok.kokcore.application.port.out.SaveRoomPort; +import com.kok.kokcore.domain.Room; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; + +@Repository +public class RedisSaveRoomPersistenceAdapter implements SaveRoomPort { + + private static final String ROOM_KEY_PREFIX = "room"; + private static final Duration ROOM_TTL = Duration.ofDays(3); + private final RedisTemplate redisTemplate; + + public RedisSaveRoomPersistenceAdapter(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public Room save(Room room) { + String key = buildKey(room.getId()); + redisTemplate.opsForValue().set(key, room, ROOM_TTL); + return room; + } + + private String buildKey(String roomId) { + return ROOM_KEY_PREFIX + ":" + roomId; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java new file mode 100644 index 00000000..26ce42f7 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java @@ -0,0 +1,20 @@ +package com.kok.kokapi.room.application.service; + +import com.kok.kokcore.application.port.out.SaveRoomPort; +import com.kok.kokcore.domain.Room; +import com.kok.kokcore.usecase.CreateRoomUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RoomService implements CreateRoomUseCase { + + private final SaveRoomPort saveRoomPort; + + @Override + public Room createRoom(String roomName, int capacity, String hostProfile, String password) { + Room room = Room.create(roomName, capacity, hostProfile, password); + return saveRoomPort.save(room); + } +} diff --git a/kok-core/build.gradle b/kok-core/build.gradle index c58c530f..1de36f58 100644 --- a/kok-core/build.gradle +++ b/kok-core/build.gradle @@ -19,6 +19,8 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java new file mode 100644 index 00000000..2f3e17a2 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.application.port.out; + +import com.kok.kokcore.domain.Room; + +public interface SaveRoomPort { + Room save(Room room); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/domain/Room.java new file mode 100644 index 00000000..43b08b97 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/domain/Room.java @@ -0,0 +1,49 @@ +package com.kok.kokcore.domain; + +import lombok.*; + +import java.io.Serializable; +import java.util.UUID; + +@Getter +@ToString +@EqualsAndHashCode +public class Room implements Serializable { + + public static final int REQUIRED_CAPACITY = 2; + public static final String ROOM_LINK_URL = "kakao://app/room?roomId="; + + private final String id; // ์•ฝ์†๋ฐฉ ID (UUID) + private final String roomName; // ์•ฝ์†๋ฐฉ ์ด๋ฆ„ + private final int capacity; // ์ฐธ์—ฌ์ธ์› ์ˆ˜ (์ตœ์†Œ 2๋ช… ์ด์ƒ) + private final String hostProfile; // ๋ฐฉ์žฅ ํ”„๋กœํ•„ ์ •๋ณด + private final String password; // ๋ฐฉ ๋น„๋ฐ€๋ฒˆํ˜ธ (์˜ต์…˜) + private final String roomLinkUrl; // ์ƒ์„ฑ๋œ ์•ฝ์†๋ฐฉ ์ž…์žฅ ๋งํฌ + + public Room(String id, String roomName, int capacity, String hostProfile, + String password, String roomLinkUrl) { + this.id = id; + this.roomName = roomName; + this.capacity = capacity; + this.hostProfile = hostProfile; + this.password = password; + this.roomLinkUrl = roomLinkUrl; + } + + public static Room create(String roomName, int capacity, String hostProfile, String password) { + if (roomName == null || roomName.trim().isEmpty()) { + throw new IllegalArgumentException("Room name is required"); + } + if (capacity < REQUIRED_CAPACITY) { + throw new IllegalArgumentException("At least 2 participants are required"); + } + if (hostProfile == null || hostProfile.trim().isEmpty()) { + throw new IllegalArgumentException("Host profile is required"); + } + + String roomId = UUID.randomUUID().toString(); + String roomLinkUrl = ROOM_LINK_URL + roomId; + + return new Room(roomId, roomName, capacity, hostProfile, password, roomLinkUrl); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java new file mode 100644 index 00000000..a26b2788 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.usecase; + +import com.kok.kokcore.domain.Room; + +public interface CreateRoomUseCase { + Room createRoom(String roomName, int capacity, String hostProfile, String password); +} From b2c59705af2d3a7155756bdb81a589f7d30cef3e Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:15:48 +0900 Subject: [PATCH 032/163] =?UTF-8?q?=E2=9C=A8=20[Feature/subway]=20load=20a?= =?UTF-8?q?nd=20save=20station=20data=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :wrench: config: add jpa dependency * :sparkles: feat: create station domain * :sparkles: feat: define read stations port * :sparkles: feat: read station data from external api * :sparkles: feat: define saving station data port * :sparkles: feat: implement saving station data port * :sparkles: feat: define save station usecase * :sparkles: feat: implement save station usecase * :sparkles: feat: save station data when application start on dev environment * :recycle: refactor: change stations config active profile * :bug: fix: fix constructor order * :recycle: refactor: change method name * :bug: fix: configuration entity scan target package * :wrench: config: add flyway dependency * :wrench: config: enable flyway on dev and prod environment * :recycle: refactor: rename external api port class and method * :wrench: config: change test dependency * :sparkles: feat: create jpql checking if data exists * :sparkles: feat: set up base station data only if no data exists * :recycle: refactor: move config package into common package * :recycle: refactor: checking if station exists in station Service * :sparkles: feat: implement database cleaner for test isolation * :white_check_mark: test: test for setting up stations data only if not exists * :sparkles: feat: ddl script for station table * :sparkles: feat: set up transaction * :recycle: refactor: optimize save all by jdbc batch insert * :recycle: refactor: remove health check usecase * :recycle: refactor: move config package level * :wrench: chore: remove /out from gitignore * :recycle: refactor: rename outbound package to out * :recycle: refactor: seperate generated primary key for station domain * :sparkles: feat: add priority field on station * :sparkles: feat: add validation if no station data to save --- .gitignore | 2 - build.gradle | 6 +- .../java/com/kok/kokapi/config/JpaConfig.java | 10 ++++ .../adapter/out/external/StationClient.java | 59 ++++++++++++++++++ .../out/external/StationClientProperties.java | 15 +++++ .../adapter/out/external/dto/Result.java | 12 ++++ .../out/external/dto/StationResponse.java | 22 +++++++ .../out/external/dto/StationResponses.java | 17 ++++++ .../out/external/dto/SubwayStationMaster.java | 15 +++++ .../StationPersistenceAdapter.java | 60 +++++++++++++++++++ .../out/persistence/StationRepository.java | 11 ++++ .../application/config/StationsConfig.java | 22 +++++++ .../application/service/StationService.java | 33 ++++++++++ .../src/main/resources/application-dev.yml | 11 ++++ .../src/main/resources/application-prod.yml | 11 ++++ .../main/resources/db/V1__init_station.sql | 11 ++++ .../common/config/DataJpaTestConfig.java | 10 ++++ .../common/config/ServiceTestConfig.java | 15 +++++ .../kokapi/common/util/DatabaseCleaner.java | 49 +++++++++++++++ .../common/util/DatabaseCleanerExtension.java | 15 +++++ .../out/external/FakeStationClient.java | 17 ++++++ .../persistence/StationRepositoryTest.java | 43 +++++++++++++ .../service/StationServiceTest.java | 49 +++++++++++++++ .../port/out/LoadStationsPort.java | 9 +++ .../port/out/ReadStationsPort.java | 6 ++ .../port/out/SaveStationsPort.java | 9 +++ .../usecase/SaveStationUseCase.java | 6 ++ .../station/domain/entity/Station.java | 56 +++++++++++++++++ 28 files changed, 598 insertions(+), 3 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClientProperties.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/Result.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/SubwayStationMaster.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java create mode 100644 kok-api/src/main/resources/db/V1__init_station.sql create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/config/DataJpaTestConfig.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java diff --git a/.gitignore b/.gitignore index 33b15727..95e64224 100644 --- a/.gitignore +++ b/.gitignore @@ -120,7 +120,6 @@ cmake-build-*/ *.iws # IntelliJ -out/ .idea/ # mpeltonen/sbt-idea plugin @@ -321,7 +320,6 @@ gradle-app.setting ======= */build/ -out/ # ํ™˜๊ฒฝ ์„ค์ • ๋ฐ OS ### *.log diff --git a/build.gradle b/build.gradle index cd57cbe5..0dff5ba0 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ subprojects { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok:1.18.34' @@ -41,7 +42,10 @@ subprojects { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' - testImplementation 'junit:junit:4.13.1' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'org.springframework.boot:spring-boot-starter-test' } } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java new file mode 100644 index 00000000..f45fa88f --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java @@ -0,0 +1,10 @@ +package com.kok.kokapi.config; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EntityScan(basePackages = {"com.kok.kokcore"}) +public class JpaConfig { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java new file mode 100644 index 00000000..7657e954 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java @@ -0,0 +1,59 @@ +package com.kok.kokapi.station.adapter.out.external; + +import com.kok.kokapi.station.adapter.out.external.dto.StationResponses; +import com.kok.kokcore.station.application.port.out.LoadStationsPort; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; +import java.util.StringJoiner; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component +@EnableConfigurationProperties(StationClientProperties.class) +public class StationClient implements LoadStationsPort { + + private static final String DELIMITER = "/"; + + private final RestClient restClient; + private final StationClientProperties properties; + + public StationClient(StationClientProperties properties) { + this.properties = properties; + this.restClient = getRestClient(); + } + + public RestClient getRestClient() { + return RestClient.builder() + .requestFactory(getRequestFactory()) + .baseUrl(properties.baseUrl()) + .build(); + } + + private ClientHttpRequestFactory getRequestFactory() { + return ClientHttpRequestFactoryBuilder.detect() + .build(ClientHttpRequestFactorySettings.defaults()); + } + + @Override + public List loadAllStations() { + StationResponses responses = restClient.get() + .uri(getTargetUri()) + .retrieve() + .body(StationResponses.class); + return responses.toStations(); + } + + public String getTargetUri() { + StringJoiner stringJoiner = new StringJoiner(DELIMITER); + return stringJoiner.add(properties.secretKey()) + .add(properties.format()) + .add(properties.service()) + .add(properties.startIdx()) + .add(properties.endIdx()) + .toString(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClientProperties.java new file mode 100644 index 00000000..151d683d --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClientProperties.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.station.adapter.out.external; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "station") +public record StationClientProperties( + String baseUrl, + String secretKey, + String format, + String service, + String startIdx, + String endIdx +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/Result.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/Result.java new file mode 100644 index 00000000..fd5599c4 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/Result.java @@ -0,0 +1,12 @@ +package com.kok.kokapi.station.adapter.out.external.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record Result( + @JsonProperty("CODE") + String code, + @JsonProperty("MESSAGE") + String message +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java new file mode 100644 index 00000000..f0c3f2cd --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java @@ -0,0 +1,22 @@ +package com.kok.kokapi.station.adapter.out.external.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.kok.kokcore.station.domain.entity.Station; + +public record StationResponse( + @JsonProperty("BLDN_ID") + long id, + @JsonProperty("BLDN_NM") + String name, + @JsonProperty("ROUTE") + String route, + @JsonProperty("LAT") + String latitude, + @JsonProperty("LOT") + String longitude +) { + + public Station toStation() { + return new Station(id, name, route, latitude, longitude); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java new file mode 100644 index 00000000..c744d132 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java @@ -0,0 +1,17 @@ +package com.kok.kokapi.station.adapter.out.external.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public record StationResponses( + @JsonProperty("subwayStationMaster") + SubwayStationMaster subwayStationMaster +) { + + public List toStations() { + return subwayStationMaster.row().stream() + .map(StationResponse::toStation) + .toList(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/SubwayStationMaster.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/SubwayStationMaster.java new file mode 100644 index 00000000..f1f992a4 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/SubwayStationMaster.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.station.adapter.out.external.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public record SubwayStationMaster( + @JsonProperty("list_total_count") + long listTotalCount, + @JsonProperty("RESULT") + Result result, + @JsonProperty("row") + List row +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java new file mode 100644 index 00000000..53160d5a --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -0,0 +1,60 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import com.kok.kokcore.station.application.port.out.ReadStationsPort; +import com.kok.kokcore.station.application.port.out.SaveStationsPort; +import com.kok.kokcore.station.domain.entity.Station; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort { + + private static final String INSERT_SQL = """ + INSERT INTO station (station_id, name, route, latitude, longitude, priority) + VALUES (?, ?, ?, ?, ?, ?) + """; + + private final StationRepository stationRepository; + private final JdbcTemplate jdbcTemplate; + + @Override + public void saveStations(List stations) { + if (stations.isEmpty()) { + log.info("No stations to save."); + return; + } + int[] batches = jdbcTemplate.batchUpdate(INSERT_SQL, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Station station = stations.get(i); + ps.setLong(1, station.getStationId()); + ps.setString(2, station.getName()); + ps.setString(3, station.getRoute()); + ps.setBigDecimal(4, station.getLatitude()); + ps.setBigDecimal(5, station.getLongitude()); + ps.setLong(6, station.getPriority()); + } + + @Override + public int getBatchSize() { + return stations.size(); + } + }); + log.info("Successfully saved a total of {} stations out of {}.", + Arrays.stream(batches).sum(), stations.size()); + } + + @Override + public boolean hasNoStations() { + return !stationRepository.existsAny(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java new file mode 100644 index 00000000..9ec8d995 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -0,0 +1,11 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import com.kok.kokcore.station.domain.entity.Station; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface StationRepository extends JpaRepository { + + @Query("SELECT EXISTS (SELECT 1 FROM Station)") + boolean existsAny(); +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java b/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java new file mode 100644 index 00000000..c79d6c50 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java @@ -0,0 +1,22 @@ +package com.kok.kokapi.station.application.config; + +import com.kok.kokapi.station.application.service.StationService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +public class StationsConfig { + + @Bean + CommandLineRunner initStations(StationService stationService) { + return new CommandLineRunner() { + @Override + @Transactional + public void run(String... args) { + stationService.saveStations(); + } + }; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java new file mode 100644 index 00000000..88c631d9 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -0,0 +1,33 @@ +package com.kok.kokapi.station.application.service; + +import com.kok.kokcore.station.application.port.out.LoadStationsPort; +import com.kok.kokcore.station.application.port.out.ReadStationsPort; +import com.kok.kokcore.station.application.port.out.SaveStationsPort; +import com.kok.kokcore.station.application.usecase.SaveStationUseCase; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class StationService implements SaveStationUseCase { + + private final LoadStationsPort loadStationsPort; + private final SaveStationsPort saveStationsPort; + private final ReadStationsPort readStationsPort; + + @Override + @Transactional + public void saveStations() { + if(hasNoStations()) { + List stations = loadStationsPort.loadAllStations(); + saveStationsPort.saveStations(stations); + } + } + + private boolean hasNoStations() { + return readStationsPort.hasNoStations(); + } +} diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 77c8d0cb..db3a71cf 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -12,6 +12,9 @@ spring: show_sql: true format_sql: true open-in-view: false + defer-datasource-initialization: true + flyway: + enabled: false data: redis: host: ${REDIS_HOST} @@ -21,3 +24,11 @@ springdoc: default-produces-media-type: application/json;charset=UTF-8 swagger-ui: path: /swagger +# open-api: http://data.seoul.go.kr/dataList/OA-21232/S/1/datasetView.do +station: + base-url: http://openapi.seoul.go.kr:8088 + secret-key: ${STATION_SECRET_KEY} + service: subwayStationMaster + format: json + start-idx: 1 + end-idx: 1000 diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 3a53d77f..cd2b535c 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -12,6 +12,9 @@ spring: show_sql: true format_sql: true open-in-view: false + defer-datasource-initialization: true + flyway: + enabled: false data: redis: host: ${REDIS_HOST} @@ -29,3 +32,11 @@ springdoc: swagger-ui: enabled: false path: /swagger +# open-api: http://data.seoul.go.kr/dataList/OA-21232/S/1/datasetView.do +station: + base-url: http://openapi.seoul.go.kr:8088 + secret-key: ${STATION_SECRET_KEY} + service: subwayStationMaster + format: json + start-idx: 1 + end-idx: 1000 diff --git a/kok-api/src/main/resources/db/V1__init_station.sql b/kok-api/src/main/resources/db/V1__init_station.sql new file mode 100644 index 00000000..4664d351 --- /dev/null +++ b/kok-api/src/main/resources/db/V1__init_station.sql @@ -0,0 +1,11 @@ +CREATE TABLE station +( + id BIGINT NOT NULL AUTO_INCREMENT, + station_id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + route VARCHAR(255) NOT NULL, + latitude DECIMAL(16, 14) NOT NULL, + longitude DECIMAL(17, 14) NOT NULL, + priority BIGINT NOT NULL, + PRIMARY KEY (id) +); diff --git a/kok-api/src/test/java/com/kok/kokapi/common/config/DataJpaTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/common/config/DataJpaTestConfig.java new file mode 100644 index 00000000..9e7e1d44 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/config/DataJpaTestConfig.java @@ -0,0 +1,10 @@ +package com.kok.kokapi.common.config; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.context.TestConfiguration; + +@TestConfiguration +@EntityScan(basePackages = "com.kok.kokcore") +public class DataJpaTestConfig { + +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java new file mode 100644 index 00000000..56bcc61a --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.common.config; + +import com.kok.kokapi.station.adapter.out.external.FakeStationClient; +import com.kok.kokcore.station.application.port.out.LoadStationsPort; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class ServiceTestConfig { + + @Bean + public LoadStationsPort loadStationsPort() { + return new FakeStationClient(); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java new file mode 100644 index 00000000..ada54813 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java @@ -0,0 +1,49 @@ +package com.kok.kokapi.common.util; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class DatabaseCleaner { + + @PersistenceContext + private EntityManager entityManager; + + @Transactional + public void cleanUp() { + entityManager.flush(); + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + for (String tableName : getTableNames()) { + entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); + entityManager.createNativeQuery( + "ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1") + .executeUpdate(); + } + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } + + private List getTableNames() { + return entityManager.getMetamodel().getEntities().stream() + .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null) + .map(entityType -> camelToSnake(entityType.getName())) + .toList(); + } + + private String camelToSnake(String camel) { + StringBuilder snake = new StringBuilder(); + for (char c : camel.toCharArray()) { + if (Character.isUpperCase(c)) { + snake.append("_"); + } + snake.append(Character.toLowerCase(c)); + } + if (snake.charAt(0) == '_') { + snake.deleteCharAt(0); + } + return snake.toString(); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java new file mode 100644 index 00000000..8c67e1a7 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.common.util; + +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +public class DatabaseCleanerExtension implements BeforeEachCallback { + + @Override + public void beforeEach(ExtensionContext context) { + DatabaseCleaner databaseCleaner = SpringExtension.getApplicationContext(context) + .getBean(DatabaseCleaner.class); + databaseCleaner.cleanUp(); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java new file mode 100644 index 00000000..64b228c9 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java @@ -0,0 +1,17 @@ +package com.kok.kokapi.station.adapter.out.external; + +import com.kok.kokcore.station.application.port.out.LoadStationsPort; +import com.kok.kokcore.station.domain.entity.Station; +import java.math.BigDecimal; +import java.util.List; + +public class FakeStationClient implements LoadStationsPort { + + @Override + public List loadAllStations() { + return List.of( + new Station(1L, "์„œ์šธ์—ญ", "1ํ˜ธ์„ ", BigDecimal.ONE, BigDecimal.ONE, 0), + new Station(2L, "ํ•ฉ์ •์—ญ", "2ํ˜ธ์„ ", BigDecimal.TEN, BigDecimal.TEN, 5) + ); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java new file mode 100644 index 00000000..e1d68399 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java @@ -0,0 +1,43 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.kok.kokapi.common.config.DataJpaTestConfig; +import com.kok.kokcore.station.domain.entity.Station; +import java.math.BigDecimal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import({DataJpaTestConfig.class}) +class StationRepositoryTest { + + @Autowired + private StationRepository stationRepository; + + @DisplayName("๋ฐ์ดํ„ฐ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์กด์žฌํ•œ๋‹ค๋ฉด, ์ฐธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void existsAny() { + // given + stationRepository.save(new Station(1L, "์„œ์šธ์—ญ", "1ํ˜ธ์„ ", BigDecimal.ONE, BigDecimal.ONE, 0)); + + // when + boolean result = stationRepository.existsAny(); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("๋ฐ์ดํ„ฐ๊ฐ€ ํ•˜๋‚˜๋„ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, ๊ฑฐ์ง“์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void notExistsAny() { + // when + boolean result = stationRepository.existsAny(); + + // then + assertThat(result).isFalse(); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java new file mode 100644 index 00000000..098bd1f5 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java @@ -0,0 +1,49 @@ +package com.kok.kokapi.station.application.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kok.kokapi.common.config.ServiceTestConfig; +import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.station.adapter.out.persistence.StationRepository; +import com.kok.kokcore.station.domain.entity.Station; +import java.math.BigDecimal; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +@ExtendWith(DatabaseCleanerExtension.class) +@Import({ServiceTestConfig.class}) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +class StationServiceTest { + + @Autowired + private StationRepository stationRepository; + @Autowired + private StationService stationService; + + @DisplayName("์ €์žฅ๋œ ์ง€ํ•˜์ฒ  ์ •๋ณด๊ฐ€ ์—†๋‹ค๋ฉด, ์ง€ํ•˜์ฒ  ์ •๋ณด(์ด 2๊ฐœ)๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ์ €์žฅํ•œ๋‹ค.") + @Test + void saveStations() { + // when + stationService.saveStations(); + + // then + assertThat(stationRepository.findAll()).hasSize(2); + } + + @DisplayName("์ด๋ฏธ ์ง€ํ•˜์ฒ  ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋‹ค๋ฉด, ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค.") + @Test + void doesNotSaveStationsIfAlreadyExists() { + //given + stationRepository.save(new Station(1L, "์„œ์šธ์—ญ", "1ํ˜ธ์„ ", BigDecimal.ONE, BigDecimal.ONE, 0)); + + // when + stationService.saveStations(); + + // then + assertThat(stationRepository.findAll()).hasSize(1); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java new file mode 100644 index 00000000..52c40188 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.application.port.out; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface LoadStationsPort { + + List loadAllStations(); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java new file mode 100644 index 00000000..f18be07d --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.station.application.port.out; + +public interface ReadStationsPort { + + boolean hasNoStations(); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java new file mode 100644 index 00000000..b603ebe6 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.application.port.out; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface SaveStationsPort { + + void saveStations(List stations); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java new file mode 100644 index 00000000..baf08bb8 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.station.application.usecase; + +public interface SaveStationUseCase { + + void saveStations(); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java new file mode 100644 index 00000000..1ba97a28 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java @@ -0,0 +1,56 @@ +package com.kok.kokcore.station.domain.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.math.BigDecimal; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Station { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private Long stationId; + @Column(nullable = false) + private String name; + @Column(nullable = false) + private String route; + @Column(nullable = false, columnDefinition = "DECIMAL(16, 14)") + private BigDecimal latitude; + @Column(nullable = false, columnDefinition = "DECIMAL(17, 14)") + private BigDecimal longitude; + @Column(nullable = false) + private Long priority; + + public Station(Long id, Long stationId, String name, String route, BigDecimal latitude, + BigDecimal longitude) { + this.id = id; + this.stationId = stationId; + this.name = name; + this.route = route; + this.latitude = latitude; + this.longitude = longitude; + } + + public Station(Long stationId, String name, String route, BigDecimal latitude, BigDecimal longitude, long priority) { + this.stationId = stationId; + this.name = name; + this.route = route; + this.latitude = latitude; + this.longitude = longitude; + this.priority = priority; + } + + public Station(Long stationId, String name, String route, String latitude, String longitude) { + this(stationId, name, route, new BigDecimal(latitude),new BigDecimal(longitude), 0); + } +} From 5840f615412549ea1cae5305fe711951b318af93 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 25 Feb 2025 23:57:47 +0900 Subject: [PATCH 033/163] =?UTF-8?q?=F0=9F=94=A7config:=20fix=20deploy=20in?= =?UTF-8?q?fo=20for=20NCP=20enviroment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 1 + .github/workflows/kok-prod-CD.yml | 1 + .gitignore | 5 +++-- infra/docker-compose-blue.yml | 2 ++ infra/docker-compose-dev.yml | 2 ++ infra/docker-compose-green.yml | 2 ++ infra/docker-compose-nginx.yml | 2 ++ .../main/java/com/kok/kokapi/KokApiApplication.java | 2 -- .../adapter/{inbound => in}/dto/LocationRequest.java | 2 +- .../{inbound => in}/rest/LocationController.java | 8 ++++---- .../{outbound => out}/dto/CentroidResponse.java | 2 +- .../{outbound => out}/dto/LocationResponse.java | 2 +- .../persistence/LocationPersistenceAdapter.java | 8 ++++---- .../persistence/LocationRepository.java | 2 +- .../centroid/application/service/LocationService.java | 2 +- .../src/main/java/com/kok/kokapi/config/JpaConfig.java | 10 ++++++++++ kok-api/src/main/resources/application-dev.yml | 10 ++++++++-- kok-api/src/main/resources/application-prod.yml | 9 +++++++-- .../kokcore/application/port/out/ReadCentroidPort.java | 7 +++++++ .../port/{outbound => out}/ReadLocationPort.java | 2 +- .../port/{outbound => out}/SaveLocationPort.java | 2 +- .../application/port/outbound/ReadCentroidPort.java | 9 --------- 22 files changed, 60 insertions(+), 32 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{inbound => in}/dto/LocationRequest.java (76%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{inbound => in}/rest/LocationController.java (94%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{outbound => out}/dto/CentroidResponse.java (85%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{outbound => out}/dto/LocationResponse.java (86%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{outbound => out}/persistence/LocationPersistenceAdapter.java (84%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{outbound => out}/persistence/LocationRepository.java (95%) create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadCentroidPort.java rename kok-core/src/main/java/com/kok/kokcore/application/port/{outbound => out}/ReadLocationPort.java (84%) rename kok-core/src/main/java/com/kok/kokcore/application/port/{outbound => out}/SaveLocationPort.java (80%) delete mode 100644 kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index dad1c19c..63236d25 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -27,6 +27,7 @@ jobs: with: host: ${{ secrets.NCP_HOST }} username: ${{ secrets.NCP_USER }} + password: ${{ secrets.NCP_PASSWORD }} key: ${{ secrets.NCP_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 2ab54041..5c27a8fb 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -26,6 +26,7 @@ jobs: with: host: ${{ secrets.NCP_HOST }} username: ${{ secrets.NCP_USER }} + password: ${{ secrets.NCP_PASSWORD }} key: ${{ secrets.NCP_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} diff --git a/.gitignore b/.gitignore index a0feda54..1d8f5f99 100644 --- a/.gitignore +++ b/.gitignore @@ -120,7 +120,6 @@ cmake-build-*/ *.iws # IntelliJ -out/ .idea/ # mpeltonen/sbt-idea plugin @@ -321,7 +320,6 @@ gradle-app.setting ======= */build/ -out/ # ํ™˜๊ฒฝ ์„ค์ • ๋ฐ OS ### *.log @@ -330,5 +328,8 @@ out/ *.class *.jar +## ํ‚ค ## +*.pem + **/application-local.yml /infra/docker-compose-local.yml diff --git a/infra/docker-compose-blue.yml b/infra/docker-compose-blue.yml index cfe4a707..794ecbdf 100644 --- a/infra/docker-compose-blue.yml +++ b/infra/docker-compose-blue.yml @@ -5,6 +5,8 @@ services: container_name: kok-blue image: ${DOCKERHUB_USERNAME}/kok_prod:latest restart: always + env_file: + - .env environment: - SPRING_PROFILES_ACTIVE=prod ports: diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml index 842f4f10..1150b1ea 100644 --- a/infra/docker-compose-dev.yml +++ b/infra/docker-compose-dev.yml @@ -5,6 +5,8 @@ services: container_name: kok-dev image: ${DOCKERHUB_USERNAME}/kok_dev:latest restart: always + env_file: + - .env environment: - SPRING_PROFILES_ACTIVE=dev ports: diff --git a/infra/docker-compose-green.yml b/infra/docker-compose-green.yml index 9e8890d7..1cc9d72e 100644 --- a/infra/docker-compose-green.yml +++ b/infra/docker-compose-green.yml @@ -5,6 +5,8 @@ services: container_name: kok-green image: ${DOCKERHUB_USERNAME}/kok_prod:latest restart: always + env_file: + - .env environment: - SPRING_PROFILES_ACTIVE=prod ports: diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index 065b1c59..b8f00e2f 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -7,6 +7,8 @@ services: restart: always volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + env_file: + - .env ports: - "80:80" networks: diff --git a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java index bfb35c65..5ec5598c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java +++ b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java @@ -6,8 +6,6 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication -@EnableJpaRepositories(basePackages = "com.kok.kokapi.*") -@EntityScan(basePackages = "com.kok.kokcore.application.domain.entity") public class KokApiApplication { public static void main(String[] args) { diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/dto/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java similarity index 76% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/dto/LocationRequest.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java index df5b4e71..c85d07b3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/dto/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.inbound.dto; +package com.kok.kokapi.centroid.adapter.in.dto; import java.math.BigDecimal; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java similarity index 94% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/rest/LocationController.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index 06c77f7c..56432ebc 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/inbound/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -1,8 +1,8 @@ -package com.kok.kokapi.centroid.adapter.inbound.rest; +package com.kok.kokapi.centroid.adapter.in.rest; -import com.kok.kokapi.centroid.adapter.inbound.dto.LocationRequest; -import com.kok.kokapi.centroid.adapter.outbound.dto.CentroidResponse; -import com.kok.kokapi.centroid.adapter.outbound.dto.LocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.LocationRequest; +import com.kok.kokapi.centroid.adapter.out.dto.CentroidResponse; +import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.geometry.GeometryConfig; import com.kok.kokcore.application.domain.entity.Location; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java similarity index 85% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/CentroidResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java index d8565f00..5447f876 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.outbound.dto; +package com.kok.kokapi.centroid.adapter.out.dto; import java.math.BigDecimal; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java similarity index 86% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/LocationResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java index d45539e4..769caee6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/dto/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.outbound.dto; +package com.kok.kokapi.centroid.adapter.out.dto; import java.math.BigDecimal; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java similarity index 84% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationPersistenceAdapter.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index d79fdb98..aaad08bb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -1,10 +1,10 @@ -package com.kok.kokapi.centroid.adapter.outbound.persistence; +package com.kok.kokapi.centroid.adapter.out.persistence; import com.kok.kokcore.application.domain.entity.Location; -import com.kok.kokcore.application.port.outbound.ReadCentroidPort; -import com.kok.kokcore.application.port.outbound.ReadLocationPort; -import com.kok.kokcore.application.port.outbound.SaveLocationPort; +import com.kok.kokcore.application.port.out.ReadCentroidPort; +import com.kok.kokcore.application.port.out.ReadLocationPort; +import com.kok.kokcore.application.port.out.SaveLocationPort; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Repository; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java similarity index 95% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationRepository.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 50148613..9a9597e1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/outbound/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.outbound.persistence; +package com.kok.kokapi.centroid.adapter.out.persistence; import com.kok.kokcore.application.domain.entity.Location; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java index f700c75c..ec1b0a1d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java @@ -1,6 +1,6 @@ package com.kok.kokapi.centroid.application.service; -import com.kok.kokapi.centroid.adapter.outbound.persistence.LocationPersistenceAdapter; +import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; import com.kok.kokcore.application.domain.entity.Location; import com.kok.kokcore.application.usecase.CreateLocationUsecase; import com.kok.kokcore.application.usecase.ReadCentroidUsecase; diff --git a/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java new file mode 100644 index 00000000..f45fa88f --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java @@ -0,0 +1,10 @@ +package com.kok.kokapi.config; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EntityScan(basePackages = {"com.kok.kokcore"}) +public class JpaConfig { + +} diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index ac269390..30283f70 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -15,8 +15,14 @@ spring: open-in-view: false data: redis: - host: ${REDIS_HOST} - port: ${REDIS_PORT} + cluster: + nodes: + - ${REDIS_HOST1}:${REDIS_PORT} + - ${REDIS_HOST2}:${REDIS_PORT} + - ${REDIS_HOST3}:${REDIS_PORT} + max-redirects: 3 + timeout: 5000 + springdoc: default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index a92fb368..84a92568 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -15,8 +15,13 @@ spring: open-in-view: false data: redis: - host: ${REDIS_HOST} - port: ${REDIS_PORT} + cluster: + nodes: + - ${REDIS_HOST1}:${REDIS_PORT} + - ${REDIS_HOST2}:${REDIS_PORT} + - ${REDIS_HOST3}:${REDIS_PORT} + max-redirects: 3 + timeout: 5000 # sentinel: # master: ${REDIS_SENTINEL_MASTER} # nodes: diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadCentroidPort.java new file mode 100644 index 00000000..10dc7fd7 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadCentroidPort.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.application.port.out; + +import org.locationtech.jts.geom.Point; + +public interface ReadCentroidPort { + Point findCentroidByUuid(String uuid); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadLocationPort.java similarity index 84% rename from kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadLocationPort.java rename to kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadLocationPort.java index 6e332e09..2e52c7e9 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadLocationPort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.application.port.outbound; +package com.kok.kokcore.application.port.out; import com.kok.kokcore.application.domain.entity.Location; diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveLocationPort.java similarity index 80% rename from kok-core/src/main/java/com/kok/kokcore/application/port/outbound/SaveLocationPort.java rename to kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveLocationPort.java index 1c959797..40c56d58 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/SaveLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveLocationPort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.application.port.outbound; +package com.kok.kokcore.application.port.out; import com.kok.kokcore.application.domain.entity.Location; import org.locationtech.jts.geom.Point; diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java deleted file mode 100644 index 2db6486a..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/application/port/outbound/ReadCentroidPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.kok.kokcore.application.port.outbound; - -import com.kok.kokcore.application.domain.entity.Location; -import org.locationtech.jts.geom.Point; -import java.util.List; - -public interface ReadCentroidPort { - Point findCentroidByUuid(String uuid); -} From a2a68a1ec82e6dcd4f116d02b0ca7ae8fff5948d Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 27 Feb 2025 14:20:55 +0900 Subject: [PATCH 034/163] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20add=20req?= =?UTF-8?q?uest=20validaton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../centroid/adapter/in/dto/LocationRequest.java | 16 ++++++++++++++++ .../adapter/in/rest/LocationController.java | 8 +++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java index c85d07b3..fac6e30b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java @@ -1,11 +1,27 @@ package com.kok.kokapi.centroid.adapter.in.dto; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; + import java.math.BigDecimal; public record LocationRequest( + + @NotBlank(message = "uuid๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") String uuid, + + @NotBlank(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") Integer memberId, + + @NotBlank(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") BigDecimal latitude, + + @NotBlank(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") BigDecimal longitude ) { } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index 56432ebc..f74e2484 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -1,5 +1,6 @@ package com.kok.kokapi.centroid.adapter.in.rest; +import com.kok.kokapi.adapter.in.web.BaseController; import com.kok.kokapi.centroid.adapter.in.dto.LocationRequest; import com.kok.kokapi.centroid.adapter.out.dto.CentroidResponse; import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; @@ -9,6 +10,7 @@ import com.kok.kokcore.application.usecase.CreateLocationUsecase; import com.kok.kokcore.application.usecase.ReadCentroidUsecase; import com.kok.kokcore.application.usecase.ReadLocationUsecase; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; import org.springframework.http.ResponseEntity; @@ -20,7 +22,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/location") -public class LocationController { +public class LocationController extends BaseController { private final CreateLocationUsecase createLocationUsecase; private final ReadCentroidUsecase readCentroidUsecase; @@ -28,7 +30,7 @@ public class LocationController { private final GeometryConfig.PointConverter pointConverter; @PostMapping("/create") - public ResponseEntity> createLocation(@RequestBody LocationRequest locationRequest) { + public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.createLocation( locationRequest.uuid(), locationRequest.memberId(), @@ -99,7 +101,7 @@ public ResponseEntity>> getLocations(@Path } @PutMapping("/update") - public ResponseEntity> updateLocation(@RequestBody LocationRequest locationRequest) { + public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.UpdateLocation( locationRequest.uuid(), locationRequest.memberId(), From 7c15816b8b952a5347a381734765524b93f675a6 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 27 Feb 2025 16:18:52 +0900 Subject: [PATCH 035/163] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20refactor?= =?UTF-8?q?=20file=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/rest/LocationController.java | 62 +++------ .../adapter/out/mapper/LocationMapper.java | 33 +++++ .../service/CentroidQueryService.java | 33 +++++ .../service/LocationCommandService.java | 36 ++++++ .../service/LocationQueryService.java | 31 +++++ .../application/service/LocationService.java | 122 ++++++++++-------- .../config/geometry/GeometryConfig.java | 22 ---- .../config/geometry/PointConverter.java | 29 +++++ .../usecase/CreateLocationUsecase.java | 6 +- .../usecase/ReadCentroidUsecase.java | 5 + 10 files changed, 257 insertions(+), 122 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index f74e2484..bb2f2b85 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -4,8 +4,8 @@ import com.kok.kokapi.centroid.adapter.in.dto.LocationRequest; import com.kok.kokapi.centroid.adapter.out.dto.CentroidResponse; import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; +import com.kok.kokapi.centroid.adapter.out.mapper.LocationMapper; import com.kok.kokapi.common.response.ApiResponseDto; -import com.kok.kokapi.config.geometry.GeometryConfig; import com.kok.kokcore.application.domain.entity.Location; import com.kok.kokcore.application.usecase.CreateLocationUsecase; import com.kok.kokcore.application.usecase.ReadCentroidUsecase; @@ -27,23 +27,19 @@ public class LocationController extends BaseController { private final CreateLocationUsecase createLocationUsecase; private final ReadCentroidUsecase readCentroidUsecase; private final ReadLocationUsecase readLocationUsecase; - private final GeometryConfig.PointConverter pointConverter; + private final LocationMapper locationMapper; @PostMapping("/create") public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { - Location location = createLocationUsecase.createLocation( + createLocationUsecase.createLocation( locationRequest.uuid(), locationRequest.memberId(), - pointConverter.fromCoordinates( - locationRequest.latitude(), - locationRequest.longitude() - ) + locationRequest.latitude(), + locationRequest.longitude() ); - if (location == null) { - throw new IllegalArgumentException("์œ„์น˜ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); - } - Pair centroid = pointConverter.toCoordinates(readCentroidUsecase.readCentroid(locationRequest.uuid())); + // ๐Ÿ”ฅ ๋ณ€ํ™˜ ๋กœ์ง์„ ์„œ๋น„์Šค์—์„œ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ณ€๊ฒฝ + Pair centroid = readCentroidUsecase.readCentroidCoordinates(locationRequest.uuid()); return ResponseEntity.ok(ApiResponseDto.success( CentroidResponse.of(locationRequest.uuid(), centroid.getFirst(), centroid.getSecond()) @@ -52,11 +48,7 @@ public ResponseEntity> createLocation(@Valid @R @GetMapping("/centroid/{uuid}") public ResponseEntity> getCentroid(@PathVariable String uuid) { - Pair centroid = pointConverter.toCoordinates(readCentroidUsecase.readCentroid(uuid)); - - if (centroid == null) { - throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } + Pair centroid = readCentroidUsecase.readCentroidCoordinates(uuid); return ResponseEntity.ok(ApiResponseDto.success( CentroidResponse.of(uuid, centroid.getFirst(), centroid.getSecond()) @@ -66,54 +58,30 @@ public ResponseEntity> getCentroid(@PathVariabl @GetMapping("/{uuid}/{memberId}") public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { Location location = readLocationUsecase.readLocation(uuid, memberId); - if (location == null) { - throw new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - Pair locationPair = pointConverter.toCoordinates(location.getPoint()); - return ResponseEntity.ok(ApiResponseDto.success( - LocationResponse.of(uuid, memberId, locationPair.getFirst(), locationPair.getSecond()) - )); + return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); } @GetMapping("/{uuid}") public ResponseEntity>> getLocations(@PathVariable String uuid) { - List locations = readLocationUsecase.readLocations(uuid); - - if (locations.isEmpty()) { - throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์œ„์น˜๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - List responses = locations.stream() - .map(location -> { - Pair coordinates = pointConverter.toCoordinates(location.getPoint()); - return LocationResponse.of( - uuid, - location.getMemberId(), - coordinates.getFirst(), - coordinates.getSecond() - ); - }) - .toList(); + List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(uuid)); return ResponseEntity.ok(ApiResponseDto.success(responses)); } @PutMapping("/update") public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { - Location location = createLocationUsecase.UpdateLocation( + Location location = createLocationUsecase.updateLocation( locationRequest.uuid(), locationRequest.memberId(), - pointConverter.fromCoordinates( - locationRequest.latitude(), - locationRequest.longitude() - ) + locationRequest.latitude(), + locationRequest.longitude() ); - Pair locationPair = pointConverter.toCoordinates(location.getPoint()); + Pair locationPair = readCentroidUsecase.readCentroidCoordinates(locationRequest.uuid()); return ResponseEntity.ok(ApiResponseDto.success( LocationResponse.of(locationRequest.uuid(), locationRequest.memberId(), locationPair.getFirst(), locationPair.getSecond()) )); } } + diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java new file mode 100644 index 00000000..c755746c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -0,0 +1,33 @@ +package com.kok.kokapi.centroid.adapter.out.mapper; + +import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; +import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokapi.config.geometry.PointConverter; +import org.springframework.stereotype.Component; +import org.springframework.data.util.Pair; +import java.math.BigDecimal; +import java.util.List; + +@Component +public class LocationMapper { + + private final PointConverter pointConverter; + + public LocationMapper(PointConverter pointConverter) { + this.pointConverter = pointConverter; + } + + public LocationResponse toResponse(Location location) { + Pair coordinates = pointConverter.toCoordinates(location.getPoint()); + return LocationResponse.of( + location.getUuid(), + location.getMemberId(), + coordinates.getFirst(), + coordinates.getSecond() + ); + } + + public List toResponseList(List locations) { + return locations.stream().map(this::toResponse).toList(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java new file mode 100644 index 00000000..78bf1b2f --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -0,0 +1,33 @@ +package com.kok.kokapi.centroid.application.service; + +import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; +import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokcore.application.usecase.ReadCentroidUsecase; +import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +public class CentroidQueryService implements ReadCentroidUsecase { + + private final LocationPersistenceAdapter locationRepository; + private final PointConverter pointConverter; + + @Override + public Point readCentroid(String uuid) { + return locationRepository.findCentroidByUuid(uuid); + } + + @Override + public Pair readCentroidCoordinates(String uuid) { + Point centroidPoint = locationRepository.findCentroidByUuid(uuid); + if (centroidPoint == null) { + throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return pointConverter.toCoordinates(centroidPoint); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java new file mode 100644 index 00000000..039e9755 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -0,0 +1,36 @@ +package com.kok.kokapi.centroid.application.service; + +import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; +import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.application.usecase.CreateLocationUsecase; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +public class LocationCommandService implements CreateLocationUsecase { + + private final LocationPersistenceAdapter locationRepository; + private final PointConverter pointConverter; + + @Override + public Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { + Point point = pointConverter.fromCoordinates(latitude, longitude); + return locationRepository.saveLocation(uuid, memberId, point); + } + + @Override + @Transactional + public Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { + Location location = locationRepository.findLocationByUuidAndMemberId(uuid, memberId) + .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); + Point newPoint = pointConverter.fromCoordinates(latitude, longitude); + location.changePoint(newPoint); + return location; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java new file mode 100644 index 00000000..0f4a6e33 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.centroid.application.service; + +import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; +import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.application.usecase.ReadLocationUsecase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class LocationQueryService implements ReadLocationUsecase{ + + private final LocationPersistenceAdapter locationRepository; + + @Override + public Location readLocation(String uuid, Integer memberId) { + return locationRepository.findLocationByUuidAndMemberId(uuid, memberId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } + + @Override + public List readLocations(String uuid) { + List locations = locationRepository.findLocationsByUuid(uuid); + if (locations.isEmpty()) { + throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์œ„์น˜๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + return locations; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java index ec1b0a1d..d5604a75 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java @@ -1,51 +1,71 @@ -package com.kok.kokapi.centroid.application.service; - -import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; -import com.kok.kokcore.application.domain.entity.Location; -import com.kok.kokcore.application.usecase.CreateLocationUsecase; -import com.kok.kokcore.application.usecase.ReadCentroidUsecase; -import com.kok.kokcore.application.usecase.ReadLocationUsecase; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import org.locationtech.jts.geom.Point; -import org.springframework.stereotype.Service; - -import java.util.List; - - -@Service -@RequiredArgsConstructor -public class LocationService implements CreateLocationUsecase, ReadCentroidUsecase, ReadLocationUsecase { - - private final LocationPersistenceAdapter locationRepository; - - @Override - public Location createLocation(String uuid, Integer memberId, Point point) { - return locationRepository.saveLocation(uuid, memberId, point); - } - - @Override - @Transactional - public Location UpdateLocation(String uuid, Integer memberId, Point point) { - Location location = locationRepository.findLocationByUuidAndMemberId(uuid, memberId).orElseThrow( - () -> new RuntimeException("ํ•ด๋‹น Id์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") - ); - location.changePoint(point); - return location; - } - - @Override - public Point readCentroid(String uuid) { - return locationRepository.findCentroidByUuid(uuid); - } - - @Override - public Location readLocation(String uuid, Integer memberId) { - return locationRepository.findLocationByUuidAndMemberId(uuid, memberId).orElse(null); - } - - @Override - public List readLocations(String uuid) { - return locationRepository.findLocationsByUuid(uuid); - } -} +//package com.kok.kokapi.centroid.application.service; +// +//import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; +//import com.kok.kokapi.config.geometry.PointConverter; +//import com.kok.kokcore.application.domain.entity.Location; +//import com.kok.kokcore.application.usecase.CreateLocationUsecase; +//import com.kok.kokcore.application.usecase.ReadCentroidUsecase; +//import com.kok.kokcore.application.usecase.ReadLocationUsecase; +//import jakarta.transaction.Transactional; +//import lombok.RequiredArgsConstructor; +//import org.locationtech.jts.geom.Point; +//import org.springframework.data.util.Pair; +//import org.springframework.stereotype.Service; +// +//import java.math.BigDecimal; +//import java.util.List; +// +//@Service +//@RequiredArgsConstructor +//public class LocationService implements CreateLocationUsecase, ReadCentroidUsecase, ReadLocationUsecase { +// +// private final LocationPersistenceAdapter locationRepository; +// private final PointConverter pointConverter; +// +// @Override +// public Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { +// Point point = pointConverter.fromCoordinates(latitude, longitude); +// return locationRepository.saveLocation(uuid, memberId, point); +// } +// +// @Override +// @Transactional +// public Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { +// Location location = locationRepository.findLocationByUuidAndMemberId(uuid, memberId) +// .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); +// Point newPoint = pointConverter.fromCoordinates(latitude, longitude); +// location.changePoint(newPoint); +// return location; +// } +// +// @Override +// public Point readCentroid(String uuid) { +// return locationRepository.findCentroidByUuid(uuid); +// } +// +// @Override +// public Pair readCentroidCoordinates(String uuid) { +// Point centroidPoint = locationRepository.findCentroidByUuid(uuid); +// if (centroidPoint == null) { +// throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); +// } +// return pointConverter.toCoordinates(centroidPoint); +// } +// +// @Override +// public Location readLocation(String uuid, Integer memberId) { +// return locationRepository.findLocationByUuidAndMemberId(uuid, memberId) +// .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); +// } +// +// @Override +// public List readLocations(String uuid) { +// List locations = locationRepository.findLocationsByUuid(uuid); +// if (locations.isEmpty()) { +// throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์œ„์น˜๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); +// } +// +// return locations; +// } +//} +// diff --git a/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java index a0e77924..b85eacec 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java @@ -22,26 +22,4 @@ public GeometryFactory geometryFactory() { public PointConverter pointConverter(GeometryFactory geometryFactory) { return new PointConverter(geometryFactory); } - - // Converter ํด๋ž˜์Šค - public static class PointConverter { - - private final GeometryFactory geometryFactory; - - public PointConverter(GeometryFactory geometryFactory) { - this.geometryFactory = geometryFactory; - } - - public Point fromCoordinates(BigDecimal latitude, BigDecimal longitude) { - Coordinate coordinate = new Coordinate(longitude.doubleValue(), latitude.doubleValue()); - Point point = geometryFactory.createPoint(coordinate); - point.setSRID(4326); // WGS84 ์ขŒํ‘œ๊ณ„ -> Kakao, Naver ๋“ฑ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ขŒํ‘œ๊ณ„ - return point; - } - - public Pair toCoordinates(Point point) { - return Pair.of(BigDecimal.valueOf(point.getY()), BigDecimal.valueOf(point.getX())); - } - } - } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java b/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java new file mode 100644 index 00000000..0fe12961 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java @@ -0,0 +1,29 @@ +package com.kok.kokapi.config.geometry; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.data.util.Pair; + +import java.math.BigDecimal; + +public class PointConverter { + + private final GeometryFactory geometryFactory; + + public PointConverter(GeometryFactory geometryFactory) { + this.geometryFactory = geometryFactory; + } + + public Point fromCoordinates(BigDecimal latitude, BigDecimal longitude) { + Coordinate coordinate = new Coordinate(longitude.doubleValue(), latitude.doubleValue()); + Point point = geometryFactory.createPoint(coordinate); + point.setSRID(4326); // WGS84 ์ขŒํ‘œ๊ณ„ -> Kakao, Naver ๋“ฑ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ขŒํ‘œ๊ณ„ + return point; + } + + public Pair toCoordinates(Point point) { + return Pair.of(BigDecimal.valueOf(point.getY()), BigDecimal.valueOf(point.getX())); + } + +} diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java index f471da37..73163993 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java @@ -3,8 +3,10 @@ import com.kok.kokcore.application.domain.entity.Location; import org.locationtech.jts.geom.Point; +import java.math.BigDecimal; + public interface CreateLocationUsecase { - Location createLocation(String uuid, Integer memberId, Point point); - Location UpdateLocation(String uuid, Integer memberId, Point point); + Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude); + Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude); } diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java b/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java index 5e3ac8ab..88b62323 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java @@ -1,7 +1,12 @@ package com.kok.kokcore.application.usecase; import org.locationtech.jts.geom.Point; +import org.springframework.data.util.Pair; + +import java.math.BigDecimal; public interface ReadCentroidUsecase { Point readCentroid(String uuid); + + Pair readCentroidCoordinates(String uuid); } From b93b8cd0c8c92883d3a9bce98cc6e58dd0c33ecb Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 27 Feb 2025 19:22:09 +0900 Subject: [PATCH 036/163] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20service?= =?UTF-8?q?=20depends=20on=20port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/rest/LocationController.java | 7 +- .../adapter/out/dto/CentroidResponse.java | 5 +- .../adapter/out/dto/LocationResponse.java | 5 +- .../service/CentroidQueryService.java | 8 +-- .../service/LocationCommandService.java | 10 +-- .../service/LocationQueryService.java | 8 +-- .../application/service/LocationService.java | 71 ------------------- .../kokcore/location/domain/LocationTest.java | 4 ++ 8 files changed, 28 insertions(+), 90 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java create mode 100644 kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index bb2f2b85..5838fb13 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -38,7 +38,6 @@ public ResponseEntity> createLocation(@Valid @R locationRequest.longitude() ); - // ๐Ÿ”ฅ ๋ณ€ํ™˜ ๋กœ์ง์„ ์„œ๋น„์Šค์—์„œ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ณ€๊ฒฝ Pair centroid = readCentroidUsecase.readCentroidCoordinates(locationRequest.uuid()); return ResponseEntity.ok(ApiResponseDto.success( @@ -77,11 +76,9 @@ public ResponseEntity> updateLocation(@Valid @R locationRequest.latitude(), locationRequest.longitude() ); - Pair locationPair = readCentroidUsecase.readCentroidCoordinates(locationRequest.uuid()); + LocationResponse response = locationMapper.toResponse(location); - return ResponseEntity.ok(ApiResponseDto.success( - LocationResponse.of(locationRequest.uuid(), locationRequest.memberId(), locationPair.getFirst(), locationPair.getSecond()) - )); + return ResponseEntity.ok(ApiResponseDto.success(response)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java index 5447f876..4f915910 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java @@ -1,6 +1,7 @@ package com.kok.kokapi.centroid.adapter.out.dto; import java.math.BigDecimal; +import java.math.RoundingMode; public record CentroidResponse( String uuid, @@ -8,6 +9,8 @@ public record CentroidResponse( BigDecimal longitude ) { public static CentroidResponse of(String uuid, BigDecimal latitude, BigDecimal longitude) { - return new CentroidResponse(uuid, latitude, longitude); + return new CentroidResponse(uuid, + latitude.setScale(6, RoundingMode.HALF_UP), + longitude.setScale(6,RoundingMode.HALF_UP)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java index 769caee6..10e6cba1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java @@ -1,6 +1,7 @@ package com.kok.kokapi.centroid.adapter.out.dto; import java.math.BigDecimal; +import java.math.RoundingMode; public record LocationResponse ( String uuid, @@ -9,6 +10,8 @@ public record LocationResponse ( BigDecimal longitude ){ public static LocationResponse of(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { - return new LocationResponse(uuid, memberId, latitude, longitude); + return new LocationResponse(uuid, memberId, + latitude.setScale(6, RoundingMode.HALF_UP), + longitude.setScale(6, RoundingMode.HALF_UP)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java index 78bf1b2f..01ff6a3f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -1,7 +1,7 @@ package com.kok.kokapi.centroid.application.service; -import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokcore.application.port.out.ReadCentroidPort; import com.kok.kokcore.application.usecase.ReadCentroidUsecase; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; @@ -14,17 +14,17 @@ @RequiredArgsConstructor public class CentroidQueryService implements ReadCentroidUsecase { - private final LocationPersistenceAdapter locationRepository; + private final ReadCentroidPort readCentroidPort; private final PointConverter pointConverter; @Override public Point readCentroid(String uuid) { - return locationRepository.findCentroidByUuid(uuid); + return readCentroidPort.findCentroidByUuid(uuid); } @Override public Pair readCentroidCoordinates(String uuid) { - Point centroidPoint = locationRepository.findCentroidByUuid(uuid); + Point centroidPoint = readCentroidPort.findCentroidByUuid(uuid); if (centroidPoint == null) { throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 039e9755..9d472f36 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -1,8 +1,9 @@ package com.kok.kokapi.centroid.application.service; -import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.application.port.out.ReadLocationPort; +import com.kok.kokcore.application.port.out.SaveLocationPort; import com.kok.kokcore.application.usecase.CreateLocationUsecase; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -15,19 +16,20 @@ @RequiredArgsConstructor public class LocationCommandService implements CreateLocationUsecase { - private final LocationPersistenceAdapter locationRepository; + private final SaveLocationPort saveLocationPort; + private final ReadLocationPort readLocationPort; private final PointConverter pointConverter; @Override public Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { Point point = pointConverter.fromCoordinates(latitude, longitude); - return locationRepository.saveLocation(uuid, memberId, point); + return saveLocationPort.saveLocation(uuid, memberId, point); } @Override @Transactional public Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { - Location location = locationRepository.findLocationByUuidAndMemberId(uuid, memberId) + Location location = readLocationPort.findLocationByUuidAndMemberId(uuid, memberId) .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); location.changePoint(newPoint); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index 0f4a6e33..f33bfee8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -1,7 +1,7 @@ package com.kok.kokapi.centroid.application.service; -import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.application.port.out.ReadLocationPort; import com.kok.kokcore.application.usecase.ReadLocationUsecase; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,17 +12,17 @@ @RequiredArgsConstructor public class LocationQueryService implements ReadLocationUsecase{ - private final LocationPersistenceAdapter locationRepository; + private final ReadLocationPort readLocationPort; @Override public Location readLocation(String uuid, Integer memberId) { - return locationRepository.findLocationByUuidAndMemberId(uuid, memberId) + return readLocationPort.findLocationByUuidAndMemberId(uuid, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } @Override public List readLocations(String uuid) { - List locations = locationRepository.findLocationsByUuid(uuid); + List locations = readLocationPort.findLocationsByUuid(uuid); if (locations.isEmpty()) { throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์œ„์น˜๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java deleted file mode 100644 index d5604a75..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationService.java +++ /dev/null @@ -1,71 +0,0 @@ -//package com.kok.kokapi.centroid.application.service; -// -//import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; -//import com.kok.kokapi.config.geometry.PointConverter; -//import com.kok.kokcore.application.domain.entity.Location; -//import com.kok.kokcore.application.usecase.CreateLocationUsecase; -//import com.kok.kokcore.application.usecase.ReadCentroidUsecase; -//import com.kok.kokcore.application.usecase.ReadLocationUsecase; -//import jakarta.transaction.Transactional; -//import lombok.RequiredArgsConstructor; -//import org.locationtech.jts.geom.Point; -//import org.springframework.data.util.Pair; -//import org.springframework.stereotype.Service; -// -//import java.math.BigDecimal; -//import java.util.List; -// -//@Service -//@RequiredArgsConstructor -//public class LocationService implements CreateLocationUsecase, ReadCentroidUsecase, ReadLocationUsecase { -// -// private final LocationPersistenceAdapter locationRepository; -// private final PointConverter pointConverter; -// -// @Override -// public Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { -// Point point = pointConverter.fromCoordinates(latitude, longitude); -// return locationRepository.saveLocation(uuid, memberId, point); -// } -// -// @Override -// @Transactional -// public Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { -// Location location = locationRepository.findLocationByUuidAndMemberId(uuid, memberId) -// .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); -// Point newPoint = pointConverter.fromCoordinates(latitude, longitude); -// location.changePoint(newPoint); -// return location; -// } -// -// @Override -// public Point readCentroid(String uuid) { -// return locationRepository.findCentroidByUuid(uuid); -// } -// -// @Override -// public Pair readCentroidCoordinates(String uuid) { -// Point centroidPoint = locationRepository.findCentroidByUuid(uuid); -// if (centroidPoint == null) { -// throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); -// } -// return pointConverter.toCoordinates(centroidPoint); -// } -// -// @Override -// public Location readLocation(String uuid, Integer memberId) { -// return locationRepository.findLocationByUuidAndMemberId(uuid, memberId) -// .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); -// } -// -// @Override -// public List readLocations(String uuid) { -// List locations = locationRepository.findLocationsByUuid(uuid); -// if (locations.isEmpty()) { -// throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์œ„์น˜๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); -// } -// -// return locations; -// } -//} -// diff --git a/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java b/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java new file mode 100644 index 00000000..9b6ed02e --- /dev/null +++ b/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java @@ -0,0 +1,4 @@ +package com.kok.kokcore.location.domain; + +public class LocationTest { +} From 8988812bf00a9d2319802494ce2cefc8766c29d3 Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Sat, 1 Mar 2025 00:14:27 +0900 Subject: [PATCH 037/163] [feature/room-participant] implements room participation and get participant profiles (#37) * :sparkles: feat: add RoomQueryService * :sparkles: feat: add RoomQueryService and RoomCommandService * :sparkles: feat: add RoomQueryService and RoomCommandService * :recycle: refactor: move core package structure under room domain * :recycle: refactor: move package structure under subdomain * :recycle: refactor: move package structure under subdomain * :sparkles: feat: implements get participants * :sparkles: feat: add RoomCreationServiceTest, RoomTest * :recycle: refactor: refactoring code * :recycle: refactor: refactoring code * :recycle: refactor: refactoring GetRoomUseCase, JoinRoomUseCase * :recycle: refactor: Separate Member VO for refactoring --- kok-api/build.gradle | 3 +- .../adapter/in/web/BaseController.java | 2 +- .../kokapi/config/{ => jpa}/JpaConfig.java | 2 +- .../kokapi/config/usecase/UseCaseConfig.java | 15 ---- .../adapter/in/web/HealthCheckController.java | 5 +- .../service/HealthCheckService.java | 2 +- .../in/dto/request/CreateRoomRequest.java | 3 + .../request/JoinRoomParticipantRequest.java | 13 ++++ .../in/dto/response/MemberResponse.java | 13 ++++ ...mResponse.java => RoomDetailResponse.java} | 12 ++-- .../in/dto/response/RoomMembersResponse.java | 7 ++ .../room/adapter/in/web/RoomController.java | 70 +++++++++++++++---- .../RedisSaveRoomPersistenceAdapter.java | 31 -------- .../persistence/RoomQueryRedisAdapter.java | 40 +++++++++++ .../out/persistence/RoomSaveRedisAdapter.java | 38 ++++++++++ .../service/RoomCreationService.java | 26 +++++++ .../service/RoomParticipantService.java | 30 ++++++++ .../application/service/RoomQueryService.java | 47 +++++++++++++ .../room/application/service/RoomService.java | 20 ------ .../common/config/ServiceTestConfig.java | 13 ++++ .../service/RoomCreationServiceTest.java | 61 ++++++++++++++++ kok-core/build.gradle | 2 - .../application/port/out/SaveRoomPort.java | 7 -- .../application/port/out/LoadRoomPort.java | 9 +++ .../application/port/out/SaveRoomPort.java | 7 ++ .../com/kok/kokcore/room/domain/Member.java | 26 +++++++ .../kok/kokcore/{ => room}/domain/Room.java | 27 +++---- .../room/usecase/CreateRoomUseCase.java | 8 +++ .../kokcore/room/usecase/GetRoomUseCase.java | 11 +++ .../kokcore/room/usecase/JoinRoomUseCase.java | 7 ++ .../kokcore/usecase/CreateRoomUseCase.java | 7 -- .../com/kok/kokcore/room/domain/RoomTest.java | 69 ++++++++++++++++++ 32 files changed, 513 insertions(+), 120 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/{ => common}/adapter/in/web/BaseController.java (74%) rename kok-api/src/main/java/com/kok/kokapi/config/{ => jpa}/JpaConfig.java (86%) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java rename kok-api/src/main/java/com/kok/kokapi/{ => monitoring}/adapter/in/web/HealthCheckController.java (74%) rename kok-api/src/main/java/com/kok/kokapi/{ => monitoring}/application/service/HealthCheckService.java (73%) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java rename kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/{RoomResponse.java => RoomDetailResponse.java} (54%) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java rename kok-core/src/main/java/com/kok/kokcore/{ => room}/domain/Room.java (69%) create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java create mode 100644 kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java diff --git a/kok-api/build.gradle b/kok-api/build.gradle index c96a7e84..d9dc858a 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -25,10 +25,11 @@ repositories { dependencies { implementation project(':kok-core') - implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java b/kok-api/src/main/java/com/kok/kokapi/common/adapter/in/web/BaseController.java similarity index 74% rename from kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java rename to kok-api/src/main/java/com/kok/kokapi/common/adapter/in/web/BaseController.java index 27231369..159a599b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/BaseController.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/adapter/in/web/BaseController.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.adapter.in.web; +package com.kok.kokapi.common.adapter.in.web; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/jpa/JpaConfig.java similarity index 86% rename from kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java rename to kok-api/src/main/java/com/kok/kokapi/config/jpa/JpaConfig.java index f45fa88f..3cbe366e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/JpaConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/jpa/JpaConfig.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.config; +package com.kok.kokapi.config.jpa; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.Configuration; diff --git a/kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java deleted file mode 100644 index 3e1889e0..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/config/usecase/UseCaseConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.kok.kokapi.config.usecase; - -import com.kok.kokapi.room.application.service.RoomService; -import com.kok.kokcore.application.port.out.SaveRoomPort; -import com.kok.kokcore.usecase.CreateRoomUseCase; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class UseCaseConfig { - @Bean - public CreateRoomUseCase createRoomUseCase(SaveRoomPort saveRoomPort) { - return new RoomService(saveRoomPort); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java similarity index 74% rename from kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java rename to kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java index 3cb96136..01a9fbd2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/adapter/in/web/HealthCheckController.java +++ b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java @@ -1,6 +1,7 @@ -package com.kok.kokapi.adapter.in.web; +package com.kok.kokapi.monitoring.adapter.in.web; -import com.kok.kokapi.application.service.HealthCheckService; +import com.kok.kokapi.monitoring.application.service.HealthCheckService; +import com.kok.kokapi.common.adapter.in.web.BaseController; import com.kok.kokapi.common.response.ApiResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java b/kok-api/src/main/java/com/kok/kokapi/monitoring/application/service/HealthCheckService.java similarity index 73% rename from kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java rename to kok-api/src/main/java/com/kok/kokapi/monitoring/application/service/HealthCheckService.java index 06adbde4..0326ad5e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/application/service/HealthCheckService.java +++ b/kok-api/src/main/java/com/kok/kokapi/monitoring/application/service/HealthCheckService.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.application.service; +package com.kok.kokapi.monitoring.application.service; import org.springframework.stereotype.Service; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java index 66f37a7d..ce30a405 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java @@ -15,6 +15,9 @@ public record CreateRoomRequest( String hostProfile, + @NotBlank(message = "๋ฐฉ์žฅ ์ด๋ฆ„(๋‹‰๋„ค์ž„)์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + String hostNickname, + @NotBlank(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") String password ) {} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java new file mode 100644 index 00000000..82c15d9c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java @@ -0,0 +1,13 @@ +package com.kok.kokapi.room.adapter.in.dto.request; + + +import jakarta.validation.constraints.NotBlank; + +public record JoinRoomParticipantRequest( + @NotBlank(message = "ํ”„๋กœํ•„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String profile, + + @NotBlank(message = "๋‹‰๋„ค์ž„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String nickname +) {} + diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java new file mode 100644 index 00000000..b02f0491 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java @@ -0,0 +1,13 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Member; + +public record MemberResponse( + String nickname, + String profile, + String role +) { + public static MemberResponse from(Member member) { + return new MemberResponse(member.getNickname(), member.getProfile(), member.getRole()); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java similarity index 54% rename from kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomResponse.java rename to kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index 65fe2f4c..bfce59db 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -1,20 +1,20 @@ package com.kok.kokapi.room.adapter.in.dto.response; -import com.kok.kokcore.domain.Room; +import com.kok.kokcore.room.domain.Room; -public record RoomResponse( +public record RoomDetailResponse( String id, String roomName, int capacity, - String hostProfile, + MemberResponse member, String roomLinkUrl ) { - public static RoomResponse from(Room room) { - return new RoomResponse( + public static RoomDetailResponse from(Room room) { + return new RoomDetailResponse( room.getId(), room.getRoomName(), room.getCapacity(), - room.getHostProfile(), + MemberResponse.from(room.getMember()), room.getRoomLinkUrl() ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java new file mode 100644 index 00000000..0e957f3e --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java @@ -0,0 +1,7 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +public record RoomMembersResponse( + String profile, + String nickname, + String role +) {} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index 1612213c..f50cd275 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -1,35 +1,79 @@ package com.kok.kokapi.room.adapter.in.web; +import com.kok.kokapi.common.adapter.in.web.BaseController; import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; -import com.kok.kokapi.room.adapter.in.dto.response.RoomResponse; -import com.kok.kokcore.domain.Room; -import com.kok.kokcore.usecase.CreateRoomUseCase; +import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; +import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.usecase.CreateRoomUseCase; +import com.kok.kokcore.room.usecase.GetRoomUseCase; +import com.kok.kokcore.room.usecase.JoinRoomUseCase; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController -@RequestMapping("/rooms") @RequiredArgsConstructor -public class RoomController { +public class RoomController extends BaseController { + private final GetRoomUseCase getRoomUseCase; private final CreateRoomUseCase createRoomUseCase; + private final JoinRoomUseCase joinRoomUseCase; - @PostMapping("/create") - public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { + @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "Retrieve detailed information for a room using its ID") + @GetMapping("/rooms/{roomId}") + public ResponseEntity> getRoomDetail(@PathVariable String roomId) { + Room room = getRoomUseCase.findRoomById(roomId); + var response = RoomDetailResponse.from(room); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } + + @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒ์„ฑ", description = "Create a new room with the provided details.") + @PostMapping("/rooms") + public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { + Member host = new Member(request.hostNickname(), request.hostProfile(), "Leader"); Room room = createRoomUseCase.createRoom( request.roomName(), request.capacity(), - request.hostProfile(), + host, request.password() ); - var response = RoomResponse.from(room); + var response = RoomDetailResponse.from(room); + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponseDto.success(response)); + } + + @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of participant profiles for the room.") + @GetMapping("/rooms/{roomId}/participants") + public ResponseEntity>> getParticipants(@PathVariable String roomId) { + Room room = getRoomUseCase.findRoomById(roomId); + List participants = getRoomUseCase.getParticipants(room.getId()); + + var response = participants.stream() + .map(member -> new RoomMembersResponse( + member.getProfile(), + member.getNickname(), + member.getRole() + )).toList(); return ResponseEntity.ok(ApiResponseDto.success(response)); } + + @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ", description = "Register a new participant profile for the room.") + @PostMapping("/rooms/{roomId}/join") + public ResponseEntity> joinRoom(@PathVariable String roomId, + @Valid @RequestBody JoinRoomParticipantRequest request) { + getRoomUseCase.findRoomById(roomId); + Member participant = new Member(request.nickname(), request.profile(), "Follower"); + joinRoomUseCase.joinRoom(roomId, participant); + return ResponseEntity.ok(ApiResponseDto.success("์•ฝ์†๋ฐฉ ์ฐธ์—ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java deleted file mode 100644 index ec60cd0b..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RedisSaveRoomPersistenceAdapter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.kok.kokapi.room.adapter.out.persistence; - -import com.kok.kokcore.application.port.out.SaveRoomPort; -import com.kok.kokcore.domain.Room; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; - -import java.time.Duration; - -@Repository -public class RedisSaveRoomPersistenceAdapter implements SaveRoomPort { - - private static final String ROOM_KEY_PREFIX = "room"; - private static final Duration ROOM_TTL = Duration.ofDays(3); - private final RedisTemplate redisTemplate; - - public RedisSaveRoomPersistenceAdapter(RedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - - @Override - public Room save(Room room) { - String key = buildKey(room.getId()); - redisTemplate.opsForValue().set(key, room, ROOM_TTL); - return room; - } - - private String buildKey(String roomId) { - return ROOM_KEY_PREFIX + ":" + roomId; - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java new file mode 100644 index 00000000..4abebb24 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java @@ -0,0 +1,40 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.application.port.out.LoadRoomPort; +import com.kok.kokcore.room.domain.Room; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class RoomQueryRedisAdapter implements LoadRoomPort { + + public static final String ROOM_KEY_PREFIX = "room:"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public Optional findRoomById(String roomId) { + String key = buildKey(roomId); + return Optional.ofNullable(redisTemplate.opsForValue().get(key)) + .flatMap(this::deserializeRoom); + } + + private Optional deserializeRoom(String roomJson) { + try { + Room room = objectMapper.readValue(roomJson, Room.class); + return Optional.of(room); + } catch (JsonProcessingException e) { + return Optional.empty(); + } + } + + private String buildKey(String roomId) { + return ROOM_KEY_PREFIX + roomId; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java new file mode 100644 index 00000000..ff5184d9 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java @@ -0,0 +1,38 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.application.port.out.SaveRoomPort; +import com.kok.kokcore.room.domain.Room; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; + +@Repository +@RequiredArgsConstructor +public class RoomSaveRedisAdapter implements SaveRoomPort { + + private static final String ROOM_KEY_PREFIX = "room"; + private static final Duration ROOM_TTL = Duration.ofDays(3); + + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public Room save(Room room) { + String key = buildKey(room.getId()); + try { + String roomJson = objectMapper.writeValueAsString(room); + redisTemplate.opsForValue().set(key, roomJson, ROOM_TTL); + } catch (JsonProcessingException e) { + throw new RuntimeException("failed to save room to Redis", e); + } + return room; + } + + private String buildKey(String roomId) { + return ROOM_KEY_PREFIX + ":" + roomId; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java new file mode 100644 index 00000000..9fdb8b4b --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java @@ -0,0 +1,26 @@ +package com.kok.kokapi.room.application.service; + +import com.kok.kokcore.room.application.port.out.SaveRoomPort; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.usecase.CreateRoomUseCase; +import com.kok.kokcore.room.usecase.JoinRoomUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RoomCreationService implements CreateRoomUseCase { + + private final SaveRoomPort saveRoomPort; + private final JoinRoomUseCase joinRoomUseCase; + + @Override + public Room createRoom(String roomName, int capacity, Member host, String password) { + Room room = Room.create(roomName, capacity, host, password); + Room savedRoom = saveRoomPort.save(room); + + joinRoomUseCase.joinRoom(savedRoom.getId(), savedRoom.getMember()); + return savedRoom; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java new file mode 100644 index 00000000..ef537fae --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java @@ -0,0 +1,30 @@ +package com.kok.kokapi.room.application.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.usecase.JoinRoomUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +public class RoomParticipantService implements JoinRoomUseCase { + + private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public void joinRoom(String roomId, Member member) { + String key = PARTICIPANT_KEY_PREFIX + roomId; + try { + String memberJson = objectMapper.writeValueAsString(member); + redisTemplate.opsForList().rightPush(key, memberJson); + } catch (JsonProcessingException e) { + throw new RuntimeException("failed to serialize member"); + } + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java new file mode 100644 index 00000000..0b007d0d --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -0,0 +1,47 @@ +package com.kok.kokapi.room.application.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.application.port.out.LoadRoomPort; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.usecase.GetRoomUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RoomQueryService implements GetRoomUseCase { + + private final LoadRoomPort loadRoomPort; + private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public Room findRoomById(String roomId) { + return loadRoomPort.findRoomById(roomId) + .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); + } + + @Override + public List getParticipants(String roomId) { + String key = PARTICIPANT_KEY_PREFIX + roomId; + List memberJson = redisTemplate.opsForList().range(key, 0, -1); + List members = new ArrayList<>(); + if (memberJson != null) { + for (String data : memberJson) { + try { + Member member = objectMapper.readValue(data, Member.class); + members.add(member); + } catch (JsonProcessingException ignored) { + } + } + } + return members; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java deleted file mode 100644 index 26ce42f7..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.kok.kokapi.room.application.service; - -import com.kok.kokcore.application.port.out.SaveRoomPort; -import com.kok.kokcore.domain.Room; -import com.kok.kokcore.usecase.CreateRoomUseCase; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RoomService implements CreateRoomUseCase { - - private final SaveRoomPort saveRoomPort; - - @Override - public Room createRoom(String roomName, int capacity, String hostProfile, String password) { - Room room = Room.create(roomName, capacity, hostProfile, password); - return saveRoomPort.save(room); - } -} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java index 56bcc61a..49ea7f87 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java @@ -1,7 +1,10 @@ package com.kok.kokapi.common.config; import com.kok.kokapi.station.adapter.out.external.FakeStationClient; +import com.kok.kokcore.room.application.port.out.SaveRoomPort; +import com.kok.kokcore.room.usecase.JoinRoomUseCase; import com.kok.kokcore.station.application.port.out.LoadStationsPort; +import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -12,4 +15,14 @@ public class ServiceTestConfig { public LoadStationsPort loadStationsPort() { return new FakeStationClient(); } + + @Bean + public SaveRoomPort saveRoomPort() { + return Mockito.mock(SaveRoomPort.class); + } + + @Bean + public JoinRoomUseCase joinRoomUseCase() { + return Mockito.mock(JoinRoomUseCase.class); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java new file mode 100644 index 00000000..104e5845 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java @@ -0,0 +1,61 @@ +package com.kok.kokapi.room.application.service; + +import com.kok.kokapi.common.config.ServiceTestConfig; +import com.kok.kokcore.room.application.port.out.SaveRoomPort; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.usecase.JoinRoomUseCase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Import({ServiceTestConfig.class}) +@SpringBootTest(classes = {RoomCreationService.class}) +class RoomCreationServiceTest { + private SaveRoomPort saveRoomPort; + private JoinRoomUseCase joinRoomUseCase; + + @Autowired + private RoomCreationService roomCreationService; + + @BeforeEach + void setUp() { + saveRoomPort = mock(SaveRoomPort.class); + joinRoomUseCase = mock(JoinRoomUseCase.class); + roomCreationService = new RoomCreationService(saveRoomPort, joinRoomUseCase); + } + + @DisplayName("์•ฝ์†๋ฐฉ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createRoom() { + String roomName = "Test Room"; + int capacity = 4; + String hostNickname = "test"; + String hostProfile = "hostProfile"; + String password = "secret"; + Member host = new Member(hostNickname, hostProfile, "Leader"); + + when(saveRoomPort.save(any(Room.class))).thenAnswer(invocation -> invocation.getArgument(0)); + Room createdRoom = roomCreationService.createRoom(roomName, capacity, host, password); + + assertAll("Room Create Test", + () -> assertNotNull(createdRoom, "Room ๊ฐ์ฒด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertNotNull(createdRoom.getId(), "์•ฝ์†๋ฐฉ ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(roomName, createdRoom.getRoomName(), "์•ฝ์†๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(capacity, createdRoom.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(password, createdRoom.getPassword(), "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertNotNull(createdRoom.getMember(), "๋ฐฉ์žฅ ์ •๋ณด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostNickname, createdRoom.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostProfile, createdRoom.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals("Leader", createdRoom.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + ); + } +} diff --git a/kok-core/build.gradle b/kok-core/build.gradle index 1de36f58..c58c530f 100644 --- a/kok-core/build.gradle +++ b/kok-core/build.gradle @@ -19,8 +19,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java deleted file mode 100644 index 2f3e17a2..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveRoomPort.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kok.kokcore.application.port.out; - -import com.kok.kokcore.domain.Room; - -public interface SaveRoomPort { - Room save(Room room); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java new file mode 100644 index 00000000..feaf9237 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.room.application.port.out; + +import com.kok.kokcore.room.domain.Room; + +import java.util.Optional; + +public interface LoadRoomPort { + Optional findRoomById(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java new file mode 100644 index 00000000..55bb64ef --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.room.application.port.out; + +import com.kok.kokcore.room.domain.Room; + +public interface SaveRoomPort { + Room save(Room room); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java new file mode 100644 index 00000000..d837b01e --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java @@ -0,0 +1,26 @@ +package com.kok.kokcore.room.domain; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +public class Member { + private final String nickname; + private final String profile; + private final String role; + + public Member(String nickname, String profile, String role) { + if (nickname == null || nickname.trim().isEmpty()) { + throw new IllegalArgumentException("Nickname is required"); + } + if (role == null || role.trim().isEmpty()) { + throw new IllegalArgumentException("Role is required"); + } + this.nickname = nickname.trim(); + this.profile = profile.trim(); + this.role = role.trim(); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java similarity index 69% rename from kok-core/src/main/java/com/kok/kokcore/domain/Room.java rename to kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 43b08b97..877660bc 100644 --- a/kok-core/src/main/java/com/kok/kokcore/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.domain; +package com.kok.kokcore.room.domain; import lombok.*; @@ -16,34 +16,35 @@ public class Room implements Serializable { private final String id; // ์•ฝ์†๋ฐฉ ID (UUID) private final String roomName; // ์•ฝ์†๋ฐฉ ์ด๋ฆ„ private final int capacity; // ์ฐธ์—ฌ์ธ์› ์ˆ˜ (์ตœ์†Œ 2๋ช… ์ด์ƒ) - private final String hostProfile; // ๋ฐฉ์žฅ ํ”„๋กœํ•„ ์ •๋ณด private final String password; // ๋ฐฉ ๋น„๋ฐ€๋ฒˆํ˜ธ (์˜ต์…˜) + private final Member member; // ๋ฐฉ ์ฐธ์—ฌ์ž private final String roomLinkUrl; // ์ƒ์„ฑ๋œ ์•ฝ์†๋ฐฉ ์ž…์žฅ ๋งํฌ - public Room(String id, String roomName, int capacity, String hostProfile, + private Room(String id, String roomName, int capacity, Member member, String password, String roomLinkUrl) { this.id = id; this.roomName = roomName; this.capacity = capacity; - this.hostProfile = hostProfile; + this.member = member; this.password = password; this.roomLinkUrl = roomLinkUrl; } - public static Room create(String roomName, int capacity, String hostProfile, String password) { + public static Room create(String roomName, int capacity, Member host, String password) { + validateParameter(roomName, capacity, host); + + String roomId = UUID.randomUUID().toString(); + String roomLinkUrl = ROOM_LINK_URL + roomId; + + return new Room(roomId, roomName, capacity, host, password, roomLinkUrl); + } + + private static void validateParameter(String roomName, int capacity, Member host) { if (roomName == null || roomName.trim().isEmpty()) { throw new IllegalArgumentException("Room name is required"); } if (capacity < REQUIRED_CAPACITY) { throw new IllegalArgumentException("At least 2 participants are required"); } - if (hostProfile == null || hostProfile.trim().isEmpty()) { - throw new IllegalArgumentException("Host profile is required"); - } - - String roomId = UUID.randomUUID().toString(); - String roomLinkUrl = ROOM_LINK_URL + roomId; - - return new Room(roomId, roomName, capacity, hostProfile, password, roomLinkUrl); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java new file mode 100644 index 00000000..bdc75c5a --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.room.usecase; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; + +public interface CreateRoomUseCase { + Room createRoom(String roomName, int capacity, Member host, String password); +} \ No newline at end of file diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java new file mode 100644 index 00000000..37c9d32f --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -0,0 +1,11 @@ +package com.kok.kokcore.room.usecase; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; + +import java.util.List; + +public interface GetRoomUseCase { + Room findRoomById(String roomId); + List getParticipants(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java new file mode 100644 index 00000000..958b7c1f --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.room.usecase; + +import com.kok.kokcore.room.domain.Member; + +public interface JoinRoomUseCase { + void joinRoom(String roomId, Member member); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java deleted file mode 100644 index a26b2788..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/usecase/CreateRoomUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kok.kokcore.usecase; - -import com.kok.kokcore.domain.Room; - -public interface CreateRoomUseCase { - Room createRoom(String roomName, int capacity, String hostProfile, String password); -} diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java new file mode 100644 index 00000000..ba211bdf --- /dev/null +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -0,0 +1,69 @@ +package com.kok.kokcore.room.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RoomTest { + + @DisplayName("์•ฝ์†๋ฐฉ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createRoom() { + // Given + String roomName = "Test Room"; + int capacity = 4; + String hostProfile = "hostProfile"; + String hostNickname = "test"; + String password = "secret"; + Member host = new Member(hostNickname, hostProfile, "Leader"); + + // When + Room room = Room.create(roomName, capacity, host, password); + + // Then + assertAll("์•ฝ์†๋ฐฉ ์ƒ์„ฑ", + () -> assertNotNull(room.getId(), "ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(roomName, room.getRoomName(), "๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(capacity, room.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostNickname, room.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostProfile, room.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals("Leader", room.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(password, room.getPassword(), "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + ); + } + + @DisplayName("์•ฝ์†๋ฐฉ ์ƒ์„ฑ ์‹คํŒจ - ์•ฝ์†๋ฐฉ ์ด๋ฆ„์ด ์—†๋Š” ๊ฒฝ์šฐ ์•ฝ์†๋ฐฉ์ด ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š”๋‹ค.") + @Test + void createRoomWithEmptyRoomName() { + // Given + String roomName = " "; + int capacity = 4; + String hostNickname = "hostNickname"; + String hostProfile = "hostProfile"; + String password = "secret"; + Member host = new Member(hostNickname, hostProfile, "Leader"); + + // When & Then + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host, password)); + assertTrue(exception.getMessage().contains("Room name is required")); + } + + @DisplayName("์•ฝ์†๋ฐฉ ์ƒ์„ฑ ์‹คํŒจ - ์ตœ์†Œ ์š”๊ตฌ ์ธ์›(2๋ช…) ๋ฏธ๋งŒ") + @Test + void createRoomWithInvalidCapacity() { + // Given + String roomName = "Test Room"; + int capacity = 1; + String hostNickname = "hostNickname"; + String hostProfile = "hostProfile"; + String password = "secret"; + Member host = new Member(hostNickname, hostProfile, "Leader"); + + // When & Then + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host, password)); + assertTrue(exception.getMessage().contains("At least 2 participants are required")); + } +} From 6106361c2cd963e78b544777158605073d7bb8fe Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 01:43:11 +0900 Subject: [PATCH 038/163] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20rrefactor:=20chang?= =?UTF-8?q?e=20dialect=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kok-api/src/main/resources/application-dev.yml | 2 +- kok-api/src/main/resources/application-prod.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 85b16a1d..da965b76 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -11,7 +11,7 @@ spring: hibernate: show_sql: true format_sql: true - dialect: org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect + dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect open-in-view: false defer-datasource-initialization: true flyway: diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 4765d279..632d8a2f 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -11,7 +11,7 @@ spring: hibernate: show_sql: true format_sql: true - dialect: org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect + dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect open-in-view: false defer-datasource-initialization: true flyway: From 145db2d4aa75c845621776f38ab1ecd4282abee7 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 02:14:08 +0900 Subject: [PATCH 039/163] =?UTF-8?q?=F0=9F=90=9Bfix:=20fix=20file=20archite?= =?UTF-8?q?cture=20inconsistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../centroid/adapter/in/rest/LocationController.java | 10 +++++----- .../centroid/adapter/out/mapper/LocationMapper.java | 2 +- .../out/persistence/LocationPersistenceAdapter.java | 8 ++++---- .../adapter/out/persistence/LocationRepository.java | 2 +- .../application/service/CentroidQueryService.java | 4 ++-- .../application/service/LocationCommandService.java | 8 ++++---- .../application/service/LocationQueryService.java | 6 +++--- kok-api/src/main/resources/application-dev.yml | 2 +- .../application/port/out/ReadCentroidPort.java | 2 +- .../application/port/out/ReadLocationPort.java | 4 ++-- .../application/port/out/SaveLocationPort.java | 4 ++-- .../domain/entity => location/domain}/Location.java | 2 +- .../usecase/CreateLocationUsecase.java | 5 ++--- .../usecase/ReadCentroidUsecase.java | 2 +- .../usecase/ReadLocationUsecase.java | 4 ++-- 15 files changed, 32 insertions(+), 33 deletions(-) rename kok-core/src/main/java/com/kok/kokcore/{ => location}/application/port/out/ReadCentroidPort.java (69%) rename kok-core/src/main/java/com/kok/kokcore/{ => location}/application/port/out/ReadLocationPort.java (68%) rename kok-core/src/main/java/com/kok/kokcore/{ => location}/application/port/out/SaveLocationPort.java (58%) rename kok-core/src/main/java/com/kok/kokcore/{application/domain/entity => location/domain}/Location.java (94%) rename kok-core/src/main/java/com/kok/kokcore/{application => location}/usecase/CreateLocationUsecase.java (66%) rename kok-core/src/main/java/com/kok/kokcore/{application => location}/usecase/ReadCentroidUsecase.java (85%) rename kok-core/src/main/java/com/kok/kokcore/{application => location}/usecase/ReadLocationUsecase.java (62%) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index 5838fb13..18017109 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -1,15 +1,15 @@ package com.kok.kokapi.centroid.adapter.in.rest; -import com.kok.kokapi.adapter.in.web.BaseController; +import com.kok.kokapi.common.adapter.in.web.BaseController; import com.kok.kokapi.centroid.adapter.in.dto.LocationRequest; import com.kok.kokapi.centroid.adapter.out.dto.CentroidResponse; import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; import com.kok.kokapi.centroid.adapter.out.mapper.LocationMapper; import com.kok.kokapi.common.response.ApiResponseDto; -import com.kok.kokcore.application.domain.entity.Location; -import com.kok.kokcore.application.usecase.CreateLocationUsecase; -import com.kok.kokcore.application.usecase.ReadCentroidUsecase; -import com.kok.kokcore.application.usecase.ReadLocationUsecase; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.usecase.CreateLocationUsecase; +import com.kok.kokcore.location.usecase.ReadCentroidUsecase; +import com.kok.kokcore.location.usecase.ReadLocationUsecase; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index c755746c..c74e78e2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -1,7 +1,7 @@ package com.kok.kokapi.centroid.adapter.out.mapper; import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; -import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.location.domain.Location; import com.kok.kokapi.config.geometry.PointConverter; import org.springframework.stereotype.Component; import org.springframework.data.util.Pair; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index aaad08bb..57b403a6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -1,10 +1,10 @@ package com.kok.kokapi.centroid.adapter.out.persistence; -import com.kok.kokcore.application.domain.entity.Location; -import com.kok.kokcore.application.port.out.ReadCentroidPort; -import com.kok.kokcore.application.port.out.ReadLocationPort; -import com.kok.kokcore.application.port.out.SaveLocationPort; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.application.port.out.ReadCentroidPort; +import com.kok.kokcore.location.application.port.out.ReadLocationPort; +import com.kok.kokcore.location.application.port.out.SaveLocationPort; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Repository; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 9a9597e1..819694c4 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -1,7 +1,7 @@ package com.kok.kokapi.centroid.adapter.out.persistence; -import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.location.domain.Location; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java index 01ff6a3f..23c57d08 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -1,8 +1,8 @@ package com.kok.kokapi.centroid.application.service; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokcore.application.port.out.ReadCentroidPort; -import com.kok.kokcore.application.usecase.ReadCentroidUsecase; +import com.kok.kokcore.location.application.port.out.ReadCentroidPort; +import com.kok.kokcore.location.usecase.ReadCentroidUsecase; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 9d472f36..276bbce9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -1,10 +1,10 @@ package com.kok.kokapi.centroid.application.service; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokcore.application.domain.entity.Location; -import com.kok.kokcore.application.port.out.ReadLocationPort; -import com.kok.kokcore.application.port.out.SaveLocationPort; -import com.kok.kokcore.application.usecase.CreateLocationUsecase; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.application.port.out.ReadLocationPort; +import com.kok.kokcore.location.application.port.out.SaveLocationPort; +import com.kok.kokcore.location.usecase.CreateLocationUsecase; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index f33bfee8..4fad519a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -1,8 +1,8 @@ package com.kok.kokapi.centroid.application.service; -import com.kok.kokcore.application.domain.entity.Location; -import com.kok.kokcore.application.port.out.ReadLocationPort; -import com.kok.kokcore.application.usecase.ReadLocationUsecase; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.application.port.out.ReadLocationPort; +import com.kok.kokcore.location.usecase.ReadLocationUsecase; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index da965b76..7287cdc6 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -6,7 +6,7 @@ spring: password: ${DB_PASSWORD} jpa: hibernate: - ddl-auto: validate + ddl-auto: update properties: hibernate: show_sql: true diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java similarity index 69% rename from kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadCentroidPort.java rename to kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java index 10dc7fd7..9d54f509 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadCentroidPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.application.port.out; +package com.kok.kokcore.location.application.port.out; import org.locationtech.jts.geom.Point; diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java similarity index 68% rename from kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadLocationPort.java rename to kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java index 2e52c7e9..5353a14d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/port/out/ReadLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java @@ -1,6 +1,6 @@ -package com.kok.kokcore.application.port.out; +package com.kok.kokcore.location.application.port.out; -import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.location.domain.Location; import java.util.List; import java.util.Optional; diff --git a/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java similarity index 58% rename from kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveLocationPort.java rename to kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java index 40c56d58..07a69186 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/port/out/SaveLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java @@ -1,6 +1,6 @@ -package com.kok.kokcore.application.port.out; +package com.kok.kokcore.location.application.port.out; -import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.location.domain.Location; import org.locationtech.jts.geom.Point; public interface SaveLocationPort { diff --git a/kok-core/src/main/java/com/kok/kokcore/application/domain/entity/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java similarity index 94% rename from kok-core/src/main/java/com/kok/kokcore/application/domain/entity/Location.java rename to kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index f3ef7d49..3f458cf7 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/domain/entity/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.application.domain.entity; +package com.kok.kokcore.location.domain; import jakarta.persistence.*; import lombok.AccessLevel; diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUsecase.java similarity index 66% rename from kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java rename to kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUsecase.java index 73163993..44445b80 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/usecase/CreateLocationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUsecase.java @@ -1,7 +1,6 @@ -package com.kok.kokcore.application.usecase; +package com.kok.kokcore.location.usecase; -import com.kok.kokcore.application.domain.entity.Location; -import org.locationtech.jts.geom.Point; +import com.kok.kokcore.location.domain.Location; import java.math.BigDecimal; diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUsecase.java similarity index 85% rename from kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java rename to kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUsecase.java index 88b62323..7ac575a2 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadCentroidUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUsecase.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.application.usecase; +package com.kok.kokcore.location.usecase; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; diff --git a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java similarity index 62% rename from kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadLocationUsecase.java rename to kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java index b8111748..3d4ecc6d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/application/usecase/ReadLocationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java @@ -1,6 +1,6 @@ -package com.kok.kokcore.application.usecase; +package com.kok.kokcore.location.usecase; -import com.kok.kokcore.application.domain.entity.Location; +import com.kok.kokcore.location.domain.Location; import java.util.List; From 41888dbca4607b75816fa730420b3f89237d521d Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 02:50:54 +0900 Subject: [PATCH 040/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20pub=20ke?= =?UTF-8?q?y=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 4 ++-- .gitignore | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index 63236d25..cde19b94 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -4,6 +4,7 @@ on: push: branches: - develop + - deploy/dev_cd # ๋ฐฐํฌ ํ…Œ์ŠคํŠธ ๋ธŒ๋žœ์น˜ jobs: deploy: @@ -16,7 +17,7 @@ jobs: run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem - name: Set permissions for SSH key file - run: chmod 600 /tmp/NCP_KEY.pem + run: chmod 400 /tmp/NCP_KEY.pem - name: Upload `docker-compose.yml` to NCP run: | @@ -27,7 +28,6 @@ jobs: with: host: ${{ secrets.NCP_HOST }} username: ${{ secrets.NCP_USER }} - password: ${{ secrets.NCP_PASSWORD }} key: ${{ secrets.NCP_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} diff --git a/.gitignore b/.gitignore index 1d8f5f99..68d7f2b4 100644 --- a/.gitignore +++ b/.gitignore @@ -330,6 +330,7 @@ gradle-app.setting ## ํ‚ค ## *.pem +*.pub **/application-local.yml /infra/docker-compose-local.yml From 42a952baae826a2bbe77fc2cbfbe02dc9e062747 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 03:51:28 +0900 Subject: [PATCH 041/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20image=20?= =?UTF-8?q?name=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/docker-compose-blue.yml | 2 +- infra/docker-compose-dev.yml | 2 +- infra/docker-compose-green.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/docker-compose-blue.yml b/infra/docker-compose-blue.yml index 794ecbdf..f19e7401 100644 --- a/infra/docker-compose-blue.yml +++ b/infra/docker-compose-blue.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-blue: container_name: kok-blue - image: ${DOCKERHUB_USERNAME}/kok_prod:latest + image: ${DOCKERHUB_USERNAME}/kok-prod:latest restart: always env_file: - .env diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml index 1150b1ea..93c8abe8 100644 --- a/infra/docker-compose-dev.yml +++ b/infra/docker-compose-dev.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-dev: container_name: kok-dev - image: ${DOCKERHUB_USERNAME}/kok_dev:latest + image: ${DOCKERHUB_USERNAME}/kok-dev:latest restart: always env_file: - .env diff --git a/infra/docker-compose-green.yml b/infra/docker-compose-green.yml index 1cc9d72e..04987e06 100644 --- a/infra/docker-compose-green.yml +++ b/infra/docker-compose-green.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-green: container_name: kok-green - image: ${DOCKERHUB_USERNAME}/kok_prod:latest + image: ${DOCKERHUB_USERNAME}/kok-prod:latest restart: always env_file: - .env From 891646c4971a53bcd7d30facdcce4744e098a709 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 04:04:31 +0900 Subject: [PATCH 042/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20image=20?= =?UTF-8?q?tag=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 8 ++++++++ infra/docker-compose-dev.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index cde19b94..a92770f8 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -13,6 +13,12 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Get Latest Docker Image Tag + id: latest_tag + run: | + LATEST_TAG=$(curl -s "https://hub.docker.com/v2/repositories/${{ secrets.DOCKERHUB_USERNAME }}/kok-dev/tags" | jq -r '.results[0].name') + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + - name: Create SSH key file run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem @@ -31,6 +37,8 @@ jobs: key: ${{ secrets.NCP_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} + + echo "KOK_DEV_TAG=${{ env.LATEST_TAG }}" > .env sudo docker compose pull sudo docker compose down sudo docker compose -f docker-compose-dev.yml up -d diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml index 93c8abe8..8056385c 100644 --- a/infra/docker-compose-dev.yml +++ b/infra/docker-compose-dev.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-dev: container_name: kok-dev - image: ${DOCKERHUB_USERNAME}/kok-dev:latest + image: ${DOCKERHUB_USERNAME}/kok-dev:${KOK_DEV_TAG} restart: always env_file: - .env From 166e7aae4b80609027ec60f1662c11adbfc0806d Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 04:17:35 +0900 Subject: [PATCH 043/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20image=20?= =?UTF-8?q?tag=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index a92770f8..faaf0e6c 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -17,7 +17,9 @@ jobs: id: latest_tag run: | LATEST_TAG=$(curl -s "https://hub.docker.com/v2/repositories/${{ secrets.DOCKERHUB_USERNAME }}/kok-dev/tags" | jq -r '.results[0].name') - echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + sed -i '/^KOK_DEV_TAG=/d' .env + echo "KOK_DEV_TAG = ${{ env.LATEST_TAG }}" >> .env + - name: Create SSH key file run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem From 6d8bf6facdbdda16c524d64a1de6558d8e36fdfa Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 04:18:50 +0900 Subject: [PATCH 044/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20image=20?= =?UTF-8?q?tag=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index faaf0e6c..930de3da 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -17,6 +17,7 @@ jobs: id: latest_tag run: | LATEST_TAG=$(curl -s "https://hub.docker.com/v2/repositories/${{ secrets.DOCKERHUB_USERNAME }}/kok-dev/tags" | jq -r '.results[0].name') + cd ${{ secrets.COMPOSE_FILE_PATH }} sed -i '/^KOK_DEV_TAG=/d' .env echo "KOK_DEV_TAG = ${{ env.LATEST_TAG }}" >> .env From 64cf756c33b159b1acc7f1a231500b9512946415 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 04:26:41 +0900 Subject: [PATCH 045/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20image=20?= =?UTF-8?q?tag=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index 930de3da..18c26f44 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -17,9 +17,7 @@ jobs: id: latest_tag run: | LATEST_TAG=$(curl -s "https://hub.docker.com/v2/repositories/${{ secrets.DOCKERHUB_USERNAME }}/kok-dev/tags" | jq -r '.results[0].name') - cd ${{ secrets.COMPOSE_FILE_PATH }} - sed -i '/^KOK_DEV_TAG=/d' .env - echo "KOK_DEV_TAG = ${{ env.LATEST_TAG }}" >> .env + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV - name: Create SSH key file @@ -41,7 +39,8 @@ jobs: script: | cd ${{ secrets.COMPOSE_FILE_PATH }} - echo "KOK_DEV_TAG=${{ env.LATEST_TAG }}" > .env + sed -i '/^KOK_DEV_TAG=/d' .env + echo "KOK_DEV_TAG=${{ env.LATEST_TAG }}" >> .env sudo docker compose pull sudo docker compose down sudo docker compose -f docker-compose-dev.yml up -d From b0c1e330b5f003522a70e1b7cb70f311797856a5 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 04:42:40 +0900 Subject: [PATCH 046/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20image=20?= =?UTF-8?q?tag=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index 18c26f44..f7bf5728 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -39,8 +39,8 @@ jobs: script: | cd ${{ secrets.COMPOSE_FILE_PATH }} - sed -i '/^KOK_DEV_TAG=/d' .env + sed -i '/^KOK_DEV_TAG = /d' .env echo "KOK_DEV_TAG=${{ env.LATEST_TAG }}" >> .env - sudo docker compose pull - sudo docker compose down + sudo docker compose -f docker-compose-dev.yml pull + sudo docker compose -f docker-compose-dev.yml down sudo docker compose -f docker-compose-dev.yml up -d From b9429ebf0ad4e2098259863ff90079bcb0483e78 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 04:56:25 +0900 Subject: [PATCH 047/163] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20change=20?= =?UTF-8?q?constraints=20for=20numeric=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kokapi/centroid/adapter/in/dto/LocationRequest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java index fac6e30b..833a8f61 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @@ -11,15 +12,15 @@ public record LocationRequest( @NotBlank(message = "uuid๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") String uuid, - @NotBlank(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") Integer memberId, - @NotBlank(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @NotNull(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") BigDecimal latitude, - @NotBlank(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @NotNull(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") BigDecimal longitude From b5c21be6f1e62915490180f8a0709abd37e86960 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 1 Mar 2025 12:45:30 +0900 Subject: [PATCH 048/163] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20add=20swa?= =?UTF-8?q?gger=20info=20for=20location=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kokapi/centroid/adapter/in/rest/LocationController.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index 18017109..6646ba46 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -10,6 +10,7 @@ import com.kok.kokcore.location.usecase.CreateLocationUsecase; import com.kok.kokcore.location.usecase.ReadCentroidUsecase; import com.kok.kokcore.location.usecase.ReadLocationUsecase; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; @@ -29,6 +30,7 @@ public class LocationController extends BaseController { private final ReadLocationUsecase readLocationUsecase; private final LocationMapper locationMapper; + @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") @PostMapping("/create") public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { createLocationUsecase.createLocation( @@ -45,6 +47,7 @@ public ResponseEntity> createLocation(@Valid @R )); } + @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its UUID") @GetMapping("/centroid/{uuid}") public ResponseEntity> getCentroid(@PathVariable String uuid) { Pair centroid = readCentroidUsecase.readCentroidCoordinates(uuid); @@ -54,6 +57,7 @@ public ResponseEntity> getCentroid(@PathVariabl )); } + @Operation(summary = "์œ„์น˜ ์กฐํšŒ", description = "Retrieve detailed information for a location using its UUID and member ID") @GetMapping("/{uuid}/{memberId}") public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { Location location = readLocationUsecase.readLocation(uuid, memberId); @@ -61,6 +65,7 @@ public ResponseEntity> getLocation(@PathVariabl return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); } + @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a UUID") @GetMapping("/{uuid}") public ResponseEntity>> getLocations(@PathVariable String uuid) { List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(uuid)); @@ -68,6 +73,7 @@ public ResponseEntity>> getLocations(@Path return ResponseEntity.ok(ApiResponseDto.success(responses)); } + @Operation(summary = "์œ„์น˜ ์ˆ˜์ •", description = "Update the location with the provided details.") @PutMapping("/update") public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.updateLocation( From d21ae094b1bb7777ebdf943921c13680f6a4dfeb Mon Sep 17 00:00:00 2001 From: MINSEOK LEE <91869302+minseokey@users.noreply.github.com> Date: Sat, 1 Mar 2025 16:10:47 +0900 Subject: [PATCH 049/163] =?UTF-8?q?=F0=9F=9A=80[Deploy/dev=20cd]=20dev=20c?= =?UTF-8?q?d=20configuration=20(#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿš€deploy: change pub key setting * ๐Ÿš€deploy: change image name setting * ๐Ÿš€deploy: change image tag setting * ๐Ÿš€deploy: change image tag setting * ๐Ÿš€deploy: change image tag setting * ๐Ÿš€deploy: change image tag setting * ๐Ÿš€deploy: change image tag setting * โ™ป๏ธrefactor: change constraints for numeric value * โ™ป๏ธrefactor: add swagger info for location controller --- .github/workflows/kok-dev-CD.yml | 18 ++++++++++++++---- .gitignore | 1 + infra/docker-compose-blue.yml | 2 +- infra/docker-compose-dev.yml | 2 +- infra/docker-compose-green.yml | 2 +- .../adapter/in/dto/LocationRequest.java | 7 ++++--- .../adapter/in/rest/LocationController.java | 6 ++++++ 7 files changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index 63236d25..f7bf5728 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -4,6 +4,7 @@ on: push: branches: - develop + - deploy/dev_cd # ๋ฐฐํฌ ํ…Œ์ŠคํŠธ ๋ธŒ๋žœ์น˜ jobs: deploy: @@ -12,11 +13,18 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Get Latest Docker Image Tag + id: latest_tag + run: | + LATEST_TAG=$(curl -s "https://hub.docker.com/v2/repositories/${{ secrets.DOCKERHUB_USERNAME }}/kok-dev/tags" | jq -r '.results[0].name') + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + + - name: Create SSH key file run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem - name: Set permissions for SSH key file - run: chmod 600 /tmp/NCP_KEY.pem + run: chmod 400 /tmp/NCP_KEY.pem - name: Upload `docker-compose.yml` to NCP run: | @@ -27,10 +35,12 @@ jobs: with: host: ${{ secrets.NCP_HOST }} username: ${{ secrets.NCP_USER }} - password: ${{ secrets.NCP_PASSWORD }} key: ${{ secrets.NCP_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} - sudo docker compose pull - sudo docker compose down + + sed -i '/^KOK_DEV_TAG = /d' .env + echo "KOK_DEV_TAG=${{ env.LATEST_TAG }}" >> .env + sudo docker compose -f docker-compose-dev.yml pull + sudo docker compose -f docker-compose-dev.yml down sudo docker compose -f docker-compose-dev.yml up -d diff --git a/.gitignore b/.gitignore index 1d8f5f99..68d7f2b4 100644 --- a/.gitignore +++ b/.gitignore @@ -330,6 +330,7 @@ gradle-app.setting ## ํ‚ค ## *.pem +*.pub **/application-local.yml /infra/docker-compose-local.yml diff --git a/infra/docker-compose-blue.yml b/infra/docker-compose-blue.yml index 794ecbdf..f19e7401 100644 --- a/infra/docker-compose-blue.yml +++ b/infra/docker-compose-blue.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-blue: container_name: kok-blue - image: ${DOCKERHUB_USERNAME}/kok_prod:latest + image: ${DOCKERHUB_USERNAME}/kok-prod:latest restart: always env_file: - .env diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml index 1150b1ea..8056385c 100644 --- a/infra/docker-compose-dev.yml +++ b/infra/docker-compose-dev.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-dev: container_name: kok-dev - image: ${DOCKERHUB_USERNAME}/kok_dev:latest + image: ${DOCKERHUB_USERNAME}/kok-dev:${KOK_DEV_TAG} restart: always env_file: - .env diff --git a/infra/docker-compose-green.yml b/infra/docker-compose-green.yml index 1cc9d72e..04987e06 100644 --- a/infra/docker-compose-green.yml +++ b/infra/docker-compose-green.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-green: container_name: kok-green - image: ${DOCKERHUB_USERNAME}/kok_prod:latest + image: ${DOCKERHUB_USERNAME}/kok-prod:latest restart: always env_file: - .env diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java index fac6e30b..833a8f61 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @@ -11,15 +12,15 @@ public record LocationRequest( @NotBlank(message = "uuid๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") String uuid, - @NotBlank(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") Integer memberId, - @NotBlank(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @NotNull(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") BigDecimal latitude, - @NotBlank(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @NotNull(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") BigDecimal longitude diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index 18017109..6646ba46 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -10,6 +10,7 @@ import com.kok.kokcore.location.usecase.CreateLocationUsecase; import com.kok.kokcore.location.usecase.ReadCentroidUsecase; import com.kok.kokcore.location.usecase.ReadLocationUsecase; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; @@ -29,6 +30,7 @@ public class LocationController extends BaseController { private final ReadLocationUsecase readLocationUsecase; private final LocationMapper locationMapper; + @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") @PostMapping("/create") public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { createLocationUsecase.createLocation( @@ -45,6 +47,7 @@ public ResponseEntity> createLocation(@Valid @R )); } + @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its UUID") @GetMapping("/centroid/{uuid}") public ResponseEntity> getCentroid(@PathVariable String uuid) { Pair centroid = readCentroidUsecase.readCentroidCoordinates(uuid); @@ -54,6 +57,7 @@ public ResponseEntity> getCentroid(@PathVariabl )); } + @Operation(summary = "์œ„์น˜ ์กฐํšŒ", description = "Retrieve detailed information for a location using its UUID and member ID") @GetMapping("/{uuid}/{memberId}") public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { Location location = readLocationUsecase.readLocation(uuid, memberId); @@ -61,6 +65,7 @@ public ResponseEntity> getLocation(@PathVariabl return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); } + @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a UUID") @GetMapping("/{uuid}") public ResponseEntity>> getLocations(@PathVariable String uuid) { List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(uuid)); @@ -68,6 +73,7 @@ public ResponseEntity>> getLocations(@Path return ResponseEntity.ok(ApiResponseDto.success(responses)); } + @Operation(summary = "์œ„์น˜ ์ˆ˜์ •", description = "Update the location with the provided details.") @PutMapping("/update") public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.updateLocation( From 05cab17736760d4d2247d1893884ac36c0611ede Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sun, 2 Mar 2025 00:57:18 +0900 Subject: [PATCH 050/163] =?UTF-8?q?=E2=9C=A8=20[Feature/testcontainers]=20?= =?UTF-8?q?implement=20testcontainers=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :recycle: refactor: move config package to same level as common * :bug: fix: remove no qualifying type of bean problem * :wrench: chore: add testcontainers dependency * :sparkles: feat: implement container creation abstract class for test * :sparkles: feat: implement service test template abstract class * :recycle: refactor: change database cleaner dialect to mysql * :chore: config: add mysql configs for container * :sparkles: feat: implement repository test template abstract class * :wrench: chore: config ddl-auto create on test * :feat: chore: activate fly-way * :bug: fix: add profile configuration on repository test * :wrench: refactor: add connect retry count for flyway * :wrench: refactor: remove mocking --------- Co-authored-by: YUN YOUNG --- kok-api/build.gradle | 5 +++ .../src/main/resources/application-dev.yml | 4 +-- .../src/main/resources/application-prod.yml | 4 +-- .../common/template/ContainerBaseTest.java | 36 +++++++++++++++++++ .../common/template/RepositoryTest.java | 18 ++++++++++ .../kokapi/common/template/ServiceTest.java | 18 ++++++++++ ...Cleaner.java => MySQLDatabaseCleaner.java} | 8 ++--- ...ava => MySQLDatabaseCleanerExtension.java} | 8 ++--- .../config/DataJpaTestConfig.java | 2 +- .../config/ServiceTestConfig.java | 17 ++------- .../service/RoomCreationServiceTest.java | 30 ++++------------ .../persistence/StationRepositoryTest.java | 8 ++--- .../service/StationServiceTest.java | 11 ++---- .../src/test/resources/application-test.yml | 18 ++++++++++ 14 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java rename kok-api/src/test/java/com/kok/kokapi/common/util/{DatabaseCleaner.java => MySQLDatabaseCleaner.java} (82%) rename kok-api/src/test/java/com/kok/kokapi/common/util/{DatabaseCleanerExtension.java => MySQLDatabaseCleanerExtension.java} (53%) rename kok-api/src/test/java/com/kok/kokapi/{common => }/config/DataJpaTestConfig.java (85%) rename kok-api/src/test/java/com/kok/kokapi/{common => }/config/ServiceTestConfig.java (50%) create mode 100644 kok-api/src/test/resources/application-test.yml diff --git a/kok-api/build.gradle b/kok-api/build.gradle index d9dc858a..6ca9ee2c 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -34,6 +34,11 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.testcontainers:testcontainers-bom:1.20.5' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:mysql' + testImplementation 'com.redis:testcontainers-redis' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 7287cdc6..1472afc0 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -13,9 +13,9 @@ spring: format_sql: true dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect open-in-view: false - defer-datasource-initialization: true + defer-datasource-initialization: false flyway: - enabled: false + enabled: true data: redis: cluster: diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 632d8a2f..f8ddcabc 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -13,9 +13,9 @@ spring: format_sql: true dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect open-in-view: false - defer-datasource-initialization: true + defer-datasource-initialization: false flyway: - enabled: false + enabled: true data: redis: cluster: diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java new file mode 100644 index 00000000..0c9c3073 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java @@ -0,0 +1,36 @@ +package com.kok.kokapi.common.template; + +import com.redis.testcontainers.RedisContainer; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MySQLContainer; + +public abstract class ContainerBaseTest { + + private static final int REDIS_PORT = 6379; + + private static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("kok") + .withUsername("root") + .withPassword("1234"); + + private static final RedisContainer redisContainer = new RedisContainer( + RedisContainer.DEFAULT_IMAGE_NAME.withTag(RedisContainer.DEFAULT_TAG) + ); + + static { + mysqlContainer.start(); + redisContainer.start(); + } + + @DynamicPropertySource + private static void dynamicProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", mysqlContainer::getJdbcUrl); + registry.add("spring.datasource.driver-class-name", mysqlContainer::getDriverClassName); + registry.add("spring.datasource.username", mysqlContainer::getUsername); + registry.add("spring.datasource.password", mysqlContainer::getPassword); + + registry.add("spring.data.redis.host", redisContainer::getHost); + registry.add("spring.data.redis.port", () -> redisContainer.getMappedPort(REDIS_PORT)); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java new file mode 100644 index 00000000..157ffe2d --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java @@ -0,0 +1,18 @@ +package com.kok.kokapi.common.template; + +import com.kok.kokapi.config.DataJpaTestConfig; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.TestPropertySource; + +@DataJpaTest +@Import({DataJpaTestConfig.class}) +@AutoConfigureTestDatabase(replace = Replace.NONE) +@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) +@Profile("test") +public abstract class RepositoryTest extends ContainerBaseTest{ + +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java new file mode 100644 index 00000000..105695d6 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java @@ -0,0 +1,18 @@ +package com.kok.kokapi.common.template; + +import com.kok.kokapi.common.util.MySQLDatabaseCleanerExtension; +import com.kok.kokapi.config.ServiceTestConfig; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.TestPropertySource; + +@ExtendWith(MySQLDatabaseCleanerExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@Import({ServiceTestConfig.class}) +@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) +@Profile("test") +public abstract class ServiceTest extends ContainerBaseTest{ + +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java similarity index 82% rename from kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java rename to kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java index ada54813..b2b89aae 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java @@ -8,7 +8,7 @@ import org.springframework.transaction.annotation.Transactional; @Component -public class DatabaseCleaner { +public class MySQLDatabaseCleaner { @PersistenceContext private EntityManager entityManager; @@ -16,14 +16,14 @@ public class DatabaseCleaner { @Transactional public void cleanUp() { entityManager.flush(); - entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); for (String tableName : getTableNames()) { entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); entityManager.createNativeQuery( - "ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1") + "ALTER TABLE " + tableName + " AUTO_INCREMENT = 1") .executeUpdate(); } - entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); } private List getTableNames() { diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java similarity index 53% rename from kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java rename to kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java index 8c67e1a7..883a572c 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java @@ -4,12 +4,12 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.test.context.junit.jupiter.SpringExtension; -public class DatabaseCleanerExtension implements BeforeEachCallback { +public class MySQLDatabaseCleanerExtension implements BeforeEachCallback { @Override public void beforeEach(ExtensionContext context) { - DatabaseCleaner databaseCleaner = SpringExtension.getApplicationContext(context) - .getBean(DatabaseCleaner.class); - databaseCleaner.cleanUp(); + MySQLDatabaseCleaner mySQLDatabaseCleaner = SpringExtension.getApplicationContext(context) + .getBean(MySQLDatabaseCleaner.class); + mySQLDatabaseCleaner.cleanUp(); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/config/DataJpaTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java similarity index 85% rename from kok-api/src/test/java/com/kok/kokapi/common/config/DataJpaTestConfig.java rename to kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java index 9e7e1d44..b30b6c03 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/config/DataJpaTestConfig.java +++ b/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.common.config; +package com.kok.kokapi.config; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.test.context.TestConfiguration; diff --git a/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java similarity index 50% rename from kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java rename to kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java index 49ea7f87..8c0042a2 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/config/ServiceTestConfig.java +++ b/kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java @@ -1,28 +1,17 @@ -package com.kok.kokapi.common.config; +package com.kok.kokapi.config; import com.kok.kokapi.station.adapter.out.external.FakeStationClient; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; -import com.kok.kokcore.room.usecase.JoinRoomUseCase; import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; @TestConfiguration public class ServiceTestConfig { @Bean + @Primary public LoadStationsPort loadStationsPort() { return new FakeStationClient(); } - - @Bean - public SaveRoomPort saveRoomPort() { - return Mockito.mock(SaveRoomPort.class); - } - - @Bean - public JoinRoomUseCase joinRoomUseCase() { - return Mockito.mock(JoinRoomUseCase.class); - } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java index 104e5845..fb70ba7a 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java @@ -1,38 +1,21 @@ package com.kok.kokapi.room.application.service; -import com.kok.kokapi.common.config.ServiceTestConfig; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.kok.kokapi.common.template.ServiceTest; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; -import com.kok.kokcore.room.usecase.JoinRoomUseCase; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -@Import({ServiceTestConfig.class}) -@SpringBootTest(classes = {RoomCreationService.class}) -class RoomCreationServiceTest { - private SaveRoomPort saveRoomPort; - private JoinRoomUseCase joinRoomUseCase; +class RoomCreationServiceTest extends ServiceTest { @Autowired private RoomCreationService roomCreationService; - @BeforeEach - void setUp() { - saveRoomPort = mock(SaveRoomPort.class); - joinRoomUseCase = mock(JoinRoomUseCase.class); - roomCreationService = new RoomCreationService(saveRoomPort, joinRoomUseCase); - } - @DisplayName("์•ฝ์†๋ฐฉ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @Test void createRoom() { @@ -43,7 +26,6 @@ void createRoom() { String password = "secret"; Member host = new Member(hostNickname, hostProfile, "Leader"); - when(saveRoomPort.save(any(Room.class))).thenAnswer(invocation -> invocation.getArgument(0)); Room createdRoom = roomCreationService.createRoom(roomName, capacity, host, password); assertAll("Room Create Test", diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java index e1d68399..b3df8257 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java @@ -2,18 +2,14 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import com.kok.kokapi.common.config.DataJpaTestConfig; +import com.kok.kokapi.common.template.RepositoryTest; import com.kok.kokcore.station.domain.entity.Station; import java.math.BigDecimal; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -@DataJpaTest -@Import({DataJpaTestConfig.class}) -class StationRepositoryTest { +class StationRepositoryTest extends RepositoryTest { @Autowired private StationRepository stationRepository; diff --git a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java index 098bd1f5..0107271a 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java @@ -2,22 +2,15 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.kok.kokapi.common.config.ServiceTestConfig; -import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.common.template.ServiceTest; import com.kok.kokapi.station.adapter.out.persistence.StationRepository; import com.kok.kokcore.station.domain.entity.Station; import java.math.BigDecimal; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; -@ExtendWith(DatabaseCleanerExtension.class) -@Import({ServiceTestConfig.class}) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) -class StationServiceTest { +class StationServiceTest extends ServiceTest { @Autowired private StationRepository stationRepository; diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml new file mode 100644 index 00000000..d00e0477 --- /dev/null +++ b/kok-api/src/test/resources/application-test.yml @@ -0,0 +1,18 @@ +spring: + profiles: + active: test + sql: + init: + mode: always + flyway: + enabled: true + connect-retries: 30 + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + defer-datasource-initialization: false + open-in-view: false + hibernate: + ddl-auto: create From d7cba1ae08006de66693215e2965d8bfc01a733f Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Sun, 2 Mar 2025 13:49:33 +0900 Subject: [PATCH 051/163] :bug: fix: update cors config (#41) --- .../kok/kokapi/config/web/WebMvcConfig.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java diff --git a/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java new file mode 100644 index 00000000..b46c16cd --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java @@ -0,0 +1,19 @@ +package com.kok.kokapi.config.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} From 118ab916a9d07e98c4f9108986570d8e6aa742ec Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Sun, 2 Mar 2025 13:50:56 +0900 Subject: [PATCH 052/163] :bug: fix: enable flyway.baseline-on-migrate to initialize schema history for non-empty schemas (#42) --- kok-api/src/main/resources/application-dev.yml | 1 + kok-api/src/main/resources/application-prod.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 1472afc0..9360d6ef 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -16,6 +16,7 @@ spring: defer-datasource-initialization: false flyway: enabled: true + baseline-on-migrate: true data: redis: cluster: diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index f8ddcabc..280f6162 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -16,6 +16,7 @@ spring: defer-datasource-initialization: false flyway: enabled: true + baseline-on-migrate: true data: redis: cluster: From 38158d97dbf7154f7f2a1ea4e7cc5d86a0ae1c2d Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 4 Mar 2025 20:53:30 +0900 Subject: [PATCH 053/163] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20add=20swa?= =?UTF-8?q?gger=20info=20for=20location=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../centroid/adapter/in/rest/LocationController.java | 11 +++++------ kok-api/src/main/resources/application-dev.yml | 8 ++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index 6646ba46..2d7ca0a2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -22,7 +22,6 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/location") public class LocationController extends BaseController { private final CreateLocationUsecase createLocationUsecase; @@ -31,7 +30,7 @@ public class LocationController extends BaseController { private final LocationMapper locationMapper; @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") - @PostMapping("/create") + @PostMapping("/location/create") public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { createLocationUsecase.createLocation( locationRequest.uuid(), @@ -48,7 +47,7 @@ public ResponseEntity> createLocation(@Valid @R } @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its UUID") - @GetMapping("/centroid/{uuid}") + @GetMapping("/location/centroid/{uuid}") public ResponseEntity> getCentroid(@PathVariable String uuid) { Pair centroid = readCentroidUsecase.readCentroidCoordinates(uuid); @@ -58,7 +57,7 @@ public ResponseEntity> getCentroid(@PathVariabl } @Operation(summary = "์œ„์น˜ ์กฐํšŒ", description = "Retrieve detailed information for a location using its UUID and member ID") - @GetMapping("/{uuid}/{memberId}") + @GetMapping("/location/{uuid}/{memberId}") public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { Location location = readLocationUsecase.readLocation(uuid, memberId); @@ -66,7 +65,7 @@ public ResponseEntity> getLocation(@PathVariabl } @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a UUID") - @GetMapping("/{uuid}") + @GetMapping("/location/{uuid}") public ResponseEntity>> getLocations(@PathVariable String uuid) { List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(uuid)); @@ -74,7 +73,7 @@ public ResponseEntity>> getLocations(@Path } @Operation(summary = "์œ„์น˜ ์ˆ˜์ •", description = "Update the location with the provided details.") - @PutMapping("/update") + @PutMapping("/location/update") public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.updateLocation( locationRequest.uuid(), diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 7287cdc6..d8138633 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -18,12 +18,8 @@ spring: enabled: false data: redis: - cluster: - nodes: - - ${REDIS_HOST1}:${REDIS_PORT} - - ${REDIS_HOST2}:${REDIS_PORT} - - ${REDIS_HOST3}:${REDIS_PORT} - max-redirects: 3 + host: ${REDIS_HOST} + port: ${REDIS_PORT} timeout: 5000 springdoc: From 4b38c189216a3d4b1edd2a310f2c99c995e2590c Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 4 Mar 2025 21:19:47 +0900 Subject: [PATCH 054/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20chage=20sed=20-i?= =?UTF-8?q?=20cd=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/kok-dev-CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index f7bf5728..60225dc8 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -40,7 +40,7 @@ jobs: cd ${{ secrets.COMPOSE_FILE_PATH }} sed -i '/^KOK_DEV_TAG = /d' .env - echo "KOK_DEV_TAG=${{ env.LATEST_TAG }}" >> .env + echo "KOK_DEV_TAG = ${{ env.LATEST_TAG }}" >> .env sudo docker compose -f docker-compose-dev.yml pull sudo docker compose -f docker-compose-dev.yml down sudo docker compose -f docker-compose-dev.yml up -d From 4b7a044a6e4ed72cfe607fe688fede84ac9a054c Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 4 Mar 2025 22:07:11 +0900 Subject: [PATCH 055/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20ncp=20re?= =?UTF-8?q?dis=20to=20instance=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/docker-compose-dev.yml | 11 +++++++++++ kok-api/src/main/resources/application-dev.yml | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml index 8056385c..eadba665 100644 --- a/infra/docker-compose-dev.yml +++ b/infra/docker-compose-dev.yml @@ -14,6 +14,17 @@ services: networks: - kok-network + # Redis ์„ค์ • + redis: + image: redis:7.0 + container_name: redis-dev + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - kok-network + networks: kok-network: driver: bridge diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index e83cd4c7..fdfbe041 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -19,8 +19,8 @@ spring: baseline-on-migrate: true data: redis: - host: ${REDIS_HOST} - port: ${REDIS_PORT} + host: localhost # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. + port: 6379 timeout: 5000 springdoc: From fef46573554ffb701b52114b99ef77d80b41e8ca Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 4 Mar 2025 22:35:59 +0900 Subject: [PATCH 056/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20add=20redis=20vol?= =?UTF-8?q?ume=20at=20docker-compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/docker-compose-dev.yml | 3 +++ kok-api/src/main/resources/application-dev.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml index eadba665..e7f753b7 100644 --- a/infra/docker-compose-dev.yml +++ b/infra/docker-compose-dev.yml @@ -25,6 +25,9 @@ services: networks: - kok-network +volumes: + redis_data: + networks: kok-network: driver: bridge diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index fdfbe041..9c22c228 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -19,7 +19,7 @@ spring: baseline-on-migrate: true data: redis: - host: localhost # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. + host: redis-dev # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. port: 6379 timeout: 5000 From a108eee9c6f764975f5995757d449344b0bcddde Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 4 Mar 2025 22:40:46 +0900 Subject: [PATCH 057/163] =?UTF-8?q?=F0=9F=9A=80deploy:=20change=20dev=20re?= =?UTF-8?q?dis=20path=20to=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kok-api/src/main/resources/application-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 9c22c228..96e03476 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -19,8 +19,8 @@ spring: baseline-on-migrate: true data: redis: - host: redis-dev # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. - port: 6379 + host: ${REDIS_HOST} # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. + port: ${REDIS_PORT} timeout: 5000 springdoc: From 18ce9a21e22ae58e795b0161371a8cdda68f2740 Mon Sep 17 00:00:00 2001 From: minseokey Date: Wed, 5 Mar 2025 03:14:07 +0900 Subject: [PATCH 058/163] =?UTF-8?q?=E2=9C=A8feature:=20implement=20route?= =?UTF-8?q?=20time=20and=20transit=20counter=20between=20user=20location?= =?UTF-8?q?=20and=20station?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/dto/{ => request}/LocationRequest.java | 2 +- .../dto/response}/CentroidResponse.java | 2 +- .../dto/response}/LocationResponse.java | 2 +- .../in/{rest => web}/LocationController.java | 8 +- .../adapter/out/mapper/LocationMapper.java | 2 +- .../RedisPublicTransportationCacheConfig.java | 30 +++++++ .../adapter/in/dto/RouteRequest.java | 12 +++ .../adapter/in/dto/RouteResponse.java | 10 +++ .../web/PublicTransfortationController.java | 31 +++++++ .../external/PublicTransportationClient.java | 39 ++++++++ .../out/external/TmapClientProperties.java | 12 +++ .../dto/TmapPublicTransportationResponse.java | 90 +++++++++++++++++++ .../TmapPublicTransportationService.java | 64 +++++++++++++ .../StationPersistenceAdapter.java | 11 ++- .../out/persistence/StationRepository.java | 4 + .../RetrievePublicTransportationUsecase.java | 9 ++ .../port/out/RetrieveStationsPort.java | 9 ++ 17 files changed, 328 insertions(+), 9 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/{ => request}/LocationRequest.java (94%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{out/dto => in/dto/response}/CentroidResponse.java (88%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{out/dto => in/dto/response}/LocationResponse.java (89%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/{rest => web}/LocationController.java (93%) create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java similarity index 94% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index 833a8f61..92ee6f7d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.in.dto; +package com.kok.kokapi.centroid.adapter.in.dto.request; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java similarity index 88% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java index 4f915910..7cda84c1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.out.dto; +package com.kok.kokapi.centroid.adapter.in.dto.response; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java similarity index 89% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index 10e6cba1..92b0060d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.out.dto; +package com.kok.kokapi.centroid.adapter.in.dto.response; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java similarity index 93% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index 2d7ca0a2..2d6e8b97 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -1,9 +1,9 @@ -package com.kok.kokapi.centroid.adapter.in.rest; +package com.kok.kokapi.centroid.adapter.in.web; import com.kok.kokapi.common.adapter.in.web.BaseController; -import com.kok.kokapi.centroid.adapter.in.dto.LocationRequest; -import com.kok.kokapi.centroid.adapter.out.dto.CentroidResponse; -import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.request.LocationRequest; +import com.kok.kokapi.centroid.adapter.in.dto.response.CentroidResponse; +import com.kok.kokapi.centroid.adapter.in.dto.response.LocationResponse; import com.kok.kokapi.centroid.adapter.out.mapper.LocationMapper; import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokcore.location.domain.Location; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index c74e78e2..d4e5d264 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -1,6 +1,6 @@ package com.kok.kokapi.centroid.adapter.out.mapper; -import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.response.LocationResponse; import com.kok.kokcore.location.domain.Location; import com.kok.kokapi.config.geometry.PointConverter; import org.springframework.stereotype.Component; diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java new file mode 100644 index 00000000..1bdf1187 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java @@ -0,0 +1,30 @@ +package com.kok.kokapi.config.redis.out; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisPublicTransportationCacheConfig { + + @Bean + public CacheManager contentCacheManager(RedisConnectionFactory cf) { + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ + + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build(); + } + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java new file mode 100644 index 00000000..85087e92 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java @@ -0,0 +1,12 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record RouteRequest( + @NotBlank(message = "UUID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String UUID, + @NotNull(message = "Member ID(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + Integer memberId +) { +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java new file mode 100644 index 00000000..f524e745 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java @@ -0,0 +1,10 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto; + +public record RouteResponse( + Integer totalTime, + Integer transferCount +) { + public static RouteResponse of(Integer totalTime, Integer transferCount) { + return new RouteResponse(totalTime, transferCount); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java new file mode 100644 index 00000000..fe040f45 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.public_transportation.adapter.in.web; + +import com.kok.kokapi.common.adapter.in.web.BaseController; +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.public_transportation.adapter.in.dto.RouteRequest; +import com.kok.kokapi.public_transportation.adapter.in.dto.RouteResponse; +import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.data.util.Pair; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class PublicTransfortationController extends BaseController { + + private final RetrievePublicTransportationUsecase retrievePublicTransportationUsecase; + + @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") + @PostMapping("/route/{stationId}") + public ResponseEntity> getPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { + Pair publicTransportation = + retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.UUID(), routeRequest.memberId()); + + return ResponseEntity.ok(ApiResponseDto.success( + RouteResponse.of(publicTransportation.getFirst(), publicTransportation.getSecond()) + )); + } + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java new file mode 100644 index 00000000..f24c2e79 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java @@ -0,0 +1,39 @@ +package com.kok.kokapi.public_transportation.adapter.out.external; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + + +@Component +@EnableConfigurationProperties(TmapClientProperties.class) +public class PublicTransportationClient { + + private final RestClient restClient; + private final TmapClientProperties properties; + + public PublicTransportationClient(TmapClientProperties properties) { + this.properties = properties; + this.restClient = getRestClient(); + } + + public RestClient getRestClient() { + return RestClient.builder() + .requestFactory(getRequestFactory()) + .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ + .baseUrl(properties.url()) // Base URL ์„ค์ • + .build(); + } + + public RestClient getClient() { + return this.restClient; + } + + private ClientHttpRequestFactory getRequestFactory() { + return ClientHttpRequestFactoryBuilder.detect() + .build(ClientHttpRequestFactorySettings.defaults()); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java new file mode 100644 index 00000000..4daf36c6 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java @@ -0,0 +1,12 @@ +package com.kok.kokapi.public_transportation.adapter.out.external; + +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@ConfigurationProperties(prefix = "tmap") +public record TmapClientProperties( + String key, + String url, + String keyname +){ +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java new file mode 100644 index 00000000..3ac57e45 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java @@ -0,0 +1,90 @@ +package com.kok.kokapi.public_transportation.adapter.out.external.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +// Tmap ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ POJO +@Getter +@Setter +public class TmapPublicTransportationResponse { + + // MetaData -> RequestParameters, Plan + // Plan -> Itinerary + // Itinerary -> Fare, totalTime, totalWalkTime, pathType, transferCount, totalDistance, totalWalkDistance + // ํ˜•์‹๊ณผ ๋‹ค๋ฅธ Json์€ ๋ฌด์‹œ. + + private MetaData metaData; + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + public static class MetaData { + private RequestParameters requestParameters; + private Plan plan; + + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + public static class RequestParameters { + private String endY; + private String endX; + private String startY; + private String startX; + private String reqDttm; + + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + public static class Plan { + private List itineraries; + + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + public static class Itinerary { + private Fare fare; + private int totalTime; + private int totalWalkTime; + private int pathType; + private int transferCount; + private int totalDistance; + private int totalWalkDistance; + + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + public static class Fare { + private RegularFare regular; + + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + public static class RegularFare { + private int totalFare; + private Currency currency; + + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @Getter + @Setter + public static class Currency { + private String symbol; + private String currency; + private String currencyCode; + + } +} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java new file mode 100644 index 00000000..187bd150 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -0,0 +1,64 @@ +package com.kok.kokapi.public_transportation.application.service; + +import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; +import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; +import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; +import com.kok.kokapi.station.adapter.out.persistence.StationPersistenceAdapter; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; +import com.kok.kokcore.station.domain.entity.Station; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TmapPublicTransportationService implements RetrievePublicTransportationUsecase { + + private final PublicTransportationClient publicTransportationClient; + private final StationPersistenceAdapter stationPersistenceAdapter; + private final LocationPersistenceAdapter locationPersistenceAdapter; + private final PointConverter pointConverter; + + @Override + public Pair retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { + // ์‚ฌ์šฉ์ž ์œ„์น˜ + Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") + ); + + Pair userLocation = pointConverter.toCoordinates(userPoint.getPoint()); + + Station station = stationPersistenceAdapter.retrieveStation(stationId).orElseThrow( + () -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") + ); + + // ์š”์ฒญ ๋ฐ์ดํ„ฐ + Map requestBody = new HashMap<>(); + requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ + requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ + requestBody.put("endX", station.getLongitude()); // ๊ฒฝ๋„ + requestBody.put("endY", station.getLatitude()); // ์œ„๋„ + requestBody.put("count", 1); // ๊ฒฝ๋กœ ํƒ์ƒ‰ ๊ฒฐ๊ณผ ์ค‘ 1๊ฐœ๋งŒ ๋ฐ˜ํ™˜ + requestBody.put("format", "json"); + + TmapPublicTransportationResponse rawRoute = publicTransportationClient.getClient().post() + .body(requestBody) // ์š”์ฒญ ๋ณธ๋ฌธ ์ถ”๊ฐ€ + .retrieve() + .body(TmapPublicTransportationResponse.class); + + + + return Pair.of( + rawRoute.getMetaData().getPlan().getItineraries().get(0).getTotalTime(), // ์ด ์†Œ์š” ์‹œ๊ฐ„ + rawRoute.getMetaData().getPlan().getItineraries().get(0).getTransferCount() // ํ™˜์Šน ํšŸ์ˆ˜ + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 53160d5a..111ea253 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -1,12 +1,15 @@ package com.kok.kokapi.station.adapter.out.persistence; import com.kok.kokcore.station.application.port.out.ReadStationsPort; +import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; import com.kok.kokcore.station.application.port.out.SaveStationsPort; import com.kok.kokcore.station.domain.entity.Station; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Arrays; import java.util.List; +import java.util.Optional; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.BatchPreparedStatementSetter; @@ -16,7 +19,7 @@ @Component @Slf4j @RequiredArgsConstructor -public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort { +public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort, RetrieveStationsPort { private static final String INSERT_SQL = """ INSERT INTO station (station_id, name, route, latitude, longitude, priority) @@ -57,4 +60,10 @@ public int getBatchSize() { public boolean hasNoStations() { return !stationRepository.existsAny(); } + + + @Override + public Optional retrieveStation(Long stationId) { + return stationRepository.findStationByStationId(stationId); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 9ec8d995..154c3da0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -4,8 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.Optional; + public interface StationRepository extends JpaRepository { @Query("SELECT EXISTS (SELECT 1 FROM Station)") boolean existsAny(); + + Optional findStationByStationId(Long stationId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java new file mode 100644 index 00000000..68b7d5c3 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.public_transfortation.usecase; + +import org.springframework.data.util.Pair; + + +public interface RetrievePublicTransportationUsecase { + // Pair<์†Œ์š”์‹œ๊ฐ„, ํ™˜์ŠนํšŸ์ˆ˜> + Pair retrievePublicTransportation(Long stationId, String UUID, Integer memberId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java new file mode 100644 index 00000000..245ebcfe --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.application.port.out; + +import com.kok.kokcore.station.domain.entity.Station; + +import java.util.Optional; + +public interface RetrieveStationsPort { + Optional retrieveStation(Long stationId); +} From 07c775d057ad4c53ea09855fe41bf2a4fc26b369 Mon Sep 17 00:00:00 2001 From: minseokey Date: Wed, 5 Mar 2025 03:23:59 +0900 Subject: [PATCH 059/163] =?UTF-8?q?=E2=9A=99chore:=20add=20tmap=20properti?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kok-api/src/main/resources/application-dev.yml | 5 +++++ kok-api/src/main/resources/application-prod.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 96e03476..227b3eaf 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -36,3 +36,8 @@ station: format: json start-idx: 1 end-idx: 1000 + +tmap: + key: ${TMAP_KEY} + url: "https://apis.openapi.sk.com/transit/routes/sub" + keyname: "appKey" \ No newline at end of file diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 280f6162..bb9c32c7 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -47,3 +47,8 @@ station: format: json start-idx: 1 end-idx: 1000 + +tmap: + key: ${TMAP_KEY} + url: "https://apis.openapi.sk.com/transit/routes/sub" + keyname: "appKey" \ No newline at end of file From 5c55c21c6245abc5ba61657a7ad19751d01f9c77 Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Thu, 6 Mar 2025 12:47:25 +0900 Subject: [PATCH 060/163] :recycle: :refactor: Replace BaseController with annotion (#46) --- .../adapter/in/rest/LocationController.java | 6 +++--- .../common/adapter/in/web/BaseController.java | 7 ------- .../kok/kokapi/config/annotion/V1Controller.java | 16 ++++++++++++++++ .../adapter/in/web/HealthCheckController.java | 7 +++---- .../room/adapter/in/web/RoomController.java | 6 +++--- 5 files changed, 25 insertions(+), 17 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/common/adapter/in/web/BaseController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java index 2d7ca0a2..d2f40258 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java @@ -1,11 +1,11 @@ package com.kok.kokapi.centroid.adapter.in.rest; -import com.kok.kokapi.common.adapter.in.web.BaseController; import com.kok.kokapi.centroid.adapter.in.dto.LocationRequest; import com.kok.kokapi.centroid.adapter.out.dto.CentroidResponse; import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; import com.kok.kokapi.centroid.adapter.out.mapper.LocationMapper; import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.usecase.CreateLocationUsecase; import com.kok.kokcore.location.usecase.ReadCentroidUsecase; @@ -20,9 +20,9 @@ import java.math.BigDecimal; import java.util.List; -@RestController +@V1Controller @RequiredArgsConstructor -public class LocationController extends BaseController { +public class LocationController { private final CreateLocationUsecase createLocationUsecase; private final ReadCentroidUsecase readCentroidUsecase; diff --git a/kok-api/src/main/java/com/kok/kokapi/common/adapter/in/web/BaseController.java b/kok-api/src/main/java/com/kok/kokapi/common/adapter/in/web/BaseController.java deleted file mode 100644 index 159a599b..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/common/adapter/in/web/BaseController.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kok.kokapi.common.adapter.in.web; - -import org.springframework.web.bind.annotation.RequestMapping; - -@RequestMapping("/v1/api") -public abstract class BaseController { -} diff --git a/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java b/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java new file mode 100644 index 00000000..b2c7718e --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java @@ -0,0 +1,16 @@ +package com.kok.kokapi.config.annotion; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@RestController +@RequestMapping("/v1/api") +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface V1Controller { +} diff --git a/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java index 01a9fbd2..259ab104 100644 --- a/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java +++ b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java @@ -1,15 +1,14 @@ package com.kok.kokapi.monitoring.adapter.in.web; +import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.monitoring.application.service.HealthCheckService; -import com.kok.kokapi.common.adapter.in.web.BaseController; import com.kok.kokapi.common.response.ApiResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -@RestController +@V1Controller @RequiredArgsConstructor -public class HealthCheckController extends BaseController { +public class HealthCheckController { private final HealthCheckService healthCheckService; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index f50cd275..d4e941cc 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -1,7 +1,7 @@ package com.kok.kokapi.room.adapter.in.web; -import com.kok.kokapi.common.adapter.in.web.BaseController; import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; @@ -20,9 +20,9 @@ import java.util.List; -@RestController +@V1Controller @RequiredArgsConstructor -public class RoomController extends BaseController { +public class RoomController { private final GetRoomUseCase getRoomUseCase; private final CreateRoomUseCase createRoomUseCase; From 0bd9ac564e62090059233a160dd5922107258000 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 6 Mar 2025 15:10:37 +0900 Subject: [PATCH 061/163] =?UTF-8?q?=E2=9C=A8feature:=20add=20caching=20for?= =?UTF-8?q?=20Tmap=20API=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RedisPublicTransportationCacheConfig.java | 17 ++++-- .../web/PublicTransfortationController.java | 7 +-- .../dto/TmapPublicTransportationResponse.java | 2 +- .../TmapPublicTransportationService.java | 53 +++++++++++-------- .../src/main/resources/application-dev.yml | 2 +- .../src/main/resources/application-prod.yml | 2 +- .../RetrievePublicTransportationUsecase.java | 7 ++- 7 files changed, 55 insertions(+), 35 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java index 1bdf1187..5f7bcb85 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java @@ -1,5 +1,6 @@ package com.kok.kokapi.config.redis.out; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; @@ -18,13 +19,21 @@ public class RedisPublicTransportationCacheConfig { @Bean - public CacheManager contentCacheManager(RedisConnectionFactory cf) { + public CacheManager contentCacheManager(RedisConnectionFactory redisConnectionFactory) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.deactivateDefaultTyping(); + + GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + // RedisCacheConfiguration ์„ค์ • RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ - return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build(); + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); } - } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java index fe040f45..9616717d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java @@ -7,10 +7,11 @@ import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -import org.springframework.data.util.Pair; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequiredArgsConstructor public class PublicTransfortationController extends BaseController { @@ -20,11 +21,11 @@ public class PublicTransfortationController extends BaseController { @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") @PostMapping("/route/{stationId}") public ResponseEntity> getPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { - Pair publicTransportation = + List publicTransportation = retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.UUID(), routeRequest.memberId()); return ResponseEntity.ok(ApiResponseDto.success( - RouteResponse.of(publicTransportation.getFirst(), publicTransportation.getSecond()) + RouteResponse.of(publicTransportation.get(0), publicTransportation.get(1)) )); } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java index 3ac57e45..1087207f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java @@ -87,4 +87,4 @@ public static class Currency { private String currencyCode; } -} \ No newline at end of file +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 187bd150..d0e92ec5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -11,10 +11,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Pair; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.HashMap; +import java.util.List; import java.util.Map; @Service @@ -27,20 +29,39 @@ public class TmapPublicTransportationService implements RetrievePublicTransporta private final LocationPersistenceAdapter locationPersistenceAdapter; private final PointConverter pointConverter; + + @Cacheable(value = "publicTransportation", cacheManager = "contentCacheManager", key = "'PTCache:' + #stationId + '-' + #UUID + '-' + #memberId") @Override - public Pair retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { - // ์‚ฌ์šฉ์ž ์œ„์น˜ - Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") + public List retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { + TmapPublicTransportationResponse rawRoute = getTmapDto(stationId, UUID, memberId); + return List.of( + rawRoute.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), // ์ด ์†Œ์š” ์‹œ๊ฐ„ + rawRoute.getMetaData().getPlan().getItineraries().getFirst().getTransferCount() // ํ™˜์Šน ํšŸ์ˆ˜ ); + } - Pair userLocation = pointConverter.toCoordinates(userPoint.getPoint()); + public TmapPublicTransportationResponse getTmapDto(Long stationId, String UUID, Integer memberId){ + log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); + return publicTransportationClient.getClient().post() + .body(buildRequestBody( + getUserLocation(UUID, memberId), + getStation(stationId))) + .retrieve() + .body(TmapPublicTransportationResponse.class); + } - Station station = stationPersistenceAdapter.retrieveStation(stationId).orElseThrow( - () -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") - ); + private Pair getUserLocation(String UUID, Integer memberId) { + Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + return pointConverter.toCoordinates(userPoint.getPoint()); + } - // ์š”์ฒญ ๋ฐ์ดํ„ฐ + private Station getStation(Long stationId) { + return stationPersistenceAdapter.retrieveStation(stationId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + } + + private Map buildRequestBody(Pair userLocation, Station station) { Map requestBody = new HashMap<>(); requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ @@ -48,17 +69,7 @@ public Pair retrievePublicTransportation(Long stationId, Strin requestBody.put("endY", station.getLatitude()); // ์œ„๋„ requestBody.put("count", 1); // ๊ฒฝ๋กœ ํƒ์ƒ‰ ๊ฒฐ๊ณผ ์ค‘ 1๊ฐœ๋งŒ ๋ฐ˜ํ™˜ requestBody.put("format", "json"); - - TmapPublicTransportationResponse rawRoute = publicTransportationClient.getClient().post() - .body(requestBody) // ์š”์ฒญ ๋ณธ๋ฌธ ์ถ”๊ฐ€ - .retrieve() - .body(TmapPublicTransportationResponse.class); - - - - return Pair.of( - rawRoute.getMetaData().getPlan().getItineraries().get(0).getTotalTime(), // ์ด ์†Œ์š” ์‹œ๊ฐ„ - rawRoute.getMetaData().getPlan().getItineraries().get(0).getTransferCount() // ํ™˜์Šน ํšŸ์ˆ˜ - ); + return requestBody; } + } diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 227b3eaf..d9b931b5 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -40,4 +40,4 @@ station: tmap: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" - keyname: "appKey" \ No newline at end of file + keyname: "appKey" diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index bb9c32c7..42f9ef89 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -51,4 +51,4 @@ station: tmap: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" - keyname: "appKey" \ No newline at end of file + keyname: "appKey" diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java index 68b7d5c3..e73b6f9d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java @@ -1,9 +1,8 @@ package com.kok.kokcore.public_transfortation.usecase; -import org.springframework.data.util.Pair; - +import java.util.List; public interface RetrievePublicTransportationUsecase { - // Pair<์†Œ์š”์‹œ๊ฐ„, ํ™˜์ŠนํšŸ์ˆ˜> - Pair retrievePublicTransportation(Long stationId, String UUID, Integer memberId); + // List{์†Œ์š”์‹œ๊ฐ„, ํ™˜์ŠนํšŸ์ˆ˜} + List retrievePublicTransportation(Long stationId, String UUID, Integer memberId); } From 2e208af04792fd01e44f6581961b96c7ca35e576 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 6 Mar 2025 18:56:11 +0900 Subject: [PATCH 062/163] =?UTF-8?q?=E2=9C=A8feature:=20add=20ConvexHull=20?= =?UTF-8?q?logic=20for=20return=20locations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/dto/{ => Request}/LocationRequest.java | 2 +- .../dto/Response}/CentroidResponse.java | 2 +- .../Response/ConvexHullLocationResponse.java | 12 +++++ .../dto/Response}/LocationResponse.java | 2 +- .../in/{rest => web}/LocationController.java | 21 +++++++-- .../adapter/out/mapper/LocationMapper.java | 2 +- .../LocationPersistenceAdapter.java | 10 ++++ .../out/persistence/LocationRepository.java | 46 +++++++++++++++++-- .../service/LocationQueryService.java | 18 ++++++-- .../port/out/ReadLocationPort.java | 2 + .../location/usecase/ReadLocationUsecase.java | 2 + 11 files changed, 102 insertions(+), 17 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/{ => Request}/LocationRequest.java (94%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{out/dto => in/dto/Response}/CentroidResponse.java (88%) create mode 100644 kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/ConvexHullLocationResponse.java rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/{out/dto => in/dto/Response}/LocationResponse.java (89%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/{rest => web}/LocationController.java (76%) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Request/LocationRequest.java similarity index 94% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Request/LocationRequest.java index 833a8f61..c95e63cb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Request/LocationRequest.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.in.dto; +package com.kok.kokapi.centroid.adapter.in.dto.Request; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/CentroidResponse.java similarity index 88% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/CentroidResponse.java index 4f915910..ffdde453 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/CentroidResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.out.dto; +package com.kok.kokapi.centroid.adapter.in.dto.Response; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/ConvexHullLocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/ConvexHullLocationResponse.java new file mode 100644 index 00000000..1b45cb44 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/ConvexHullLocationResponse.java @@ -0,0 +1,12 @@ +package com.kok.kokapi.centroid.adapter.in.dto.Response; + +import java.util.List; + +public record ConvexHullLocationResponse( + List convexHull, + List inside +) { + public static ConvexHullLocationResponse of(List convexHull, List inside) { + return new ConvexHullLocationResponse(convexHull, inside); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/LocationResponse.java similarity index 89% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/LocationResponse.java index 10e6cba1..290291cd 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/dto/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/LocationResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.out.dto; +package com.kok.kokapi.centroid.adapter.in.dto.Response; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java similarity index 76% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index d2f40258..c0836863 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/rest/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -1,8 +1,9 @@ -package com.kok.kokapi.centroid.adapter.in.rest; +package com.kok.kokapi.centroid.adapter.in.web; -import com.kok.kokapi.centroid.adapter.in.dto.LocationRequest; -import com.kok.kokapi.centroid.adapter.out.dto.CentroidResponse; -import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.Request.LocationRequest; +import com.kok.kokapi.centroid.adapter.in.dto.Response.CentroidResponse; +import com.kok.kokapi.centroid.adapter.in.dto.Response.ConvexHullLocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.Response.LocationResponse; import com.kok.kokapi.centroid.adapter.out.mapper.LocationMapper; import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; @@ -46,6 +47,7 @@ public ResponseEntity> createLocation(@Valid @R )); } + // For Test @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its UUID") @GetMapping("/location/centroid/{uuid}") public ResponseEntity> getCentroid(@PathVariable String uuid) { @@ -56,7 +58,7 @@ public ResponseEntity> getCentroid(@PathVariabl )); } - @Operation(summary = "์œ„์น˜ ์กฐํšŒ", description = "Retrieve detailed information for a location using its UUID and member ID") + @Operation(summary = "์œ„์น˜ ์กฐํšŒ Basic", description = "Retrieve detailed information for a location using its UUID and member ID") @GetMapping("/location/{uuid}/{memberId}") public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { Location location = readLocationUsecase.readLocation(uuid, memberId); @@ -64,6 +66,15 @@ public ResponseEntity> getLocation(@PathVariabl return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); } + @Operation(summary = "์œ„์น˜์กฐํšŒ ConvexHull", description = "Retrieve the ConvexHull inside list, outside list of locations for a UUID") + @GetMapping("/location/ConvH/{uuid}") + public ResponseEntity> getConvexHullLocations(@PathVariable String uuid){ + List convexHull = locationMapper.toResponseList(readLocationUsecase.readConvexHull(uuid)); + List inside = locationMapper.toResponseList(readLocationUsecase.readInsideConvexHull(uuid)); + + return ResponseEntity.ok(ApiResponseDto.success(ConvexHullLocationResponse.of(convexHull, inside ))); + } + @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a UUID") @GetMapping("/location/{uuid}") public ResponseEntity>> getLocations(@PathVariable String uuid) { diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index c74e78e2..14b17f0f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -1,6 +1,6 @@ package com.kok.kokapi.centroid.adapter.out.mapper; -import com.kok.kokapi.centroid.adapter.out.dto.LocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.Response.LocationResponse; import com.kok.kokcore.location.domain.Location; import com.kok.kokapi.config.geometry.PointConverter; import org.springframework.stereotype.Component; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index 57b403a6..73aeb3f6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -28,6 +28,16 @@ public List findLocationsByUuid(String uuid) { return locationRepository.findLocationsByUuid(uuid); } + @Override + public List findInsideConvexHull(String uuid) { + return locationRepository.findInsideConvexHull(uuid); + } + + @Override + public List findConvexHull(String uuid) { + return locationRepository.findConvexHull(uuid); + } + @Override public Point findCentroidByUuid(String uuid) { String centroidWKT = locationRepository.findCentroidByUuid(uuid); // WKT ํ˜•์‹์œผ๋กœ ๋ฐ›์Œ diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 819694c4..ee6779e5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -1,6 +1,5 @@ package com.kok.kokapi.centroid.adapter.out.persistence; - import com.kok.kokcore.location.domain.Location; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -8,7 +7,6 @@ import java.util.List; import java.util.Optional; - public interface LocationRepository extends JpaRepository { /* ํ•ด๋‹น ์ฟผ๋ฆฌ๋Š” ํŠน์ • UUID๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ„์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ํ•ด๋‹น ์œ„์น˜๋“ค์˜ ์ค‘์‹ฌ์ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. @@ -18,10 +16,52 @@ public interface LocationRepository extends JpaRepository { ๊ณ„์‚ฐ๋œ ์ค‘์‹ฌ์ ์„ ๋‹ค์‹œ SRID 4326 (์œ„๋„/๊ฒฝ๋„)์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (์ง€๋„ API์™€ ํ˜ธํ™˜์„ฑ ์œ ์ง€) ์ตœ์ข…์ ์œผ๋กœ ST_AsText๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ WKT(Well-Known Text) ํฌ๋งท์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. */ - @Query(value = "SELECT ST_AsText(ST_Transform(ST_Centroid(ST_Collect(ST_Transform(point, 3857))), 4326)) FROM location WHERE uuid = :uuid", nativeQuery = true) + @Query(value = """ + SELECT ST_AsText( + ST_Transform( + ST_Centroid( + ST_Collect( + ST_Transform(point, 3857))),4326) + ) + FROM location + WHERE uuid = :uuid + """, nativeQuery = true) String findCentroidByUuid(@Param("uuid") String uuid); Optional findLocationByUuidAndMemberId(String uuid, Integer memberId); List findLocationsByUuid(String uuid); + + @Query(value = """ + WITH ConvexHull AS ( + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(point)))) AS hull + FROM location + WHERE uuid = :uuid + ) + SELECT l.* + FROM location l, ConvexHull ch + WHERE l.uuid = :uuid + AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.point))) + """, nativeQuery = true) + List findInsideConvexHull(@Param("uuid") String uuid); + + @Query(value = """ + WITH ConvexHull AS ( + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(point)))) AS hull, + ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(point))))) AS center + FROM location + WHERE uuid = :uuid + ) + SELECT l.*, + ATAN2( + ST_Y(ST_GeomFromText(ST_AsText(l.point))) - ST_Y(ch.center), + ST_X(ST_GeomFromText(ST_AsText(l.point))) - ST_X(ch.center) + ) AS angle + FROM location l, ConvexHull ch + WHERE l.uuid = :uuid + AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.point))) + ORDER BY angle + """, nativeQuery = true) + List findConvexHull(@Param("uuid") String uuid); + } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index 4fad519a..5b5970e5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -1,5 +1,6 @@ package com.kok.kokapi.centroid.application.service; +import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.usecase.ReadLocationUsecase; @@ -13,6 +14,7 @@ public class LocationQueryService implements ReadLocationUsecase{ private final ReadLocationPort readLocationPort; + private final PointConverter pointConverter; @Override public Location readLocation(String uuid, Integer memberId) { @@ -22,10 +24,16 @@ public Location readLocation(String uuid, Integer memberId) { @Override public List readLocations(String uuid) { - List locations = readLocationPort.findLocationsByUuid(uuid); - if (locations.isEmpty()) { - throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์œ„์น˜๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - return locations; + return readLocationPort.findLocationsByUuid(uuid); + } + + @Override + public List readInsideConvexHull(String uuid) { + return readLocationPort.findInsideConvexHull(uuid); + } + + @Override + public List readConvexHull(String uuid) { + return readLocationPort.findConvexHull(uuid); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java index 5353a14d..c70871e3 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java @@ -8,4 +8,6 @@ public interface ReadLocationPort { Optional findLocationByUuidAndMemberId(String uuid, Integer memberId); List findLocationsByUuid(String uuid); + List findInsideConvexHull(String uuid); + List findConvexHull(String uuid); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java index 3d4ecc6d..330bda68 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java @@ -7,4 +7,6 @@ public interface ReadLocationUsecase { Location readLocation(String uuid, Integer memberId); List readLocations(String uuid); + List readInsideConvexHull(String uuid); + List readConvexHull(String uuid); } From c9bd2ae222117d97cf640e9e975cbe3270c4606f Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 6 Mar 2025 18:59:34 +0900 Subject: [PATCH 063/163] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20remove=20?= =?UTF-8?q?dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../centroid/application/service/LocationQueryService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index 5b5970e5..07df7c24 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -1,6 +1,5 @@ package com.kok.kokapi.centroid.application.service; -import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.usecase.ReadLocationUsecase; @@ -14,7 +13,6 @@ public class LocationQueryService implements ReadLocationUsecase{ private final ReadLocationPort readLocationPort; - private final PointConverter pointConverter; @Override public Location readLocation(String uuid, Integer memberId) { From b3c3d076dd14fbf1521b1e23adb26fabdeaa052c Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 6 Mar 2025 21:22:30 +0900 Subject: [PATCH 064/163] :sparkles: [Feature/Room] implement finding Room for Client Sharing link (#49) * :recycle: refactor: remove url from Room domain * :recycle: refactor: remove unused parameter from Room * :white_check_mark: test: add test for finding room query service * :recycle: refactor: add end of line --- .../in/dto/response/RoomDetailResponse.java | 6 +-- .../application/service/RoomQueryService.java | 3 +- .../service/RoomQueryServiceTest.java | 46 +++++++++++++++++++ .../com/kok/kokcore/room/domain/Room.java | 13 ++---- 4 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index bfce59db..09d69628 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -6,16 +6,14 @@ public record RoomDetailResponse( String id, String roomName, int capacity, - MemberResponse member, - String roomLinkUrl + MemberResponse member ) { public static RoomDetailResponse from(Room room) { return new RoomDetailResponse( room.getId(), room.getRoomName(), room.getCapacity(), - MemberResponse.from(room.getMember()), - room.getRoomLinkUrl() + MemberResponse.from(room.getMember()) ); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index 0b007d0d..d16d3dba 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -17,8 +17,9 @@ @RequiredArgsConstructor public class RoomQueryService implements GetRoomUseCase { - private final LoadRoomPort loadRoomPort; private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; + + private final LoadRoomPort loadRoomPort; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java new file mode 100644 index 00000000..f16c8c3f --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java @@ -0,0 +1,46 @@ +package com.kok.kokapi.room.application.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.kok.kokapi.common.template.ServiceTest; +import com.kok.kokcore.room.application.port.out.SaveRoomPort; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class RoomQueryServiceTest extends ServiceTest { + + @Autowired + private SaveRoomPort saveRoomPort; + @Autowired + private RoomQueryService roomQueryService; + + @DisplayName("์กด์žฌํ•˜๋Š” ๋ฐฉ์˜ ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค.") + @Test + void findRoomById() { + // given + Member member = new Member("nickname", "image", "Leader"); + Room room = saveRoomPort.save(Room.create("room", 2, member, "1234")); + + // when + Room result = roomQueryService.findRoomById(room.getId()); + + // then + assertThat(result).isEqualTo(room); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์˜ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋ ค๊ณ  ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void cannotFindRoomById() { + // given + String roomId = "unknownId"; + + // when & then + assertThatThrownBy(() -> roomQueryService.findRoomById(roomId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Room not found with id: " + roomId); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 877660bc..d440f90a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -11,35 +11,30 @@ public class Room implements Serializable { public static final int REQUIRED_CAPACITY = 2; - public static final String ROOM_LINK_URL = "kakao://app/room?roomId="; private final String id; // ์•ฝ์†๋ฐฉ ID (UUID) private final String roomName; // ์•ฝ์†๋ฐฉ ์ด๋ฆ„ private final int capacity; // ์ฐธ์—ฌ์ธ์› ์ˆ˜ (์ตœ์†Œ 2๋ช… ์ด์ƒ) private final String password; // ๋ฐฉ ๋น„๋ฐ€๋ฒˆํ˜ธ (์˜ต์…˜) private final Member member; // ๋ฐฉ ์ฐธ์—ฌ์ž - private final String roomLinkUrl; // ์ƒ์„ฑ๋œ ์•ฝ์†๋ฐฉ ์ž…์žฅ ๋งํฌ - private Room(String id, String roomName, int capacity, Member member, - String password, String roomLinkUrl) { + private Room(String id, String roomName, int capacity, Member member, String password) { this.id = id; this.roomName = roomName; this.capacity = capacity; this.member = member; this.password = password; - this.roomLinkUrl = roomLinkUrl; } public static Room create(String roomName, int capacity, Member host, String password) { - validateParameter(roomName, capacity, host); + validateParameter(roomName, capacity); String roomId = UUID.randomUUID().toString(); - String roomLinkUrl = ROOM_LINK_URL + roomId; - return new Room(roomId, roomName, capacity, host, password, roomLinkUrl); + return new Room(roomId, roomName, capacity, host, password); } - private static void validateParameter(String roomName, int capacity, Member host) { + private static void validateParameter(String roomName, int capacity) { if (roomName == null || roomName.trim().isEmpty()) { throw new IllegalArgumentException("Room name is required"); } From 92f03a45bebd9d96beb2e520a0eca2b776dbda18 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 8 Mar 2025 19:17:59 +0900 Subject: [PATCH 065/163] :recycle: refactor: change package path --- .../{Request => request}/LocationRequest.java | 2 +- .../CentroidResponse.java | 2 +- .../ConvexHullLocationResponse.java | 2 +- .../LocationResponse.java | 2 +- .../adapter/in/web/LocationController.java | 20 +++++++++---------- .../adapter/out/mapper/LocationMapper.java | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/{Request => request}/LocationRequest.java (94%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/{Response => response}/CentroidResponse.java (88%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/{Response => response}/ConvexHullLocationResponse.java (85%) rename kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/{Response => response}/LocationResponse.java (89%) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Request/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java similarity index 94% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Request/LocationRequest.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index c95e63cb..92ee6f7d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Request/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.in.dto.Request; +package com.kok.kokapi.centroid.adapter.in.dto.request; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java similarity index 88% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/CentroidResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java index ffdde453..7cda84c1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.in.dto.Response; +package com.kok.kokapi.centroid.adapter.in.dto.response; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/ConvexHullLocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java similarity index 85% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/ConvexHullLocationResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java index 1b45cb44..4fafd2b4 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/ConvexHullLocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.in.dto.Response; +package com.kok.kokapi.centroid.adapter.in.dto.response; import java.util.List; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java similarity index 89% rename from kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/LocationResponse.java rename to kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index 290291cd..92b0060d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/Response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.centroid.adapter.in.dto.Response; +package com.kok.kokapi.centroid.adapter.in.dto.response; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index c0836863..dd169e31 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -1,9 +1,9 @@ package com.kok.kokapi.centroid.adapter.in.web; -import com.kok.kokapi.centroid.adapter.in.dto.Request.LocationRequest; -import com.kok.kokapi.centroid.adapter.in.dto.Response.CentroidResponse; -import com.kok.kokapi.centroid.adapter.in.dto.Response.ConvexHullLocationResponse; -import com.kok.kokapi.centroid.adapter.in.dto.Response.LocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.request.LocationRequest; +import com.kok.kokapi.centroid.adapter.in.dto.response.CentroidResponse; +import com.kok.kokapi.centroid.adapter.in.dto.response.ConvexHullLocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.response.LocationResponse; import com.kok.kokapi.centroid.adapter.out.mapper.LocationMapper; import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; @@ -31,7 +31,7 @@ public class LocationController { private final LocationMapper locationMapper; @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") - @PostMapping("/location/create") + @PostMapping("/locations") public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { createLocationUsecase.createLocation( locationRequest.uuid(), @@ -49,7 +49,7 @@ public ResponseEntity> createLocation(@Valid @R // For Test @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its UUID") - @GetMapping("/location/centroid/{uuid}") + @GetMapping("/locations/centroid/{uuid}") public ResponseEntity> getCentroid(@PathVariable String uuid) { Pair centroid = readCentroidUsecase.readCentroidCoordinates(uuid); @@ -59,7 +59,7 @@ public ResponseEntity> getCentroid(@PathVariabl } @Operation(summary = "์œ„์น˜ ์กฐํšŒ Basic", description = "Retrieve detailed information for a location using its UUID and member ID") - @GetMapping("/location/{uuid}/{memberId}") + @GetMapping("/locations/{uuid}/{memberId}") public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { Location location = readLocationUsecase.readLocation(uuid, memberId); @@ -67,7 +67,7 @@ public ResponseEntity> getLocation(@PathVariabl } @Operation(summary = "์œ„์น˜์กฐํšŒ ConvexHull", description = "Retrieve the ConvexHull inside list, outside list of locations for a UUID") - @GetMapping("/location/ConvH/{uuid}") + @GetMapping("/locations/ConvH/{uuid}") public ResponseEntity> getConvexHullLocations(@PathVariable String uuid){ List convexHull = locationMapper.toResponseList(readLocationUsecase.readConvexHull(uuid)); List inside = locationMapper.toResponseList(readLocationUsecase.readInsideConvexHull(uuid)); @@ -76,7 +76,7 @@ public ResponseEntity> getConvexHullL } @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a UUID") - @GetMapping("/location/{uuid}") + @GetMapping("/locations/{uuid}") public ResponseEntity>> getLocations(@PathVariable String uuid) { List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(uuid)); @@ -84,7 +84,7 @@ public ResponseEntity>> getLocations(@Path } @Operation(summary = "์œ„์น˜ ์ˆ˜์ •", description = "Update the location with the provided details.") - @PutMapping("/location/update") + @PutMapping("/locations") public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.updateLocation( locationRequest.uuid(), diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index 14b17f0f..d4e5d264 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -1,6 +1,6 @@ package com.kok.kokapi.centroid.adapter.out.mapper; -import com.kok.kokapi.centroid.adapter.in.dto.Response.LocationResponse; +import com.kok.kokapi.centroid.adapter.in.dto.response.LocationResponse; import com.kok.kokcore.location.domain.Location; import com.kok.kokapi.config.geometry.PointConverter; import org.springframework.stereotype.Component; From da31cedc8809f08fa7e900d0ab0bc8b14c25025e Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 8 Mar 2025 19:44:54 +0900 Subject: [PATCH 066/163] :recycle: refactor: add error handler on restClient call" --- .../adapter/in/web/PublicTransfortationController.java | 1 - .../external/dto/TmapPublicTransportationResponse.java | 8 +------- .../service/TmapPublicTransportationService.java | 8 ++++++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java index da8aa94b..d39066a1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java @@ -13,7 +13,6 @@ import java.util.List; @V1Controller -@RestController @RequiredArgsConstructor public class PublicTransfortationController { diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java index 1087207f..e243edc7 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java @@ -9,6 +9,7 @@ // Tmap ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ POJO @Getter @Setter +@JsonIgnoreProperties(ignoreUnknown = true) public class TmapPublicTransportationResponse { // MetaData -> RequestParameters, Plan @@ -18,7 +19,6 @@ public class TmapPublicTransportationResponse { private MetaData metaData; - @JsonIgnoreProperties(ignoreUnknown = true) @Getter @Setter public static class MetaData { @@ -27,7 +27,6 @@ public static class MetaData { } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter @Setter public static class RequestParameters { @@ -39,7 +38,6 @@ public static class RequestParameters { } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter @Setter public static class Plan { @@ -47,7 +45,6 @@ public static class Plan { } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter @Setter public static class Itinerary { @@ -61,7 +58,6 @@ public static class Itinerary { } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter @Setter public static class Fare { @@ -69,7 +65,6 @@ public static class Fare { } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter @Setter public static class RegularFare { @@ -78,7 +73,6 @@ public static class RegularFare { } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter @Setter public static class Currency { diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index d0e92ec5..9c2c0334 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Pair; import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -33,20 +34,23 @@ public class TmapPublicTransportationService implements RetrievePublicTransporta @Cacheable(value = "publicTransportation", cacheManager = "contentCacheManager", key = "'PTCache:' + #stationId + '-' + #UUID + '-' + #memberId") @Override public List retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { - TmapPublicTransportationResponse rawRoute = getTmapDto(stationId, UUID, memberId); + TmapPublicTransportationResponse rawRoute = callPublicTransportRoute(stationId, UUID, memberId); return List.of( rawRoute.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), // ์ด ์†Œ์š” ์‹œ๊ฐ„ rawRoute.getMetaData().getPlan().getItineraries().getFirst().getTransferCount() // ํ™˜์Šน ํšŸ์ˆ˜ ); } - public TmapPublicTransportationResponse getTmapDto(Long stationId, String UUID, Integer memberId){ + public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String UUID, Integer memberId){ log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); return publicTransportationClient.getClient().post() .body(buildRequestBody( getUserLocation(UUID, memberId), getStation(stationId))) .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค." + status); + }) .body(TmapPublicTransportationResponse.class); } From 87d5a1e5326c836141540b5909d7f3f415679d80 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 8 Mar 2025 23:44:26 +0900 Subject: [PATCH 067/163] =?UTF-8?q?=F0=9F=90=9B=20[Fix/station-normalizati?= =?UTF-8?q?on]=20normalize=20station=20table=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: normalize station entity into station and route & make StationRouteDtos for api-core * :sparkles: feat: batch save routes by station * :sparkles: feat: get Station distinct by name * :bug: fix: fix INSERT_STATION_SQL error * :bug: fix: fix wrong field * :bug: fix: fix wrong field length constraint * :recycle: refactor: change route entity field name (route -> name) * :recycle: refactor: normalize flyway ddl station table * :recycle: refactor: seperate route save responsibility from save station port * :recycle: refactor: refactor using MapSqlParameterSource and NamedParameterJdbcTemplate * :recycle: refactor: remove duplicated Transactional and refactor to lambda expression * :recycle: refactor: update redis, mysql version * :sparkles: feat: update location ddl for flyway * :wrench: config: sync with application-dev configuration * :wrench: config: change ddl-auto to validate for dev environment * :recycle: refactor: add logging for open api result * :recycle: refactor: modify log level to debug * :recycle: refactor: change component annotation to repository annotation * :sparkles: feat: return only saved stations * :recycle: refactor: implement batch update for route and station * :sparkles: feat: handle server error of external api --- .../adapter/out/external/StationClient.java | 18 +++- .../out/external/StationErrorHandler.java | 34 +++++++ .../out/external/dto/StationResponse.java | 6 +- .../out/external/dto/StationResponses.java | 11 +-- .../persistence/RoutePersistenceAdapter.java | 47 +++++++++ .../StationPersistenceAdapter.java | 63 ++++++------ .../out/persistence/StationRepository.java | 3 + .../application/config/StationsConfig.java | 9 +- .../application/service/StationService.java | 14 +-- .../src/main/resources/application-dev.yml | 2 +- .../main/resources/db/V1__init_station.sql | 32 +++++-- .../common/template/ContainerBaseTest.java | 10 +- .../out/external/FakeStationClient.java | 14 +-- .../StationPersistenceAdapterTest.java | 37 ++++++++ .../persistence/StationRepositoryTest.java | 2 +- .../service/StationServiceTest.java | 2 +- .../src/test/resources/application-test.yml | 8 +- .../port/out/LoadStationsPort.java | 5 +- .../application/port/out/SaveRoutePort.java | 9 ++ .../port/out/SaveStationsPort.java | 2 +- .../port/out/dto/StationRouteDto.java | 25 +++++ .../port/out/dto/StationRouteDtos.java | 44 +++++++++ .../kokcore/station/domain/entity/Route.java | 36 +++++++ .../station/domain/entity/Station.java | 24 +---- .../port/out/dto/StationRouteDtosTest.java | 95 +++++++++++++++++++ 25 files changed, 442 insertions(+), 110 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java create mode 100644 kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java index 7657e954..2b1b430b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java @@ -2,9 +2,9 @@ import com.kok.kokapi.station.adapter.out.external.dto.StationResponses; import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; +import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; import java.util.StringJoiner; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; @@ -12,6 +12,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +@Slf4j @Component @EnableConfigurationProperties(StationClientProperties.class) public class StationClient implements LoadStationsPort { @@ -20,9 +21,11 @@ public class StationClient implements LoadStationsPort { private final RestClient restClient; private final StationClientProperties properties; + private final StationErrorHandler stationErrorHandler; - public StationClient(StationClientProperties properties) { + public StationClient(StationClientProperties properties, StationErrorHandler stationErrorHandler) { this.properties = properties; + this.stationErrorHandler = stationErrorHandler; this.restClient = getRestClient(); } @@ -30,6 +33,7 @@ public RestClient getRestClient() { return RestClient.builder() .requestFactory(getRequestFactory()) .baseUrl(properties.baseUrl()) + .defaultStatusHandler(stationErrorHandler) .build(); } @@ -39,12 +43,16 @@ private ClientHttpRequestFactory getRequestFactory() { } @Override - public List loadAllStations() { + public StationRouteDtos loadAllStations() { StationResponses responses = restClient.get() .uri(getTargetUri()) .retrieve() .body(StationResponses.class); - return responses.toStations(); + log.debug("Seoul Data Open API Status Code: {}, Message: {}", + responses.subwayStationMaster().result().code(), + responses.subwayStationMaster().result().message() + ); + return responses.toStationRouteDtos(); } public String getTargetUri() { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java new file mode 100644 index 00000000..408e4d2d --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java @@ -0,0 +1,34 @@ +package com.kok.kokapi.station.adapter.out.external; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokapi.station.adapter.out.external.dto.StationResponses; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResponseErrorHandler; + +@Component +@RequiredArgsConstructor +public class StationErrorHandler implements ResponseErrorHandler { + + private static final List SERVER_ERROR = List.of("ERROR-500", "ERROR-600", "ERROR-601"); + private final ObjectMapper objectMapper; + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + String code = objectMapper.readValue(response.getBody(), StationResponses.class) + .subwayStationMaster() + .result() + .code(); + return SERVER_ERROR.contains(code); + } + + @Override + public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException { + throw new RuntimeException("์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ง€์†์ ์œผ๋กœ ๋ฐœ์ƒ์‹œ ์—ด๋ฆฐ ๋ฐ์ดํ„ฐ ๊ด‘์žฅ์œผ๋กœ ๋ฌธ์˜(Q&A) ๋ฐ”๋ž๋‹ˆ๋‹ค."); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java index f0c3f2cd..24245da5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java @@ -1,7 +1,7 @@ package com.kok.kokapi.station.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.application.port.out.dto.StationRouteDto; public record StationResponse( @JsonProperty("BLDN_ID") @@ -16,7 +16,7 @@ public record StationResponse( String longitude ) { - public Station toStation() { - return new Station(id, name, route, latitude, longitude); + public StationRouteDto toStationRouteDto() { + return new StationRouteDto(name, latitude, longitude, id, route); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java index c744d132..c77c4dd0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java @@ -1,17 +1,16 @@ package com.kok.kokapi.station.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; +import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; public record StationResponses( @JsonProperty("subwayStationMaster") SubwayStationMaster subwayStationMaster ) { - public List toStations() { - return subwayStationMaster.row().stream() - .map(StationResponse::toStation) - .toList(); + public StationRouteDtos toStationRouteDtos() { + return new StationRouteDtos(subwayStationMaster.row().stream() + .map(StationResponse::toStationRouteDto) + .toList()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java new file mode 100644 index 00000000..90a8b872 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java @@ -0,0 +1,47 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import com.kok.kokcore.station.application.port.out.SaveRoutePort; +import com.kok.kokcore.station.domain.entity.Route; +import java.util.List; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@Slf4j +@RequiredArgsConstructor +public class RoutePersistenceAdapter implements SaveRoutePort { + + private static final String INSERT_ROUTE_SQL = """ + INSERT INTO route (code, name, station_id) + VALUES (:code, :name, :station_id) + """; + private static final Function mapToParams = route -> + new MapSqlParameterSource() + .addValue("code", route.getCode()) + .addValue("name", route.getName()) + .addValue("station_id", route.getStation().getId()); + + private final NamedParameterJdbcTemplate jdbcTemplate; + + @Override + public void saveRoutes(List routes) { + if (routes.isEmpty()) { + log.debug("No routes to save."); + return; + } + batchInsertRoutes(routes); + } + + private void batchInsertRoutes(List routes) { + MapSqlParameterSource[] batchParams = routes.stream() + .map(mapToParams) + .toArray(MapSqlParameterSource[]::new); + int[] batched = jdbcTemplate.batchUpdate(INSERT_ROUTE_SQL, batchParams); + log.debug("Successfully saved a total of {} routes out of {}.", batched.length, + routes.size()); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 111ea253..86e18e6b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -4,56 +4,52 @@ import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; import com.kok.kokcore.station.application.port.out.SaveStationsPort; import com.kok.kokcore.station.domain.entity.Station; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.Arrays; import java.util.List; +import java.util.function.Function; import java.util.Optional; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.jdbc.core.BatchPreparedStatementSetter; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; -@Component +@Repository @Slf4j @RequiredArgsConstructor public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort, RetrieveStationsPort { - private static final String INSERT_SQL = """ - INSERT INTO station (station_id, name, route, latitude, longitude, priority) - VALUES (?, ?, ?, ?, ?, ?) + private static final String INSERT_STATION_SQL = """ + INSERT INTO station (name, latitude, longitude, priority) + VALUES (:name, :latitude, :longitude, :priority) """; + private static final Function mapToParams = station -> + new MapSqlParameterSource() + .addValue("name", station.getName()) + .addValue("latitude", station.getLatitude()) + .addValue("longitude", station.getLongitude()) + .addValue("priority", station.getPriority()); private final StationRepository stationRepository; - private final JdbcTemplate jdbcTemplate; + private final NamedParameterJdbcTemplate jdbcTemplate; @Override - public void saveStations(List stations) { + public List saveStations(List stations) { if (stations.isEmpty()) { - log.info("No stations to save."); - return; + log.debug("No stations to save."); + return List.of(); } - int[] batches = jdbcTemplate.batchUpdate(INSERT_SQL, new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - Station station = stations.get(i); - ps.setLong(1, station.getStationId()); - ps.setString(2, station.getName()); - ps.setString(3, station.getRoute()); - ps.setBigDecimal(4, station.getLatitude()); - ps.setBigDecimal(5, station.getLongitude()); - ps.setLong(6, station.getPriority()); - } + return batchInsertStations(stations); + } - @Override - public int getBatchSize() { - return stations.size(); - } - }); - log.info("Successfully saved a total of {} stations out of {}.", - Arrays.stream(batches).sum(), stations.size()); + private List batchInsertStations(List stations) { + MapSqlParameterSource[] batchParams = stations.stream() + .map(mapToParams) + .toArray(MapSqlParameterSource[]::new); + int[] batched = jdbcTemplate.batchUpdate(INSERT_STATION_SQL, batchParams); + log.debug("Successfully saved a total of {} stations out of {}.", batched.length, + stations.size()); + List names = stations.stream().map(Station::getName).toList(); + return stationRepository.findAllByNameIn(names); } @Override @@ -67,3 +63,4 @@ public Optional retrieveStation(Long stationId) { return stationRepository.findStationByStationId(stationId); } } + diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 154c3da0..0e91a633 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -1,6 +1,7 @@ package com.kok.kokapi.station.adapter.out.persistence; import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -11,5 +12,7 @@ public interface StationRepository extends JpaRepository { @Query("SELECT EXISTS (SELECT 1 FROM Station)") boolean existsAny(); + List findAllByNameIn(List names); + Optional findStationByStationId(Long stationId); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java b/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java index c79d6c50..b424df1b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java @@ -4,19 +4,12 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.annotation.Transactional; @Configuration public class StationsConfig { @Bean CommandLineRunner initStations(StationService stationService) { - return new CommandLineRunner() { - @Override - @Transactional - public void run(String... args) { - stationService.saveStations(); - } - }; + return args -> stationService.saveStations(); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index 88c631d9..ecafe37f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -2,7 +2,9 @@ import com.kok.kokcore.station.application.port.out.LoadStationsPort; import com.kok.kokcore.station.application.port.out.ReadStationsPort; +import com.kok.kokcore.station.application.port.out.SaveRoutePort; import com.kok.kokcore.station.application.port.out.SaveStationsPort; +import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; import com.kok.kokcore.station.application.usecase.SaveStationUseCase; import com.kok.kokcore.station.domain.entity.Station; import java.util.List; @@ -17,17 +19,15 @@ public class StationService implements SaveStationUseCase { private final LoadStationsPort loadStationsPort; private final SaveStationsPort saveStationsPort; private final ReadStationsPort readStationsPort; + private final SaveRoutePort saveRoutePort; @Override @Transactional public void saveStations() { - if(hasNoStations()) { - List stations = loadStationsPort.loadAllStations(); - saveStationsPort.saveStations(stations); + if(readStationsPort.hasNoStations()) { + StationRouteDtos stationRouteDtos = loadStationsPort.loadAllStations(); + List stations = saveStationsPort.saveStations(stationRouteDtos.toStations()); + saveRoutePort.saveRoutes(stationRouteDtos.toRoutesByStations(stations)); } } - - private boolean hasNoStations() { - return readStationsPort.hasNoStations(); - } } diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index d9b931b5..4def3d8b 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -6,7 +6,7 @@ spring: password: ${DB_PASSWORD} jpa: hibernate: - ddl-auto: update + ddl-auto: validate properties: hibernate: show_sql: true diff --git a/kok-api/src/main/resources/db/V1__init_station.sql b/kok-api/src/main/resources/db/V1__init_station.sql index 4664d351..0cffbc70 100644 --- a/kok-api/src/main/resources/db/V1__init_station.sql +++ b/kok-api/src/main/resources/db/V1__init_station.sql @@ -1,11 +1,29 @@ CREATE TABLE station ( - id BIGINT NOT NULL AUTO_INCREMENT, - station_id BIGINT NOT NULL, - name VARCHAR(255) NOT NULL, - route VARCHAR(255) NOT NULL, - latitude DECIMAL(16, 14) NOT NULL, - longitude DECIMAL(17, 14) NOT NULL, - priority BIGINT NOT NULL, + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(20) NOT NULL, + latitude DECIMAL(16, 14) NOT NULL, + longitude DECIMAL(17, 14) NOT NULL, + priority BIGINT NOT NULL, PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE route +( + id BIGINT NOT NULL AUTO_INCREMENT, + code BIGINT NOT NULL, + station_id BIGINT NOT NULL, + name VARCHAR(20) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (station_id) REFERENCES station (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +create table location +( + id bigint auto_increment primary key, + member_id int not null, + point point not null, + uuid varchar(255) not null, + constraint UKrgpajb4rsivb4gj9xn2qowgw6 + unique (uuid, member_id) ); diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java index 0c9c3073..1861f537 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java @@ -9,13 +9,12 @@ public abstract class ContainerBaseTest { private static final int REDIS_PORT = 6379; - private static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.0") - .withDatabaseName("kok") + private static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.4") + .withDatabaseName("kok-db") .withUsername("root") .withPassword("1234"); - private static final RedisContainer redisContainer = new RedisContainer( - RedisContainer.DEFAULT_IMAGE_NAME.withTag(RedisContainer.DEFAULT_TAG) + private static final RedisContainer redisContainer = new RedisContainer("redis:7.0" ); static { @@ -32,5 +31,8 @@ private static void dynamicProperties(DynamicPropertyRegistry registry) { registry.add("spring.data.redis.host", redisContainer::getHost); registry.add("spring.data.redis.port", () -> redisContainer.getMappedPort(REDIS_PORT)); + + registry.add("spring.flyway.enabled", () -> true); + registry.add("spring.flyway.baseline-on-migrate", () -> true); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java index 64b228c9..5eb98f87 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java @@ -1,17 +1,19 @@ package com.kok.kokapi.station.adapter.out.external; import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import com.kok.kokcore.station.domain.entity.Station; -import java.math.BigDecimal; +import com.kok.kokcore.station.application.port.out.dto.StationRouteDto; +import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; import java.util.List; public class FakeStationClient implements LoadStationsPort { @Override - public List loadAllStations() { - return List.of( - new Station(1L, "์„œ์šธ์—ญ", "1ํ˜ธ์„ ", BigDecimal.ONE, BigDecimal.ONE, 0), - new Station(2L, "ํ•ฉ์ •์—ญ", "2ํ˜ธ์„ ", BigDecimal.TEN, BigDecimal.TEN, 5) + public StationRouteDtos loadAllStations() { + return new StationRouteDtos( + List.of( + new StationRouteDto("์„œ์šธ์—ญ", "1", "2", 1L, "1ํ˜ธ์„ "), + new StationRouteDto("ํ•ฉ์ •์—ญ", "10", "10", 2L, "2ํ˜ธ์„ ") + ) ); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java new file mode 100644 index 00000000..af8a4587 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java @@ -0,0 +1,37 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.kok.kokapi.common.template.ServiceTest; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class StationPersistenceAdapterTest extends ServiceTest { + + @Autowired + private StationRepository stationRepository; + @Autowired + private StationPersistenceAdapter stationPersistenceAdapter; + + @DisplayName("์ƒˆ๋กœ ์ €์žฅ๋œ ์ง€ํ•˜์ฒ  ๋ชฉ๋ก๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void saveStationsAndReturn() { + // given + Station savedStation = new Station("๋ง์›์—ญ", "12.345","123.456"); + List stations = List.of(new Station("ํ•ฉ์ •์—ญ", "12.345","123.456")); + stationRepository.save(savedStation); + + // when + List result = stationPersistenceAdapter.saveStations(stations); + + // then + assertAll( + () -> assertThat(result).hasSize(1), + () -> assertThat(result.get(0).getName()).isEqualTo("ํ•ฉ์ •์—ญ") + ); + } +} \ No newline at end of file diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java index b3df8257..1eee75a2 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationRepositoryTest.java @@ -18,7 +18,7 @@ class StationRepositoryTest extends RepositoryTest { @Test void existsAny() { // given - stationRepository.save(new Station(1L, "์„œ์šธ์—ญ", "1ํ˜ธ์„ ", BigDecimal.ONE, BigDecimal.ONE, 0)); + stationRepository.save(new Station("์„œ์šธ์—ญ", BigDecimal.ONE, BigDecimal.ONE, 0)); // when boolean result = stationRepository.existsAny(); diff --git a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java index 0107271a..0f083958 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java @@ -31,7 +31,7 @@ void saveStations() { @Test void doesNotSaveStationsIfAlreadyExists() { //given - stationRepository.save(new Station(1L, "์„œ์šธ์—ญ", "1ํ˜ธ์„ ", BigDecimal.ONE, BigDecimal.ONE, 0)); + stationRepository.save(new Station("์„œ์šธ์—ญ", BigDecimal.ONE, BigDecimal.ONE, 0)); // when stationService.saveStations(); diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index d00e0477..5aa32c92 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -1,18 +1,18 @@ spring: profiles: active: test - sql: - init: - mode: always flyway: enabled: true connect-retries: 30 + baseline-on-migrate: true jpa: show-sql: true properties: hibernate: + show_sql: true format_sql: true + dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect defer-datasource-initialization: false open-in-view: false hibernate: - ddl-auto: create + ddl-auto: update diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java index 52c40188..31d36206 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java @@ -1,9 +1,8 @@ package com.kok.kokcore.station.application.port.out; -import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; +import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; public interface LoadStationsPort { - List loadAllStations(); + StationRouteDtos loadAllStations(); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java new file mode 100644 index 00000000..7d050c7e --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.application.port.out; + +import com.kok.kokcore.station.domain.entity.Route; +import java.util.List; + +public interface SaveRoutePort { + + void saveRoutes(List routes); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java index b603ebe6..d781d35e 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java @@ -5,5 +5,5 @@ public interface SaveStationsPort { - void saveStations(List stations); + List saveStations(List stations); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java new file mode 100644 index 00000000..aecd3890 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java @@ -0,0 +1,25 @@ +package com.kok.kokcore.station.application.port.out.dto; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; + +public record StationRouteDto( + String name, + String latitude, + String longitude, + Long stationId, + String route +) { + + public boolean hasName(Station station) { + return name.equals(station.getName()); + } + + public Station toStation() { + return new Station(name, latitude, longitude); + } + + public Route toRouteByStation(Station station) { + return new Route(stationId, route, station); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java new file mode 100644 index 00000000..f0889157 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java @@ -0,0 +1,44 @@ +package com.kok.kokcore.station.application.port.out.dto; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public record StationRouteDtos( + List stationRouteDtos +) { + + public boolean isEmpty() { + return stationRouteDtos().isEmpty(); + } + + public List toStations() { + return distinctByName().stream() + .map(StationRouteDto::toStation) + .toList(); + } + + private List distinctByName() { + return new ArrayList<>(stationRouteDtos.stream() + .collect(Collectors.toMap( + StationRouteDto::name, + dto -> dto, + (existing, replacement) -> existing + )) + .values()); + } + + public List toRoutesByStations(List stations){ + List routes = new ArrayList<>(); + for (Station station : stations) { + List routesOfStation = stationRouteDtos.stream() + .filter(stationRouteDto -> stationRouteDto.hasName(station)) + .map(stationRouteDto -> stationRouteDto.toRouteByStation(station)) + .toList(); + routes.addAll(routesOfStation); + } + return routes; + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java new file mode 100644 index 00000000..e591e925 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java @@ -0,0 +1,36 @@ +package com.kok.kokcore.station.domain.entity; + +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 lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Route { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private Long code; + @Column(nullable = false, length = 20) + private String name; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "station_id", nullable = false) + private Station station; + + public Route(Long code, String name, Station station) { + this.code = code; + this.name = name; + this.station = station; + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java index 1ba97a28..9ea21487 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java @@ -18,12 +18,8 @@ public class Station { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private Long stationId; - @Column(nullable = false) + @Column(nullable = false, length = 20) private String name; - @Column(nullable = false) - private String route; @Column(nullable = false, columnDefinition = "DECIMAL(16, 14)") private BigDecimal latitude; @Column(nullable = false, columnDefinition = "DECIMAL(17, 14)") @@ -31,26 +27,14 @@ public class Station { @Column(nullable = false) private Long priority; - public Station(Long id, Long stationId, String name, String route, BigDecimal latitude, - BigDecimal longitude) { - this.id = id; - this.stationId = stationId; - this.name = name; - this.route = route; - this.latitude = latitude; - this.longitude = longitude; - } - - public Station(Long stationId, String name, String route, BigDecimal latitude, BigDecimal longitude, long priority) { - this.stationId = stationId; + public Station(String name, BigDecimal latitude, BigDecimal longitude, long priority) { this.name = name; - this.route = route; this.latitude = latitude; this.longitude = longitude; this.priority = priority; } - public Station(Long stationId, String name, String route, String latitude, String longitude) { - this(stationId, name, route, new BigDecimal(latitude),new BigDecimal(longitude), 0); + public Station(String name,String latitude, String longitude) { + this(name, new BigDecimal(latitude),new BigDecimal(longitude), 0); } } diff --git a/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java b/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java new file mode 100644 index 00000000..6fd8c227 --- /dev/null +++ b/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java @@ -0,0 +1,95 @@ +package com.kok.kokcore.station.application.port.out.dto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class StationRouteDtosTest { + + @DisplayName("stationRouteDtos๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ์ฐธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isEmpty() { + // given + StationRouteDtos stationRouteDtos = new StationRouteDtos(List.of()); + + // when + boolean result = stationRouteDtos.isEmpty(); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("stationRouteDtos๊ฐ€ ๋น„์–ด ์žˆ์ง€ ์•Š์œผ๋ฉด ๊ฑฐ์ง“์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isNotEmpty() { + // given + StationRouteDto stationRouteDto = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, + "1ํ˜ธ์„ "); + StationRouteDtos stationRouteDtos = new StationRouteDtos(List.of(stationRouteDto)); + + // when + boolean result = stationRouteDtos.isEmpty(); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("stationRouteDtos๋ฅผ Station ๋ฆฌ์ŠคํŠธ๋กœ ์ค‘๋ณต ์—†์ด ๋ณ€ํ™˜ํ•œ๋‹ค.") + @Test + void toStations() { + // given + StationRouteDto stationRouteDto1 = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, + "1ํ˜ธ์„ "); + StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, + "2ํ˜ธ์„ "); + StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.496", "127.029", 3L, "์‹ ๋ถ„๋‹น์„ "); + StationRouteDtos stationRouteDtos = new StationRouteDtos( + List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) + ); + + // when + List stations = stationRouteDtos.toStations(); + + // then + List names = stations.stream().map(Station::getName).toList(); + + assertAll( + () -> assertThat(stations).hasSize(2), + () -> assertThat(names).containsExactlyInAnyOrder("์„œ์šธ์—ญ", "๊ฐ•๋‚จ์—ญ") + ); + } + + @DisplayName("stationRouteDtos๋ฅผ Route ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.") + @Test + void toRoutesByStations() { + // given + StationRouteDto stationRouteDto1 = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, + "1ํ˜ธ์„ "); + StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, + "2ํ˜ธ์„ "); + StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 3L, "์‹ ๋ถ„๋‹น์„ "); + StationRouteDtos stationRouteDtos = new StationRouteDtos( + List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) + ); + Station station1 = new Station("์„œ์šธ์—ญ", "37.556", "126.972"); + Station station2 = new Station("๊ฐ•๋‚จ์—ญ", "37.497", "127.028"); + + // when + List routes = stationRouteDtos.toRoutesByStations(List.of(station1, station2)); + + // then + List codes = routes.stream().map(Route::getCode).toList(); + List stations = routes.stream().map(Route::getStation).distinct().toList(); + + assertAll( + () -> assertThat(routes).hasSize(3), + () -> assertThat(codes).containsExactlyInAnyOrder(1L, 2L, 3L), + () -> assertThat(stations).containsExactlyInAnyOrder(station1, station2) + ); + } +} \ No newline at end of file From 0938854d8389c04ba411319d165bdd19928683cb Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Sun, 9 Mar 2025 00:34:12 +0900 Subject: [PATCH 068/163] [Feature/Room] Implement random profile generation (#53) * :sparkles: feat: Implement random profile generation * :sparkles: feat: Implement list member profiles * :sparkles: feat: Implement random profile generation * :sparkles: feat: Implement list member profiles * :sparkles: feat: Implement list member profiles * :sparkles: feat: delete password field at Room * :sparkles: feat: delete RoomMemberQueryAdapter * :recycle: refactor: remove unnecessary files and fixed typos * :recycle: refactor: refactoring code review * :recycle: refactor: refactoring code review * :recycle: refactor: refactoring code review --- .../in/dto/request/CreateRoomRequest.java | 8 ++-- .../in/dto/response/JoinRoomResponse.java | 4 ++ .../in/dto/response/MemberResponse.java | 3 +- .../dto/response/RandomProfileResponse.java | 6 +++ .../in/dto/response/RoomCreateResponse.java | 23 ++++++++++ .../in/dto/response/RoomDetailResponse.java | 6 ++- .../in/dto/response/RoomMembersResponse.java | 4 +- .../room/adapter/in/web/RoomController.java | 43 +++++++++-------- .../adapter/in/web/RoomProfileController.java | 46 +++++++++++++++++++ .../RoomParticipantQueryAdapter.java | 44 ++++++++++++++++++ .../RoomParticipantSaveAdapter.java | 33 +++++++++++++ .../service/RandomProfileService.java | 46 +++++++++++++++++++ .../service/RoomCreationService.java | 4 +- .../service/RoomParticipantService.java | 31 +++++++------ .../src/main/resources/application-dev.yml | 3 +- .../src/main/resources/application-prod.yml | 3 +- .../service/RoomCreationServiceTest.java | 9 ++-- .../service/RoomQueryServiceTest.java | 5 +- .../port/out/LoadRoomParticipantsPort.java | 9 ++++ .../port/out/SaveRoomParticipantsPort.java | 7 +++ .../com/kok/kokcore/room/domain/Member.java | 10 ++-- .../com/kok/kokcore/room/domain/Profile.java | 18 ++++++++ .../com/kok/kokcore/room/domain/Room.java | 11 ++--- .../kokcore/room/domain/vo/MemberRole.java | 6 +++ .../usecase/CreateRandomProfileUseCase.java | 7 +++ .../room/usecase/CreateRoomUseCase.java | 4 +- .../room/usecase/GetMemberProfileUseCase.java | 9 ++++ .../kokcore/room/usecase/JoinRoomUseCase.java | 2 +- ...cation.properties => application-prod.yml} | 0 .../com/kok/kokcore/room/domain/RoomTest.java | 17 ++++--- 30 files changed, 343 insertions(+), 78 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/domain/vo/MemberRole.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java rename kok-core/src/main/resources/{application.properties => application-prod.yml} (100%) diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java index ce30a405..03c5cf38 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java @@ -13,11 +13,9 @@ public record CreateRoomRequest( @Min(value = 2, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") Integer capacity, + @NotBlank(message = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") String hostProfile, - @NotBlank(message = "๋ฐฉ์žฅ ์ด๋ฆ„(๋‹‰๋„ค์ž„)์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") - String hostNickname, - - @NotBlank(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") - String password + @NotBlank(message = "๋‹‰๋„ค์ž„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + String hostNickname ) {} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java new file mode 100644 index 00000000..43203c41 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java @@ -0,0 +1,4 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +public record JoinRoomResponse(int participantCount, int memberId) { +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java index b02f0491..f6f4df08 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java @@ -1,11 +1,12 @@ package com.kok.kokapi.room.adapter.in.dto.response; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.vo.MemberRole; public record MemberResponse( String nickname, String profile, - String role + MemberRole role ) { public static MemberResponse from(Member member) { return new MemberResponse(member.getNickname(), member.getProfile(), member.getRole()); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java new file mode 100644 index 00000000..4dd44a50 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java @@ -0,0 +1,6 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +public record RandomProfileResponse( + String imageUrl, + String nickname +){} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java new file mode 100644 index 00000000..0361af08 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java @@ -0,0 +1,23 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Room; + +public record RoomCreateResponse( + String id, + String roomName, + int capacity, + MemberResponse member, + int memberId, + int participantCount +) { + public static RoomCreateResponse from(Room room, int memberId, int participantCount) { + return new RoomCreateResponse( + room.getId(), + room.getRoomName(), + room.getCapacity(), + MemberResponse.from(room.getMember()), + memberId, + participantCount + ); + } +} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index 09d69628..df851571 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -13,7 +13,11 @@ public static RoomDetailResponse from(Room room) { room.getId(), room.getRoomName(), room.getCapacity(), - MemberResponse.from(room.getMember()) + getLeaderResponse(room) ); } + + private static MemberResponse getLeaderResponse(Room room) { + return MemberResponse.from(room.getMember()); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java index 0e957f3e..09057083 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java @@ -1,7 +1,9 @@ package com.kok.kokapi.room.adapter.in.dto.response; +import com.kok.kokcore.room.domain.vo.MemberRole; + public record RoomMembersResponse( String profile, String nickname, - String role + MemberRole role ) {} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index d4e941cc..475454d0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -4,10 +4,12 @@ import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; +import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomCreateResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; import com.kok.kokcore.room.domain.Member; -import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.domain.vo.MemberRole; import com.kok.kokcore.room.usecase.CreateRoomUseCase; import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.room.usecase.JoinRoomUseCase; @@ -28,35 +30,35 @@ public class RoomController { private final CreateRoomUseCase createRoomUseCase; private final JoinRoomUseCase joinRoomUseCase; - @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "Retrieve detailed information for a room using its ID") + @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ์•ฝ์†๋ฐฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}") public ResponseEntity> getRoomDetail(@PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId); + var room = getRoomUseCase.findRoomById(roomId); var response = RoomDetailResponse.from(room); return ResponseEntity.ok(ApiResponseDto.success(response)); } - @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒ์„ฑ", description = "Create a new room with the provided details.") + @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒ์„ฑ", description = "์ƒˆ๋กœ์šด ์•ฝ์†๋ฐฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms") - public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { - Member host = new Member(request.hostNickname(), request.hostProfile(), "Leader"); - Room room = createRoomUseCase.createRoom( + public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { + var host = new Member(request.hostNickname(), request.hostProfile(), MemberRole.LEADER); + var room = createRoomUseCase.createRoom( request.roomName(), request.capacity(), - host, - request.password() + host ); - var response = RoomDetailResponse.from(room); + var response = RoomCreateResponse.from(room, 1, 1); + return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponseDto.success(response)); } - @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of participant profiles for the room.") + @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์ฐธ์—ฌ์ž๋“ค์˜ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}/participants") public ResponseEntity>> getParticipants(@PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId); - List participants = getRoomUseCase.getParticipants(room.getId()); + var room = getRoomUseCase.findRoomById(roomId); + var participants = getRoomUseCase.getParticipants(room.getId()); var response = participants.stream() .map(member -> new RoomMembersResponse( @@ -67,13 +69,16 @@ public ResponseEntity>> getParticipants return ResponseEntity.ok(ApiResponseDto.success(response)); } - @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ", description = "Register a new participant profile for the room.") + @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms/{roomId}/join") - public ResponseEntity> joinRoom(@PathVariable String roomId, - @Valid @RequestBody JoinRoomParticipantRequest request) { + public ResponseEntity> joinRoom(@PathVariable String roomId, + @Valid @RequestBody JoinRoomParticipantRequest request) { + getRoomUseCase.findRoomById(roomId); - Member participant = new Member(request.nickname(), request.profile(), "Follower"); - joinRoomUseCase.joinRoom(roomId, participant); - return ResponseEntity.ok(ApiResponseDto.success("์•ฝ์†๋ฐฉ ์ฐธ์—ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + var participant = new Member(request.nickname(), request.profile(), MemberRole.FOLLOWER); + var participantCount = joinRoomUseCase.joinRoom(roomId, participant); + var memberId = participantCount; + JoinRoomResponse response = new JoinRoomResponse(participantCount, memberId); + return ResponseEntity.ok(ApiResponseDto.success(response)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java new file mode 100644 index 00000000..d70d18be --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java @@ -0,0 +1,46 @@ +package com.kok.kokapi.room.adapter.in.web; + +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.annotion.V1Controller; +import com.kok.kokapi.room.adapter.in.dto.response.RandomProfileResponse; +import com.kok.kokcore.room.domain.Profile; +import com.kok.kokcore.room.usecase.CreateRandomProfileUseCase; +import com.kok.kokcore.room.usecase.GetMemberProfileUseCase; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@V1Controller +@RequiredArgsConstructor +public class RoomProfileController { + + private final CreateRandomProfileUseCase createRandomProfileUseCase; + private final GetMemberProfileUseCase getMemberProfileUseCase; + + @Operation(summary = "๋žœ๋ค ํ”„๋กœํ•„ ๋ฐ ๋‹‰๋„ค์ž„ ์กฐํšŒ", description = "๋žœ๋ค์œผ๋กœ ์ƒ์„ฑํ•œ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€์™€ ๋‹‰๋„ค์ž„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/rooms/profile/random") + public ResponseEntity> getRandomProfile() { + Profile profile = createRandomProfileUseCase.createProfile(); + RandomProfileResponse response = new RandomProfileResponse( + profile.getImageUrl(), + profile.getNickname() + ); + + return ResponseEntity.ok(ApiResponseDto.success(response)); + } + + @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž์˜ ํ”„๋กœํ•„ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž๊ฐ€ ๊ธฐ์กด์— ์ƒ์„ฑํ•œ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉฐ, ์—†์„ ๊ฒฝ์šฐ ๋นˆ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/rooms/{roomId}/profiles") + public ResponseEntity>> getMemberProfiles(@PathVariable String roomId) { + List profiles = getMemberProfileUseCase.getProfilesByRoomId(roomId); + List responseList = profiles.stream() + .map(profile -> new RandomProfileResponse(profile.getImageUrl(), profile.getNickname())) + .toList(); + + return ResponseEntity.ok(ApiResponseDto.success(responseList)); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java new file mode 100644 index 00000000..86770935 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java @@ -0,0 +1,44 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.application.port.out.LoadRoomParticipantsPort; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Profile; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class RoomParticipantQueryAdapter implements LoadRoomParticipantsPort { + + private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public List getProfilesByRoomId(String roomId) { + String key = PARTICIPANT_KEY_PREFIX + roomId; + List profileJson = redisTemplate.opsForList().range(key, 0, -1); + + if (profileJson == null || profileJson.isEmpty()) { + return List.of(); + } + + return profileJson.stream() + .map(this::deserializeProfile) + .toList(); + } + + private Profile deserializeProfile(String profileJson) { + try { + Member member = objectMapper.readValue(profileJson, Member.class); + return new Profile(member.getProfile(), member.getNickname()); + } catch (JsonProcessingException e) { + throw new RuntimeException("failed to deserialize profile"); + } + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java new file mode 100644 index 00000000..8be7f9ae --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java @@ -0,0 +1,33 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.application.port.out.SaveRoomParticipantsPort; +import com.kok.kokcore.room.domain.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class RoomParticipantSaveAdapter implements SaveRoomParticipantsPort { + + private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public int joinRoom(String roomId, Member member) { + String key = PARTICIPANT_KEY_PREFIX + roomId; + + try { + String memberJson = objectMapper.writeValueAsString(member); + redisTemplate.opsForList().rightPush(key, memberJson); + + Long participantCount = redisTemplate.opsForList().size(key); + return participantCount != null ? participantCount.intValue() : 0; + } catch (JsonProcessingException e) { + throw new RuntimeException("failed to serialize member"); + } + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java new file mode 100644 index 00000000..7f80cc99 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java @@ -0,0 +1,46 @@ +package com.kok.kokapi.room.application.service; + +import com.kok.kokcore.room.domain.Profile; +import com.kok.kokcore.room.usecase.CreateRandomProfileUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Random; + +@Service +@RequiredArgsConstructor +public class RandomProfileService implements CreateRandomProfileUseCase { + + @Value("${ncp.object-storage-url}") + private String objectStorageUrl; + + private static final List ADJECTIVES = List.of( + "๋ฐฐ๊ณ ํ”ˆ", "๋ฉ‹์žˆ๋Š”", "์ฟจํ•œ ", "๋น ๋ฅธ", "์นœ์ ˆํ•œ", "์˜๋ฆฌํ•œ", "ํ–‰๋ณตํ•œ", "์กฐ์šฉํ•œ", "๊ฐ•๋ ฅํ•œ", "์ž์œ ๋กœ์šด", + "์Šค๋งˆํŠธํ•œ", "์ž˜์ƒ๊ธด", "๊ท€์—ฌ์šด", "ํ‰ํ™”๋กœ์šด", "๋น›๋‚˜๋Š”", "๋˜‘๋˜‘ํ•œ", "์‹ ๋‚˜๋Š”", "๋ถ€๋“œ๋Ÿฌ์šด", "์—‰๋šฑํ•œ", "๋ฌด์„œ์šด" + ); + + private static final List NOUNS = List.of( + "ํ† ๋ฏธ", "์ง€๋ฏธ", "๋ผ์ด์–ธ", "๋ฃจ์นด์Šค", "๋ฐ์ด๋น—", "์— ๋งˆ", "์˜ฌ๋ฆฌ๋ฒ„", "์†Œํ”ผ", "๋ด‰๋ด‰์ด", "ํ”ผ์น˜", + "๋ฌด์ง€", "ํŠœ๋ธŒ", "ํ”„๋กœ๋„", "์ฝ˜", "๋ธŒ๋ผ์šด", "์ฝ”์ฝ”", "ํ”„๋ Œ์ฆˆ", "ํ† ๋ผ", "ํŽญ์ˆ˜", "๋ฝ€๋กœ๋กœ" + ); + + @Override + public Profile createProfile() { + String imageUrl = getRandomImageUrl(); + String nickname = generateRandomNickname(); + return new Profile(imageUrl, nickname); + } + + private String getRandomImageUrl() { + int randomImageIndex = new Random().nextInt(9) + 1; + return objectStorageUrl + "/profile_default/" + randomImageIndex + ".svg"; + } + + private String generateRandomNickname() { + String adjective = ADJECTIVES.get(new Random().nextInt(ADJECTIVES.size())); + String noun = NOUNS.get(new Random().nextInt(NOUNS.size())); + return adjective + " " + noun; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java index 9fdb8b4b..23224550 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java @@ -16,8 +16,8 @@ public class RoomCreationService implements CreateRoomUseCase { private final JoinRoomUseCase joinRoomUseCase; @Override - public Room createRoom(String roomName, int capacity, Member host, String password) { - Room room = Room.create(roomName, capacity, host, password); + public Room createRoom(String roomName, int capacity, Member host) { + Room room = Room.create(roomName, capacity, host); Room savedRoom = saveRoomPort.save(room); joinRoomUseCase.joinRoom(savedRoom.getId(), savedRoom.getMember()); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java index ef537fae..02b3d690 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java @@ -1,30 +1,31 @@ package com.kok.kokapi.room.application.service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.application.port.out.LoadRoomParticipantsPort; +import com.kok.kokcore.room.application.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Profile; +import com.kok.kokcore.room.usecase.GetMemberProfileUseCase; import com.kok.kokcore.room.usecase.JoinRoomUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor -public class RoomParticipantService implements JoinRoomUseCase { +public class RoomParticipantService implements JoinRoomUseCase, GetMemberProfileUseCase { + + private final LoadRoomParticipantsPort loadRoomParticipantsPort; + private final SaveRoomParticipantsPort saveRoomParticipantsPort; - private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; - private final RedisTemplate redisTemplate; - private final ObjectMapper objectMapper; + @Override + public int joinRoom(String roomId, Member member) { + return saveRoomParticipantsPort.joinRoom(roomId, member); + } @Override - public void joinRoom(String roomId, Member member) { - String key = PARTICIPANT_KEY_PREFIX + roomId; - try { - String memberJson = objectMapper.writeValueAsString(member); - redisTemplate.opsForList().rightPush(key, memberJson); - } catch (JsonProcessingException e) { - throw new RuntimeException("failed to serialize member"); - } + public List getProfilesByRoomId(String roomId) { + return loadRoomParticipantsPort.getProfilesByRoomId(roomId); } } diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 4def3d8b..2c08c1a9 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -36,7 +36,8 @@ station: format: json start-idx: 1 end-idx: 1000 - +ncp: + object-storage-url: ${OBJECT_STORAGE_URL} tmap: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 42f9ef89..788f52d7 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -47,7 +47,8 @@ station: format: json start-idx: 1 end-idx: 1000 - +ncp: + object-storage-url: ${OBJECT_STORAGE_URL} tmap: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java index fb70ba7a..2f2d6afb 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java @@ -7,6 +7,7 @@ import com.kok.kokapi.common.template.ServiceTest; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.domain.vo.MemberRole; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -23,21 +24,19 @@ void createRoom() { int capacity = 4; String hostNickname = "test"; String hostProfile = "hostProfile"; - String password = "secret"; - Member host = new Member(hostNickname, hostProfile, "Leader"); + Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); - Room createdRoom = roomCreationService.createRoom(roomName, capacity, host, password); + Room createdRoom = roomCreationService.createRoom(roomName, capacity, host); assertAll("Room Create Test", () -> assertNotNull(createdRoom, "Room ๊ฐ์ฒด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertNotNull(createdRoom.getId(), "์•ฝ์†๋ฐฉ ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(roomName, createdRoom.getRoomName(), "์•ฝ์†๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(capacity, createdRoom.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(password, createdRoom.getPassword(), "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertNotNull(createdRoom.getMember(), "๋ฐฉ์žฅ ์ •๋ณด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(hostNickname, createdRoom.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(hostProfile, createdRoom.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals("Leader", createdRoom.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + () -> assertEquals(MemberRole.LEADER, createdRoom.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") ); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java index f16c8c3f..560c7f58 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java @@ -7,6 +7,7 @@ import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.domain.vo.MemberRole; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,8 +23,8 @@ class RoomQueryServiceTest extends ServiceTest { @Test void findRoomById() { // given - Member member = new Member("nickname", "image", "Leader"); - Room room = saveRoomPort.save(Room.create("room", 2, member, "1234")); + Member member = new Member("nickname", "image", MemberRole.LEADER); + Room room = saveRoomPort.save(Room.create("room", 2, member)); // when Room result = roomQueryService.findRoomById(room.getId()); diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java new file mode 100644 index 00000000..c0a8380b --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.room.application.port.out; + +import com.kok.kokcore.room.domain.Profile; + +import java.util.List; + +public interface LoadRoomParticipantsPort { + List getProfilesByRoomId(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java new file mode 100644 index 00000000..b41a31d4 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.room.application.port.out; + +import com.kok.kokcore.room.domain.Member; + +public interface SaveRoomParticipantsPort { + int joinRoom(String roomId, Member member); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java index d837b01e..cd57d779 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java @@ -1,5 +1,6 @@ package com.kok.kokcore.room.domain; +import com.kok.kokcore.room.domain.vo.MemberRole; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -10,17 +11,14 @@ public class Member { private final String nickname; private final String profile; - private final String role; + private final MemberRole role; - public Member(String nickname, String profile, String role) { + public Member(String nickname, String profile, MemberRole role) { if (nickname == null || nickname.trim().isEmpty()) { throw new IllegalArgumentException("Nickname is required"); } - if (role == null || role.trim().isEmpty()) { - throw new IllegalArgumentException("Role is required"); - } this.nickname = nickname.trim(); this.profile = profile.trim(); - this.role = role.trim(); + this.role = role; } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java new file mode 100644 index 00000000..86e5bd7a --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java @@ -0,0 +1,18 @@ +package com.kok.kokcore.room.domain; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +public class Profile { + private final String imageUrl; + private final String nickname; + + public Profile(String imageUrl, String nickname) { + this.imageUrl = imageUrl; + this.nickname = nickname; + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index d440f90a..d28cc226 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -11,27 +11,24 @@ public class Room implements Serializable { public static final int REQUIRED_CAPACITY = 2; - private final String id; // ์•ฝ์†๋ฐฉ ID (UUID) private final String roomName; // ์•ฝ์†๋ฐฉ ์ด๋ฆ„ private final int capacity; // ์ฐธ์—ฌ์ธ์› ์ˆ˜ (์ตœ์†Œ 2๋ช… ์ด์ƒ) - private final String password; // ๋ฐฉ ๋น„๋ฐ€๋ฒˆํ˜ธ (์˜ต์…˜) - private final Member member; // ๋ฐฉ ์ฐธ์—ฌ์ž + private final Member member; // ๋ฐฉ ์ฐธ์—ฌ์ž - private Room(String id, String roomName, int capacity, Member member, String password) { + private Room(String id, String roomName, int capacity, Member member) { this.id = id; this.roomName = roomName; this.capacity = capacity; this.member = member; - this.password = password; } - public static Room create(String roomName, int capacity, Member host, String password) { + public static Room create(String roomName, int capacity, Member member) { validateParameter(roomName, capacity); String roomId = UUID.randomUUID().toString(); - return new Room(roomId, roomName, capacity, host, password); + return new Room(roomId, roomName, capacity, member); } private static void validateParameter(String roomName, int capacity) { diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/MemberRole.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/MemberRole.java new file mode 100644 index 00000000..40cbf6e5 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/MemberRole.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.room.domain.vo; + +public enum MemberRole { + LEADER, + FOLLOWER +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java new file mode 100644 index 00000000..1d9ac350 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.room.usecase; + +import com.kok.kokcore.room.domain.Profile; + +public interface CreateRandomProfileUseCase { + Profile createProfile(); +} \ No newline at end of file diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java index bdc75c5a..166e6a5a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java @@ -4,5 +4,5 @@ import com.kok.kokcore.room.domain.Room; public interface CreateRoomUseCase { - Room createRoom(String roomName, int capacity, Member host, String password); -} \ No newline at end of file + Room createRoom(String roomName, int capacity, Member host); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java new file mode 100644 index 00000000..f881da75 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.room.usecase; + +import com.kok.kokcore.room.domain.Profile; + +import java.util.List; + +public interface GetMemberProfileUseCase { + List getProfilesByRoomId(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java index 958b7c1f..0044524d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java @@ -3,5 +3,5 @@ import com.kok.kokcore.room.domain.Member; public interface JoinRoomUseCase { - void joinRoom(String roomId, Member member); + int joinRoom(String roomId, Member member); } diff --git a/kok-core/src/main/resources/application.properties b/kok-core/src/main/resources/application-prod.yml similarity index 100% rename from kok-core/src/main/resources/application.properties rename to kok-core/src/main/resources/application-prod.yml diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java index ba211bdf..e64a342e 100644 --- a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -1,5 +1,6 @@ package com.kok.kokcore.room.domain; +import com.kok.kokcore.room.domain.vo.MemberRole; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,10 +17,10 @@ void createRoom() { String hostProfile = "hostProfile"; String hostNickname = "test"; String password = "secret"; - Member host = new Member(hostNickname, hostProfile, "Leader"); + Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When - Room room = Room.create(roomName, capacity, host, password); + Room room = Room.create(roomName, capacity, host); // Then assertAll("์•ฝ์†๋ฐฉ ์ƒ์„ฑ", @@ -28,8 +29,7 @@ void createRoom() { () -> assertEquals(capacity, room.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(hostNickname, room.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(hostProfile, room.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals("Leader", room.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(password, room.getPassword(), "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + () -> assertEquals(MemberRole.LEADER, room.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") ); } @@ -42,11 +42,11 @@ void createRoomWithEmptyRoomName() { String hostNickname = "hostNickname"; String hostProfile = "hostProfile"; String password = "secret"; - Member host = new Member(hostNickname, hostProfile, "Leader"); + Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When & Then IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host, password)); + assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host)); assertTrue(exception.getMessage().contains("Room name is required")); } @@ -58,12 +58,11 @@ void createRoomWithInvalidCapacity() { int capacity = 1; String hostNickname = "hostNickname"; String hostProfile = "hostProfile"; - String password = "secret"; - Member host = new Member(hostNickname, hostProfile, "Leader"); + Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When & Then IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host, password)); + assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host)); assertTrue(exception.getMessage().contains("At least 2 participants are required")); } } From 03a677990163d3cf2fbb3692aa9c860aa69e8339 Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Sun, 9 Mar 2025 01:01:43 +0900 Subject: [PATCH 069/163] :bug: fix: fix query method referencing wrong field --- .../adapter/out/persistence/StationPersistenceAdapter.java | 2 +- .../station/adapter/out/persistence/StationRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 86e18e6b..68e17bd3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -60,7 +60,7 @@ public boolean hasNoStations() { @Override public Optional retrieveStation(Long stationId) { - return stationRepository.findStationByStationId(stationId); + return stationRepository.findStationById(stationId); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 0e91a633..9f1aef9e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -14,5 +14,5 @@ public interface StationRepository extends JpaRepository { List findAllByNameIn(List names); - Optional findStationByStationId(Long stationId); + Optional findStationById(Long stationId); } From 42b5625d14d2e2de4f6bcc31a157427d39ca796d Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Sun, 9 Mar 2025 01:11:32 +0900 Subject: [PATCH 070/163] :wrench: config: activate test on build --- .github/workflows/kok-CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index 7de15e9d..a4dd51bf 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -49,7 +49,7 @@ jobs: - name: Build with Gradle (๋ฉ€ํ‹ฐ๋ชจ๋“ˆ) run: | - ./gradlew clean build -x test + ./gradlew clean build - name: Save Build Artifacts uses: actions/upload-artifact@v4 From 45537a925277324c005c75ea153ccf3967f89812 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sun, 9 Mar 2025 02:03:25 +0900 Subject: [PATCH 071/163] :sparkles: feature: add complex public transportation route --- .../adapter/out/mapper/LocationMapper.java | 2 +- .../out/persistence/LocationRepository.java | 16 +- .../adapter/in/dto/RouteResponse.java | 10 -- ...lexPublicTransportationParsedResponse.java | 35 +++++ ...mapPublicTransportationParsedResponse.java | 10 ++ .../web/PublicTransfortationController.java | 32 ---- .../web/PublicTransportationController.java | 48 ++++++ .../external/PublicTransportationClient.java | 61 +++++++- .../PublicTransportationComplexClient.java | 98 ++++++++++++ .../out/external/TmapClientProperties.java | 3 +- .../external/TmapComplexClientProperties.java | 11 ++ ...apComplexPublicTransportationResponse.java | 147 ++++++++++++++++++ .../TmapPublicTransportationService.java | 109 +++++++------ .../StationPersistenceAdapter.java | 2 +- .../out/persistence/StationRepository.java | 2 +- .../common/template/ContainerBaseTest.java | 2 +- .../kok/kokcore/location/domain/Location.java | 8 +- .../RetrievePublicTransportationUsecase.java | 7 +- 18 files changed, 490 insertions(+), 113 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index d4e5d264..87cda80a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -18,7 +18,7 @@ public LocationMapper(PointConverter pointConverter) { } public LocationResponse toResponse(Location location) { - Pair coordinates = pointConverter.toCoordinates(location.getPoint()); + Pair coordinates = pointConverter.toCoordinates(location.getLocation_point()); return LocationResponse.of( location.getUuid(), location.getMemberId(), diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index ee6779e5..1140573a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -21,7 +21,7 @@ SELECT ST_AsText( ST_Transform( ST_Centroid( ST_Collect( - ST_Transform(point, 3857))),4326) + ST_Transform(location_point, 3857))),4326) ) FROM location WHERE uuid = :uuid @@ -34,32 +34,32 @@ SELECT ST_AsText( @Query(value = """ WITH ConvexHull AS ( - SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(point)))) AS hull + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull FROM location WHERE uuid = :uuid ) SELECT l.* FROM location l, ConvexHull ch WHERE l.uuid = :uuid - AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.point))) + AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) """, nativeQuery = true) List findInsideConvexHull(@Param("uuid") String uuid); @Query(value = """ WITH ConvexHull AS ( - SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(point)))) AS hull, - ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(point))))) AS center + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull, + ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point))))) AS center FROM location WHERE uuid = :uuid ) SELECT l.*, ATAN2( - ST_Y(ST_GeomFromText(ST_AsText(l.point))) - ST_Y(ch.center), - ST_X(ST_GeomFromText(ST_AsText(l.point))) - ST_X(ch.center) + ST_Y(ST_GeomFromText(ST_AsText(l.location_point))) - ST_Y(ch.center), + ST_X(ST_GeomFromText(ST_AsText(l.location_point))) - ST_X(ch.center) ) AS angle FROM location l, ConvexHull ch WHERE l.uuid = :uuid - AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.point))) + AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) ORDER BY angle """, nativeQuery = true) List findConvexHull(@Param("uuid") String uuid); diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java deleted file mode 100644 index f524e745..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokapi.public_transportation.adapter.in.dto; - -public record RouteResponse( - Integer totalTime, - Integer transferCount -) { - public static RouteResponse of(Integer totalTime, Integer transferCount) { - return new RouteResponse(totalTime, transferCount); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java new file mode 100644 index 00000000..9c3166fa --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java @@ -0,0 +1,35 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.List; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public class TmapComplexPublicTransportationParsedResponse implements Serializable { + + private ParsedItinerary parsedItinerary; + // DTO ํด๋ž˜์Šค ์ •์˜ + @Getter + @Setter + public static class ParsedItinerary { + private int totalDistance; + private int totalTime; + private List legs; + } + + @Getter + @Setter + public static class ParsedLeg { + private String mode; + private int distance; + private int sectionTime; + private String route; // ์ง€ํ•˜์ฒ ์ด๋ฉด ํ˜ธ์„  ์ •๋ณด, ๋ฒ„์Šค๋ฉด ๋…ธ์„  ์ •๋ณด + private String routeColor; // ๋…ธ์„  ์ƒ‰ + + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java new file mode 100644 index 00000000..8a880616 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java @@ -0,0 +1,10 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto; + +public record TmapPublicTransportationParsedResponse( + Integer totalTime, + Integer transferCount +) { + public static TmapPublicTransportationParsedResponse of(Integer totalTime, Integer transferCount) { + return new TmapPublicTransportationParsedResponse(totalTime, transferCount); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java deleted file mode 100644 index d39066a1..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransfortationController.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.kok.kokapi.public_transportation.adapter.in.web; - -import com.kok.kokapi.common.response.ApiResponseDto; -import com.kok.kokapi.config.annotion.V1Controller; -import com.kok.kokapi.public_transportation.adapter.in.dto.RouteRequest; -import com.kok.kokapi.public_transportation.adapter.in.dto.RouteResponse; -import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; -import io.swagger.v3.oas.annotations.Operation; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@V1Controller -@RequiredArgsConstructor -public class PublicTransfortationController { - - private final RetrievePublicTransportationUsecase retrievePublicTransportationUsecase; - - @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") - @PostMapping("/route/{stationId}") - public ResponseEntity> getPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { - List publicTransportation = - retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.UUID(), routeRequest.memberId()); - - return ResponseEntity.ok(ApiResponseDto.success( - RouteResponse.of(publicTransportation.get(0), publicTransportation.get(1)) - )); - } - -} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java new file mode 100644 index 00000000..f30aac8c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java @@ -0,0 +1,48 @@ +package com.kok.kokapi.public_transportation.adapter.in.web; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.annotion.V1Controller; +import com.kok.kokapi.public_transportation.adapter.in.dto.RouteRequest; +import com.kok.kokapi.public_transportation.adapter.in.dto.TmapPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse; +import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@V1Controller +@RequiredArgsConstructor +public class PublicTransportationController { + + private final RetrievePublicTransportationUsecase retrievePublicTransportationUsecase; + private final ObjectMapper objectMapper; + + @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") + @PostMapping("/route/{stationId}") + public ResponseEntity> getPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { + try { + TmapPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.UUID(), routeRequest.memberId()) + ,TmapPublicTransportationParsedResponse.class); + return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); + } catch (JsonProcessingException e) { + throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); + } + } + + @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์ „๋ฌธ ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") + @PostMapping("/route/complex/{stationId}") + public ResponseEntity> getComplexPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { + try { + TmapComplexPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrieveComplexPublicTransportation(stationId, routeRequest.UUID(), routeRequest.memberId()) + , TmapComplexPublicTransportationParsedResponse.class); + return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); + } catch (JsonProcessingException e) { + throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); + } + } + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java index f24c2e79..f911d925 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java @@ -1,22 +1,43 @@ package com.kok.kokapi.public_transportation.adapter.out.external; +import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; +import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; +import com.kok.kokapi.station.adapter.out.persistence.StationPersistenceAdapter; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.station.domain.entity.Station; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; +import org.springframework.data.util.Pair; +import org.springframework.http.HttpStatusCode; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + @Component @EnableConfigurationProperties(TmapClientProperties.class) +@Slf4j public class PublicTransportationClient { private final RestClient restClient; private final TmapClientProperties properties; - public PublicTransportationClient(TmapClientProperties properties) { + private final StationPersistenceAdapter stationPersistenceAdapter; + private final LocationPersistenceAdapter locationPersistenceAdapter; + private final PointConverter pointConverter; + + public PublicTransportationClient(TmapClientProperties properties, StationPersistenceAdapter stationPersistenceAdapter, LocationPersistenceAdapter locationPersistenceAdapter, PointConverter pointConverter) { this.properties = properties; + this.stationPersistenceAdapter = stationPersistenceAdapter; + this.locationPersistenceAdapter = locationPersistenceAdapter; + this.pointConverter = pointConverter; this.restClient = getRestClient(); } @@ -36,4 +57,42 @@ private ClientHttpRequestFactory getRequestFactory() { return ClientHttpRequestFactoryBuilder.detect() .build(ClientHttpRequestFactorySettings.defaults()); } + + public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String UUID, Integer memberId) { + log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); + return getClient().post() + .body(buildRequestBody( + getUserLocation(UUID, memberId), + getStation(stationId))) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); + }) + .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); + }) + .body(TmapPublicTransportationResponse.class); + } + + private Map buildRequestBody(Pair userLocation, Station station) { + Map requestBody = new HashMap<>(); + requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ + requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ + requestBody.put("endX", station.getLongitude()); // ๊ฒฝ๋„ + requestBody.put("endY", station.getLatitude()); // ์œ„๋„ + requestBody.put("count", 1); // ๊ฒฝ๋กœ ํƒ์ƒ‰ ๊ฒฐ๊ณผ ์ค‘ 1๊ฐœ๋งŒ ๋ฐ˜ํ™˜ + requestBody.put("format", "json"); + return requestBody; + } + + private Pair getUserLocation(String UUID, Integer memberId) { + Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + return pointConverter.toCoordinates(userPoint.getLocation_point()); + } + + private Station getStation(Long stationId) { + return stationPersistenceAdapter.retrieveStation(stationId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java new file mode 100644 index 00000000..cf4a9662 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java @@ -0,0 +1,98 @@ +package com.kok.kokapi.public_transportation.adapter.out.external; + +import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; +import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; +import com.kok.kokapi.station.adapter.out.persistence.StationPersistenceAdapter; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.station.domain.entity.Station; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; +import org.springframework.data.util.Pair; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +@Component +@EnableConfigurationProperties(TmapComplexClientProperties.class) +@Slf4j +public class PublicTransportationComplexClient { + private final RestClient restClient; + private final TmapComplexClientProperties properties; + + private final StationPersistenceAdapter stationPersistenceAdapter; + private final LocationPersistenceAdapter locationPersistenceAdapter; + private final PointConverter pointConverter; + + public PublicTransportationComplexClient(TmapComplexClientProperties properties, StationPersistenceAdapter stationPersistenceAdapter, LocationPersistenceAdapter locationPersistenceAdapter, PointConverter pointConverter) { + this.properties = properties; + this.stationPersistenceAdapter = stationPersistenceAdapter; + this.locationPersistenceAdapter = locationPersistenceAdapter; + this.pointConverter = pointConverter; + this.restClient = getRestClient(); + } + + public RestClient getRestClient() { + return RestClient.builder() + .requestFactory(getRequestFactory()) + .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ + .baseUrl(properties.url()) // Base URL ์„ค์ • + .build(); + } + + public RestClient getClient() { + return this.restClient; + } + + private ClientHttpRequestFactory getRequestFactory() { + return ClientHttpRequestFactoryBuilder.detect() + .build(ClientHttpRequestFactorySettings.defaults()); + } + + public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, String UUID, Integer memberId){ + log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); + return getClient().post() + .body(buildRequestBody( + getUserLocation(UUID, memberId), + getStation(stationId))) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); + }) + .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); + }) + .body(TmapComplexPublicTransportationResponse.class); + } + + private Pair getUserLocation(String UUID, Integer memberId) { + Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + return pointConverter.toCoordinates(userPoint.getLocation_point()); + } + + private Station getStation(Long stationId) { + return stationPersistenceAdapter.retrieveStation(stationId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + } + + private Map buildRequestBody(Pair userLocation, Station station) { + Map requestBody = new HashMap<>(); + requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ + requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ + requestBody.put("endX", station.getLongitude()); // ๊ฒฝ๋„ + requestBody.put("endY", station.getLatitude()); // ์œ„๋„ + requestBody.put("count", 1); // ๊ฒฝ๋กœ ํƒ์ƒ‰ ๊ฒฐ๊ณผ ์ค‘ 1๊ฐœ๋งŒ ๋ฐ˜ํ™˜ + requestBody.put("format", "json"); + return requestBody; + } + +} + diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java index 4daf36c6..bfc502bf 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java @@ -2,8 +2,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "tmap") +@ConfigurationProperties(prefix = "tmap-sub") public record TmapClientProperties( String key, String url, diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java new file mode 100644 index 00000000..650785bf --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java @@ -0,0 +1,11 @@ +package com.kok.kokapi.public_transportation.adapter.out.external; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "tmap-complex") +public record TmapComplexClientProperties( + String key, + String url, + String keyname +) { +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java new file mode 100644 index 00000000..b3f8e5a4 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java @@ -0,0 +1,147 @@ +package com.kok.kokapi.public_transportation.adapter.out.external.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public class TmapComplexPublicTransportationResponse { + @JsonProperty("metaData") + private MetaData metaData; + + @Getter + @Setter + public static class MetaData { + @JsonProperty("requestParameters") + private RequestParameters requestParameters; + + @JsonProperty("plan") + private Plan plan; + } + + @Getter + @Setter + public static class RequestParameters { + private int busCount; + private int expressbusCount; + private int subwayCount; + private int airplaneCount; + private String locale; + private double endY; + private double endX; + private int wideareaRouteCount; + private int subwayBusCount; + private double startY; + private double startX; + private int ferryCount; + private int trainCount; + private String reqDttm; + } + + @Getter + @Setter + public static class Plan { + @JsonProperty("itineraries") + private List itineraries; + } + + @Getter + @Setter + public static class Itinerary { + @JsonProperty("fare") + private Fare fare; + + private int totalTime; + private List legs; + private int totalWalkTime; + private int transferCount; + private int totalDistance; + private int pathType; + private int totalWalkDistance; + } + + @Getter + @Setter + public static class Fare { + @JsonProperty("regular") + private RegularFare regular; + } + + @Getter + @Setter + public static class RegularFare { + private int totalFare; + private Currency currency; + } + + @Getter + @Setter + public static class Currency { + private String symbol; + private String currency; + private String currencyCode; + } + + @Getter + @Setter + public static class Leg { + private String mode; + private int sectionTime; + private int distance; + private Location start; + private Location end; + private List steps; + private String routeColor; + private String route; + private String routeId; + private int service; + private PassStopList passStopList; + private int type; + private PassShape passShape; + } + + @Getter + @Setter + public static class Location { + private String name; + private double lon; + private double lat; + } + + @Getter + @Setter + public static class Step { + private String streetName; + private int distance; + private String description; + private String linestring; + } + + @Getter + @Setter + public static class PassStopList { + private List stationList; + } + + @Getter + @Setter + public static class Station { + private int index; + private String stationName; + private String lon; + private String lat; + private String stationID; + } + + @Getter + @Setter + public static class PassShape { + private String linestring; + } + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 9c2c0334..40d0b1fb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -1,24 +1,24 @@ package com.kok.kokapi.public_transportation.application.service; -import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; -import com.kok.kokapi.config.geometry.PointConverter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.TmapPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; +import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationComplexClient; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; -import com.kok.kokapi.station.adapter.out.persistence.StationPersistenceAdapter; -import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; -import com.kok.kokcore.station.domain.entity.Station; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.util.Pair; import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; -import java.math.BigDecimal; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.stream.Collectors; + +import static com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse.*; +import static com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse.*; @Service @RequiredArgsConstructor @@ -26,54 +26,67 @@ public class TmapPublicTransportationService implements RetrievePublicTransportationUsecase { private final PublicTransportationClient publicTransportationClient; - private final StationPersistenceAdapter stationPersistenceAdapter; - private final LocationPersistenceAdapter locationPersistenceAdapter; - private final PointConverter pointConverter; + private final PublicTransportationComplexClient publicTransportationComplexClient; + private final ObjectMapper objectMapper; - @Cacheable(value = "publicTransportation", cacheManager = "contentCacheManager", key = "'PTCache:' + #stationId + '-' + #UUID + '-' + #memberId") + @Cacheable(value = "sub", cacheManager = "contentCacheManager", key = "'PTSubCache:' + #stationId + '-' + #UUID + '-' + #memberId") @Override - public List retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { - TmapPublicTransportationResponse rawRoute = callPublicTransportRoute(stationId, UUID, memberId); - return List.of( - rawRoute.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), // ์ด ์†Œ์š” ์‹œ๊ฐ„ - rawRoute.getMetaData().getPlan().getItineraries().getFirst().getTransferCount() // ํ™˜์Šน ํšŸ์ˆ˜ - ); + public String retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { + TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute(stationId, UUID, memberId); + try { + return objectMapper.writeValueAsString(parseTmapResponse(rawRoute)); + } catch (JsonProcessingException e) { + throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); + } } - public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String UUID, Integer memberId){ - log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); - return publicTransportationClient.getClient().post() - .body(buildRequestBody( - getUserLocation(UUID, memberId), - getStation(stationId))) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค." + status); - }) - .body(TmapPublicTransportationResponse.class); + @Cacheable(value = "complex", cacheManager = "contentCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #UUID + '-' + #memberId") + @Override + public String retrieveComplexPublicTransportation(Long stationId, String UUID, Integer memberId) { + TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute(stationId, UUID, memberId); + try { + return objectMapper.writeValueAsString(parseComplexTmapResponse(rawRoute)); + } catch (JsonProcessingException e) { + throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); + } } - private Pair getUserLocation(String UUID, Integer memberId) { - Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); - return pointConverter.toCoordinates(userPoint.getPoint()); - } + public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse(TmapComplexPublicTransportationResponse response) { + if (response == null || response.getMetaData() == null || response.getMetaData().getPlan() == null + || response.getMetaData().getPlan().getItineraries() == null || response.getMetaData().getPlan().getItineraries().isEmpty()) { + return null; + } + + Itinerary itinerary = response.getMetaData().getPlan().getItineraries().getFirst(); + ParsedItinerary parsedItinerary = new ParsedItinerary(); + parsedItinerary.setTotalDistance(itinerary.getTotalDistance()); + parsedItinerary.setTotalTime(itinerary.getTotalTime()); + + List parsedLegs = itinerary.getLegs().stream().map(leg -> { + ParsedLeg parsedLeg = new ParsedLeg(); + parsedLeg.setMode(leg.getMode()); + parsedLeg.setDistance(leg.getDistance()); + parsedLeg.setSectionTime(leg.getSectionTime()); + parsedLeg.setRoute(leg.getRoute()); + parsedLeg.setRouteColor(leg.getRouteColor()); + return parsedLeg; + }).collect(Collectors.toList()); + + parsedItinerary.setLegs(parsedLegs); - private Station getStation(Long stationId) { - return stationPersistenceAdapter.retrieveStation(stationId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + TmapComplexPublicTransportationParsedResponse parsedRequest = new TmapComplexPublicTransportationParsedResponse(); + parsedRequest.setParsedItinerary(parsedItinerary); + return parsedRequest; } - private Map buildRequestBody(Pair userLocation, Station station) { - Map requestBody = new HashMap<>(); - requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ - requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ - requestBody.put("endX", station.getLongitude()); // ๊ฒฝ๋„ - requestBody.put("endY", station.getLatitude()); // ์œ„๋„ - requestBody.put("count", 1); // ๊ฒฝ๋กœ ํƒ์ƒ‰ ๊ฒฐ๊ณผ ์ค‘ 1๊ฐœ๋งŒ ๋ฐ˜ํ™˜ - requestBody.put("format", "json"); - return requestBody; + public TmapPublicTransportationParsedResponse parseTmapResponse(TmapPublicTransportationResponse response) { + if (response == null || response.getMetaData() == null || response.getMetaData().getPlan() == null || response.getMetaData().getPlan().getItineraries() == null ) { + return null; + } + return TmapPublicTransportationParsedResponse.of( + response.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), + response.getMetaData().getPlan().getItineraries().getFirst().getTransferCount()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 111ea253..01cb516c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -64,6 +64,6 @@ public boolean hasNoStations() { @Override public Optional retrieveStation(Long stationId) { - return stationRepository.findStationByStationId(stationId); + return stationRepository.findStationById(stationId); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 154c3da0..ab0ebcaf 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -11,5 +11,5 @@ public interface StationRepository extends JpaRepository { @Query("SELECT EXISTS (SELECT 1 FROM Station)") boolean existsAny(); - Optional findStationByStationId(Long stationId); + Optional findStationById(Long stationId); } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java index 0c9c3073..2df0b337 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java @@ -9,7 +9,7 @@ public abstract class ContainerBaseTest { private static final int REDIS_PORT = 6379; - private static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.0") + private static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.4") .withDatabaseName("kok") .withUsername("root") .withPassword("1234"); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index 3f458cf7..11b18699 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -28,17 +28,17 @@ public class Location { private Integer memberId; @Column(nullable = false, columnDefinition = "POINT SRID 4326") - private Point point; + private Point location_point; - public Location(String uuid, Integer memberId, Point point) { + public Location(String uuid, Integer memberId, Point location_point) { this.uuid = uuid; this.memberId = memberId; - this.point = point; + this.location_point = location_point; } // ๋”ํ‹ฐ์ฒดํ‚น public void changePoint(Point point) { - this.point = point; + this.location_point = point; } } diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java index e73b6f9d..11ed831a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java @@ -1,8 +1,7 @@ package com.kok.kokcore.public_transfortation.usecase; -import java.util.List; - public interface RetrievePublicTransportationUsecase { - // List{์†Œ์š”์‹œ๊ฐ„, ํ™˜์ŠนํšŸ์ˆ˜} - List retrievePublicTransportation(Long stationId, String UUID, Integer memberId); + + String retrievePublicTransportation(Long stationId, String UUID, Integer memberId); + String retrieveComplexPublicTransportation(Long stationId, String UUID, Integer memberId); } From c44da29502107b8ead270ed52dce7c056498d107 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sun, 9 Mar 2025 02:09:51 +0900 Subject: [PATCH 072/163] :sparkles: feature: add complex public properties --- kok-api/src/main/resources/application-dev.yml | 6 +++++- kok-api/src/main/resources/application-prod.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index d9b931b5..d324c02f 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -37,7 +37,11 @@ station: start-idx: 1 end-idx: 1000 -tmap: +tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" keyname: "appKey" +tmap-complex: + key: ${TMAP_KEY} + url: "https://apis.openapi.sk.com/transit/routes" + keyname: "appKey" diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 42f9ef89..a1df9fcb 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -48,7 +48,11 @@ station: start-idx: 1 end-idx: 1000 -tmap: +tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" keyname: "appKey" +tmap-complex: + key: ${TMAP_KEY} + url: "https://apis.openapi.sk.com/transit/routes" + keyname: "appKey" From 459a3ce6afbc449b6df886e522b87f421d393021 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sun, 9 Mar 2025 02:27:40 +0900 Subject: [PATCH 073/163] :recycle: refactor: change dependency adapter to port --- .../external/PublicTransportationClient.java | 18 +++++++++--------- .../PublicTransportationComplexClient.java | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java index f911d925..16099f43 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java @@ -1,10 +1,10 @@ package com.kok.kokapi.public_transportation.adapter.out.external; -import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; -import com.kok.kokapi.station.adapter.out.persistence.StationPersistenceAdapter; +import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; import com.kok.kokcore.station.domain.entity.Station; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -29,14 +29,14 @@ public class PublicTransportationClient { private final RestClient restClient; private final TmapClientProperties properties; - private final StationPersistenceAdapter stationPersistenceAdapter; - private final LocationPersistenceAdapter locationPersistenceAdapter; + private final RetrieveStationsPort retrieveStationsPort; + private final ReadLocationPort readLocationPort; private final PointConverter pointConverter; - public PublicTransportationClient(TmapClientProperties properties, StationPersistenceAdapter stationPersistenceAdapter, LocationPersistenceAdapter locationPersistenceAdapter, PointConverter pointConverter) { + public PublicTransportationClient(TmapClientProperties properties, RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, PointConverter pointConverter) { this.properties = properties; - this.stationPersistenceAdapter = stationPersistenceAdapter; - this.locationPersistenceAdapter = locationPersistenceAdapter; + this.retrieveStationsPort = retrieveStationsPort; + this.readLocationPort = readLocationPort; this.pointConverter = pointConverter; this.restClient = getRestClient(); } @@ -86,13 +86,13 @@ private Map buildRequestBody(Pair userLo } private Pair getUserLocation(String UUID, Integer memberId) { - Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) + Location userPoint = readLocationPort.findLocationByUuidAndMemberId(UUID, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } private Station getStation(Long stationId) { - return stationPersistenceAdapter.retrieveStation(stationId) + return retrieveStationsPort.retrieveStation(stationId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java index cf4a9662..a214f710 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java @@ -1,10 +1,10 @@ package com.kok.kokapi.public_transportation.adapter.out.external; -import com.kok.kokapi.centroid.adapter.out.persistence.LocationPersistenceAdapter; import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; -import com.kok.kokapi.station.adapter.out.persistence.StationPersistenceAdapter; +import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; import com.kok.kokcore.station.domain.entity.Station; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -27,14 +27,14 @@ public class PublicTransportationComplexClient { private final RestClient restClient; private final TmapComplexClientProperties properties; - private final StationPersistenceAdapter stationPersistenceAdapter; - private final LocationPersistenceAdapter locationPersistenceAdapter; + private final RetrieveStationsPort retrieveStationsPort; + private final ReadLocationPort readLocationPort; private final PointConverter pointConverter; - public PublicTransportationComplexClient(TmapComplexClientProperties properties, StationPersistenceAdapter stationPersistenceAdapter, LocationPersistenceAdapter locationPersistenceAdapter, PointConverter pointConverter) { + public PublicTransportationComplexClient(TmapComplexClientProperties properties, RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, PointConverter pointConverter) { this.properties = properties; - this.stationPersistenceAdapter = stationPersistenceAdapter; - this.locationPersistenceAdapter = locationPersistenceAdapter; + this.retrieveStationsPort = retrieveStationsPort; + this.readLocationPort = readLocationPort; this.pointConverter = pointConverter; this.restClient = getRestClient(); } @@ -73,13 +73,13 @@ public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(L } private Pair getUserLocation(String UUID, Integer memberId) { - Location userPoint = locationPersistenceAdapter.findLocationByUuidAndMemberId(UUID, memberId) + Location userPoint = readLocationPort.findLocationByUuidAndMemberId(UUID, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } private Station getStation(Long stationId) { - return stationPersistenceAdapter.retrieveStation(stationId) + return retrieveStationsPort.retrieveStation(stationId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); } From de914951464477d9f470cf93d5e0283c41b6fa6b Mon Sep 17 00:00:00 2001 From: minseokey Date: Sun, 9 Mar 2025 02:31:41 +0900 Subject: [PATCH 074/163] :recycle: refactor: findout typo --- .../service/TmapPublicTransportationService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 40d0b1fb..83da47d8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -75,9 +75,9 @@ public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse(Tm parsedItinerary.setLegs(parsedLegs); - TmapComplexPublicTransportationParsedResponse parsedRequest = new TmapComplexPublicTransportationParsedResponse(); - parsedRequest.setParsedItinerary(parsedItinerary); - return parsedRequest; + TmapComplexPublicTransportationParsedResponse parsedResponse = new TmapComplexPublicTransportationParsedResponse(); + parsedResponse.setParsedItinerary(parsedItinerary); + return parsedResponse; } public TmapPublicTransportationParsedResponse parseTmapResponse(TmapPublicTransportationResponse response) { From eeaf64dd152b6dff7001f5ac58b83bad598c940c Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Sun, 9 Mar 2025 03:22:52 +0900 Subject: [PATCH 075/163] :wrench: config: deactivate test on build --- .github/workflows/kok-CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-CI.yml b/.github/workflows/kok-CI.yml index a4dd51bf..7de15e9d 100644 --- a/.github/workflows/kok-CI.yml +++ b/.github/workflows/kok-CI.yml @@ -49,7 +49,7 @@ jobs: - name: Build with Gradle (๋ฉ€ํ‹ฐ๋ชจ๋“ˆ) run: | - ./gradlew clean build + ./gradlew clean build -x test - name: Save Build Artifacts uses: actions/upload-artifact@v4 From eb1b884dfebb08aa07be9dd3f3254e4fb302a33f Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Sun, 9 Mar 2025 03:23:51 +0900 Subject: [PATCH 076/163] :wrench: config: add exposed port for redis on test container --- .../com/kok/kokapi/common/template/ContainerBaseTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java index 1861f537..5d51c7f7 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ContainerBaseTest.java @@ -14,8 +14,8 @@ public abstract class ContainerBaseTest { .withUsername("root") .withPassword("1234"); - private static final RedisContainer redisContainer = new RedisContainer("redis:7.0" - ); + private static final RedisContainer redisContainer = new RedisContainer("redis:7.0") + .withExposedPorts(REDIS_PORT); static { mysqlContainer.start(); From 206f62d755ae94f9e7c30a958edd72140950e4ee Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Sun, 9 Mar 2025 03:52:35 +0900 Subject: [PATCH 077/163] :wrench: config: disable flyway.. --- kok-api/src/main/resources/application-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index e3fb078e..197955bf 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -13,9 +13,9 @@ spring: format_sql: true dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect open-in-view: false - defer-datasource-initialization: false + defer-datasource-initialization: true flyway: - enabled: true + enabled: false baseline-on-migrate: true data: redis: From 0039a223eee16fcbe633841a5a2613522290c782 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sun, 9 Mar 2025 04:19:43 +0900 Subject: [PATCH 078/163] [Fix/station] fix station config error (#58) * :bug: fix: disable error handler due to stream consumed * :bug: fix: change field --- .../kok/kokapi/station/adapter/out/external/StationClient.java | 2 +- kok-api/src/main/resources/db/V1__init_station.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java index 2b1b430b..eac61006 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java @@ -33,7 +33,7 @@ public RestClient getRestClient() { return RestClient.builder() .requestFactory(getRequestFactory()) .baseUrl(properties.baseUrl()) - .defaultStatusHandler(stationErrorHandler) + //.defaultStatusHandler(stationErrorHandler) .build(); } diff --git a/kok-api/src/main/resources/db/V1__init_station.sql b/kok-api/src/main/resources/db/V1__init_station.sql index 0cffbc70..6882fc10 100644 --- a/kok-api/src/main/resources/db/V1__init_station.sql +++ b/kok-api/src/main/resources/db/V1__init_station.sql @@ -22,7 +22,7 @@ create table location ( id bigint auto_increment primary key, member_id int not null, - point point not null, + location_point point not null, uuid varchar(255) not null, constraint UKrgpajb4rsivb4gj9xn2qowgw6 unique (uuid, member_id) From ef59a82a5cedf96fc7bbec8d20c36eab22760198 Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Mon, 10 Mar 2025 16:38:33 +0900 Subject: [PATCH 079/163] :bug: fix: add missing configuration --- kok-api/src/test/resources/application-test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index 5aa32c92..ed83d5bf 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -16,3 +16,14 @@ spring: open-in-view: false hibernate: ddl-auto: update +### ์‹ค์ œ ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์งœ ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +ncp: + object-storage-url: mock +tmap-sub: + key: mock + url: mock + keyname: mock +tmap-complex: + key: mock + url: mock + keyname: mock \ No newline at end of file From 8b309e09c908b230d2bf2cd31f8f2262744f9591 Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Mon, 10 Mar 2025 16:56:45 +0900 Subject: [PATCH 080/163] :white_check_mark: test: make context loads test use testContainer --- .../src/test/java/com/kok/kokapi/KokApiApplicationTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java b/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java index 0e970898..f1d8c6e4 100644 --- a/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java +++ b/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java @@ -1,10 +1,11 @@ package com.kok.kokapi; +import com.kok.kokapi.common.template.ServiceTest; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class KokApiApplicationTests { +class KokApiApplicationTests extends ServiceTest { @Test void contextLoads() { From c513d4e9493534c04f7f32e9ad1e26f05ec6d2bc Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Mon, 10 Mar 2025 17:36:11 +0900 Subject: [PATCH 081/163] :white_check_mark: test: use mock for external api --- .../java/com/kok/kokapi/common/template/ServiceTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java index 105695d6..1648b445 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java @@ -2,11 +2,14 @@ import com.kok.kokapi.common.util.MySQLDatabaseCleanerExtension; import com.kok.kokapi.config.ServiceTestConfig; +import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; +import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationComplexClient; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; @ExtendWith(MySQLDatabaseCleanerExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @@ -15,4 +18,8 @@ @Profile("test") public abstract class ServiceTest extends ContainerBaseTest{ + @MockitoBean + protected PublicTransportationComplexClient publicTransportationComplexClient; + @MockitoBean + protected PublicTransportationClient publicTransportationClient; } From 1c55205f27b6600efd3b48ddd7a4ed0fa11ceb8f Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 20:53:04 +0900 Subject: [PATCH 082/163] :bug: fix: change wrong flyway script path to default flyway script path --- .../db/{V1__init_station.sql => migration/V1__create_tables.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename kok-api/src/main/resources/db/{V1__init_station.sql => migration/V1__create_tables.sql} (100%) diff --git a/kok-api/src/main/resources/db/V1__init_station.sql b/kok-api/src/main/resources/db/migration/V1__create_tables.sql similarity index 100% rename from kok-api/src/main/resources/db/V1__init_station.sql rename to kok-api/src/main/resources/db/migration/V1__create_tables.sql From 566dbdc9c6bb3e9a4378832e7b3883b1f2f276ec Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 20:54:24 +0900 Subject: [PATCH 083/163] :wrench: config: change ddl auto to validate --- kok-api/src/test/resources/application-test.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index 5aa32c92..0c92ab99 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -15,4 +15,14 @@ spring: defer-datasource-initialization: false open-in-view: false hibernate: - ddl-auto: update + ddl-auto: validate +ncp: + object-storage-url: mock +tmap-sub: + key: mock + url: mock + keyname: mock +tmap-complex: + key: mock + url: mock + keyname: mock From a8837cda42f5937f326712c99457c923a5f8c9fb Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 20:55:51 +0900 Subject: [PATCH 084/163] :wrench: config: add flyway locations --- kok-api/src/main/resources/application-dev.yml | 1 + kok-api/src/main/resources/application-prod.yml | 1 + kok-api/src/test/resources/application-test.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 197955bf..c9735045 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -16,6 +16,7 @@ spring: defer-datasource-initialization: true flyway: enabled: false + locations: classpath:db/migration baseline-on-migrate: true data: redis: diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index c8b58d94..d6528d84 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -16,6 +16,7 @@ spring: defer-datasource-initialization: false flyway: enabled: true + locations: classpath:db/migration baseline-on-migrate: true data: redis: diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index 0c92ab99..d645ad26 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -4,6 +4,7 @@ spring: flyway: enabled: true connect-retries: 30 + locations: classpath:db/migration baseline-on-migrate: true jpa: show-sql: true From daaefebe02450a2fa14290a4d6ae14a0d1e4721b Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 20:56:17 +0900 Subject: [PATCH 085/163] :wrench: config: activate flyway on dev --- kok-api/src/main/resources/application-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index c9735045..d04ebf88 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -13,9 +13,9 @@ spring: format_sql: true dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect open-in-view: false - defer-datasource-initialization: true + defer-datasource-initialization: false flyway: - enabled: false + enabled: true locations: classpath:db/migration baseline-on-migrate: true data: From d383820460928fb2a7373ef39202336d287b45d9 Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 20:58:54 +0900 Subject: [PATCH 086/163] :recycle: refactor: add EOL --- kok-api/src/test/resources/application-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index ed83d5bf..8726e672 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -26,4 +26,4 @@ tmap-sub: tmap-complex: key: mock url: mock - keyname: mock \ No newline at end of file + keyname: mock From 596382e5bc380c14143d9f0c97e234bed664ff17 Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 20:59:34 +0900 Subject: [PATCH 087/163] :recycle: refactor: fix indent format --- kok-api/src/test/resources/application-test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index d645ad26..8fc1555e 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -18,12 +18,12 @@ spring: hibernate: ddl-auto: validate ncp: - object-storage-url: mock + object-storage-url: mock tmap-sub: - key: mock - url: mock - keyname: mock + key: mock + url: mock + keyname: mock tmap-complex: - key: mock - url: mock - keyname: mock + key: mock + url: mock + keyname: mock From 277f56cafa789f0b4d8e8965f58962270fb3412f Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 22:47:16 +0900 Subject: [PATCH 088/163] :sparkles: feat: add priority data sql script --- .../src/main/resources/db/data/priority.sql | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 kok-api/src/main/resources/db/data/priority.sql diff --git a/kok-api/src/main/resources/db/data/priority.sql b/kok-api/src/main/resources/db/data/priority.sql new file mode 100644 index 00000000..f3710d70 --- /dev/null +++ b/kok-api/src/main/resources/db/data/priority.sql @@ -0,0 +1,104 @@ +-- priority 1 +UPDATE station +SET priority = 1 +WHERE name IN ( + '4.19๋ฏผ์ฃผ๋ฌ˜์ง€', '๊ฐ€์˜ค๋ฆฌ', '๊ฐ•์ผ', '๊ฐœ๋กฑ', '๊ฐœํ™”', '๊ฑฐ์—ฌ', '๊ณ ๋•', '๊ธธ๋™', + '๋‚จํƒœ๋ น', '๋‹น๊ณ ๊ฐœ', '๋Œ€์ฒญ', '๋„๋ด‰์‚ฐ', '๋…๋ฐ”์œ„', '๋‘”์ดŒ๋™', '๋งˆ๋“ค', '๋งˆ์ฒœ', + '๋งค๋ด‰', '๋ช…์ผ', '๋ฌด์•…์žฌ', '๋ฐฉํ™”', '์‚ผ์–‘', '์‚ผ์–‘์‚ฌ๊ฑฐ๋ฆฌ', '์ƒ๊ณ„', '์ƒ์ผ๋™', + '์ƒˆ์ ˆ(์‹ ์‚ฌ)', '์„์ˆ˜', '์†”์ƒ˜', '์ˆ˜๋ฝ์‚ฐ', '์‹ ๋‚ด', '์‹ ์ด๋ฌธ', '์Œ๋ฌธ', '์—ญ์ดŒ', + '์‘๋ด‰', '์‘์•”', '์ผ์›', '์ •๋ฆ‰', '์ค‘๊ณ„', '์ค‘์•™๋ณดํ›ˆ๋ณ‘์›', 'ํ•˜๊ณ„', 'ํ•™์—ฌ์šธ', 'ํ™”๊ณ„' + ); + +-- priority 2 +UPDATE station +SET priority = 2 +WHERE name IN ( + '๊ฐœ๋ด‰', '๊ฐœํ™”์‚ฐ', '๊น€ํฌ๊ณตํ•ญ', '๋‚จ๊ตฌ๋กœ', '๋‚จ์„ฑ', '๋‚ด๋ฐฉ', '๋…น์ฒœ', '๋„๋ฆผ์ฒœ', + '๋Œ๊ณถ์ด', '๋™๋Œ€์ž…๊ตฌ', '๋‘”์ดŒ์˜ค๋ฅœ', '๋จน๊ณจ', '๋ฉด๋ชฉ', '๋ฏธ์•„(์„œ์šธ์‚ฌ์ด๋ฒ„๋Œ€ํ•™)', '๋ด‰์ฒœ', + '์‚ฌ๊ฐ€์ •', '์ƒ๋„', '์ƒ๋ด‰(์‹œ์™ธ๋ฒ„์Šคํ„ฐ๋ฏธ๋„)', '์„œ์›', '์ˆ˜์ƒ‰', '์‹ ๋‹ต', '์‹ ๋Œ€๋ฐฉ์‚ผ๊ฑฐ๋ฆฌ', + '์‹ ์ •๋„ค๊ฑฐ๋ฆฌ', '์‹ ํ’', '์•”์‚ฌ', '์•”์‚ฌ์—ญ์‚ฌ๊ณต์›', '์–‘์ฒœ๊ตฌ์ฒญ', '์˜ค๊ธˆ', '์˜ค๋ฅ˜๋™', + '์˜จ์ˆ˜(์„ฑ๊ณตํšŒ๋Œ€์ž…๊ตฌ)', '์šฉ๋‹ต', '์šฉ๋งˆ์‚ฐ', '์šฐ์žฅ์‚ฐ', '์ด์ˆ˜', '์žฅ์Šน๋ฐฐ๊ธฐ', '์ œ๊ธฐ๋™', + '์ค‘๊ณก', '์ค‘ํ™”', '์ฆ๋ฏธ', '์ฒœ์™•', '์ฒญ๊ณ„์‚ฐ์ž…๊ตฌ', 'ํ•™๋™', 'ํ™์ œ', 'ํ™”๊ณก' + ); + +-- priority 3 +UPDATE station +SET priority = 3 +WHERE name IN ( + '๊ฐ•๋ณ€(๋™์„œ์šธํ„ฐ๋ฏธ๋„)', '๊ตฌ๋กœ', '๊ตฌ๋ฐ˜ํฌ', '๊ตญํšŒ์˜์‚ฌ๋‹น', '๊ตฝ์€๋‹ค๋ฆฌ(๊ฐ•๋™๊ตฌ๋ฏผํšŒ๊ด€์•ž)', '๊ธˆํ˜ธ', + '๊ธธ์Œ', '๊นŒ์น˜์‚ฐ', '๋‹ต์‹ญ๋ฆฌ', '๋Œ€๋ชจ์‚ฐ์ž…๊ตฌ', '๋„๋ด‰', '๋…๋ฆฝ๋ฌธ', '๋งˆ์žฅ', '๋ง์šฐ', '๋ฏธ์•„์‚ฌ๊ฑฐ๋ฆฌ', + '๋ฐœ์‚ฐ', '๋ฐฉํ•™', '๋ณด๋ฌธ', '๋ด‰ํ™”์‚ฐ(์„œ์šธ์˜๋ฃŒ์›)', '๋ถํ•œ์‚ฐ๋ณด๊ตญ๋ฌธ', '์ƒ์›”๊ณก(ํ•œ๊ตญ๊ณผํ•™๊ธฐ์ˆ ์—ฐ๊ตฌ์›)', + '์„œ๋น™๊ณ ', '์†ก์ •', '์ˆ˜์œ (๊ฐ•๋ถ๊ตฌ์ฒญ)', '์‹ ๊ธˆํ˜ธ', '์‹ ๋ฐฉํ™”', '์‹ ์ •(์€ํ–‰์ •)', '์• ์˜ค๊ฐœ', '์–‘์›', + '์–‘์ฒœํ–ฅ๊ต', '์–ธ์ฃผ', '์—ฐ์‹ ๋‚ด', '์—ผ์ฐฝ', '์šฉ๋‘(๋™๋Œ€๋ฌธ๊ตฌ์ฒญ)', '์›”๊ณก(๋™๋•์—ฌ๋Œ€)', '์ž ์‹ค๋‚˜๋ฃจ', + '์ž ์‹ค์ƒˆ๋‚ด', '์žฅ์ง€', '์ฐฝ๋™', 'ํƒœ๋ฆ‰์ž…๊ตฌ', 'ํ•œ์„ฑ๋ฐฑ์ œ', 'ํ–‰๋‹น', 'ํ™”๋ž‘๋Œ€(์„œ์šธ์—ฌ๋Œ€์ž…๊ตฌ)', '๋…น๋ฒˆ' + ); + +-- priority 4 +UPDATE station +SET priority = 4 +WHERE name IN ( + '๊ฐ€๋ฝ์‹œ์žฅ', '๊ด€์•…', '๊ตฌ๋ฃก', '๊ตฌ์˜(๊ด‘์ง„๊ตฌ์ฒญ)', '๊ตฌํŒŒ๋ฐœ', '๊ธˆ์ฒœ๊ตฌ์ฒญ', '๋…ธ์›', '๋ฐฉ๋ฐฐ', + '๋ณต์ •', '๋ถˆ๊ด‘', '์‚ฌํ‰', '์„๊ณ„', '์„ ์œ ๋„', '์†”๋ฐญ๊ณต์›', '์‹ ๋Œ€๋ฐฉ', '์‹ ๋ชฉ๋™', '์‹ ๋ฐ˜ํฌ', + '์‹ ์„ค๋™', '์ค‘๋ž‘' + ); + +-- priority 5 +UPDATE station +SET priority = 5 +WHERE name IN ( + '๊ฐ€์ขŒ', '๊ฐ•๋™', '๊ฐ•๋™๊ตฌ์ฒญ', '๊ณต๋ฆ‰(์„œ์šธ๊ณผํ•™๊ธฐ์ˆ ๋Œ€)', '๊ด€์•…์‚ฐ(์„œ์šธ๋Œ€)', '๊ด‘๋‚˜๋ฃจ(์žฅ์‹ ๋Œ€)', + '๊ด‘ํฅ์ฐฝ(์„œ๊ฐ•)', '๊ตฐ์ž(๋Šฅ๋™)', '๋Œ€๋ฆผ(๊ตฌ๋กœ๊ตฌ์ฒญ)', '๋Œ€์น˜', '๋ชฉ๋™', '๋ชฝ์ดŒํ† ์„ฑ(ํ‰ํ™”์˜๋ฌธ)', + '๋ฌธ์ •', '์‚ผ์ „', '์ƒ์™•์‹ญ๋ฆฌ', '์„์ดŒ๊ณ ๋ถ„', '์ˆญ์‹ค๋Œ€์ž…๊ตฌ(์‚ดํ”ผ์žฌ)', '์‹ ๊ธธ', '์•„์ฐจ์‚ฐ(์–ด๋ฆฐ์ด๋Œ€๊ณต์›ํ›„๋ฌธ)', + '์•„ํ˜„', '์˜ค๋ชฉ๊ต(๋ชฉ๋™์šด๋™์žฅ์•ž)', '์˜ฅ์ˆ˜', '์›”๊ณ„', '์ด์ดŒ(๊ตญ๋ฆฝ์ค‘์•™๋ฐ•๋ฌผ๊ด€)', '์ž ์›', '์žฅํ•œํ‰', + '์ฆ์‚ฐ(๋ช…์ง€๋Œ€์•ž)', '์ฐฝ์‹ ', '์ฒญ๊ตฌ', '์ถฉ์ •๋กœ(๊ฒฝ๊ธฐ๋Œ€์ž…๊ตฌ)', 'ํ•œ์„ฑ๋Œ€์ž…๊ตฌ(์‚ผ์„ ๊ต)', 'ํ•œํ‹ฐ' + ); + +-- priority 6 +UPDATE station +SET priority = 6 +WHERE name IN ( + '๊ฐ€์‚ฐ๋””์ง€ํ„ธ๋‹จ์ง€', '๊ฐœํฌ๋™', '๊ณตํ•ญ์‹œ์žฅ', '๊ตฌ๋กœ๋””์ง€ํ„ธ๋‹จ์ง€', '๋‚™์„ฑ๋Œ€', '๋‹น์‚ฐ', '๋Œ€๋ฐฉ', + '๋„๊ณก', '๋™๋Œ€๋ฌธ', '๋“ฑ์ดŒ', '๋งˆ๊ณก', '๋งˆ๊ณก๋‚˜๋ฃจ', '๋ณด๋ผ๋งค', '๋ณด๋ผ๋งค๊ณต์›', '๋ณด๋ผ๋งค๋ณ‘์›', + '๋ถํ•œ์‚ฐ์šฐ์ด', '์ƒ›๊ฐ•', '์„œ์šธ๋Œ€๋ฒค์ฒ˜ํƒ€์šด', '์„œ์šธ๋Œ€์ž…๊ตฌ(๊ด€์•…๊ตฌ์ฒญ)', '์„œ์šธ์ง€๋ฐฉ๋ณ‘๋ฌด์ฒญ', '์„ ์ •๋ฆ‰', + '์„ฑ์‹ ์—ฌ๋Œ€์ž…๊ตฌ(๋ˆ์•”)', '์‹ ๋‹น', '์•ฝ์ˆ˜', '์ข…๋กœ5๊ฐ€', '์ข…ํ•ฉ์šด๋™์žฅ', '์ด์‹ ๋Œ€์ž…๊ตฌ(์ด์ˆ˜)', 'ํ•œ์–‘๋Œ€', + 'ํšŒํ˜„(๋‚จ๋Œ€๋ฌธ์‹œ์žฅ)' + ); + +-- priority 7 +UPDATE station +SET priority = 7 +WHERE name IN ( + '๊ฐ€์–‘', '๊ฐ•๋‚จ๊ตฌ์ฒญ', '๊ด‘์šด๋Œ€', '๊ต๋Œ€(๋ฒ•์›.๊ฒ€์ฐฐ์ฒญ)', '๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„(์˜ˆ์ˆ ์˜์ „๋‹น)', '๋‹น๊ณก', + '๋™๋ฌ˜์•ž', '๋””์ง€ํ„ธ๋ฏธ๋””์–ด์‹œํ‹ฐ', '๋งˆํฌ', '์‚ฌ๋‹น', '์‚ผ์„ฑ์ค‘์•™', '์„œ๋Œ€๋ฌธ', '์„œ์šธ์ˆฒ', '์†กํŒŒ๋‚˜๋ฃจ', + '์ˆ˜์„œ', '์ˆ™๋Œ€์ž…๊ตฌ(๊ฐˆ์›”)', '์‹ ๋„๋ฆผ', '์—ฌ์˜๋‚˜๋ฃจ', '์˜๋“ฑํฌ๊ตฌ์ฒญ', '์˜๋“ฑํฌ์‹œ์žฅ', '์˜ฌ๋ฆผํ”ฝ๊ณต์›(ํ•œ๊ตญ์ฒด๋Œ€)', + '์ฒญ๋Ÿ‰๋ฆฌ(์„œ์šธ์‹œ๋ฆฝ๋Œ€์ž…๊ตฌ)', 'ํšŒ๊ธฐ', 'ํ‘์„(์ค‘์•™๋Œ€์ž…๊ตฌ)' + ); + +-- priority 8 +UPDATE station +SET priority = 8 +WHERE name IN ( + '๋…ธ๋“ค', '๋…ธ๋Ÿ‰์ง„', '๋™๋Œ€๋ฌธ์—ญ์‚ฌ๋ฌธํ™”๊ณต์›', '๋™์ž‘(ํ˜„์ถฉ์›)', '๋š์„ฌ', '๋ฌธ๋ž˜', '๋ฐ˜ํฌ', '๋ฐฉ์ด', + '์‚ผ์„ฑ', '์‚ผ์„ฑ(๋ฌด์—ญ์„ผํ„ฐ)', '์„œ๊ฐ•๋Œ€', '์„œ์ดˆ', '์„ ๋ฆ‰', '์†กํŒŒ', '์‹ ๋ฆผ', '์–‘์žฌ(์„œ์ดˆ๊ตฌ์ฒญ)', + '์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ(๋งคํ—Œ)', '์–ด๋ฆฐ์ด๋Œ€๊ณต์›(์„ธ์ข…๋Œ€)', '์™ธ๋Œ€์•ž', '์šฉ์‚ฐ', '์„์ง€๋กœ3๊ฐ€', '์„์ง€๋กœ4๊ฐ€', + '์ฒœํ˜ธ(ํ’๋‚ฉํ† ์„ฑ)', '์ฒญ๋‹ด', '์ถฉ๋ฌด๋กœ' + ); + +-- priority 9 +UPDATE station +SET priority = 9 +WHERE name IN ( + '๊ฒฝ๋ณต๊ถ(์ •๋ถ€์„œ์šธ์ฒญ์‚ฌ)', '๊ณ ์†ํ„ฐ๋ฏธ๋„', '๊ด‘ํ™”๋ฌธ(์„ธ์ข…๋ฌธํ™”ํšŒ๊ด€)', '๋‚จ์˜', '๋ช…๋™', '๋ด‰์€์‚ฌ', + '์‚ผ๊ฐ์ง€', '์„œ์šธ', '์„œ์šธ์—ญ', '์„์ดŒ', '์‹œ์ฒญ', '์‹ ๋…ผํ˜„', '์‹ ์‚ฌ', '์‹ ์šฉ์‚ฐ', '์••๊ตฌ์ •๋กœ๋ฐ์˜ค', + '์—ฌ์˜๋„', '์™•์‹ญ๋ฆฌ(์„ฑ๋™๊ตฌ์ฒญ)', '์ž ์‹ค(์†กํŒŒ๊ตฌ์ฒญ)', '์ข…๊ฐ', '์ข…๋กœ3๊ฐ€', 'ํšจ์ฐฝ๊ณต์›์•ž' + ); + +-- priority 10 +UPDATE station +SET priority = 10 +WHERE name IN ( + '๊ฐ•๋‚จ', '๊ฑด๋Œ€์ž…๊ตฌ', '๊ณ ๋ ค๋Œ€(์ข…์•”)', '๊ณต๋•', '๋…น์‚ฌํ‰(์šฉ์‚ฐ๊ตฌ์ฒญ)', '๋…ผํ˜„', '๋Œ€ํฅ(์„œ๊ฐ•๋Œ€์•ž)', + '๋š์„ฌ์œ ์›์ง€', '๋งˆํฌ๊ตฌ์ฒญ', '๋ง์›', '๋ฒ„ํ‹ฐ๊ณ ๊ฐœ', '์ƒ์ˆ˜', '์„ฑ์ˆ˜', '์‹ ์ดŒ', '์•ˆ๊ตญ', '์•ˆ์•”(๊ณ ๋Œ€๋ณ‘์›์•ž)', + '์••๊ตฌ์ •', '์—ญ์‚ผ', '์˜๋“ฑํฌ', '์›”๋“œ์ปต๊ฒฝ๊ธฐ์žฅ(์„ฑ์‚ฐ)', '์„์ง€๋กœ์ž…๊ตฌ', '์ด๋Œ€', '์ดํƒœ์›', 'ํ•œ๊ฐ•์ง„', + 'ํ•œ๋‚จ', 'ํ•ฉ์ •', 'ํ˜œํ™”', 'ํ™๋Œ€์ž…๊ตฌ' + ); \ No newline at end of file From eda278bf070242b04518bda71ef49880454531d6 Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 22:56:04 +0900 Subject: [PATCH 089/163] :recycle: refactor: rename init sql script name --- .../db/migration/{V1__create_tables.sql => V1__init.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename kok-api/src/main/resources/db/migration/{V1__create_tables.sql => V1__init.sql} (100%) diff --git a/kok-api/src/main/resources/db/migration/V1__create_tables.sql b/kok-api/src/main/resources/db/migration/V1__init.sql similarity index 100% rename from kok-api/src/main/resources/db/migration/V1__create_tables.sql rename to kok-api/src/main/resources/db/migration/V1__init.sql From 4f19ebc2a8af07fe43d341e79b5cbe3c44b741b1 Mon Sep 17 00:00:00 2001 From: linirini <2001yerin@naver.com> Date: Tue, 11 Mar 2025 22:56:31 +0900 Subject: [PATCH 090/163] :wrench: config: set up flyway baseline-version for dev environment --- kok-api/src/main/resources/application-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index d04ebf88..f73e280c 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -18,6 +18,7 @@ spring: enabled: true locations: classpath:db/migration baseline-on-migrate: true + baseline-version: 1 # V1 ์ดํ•˜์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์ด๋ฏธ ์ ์šฉ. data: redis: host: ${REDIS_HOST} # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. From 58ea7a11098ebb989cf9e404d4deda8e994ae6ec Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Mar 2025 03:10:41 +0900 Subject: [PATCH 091/163] :sparkles: feat: add station recommend --- .../adapter/in/web/LocationController.java | 12 +- .../service/CentroidQueryService.java | 4 +- .../service/LocationCommandService.java | 4 +- .../service/LocationQueryService.java | 4 +- .../web/PublicTransportationController.java | 4 +- .../TmapPublicTransportationService.java | 4 +- .../persistence/RoutePersistenceAdapter.java | 13 +- .../StationPersistenceAdapter.java | 17 ++- .../out/persistence/StationRepository.java | 12 ++ .../application/service/StationService.java | 130 +++++++++++++++++- ...secase.java => CreateLocationUseCase.java} | 2 +- ...dUsecase.java => ReadCentroidUseCase.java} | 2 +- ...nUsecase.java => ReadLocationUseCase.java} | 2 +- ... RetrievePublicTransportationUseCase.java} | 2 +- .../port/out/RetrieveStationsPort.java | 3 + 15 files changed, 186 insertions(+), 29 deletions(-) rename kok-core/src/main/java/com/kok/kokcore/location/usecase/{CreateLocationUsecase.java => CreateLocationUseCase.java} (89%) rename kok-core/src/main/java/com/kok/kokcore/location/usecase/{ReadCentroidUsecase.java => ReadCentroidUseCase.java} (87%) rename kok-core/src/main/java/com/kok/kokcore/location/usecase/{ReadLocationUsecase.java => ReadLocationUseCase.java} (89%) rename kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/{RetrievePublicTransportationUsecase.java => RetrievePublicTransportationUseCase.java} (81%) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index dd169e31..0c14a2fb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -8,9 +8,9 @@ import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.usecase.CreateLocationUsecase; -import com.kok.kokcore.location.usecase.ReadCentroidUsecase; -import com.kok.kokcore.location.usecase.ReadLocationUsecase; +import com.kok.kokcore.location.usecase.CreateLocationUseCase; +import com.kok.kokcore.location.usecase.ReadCentroidUseCase; +import com.kok.kokcore.location.usecase.ReadLocationUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -25,9 +25,9 @@ @RequiredArgsConstructor public class LocationController { - private final CreateLocationUsecase createLocationUsecase; - private final ReadCentroidUsecase readCentroidUsecase; - private final ReadLocationUsecase readLocationUsecase; + private final CreateLocationUseCase createLocationUsecase; + private final ReadCentroidUseCase readCentroidUsecase; + private final ReadLocationUseCase readLocationUsecase; private final LocationMapper locationMapper; @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java index 23c57d08..914fa70a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -2,7 +2,7 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.location.application.port.out.ReadCentroidPort; -import com.kok.kokcore.location.usecase.ReadCentroidUsecase; +import com.kok.kokcore.location.usecase.ReadCentroidUseCase; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; @@ -12,7 +12,7 @@ @Service @RequiredArgsConstructor -public class CentroidQueryService implements ReadCentroidUsecase { +public class CentroidQueryService implements ReadCentroidUseCase { private final ReadCentroidPort readCentroidPort; private final PointConverter pointConverter; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 276bbce9..398b2ada 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -4,7 +4,7 @@ import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.application.port.out.SaveLocationPort; -import com.kok.kokcore.location.usecase.CreateLocationUsecase; +import com.kok.kokcore.location.usecase.CreateLocationUseCase; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; @@ -14,7 +14,7 @@ @Service @RequiredArgsConstructor -public class LocationCommandService implements CreateLocationUsecase { +public class LocationCommandService implements CreateLocationUseCase { private final SaveLocationPort saveLocationPort; private final ReadLocationPort readLocationPort; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index 07df7c24..0d14b455 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -2,7 +2,7 @@ import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.application.port.out.ReadLocationPort; -import com.kok.kokcore.location.usecase.ReadLocationUsecase; +import com.kok.kokcore.location.usecase.ReadLocationUseCase; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -10,7 +10,7 @@ @Service @RequiredArgsConstructor -public class LocationQueryService implements ReadLocationUsecase{ +public class LocationQueryService implements ReadLocationUseCase { private final ReadLocationPort readLocationPort; diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java index f30aac8c..bf96fdaa 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java @@ -7,7 +7,7 @@ import com.kok.kokapi.public_transportation.adapter.in.dto.RouteRequest; import com.kok.kokapi.public_transportation.adapter.in.dto.TmapPublicTransportationParsedResponse; import com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse; -import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; +import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUseCase; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -18,7 +18,7 @@ @RequiredArgsConstructor public class PublicTransportationController { - private final RetrievePublicTransportationUsecase retrievePublicTransportationUsecase; + private final RetrievePublicTransportationUseCase retrievePublicTransportationUsecase; private final ObjectMapper objectMapper; @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 83da47d8..065ba1c6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -8,7 +8,7 @@ import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationComplexClient; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; -import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUsecase; +import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUseCase; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; @@ -23,7 +23,7 @@ @Service @RequiredArgsConstructor @Slf4j -public class TmapPublicTransportationService implements RetrievePublicTransportationUsecase { +public class TmapPublicTransportationService implements RetrievePublicTransportationUseCase { private final PublicTransportationClient publicTransportationClient; private final PublicTransportationComplexClient publicTransportationComplexClient; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java index 90a8b872..0a603034 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java @@ -1,9 +1,12 @@ package com.kok.kokapi.station.adapter.out.persistence; +import com.kok.kokcore.station.application.port.out.RetrieveRoutePort; import com.kok.kokcore.station.application.port.out.SaveRoutePort; import com.kok.kokcore.station.domain.entity.Route; import java.util.List; import java.util.function.Function; + +import com.kok.kokcore.station.domain.entity.Station; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -13,7 +16,9 @@ @Repository @Slf4j @RequiredArgsConstructor -public class RoutePersistenceAdapter implements SaveRoutePort { +public class RoutePersistenceAdapter implements SaveRoutePort, RetrieveRoutePort { + + private final RouteRepository routeRepository; private static final String INSERT_ROUTE_SQL = """ INSERT INTO route (code, name, station_id) @@ -44,4 +49,10 @@ private void batchInsertRoutes(List routes) { log.debug("Successfully saved a total of {} routes out of {}.", batched.length, routes.size()); } + + + @Override + public List retrieveRoutes(Station station) { + return routeRepository.findAllByStationOrderByName(station); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 68e17bd3..44f7557c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -1,14 +1,19 @@ package com.kok.kokapi.station.adapter.out.persistence; +import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.station.application.port.out.ReadStationsPort; import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; import com.kok.kokcore.station.application.port.out.SaveStationsPort; import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; + +import java.math.BigDecimal; +import java.util.*; import java.util.function.Function; -import java.util.Optional; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.Point; +import org.springframework.data.util.Pair; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; @@ -18,6 +23,8 @@ @RequiredArgsConstructor public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort, RetrieveStationsPort { + private final PointConverter pointConverter; + private static final String INSERT_STATION_SQL = """ INSERT INTO station (name, latitude, longitude, priority) VALUES (:name, :latitude, :longitude, :priority) @@ -62,5 +69,11 @@ public boolean hasNoStations() { public Optional retrieveStation(Long stationId) { return stationRepository.findStationById(stationId); } + + @Override + public List retrieveInRangeStations(Point centroid, double dist) { + Pair lonLat = pointConverter.toCoordinates(centroid); + return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), lonLat.getSecond(), dist); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 9f1aef9e..afd03ab8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -1,7 +1,11 @@ package com.kok.kokapi.station.adapter.out.persistence; import com.kok.kokcore.station.domain.entity.Station; + +import java.math.BigDecimal; import java.util.List; + +import io.lettuce.core.dynamic.annotation.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -15,4 +19,12 @@ public interface StationRepository extends JpaRepository { List findAllByNameIn(List names); Optional findStationById(Long stationId); + + @Query(value = """ + SELECT * FROM station + WHERE ST_Distance_Sphere(Point(longitude, latitude), Point(:lon, :lat)) < :distance + AND priority > 0 + """, nativeQuery = true) + List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, @Param("lat") BigDecimal lat, @Param("distance") Double distance); + } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index ecafe37f..7aa2475a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -1,25 +1,33 @@ package com.kok.kokapi.station.application.service; -import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import com.kok.kokcore.station.application.port.out.ReadStationsPort; -import com.kok.kokcore.station.application.port.out.SaveRoutePort; -import com.kok.kokcore.station.application.port.out.SaveStationsPort; +import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokcore.station.application.port.out.*; import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; import com.kok.kokcore.station.application.usecase.SaveStationUseCase; import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; + +import java.math.BigDecimal; +import java.util.*; + import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.Point; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -public class StationService implements SaveStationUseCase { +@Slf4j +public class StationService implements SaveStationUseCase, RecommendStationUseCase { private final LoadStationsPort loadStationsPort; private final SaveStationsPort saveStationsPort; private final ReadStationsPort readStationsPort; private final SaveRoutePort saveRoutePort; + private final RetrieveStationsPort retrieveStationsPort; + private final PointConverter pointConverter; @Override @Transactional @@ -30,4 +38,114 @@ public void saveStations() { saveRoutePort.saveRoutes(stationRouteDtos.toRoutesByStations(stations)); } } + + @Override + public List recommendStations(Point centroid) { + double dist = 100; + List stations = List.of(); + + // ์ตœ๋Œ€ ํƒ์ƒ‰ ๊ฑฐ๋ฆฌ ์ œํ•œ 10km + int VOTE_NUM = 2; + while (dist < 10000) { + stations = retrieveStationsPort.retrieveInRangeStations(centroid, dist); + log.info("Counter : " + stations.size() + " Dist : " + dist); + if (stations.size() >= VOTE_NUM) { + break; + } + dist *= 1.5; + } + + // ์—ญ์„ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + if (stations.isEmpty()) { + throw new RuntimeException("๋ฒ”์œ„ ๋‚ด์— ์ง€ํ•˜์ฒ ์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค."); + } + + // ๊ฑฐ๋ฆฌ ๋ฐ ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ํ™•๋ฅ  ๊ณ„์‚ฐ + Map probabilityMap = calculateProbabilities(stations, centroid); + + // ํ™•๋ฅ ์— ๋”ฐ๋ผ ๋žœ๋ค์œผ๋กœ ์ƒ์œ„ 2๊ฐœ ์ง€ํ•˜์ฒ  ์„ ํƒ + return selectTopStations(probabilityMap, VOTE_NUM); + } + + private Map calculateProbabilities(List stations, Point centroid) { + Map weightedDistances = new HashMap<>(); + double distanceSum = 0; + double totalPriority = 0; + + for (Station station : stations) { + double distance = calculateDistance(centroid, station); + if (distance == 0) distance = Double.MIN_VALUE; // 0 ๊ฑฐ๋ฆฌ ๋ฐฉ์ง€ + + double weight = (1 / distance) * station.getPriority(); + weightedDistances.put(station, weight); + distanceSum += (1 / distance); + totalPriority += station.getPriority(); + } + + // ํ™•๋ฅ  ๊ณ„์‚ฐ + Map probabilityMap = new HashMap<>(); + for (Map.Entry entry : weightedDistances.entrySet()) { + probabilityMap.put(entry.getKey(), entry.getValue() / (distanceSum * totalPriority)); + } + + return probabilityMap; + } + + private List selectTopStations(Map probabilityMap, int count) { + List selectedStations = new ArrayList<>(); + Random random = new Random(); + + while (selectedStations.size() < count && !probabilityMap.isEmpty()) { + double rand = random.nextDouble(); + double cumulativeProbability = 0; + Station selectedStation = null; + + for (Map.Entry entry : probabilityMap.entrySet()) { + cumulativeProbability += entry.getValue(); + if (rand <= cumulativeProbability) { + selectedStation = entry.getKey(); + break; + } + } + + if (selectedStation != null) { + selectedStations.add(selectedStation); + probabilityMap.remove(selectedStation); + } + } + + return selectedStations; + } + +// //์ •๊ตํ•œ ๋ฏธํ„ฐ ๊ฑฐ๋ฆฌ (Haversine) ๊ณ„์‚ฐ +// private double calculateDistance(Point p1, Station station) { +// Pair coordinates = pointConverter.toCoordinates(p1); +// double lat1 = coordinates.getFirst().doubleValue(); +// double lon1 = coordinates.getSecond().doubleValue(); +// double lat2 = station.getLatitude().doubleValue(); +// double lon2 = station.getLongitude().doubleValue(); +// +// // Haversine ๊ณต์‹ ์‚ฌ์šฉ (๋‹จ์œ„: ๋ฏธํ„ฐ) +// double R = 6371000; +// double dLat = Math.toRadians(lat2 - lat1); +// double dLon = Math.toRadians(lon2 - lon1); +// double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + +// Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * +// Math.sin(dLon / 2) * Math.sin(dLon / 2); +// double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +// +// return R * c; // ๊ฒฐ๊ณผ: ๋ฏธํ„ฐ ๋‹จ์œ„ ๊ฑฐ๋ฆฌ +// } + + // ์œ ํด๋ฆฌ๋“œ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜. + private double calculateDistance(Point p1, Station station) { + Pair coordinates = pointConverter.toCoordinates(p1); + double lat1 = coordinates.getFirst().doubleValue(); + double lon1 = coordinates.getSecond().doubleValue(); + double lat2 = station.getLatitude().doubleValue(); + double lon2 = station.getLongitude().doubleValue(); + + // ์œ ํด๋ฆฌ๋“œ ๊ฑฐ๋ฆฌ ๊ณต์‹ ์ ์šฉ + return Math.sqrt(Math.pow(lat2 - lat1, 2) + Math.pow(lon2 - lon1, 2)); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java similarity index 89% rename from kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUsecase.java rename to kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java index 44445b80..dd3fa308 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; -public interface CreateLocationUsecase { +public interface CreateLocationUseCase { Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude); Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUsecase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUseCase.java similarity index 87% rename from kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUsecase.java rename to kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUseCase.java index 7ac575a2..3d0b7dd7 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUseCase.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; -public interface ReadCentroidUsecase { +public interface ReadCentroidUseCase { Point readCentroid(String uuid); Pair readCentroidCoordinates(String uuid); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java similarity index 89% rename from kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java rename to kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java index 330bda68..4a91e01d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java @@ -4,7 +4,7 @@ import java.util.List; -public interface ReadLocationUsecase { +public interface ReadLocationUseCase { Location readLocation(String uuid, Integer memberId); List readLocations(String uuid); List readInsideConvexHull(String uuid); diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java similarity index 81% rename from kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java rename to kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java index 11ed831a..01e098dd 100644 --- a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUsecase.java +++ b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java @@ -1,6 +1,6 @@ package com.kok.kokcore.public_transfortation.usecase; -public interface RetrievePublicTransportationUsecase { +public interface RetrievePublicTransportationUseCase { String retrievePublicTransportation(Long stationId, String UUID, Integer memberId); String retrieveComplexPublicTransportation(Long stationId, String UUID, Integer memberId); diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java index 245ebcfe..f0ca5677 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java @@ -1,9 +1,12 @@ package com.kok.kokcore.station.application.port.out; import com.kok.kokcore.station.domain.entity.Station; +import org.locationtech.jts.geom.Point; +import java.util.List; import java.util.Optional; public interface RetrieveStationsPort { Optional retrieveStation(Long stationId); + List retrieveInRangeStations(Point centroid, double dist); } From 787c6eefb69a61e36f4e4d1e18d841a71bbc9e77 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Mar 2025 03:43:01 +0900 Subject: [PATCH 092/163] :sparkles: feat: add station recommend added --- .github/workflows/kok-TEST.yml | 53 +++++++++++++++++++ .../response/RecommendedStationResponse.java | 20 +++++++ .../adapter/in/web/StationController.java | 38 +++++++++++++ .../out/persistence/RouteRepository.java | 12 +++++ .../application/service/RouteService.java | 22 ++++++++ .../port/out/RetrieveRoutePort.java | 10 ++++ .../usecase/RecommendStationUseCase.java | 11 ++++ .../usecase/RetrieveRouteUseCase.java | 10 ++++ 8 files changed, 176 insertions(+) create mode 100644 .github/workflows/kok-TEST.yml create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java diff --git a/.github/workflows/kok-TEST.yml b/.github/workflows/kok-TEST.yml new file mode 100644 index 00000000..24604aca --- /dev/null +++ b/.github/workflows/kok-TEST.yml @@ -0,0 +1,53 @@ +name: kok-TEST (on container env) + +on: + workflow_dispatch: + + pull_request: + branches: + - develop + - main + push: + branches: + - develop + - main + +jobs: + test: + name: Test-Auth + runs-on: ubuntu-latest + services: + redis: + image: redis:7.0 + container_name: my-redis + ports: + - "6379:6379" + env: + TZ: "Asia/Seoul" + + mysql: + image: mysql:8.4 + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${{secrets.Test_PW}} + MYSQL_DATABASE: ${{secrets.Test_DB}} + MYSQL_ROOT_USER: ${{secrets.Test_User}} + +steps: + - name: Checkout Source + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: '21' + + - name: Grant execute permission to gradlew + run: chmod +x ./gradlew + working-directory: ${{ env.working-directory }} + + - name: Test with gradle + run: ./gradlew test + working-directory: ${{ env.working-directory }} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java new file mode 100644 index 00000000..460ca765 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java @@ -0,0 +1,20 @@ +package com.kok.kokapi.station.adapter.in.dto.response; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; + +import java.util.List; +import java.util.stream.Collectors; + +public record RecommendedStationResponse( + + List routes, + Station station +) { + public static RecommendedStationResponse of(Station station, List routes) { + return new RecommendedStationResponse( + routes.stream().map(Route::getName).collect(Collectors.toList()), + station + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java new file mode 100644 index 00000000..4948f7a5 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -0,0 +1,38 @@ +package com.kok.kokapi.station.adapter.in.web; + +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.annotion.V1Controller; +import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; +import com.kok.kokcore.location.usecase.ReadCentroidUseCase; +import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; +import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@V1Controller +@RequiredArgsConstructor +public class StationController { + + private final RecommendStationUseCase recommendStationUseCase; + private final ReadCentroidUseCase readCentroidUsecase; + private final RetrieveRouteUseCase retrieveRouteUseCase; + + @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ์ถ”์ฒœ", description = "Recommend subway stations based on the user's location.") + @GetMapping("/stations/recommend/{uuid}") + public ResponseEntity>> recommendStations(@PathVariable String uuid) { + Point centroid = readCentroidUsecase.readCentroid(uuid); + + List recommendedStations = recommendStationUseCase.recommendStations(centroid).stream() + .map(station -> RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) + .toList(); + + return ResponseEntity.ok(ApiResponseDto.success(recommendedStations)); + } + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java new file mode 100644 index 00000000..6f04c243 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java @@ -0,0 +1,12 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface RouteRepository extends JpaRepository { + + List findAllByStationOrderByName(Station station); +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java new file mode 100644 index 00000000..56eb194e --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java @@ -0,0 +1,22 @@ +package com.kok.kokapi.station.application.service; + +import com.kok.kokcore.station.application.port.out.RetrieveRoutePort; +import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RouteService implements RetrieveRouteUseCase { + + private final RetrieveRoutePort retrieveRoutePort; + + @Override + public List retrieveRoutes(Station station) { + return retrieveRoutePort.retrieveRoutes(station); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java new file mode 100644 index 00000000..4fd0652c --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.station.application.port.out; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; + +import java.util.List; + +public interface RetrieveRoutePort { + List retrieveRoutes(Station station); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java new file mode 100644 index 00000000..1b83555d --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java @@ -0,0 +1,11 @@ +package com.kok.kokcore.station.application.usecase; + +import com.kok.kokcore.station.domain.entity.Station; +import org.locationtech.jts.geom.Point; + +import java.util.List; + +public interface RecommendStationUseCase { + + List recommendStations(Point centroid); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java new file mode 100644 index 00000000..56488848 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.station.application.usecase; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; + +import java.util.List; + +public interface RetrieveRouteUseCase { + List retrieveRoutes(Station station); +} From b7c8ad353b685b22d6877465a2c6ed12c8e1533a Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Mar 2025 03:49:26 +0900 Subject: [PATCH 093/163] :coffin: chore: remove test workflow --- .github/workflows/kok-TEST.yml | 53 ---------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 .github/workflows/kok-TEST.yml diff --git a/.github/workflows/kok-TEST.yml b/.github/workflows/kok-TEST.yml deleted file mode 100644 index 24604aca..00000000 --- a/.github/workflows/kok-TEST.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: kok-TEST (on container env) - -on: - workflow_dispatch: - - pull_request: - branches: - - develop - - main - push: - branches: - - develop - - main - -jobs: - test: - name: Test-Auth - runs-on: ubuntu-latest - services: - redis: - image: redis:7.0 - container_name: my-redis - ports: - - "6379:6379" - env: - TZ: "Asia/Seoul" - - mysql: - image: mysql:8.4 - ports: - - "3306:3306" - environment: - MYSQL_ROOT_PASSWORD: ${{secrets.Test_PW}} - MYSQL_DATABASE: ${{secrets.Test_DB}} - MYSQL_ROOT_USER: ${{secrets.Test_User}} - -steps: - - name: Checkout Source - uses: actions/checkout@v4 - - - name: Set up JDK 21 - uses: actions/setup-java@v2 - with: - distribution: 'zulu' - java-version: '21' - - - name: Grant execute permission to gradlew - run: chmod +x ./gradlew - working-directory: ${{ env.working-directory }} - - - name: Test with gradle - run: ./gradlew test - working-directory: ${{ env.working-directory }} \ No newline at end of file From 3aa97527fd6d387518d64c58fdda6ecfaa508abd Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Mar 2025 14:33:44 +0900 Subject: [PATCH 094/163] :sparkles: add cache for station recommendation --- .../config/redis/out/RedisCacheConfig.java | 71 +++++++++++++++++++ .../RedisPublicTransportationCacheConfig.java | 39 ---------- .../TmapPublicTransportationService.java | 4 +- .../adapter/in/web/StationController.java | 2 +- .../application/service/StationService.java | 4 +- .../usecase/RecommendStationUseCase.java | 3 +- 6 files changed, 79 insertions(+), 44 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java new file mode 100644 index 00000000..27d096b3 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java @@ -0,0 +1,71 @@ +package com.kok.kokapi.config.redis.out; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisCacheConfig { + + @Bean("cacheManager") + @Primary + public CacheManager defaultCacheManager(RedisConnectionFactory redisConnectionFactory){ + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .build(); + } + + @Bean("stationCacheManager") + public CacheManager stationCacheManager(RedisConnectionFactory redisConnectionFactory) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.activateDefaultTyping( + LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL + ); // ํƒ€์ž… ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜์—ฌ ์—ญ์ง๋ ฌํ™”ํ•  ๋•Œ ์›๋ณธ ํƒ€์ž…์„ ์œ ์ง€ + + // Jackson Serializer ์„ค์ • + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + // RedisCacheConfiguration ์„ค์ • + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) + .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 3์ผ + + return RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); + } + + @Bean("publicTransportationCacheManager") + public CacheManager publicTransportationCacheManager(RedisConnectionFactory redisConnectionFactory) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.deactivateDefaultTyping(); + + GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + // RedisCacheConfiguration ์„ค์ • + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) + .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java deleted file mode 100644 index 5f7bcb85..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisPublicTransportationCacheConfig.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.kok.kokapi.config.redis.out; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import java.time.Duration; - -@EnableCaching -@Configuration -public class RedisPublicTransportationCacheConfig { - - @Bean - public CacheManager contentCacheManager(RedisConnectionFactory redisConnectionFactory) { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.deactivateDefaultTyping(); - - GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); - - // RedisCacheConfiguration ์„ค์ • - RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) - .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ - - return RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration) - .build(); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 065ba1c6..bcd65522 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -30,7 +30,7 @@ public class TmapPublicTransportationService implements RetrievePublicTransporta private final ObjectMapper objectMapper; - @Cacheable(value = "sub", cacheManager = "contentCacheManager", key = "'PTSubCache:' + #stationId + '-' + #UUID + '-' + #memberId") + @Cacheable(value = "sub", cacheManager = "publicTransportationCacheManager", key = "'PTSubCache:' + #stationId + '-' + #UUID + '-' + #memberId") @Override public String retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute(stationId, UUID, memberId); @@ -41,7 +41,7 @@ public String retrievePublicTransportation(Long stationId, String UUID, Integer } } - @Cacheable(value = "complex", cacheManager = "contentCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #UUID + '-' + #memberId") + @Cacheable(value = "complex", cacheManager = "publicTransportationCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #UUID + '-' + #memberId") @Override public String retrieveComplexPublicTransportation(Long stationId, String UUID, Integer memberId) { TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute(stationId, UUID, memberId); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index 4948f7a5..be6453cc 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -28,7 +28,7 @@ public class StationController { public ResponseEntity>> recommendStations(@PathVariable String uuid) { Point centroid = readCentroidUsecase.readCentroid(uuid); - List recommendedStations = recommendStationUseCase.recommendStations(centroid).stream() + List recommendedStations = recommendStationUseCase.recommendStations(centroid, uuid).stream() .map(station -> RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) .toList(); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index 7aa2475a..8ed8729d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -13,6 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.locationtech.jts.geom.Point; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,7 +41,8 @@ public void saveStations() { } @Override - public List recommendStations(Point centroid) { + @Cacheable(value = "recommendStations",cacheManager = "stationCacheManager", key = "#uuid") + public List recommendStations(Point centroid, String uuid) { double dist = 100; List stations = List.of(); diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java index 1b83555d..e7ce3328 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java @@ -5,7 +5,8 @@ import java.util.List; + public interface RecommendStationUseCase { - List recommendStations(Point centroid); + List recommendStations(Point centroid, String uuid); } From 418876cc47920386653cd7ec872095aa88ad0fe5 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Mar 2025 19:44:50 +0900 Subject: [PATCH 095/163] :recycle: refactor: move Centroid processing from controller to service layer --- .../application/service/LocationCommandService.java | 2 +- .../in/dto/response/RecommendedStationResponse.java | 3 +-- .../station/adapter/in/web/StationController.java | 6 +----- .../station/application/service/StationService.java | 13 ++++++++----- .../usecase/RecommendStationUseCase.java | 3 +-- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 398b2ada..0c01f8dd 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -30,7 +30,7 @@ public Location createLocation(String uuid, Integer memberId, BigDecimal latitud @Transactional public Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { Location location = readLocationPort.findLocationByUuidAndMemberId(uuid, memberId) - .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); location.changePoint(newPoint); return location; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java index 460ca765..e1925816 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java @@ -4,7 +4,6 @@ import com.kok.kokcore.station.domain.entity.Station; import java.util.List; -import java.util.stream.Collectors; public record RecommendedStationResponse( @@ -13,7 +12,7 @@ public record RecommendedStationResponse( ) { public static RecommendedStationResponse of(Station station, List routes) { return new RecommendedStationResponse( - routes.stream().map(Route::getName).collect(Collectors.toList()), + routes.stream().map(Route::getName).toList(), station ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index be6453cc..25d2d392 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -3,12 +3,10 @@ import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; -import com.kok.kokcore.location.usecase.ReadCentroidUseCase; import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -import org.locationtech.jts.geom.Point; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -20,15 +18,13 @@ public class StationController { private final RecommendStationUseCase recommendStationUseCase; - private final ReadCentroidUseCase readCentroidUsecase; private final RetrieveRouteUseCase retrieveRouteUseCase; @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ์ถ”์ฒœ", description = "Recommend subway stations based on the user's location.") @GetMapping("/stations/recommend/{uuid}") public ResponseEntity>> recommendStations(@PathVariable String uuid) { - Point centroid = readCentroidUsecase.readCentroid(uuid); - List recommendedStations = recommendStationUseCase.recommendStations(centroid, uuid).stream() + List recommendedStations = recommendStationUseCase.recommendStations(uuid).stream() .map(station -> RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) .toList(); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index 8ed8729d..08d3c345 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -1,6 +1,7 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokcore.location.application.port.out.ReadCentroidPort; import com.kok.kokcore.station.application.port.out.*; import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; @@ -26,6 +27,7 @@ public class StationService implements SaveStationUseCase, RecommendStationUseCa private final LoadStationsPort loadStationsPort; private final SaveStationsPort saveStationsPort; private final ReadStationsPort readStationsPort; + private final ReadCentroidPort readCentroidPort; private final SaveRoutePort saveRoutePort; private final RetrieveStationsPort retrieveStationsPort; private final PointConverter pointConverter; @@ -42,16 +44,17 @@ public void saveStations() { @Override @Cacheable(value = "recommendStations",cacheManager = "stationCacheManager", key = "#uuid") - public List recommendStations(Point centroid, String uuid) { + public List recommendStations(String uuid) { + Point centroid = readCentroidPort.findCentroidByUuid(uuid); + int RECOMMEND_NUM = 2; double dist = 100; List stations = List.of(); // ์ตœ๋Œ€ ํƒ์ƒ‰ ๊ฑฐ๋ฆฌ ์ œํ•œ 10km - int VOTE_NUM = 2; while (dist < 10000) { stations = retrieveStationsPort.retrieveInRangeStations(centroid, dist); - log.info("Counter : " + stations.size() + " Dist : " + dist); - if (stations.size() >= VOTE_NUM) { + log.info("Counter : {} Dist : {}", stations.size(), dist); + if (stations.size() >= RECOMMEND_NUM) { break; } dist *= 1.5; @@ -66,7 +69,7 @@ public List recommendStations(Point centroid, String uuid) { Map probabilityMap = calculateProbabilities(stations, centroid); // ํ™•๋ฅ ์— ๋”ฐ๋ผ ๋žœ๋ค์œผ๋กœ ์ƒ์œ„ 2๊ฐœ ์ง€ํ•˜์ฒ  ์„ ํƒ - return selectTopStations(probabilityMap, VOTE_NUM); + return selectTopStations(probabilityMap, RECOMMEND_NUM); } private Map calculateProbabilities(List stations, Point centroid) { diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java index e7ce3328..c8328eb8 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java @@ -1,12 +1,11 @@ package com.kok.kokcore.station.application.usecase; import com.kok.kokcore.station.domain.entity.Station; -import org.locationtech.jts.geom.Point; import java.util.List; public interface RecommendStationUseCase { - List recommendStations(Point centroid, String uuid); + List recommendStations(String uuid); } From 9ed21db2321caeee553725e16d414a159e95fa03 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 13 Mar 2025 23:49:40 +0900 Subject: [PATCH 096/163] :recycle: refactor: change lon, lat order --- .../centroid/adapter/in/dto/response/CentroidResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java index 7cda84c1..f34491b8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java @@ -8,7 +8,7 @@ public record CentroidResponse( BigDecimal latitude, BigDecimal longitude ) { - public static CentroidResponse of(String uuid, BigDecimal latitude, BigDecimal longitude) { + public static CentroidResponse of(String uuid, BigDecimal longitude, BigDecimal latitude) { return new CentroidResponse(uuid, latitude.setScale(6, RoundingMode.HALF_UP), longitude.setScale(6,RoundingMode.HALF_UP)); From e6c2bdebca1d43caf847b6e24f1f9e1fdc42fa94 Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Tue, 18 Mar 2025 23:38:45 +0900 Subject: [PATCH 097/163] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20[Fix/appointment-r?= =?UTF-8?q?oom]=20update=20room=20response=20and=20change=20new=20profile?= =?UTF-8?q?=20images=20=20(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :recycle: refactor: add memberUUID * :recycle: refactor: add memberUUID * :recycle: refactor: add memberUUID * :recycle: refactor: refactoring code review * :recycle: refactor: refactoring code review --- .../in/dto/request/CreateRoomRequest.java | 2 + .../in/dto/response/JoinRoomResponse.java | 10 ++++- .../in/dto/response/MemberResponse.java | 7 ++- .../in/dto/response/RoomCreateResponse.java | 12 ++--- .../in/dto/response/RoomMembersResponse.java | 1 + .../room/adapter/in/web/RoomController.java | 42 ++++++++++++------ .../adapter/in/web/RoomProfileController.java | 16 ------- .../RoomParticipantQueryAdapter.java | 44 ------------------- .../service/RandomProfileService.java | 4 +- .../service/RoomParticipantService.java | 13 +----- .../port/out/LoadRoomParticipantsPort.java | 9 ---- .../com/kok/kokcore/room/domain/Member.java | 11 ++++- .../room/usecase/GetMemberProfileUseCase.java | 9 ---- 13 files changed, 63 insertions(+), 117 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java index 03c5cf38..54e42dd1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java @@ -1,6 +1,7 @@ package com.kok.kokapi.room.adapter.in.dto.request; +import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -11,6 +12,7 @@ public record CreateRoomRequest( String roomName, @Min(value = 2, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Max(value = 15, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ๋Œ€ 15๋ช…๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") Integer capacity, @NotBlank(message = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java index 43203c41..244859e1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java @@ -1,4 +1,10 @@ package com.kok.kokapi.room.adapter.in.dto.response; -public record JoinRoomResponse(int participantCount, int memberId) { -} +public record JoinRoomResponse( + String id, + String profile, + String nickname, + int participantCount, + int nonParticipantCount +) { } + diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java index f6f4df08..922e5ac7 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java @@ -4,11 +4,16 @@ import com.kok.kokcore.room.domain.vo.MemberRole; public record MemberResponse( + String id, String nickname, String profile, MemberRole role ) { public static MemberResponse from(Member member) { - return new MemberResponse(member.getNickname(), member.getProfile(), member.getRole()); + return new MemberResponse( + member.getMemberId(), + member.getNickname(), + member.getProfile(), + member.getRole()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java index 0361af08..3894841c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java @@ -7,17 +7,17 @@ public record RoomCreateResponse( String roomName, int capacity, MemberResponse member, - int memberId, - int participantCount + int participantCount, + int nonParticipantCount ) { - public static RoomCreateResponse from(Room room, int memberId, int participantCount) { + public static RoomCreateResponse from(Room room, int participantCount, int nonParticipantCount) { return new RoomCreateResponse( room.getId(), room.getRoomName(), room.getCapacity(), MemberResponse.from(room.getMember()), - memberId, - participantCount + participantCount, + nonParticipantCount ); } -} \ No newline at end of file +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java index 09057083..75339512 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java @@ -3,6 +3,7 @@ import com.kok.kokcore.room.domain.vo.MemberRole; public record RoomMembersResponse( + String memberId, String profile, String nickname, MemberRole role diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index 475454d0..d75bf3a3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -9,6 +9,7 @@ import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; import com.kok.kokcore.room.usecase.CreateRoomUseCase; import com.kok.kokcore.room.usecase.GetRoomUseCase; @@ -41,14 +42,17 @@ public ResponseEntity> getRoomDetail(@PathVar @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒ์„ฑ", description = "์ƒˆ๋กœ์šด ์•ฝ์†๋ฐฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms") public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { - var host = new Member(request.hostNickname(), request.hostProfile(), MemberRole.LEADER); - var room = createRoomUseCase.createRoom( + String nickname = request.hostNickname(); + String profile = request.hostProfile(); + Member host = new Member(nickname, profile, MemberRole.LEADER); + + Room room = createRoomUseCase.createRoom( request.roomName(), request.capacity(), host ); - var response = RoomCreateResponse.from(room, 1, 1); + RoomCreateResponse response = RoomCreateResponse.from(room, 1, room.getCapacity() - 1); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponseDto.success(response)); @@ -57,28 +61,38 @@ public ResponseEntity> createRoom(@Valid @Req @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์ฐธ์—ฌ์ž๋“ค์˜ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}/participants") public ResponseEntity>> getParticipants(@PathVariable String roomId) { - var room = getRoomUseCase.findRoomById(roomId); - var participants = getRoomUseCase.getParticipants(room.getId()); + Room room = getRoomUseCase.findRoomById(roomId); + List participants = getRoomUseCase.getParticipants(room.getId()); - var response = participants.stream() + List response = participants.stream() .map(member -> new RoomMembersResponse( + member.getMemberId(), member.getProfile(), member.getNickname(), member.getRole() )).toList(); + return ResponseEntity.ok(ApiResponseDto.success(response)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms/{roomId}/join") - public ResponseEntity> joinRoom(@PathVariable String roomId, - @Valid @RequestBody JoinRoomParticipantRequest request) { - - getRoomUseCase.findRoomById(roomId); - var participant = new Member(request.nickname(), request.profile(), MemberRole.FOLLOWER); - var participantCount = joinRoomUseCase.joinRoom(roomId, participant); - var memberId = participantCount; - JoinRoomResponse response = new JoinRoomResponse(participantCount, memberId); + public ResponseEntity> joinRoom(@PathVariable String roomId, @Valid @RequestBody JoinRoomParticipantRequest request) { + + Room room = getRoomUseCase.findRoomById(roomId); + + Member participant = new Member(request.nickname(), request.profile(), MemberRole.FOLLOWER); + int participantCount = joinRoomUseCase.joinRoom(roomId, participant); + int nonParticipantCount = room.getCapacity() - participantCount; + + JoinRoomResponse response = new JoinRoomResponse( + participant.getMemberId(), + participant.getProfile(), + participant.getNickname(), + participantCount, + nonParticipantCount + ); + return ResponseEntity.ok(ApiResponseDto.success(response)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java index d70d18be..041845e9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java @@ -5,21 +5,16 @@ import com.kok.kokapi.room.adapter.in.dto.response.RandomProfileResponse; import com.kok.kokcore.room.domain.Profile; import com.kok.kokcore.room.usecase.CreateRandomProfileUseCase; -import com.kok.kokcore.room.usecase.GetMemberProfileUseCase; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; - -import java.util.List; @V1Controller @RequiredArgsConstructor public class RoomProfileController { private final CreateRandomProfileUseCase createRandomProfileUseCase; - private final GetMemberProfileUseCase getMemberProfileUseCase; @Operation(summary = "๋žœ๋ค ํ”„๋กœํ•„ ๋ฐ ๋‹‰๋„ค์ž„ ์กฐํšŒ", description = "๋žœ๋ค์œผ๋กœ ์ƒ์„ฑํ•œ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€์™€ ๋‹‰๋„ค์ž„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/profile/random") @@ -32,15 +27,4 @@ public ResponseEntity> getRandomProfile() return ResponseEntity.ok(ApiResponseDto.success(response)); } - - @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž์˜ ํ”„๋กœํ•„ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž๊ฐ€ ๊ธฐ์กด์— ์ƒ์„ฑํ•œ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉฐ, ์—†์„ ๊ฒฝ์šฐ ๋นˆ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") - @GetMapping("/rooms/{roomId}/profiles") - public ResponseEntity>> getMemberProfiles(@PathVariable String roomId) { - List profiles = getMemberProfileUseCase.getProfilesByRoomId(roomId); - List responseList = profiles.stream() - .map(profile -> new RandomProfileResponse(profile.getImageUrl(), profile.getNickname())) - .toList(); - - return ResponseEntity.ok(ApiResponseDto.success(responseList)); - } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java deleted file mode 100644 index 86770935..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryAdapter.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.kok.kokapi.room.adapter.out.persistence; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.LoadRoomParticipantsPort; -import com.kok.kokcore.room.domain.Member; -import com.kok.kokcore.room.domain.Profile; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -@RequiredArgsConstructor -public class RoomParticipantQueryAdapter implements LoadRoomParticipantsPort { - - private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; - private final RedisTemplate redisTemplate; - private final ObjectMapper objectMapper; - - @Override - public List getProfilesByRoomId(String roomId) { - String key = PARTICIPANT_KEY_PREFIX + roomId; - List profileJson = redisTemplate.opsForList().range(key, 0, -1); - - if (profileJson == null || profileJson.isEmpty()) { - return List.of(); - } - - return profileJson.stream() - .map(this::deserializeProfile) - .toList(); - } - - private Profile deserializeProfile(String profileJson) { - try { - Member member = objectMapper.readValue(profileJson, Member.class); - return new Profile(member.getProfile(), member.getNickname()); - } catch (JsonProcessingException e) { - throw new RuntimeException("failed to deserialize profile"); - } - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java index 7f80cc99..86da3675 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java @@ -34,8 +34,8 @@ public Profile createProfile() { } private String getRandomImageUrl() { - int randomImageIndex = new Random().nextInt(9) + 1; - return objectStorageUrl + "/profile_default/" + randomImageIndex + ".svg"; + int randomImageIndex = new Random().nextInt(15) + 1; + return objectStorageUrl + "/profile_default/" + randomImageIndex + ".png"; } private String generateRandomNickname() { diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java index 02b3d690..81c4b800 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java @@ -1,22 +1,15 @@ package com.kok.kokapi.room.application.service; -import com.kok.kokcore.room.application.port.out.LoadRoomParticipantsPort; import com.kok.kokcore.room.application.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.domain.Member; -import com.kok.kokcore.room.domain.Profile; -import com.kok.kokcore.room.usecase.GetMemberProfileUseCase; import com.kok.kokcore.room.usecase.JoinRoomUseCase; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor -public class RoomParticipantService implements JoinRoomUseCase, GetMemberProfileUseCase { - - private final LoadRoomParticipantsPort loadRoomParticipantsPort; +public class RoomParticipantService implements JoinRoomUseCase { private final SaveRoomParticipantsPort saveRoomParticipantsPort; @Override @@ -24,8 +17,4 @@ public int joinRoom(String roomId, Member member) { return saveRoomParticipantsPort.joinRoom(roomId, member); } - @Override - public List getProfilesByRoomId(String roomId) { - return loadRoomParticipantsPort.getProfilesByRoomId(roomId); - } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java deleted file mode 100644 index c0a8380b..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantsPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.kok.kokcore.room.application.port.out; - -import com.kok.kokcore.room.domain.Profile; - -import java.util.List; - -public interface LoadRoomParticipantsPort { - List getProfilesByRoomId(String roomId); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java index cd57d779..873bf583 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java @@ -5,18 +5,25 @@ import lombok.Getter; import lombok.ToString; +import java.util.UUID; + @Getter @ToString @EqualsAndHashCode public class Member { + private final String memberId; private final String nickname; private final String profile; private final MemberRole role; public Member(String nickname, String profile, MemberRole role) { - if (nickname == null || nickname.trim().isEmpty()) { - throw new IllegalArgumentException("Nickname is required"); + if (nickname == null || nickname.isEmpty()) { + throw new IllegalArgumentException("Nickname is required."); + } + if (profile == null || profile.isEmpty()) { + throw new IllegalArgumentException("Profile is required."); } + this.memberId = UUID.randomUUID().toString(); this.nickname = nickname.trim(); this.profile = profile.trim(); this.role = role; diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java deleted file mode 100644 index f881da75..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetMemberProfileUseCase.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.kok.kokcore.room.usecase; - -import com.kok.kokcore.room.domain.Profile; - -import java.util.List; - -public interface GetMemberProfileUseCase { - List getProfilesByRoomId(String roomId); -} From c21d39dc6690259b37419e114949189afec877f6 Mon Sep 17 00:00:00 2001 From: minseokey Date: Wed, 19 Mar 2025 16:08:32 +0900 Subject: [PATCH 098/163] :recycle: refactor: change memberId Integer to String --- .../adapter/in/dto/request/LocationRequest.java | 2 +- .../adapter/in/dto/response/LocationResponse.java | 4 ++-- .../centroid/adapter/in/web/LocationController.java | 10 +++++----- .../out/persistence/LocationPersistenceAdapter.java | 10 ++++++++-- .../adapter/out/persistence/LocationRepository.java | 2 +- .../application/service/CentroidQueryService.java | 4 ++-- .../application/service/LocationCommandService.java | 4 ++-- .../application/service/LocationQueryService.java | 2 +- .../adapter/in/dto/RouteRequest.java | 2 +- .../out/external/PublicTransportationClient.java | 4 ++-- .../external/PublicTransportationComplexClient.java | 4 ++-- .../service/TmapPublicTransportationService.java | 4 ++-- .../station/adapter/in/web/StationController.java | 3 ++- .../out/persistence/RoutePersistenceAdapter.java | 2 ++ .../out/persistence/StationPersistenceAdapter.java | 3 +++ .../application/port/out/ReadLocationPort.java | 2 +- .../application/port/out/SaveLocationPort.java | 2 +- .../java/com/kok/kokcore/location/domain/Location.java | 4 ++-- .../location/usecase/CreateLocationUseCase.java | 4 ++-- ...adCentroidUseCase.java => LoadCentroidUseCase.java} | 2 +- .../kokcore/location/usecase/ReadLocationUseCase.java | 2 +- .../usecase/RetrievePublicTransportationUseCase.java | 4 ++-- 22 files changed, 46 insertions(+), 34 deletions(-) rename kok-core/src/main/java/com/kok/kokcore/location/usecase/{ReadCentroidUseCase.java => LoadCentroidUseCase.java} (87%) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index 92ee6f7d..fd247816 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -13,7 +13,7 @@ public record LocationRequest( String uuid, @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - Integer memberId, + String memberId, @NotNull(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index 92b0060d..1011f414 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -5,11 +5,11 @@ public record LocationResponse ( String uuid, - Integer memberId, + String memberId, BigDecimal latitude, BigDecimal longitude ){ - public static LocationResponse of(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { + public static LocationResponse of(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude) { return new LocationResponse(uuid, memberId, latitude.setScale(6, RoundingMode.HALF_UP), longitude.setScale(6, RoundingMode.HALF_UP)); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index 0c14a2fb..ac62daf2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -9,7 +9,7 @@ import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.usecase.CreateLocationUseCase; -import com.kok.kokcore.location.usecase.ReadCentroidUseCase; +import com.kok.kokcore.location.usecase.LoadCentroidUseCase; import com.kok.kokcore.location.usecase.ReadLocationUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -26,7 +26,7 @@ public class LocationController { private final CreateLocationUseCase createLocationUsecase; - private final ReadCentroidUseCase readCentroidUsecase; + private final LoadCentroidUseCase loadCentroidUsecase; private final ReadLocationUseCase readLocationUsecase; private final LocationMapper locationMapper; @@ -40,7 +40,7 @@ public ResponseEntity> createLocation(@Valid @R locationRequest.longitude() ); - Pair centroid = readCentroidUsecase.readCentroidCoordinates(locationRequest.uuid()); + Pair centroid = loadCentroidUsecase.readCentroidCoordinates(locationRequest.uuid()); return ResponseEntity.ok(ApiResponseDto.success( CentroidResponse.of(locationRequest.uuid(), centroid.getFirst(), centroid.getSecond()) @@ -51,7 +51,7 @@ public ResponseEntity> createLocation(@Valid @R @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its UUID") @GetMapping("/locations/centroid/{uuid}") public ResponseEntity> getCentroid(@PathVariable String uuid) { - Pair centroid = readCentroidUsecase.readCentroidCoordinates(uuid); + Pair centroid = loadCentroidUsecase.readCentroidCoordinates(uuid); return ResponseEntity.ok(ApiResponseDto.success( CentroidResponse.of(uuid, centroid.getFirst(), centroid.getSecond()) @@ -60,7 +60,7 @@ public ResponseEntity> getCentroid(@PathVariabl @Operation(summary = "์œ„์น˜ ์กฐํšŒ Basic", description = "Retrieve detailed information for a location using its UUID and member ID") @GetMapping("/locations/{uuid}/{memberId}") - public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable Integer memberId) { + public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable String memberId) { Location location = readLocationUsecase.readLocation(uuid, memberId); return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index 73aeb3f6..e1007d87 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -9,6 +9,7 @@ import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Repository; import org.locationtech.jts.io.WKTReader; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -19,26 +20,31 @@ public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocatio private final LocationRepository locationRepository; @Override - public Optional findLocationByUuidAndMemberId(String uuid, Integer memberId) { + @Transactional(readOnly = true) + public Optional findLocationByUuidAndMemberId(String uuid, String memberId) { return locationRepository.findLocationByUuidAndMemberId(uuid, memberId); } @Override + @Transactional(readOnly = true) public List findLocationsByUuid(String uuid) { return locationRepository.findLocationsByUuid(uuid); } @Override + @Transactional(readOnly = true) public List findInsideConvexHull(String uuid) { return locationRepository.findInsideConvexHull(uuid); } @Override + @Transactional(readOnly = true) public List findConvexHull(String uuid) { return locationRepository.findConvexHull(uuid); } @Override + @Transactional(readOnly = true) public Point findCentroidByUuid(String uuid) { String centroidWKT = locationRepository.findCentroidByUuid(uuid); // WKT ํ˜•์‹์œผ๋กœ ๋ฐ›์Œ if (centroidWKT == null) { @@ -54,7 +60,7 @@ public Point findCentroidByUuid(String uuid) { } @Override - public Location saveLocation(String uuid, Integer memberId, Point point) { + public Location saveLocation(String uuid, String memberId, Point point) { return locationRepository.save(new Location(uuid, memberId, point)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 1140573a..9cd4a32f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -28,7 +28,7 @@ SELECT ST_AsText( """, nativeQuery = true) String findCentroidByUuid(@Param("uuid") String uuid); - Optional findLocationByUuidAndMemberId(String uuid, Integer memberId); + Optional findLocationByUuidAndMemberId(String uuid, String memberId); List findLocationsByUuid(String uuid); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java index 914fa70a..17389e4f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -2,7 +2,7 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.location.application.port.out.ReadCentroidPort; -import com.kok.kokcore.location.usecase.ReadCentroidUseCase; +import com.kok.kokcore.location.usecase.LoadCentroidUseCase; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; @@ -12,7 +12,7 @@ @Service @RequiredArgsConstructor -public class CentroidQueryService implements ReadCentroidUseCase { +public class CentroidQueryService implements LoadCentroidUseCase { private final ReadCentroidPort readCentroidPort; private final PointConverter pointConverter; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 0c01f8dd..20517ccf 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -21,14 +21,14 @@ public class LocationCommandService implements CreateLocationUseCase { private final PointConverter pointConverter; @Override - public Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { + public Location createLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude) { Point point = pointConverter.fromCoordinates(latitude, longitude); return saveLocationPort.saveLocation(uuid, memberId, point); } @Override @Transactional - public Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude) { + public Location updateLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude) { Location location = readLocationPort.findLocationByUuidAndMemberId(uuid, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index 0d14b455..a3a4e628 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -15,7 +15,7 @@ public class LocationQueryService implements ReadLocationUseCase { private final ReadLocationPort readLocationPort; @Override - public Location readLocation(String uuid, Integer memberId) { + public Location readLocation(String uuid, String memberId) { return readLocationPort.findLocationByUuidAndMemberId(uuid, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java index 85087e92..78e25aaa 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java @@ -7,6 +7,6 @@ public record RouteRequest( @NotBlank(message = "UUID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") String UUID, @NotNull(message = "Member ID(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - Integer memberId + String memberId ) { } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java index 16099f43..e2610f09 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java @@ -58,7 +58,7 @@ private ClientHttpRequestFactory getRequestFactory() { .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String UUID, Integer memberId) { + public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String UUID, String memberId) { log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); return getClient().post() .body(buildRequestBody( @@ -85,7 +85,7 @@ private Map buildRequestBody(Pair userLo return requestBody; } - private Pair getUserLocation(String UUID, Integer memberId) { + private Pair getUserLocation(String UUID, String memberId) { Location userPoint = readLocationPort.findLocationByUuidAndMemberId(UUID, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java index a214f710..fd6cbd14 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java @@ -56,7 +56,7 @@ private ClientHttpRequestFactory getRequestFactory() { .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, String UUID, Integer memberId){ + public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, String UUID, String memberId){ log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); return getClient().post() .body(buildRequestBody( @@ -72,7 +72,7 @@ public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(L .body(TmapComplexPublicTransportationResponse.class); } - private Pair getUserLocation(String UUID, Integer memberId) { + private Pair getUserLocation(String UUID, String memberId) { Location userPoint = readLocationPort.findLocationByUuidAndMemberId(UUID, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index bcd65522..18da80c5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -32,7 +32,7 @@ public class TmapPublicTransportationService implements RetrievePublicTransporta @Cacheable(value = "sub", cacheManager = "publicTransportationCacheManager", key = "'PTSubCache:' + #stationId + '-' + #UUID + '-' + #memberId") @Override - public String retrievePublicTransportation(Long stationId, String UUID, Integer memberId) { + public String retrievePublicTransportation(Long stationId, String UUID, String memberId) { TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute(stationId, UUID, memberId); try { return objectMapper.writeValueAsString(parseTmapResponse(rawRoute)); @@ -43,7 +43,7 @@ public String retrievePublicTransportation(Long stationId, String UUID, Integer @Cacheable(value = "complex", cacheManager = "publicTransportationCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #UUID + '-' + #memberId") @Override - public String retrieveComplexPublicTransportation(Long stationId, String UUID, Integer memberId) { + public String retrieveComplexPublicTransportation(Long stationId, String UUID, String memberId) { TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute(stationId, UUID, memberId); try { return objectMapper.writeValueAsString(parseComplexTmapResponse(rawRoute)); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index 25d2d392..d650ffe6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -25,7 +25,8 @@ public class StationController { public ResponseEntity>> recommendStations(@PathVariable String uuid) { List recommendedStations = recommendStationUseCase.recommendStations(uuid).stream() - .map(station -> RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) + .map(station -> + RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) .toList(); return ResponseEntity.ok(ApiResponseDto.success(recommendedStations)); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java index 0a603034..07ed02fe 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java @@ -12,6 +12,7 @@ import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository @Slf4j @@ -52,6 +53,7 @@ private void batchInsertRoutes(List routes) { @Override + @Transactional(readOnly = true) public List retrieveRoutes(Station station) { return routeRepository.findAllByStationOrderByName(station); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 44f7557c..31f247a7 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -17,6 +17,7 @@ import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository @Slf4j @@ -66,11 +67,13 @@ public boolean hasNoStations() { @Override + @Transactional(readOnly = true) public Optional retrieveStation(Long stationId) { return stationRepository.findStationById(stationId); } @Override + @Transactional(readOnly = true) public List retrieveInRangeStations(Point centroid, double dist) { Pair lonLat = pointConverter.toCoordinates(centroid); return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), lonLat.getSecond(), dist); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java index c70871e3..c4043ca5 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java @@ -6,7 +6,7 @@ import java.util.Optional; public interface ReadLocationPort { - Optional findLocationByUuidAndMemberId(String uuid, Integer memberId); + Optional findLocationByUuidAndMemberId(String uuid, String memberId); List findLocationsByUuid(String uuid); List findInsideConvexHull(String uuid); List findConvexHull(String uuid); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java index 07a69186..a32a3dd7 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java @@ -4,5 +4,5 @@ import org.locationtech.jts.geom.Point; public interface SaveLocationPort { - Location saveLocation(String uuid, Integer memberId, Point point); + Location saveLocation(String uuid, String memberId, Point point); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index 11b18699..5eaf4814 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -25,13 +25,13 @@ public class Location { private String uuid; @Column(nullable = false) - private Integer memberId; + private String memberId; @Column(nullable = false, columnDefinition = "POINT SRID 4326") private Point location_point; - public Location(String uuid, Integer memberId, Point location_point) { + public Location(String uuid, String memberId, Point location_point) { this.uuid = uuid; this.memberId = memberId; this.location_point = location_point; diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java index dd3fa308..c54b72e7 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java @@ -6,6 +6,6 @@ public interface CreateLocationUseCase { - Location createLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude); - Location updateLocation(String uuid, Integer memberId, BigDecimal latitude, BigDecimal longitude); + Location createLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude); + Location updateLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java similarity index 87% rename from kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUseCase.java rename to kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java index 3d0b7dd7..62cd92c7 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadCentroidUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; -public interface ReadCentroidUseCase { +public interface LoadCentroidUseCase { Point readCentroid(String uuid); Pair readCentroidCoordinates(String uuid); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java index 4a91e01d..f74bfc3f 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java @@ -5,7 +5,7 @@ import java.util.List; public interface ReadLocationUseCase { - Location readLocation(String uuid, Integer memberId); + Location readLocation(String uuid, String memberId); List readLocations(String uuid); List readInsideConvexHull(String uuid); List readConvexHull(String uuid); diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java index 01e098dd..77e6f758 100644 --- a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java @@ -2,6 +2,6 @@ public interface RetrievePublicTransportationUseCase { - String retrievePublicTransportation(Long stationId, String UUID, Integer memberId); - String retrieveComplexPublicTransportation(Long stationId, String UUID, Integer memberId); + String retrievePublicTransportation(Long stationId, String UUID, String memberId); + String retrieveComplexPublicTransportation(Long stationId, String UUID, String memberId); } From 6347471c8b3d633d3607eed565780f42c9c45a2e Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 20 Mar 2025 01:55:39 +0900 Subject: [PATCH 099/163] :recycle: refactor: change uuid to room_id --- .../in/dto/request/LocationRequest.java | 4 +- .../in/dto/response/CentroidResponse.java | 6 +-- .../in/dto/response/LocationResponse.java | 6 +-- .../adapter/in/web/LocationController.java | 44 +++++++++---------- .../adapter/out/mapper/LocationMapper.java | 2 +- .../LocationPersistenceAdapter.java | 24 +++++----- .../out/persistence/LocationRepository.java | 22 +++++----- .../service/CentroidQueryService.java | 10 ++--- .../service/LocationCommandService.java | 8 ++-- .../service/LocationQueryService.java | 16 +++---- .../adapter/in/dto/RouteRequest.java | 4 +- .../web/PublicTransportationController.java | 4 +- .../external/PublicTransportationClient.java | 12 ++--- .../PublicTransportationComplexClient.java | 12 ++--- .../TmapPublicTransportationService.java | 12 ++--- .../adapter/in/web/StationController.java | 6 +-- .../application/service/StationService.java | 6 +-- .../main/resources/db/migration/V1__init.sql | 4 +- .../port/out/ReadCentroidPort.java | 2 +- .../port/out/ReadLocationPort.java | 8 ++-- .../port/out/SaveLocationPort.java | 2 +- .../kok/kokcore/location/domain/Location.java | 8 ++-- .../usecase/CreateLocationUseCase.java | 4 +- .../location/usecase/LoadCentroidUseCase.java | 4 +- .../location/usecase/ReadLocationUseCase.java | 8 ++-- .../RetrievePublicTransportationUseCase.java | 4 +- .../usecase/RecommendStationUseCase.java | 2 +- 27 files changed, 122 insertions(+), 122 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index fd247816..b32a21a5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -9,8 +9,8 @@ public record LocationRequest( - @NotBlank(message = "uuid๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String uuid, + @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String roomId, @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") String memberId, diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java index f34491b8..760059da 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java @@ -4,12 +4,12 @@ import java.math.RoundingMode; public record CentroidResponse( - String uuid, + String roomId, BigDecimal latitude, BigDecimal longitude ) { - public static CentroidResponse of(String uuid, BigDecimal longitude, BigDecimal latitude) { - return new CentroidResponse(uuid, + public static CentroidResponse of(String roomId, BigDecimal longitude, BigDecimal latitude) { + return new CentroidResponse(roomId, latitude.setScale(6, RoundingMode.HALF_UP), longitude.setScale(6,RoundingMode.HALF_UP)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index 1011f414..c30423c3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -4,13 +4,13 @@ import java.math.RoundingMode; public record LocationResponse ( - String uuid, + String roomId, String memberId, BigDecimal latitude, BigDecimal longitude ){ - public static LocationResponse of(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude) { - return new LocationResponse(uuid, memberId, + public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { + return new LocationResponse(roomId, memberId, latitude.setScale(6, RoundingMode.HALF_UP), longitude.setScale(6, RoundingMode.HALF_UP)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index ac62daf2..af659406 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -34,51 +34,51 @@ public class LocationController { @PostMapping("/locations") public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { createLocationUsecase.createLocation( - locationRequest.uuid(), + locationRequest.roomId(), locationRequest.memberId(), locationRequest.latitude(), locationRequest.longitude() ); - Pair centroid = loadCentroidUsecase.readCentroidCoordinates(locationRequest.uuid()); + Pair centroid = loadCentroidUsecase.readCentroidCoordinates(locationRequest.roomId()); return ResponseEntity.ok(ApiResponseDto.success( - CentroidResponse.of(locationRequest.uuid(), centroid.getFirst(), centroid.getSecond()) + CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) )); } // For Test - @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its UUID") - @GetMapping("/locations/centroid/{uuid}") - public ResponseEntity> getCentroid(@PathVariable String uuid) { - Pair centroid = loadCentroidUsecase.readCentroidCoordinates(uuid); + @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its roomId") + @GetMapping("/locations/centroid/{roomId}") + public ResponseEntity> getCentroid(@PathVariable String roomId) { + Pair centroid = loadCentroidUsecase.readCentroidCoordinates(roomId); return ResponseEntity.ok(ApiResponseDto.success( - CentroidResponse.of(uuid, centroid.getFirst(), centroid.getSecond()) + CentroidResponse.of(roomId, centroid.getFirst(), centroid.getSecond()) )); } - @Operation(summary = "์œ„์น˜ ์กฐํšŒ Basic", description = "Retrieve detailed information for a location using its UUID and member ID") - @GetMapping("/locations/{uuid}/{memberId}") - public ResponseEntity> getLocation(@PathVariable String uuid, @PathVariable String memberId) { - Location location = readLocationUsecase.readLocation(uuid, memberId); + @Operation(summary = "์œ„์น˜ ์กฐํšŒ Basic", description = "Retrieve detailed information for a location using its roomId and member ID") + @GetMapping("/locations/{roomId}/{memberId}") + public ResponseEntity> getLocation(@PathVariable String roomId, @PathVariable String memberId) { + Location location = readLocationUsecase.readLocation(roomId, memberId); return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); } - @Operation(summary = "์œ„์น˜์กฐํšŒ ConvexHull", description = "Retrieve the ConvexHull inside list, outside list of locations for a UUID") - @GetMapping("/locations/ConvH/{uuid}") - public ResponseEntity> getConvexHullLocations(@PathVariable String uuid){ - List convexHull = locationMapper.toResponseList(readLocationUsecase.readConvexHull(uuid)); - List inside = locationMapper.toResponseList(readLocationUsecase.readInsideConvexHull(uuid)); + @Operation(summary = "์œ„์น˜์กฐํšŒ ConvexHull", description = "Retrieve the ConvexHull inside list, outside list of locations for a roomId") + @GetMapping("/locations/ConvH/{roomId}") + public ResponseEntity> getConvexHullLocations(@PathVariable String roomId){ + List convexHull = locationMapper.toResponseList(readLocationUsecase.readConvexHull(roomId)); + List inside = locationMapper.toResponseList(readLocationUsecase.readInsideConvexHull(roomId)); return ResponseEntity.ok(ApiResponseDto.success(ConvexHullLocationResponse.of(convexHull, inside ))); } - @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a UUID") - @GetMapping("/locations/{uuid}") - public ResponseEntity>> getLocations(@PathVariable String uuid) { - List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(uuid)); + @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a roomId") + @GetMapping("/locations/{roomId}") + public ResponseEntity>> getLocations(@PathVariable String roomId) { + List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(roomId)); return ResponseEntity.ok(ApiResponseDto.success(responses)); } @@ -87,7 +87,7 @@ public ResponseEntity>> getLocations(@Path @PutMapping("/locations") public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.updateLocation( - locationRequest.uuid(), + locationRequest.roomId(), locationRequest.memberId(), locationRequest.latitude(), locationRequest.longitude() diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index 87cda80a..65abee45 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -20,7 +20,7 @@ public LocationMapper(PointConverter pointConverter) { public LocationResponse toResponse(Location location) { Pair coordinates = pointConverter.toCoordinates(location.getLocation_point()); return LocationResponse.of( - location.getUuid(), + location.getRoomId(), location.getMemberId(), coordinates.getFirst(), coordinates.getSecond() diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index e1007d87..12f85028 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -21,32 +21,32 @@ public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocatio @Override @Transactional(readOnly = true) - public Optional findLocationByUuidAndMemberId(String uuid, String memberId) { - return locationRepository.findLocationByUuidAndMemberId(uuid, memberId); + public Optional findLocationByRoomIdAndMemberId(String roomId, String memberId) { + return locationRepository.findLocationByRoomIdAndMemberId(roomId, memberId); } @Override @Transactional(readOnly = true) - public List findLocationsByUuid(String uuid) { - return locationRepository.findLocationsByUuid(uuid); + public List findLocationsByRoomId(String roomId) { + return locationRepository.findLocationsByRoomId(roomId); } @Override @Transactional(readOnly = true) - public List findInsideConvexHull(String uuid) { - return locationRepository.findInsideConvexHull(uuid); + public List findInsideConvexHull(String roomId) { + return locationRepository.findInsideConvexHull(roomId); } @Override @Transactional(readOnly = true) - public List findConvexHull(String uuid) { - return locationRepository.findConvexHull(uuid); + public List findConvexHull(String roomId) { + return locationRepository.findConvexHull(roomId); } @Override @Transactional(readOnly = true) - public Point findCentroidByUuid(String uuid) { - String centroidWKT = locationRepository.findCentroidByUuid(uuid); // WKT ํ˜•์‹์œผ๋กœ ๋ฐ›์Œ + public Point findCentroidByRoomId(String roomId) { + String centroidWKT = locationRepository.findCentroidByRoomId(roomId); // WKT ํ˜•์‹์œผ๋กœ ๋ฐ›์Œ if (centroidWKT == null) { return null; } @@ -60,7 +60,7 @@ public Point findCentroidByUuid(String uuid) { } @Override - public Location saveLocation(String uuid, String memberId, Point point) { - return locationRepository.save(new Location(uuid, memberId, point)); + public Location saveLocation(String roomId, String memberId, Point point) { + return locationRepository.save(new Location(roomId, memberId, point)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 9cd4a32f..458e2a7e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -9,7 +9,7 @@ public interface LocationRepository extends JpaRepository { - /* ํ•ด๋‹น ์ฟผ๋ฆฌ๋Š” ํŠน์ • UUID๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ„์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ํ•ด๋‹น ์œ„์น˜๋“ค์˜ ์ค‘์‹ฌ์ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + /* ํ•ด๋‹น ์ฟผ๋ฆฌ๋Š” ํŠน์ • roomId๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ„์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ํ•ด๋‹น ์œ„์น˜๋“ค์˜ ์ค‘์‹ฌ์ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ๋จผ์ €, SRID 4326 (์œ„๋„/๊ฒฝ๋„) ์ขŒํ‘œ๊ณ„๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ SRID 3857 (ํ‰๋ฉด ์ขŒํ‘œ๊ณ„)๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ณ€ํ™˜๋œ ์ขŒํ‘œ๋“ค์„ ST_Collect๋ฅผ ์ด์šฉํ•ด ํ•˜๋‚˜์˜ MULTIPOINT ํ˜•ํƒœ๋กœ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๋ณ‘ํ•ฉ๋œ MULTIPOINT์—์„œ ST_Centroid๋ฅผ ์‚ฌ์šฉํ•ด ์ค‘์‹ฌ์ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. @@ -24,33 +24,33 @@ SELECT ST_AsText( ST_Transform(location_point, 3857))),4326) ) FROM location - WHERE uuid = :uuid + WHERE room_id = :roomId """, nativeQuery = true) - String findCentroidByUuid(@Param("uuid") String uuid); + String findCentroidByRoomId(@Param("roomId") String roomId); - Optional findLocationByUuidAndMemberId(String uuid, String memberId); + Optional findLocationByRoomIdAndMemberId(String roomId, String memberId); - List findLocationsByUuid(String uuid); + List findLocationsByRoomId(String roomId); @Query(value = """ WITH ConvexHull AS ( SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull FROM location - WHERE uuid = :uuid + WHERE room_id = :roomId ) SELECT l.* FROM location l, ConvexHull ch - WHERE l.uuid = :uuid + WHERE l.room_id = :roomId AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) """, nativeQuery = true) - List findInsideConvexHull(@Param("uuid") String uuid); + List findInsideConvexHull(@Param("roomId") String roomId); @Query(value = """ WITH ConvexHull AS ( SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull, ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point))))) AS center FROM location - WHERE uuid = :uuid + WHERE room_id = :roomId ) SELECT l.*, ATAN2( @@ -58,10 +58,10 @@ SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS ST_X(ST_GeomFromText(ST_AsText(l.location_point))) - ST_X(ch.center) ) AS angle FROM location l, ConvexHull ch - WHERE l.uuid = :uuid + WHERE l.room_id = :roomId AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) ORDER BY angle """, nativeQuery = true) - List findConvexHull(@Param("uuid") String uuid); + List findConvexHull(@Param("roomId") String roomId); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java index 17389e4f..0619867c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -18,15 +18,15 @@ public class CentroidQueryService implements LoadCentroidUseCase { private final PointConverter pointConverter; @Override - public Point readCentroid(String uuid) { - return readCentroidPort.findCentroidByUuid(uuid); + public Point readCentroid(String roomId) { + return readCentroidPort.findCentroidByRoomId(roomId); } @Override - public Pair readCentroidCoordinates(String uuid) { - Point centroidPoint = readCentroidPort.findCentroidByUuid(uuid); + public Pair readCentroidCoordinates(String roomId) { + Point centroidPoint = readCentroidPort.findCentroidByRoomId(roomId); if (centroidPoint == null) { - throw new IllegalArgumentException("ํ•ด๋‹น UUID์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + throw new IllegalArgumentException("ํ•ด๋‹น roomId์— ๋Œ€ํ•œ ์ค‘์‹ฌ์ ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } return pointConverter.toCoordinates(centroidPoint); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 20517ccf..3480efe6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -21,15 +21,15 @@ public class LocationCommandService implements CreateLocationUseCase { private final PointConverter pointConverter; @Override - public Location createLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude) { + public Location createLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { Point point = pointConverter.fromCoordinates(latitude, longitude); - return saveLocationPort.saveLocation(uuid, memberId, point); + return saveLocationPort.saveLocation(roomId, memberId, point); } @Override @Transactional - public Location updateLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude) { - Location location = readLocationPort.findLocationByUuidAndMemberId(uuid, memberId) + public Location updateLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { + Location location = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); location.changePoint(newPoint); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index a3a4e628..3b496730 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -15,23 +15,23 @@ public class LocationQueryService implements ReadLocationUseCase { private final ReadLocationPort readLocationPort; @Override - public Location readLocation(String uuid, String memberId) { - return readLocationPort.findLocationByUuidAndMemberId(uuid, memberId) + public Location readLocation(String roomId, String memberId) { + return readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } @Override - public List readLocations(String uuid) { - return readLocationPort.findLocationsByUuid(uuid); + public List readLocations(String roomId) { + return readLocationPort.findLocationsByRoomId(roomId); } @Override - public List readInsideConvexHull(String uuid) { - return readLocationPort.findInsideConvexHull(uuid); + public List readInsideConvexHull(String roomId) { + return readLocationPort.findInsideConvexHull(roomId); } @Override - public List readConvexHull(String uuid) { - return readLocationPort.findConvexHull(uuid); + public List readConvexHull(String roomId) { + return readLocationPort.findConvexHull(roomId); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java index 78e25aaa..f4aad574 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java @@ -4,8 +4,8 @@ import jakarta.validation.constraints.NotNull; public record RouteRequest( - @NotBlank(message = "UUID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String UUID, + @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String roomId, @NotNull(message = "Member ID(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") String memberId ) { diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java index bf96fdaa..3b8f7725 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java @@ -25,7 +25,7 @@ public class PublicTransportationController { @PostMapping("/route/{stationId}") public ResponseEntity> getPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { try { - TmapPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.UUID(), routeRequest.memberId()) + TmapPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.roomId(), routeRequest.memberId()) ,TmapPublicTransportationParsedResponse.class); return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); } catch (JsonProcessingException e) { @@ -37,7 +37,7 @@ public ResponseEntity> ge @PostMapping("/route/complex/{stationId}") public ResponseEntity> getComplexPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { try { - TmapComplexPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrieveComplexPublicTransportation(stationId, routeRequest.UUID(), routeRequest.memberId()) + TmapComplexPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrieveComplexPublicTransportation(stationId, routeRequest.roomId(), routeRequest.memberId()) , TmapComplexPublicTransportationParsedResponse.class); return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); } catch (JsonProcessingException e) { diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java index e2610f09..89f89a39 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java @@ -58,11 +58,11 @@ private ClientHttpRequestFactory getRequestFactory() { .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String UUID, String memberId) { - log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); + public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String roomId, String memberId) { + log.info("Tmap api call : {}-{}-{}", stationId, roomId, memberId); return getClient().post() .body(buildRequestBody( - getUserLocation(UUID, memberId), + getUserLocation(roomId, memberId), getStation(stationId))) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { @@ -85,9 +85,9 @@ private Map buildRequestBody(Pair userLo return requestBody; } - private Pair getUserLocation(String UUID, String memberId) { - Location userPoint = readLocationPort.findLocationByUuidAndMemberId(UUID, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + private Pair getUserLocation(String roomId, String memberId) { + Location userPoint = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java index fd6cbd14..8bfd0cbf 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java @@ -56,11 +56,11 @@ private ClientHttpRequestFactory getRequestFactory() { .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, String UUID, String memberId){ - log.info("Tmap api call : {}-{}-{}", stationId, UUID, memberId); + public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, String roomId, String memberId){ + log.info("Tmap api call : {}-{}-{}", stationId, roomId, memberId); return getClient().post() .body(buildRequestBody( - getUserLocation(UUID, memberId), + getUserLocation(roomId, memberId), getStation(stationId))) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { @@ -72,9 +72,9 @@ public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(L .body(TmapComplexPublicTransportationResponse.class); } - private Pair getUserLocation(String UUID, String memberId) { - Location userPoint = readLocationPort.findLocationByUuidAndMemberId(UUID, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + private Pair getUserLocation(String roomId, String memberId) { + Location userPoint = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 18da80c5..9bba411e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -30,10 +30,10 @@ public class TmapPublicTransportationService implements RetrievePublicTransporta private final ObjectMapper objectMapper; - @Cacheable(value = "sub", cacheManager = "publicTransportationCacheManager", key = "'PTSubCache:' + #stationId + '-' + #UUID + '-' + #memberId") + @Cacheable(value = "sub", cacheManager = "publicTransportationCacheManager", key = "'PTSubCache:' + #stationId + '-' + #roomId + '-' + #memberId") @Override - public String retrievePublicTransportation(Long stationId, String UUID, String memberId) { - TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute(stationId, UUID, memberId); + public String retrievePublicTransportation(Long stationId, String roomId, String memberId) { + TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute(stationId, roomId, memberId); try { return objectMapper.writeValueAsString(parseTmapResponse(rawRoute)); } catch (JsonProcessingException e) { @@ -41,10 +41,10 @@ public String retrievePublicTransportation(Long stationId, String UUID, String m } } - @Cacheable(value = "complex", cacheManager = "publicTransportationCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #UUID + '-' + #memberId") + @Cacheable(value = "complex", cacheManager = "publicTransportationCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #roomId + '-' + #memberId") @Override - public String retrieveComplexPublicTransportation(Long stationId, String UUID, String memberId) { - TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute(stationId, UUID, memberId); + public String retrieveComplexPublicTransportation(Long stationId, String roomId, String memberId) { + TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute(stationId, roomId, memberId); try { return objectMapper.writeValueAsString(parseComplexTmapResponse(rawRoute)); } catch (JsonProcessingException e) { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index d650ffe6..cec4f5d9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -21,10 +21,10 @@ public class StationController { private final RetrieveRouteUseCase retrieveRouteUseCase; @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ์ถ”์ฒœ", description = "Recommend subway stations based on the user's location.") - @GetMapping("/stations/recommend/{uuid}") - public ResponseEntity>> recommendStations(@PathVariable String uuid) { + @GetMapping("/stations/recommend/{roomId}") + public ResponseEntity>> recommendStations(@PathVariable String roomId) { - List recommendedStations = recommendStationUseCase.recommendStations(uuid).stream() + List recommendedStations = recommendStationUseCase.recommendStations(roomId).stream() .map(station -> RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) .toList(); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index 08d3c345..68747ce2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -43,9 +43,9 @@ public void saveStations() { } @Override - @Cacheable(value = "recommendStations",cacheManager = "stationCacheManager", key = "#uuid") - public List recommendStations(String uuid) { - Point centroid = readCentroidPort.findCentroidByUuid(uuid); + @Cacheable(value = "recommendStations",cacheManager = "stationCacheManager", key = "#roomId") + public List recommendStations(String roomId) { + Point centroid = readCentroidPort.findCentroidByRoomId(roomId); int RECOMMEND_NUM = 2; double dist = 100; List stations = List.of(); diff --git a/kok-api/src/main/resources/db/migration/V1__init.sql b/kok-api/src/main/resources/db/migration/V1__init.sql index 6882fc10..4bddd843 100644 --- a/kok-api/src/main/resources/db/migration/V1__init.sql +++ b/kok-api/src/main/resources/db/migration/V1__init.sql @@ -23,7 +23,7 @@ create table location id bigint auto_increment primary key, member_id int not null, location_point point not null, - uuid varchar(255) not null, + room_id varchar(255) not null, constraint UKrgpajb4rsivb4gj9xn2qowgw6 - unique (uuid, member_id) + unique (room_id, member_id) ); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java index 9d54f509..f664cea5 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java @@ -3,5 +3,5 @@ import org.locationtech.jts.geom.Point; public interface ReadCentroidPort { - Point findCentroidByUuid(String uuid); + Point findCentroidByRoomId(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java index c4043ca5..caedb798 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java @@ -6,8 +6,8 @@ import java.util.Optional; public interface ReadLocationPort { - Optional findLocationByUuidAndMemberId(String uuid, String memberId); - List findLocationsByUuid(String uuid); - List findInsideConvexHull(String uuid); - List findConvexHull(String uuid); + Optional findLocationByRoomIdAndMemberId(String roomId, String memberId); + List findLocationsByRoomId(String roomId); + List findInsideConvexHull(String roomId); + List findConvexHull(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java index a32a3dd7..45656669 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java @@ -4,5 +4,5 @@ import org.locationtech.jts.geom.Point; public interface SaveLocationPort { - Location saveLocation(String uuid, String memberId, Point point); + Location saveLocation(String roomId, String memberId, Point point); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index 5eaf4814..2f041c44 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -12,7 +12,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "location" , uniqueConstraints = { - @UniqueConstraint(columnNames = {"uuid", "member_id"}) + @UniqueConstraint(columnNames = {"room_id", "member_id"}) }) public class Location { @@ -22,7 +22,7 @@ public class Location { private Long id; @Column(nullable = false) - private String uuid; + private String roomId; @Column(nullable = false) private String memberId; @@ -31,8 +31,8 @@ public class Location { private Point location_point; - public Location(String uuid, String memberId, Point location_point) { - this.uuid = uuid; + public Location(String roomId, String memberId, Point location_point) { + this.roomId = roomId; this.memberId = memberId; this.location_point = location_point; } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java index c54b72e7..fc3a1033 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java @@ -6,6 +6,6 @@ public interface CreateLocationUseCase { - Location createLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude); - Location updateLocation(String uuid, String memberId, BigDecimal latitude, BigDecimal longitude); + Location createLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude); + Location updateLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java index 62cd92c7..1c9e2ff3 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java @@ -6,7 +6,7 @@ import java.math.BigDecimal; public interface LoadCentroidUseCase { - Point readCentroid(String uuid); + Point readCentroid(String roomId); - Pair readCentroidCoordinates(String uuid); + Pair readCentroidCoordinates(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java index f74bfc3f..f719144b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java @@ -5,8 +5,8 @@ import java.util.List; public interface ReadLocationUseCase { - Location readLocation(String uuid, String memberId); - List readLocations(String uuid); - List readInsideConvexHull(String uuid); - List readConvexHull(String uuid); + Location readLocation(String roomId, String memberId); + List readLocations(String roomId); + List readInsideConvexHull(String roomId); + List readConvexHull(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java index 77e6f758..0cf1b74d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java @@ -2,6 +2,6 @@ public interface RetrievePublicTransportationUseCase { - String retrievePublicTransportation(Long stationId, String UUID, String memberId); - String retrieveComplexPublicTransportation(Long stationId, String UUID, String memberId); + String retrievePublicTransportation(Long stationId, String roomId, String memberId); + String retrieveComplexPublicTransportation(Long stationId, String roomId, String memberId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java index c8328eb8..84e8d225 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java @@ -7,5 +7,5 @@ public interface RecommendStationUseCase { - List recommendStations(String uuid); + List recommendStations(String roomId); } From 30ae41f609b2a0e8ce353ad74eeccce27e986527 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:28:39 +0900 Subject: [PATCH 100/163] =?UTF-8?q?=E2=9C=A8=20[Feature/share-link]=20Add?= =?UTF-8?q?=20checking=20location=20input=20termination=20logic=20on=20roo?= =?UTF-8?q?m=20detail=20API=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: fix: fix ddl type error * :recycle: refactor: change repository test from @DataJpaTest to @SpringBootTest(NONE) due to requirement of Redis dependency * :sparkles: feat: check if all memberIds exists in location * :white_check_mark: test: add redis database cleaner for test isolation * :sparkles: feat: implement counting participants port * :sparkles: feat: add deadline field and check if deadlinePassed * :recycle: refactor: refactor to count participants with location by room id * :recycle: refactor: remove unused method * :recycle: refactor: check if location input ended * :sparkles: feat: implement room facade service and getRoomDetail controller * :recycle: refactor: remove unused method * :recycle: refactor: remove unused method * :recycle: refactor: add eol * :recycle: refactor: refactor integration test * :recycle: refactor: remove unused response field * :recycle: refactor: refactor room integration test * :sparkles: feat: seperate api for reading room detail and checking if it is voteMode * :sparkles: feat: add createdDateTime field on room domain * :recycle: refactor: add try-with-resource * :bug: fix: fix integration test and fix key error --- kok-api/build.gradle | 1 + .../LocationPersistenceAdapter.java | 1 + .../out/persistence/LocationRepository.java | 75 +++++----- .../in/dto/response/RoomDetailResponse.java | 23 +-- .../in/dto/response/RoomStatusResponse.java | 5 + .../room/adapter/in/web/RoomController.java | 69 +++++---- .../RoomParticipantQueryRedisAdapter.java | 28 ++++ .../persistence/RoomQueryRedisAdapter.java | 5 +- .../out/persistence/RoomSaveRedisAdapter.java | 3 +- .../service/RoomFacadeService.java | 34 +++++ .../application/service/RoomQueryService.java | 15 +- .../main/resources/db/migration/V1__init.sql | 2 +- .../common/template/IntegrationTest.java | 28 ++++ .../common/template/RepositoryTest.java | 11 +- .../kokapi/common/template/ServiceTest.java | 8 +- .../kokapi/common/util/DatabaseCleaner.java | 6 + ...ion.java => DatabaseCleanerExtension.java} | 9 +- .../common/util/MySQLDatabaseCleaner.java | 2 +- .../common/util/RedisDatabaseCleaner.java | 29 ++++ .../kok/kokapi/config/DataJpaTestConfig.java | 10 -- ...fig.java => StationTestConfiguration.java} | 2 +- .../com/kok/kokapi/fixture/MemberFixture.java | 15 ++ .../com/kok/kokapi/fixture/PointFixture.java | 15 ++ .../com/kok/kokapi/fixture/RoomFixture.java | 11 ++ .../adapter/in/web/RoomIntegrationTest.java | 138 ++++++++++++++++++ .../RoomParticipantQueryRedisAdapterTest.java | 31 ++++ .../src/test/resources/application-test.yml | 2 +- .../port/out/LoadRoomParticipantPort.java | 6 + .../com/kok/kokcore/room/domain/Room.java | 23 ++- .../kokcore/room/usecase/GetRoomUseCase.java | 5 +- .../com/kok/kokcore/room/domain/RoomTest.java | 73 +++++++-- 31 files changed, 559 insertions(+), 126 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java rename kok-api/src/test/java/com/kok/kokapi/common/util/{MySQLDatabaseCleanerExtension.java => DatabaseCleanerExtension.java} (53%) create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java delete mode 100644 kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java rename kok-api/src/test/java/com/kok/kokapi/config/{ServiceTestConfig.java => StationTestConfiguration.java} (92%) create mode 100644 kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantPort.java diff --git a/kok-api/build.gradle b/kok-api/build.gradle index 6ca9ee2c..d55d20ab 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -39,6 +39,7 @@ dependencies { testImplementation 'org.testcontainers:mysql' testImplementation 'com.redis:testcontainers-redis' + testImplementation 'io.rest-assured:rest-assured:5.3.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index 12f85028..01bf4428 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -17,6 +17,7 @@ @Repository @RequiredArgsConstructor public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocationPort, ReadLocationPort { + private final LocationRepository locationRepository; @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 458e2a7e..78346a99 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -1,11 +1,11 @@ package com.kok.kokapi.centroid.adapter.out.persistence; import com.kok.kokcore.location.domain.Location; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; -import java.util.Optional; public interface LocationRepository extends JpaRepository { @@ -17,15 +17,15 @@ public interface LocationRepository extends JpaRepository { ์ตœ์ข…์ ์œผ๋กœ ST_AsText๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ WKT(Well-Known Text) ํฌ๋งท์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. */ @Query(value = """ - SELECT ST_AsText( - ST_Transform( - ST_Centroid( - ST_Collect( - ST_Transform(location_point, 3857))),4326) - ) - FROM location - WHERE room_id = :roomId - """, nativeQuery = true) + SELECT ST_AsText( + ST_Transform( + ST_Centroid( + ST_Collect( + ST_Transform(location_point, 3857))),4326) + ) + FROM location + WHERE room_id = :roomId + """, nativeQuery = true) String findCentroidByRoomId(@Param("roomId") String roomId); Optional findLocationByRoomIdAndMemberId(String roomId, String memberId); @@ -33,35 +33,34 @@ SELECT ST_AsText( List findLocationsByRoomId(String roomId); @Query(value = """ - WITH ConvexHull AS ( - SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull - FROM location - WHERE room_id = :roomId - ) - SELECT l.* - FROM location l, ConvexHull ch - WHERE l.room_id = :roomId - AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) - """, nativeQuery = true) + WITH ConvexHull AS ( + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull + FROM location + WHERE room_id = :roomId + ) + SELECT l.* + FROM location l, ConvexHull ch + WHERE l.room_id = :roomId + AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) + """, nativeQuery = true) List findInsideConvexHull(@Param("roomId") String roomId); @Query(value = """ - WITH ConvexHull AS ( - SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull, - ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point))))) AS center - FROM location - WHERE room_id = :roomId - ) - SELECT l.*, - ATAN2( - ST_Y(ST_GeomFromText(ST_AsText(l.location_point))) - ST_Y(ch.center), - ST_X(ST_GeomFromText(ST_AsText(l.location_point))) - ST_X(ch.center) - ) AS angle - FROM location l, ConvexHull ch - WHERE l.room_id = :roomId - AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) - ORDER BY angle - """, nativeQuery = true) + WITH ConvexHull AS ( + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull, + ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point))))) AS center + FROM location + WHERE room_id = :roomId + ) + SELECT l.*, + ATAN2( + ST_Y(ST_GeomFromText(ST_AsText(l.location_point))) - ST_Y(ch.center), + ST_X(ST_GeomFromText(ST_AsText(l.location_point))) - ST_X(ch.center) + ) AS angle + FROM location l, ConvexHull ch + WHERE l.room_id = :roomId + AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) + ORDER BY angle + """, nativeQuery = true) List findConvexHull(@Param("roomId") String roomId); - } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index df851571..51718e4e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -3,21 +3,22 @@ import com.kok.kokcore.room.domain.Room; public record RoomDetailResponse( - String id, - String roomName, - int capacity, - MemberResponse member + String id, + String roomName, + int nonParticipantCount, + boolean isFulled ) { - public static RoomDetailResponse from(Room room) { + + public static RoomDetailResponse of(Room room, int participantCount) { return new RoomDetailResponse( - room.getId(), - room.getRoomName(), - room.getCapacity(), - getLeaderResponse(room) + room.getId(), + room.getRoomName(), + room.getCapacity() - participantCount, + isFulled(room, participantCount) ); } - private static MemberResponse getLeaderResponse(Room room) { - return MemberResponse.from(room.getMember()); + private static boolean isFulled(Room room, int participantCount) { + return room.getCapacity() == participantCount; } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java new file mode 100644 index 00000000..6659d970 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java @@ -0,0 +1,5 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +public record RoomStatusResponse(boolean isVoteMode) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index d75bf3a3..b42c5d73 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -6,8 +6,10 @@ import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomCreateResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; +import com.kok.kokapi.room.application.service.RoomFacadeService; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; @@ -16,68 +18,83 @@ import com.kok.kokcore.room.usecase.JoinRoomUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; +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; @V1Controller @RequiredArgsConstructor public class RoomController { + private final RoomFacadeService roomFacadeService; private final GetRoomUseCase getRoomUseCase; private final CreateRoomUseCase createRoomUseCase; private final JoinRoomUseCase joinRoomUseCase; - @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ์•ฝ์†๋ฐฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ์•ฝ์†๋ฐฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ํˆฌํ‘œ ๋ชจ๋“œ์— ๋Œ€ํ•œ ๊ฐ’์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}") - public ResponseEntity> getRoomDetail(@PathVariable String roomId) { - var room = getRoomUseCase.findRoomById(roomId); - var response = RoomDetailResponse.from(room); + public ResponseEntity> getRoomDetail( + @PathVariable String roomId) { + RoomDetailResponse response = roomFacadeService.findByRoomId(roomId); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } + + @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒํƒœ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์•ฝ์†๋ฐฉ์ด ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ์ค‘์ด๋ฉด false๋ฅผ, ํˆฌํ‘œ ์ง„ํ–‰ ์ค‘์ด๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/rooms/{roomId}/status") + public ResponseEntity> getRoomStatus( + @PathVariable String roomId) { + RoomStatusResponse response = roomFacadeService.getRoomStatus(roomId, LocalDateTime.now()); return ResponseEntity.ok(ApiResponseDto.success(response)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒ์„ฑ", description = "์ƒˆ๋กœ์šด ์•ฝ์†๋ฐฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms") - public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { + public ResponseEntity> createRoom( + @Valid @RequestBody CreateRoomRequest request) { String nickname = request.hostNickname(); String profile = request.hostProfile(); Member host = new Member(nickname, profile, MemberRole.LEADER); Room room = createRoomUseCase.createRoom( - request.roomName(), - request.capacity(), - host + request.roomName(), + request.capacity(), + host ); RoomCreateResponse response = RoomCreateResponse.from(room, 1, room.getCapacity() - 1); return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponseDto.success(response)); + .body(ApiResponseDto.success(response)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์ฐธ์—ฌ์ž๋“ค์˜ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}/participants") - public ResponseEntity>> getParticipants(@PathVariable String roomId) { + public ResponseEntity>> getParticipants( + @PathVariable String roomId) { Room room = getRoomUseCase.findRoomById(roomId); List participants = getRoomUseCase.getParticipants(room.getId()); List response = participants.stream() - .map(member -> new RoomMembersResponse( - member.getMemberId(), - member.getProfile(), - member.getNickname(), - member.getRole() - )).toList(); + .map(member -> new RoomMembersResponse( + member.getMemberId(), + member.getProfile(), + member.getNickname(), + member.getRole() + )).toList(); return ResponseEntity.ok(ApiResponseDto.success(response)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms/{roomId}/join") - public ResponseEntity> joinRoom(@PathVariable String roomId, @Valid @RequestBody JoinRoomParticipantRequest request) { + public ResponseEntity> joinRoom(@PathVariable String roomId, + @Valid @RequestBody JoinRoomParticipantRequest request) { Room room = getRoomUseCase.findRoomById(roomId); @@ -86,11 +103,11 @@ public ResponseEntity> joinRoom(@PathVariable S int nonParticipantCount = room.getCapacity() - participantCount; JoinRoomResponse response = new JoinRoomResponse( - participant.getMemberId(), - participant.getProfile(), - participant.getNickname(), - participantCount, - nonParticipantCount + participant.getMemberId(), + participant.getProfile(), + participant.getNickname(), + participantCount, + nonParticipantCount ); return ResponseEntity.ok(ApiResponseDto.success(response)); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java new file mode 100644 index 00000000..76ca63f0 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java @@ -0,0 +1,28 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import com.kok.kokcore.room.application.port.out.LoadRoomParticipantPort; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class RoomParticipantQueryRedisAdapter implements LoadRoomParticipantPort { + + public static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; + + private final RedisTemplate redisTemplate; + + @Override + public Long countParticipantsById(String roomId) { + String key = buildKey(roomId); + if (!redisTemplate.hasKey(key)) { + return 0L; + } + return redisTemplate.opsForList().size(key); + } + + private String buildKey(String roomId) { + return PARTICIPANT_KEY_PREFIX + roomId; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java index 4abebb24..818029ae 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java @@ -4,12 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokcore.room.application.port.out.LoadRoomPort; import com.kok.kokcore.room.domain.Room; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; -import java.util.Optional; - @Repository @RequiredArgsConstructor public class RoomQueryRedisAdapter implements LoadRoomPort { @@ -22,7 +21,7 @@ public class RoomQueryRedisAdapter implements LoadRoomPort { public Optional findRoomById(String roomId) { String key = buildKey(roomId); return Optional.ofNullable(redisTemplate.opsForValue().get(key)) - .flatMap(this::deserializeRoom); + .flatMap(this::deserializeRoom); } private Optional deserializeRoom(String roomJson) { diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java index ff5184d9..411e32af 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java @@ -4,12 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Room; +import java.time.Duration; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; -import java.time.Duration; - @Repository @RequiredArgsConstructor public class RoomSaveRedisAdapter implements SaveRoomPort { diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java new file mode 100644 index 00000000..ff40c61e --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java @@ -0,0 +1,34 @@ +package com.kok.kokapi.room.application.service; + +import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.usecase.ReadLocationUseCase; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.usecase.GetRoomUseCase; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RoomFacadeService { + + private final GetRoomUseCase getRoomUseCase; + private final ReadLocationUseCase readLocationUseCase; + + public RoomDetailResponse findByRoomId(String roomId) { + Room room = getRoomUseCase.findRoomById(roomId); + int participantsCount = getRoomUseCase.getParticipantsCount(roomId); + return RoomDetailResponse.of(room, participantsCount); + } + + public RoomStatusResponse getRoomStatus(String roomId, LocalDateTime current) { + Room room = getRoomUseCase.findRoomById(roomId); + List locations = readLocationUseCase.readLocations(roomId); + int locationInputCount = locations.size(); + boolean isVoteMode = room.hasLocationInputEnded(locationInputCount, current); + return new RoomStatusResponse(isVoteMode); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index d16d3dba..060b5961 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -2,17 +2,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.application.port.out.LoadRoomParticipantPort; import com.kok.kokcore.room.application.port.out.LoadRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.usecase.GetRoomUseCase; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; - @Service @RequiredArgsConstructor public class RoomQueryService implements GetRoomUseCase { @@ -20,13 +20,14 @@ public class RoomQueryService implements GetRoomUseCase { private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; private final LoadRoomPort loadRoomPort; + private final LoadRoomParticipantPort loadRoomParticipantPort; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; @Override public Room findRoomById(String roomId) { return loadRoomPort.findRoomById(roomId) - .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); + .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); } @Override @@ -45,4 +46,10 @@ public List getParticipants(String roomId) { } return members; } + + @Override + public int getParticipantsCount(String roomId) { + Long participantCount = loadRoomParticipantPort.countParticipantsById(roomId); + return participantCount.intValue(); + } } diff --git a/kok-api/src/main/resources/db/migration/V1__init.sql b/kok-api/src/main/resources/db/migration/V1__init.sql index 4bddd843..e76d562f 100644 --- a/kok-api/src/main/resources/db/migration/V1__init.sql +++ b/kok-api/src/main/resources/db/migration/V1__init.sql @@ -21,7 +21,7 @@ CREATE TABLE route create table location ( id bigint auto_increment primary key, - member_id int not null, + member_id varchar(255) not null, location_point point not null, room_id varchar(255) not null, constraint UKrgpajb4rsivb4gj9xn2qowgw6 diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java new file mode 100644 index 00000000..4bae3a38 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java @@ -0,0 +1,28 @@ +package com.kok.kokapi.common.template; + +import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.config.StationTestConfiguration; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.TestPropertySource; + +@ExtendWith(DatabaseCleanerExtension.class) +@Import(StationTestConfiguration.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) +@Profile("test") +public abstract class IntegrationTest extends ContainerBaseTest{ + + @LocalServerPort + private int port; + + @BeforeEach + void setPort(){ + RestAssured.port = port; + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java index 157ffe2d..65a51dfe 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java @@ -1,15 +1,18 @@ package com.kok.kokapi.common.template; -import com.kok.kokapi.config.DataJpaTestConfig; +import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.config.StationTestConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; import org.springframework.test.context.TestPropertySource; -@DataJpaTest -@Import({DataJpaTestConfig.class}) +@ExtendWith(DatabaseCleanerExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@Import({StationTestConfiguration.class}) @AutoConfigureTestDatabase(replace = Replace.NONE) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) @Profile("test") diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java index 1648b445..8be46beb 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java @@ -1,7 +1,7 @@ package com.kok.kokapi.common.template; -import com.kok.kokapi.common.util.MySQLDatabaseCleanerExtension; -import com.kok.kokapi.config.ServiceTestConfig; +import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.config.StationTestConfiguration; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationComplexClient; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,9 +11,9 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; -@ExtendWith(MySQLDatabaseCleanerExtension.class) +@ExtendWith(DatabaseCleanerExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) -@Import({ServiceTestConfig.class}) +@Import({StationTestConfiguration.class}) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) @Profile("test") public abstract class ServiceTest extends ContainerBaseTest{ diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java new file mode 100644 index 00000000..0ea61763 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java @@ -0,0 +1,6 @@ +package com.kok.kokapi.common.util; + +public interface DatabaseCleaner { + + void cleanUp(); +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java similarity index 53% rename from kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java rename to kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java index 883a572c..19b006e7 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java @@ -4,12 +4,13 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.test.context.junit.jupiter.SpringExtension; -public class MySQLDatabaseCleanerExtension implements BeforeEachCallback { +public class DatabaseCleanerExtension implements BeforeEachCallback { @Override public void beforeEach(ExtensionContext context) { - MySQLDatabaseCleaner mySQLDatabaseCleaner = SpringExtension.getApplicationContext(context) - .getBean(MySQLDatabaseCleaner.class); - mySQLDatabaseCleaner.cleanUp(); + SpringExtension.getApplicationContext(context) + .getBeansOfType(DatabaseCleaner.class) + .values() + .forEach(DatabaseCleaner::cleanUp); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java index b2b89aae..a78ae39f 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java @@ -8,7 +8,7 @@ import org.springframework.transaction.annotation.Transactional; @Component -public class MySQLDatabaseCleaner { +public class MySQLDatabaseCleaner implements DatabaseCleaner{ @PersistenceContext private EntityManager entityManager; diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java new file mode 100644 index 00000000..c7a8a9de --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java @@ -0,0 +1,29 @@ +package com.kok.kokapi.common.util; + +import java.util.Objects; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +public class RedisDatabaseCleaner implements DatabaseCleaner { + + private final RedisTemplate redisTemplate; + + public RedisDatabaseCleaner(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public void cleanUp() { + RedisConnectionFactory connectionFactory = Objects.requireNonNull( + redisTemplate.getConnectionFactory(), + "RedisConnectionFactory must not be null" + ); + + try (RedisConnection connection = connectionFactory.getConnection()) { + connection.serverCommands().flushDb(); + } + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java deleted file mode 100644 index b30b6c03..00000000 --- a/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokapi.config; - -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.boot.test.context.TestConfiguration; - -@TestConfiguration -@EntityScan(basePackages = "com.kok.kokcore") -public class DataJpaTestConfig { - -} diff --git a/kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java similarity index 92% rename from kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java rename to kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java index 8c0042a2..034aac0b 100644 --- a/kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java +++ b/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java @@ -7,7 +7,7 @@ import org.springframework.context.annotation.Primary; @TestConfiguration -public class ServiceTestConfig { +public class StationTestConfiguration { @Bean @Primary diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java new file mode 100644 index 00000000..e338306f --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.fixture; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.vo.MemberRole; + +public class MemberFixture { + + public static Member createFollower(){ + return new Member("follower", "profile.svg", MemberRole.FOLLOWER); + } + + public static Member createLeader() { + return new Member("leader", "profile.svg", MemberRole.LEADER); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java new file mode 100644 index 00000000..2104d6a0 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.fixture; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; + +public class PointFixture { + + public static Point create(){ + GeometryFactory geometryFactory = new GeometryFactory(); + Coordinate coordinate = new Coordinate(127.02758, 37.49794); + + return geometryFactory.createPoint(coordinate); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java new file mode 100644 index 00000000..5fb2a383 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java @@ -0,0 +1,11 @@ +package com.kok.kokapi.fixture; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; + +public class RoomFixture { + + public static Room create(int capacity, Member member) { + return Room.create("room", capacity, member); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java new file mode 100644 index 00000000..2a88ab0e --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -0,0 +1,138 @@ +package com.kok.kokapi.room.adapter.in.web; + +import static org.hamcrest.Matchers.is; + +import com.kok.kokapi.centroid.adapter.in.dto.request.LocationRequest; +import com.kok.kokapi.common.template.IntegrationTest; +import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; +import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; +import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomCreateResponse; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +class RoomIntegrationTest extends IntegrationTest { + + @DisplayName("์•ฝ์†๋ฐฉ ์‹œ๋‚˜๋ฆฌ์˜ค") + @TestFactory + Stream getRoomDetail() { + AtomicReference createRoomResponse = new AtomicReference<>(); + AtomicReference joinRoomResponse = new AtomicReference<>(); + + return Stream.of( + DynamicTest.dynamicTest("์•ฝ์†๋ฐฉ์„ ์ƒ์„ฑํ•œ๋‹ค.", + () -> createRoomResponse.set(createRoom( + new CreateRoomRequest("room", 2, "hostProfile.svg", "hostNickname")))), + + inputLocation("๋ฐฉ์žฅ์ด ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse), + + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๊ณ , isFulled๋Š” false์ด๋‹ค.", createRoomResponse, 1, + false), + + DynamicTest.dynamicTest("ํŒ”๋กœ์›Œ๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•œ๋‹ค.", + () -> joinRoomResponse.set(joinRoom(createRoomResponse.get().id(), + new JoinRoomParticipantRequest("profile", "follower")))), + + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๊ณ , isFulled๋Š” true์ด๋‹ค.", createRoomResponse, 0, + true), + + checkVoteMode("์•„์ง ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜๊ธฐ์— voteMode๋Š” false์ด๋‹ค.", createRoomResponse, false), + + inputLocation("ํŒ”๋กœ์›Œ๊ฐ€ ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse, joinRoomResponse), + + checkVoteMode("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ชจ๋“  ์ฐธ์—ฌ์ž๊ฐ€ ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ–ˆ๊ธฐ์— voteMode๋Š” true์ด๋‹ค.", + createRoomResponse, true) + ); + } + + private RoomCreateResponse createRoom(CreateRoomRequest request) { + return RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/v1/api/rooms") + .then().log().all() + .assertThat().statusCode(201) + .extract().body().jsonPath().getObject("data", RoomCreateResponse.class); + } + + private JoinRoomResponse joinRoom(String roomId, JoinRoomParticipantRequest request) { + return RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/v1/api/rooms/" + roomId + "/join") + .then().log().all() + .assertThat().statusCode(200) + .extract().body().jsonPath().getObject("data", JoinRoomResponse.class); + } + + private static DynamicTest inputLocation(String message, + AtomicReference createRoomResponse) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + String memberId = createRoomResponse.get().member().id(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(new LocationRequest(roomId, memberId, new BigDecimal("37"), + new BigDecimal("127"))) + .when().post("/v1/api/locations") + .then().log().all() + .assertThat().statusCode(200); + }); + } + + private static DynamicTest getRoomDetail(String message, + AtomicReference createRoomResponse, int nonParticipantCount, + boolean expectedIsFulled) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/v1/api/rooms/" + roomId) + .then().log().all() + .assertThat().statusCode(200) + .body("data.nonParticipantCount", is(nonParticipantCount)) + .body("data.isFulled", is(expectedIsFulled)); + }); + } + + private static DynamicTest inputLocation(String message, + AtomicReference createRoomResponse, + AtomicReference joinRoomResponse) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + String memberId = joinRoomResponse.get().id(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(new LocationRequest(roomId, memberId, new BigDecimal("37"), + new BigDecimal("127"))) + .when().post("/v1/api/locations") + .then().log().all() + .assertThat().statusCode(200); + }); + } + + private static DynamicTest checkVoteMode(String message, + AtomicReference createRoomResponse, boolean expectedIsVoteMode) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/v1/api/rooms/" + roomId + "/status") + .then().log().all() + .assertThat().statusCode(200) + .body("data.isVoteMode", is(expectedIsVoteMode)); + }); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java new file mode 100644 index 00000000..b0cdf131 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kok.kokapi.common.template.RepositoryTest; +import com.kok.kokapi.fixture.MemberFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class RoomParticipantQueryRedisAdapterTest extends RepositoryTest { + + @Autowired + private RoomParticipantQueryRedisAdapter roomParticipantQueryRedisAdapter; + @Autowired + private RoomParticipantSaveAdapter roomParticipantSaveAdapter; + + @DisplayName("๋ฐฉ์˜ ์ฐธ์—ฌ ์ธ์›์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void getParticipantCount() { + // given + String roomId = "roomId"; + roomParticipantSaveAdapter.joinRoom(roomId, MemberFixture.createLeader()); + + // when + Long participantCount = roomParticipantQueryRedisAdapter.countParticipantsById(roomId); + + // then + assertThat(participantCount).isEqualTo(1); + } +} diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index cdc21862..53e3af3c 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -16,8 +16,8 @@ spring: defer-datasource-initialization: false open-in-view: false hibernate: -### ์‹ค์ œ ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์งœ ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ddl-auto: validate +### ์‹ค์ œ ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์งœ ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ncp: object-storage-url: mock tmap-sub: diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantPort.java new file mode 100644 index 00000000..1f873fb9 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantPort.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.room.application.port.out; + +public interface LoadRoomParticipantPort { + + Long countParticipantsById(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index d28cc226..6ea77961 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -1,26 +1,34 @@ package com.kok.kokcore.room.domain; -import lombok.*; - import java.io.Serializable; +import java.time.LocalDateTime; import java.util.UUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; @Getter @ToString @EqualsAndHashCode public class Room implements Serializable { - public static final int REQUIRED_CAPACITY = 2; + private static final int REQUIRED_CAPACITY = 2; + private static final long LOCATION_INPUT_TIME_LIMIT = 6; + private final String id; // ์•ฝ์†๋ฐฉ ID (UUID) private final String roomName; // ์•ฝ์†๋ฐฉ ์ด๋ฆ„ private final int capacity; // ์ฐธ์—ฌ์ธ์› ์ˆ˜ (์ตœ์†Œ 2๋ช… ์ด์ƒ) private final Member member; // ๋ฐฉ ์ฐธ์—ฌ์ž + private final LocalDateTime locationInputLimitDateTime; // ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ์ผ์‹œ + private final LocalDateTime createdDateTime; // ๋ฐฉ ์ƒ์„ฑ์ผ์‹œ private Room(String id, String roomName, int capacity, Member member) { this.id = id; this.roomName = roomName; this.capacity = capacity; this.member = member; + this.createdDateTime = LocalDateTime.now().withNano(0); + this.locationInputLimitDateTime = createdDateTime.plusHours(LOCATION_INPUT_TIME_LIMIT); } public static Room create(String roomName, int capacity, Member member) { @@ -39,4 +47,13 @@ private static void validateParameter(String roomName, int capacity) { throw new IllegalArgumentException("At least 2 participants are required"); } } + + public boolean hasLocationInputEnded(long locationInputCount, LocalDateTime currentTime) { + return isAllLocationInput(locationInputCount) || currentTime.isAfter( + locationInputLimitDateTime); + } + + private boolean isAllLocationInput(long participantCount) { + return participantCount == capacity; + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java index 37c9d32f..bfa586a5 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -2,10 +2,13 @@ import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; - import java.util.List; public interface GetRoomUseCase { + Room findRoomById(String roomId); + List getParticipants(String roomId); + + int getParticipantsCount(String roomId); } diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java index e64a342e..576d21e1 100644 --- a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -1,11 +1,17 @@ package com.kok.kokcore.room.domain; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.kok.kokcore.room.domain.vo.MemberRole; +import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class RoomTest { @DisplayName("์•ฝ์†๋ฐฉ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @@ -16,7 +22,7 @@ void createRoom() { int capacity = 4; String hostProfile = "hostProfile"; String hostNickname = "test"; - String password = "secret"; + LocalDateTime deadline = LocalDateTime.now().withNano(0).plusHours(6); Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When @@ -24,12 +30,15 @@ void createRoom() { // Then assertAll("์•ฝ์†๋ฐฉ ์ƒ์„ฑ", - () -> assertNotNull(room.getId(), "ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(roomName, room.getRoomName(), "๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(capacity, room.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostNickname, room.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostProfile, room.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(MemberRole.LEADER, room.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + () -> assertNotNull(room.getId(), "ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(roomName, room.getRoomName(), "๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(capacity, room.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(deadline.getHour(), room.getLocationInputLimitDateTime().getHour(), + "์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์ด ์ •ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostNickname, room.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostProfile, room.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(MemberRole.LEADER, room.getMember().getRole(), + "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") ); } @@ -41,12 +50,12 @@ void createRoomWithEmptyRoomName() { int capacity = 4; String hostNickname = "hostNickname"; String hostProfile = "hostProfile"; - String password = "secret"; Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When & Then IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host)); + assertThrows(IllegalArgumentException.class, + () -> Room.create(roomName, capacity, host)); assertTrue(exception.getMessage().contains("Room name is required")); } @@ -62,7 +71,47 @@ void createRoomWithInvalidCapacity() { // When & Then IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host)); + assertThrows(IllegalArgumentException.class, + () -> Room.create(roomName, capacity, host)); assertTrue(exception.getMessage().contains("At least 2 participants are required")); } + + @DisplayName("์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void hasLocationInputEndedByDeadlineExceeded() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.hasLocationInputEnded(1, LocalDateTime.now().plusHours(6)); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("๋ชจ๋“  ์ฐธ๊ฐ€์ž๊ฐ€ ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void hasLocationInputEndedByAllParticipantCompleted() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.hasLocationInputEnded(2, LocalDateTime.now().plusHours(5)); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜์ง€๋„, ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€๋„ ์•Š์•˜์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void hasNotLocationInputEnded() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.hasLocationInputEnded(1, LocalDateTime.now().plusHours(5)); + + // then + assertThat(result).isFalse(); + } } From 3bd877c98873a915d6f8890236faf98d39ecb653 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:43:29 +0900 Subject: [PATCH 101/163] =?UTF-8?q?=E2=9C=A8=20[Feature/RoomParticipant]?= =?UTF-8?q?=20add=20boolean=20field=20if=20room=20is=20full=20for=20gettin?= =?UTF-8?q?g=20profile=20list=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :recycle: refactor: remove isFulled field from room detail response * :sparkles: feat: add isFull field for getting profile list for room --- .../in/dto/response/RoomDetailResponse.java | 10 ++----- .../in/dto/response/RoomMembersResponses.java | 25 ++++++++++++++++ .../room/adapter/in/web/RoomController.java | 15 +++------- .../adapter/in/web/RoomIntegrationTest.java | 30 ++++++++++++++----- .../com/kok/kokcore/room/domain/Room.java | 4 +++ 5 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index 51718e4e..bf68567a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -5,20 +5,14 @@ public record RoomDetailResponse( String id, String roomName, - int nonParticipantCount, - boolean isFulled + int nonParticipantCount ) { public static RoomDetailResponse of(Room room, int participantCount) { return new RoomDetailResponse( room.getId(), room.getRoomName(), - room.getCapacity() - participantCount, - isFulled(room, participantCount) + room.getCapacity() - participantCount ); } - - private static boolean isFulled(Room room, int participantCount) { - return room.getCapacity() == participantCount; - } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java new file mode 100644 index 00000000..bb3b5089 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java @@ -0,0 +1,25 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import java.util.List; + +public record RoomMembersResponses( + boolean isFull, + List members +) { + + public static RoomMembersResponses of(Room room, List members) { + return new RoomMembersResponses(room.isFull(members.size()), getMemberResponses(members)); + } + + private static List getMemberResponses(List members) { + return members.stream() + .map(member -> new RoomMembersResponse( + member.getMemberId(), + member.getProfile(), + member.getNickname(), + member.getRole() + )).toList(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index b42c5d73..d565cc64 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -7,7 +7,7 @@ import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomCreateResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponses; import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; import com.kok.kokapi.room.application.service.RoomFacadeService; import com.kok.kokcore.room.domain.Member; @@ -75,20 +75,13 @@ public ResponseEntity> createRoom( @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์ฐธ์—ฌ์ž๋“ค์˜ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}/participants") - public ResponseEntity>> getParticipants( + public ResponseEntity> getParticipants( @PathVariable String roomId) { Room room = getRoomUseCase.findRoomById(roomId); List participants = getRoomUseCase.getParticipants(room.getId()); + RoomMembersResponses responses = RoomMembersResponses.of(room, participants); - List response = participants.stream() - .map(member -> new RoomMembersResponse( - member.getMemberId(), - member.getProfile(), - member.getNickname(), - member.getRole() - )).toList(); - - return ResponseEntity.ok(ApiResponseDto.success(response)); + return ResponseEntity.ok(ApiResponseDto.success(responses)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•ฉ๋‹ˆ๋‹ค.") diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java index 2a88ab0e..d557d492 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -1,5 +1,6 @@ package com.kok.kokapi.room.adapter.in.web; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import com.kok.kokapi.centroid.adapter.in.dto.request.LocationRequest; @@ -32,14 +33,15 @@ Stream getRoomDetail() { inputLocation("๋ฐฉ์žฅ์ด ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse), - getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๊ณ , isFulled๋Š” false์ด๋‹ค.", createRoomResponse, 1, - false), + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๋‹ค", createRoomResponse, 1), DynamicTest.dynamicTest("ํŒ”๋กœ์›Œ๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•œ๋‹ค.", () -> joinRoomResponse.set(joinRoom(createRoomResponse.get().id(), new JoinRoomParticipantRequest("profile", "follower")))), - getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๊ณ , isFulled๋Š” true์ด๋‹ค.", createRoomResponse, 0, + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๋‹ค.", createRoomResponse, 0), + + getRoomMembers("์•ฝ์†๋ฐฉ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉด isFulใ…ฃ์€ true์ด๊ณ , 2๋ช…์˜ ํ”„๋กœํ•„์ด ์žˆ๋‹ค", createRoomResponse, 2, true), checkVoteMode("์•„์ง ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜๊ธฐ์— voteMode๋Š” false์ด๋‹ค.", createRoomResponse, false), @@ -89,8 +91,7 @@ private static DynamicTest inputLocation(String message, } private static DynamicTest getRoomDetail(String message, - AtomicReference createRoomResponse, int nonParticipantCount, - boolean expectedIsFulled) { + AtomicReference createRoomResponse, int nonParticipantCount) { return DynamicTest.dynamicTest(message, () -> { String roomId = createRoomResponse.get().id(); @@ -99,8 +100,23 @@ private static DynamicTest getRoomDetail(String message, .when().get("/v1/api/rooms/" + roomId) .then().log().all() .assertThat().statusCode(200) - .body("data.nonParticipantCount", is(nonParticipantCount)) - .body("data.isFulled", is(expectedIsFulled)); + .body("data.nonParticipantCount", is(nonParticipantCount)); + }); + } + + private static DynamicTest getRoomMembers(String message, + AtomicReference createRoomResponse, int profileCount, + boolean expectedIsFull) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/v1/api/rooms/" + roomId + "/participants") + .then().log().all() + .assertThat().statusCode(200) + .body("data.members", hasSize(profileCount)) + .body("data.isFull", is(expectedIsFull)); }); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 6ea77961..6a1007de 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -56,4 +56,8 @@ public boolean hasLocationInputEnded(long locationInputCount, LocalDateTime curr private boolean isAllLocationInput(long participantCount) { return participantCount == capacity; } + + public boolean isFull(int participantCount) { + return capacity == participantCount; + } } From b7a0d21d4b6f97a11052e4d66f207a2481b414d0 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:34:37 +0900 Subject: [PATCH 102/163] [Refactor/v1] refactor code (#75) * :recycle: refactor: refactor package structure * :art: style: reformat code by google style * :recycle: refactor: rename static method --- kok-api/build.gradle | 58 ++++++++-------- .../com/kok/kokapi/KokApiApplication.java | 8 +-- .../in/dto/request/LocationRequest.java | 26 ++++---- .../in/dto/response/CentroidResponse.java | 11 ++-- .../response/ConvexHullLocationResponse.java | 8 ++- .../in/dto/response/LocationResponse.java | 12 ++-- .../adapter/in/web/LocationController.java | 66 +++++++++++-------- .../adapter/out/mapper/LocationMapper.java | 17 ++--- .../LocationPersistenceAdapter.java | 16 ++--- .../service/CentroidQueryService.java | 5 +- .../service/LocationCommandService.java | 15 +++-- .../service/LocationQueryService.java | 7 +- .../exception/GlobalExceptionHandler.java | 36 +++++----- .../kokapi/config/annotion/V1Controller.java | 6 +- .../config/geometry/GeometryConfig.java | 6 -- .../config/geometry/PointConverter.java | 5 +- .../redis/{out => }/RedisCacheConfig.java | 50 +++++++------- .../kok/kokapi/config/redis/RedisConfig.java | 3 +- .../kokapi/config/swagger/SwaggerConfig.java | 18 ++--- .../kok/kokapi/config/web/WebMvcConfig.java | 10 +-- .../adapter/in/web/HealthCheckController.java | 2 +- .../adapter/in/dto/RouteRequest.java | 12 ---- .../adapter/in/dto/request/RouteRequest.java | 13 ++++ ...lexPublicTransportationParsedResponse.java | 10 +-- ...mapPublicTransportationParsedResponse.java | 10 +-- .../web/PublicTransportationController.java | 30 +++++---- .../external/PublicTransportationClient.java | 57 ++++++++-------- .../PublicTransportationComplexClient.java | 58 ++++++++-------- .../out/external/TmapClientProperties.java | 9 +-- .../external/TmapComplexClientProperties.java | 7 +- ...apComplexPublicTransportationResponse.java | 17 ++++- .../dto/TmapPublicTransportationResponse.java | 10 ++- .../TmapPublicTransportationService.java | 49 ++++++++------ .../in/dto/request/CreateRoomRequest.java | 24 +++---- .../request/JoinRoomParticipantRequest.java | 12 ++-- .../in/dto/response/CreateRoomResponse.java | 24 +++++++ .../in/dto/response/JoinRoomResponse.java | 14 ++-- .../in/dto/response/MemberResponse.java | 17 ++--- .../dto/response/RandomProfileResponse.java | 8 ++- .../in/dto/response/RoomCreateResponse.java | 23 ------- .../in/dto/response/RoomMembersResponse.java | 12 ++-- .../room/adapter/in/web/RoomController.java | 6 +- .../adapter/in/web/RoomProfileController.java | 4 +- .../RoomParticipantQueryRedisAdapter.java | 2 +- .../RoomParticipantSaveAdapter.java | 2 +- .../persistence/RoomQueryRedisAdapter.java | 2 +- .../out/persistence/RoomSaveRedisAdapter.java | 2 +- .../service/RandomProfileService.java | 13 ++-- .../service/RoomCreationService.java | 2 +- .../service/RoomParticipantService.java | 3 +- .../application/service/RoomQueryService.java | 4 +- .../response/RecommendedStationResponse.java | 10 +-- .../adapter/in/web/StationController.java | 22 ++++--- .../adapter/out/external/StationClient.java | 7 +- .../out/external/StationErrorHandler.java | 3 +- .../out/external/dto/StationResponse.java | 2 +- .../out/external/dto/StationResponses.java | 2 +- .../persistence/RoutePersistenceAdapter.java | 7 +- .../out/persistence/RouteRepository.java | 3 +- .../StationPersistenceAdapter.java | 17 ++--- .../out/persistence/StationRepository.java | 18 +++-- .../application/service/RouteService.java | 7 +- .../application/service/StationService.java | 30 +++++---- .../config/StationsConfig.java | 2 +- .../src/main/resources/application-dev.yml | 4 +- .../src/main/resources/application-prod.yml | 2 +- .../main/resources/db/migration/V1__init.sql | 8 +-- .../kok/kokapi/KokApiApplicationTests.java | 6 +- .../common/template/IntegrationTest.java | 4 +- .../common/template/RepositoryTest.java | 2 +- .../kokapi/common/template/ServiceTest.java | 2 +- .../common/util/MySQLDatabaseCleaner.java | 2 +- .../config/StationTestConfiguration.java | 2 +- .../com/kok/kokapi/fixture/MemberFixture.java | 2 +- .../com/kok/kokapi/fixture/PointFixture.java | 2 +- .../adapter/in/web/RoomIntegrationTest.java | 14 ++-- .../service/RoomCreationServiceTest.java | 19 +++--- .../service/RoomQueryServiceTest.java | 2 +- .../out/external/FakeStationClient.java | 6 +- .../StationPersistenceAdapterTest.java | 6 +- kok-core/build.gradle | 22 +++---- .../com/kok/kokcore/KokCoreApplication.java | 6 +- .../kok/kokcore/location/domain/Location.java | 16 +++-- .../port/out/ReadCentroidPort.java | 3 +- .../port/out/ReadLocationPort.java | 7 +- .../port/out/SaveLocationPort.java | 3 +- .../usecase/CreateLocationUseCase.java | 9 ++- .../location/usecase/LoadCentroidUseCase.java | 4 +- .../location/usecase/ReadLocationUseCase.java | 5 +- .../RetrievePublicTransportationUseCase.java | 3 +- .../com/kok/kokcore/room/domain/Member.java | 4 +- .../com/kok/kokcore/room/domain/Profile.java | 1 + .../port/out/LoadRoomParticipantPort.java | 2 +- .../port/out/LoadRoomPort.java | 4 +- .../port/out/SaveRoomParticipantsPort.java | 3 +- .../port/out/SaveRoomPort.java | 3 +- .../usecase/CreateRandomProfileUseCase.java | 3 +- .../room/usecase/CreateRoomUseCase.java | 1 + .../kokcore/room/usecase/JoinRoomUseCase.java | 1 + .../port/out/LoadStationsPort.java | 8 --- .../station/domain/entity/Station.java | 4 +- .../station/port/out/LoadStationsPort.java | 8 +++ .../port/out/ReadStationsPort.java | 2 +- .../port/out/RetrieveRoutePort.java | 4 +- .../port/out/RetrieveStationsPort.java | 7 +- .../port/out/SaveRoutePort.java | 2 +- .../port/out/SaveStationsPort.java | 2 +- .../port/out/dto/StationRouteDto.java | 2 +- .../port/out/dto/StationRouteDtos.java | 4 +- .../usecase/RecommendStationUseCase.java | 3 +- .../usecase/RetrieveRouteUseCase.java | 4 +- .../usecase/SaveStationUseCase.java | 2 +- .../kok/kokcore/KokCoreApplicationTests.java | 6 +- .../kokcore/location/domain/LocationTest.java | 1 + .../port/out/dto/StationRouteDtosTest.java | 10 +-- 115 files changed, 671 insertions(+), 552 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/config/redis/{out => }/RedisCacheConfig.java (61%) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java rename kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/{ => response}/TmapComplexPublicTransportationParsedResponse.java (92%) rename kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/{ => response}/TmapPublicTransportationParsedResponse.java (56%) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java rename kok-api/src/main/java/com/kok/kokapi/station/{application => }/config/StationsConfig.java (89%) rename kok-core/src/main/java/com/kok/kokcore/location/{application => }/port/out/ReadCentroidPort.java (69%) rename kok-core/src/main/java/com/kok/kokcore/location/{application => }/port/out/ReadLocationPort.java (87%) rename kok-core/src/main/java/com/kok/kokcore/location/{application => }/port/out/SaveLocationPort.java (78%) rename kok-core/src/main/java/com/kok/kokcore/{public_transfortation => public_transportation}/usecase/RetrievePublicTransportationUseCase.java (81%) rename kok-core/src/main/java/com/kok/kokcore/room/{application => }/port/out/LoadRoomParticipantPort.java (64%) rename kok-core/src/main/java/com/kok/kokcore/room/{application => }/port/out/LoadRoomPort.java (75%) rename kok-core/src/main/java/com/kok/kokcore/room/{application => }/port/out/SaveRoomParticipantsPort.java (73%) rename kok-core/src/main/java/com/kok/kokcore/room/{application => }/port/out/SaveRoomPort.java (66%) delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/port/out/ReadStationsPort.java (56%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/port/out/RetrieveRoutePort.java (80%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/port/out/RetrieveStationsPort.java (85%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/port/out/SaveRoutePort.java (74%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/port/out/SaveStationsPort.java (76%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/port/out/dto/StationRouteDto.java (90%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/port/out/dto/StationRouteDtos.java (95%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/usecase/RecommendStationUseCase.java (76%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/usecase/RetrieveRouteUseCase.java (80%) rename kok-core/src/main/java/com/kok/kokcore/station/{application => }/usecase/SaveStationUseCase.java (55%) rename kok-core/src/test/java/com/kok/kokcore/station/{application => }/port/out/dto/StationRouteDtosTest.java (95%) diff --git a/kok-api/build.gradle b/kok-api/build.gradle index d55d20ab..e2484971 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -1,49 +1,49 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.2' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.kok' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation project(':kok-core') - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' - - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - - testImplementation 'org.testcontainers:testcontainers-bom:1.20.5' - testImplementation 'org.testcontainers:junit-jupiter' - testImplementation 'org.testcontainers:mysql' - testImplementation 'com.redis:testcontainers-redis' - - testImplementation 'io.rest-assured:rest-assured:5.3.1' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation project(':kok-core') + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'org.testcontainers:testcontainers-bom:1.20.5' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:mysql' + testImplementation 'com.redis:testcontainers-redis' + + testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java index 5ec5598c..8599d4c7 100644 --- a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java +++ b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java @@ -2,14 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication public class KokApiApplication { - public static void main(String[] args) { - SpringApplication.run(KokApiApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(KokApiApplication.class, args); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index b32a21a5..6e67db3c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -4,25 +4,25 @@ import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; - import java.math.BigDecimal; public record LocationRequest( - @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String roomId, + @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String roomId, - @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String memberId, + @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String memberId, - @NotNull(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - BigDecimal latitude, + @NotNull(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + BigDecimal latitude, - @NotNull(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - BigDecimal longitude + @NotNull(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + BigDecimal longitude ) { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java index 760059da..e188c728 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java @@ -4,13 +4,14 @@ import java.math.RoundingMode; public record CentroidResponse( - String roomId, - BigDecimal latitude, - BigDecimal longitude + String roomId, + BigDecimal latitude, + BigDecimal longitude ) { + public static CentroidResponse of(String roomId, BigDecimal longitude, BigDecimal latitude) { return new CentroidResponse(roomId, - latitude.setScale(6, RoundingMode.HALF_UP), - longitude.setScale(6,RoundingMode.HALF_UP)); + latitude.setScale(6, RoundingMode.HALF_UP), + longitude.setScale(6, RoundingMode.HALF_UP)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java index 4fafd2b4..a10b0c78 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java @@ -3,10 +3,12 @@ import java.util.List; public record ConvexHullLocationResponse( - List convexHull, - List inside + List convexHull, + List inside ) { - public static ConvexHullLocationResponse of(List convexHull, List inside) { + + public static ConvexHullLocationResponse of(List convexHull, + List inside) { return new ConvexHullLocationResponse(convexHull, inside); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index c30423c3..7ee197a9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -3,15 +3,17 @@ import java.math.BigDecimal; import java.math.RoundingMode; -public record LocationResponse ( +public record LocationResponse( String roomId, String memberId, BigDecimal latitude, BigDecimal longitude -){ - public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { +) { + + public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude) { return new LocationResponse(roomId, memberId, - latitude.setScale(6, RoundingMode.HALF_UP), - longitude.setScale(6, RoundingMode.HALF_UP)); + latitude.setScale(6, RoundingMode.HALF_UP), + longitude.setScale(6, RoundingMode.HALF_UP)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index af659406..af8e93a5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -13,13 +13,16 @@ import com.kok.kokcore.location.usecase.ReadLocationUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; +import java.math.BigDecimal; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.math.BigDecimal; -import java.util.List; +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; @V1Controller @RequiredArgsConstructor @@ -32,35 +35,39 @@ public class LocationController { @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") @PostMapping("/locations") - public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { + public ResponseEntity> createLocation( + @Valid @RequestBody LocationRequest locationRequest) { createLocationUsecase.createLocation( - locationRequest.roomId(), - locationRequest.memberId(), - locationRequest.latitude(), - locationRequest.longitude() + locationRequest.roomId(), + locationRequest.memberId(), + locationRequest.latitude(), + locationRequest.longitude() ); - Pair centroid = loadCentroidUsecase.readCentroidCoordinates(locationRequest.roomId()); + Pair centroid = loadCentroidUsecase.readCentroidCoordinates( + locationRequest.roomId()); return ResponseEntity.ok(ApiResponseDto.success( - CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) + CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) )); } // For Test @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its roomId") @GetMapping("/locations/centroid/{roomId}") - public ResponseEntity> getCentroid(@PathVariable String roomId) { + public ResponseEntity> getCentroid( + @PathVariable String roomId) { Pair centroid = loadCentroidUsecase.readCentroidCoordinates(roomId); return ResponseEntity.ok(ApiResponseDto.success( - CentroidResponse.of(roomId, centroid.getFirst(), centroid.getSecond()) + CentroidResponse.of(roomId, centroid.getFirst(), centroid.getSecond()) )); } @Operation(summary = "์œ„์น˜ ์กฐํšŒ Basic", description = "Retrieve detailed information for a location using its roomId and member ID") @GetMapping("/locations/{roomId}/{memberId}") - public ResponseEntity> getLocation(@PathVariable String roomId, @PathVariable String memberId) { + public ResponseEntity> getLocation(@PathVariable String roomId, + @PathVariable String memberId) { Location location = readLocationUsecase.readLocation(roomId, memberId); return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); @@ -68,29 +75,36 @@ public ResponseEntity> getLocation(@PathVariabl @Operation(summary = "์œ„์น˜์กฐํšŒ ConvexHull", description = "Retrieve the ConvexHull inside list, outside list of locations for a roomId") @GetMapping("/locations/ConvH/{roomId}") - public ResponseEntity> getConvexHullLocations(@PathVariable String roomId){ - List convexHull = locationMapper.toResponseList(readLocationUsecase.readConvexHull(roomId)); - List inside = locationMapper.toResponseList(readLocationUsecase.readInsideConvexHull(roomId)); - - return ResponseEntity.ok(ApiResponseDto.success(ConvexHullLocationResponse.of(convexHull, inside ))); + public ResponseEntity> getConvexHullLocations( + @PathVariable String roomId) { + List convexHull = locationMapper.toResponseList( + readLocationUsecase.readConvexHull(roomId)); + List inside = locationMapper.toResponseList( + readLocationUsecase.readInsideConvexHull(roomId)); + + return ResponseEntity.ok( + ApiResponseDto.success(ConvexHullLocationResponse.of(convexHull, inside))); } @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a roomId") @GetMapping("/locations/{roomId}") - public ResponseEntity>> getLocations(@PathVariable String roomId) { - List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(roomId)); + public ResponseEntity>> getLocations( + @PathVariable String roomId) { + List responses = locationMapper.toResponseList( + readLocationUsecase.readLocations(roomId)); return ResponseEntity.ok(ApiResponseDto.success(responses)); } @Operation(summary = "์œ„์น˜ ์ˆ˜์ •", description = "Update the location with the provided details.") @PutMapping("/locations") - public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { + public ResponseEntity> updateLocation( + @Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.updateLocation( - locationRequest.roomId(), - locationRequest.memberId(), - locationRequest.latitude(), - locationRequest.longitude() + locationRequest.roomId(), + locationRequest.memberId(), + locationRequest.latitude(), + locationRequest.longitude() ); LocationResponse response = locationMapper.toResponse(location); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index 65abee45..2b9a1b98 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -1,12 +1,12 @@ package com.kok.kokapi.centroid.adapter.out.mapper; import com.kok.kokapi.centroid.adapter.in.dto.response.LocationResponse; -import com.kok.kokcore.location.domain.Location; import com.kok.kokapi.config.geometry.PointConverter; -import org.springframework.stereotype.Component; -import org.springframework.data.util.Pair; +import com.kok.kokcore.location.domain.Location; import java.math.BigDecimal; import java.util.List; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; @Component public class LocationMapper { @@ -18,12 +18,13 @@ public LocationMapper(PointConverter pointConverter) { } public LocationResponse toResponse(Location location) { - Pair coordinates = pointConverter.toCoordinates(location.getLocation_point()); + Pair coordinates = pointConverter.toCoordinates( + location.getLocation_point()); return LocationResponse.of( - location.getRoomId(), - location.getMemberId(), - coordinates.getFirst(), - coordinates.getSecond() + location.getRoomId(), + location.getMemberId(), + coordinates.getFirst(), + coordinates.getSecond() ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index 01bf4428..cf79d95e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -2,21 +2,21 @@ import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.application.port.out.ReadCentroidPort; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; -import com.kok.kokcore.location.application.port.out.SaveLocationPort; +import com.kok.kokcore.location.port.out.ReadCentroidPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; +import com.kok.kokcore.location.port.out.SaveLocationPort; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; -import org.springframework.stereotype.Repository; import org.locationtech.jts.io.WKTReader; +import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Optional; - @Repository @RequiredArgsConstructor -public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocationPort, ReadLocationPort { +public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocationPort, + ReadLocationPort { private final LocationRepository locationRepository; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java index 0619867c..768b3856 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -1,15 +1,14 @@ package com.kok.kokapi.centroid.application.service; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokcore.location.application.port.out.ReadCentroidPort; +import com.kok.kokcore.location.port.out.ReadCentroidPort; import com.kok.kokcore.location.usecase.LoadCentroidUseCase; +import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; -import java.math.BigDecimal; - @Service @RequiredArgsConstructor public class CentroidQueryService implements LoadCentroidUseCase { diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 3480efe6..06f72bf0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -2,16 +2,15 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; -import com.kok.kokcore.location.application.port.out.SaveLocationPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; +import com.kok.kokcore.location.port.out.SaveLocationPort; import com.kok.kokcore.location.usecase.CreateLocationUseCase; import jakarta.transaction.Transactional; +import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Service; -import java.math.BigDecimal; - @Service @RequiredArgsConstructor public class LocationCommandService implements CreateLocationUseCase { @@ -21,16 +20,18 @@ public class LocationCommandService implements CreateLocationUseCase { private final PointConverter pointConverter; @Override - public Location createLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { + public Location createLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude) { Point point = pointConverter.fromCoordinates(latitude, longitude); return saveLocationPort.saveLocation(roomId, memberId, point); } @Override @Transactional - public Location updateLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { + public Location updateLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude) { Location location = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); location.changePoint(newPoint); return location; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index 3b496730..137be13a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -1,13 +1,12 @@ package com.kok.kokapi.centroid.application.service; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.location.usecase.ReadLocationUseCase; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class LocationQueryService implements ReadLocationUseCase { @@ -17,7 +16,7 @@ public class LocationQueryService implements ReadLocationUseCase { @Override public Location readLocation(String roomId, String memberId) { return readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java index 9951a59c..7093159d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java @@ -1,48 +1,50 @@ package com.kok.kokapi.common.exception; -import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.common.error.ErrorCode; +import com.kok.kokapi.common.response.ApiResponseDto; import jakarta.validation.ConstraintViolationException; +import java.util.stream.Collectors; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.util.stream.Collectors; - @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity> handleConstraintViolationException(ConstraintViolationException ex) { + public ResponseEntity> handleConstraintViolationException( + ConstraintViolationException ex) { String message = ex.getConstraintViolations() - .stream() - .map(cv -> cv.getPropertyPath() + " " + cv.getMessage()) - .collect(Collectors.joining(", ")); + .stream() + .map(cv -> cv.getPropertyPath() + " " + cv.getMessage()) + .collect(Collectors.joining(", ")); return ResponseEntity.badRequest() - .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); + .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + public ResponseEntity> handleMethodArgumentNotValidException( + MethodArgumentNotValidException ex) { String message = ex.getBindingResult().getFieldErrors() - .stream() - .map(fe -> fe.getField() + " " + fe.getDefaultMessage()) - .collect(Collectors.joining(", ")); + .stream() + .map(fe -> fe.getField() + " " + fe.getDefaultMessage()) + .collect(Collectors.joining(", ")); return ResponseEntity.badRequest() - .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); + .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); } @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleBadRequestException(IllegalArgumentException ex) { + public ResponseEntity> handleBadRequestException( + IllegalArgumentException ex) { return ResponseEntity.badRequest() - .body(ApiResponseDto.error(ErrorCode.BAD_REQUEST, ex.getMessage())); + .body(ApiResponseDto.error(ErrorCode.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity> handleGlobalException(Exception ex) { return ResponseEntity - .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) - .body(ApiResponseDto.error(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage())); + .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + .body(ApiResponseDto.error(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage())); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java b/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java index b2c7718e..68048c9e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java @@ -1,16 +1,16 @@ package com.kok.kokapi.config.annotion; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/v1/api") @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface V1Controller { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java index b85eacec..417f7891 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java @@ -1,14 +1,8 @@ package com.kok.kokapi.config.geometry; -import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.locationtech.jts.geom.Point; -import org.springframework.data.util.Pair; - -import java.math.BigDecimal; -import java.util.List; @Configuration public class GeometryConfig { diff --git a/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java b/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java index 0fe12961..6db1a014 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java @@ -1,12 +1,11 @@ package com.kok.kokapi.config.geometry; +import java.math.BigDecimal; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; -import java.math.BigDecimal; - public class PointConverter { private final GeometryFactory geometryFactory; @@ -22,7 +21,7 @@ public Point fromCoordinates(BigDecimal latitude, BigDecimal longitude) { return point; } - public Pair toCoordinates(Point point) { + public Pair toCoordinates(Point point) { return Pair.of(BigDecimal.valueOf(point.getY()), BigDecimal.valueOf(point.getX())); } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java similarity index 61% rename from kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java rename to kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java index 27d096b3..66e47d92 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java @@ -1,7 +1,8 @@ -package com.kok.kokapi.config.redis.out; +package com.kok.kokapi.config.redis; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import java.time.Duration; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; @@ -14,58 +15,63 @@ import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; -import java.time.Duration; - @EnableCaching @Configuration public class RedisCacheConfig { @Bean("cacheManager") @Primary - public CacheManager defaultCacheManager(RedisConnectionFactory redisConnectionFactory){ + public CacheManager defaultCacheManager(RedisConnectionFactory redisConnectionFactory) { return RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .build(); + .fromConnectionFactory(redisConnectionFactory) + .build(); } @Bean("stationCacheManager") public CacheManager stationCacheManager(RedisConnectionFactory redisConnectionFactory) { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.activateDefaultTyping( - LaissezFaireSubTypeValidator.instance, - ObjectMapper.DefaultTyping.NON_FINAL + LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL ); // ํƒ€์ž… ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜์—ฌ ์—ญ์ง๋ ฌํ™”ํ•  ๋•Œ ์›๋ณธ ํƒ€์ž…์„ ์œ ์ง€ // Jackson Serializer ์„ค์ • - GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper); + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer( + objectMapper); // RedisCacheConfiguration ์„ค์ • RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) - .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 3์ผ + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(serializer)) + .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 3์ผ return RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration) - .build(); + .cacheDefaults(redisCacheConfiguration) + .build(); } @Bean("publicTransportationCacheManager") - public CacheManager publicTransportationCacheManager(RedisConnectionFactory redisConnectionFactory) { + public CacheManager publicTransportationCacheManager( + RedisConnectionFactory redisConnectionFactory) { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.deactivateDefaultTyping(); - GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); + GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer( + objectMapper); // RedisCacheConfiguration ์„ค์ • RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) - .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) + .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ return RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration) - .build(); + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java index a28fee9d..8412c3b2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java @@ -14,7 +14,8 @@ public class RedisConfig { // ์ถ”ํ›„ ConnectionFactory์„ค์ • ๋ณ€๊ฒฝ์„ ๊ณ ๋ ค. (Sentinel, Cluster, etc...) @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); diff --git a/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java index 27f9b423..dcd65704 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java @@ -1,9 +1,9 @@ package com.kok.kokapi.config.swagger; -import org.springdoc.core.models.GroupedOpenApi; -import org.springframework.context.annotation.Bean; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @@ -12,18 +12,18 @@ public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() - .info(new Info() - .title("KOK") - .version("1.0") - .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")); + .info(new Info() + .title("KOK") + .version("1.0") + .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")); } @Bean public GroupedOpenApi v1Api() { return GroupedOpenApi.builder() - .group("V1 API") // Swagger UI์—์„œ "V1 API" ๊ทธ๋ฃน์œผ๋กœ ํ‘œ์‹œ - .pathsToMatch("/v1/api/**") // v1 ๊ด€๋ จ API๋งŒ ํฌํ•จ - .build(); + .group("V1 API") // Swagger UI์—์„œ "V1 API" ๊ทธ๋ฃน์œผ๋กœ ํ‘œ์‹œ + .pathsToMatch("/v1/api/**") // v1 ๊ด€๋ จ API๋งŒ ํฌํ•จ + .build(); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java index b46c16cd..cc81a5e3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java @@ -10,10 +10,10 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOriginPatterns("*") - .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true) - .maxAge(3600); + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java index 259ab104..2c7d5a6d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java +++ b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java @@ -1,8 +1,8 @@ package com.kok.kokapi.monitoring.adapter.in.web; +import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.monitoring.application.service.HealthCheckService; -import com.kok.kokapi.common.response.ApiResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java deleted file mode 100644 index f4aad574..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.kok.kokapi.public_transportation.adapter.in.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -public record RouteRequest( - @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String roomId, - @NotNull(message = "Member ID(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String memberId -) { -} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java new file mode 100644 index 00000000..83f631df --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java @@ -0,0 +1,13 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record RouteRequest( + @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String roomId, + @NotNull(message = "Member ID(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String memberId +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapComplexPublicTransportationParsedResponse.java similarity index 92% rename from kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java rename to kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapComplexPublicTransportationParsedResponse.java index 9c3166fa..d6594f85 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapComplexPublicTransportationParsedResponse.java @@ -1,11 +1,10 @@ -package com.kok.kokapi.public_transportation.adapter.in.dto; +package com.kok.kokapi.public_transportation.adapter.in.dto.response; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.Setter; - import java.io.Serializable; import java.util.List; +import lombok.Getter; +import lombok.Setter; @Getter @Setter @@ -13,10 +12,12 @@ public class TmapComplexPublicTransportationParsedResponse implements Serializable { private ParsedItinerary parsedItinerary; + // DTO ํด๋ž˜์Šค ์ •์˜ @Getter @Setter public static class ParsedItinerary { + private int totalDistance; private int totalTime; private List legs; @@ -25,6 +26,7 @@ public static class ParsedItinerary { @Getter @Setter public static class ParsedLeg { + private String mode; private int distance; private int sectionTime; diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapPublicTransportationParsedResponse.java similarity index 56% rename from kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java rename to kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapPublicTransportationParsedResponse.java index 8a880616..bbf2ab9a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapPublicTransportationParsedResponse.java @@ -1,10 +1,12 @@ -package com.kok.kokapi.public_transportation.adapter.in.dto; +package com.kok.kokapi.public_transportation.adapter.in.dto.response; public record TmapPublicTransportationParsedResponse( - Integer totalTime, - Integer transferCount + Integer totalTime, + Integer transferCount ) { - public static TmapPublicTransportationParsedResponse of(Integer totalTime, Integer transferCount) { + + public static TmapPublicTransportationParsedResponse of(Integer totalTime, + Integer transferCount) { return new TmapPublicTransportationParsedResponse(totalTime, transferCount); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java index 3b8f7725..8e0056aa 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java @@ -4,14 +4,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; -import com.kok.kokapi.public_transportation.adapter.in.dto.RouteRequest; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapPublicTransportationParsedResponse; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse; -import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUseCase; +import com.kok.kokapi.public_transportation.adapter.in.dto.request.RouteRequest; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; +import com.kok.kokcore.public_transportation.usecase.RetrievePublicTransportationUseCase; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; @V1Controller @@ -23,10 +25,13 @@ public class PublicTransportationController { @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") @PostMapping("/route/{stationId}") - public ResponseEntity> getPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { + public ResponseEntity> getPublicTransportation( + @PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { try { - TmapPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.roomId(), routeRequest.memberId()) - ,TmapPublicTransportationParsedResponse.class); + TmapPublicTransportationParsedResponse publicTransportation = objectMapper.readValue( + retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, + routeRequest.roomId(), routeRequest.memberId()) + , TmapPublicTransportationParsedResponse.class); return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); } catch (JsonProcessingException e) { throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); @@ -35,10 +40,13 @@ public ResponseEntity> ge @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์ „๋ฌธ ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") @PostMapping("/route/complex/{stationId}") - public ResponseEntity> getComplexPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { + public ResponseEntity> getComplexPublicTransportation( + @PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { try { - TmapComplexPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrieveComplexPublicTransportation(stationId, routeRequest.roomId(), routeRequest.memberId()) - , TmapComplexPublicTransportationParsedResponse.class); + TmapComplexPublicTransportationParsedResponse publicTransportation = objectMapper.readValue( + retrievePublicTransportationUsecase.retrieveComplexPublicTransportation(stationId, + routeRequest.roomId(), routeRequest.memberId()) + , TmapComplexPublicTransportationParsedResponse.class); return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); } catch (JsonProcessingException e) { throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java index 89f89a39..bb658060 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java @@ -2,10 +2,13 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; @@ -16,10 +19,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; - @Component @EnableConfigurationProperties(TmapClientProperties.class) @@ -33,7 +32,9 @@ public class PublicTransportationClient { private final ReadLocationPort readLocationPort; private final PointConverter pointConverter; - public PublicTransportationClient(TmapClientProperties properties, RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, PointConverter pointConverter) { + public PublicTransportationClient(TmapClientProperties properties, + RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, + PointConverter pointConverter) { this.properties = properties; this.retrieveStationsPort = retrieveStationsPort; this.readLocationPort = readLocationPort; @@ -43,10 +44,10 @@ public PublicTransportationClient(TmapClientProperties properties, RetrieveStati public RestClient getRestClient() { return RestClient.builder() - .requestFactory(getRequestFactory()) - .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ - .baseUrl(properties.url()) // Base URL ์„ค์ • - .build(); + .requestFactory(getRequestFactory()) + .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ + .baseUrl(properties.url()) // Base URL ์„ค์ • + .build(); } public RestClient getClient() { @@ -55,26 +56,28 @@ public RestClient getClient() { private ClientHttpRequestFactory getRequestFactory() { return ClientHttpRequestFactoryBuilder.detect() - .build(ClientHttpRequestFactorySettings.defaults()); + .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String roomId, String memberId) { + public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String roomId, + String memberId) { log.info("Tmap api call : {}-{}-{}", stationId, roomId, memberId); return getClient().post() - .body(buildRequestBody( - getUserLocation(roomId, memberId), - getStation(stationId))) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); - }) - .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); - }) - .body(TmapPublicTransportationResponse.class); + .body(buildRequestBody( + getUserLocation(roomId, memberId), + getStation(stationId))) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); + }) + .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); + }) + .body(TmapPublicTransportationResponse.class); } - private Map buildRequestBody(Pair userLocation, Station station) { + private Map buildRequestBody(Pair userLocation, + Station station) { Map requestBody = new HashMap<>(); requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ @@ -87,12 +90,12 @@ private Map buildRequestBody(Pair userLo private Pair getUserLocation(String roomId, String memberId) { Location userPoint = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } private Station getStation(Long stationId) { return retrieveStationsPort.retrieveStation(stationId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java index 8bfd0cbf..5419d3c9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java @@ -2,10 +2,13 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; @@ -16,14 +19,11 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; - @Component @EnableConfigurationProperties(TmapComplexClientProperties.class) @Slf4j public class PublicTransportationComplexClient { + private final RestClient restClient; private final TmapComplexClientProperties properties; @@ -31,7 +31,9 @@ public class PublicTransportationComplexClient { private final ReadLocationPort readLocationPort; private final PointConverter pointConverter; - public PublicTransportationComplexClient(TmapComplexClientProperties properties, RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, PointConverter pointConverter) { + public PublicTransportationComplexClient(TmapComplexClientProperties properties, + RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, + PointConverter pointConverter) { this.properties = properties; this.retrieveStationsPort = retrieveStationsPort; this.readLocationPort = readLocationPort; @@ -41,10 +43,10 @@ public PublicTransportationComplexClient(TmapComplexClientProperties properties, public RestClient getRestClient() { return RestClient.builder() - .requestFactory(getRequestFactory()) - .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ - .baseUrl(properties.url()) // Base URL ์„ค์ • - .build(); + .requestFactory(getRequestFactory()) + .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ + .baseUrl(properties.url()) // Base URL ์„ค์ • + .build(); } public RestClient getClient() { @@ -53,37 +55,39 @@ public RestClient getClient() { private ClientHttpRequestFactory getRequestFactory() { return ClientHttpRequestFactoryBuilder.detect() - .build(ClientHttpRequestFactorySettings.defaults()); + .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, String roomId, String memberId){ + public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, + String roomId, String memberId) { log.info("Tmap api call : {}-{}-{}", stationId, roomId, memberId); return getClient().post() - .body(buildRequestBody( - getUserLocation(roomId, memberId), - getStation(stationId))) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); - }) - .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); - }) - .body(TmapComplexPublicTransportationResponse.class); + .body(buildRequestBody( + getUserLocation(roomId, memberId), + getStation(stationId))) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); + }) + .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); + }) + .body(TmapComplexPublicTransportationResponse.class); } private Pair getUserLocation(String roomId, String memberId) { Location userPoint = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } private Station getStation(Long stationId) { return retrieveStationsPort.retrieveStation(stationId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); } - private Map buildRequestBody(Pair userLocation, Station station) { + private Map buildRequestBody(Pair userLocation, + Station station) { Map requestBody = new HashMap<>(); requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java index bfc502bf..c9c5b3b1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java @@ -4,8 +4,9 @@ @ConfigurationProperties(prefix = "tmap-sub") public record TmapClientProperties( - String key, - String url, - String keyname -){ + String key, + String url, + String keyname +) { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java index 650785bf..812e2841 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java @@ -4,8 +4,9 @@ @ConfigurationProperties(prefix = "tmap-complex") public record TmapComplexClientProperties( - String key, - String url, - String keyname + String key, + String url, + String keyname ) { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java index b3f8e5a4..f14bf2c0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java @@ -2,21 +2,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Getter; import lombok.Setter; -import java.util.List; - @Getter @Setter @JsonIgnoreProperties(ignoreUnknown = true) public class TmapComplexPublicTransportationResponse { + @JsonProperty("metaData") private MetaData metaData; @Getter @Setter public static class MetaData { + @JsonProperty("requestParameters") private RequestParameters requestParameters; @@ -27,6 +28,7 @@ public static class MetaData { @Getter @Setter public static class RequestParameters { + private int busCount; private int expressbusCount; private int subwayCount; @@ -46,6 +48,7 @@ public static class RequestParameters { @Getter @Setter public static class Plan { + @JsonProperty("itineraries") private List itineraries; } @@ -53,6 +56,7 @@ public static class Plan { @Getter @Setter public static class Itinerary { + @JsonProperty("fare") private Fare fare; @@ -68,6 +72,7 @@ public static class Itinerary { @Getter @Setter public static class Fare { + @JsonProperty("regular") private RegularFare regular; } @@ -75,6 +80,7 @@ public static class Fare { @Getter @Setter public static class RegularFare { + private int totalFare; private Currency currency; } @@ -82,6 +88,7 @@ public static class RegularFare { @Getter @Setter public static class Currency { + private String symbol; private String currency; private String currencyCode; @@ -90,6 +97,7 @@ public static class Currency { @Getter @Setter public static class Leg { + private String mode; private int sectionTime; private int distance; @@ -108,6 +116,7 @@ public static class Leg { @Getter @Setter public static class Location { + private String name; private double lon; private double lat; @@ -116,6 +125,7 @@ public static class Location { @Getter @Setter public static class Step { + private String streetName; private int distance; private String description; @@ -125,12 +135,14 @@ public static class Step { @Getter @Setter public static class PassStopList { + private List stationList; } @Getter @Setter public static class Station { + private int index; private String stationName; private String lon; @@ -141,6 +153,7 @@ public static class Station { @Getter @Setter public static class PassShape { + private String linestring; } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java index e243edc7..28c2a1e8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java @@ -1,11 +1,10 @@ package com.kok.kokapi.public_transportation.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; import lombok.Getter; import lombok.Setter; -import java.util.List; - // Tmap ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ POJO @Getter @Setter @@ -22,6 +21,7 @@ public class TmapPublicTransportationResponse { @Getter @Setter public static class MetaData { + private RequestParameters requestParameters; private Plan plan; @@ -30,6 +30,7 @@ public static class MetaData { @Getter @Setter public static class RequestParameters { + private String endY; private String endX; private String startY; @@ -41,6 +42,7 @@ public static class RequestParameters { @Getter @Setter public static class Plan { + private List itineraries; } @@ -48,6 +50,7 @@ public static class Plan { @Getter @Setter public static class Itinerary { + private Fare fare; private int totalTime; private int totalWalkTime; @@ -61,6 +64,7 @@ public static class Itinerary { @Getter @Setter public static class Fare { + private RegularFare regular; } @@ -68,6 +72,7 @@ public static class Fare { @Getter @Setter public static class RegularFare { + private int totalFare; private Currency currency; @@ -76,6 +81,7 @@ public static class RegularFare { @Getter @Setter public static class Currency { + private String symbol; private String currency; private String currencyCode; diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 9bba411e..827ef2f0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -1,25 +1,25 @@ package com.kok.kokapi.public_transportation.application.service; +import static com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse.ParsedItinerary; +import static com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse.ParsedLeg; +import static com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse.Itinerary; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapPublicTransportationParsedResponse; -import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationComplexClient; +import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; -import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUseCase; +import com.kok.kokcore.public_transportation.usecase.RetrievePublicTransportationUseCase; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.stream.Collectors; - -import static com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse.*; -import static com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse.*; - @Service @RequiredArgsConstructor @Slf4j @@ -33,7 +33,8 @@ public class TmapPublicTransportationService implements RetrievePublicTransporta @Cacheable(value = "sub", cacheManager = "publicTransportationCacheManager", key = "'PTSubCache:' + #stationId + '-' + #roomId + '-' + #memberId") @Override public String retrievePublicTransportation(Long stationId, String roomId, String memberId) { - TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute(stationId, roomId, memberId); + TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute( + stationId, roomId, memberId); try { return objectMapper.writeValueAsString(parseTmapResponse(rawRoute)); } catch (JsonProcessingException e) { @@ -43,8 +44,10 @@ public String retrievePublicTransportation(Long stationId, String roomId, String @Cacheable(value = "complex", cacheManager = "publicTransportationCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #roomId + '-' + #memberId") @Override - public String retrieveComplexPublicTransportation(Long stationId, String roomId, String memberId) { - TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute(stationId, roomId, memberId); + public String retrieveComplexPublicTransportation(Long stationId, String roomId, + String memberId) { + TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute( + stationId, roomId, memberId); try { return objectMapper.writeValueAsString(parseComplexTmapResponse(rawRoute)); } catch (JsonProcessingException e) { @@ -52,9 +55,12 @@ public String retrieveComplexPublicTransportation(Long stationId, String roomId, } } - public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse(TmapComplexPublicTransportationResponse response) { - if (response == null || response.getMetaData() == null || response.getMetaData().getPlan() == null - || response.getMetaData().getPlan().getItineraries() == null || response.getMetaData().getPlan().getItineraries().isEmpty()) { + public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse( + TmapComplexPublicTransportationResponse response) { + if (response == null || response.getMetaData() == null + || response.getMetaData().getPlan() == null + || response.getMetaData().getPlan().getItineraries() == null || response.getMetaData() + .getPlan().getItineraries().isEmpty()) { return null; } @@ -80,13 +86,16 @@ public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse(Tm return parsedResponse; } - public TmapPublicTransportationParsedResponse parseTmapResponse(TmapPublicTransportationResponse response) { - if (response == null || response.getMetaData() == null || response.getMetaData().getPlan() == null || response.getMetaData().getPlan().getItineraries() == null ) { + public TmapPublicTransportationParsedResponse parseTmapResponse( + TmapPublicTransportationResponse response) { + if (response == null || response.getMetaData() == null + || response.getMetaData().getPlan() == null + || response.getMetaData().getPlan().getItineraries() == null) { return null; } return TmapPublicTransportationParsedResponse.of( - response.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), - response.getMetaData().getPlan().getItineraries().getFirst().getTransferCount()); + response.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), + response.getMetaData().getPlan().getItineraries().getFirst().getTransferCount()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java index 54e42dd1..50f72880 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java @@ -7,17 +7,19 @@ import jakarta.validation.constraints.Size; public record CreateRoomRequest( - @NotBlank(message = "๋ฐฉ ์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") - @Size(max = 30, message = "๋ฐฉ ์ด๋ฆ„์€ ์ตœ๋Œ€ 30์ž๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") - String roomName, + @NotBlank(message = "๋ฐฉ ์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + @Size(max = 30, message = "๋ฐฉ ์ด๋ฆ„์€ ์ตœ๋Œ€ 30์ž๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") + String roomName, - @Min(value = 2, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - @Max(value = 15, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ๋Œ€ 15๋ช…๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") - Integer capacity, + @Min(value = 2, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Max(value = 15, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ๋Œ€ 15๋ช…๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") + Integer capacity, - @NotBlank(message = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") - String hostProfile, + @NotBlank(message = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") + String hostProfile, - @NotBlank(message = "๋‹‰๋„ค์ž„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") - String hostNickname -) {} + @NotBlank(message = "๋‹‰๋„ค์ž„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + String hostNickname +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java index 82c15d9c..b8b94c47 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java @@ -4,10 +4,12 @@ import jakarta.validation.constraints.NotBlank; public record JoinRoomParticipantRequest( - @NotBlank(message = "ํ”„๋กœํ•„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String profile, + @NotBlank(message = "ํ”„๋กœํ•„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String profile, - @NotBlank(message = "๋‹‰๋„ค์ž„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String nickname -) {} + @NotBlank(message = "๋‹‰๋„ค์ž„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String nickname +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java new file mode 100644 index 00000000..2d5aa2c2 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java @@ -0,0 +1,24 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Room; + +public record CreateRoomResponse( + String id, + String roomName, + int capacity, + MemberResponse member, + int participantCount, + int nonParticipantCount +) { + + public static CreateRoomResponse of(Room room, int participantCount, int nonParticipantCount) { + return new CreateRoomResponse( + room.getId(), + room.getRoomName(), + room.getCapacity(), + MemberResponse.from(room.getMember()), + participantCount, + nonParticipantCount + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java index 244859e1..22a987ce 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java @@ -1,10 +1,12 @@ package com.kok.kokapi.room.adapter.in.dto.response; public record JoinRoomResponse( - String id, - String profile, - String nickname, - int participantCount, - int nonParticipantCount -) { } + String id, + String profile, + String nickname, + int participantCount, + int nonParticipantCount +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java index 922e5ac7..a18e8008 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java @@ -4,16 +4,17 @@ import com.kok.kokcore.room.domain.vo.MemberRole; public record MemberResponse( - String id, - String nickname, - String profile, - MemberRole role + String id, + String nickname, + String profile, + MemberRole role ) { + public static MemberResponse from(Member member) { return new MemberResponse( - member.getMemberId(), - member.getNickname(), - member.getProfile(), - member.getRole()); + member.getMemberId(), + member.getNickname(), + member.getProfile(), + member.getRole()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java index 4dd44a50..dbfea7d8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java @@ -1,6 +1,8 @@ package com.kok.kokapi.room.adapter.in.dto.response; public record RandomProfileResponse( - String imageUrl, - String nickname -){} + String imageUrl, + String nickname +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java deleted file mode 100644 index 3894841c..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.kok.kokapi.room.adapter.in.dto.response; - -import com.kok.kokcore.room.domain.Room; - -public record RoomCreateResponse( - String id, - String roomName, - int capacity, - MemberResponse member, - int participantCount, - int nonParticipantCount -) { - public static RoomCreateResponse from(Room room, int participantCount, int nonParticipantCount) { - return new RoomCreateResponse( - room.getId(), - room.getRoomName(), - room.getCapacity(), - MemberResponse.from(room.getMember()), - participantCount, - nonParticipantCount - ); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java index 75339512..14fe2f25 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java @@ -3,8 +3,10 @@ import com.kok.kokcore.room.domain.vo.MemberRole; public record RoomMembersResponse( - String memberId, - String profile, - String nickname, - MemberRole role -) {} + String memberId, + String profile, + String nickname, + MemberRole role +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index d565cc64..d8efb62c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -4,8 +4,8 @@ import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; +import com.kok.kokapi.room.adapter.in.dto.response.CreateRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomCreateResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponses; import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; @@ -55,7 +55,7 @@ public ResponseEntity> getRoomStatus( @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒ์„ฑ", description = "์ƒˆ๋กœ์šด ์•ฝ์†๋ฐฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms") - public ResponseEntity> createRoom( + public ResponseEntity> createRoom( @Valid @RequestBody CreateRoomRequest request) { String nickname = request.hostNickname(); String profile = request.hostProfile(); @@ -67,7 +67,7 @@ public ResponseEntity> createRoom( host ); - RoomCreateResponse response = RoomCreateResponse.from(room, 1, room.getCapacity() - 1); + CreateRoomResponse response = CreateRoomResponse.of(room, 1, room.getCapacity() - 1); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponseDto.success(response)); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java index 041845e9..a4ecc9cb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java @@ -21,8 +21,8 @@ public class RoomProfileController { public ResponseEntity> getRandomProfile() { Profile profile = createRandomProfileUseCase.createProfile(); RandomProfileResponse response = new RandomProfileResponse( - profile.getImageUrl(), - profile.getNickname() + profile.getImageUrl(), + profile.getNickname() ); return ResponseEntity.ok(ApiResponseDto.success(response)); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java index 76ca63f0..00b8010c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java @@ -1,6 +1,6 @@ package com.kok.kokapi.room.adapter.out.persistence; -import com.kok.kokcore.room.application.port.out.LoadRoomParticipantPort; +import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java index 8be7f9ae..178f0408 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java @@ -2,8 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.port.out.SaveRoomParticipantsPort; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java index 818029ae..d0d6b786 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java @@ -2,8 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.LoadRoomPort; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.LoadRoomPort; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java index 411e32af..7b882e84 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java @@ -2,8 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.SaveRoomPort; import java.time.Duration; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java index 86da3675..1fd2142b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java @@ -2,13 +2,12 @@ import com.kok.kokcore.room.domain.Profile; import com.kok.kokcore.room.usecase.CreateRandomProfileUseCase; +import java.util.List; +import java.util.Random; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.Random; - @Service @RequiredArgsConstructor public class RandomProfileService implements CreateRandomProfileUseCase { @@ -17,13 +16,13 @@ public class RandomProfileService implements CreateRandomProfileUseCase { private String objectStorageUrl; private static final List ADJECTIVES = List.of( - "๋ฐฐ๊ณ ํ”ˆ", "๋ฉ‹์žˆ๋Š”", "์ฟจํ•œ ", "๋น ๋ฅธ", "์นœ์ ˆํ•œ", "์˜๋ฆฌํ•œ", "ํ–‰๋ณตํ•œ", "์กฐ์šฉํ•œ", "๊ฐ•๋ ฅํ•œ", "์ž์œ ๋กœ์šด", - "์Šค๋งˆํŠธํ•œ", "์ž˜์ƒ๊ธด", "๊ท€์—ฌ์šด", "ํ‰ํ™”๋กœ์šด", "๋น›๋‚˜๋Š”", "๋˜‘๋˜‘ํ•œ", "์‹ ๋‚˜๋Š”", "๋ถ€๋“œ๋Ÿฌ์šด", "์—‰๋šฑํ•œ", "๋ฌด์„œ์šด" + "๋ฐฐ๊ณ ํ”ˆ", "๋ฉ‹์žˆ๋Š”", "์ฟจํ•œ ", "๋น ๋ฅธ", "์นœ์ ˆํ•œ", "์˜๋ฆฌํ•œ", "ํ–‰๋ณตํ•œ", "์กฐ์šฉํ•œ", "๊ฐ•๋ ฅํ•œ", "์ž์œ ๋กœ์šด", + "์Šค๋งˆํŠธํ•œ", "์ž˜์ƒ๊ธด", "๊ท€์—ฌ์šด", "ํ‰ํ™”๋กœ์šด", "๋น›๋‚˜๋Š”", "๋˜‘๋˜‘ํ•œ", "์‹ ๋‚˜๋Š”", "๋ถ€๋“œ๋Ÿฌ์šด", "์—‰๋šฑํ•œ", "๋ฌด์„œ์šด" ); private static final List NOUNS = List.of( - "ํ† ๋ฏธ", "์ง€๋ฏธ", "๋ผ์ด์–ธ", "๋ฃจ์นด์Šค", "๋ฐ์ด๋น—", "์— ๋งˆ", "์˜ฌ๋ฆฌ๋ฒ„", "์†Œํ”ผ", "๋ด‰๋ด‰์ด", "ํ”ผ์น˜", - "๋ฌด์ง€", "ํŠœ๋ธŒ", "ํ”„๋กœ๋„", "์ฝ˜", "๋ธŒ๋ผ์šด", "์ฝ”์ฝ”", "ํ”„๋ Œ์ฆˆ", "ํ† ๋ผ", "ํŽญ์ˆ˜", "๋ฝ€๋กœ๋กœ" + "ํ† ๋ฏธ", "์ง€๋ฏธ", "๋ผ์ด์–ธ", "๋ฃจ์นด์Šค", "๋ฐ์ด๋น—", "์— ๋งˆ", "์˜ฌ๋ฆฌ๋ฒ„", "์†Œํ”ผ", "๋ด‰๋ด‰์ด", "ํ”ผ์น˜", + "๋ฌด์ง€", "ํŠœ๋ธŒ", "ํ”„๋กœ๋„", "์ฝ˜", "๋ธŒ๋ผ์šด", "์ฝ”์ฝ”", "ํ”„๋ Œ์ฆˆ", "ํ† ๋ผ", "ํŽญ์ˆ˜", "๋ฝ€๋กœ๋กœ" ); @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java index 23224550..19cba513 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java @@ -1,8 +1,8 @@ package com.kok.kokapi.room.application.service; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.SaveRoomPort; import com.kok.kokcore.room.usecase.CreateRoomUseCase; import com.kok.kokcore.room.usecase.JoinRoomUseCase; import lombok.RequiredArgsConstructor; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java index 81c4b800..38085c63 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java @@ -1,7 +1,7 @@ package com.kok.kokapi.room.application.service; -import com.kok.kokcore.room.application.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.usecase.JoinRoomUseCase; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -10,6 +10,7 @@ @Service @RequiredArgsConstructor public class RoomParticipantService implements JoinRoomUseCase { + private final SaveRoomParticipantsPort saveRoomParticipantsPort; @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index 060b5961..4834c775 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -2,10 +2,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.LoadRoomParticipantPort; -import com.kok.kokcore.room.application.port.out.LoadRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; +import com.kok.kokcore.room.port.out.LoadRoomPort; import com.kok.kokcore.room.usecase.GetRoomUseCase; import java.util.ArrayList; import java.util.List; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java index e1925816..5d3f15d2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java @@ -2,18 +2,18 @@ import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; - import java.util.List; public record RecommendedStationResponse( - List routes, - Station station + List routes, + Station station ) { + public static RecommendedStationResponse of(Station station, List routes) { return new RecommendedStationResponse( - routes.stream().map(Route::getName).toList(), - station + routes.stream().map(Route::getName).toList(), + station ); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index cec4f5d9..cb332abf 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -3,16 +3,15 @@ import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; -import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; -import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.usecase.RecommendStationUseCase; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import io.swagger.v3.oas.annotations.Operation; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import java.util.List; - @V1Controller @RequiredArgsConstructor public class StationController { @@ -22,12 +21,15 @@ public class StationController { @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ์ถ”์ฒœ", description = "Recommend subway stations based on the user's location.") @GetMapping("/stations/recommend/{roomId}") - public ResponseEntity>> recommendStations(@PathVariable String roomId) { - - List recommendedStations = recommendStationUseCase.recommendStations(roomId).stream() - .map(station -> - RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) - .toList(); + public ResponseEntity>> recommendStations( + @PathVariable String roomId) { + + List recommendedStations = recommendStationUseCase.recommendStations( + roomId).stream() + .map(station -> + RecommendedStationResponse.of(station, + retrieveRouteUseCase.retrieveRoutes(station))) + .toList(); return ResponseEntity.ok(ApiResponseDto.success(recommendedStations)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java index eac61006..fdb022cd 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java @@ -1,8 +1,8 @@ package com.kok.kokapi.station.adapter.out.external; import com.kok.kokapi.station.adapter.out.external.dto.StationResponses; -import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.port.out.LoadStationsPort; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; import java.util.StringJoiner; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -23,7 +23,8 @@ public class StationClient implements LoadStationsPort { private final StationClientProperties properties; private final StationErrorHandler stationErrorHandler; - public StationClient(StationClientProperties properties, StationErrorHandler stationErrorHandler) { + public StationClient(StationClientProperties properties, + StationErrorHandler stationErrorHandler) { this.properties = properties; this.stationErrorHandler = stationErrorHandler; this.restClient = getRestClient(); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java index 408e4d2d..f4487e65 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java @@ -28,7 +28,8 @@ public boolean hasError(ClientHttpResponse response) throws IOException { } @Override - public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException { + public void handleError(URI url, HttpMethod method, ClientHttpResponse response) + throws IOException { throw new RuntimeException("์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ง€์†์ ์œผ๋กœ ๋ฐœ์ƒ์‹œ ์—ด๋ฆฐ ๋ฐ์ดํ„ฐ ๊ด‘์žฅ์œผ๋กœ ๋ฌธ์˜(Q&A) ๋ฐ”๋ž๋‹ˆ๋‹ค."); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java index 24245da5..9c65262e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java @@ -1,7 +1,7 @@ package com.kok.kokapi.station.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDto; +import com.kok.kokcore.station.port.out.dto.StationRouteDto; public record StationResponse( @JsonProperty("BLDN_ID") diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java index c77c4dd0..3c9ce15e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java @@ -1,7 +1,7 @@ package com.kok.kokapi.station.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; public record StationResponses( @JsonProperty("subwayStationMaster") diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java index 07ed02fe..55232744 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java @@ -1,12 +1,11 @@ package com.kok.kokapi.station.adapter.out.persistence; -import com.kok.kokcore.station.application.port.out.RetrieveRoutePort; -import com.kok.kokcore.station.application.port.out.SaveRoutePort; import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveRoutePort; +import com.kok.kokcore.station.port.out.SaveRoutePort; import java.util.List; import java.util.function.Function; - -import com.kok.kokcore.station.domain.entity.Station; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java index 6f04c243..0fdf9a71 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java @@ -2,9 +2,8 @@ import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; -import org.springframework.data.jpa.repository.JpaRepository; - import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; public interface RouteRepository extends JpaRepository { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 31f247a7..4af94f19 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -1,15 +1,14 @@ package com.kok.kokapi.station.adapter.out.persistence; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokcore.station.application.port.out.ReadStationsPort; -import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; -import com.kok.kokcore.station.application.port.out.SaveStationsPort; import com.kok.kokcore.station.domain.entity.Station; - +import com.kok.kokcore.station.port.out.ReadStationsPort; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; +import com.kok.kokcore.station.port.out.SaveStationsPort; import java.math.BigDecimal; -import java.util.*; +import java.util.List; +import java.util.Optional; import java.util.function.Function; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.locationtech.jts.geom.Point; @@ -22,7 +21,8 @@ @Repository @Slf4j @RequiredArgsConstructor -public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort, RetrieveStationsPort { +public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort, + RetrieveStationsPort { private final PointConverter pointConverter; @@ -76,7 +76,8 @@ public Optional retrieveStation(Long stationId) { @Transactional(readOnly = true) public List retrieveInRangeStations(Point centroid, double dist) { Pair lonLat = pointConverter.toCoordinates(centroid); - return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), lonLat.getSecond(), dist); + return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), + lonLat.getSecond(), dist); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index afd03ab8..78ef4547 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -1,16 +1,13 @@ package com.kok.kokapi.station.adapter.out.persistence; import com.kok.kokcore.station.domain.entity.Station; - +import io.lettuce.core.dynamic.annotation.Param; import java.math.BigDecimal; import java.util.List; - -import io.lettuce.core.dynamic.annotation.Param; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.Optional; - public interface StationRepository extends JpaRepository { @Query("SELECT EXISTS (SELECT 1 FROM Station)") @@ -21,10 +18,11 @@ public interface StationRepository extends JpaRepository { Optional findStationById(Long stationId); @Query(value = """ - SELECT * FROM station - WHERE ST_Distance_Sphere(Point(longitude, latitude), Point(:lon, :lat)) < :distance - AND priority > 0 - """, nativeQuery = true) - List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, @Param("lat") BigDecimal lat, @Param("distance") Double distance); + SELECT * FROM station + WHERE ST_Distance_Sphere(Point(longitude, latitude), Point(:lon, :lat)) < :distance + AND priority > 0 + """, nativeQuery = true) + List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, + @Param("lat") BigDecimal lat, @Param("distance") Double distance); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java index 56eb194e..23003da3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java @@ -1,14 +1,13 @@ package com.kok.kokapi.station.application.service; -import com.kok.kokcore.station.application.port.out.RetrieveRoutePort; -import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveRoutePort; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class RouteService implements RetrieveRouteUseCase { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index 68747ce2..d1c4cf40 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -1,16 +1,22 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokcore.location.application.port.out.ReadCentroidPort; -import com.kok.kokcore.station.application.port.out.*; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; -import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; -import com.kok.kokcore.station.application.usecase.SaveStationUseCase; +import com.kok.kokcore.location.port.out.ReadCentroidPort; import com.kok.kokcore.station.domain.entity.Station; - +import com.kok.kokcore.station.port.out.LoadStationsPort; +import com.kok.kokcore.station.port.out.ReadStationsPort; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; +import com.kok.kokcore.station.port.out.SaveRoutePort; +import com.kok.kokcore.station.port.out.SaveStationsPort; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.usecase.RecommendStationUseCase; +import com.kok.kokcore.station.usecase.SaveStationUseCase; import java.math.BigDecimal; -import java.util.*; - +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.locationtech.jts.geom.Point; @@ -35,7 +41,7 @@ public class StationService implements SaveStationUseCase, RecommendStationUseCa @Override @Transactional public void saveStations() { - if(readStationsPort.hasNoStations()) { + if (readStationsPort.hasNoStations()) { StationRouteDtos stationRouteDtos = loadStationsPort.loadAllStations(); List stations = saveStationsPort.saveStations(stationRouteDtos.toStations()); saveRoutePort.saveRoutes(stationRouteDtos.toRoutesByStations(stations)); @@ -43,7 +49,7 @@ public void saveStations() { } @Override - @Cacheable(value = "recommendStations",cacheManager = "stationCacheManager", key = "#roomId") + @Cacheable(value = "recommendStations", cacheManager = "stationCacheManager", key = "#roomId") public List recommendStations(String roomId) { Point centroid = readCentroidPort.findCentroidByRoomId(roomId); int RECOMMEND_NUM = 2; @@ -79,7 +85,9 @@ private Map calculateProbabilities(List stations, Poin for (Station station : stations) { double distance = calculateDistance(centroid, station); - if (distance == 0) distance = Double.MIN_VALUE; // 0 ๊ฑฐ๋ฆฌ ๋ฐฉ์ง€ + if (distance == 0) { + distance = Double.MIN_VALUE; // 0 ๊ฑฐ๋ฆฌ ๋ฐฉ์ง€ + } double weight = (1 / distance) * station.getPriority(); weightedDistances.put(station, weight); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java b/kok-api/src/main/java/com/kok/kokapi/station/config/StationsConfig.java similarity index 89% rename from kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java rename to kok-api/src/main/java/com/kok/kokapi/station/config/StationsConfig.java index b424df1b..68ffde0c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/config/StationsConfig.java @@ -1,4 +1,4 @@ -package com.kok.kokapi.station.application.config; +package com.kok.kokapi.station.config; import com.kok.kokapi.station.application.service.StationService; import org.springframework.boot.CommandLineRunner; diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index f73e280c..0e9064c7 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -38,10 +38,10 @@ station: format: json start-idx: 1 end-idx: 1000 - + ncp: object-storage-url: ${OBJECT_STORAGE_URL} - + tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index d6528d84..eb2d8734 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -51,7 +51,7 @@ station: ncp: object-storage-url: ${OBJECT_STORAGE_URL} - + tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-api/src/main/resources/db/migration/V1__init.sql b/kok-api/src/main/resources/db/migration/V1__init.sql index e76d562f..6c325391 100644 --- a/kok-api/src/main/resources/db/migration/V1__init.sql +++ b/kok-api/src/main/resources/db/migration/V1__init.sql @@ -20,10 +20,10 @@ CREATE TABLE route create table location ( - id bigint auto_increment primary key, - member_id varchar(255) not null, - location_point point not null, - room_id varchar(255) not null, + id bigint auto_increment primary key, + member_id varchar(255) not null, + location_point point not null, + room_id varchar(255) not null, constraint UKrgpajb4rsivb4gj9xn2qowgw6 unique (room_id, member_id) ); diff --git a/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java b/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java index f1d8c6e4..577fee89 100644 --- a/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java +++ b/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java @@ -7,8 +7,8 @@ @SpringBootTest class KokApiApplicationTests extends ServiceTest { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java index 4bae3a38..a3aa046d 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java @@ -16,13 +16,13 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) @Profile("test") -public abstract class IntegrationTest extends ContainerBaseTest{ +public abstract class IntegrationTest extends ContainerBaseTest { @LocalServerPort private int port; @BeforeEach - void setPort(){ + void setPort() { RestAssured.port = port; } } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java index 65a51dfe..b866a12c 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java @@ -16,6 +16,6 @@ @AutoConfigureTestDatabase(replace = Replace.NONE) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) @Profile("test") -public abstract class RepositoryTest extends ContainerBaseTest{ +public abstract class RepositoryTest extends ContainerBaseTest { } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java index 8be46beb..706ac3a3 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java @@ -16,7 +16,7 @@ @Import({StationTestConfiguration.class}) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) @Profile("test") -public abstract class ServiceTest extends ContainerBaseTest{ +public abstract class ServiceTest extends ContainerBaseTest { @MockitoBean protected PublicTransportationComplexClient publicTransportationComplexClient; diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java index a78ae39f..466db4f6 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java @@ -8,7 +8,7 @@ import org.springframework.transaction.annotation.Transactional; @Component -public class MySQLDatabaseCleaner implements DatabaseCleaner{ +public class MySQLDatabaseCleaner implements DatabaseCleaner { @PersistenceContext private EntityManager entityManager; diff --git a/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java b/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java index 034aac0b..52ab1321 100644 --- a/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java +++ b/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java @@ -1,7 +1,7 @@ package com.kok.kokapi.config; import com.kok.kokapi.station.adapter.out.external.FakeStationClient; -import com.kok.kokcore.station.application.port.out.LoadStationsPort; +import com.kok.kokcore.station.port.out.LoadStationsPort; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java index e338306f..18872ba1 100644 --- a/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java @@ -5,7 +5,7 @@ public class MemberFixture { - public static Member createFollower(){ + public static Member createFollower() { return new Member("follower", "profile.svg", MemberRole.FOLLOWER); } diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java index 2104d6a0..5bc11b39 100644 --- a/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java @@ -6,7 +6,7 @@ public class PointFixture { - public static Point create(){ + public static Point create() { GeometryFactory geometryFactory = new GeometryFactory(); Coordinate coordinate = new Coordinate(127.02758, 37.49794); diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java index d557d492..1e50a36a 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -7,8 +7,8 @@ import com.kok.kokapi.common.template.IntegrationTest; import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; +import com.kok.kokapi.room.adapter.in.dto.response.CreateRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomCreateResponse; import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.math.BigDecimal; @@ -23,7 +23,7 @@ class RoomIntegrationTest extends IntegrationTest { @DisplayName("์•ฝ์†๋ฐฉ ์‹œ๋‚˜๋ฆฌ์˜ค") @TestFactory Stream getRoomDetail() { - AtomicReference createRoomResponse = new AtomicReference<>(); + AtomicReference createRoomResponse = new AtomicReference<>(); AtomicReference joinRoomResponse = new AtomicReference<>(); return Stream.of( @@ -53,14 +53,14 @@ Stream getRoomDetail() { ); } - private RoomCreateResponse createRoom(CreateRoomRequest request) { + private CreateRoomResponse createRoom(CreateRoomRequest request) { return RestAssured.given().log().all() .contentType(ContentType.JSON) .body(request) .when().post("/v1/api/rooms") .then().log().all() .assertThat().statusCode(201) - .extract().body().jsonPath().getObject("data", RoomCreateResponse.class); + .extract().body().jsonPath().getObject("data", CreateRoomResponse.class); } private JoinRoomResponse joinRoom(String roomId, JoinRoomParticipantRequest request) { @@ -74,7 +74,7 @@ private JoinRoomResponse joinRoom(String roomId, JoinRoomParticipantRequest requ } private static DynamicTest inputLocation(String message, - AtomicReference createRoomResponse) { + AtomicReference createRoomResponse) { return DynamicTest.dynamicTest(message, () -> { String roomId = createRoomResponse.get().id(); @@ -121,7 +121,7 @@ private static DynamicTest getRoomMembers(String message, } private static DynamicTest inputLocation(String message, - AtomicReference createRoomResponse, + AtomicReference createRoomResponse, AtomicReference joinRoomResponse) { return DynamicTest.dynamicTest(message, () -> { @@ -139,7 +139,7 @@ private static DynamicTest inputLocation(String message, } private static DynamicTest checkVoteMode(String message, - AtomicReference createRoomResponse, boolean expectedIsVoteMode) { + AtomicReference createRoomResponse, boolean expectedIsVoteMode) { return DynamicTest.dynamicTest(message, () -> { String roomId = createRoomResponse.get().id(); diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java index 2f2d6afb..63894505 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java @@ -29,14 +29,17 @@ void createRoom() { Room createdRoom = roomCreationService.createRoom(roomName, capacity, host); assertAll("Room Create Test", - () -> assertNotNull(createdRoom, "Room ๊ฐ์ฒด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertNotNull(createdRoom.getId(), "์•ฝ์†๋ฐฉ ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(roomName, createdRoom.getRoomName(), "์•ฝ์†๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(capacity, createdRoom.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertNotNull(createdRoom.getMember(), "๋ฐฉ์žฅ ์ •๋ณด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostNickname, createdRoom.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostProfile, createdRoom.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(MemberRole.LEADER, createdRoom.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + () -> assertNotNull(createdRoom, "Room ๊ฐ์ฒด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertNotNull(createdRoom.getId(), "์•ฝ์†๋ฐฉ ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(roomName, createdRoom.getRoomName(), "์•ฝ์†๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(capacity, createdRoom.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertNotNull(createdRoom.getMember(), "๋ฐฉ์žฅ ์ •๋ณด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostNickname, createdRoom.getMember().getNickname(), + "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostProfile, createdRoom.getMember().getProfile(), + "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(MemberRole.LEADER, createdRoom.getMember().getRole(), + "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") ); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java index 560c7f58..e2f134a7 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java @@ -4,10 +4,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.kok.kokapi.common.template.ServiceTest; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; +import com.kok.kokcore.room.port.out.SaveRoomPort; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java index 5eb98f87..1bb0b5b2 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java @@ -1,8 +1,8 @@ package com.kok.kokapi.station.adapter.out.external; -import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDto; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.port.out.LoadStationsPort; +import com.kok.kokcore.station.port.out.dto.StationRouteDto; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; import java.util.List; public class FakeStationClient implements LoadStationsPort { diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java index af8a4587..8c05ed50 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java @@ -21,8 +21,8 @@ class StationPersistenceAdapterTest extends ServiceTest { @Test void saveStationsAndReturn() { // given - Station savedStation = new Station("๋ง์›์—ญ", "12.345","123.456"); - List stations = List.of(new Station("ํ•ฉ์ •์—ญ", "12.345","123.456")); + Station savedStation = new Station("๋ง์›์—ญ", "12.345", "123.456"); + List stations = List.of(new Station("ํ•ฉ์ •์—ญ", "12.345", "123.456")); stationRepository.save(savedStation); // when @@ -34,4 +34,4 @@ void saveStationsAndReturn() { () -> assertThat(result.get(0).getName()).isEqualTo("ํ•ฉ์ •์—ญ") ); } -} \ No newline at end of file +} diff --git a/kok-core/build.gradle b/kok-core/build.gradle index c58c530f..9892958b 100644 --- a/kok-core/build.gradle +++ b/kok-core/build.gradle @@ -1,28 +1,28 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.2' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.kok' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java b/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java index a11e019f..a3f2659b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java +++ b/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class KokCoreApplication { - public static void main(String[] args) { - SpringApplication.run(KokCoreApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(KokCoreApplication.class, args); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index 2f041c44..4641d8ae 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -1,6 +1,12 @@ package com.kok.kokcore.location.domain; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,10 +16,10 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "location" , - uniqueConstraints = { - @UniqueConstraint(columnNames = {"room_id", "member_id"}) - }) +@Table(name = "location", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"room_id", "member_id"}) + }) public class Location { diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadCentroidPort.java similarity index 69% rename from kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java rename to kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadCentroidPort.java index f664cea5..0b659b96 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadCentroidPort.java @@ -1,7 +1,8 @@ -package com.kok.kokcore.location.application.port.out; +package com.kok.kokcore.location.port.out; import org.locationtech.jts.geom.Point; public interface ReadCentroidPort { + Point findCentroidByRoomId(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java similarity index 87% rename from kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java rename to kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java index caedb798..677abbf6 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java @@ -1,13 +1,16 @@ -package com.kok.kokcore.location.application.port.out; +package com.kok.kokcore.location.port.out; import com.kok.kokcore.location.domain.Location; - import java.util.List; import java.util.Optional; public interface ReadLocationPort { + Optional findLocationByRoomIdAndMemberId(String roomId, String memberId); + List findLocationsByRoomId(String roomId); + List findInsideConvexHull(String roomId); + List findConvexHull(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java similarity index 78% rename from kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java rename to kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java index 45656669..1bf1e537 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java @@ -1,8 +1,9 @@ -package com.kok.kokcore.location.application.port.out; +package com.kok.kokcore.location.port.out; import com.kok.kokcore.location.domain.Location; import org.locationtech.jts.geom.Point; public interface SaveLocationPort { + Location saveLocation(String roomId, String memberId, Point point); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java index fc3a1033..0c8a87d6 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java @@ -1,11 +1,14 @@ package com.kok.kokcore.location.usecase; import com.kok.kokcore.location.domain.Location; - import java.math.BigDecimal; public interface CreateLocationUseCase { - Location createLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude); - Location updateLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude); + + Location createLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude); + + Location updateLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java index 1c9e2ff3..bba313b4 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java @@ -1,11 +1,11 @@ package com.kok.kokcore.location.usecase; +import java.math.BigDecimal; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; -import java.math.BigDecimal; - public interface LoadCentroidUseCase { + Point readCentroid(String roomId); Pair readCentroidCoordinates(String roomId); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java index f719144b..9cb06333 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java @@ -1,12 +1,15 @@ package com.kok.kokcore.location.usecase; import com.kok.kokcore.location.domain.Location; - import java.util.List; public interface ReadLocationUseCase { + Location readLocation(String roomId, String memberId); + List readLocations(String roomId); + List readInsideConvexHull(String roomId); + List readConvexHull(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/public_transportation/usecase/RetrievePublicTransportationUseCase.java similarity index 81% rename from kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java rename to kok-core/src/main/java/com/kok/kokcore/public_transportation/usecase/RetrievePublicTransportationUseCase.java index 0cf1b74d..0d292eb1 100644 --- a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/public_transportation/usecase/RetrievePublicTransportationUseCase.java @@ -1,7 +1,8 @@ -package com.kok.kokcore.public_transfortation.usecase; +package com.kok.kokcore.public_transportation.usecase; public interface RetrievePublicTransportationUseCase { String retrievePublicTransportation(Long stationId, String roomId, String memberId); + String retrieveComplexPublicTransportation(Long stationId, String roomId, String memberId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java index 873bf583..c8a9e5b7 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java @@ -1,16 +1,16 @@ package com.kok.kokcore.room.domain; import com.kok.kokcore.room.domain.vo.MemberRole; +import java.util.UUID; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; -import java.util.UUID; - @Getter @ToString @EqualsAndHashCode public class Member { + private final String memberId; private final String nickname; private final String profile; diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java index 86e5bd7a..1ec61e9f 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java @@ -8,6 +8,7 @@ @ToString @EqualsAndHashCode public class Profile { + private final String imageUrl; private final String nickname; diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java similarity index 64% rename from kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantPort.java rename to kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java index 1f873fb9..543a0007 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomParticipantPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.room.application.port.out; +package com.kok.kokcore.room.port.out; public interface LoadRoomParticipantPort { diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java similarity index 75% rename from kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java rename to kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java index feaf9237..f9a5473a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java @@ -1,9 +1,9 @@ -package com.kok.kokcore.room.application.port.out; +package com.kok.kokcore.room.port.out; import com.kok.kokcore.room.domain.Room; - import java.util.Optional; public interface LoadRoomPort { + Optional findRoomById(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomParticipantsPort.java similarity index 73% rename from kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java rename to kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomParticipantsPort.java index b41a31d4..8f4de49a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomParticipantsPort.java @@ -1,7 +1,8 @@ -package com.kok.kokcore.room.application.port.out; +package com.kok.kokcore.room.port.out; import com.kok.kokcore.room.domain.Member; public interface SaveRoomParticipantsPort { + int joinRoom(String roomId, Member member); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomPort.java similarity index 66% rename from kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java rename to kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomPort.java index 55bb64ef..1cbeeb64 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomPort.java @@ -1,7 +1,8 @@ -package com.kok.kokcore.room.application.port.out; +package com.kok.kokcore.room.port.out; import com.kok.kokcore.room.domain.Room; public interface SaveRoomPort { + Room save(Room room); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java index 1d9ac350..6cbecdc1 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java @@ -3,5 +3,6 @@ import com.kok.kokcore.room.domain.Profile; public interface CreateRandomProfileUseCase { + Profile createProfile(); -} \ No newline at end of file +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java index 166e6a5a..b686dc74 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java @@ -4,5 +4,6 @@ import com.kok.kokcore.room.domain.Room; public interface CreateRoomUseCase { + Room createRoom(String roomName, int capacity, Member host); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java index 0044524d..cce7b952 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java @@ -3,5 +3,6 @@ import com.kok.kokcore.room.domain.Member; public interface JoinRoomUseCase { + int joinRoom(String roomId, Member member); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java deleted file mode 100644 index 31d36206..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; - -public interface LoadStationsPort { - - StationRouteDtos loadAllStations(); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java index 9ea21487..34f43a6b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java @@ -34,7 +34,7 @@ public Station(String name, BigDecimal latitude, BigDecimal longitude, long prio this.priority = priority; } - public Station(String name,String latitude, String longitude) { - this(name, new BigDecimal(latitude),new BigDecimal(longitude), 0); + public Station(String name, String latitude, String longitude) { + this(name, new BigDecimal(latitude), new BigDecimal(longitude), 0); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java new file mode 100644 index 00000000..4b6bbcfb --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; + +public interface LoadStationsPort { + + StationRouteDtos loadAllStations(); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadStationsPort.java similarity index 56% rename from kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadStationsPort.java index f18be07d..e173fd00 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadStationsPort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.station.application.port.out; +package com.kok.kokcore.station.port.out; public interface ReadStationsPort { diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveRoutePort.java similarity index 80% rename from kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveRoutePort.java index 4fd0652c..f2e82a77 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveRoutePort.java @@ -1,10 +1,10 @@ -package com.kok.kokcore.station.application.port.out; +package com.kok.kokcore.station.port.out; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; - import java.util.List; public interface RetrieveRoutePort { + List retrieveRoutes(Station station); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java similarity index 85% rename from kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java index f0ca5677..11d68c29 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java @@ -1,12 +1,13 @@ -package com.kok.kokcore.station.application.port.out; +package com.kok.kokcore.station.port.out; import com.kok.kokcore.station.domain.entity.Station; -import org.locationtech.jts.geom.Point; - import java.util.List; import java.util.Optional; +import org.locationtech.jts.geom.Point; public interface RetrieveStationsPort { + Optional retrieveStation(Long stationId); + List retrieveInRangeStations(Point centroid, double dist); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveRoutePort.java similarity index 74% rename from kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveRoutePort.java index 7d050c7e..a7024765 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveRoutePort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.station.application.port.out; +package com.kok.kokcore.station.port.out; import com.kok.kokcore.station.domain.entity.Route; import java.util.List; diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveStationsPort.java similarity index 76% rename from kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveStationsPort.java index d781d35e..c773107d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveStationsPort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.station.application.port.out; +package com.kok.kokcore.station.port.out; import com.kok.kokcore.station.domain.entity.Station; import java.util.List; diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java similarity index 90% rename from kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java index aecd3890..6b898913 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.station.application.port.out.dto; +package com.kok.kokcore.station.port.out.dto; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java similarity index 95% rename from kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java index f0889157..a2ff88f8 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.station.application.port.out.dto; +package com.kok.kokcore.station.port.out.dto; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; @@ -30,7 +30,7 @@ private List distinctByName() { .values()); } - public List toRoutesByStations(List stations){ + public List toRoutesByStations(List stations) { List routes = new ArrayList<>(); for (Station station : stations) { List routesOfStation = stationRouteDtos.stream() diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java similarity index 76% rename from kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java rename to kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java index 84e8d225..d3885dbb 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java @@ -1,7 +1,6 @@ -package com.kok.kokcore.station.application.usecase; +package com.kok.kokcore.station.usecase; import com.kok.kokcore.station.domain.entity.Station; - import java.util.List; diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RetrieveRouteUseCase.java similarity index 80% rename from kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java rename to kok-core/src/main/java/com/kok/kokcore/station/usecase/RetrieveRouteUseCase.java index 56488848..d8bc8b4a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RetrieveRouteUseCase.java @@ -1,10 +1,10 @@ -package com.kok.kokcore.station.application.usecase; +package com.kok.kokcore.station.usecase; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; - import java.util.List; public interface RetrieveRouteUseCase { + List retrieveRoutes(Station station); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/SaveStationUseCase.java similarity index 55% rename from kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java rename to kok-core/src/main/java/com/kok/kokcore/station/usecase/SaveStationUseCase.java index baf08bb8..5aa616bd 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/SaveStationUseCase.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.station.application.usecase; +package com.kok.kokcore.station.usecase; public interface SaveStationUseCase { diff --git a/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java b/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java index ea8c4192..47c1898f 100644 --- a/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java +++ b/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class KokCoreApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java b/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java index 9b6ed02e..3c4b252f 100644 --- a/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java @@ -1,4 +1,5 @@ package com.kok.kokcore.location.domain; public class LocationTest { + } diff --git a/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java b/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java similarity index 95% rename from kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java rename to kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java index 6fd8c227..9e499f07 100644 --- a/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.station.application.port.out.dto; +package com.kok.kokcore.station.port.out.dto; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -47,7 +47,8 @@ void toStations() { "1ํ˜ธ์„ "); StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, "2ํ˜ธ์„ "); - StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.496", "127.029", 3L, "์‹ ๋ถ„๋‹น์„ "); + StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.496", "127.029", 3L, + "์‹ ๋ถ„๋‹น์„ "); StationRouteDtos stationRouteDtos = new StationRouteDtos( List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) ); @@ -72,7 +73,8 @@ void toRoutesByStations() { "1ํ˜ธ์„ "); StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, "2ํ˜ธ์„ "); - StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 3L, "์‹ ๋ถ„๋‹น์„ "); + StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 3L, + "์‹ ๋ถ„๋‹น์„ "); StationRouteDtos stationRouteDtos = new StationRouteDtos( List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) ); @@ -92,4 +94,4 @@ void toRoutesByStations() { () -> assertThat(stations).containsExactlyInAnyOrder(station1, station2) ); } -} \ No newline at end of file +} From d736f246df9e1195158c44947942d7cff5294e55 Mon Sep 17 00:00:00 2001 From: minseokey Date: Thu, 27 Mar 2025 19:04:34 +0900 Subject: [PATCH 103/163] :sparkles: feature: add custom voting candidate function --- .../config/redis/out/RedisCacheConfig.java | 2 +- .../adapter/in/web/StationController.java | 33 +++++++++++++- .../CustomStationCommandRedisAdapter.java | 45 +++++++++++++++++++ .../CustomStationQueryRedisAdapter.java | 41 +++++++++++++++++ .../StationPersistenceAdapter.java | 5 +++ .../out/persistence/StationRepository.java | 5 +++ .../service/StationFacadeService.java | 38 ++++++++++++++++ .../application/service/StationService.java | 26 ++++++++++- .../port/out/ReadCustomStationsPort.java | 8 ++++ .../port/out/RetrieveStationsPort.java | 1 + .../port/out/SaveCustomStationsPort.java | 8 ++++ .../usecase/CustomStationUseCase.java | 14 ++++++ .../usecase/RecommendStationUseCase.java | 1 + 13 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java index 27d096b3..3285d22a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java @@ -61,7 +61,7 @@ public CacheManager publicTransportationCacheManager(RedisConnectionFactory redi RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) - .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ + .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ return RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(redisConnectionFactory) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index cec4f5d9..be9e1455 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -3,8 +3,11 @@ import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; +import com.kok.kokapi.station.application.service.StationFacadeService; +import com.kok.kokcore.station.application.usecase.CustomStationUseCase; import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.domain.entity.Station; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -12,15 +15,18 @@ import org.springframework.web.bind.annotation.PathVariable; import java.util.List; +import org.springframework.web.bind.annotation.PostMapping; @V1Controller @RequiredArgsConstructor public class StationController { private final RecommendStationUseCase recommendStationUseCase; + private final CustomStationUseCase customStationUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; + private final StationFacadeService stationFacadeService; - @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ์ถ”์ฒœ", description = "Recommend subway stations based on the user's location.") + @Operation(summary = "์ถ”์ฒœ ์ง€ํ•˜์ฒ ์—ญ ๋ฆฌํ„ด", description = "Recommend subway stations based on the user's location.") @GetMapping("/stations/recommend/{roomId}") public ResponseEntity>> recommendStations(@PathVariable String roomId) { @@ -32,4 +38,29 @@ public ResponseEntity>> recommen return ResponseEntity.ok(ApiResponseDto.success(recommendedStations)); } + @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ๊ฒ€์ƒ‰", description = "Search for subway stations based on the keyword.") + @GetMapping("/stations/search/{keyword}") + public ResponseEntity>> searchStations(@PathVariable String keyword) { + + List recommendedStations = customStationUseCase.searchStations(keyword).stream() + .map(station -> + RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) + .toList(); + + return ResponseEntity.ok(ApiResponseDto.success(recommendedStations)); + } + + @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ์ถ”๊ฐ€", description = "Add a subway station to the user's custom list.") + @PostMapping("/stations/custom/{roomId}/{stationId}") + public ResponseEntity> addCustomStations(@PathVariable String roomId, @PathVariable Long stationId) { + Station station = customStationUseCase.addCustomStations(roomId, stationId); + return ResponseEntity.ok(ApiResponseDto.success(RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station)))); + } + + @Operation(summary = "ํˆฌํ‘œํ•  ์ง€ํ•˜์ฒ ์—ญ ๋ฆฌ์ŠคํŠธ ๋ฆฌํ„ด", description = "Return a list of subway stations to vote on.") + @GetMapping("/stations/candidate/{roomId}") + public ResponseEntity>> getCustomRecommendedStations(@PathVariable String roomId) { + List candidateStations = stationFacadeService.getCandidateStationResponse(roomId); + return ResponseEntity.ok(ApiResponseDto.success(candidateStations)); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java new file mode 100644 index 00000000..ef71f2fc --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java @@ -0,0 +1,45 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.station.application.port.out.SaveCustomStationsPort; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class CustomStationCommandRedisAdapter implements SaveCustomStationsPort { + + private final String CUSTOM_STATION_PREFIX = "customStations:"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public Station addCustomStations(String roomId, Station station) { + String key = buildKey(roomId); + + try { + String stationJson = redisTemplate.opsForValue().get(key); + List stationList = (stationJson == null) + ? new ArrayList<>() + : objectMapper.readValue(stationJson, new TypeReference<>() {}); + + stationList.add(station); + String updatedJson = objectMapper.writeValueAsString(stationList); + redisTemplate.opsForValue().set(key, updatedJson); + return station; + + } catch (JsonProcessingException e) { + return null; + } + } + + private String buildKey(String roomId) { + return CUSTOM_STATION_PREFIX + roomId; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java new file mode 100644 index 00000000..192a3bcd --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java @@ -0,0 +1,41 @@ +package com.kok.kokapi.station.adapter.out.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.station.application.port.out.ReadCustomStationsPort; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class CustomStationQueryRedisAdapter implements ReadCustomStationsPort { + + private final String CUSTOM_STATION_PREFIX = "customStations:"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + @Override + public List findRecommendedStationsByRoomId(String roomId) { + String key = buildKey(roomId); + String stationJson = redisTemplate.opsForValue().get(key); + + if (stationJson == null) { + return Collections.emptyList(); + } + + try { + return objectMapper.readValue(stationJson, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + return Collections.emptyList(); + } + } + + private String buildKey(String roomId) { + return CUSTOM_STATION_PREFIX + roomId; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 31f247a7..d9f53d6b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -78,5 +78,10 @@ public List retrieveInRangeStations(Point centroid, double dist) { Pair lonLat = pointConverter.toCoordinates(centroid); return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), lonLat.getSecond(), dist); } + + @Override + public List retrieveStationsByKeyword(String keyword) { + return stationRepository.findStationsByKeyword(keyword); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index afd03ab8..94a7ce77 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -27,4 +27,9 @@ WHERE ST_Distance_Sphere(Point(longitude, latitude), Point(:lon, :lat)) < :dista """, nativeQuery = true) List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, @Param("lat") BigDecimal lat, @Param("distance") Double distance); + @Query(value = """ + SELECT * FROM station + WHERE name LIKE CONCAT('%', :keyword, '%') + """, nativeQuery = true) + List findStationsByKeyword(@Param("keyword") String keyword); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java new file mode 100644 index 00000000..20e010f8 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java @@ -0,0 +1,38 @@ +package com.kok.kokapi.station.application.service; + +import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; +import com.kok.kokcore.station.application.usecase.CustomStationUseCase; +import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; +import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class StationFacadeService { + + private final RecommendStationUseCase recommendStationUseCase; + private final CustomStationUseCase customStationUseCase; + private final RetrieveRouteUseCase retrieveRouteUseCase; + + public List getCandidateStationResponse(String roomId) { + + return getCandidateStation(roomId).stream() + .map(station -> + RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) + .toList(); + } + + public List getCandidateStation(String roomId) { + List recommendedStations = recommendStationUseCase.recommendStations(roomId); + List customStations = customStationUseCase.getCustomRecommendedStations(roomId); + + return Stream.concat( + recommendedStations.stream(), + customStations.stream() + ).toList(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index 68747ce2..fe16b939 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -1,9 +1,12 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.config.geometry.PointConverter; +import com.kok.kokapi.station.adapter.out.persistence.CustomStationCommandRedisAdapter; +import com.kok.kokapi.station.adapter.out.persistence.CustomStationQueryRedisAdapter; import com.kok.kokcore.location.application.port.out.ReadCentroidPort; import com.kok.kokcore.station.application.port.out.*; import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.application.usecase.CustomStationUseCase; import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; import com.kok.kokcore.station.application.usecase.SaveStationUseCase; import com.kok.kokcore.station.domain.entity.Station; @@ -22,7 +25,8 @@ @Service @RequiredArgsConstructor @Slf4j -public class StationService implements SaveStationUseCase, RecommendStationUseCase { +public class StationService implements SaveStationUseCase, RecommendStationUseCase, + CustomStationUseCase { private final LoadStationsPort loadStationsPort; private final SaveStationsPort saveStationsPort; @@ -30,6 +34,8 @@ public class StationService implements SaveStationUseCase, RecommendStationUseCa private final ReadCentroidPort readCentroidPort; private final SaveRoutePort saveRoutePort; private final RetrieveStationsPort retrieveStationsPort; + private final CustomStationQueryRedisAdapter customStationQueryRedisAdapter; + private final CustomStationCommandRedisAdapter customStationCommandRedisAdapter; private final PointConverter pointConverter; @Override @@ -42,6 +48,24 @@ public void saveStations() { } } + @Override + @Cacheable(value = "searchStations",cacheManager = "stationCacheManager", key = "#keyword") + public List searchStations(String keyword) { + return retrieveStationsPort.retrieveStationsByKeyword(keyword); + } + + @Override + public Station addCustomStations(String roomId, Long stationId) { + Station station = retrieveStationsPort.retrieveStation(stationId) + .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + return customStationCommandRedisAdapter.addCustomStations(roomId, station); + } + + @Override + public List getCustomRecommendedStations(String roomId) { + return customStationQueryRedisAdapter.findRecommendedStationsByRoomId(roomId); + } + @Override @Cacheable(value = "recommendStations",cacheManager = "stationCacheManager", key = "#roomId") public List recommendStations(String roomId) { diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java new file mode 100644 index 00000000..6d246a5d --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.application.port.out; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface ReadCustomStationsPort { + List findRecommendedStationsByRoomId(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java index f0ca5677..615b6149 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java @@ -9,4 +9,5 @@ public interface RetrieveStationsPort { Optional retrieveStation(Long stationId); List retrieveInRangeStations(Point centroid, double dist); + List retrieveStationsByKeyword(String keyword); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java new file mode 100644 index 00000000..7be52d62 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.application.port.out; + +import com.kok.kokcore.station.domain.entity.Station; + +public interface SaveCustomStationsPort { + + Station addCustomStations(String roomId, Station station); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java new file mode 100644 index 00000000..9629ccc1 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java @@ -0,0 +1,14 @@ +package com.kok.kokcore.station.application.usecase; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface CustomStationUseCase { + + Station addCustomStations(String roomId, Long stationId); + + List getCustomRecommendedStations(String roomId); + + List searchStations(String keyword); + +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java index 84e8d225..3130deb4 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java @@ -8,4 +8,5 @@ public interface RecommendStationUseCase { List recommendStations(String roomId); + } From 97e1f4d9a52d78ec67ccf837d8822e43d5d5757e Mon Sep 17 00:00:00 2001 From: minseokey Date: Fri, 28 Mar 2025 01:44:31 +0900 Subject: [PATCH 104/163] :recycle: fix: resolve conflicts --- kok-api/build.gradle | 57 +++++------ .../com/kok/kokapi/KokApiApplication.java | 8 +- .../in/dto/request/LocationRequest.java | 26 ++--- .../in/dto/response/CentroidResponse.java | 11 ++- .../response/ConvexHullLocationResponse.java | 8 +- .../in/dto/response/LocationResponse.java | 12 ++- .../adapter/in/web/LocationController.java | 66 ++++++++----- .../adapter/out/mapper/LocationMapper.java | 17 ++-- .../LocationPersistenceAdapter.java | 17 ++-- .../out/persistence/LocationRepository.java | 75 ++++++++------- .../service/CentroidQueryService.java | 5 +- .../service/LocationCommandService.java | 15 +-- .../service/LocationQueryService.java | 7 +- .../exception/GlobalExceptionHandler.java | 36 +++---- .../kokapi/config/annotion/V1Controller.java | 6 +- .../config/geometry/GeometryConfig.java | 6 -- .../config/geometry/PointConverter.java | 5 +- .../kok/kokapi/config/redis/RedisConfig.java | 3 +- .../config/redis/out/RedisCacheConfig.java | 71 -------------- .../kokapi/config/swagger/SwaggerConfig.java | 18 ++-- .../kok/kokapi/config/web/WebMvcConfig.java | 10 +- .../adapter/in/web/HealthCheckController.java | 2 +- .../adapter/in/dto/RouteRequest.java | 12 --- ...lexPublicTransportationParsedResponse.java | 35 ------- ...mapPublicTransportationParsedResponse.java | 10 -- .../web/PublicTransportationController.java | 30 +++--- .../external/PublicTransportationClient.java | 57 +++++------ .../PublicTransportationComplexClient.java | 58 +++++------ .../out/external/TmapClientProperties.java | 9 +- .../external/TmapComplexClientProperties.java | 7 +- ...apComplexPublicTransportationResponse.java | 17 +++- .../dto/TmapPublicTransportationResponse.java | 10 +- .../TmapPublicTransportationService.java | 49 ++++++---- .../in/dto/request/CreateRoomRequest.java | 24 ++--- .../request/JoinRoomParticipantRequest.java | 12 ++- .../in/dto/response/JoinRoomResponse.java | 14 +-- .../in/dto/response/MemberResponse.java | 17 ++-- .../dto/response/RandomProfileResponse.java | 8 +- .../in/dto/response/RoomCreateResponse.java | 23 ----- .../in/dto/response/RoomDetailResponse.java | 21 ++-- .../in/dto/response/RoomMembersResponse.java | 12 ++- .../room/adapter/in/web/RoomController.java | 72 ++++++++------ .../adapter/in/web/RoomProfileController.java | 4 +- .../RoomParticipantSaveAdapter.java | 2 +- .../persistence/RoomQueryRedisAdapter.java | 7 +- .../out/persistence/RoomSaveRedisAdapter.java | 5 +- .../service/RandomProfileService.java | 13 ++- .../service/RoomCreationService.java | 2 +- .../service/RoomParticipantService.java | 3 +- .../application/service/RoomQueryService.java | 17 +++- .../response/RecommendedStationResponse.java | 10 +- .../adapter/in/web/StationController.java | 6 +- .../adapter/out/external/StationClient.java | 7 +- .../out/external/StationErrorHandler.java | 3 +- .../out/external/dto/StationResponse.java | 2 +- .../out/external/dto/StationResponses.java | 2 +- .../CustomStationCommandRedisAdapter.java | 2 +- .../CustomStationQueryRedisAdapter.java | 2 +- .../persistence/RoutePersistenceAdapter.java | 7 +- .../out/persistence/RouteRepository.java | 3 +- .../StationPersistenceAdapter.java | 17 ++-- .../out/persistence/StationRepository.java | 18 ++-- .../application/config/StationsConfig.java | 15 --- .../application/service/RouteService.java | 7 +- .../service/StationFacadeService.java | 6 +- .../application/service/StationService.java | 12 +-- .../src/main/resources/application-dev.yml | 4 +- .../src/main/resources/application-prod.yml | 2 +- .../main/resources/db/migration/V1__init.sql | 8 +- .../kok/kokapi/KokApiApplicationTests.java | 6 +- .../common/template/RepositoryTest.java | 13 ++- .../kokapi/common/template/ServiceTest.java | 10 +- .../common/util/MySQLDatabaseCleaner.java | 2 +- .../util/MySQLDatabaseCleanerExtension.java | 15 --- .../kok/kokapi/config/DataJpaTestConfig.java | 10 -- .../kok/kokapi/config/ServiceTestConfig.java | 17 ---- .../service/RoomCreationServiceTest.java | 19 ++-- .../service/RoomQueryServiceTest.java | 2 +- .../out/external/FakeStationClient.java | 6 +- .../StationPersistenceAdapterTest.java | 6 +- .../src/test/resources/application-test.yml | 2 +- kok-core/build.gradle | 22 ++--- .../com/kok/kokcore/KokCoreApplication.java | 6 +- .../port/out/ReadCentroidPort.java | 7 -- .../port/out/ReadLocationPort.java | 13 --- .../port/out/SaveLocationPort.java | 8 -- .../kok/kokcore/location/domain/Location.java | 16 +++- .../usecase/CreateLocationUseCase.java | 9 +- .../location/usecase/LoadCentroidUseCase.java | 4 +- .../location/usecase/ReadLocationUseCase.java | 5 +- .../RetrievePublicTransportationUseCase.java | 7 -- .../application/port/out/LoadRoomPort.java | 9 -- .../port/out/SaveRoomParticipantsPort.java | 7 -- .../application/port/out/SaveRoomPort.java | 7 -- .../com/kok/kokcore/room/domain/Member.java | 4 +- .../com/kok/kokcore/room/domain/Profile.java | 1 + .../com/kok/kokcore/room/domain/Room.java | 27 +++++- .../usecase/CreateRandomProfileUseCase.java | 3 +- .../room/usecase/CreateRoomUseCase.java | 1 + .../kokcore/room/usecase/GetRoomUseCase.java | 5 +- .../kokcore/room/usecase/JoinRoomUseCase.java | 1 + .../port/out/LoadStationsPort.java | 8 -- .../port/out/ReadCustomStationsPort.java | 8 -- .../port/out/ReadStationsPort.java | 6 -- .../port/out/RetrieveRoutePort.java | 10 -- .../port/out/RetrieveStationsPort.java | 13 --- .../port/out/SaveCustomStationsPort.java | 8 -- .../application/port/out/SaveRoutePort.java | 9 -- .../port/out/SaveStationsPort.java | 9 -- .../port/out/dto/StationRouteDto.java | 25 ----- .../port/out/dto/StationRouteDtos.java | 44 --------- .../usecase/CustomStationUseCase.java | 14 --- .../usecase/RecommendStationUseCase.java | 12 --- .../usecase/RetrieveRouteUseCase.java | 10 -- .../usecase/SaveStationUseCase.java | 6 -- .../station/domain/entity/Station.java | 4 +- .../kok/kokcore/KokCoreApplicationTests.java | 6 +- .../kokcore/location/domain/LocationTest.java | 1 + .../com/kok/kokcore/room/domain/RoomTest.java | 73 +++++++++++--- .../port/out/dto/StationRouteDtosTest.java | 95 ------------------- 120 files changed, 713 insertions(+), 1082 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java delete mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java delete mode 100644 kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java delete mode 100644 kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java delete mode 100644 kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java diff --git a/kok-api/build.gradle b/kok-api/build.gradle index 6ca9ee2c..e2484971 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -1,48 +1,49 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.2' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.kok' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation project(':kok-core') - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' - - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - - testImplementation 'org.testcontainers:testcontainers-bom:1.20.5' - testImplementation 'org.testcontainers:junit-jupiter' - testImplementation 'org.testcontainers:mysql' - testImplementation 'com.redis:testcontainers-redis' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation project(':kok-core') + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'org.testcontainers:testcontainers-bom:1.20.5' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:mysql' + testImplementation 'com.redis:testcontainers-redis' + + testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java index 5ec5598c..8599d4c7 100644 --- a/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java +++ b/kok-api/src/main/java/com/kok/kokapi/KokApiApplication.java @@ -2,14 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication public class KokApiApplication { - public static void main(String[] args) { - SpringApplication.run(KokApiApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(KokApiApplication.class, args); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index b32a21a5..6e67db3c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -4,25 +4,25 @@ import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; - import java.math.BigDecimal; public record LocationRequest( - @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String roomId, + @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String roomId, - @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String memberId, + @NotNull(message = "memberId(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String memberId, - @NotNull(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - BigDecimal latitude, + @NotNull(message = "latitude(์œ„๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + BigDecimal latitude, - @NotNull(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - BigDecimal longitude + @NotNull(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + BigDecimal longitude ) { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java index 760059da..e188c728 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/CentroidResponse.java @@ -4,13 +4,14 @@ import java.math.RoundingMode; public record CentroidResponse( - String roomId, - BigDecimal latitude, - BigDecimal longitude + String roomId, + BigDecimal latitude, + BigDecimal longitude ) { + public static CentroidResponse of(String roomId, BigDecimal longitude, BigDecimal latitude) { return new CentroidResponse(roomId, - latitude.setScale(6, RoundingMode.HALF_UP), - longitude.setScale(6,RoundingMode.HALF_UP)); + latitude.setScale(6, RoundingMode.HALF_UP), + longitude.setScale(6, RoundingMode.HALF_UP)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java index 4fafd2b4..a10b0c78 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/ConvexHullLocationResponse.java @@ -3,10 +3,12 @@ import java.util.List; public record ConvexHullLocationResponse( - List convexHull, - List inside + List convexHull, + List inside ) { - public static ConvexHullLocationResponse of(List convexHull, List inside) { + + public static ConvexHullLocationResponse of(List convexHull, + List inside) { return new ConvexHullLocationResponse(convexHull, inside); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index c30423c3..7ee197a9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -3,15 +3,17 @@ import java.math.BigDecimal; import java.math.RoundingMode; -public record LocationResponse ( +public record LocationResponse( String roomId, String memberId, BigDecimal latitude, BigDecimal longitude -){ - public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { +) { + + public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude) { return new LocationResponse(roomId, memberId, - latitude.setScale(6, RoundingMode.HALF_UP), - longitude.setScale(6, RoundingMode.HALF_UP)); + latitude.setScale(6, RoundingMode.HALF_UP), + longitude.setScale(6, RoundingMode.HALF_UP)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index af659406..af8e93a5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -13,13 +13,16 @@ import com.kok.kokcore.location.usecase.ReadLocationUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; +import java.math.BigDecimal; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.math.BigDecimal; -import java.util.List; +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; @V1Controller @RequiredArgsConstructor @@ -32,35 +35,39 @@ public class LocationController { @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") @PostMapping("/locations") - public ResponseEntity> createLocation(@Valid @RequestBody LocationRequest locationRequest) { + public ResponseEntity> createLocation( + @Valid @RequestBody LocationRequest locationRequest) { createLocationUsecase.createLocation( - locationRequest.roomId(), - locationRequest.memberId(), - locationRequest.latitude(), - locationRequest.longitude() + locationRequest.roomId(), + locationRequest.memberId(), + locationRequest.latitude(), + locationRequest.longitude() ); - Pair centroid = loadCentroidUsecase.readCentroidCoordinates(locationRequest.roomId()); + Pair centroid = loadCentroidUsecase.readCentroidCoordinates( + locationRequest.roomId()); return ResponseEntity.ok(ApiResponseDto.success( - CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) + CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) )); } // For Test @Operation(summary = "์ค‘์‹ฌ ์ขŒํ‘œ ์กฐํšŒ", description = "Retrieve the centroid coordinates for a location using its roomId") @GetMapping("/locations/centroid/{roomId}") - public ResponseEntity> getCentroid(@PathVariable String roomId) { + public ResponseEntity> getCentroid( + @PathVariable String roomId) { Pair centroid = loadCentroidUsecase.readCentroidCoordinates(roomId); return ResponseEntity.ok(ApiResponseDto.success( - CentroidResponse.of(roomId, centroid.getFirst(), centroid.getSecond()) + CentroidResponse.of(roomId, centroid.getFirst(), centroid.getSecond()) )); } @Operation(summary = "์œ„์น˜ ์กฐํšŒ Basic", description = "Retrieve detailed information for a location using its roomId and member ID") @GetMapping("/locations/{roomId}/{memberId}") - public ResponseEntity> getLocation(@PathVariable String roomId, @PathVariable String memberId) { + public ResponseEntity> getLocation(@PathVariable String roomId, + @PathVariable String memberId) { Location location = readLocationUsecase.readLocation(roomId, memberId); return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); @@ -68,29 +75,36 @@ public ResponseEntity> getLocation(@PathVariabl @Operation(summary = "์œ„์น˜์กฐํšŒ ConvexHull", description = "Retrieve the ConvexHull inside list, outside list of locations for a roomId") @GetMapping("/locations/ConvH/{roomId}") - public ResponseEntity> getConvexHullLocations(@PathVariable String roomId){ - List convexHull = locationMapper.toResponseList(readLocationUsecase.readConvexHull(roomId)); - List inside = locationMapper.toResponseList(readLocationUsecase.readInsideConvexHull(roomId)); - - return ResponseEntity.ok(ApiResponseDto.success(ConvexHullLocationResponse.of(convexHull, inside ))); + public ResponseEntity> getConvexHullLocations( + @PathVariable String roomId) { + List convexHull = locationMapper.toResponseList( + readLocationUsecase.readConvexHull(roomId)); + List inside = locationMapper.toResponseList( + readLocationUsecase.readInsideConvexHull(roomId)); + + return ResponseEntity.ok( + ApiResponseDto.success(ConvexHullLocationResponse.of(convexHull, inside))); } @Operation(summary = "์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ", description = "Retrieve the list of locations for a roomId") @GetMapping("/locations/{roomId}") - public ResponseEntity>> getLocations(@PathVariable String roomId) { - List responses = locationMapper.toResponseList(readLocationUsecase.readLocations(roomId)); + public ResponseEntity>> getLocations( + @PathVariable String roomId) { + List responses = locationMapper.toResponseList( + readLocationUsecase.readLocations(roomId)); return ResponseEntity.ok(ApiResponseDto.success(responses)); } @Operation(summary = "์œ„์น˜ ์ˆ˜์ •", description = "Update the location with the provided details.") @PutMapping("/locations") - public ResponseEntity> updateLocation(@Valid @RequestBody LocationRequest locationRequest) { + public ResponseEntity> updateLocation( + @Valid @RequestBody LocationRequest locationRequest) { Location location = createLocationUsecase.updateLocation( - locationRequest.roomId(), - locationRequest.memberId(), - locationRequest.latitude(), - locationRequest.longitude() + locationRequest.roomId(), + locationRequest.memberId(), + locationRequest.latitude(), + locationRequest.longitude() ); LocationResponse response = locationMapper.toResponse(location); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index 65abee45..2b9a1b98 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -1,12 +1,12 @@ package com.kok.kokapi.centroid.adapter.out.mapper; import com.kok.kokapi.centroid.adapter.in.dto.response.LocationResponse; -import com.kok.kokcore.location.domain.Location; import com.kok.kokapi.config.geometry.PointConverter; -import org.springframework.stereotype.Component; -import org.springframework.data.util.Pair; +import com.kok.kokcore.location.domain.Location; import java.math.BigDecimal; import java.util.List; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; @Component public class LocationMapper { @@ -18,12 +18,13 @@ public LocationMapper(PointConverter pointConverter) { } public LocationResponse toResponse(Location location) { - Pair coordinates = pointConverter.toCoordinates(location.getLocation_point()); + Pair coordinates = pointConverter.toCoordinates( + location.getLocation_point()); return LocationResponse.of( - location.getRoomId(), - location.getMemberId(), - coordinates.getFirst(), - coordinates.getSecond() + location.getRoomId(), + location.getMemberId(), + coordinates.getFirst(), + coordinates.getSecond() ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index 12f85028..cf79d95e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -2,21 +2,22 @@ import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.application.port.out.ReadCentroidPort; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; -import com.kok.kokcore.location.application.port.out.SaveLocationPort; +import com.kok.kokcore.location.port.out.ReadCentroidPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; +import com.kok.kokcore.location.port.out.SaveLocationPort; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; -import org.springframework.stereotype.Repository; import org.locationtech.jts.io.WKTReader; +import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Optional; - @Repository @RequiredArgsConstructor -public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocationPort, ReadLocationPort { +public class LocationPersistenceAdapter implements ReadCentroidPort, SaveLocationPort, + ReadLocationPort { + private final LocationRepository locationRepository; @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 458e2a7e..78346a99 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -1,11 +1,11 @@ package com.kok.kokapi.centroid.adapter.out.persistence; import com.kok.kokcore.location.domain.Location; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; -import java.util.Optional; public interface LocationRepository extends JpaRepository { @@ -17,15 +17,15 @@ public interface LocationRepository extends JpaRepository { ์ตœ์ข…์ ์œผ๋กœ ST_AsText๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ WKT(Well-Known Text) ํฌ๋งท์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. */ @Query(value = """ - SELECT ST_AsText( - ST_Transform( - ST_Centroid( - ST_Collect( - ST_Transform(location_point, 3857))),4326) - ) - FROM location - WHERE room_id = :roomId - """, nativeQuery = true) + SELECT ST_AsText( + ST_Transform( + ST_Centroid( + ST_Collect( + ST_Transform(location_point, 3857))),4326) + ) + FROM location + WHERE room_id = :roomId + """, nativeQuery = true) String findCentroidByRoomId(@Param("roomId") String roomId); Optional findLocationByRoomIdAndMemberId(String roomId, String memberId); @@ -33,35 +33,34 @@ SELECT ST_AsText( List findLocationsByRoomId(String roomId); @Query(value = """ - WITH ConvexHull AS ( - SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull - FROM location - WHERE room_id = :roomId - ) - SELECT l.* - FROM location l, ConvexHull ch - WHERE l.room_id = :roomId - AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) - """, nativeQuery = true) + WITH ConvexHull AS ( + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull + FROM location + WHERE room_id = :roomId + ) + SELECT l.* + FROM location l, ConvexHull ch + WHERE l.room_id = :roomId + AND ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) + """, nativeQuery = true) List findInsideConvexHull(@Param("roomId") String roomId); @Query(value = """ - WITH ConvexHull AS ( - SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull, - ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point))))) AS center - FROM location - WHERE room_id = :roomId - ) - SELECT l.*, - ATAN2( - ST_Y(ST_GeomFromText(ST_AsText(l.location_point))) - ST_Y(ch.center), - ST_X(ST_GeomFromText(ST_AsText(l.location_point))) - ST_X(ch.center) - ) AS angle - FROM location l, ConvexHull ch - WHERE l.room_id = :roomId - AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) - ORDER BY angle - """, nativeQuery = true) + WITH ConvexHull AS ( + SELECT ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point)))) AS hull, + ST_Centroid(ST_ConvexHull(ST_Collect(ST_GeomFromText(ST_AsText(location_point))))) AS center + FROM location + WHERE room_id = :roomId + ) + SELECT l.*, + ATAN2( + ST_Y(ST_GeomFromText(ST_AsText(l.location_point))) - ST_Y(ch.center), + ST_X(ST_GeomFromText(ST_AsText(l.location_point))) - ST_X(ch.center) + ) AS angle + FROM location l, ConvexHull ch + WHERE l.room_id = :roomId + AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) + ORDER BY angle + """, nativeQuery = true) List findConvexHull(@Param("roomId") String roomId); - } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java index 0619867c..768b3856 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/CentroidQueryService.java @@ -1,15 +1,14 @@ package com.kok.kokapi.centroid.application.service; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokcore.location.application.port.out.ReadCentroidPort; +import com.kok.kokcore.location.port.out.ReadCentroidPort; import com.kok.kokcore.location.usecase.LoadCentroidUseCase; +import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; -import java.math.BigDecimal; - @Service @RequiredArgsConstructor public class CentroidQueryService implements LoadCentroidUseCase { diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 3480efe6..06f72bf0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -2,16 +2,15 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; -import com.kok.kokcore.location.application.port.out.SaveLocationPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; +import com.kok.kokcore.location.port.out.SaveLocationPort; import com.kok.kokcore.location.usecase.CreateLocationUseCase; import jakarta.transaction.Transactional; +import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Service; -import java.math.BigDecimal; - @Service @RequiredArgsConstructor public class LocationCommandService implements CreateLocationUseCase { @@ -21,16 +20,18 @@ public class LocationCommandService implements CreateLocationUseCase { private final PointConverter pointConverter; @Override - public Location createLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { + public Location createLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude) { Point point = pointConverter.fromCoordinates(latitude, longitude); return saveLocationPort.saveLocation(roomId, memberId, point); } @Override @Transactional - public Location updateLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude) { + public Location updateLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude) { Location location = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); location.changePoint(newPoint); return location; diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java index 3b496730..137be13a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationQueryService.java @@ -1,13 +1,12 @@ package com.kok.kokapi.centroid.application.service; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.location.usecase.ReadLocationUseCase; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class LocationQueryService implements ReadLocationUseCase { @@ -17,7 +16,7 @@ public class LocationQueryService implements ReadLocationUseCase { @Override public Location readLocation(String roomId, String memberId) { return readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java index 9951a59c..7093159d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java @@ -1,48 +1,50 @@ package com.kok.kokapi.common.exception; -import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.common.error.ErrorCode; +import com.kok.kokapi.common.response.ApiResponseDto; import jakarta.validation.ConstraintViolationException; +import java.util.stream.Collectors; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.util.stream.Collectors; - @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity> handleConstraintViolationException(ConstraintViolationException ex) { + public ResponseEntity> handleConstraintViolationException( + ConstraintViolationException ex) { String message = ex.getConstraintViolations() - .stream() - .map(cv -> cv.getPropertyPath() + " " + cv.getMessage()) - .collect(Collectors.joining(", ")); + .stream() + .map(cv -> cv.getPropertyPath() + " " + cv.getMessage()) + .collect(Collectors.joining(", ")); return ResponseEntity.badRequest() - .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); + .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + public ResponseEntity> handleMethodArgumentNotValidException( + MethodArgumentNotValidException ex) { String message = ex.getBindingResult().getFieldErrors() - .stream() - .map(fe -> fe.getField() + " " + fe.getDefaultMessage()) - .collect(Collectors.joining(", ")); + .stream() + .map(fe -> fe.getField() + " " + fe.getDefaultMessage()) + .collect(Collectors.joining(", ")); return ResponseEntity.badRequest() - .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); + .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); } @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleBadRequestException(IllegalArgumentException ex) { + public ResponseEntity> handleBadRequestException( + IllegalArgumentException ex) { return ResponseEntity.badRequest() - .body(ApiResponseDto.error(ErrorCode.BAD_REQUEST, ex.getMessage())); + .body(ApiResponseDto.error(ErrorCode.BAD_REQUEST, ex.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity> handleGlobalException(Exception ex) { return ResponseEntity - .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) - .body(ApiResponseDto.error(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage())); + .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + .body(ApiResponseDto.error(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage())); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java b/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java index b2c7718e..68048c9e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/annotion/V1Controller.java @@ -1,16 +1,16 @@ package com.kok.kokapi.config.annotion; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/v1/api") @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface V1Controller { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java index b85eacec..417f7891 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/geometry/GeometryConfig.java @@ -1,14 +1,8 @@ package com.kok.kokapi.config.geometry; -import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.locationtech.jts.geom.Point; -import org.springframework.data.util.Pair; - -import java.math.BigDecimal; -import java.util.List; @Configuration public class GeometryConfig { diff --git a/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java b/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java index 0fe12961..6db1a014 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/geometry/PointConverter.java @@ -1,12 +1,11 @@ package com.kok.kokapi.config.geometry; +import java.math.BigDecimal; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; -import java.math.BigDecimal; - public class PointConverter { private final GeometryFactory geometryFactory; @@ -22,7 +21,7 @@ public Point fromCoordinates(BigDecimal latitude, BigDecimal longitude) { return point; } - public Pair toCoordinates(Point point) { + public Pair toCoordinates(Point point) { return Pair.of(BigDecimal.valueOf(point.getY()), BigDecimal.valueOf(point.getX())); } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java index a28fee9d..8412c3b2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java @@ -14,7 +14,8 @@ public class RedisConfig { // ์ถ”ํ›„ ConnectionFactory์„ค์ • ๋ณ€๊ฒฝ์„ ๊ณ ๋ ค. (Sentinel, Cluster, etc...) @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java deleted file mode 100644 index 3285d22a..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/out/RedisCacheConfig.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.kok.kokapi.config.redis.out; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import java.time.Duration; - -@EnableCaching -@Configuration -public class RedisCacheConfig { - - @Bean("cacheManager") - @Primary - public CacheManager defaultCacheManager(RedisConnectionFactory redisConnectionFactory){ - return RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .build(); - } - - @Bean("stationCacheManager") - public CacheManager stationCacheManager(RedisConnectionFactory redisConnectionFactory) { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.activateDefaultTyping( - LaissezFaireSubTypeValidator.instance, - ObjectMapper.DefaultTyping.NON_FINAL - ); // ํƒ€์ž… ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜์—ฌ ์—ญ์ง๋ ฌํ™”ํ•  ๋•Œ ์›๋ณธ ํƒ€์ž…์„ ์œ ์ง€ - - // Jackson Serializer ์„ค์ • - GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper); - - // RedisCacheConfiguration ์„ค์ • - RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) - .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 3์ผ - - return RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration) - .build(); - } - - @Bean("publicTransportationCacheManager") - public CacheManager publicTransportationCacheManager(RedisConnectionFactory redisConnectionFactory) { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.deactivateDefaultTyping(); - - GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); - - // RedisCacheConfiguration ์„ค์ • - RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) - .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ - - return RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration) - .build(); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java index 27f9b423..dcd65704 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java @@ -1,9 +1,9 @@ package com.kok.kokapi.config.swagger; -import org.springdoc.core.models.GroupedOpenApi; -import org.springframework.context.annotation.Bean; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @@ -12,18 +12,18 @@ public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() - .info(new Info() - .title("KOK") - .version("1.0") - .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")); + .info(new Info() + .title("KOK") + .version("1.0") + .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")); } @Bean public GroupedOpenApi v1Api() { return GroupedOpenApi.builder() - .group("V1 API") // Swagger UI์—์„œ "V1 API" ๊ทธ๋ฃน์œผ๋กœ ํ‘œ์‹œ - .pathsToMatch("/v1/api/**") // v1 ๊ด€๋ จ API๋งŒ ํฌํ•จ - .build(); + .group("V1 API") // Swagger UI์—์„œ "V1 API" ๊ทธ๋ฃน์œผ๋กœ ํ‘œ์‹œ + .pathsToMatch("/v1/api/**") // v1 ๊ด€๋ จ API๋งŒ ํฌํ•จ + .build(); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java index b46c16cd..cc81a5e3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/web/WebMvcConfig.java @@ -10,10 +10,10 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOriginPatterns("*") - .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true) - .maxAge(3600); + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java index 259ab104..2c7d5a6d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java +++ b/kok-api/src/main/java/com/kok/kokapi/monitoring/adapter/in/web/HealthCheckController.java @@ -1,8 +1,8 @@ package com.kok.kokapi.monitoring.adapter.in.web; +import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.monitoring.application.service.HealthCheckService; -import com.kok.kokapi.common.response.ApiResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java deleted file mode 100644 index f4aad574..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/RouteRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.kok.kokapi.public_transportation.adapter.in.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -public record RouteRequest( - @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String roomId, - @NotNull(message = "Member ID(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String memberId -) { -} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java deleted file mode 100644 index 9c3166fa..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapComplexPublicTransportationParsedResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.kok.kokapi.public_transportation.adapter.in.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.Setter; - -import java.io.Serializable; -import java.util.List; - -@Getter -@Setter -@JsonIgnoreProperties(ignoreUnknown = true) -public class TmapComplexPublicTransportationParsedResponse implements Serializable { - - private ParsedItinerary parsedItinerary; - // DTO ํด๋ž˜์Šค ์ •์˜ - @Getter - @Setter - public static class ParsedItinerary { - private int totalDistance; - private int totalTime; - private List legs; - } - - @Getter - @Setter - public static class ParsedLeg { - private String mode; - private int distance; - private int sectionTime; - private String route; // ์ง€ํ•˜์ฒ ์ด๋ฉด ํ˜ธ์„  ์ •๋ณด, ๋ฒ„์Šค๋ฉด ๋…ธ์„  ์ •๋ณด - private String routeColor; // ๋…ธ์„  ์ƒ‰ - - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java deleted file mode 100644 index 8a880616..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/TmapPublicTransportationParsedResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokapi.public_transportation.adapter.in.dto; - -public record TmapPublicTransportationParsedResponse( - Integer totalTime, - Integer transferCount -) { - public static TmapPublicTransportationParsedResponse of(Integer totalTime, Integer transferCount) { - return new TmapPublicTransportationParsedResponse(totalTime, transferCount); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java index 3b8f7725..8e0056aa 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/web/PublicTransportationController.java @@ -4,14 +4,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; -import com.kok.kokapi.public_transportation.adapter.in.dto.RouteRequest; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapPublicTransportationParsedResponse; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse; -import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUseCase; +import com.kok.kokapi.public_transportation.adapter.in.dto.request.RouteRequest; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; +import com.kok.kokcore.public_transportation.usecase.RetrievePublicTransportationUseCase; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; @V1Controller @@ -23,10 +25,13 @@ public class PublicTransportationController { @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") @PostMapping("/route/{stationId}") - public ResponseEntity> getPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { + public ResponseEntity> getPublicTransportation( + @PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { try { - TmapPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, routeRequest.roomId(), routeRequest.memberId()) - ,TmapPublicTransportationParsedResponse.class); + TmapPublicTransportationParsedResponse publicTransportation = objectMapper.readValue( + retrievePublicTransportationUsecase.retrievePublicTransportation(stationId, + routeRequest.roomId(), routeRequest.memberId()) + , TmapPublicTransportationParsedResponse.class); return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); } catch (JsonProcessingException e) { throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); @@ -35,10 +40,13 @@ public ResponseEntity> ge @Operation(summary = "๋Œ€์ค‘๊ตํ†ต ์ „๋ฌธ ์กฐํšŒ", description = "Retrieve the total time and transfer count for a route using the station ID") @PostMapping("/route/complex/{stationId}") - public ResponseEntity> getComplexPublicTransportation(@PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { + public ResponseEntity> getComplexPublicTransportation( + @PathVariable Long stationId, @RequestBody RouteRequest routeRequest) { try { - TmapComplexPublicTransportationParsedResponse publicTransportation = objectMapper.readValue(retrievePublicTransportationUsecase.retrieveComplexPublicTransportation(stationId, routeRequest.roomId(), routeRequest.memberId()) - , TmapComplexPublicTransportationParsedResponse.class); + TmapComplexPublicTransportationParsedResponse publicTransportation = objectMapper.readValue( + retrievePublicTransportationUsecase.retrieveComplexPublicTransportation(stationId, + routeRequest.roomId(), routeRequest.memberId()) + , TmapComplexPublicTransportationParsedResponse.class); return ResponseEntity.ok(ApiResponseDto.success(publicTransportation)); } catch (JsonProcessingException e) { throw new RuntimeException("ํŒŒ์‹ฑ ์‹คํŒจ.."); diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java index 89f89a39..bb658060 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationClient.java @@ -2,10 +2,13 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; @@ -16,10 +19,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; - @Component @EnableConfigurationProperties(TmapClientProperties.class) @@ -33,7 +32,9 @@ public class PublicTransportationClient { private final ReadLocationPort readLocationPort; private final PointConverter pointConverter; - public PublicTransportationClient(TmapClientProperties properties, RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, PointConverter pointConverter) { + public PublicTransportationClient(TmapClientProperties properties, + RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, + PointConverter pointConverter) { this.properties = properties; this.retrieveStationsPort = retrieveStationsPort; this.readLocationPort = readLocationPort; @@ -43,10 +44,10 @@ public PublicTransportationClient(TmapClientProperties properties, RetrieveStati public RestClient getRestClient() { return RestClient.builder() - .requestFactory(getRequestFactory()) - .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ - .baseUrl(properties.url()) // Base URL ์„ค์ • - .build(); + .requestFactory(getRequestFactory()) + .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ + .baseUrl(properties.url()) // Base URL ์„ค์ • + .build(); } public RestClient getClient() { @@ -55,26 +56,28 @@ public RestClient getClient() { private ClientHttpRequestFactory getRequestFactory() { return ClientHttpRequestFactoryBuilder.detect() - .build(ClientHttpRequestFactorySettings.defaults()); + .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String roomId, String memberId) { + public TmapPublicTransportationResponse callPublicTransportRoute(Long stationId, String roomId, + String memberId) { log.info("Tmap api call : {}-{}-{}", stationId, roomId, memberId); return getClient().post() - .body(buildRequestBody( - getUserLocation(roomId, memberId), - getStation(stationId))) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); - }) - .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); - }) - .body(TmapPublicTransportationResponse.class); + .body(buildRequestBody( + getUserLocation(roomId, memberId), + getStation(stationId))) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); + }) + .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); + }) + .body(TmapPublicTransportationResponse.class); } - private Map buildRequestBody(Pair userLocation, Station station) { + private Map buildRequestBody(Pair userLocation, + Station station) { Map requestBody = new HashMap<>(); requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ @@ -87,12 +90,12 @@ private Map buildRequestBody(Pair userLo private Pair getUserLocation(String roomId, String memberId) { Location userPoint = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } private Station getStation(Long stationId) { return retrieveStationsPort.retrieveStation(stationId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java index 8bfd0cbf..5419d3c9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/PublicTransportationComplexClient.java @@ -2,10 +2,13 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; -import com.kok.kokcore.location.application.port.out.ReadLocationPort; import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; +import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; @@ -16,14 +19,11 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; - @Component @EnableConfigurationProperties(TmapComplexClientProperties.class) @Slf4j public class PublicTransportationComplexClient { + private final RestClient restClient; private final TmapComplexClientProperties properties; @@ -31,7 +31,9 @@ public class PublicTransportationComplexClient { private final ReadLocationPort readLocationPort; private final PointConverter pointConverter; - public PublicTransportationComplexClient(TmapComplexClientProperties properties, RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, PointConverter pointConverter) { + public PublicTransportationComplexClient(TmapComplexClientProperties properties, + RetrieveStationsPort retrieveStationsPort, ReadLocationPort readLocationPort, + PointConverter pointConverter) { this.properties = properties; this.retrieveStationsPort = retrieveStationsPort; this.readLocationPort = readLocationPort; @@ -41,10 +43,10 @@ public PublicTransportationComplexClient(TmapComplexClientProperties properties, public RestClient getRestClient() { return RestClient.builder() - .requestFactory(getRequestFactory()) - .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ - .baseUrl(properties.url()) // Base URL ์„ค์ • - .build(); + .requestFactory(getRequestFactory()) + .defaultHeader(properties.keyname(), properties.key()) // API Key ์ถ”๊ฐ€ + .baseUrl(properties.url()) // Base URL ์„ค์ • + .build(); } public RestClient getClient() { @@ -53,37 +55,39 @@ public RestClient getClient() { private ClientHttpRequestFactory getRequestFactory() { return ClientHttpRequestFactoryBuilder.detect() - .build(ClientHttpRequestFactorySettings.defaults()); + .build(ClientHttpRequestFactorySettings.defaults()); } - public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, String roomId, String memberId){ + public TmapComplexPublicTransportationResponse callComplexPublicTransportRoute(Long stationId, + String roomId, String memberId) { log.info("Tmap api call : {}-{}-{}", stationId, roomId, memberId); return getClient().post() - .body(buildRequestBody( - getUserLocation(roomId, memberId), - getStation(stationId))) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); - }) - .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { - throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); - }) - .body(TmapComplexPublicTransportationResponse.class); + .body(buildRequestBody( + getUserLocation(roomId, memberId), + getStation(stationId))) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 4xx" + status); + }) + .onStatus(HttpStatusCode::is5xxServerError, (status, response) -> { + throw new RuntimeException("Tmap api ํ˜ธ์ถœ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. by 5xx" + status); + }) + .body(TmapComplexPublicTransportationResponse.class); } private Pair getUserLocation(String roomId, String memberId) { Location userPoint = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น roomId์˜ ์‚ฌ์šฉ์ž ์œ„์น˜๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); return pointConverter.toCoordinates(userPoint.getLocation_point()); } private Station getStation(Long stationId) { return retrieveStationsPort.retrieveStation(stationId) - .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); } - private Map buildRequestBody(Pair userLocation, Station station) { + private Map buildRequestBody(Pair userLocation, + Station station) { Map requestBody = new HashMap<>(); requestBody.put("startX", userLocation.getSecond()); // ๊ฒฝ๋„ requestBody.put("startY", userLocation.getFirst()); // ์œ„๋„ diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java index bfc502bf..c9c5b3b1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapClientProperties.java @@ -4,8 +4,9 @@ @ConfigurationProperties(prefix = "tmap-sub") public record TmapClientProperties( - String key, - String url, - String keyname -){ + String key, + String url, + String keyname +) { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java index 650785bf..812e2841 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/TmapComplexClientProperties.java @@ -4,8 +4,9 @@ @ConfigurationProperties(prefix = "tmap-complex") public record TmapComplexClientProperties( - String key, - String url, - String keyname + String key, + String url, + String keyname ) { + } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java index b3f8e5a4..f14bf2c0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapComplexPublicTransportationResponse.java @@ -2,21 +2,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Getter; import lombok.Setter; -import java.util.List; - @Getter @Setter @JsonIgnoreProperties(ignoreUnknown = true) public class TmapComplexPublicTransportationResponse { + @JsonProperty("metaData") private MetaData metaData; @Getter @Setter public static class MetaData { + @JsonProperty("requestParameters") private RequestParameters requestParameters; @@ -27,6 +28,7 @@ public static class MetaData { @Getter @Setter public static class RequestParameters { + private int busCount; private int expressbusCount; private int subwayCount; @@ -46,6 +48,7 @@ public static class RequestParameters { @Getter @Setter public static class Plan { + @JsonProperty("itineraries") private List itineraries; } @@ -53,6 +56,7 @@ public static class Plan { @Getter @Setter public static class Itinerary { + @JsonProperty("fare") private Fare fare; @@ -68,6 +72,7 @@ public static class Itinerary { @Getter @Setter public static class Fare { + @JsonProperty("regular") private RegularFare regular; } @@ -75,6 +80,7 @@ public static class Fare { @Getter @Setter public static class RegularFare { + private int totalFare; private Currency currency; } @@ -82,6 +88,7 @@ public static class RegularFare { @Getter @Setter public static class Currency { + private String symbol; private String currency; private String currencyCode; @@ -90,6 +97,7 @@ public static class Currency { @Getter @Setter public static class Leg { + private String mode; private int sectionTime; private int distance; @@ -108,6 +116,7 @@ public static class Leg { @Getter @Setter public static class Location { + private String name; private double lon; private double lat; @@ -116,6 +125,7 @@ public static class Location { @Getter @Setter public static class Step { + private String streetName; private int distance; private String description; @@ -125,12 +135,14 @@ public static class Step { @Getter @Setter public static class PassStopList { + private List stationList; } @Getter @Setter public static class Station { + private int index; private String stationName; private String lon; @@ -141,6 +153,7 @@ public static class Station { @Getter @Setter public static class PassShape { + private String linestring; } diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java index e243edc7..28c2a1e8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/out/external/dto/TmapPublicTransportationResponse.java @@ -1,11 +1,10 @@ package com.kok.kokapi.public_transportation.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; import lombok.Getter; import lombok.Setter; -import java.util.List; - // Tmap ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ POJO @Getter @Setter @@ -22,6 +21,7 @@ public class TmapPublicTransportationResponse { @Getter @Setter public static class MetaData { + private RequestParameters requestParameters; private Plan plan; @@ -30,6 +30,7 @@ public static class MetaData { @Getter @Setter public static class RequestParameters { + private String endY; private String endX; private String startY; @@ -41,6 +42,7 @@ public static class RequestParameters { @Getter @Setter public static class Plan { + private List itineraries; } @@ -48,6 +50,7 @@ public static class Plan { @Getter @Setter public static class Itinerary { + private Fare fare; private int totalTime; private int totalWalkTime; @@ -61,6 +64,7 @@ public static class Itinerary { @Getter @Setter public static class Fare { + private RegularFare regular; } @@ -68,6 +72,7 @@ public static class Fare { @Getter @Setter public static class RegularFare { + private int totalFare; private Currency currency; @@ -76,6 +81,7 @@ public static class RegularFare { @Getter @Setter public static class Currency { + private String symbol; private String currency; private String currencyCode; diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java index 9bba411e..827ef2f0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/application/service/TmapPublicTransportationService.java @@ -1,25 +1,25 @@ package com.kok.kokapi.public_transportation.application.service; +import static com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse.ParsedItinerary; +import static com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse.ParsedLeg; +import static com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse.Itinerary; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse; -import com.kok.kokapi.public_transportation.adapter.in.dto.TmapPublicTransportationParsedResponse; -import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapComplexPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationComplexClient; +import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse; import com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapPublicTransportationResponse; -import com.kok.kokcore.public_transfortation.usecase.RetrievePublicTransportationUseCase; +import com.kok.kokcore.public_transportation.usecase.RetrievePublicTransportationUseCase; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.stream.Collectors; - -import static com.kok.kokapi.public_transportation.adapter.in.dto.TmapComplexPublicTransportationParsedResponse.*; -import static com.kok.kokapi.public_transportation.adapter.out.external.dto.TmapComplexPublicTransportationResponse.*; - @Service @RequiredArgsConstructor @Slf4j @@ -33,7 +33,8 @@ public class TmapPublicTransportationService implements RetrievePublicTransporta @Cacheable(value = "sub", cacheManager = "publicTransportationCacheManager", key = "'PTSubCache:' + #stationId + '-' + #roomId + '-' + #memberId") @Override public String retrievePublicTransportation(Long stationId, String roomId, String memberId) { - TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute(stationId, roomId, memberId); + TmapPublicTransportationResponse rawRoute = publicTransportationClient.callPublicTransportRoute( + stationId, roomId, memberId); try { return objectMapper.writeValueAsString(parseTmapResponse(rawRoute)); } catch (JsonProcessingException e) { @@ -43,8 +44,10 @@ public String retrievePublicTransportation(Long stationId, String roomId, String @Cacheable(value = "complex", cacheManager = "publicTransportationCacheManager", key = "'PTComplexCache:' + #stationId + '-' + #roomId + '-' + #memberId") @Override - public String retrieveComplexPublicTransportation(Long stationId, String roomId, String memberId) { - TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute(stationId, roomId, memberId); + public String retrieveComplexPublicTransportation(Long stationId, String roomId, + String memberId) { + TmapComplexPublicTransportationResponse rawRoute = publicTransportationComplexClient.callComplexPublicTransportRoute( + stationId, roomId, memberId); try { return objectMapper.writeValueAsString(parseComplexTmapResponse(rawRoute)); } catch (JsonProcessingException e) { @@ -52,9 +55,12 @@ public String retrieveComplexPublicTransportation(Long stationId, String roomId, } } - public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse(TmapComplexPublicTransportationResponse response) { - if (response == null || response.getMetaData() == null || response.getMetaData().getPlan() == null - || response.getMetaData().getPlan().getItineraries() == null || response.getMetaData().getPlan().getItineraries().isEmpty()) { + public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse( + TmapComplexPublicTransportationResponse response) { + if (response == null || response.getMetaData() == null + || response.getMetaData().getPlan() == null + || response.getMetaData().getPlan().getItineraries() == null || response.getMetaData() + .getPlan().getItineraries().isEmpty()) { return null; } @@ -80,13 +86,16 @@ public TmapComplexPublicTransportationParsedResponse parseComplexTmapResponse(Tm return parsedResponse; } - public TmapPublicTransportationParsedResponse parseTmapResponse(TmapPublicTransportationResponse response) { - if (response == null || response.getMetaData() == null || response.getMetaData().getPlan() == null || response.getMetaData().getPlan().getItineraries() == null ) { + public TmapPublicTransportationParsedResponse parseTmapResponse( + TmapPublicTransportationResponse response) { + if (response == null || response.getMetaData() == null + || response.getMetaData().getPlan() == null + || response.getMetaData().getPlan().getItineraries() == null) { return null; } return TmapPublicTransportationParsedResponse.of( - response.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), - response.getMetaData().getPlan().getItineraries().getFirst().getTransferCount()); + response.getMetaData().getPlan().getItineraries().getFirst().getTotalTime(), + response.getMetaData().getPlan().getItineraries().getFirst().getTransferCount()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java index 54e42dd1..50f72880 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/CreateRoomRequest.java @@ -7,17 +7,19 @@ import jakarta.validation.constraints.Size; public record CreateRoomRequest( - @NotBlank(message = "๋ฐฉ ์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") - @Size(max = 30, message = "๋ฐฉ ์ด๋ฆ„์€ ์ตœ๋Œ€ 30์ž๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") - String roomName, + @NotBlank(message = "๋ฐฉ ์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + @Size(max = 30, message = "๋ฐฉ ์ด๋ฆ„์€ ์ตœ๋Œ€ 30์ž๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") + String roomName, - @Min(value = 2, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - @Max(value = 15, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ๋Œ€ 15๋ช…๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") - Integer capacity, + @Min(value = 2, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ์†Œ 2๋ช… ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Max(value = 15, message = "์ฐธ์—ฌ ์ธ์› ์ˆ˜๋Š” ์ตœ๋Œ€ 15๋ช…๊นŒ์ง€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") + Integer capacity, - @NotBlank(message = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") - String hostProfile, + @NotBlank(message = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") + String hostProfile, - @NotBlank(message = "๋‹‰๋„ค์ž„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") - String hostNickname -) {} + @NotBlank(message = "๋‹‰๋„ค์ž„์€ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ž…๋‹ˆ๋‹ค.") + String hostNickname +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java index 82c15d9c..b8b94c47 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/request/JoinRoomParticipantRequest.java @@ -4,10 +4,12 @@ import jakarta.validation.constraints.NotBlank; public record JoinRoomParticipantRequest( - @NotBlank(message = "ํ”„๋กœํ•„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String profile, + @NotBlank(message = "ํ”„๋กœํ•„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String profile, - @NotBlank(message = "๋‹‰๋„ค์ž„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String nickname -) {} + @NotBlank(message = "๋‹‰๋„ค์ž„ ์ •๋ณด๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String nickname +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java index 244859e1..22a987ce 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/JoinRoomResponse.java @@ -1,10 +1,12 @@ package com.kok.kokapi.room.adapter.in.dto.response; public record JoinRoomResponse( - String id, - String profile, - String nickname, - int participantCount, - int nonParticipantCount -) { } + String id, + String profile, + String nickname, + int participantCount, + int nonParticipantCount +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java index 922e5ac7..a18e8008 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/MemberResponse.java @@ -4,16 +4,17 @@ import com.kok.kokcore.room.domain.vo.MemberRole; public record MemberResponse( - String id, - String nickname, - String profile, - MemberRole role + String id, + String nickname, + String profile, + MemberRole role ) { + public static MemberResponse from(Member member) { return new MemberResponse( - member.getMemberId(), - member.getNickname(), - member.getProfile(), - member.getRole()); + member.getMemberId(), + member.getNickname(), + member.getProfile(), + member.getRole()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java index 4dd44a50..dbfea7d8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RandomProfileResponse.java @@ -1,6 +1,8 @@ package com.kok.kokapi.room.adapter.in.dto.response; public record RandomProfileResponse( - String imageUrl, - String nickname -){} + String imageUrl, + String nickname +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java deleted file mode 100644 index 3894841c..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomCreateResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.kok.kokapi.room.adapter.in.dto.response; - -import com.kok.kokcore.room.domain.Room; - -public record RoomCreateResponse( - String id, - String roomName, - int capacity, - MemberResponse member, - int participantCount, - int nonParticipantCount -) { - public static RoomCreateResponse from(Room room, int participantCount, int nonParticipantCount) { - return new RoomCreateResponse( - room.getId(), - room.getRoomName(), - room.getCapacity(), - MemberResponse.from(room.getMember()), - participantCount, - nonParticipantCount - ); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index df851571..bf68567a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -3,21 +3,16 @@ import com.kok.kokcore.room.domain.Room; public record RoomDetailResponse( - String id, - String roomName, - int capacity, - MemberResponse member + String id, + String roomName, + int nonParticipantCount ) { - public static RoomDetailResponse from(Room room) { + + public static RoomDetailResponse of(Room room, int participantCount) { return new RoomDetailResponse( - room.getId(), - room.getRoomName(), - room.getCapacity(), - getLeaderResponse(room) + room.getId(), + room.getRoomName(), + room.getCapacity() - participantCount ); } - - private static MemberResponse getLeaderResponse(Room room) { - return MemberResponse.from(room.getMember()); - } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java index 75339512..14fe2f25 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java @@ -3,8 +3,10 @@ import com.kok.kokcore.room.domain.vo.MemberRole; public record RoomMembersResponse( - String memberId, - String profile, - String nickname, - MemberRole role -) {} + String memberId, + String profile, + String nickname, + MemberRole role +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index d75bf3a3..d8efb62c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -4,10 +4,12 @@ import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; +import com.kok.kokapi.room.adapter.in.dto.response.CreateRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomCreateResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponses; +import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; +import com.kok.kokapi.room.application.service.RoomFacadeService; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; @@ -16,68 +18,76 @@ import com.kok.kokcore.room.usecase.JoinRoomUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; +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; @V1Controller @RequiredArgsConstructor public class RoomController { + private final RoomFacadeService roomFacadeService; private final GetRoomUseCase getRoomUseCase; private final CreateRoomUseCase createRoomUseCase; private final JoinRoomUseCase joinRoomUseCase; - @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ์•ฝ์†๋ฐฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ์•ฝ์†๋ฐฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ํˆฌํ‘œ ๋ชจ๋“œ์— ๋Œ€ํ•œ ๊ฐ’์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}") - public ResponseEntity> getRoomDetail(@PathVariable String roomId) { - var room = getRoomUseCase.findRoomById(roomId); - var response = RoomDetailResponse.from(room); + public ResponseEntity> getRoomDetail( + @PathVariable String roomId) { + RoomDetailResponse response = roomFacadeService.findByRoomId(roomId); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } + + @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒํƒœ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์•ฝ์†๋ฐฉ์ด ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ์ค‘์ด๋ฉด false๋ฅผ, ํˆฌํ‘œ ์ง„ํ–‰ ์ค‘์ด๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/rooms/{roomId}/status") + public ResponseEntity> getRoomStatus( + @PathVariable String roomId) { + RoomStatusResponse response = roomFacadeService.getRoomStatus(roomId, LocalDateTime.now()); return ResponseEntity.ok(ApiResponseDto.success(response)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒ์„ฑ", description = "์ƒˆ๋กœ์šด ์•ฝ์†๋ฐฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms") - public ResponseEntity> createRoom(@Valid @RequestBody CreateRoomRequest request) { + public ResponseEntity> createRoom( + @Valid @RequestBody CreateRoomRequest request) { String nickname = request.hostNickname(); String profile = request.hostProfile(); Member host = new Member(nickname, profile, MemberRole.LEADER); Room room = createRoomUseCase.createRoom( - request.roomName(), - request.capacity(), - host + request.roomName(), + request.capacity(), + host ); - RoomCreateResponse response = RoomCreateResponse.from(room, 1, room.getCapacity() - 1); + CreateRoomResponse response = CreateRoomResponse.of(room, 1, room.getCapacity() - 1); return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponseDto.success(response)); + .body(ApiResponseDto.success(response)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์ฐธ์—ฌ์ž๋“ค์˜ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}/participants") - public ResponseEntity>> getParticipants(@PathVariable String roomId) { + public ResponseEntity> getParticipants( + @PathVariable String roomId) { Room room = getRoomUseCase.findRoomById(roomId); List participants = getRoomUseCase.getParticipants(room.getId()); + RoomMembersResponses responses = RoomMembersResponses.of(room, participants); - List response = participants.stream() - .map(member -> new RoomMembersResponse( - member.getMemberId(), - member.getProfile(), - member.getNickname(), - member.getRole() - )).toList(); - - return ResponseEntity.ok(ApiResponseDto.success(response)); + return ResponseEntity.ok(ApiResponseDto.success(responses)); } @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•ฉ๋‹ˆ๋‹ค.") @PostMapping("/rooms/{roomId}/join") - public ResponseEntity> joinRoom(@PathVariable String roomId, @Valid @RequestBody JoinRoomParticipantRequest request) { + public ResponseEntity> joinRoom(@PathVariable String roomId, + @Valid @RequestBody JoinRoomParticipantRequest request) { Room room = getRoomUseCase.findRoomById(roomId); @@ -86,11 +96,11 @@ public ResponseEntity> joinRoom(@PathVariable S int nonParticipantCount = room.getCapacity() - participantCount; JoinRoomResponse response = new JoinRoomResponse( - participant.getMemberId(), - participant.getProfile(), - participant.getNickname(), - participantCount, - nonParticipantCount + participant.getMemberId(), + participant.getProfile(), + participant.getNickname(), + participantCount, + nonParticipantCount ); return ResponseEntity.ok(ApiResponseDto.success(response)); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java index 041845e9..a4ecc9cb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java @@ -21,8 +21,8 @@ public class RoomProfileController { public ResponseEntity> getRandomProfile() { Profile profile = createRandomProfileUseCase.createProfile(); RandomProfileResponse response = new RandomProfileResponse( - profile.getImageUrl(), - profile.getNickname() + profile.getImageUrl(), + profile.getNickname() ); return ResponseEntity.ok(ApiResponseDto.success(response)); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java index 8be7f9ae..178f0408 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java @@ -2,8 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.port.out.SaveRoomParticipantsPort; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java index 4abebb24..d0d6b786 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java @@ -2,14 +2,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.LoadRoomPort; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.LoadRoomPort; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; -import java.util.Optional; - @Repository @RequiredArgsConstructor public class RoomQueryRedisAdapter implements LoadRoomPort { @@ -22,7 +21,7 @@ public class RoomQueryRedisAdapter implements LoadRoomPort { public Optional findRoomById(String roomId) { String key = buildKey(roomId); return Optional.ofNullable(redisTemplate.opsForValue().get(key)) - .flatMap(this::deserializeRoom); + .flatMap(this::deserializeRoom); } private Optional deserializeRoom(String roomJson) { diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java index ff5184d9..7b882e84 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java @@ -2,14 +2,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.SaveRoomPort; +import java.time.Duration; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; -import java.time.Duration; - @Repository @RequiredArgsConstructor public class RoomSaveRedisAdapter implements SaveRoomPort { diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java index 86da3675..1fd2142b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java @@ -2,13 +2,12 @@ import com.kok.kokcore.room.domain.Profile; import com.kok.kokcore.room.usecase.CreateRandomProfileUseCase; +import java.util.List; +import java.util.Random; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.Random; - @Service @RequiredArgsConstructor public class RandomProfileService implements CreateRandomProfileUseCase { @@ -17,13 +16,13 @@ public class RandomProfileService implements CreateRandomProfileUseCase { private String objectStorageUrl; private static final List ADJECTIVES = List.of( - "๋ฐฐ๊ณ ํ”ˆ", "๋ฉ‹์žˆ๋Š”", "์ฟจํ•œ ", "๋น ๋ฅธ", "์นœ์ ˆํ•œ", "์˜๋ฆฌํ•œ", "ํ–‰๋ณตํ•œ", "์กฐ์šฉํ•œ", "๊ฐ•๋ ฅํ•œ", "์ž์œ ๋กœ์šด", - "์Šค๋งˆํŠธํ•œ", "์ž˜์ƒ๊ธด", "๊ท€์—ฌ์šด", "ํ‰ํ™”๋กœ์šด", "๋น›๋‚˜๋Š”", "๋˜‘๋˜‘ํ•œ", "์‹ ๋‚˜๋Š”", "๋ถ€๋“œ๋Ÿฌ์šด", "์—‰๋šฑํ•œ", "๋ฌด์„œ์šด" + "๋ฐฐ๊ณ ํ”ˆ", "๋ฉ‹์žˆ๋Š”", "์ฟจํ•œ ", "๋น ๋ฅธ", "์นœ์ ˆํ•œ", "์˜๋ฆฌํ•œ", "ํ–‰๋ณตํ•œ", "์กฐ์šฉํ•œ", "๊ฐ•๋ ฅํ•œ", "์ž์œ ๋กœ์šด", + "์Šค๋งˆํŠธํ•œ", "์ž˜์ƒ๊ธด", "๊ท€์—ฌ์šด", "ํ‰ํ™”๋กœ์šด", "๋น›๋‚˜๋Š”", "๋˜‘๋˜‘ํ•œ", "์‹ ๋‚˜๋Š”", "๋ถ€๋“œ๋Ÿฌ์šด", "์—‰๋šฑํ•œ", "๋ฌด์„œ์šด" ); private static final List NOUNS = List.of( - "ํ† ๋ฏธ", "์ง€๋ฏธ", "๋ผ์ด์–ธ", "๋ฃจ์นด์Šค", "๋ฐ์ด๋น—", "์— ๋งˆ", "์˜ฌ๋ฆฌ๋ฒ„", "์†Œํ”ผ", "๋ด‰๋ด‰์ด", "ํ”ผ์น˜", - "๋ฌด์ง€", "ํŠœ๋ธŒ", "ํ”„๋กœ๋„", "์ฝ˜", "๋ธŒ๋ผ์šด", "์ฝ”์ฝ”", "ํ”„๋ Œ์ฆˆ", "ํ† ๋ผ", "ํŽญ์ˆ˜", "๋ฝ€๋กœ๋กœ" + "ํ† ๋ฏธ", "์ง€๋ฏธ", "๋ผ์ด์–ธ", "๋ฃจ์นด์Šค", "๋ฐ์ด๋น—", "์— ๋งˆ", "์˜ฌ๋ฆฌ๋ฒ„", "์†Œํ”ผ", "๋ด‰๋ด‰์ด", "ํ”ผ์น˜", + "๋ฌด์ง€", "ํŠœ๋ธŒ", "ํ”„๋กœ๋„", "์ฝ˜", "๋ธŒ๋ผ์šด", "์ฝ”์ฝ”", "ํ”„๋ Œ์ฆˆ", "ํ† ๋ผ", "ํŽญ์ˆ˜", "๋ฝ€๋กœ๋กœ" ); @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java index 23224550..19cba513 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java @@ -1,8 +1,8 @@ package com.kok.kokapi.room.application.service; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.SaveRoomPort; import com.kok.kokcore.room.usecase.CreateRoomUseCase; import com.kok.kokcore.room.usecase.JoinRoomUseCase; import lombok.RequiredArgsConstructor; diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java index 81c4b800..38085c63 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomParticipantService.java @@ -1,7 +1,7 @@ package com.kok.kokapi.room.application.service; -import com.kok.kokcore.room.application.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.port.out.SaveRoomParticipantsPort; import com.kok.kokcore.room.usecase.JoinRoomUseCase; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -10,6 +10,7 @@ @Service @RequiredArgsConstructor public class RoomParticipantService implements JoinRoomUseCase { + private final SaveRoomParticipantsPort saveRoomParticipantsPort; @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index d16d3dba..4834c775 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -2,17 +2,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.room.application.port.out.LoadRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; +import com.kok.kokcore.room.port.out.LoadRoomPort; import com.kok.kokcore.room.usecase.GetRoomUseCase; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; - @Service @RequiredArgsConstructor public class RoomQueryService implements GetRoomUseCase { @@ -20,13 +20,14 @@ public class RoomQueryService implements GetRoomUseCase { private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; private final LoadRoomPort loadRoomPort; + private final LoadRoomParticipantPort loadRoomParticipantPort; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; @Override public Room findRoomById(String roomId) { return loadRoomPort.findRoomById(roomId) - .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); + .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); } @Override @@ -45,4 +46,10 @@ public List getParticipants(String roomId) { } return members; } + + @Override + public int getParticipantsCount(String roomId) { + Long participantCount = loadRoomParticipantPort.countParticipantsById(roomId); + return participantCount.intValue(); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java index e1925816..5d3f15d2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/dto/response/RecommendedStationResponse.java @@ -2,18 +2,18 @@ import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; - import java.util.List; public record RecommendedStationResponse( - List routes, - Station station + List routes, + Station station ) { + public static RecommendedStationResponse of(Station station, List routes) { return new RecommendedStationResponse( - routes.stream().map(Route::getName).toList(), - station + routes.stream().map(Route::getName).toList(), + station ); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index be9e1455..1ddffbbe 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -4,9 +4,9 @@ import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; import com.kok.kokapi.station.application.service.StationFacadeService; -import com.kok.kokcore.station.application.usecase.CustomStationUseCase; -import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; -import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.usecase.CustomStationUseCase; +import com.kok.kokcore.station.usecase.RecommendStationUseCase; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.domain.entity.Station; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java index eac61006..fdb022cd 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationClient.java @@ -1,8 +1,8 @@ package com.kok.kokapi.station.adapter.out.external; import com.kok.kokapi.station.adapter.out.external.dto.StationResponses; -import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.port.out.LoadStationsPort; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; import java.util.StringJoiner; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -23,7 +23,8 @@ public class StationClient implements LoadStationsPort { private final StationClientProperties properties; private final StationErrorHandler stationErrorHandler; - public StationClient(StationClientProperties properties, StationErrorHandler stationErrorHandler) { + public StationClient(StationClientProperties properties, + StationErrorHandler stationErrorHandler) { this.properties = properties; this.stationErrorHandler = stationErrorHandler; this.restClient = getRestClient(); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java index 408e4d2d..f4487e65 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/StationErrorHandler.java @@ -28,7 +28,8 @@ public boolean hasError(ClientHttpResponse response) throws IOException { } @Override - public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException { + public void handleError(URI url, HttpMethod method, ClientHttpResponse response) + throws IOException { throw new RuntimeException("์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ์ง€์†์ ์œผ๋กœ ๋ฐœ์ƒ์‹œ ์—ด๋ฆฐ ๋ฐ์ดํ„ฐ ๊ด‘์žฅ์œผ๋กœ ๋ฌธ์˜(Q&A) ๋ฐ”๋ž๋‹ˆ๋‹ค."); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java index 24245da5..9c65262e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponse.java @@ -1,7 +1,7 @@ package com.kok.kokapi.station.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDto; +import com.kok.kokcore.station.port.out.dto.StationRouteDto; public record StationResponse( @JsonProperty("BLDN_ID") diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java index c77c4dd0..3c9ce15e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/external/dto/StationResponses.java @@ -1,7 +1,7 @@ package com.kok.kokapi.station.adapter.out.external.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; public record StationResponses( @JsonProperty("subwayStationMaster") diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java index ef71f2fc..1dd6b2cc 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.station.application.port.out.SaveCustomStationsPort; +import com.kok.kokcore.station.port.out.SaveCustomStationsPort; import com.kok.kokcore.station.domain.entity.Station; import java.util.ArrayList; import java.util.List; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java index 192a3bcd..0ad7bcae 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.station.application.port.out.ReadCustomStationsPort; +import com.kok.kokcore.station.port.out.ReadCustomStationsPort; import com.kok.kokcore.station.domain.entity.Station; import java.util.Collections; import java.util.List; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java index 07ed02fe..55232744 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java @@ -1,12 +1,11 @@ package com.kok.kokapi.station.adapter.out.persistence; -import com.kok.kokcore.station.application.port.out.RetrieveRoutePort; -import com.kok.kokcore.station.application.port.out.SaveRoutePort; import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveRoutePort; +import com.kok.kokcore.station.port.out.SaveRoutePort; import java.util.List; import java.util.function.Function; - -import com.kok.kokcore.station.domain.entity.Station; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java index 6f04c243..0fdf9a71 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java @@ -2,9 +2,8 @@ import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; -import org.springframework.data.jpa.repository.JpaRepository; - import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; public interface RouteRepository extends JpaRepository { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index d9f53d6b..a7f2e300 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -1,15 +1,14 @@ package com.kok.kokapi.station.adapter.out.persistence; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokcore.station.application.port.out.ReadStationsPort; -import com.kok.kokcore.station.application.port.out.RetrieveStationsPort; -import com.kok.kokcore.station.application.port.out.SaveStationsPort; import com.kok.kokcore.station.domain.entity.Station; - +import com.kok.kokcore.station.port.out.ReadStationsPort; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; +import com.kok.kokcore.station.port.out.SaveStationsPort; import java.math.BigDecimal; -import java.util.*; +import java.util.List; +import java.util.Optional; import java.util.function.Function; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.locationtech.jts.geom.Point; @@ -22,7 +21,8 @@ @Repository @Slf4j @RequiredArgsConstructor -public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort, RetrieveStationsPort { +public class StationPersistenceAdapter implements SaveStationsPort, ReadStationsPort, + RetrieveStationsPort { private final PointConverter pointConverter; @@ -76,7 +76,8 @@ public Optional retrieveStation(Long stationId) { @Transactional(readOnly = true) public List retrieveInRangeStations(Point centroid, double dist) { Pair lonLat = pointConverter.toCoordinates(centroid); - return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), lonLat.getSecond(), dist); + return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), + lonLat.getSecond(), dist); } @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 94a7ce77..27f8dcd3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -1,16 +1,13 @@ package com.kok.kokapi.station.adapter.out.persistence; import com.kok.kokcore.station.domain.entity.Station; - +import io.lettuce.core.dynamic.annotation.Param; import java.math.BigDecimal; import java.util.List; - -import io.lettuce.core.dynamic.annotation.Param; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.Optional; - public interface StationRepository extends JpaRepository { @Query("SELECT EXISTS (SELECT 1 FROM Station)") @@ -21,11 +18,12 @@ public interface StationRepository extends JpaRepository { Optional findStationById(Long stationId); @Query(value = """ - SELECT * FROM station - WHERE ST_Distance_Sphere(Point(longitude, latitude), Point(:lon, :lat)) < :distance - AND priority > 0 - """, nativeQuery = true) - List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, @Param("lat") BigDecimal lat, @Param("distance") Double distance); + SELECT * FROM station + WHERE ST_Distance_Sphere(Point(longitude, latitude), Point(:lon, :lat)) < :distance + AND priority > 0 + """, nativeQuery = true) + List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, + @Param("lat") BigDecimal lat, @Param("distance") Double distance); @Query(value = """ SELECT * FROM station diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java b/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java deleted file mode 100644 index b424df1b..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/config/StationsConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.kok.kokapi.station.application.config; - -import com.kok.kokapi.station.application.service.StationService; -import org.springframework.boot.CommandLineRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class StationsConfig { - - @Bean - CommandLineRunner initStations(StationService stationService) { - return args -> stationService.saveStations(); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java index 56eb194e..23003da3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/RouteService.java @@ -1,14 +1,13 @@ package com.kok.kokapi.station.application.service; -import com.kok.kokcore.station.application.port.out.RetrieveRoutePort; -import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveRoutePort; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class RouteService implements RetrieveRouteUseCase { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java index 20e010f8..12252b2d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java @@ -1,9 +1,9 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; -import com.kok.kokcore.station.application.usecase.CustomStationUseCase; -import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; -import com.kok.kokcore.station.application.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.usecase.CustomStationUseCase; +import com.kok.kokcore.station.usecase.RecommendStationUseCase; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.domain.entity.Station; import java.util.List; import java.util.stream.Stream; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index fe16b939..b5eae2ab 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -3,12 +3,12 @@ import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokapi.station.adapter.out.persistence.CustomStationCommandRedisAdapter; import com.kok.kokapi.station.adapter.out.persistence.CustomStationQueryRedisAdapter; -import com.kok.kokcore.location.application.port.out.ReadCentroidPort; -import com.kok.kokcore.station.application.port.out.*; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; -import com.kok.kokcore.station.application.usecase.CustomStationUseCase; -import com.kok.kokcore.station.application.usecase.RecommendStationUseCase; -import com.kok.kokcore.station.application.usecase.SaveStationUseCase; +import com.kok.kokcore.location.port.out.ReadCentroidPort; +import com.kok.kokcore.station.port.out.*; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.usecase.CustomStationUseCase; +import com.kok.kokcore.station.usecase.RecommendStationUseCase; +import com.kok.kokcore.station.usecase.SaveStationUseCase; import com.kok.kokcore.station.domain.entity.Station; import java.math.BigDecimal; diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index f73e280c..0e9064c7 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -38,10 +38,10 @@ station: format: json start-idx: 1 end-idx: 1000 - + ncp: object-storage-url: ${OBJECT_STORAGE_URL} - + tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index d6528d84..eb2d8734 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -51,7 +51,7 @@ station: ncp: object-storage-url: ${OBJECT_STORAGE_URL} - + tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-api/src/main/resources/db/migration/V1__init.sql b/kok-api/src/main/resources/db/migration/V1__init.sql index 4bddd843..6c325391 100644 --- a/kok-api/src/main/resources/db/migration/V1__init.sql +++ b/kok-api/src/main/resources/db/migration/V1__init.sql @@ -20,10 +20,10 @@ CREATE TABLE route create table location ( - id bigint auto_increment primary key, - member_id int not null, - location_point point not null, - room_id varchar(255) not null, + id bigint auto_increment primary key, + member_id varchar(255) not null, + location_point point not null, + room_id varchar(255) not null, constraint UKrgpajb4rsivb4gj9xn2qowgw6 unique (room_id, member_id) ); diff --git a/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java b/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java index f1d8c6e4..577fee89 100644 --- a/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java +++ b/kok-api/src/test/java/com/kok/kokapi/KokApiApplicationTests.java @@ -7,8 +7,8 @@ @SpringBootTest class KokApiApplicationTests extends ServiceTest { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java index 157ffe2d..b866a12c 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/RepositoryTest.java @@ -1,18 +1,21 @@ package com.kok.kokapi.common.template; -import com.kok.kokapi.config.DataJpaTestConfig; +import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.config.StationTestConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; import org.springframework.test.context.TestPropertySource; -@DataJpaTest -@Import({DataJpaTestConfig.class}) +@ExtendWith(DatabaseCleanerExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@Import({StationTestConfiguration.class}) @AutoConfigureTestDatabase(replace = Replace.NONE) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) @Profile("test") -public abstract class RepositoryTest extends ContainerBaseTest{ +public abstract class RepositoryTest extends ContainerBaseTest { } diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java index 1648b445..706ac3a3 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/ServiceTest.java @@ -1,7 +1,7 @@ package com.kok.kokapi.common.template; -import com.kok.kokapi.common.util.MySQLDatabaseCleanerExtension; -import com.kok.kokapi.config.ServiceTestConfig; +import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.config.StationTestConfiguration; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationClient; import com.kok.kokapi.public_transportation.adapter.out.external.PublicTransportationComplexClient; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,12 +11,12 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; -@ExtendWith(MySQLDatabaseCleanerExtension.class) +@ExtendWith(DatabaseCleanerExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) -@Import({ServiceTestConfig.class}) +@Import({StationTestConfiguration.class}) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) @Profile("test") -public abstract class ServiceTest extends ContainerBaseTest{ +public abstract class ServiceTest extends ContainerBaseTest { @MockitoBean protected PublicTransportationComplexClient publicTransportationComplexClient; diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java index b2b89aae..466db4f6 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java @@ -8,7 +8,7 @@ import org.springframework.transaction.annotation.Transactional; @Component -public class MySQLDatabaseCleaner { +public class MySQLDatabaseCleaner implements DatabaseCleaner { @PersistenceContext private EntityManager entityManager; diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java deleted file mode 100644 index 883a572c..00000000 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleanerExtension.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.kok.kokapi.common.util; - -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -public class MySQLDatabaseCleanerExtension implements BeforeEachCallback { - - @Override - public void beforeEach(ExtensionContext context) { - MySQLDatabaseCleaner mySQLDatabaseCleaner = SpringExtension.getApplicationContext(context) - .getBean(MySQLDatabaseCleaner.class); - mySQLDatabaseCleaner.cleanUp(); - } -} diff --git a/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java deleted file mode 100644 index b30b6c03..00000000 --- a/kok-api/src/test/java/com/kok/kokapi/config/DataJpaTestConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokapi.config; - -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.boot.test.context.TestConfiguration; - -@TestConfiguration -@EntityScan(basePackages = "com.kok.kokcore") -public class DataJpaTestConfig { - -} diff --git a/kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java b/kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java deleted file mode 100644 index 8c0042a2..00000000 --- a/kok-api/src/test/java/com/kok/kokapi/config/ServiceTestConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.kok.kokapi.config; - -import com.kok.kokapi.station.adapter.out.external.FakeStationClient; -import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; - -@TestConfiguration -public class ServiceTestConfig { - - @Bean - @Primary - public LoadStationsPort loadStationsPort() { - return new FakeStationClient(); - } -} diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java index 2f2d6afb..63894505 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java @@ -29,14 +29,17 @@ void createRoom() { Room createdRoom = roomCreationService.createRoom(roomName, capacity, host); assertAll("Room Create Test", - () -> assertNotNull(createdRoom, "Room ๊ฐ์ฒด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertNotNull(createdRoom.getId(), "์•ฝ์†๋ฐฉ ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(roomName, createdRoom.getRoomName(), "์•ฝ์†๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(capacity, createdRoom.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertNotNull(createdRoom.getMember(), "๋ฐฉ์žฅ ์ •๋ณด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostNickname, createdRoom.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostProfile, createdRoom.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(MemberRole.LEADER, createdRoom.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + () -> assertNotNull(createdRoom, "Room ๊ฐ์ฒด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertNotNull(createdRoom.getId(), "์•ฝ์†๋ฐฉ ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(roomName, createdRoom.getRoomName(), "์•ฝ์†๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(capacity, createdRoom.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertNotNull(createdRoom.getMember(), "๋ฐฉ์žฅ ์ •๋ณด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostNickname, createdRoom.getMember().getNickname(), + "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostProfile, createdRoom.getMember().getProfile(), + "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(MemberRole.LEADER, createdRoom.getMember().getRole(), + "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") ); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java index 560c7f58..e2f134a7 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java @@ -4,10 +4,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.kok.kokapi.common.template.ServiceTest; -import com.kok.kokcore.room.application.port.out.SaveRoomPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; +import com.kok.kokcore.room.port.out.SaveRoomPort; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java index 5eb98f87..1bb0b5b2 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/external/FakeStationClient.java @@ -1,8 +1,8 @@ package com.kok.kokapi.station.adapter.out.external; -import com.kok.kokcore.station.application.port.out.LoadStationsPort; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDto; -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.port.out.LoadStationsPort; +import com.kok.kokcore.station.port.out.dto.StationRouteDto; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; import java.util.List; public class FakeStationClient implements LoadStationsPort { diff --git a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java index af8a4587..8c05ed50 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapterTest.java @@ -21,8 +21,8 @@ class StationPersistenceAdapterTest extends ServiceTest { @Test void saveStationsAndReturn() { // given - Station savedStation = new Station("๋ง์›์—ญ", "12.345","123.456"); - List stations = List.of(new Station("ํ•ฉ์ •์—ญ", "12.345","123.456")); + Station savedStation = new Station("๋ง์›์—ญ", "12.345", "123.456"); + List stations = List.of(new Station("ํ•ฉ์ •์—ญ", "12.345", "123.456")); stationRepository.save(savedStation); // when @@ -34,4 +34,4 @@ void saveStationsAndReturn() { () -> assertThat(result.get(0).getName()).isEqualTo("ํ•ฉ์ •์—ญ") ); } -} \ No newline at end of file +} diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index cdc21862..53e3af3c 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -16,8 +16,8 @@ spring: defer-datasource-initialization: false open-in-view: false hibernate: -### ์‹ค์ œ ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์งœ ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ddl-auto: validate +### ์‹ค์ œ ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์งœ ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ncp: object-storage-url: mock tmap-sub: diff --git a/kok-core/build.gradle b/kok-core/build.gradle index c58c530f..9892958b 100644 --- a/kok-core/build.gradle +++ b/kok-core/build.gradle @@ -1,28 +1,28 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.2' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.kok' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java b/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java index a11e019f..a3f2659b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java +++ b/kok-core/src/main/java/com/kok/kokcore/KokCoreApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class KokCoreApplication { - public static void main(String[] args) { - SpringApplication.run(KokCoreApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(KokCoreApplication.class, args); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java deleted file mode 100644 index f664cea5..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadCentroidPort.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kok.kokcore.location.application.port.out; - -import org.locationtech.jts.geom.Point; - -public interface ReadCentroidPort { - Point findCentroidByRoomId(String roomId); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java deleted file mode 100644 index caedb798..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/ReadLocationPort.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.kok.kokcore.location.application.port.out; - -import com.kok.kokcore.location.domain.Location; - -import java.util.List; -import java.util.Optional; - -public interface ReadLocationPort { - Optional findLocationByRoomIdAndMemberId(String roomId, String memberId); - List findLocationsByRoomId(String roomId); - List findInsideConvexHull(String roomId); - List findConvexHull(String roomId); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java deleted file mode 100644 index 45656669..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/location/application/port/out/SaveLocationPort.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kok.kokcore.location.application.port.out; - -import com.kok.kokcore.location.domain.Location; -import org.locationtech.jts.geom.Point; - -public interface SaveLocationPort { - Location saveLocation(String roomId, String memberId, Point point); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index 2f041c44..4641d8ae 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -1,6 +1,12 @@ package com.kok.kokcore.location.domain; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,10 +16,10 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "location" , - uniqueConstraints = { - @UniqueConstraint(columnNames = {"room_id", "member_id"}) - }) +@Table(name = "location", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"room_id", "member_id"}) + }) public class Location { diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java index fc3a1033..0c8a87d6 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java @@ -1,11 +1,14 @@ package com.kok.kokcore.location.usecase; import com.kok.kokcore.location.domain.Location; - import java.math.BigDecimal; public interface CreateLocationUseCase { - Location createLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude); - Location updateLocation(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude); + + Location createLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude); + + Location updateLocation(String roomId, String memberId, BigDecimal latitude, + BigDecimal longitude); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java index 1c9e2ff3..bba313b4 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/LoadCentroidUseCase.java @@ -1,11 +1,11 @@ package com.kok.kokcore.location.usecase; +import java.math.BigDecimal; import org.locationtech.jts.geom.Point; import org.springframework.data.util.Pair; -import java.math.BigDecimal; - public interface LoadCentroidUseCase { + Point readCentroid(String roomId); Pair readCentroidCoordinates(String roomId); diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java index f719144b..9cb06333 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/ReadLocationUseCase.java @@ -1,12 +1,15 @@ package com.kok.kokcore.location.usecase; import com.kok.kokcore.location.domain.Location; - import java.util.List; public interface ReadLocationUseCase { + Location readLocation(String roomId, String memberId); + List readLocations(String roomId); + List readInsideConvexHull(String roomId); + List readConvexHull(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java deleted file mode 100644 index 0cf1b74d..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/public_transfortation/usecase/RetrievePublicTransportationUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kok.kokcore.public_transfortation.usecase; - -public interface RetrievePublicTransportationUseCase { - - String retrievePublicTransportation(Long stationId, String roomId, String memberId); - String retrieveComplexPublicTransportation(Long stationId, String roomId, String memberId); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java deleted file mode 100644 index feaf9237..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/LoadRoomPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.kok.kokcore.room.application.port.out; - -import com.kok.kokcore.room.domain.Room; - -import java.util.Optional; - -public interface LoadRoomPort { - Optional findRoomById(String roomId); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java deleted file mode 100644 index b41a31d4..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomParticipantsPort.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kok.kokcore.room.application.port.out; - -import com.kok.kokcore.room.domain.Member; - -public interface SaveRoomParticipantsPort { - int joinRoom(String roomId, Member member); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java deleted file mode 100644 index 55bb64ef..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/room/application/port/out/SaveRoomPort.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.kok.kokcore.room.application.port.out; - -import com.kok.kokcore.room.domain.Room; - -public interface SaveRoomPort { - Room save(Room room); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java index 873bf583..c8a9e5b7 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Member.java @@ -1,16 +1,16 @@ package com.kok.kokcore.room.domain; import com.kok.kokcore.room.domain.vo.MemberRole; +import java.util.UUID; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; -import java.util.UUID; - @Getter @ToString @EqualsAndHashCode public class Member { + private final String memberId; private final String nickname; private final String profile; diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java index 86e5bd7a..1ec61e9f 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Profile.java @@ -8,6 +8,7 @@ @ToString @EqualsAndHashCode public class Profile { + private final String imageUrl; private final String nickname; diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index d28cc226..6a1007de 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -1,26 +1,34 @@ package com.kok.kokcore.room.domain; -import lombok.*; - import java.io.Serializable; +import java.time.LocalDateTime; import java.util.UUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; @Getter @ToString @EqualsAndHashCode public class Room implements Serializable { - public static final int REQUIRED_CAPACITY = 2; + private static final int REQUIRED_CAPACITY = 2; + private static final long LOCATION_INPUT_TIME_LIMIT = 6; + private final String id; // ์•ฝ์†๋ฐฉ ID (UUID) private final String roomName; // ์•ฝ์†๋ฐฉ ์ด๋ฆ„ private final int capacity; // ์ฐธ์—ฌ์ธ์› ์ˆ˜ (์ตœ์†Œ 2๋ช… ์ด์ƒ) private final Member member; // ๋ฐฉ ์ฐธ์—ฌ์ž + private final LocalDateTime locationInputLimitDateTime; // ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ์ผ์‹œ + private final LocalDateTime createdDateTime; // ๋ฐฉ ์ƒ์„ฑ์ผ์‹œ private Room(String id, String roomName, int capacity, Member member) { this.id = id; this.roomName = roomName; this.capacity = capacity; this.member = member; + this.createdDateTime = LocalDateTime.now().withNano(0); + this.locationInputLimitDateTime = createdDateTime.plusHours(LOCATION_INPUT_TIME_LIMIT); } public static Room create(String roomName, int capacity, Member member) { @@ -39,4 +47,17 @@ private static void validateParameter(String roomName, int capacity) { throw new IllegalArgumentException("At least 2 participants are required"); } } + + public boolean hasLocationInputEnded(long locationInputCount, LocalDateTime currentTime) { + return isAllLocationInput(locationInputCount) || currentTime.isAfter( + locationInputLimitDateTime); + } + + private boolean isAllLocationInput(long participantCount) { + return participantCount == capacity; + } + + public boolean isFull(int participantCount) { + return capacity == participantCount; + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java index 1d9ac350..6cbecdc1 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRandomProfileUseCase.java @@ -3,5 +3,6 @@ import com.kok.kokcore.room.domain.Profile; public interface CreateRandomProfileUseCase { + Profile createProfile(); -} \ No newline at end of file +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java index 166e6a5a..b686dc74 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/CreateRoomUseCase.java @@ -4,5 +4,6 @@ import com.kok.kokcore.room.domain.Room; public interface CreateRoomUseCase { + Room createRoom(String roomName, int capacity, Member host); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java index 37c9d32f..bfa586a5 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -2,10 +2,13 @@ import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; - import java.util.List; public interface GetRoomUseCase { + Room findRoomById(String roomId); + List getParticipants(String roomId); + + int getParticipantsCount(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java index 0044524d..cce7b952 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/JoinRoomUseCase.java @@ -3,5 +3,6 @@ import com.kok.kokcore.room.domain.Member; public interface JoinRoomUseCase { + int joinRoom(String roomId, Member member); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java deleted file mode 100644 index 31d36206..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/LoadStationsPort.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.application.port.out.dto.StationRouteDtos; - -public interface LoadStationsPort { - - StationRouteDtos loadAllStations(); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java deleted file mode 100644 index 6d246a5d..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadCustomStationsPort.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; - -public interface ReadCustomStationsPort { - List findRecommendedStationsByRoomId(String roomId); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java deleted file mode 100644 index f18be07d..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/ReadStationsPort.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -public interface ReadStationsPort { - - boolean hasNoStations(); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java deleted file mode 100644 index 4fd0652c..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveRoutePort.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.domain.entity.Route; -import com.kok.kokcore.station.domain.entity.Station; - -import java.util.List; - -public interface RetrieveRoutePort { - List retrieveRoutes(Station station); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java deleted file mode 100644 index 615b6149..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/RetrieveStationsPort.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.domain.entity.Station; -import org.locationtech.jts.geom.Point; - -import java.util.List; -import java.util.Optional; - -public interface RetrieveStationsPort { - Optional retrieveStation(Long stationId); - List retrieveInRangeStations(Point centroid, double dist); - List retrieveStationsByKeyword(String keyword); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java deleted file mode 100644 index 7be52d62..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveCustomStationsPort.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.domain.entity.Station; - -public interface SaveCustomStationsPort { - - Station addCustomStations(String roomId, Station station); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java deleted file mode 100644 index 7d050c7e..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveRoutePort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.domain.entity.Route; -import java.util.List; - -public interface SaveRoutePort { - - void saveRoutes(List routes); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java deleted file mode 100644 index d781d35e..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/SaveStationsPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.kok.kokcore.station.application.port.out; - -import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; - -public interface SaveStationsPort { - - List saveStations(List stations); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java deleted file mode 100644 index aecd3890..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.kok.kokcore.station.application.port.out.dto; - -import com.kok.kokcore.station.domain.entity.Route; -import com.kok.kokcore.station.domain.entity.Station; - -public record StationRouteDto( - String name, - String latitude, - String longitude, - Long stationId, - String route -) { - - public boolean hasName(Station station) { - return name.equals(station.getName()); - } - - public Station toStation() { - return new Station(name, latitude, longitude); - } - - public Route toRouteByStation(Station station) { - return new Route(stationId, route, station); - } -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java b/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java deleted file mode 100644 index f0889157..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtos.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.kok.kokcore.station.application.port.out.dto; - -import com.kok.kokcore.station.domain.entity.Route; -import com.kok.kokcore.station.domain.entity.Station; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -public record StationRouteDtos( - List stationRouteDtos -) { - - public boolean isEmpty() { - return stationRouteDtos().isEmpty(); - } - - public List toStations() { - return distinctByName().stream() - .map(StationRouteDto::toStation) - .toList(); - } - - private List distinctByName() { - return new ArrayList<>(stationRouteDtos.stream() - .collect(Collectors.toMap( - StationRouteDto::name, - dto -> dto, - (existing, replacement) -> existing - )) - .values()); - } - - public List toRoutesByStations(List stations){ - List routes = new ArrayList<>(); - for (Station station : stations) { - List routesOfStation = stationRouteDtos.stream() - .filter(stationRouteDto -> stationRouteDto.hasName(station)) - .map(stationRouteDto -> stationRouteDto.toRouteByStation(station)) - .toList(); - routes.addAll(routesOfStation); - } - return routes; - } -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java deleted file mode 100644 index 9629ccc1..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/CustomStationUseCase.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.kok.kokcore.station.application.usecase; - -import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; - -public interface CustomStationUseCase { - - Station addCustomStations(String roomId, Long stationId); - - List getCustomRecommendedStations(String roomId); - - List searchStations(String keyword); - -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java deleted file mode 100644 index 3130deb4..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RecommendStationUseCase.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.kok.kokcore.station.application.usecase; - -import com.kok.kokcore.station.domain.entity.Station; - -import java.util.List; - - -public interface RecommendStationUseCase { - - List recommendStations(String roomId); - -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java deleted file mode 100644 index 56488848..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/RetrieveRouteUseCase.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokcore.station.application.usecase; - -import com.kok.kokcore.station.domain.entity.Route; -import com.kok.kokcore.station.domain.entity.Station; - -import java.util.List; - -public interface RetrieveRouteUseCase { - List retrieveRoutes(Station station); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java deleted file mode 100644 index baf08bb8..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/application/usecase/SaveStationUseCase.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.kok.kokcore.station.application.usecase; - -public interface SaveStationUseCase { - - void saveStations(); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java index 9ea21487..34f43a6b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java @@ -34,7 +34,7 @@ public Station(String name, BigDecimal latitude, BigDecimal longitude, long prio this.priority = priority; } - public Station(String name,String latitude, String longitude) { - this(name, new BigDecimal(latitude),new BigDecimal(longitude), 0); + public Station(String name, String latitude, String longitude) { + this(name, new BigDecimal(latitude), new BigDecimal(longitude), 0); } } diff --git a/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java b/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java index ea8c4192..47c1898f 100644 --- a/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java +++ b/kok-core/src/test/java/com/kok/kokcore/KokCoreApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class KokCoreApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java b/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java index 9b6ed02e..3c4b252f 100644 --- a/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/location/domain/LocationTest.java @@ -1,4 +1,5 @@ package com.kok.kokcore.location.domain; public class LocationTest { + } diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java index e64a342e..576d21e1 100644 --- a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -1,11 +1,17 @@ package com.kok.kokcore.room.domain; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.kok.kokcore.room.domain.vo.MemberRole; +import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class RoomTest { @DisplayName("์•ฝ์†๋ฐฉ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @@ -16,7 +22,7 @@ void createRoom() { int capacity = 4; String hostProfile = "hostProfile"; String hostNickname = "test"; - String password = "secret"; + LocalDateTime deadline = LocalDateTime.now().withNano(0).plusHours(6); Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When @@ -24,12 +30,15 @@ void createRoom() { // Then assertAll("์•ฝ์†๋ฐฉ ์ƒ์„ฑ", - () -> assertNotNull(room.getId(), "ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(roomName, room.getRoomName(), "๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(capacity, room.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostNickname, room.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(hostProfile, room.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(MemberRole.LEADER, room.getMember().getRole(), "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + () -> assertNotNull(room.getId(), "ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(roomName, room.getRoomName(), "๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(capacity, room.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(deadline.getHour(), room.getLocationInputLimitDateTime().getHour(), + "์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์ด ์ •ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostNickname, room.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(hostProfile, room.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(MemberRole.LEADER, room.getMember().getRole(), + "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") ); } @@ -41,12 +50,12 @@ void createRoomWithEmptyRoomName() { int capacity = 4; String hostNickname = "hostNickname"; String hostProfile = "hostProfile"; - String password = "secret"; Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When & Then IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host)); + assertThrows(IllegalArgumentException.class, + () -> Room.create(roomName, capacity, host)); assertTrue(exception.getMessage().contains("Room name is required")); } @@ -62,7 +71,47 @@ void createRoomWithInvalidCapacity() { // When & Then IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> Room.create(roomName, capacity, host)); + assertThrows(IllegalArgumentException.class, + () -> Room.create(roomName, capacity, host)); assertTrue(exception.getMessage().contains("At least 2 participants are required")); } + + @DisplayName("์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void hasLocationInputEndedByDeadlineExceeded() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.hasLocationInputEnded(1, LocalDateTime.now().plusHours(6)); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("๋ชจ๋“  ์ฐธ๊ฐ€์ž๊ฐ€ ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void hasLocationInputEndedByAllParticipantCompleted() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.hasLocationInputEnded(2, LocalDateTime.now().plusHours(5)); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜์ง€๋„, ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€๋„ ์•Š์•˜์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void hasNotLocationInputEnded() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.hasLocationInputEnded(1, LocalDateTime.now().plusHours(5)); + + // then + assertThat(result).isFalse(); + } } diff --git a/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java b/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java deleted file mode 100644 index 6fd8c227..00000000 --- a/kok-core/src/test/java/com/kok/kokcore/station/application/port/out/dto/StationRouteDtosTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.kok.kokcore.station.application.port.out.dto; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.kok.kokcore.station.domain.entity.Route; -import com.kok.kokcore.station.domain.entity.Station; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class StationRouteDtosTest { - - @DisplayName("stationRouteDtos๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ์ฐธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void isEmpty() { - // given - StationRouteDtos stationRouteDtos = new StationRouteDtos(List.of()); - - // when - boolean result = stationRouteDtos.isEmpty(); - - // then - assertThat(result).isTrue(); - } - - @DisplayName("stationRouteDtos๊ฐ€ ๋น„์–ด ์žˆ์ง€ ์•Š์œผ๋ฉด ๊ฑฐ์ง“์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void isNotEmpty() { - // given - StationRouteDto stationRouteDto = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, - "1ํ˜ธ์„ "); - StationRouteDtos stationRouteDtos = new StationRouteDtos(List.of(stationRouteDto)); - - // when - boolean result = stationRouteDtos.isEmpty(); - - // then - assertThat(result).isFalse(); - } - - @DisplayName("stationRouteDtos๋ฅผ Station ๋ฆฌ์ŠคํŠธ๋กœ ์ค‘๋ณต ์—†์ด ๋ณ€ํ™˜ํ•œ๋‹ค.") - @Test - void toStations() { - // given - StationRouteDto stationRouteDto1 = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, - "1ํ˜ธ์„ "); - StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, - "2ํ˜ธ์„ "); - StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.496", "127.029", 3L, "์‹ ๋ถ„๋‹น์„ "); - StationRouteDtos stationRouteDtos = new StationRouteDtos( - List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) - ); - - // when - List stations = stationRouteDtos.toStations(); - - // then - List names = stations.stream().map(Station::getName).toList(); - - assertAll( - () -> assertThat(stations).hasSize(2), - () -> assertThat(names).containsExactlyInAnyOrder("์„œ์šธ์—ญ", "๊ฐ•๋‚จ์—ญ") - ); - } - - @DisplayName("stationRouteDtos๋ฅผ Route ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.") - @Test - void toRoutesByStations() { - // given - StationRouteDto stationRouteDto1 = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, - "1ํ˜ธ์„ "); - StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, - "2ํ˜ธ์„ "); - StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 3L, "์‹ ๋ถ„๋‹น์„ "); - StationRouteDtos stationRouteDtos = new StationRouteDtos( - List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) - ); - Station station1 = new Station("์„œ์šธ์—ญ", "37.556", "126.972"); - Station station2 = new Station("๊ฐ•๋‚จ์—ญ", "37.497", "127.028"); - - // when - List routes = stationRouteDtos.toRoutesByStations(List.of(station1, station2)); - - // then - List codes = routes.stream().map(Route::getCode).toList(); - List stations = routes.stream().map(Route::getStation).distinct().toList(); - - assertAll( - () -> assertThat(routes).hasSize(3), - () -> assertThat(codes).containsExactlyInAnyOrder(1L, 2L, 3L), - () -> assertThat(stations).containsExactlyInAnyOrder(station1, station2) - ); - } -} \ No newline at end of file From afb989b46555fee05588d5b0ba2b05e08b7d7296 Mon Sep 17 00:00:00 2001 From: minseokey Date: Fri, 28 Mar 2025 01:50:45 +0900 Subject: [PATCH 105/163] :recycle: fix: resolve conflicts --- .../kokapi/config/redis/RedisCacheConfig.java | 71 ++++++++ .../adapter/in/dto/request/RouteRequest.java | 13 ++ ...lexPublicTransportationParsedResponse.java | 37 +++++ ...mapPublicTransportationParsedResponse.java | 12 ++ .../in/dto/response/CreateRoomResponse.java | 24 +++ .../in/dto/response/RoomMembersResponses.java | 25 +++ .../in/dto/response/RoomStatusResponse.java | 5 + .../RoomParticipantQueryRedisAdapter.java | 28 ++++ .../service/RoomFacadeService.java | 34 ++++ .../kokapi/station/config/StationsConfig.java | 15 ++ .../common/template/IntegrationTest.java | 28 ++++ .../kokapi/common/util/DatabaseCleaner.java | 6 + .../common/util/DatabaseCleanerExtension.java | 16 ++ .../common/util/RedisDatabaseCleaner.java | 29 ++++ .../config/StationTestConfiguration.java | 17 ++ .../com/kok/kokapi/fixture/MemberFixture.java | 15 ++ .../com/kok/kokapi/fixture/PointFixture.java | 15 ++ .../com/kok/kokapi/fixture/RoomFixture.java | 11 ++ .../adapter/in/web/RoomIntegrationTest.java | 154 ++++++++++++++++++ .../RoomParticipantQueryRedisAdapterTest.java | 31 ++++ .../location/port/out/ReadCentroidPort.java | 8 + .../location/port/out/ReadLocationPort.java | 16 ++ .../location/port/out/SaveLocationPort.java | 9 + .../RetrievePublicTransportationUseCase.java | 8 + .../port/out/LoadRoomParticipantPort.java | 6 + .../kokcore/room/port/out/LoadRoomPort.java | 9 + .../port/out/SaveRoomParticipantsPort.java | 8 + .../kokcore/room/port/out/SaveRoomPort.java | 8 + .../station/port/out/LoadStationsPort.java | 8 + .../port/out/ReadCustomStationsPort.java | 8 + .../station/port/out/ReadStationsPort.java | 6 + .../station/port/out/RetrieveRoutePort.java | 10 ++ .../port/out/RetrieveStationsPort.java | 14 ++ .../port/out/SaveCustomStationsPort.java | 8 + .../station/port/out/SaveRoutePort.java | 9 + .../station/port/out/SaveStationsPort.java | 9 + .../station/port/out/dto/StationRouteDto.java | 25 +++ .../port/out/dto/StationRouteDtos.java | 44 +++++ .../station/usecase/CustomStationUseCase.java | 14 ++ .../usecase/RecommendStationUseCase.java | 11 ++ .../station/usecase/RetrieveRouteUseCase.java | 10 ++ .../station/usecase/SaveStationUseCase.java | 6 + .../port/out/dto/StationRouteDtosTest.java | 97 +++++++++++ 43 files changed, 937 insertions(+) create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapComplexPublicTransportationParsedResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapPublicTransportationParsedResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/station/config/StationsConfig.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadCentroidPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/public_transportation/usecase/RetrievePublicTransportationUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomParticipantsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveRoutePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveRoutePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/usecase/CustomStationUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/usecase/RetrieveRouteUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/usecase/SaveStationUseCase.java create mode 100644 kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java new file mode 100644 index 00000000..2a69fa38 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java @@ -0,0 +1,71 @@ +package com.kok.kokapi.config.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisCacheConfig { + + @Bean("cacheManager") + @Primary + public CacheManager defaultCacheManager(RedisConnectionFactory redisConnectionFactory){ + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .build(); + } + + @Bean("stationCacheManager") + public CacheManager stationCacheManager(RedisConnectionFactory redisConnectionFactory) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.activateDefaultTyping( + LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL + ); // ํƒ€์ž… ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜์—ฌ ์—ญ์ง๋ ฌํ™”ํ•  ๋•Œ ์›๋ณธ ํƒ€์ž…์„ ์œ ์ง€ + + // Jackson Serializer ์„ค์ • + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + // RedisCacheConfiguration ์„ค์ • + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)) + .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 3์ผ + + return RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); + } + + @Bean("publicTransportationCacheManager") + public CacheManager publicTransportationCacheManager(RedisConnectionFactory redisConnectionFactory) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.deactivateDefaultTyping(); + + GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + // RedisCacheConfiguration ์„ค์ • + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) + .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java new file mode 100644 index 00000000..83f631df --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/request/RouteRequest.java @@ -0,0 +1,13 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record RouteRequest( + @NotBlank(message = "roomId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String roomId, + @NotNull(message = "Member ID(๋ฉค๋ฒ„ ์ผ๋ จ๋ฒˆํ˜ธ)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String memberId +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapComplexPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapComplexPublicTransportationParsedResponse.java new file mode 100644 index 00000000..d6594f85 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapComplexPublicTransportationParsedResponse.java @@ -0,0 +1,37 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.Serializable; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public class TmapComplexPublicTransportationParsedResponse implements Serializable { + + private ParsedItinerary parsedItinerary; + + // DTO ํด๋ž˜์Šค ์ •์˜ + @Getter + @Setter + public static class ParsedItinerary { + + private int totalDistance; + private int totalTime; + private List legs; + } + + @Getter + @Setter + public static class ParsedLeg { + + private String mode; + private int distance; + private int sectionTime; + private String route; // ์ง€ํ•˜์ฒ ์ด๋ฉด ํ˜ธ์„  ์ •๋ณด, ๋ฒ„์Šค๋ฉด ๋…ธ์„  ์ •๋ณด + private String routeColor; // ๋…ธ์„  ์ƒ‰ + + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapPublicTransportationParsedResponse.java b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapPublicTransportationParsedResponse.java new file mode 100644 index 00000000..bbf2ab9a --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/public_transportation/adapter/in/dto/response/TmapPublicTransportationParsedResponse.java @@ -0,0 +1,12 @@ +package com.kok.kokapi.public_transportation.adapter.in.dto.response; + +public record TmapPublicTransportationParsedResponse( + Integer totalTime, + Integer transferCount +) { + + public static TmapPublicTransportationParsedResponse of(Integer totalTime, + Integer transferCount) { + return new TmapPublicTransportationParsedResponse(totalTime, transferCount); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java new file mode 100644 index 00000000..2d5aa2c2 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/CreateRoomResponse.java @@ -0,0 +1,24 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Room; + +public record CreateRoomResponse( + String id, + String roomName, + int capacity, + MemberResponse member, + int participantCount, + int nonParticipantCount +) { + + public static CreateRoomResponse of(Room room, int participantCount, int nonParticipantCount) { + return new CreateRoomResponse( + room.getId(), + room.getRoomName(), + room.getCapacity(), + MemberResponse.from(room.getMember()), + participantCount, + nonParticipantCount + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java new file mode 100644 index 00000000..bb3b5089 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java @@ -0,0 +1,25 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import java.util.List; + +public record RoomMembersResponses( + boolean isFull, + List members +) { + + public static RoomMembersResponses of(Room room, List members) { + return new RoomMembersResponses(room.isFull(members.size()), getMemberResponses(members)); + } + + private static List getMemberResponses(List members) { + return members.stream() + .map(member -> new RoomMembersResponse( + member.getMemberId(), + member.getProfile(), + member.getNickname(), + member.getRole() + )).toList(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java new file mode 100644 index 00000000..6659d970 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java @@ -0,0 +1,5 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +public record RoomStatusResponse(boolean isVoteMode) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java new file mode 100644 index 00000000..00b8010c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java @@ -0,0 +1,28 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class RoomParticipantQueryRedisAdapter implements LoadRoomParticipantPort { + + public static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; + + private final RedisTemplate redisTemplate; + + @Override + public Long countParticipantsById(String roomId) { + String key = buildKey(roomId); + if (!redisTemplate.hasKey(key)) { + return 0L; + } + return redisTemplate.opsForList().size(key); + } + + private String buildKey(String roomId) { + return PARTICIPANT_KEY_PREFIX + roomId; + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java new file mode 100644 index 00000000..ff40c61e --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java @@ -0,0 +1,34 @@ +package com.kok.kokapi.room.application.service; + +import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; +import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.usecase.ReadLocationUseCase; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.usecase.GetRoomUseCase; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RoomFacadeService { + + private final GetRoomUseCase getRoomUseCase; + private final ReadLocationUseCase readLocationUseCase; + + public RoomDetailResponse findByRoomId(String roomId) { + Room room = getRoomUseCase.findRoomById(roomId); + int participantsCount = getRoomUseCase.getParticipantsCount(roomId); + return RoomDetailResponse.of(room, participantsCount); + } + + public RoomStatusResponse getRoomStatus(String roomId, LocalDateTime current) { + Room room = getRoomUseCase.findRoomById(roomId); + List locations = readLocationUseCase.readLocations(roomId); + int locationInputCount = locations.size(); + boolean isVoteMode = room.hasLocationInputEnded(locationInputCount, current); + return new RoomStatusResponse(isVoteMode); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/station/config/StationsConfig.java b/kok-api/src/main/java/com/kok/kokapi/station/config/StationsConfig.java new file mode 100644 index 00000000..68ffde0c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/station/config/StationsConfig.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.station.config; + +import com.kok.kokapi.station.application.service.StationService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class StationsConfig { + + @Bean + CommandLineRunner initStations(StationService stationService) { + return args -> stationService.saveStations(); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java new file mode 100644 index 00000000..a3aa046d --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/template/IntegrationTest.java @@ -0,0 +1,28 @@ +package com.kok.kokapi.common.template; + +import com.kok.kokapi.common.util.DatabaseCleanerExtension; +import com.kok.kokapi.config.StationTestConfiguration; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.TestPropertySource; + +@ExtendWith(DatabaseCleanerExtension.class) +@Import(StationTestConfiguration.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) +@Profile("test") +public abstract class IntegrationTest extends ContainerBaseTest { + + @LocalServerPort + private int port; + + @BeforeEach + void setPort() { + RestAssured.port = port; + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java new file mode 100644 index 00000000..0ea61763 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleaner.java @@ -0,0 +1,6 @@ +package com.kok.kokapi.common.util; + +public interface DatabaseCleaner { + + void cleanUp(); +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java new file mode 100644 index 00000000..19b006e7 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/DatabaseCleanerExtension.java @@ -0,0 +1,16 @@ +package com.kok.kokapi.common.util; + +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +public class DatabaseCleanerExtension implements BeforeEachCallback { + + @Override + public void beforeEach(ExtensionContext context) { + SpringExtension.getApplicationContext(context) + .getBeansOfType(DatabaseCleaner.class) + .values() + .forEach(DatabaseCleaner::cleanUp); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java new file mode 100644 index 00000000..c7a8a9de --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/RedisDatabaseCleaner.java @@ -0,0 +1,29 @@ +package com.kok.kokapi.common.util; + +import java.util.Objects; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +public class RedisDatabaseCleaner implements DatabaseCleaner { + + private final RedisTemplate redisTemplate; + + public RedisDatabaseCleaner(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public void cleanUp() { + RedisConnectionFactory connectionFactory = Objects.requireNonNull( + redisTemplate.getConnectionFactory(), + "RedisConnectionFactory must not be null" + ); + + try (RedisConnection connection = connectionFactory.getConnection()) { + connection.serverCommands().flushDb(); + } + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java b/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java new file mode 100644 index 00000000..52ab1321 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/config/StationTestConfiguration.java @@ -0,0 +1,17 @@ +package com.kok.kokapi.config; + +import com.kok.kokapi.station.adapter.out.external.FakeStationClient; +import com.kok.kokcore.station.port.out.LoadStationsPort; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +@TestConfiguration +public class StationTestConfiguration { + + @Bean + @Primary + public LoadStationsPort loadStationsPort() { + return new FakeStationClient(); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java new file mode 100644 index 00000000..18872ba1 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/MemberFixture.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.fixture; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.vo.MemberRole; + +public class MemberFixture { + + public static Member createFollower() { + return new Member("follower", "profile.svg", MemberRole.FOLLOWER); + } + + public static Member createLeader() { + return new Member("leader", "profile.svg", MemberRole.LEADER); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java new file mode 100644 index 00000000..5bc11b39 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/PointFixture.java @@ -0,0 +1,15 @@ +package com.kok.kokapi.fixture; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; + +public class PointFixture { + + public static Point create() { + GeometryFactory geometryFactory = new GeometryFactory(); + Coordinate coordinate = new Coordinate(127.02758, 37.49794); + + return geometryFactory.createPoint(coordinate); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java b/kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java new file mode 100644 index 00000000..5fb2a383 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/fixture/RoomFixture.java @@ -0,0 +1,11 @@ +package com.kok.kokapi.fixture; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; + +public class RoomFixture { + + public static Room create(int capacity, Member member) { + return Room.create("room", capacity, member); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java new file mode 100644 index 00000000..b1fbc4ed --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -0,0 +1,154 @@ +package com.kok.kokapi.room.adapter.in.web; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import com.kok.kokapi.centroid.adapter.in.dto.request.LocationRequest; +import com.kok.kokapi.common.template.IntegrationTest; +import com.kok.kokapi.room.adapter.in.dto.request.CreateRoomRequest; +import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; +import com.kok.kokapi.room.adapter.in.dto.response.CreateRoomResponse; +import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +class RoomIntegrationTest extends IntegrationTest { + + @DisplayName("์•ฝ์†๋ฐฉ ์‹œ๋‚˜๋ฆฌ์˜ค") + @TestFactory + Stream getRoomDetail() { + AtomicReference createRoomResponse = new AtomicReference<>(); + AtomicReference joinRoomResponse = new AtomicReference<>(); + + return Stream.of( + DynamicTest.dynamicTest("์•ฝ์†๋ฐฉ์„ ์ƒ์„ฑํ•œ๋‹ค.", + () -> createRoomResponse.set(createRoom( + new CreateRoomRequest("room", 2, "hostProfile.svg", "hostNickname")))), + + inputLocation("๋ฐฉ์žฅ์ด ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse), + + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๋‹ค", createRoomResponse, 1), + + DynamicTest.dynamicTest("ํŒ”๋กœ์›Œ๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•œ๋‹ค.", + () -> joinRoomResponse.set(joinRoom(createRoomResponse.get().id(), + new JoinRoomParticipantRequest("profile", "follower")))), + + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๋‹ค.", createRoomResponse, 0), + + getRoomMembers("์•ฝ์†๋ฐฉ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉด isFulใ…ฃ์€ true์ด๊ณ , 2๋ช…์˜ ํ”„๋กœํ•„์ด ์žˆ๋‹ค", createRoomResponse, 2, + true), + + checkVoteMode("์•„์ง ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜๊ธฐ์— voteMode๋Š” false์ด๋‹ค.", createRoomResponse, false), + + inputLocation("ํŒ”๋กœ์›Œ๊ฐ€ ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse, joinRoomResponse), + + checkVoteMode("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ชจ๋“  ์ฐธ์—ฌ์ž๊ฐ€ ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ–ˆ๊ธฐ์— voteMode๋Š” true์ด๋‹ค.", + createRoomResponse, true) + ); + } + + private CreateRoomResponse createRoom(CreateRoomRequest request) { + return RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/v1/api/rooms") + .then().log().all() + .assertThat().statusCode(201) + .extract().body().jsonPath().getObject("data", CreateRoomResponse.class); + } + + private JoinRoomResponse joinRoom(String roomId, JoinRoomParticipantRequest request) { + return RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/v1/api/rooms/" + roomId + "/join") + .then().log().all() + .assertThat().statusCode(200) + .extract().body().jsonPath().getObject("data", JoinRoomResponse.class); + } + + private static DynamicTest inputLocation(String message, + AtomicReference createRoomResponse) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + String memberId = createRoomResponse.get().member().id(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(new LocationRequest(roomId, memberId, new BigDecimal("37"), + new BigDecimal("127"))) + .when().post("/v1/api/locations") + .then().log().all() + .assertThat().statusCode(200); + }); + } + + private static DynamicTest getRoomDetail(String message, + AtomicReference createRoomResponse, int nonParticipantCount) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/v1/api/rooms/" + roomId) + .then().log().all() + .assertThat().statusCode(200) + .body("data.nonParticipantCount", is(nonParticipantCount)); + }); + } + + private static DynamicTest getRoomMembers(String message, + AtomicReference createRoomResponse, int profileCount, + boolean expectedIsFull) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/v1/api/rooms/" + roomId + "/participants") + .then().log().all() + .assertThat().statusCode(200) + .body("data.members", hasSize(profileCount)) + .body("data.isFull", is(expectedIsFull)); + }); + } + + private static DynamicTest inputLocation(String message, + AtomicReference createRoomResponse, + AtomicReference joinRoomResponse) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + String memberId = joinRoomResponse.get().id(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(new LocationRequest(roomId, memberId, new BigDecimal("37"), + new BigDecimal("127"))) + .when().post("/v1/api/locations") + .then().log().all() + .assertThat().statusCode(200); + }); + } + + private static DynamicTest checkVoteMode(String message, + AtomicReference createRoomResponse, boolean expectedIsVoteMode) { + return DynamicTest.dynamicTest(message, + () -> { + String roomId = createRoomResponse.get().id(); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/v1/api/rooms/" + roomId + "/status") + .then().log().all() + .assertThat().statusCode(200) + .body("data.isVoteMode", is(expectedIsVoteMode)); + }); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java new file mode 100644 index 00000000..b0cdf131 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kok.kokapi.common.template.RepositoryTest; +import com.kok.kokapi.fixture.MemberFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class RoomParticipantQueryRedisAdapterTest extends RepositoryTest { + + @Autowired + private RoomParticipantQueryRedisAdapter roomParticipantQueryRedisAdapter; + @Autowired + private RoomParticipantSaveAdapter roomParticipantSaveAdapter; + + @DisplayName("๋ฐฉ์˜ ์ฐธ์—ฌ ์ธ์›์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void getParticipantCount() { + // given + String roomId = "roomId"; + roomParticipantSaveAdapter.joinRoom(roomId, MemberFixture.createLeader()); + + // when + Long participantCount = roomParticipantQueryRedisAdapter.countParticipantsById(roomId); + + // then + assertThat(participantCount).isEqualTo(1); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadCentroidPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadCentroidPort.java new file mode 100644 index 00000000..0b659b96 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadCentroidPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.location.port.out; + +import org.locationtech.jts.geom.Point; + +public interface ReadCentroidPort { + + Point findCentroidByRoomId(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java new file mode 100644 index 00000000..677abbf6 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java @@ -0,0 +1,16 @@ +package com.kok.kokcore.location.port.out; + +import com.kok.kokcore.location.domain.Location; +import java.util.List; +import java.util.Optional; + +public interface ReadLocationPort { + + Optional findLocationByRoomIdAndMemberId(String roomId, String memberId); + + List findLocationsByRoomId(String roomId); + + List findInsideConvexHull(String roomId); + + List findConvexHull(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java new file mode 100644 index 00000000..1bf1e537 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.location.port.out; + +import com.kok.kokcore.location.domain.Location; +import org.locationtech.jts.geom.Point; + +public interface SaveLocationPort { + + Location saveLocation(String roomId, String memberId, Point point); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/public_transportation/usecase/RetrievePublicTransportationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/public_transportation/usecase/RetrievePublicTransportationUseCase.java new file mode 100644 index 00000000..0d292eb1 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/public_transportation/usecase/RetrievePublicTransportationUseCase.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.public_transportation.usecase; + +public interface RetrievePublicTransportationUseCase { + + String retrievePublicTransportation(Long stationId, String roomId, String memberId); + + String retrieveComplexPublicTransportation(Long stationId, String roomId, String memberId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java new file mode 100644 index 00000000..543a0007 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.room.port.out; + +public interface LoadRoomParticipantPort { + + Long countParticipantsById(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java new file mode 100644 index 00000000..f9a5473a --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.room.port.out; + +import com.kok.kokcore.room.domain.Room; +import java.util.Optional; + +public interface LoadRoomPort { + + Optional findRoomById(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomParticipantsPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomParticipantsPort.java new file mode 100644 index 00000000..8f4de49a --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomParticipantsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.room.port.out; + +import com.kok.kokcore.room.domain.Member; + +public interface SaveRoomParticipantsPort { + + int joinRoom(String roomId, Member member); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomPort.java new file mode 100644 index 00000000..1cbeeb64 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/SaveRoomPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.room.port.out; + +import com.kok.kokcore.room.domain.Room; + +public interface SaveRoomPort { + + Room save(Room room); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java new file mode 100644 index 00000000..4b6bbcfb --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadStationsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; + +public interface LoadStationsPort { + + StationRouteDtos loadAllStations(); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java new file mode 100644 index 00000000..1b4650c4 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface ReadCustomStationsPort { + List findRecommendedStationsByRoomId(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadStationsPort.java new file mode 100644 index 00000000..e173fd00 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadStationsPort.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.station.port.out; + +public interface ReadStationsPort { + + boolean hasNoStations(); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveRoutePort.java new file mode 100644 index 00000000..f2e82a77 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveRoutePort.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface RetrieveRoutePort { + + List retrieveRoutes(Station station); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java new file mode 100644 index 00000000..23af994f --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java @@ -0,0 +1,14 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; +import java.util.Optional; +import org.locationtech.jts.geom.Point; + +public interface RetrieveStationsPort { + + Optional retrieveStation(Long stationId); + + List retrieveInRangeStations(Point centroid, double dist); + List retrieveStationsByKeyword(String keyword); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java new file mode 100644 index 00000000..54a3884e --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Station; + +public interface SaveCustomStationsPort { + + Station addCustomStations(String roomId, Station station); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveRoutePort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveRoutePort.java new file mode 100644 index 00000000..a7024765 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveRoutePort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Route; +import java.util.List; + +public interface SaveRoutePort { + + void saveRoutes(List routes); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveStationsPort.java new file mode 100644 index 00000000..c773107d --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveStationsPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface SaveStationsPort { + + List saveStations(List stations); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java new file mode 100644 index 00000000..6b898913 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java @@ -0,0 +1,25 @@ +package com.kok.kokcore.station.port.out.dto; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; + +public record StationRouteDto( + String name, + String latitude, + String longitude, + Long stationId, + String route +) { + + public boolean hasName(Station station) { + return name.equals(station.getName()); + } + + public Station toStation() { + return new Station(name, latitude, longitude); + } + + public Route toRouteByStation(Station station) { + return new Route(stationId, route, station); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java new file mode 100644 index 00000000..a2ff88f8 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java @@ -0,0 +1,44 @@ +package com.kok.kokcore.station.port.out.dto; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public record StationRouteDtos( + List stationRouteDtos +) { + + public boolean isEmpty() { + return stationRouteDtos().isEmpty(); + } + + public List toStations() { + return distinctByName().stream() + .map(StationRouteDto::toStation) + .toList(); + } + + private List distinctByName() { + return new ArrayList<>(stationRouteDtos.stream() + .collect(Collectors.toMap( + StationRouteDto::name, + dto -> dto, + (existing, replacement) -> existing + )) + .values()); + } + + public List toRoutesByStations(List stations) { + List routes = new ArrayList<>(); + for (Station station : stations) { + List routesOfStation = stationRouteDtos.stream() + .filter(stationRouteDto -> stationRouteDto.hasName(station)) + .map(stationRouteDto -> stationRouteDto.toRouteByStation(station)) + .toList(); + routes.addAll(routesOfStation); + } + return routes; + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/CustomStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/CustomStationUseCase.java new file mode 100644 index 00000000..e40e5eb9 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/CustomStationUseCase.java @@ -0,0 +1,14 @@ +package com.kok.kokcore.station.usecase; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface CustomStationUseCase { + + Station addCustomStations(String roomId, Long stationId); + + List getCustomRecommendedStations(String roomId); + + List searchStations(String keyword); + +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java new file mode 100644 index 00000000..be82b35c --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java @@ -0,0 +1,11 @@ +package com.kok.kokcore.station.usecase; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + + +public interface RecommendStationUseCase { + + List recommendStations(String roomId); + +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/RetrieveRouteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RetrieveRouteUseCase.java new file mode 100644 index 00000000..d8bc8b4a --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/RetrieveRouteUseCase.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.station.usecase; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface RetrieveRouteUseCase { + + List retrieveRoutes(Station station); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/SaveStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/SaveStationUseCase.java new file mode 100644 index 00000000..5aa616bd --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/SaveStationUseCase.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.station.usecase; + +public interface SaveStationUseCase { + + void saveStations(); +} diff --git a/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java b/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java new file mode 100644 index 00000000..9e499f07 --- /dev/null +++ b/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java @@ -0,0 +1,97 @@ +package com.kok.kokcore.station.port.out.dto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class StationRouteDtosTest { + + @DisplayName("stationRouteDtos๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ์ฐธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isEmpty() { + // given + StationRouteDtos stationRouteDtos = new StationRouteDtos(List.of()); + + // when + boolean result = stationRouteDtos.isEmpty(); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("stationRouteDtos๊ฐ€ ๋น„์–ด ์žˆ์ง€ ์•Š์œผ๋ฉด ๊ฑฐ์ง“์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isNotEmpty() { + // given + StationRouteDto stationRouteDto = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, + "1ํ˜ธ์„ "); + StationRouteDtos stationRouteDtos = new StationRouteDtos(List.of(stationRouteDto)); + + // when + boolean result = stationRouteDtos.isEmpty(); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("stationRouteDtos๋ฅผ Station ๋ฆฌ์ŠคํŠธ๋กœ ์ค‘๋ณต ์—†์ด ๋ณ€ํ™˜ํ•œ๋‹ค.") + @Test + void toStations() { + // given + StationRouteDto stationRouteDto1 = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, + "1ํ˜ธ์„ "); + StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, + "2ํ˜ธ์„ "); + StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.496", "127.029", 3L, + "์‹ ๋ถ„๋‹น์„ "); + StationRouteDtos stationRouteDtos = new StationRouteDtos( + List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) + ); + + // when + List stations = stationRouteDtos.toStations(); + + // then + List names = stations.stream().map(Station::getName).toList(); + + assertAll( + () -> assertThat(stations).hasSize(2), + () -> assertThat(names).containsExactlyInAnyOrder("์„œ์šธ์—ญ", "๊ฐ•๋‚จ์—ญ") + ); + } + + @DisplayName("stationRouteDtos๋ฅผ Route ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.") + @Test + void toRoutesByStations() { + // given + StationRouteDto stationRouteDto1 = new StationRouteDto("์„œ์šธ์—ญ", "37.556", "126.972", 1L, + "1ํ˜ธ์„ "); + StationRouteDto stationRouteDto2 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 2L, + "2ํ˜ธ์„ "); + StationRouteDto stationRouteDto3 = new StationRouteDto("๊ฐ•๋‚จ์—ญ", "37.497", "127.028", 3L, + "์‹ ๋ถ„๋‹น์„ "); + StationRouteDtos stationRouteDtos = new StationRouteDtos( + List.of(stationRouteDto1, stationRouteDto2, stationRouteDto3) + ); + Station station1 = new Station("์„œ์šธ์—ญ", "37.556", "126.972"); + Station station2 = new Station("๊ฐ•๋‚จ์—ญ", "37.497", "127.028"); + + // when + List routes = stationRouteDtos.toRoutesByStations(List.of(station1, station2)); + + // then + List codes = routes.stream().map(Route::getCode).toList(); + List stations = routes.stream().map(Route::getStation).distinct().toList(); + + assertAll( + () -> assertThat(routes).hasSize(3), + () -> assertThat(codes).containsExactlyInAnyOrder(1L, 2L, 3L), + () -> assertThat(stations).containsExactlyInAnyOrder(station1, station2) + ); + } +} From 46a80c83f379675f1229adca44d225a71e4e7a49 Mon Sep 17 00:00:00 2001 From: minseokey Date: Fri, 28 Mar 2025 10:06:32 +0900 Subject: [PATCH 106/163] :recycle: fix: resolve conflicts --- .../adapter/in/web/StationController.java | 43 +++++++++++-------- .../CustomStationCommandRedisAdapter.java | 5 ++- .../CustomStationQueryRedisAdapter.java | 5 ++- .../out/persistence/StationRepository.java | 6 +-- .../service/StationFacadeService.java | 3 +- .../application/service/StationService.java | 10 +++-- .../port/out/ReadCustomStationsPort.java | 1 + .../port/out/RetrieveStationsPort.java | 1 + 8 files changed, 45 insertions(+), 29 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index 1ddffbbe..609bb277 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -4,17 +4,16 @@ import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; import com.kok.kokapi.station.application.service.StationFacadeService; +import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.usecase.CustomStationUseCase; import com.kok.kokcore.station.usecase.RecommendStationUseCase; import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; -import com.kok.kokcore.station.domain.entity.Station; import io.swagger.v3.oas.annotations.Operation; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; - -import java.util.List; import org.springframework.web.bind.annotation.PostMapping; @V1Controller @@ -28,39 +27,49 @@ public class StationController { @Operation(summary = "์ถ”์ฒœ ์ง€ํ•˜์ฒ ์—ญ ๋ฆฌํ„ด", description = "Recommend subway stations based on the user's location.") @GetMapping("/stations/recommend/{roomId}") - public ResponseEntity>> recommendStations(@PathVariable String roomId) { + public ResponseEntity>> recommendStations( + @PathVariable String roomId) { - List recommendedStations = recommendStationUseCase.recommendStations(roomId).stream() - .map(station -> - RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) - .toList(); + List recommendedStations = recommendStationUseCase.recommendStations( + roomId).stream() + .map(station -> + RecommendedStationResponse.of(station, + retrieveRouteUseCase.retrieveRoutes(station))) + .toList(); return ResponseEntity.ok(ApiResponseDto.success(recommendedStations)); } @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ๊ฒ€์ƒ‰", description = "Search for subway stations based on the keyword.") @GetMapping("/stations/search/{keyword}") - public ResponseEntity>> searchStations(@PathVariable String keyword) { + public ResponseEntity>> searchStations( + @PathVariable String keyword) { - List recommendedStations = customStationUseCase.searchStations(keyword).stream() - .map(station -> - RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) - .toList(); + List recommendedStations = customStationUseCase.searchStations( + keyword).stream() + .map(station -> + RecommendedStationResponse.of(station, + retrieveRouteUseCase.retrieveRoutes(station))) + .toList(); return ResponseEntity.ok(ApiResponseDto.success(recommendedStations)); } @Operation(summary = "์ง€ํ•˜์ฒ ์—ญ ์ถ”๊ฐ€", description = "Add a subway station to the user's custom list.") @PostMapping("/stations/custom/{roomId}/{stationId}") - public ResponseEntity> addCustomStations(@PathVariable String roomId, @PathVariable Long stationId) { + public ResponseEntity> addCustomStations( + @PathVariable String roomId, @PathVariable Long stationId) { Station station = customStationUseCase.addCustomStations(roomId, stationId); - return ResponseEntity.ok(ApiResponseDto.success(RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station)))); + return ResponseEntity.ok(ApiResponseDto.success( + RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station)))); } @Operation(summary = "ํˆฌํ‘œํ•  ์ง€ํ•˜์ฒ ์—ญ ๋ฆฌ์ŠคํŠธ ๋ฆฌํ„ด", description = "Return a list of subway stations to vote on.") @GetMapping("/stations/candidate/{roomId}") - public ResponseEntity>> getCustomRecommendedStations(@PathVariable String roomId) { - List candidateStations = stationFacadeService.getCandidateStationResponse(roomId); + public ResponseEntity>> getCustomRecommendedStations( + @PathVariable String roomId) { + List candidateStations = stationFacadeService.getCandidateStationResponse( + roomId); return ResponseEntity.ok(ApiResponseDto.success(candidateStations)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java index 1dd6b2cc..623f3581 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.station.port.out.SaveCustomStationsPort; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.SaveCustomStationsPort; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -27,7 +27,8 @@ public Station addCustomStations(String roomId, Station station) { String stationJson = redisTemplate.opsForValue().get(key); List stationList = (stationJson == null) ? new ArrayList<>() - : objectMapper.readValue(stationJson, new TypeReference<>() {}); + : objectMapper.readValue(stationJson, new TypeReference<>() { + }); stationList.add(station); String updatedJson = objectMapper.writeValueAsString(stationList); diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java index 0ad7bcae..503bc51b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.kok.kokcore.station.port.out.ReadCustomStationsPort; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.ReadCustomStationsPort; import java.util.Collections; import java.util.List; import lombok.RequiredArgsConstructor; @@ -29,7 +29,8 @@ public List findRecommendedStationsByRoomId(String roomId) { } try { - return objectMapper.readValue(stationJson, new TypeReference<>() {}); + return objectMapper.readValue(stationJson, new TypeReference<>() { + }); } catch (JsonProcessingException e) { return Collections.emptyList(); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 27f8dcd3..8f5ecdcd 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -26,8 +26,8 @@ List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, @Param("lat") BigDecimal lat, @Param("distance") Double distance); @Query(value = """ - SELECT * FROM station - WHERE name LIKE CONCAT('%', :keyword, '%') - """, nativeQuery = true) + SELECT * FROM station + WHERE name LIKE CONCAT('%', :keyword, '%') + """, nativeQuery = true) List findStationsByKeyword(@Param("keyword") String keyword); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java index 12252b2d..2ffc13ab 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java @@ -22,7 +22,8 @@ public List getCandidateStationResponse(String roomI return getCandidateStation(roomId).stream() .map(station -> - RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station))) + RecommendedStationResponse.of(station, + retrieveRouteUseCase.retrieveRoutes(station))) .toList(); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index b5eae2ab..f4e3df94 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -41,7 +41,7 @@ public class StationService implements SaveStationUseCase, RecommendStationUseCa @Override @Transactional public void saveStations() { - if(readStationsPort.hasNoStations()) { + if (readStationsPort.hasNoStations()) { StationRouteDtos stationRouteDtos = loadStationsPort.loadAllStations(); List stations = saveStationsPort.saveStations(stationRouteDtos.toStations()); saveRoutePort.saveRoutes(stationRouteDtos.toRoutesByStations(stations)); @@ -49,7 +49,7 @@ public void saveStations() { } @Override - @Cacheable(value = "searchStations",cacheManager = "stationCacheManager", key = "#keyword") + @Cacheable(value = "searchStations", cacheManager = "stationCacheManager", key = "#keyword") public List searchStations(String keyword) { return retrieveStationsPort.retrieveStationsByKeyword(keyword); } @@ -67,7 +67,7 @@ public List getCustomRecommendedStations(String roomId) { } @Override - @Cacheable(value = "recommendStations",cacheManager = "stationCacheManager", key = "#roomId") + @Cacheable(value = "recommendStations", cacheManager = "stationCacheManager", key = "#roomId") public List recommendStations(String roomId) { Point centroid = readCentroidPort.findCentroidByRoomId(roomId); int RECOMMEND_NUM = 2; @@ -103,7 +103,9 @@ private Map calculateProbabilities(List stations, Poin for (Station station : stations) { double distance = calculateDistance(centroid, station); - if (distance == 0) distance = Double.MIN_VALUE; // 0 ๊ฑฐ๋ฆฌ ๋ฐฉ์ง€ + if (distance == 0) { + distance = Double.MIN_VALUE; // 0 ๊ฑฐ๋ฆฌ ๋ฐฉ์ง€ + } double weight = (1 / distance) * station.getPriority(); weightedDistances.put(station, weight); diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java index 1b4650c4..c4afabdf 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java @@ -4,5 +4,6 @@ import java.util.List; public interface ReadCustomStationsPort { + List findRecommendedStationsByRoomId(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java index 23af994f..6ae6b0be 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/RetrieveStationsPort.java @@ -10,5 +10,6 @@ public interface RetrieveStationsPort { Optional retrieveStation(Long stationId); List retrieveInRangeStations(Point centroid, double dist); + List retrieveStationsByKeyword(String keyword); } From 3404bd570a084b2144c61cc85c0cd3826ca2d77e Mon Sep 17 00:00:00 2001 From: minseokey Date: Fri, 28 Mar 2025 19:00:31 +0900 Subject: [PATCH 107/163] :recycle: refactor: add locationName column --- .../centroid/adapter/in/dto/request/LocationRequest.java | 5 ++++- .../adapter/in/dto/response/LocationResponse.java | 8 +++++--- .../centroid/adapter/in/web/LocationController.java | 6 ++++-- .../centroid/adapter/out/mapper/LocationMapper.java | 3 ++- .../out/persistence/LocationPersistenceAdapter.java | 4 ++-- .../application/service/LocationCommandService.java | 7 ++++--- .../com/kok/kokapi/config/redis/RedisCacheConfig.java | 2 +- .../java/com/kok/kokcore/location/domain/Location.java | 9 ++++++++- .../kok/kokcore/location/port/out/SaveLocationPort.java | 2 +- .../kokcore/location/usecase/CreateLocationUseCase.java | 4 ++-- 10 files changed, 33 insertions(+), 17 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index 6e67db3c..4ad14e43 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -22,7 +22,10 @@ public record LocationRequest( @NotNull(message = "longitude(๊ฒฝ๋„)๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") @DecimalMin(value = "124.0", message = "๊ฒฝ๋„๋Š” 124.0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") - BigDecimal longitude + BigDecimal longitude, + + @NotNull(message = "locationName(์œ„์น˜๋ช…)์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String locationName ) { } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index 7ee197a9..cf299b0c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -7,13 +7,15 @@ public record LocationResponse( String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude + BigDecimal longitude, + String locationName ) { public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude) { + BigDecimal longitude, String locationName) { return new LocationResponse(roomId, memberId, latitude.setScale(6, RoundingMode.HALF_UP), - longitude.setScale(6, RoundingMode.HALF_UP)); + longitude.setScale(6, RoundingMode.HALF_UP), + locationName); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index af8e93a5..d2285c62 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -41,7 +41,8 @@ public ResponseEntity> createLocation( locationRequest.roomId(), locationRequest.memberId(), locationRequest.latitude(), - locationRequest.longitude() + locationRequest.longitude(), + locationRequest.locationName() ); Pair centroid = loadCentroidUsecase.readCentroidCoordinates( @@ -104,7 +105,8 @@ public ResponseEntity> updateLocation( locationRequest.roomId(), locationRequest.memberId(), locationRequest.latitude(), - locationRequest.longitude() + locationRequest.longitude(), + locationRequest.locationName() ); LocationResponse response = locationMapper.toResponse(location); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index 2b9a1b98..1b45bcd2 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -24,7 +24,8 @@ public LocationResponse toResponse(Location location) { location.getRoomId(), location.getMemberId(), coordinates.getFirst(), - coordinates.getSecond() + coordinates.getSecond(), + location.getLocationName() ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index cf79d95e..daf726e1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -61,7 +61,7 @@ public Point findCentroidByRoomId(String roomId) { } @Override - public Location saveLocation(String roomId, String memberId, Point point) { - return locationRepository.save(new Location(roomId, memberId, point)); + public Location saveLocation(String roomId, String memberId, Point point, String locationName) { + return locationRepository.save(new Location(roomId, memberId, point, locationName)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 06f72bf0..834828d9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -21,19 +21,20 @@ public class LocationCommandService implements CreateLocationUseCase { @Override public Location createLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude) { + BigDecimal longitude, String locationName) { Point point = pointConverter.fromCoordinates(latitude, longitude); - return saveLocationPort.saveLocation(roomId, memberId, point); + return saveLocationPort.saveLocation(roomId, memberId, point, locationName); } @Override @Transactional public Location updateLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude) { + BigDecimal longitude, String locationName) { Location location = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); location.changePoint(newPoint); + location.changeLocationName(locationName); return location; } } diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java index 88489801..669a05a5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisCacheConfig.java @@ -67,7 +67,7 @@ public CacheManager publicTransportationCacheManager( new StringRedisSerializer())) .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) - .entryTtl(Duration.ofMinutes(30L)); // ์บ์‹œ ์ˆ˜๋ช… 30๋ถ„ + .entryTtl(Duration.ofDays(3)); // ์บ์‹œ ์ˆ˜๋ช… 3์ผ return RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(redisConnectionFactory) diff --git a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index 4641d8ae..d7724e7f 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -36,15 +36,22 @@ public class Location { @Column(nullable = false, columnDefinition = "POINT SRID 4326") private Point location_point; + @Column(nullable = false) + private String locationName; - public Location(String roomId, String memberId, Point location_point) { + public Location(String roomId, String memberId, Point location_point, String locationName) { this.roomId = roomId; this.memberId = memberId; this.location_point = location_point; + this.locationName = locationName; } // ๋”ํ‹ฐ์ฒดํ‚น public void changePoint(Point point) { this.location_point = point; } + + public void changeLocationName(String name) { + this.locationName = name; + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java index 1bf1e537..b1154cf2 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java @@ -5,5 +5,5 @@ public interface SaveLocationPort { - Location saveLocation(String roomId, String memberId, Point point); + Location saveLocation(String roomId, String memberId, Point point, String locationName); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java index 0c8a87d6..2d5022eb 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java @@ -7,8 +7,8 @@ public interface CreateLocationUseCase { Location createLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude); + BigDecimal longitude, String locationName); Location updateLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude); + BigDecimal longitude, String locationName); } From 8c2b2df1f59d707a4789e722c9086fa70ba105c7 Mon Sep 17 00:00:00 2001 From: minseokey Date: Fri, 28 Mar 2025 19:03:15 +0900 Subject: [PATCH 108/163] :recycle: refactor: change native query to query method --- .../adapter/out/persistence/StationPersistenceAdapter.java | 2 +- .../station/adapter/out/persistence/StationRepository.java | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index a7f2e300..3eb4152b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -82,7 +82,7 @@ public List retrieveInRangeStations(Point centroid, double dist) { @Override public List retrieveStationsByKeyword(String keyword) { - return stationRepository.findStationsByKeyword(keyword); + return stationRepository.findByNameContaining(keyword); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java index 8f5ecdcd..d1340b64 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationRepository.java @@ -25,9 +25,5 @@ WHERE ST_Distance_Sphere(Point(longitude, latitude), Point(:lon, :lat)) < :dista List findInRangeStationsByCentroid(@Param("lon") BigDecimal lon, @Param("lat") BigDecimal lat, @Param("distance") Double distance); - @Query(value = """ - SELECT * FROM station - WHERE name LIKE CONCAT('%', :keyword, '%') - """, nativeQuery = true) - List findStationsByKeyword(@Param("keyword") String keyword); + List findByNameContaining(String keyword); } From 2e205640f3e9d8442c242533bffc5e42c39faaa4 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 29 Mar 2025 11:37:12 +0900 Subject: [PATCH 109/163] :recycle: refactor: change name of recommended stations by subject --- .../adapter/in/web/StationController.java | 14 ++++---- ...rRecommendStationCommandRedisAdapter.java} | 10 +++--- ...serRecommendStationQueryRedisAdapter.java} | 10 +++--- .../service/StationFacadeService.java | 12 +++---- .../application/service/StationService.java | 32 ++++++++----------- ...ava => ReadUserRecommendStationsPort.java} | 4 +-- .../port/out/SaveCustomStationsPort.java | 8 ----- .../out/SaveUserRecommendStationsPort.java | 8 +++++ ...eCase.java => SystemRecommendUseCase.java} | 4 +-- ...UseCase.java => UserRecommendUseCase.java} | 6 ++-- 10 files changed, 52 insertions(+), 56 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/{CustomStationCommandRedisAdapter.java => UserRecommendStationCommandRedisAdapter.java} (77%) rename kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/{CustomStationQueryRedisAdapter.java => UserRecommendStationQueryRedisAdapter.java} (74%) rename kok-core/src/main/java/com/kok/kokcore/station/port/out/{ReadCustomStationsPort.java => ReadUserRecommendStationsPort.java} (51%) delete mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveUserRecommendStationsPort.java rename kok-core/src/main/java/com/kok/kokcore/station/usecase/{RecommendStationUseCase.java => SystemRecommendUseCase.java} (55%) rename kok-core/src/main/java/com/kok/kokcore/station/usecase/{CustomStationUseCase.java => UserRecommendUseCase.java} (51%) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index 609bb277..17278e38 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -5,9 +5,9 @@ import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; import com.kok.kokapi.station.application.service.StationFacadeService; import com.kok.kokcore.station.domain.entity.Station; -import com.kok.kokcore.station.usecase.CustomStationUseCase; -import com.kok.kokcore.station.usecase.RecommendStationUseCase; import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.usecase.SystemRecommendUseCase; +import com.kok.kokcore.station.usecase.UserRecommendUseCase; import io.swagger.v3.oas.annotations.Operation; import java.util.List; import lombok.RequiredArgsConstructor; @@ -20,8 +20,8 @@ @RequiredArgsConstructor public class StationController { - private final RecommendStationUseCase recommendStationUseCase; - private final CustomStationUseCase customStationUseCase; + private final SystemRecommendUseCase systemRecommendUseCase; + private final UserRecommendUseCase userRecommendUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; private final StationFacadeService stationFacadeService; @@ -30,7 +30,7 @@ public class StationController { public ResponseEntity>> recommendStations( @PathVariable String roomId) { - List recommendedStations = recommendStationUseCase.recommendStations( + List recommendedStations = systemRecommendUseCase.systemRecommendStation( roomId).stream() .map(station -> RecommendedStationResponse.of(station, @@ -45,7 +45,7 @@ public ResponseEntity>> recommen public ResponseEntity>> searchStations( @PathVariable String keyword) { - List recommendedStations = customStationUseCase.searchStations( + List recommendedStations = userRecommendUseCase.searchStations( keyword).stream() .map(station -> RecommendedStationResponse.of(station, @@ -59,7 +59,7 @@ public ResponseEntity>> searchSt @PostMapping("/stations/custom/{roomId}/{stationId}") public ResponseEntity> addCustomStations( @PathVariable String roomId, @PathVariable Long stationId) { - Station station = customStationUseCase.addCustomStations(roomId, stationId); + Station station = userRecommendUseCase.addUserRecommendStation(roomId, stationId); return ResponseEntity.ok(ApiResponseDto.success( RecommendedStationResponse.of(station, retrieveRouteUseCase.retrieveRoutes(station)))); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java similarity index 77% rename from kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java rename to kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java index 623f3581..586ae423 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokcore.station.domain.entity.Station; -import com.kok.kokcore.station.port.out.SaveCustomStationsPort; +import com.kok.kokcore.station.port.out.SaveUserRecommendStationsPort; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -13,14 +13,14 @@ @RequiredArgsConstructor @Repository -public class CustomStationCommandRedisAdapter implements SaveCustomStationsPort { +public class UserRecommendStationCommandRedisAdapter implements SaveUserRecommendStationsPort { - private final String CUSTOM_STATION_PREFIX = "customStations:"; + private final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStation:"; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; @Override - public Station addCustomStations(String roomId, Station station) { + public Station addUserRecommendStation(String roomId, Station station) { String key = buildKey(roomId); try { @@ -41,6 +41,6 @@ public Station addCustomStations(String roomId, Station station) { } private String buildKey(String roomId) { - return CUSTOM_STATION_PREFIX + roomId; + return USER_RECOMMEND_STATION_PREFIX + roomId; } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationQueryRedisAdapter.java similarity index 74% rename from kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java rename to kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationQueryRedisAdapter.java index 503bc51b..53d97716 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/CustomStationQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationQueryRedisAdapter.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokcore.station.domain.entity.Station; -import com.kok.kokcore.station.port.out.ReadCustomStationsPort; +import com.kok.kokcore.station.port.out.ReadUserRecommendStationsPort; import java.util.Collections; import java.util.List; import lombok.RequiredArgsConstructor; @@ -13,14 +13,14 @@ @Repository @RequiredArgsConstructor -public class CustomStationQueryRedisAdapter implements ReadCustomStationsPort { +public class UserRecommendStationQueryRedisAdapter implements ReadUserRecommendStationsPort { - private final String CUSTOM_STATION_PREFIX = "customStations:"; + private final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStations:"; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; @Override - public List findRecommendedStationsByRoomId(String roomId) { + public List findUserRecommendedStationsByRoomId(String roomId) { String key = buildKey(roomId); String stationJson = redisTemplate.opsForValue().get(key); @@ -37,6 +37,6 @@ public List findRecommendedStationsByRoomId(String roomId) { } private String buildKey(String roomId) { - return CUSTOM_STATION_PREFIX + roomId; + return USER_RECOMMEND_STATION_PREFIX + roomId; } } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java index 2ffc13ab..a3360e52 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java @@ -1,10 +1,10 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; -import com.kok.kokcore.station.usecase.CustomStationUseCase; -import com.kok.kokcore.station.usecase.RecommendStationUseCase; import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.usecase.SystemRecommendUseCase; +import com.kok.kokcore.station.usecase.UserRecommendUseCase; import java.util.List; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; @@ -14,8 +14,8 @@ @Service public class StationFacadeService { - private final RecommendStationUseCase recommendStationUseCase; - private final CustomStationUseCase customStationUseCase; + private final SystemRecommendUseCase systemRecommendedUseCase; + private final UserRecommendUseCase userRecommendUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; public List getCandidateStationResponse(String roomId) { @@ -28,8 +28,8 @@ public List getCandidateStationResponse(String roomI } public List getCandidateStation(String roomId) { - List recommendedStations = recommendStationUseCase.recommendStations(roomId); - List customStations = customStationUseCase.getCustomRecommendedStations(roomId); + List recommendedStations = systemRecommendedUseCase.systemRecommendStation(roomId); + List customStations = userRecommendUseCase.getUserRecommendStation(roomId); return Stream.concat( recommendedStations.stream(), diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index 3b67a86e..daae47b3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -1,13 +1,10 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokapi.station.adapter.out.persistence.CustomStationCommandRedisAdapter; -import com.kok.kokapi.station.adapter.out.persistence.CustomStationQueryRedisAdapter; +import com.kok.kokapi.station.adapter.out.persistence.UserRecommendStationCommandRedisAdapter; +import com.kok.kokapi.station.adapter.out.persistence.UserRecommendStationQueryRedisAdapter; import com.kok.kokcore.location.port.out.ReadCentroidPort; -import com.kok.kokcore.station.port.out.*; import com.kok.kokcore.station.port.out.dto.StationRouteDtos; -import com.kok.kokcore.station.usecase.CustomStationUseCase; -import com.kok.kokcore.station.usecase.RecommendStationUseCase; import com.kok.kokcore.station.usecase.SaveStationUseCase; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.port.out.LoadStationsPort; @@ -15,9 +12,8 @@ import com.kok.kokcore.station.port.out.RetrieveStationsPort; import com.kok.kokcore.station.port.out.SaveRoutePort; import com.kok.kokcore.station.port.out.SaveStationsPort; -import com.kok.kokcore.station.port.out.dto.StationRouteDtos; -import com.kok.kokcore.station.usecase.RecommendStationUseCase; -import com.kok.kokcore.station.usecase.SaveStationUseCase; +import com.kok.kokcore.station.usecase.SystemRecommendUseCase; +import com.kok.kokcore.station.usecase.UserRecommendUseCase; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; @@ -35,8 +31,8 @@ @Service @RequiredArgsConstructor @Slf4j -public class StationService implements SaveStationUseCase, RecommendStationUseCase, - CustomStationUseCase { +public class StationService implements SaveStationUseCase, SystemRecommendUseCase, + UserRecommendUseCase { private final LoadStationsPort loadStationsPort; private final SaveStationsPort saveStationsPort; @@ -44,8 +40,8 @@ public class StationService implements SaveStationUseCase, RecommendStationUseCa private final ReadCentroidPort readCentroidPort; private final SaveRoutePort saveRoutePort; private final RetrieveStationsPort retrieveStationsPort; - private final CustomStationQueryRedisAdapter customStationQueryRedisAdapter; - private final CustomStationCommandRedisAdapter customStationCommandRedisAdapter; + private final UserRecommendStationQueryRedisAdapter userRecommendStationQueryRedisAdapter; + private final UserRecommendStationCommandRedisAdapter userRecommendStationCommandRedisAdapter; private final PointConverter pointConverter; @Override @@ -65,20 +61,20 @@ public List searchStations(String keyword) { } @Override - public Station addCustomStations(String roomId, Long stationId) { + public Station addUserRecommendStation(String roomId, Long stationId) { Station station = retrieveStationsPort.retrieveStation(stationId) .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); - return customStationCommandRedisAdapter.addCustomStations(roomId, station); + return userRecommendStationCommandRedisAdapter.addUserRecommendStation(roomId, station); } @Override - public List getCustomRecommendedStations(String roomId) { - return customStationQueryRedisAdapter.findRecommendedStationsByRoomId(roomId); + public List getUserRecommendStation(String roomId) { + return userRecommendStationQueryRedisAdapter.findUserRecommendedStationsByRoomId(roomId); } @Override - @Cacheable(value = "recommendStations", cacheManager = "stationCacheManager", key = "#roomId") - public List recommendStations(String roomId) { + @Cacheable(value = "systemRecommendStations", cacheManager = "stationCacheManager", key = "#roomId") + public List systemRecommendStation(String roomId) { Point centroid = readCentroidPort.findCentroidByRoomId(roomId); int RECOMMEND_NUM = 2; double dist = 100; diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadUserRecommendStationsPort.java similarity index 51% rename from kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java rename to kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadUserRecommendStationsPort.java index c4afabdf..b97c4478 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadCustomStationsPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/ReadUserRecommendStationsPort.java @@ -3,7 +3,7 @@ import com.kok.kokcore.station.domain.entity.Station; import java.util.List; -public interface ReadCustomStationsPort { +public interface ReadUserRecommendStationsPort { - List findRecommendedStationsByRoomId(String roomId); + List findUserRecommendedStationsByRoomId(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java deleted file mode 100644 index 54a3884e..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveCustomStationsPort.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kok.kokcore.station.port.out; - -import com.kok.kokcore.station.domain.entity.Station; - -public interface SaveCustomStationsPort { - - Station addCustomStations(String roomId, Station station); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveUserRecommendStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveUserRecommendStationsPort.java new file mode 100644 index 00000000..4ce7cd7f --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/SaveUserRecommendStationsPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Station; + +public interface SaveUserRecommendStationsPort { + + Station addUserRecommendStation(String roomId, Station station); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/SystemRecommendUseCase.java similarity index 55% rename from kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java rename to kok-core/src/main/java/com/kok/kokcore/station/usecase/SystemRecommendUseCase.java index d3885dbb..88795083 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/usecase/RecommendStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/SystemRecommendUseCase.java @@ -4,7 +4,7 @@ import java.util.List; -public interface RecommendStationUseCase { +public interface SystemRecommendUseCase { - List recommendStations(String roomId); + List systemRecommendStation(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/CustomStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/UserRecommendUseCase.java similarity index 51% rename from kok-core/src/main/java/com/kok/kokcore/station/usecase/CustomStationUseCase.java rename to kok-core/src/main/java/com/kok/kokcore/station/usecase/UserRecommendUseCase.java index e40e5eb9..683f8491 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/usecase/CustomStationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/UserRecommendUseCase.java @@ -3,11 +3,11 @@ import com.kok.kokcore.station.domain.entity.Station; import java.util.List; -public interface CustomStationUseCase { +public interface UserRecommendUseCase { - Station addCustomStations(String roomId, Long stationId); + Station addUserRecommendStation(String roomId, Long stationId); - List getCustomRecommendedStations(String roomId); + List getUserRecommendStation(String roomId); List searchStations(String keyword); From a79ed932cb14fdb2953798337c6c5e5e4bc58478 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 29 Mar 2025 11:57:52 +0900 Subject: [PATCH 110/163] :recycle: refactor: change name locationName to location --- .../adapter/in/dto/request/LocationRequest.java | 4 ++-- .../adapter/in/dto/response/LocationResponse.java | 6 +++--- .../centroid/adapter/in/web/LocationController.java | 4 ++-- .../centroid/adapter/out/mapper/LocationMapper.java | 2 +- .../out/persistence/LocationPersistenceAdapter.java | 4 ++-- .../application/service/LocationCommandService.java | 8 ++++---- .../java/com/kok/kokcore/location/domain/Location.java | 10 +++++----- .../kokcore/location/port/out/SaveLocationPort.java | 2 +- .../location/usecase/CreateLocationUseCase.java | 4 ++-- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java index 4ad14e43..a5d7d1ab 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/request/LocationRequest.java @@ -24,8 +24,8 @@ public record LocationRequest( @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132.0 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") BigDecimal longitude, - @NotNull(message = "locationName(์œ„์น˜๋ช…)์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") - String locationName + @NotNull(message = "name(์œ„์น˜๋ช…)์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + String name ) { } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index cf299b0c..aaecd455 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -8,14 +8,14 @@ public record LocationResponse( String memberId, BigDecimal latitude, BigDecimal longitude, - String locationName + String name ) { public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude, String locationName) { + BigDecimal longitude, String name) { return new LocationResponse(roomId, memberId, latitude.setScale(6, RoundingMode.HALF_UP), longitude.setScale(6, RoundingMode.HALF_UP), - locationName); + name); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index d2285c62..fad852d6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -42,7 +42,7 @@ public ResponseEntity> createLocation( locationRequest.memberId(), locationRequest.latitude(), locationRequest.longitude(), - locationRequest.locationName() + locationRequest.name() ); Pair centroid = loadCentroidUsecase.readCentroidCoordinates( @@ -106,7 +106,7 @@ public ResponseEntity> updateLocation( locationRequest.memberId(), locationRequest.latitude(), locationRequest.longitude(), - locationRequest.locationName() + locationRequest.name() ); LocationResponse response = locationMapper.toResponse(location); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index 1b45bcd2..f222e0b8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -25,7 +25,7 @@ public LocationResponse toResponse(Location location) { location.getMemberId(), coordinates.getFirst(), coordinates.getSecond(), - location.getLocationName() + location.getName() ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index daf726e1..c802c299 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -61,7 +61,7 @@ public Point findCentroidByRoomId(String roomId) { } @Override - public Location saveLocation(String roomId, String memberId, Point point, String locationName) { - return locationRepository.save(new Location(roomId, memberId, point, locationName)); + public Location saveLocation(String roomId, String memberId, Point point, String name) { + return locationRepository.save(new Location(roomId, memberId, point, name)); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java index 834828d9..3846a357 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/application/service/LocationCommandService.java @@ -21,20 +21,20 @@ public class LocationCommandService implements CreateLocationUseCase { @Override public Location createLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude, String locationName) { + BigDecimal longitude, String name) { Point point = pointConverter.fromCoordinates(latitude, longitude); - return saveLocationPort.saveLocation(roomId, memberId, point, locationName); + return saveLocationPort.saveLocation(roomId, memberId, point, name); } @Override @Transactional public Location updateLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude, String locationName) { + BigDecimal longitude, String name) { Location location = readLocationPort.findLocationByRoomIdAndMemberId(roomId, memberId) .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ID์˜ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")); Point newPoint = pointConverter.fromCoordinates(latitude, longitude); location.changePoint(newPoint); - location.changeLocationName(locationName); + location.changeName(name); return location; } } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java index d7724e7f..17c3f337 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/domain/Location.java @@ -37,13 +37,13 @@ public class Location { private Point location_point; @Column(nullable = false) - private String locationName; + private String name; - public Location(String roomId, String memberId, Point location_point, String locationName) { + public Location(String roomId, String memberId, Point location_point, String name) { this.roomId = roomId; this.memberId = memberId; this.location_point = location_point; - this.locationName = locationName; + this.name = name; } // ๋”ํ‹ฐ์ฒดํ‚น @@ -51,7 +51,7 @@ public void changePoint(Point point) { this.location_point = point; } - public void changeLocationName(String name) { - this.locationName = name; + public void changeName(String name) { + this.name = name; } } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java index b1154cf2..8d5e5d25 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/SaveLocationPort.java @@ -5,5 +5,5 @@ public interface SaveLocationPort { - Location saveLocation(String roomId, String memberId, Point point, String locationName); + Location saveLocation(String roomId, String memberId, Point point, String name); } diff --git a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java index 2d5022eb..2a4df22c 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/usecase/CreateLocationUseCase.java @@ -7,8 +7,8 @@ public interface CreateLocationUseCase { Location createLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude, String locationName); + BigDecimal longitude, String name); Location updateLocation(String roomId, String memberId, BigDecimal latitude, - BigDecimal longitude, String locationName); + BigDecimal longitude, String name); } From 7e82755e59839d33066ff82c481ade9d378547e6 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 29 Mar 2025 12:02:04 +0900 Subject: [PATCH 111/163] :hammer: refactor: add V2 script for new column name --- .../main/resources/db/migration/V2__init.sql | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 kok-api/src/main/resources/db/migration/V2__init.sql diff --git a/kok-api/src/main/resources/db/migration/V2__init.sql b/kok-api/src/main/resources/db/migration/V2__init.sql new file mode 100644 index 00000000..9bebfaf1 --- /dev/null +++ b/kok-api/src/main/resources/db/migration/V2__init.sql @@ -0,0 +1,34 @@ +CREATE TABLE station +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(20) NOT NULL, + latitude DECIMAL(16, 14) NOT NULL, + longitude DECIMAL(17, 14) NOT NULL, + priority BIGINT NOT NULL, + PRIMARY KEY (id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE route +( + id BIGINT NOT NULL AUTO_INCREMENT, + code BIGINT NOT NULL, + station_id BIGINT NOT NULL, + name VARCHAR(20) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (station_id) REFERENCES station (id) ON DELETE CASCADE +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +create table location +( + id bigint auto_increment primary key, + member_id varchar(255) not null, + location_point point not null, + room_id varchar(255) not null, + name varchar(255) not null, + constraint UKrgpajb4rsivb4gj9xn2qowgw6 + unique (room_id, member_id) +); From 82865c2cbe277b845bd13b3cd0984f848f40e0f9 Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 29 Mar 2025 13:49:32 +0900 Subject: [PATCH 112/163] :hammer: refactor: refactor V2 script for new column name --- .../db/migration/V2__add_location_name.sql | 5 +++ .../main/resources/db/migration/V2__init.sql | 34 ------------------- 2 files changed, 5 insertions(+), 34 deletions(-) create mode 100644 kok-api/src/main/resources/db/migration/V2__add_location_name.sql delete mode 100644 kok-api/src/main/resources/db/migration/V2__init.sql diff --git a/kok-api/src/main/resources/db/migration/V2__add_location_name.sql b/kok-api/src/main/resources/db/migration/V2__add_location_name.sql new file mode 100644 index 00000000..0cfd43fe --- /dev/null +++ b/kok-api/src/main/resources/db/migration/V2__add_location_name.sql @@ -0,0 +1,5 @@ +ALTER TABLE location + ADD COLUMN name VARCHAR(255) DEFAULT 'unknown'; + +ALTER TABLE location + MODIFY COLUMN name VARCHAR(255) NOT NULL; \ No newline at end of file diff --git a/kok-api/src/main/resources/db/migration/V2__init.sql b/kok-api/src/main/resources/db/migration/V2__init.sql deleted file mode 100644 index 9bebfaf1..00000000 --- a/kok-api/src/main/resources/db/migration/V2__init.sql +++ /dev/null @@ -1,34 +0,0 @@ -CREATE TABLE station -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(20) NOT NULL, - latitude DECIMAL(16, 14) NOT NULL, - longitude DECIMAL(17, 14) NOT NULL, - priority BIGINT NOT NULL, - PRIMARY KEY (id) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_unicode_ci; - -CREATE TABLE route -( - id BIGINT NOT NULL AUTO_INCREMENT, - code BIGINT NOT NULL, - station_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, - PRIMARY KEY (id), - FOREIGN KEY (station_id) REFERENCES station (id) ON DELETE CASCADE -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_unicode_ci; - -create table location -( - id bigint auto_increment primary key, - member_id varchar(255) not null, - location_point point not null, - room_id varchar(255) not null, - name varchar(255) not null, - constraint UKrgpajb4rsivb4gj9xn2qowgw6 - unique (room_id, member_id) -); From 5bb64025bbfc17dd8a5345d4b6972173f461f35b Mon Sep 17 00:00:00 2001 From: minseokey Date: Sat, 29 Mar 2025 13:58:13 +0900 Subject: [PATCH 113/163] :hammer: refactor: add name field for test --- .../kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java index b1fbc4ed..df2be0cb 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -83,7 +83,7 @@ private static DynamicTest inputLocation(String message, RestAssured.given().log().all() .contentType(ContentType.JSON) .body(new LocationRequest(roomId, memberId, new BigDecimal("37"), - new BigDecimal("127"))) + new BigDecimal("127"), "test")) .when().post("/v1/api/locations") .then().log().all() .assertThat().statusCode(200); @@ -131,7 +131,7 @@ private static DynamicTest inputLocation(String message, RestAssured.given().log().all() .contentType(ContentType.JSON) .body(new LocationRequest(roomId, memberId, new BigDecimal("37"), - new BigDecimal("127"))) + new BigDecimal("127"), "test")) .when().post("/v1/api/locations") .then().log().all() .assertThat().statusCode(200); From bb3a9d88d6bb532e39a443f99c15a9e02c21b9fc Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:20:24 +0900 Subject: [PATCH 114/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20implement?= =?UTF-8?q?=20getting=20candidates=20API=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: create domain for voting * :white_check_mark: test: create RedisDatabaseCleaner And change RepositoryTest into SpringBootTest for testing redis * :wrench: config: fix ddl type error * :sparkles: feat: implement port for saving candidate * :sparkles: feat: implement port for saving votes * :sparkles: feat: implement port for loading candidates * :sparkles: feat: implement checking if candidates exists * :sparkles: feat: implement getting recommended stations from cache * :recycle: refactor: move getting recommended stations from adapter to service * :sparkle: feat: implement get candidate usecase * :sparkle: feat: implement get candidate Facade service and Controller * :sparkle: feat: add updating route names into "1ํ˜ธ์„ " * :sparkle: feat: implement deleting cache of recommended Stations * :sparkle: feat: delete cached recommended stations after saving candidates * :recycle: refactor: refactor key-format for candidate * :recycle: refactor: merge conflict * :recycle: refactor: remove route response * :recycle: refactor: use Station Facade Service for finding candidates --------- Co-authored-by: YUN YOUNG --- .../exception/GlobalExceptionHandler.java | 5 +- .../persistence/RoutePersistenceAdapter.java | 3 + .../out/persistence/RouteRepository.java | 6 ++ .../application/service/StationService.java | 26 ++++-- .../in/dto/response/CandidateResponse.java | 36 ++++++++ .../in/dto/response/CommentResponse.java | 9 ++ .../vote/adapter/in/web/VoteController.java | 32 +++++++ .../CandidateCommandRedisAdapter.java | 40 +++++++++ .../CandidateQueryRedisAdapter.java | 39 +++++++++ .../persistence/VoteCommandRedisAdapter.java | 57 +++++++++++++ .../application/service/CandidateService.java | 29 +++++++ .../service/VoteFacadeService.java | 61 ++++++++++++++ .../common/util/MySQLDatabaseCleaner.java | 1 + .../service/StationServiceTest.java | 7 ++ .../CandidateCommandRedisAdapterTest.java | 45 ++++++++++ .../CandidateQueryRedisAdapterTest.java | 84 +++++++++++++++++++ .../VoteCommandRedisAdapterTest.java | 62 ++++++++++++++ .../station/domain/entity/Station.java | 2 + .../port/out/LoadRecommendedStationsPort.java | 9 ++ .../station/usecase/GetStationUseCase.java | 8 ++ .../port/out/LoadCandidatePort.java | 11 +++ .../port/out/SaveCandidatePort.java | 8 ++ .../application/port/out/SaveVotePort.java | 10 +++ .../kok/kokcore/vote/domain/Candidate.java | 17 ++++ .../com/kok/kokcore/vote/domain/Vote.java | 38 +++++++++ .../kokcore/vote/domain/vo/VoteStatus.java | 23 +++++ .../vote/usecase/CreateVoteUseCase.java | 10 +++ .../vote/usecase/GetCandidateUseCase.java | 10 +++ 28 files changed, 676 insertions(+), 12 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CommentResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadRecommendedStationsPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/usecase/GetStationUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/LoadCandidatePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveCandidatePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/domain/Candidate.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java diff --git a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java index 7093159d..8c7235e6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java +++ b/kok-api/src/main/java/com/kok/kokapi/common/exception/GlobalExceptionHandler.java @@ -34,9 +34,8 @@ public ResponseEntity> handleMethodArgumentNotValidExceptio .body(ApiResponseDto.error(ErrorCode.INVALID_INPUT, message)); } - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleBadRequestException( - IllegalArgumentException ex) { + @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class}) + public ResponseEntity> handleBadRequestException(RuntimeException ex) { return ResponseEntity.badRequest() .body(ApiResponseDto.error(ErrorCode.BAD_REQUEST, ex.getMessage())); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java index 55232744..2a80312c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java @@ -18,6 +18,7 @@ @RequiredArgsConstructor public class RoutePersistenceAdapter implements SaveRoutePort, RetrieveRoutePort { + public static final List ROUTE_ONE_LIST = List.of("๊ฒฝ๋ถ€์„ ", "๊ฒฝ์ธ์„ ", "๊ฒฝ์›์„ ", "์žฅํ•ญ์„ "); private final RouteRepository routeRepository; private static final String INSERT_ROUTE_SQL = """ @@ -39,6 +40,8 @@ public void saveRoutes(List routes) { return; } batchInsertRoutes(routes); + int updatedCount = routeRepository.updateRouteNameToRouteOne(ROUTE_ONE_LIST); + log.debug("Successfully changed {} route name to \"1ํ˜ธ์„ \".", updatedCount); } private void batchInsertRoutes(List routes) { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java index 0fdf9a71..7c048cbc 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java @@ -4,8 +4,14 @@ import com.kok.kokcore.station.domain.entity.Station; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface RouteRepository extends JpaRepository { List findAllByStationOrderByName(Station station); + + @Modifying + @Query("UPDATE Route r SET r.name = '1ํ˜ธ์„ ' WHERE r.name IN :names") + int updateRouteNameToRouteOne(List names); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java index daae47b3..5155f6e8 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationService.java @@ -1,17 +1,18 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.config.geometry.PointConverter; -import com.kok.kokapi.station.adapter.out.persistence.UserRecommendStationCommandRedisAdapter; -import com.kok.kokapi.station.adapter.out.persistence.UserRecommendStationQueryRedisAdapter; import com.kok.kokcore.location.port.out.ReadCentroidPort; -import com.kok.kokcore.station.port.out.dto.StationRouteDtos; -import com.kok.kokcore.station.usecase.SaveStationUseCase; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.port.out.LoadStationsPort; import com.kok.kokcore.station.port.out.ReadStationsPort; +import com.kok.kokcore.station.port.out.ReadUserRecommendStationsPort; import com.kok.kokcore.station.port.out.RetrieveStationsPort; import com.kok.kokcore.station.port.out.SaveRoutePort; import com.kok.kokcore.station.port.out.SaveStationsPort; +import com.kok.kokcore.station.port.out.SaveUserRecommendStationsPort; +import com.kok.kokcore.station.port.out.dto.StationRouteDtos; +import com.kok.kokcore.station.usecase.GetStationUseCase; +import com.kok.kokcore.station.usecase.SaveStationUseCase; import com.kok.kokcore.station.usecase.SystemRecommendUseCase; import com.kok.kokcore.station.usecase.UserRecommendUseCase; import java.math.BigDecimal; @@ -32,7 +33,7 @@ @RequiredArgsConstructor @Slf4j public class StationService implements SaveStationUseCase, SystemRecommendUseCase, - UserRecommendUseCase { + UserRecommendUseCase, GetStationUseCase { private final LoadStationsPort loadStationsPort; private final SaveStationsPort saveStationsPort; @@ -40,8 +41,8 @@ public class StationService implements SaveStationUseCase, SystemRecommendUseCas private final ReadCentroidPort readCentroidPort; private final SaveRoutePort saveRoutePort; private final RetrieveStationsPort retrieveStationsPort; - private final UserRecommendStationQueryRedisAdapter userRecommendStationQueryRedisAdapter; - private final UserRecommendStationCommandRedisAdapter userRecommendStationCommandRedisAdapter; + private final ReadUserRecommendStationsPort readUserRecommendStationPort; + private final SaveUserRecommendStationsPort saveUserRecommendStationsPort; private final PointConverter pointConverter; @Override @@ -64,12 +65,12 @@ public List searchStations(String keyword) { public Station addUserRecommendStation(String roomId, Long stationId) { Station station = retrieveStationsPort.retrieveStation(stationId) .orElseThrow(() -> new RuntimeException("ํ•ด๋‹น ์—ญ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")); - return userRecommendStationCommandRedisAdapter.addUserRecommendStation(roomId, station); + return saveUserRecommendStationsPort.addUserRecommendStation(roomId, station); } @Override public List getUserRecommendStation(String roomId) { - return userRecommendStationQueryRedisAdapter.findUserRecommendedStationsByRoomId(roomId); + return readUserRecommendStationPort.findUserRecommendedStationsByRoomId(roomId); } @Override @@ -185,4 +186,11 @@ private double calculateDistance(Point p1, Station station) { // ์œ ํด๋ฆฌ๋“œ ๊ฑฐ๋ฆฌ ๊ณต์‹ ์ ์šฉ return Math.sqrt(Math.pow(lat2 - lat1, 2) + Math.pow(lon2 - lon1, 2)); } + + @Override + public Station getStation(long stationId) { + return retrieveStationsPort.retrieveStation(stationId) + .orElseThrow( + () -> new IllegalArgumentException("Cannot find station with id: " + stationId)); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java new file mode 100644 index 00000000..c5f08c4c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java @@ -0,0 +1,36 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public record CandidateResponse( + long stationId, + String stationName, + List routes, + int totalTime, + int transferCount, + List comments +) { + + public static CandidateResponse of( + Station station, + List routes, + TmapPublicTransportationParsedResponse transportationParsedResponse, + List comments //comment ๋„๋ฉ”์ธ ๊ตฌํ˜„ ์‹œ List๋กœ ๊ต์ฒด + ) { + return new CandidateResponse( + station.getId(), + station.getName(), + getRoutes(routes), + transportationParsedResponse.totalTime(), + transportationParsedResponse.transferCount(), + comments + ); + } + + private static List getRoutes(List routes) { + return routes.stream().map(Route::getName).toList(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CommentResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CommentResponse.java new file mode 100644 index 00000000..143d7135 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CommentResponse.java @@ -0,0 +1,9 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +public record CommentResponse( + String memberId, + String imageUrl, + String comment +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java new file mode 100644 index 00000000..faa8c519 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -0,0 +1,32 @@ +package com.kok.kokapi.vote.adapter.in.web; + +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.annotion.V1Controller; +import com.kok.kokapi.station.application.service.StationFacadeService; +import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; +import com.kok.kokapi.vote.application.service.VoteFacadeService; +import com.kok.kokcore.station.domain.entity.Station; +import io.swagger.v3.oas.annotations.Operation; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@V1Controller +@RequiredArgsConstructor +public class VoteController { + + private final VoteFacadeService voteFacadeService; + private final StationFacadeService stationFacadeService; + + @Operation(summary = "ํˆฌํ‘œ ํ›„๋ณด์ง€ ๋ชฉ๋ก ์กฐํšŒ", description = "๋ฐฉ ID๊ณผ ์‚ฌ์šฉ์ž ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํˆฌํ‘œ ํ›„๋ณด์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/votes/{roomId}/{memberId}/candidates") + public ResponseEntity>> getCandidates( + @PathVariable String roomId, @PathVariable String memberId) { + List stations = stationFacadeService.getCandidateStation(roomId); + List responses = voteFacadeService.getCandidates(roomId, memberId, + stations); + return ResponseEntity.ok(ApiResponseDto.success(responses)); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java new file mode 100644 index 00000000..24bf8d73 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java @@ -0,0 +1,40 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import com.kok.kokcore.vote.application.port.out.SaveCandidatePort; +import com.kok.kokcore.vote.domain.Candidate; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class CandidateCommandRedisAdapter implements SaveCandidatePort { + + private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + + private final RedisTemplate redisTemplate; + + @Override + public void saveAll(List candidates) { + validate(candidates); + String key = getCandidateKey(candidates); + Object[] stationIds = candidates.stream() + .map(Candidate::getStationId) + .toArray(); + + redisTemplate.opsForSet().add(key, stationIds); + } + + private void validate(List candidates) { + if (Objects.isNull(candidates) || candidates.isEmpty()) { + throw new IllegalArgumentException("No candidates to save"); + } + } + + private String getCandidateKey(List candidates) { + String roomId = candidates.getFirst().getRoomId(); + return String.format(CANDIDATE_KEY_FORMAT, roomId); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java new file mode 100644 index 00000000..547ec175 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java @@ -0,0 +1,39 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import com.kok.kokcore.vote.application.port.out.LoadCandidatePort; +import com.kok.kokcore.vote.domain.Candidate; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class CandidateQueryRedisAdapter implements LoadCandidatePort { + + private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + + private final RedisTemplate redisTemplate; + + @Override + public List findByRoomId(String roomId) { + Set stationIds = redisTemplate.opsForSet().members(getCandidateKey(roomId)); + if (Objects.isNull(stationIds)) { + return List.of(); + } + return stationIds.stream() + .map(stationId -> new Candidate(roomId, Long.parseLong(stationId.toString()))) + .toList(); + } + + @Override + public boolean isExistsByRoomId(String roomId) { + return redisTemplate.hasKey(getCandidateKey(roomId)); + } + + private String getCandidateKey(String roomId) { + return String.format(CANDIDATE_KEY_FORMAT, roomId); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java new file mode 100644 index 00000000..66919c6b --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java @@ -0,0 +1,57 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import com.kok.kokcore.vote.application.port.out.SaveVotePort; +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class VoteCommandRedisAdapter implements SaveVotePort { + + private static final String VOTES_KEY = "vote:"; + private static final String CANDIDATE_KEY = "candidate:"; + + public final RedisTemplate redisTemplate; + + @Override + public void saveAll(List votes) { + validate(votes); + List agreeVotes = getAgreeVotes(votes); + List disagreeVotes = getDisagreeVotes(votes); + save(agreeVotes); + save(disagreeVotes); + } + + private void validate(List votes) { + if (Objects.isNull(votes) || votes.isEmpty()) { + throw new IllegalArgumentException("No votes to save"); + } + } + + private List getAgreeVotes(List votes) { + return votes.stream().filter(vote -> vote.getVoteStatus().isAgree()).toList(); + } + + private List getDisagreeVotes(List votes) { + return votes.stream().filter(vote -> vote.getVoteStatus().isDisagree()).toList(); + } + + private void save(List agreeVotes) { + String key = getKey(agreeVotes.getFirst()); + Object[] memberIds = agreeVotes.stream().map(Vote::getMemberId).toArray(); + redisTemplate.opsForSet().add(key, memberIds); + } + + private String getKey(Vote vote) { + StringJoiner joiner = new StringJoiner(":"); + joiner.add(VOTES_KEY + vote.getRoomId()); + joiner.add(CANDIDATE_KEY + vote.getStationId()); + joiner.add(vote.getVoteStatus().getName()); + return joiner.toString(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java new file mode 100644 index 00000000..047fa8ad --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java @@ -0,0 +1,29 @@ +package com.kok.kokapi.vote.application.service; + +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.vote.application.port.out.LoadCandidatePort; +import com.kok.kokcore.vote.application.port.out.SaveCandidatePort; +import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.usecase.GetCandidateUseCase; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CandidateService implements GetCandidateUseCase { + + private final SaveCandidatePort saveCandidatePort; + private final LoadCandidatePort loadCandidatePort; + + @Override + public List getCandidate(String roomId, List stations) { + if (!loadCandidatePort.isExistsByRoomId(roomId)) { + List candidates = stations.stream() + .map(station -> new Candidate(roomId, station.getId())) + .toList(); + saveCandidatePort.saveAll(candidates); + } + return loadCandidatePort.findByRoomId(roomId); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java new file mode 100644 index 00000000..9e72c0b2 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -0,0 +1,61 @@ +package com.kok.kokapi.vote.application.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; +import com.kok.kokapi.public_transportation.application.service.TmapPublicTransportationService; +import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.usecase.GetStationUseCase; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.usecase.GetCandidateUseCase; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class VoteFacadeService { + + private final GetCandidateUseCase getCandidateUseCase; + private final GetStationUseCase getStationUseCase; + private final RetrieveRouteUseCase retrieveRouteUseCase; + private final TmapPublicTransportationService tmapPublicTransportationService; + private final ObjectMapper objectMapper; + + public List getCandidates(String roomId, String memberId, + List stations) { + List responses = new ArrayList<>(); + List candidates = getCandidateUseCase.getCandidate(roomId, stations); + for (Candidate candidate : candidates) { + Station station = getStationUseCase.getStation(candidate.getStationId()); + List routes = retrieveRouteUseCase.retrieveRoutes(station); + TmapPublicTransportationParsedResponse transportationParsedResponse = getTransportationParsedResponse( + roomId, + memberId, + station + ); + CandidateResponse.of(station, routes, transportationParsedResponse, List.of()); + } + return responses; + } + + private TmapPublicTransportationParsedResponse getTransportationParsedResponse( + String roomId, String memberId, Station station) { + String content = tmapPublicTransportationService.retrievePublicTransportation( + station.getId(), + roomId, + memberId + ); + try { + return objectMapper.readValue(content, TmapPublicTransportationParsedResponse.class); + } catch (JsonProcessingException e) { + throw new RuntimeException( + "Failed parsing into \"TmapPublicTransportationParsedResponse\" for " + content); + } + } + +} diff --git a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java index 466db4f6..a1c0376f 100644 --- a/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java +++ b/kok-api/src/test/java/com/kok/kokapi/common/util/MySQLDatabaseCleaner.java @@ -13,6 +13,7 @@ public class MySQLDatabaseCleaner implements DatabaseCleaner { @PersistenceContext private EntityManager entityManager; + @Override @Transactional public void cleanUp() { entityManager.flush(); diff --git a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java index 0f083958..a870c386 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java @@ -9,13 +9,20 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.CacheManager; class StationServiceTest extends ServiceTest { + private static final String RECOMMEND_STATIONS_CACHE_KEY = "recommendStations"; + @Autowired private StationRepository stationRepository; @Autowired private StationService stationService; + @Autowired + @Qualifier("stationCacheManager") + private CacheManager cacheManager; @DisplayName("์ €์žฅ๋œ ์ง€ํ•˜์ฒ  ์ •๋ณด๊ฐ€ ์—†๋‹ค๋ฉด, ์ง€ํ•˜์ฒ  ์ •๋ณด(์ด 2๊ฐœ)๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ์ €์žฅํ•œ๋‹ค.") @Test diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java new file mode 100644 index 00000000..cdbf159e --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java @@ -0,0 +1,45 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kok.kokapi.common.template.RepositoryTest; +import com.kok.kokcore.vote.domain.Candidate; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +class CandidateCommandRedisAdapterTest extends RepositoryTest { + + private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + + @Autowired + private CandidateCommandRedisAdapter candidateCommandRedisAdapter; + @Autowired + private RedisTemplate redisTemplate; + + @DisplayName("Candidate์„ ์ €์žฅํ•œ๋‹ค.") + @Test + void saveAllCandidates() { + // given + String roomId = "roomId"; + String key = getCandidateKey(roomId); + List candidates = List.of( + new Candidate(roomId, 1), + new Candidate(roomId, 2) + ); + + // when + candidateCommandRedisAdapter.saveAll(candidates); + + // then + Set results = redisTemplate.opsForSet().members(key); + assertThat(results).containsExactlyInAnyOrder(1, 2); + } + + private String getCandidateKey(String roomId) { + return String.format(CANDIDATE_KEY_FORMAT, roomId); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java new file mode 100644 index 00000000..45f4e1ab --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java @@ -0,0 +1,84 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kok.kokapi.common.template.RepositoryTest; +import com.kok.kokcore.vote.domain.Candidate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +class CandidateQueryRedisAdapterTest extends RepositoryTest { + + private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + + @Autowired + private CandidateQueryRedisAdapter candidateQueryRedisAdapter; + @Autowired + private RedisTemplate redisTemplate; + + @DisplayName("roomId๋กœ ํ›„๋ณด์ง€๋ฅผ ์กฐํšŒํ•œ๋‹ค.") + @Test + void findByRoomId() { + // given + String roomId = "roomId"; + String key = getCandidateKey(roomId); + Candidate candidate = new Candidate(roomId, 1); + Candidate candidate2 = new Candidate(roomId, 2); + Candidate candidate3 = new Candidate(roomId, 3); + redisTemplate.opsForSet().add(key, "1", "2", "3"); + + // when + List result = candidateQueryRedisAdapter.findByRoomId(roomId); + + // then + assertThat(result).containsExactlyInAnyOrder(candidate, candidate2, candidate3); + } + + @DisplayName("roomId์— ํ•ด๋‹นํ•˜๋Š” ํ›„๋ณด์ง€๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void findByRoomIdWhenNotExist() { + // given + String roomId = "nonexistent"; + + // when + List result = candidateQueryRedisAdapter.findByRoomId(roomId); + + // then + assertThat(result).isEmpty(); + } + + @DisplayName("roomId์— ๋Œ€ํ•œ ํ›„๋ณด์ง€๊ฐ€ ์กด์žฌํ•œ๋‹ค.") + @Test + void isExistsByRoomId() { + // given + String roomId = "roomId"; + String key = getCandidateKey(roomId); + redisTemplate.opsForSet().add(key, "1"); + + // when + boolean result = candidateQueryRedisAdapter.isExistsByRoomId(roomId); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("roomId์— ๋Œ€ํ•œ ํ›„๋ณด์ง€๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค.") + @Test + void doesNotExistsByRoomId() { + // given + String roomId = "unknownRoomId"; + + // when + boolean result = candidateQueryRedisAdapter.isExistsByRoomId(roomId); + + // then + assertThat(result).isFalse(); + } + + private String getCandidateKey(String roomId) { + return String.format(CANDIDATE_KEY_FORMAT, roomId); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java new file mode 100644 index 00000000..a8e4141a --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java @@ -0,0 +1,62 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.kok.kokapi.common.template.RepositoryTest; +import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +class VoteCommandRedisAdapterTest extends RepositoryTest { + + private static final String VOTES_KEY = "vote:"; + private static final String CANDIDATE_KEY = "candidate:"; + + @Autowired + private VoteCommandRedisAdapter voteCommandRedisAdapter; + @Autowired + private RedisTemplate redisTemplate; + + @DisplayName("์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•œ๋‹ค.") + @Test + void saveAllVotesOfMember() { + // given + String roomId = "roomId"; + long stationId = 1; + Candidate candidate = new Candidate(roomId, stationId); + List votes = List.of( + new Vote(candidate, "memberId1", VoteStatus.AGREE), + new Vote(candidate, "memberId2", VoteStatus.DISAGREE), + new Vote(candidate, "memberId3", VoteStatus.DISAGREE) + ); + + // when + voteCommandRedisAdapter.saveAll(votes); + + // then + Set agreeResult = redisTemplate.opsForSet() + .members(getKey(roomId, stationId, VoteStatus.AGREE)); + Set disagreeResult = redisTemplate.opsForSet() + .members(getKey(roomId, stationId, VoteStatus.DISAGREE)); + assertAll( + () -> assertThat(agreeResult).containsExactlyInAnyOrder("memberId1"), + () -> assertThat(disagreeResult).containsExactlyInAnyOrder("memberId2", "memberId3") + ); + } + + private String getKey(String roomId, long stationId, VoteStatus voteStatus) { + StringJoiner joiner = new StringJoiner(":"); + joiner.add(VOTES_KEY + roomId); + joiner.add(CANDIDATE_KEY + stationId); + joiner.add(voteStatus.getName()); + return joiner.toString(); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java index 34f43a6b..e613edf3 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Station.java @@ -7,12 +7,14 @@ import jakarta.persistence.Id; import java.math.BigDecimal; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode public class Station { @Id diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadRecommendedStationsPort.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadRecommendedStationsPort.java new file mode 100644 index 00000000..92e9e57d --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/LoadRecommendedStationsPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.port.out; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface LoadRecommendedStationsPort { + + List getStationsByRoomId(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/GetStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/GetStationUseCase.java new file mode 100644 index 00000000..325b37e4 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/GetStationUseCase.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.station.usecase; + +import com.kok.kokcore.station.domain.entity.Station; + +public interface GetStationUseCase { + + Station getStation(long stationId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/LoadCandidatePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/LoadCandidatePort.java new file mode 100644 index 00000000..9fd49858 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/LoadCandidatePort.java @@ -0,0 +1,11 @@ +package com.kok.kokcore.vote.application.port.out; + +import com.kok.kokcore.vote.domain.Candidate; +import java.util.List; + +public interface LoadCandidatePort { + + List findByRoomId(String roomId); + + boolean isExistsByRoomId(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveCandidatePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveCandidatePort.java new file mode 100644 index 00000000..7a9db073 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveCandidatePort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.vote.application.port.out; + +import com.kok.kokcore.vote.domain.Candidate; +import java.util.List; + +public interface SaveCandidatePort { + void saveAll(List candidates); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java new file mode 100644 index 00000000..a247168f --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.vote.application.port.out; + +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; + +public interface SaveVotePort { + + void saveAll(List votes); + +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/Candidate.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Candidate.java new file mode 100644 index 00000000..c821e2b5 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Candidate.java @@ -0,0 +1,17 @@ +package com.kok.kokcore.vote.domain; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +public class Candidate { + + private final String roomId; + private final long stationId; + + public Candidate(String roomId, long stationId) { + this.roomId = roomId; + this.stationId = stationId; + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java new file mode 100644 index 00000000..25766f9c --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java @@ -0,0 +1,38 @@ +package com.kok.kokcore.vote.domain; + +import com.kok.kokcore.vote.domain.vo.VoteStatus; +import lombok.Getter; + +@Getter +public class Vote { + + private Candidate candidate; + private String memberId; + private VoteStatus voteStatus; + + public Vote(Candidate candidate, String memberId, VoteStatus voteStatus) { + this.candidate = candidate; + this.memberId = memberId; + this.voteStatus = voteStatus; + } + + public Vote(String roomId, String memberId, long stationId) { + this(new Candidate(roomId, stationId), memberId, VoteStatus.DISAGREE); + } + + public void agree() { + this.voteStatus = VoteStatus.AGREE; + } + + public void disagree() { + this.voteStatus = VoteStatus.DISAGREE; + } + + public String getRoomId() { + return candidate.getRoomId(); + } + + public long getStationId() { + return candidate.getStationId(); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java new file mode 100644 index 00000000..9dc2bd30 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java @@ -0,0 +1,23 @@ +package com.kok.kokcore.vote.domain.vo; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum VoteStatus { + + AGREE("agree"), + DISAGREE("disagree"), + ; + + private final String name; + + public boolean isAgree() { + return this.equals(AGREE); + } + + public boolean isDisagree() { + return this.equals(DISAGREE); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java new file mode 100644 index 00000000..ab68a071 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.vote.usecase; + +import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; + +public interface CreateVoteUseCase { + + void createVotes(List candidates, List votes); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java new file mode 100644 index 00000000..d0f4497c --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.vote.usecase; + +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.vote.domain.Candidate; +import java.util.List; + +public interface GetCandidateUseCase { + + List getCandidate(String roomId, List stations); +} From 9c44744dd8c780a5bc358e98d16777cfe8381d82 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 29 Mar 2025 16:17:53 +0900 Subject: [PATCH 115/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20implement?= =?UTF-8?q?=20save=20vote=20API=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: create domain for voting * :white_check_mark: test: create RedisDatabaseCleaner And change RepositoryTest into SpringBootTest for testing redis * :wrench: config: fix ddl type error * :sparkles: feat: implement port for saving candidate * :sparkles: feat: implement port for saving votes * :sparkles: feat: implement port for loading candidates * :sparkles: feat: implement checking if candidates exists * :sparkles: feat: implement getting recommended stations from cache * :recycle: refactor: move getting recommended stations from adapter to service * :sparkle: feat: implement get candidate usecase * :sparkle: feat: implement get candidate Facade service and Controller * :sparkle: feat: add updating route names into "1ํ˜ธ์„ " * :sparkle: feat: save votes by member * :white_check_mark: test: test for saving votes by candidate * :sparkles: feat: implement saving vote usecase * :sparkle: feat: implement deleting cache of recommended Stations * :sparkle: feat: delete cached recommended stations after saving candidates * :sparkle: feat: implement creating Vote Controller and facade service * :sparkle: feat: implement loadVotePort checking if votes exists by member * :recycle: refactor: refactor key-format for candidate * :sparkles: feat: implement delete vote port deleting votes by candidate * :sparkles: feat: implement delete vote port deleting all votes by roomId and memberId * :recycle: refactor: rename key constant * :sparkles: feat: implement load vote port finding all votes by memberId And roomId * :sparkles: feat: add logic deleting previous vote data on saveVoteUsecase * :recycle: refactor: merge conflict * :recycle: refactor: move package for port * :sparkles: feat: throw exception if cannot parse to Long * :sparkles: feat: handle redis error * :sparkles: feat: implement retry for redis connection failure error * :recycle: refactor: implement getting saved candidates by roomID --------- Co-authored-by: YUN YOUNG --- kok-api/build.gradle | 2 + .../kok/kokapi/common/util/RedisExecutor.java | 75 +++++++++++ .../kok/kokapi/config/redis/RedisConfig.java | 2 + .../StationPersistenceAdapter.java | 7 +- .../adapter/in/dto/request/VoteRequest.java | 9 ++ .../in/dto/response/RouteResponse.java | 8 ++ .../vote/adapter/in/web/VoteController.java | 14 ++ .../CandidateCommandRedisAdapter.java | 7 +- .../CandidateQueryRedisAdapter.java | 29 ++++- .../persistence/VoteCommandRedisAdapter.java | 91 +++++++++---- .../persistence/VoteQueryRedisAdapter.java | 62 +++++++++ .../application/service/CandidateService.java | 14 +- .../service/VoteFacadeService.java | 29 ++++- .../vote/application/service/VoteService.java | 38 ++++++ .../service/StationServiceTest.java | 7 - .../VoteCommandRedisAdapterTest.java | 120 ++++++++++++++---- .../VoteQueryRedisAdapterTest.java | 75 +++++++++++ .../application/service/VoteServiceTest.java | 104 +++++++++++++++ .../application/port/out/SaveVotePort.java | 10 -- .../com/kok/kokcore/vote/domain/Vote.java | 16 +-- .../kokcore/vote/domain/vo/VoteStatus.java | 12 +- .../kokcore/vote/port/out/DeleteVotePort.java | 10 ++ .../port/out/LoadCandidatePort.java | 2 +- .../kokcore/vote/port/out/LoadVotePort.java | 11 ++ .../port/out/SaveCandidatePort.java | 3 +- .../kokcore/vote/port/out/SaveVotePort.java | 11 ++ .../vote/usecase/GetCandidateUseCase.java | 4 +- .../kokcore/vote/usecase/SaveVoteUseCase.java | 9 ++ 28 files changed, 684 insertions(+), 97 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/common/util/RedisExecutor.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/request/VoteRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/RouteResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java rename kok-core/src/main/java/com/kok/kokcore/vote/{application => }/port/out/LoadCandidatePort.java (80%) create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java rename kok-core/src/main/java/com/kok/kokcore/vote/{application => }/port/out/SaveCandidatePort.java (75%) create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java diff --git a/kok-api/build.gradle b/kok-api/build.gradle index e2484971..195f49e5 100644 --- a/kok-api/build.gradle +++ b/kok-api/build.gradle @@ -30,6 +30,8 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework:spring-aspects' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/kok-api/src/main/java/com/kok/kokapi/common/util/RedisExecutor.java b/kok-api/src/main/java/com/kok/kokapi/common/util/RedisExecutor.java new file mode 100644 index 00000000..a637ce49 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/common/util/RedisExecutor.java @@ -0,0 +1,75 @@ +package com.kok.kokapi.common.util; + +import java.util.function.Supplier; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; + +@Slf4j +public class RedisExecutor { + + /** + * Redis ์ž‘์—…์„ ์‹คํ–‰ํ•˜๊ณ  ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ fallback ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public static T runOrElseGet(String operationName, Supplier operation, T fallbackValue) { + try { + return retry(operationName, operation); + } catch (RedisConnectionFailureException e) { + log.error("[Redis][{}] Connection failure. Retry or alert needed.", operationName, e); + } catch (RedisSystemException e) { + log.error("[Redis][{}] System error (serialization, etc).", operationName, e); + } catch (DataAccessException e) { + log.warn("[Redis][{}] Data access issue.", operationName, e); + } catch (Exception e) { + log.error("[Redis][{}] Unexpected exception.", operationName, e); + } + return fallbackValue; + } + + /** + * Redis ์ž‘์—…์„ ์‹คํ–‰ํ•˜๊ณ  ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋˜์ง‘๋‹ˆ๋‹ค. + */ + public static T runOrThrow(String operationName, Supplier operation) { + try { + return retry(operationName, operation); + } catch (RedisConnectionFailureException e) { + log.error("[Redis][{}] Connection failure.", operationName, e); + throw e; + } catch (RedisSystemException e) { + log.error("[Redis][{}] System error (serialization, etc).", operationName, e); + throw e; + } catch (DataAccessException e) { + log.warn("[Redis][{}] Data access issue.", operationName, e); + throw e; + } catch (Exception e) { + log.error("[Redis][{}] Unexpected exception.", operationName, e); + throw e; + } + } + + /** + * ๋ฐ˜ํ™˜๊ฐ’์ด ์—†๋Š” Redis ์ž‘์—… ์‹คํ–‰ ๋ฐ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ throwํ•˜๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + public static void runOrThrow(String operationName, Runnable operation) { + runOrThrow(operationName, () -> { + operation.run(); + return null; + }); + } + + /** + * Redis ์—ฐ๊ฒฐ ์‹คํŒจ์— ๋Œ€ํ•ด์„œ๋งŒ ์žฌ์‹œ๋„ํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ์ฆ‰์‹œ ์ฒ˜๋ฆฌ + */ + @Retryable( + value = RedisConnectionFailureException.class, + maxAttempts = 3, + backoff = @Backoff(delay = 300) + ) + protected static T retry(String operationName, Supplier operation) { + log.debug("[Redis][{}] Retrying operation.", operationName); + return operation.get(); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java index 8412c3b2..c92cc465 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/redis/RedisConfig.java @@ -7,9 +7,11 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.retry.annotation.EnableRetry; @Configuration @RequiredArgsConstructor +@EnableRetry public class RedisConfig { // ์ถ”ํ›„ ConnectionFactory์„ค์ • ๋ณ€๊ฒฝ์„ ๊ณ ๋ ค. (Sentinel, Cluster, etc...) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java index 3eb4152b..6d1dae62 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/StationPersistenceAdapter.java @@ -76,8 +76,11 @@ public Optional retrieveStation(Long stationId) { @Transactional(readOnly = true) public List retrieveInRangeStations(Point centroid, double dist) { Pair lonLat = pointConverter.toCoordinates(centroid); - return stationRepository.findInRangeStationsByCentroid(lonLat.getFirst(), - lonLat.getSecond(), dist); + return stationRepository.findInRangeStationsByCentroid( + lonLat.getFirst(), + lonLat.getSecond(), + dist + ); } @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/request/VoteRequest.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/request/VoteRequest.java new file mode 100644 index 00000000..f3363d5c --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/request/VoteRequest.java @@ -0,0 +1,9 @@ +package com.kok.kokapi.vote.adapter.in.dto.request; + +import java.util.List; + +public record VoteRequest( + List agreedStationIds +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/RouteResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/RouteResponse.java new file mode 100644 index 00000000..92c8fd38 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/RouteResponse.java @@ -0,0 +1,8 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +public record RouteResponse( + String name + //TODO: String color +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index faa8c519..8b927292 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -3,6 +3,7 @@ import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; import com.kok.kokapi.station.application.service.StationFacadeService; +import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokapi.vote.application.service.VoteFacadeService; import com.kok.kokcore.station.domain.entity.Station; @@ -12,6 +13,8 @@ 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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; @V1Controller @RequiredArgsConstructor @@ -29,4 +32,15 @@ public ResponseEntity>> getCandidates( stations); return ResponseEntity.ok(ApiResponseDto.success(responses)); } + + @Operation(summary = "ํˆฌํ‘œํ•˜๊ธฐ", description = "๋ฐฉ ID์™€ ์‚ฌ์šฉ์ž ID์— ๋Œ€ํ•œ ์ฐฌ์„ฑ ํˆฌํ‘œ ๋ชฉ๋ก์„ ๋ฐ›์•„์„œ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.") + @PostMapping("/votes/{roomId}/{memberId}") + public ResponseEntity> createVote( + @PathVariable String roomId, + @PathVariable String memberId, + @RequestBody VoteRequest voteRequest + ) { + voteFacadeService.createVote(roomId, memberId, voteRequest); + return ResponseEntity.ok(ApiResponseDto.success(null)); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java index 24bf8d73..7f09081b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java @@ -1,7 +1,8 @@ package com.kok.kokapi.vote.adapter.out.persistence; -import com.kok.kokcore.vote.application.port.out.SaveCandidatePort; +import com.kok.kokapi.common.util.RedisExecutor; import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.port.out.SaveCandidatePort; import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -24,7 +25,9 @@ public void saveAll(List candidates) { .map(Candidate::getStationId) .toArray(); - redisTemplate.opsForSet().add(key, stationIds); + RedisExecutor.runOrThrow("saveAll", () -> + redisTemplate.opsForSet().add(key, stationIds) + ); } private void validate(List candidates) { diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java index 547ec175..e279fa46 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java @@ -1,15 +1,18 @@ package com.kok.kokapi.vote.adapter.out.persistence; -import com.kok.kokcore.vote.application.port.out.LoadCandidatePort; +import com.kok.kokapi.common.util.RedisExecutor; import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.port.out.LoadCandidatePort; import java.util.List; import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; @Repository +@Slf4j @RequiredArgsConstructor public class CandidateQueryRedisAdapter implements LoadCandidatePort { @@ -19,18 +22,30 @@ public class CandidateQueryRedisAdapter implements LoadCandidatePort { @Override public List findByRoomId(String roomId) { - Set stationIds = redisTemplate.opsForSet().members(getCandidateKey(roomId)); - if (Objects.isNull(stationIds)) { - return List.of(); - } + Set stationIds = RedisExecutor.runOrElseGet("findByRoomId", () -> + redisTemplate.opsForSet().members(getCandidateKey(roomId)), Set.of() + ); + return stationIds.stream() - .map(stationId -> new Candidate(roomId, Long.parseLong(stationId.toString()))) + .map(this::getStationId) + .filter(Objects::nonNull) + .map(stationId -> new Candidate(roomId, stationId)) .toList(); } + private Long getStationId(Object stationId) { + try { + return Long.parseLong(stationId.toString()); + } catch (NumberFormatException e) { + log.warn("Invalid stationId format in Redis: {}", stationId, e); + return null; + } + } + @Override public boolean isExistsByRoomId(String roomId) { - return redisTemplate.hasKey(getCandidateKey(roomId)); + return RedisExecutor.runOrElseGet("isExistsByRoomId", () -> + Boolean.TRUE.equals(redisTemplate.hasKey(getCandidateKey(roomId))), false); } private String getCandidateKey(String roomId) { diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java index 66919c6b..31f73790 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java @@ -1,30 +1,50 @@ package com.kok.kokapi.vote.adapter.out.persistence; -import com.kok.kokcore.vote.application.port.out.SaveVotePort; +import com.kok.kokapi.common.util.RedisExecutor; import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.port.out.DeleteVotePort; +import com.kok.kokcore.vote.port.out.SaveVotePort; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.StringJoiner; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; +@Slf4j @Repository @RequiredArgsConstructor -public class VoteCommandRedisAdapter implements SaveVotePort { +public class VoteCommandRedisAdapter implements SaveVotePort, DeleteVotePort { - private static final String VOTES_KEY = "vote:"; - private static final String CANDIDATE_KEY = "candidate:"; + private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; + private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; - public final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; @Override - public void saveAll(List votes) { + public void saveByCandidate(Vote vote) { + String key = getCandidateVoteKey(vote); + RedisExecutor.runOrThrow("saveByCandidate", () -> + redisTemplate.opsForSet().add(key, vote.getMemberId()) + ); + } + + @Override + public void saveAllByMember(List votes) { validate(votes); - List agreeVotes = getAgreeVotes(votes); - List disagreeVotes = getDisagreeVotes(votes); - save(agreeVotes); - save(disagreeVotes); + String roomId = votes.getFirst().getRoomId(); + String memberId = votes.getFirst().getMemberId(); + String key = getMemberVoteKey(roomId, memberId); + Map value = votes.stream() + .collect(Collectors.toMap( + Vote::getStationId, + vote -> vote.getVoteStatus().getName() + )); + RedisExecutor.runOrThrow("saveAllByMember", () -> + redisTemplate.opsForHash().putAll(key, value) + ); } private void validate(List votes) { @@ -33,25 +53,46 @@ private void validate(List votes) { } } - private List getAgreeVotes(List votes) { - return votes.stream().filter(vote -> vote.getVoteStatus().isAgree()).toList(); + @Override + public void deleteByCandidate(Vote vote) { + String key = getCandidateVoteKey(vote); + RedisExecutor.runOrThrow("deleteByCandidate", () -> { + Long result = redisTemplate.opsForSet().remove(key, vote.getMemberId()); + if (isNotRemoved(result)) { + log.warn( + "Failed to remove memberId from candidate set or key not found: key={}, memberId={}", + key, + vote.getMemberId() + ); + } + }); + } + + private static boolean isNotRemoved(Long removed) { + return Objects.isNull(removed) || removed == 0L; + } + + @Override + public void deleteAllByRoomIdAndMemberId(String roomId, String memberId) { + String key = getMemberVoteKey(roomId, memberId); + RedisExecutor.runOrThrow("deleteAllRoomIdAndMemberId", () -> { + Boolean result = redisTemplate.delete(key); + if (isNotRemoved(result)) { + log.warn("Key not found or already expired: {}", key); + } + }); } - private List getDisagreeVotes(List votes) { - return votes.stream().filter(vote -> vote.getVoteStatus().isDisagree()).toList(); + private static boolean isNotRemoved(Boolean result) { + return result.equals(Boolean.FALSE); } - private void save(List agreeVotes) { - String key = getKey(agreeVotes.getFirst()); - Object[] memberIds = agreeVotes.stream().map(Vote::getMemberId).toArray(); - redisTemplate.opsForSet().add(key, memberIds); + private String getMemberVoteKey(String roomId, String memberId) { + return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); } - private String getKey(Vote vote) { - StringJoiner joiner = new StringJoiner(":"); - joiner.add(VOTES_KEY + vote.getRoomId()); - joiner.add(CANDIDATE_KEY + vote.getStationId()); - joiner.add(vote.getVoteStatus().getName()); - return joiner.toString(); + private String getCandidateVoteKey(Vote vote) { + return String.format(CANDIDATE_VOTE_KEY_FORMAT, vote.getRoomId(), vote.getStationId(), + vote.getVoteStatus().getName()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java new file mode 100644 index 00000000..5cd20a19 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java @@ -0,0 +1,62 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import com.kok.kokapi.common.util.RedisExecutor; +import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.port.out.LoadVotePort; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class VoteQueryRedisAdapter implements LoadVotePort { + + private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; + + private final RedisTemplate redisTemplate; + + @Override + public boolean isExistsByRoomIdAndMemberId(String roomId, String memberId) { + String key = getMemberVoteKey(roomId, memberId); + return RedisExecutor.runOrElseGet("isExistsByRoomIdAndMemberId", () -> + !redisTemplate.opsForHash().entries(key).isEmpty(), false + ); + } + + @Override + public List findAllByRoomIdAndMemberId(String roomId, String memberId) { + String key = getMemberVoteKey(roomId, memberId); + return RedisExecutor.runOrElseGet("findAllByRoomIdAndMemberId", () -> { + Map voteInfos = redisTemplate.opsForHash().entries(key); + List votes = new ArrayList<>(voteInfos.size()); + for (Entry voteInfo : voteInfos.entrySet()) { + Long stationId = getStationId(voteInfo); + if (stationId == null) { + continue; + } + String voteStatus = String.valueOf(voteInfo.getValue()); + votes.add(new Vote(roomId, stationId, memberId, voteStatus)); + } + return votes; + }, List.of()); + } + + private Long getStationId(Entry voteInfo) { + try { + return Long.valueOf(voteInfo.getKey().toString()); + } catch (NumberFormatException e) { + log.warn("Invalid stationId format in Redis: {}", voteInfo.getKey(), e); + return null; + } + } + + private String getMemberVoteKey(String roomId, String memberId) { + return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java index 047fa8ad..243570da 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java @@ -1,9 +1,9 @@ package com.kok.kokapi.vote.application.service; import com.kok.kokcore.station.domain.entity.Station; -import com.kok.kokcore.vote.application.port.out.LoadCandidatePort; -import com.kok.kokcore.vote.application.port.out.SaveCandidatePort; import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.port.out.LoadCandidatePort; +import com.kok.kokcore.vote.port.out.SaveCandidatePort; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; import java.util.List; import lombok.RequiredArgsConstructor; @@ -17,7 +17,7 @@ public class CandidateService implements GetCandidateUseCase { private final LoadCandidatePort loadCandidatePort; @Override - public List getCandidate(String roomId, List stations) { + public List saveAndGetCandidates(String roomId, List stations) { if (!loadCandidatePort.isExistsByRoomId(roomId)) { List candidates = stations.stream() .map(station -> new Candidate(roomId, station.getId())) @@ -26,4 +26,12 @@ public List getCandidate(String roomId, List stations) { } return loadCandidatePort.findByRoomId(roomId); } + + @Override + public List getCandidates(String roomId) { + if (!loadCandidatePort.isExistsByRoomId(roomId)) { + throw new IllegalArgumentException("Cannot find candidates for roomId: " + roomId); + } + return loadCandidatePort.findByRoomId(roomId); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index 9e72c0b2..f2124e88 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -4,13 +4,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; import com.kok.kokapi.public_transportation.application.service.TmapPublicTransportationService; +import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.usecase.GetStationUseCase; import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; +import com.kok.kokcore.vote.usecase.SaveVoteUseCase; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -23,13 +27,14 @@ public class VoteFacadeService { private final GetCandidateUseCase getCandidateUseCase; private final GetStationUseCase getStationUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; + private final SaveVoteUseCase saveVoteUseCase; private final TmapPublicTransportationService tmapPublicTransportationService; private final ObjectMapper objectMapper; public List getCandidates(String roomId, String memberId, List stations) { List responses = new ArrayList<>(); - List candidates = getCandidateUseCase.getCandidate(roomId, stations); + List candidates = getCandidateUseCase.saveAndGetCandidates(roomId, stations); for (Candidate candidate : candidates) { Station station = getStationUseCase.getStation(candidate.getStationId()); List routes = retrieveRouteUseCase.retrieveRoutes(station); @@ -58,4 +63,26 @@ private TmapPublicTransportationParsedResponse getTransportationParsedResponse( } } + public void createVote(String roomId, String memberId, VoteRequest voteRequest) { + List votes = getVotes(roomId, memberId, voteRequest); + saveVoteUseCase.saveVotes(votes); + } + + private List getVotes(String roomId, String memberId, VoteRequest voteRequest) { + List votes = new ArrayList<>(); + List agreedStationIds = voteRequest.agreedStationIds(); + List candidates = getCandidateUseCase.getCandidates(roomId); + for (Candidate candidate : candidates) { + if (isAgree(agreedStationIds, candidate)) { + votes.add(new Vote(candidate, memberId, VoteStatus.AGREE)); + continue; + } + votes.add(new Vote(candidate, memberId, VoteStatus.DISAGREE)); + } + return votes; + } + + private static boolean isAgree(List agreedStationIds, Candidate candidate) { + return agreedStationIds.contains(candidate.getStationId()); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java new file mode 100644 index 00000000..4d824809 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -0,0 +1,38 @@ +package com.kok.kokapi.vote.application.service; + +import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.port.out.DeleteVotePort; +import com.kok.kokcore.vote.port.out.LoadVotePort; +import com.kok.kokcore.vote.port.out.SaveVotePort; +import com.kok.kokcore.vote.usecase.SaveVoteUseCase; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class VoteService implements SaveVoteUseCase { + + private final SaveVotePort saveVotePort; + private final LoadVotePort loadVotePort; + private final DeleteVotePort deleteVotePort; + + @Override + public void saveVotes(List votes) { + String roomId = votes.getFirst().getRoomId(); + String memberId = votes.getFirst().getMemberId(); + initiate(roomId, memberId); + saveVotePort.saveAllByMember(votes); + for (Vote vote : votes) { + saveVotePort.saveByCandidate(vote); + } + } + + private void initiate(String roomId, String memberId) { + if (loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { + List votes = loadVotePort.findAllByRoomIdAndMemberId(roomId, memberId); + votes.forEach(deleteVotePort::deleteByCandidate); + deleteVotePort.deleteAllByRoomIdAndMemberId(roomId, memberId); + } + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java index a870c386..0f083958 100644 --- a/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/station/application/service/StationServiceTest.java @@ -9,20 +9,13 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.cache.CacheManager; class StationServiceTest extends ServiceTest { - private static final String RECOMMEND_STATIONS_CACHE_KEY = "recommendStations"; - @Autowired private StationRepository stationRepository; @Autowired private StationService stationService; - @Autowired - @Qualifier("stationCacheManager") - private CacheManager cacheManager; @DisplayName("์ €์žฅ๋œ ์ง€ํ•˜์ฒ  ์ •๋ณด๊ฐ€ ์—†๋‹ค๋ฉด, ์ง€ํ•˜์ฒ  ์ •๋ณด(์ด 2๊ฐœ)๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ์ €์žฅํ•œ๋‹ค.") @Test diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java index a8e4141a..933750be 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java @@ -8,8 +8,8 @@ import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.domain.vo.VoteStatus; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.StringJoiner; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -17,46 +17,122 @@ class VoteCommandRedisAdapterTest extends RepositoryTest { - private static final String VOTES_KEY = "vote:"; - private static final String CANDIDATE_KEY = "candidate:"; + private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; + private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; @Autowired private VoteCommandRedisAdapter voteCommandRedisAdapter; @Autowired private RedisTemplate redisTemplate; + @DisplayName("ํ›„๋ณด๋ณ„๋กœ ์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค.") + @Test + void saveVoteByCandidate() { + // given + String existingMemberId = "memberId2"; + String memberId = "memberId"; + Candidate candidate = new Candidate("roomId", 1); + Vote vote = new Vote(candidate, memberId, VoteStatus.AGREE); + String key = getCandidateVoteKey(vote); + redisTemplate.opsForSet().add(key, existingMemberId); + + // when + voteCommandRedisAdapter.saveByCandidate(vote); + + // then + Set memberIds = redisTemplate.opsForSet().members(key); + assertThat(memberIds).containsExactlyInAnyOrder(existingMemberId, memberId); + } + @DisplayName("์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•œ๋‹ค.") @Test - void saveAllVotesOfMember() { + void saveAllByCandidateVotesOfMember() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + Candidate candidate = new Candidate(roomId, 1); + Candidate candidate2 = new Candidate(roomId, 2); + Candidate candidate3 = new Candidate(roomId, 3); + List votes = List.of( + new Vote(candidate, memberId, VoteStatus.AGREE), + new Vote(candidate2, memberId, VoteStatus.DISAGREE), + new Vote(candidate3, memberId, VoteStatus.DISAGREE) + ); + + // when + voteCommandRedisAdapter.saveAllByMember(votes); + + // then + Map result = redisTemplate.opsForHash() + .entries(getMemberVoteKey(roomId, memberId)); + assertThat(result).containsAllEntriesOf( + Map.of( + votes.get(0).getStationId(), votes.get(0).getVoteStatus().getName(), + votes.get(1).getStationId(), votes.get(1).getVoteStatus().getName(), + votes.get(2).getStationId(), votes.get(2).getVoteStatus().getName() + ) + ); + } + + @DisplayName("ํ›„๋ณด๋ณ„๋กœ ์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์‚ญ์ œํ•œ๋‹ค.") + @Test + void deleteVoteByCandidate() { + // given + String memberId = "memberId"; + String memberId2 = "memberId2"; + Candidate candidate = new Candidate("roomId", 1); + Vote vote = new Vote(candidate, memberId, VoteStatus.AGREE); + String key = getCandidateVoteKey(vote); + redisTemplate.opsForSet().add(key, memberId, memberId2); + + // when + voteCommandRedisAdapter.deleteByCandidate(vote); + + // then + Set memberIds = redisTemplate.opsForSet().members(key); + assertThat(memberIds).containsExactlyInAnyOrder(memberId2); + } + + @DisplayName("์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์‚ญ์ œํ•œ๋‹ค.") + @Test + void deleteAllByRoomIdAndMemberId() { // given String roomId = "roomId"; - long stationId = 1; - Candidate candidate = new Candidate(roomId, stationId); + String memberId = "memberId"; + Candidate candidate = new Candidate(roomId, 1); + Candidate candidate2 = new Candidate(roomId, 2); + Candidate candidate3 = new Candidate(roomId, 3); + String key = getMemberVoteKey(roomId, memberId); List votes = List.of( - new Vote(candidate, "memberId1", VoteStatus.AGREE), - new Vote(candidate, "memberId2", VoteStatus.DISAGREE), - new Vote(candidate, "memberId3", VoteStatus.DISAGREE) + new Vote(candidate, memberId, VoteStatus.AGREE), + new Vote(candidate2, memberId, VoteStatus.DISAGREE), + new Vote(candidate3, memberId, VoteStatus.DISAGREE) ); + redisTemplate.opsForHash().putAll(key, Map.of( + votes.get(0).getStationId(), votes.get(0).getVoteStatus().getName(), + votes.get(1).getStationId(), votes.get(1).getVoteStatus().getName(), + votes.get(2).getStationId(), votes.get(2).getVoteStatus().getName() + )); + Long before = redisTemplate.opsForHash().size(key); // when - voteCommandRedisAdapter.saveAll(votes); + voteCommandRedisAdapter.deleteAllByRoomIdAndMemberId(roomId, memberId); // then - Set agreeResult = redisTemplate.opsForSet() - .members(getKey(roomId, stationId, VoteStatus.AGREE)); - Set disagreeResult = redisTemplate.opsForSet() - .members(getKey(roomId, stationId, VoteStatus.DISAGREE)); + Long after = redisTemplate.opsForHash().size(key); assertAll( - () -> assertThat(agreeResult).containsExactlyInAnyOrder("memberId1"), - () -> assertThat(disagreeResult).containsExactlyInAnyOrder("memberId2", "memberId3") + () -> assertThat(before).isEqualTo(3), + () -> assertThat(after).isEqualTo(0), + () -> assertThat(redisTemplate.hasKey(key)).isFalse() ); } - private String getKey(String roomId, long stationId, VoteStatus voteStatus) { - StringJoiner joiner = new StringJoiner(":"); - joiner.add(VOTES_KEY + roomId); - joiner.add(CANDIDATE_KEY + stationId); - joiner.add(voteStatus.getName()); - return joiner.toString(); + private String getCandidateVoteKey(Vote vote) { + return String.format(CANDIDATE_VOTE_KEY_FORMAT, vote.getRoomId(), vote.getStationId(), + vote.getVoteStatus().getName()); + } + + private String getMemberVoteKey(String roomId, String memberId) { + return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java new file mode 100644 index 00000000..e0270d00 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java @@ -0,0 +1,75 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kok.kokapi.common.template.RepositoryTest; +import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +class VoteQueryRedisAdapterTest extends RepositoryTest { + + private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; + + @Autowired + private VoteQueryRedisAdapter voteQueryRedisAdapter; + @Autowired + private RedisTemplate redisTemplate; + + @DisplayName("roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ํˆฌํ‘œ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.") + @Test + void isExistsByRoomIdAndMemberId() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + String key = getMemberVoteKey(roomId, memberId); + redisTemplate.opsForHash().putAll(key, Map.of(1L, VoteStatus.AGREE.getName())); + + // when + boolean result = voteQueryRedisAdapter.isExistsByRoomIdAndMemberId(roomId, memberId); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("ํ•ด๋‹น roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ํˆฌํ‘œ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isNotExistsByRoomIdAndMemberId() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + + // when + boolean result = voteQueryRedisAdapter.isExistsByRoomIdAndMemberId(roomId, memberId); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ๋ชจ๋“  ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค.") + @Test + void findAllByRoomIdAndMemberId() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + String key = getMemberVoteKey(roomId, memberId); + Vote vote = new Vote(roomId, 1L, memberId, VoteStatus.AGREE.getName()); + redisTemplate.opsForHash() + .putAll(key, Map.of(vote.getStationId(), vote.getVoteStatus().getName())); + + // when + List votes = voteQueryRedisAdapter.findAllByRoomIdAndMemberId(roomId, memberId); + + // then + assertThat(votes).containsExactlyInAnyOrder(vote); + } + + private String getMemberVoteKey(String roomId, String memberId) { + return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java new file mode 100644 index 00000000..000cd627 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java @@ -0,0 +1,104 @@ +package com.kok.kokapi.vote.application.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.kok.kokapi.common.template.ServiceTest; +import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +class VoteServiceTest extends ServiceTest { + + private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; + private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; + + @Autowired + private VoteService voteService; + @Autowired + private RedisTemplate redisTemplate; + + @DisplayName("์‚ฌ์šฉ์ž ํˆฌํ‘œ ์ •๋ณด๋ฅผ ํ›„๋ณด๋ณ„/์‚ฌ์šฉ์ž๋ณ„๋กœ ๋ชจ๋‘ ์ €์žฅํ•œ๋‹ค.") + @Test + void saveVotes() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + List votes = List.of( + new Vote(new Candidate(roomId, 1), memberId, VoteStatus.AGREE), + new Vote(new Candidate(roomId, 2), memberId, VoteStatus.DISAGREE) + ); + String memberKey = getMemberVoteKey(roomId, memberId); + String agreeCandidateKey = getCandidateVoteKey(roomId, 1, VoteStatus.AGREE); + String disagreeCandidateKey = getCandidateVoteKey(roomId, 2, VoteStatus.DISAGREE); + + // when + voteService.saveVotes(votes); + + // then + Map storedVotes = redisTemplate.opsForHash().entries(memberKey); + Set agreeMemberIds = redisTemplate.opsForSet().members(agreeCandidateKey); + Set disagreeMemberIds = redisTemplate.opsForSet().members(disagreeCandidateKey); + + assertAll( + () -> assertThat(storedVotes).containsEntry(1L, VoteStatus.AGREE.getName()), + () -> assertThat(storedVotes).containsEntry(2L, VoteStatus.DISAGREE.getName()), + () -> assertThat(agreeMemberIds).containsExactlyInAnyOrder(memberId), + () -> assertThat(disagreeMemberIds).containsExactlyInAnyOrder(memberId) + ); + } + + @DisplayName("์‚ฌ์šฉ์ž ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์ „์— ์ด์ „ ํˆฌํ‘œ ๋‚ด์—ญ์„ ์‚ญ์ œํ•œ๋‹ค.") + @Test + void initiateBeforeSaveVote() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + Candidate candidate = new Candidate(roomId, 1); + Candidate candidate2 = new Candidate(roomId, 2); + List votes = List.of( + new Vote(candidate, memberId, VoteStatus.AGREE), + new Vote(candidate2, memberId, VoteStatus.DISAGREE) + ); + voteService.saveVotes(votes); + List newVotes = List.of( + new Vote(candidate, memberId, VoteStatus.DISAGREE), + new Vote(candidate2, memberId, VoteStatus.DISAGREE) + ); + + // when + voteService.saveVotes(newVotes); + + // then + Long storedVoteCount = redisTemplate.opsForHash() + .size(getMemberVoteKey(roomId, memberId)); + Long agreeCountForStation1 = redisTemplate.opsForSet() + .size(getCandidateVoteKey(roomId, 1, VoteStatus.AGREE)); + Long disagreeCountForStation1 = redisTemplate.opsForSet() + .size(getCandidateVoteKey(roomId, 1, VoteStatus.DISAGREE)); + Long disagreeCountForStation2 = redisTemplate.opsForSet() + .size(getCandidateVoteKey(roomId, 2, VoteStatus.DISAGREE)); + assertAll( + () -> assertThat(storedVoteCount).isEqualTo(2), + () -> assertThat(agreeCountForStation1).isZero(), + () -> assertThat(disagreeCountForStation1).isEqualTo(1), + () -> assertThat(disagreeCountForStation2).isEqualTo(1) + ); + } + + private String getMemberVoteKey(String roomId, String memberId) { + return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); + } + + private String getCandidateVoteKey(String roomId, long stationId, VoteStatus status) { + return String.format(CANDIDATE_VOTE_KEY_FORMAT, roomId, stationId, status.getName()); + } +} + diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java deleted file mode 100644 index a247168f..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveVotePort.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokcore.vote.application.port.out; - -import com.kok.kokcore.vote.domain.Vote; -import java.util.List; - -public interface SaveVotePort { - - void saveAll(List votes); - -} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java index 25766f9c..3d4cc911 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java @@ -1,14 +1,16 @@ package com.kok.kokcore.vote.domain; import com.kok.kokcore.vote.domain.vo.VoteStatus; +import lombok.EqualsAndHashCode; import lombok.Getter; @Getter +@EqualsAndHashCode public class Vote { - private Candidate candidate; - private String memberId; - private VoteStatus voteStatus; + private final Candidate candidate; + private final String memberId; + private final VoteStatus voteStatus; public Vote(Candidate candidate, String memberId, VoteStatus voteStatus) { this.candidate = candidate; @@ -20,12 +22,8 @@ public Vote(String roomId, String memberId, long stationId) { this(new Candidate(roomId, stationId), memberId, VoteStatus.DISAGREE); } - public void agree() { - this.voteStatus = VoteStatus.AGREE; - } - - public void disagree() { - this.voteStatus = VoteStatus.DISAGREE; + public Vote(String roomId, Long stationId, String memberId, String voteStatus) { + this(new Candidate(roomId, stationId), memberId, VoteStatus.findByName(voteStatus)); } public String getRoomId() { diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java index 9dc2bd30..0af846b0 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java @@ -1,5 +1,6 @@ package com.kok.kokcore.vote.domain.vo; +import java.util.Arrays; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -13,11 +14,10 @@ public enum VoteStatus { private final String name; - public boolean isAgree() { - return this.equals(AGREE); - } - - public boolean isDisagree() { - return this.equals(DISAGREE); + public static VoteStatus findByName(String name) { + return Arrays.stream(values()) + .filter(voteStatus -> voteStatus.getName().equalsIgnoreCase(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No status with name: " + name)); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java new file mode 100644 index 00000000..9609c8fd --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java @@ -0,0 +1,10 @@ +package com.kok.kokcore.vote.port.out; + +import com.kok.kokcore.vote.domain.Vote; + +public interface DeleteVotePort { + + void deleteByCandidate(Vote vote); + + void deleteAllByRoomIdAndMemberId(String roomId, String memberId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/LoadCandidatePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadCandidatePort.java similarity index 80% rename from kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/LoadCandidatePort.java rename to kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadCandidatePort.java index 9fd49858..437a6971 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/LoadCandidatePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadCandidatePort.java @@ -1,4 +1,4 @@ -package com.kok.kokcore.vote.application.port.out; +package com.kok.kokcore.vote.port.out; import com.kok.kokcore.vote.domain.Candidate; import java.util.List; diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java new file mode 100644 index 00000000..eb482d54 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java @@ -0,0 +1,11 @@ +package com.kok.kokcore.vote.port.out; + +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; + +public interface LoadVotePort { + + boolean isExistsByRoomIdAndMemberId(String roomId, String memberId); + + List findAllByRoomIdAndMemberId(String roomId, String memberId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveCandidatePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveCandidatePort.java similarity index 75% rename from kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveCandidatePort.java rename to kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveCandidatePort.java index 7a9db073..8be0c0ea 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/application/port/out/SaveCandidatePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveCandidatePort.java @@ -1,8 +1,9 @@ -package com.kok.kokcore.vote.application.port.out; +package com.kok.kokcore.vote.port.out; import com.kok.kokcore.vote.domain.Candidate; import java.util.List; public interface SaveCandidatePort { + void saveAll(List candidates); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java new file mode 100644 index 00000000..11ae5a85 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java @@ -0,0 +1,11 @@ +package com.kok.kokcore.vote.port.out; + +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; + +public interface SaveVotePort { + + void saveByCandidate(Vote vote); + + void saveAllByMember(List votes); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java index d0f4497c..bb64b049 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java @@ -6,5 +6,7 @@ public interface GetCandidateUseCase { - List getCandidate(String roomId, List stations); + List saveAndGetCandidates(String roomId, List stations); + + List getCandidates(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java new file mode 100644 index 00000000..1bc14057 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.vote.usecase; + +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; + +public interface SaveVoteUseCase { + + void saveVotes(List votes); +} From 62957a51b7375b3b6037ac439748cf5202f4e8a7 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 29 Mar 2025 16:48:20 +0900 Subject: [PATCH 116/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20implement?= =?UTF-8?q?=20getting=20members=20vote=20status=20API=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: create domain for voting * :white_check_mark: test: create RedisDatabaseCleaner And change RepositoryTest into SpringBootTest for testing redis * :wrench: config: fix ddl type error * :sparkles: feat: implement port for saving candidate * :sparkles: feat: implement port for saving votes * :sparkles: feat: implement port for loading candidates * :sparkles: feat: implement checking if candidates exists * :sparkles: feat: implement getting recommended stations from cache * :recycle: refactor: move getting recommended stations from adapter to service * :sparkle: feat: implement get candidate usecase * :sparkle: feat: implement get candidate Facade service and Controller * :sparkle: feat: add updating route names into "1ํ˜ธ์„ " * :sparkle: feat: save votes by member * :white_check_mark: test: test for saving votes by candidate * :sparkles: feat: implement saving vote usecase * :sparkle: feat: implement deleting cache of recommended Stations * :sparkle: feat: delete cached recommended stations after saving candidates * :sparkle: feat: implement creating Vote Controller and facade service * :sparkle: feat: implement loadVotePort checking if votes exists by member * :recycle: refactor: refactor key-format for candidate * :sparkles: feat: implement delete vote port deleting votes by candidate * :sparkles: feat: implement delete vote port deleting all votes by roomId and memberId * :recycle: refactor: rename key constant * :sparkles: feat: implement load vote port finding all votes by memberId And roomId * :sparkles: feat: add logic deleting previous vote data on saveVoteUsecase * :recycle: refactor: merge conflict * :recycle: refactor: move package for port * :sparkles: feat: implement vote controller for getting members with vote status * :sparkles: feat: implement get vote usecase checking if member voted * :recycle: refactor: rename method * :recycle: refactor: clear automatically when bulk insert * :sparkles: feat: add roomId validation * :recycle: refactor: move finding members by room id logic from service to port --------- Co-authored-by: YUN YOUNG --- .../RoomParticipantQueryRedisAdapter.java | 23 ++++++++++ .../persistence/RoomQueryRedisAdapter.java | 6 +++ .../application/service/RoomQueryService.java | 27 ++++-------- .../out/persistence/RouteRepository.java | 5 ++- ...erRecommendStationCommandRedisAdapter.java | 3 +- ...UserRecommendStationQueryRedisAdapter.java | 3 +- .../response/MemberVoteStatusResponse.java | 20 +++++++++ .../vote/adapter/in/web/VoteController.java | 9 ++++ .../service/VoteFacadeService.java | 17 ++++++++ .../vote/application/service/VoteService.java | 8 +++- .../application/service/VoteServiceTest.java | 42 ++++++++++++++++++- .../port/out/LoadRoomParticipantPort.java | 5 +++ .../kokcore/room/port/out/LoadRoomPort.java | 2 + .../DeleteRecommendStationUseCase.java | 6 +++ .../usecase/GetRecommendStationUseCase.java | 9 ++++ .../vote/usecase/CreateVoteUseCase.java | 10 ----- .../kokcore/vote/usecase/GetVoteUseCase.java | 6 +++ 17 files changed, 165 insertions(+), 36 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/usecase/DeleteRecommendStationUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/station/usecase/GetRecommendStationUseCase.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java index 00b8010c..0d4cb0bb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java @@ -1,6 +1,11 @@ package com.kok.kokapi.room.adapter.out.persistence; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; @@ -12,6 +17,7 @@ public class RoomParticipantQueryRedisAdapter implements LoadRoomParticipantPort public static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; @Override public Long countParticipantsById(String roomId) { @@ -22,6 +28,23 @@ public Long countParticipantsById(String roomId) { return redisTemplate.opsForList().size(key); } + @Override + public List findMembersByRoomId(String roomId) { + String key = PARTICIPANT_KEY_PREFIX + roomId; + List memberJson = redisTemplate.opsForList().range(key, 0, -1); + List members = new ArrayList<>(); + if (memberJson != null) { + for (String data : memberJson) { + try { + Member member = objectMapper.readValue(data, Member.class); + members.add(member); + } catch (JsonProcessingException ignored) { + } + } + } + return members; + } + private String buildKey(String roomId) { return PARTICIPANT_KEY_PREFIX + roomId; } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java index d0d6b786..fcc61fa9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomQueryRedisAdapter.java @@ -24,6 +24,12 @@ public Optional findRoomById(String roomId) { .flatMap(this::deserializeRoom); } + @Override + public boolean isExistsByRoomId(String roomId) { + String key = buildKey(roomId); + return redisTemplate.hasKey(key); + } + private Optional deserializeRoom(String roomJson) { try { Room room = objectMapper.readValue(roomJson, Room.class); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index 4834c775..ac31788d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -1,28 +1,20 @@ package com.kok.kokapi.room.application.service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; import com.kok.kokcore.room.port.out.LoadRoomPort; import com.kok.kokcore.room.usecase.GetRoomUseCase; -import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class RoomQueryService implements GetRoomUseCase { - private static final String PARTICIPANT_KEY_PREFIX = "room:participants:"; - private final LoadRoomPort loadRoomPort; private final LoadRoomParticipantPort loadRoomParticipantPort; - private final RedisTemplate redisTemplate; - private final ObjectMapper objectMapper; @Override public Room findRoomById(String roomId) { @@ -32,19 +24,14 @@ public Room findRoomById(String roomId) { @Override public List getParticipants(String roomId) { - String key = PARTICIPANT_KEY_PREFIX + roomId; - List memberJson = redisTemplate.opsForList().range(key, 0, -1); - List members = new ArrayList<>(); - if (memberJson != null) { - for (String data : memberJson) { - try { - Member member = objectMapper.readValue(data, Member.class); - members.add(member); - } catch (JsonProcessingException ignored) { - } - } + validate(roomId); + return loadRoomParticipantPort.findMembersByRoomId(roomId); + } + + private void validate(String roomId) { + if (!loadRoomPort.isExistsByRoomId(roomId)) { + throw new IllegalArgumentException("Room not found with id: " + roomId); } - return members; } @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java index 7c048cbc..a758434e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java @@ -6,12 +6,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface RouteRepository extends JpaRepository { List findAllByStationOrderByName(Station station); - @Modifying + @Modifying(clearAutomatically = true) @Query("UPDATE Route r SET r.name = '1ํ˜ธ์„ ' WHERE r.name IN :names") - int updateRouteNameToRouteOne(List names); + int updateRouteNameToRouteOne(@Param("names") List names); } diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java index 586ae423..51d1acac 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java @@ -15,7 +15,8 @@ @Repository public class UserRecommendStationCommandRedisAdapter implements SaveUserRecommendStationsPort { - private final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStation:"; + private static final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStation:"; + private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationQueryRedisAdapter.java index 53d97716..c906bb85 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationQueryRedisAdapter.java @@ -15,7 +15,8 @@ @RequiredArgsConstructor public class UserRecommendStationQueryRedisAdapter implements ReadUserRecommendStationsPort { - private final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStations:"; + private static final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStations:"; + private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java new file mode 100644 index 00000000..24cce19f --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java @@ -0,0 +1,20 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Member; + +public record MemberVoteStatusResponse( + String memberId, + String nickname, + String imageUrl, + boolean isVoted +) { + + public static MemberVoteStatusResponse of(Member member, boolean isVoted) { + return new MemberVoteStatusResponse( + member.getMemberId(), + member.getNickname(), + member.getProfile(), + isVoted + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 8b927292..1f08e251 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -5,6 +5,7 @@ import com.kok.kokapi.station.application.service.StationFacadeService; import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; import com.kok.kokapi.vote.application.service.VoteFacadeService; import com.kok.kokcore.station.domain.entity.Station; import io.swagger.v3.oas.annotations.Operation; @@ -43,4 +44,12 @@ public ResponseEntity> createVote( voteFacadeService.createVote(roomId, memberId, voteRequest); return ResponseEntity.ok(ApiResponseDto.success(null)); } + + @Operation(summary = "์‚ฌ์šฉ์ž๋ณ„ ํˆฌํ‘œ ์ƒํƒœ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด์™€ ํˆฌํ‘œ ์ƒํƒœ(ํˆฌํ‘œ ์ „/ํˆฌํ‘œ ์™„๋ฃŒ)๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/vote/{roomId}/status") + public ResponseEntity>> getMemberVoteStatus( + @PathVariable String roomId) { + List responses = voteFacadeService.getMemberVoteStatus(roomId); + return ResponseEntity.ok(ApiResponseDto.success(responses)); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index f2124e88..285092ca 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -6,6 +6,9 @@ import com.kok.kokapi.public_transportation.application.service.TmapPublicTransportationService; import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.usecase.GetStationUseCase; @@ -14,6 +17,7 @@ import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.domain.vo.VoteStatus; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; +import com.kok.kokcore.vote.usecase.GetVoteUseCase; import com.kok.kokcore.vote.usecase.SaveVoteUseCase; import java.util.ArrayList; import java.util.List; @@ -28,6 +32,8 @@ public class VoteFacadeService { private final GetStationUseCase getStationUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; private final SaveVoteUseCase saveVoteUseCase; + private final GetVoteUseCase getVoteUseCase; + private final GetRoomUseCase getRoomUseCase; private final TmapPublicTransportationService tmapPublicTransportationService; private final ObjectMapper objectMapper; @@ -85,4 +91,15 @@ private List getVotes(String roomId, String memberId, VoteRequest voteRequ private static boolean isAgree(List agreedStationIds, Candidate candidate) { return agreedStationIds.contains(candidate.getStationId()); } + + public List getMemberVoteStatus(String roomId) { + List members = getRoomUseCase.getParticipants(roomId); + List responses = new ArrayList<>(); + for (Member member : members) { + boolean isVoted = getVoteUseCase.isVotedByMember(roomId, member.getMemberId()); + MemberVoteStatusResponse response = MemberVoteStatusResponse.of(member, isVoted); + responses.add(response); + } + return responses; + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java index 4d824809..e4f4c9b6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -4,6 +4,7 @@ import com.kok.kokcore.vote.port.out.DeleteVotePort; import com.kok.kokcore.vote.port.out.LoadVotePort; import com.kok.kokcore.vote.port.out.SaveVotePort; +import com.kok.kokcore.vote.usecase.GetVoteUseCase; import com.kok.kokcore.vote.usecase.SaveVoteUseCase; import java.util.List; import lombok.RequiredArgsConstructor; @@ -11,7 +12,7 @@ @Service @RequiredArgsConstructor -public class VoteService implements SaveVoteUseCase { +public class VoteService implements SaveVoteUseCase, GetVoteUseCase { private final SaveVotePort saveVotePort; private final LoadVotePort loadVotePort; @@ -35,4 +36,9 @@ private void initiate(String roomId, String memberId) { deleteVotePort.deleteAllByRoomIdAndMemberId(roomId, memberId); } } + + @Override + public boolean isVotedByMember(String roomId, String memberId) { + return loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java index 000cd627..cdeaf3e4 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java @@ -7,6 +7,7 @@ import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.domain.vo.VoteStatus; +import com.kok.kokcore.vote.port.out.SaveVotePort; import java.util.List; import java.util.Map; import java.util.Set; @@ -23,6 +24,8 @@ class VoteServiceTest extends ServiceTest { @Autowired private VoteService voteService; @Autowired + private SaveVotePort saveVotePort; + @Autowired private RedisTemplate redisTemplate; @DisplayName("์‚ฌ์šฉ์ž ํˆฌํ‘œ ์ •๋ณด๋ฅผ ํ›„๋ณด๋ณ„/์‚ฌ์šฉ์ž๋ณ„๋กœ ๋ชจ๋‘ ์ €์žฅํ•œ๋‹ค.") @@ -67,7 +70,10 @@ void initiateBeforeSaveVote() { new Vote(candidate, memberId, VoteStatus.AGREE), new Vote(candidate2, memberId, VoteStatus.DISAGREE) ); - voteService.saveVotes(votes); + saveVotePort.saveAllByMember(votes); + for (Vote vote : votes) { + saveVotePort.saveByCandidate(vote); + } List newVotes = List.of( new Vote(candidate, memberId, VoteStatus.DISAGREE), new Vote(candidate2, memberId, VoteStatus.DISAGREE) @@ -93,6 +99,40 @@ void initiateBeforeSaveVote() { ); } + @DisplayName("์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œ๋ฅผ ์™„๋ฃŒํ–ˆ์œผ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isVotedByMember() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + Candidate candidate = new Candidate(roomId, 1); + List votes = List.of(new Vote(candidate, memberId, VoteStatus.AGREE)); + saveVotePort.saveAllByMember(votes); + for (Vote vote : votes) { + saveVotePort.saveByCandidate(vote); + } + + // when + boolean result = voteService.isVotedByMember(roomId, memberId); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œ๋ฅผ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isNotVotedByMember() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + + // when + boolean result = voteService.isVotedByMember(roomId, memberId); + + // then + assertThat(result).isFalse(); + } + private String getMemberVoteKey(String roomId, String memberId) { return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java index 543a0007..6d3d404d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java @@ -1,6 +1,11 @@ package com.kok.kokcore.room.port.out; +import com.kok.kokcore.room.domain.Member; +import java.util.List; + public interface LoadRoomParticipantPort { Long countParticipantsById(String roomId); + + List findMembersByRoomId(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java index f9a5473a..5ecdf9ad 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomPort.java @@ -6,4 +6,6 @@ public interface LoadRoomPort { Optional findRoomById(String roomId); + + boolean isExistsByRoomId(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/DeleteRecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/DeleteRecommendStationUseCase.java new file mode 100644 index 00000000..c0ecfa88 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/DeleteRecommendStationUseCase.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.station.usecase; + +public interface DeleteRecommendStationUseCase { + + void deleteRecommendedStations(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/station/usecase/GetRecommendStationUseCase.java b/kok-core/src/main/java/com/kok/kokcore/station/usecase/GetRecommendStationUseCase.java new file mode 100644 index 00000000..6594279c --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/station/usecase/GetRecommendStationUseCase.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.station.usecase; + +import com.kok.kokcore.station.domain.entity.Station; +import java.util.List; + +public interface GetRecommendStationUseCase { + + List getRecommendedStations(String roomId); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java deleted file mode 100644 index ab68a071..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/CreateVoteUseCase.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.kok.kokcore.vote.usecase; - -import com.kok.kokcore.vote.domain.Candidate; -import com.kok.kokcore.vote.domain.Vote; -import java.util.List; - -public interface CreateVoteUseCase { - - void createVotes(List candidates, List votes); -} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java new file mode 100644 index 00000000..2be528e6 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java @@ -0,0 +1,6 @@ +package com.kok.kokcore.vote.usecase; + +public interface GetVoteUseCase { + + boolean isVotedByMember(String roomId, String memberId); +} From 9e11a76c6ae61d46e38e8fd1132270516b70f7b2 Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Sat, 29 Mar 2025 17:09:51 +0900 Subject: [PATCH 117/163] :sparkles: [Feature/location] add member profile image field to LocationResponse (#83) * :sparkles: feat: add imageUrl field LocationResponse * :sparkles: feat: add imageUrl field LocationResponse * :bug: fix: resolve conflict --- .../adapter/in/dto/response/LocationResponse.java | 11 ++++++++++- .../adapter/in/web/LocationController.java | 8 +++++++- .../adapter/out/mapper/LocationMapper.java | 14 ++++++++++++++ .../room/application/service/RoomQueryService.java | 8 ++++++++ .../kok/kokcore/room/usecase/GetRoomUseCase.java | 2 ++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java index aaecd455..5f9bedc9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/dto/response/LocationResponse.java @@ -6,6 +6,7 @@ public record LocationResponse( String roomId, String memberId, + String imageUrl, BigDecimal latitude, BigDecimal longitude, String name @@ -13,7 +14,15 @@ public record LocationResponse( public static LocationResponse of(String roomId, String memberId, BigDecimal latitude, BigDecimal longitude, String name) { - return new LocationResponse(roomId, memberId, + return new LocationResponse(roomId, memberId, "", + latitude.setScale(6, RoundingMode.HALF_UP), + longitude.setScale(6, RoundingMode.HALF_UP), + name); + } + + public static LocationResponse of(String roomId, String memberId, String imageUrl, BigDecimal latitude, + BigDecimal longitude, String name) { + return new LocationResponse(roomId, memberId, imageUrl, latitude.setScale(6, RoundingMode.HALF_UP), longitude.setScale(6, RoundingMode.HALF_UP), name); diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index fad852d6..fef72f7d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -11,6 +11,8 @@ import com.kok.kokcore.location.usecase.CreateLocationUseCase; import com.kok.kokcore.location.usecase.LoadCentroidUseCase; import com.kok.kokcore.location.usecase.ReadLocationUseCase; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.usecase.GetRoomUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import java.math.BigDecimal; @@ -31,6 +33,7 @@ public class LocationController { private final CreateLocationUseCase createLocationUsecase; private final LoadCentroidUseCase loadCentroidUsecase; private final ReadLocationUseCase readLocationUsecase; + private final GetRoomUseCase getRoomUseCase; private final LocationMapper locationMapper; @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") @@ -70,8 +73,11 @@ public ResponseEntity> getCentroid( public ResponseEntity> getLocation(@PathVariable String roomId, @PathVariable String memberId) { Location location = readLocationUsecase.readLocation(roomId, memberId); + Member member = getRoomUseCase.getParticipant(roomId, memberId); - return ResponseEntity.ok(ApiResponseDto.success(locationMapper.toResponse(location))); + return ResponseEntity.ok(ApiResponseDto.success( + locationMapper.toResponse(location, member) + )); } @Operation(summary = "์œ„์น˜์กฐํšŒ ConvexHull", description = "Retrieve the ConvexHull inside list, outside list of locations for a roomId") diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java index f222e0b8..c40656ad 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/mapper/LocationMapper.java @@ -3,6 +3,7 @@ import com.kok.kokapi.centroid.adapter.in.dto.response.LocationResponse; import com.kok.kokapi.config.geometry.PointConverter; import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.room.domain.Member; import java.math.BigDecimal; import java.util.List; import org.springframework.data.util.Pair; @@ -29,6 +30,19 @@ public LocationResponse toResponse(Location location) { ); } + public LocationResponse toResponse(Location location, Member member) { + Pair coordinates = pointConverter.toCoordinates( + location.getLocation_point()); + return LocationResponse.of( + location.getRoomId(), + member.getMemberId(), + member.getProfile(), + coordinates.getFirst(), + coordinates.getSecond(), + location.getName() + ); + } + public List toResponseList(List locations) { return locations.stream().map(this::toResponse).toList(); } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index ac31788d..80560bc6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -34,6 +34,14 @@ private void validate(String roomId) { } } + @Override + public Member getParticipant(String roomId, String memberId) { + return getParticipants(roomId).stream() + .filter(member -> member.getMemberId().equals(memberId)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Member not found with id: " + memberId)); + } + @Override public int getParticipantsCount(String roomId) { Long participantCount = loadRoomParticipantPort.countParticipantsById(roomId); diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java index bfa586a5..3ca756d2 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -10,5 +10,7 @@ public interface GetRoomUseCase { List getParticipants(String roomId); + Member getParticipant(String roomId, String memberId); + int getParticipantsCount(String roomId); } From 5192973b4fb6d6441880c2854572c41628e67b31 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:28:43 +0900 Subject: [PATCH 118/163] :sparkles: feat: add isVoteMode on room detail response (#86) --- .../in/dto/response/RoomDetailResponse.java | 8 ++++--- .../room/adapter/in/web/RoomController.java | 2 +- .../service/RoomFacadeService.java | 13 ++++++++---- .../adapter/in/web/RoomIntegrationTest.java | 21 ++++++++++++------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index bf68567a..9188634b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -5,14 +5,16 @@ public record RoomDetailResponse( String id, String roomName, - int nonParticipantCount + int nonParticipantCount, + boolean isVoteMode ) { - public static RoomDetailResponse of(Room room, int participantCount) { + public static RoomDetailResponse of(Room room, int participantCount, boolean isVoteMode) { return new RoomDetailResponse( room.getId(), room.getRoomName(), - room.getCapacity() - participantCount + room.getCapacity() - participantCount, + isVoteMode ); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index d8efb62c..cde0ecf6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -41,7 +41,7 @@ public class RoomController { @GetMapping("/rooms/{roomId}") public ResponseEntity> getRoomDetail( @PathVariable String roomId) { - RoomDetailResponse response = roomFacadeService.findByRoomId(roomId); + RoomDetailResponse response = roomFacadeService.findByRoomId(roomId, LocalDateTime.now()); return ResponseEntity.ok(ApiResponseDto.success(response)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java index ff40c61e..810ea3ab 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java @@ -18,17 +18,22 @@ public class RoomFacadeService { private final GetRoomUseCase getRoomUseCase; private final ReadLocationUseCase readLocationUseCase; - public RoomDetailResponse findByRoomId(String roomId) { + public RoomDetailResponse findByRoomId(String roomId, LocalDateTime current) { Room room = getRoomUseCase.findRoomById(roomId); int participantsCount = getRoomUseCase.getParticipantsCount(roomId); - return RoomDetailResponse.of(room, participantsCount); + boolean isVoteMode = getVoteMode(roomId, current, room); + return RoomDetailResponse.of(room, participantsCount, isVoteMode); } public RoomStatusResponse getRoomStatus(String roomId, LocalDateTime current) { Room room = getRoomUseCase.findRoomById(roomId); + boolean isVoteMode = getVoteMode(roomId, current, room); + return new RoomStatusResponse(isVoteMode); + } + + private boolean getVoteMode(String roomId, LocalDateTime current, Room room) { List locations = readLocationUseCase.readLocations(roomId); int locationInputCount = locations.size(); - boolean isVoteMode = room.hasLocationInputEnded(locationInputCount, current); - return new RoomStatusResponse(isVoteMode); + return room.hasLocationInputEnded(locationInputCount, current); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java index df2be0cb..b1e057ec 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -33,15 +33,15 @@ Stream getRoomDetail() { inputLocation("๋ฐฉ์žฅ์ด ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse), - getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๋‹ค", createRoomResponse, 1), + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๋‹ค", createRoomResponse, 1, false), DynamicTest.dynamicTest("ํŒ”๋กœ์›Œ๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•œ๋‹ค.", () -> joinRoomResponse.set(joinRoom(createRoomResponse.get().id(), new JoinRoomParticipantRequest("profile", "follower")))), - getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๋‹ค.", createRoomResponse, 0), + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๋‹ค.", createRoomResponse, 0, false), - getRoomMembers("์•ฝ์†๋ฐฉ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉด isFulใ…ฃ์€ true์ด๊ณ , 2๋ช…์˜ ํ”„๋กœํ•„์ด ์žˆ๋‹ค", createRoomResponse, 2, + getRoomMembers("์•ฝ์†๋ฐฉ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉด isFull์€ true์ด๊ณ , 2๋ช…์˜ ํ”„๋กœํ•„์ด ์žˆ๋‹ค", createRoomResponse, 2, true), checkVoteMode("์•„์ง ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜๊ธฐ์— voteMode๋Š” false์ด๋‹ค.", createRoomResponse, false), @@ -90,8 +90,12 @@ private static DynamicTest inputLocation(String message, }); } - private static DynamicTest getRoomDetail(String message, - AtomicReference createRoomResponse, int nonParticipantCount) { + private static DynamicTest getRoomDetail( + String message, + AtomicReference createRoomResponse, + int nonParticipantCount, + boolean isVoteMode + ) { return DynamicTest.dynamicTest(message, () -> { String roomId = createRoomResponse.get().id(); @@ -100,7 +104,8 @@ private static DynamicTest getRoomDetail(String message, .when().get("/v1/api/rooms/" + roomId) .then().log().all() .assertThat().statusCode(200) - .body("data.nonParticipantCount", is(nonParticipantCount)); + .body("data.nonParticipantCount", is(nonParticipantCount)) + .body("data.isVoteMode", is(isVoteMode)); }); } @@ -139,7 +144,7 @@ private static DynamicTest inputLocation(String message, } private static DynamicTest checkVoteMode(String message, - AtomicReference createRoomResponse, boolean expectedIsVoteMode) { + AtomicReference createRoomResponse, boolean isVoteMode) { return DynamicTest.dynamicTest(message, () -> { String roomId = createRoomResponse.get().id(); @@ -148,7 +153,7 @@ private static DynamicTest checkVoteMode(String message, .when().get("/v1/api/rooms/" + roomId + "/status") .then().log().all() .assertThat().statusCode(200) - .body("data.isVoteMode", is(expectedIsVoteMode)); + .body("data.isVoteMode", is(isVoteMode)); }); } } From 81dbdee7adcf1401483b34da4f867f8597c91c9d Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:19:05 +0900 Subject: [PATCH 119/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20implement?= =?UTF-8?q?=20getting=20vote=20deadline=20API=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: add voteLimitDateTime and initiate on Room domain * :sparkles: feat: implement update room port * :sparkles: feat: implement updating vote limit date time when all location input registered * :sparkles: feat: implement getting vote deadline controller --- .../adapter/in/web/LocationController.java | 5 ++ .../out/persistence/RoomSaveRedisAdapter.java | 16 ++++- .../service/RoomCommandService.java | 53 +++++++++++++++++ .../service/RoomCreationService.java | 26 --------- .../in/dto/response/VoteDeadlineResponse.java | 11 ++++ .../vote/adapter/in/web/VoteController.java | 13 +++++ .../persistence/RoomSaveRedisAdapterTest.java | 58 +++++++++++++++++++ ...eTest.java => RoomCommandServiceTest.java} | 6 +- .../com/kok/kokcore/room/domain/Room.java | 9 ++- .../kokcore/room/port/out/UpdateRoomPort.java | 8 +++ .../room/usecase/UpdateRoomUseCase.java | 8 +++ .../com/kok/kokcore/room/domain/RoomTest.java | 11 +++- 12 files changed, 190 insertions(+), 34 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java create mode 100644 kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapterTest.java rename kok-api/src/test/java/com/kok/kokapi/room/application/service/{RoomCreationServiceTest.java => RoomCommandServiceTest.java} (91%) create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/port/out/UpdateRoomPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index fef72f7d..a9acdb84 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -13,9 +13,11 @@ import com.kok.kokcore.location.usecase.ReadLocationUseCase; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.usecase.GetRoomUseCase; +import com.kok.kokcore.room.usecase.UpdateRoomUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.util.Pair; @@ -34,6 +36,7 @@ public class LocationController { private final LoadCentroidUseCase loadCentroidUsecase; private final ReadLocationUseCase readLocationUsecase; private final GetRoomUseCase getRoomUseCase; + private final UpdateRoomUseCase updateRoomUseCase; private final LocationMapper locationMapper; @Operation(summary = "์œ„์น˜ ์ž…๋ ฅ", description = "Create a new location with the provided details.") @@ -51,6 +54,8 @@ public ResponseEntity> createLocation( Pair centroid = loadCentroidUsecase.readCentroidCoordinates( locationRequest.roomId()); + updateRoomUseCase.updateRoomVoteDeadline(locationRequest.roomId(), LocalDateTime.now()); + return ResponseEntity.ok(ApiResponseDto.success( CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) )); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java index 7b882e84..d41acc21 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.port.out.SaveRoomPort; +import com.kok.kokcore.room.port.out.UpdateRoomPort; import java.time.Duration; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; @@ -11,7 +12,7 @@ @Repository @RequiredArgsConstructor -public class RoomSaveRedisAdapter implements SaveRoomPort { +public class RoomSaveRedisAdapter implements SaveRoomPort, UpdateRoomPort { private static final String ROOM_KEY_PREFIX = "room"; private static final Duration ROOM_TTL = Duration.ofDays(3); @@ -31,6 +32,19 @@ public Room save(Room room) { return room; } + @Override + public void update(Room room) { + String key = buildKey(room.getId()); + try { + String roomJson = objectMapper.writeValueAsString(room); + Duration currentTtl = Duration.ofSeconds(redisTemplate.getExpire(key)); + + redisTemplate.opsForValue().set(key, roomJson, currentTtl); + } catch (JsonProcessingException e) { + throw new RuntimeException("failed to update room in Redis", e); + } + } + private String buildKey(String roomId) { return ROOM_KEY_PREFIX + ":" + roomId; } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java new file mode 100644 index 00000000..20c6a4d7 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java @@ -0,0 +1,53 @@ +package com.kok.kokapi.room.application.service; + +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.port.out.ReadLocationPort; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.LoadRoomPort; +import com.kok.kokcore.room.port.out.SaveRoomPort; +import com.kok.kokcore.room.port.out.UpdateRoomPort; +import com.kok.kokcore.room.usecase.CreateRoomUseCase; +import com.kok.kokcore.room.usecase.JoinRoomUseCase; +import com.kok.kokcore.room.usecase.UpdateRoomUseCase; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RoomCommandService implements CreateRoomUseCase, UpdateRoomUseCase { + + private final SaveRoomPort saveRoomPort; + private final LoadRoomPort loadRoomPort; + private final ReadLocationPort readLocationPort; + private final UpdateRoomPort updateRoomPort; + private final JoinRoomUseCase joinRoomUseCase; + + @Override + public Room createRoom(String roomName, int capacity, Member host) { + Room room = Room.create(roomName, capacity, host); + Room savedRoom = saveRoomPort.save(room); + + joinRoomUseCase.joinRoom(savedRoom.getId(), savedRoom.getMember()); + return savedRoom; + } + + @Override + public void updateRoomVoteDeadline(String roomId, LocalDateTime current) { + Room room = loadRoomPort.findRoomById(roomId) + .orElseThrow( + () -> new IllegalArgumentException("Cannot find room with roomId: " + roomId)); + if (shouldUpdateVoteDeadline(room, current)) { + room.updateVoteDeadline(current); + updateRoomPort.update(room); + } + } + + private boolean shouldUpdateVoteDeadline(Room room, LocalDateTime current) { + List locations = readLocationPort.findLocationsByRoomId(room.getId()); + int locationInputCount = locations.size(); + return room.hasLocationInputEnded(locationInputCount, current); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java deleted file mode 100644 index 19cba513..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCreationService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.kok.kokapi.room.application.service; - -import com.kok.kokcore.room.domain.Member; -import com.kok.kokcore.room.domain.Room; -import com.kok.kokcore.room.port.out.SaveRoomPort; -import com.kok.kokcore.room.usecase.CreateRoomUseCase; -import com.kok.kokcore.room.usecase.JoinRoomUseCase; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RoomCreationService implements CreateRoomUseCase { - - private final SaveRoomPort saveRoomPort; - private final JoinRoomUseCase joinRoomUseCase; - - @Override - public Room createRoom(String roomName, int capacity, Member host) { - Room room = Room.create(roomName, capacity, host); - Room savedRoom = saveRoomPort.save(room); - - joinRoomUseCase.joinRoom(savedRoom.getId(), savedRoom.getMember()); - return savedRoom; - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java new file mode 100644 index 00000000..70506c0b --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java @@ -0,0 +1,11 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Room; +import java.time.LocalDateTime; + +public record VoteDeadlineResponse(LocalDateTime endAt) { + + public static VoteDeadlineResponse from(Room room) { + return new VoteDeadlineResponse(room.getVoteLimitDateTime()); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 1f08e251..87b5a870 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -6,7 +6,10 @@ import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.VoteDeadlineResponse; import com.kok.kokapi.vote.application.service.VoteFacadeService; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.station.domain.entity.Station; import io.swagger.v3.oas.annotations.Operation; import java.util.List; @@ -23,6 +26,7 @@ public class VoteController { private final VoteFacadeService voteFacadeService; private final StationFacadeService stationFacadeService; + private final GetRoomUseCase getRoomUseCase; @Operation(summary = "ํˆฌํ‘œ ํ›„๋ณด์ง€ ๋ชฉ๋ก ์กฐํšŒ", description = "๋ฐฉ ID๊ณผ ์‚ฌ์šฉ์ž ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํˆฌํ‘œ ํ›„๋ณด์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/{memberId}/candidates") @@ -52,4 +56,13 @@ public ResponseEntity>> getMemberV List responses = voteFacadeService.getMemberVoteStatus(roomId); return ResponseEntity.ok(ApiResponseDto.success(responses)); } + + @Operation(summary = "ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•œ ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/vote/{roomId}/deadline") + public ResponseEntity> getVoteDeadline( + @PathVariable String roomId) { + Room room = getRoomUseCase.findRoomById(roomId); + VoteDeadlineResponse response = VoteDeadlineResponse.from(room); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapterTest.java new file mode 100644 index 00000000..ee49b3cd --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapterTest.java @@ -0,0 +1,58 @@ +package com.kok.kokapi.room.adapter.out.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokapi.common.template.RepositoryTest; +import com.kok.kokapi.fixture.MemberFixture; +import com.kok.kokapi.fixture.RoomFixture; +import com.kok.kokcore.room.domain.Room; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +class RoomSaveRedisAdapterTest extends RepositoryTest { + + private static final String ROOM_KEY_PREFIX = "room"; + + @Autowired + private RoomSaveRedisAdapter roomSaveRedisAdapter; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private ObjectMapper objectMapper; + + @Test + void updateRoom() throws Exception { + // given + Room room = RoomFixture.create(3, MemberFixture.createLeader()); + String key = buildKey(room.getId()); + roomSaveRedisAdapter.save(room); + Long ttlBefore = redisTemplate.getExpire(key); + LocalDateTime voteDeadlineBefore = room.getVoteLimitDateTime(); + + Thread.sleep(1000); + + // when + room.updateVoteDeadline(LocalDateTime.now()); + roomSaveRedisAdapter.update(room); + + // then + String result = redisTemplate.opsForValue().get(key); + Long ttlAfter = redisTemplate.getExpire(key); + LocalDateTime voteDeadlineAfter = objectMapper.readValue(result, Room.class) + .getVoteLimitDateTime(); + + assertAll( + () -> assertThat(ttlAfter).isGreaterThan(0L), + () -> assertThat(ttlAfter).isLessThan(ttlBefore), + () -> assertThat(voteDeadlineAfter).isBefore(voteDeadlineBefore) + ); + } + + private String buildKey(String roomId) { + return ROOM_KEY_PREFIX + ":" + roomId; + } +} diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java similarity index 91% rename from kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java rename to kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java index 63894505..df685e73 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCreationServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java @@ -12,10 +12,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -class RoomCreationServiceTest extends ServiceTest { +class RoomCommandServiceTest extends ServiceTest { @Autowired - private RoomCreationService roomCreationService; + private RoomCommandService roomCommandService; @DisplayName("์•ฝ์†๋ฐฉ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @Test @@ -26,7 +26,7 @@ void createRoom() { String hostProfile = "hostProfile"; Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); - Room createdRoom = roomCreationService.createRoom(roomName, capacity, host); + Room createdRoom = roomCommandService.createRoom(roomName, capacity, host); assertAll("Room Create Test", () -> assertNotNull(createdRoom, "Room ๊ฐ์ฒด๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 6a1007de..482b8151 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -14,13 +14,15 @@ public class Room implements Serializable { private static final int REQUIRED_CAPACITY = 2; private static final long LOCATION_INPUT_TIME_LIMIT = 6; + private static final long VOTE_TIME_LIMIT = 12; private final String id; // ์•ฝ์†๋ฐฉ ID (UUID) private final String roomName; // ์•ฝ์†๋ฐฉ ์ด๋ฆ„ private final int capacity; // ์ฐธ์—ฌ์ธ์› ์ˆ˜ (์ตœ์†Œ 2๋ช… ์ด์ƒ) private final Member member; // ๋ฐฉ ์ฐธ์—ฌ์ž - private final LocalDateTime locationInputLimitDateTime; // ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ์ผ์‹œ private final LocalDateTime createdDateTime; // ๋ฐฉ ์ƒ์„ฑ์ผ์‹œ + private final LocalDateTime locationInputLimitDateTime; // ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ์ผ์‹œ + private LocalDateTime voteLimitDateTime; // ํˆฌํ‘œ ๋งˆ๊ฐ์ผ์‹œ private Room(String id, String roomName, int capacity, Member member) { this.id = id; @@ -29,6 +31,7 @@ private Room(String id, String roomName, int capacity, Member member) { this.member = member; this.createdDateTime = LocalDateTime.now().withNano(0); this.locationInputLimitDateTime = createdDateTime.plusHours(LOCATION_INPUT_TIME_LIMIT); + this.voteLimitDateTime = locationInputLimitDateTime.plusHours(VOTE_TIME_LIMIT); } public static Room create(String roomName, int capacity, Member member) { @@ -60,4 +63,8 @@ private boolean isAllLocationInput(long participantCount) { public boolean isFull(int participantCount) { return capacity == participantCount; } + + public void updateVoteDeadline(LocalDateTime current) { + this.voteLimitDateTime = current.plusHours(VOTE_TIME_LIMIT); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/UpdateRoomPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/UpdateRoomPort.java new file mode 100644 index 00000000..ba6625b1 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/UpdateRoomPort.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.room.port.out; + +import com.kok.kokcore.room.domain.Room; + +public interface UpdateRoomPort { + + void update(Room room); +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java new file mode 100644 index 00000000..da2aa328 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.room.usecase; + +import java.time.LocalDateTime; + +public interface UpdateRoomUseCase { + + void updateRoomVoteDeadline(String roomId, LocalDateTime current); +} diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java index 576d21e1..95d78c50 100644 --- a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -22,7 +22,8 @@ void createRoom() { int capacity = 4; String hostProfile = "hostProfile"; String hostNickname = "test"; - LocalDateTime deadline = LocalDateTime.now().withNano(0).plusHours(6); + LocalDateTime locationInputDeadline = LocalDateTime.now().withNano(0).plusHours(6); + LocalDateTime voteDeadline = locationInputDeadline.plusHours(12); Member host = new Member(hostNickname, hostProfile, MemberRole.LEADER); // When @@ -33,8 +34,12 @@ void createRoom() { () -> assertNotNull(room.getId(), "ID๋Š” null์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(roomName, room.getRoomName(), "๋ฐฉ ์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(capacity, room.getCapacity(), "์ฐธ์—ฌ ์ธ์› ์ˆ˜๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), - () -> assertEquals(deadline.getHour(), room.getLocationInputLimitDateTime().getHour(), - "์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์ด ์ •ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + () -> assertEquals(locationInputDeadline.getHour(), + room.getLocationInputLimitDateTime().getHour(), + "์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„ ๋ฐฉ ์ƒ์„ฑ ์‹œ์ ์œผ๋กœ๋ถ€ํ„ฐ 6์‹œ๊ฐ„ ๋’ค์ž…๋‹ˆ๋‹ค."), + () -> assertEquals(voteDeadline.getHour(), + room.getVoteLimitDateTime().getHour(), + "ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„์€ ์ฒ˜์Œ์—๋Š” ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ์ ์œผ๋กœ๋ถ€ํ„ฐ 12์‹œ๊ฐ„ ๋’ค์ž…๋‹ˆ๋‹ค."), () -> assertEquals(hostNickname, room.getMember().getNickname(), "๋ฐฉ์žฅ ๋‹‰๋„ค์ž„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(hostProfile, room.getMember().getProfile(), "๋ฐฉ์žฅ ํ”„๋กœํ•„์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), () -> assertEquals(MemberRole.LEADER, room.getMember().getRole(), From bd3144b919f8c5d755ba0b7b8bfa569c2cc61c39 Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Sun, 30 Mar 2025 00:55:48 +0900 Subject: [PATCH 120/163] :sparkles: feat: implements places search api (#88) --- .../adapter/in/dto/request/PlacesRequest.java | 31 +++++ .../in/dto/response/PlaceResponse.java | 31 +++++ .../in/dto/response/PlacesResponse.java | 23 ++++ .../adapter/in/web/PlacesController.java | 28 +++++ .../out/external/GooglePlaceApiAdapter.java | 109 ++++++++++++++++++ .../application/service/PlacesService.java | 20 ++++ .../src/main/resources/application-dev.yml | 5 +- .../src/main/resources/application-prod.yml | 5 +- .../kokcore/places/domain/model/Place.java | 17 +++ .../places/domain/model/PlacesResult.java | 7 ++ .../places/domain/model/vo/PlaceType.java | 42 +++++++ .../kokcore/places/port/in/PlaceInput.java | 14 +++ .../places/port/out/LoadPlacesPort.java | 9 ++ .../places/usecase/SearchPlaceUseCase.java | 12 ++ 14 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/request/PlacesRequest.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlaceResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlacesResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/place/adapter/in/web/PlacesController.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/place/adapter/out/external/GooglePlaceApiAdapter.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/place/application/service/PlacesService.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/places/domain/model/Place.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/places/domain/model/PlacesResult.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/places/domain/model/vo/PlaceType.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/places/port/in/PlaceInput.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/places/port/out/LoadPlacesPort.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/places/usecase/SearchPlaceUseCase.java diff --git a/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/request/PlacesRequest.java b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/request/PlacesRequest.java new file mode 100644 index 00000000..a79ab791 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/request/PlacesRequest.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.place.adapter.in.dto.request; + +import com.kok.kokcore.places.port.in.PlaceInput; +import com.kok.kokcore.places.domain.model.vo.PlaceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; + + +public record PlacesRequest( + @NotBlank(message = "์žฅ์†Œ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + PlaceType placeType, + + @DecimalMin(value = "33.0", message = "์œ„๋„๋Š” 33 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "43.0", message = "์œ„๋„๋Š” 43 ์ดํ•˜์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Schema(defaultValue = "37.5665", description = "์œ„๋„") + double latitude, + + @DecimalMin(value = "123.0", message = "๊ฒฝ๋„๋Š” 123 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @DecimalMax(value = "132.0", message = "๊ฒฝ๋„๋Š” 132 ์ดํ•˜์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Schema(defaultValue = "126.9788", description = "๊ฒฝ๋„") + double longitude, + + @Schema(defaultValue = "20", description = "์ตœ๋Œ€ ๊ฐœ์ˆ˜ 20") + Integer maxCount +) { + public PlaceInput toPlaceInput() { + return new PlaceInput(placeType, latitude, longitude, maxCount); + } +} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlaceResponse.java b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlaceResponse.java new file mode 100644 index 00000000..ae134677 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlaceResponse.java @@ -0,0 +1,31 @@ +package com.kok.kokapi.place.adapter.in.dto.response; + +import com.kok.kokcore.places.domain.model.Place; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class PlaceResponse { + private final String displayName; + private final String formattedAddress; + private final Location location; + + public PlaceResponse(Place place) { + this.displayName = place.getName(); + this.formattedAddress = place.getAddress(); + this.location = new Location(place.getLatitude(), place.getLongitude()); + } + + @Getter + @ToString + public static class Location { + private final double latitude; + private final double longitude; + + public Location(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + } +} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlacesResponse.java b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlacesResponse.java new file mode 100644 index 00000000..73a40c7b --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/dto/response/PlacesResponse.java @@ -0,0 +1,23 @@ +package com.kok.kokapi.place.adapter.in.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.kok.kokcore.places.domain.model.PlacesResult; +import java.util.stream.Collectors; + +import java.util.List; +import lombok.Getter; +import lombok.ToString; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +@ToString +public class PlacesResponse { + private final List placeResponses; + + public PlacesResponse(PlacesResult placesResult) { + this.placeResponses = placesResult.places() + .stream() + .map(PlaceResponse::new) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/web/PlacesController.java b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/web/PlacesController.java new file mode 100644 index 00000000..727a6a4b --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/place/adapter/in/web/PlacesController.java @@ -0,0 +1,28 @@ +package com.kok.kokapi.place.adapter.in.web; + +import com.kok.kokapi.common.response.ApiResponseDto; +import com.kok.kokapi.config.annotion.V1Controller; +import com.kok.kokapi.place.adapter.in.dto.request.PlacesRequest; +import com.kok.kokapi.place.adapter.in.dto.response.PlacesResponse; +import com.kok.kokcore.places.usecase.SearchPlaceUseCase; +import com.kok.kokcore.places.domain.model.PlacesResult; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@V1Controller +@RequiredArgsConstructor +public class PlacesController { + + private final SearchPlaceUseCase searchPlaceUseCase; + + @Operation(summary = "์ฃผ๋ณ€ ํ”Œ๋ ˆ์ด์Šค ์กฐํšŒ", description = "์ถ”์ฒœ ์žฅ์†Œ ์ฃผ๋ณ€์˜ ํ•ซํ”Œ๋ ˆ์ด์Šค ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @PostMapping("/search/places") + public ResponseEntity> searchPlaces(@RequestBody PlacesRequest request) throws Exception { + PlacesResult result = searchPlaceUseCase.getPlaces(request.toPlaceInput()); + PlacesResponse response = new PlacesResponse(result); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } +} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/place/adapter/out/external/GooglePlaceApiAdapter.java b/kok-api/src/main/java/com/kok/kokapi/place/adapter/out/external/GooglePlaceApiAdapter.java new file mode 100644 index 00000000..029994d2 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/place/adapter/out/external/GooglePlaceApiAdapter.java @@ -0,0 +1,109 @@ +package com.kok.kokapi.place.adapter.out.external; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokcore.places.port.in.PlaceInput; +import com.kok.kokcore.places.port.out.LoadPlacesPort; +import com.kok.kokcore.places.domain.model.Place; +import com.kok.kokcore.places.domain.model.PlacesResult; +import com.kok.kokcore.places.domain.model.vo.PlaceType; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GooglePlaceApiAdapter implements LoadPlacesPort { + + @Value("${google.places.api.key}") + private String apiKey; + + private static final String GOOGLE_PLACE_BASE_URL = "https://places.googleapis.com/v1/places:searchNearby"; + private static final double DEFAULT_RADIUS = 5000.0; + + private final ObjectMapper objectMapper; + + @Override + public PlacesResult getPlaces(PlaceInput input) { + String jsonBody = buildRequestBody(input); + RestClient restClient = RestClient.create(); + + try { + String responseBody = restClient.method(HttpMethod.POST) + .uri(GOOGLE_PLACE_BASE_URL) + .header("Content-Type", "application/json") + .header("X-Goog-Api-Key", apiKey) + .header("X-Goog-FieldMask", + "places.displayName,places.formattedAddress,places.location").body(jsonBody) + .retrieve() + .body(String.class); + + return mapToPlacesResult(responseBody, input.placeType()); + } catch (IOException e) { + log.error("Error while calling Google Places API", e); + throw new RuntimeException("Call Google Places API Failed", e); + } + } + + private static String buildRequestBody(PlaceInput input) { + String includedTypes = input.placeType().getPlaceCategories().stream() + .map(category -> "\"" + category + "\"") + .collect(Collectors.joining(", ")); + + return String.format(""" + { + "includedTypes": [%s], + "maxResultCount": %d, + "locationRestriction": { + "circle": { + "center": { + "latitude": %.6f, + "longitude": %.6f + }, + "radius": %.1f + } + }, + "languageCode": "ko" + } + """, + includedTypes, + input.maxCount(), + input.latitude(), + input.longitude(), + DEFAULT_RADIUS); + } + + private PlacesResult mapToPlacesResult(String responseBody, PlaceType placeType) throws JsonProcessingException { + JsonNode root = objectMapper.readTree(responseBody); + JsonNode placesNode = root.get("places"); + List places = new ArrayList<>(); + + for (JsonNode node : placesNode) { + String name = node.path("displayName").path("text").asText(); + String address = node.path("formattedAddress").asText(); + double latitude = node.path("location").path("latitude").asDouble(); + double longitude = node.path("location").path("longitude").asDouble(); + + Place place = Place.builder() + .name(name) + .address(address) + .latitude(latitude) + .longitude(longitude) + .placeType(placeType) + .build(); + + places.add(place); + } + return new PlacesResult(places); + } +} \ No newline at end of file diff --git a/kok-api/src/main/java/com/kok/kokapi/place/application/service/PlacesService.java b/kok-api/src/main/java/com/kok/kokapi/place/application/service/PlacesService.java new file mode 100644 index 00000000..30ee24de --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/place/application/service/PlacesService.java @@ -0,0 +1,20 @@ +package com.kok.kokapi.place.application.service; + +import com.kok.kokcore.places.port.in.PlaceInput; +import com.kok.kokcore.places.port.out.LoadPlacesPort; +import com.kok.kokcore.places.usecase.SearchPlaceUseCase; +import com.kok.kokcore.places.domain.model.PlacesResult; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PlacesService implements SearchPlaceUseCase { + + private final LoadPlacesPort loadPlacesPort; + + @Override + public PlacesResult getPlaces(PlaceInput input) { + return loadPlacesPort.getPlaces(input); + } +} \ No newline at end of file diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 0e9064c7..85043e0f 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -41,7 +41,10 @@ station: ncp: object-storage-url: ${OBJECT_STORAGE_URL} - +google: + places: + api: + key: ${GOOGLE_PLACE_API_KEY} tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index eb2d8734..fef9aa9b 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -51,7 +51,10 @@ station: ncp: object-storage-url: ${OBJECT_STORAGE_URL} - +google: + places: + api: + key: ${GOOGLE_API_KEY} tmap-sub: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes/sub" diff --git a/kok-core/src/main/java/com/kok/kokcore/places/domain/model/Place.java b/kok-core/src/main/java/com/kok/kokcore/places/domain/model/Place.java new file mode 100644 index 00000000..a707f45f --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/places/domain/model/Place.java @@ -0,0 +1,17 @@ +package com.kok.kokcore.places.domain.model; + +import com.kok.kokcore.places.domain.model.vo.PlaceType; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@Builder +@ToString +public class Place { + private final String name; + private final String address; + private final double latitude; + private final double longitude; + private final PlaceType placeType; +} \ No newline at end of file diff --git a/kok-core/src/main/java/com/kok/kokcore/places/domain/model/PlacesResult.java b/kok-core/src/main/java/com/kok/kokcore/places/domain/model/PlacesResult.java new file mode 100644 index 00000000..7e8aaa4d --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/places/domain/model/PlacesResult.java @@ -0,0 +1,7 @@ +package com.kok.kokcore.places.domain.model; + +import java.util.List; + +public record PlacesResult( + List places +) { } \ No newline at end of file diff --git a/kok-core/src/main/java/com/kok/kokcore/places/domain/model/vo/PlaceType.java b/kok-core/src/main/java/com/kok/kokcore/places/domain/model/vo/PlaceType.java new file mode 100644 index 00000000..9e9dd1ec --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/places/domain/model/vo/PlaceType.java @@ -0,0 +1,42 @@ +package com.kok.kokcore.places.domain.model.vo; + +import java.util.List; +import lombok.Getter; + +/** + * Place ์œ ํ˜• Enum + */ +@Getter +public enum PlaceType { + ACTIVITY(List.of( + "amusement_park", "aquarium", "art_gallery", "bowling_alley", "gym", "movie_theater", + "museum", "night_club", "spa", "stadium", "tourist_attraction", "zoo" + )), + RESTAURANT(List.of( + "restaurant", "meal_delivery", "meal_takeaway" + )), + CAFE(List.of( + "cafe", "bakery" + )), + BAR(List.of("bar")), + PARK(List.of( + "park", "campground", "rv_park" + )), + CULTURE(List.of( + "library", "church", "hindu_temple", "mosque", "synagogue" + )), + EVENT(List.of( + "casino", "movie_theater", "stadium" + )), + SHOPPING(List.of( + "shopping_mall", "clothing_store", "convenience_store", "department_store", + "electronics_store", "furniture_store", "hardware_store", "home_goods_store", + "jewelry_store", "liquor_store", "shoe_store", "store", "supermarket" + )); + + private final List placeCategories; + + PlaceType(List placeCategories) { + this.placeCategories = placeCategories; + } +} \ No newline at end of file diff --git a/kok-core/src/main/java/com/kok/kokcore/places/port/in/PlaceInput.java b/kok-core/src/main/java/com/kok/kokcore/places/port/in/PlaceInput.java new file mode 100644 index 00000000..ce6501ec --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/places/port/in/PlaceInput.java @@ -0,0 +1,14 @@ +package com.kok.kokcore.places.port.in; + +import com.kok.kokcore.places.domain.model.vo.PlaceType; + +public record PlaceInput( + PlaceType placeType, + double latitude, + double longitude, + Integer maxCount +) { + public PlaceInput { + maxCount = (maxCount == null) ? 20 : maxCount; + } +} \ No newline at end of file diff --git a/kok-core/src/main/java/com/kok/kokcore/places/port/out/LoadPlacesPort.java b/kok-core/src/main/java/com/kok/kokcore/places/port/out/LoadPlacesPort.java new file mode 100644 index 00000000..65ead4f4 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/places/port/out/LoadPlacesPort.java @@ -0,0 +1,9 @@ +package com.kok.kokcore.places.port.out; + + +import com.kok.kokcore.places.port.in.PlaceInput; +import com.kok.kokcore.places.domain.model.PlacesResult; + +public interface LoadPlacesPort { + PlacesResult getPlaces(PlaceInput input); +} \ No newline at end of file diff --git a/kok-core/src/main/java/com/kok/kokcore/places/usecase/SearchPlaceUseCase.java b/kok-core/src/main/java/com/kok/kokcore/places/usecase/SearchPlaceUseCase.java new file mode 100644 index 00000000..19e2e3ee --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/places/usecase/SearchPlaceUseCase.java @@ -0,0 +1,12 @@ +package com.kok.kokcore.places.usecase; + +import com.kok.kokcore.places.port.in.PlaceInput; +import com.kok.kokcore.places.domain.model.PlacesResult; + +/** + * ์ฃผ๋ณ€ ์žฅ์†Œ ๊ฒ€์ƒ‰ ์œ ์Šค์ผ€์ด์Šค + * PlaceInput์„ ๋ฐ›์•„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ(PlacesResult)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ +public interface SearchPlaceUseCase { + PlacesResult getPlaces(PlaceInput input) throws Exception; +} \ No newline at end of file From 82787cb3d2a4399f9acd3ff1c60fdb061e0078ed Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Sun, 30 Mar 2025 19:53:22 +0900 Subject: [PATCH 121/163] :sparkles: feat: add address field to RoomParticipantResponse (#92) --- .../in/dto/response/RoomMembersResponses.java | 25 -------------- ...onse.java => RoomParticipantResponse.java} | 9 +++-- .../response/RoomParticipantsResponse.java | 33 +++++++++++++++++++ .../room/adapter/in/web/RoomController.java | 10 ++++-- .../adapter/in/web/RoomProfileController.java | 2 +- 5 files changed, 45 insertions(+), 34 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java rename kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/{RoomMembersResponse.java => RoomParticipantResponse.java} (66%) create mode 100644 kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomParticipantsResponse.java diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java deleted file mode 100644 index bb3b5089..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponses.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.kok.kokapi.room.adapter.in.dto.response; - -import com.kok.kokcore.room.domain.Member; -import com.kok.kokcore.room.domain.Room; -import java.util.List; - -public record RoomMembersResponses( - boolean isFull, - List members -) { - - public static RoomMembersResponses of(Room room, List members) { - return new RoomMembersResponses(room.isFull(members.size()), getMemberResponses(members)); - } - - private static List getMemberResponses(List members) { - return members.stream() - .map(member -> new RoomMembersResponse( - member.getMemberId(), - member.getProfile(), - member.getNickname(), - member.getRole() - )).toList(); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomParticipantResponse.java similarity index 66% rename from kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java rename to kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomParticipantResponse.java index 14fe2f25..f1cb5101 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomMembersResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomParticipantResponse.java @@ -2,11 +2,10 @@ import com.kok.kokcore.room.domain.vo.MemberRole; -public record RoomMembersResponse( +public record RoomParticipantResponse( String memberId, String profile, String nickname, - MemberRole role -) { - -} + MemberRole role, + String address +) { } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomParticipantsResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomParticipantsResponse.java new file mode 100644 index 00000000..bade6e50 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomParticipantsResponse.java @@ -0,0 +1,33 @@ +package com.kok.kokapi.room.adapter.in.dto.response; + +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public record RoomParticipantsResponse( + boolean isFull, + List members +) { + + public static RoomParticipantsResponse of(Room room, List members, List locations) { + Map locationMap = locations.stream() + .collect(Collectors.toMap( + Location::getMemberId, + Location::getName + )); + + List responses = members.stream() + .map(member -> new RoomParticipantResponse( + member.getMemberId(), + member.getProfile(), + member.getNickname(), + member.getRole(), + locationMap.getOrDefault(member.getMemberId(), "") + )).toList(); + + return new RoomParticipantsResponse(room.isFull(members.size()), responses); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index cde0ecf6..fd8cf30e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -7,9 +7,11 @@ import com.kok.kokapi.room.adapter.in.dto.response.CreateRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomMembersResponses; +import com.kok.kokapi.room.adapter.in.dto.response.RoomParticipantsResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; import com.kok.kokapi.room.application.service.RoomFacadeService; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.usecase.ReadLocationUseCase; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; @@ -36,6 +38,7 @@ public class RoomController { private final GetRoomUseCase getRoomUseCase; private final CreateRoomUseCase createRoomUseCase; private final JoinRoomUseCase joinRoomUseCase; + private final ReadLocationUseCase readLocationUseCase; @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ์•ฝ์†๋ฐฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ํˆฌํ‘œ ๋ชจ๋“œ์— ๋Œ€ํ•œ ๊ฐ’์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}") @@ -75,12 +78,13 @@ public ResponseEntity> createRoom( @Operation(summary = "์•ฝ์†๋ฐฉ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ชฉ๋ก ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌ ์ค‘์ธ ์ฐธ์—ฌ์ž๋“ค์˜ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}/participants") - public ResponseEntity> getParticipants( + public ResponseEntity> getParticipants( @PathVariable String roomId) { Room room = getRoomUseCase.findRoomById(roomId); List participants = getRoomUseCase.getParticipants(room.getId()); - RoomMembersResponses responses = RoomMembersResponses.of(room, participants); + List locations = readLocationUseCase.readLocations(room.getId()); + RoomParticipantsResponse responses = RoomParticipantsResponse.of(room, participants, locations); return ResponseEntity.ok(ApiResponseDto.success(responses)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java index a4ecc9cb..08fffc04 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomProfileController.java @@ -16,7 +16,7 @@ public class RoomProfileController { private final CreateRandomProfileUseCase createRandomProfileUseCase; - @Operation(summary = "๋žœ๋ค ํ”„๋กœํ•„ ๋ฐ ๋‹‰๋„ค์ž„ ์กฐํšŒ", description = "๋žœ๋ค์œผ๋กœ ์ƒ์„ฑํ•œ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€์™€ ๋‹‰๋„ค์ž„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") + @Operation(summary = "๋žœ๋ค ํ”„๋กœํ•„ ๋ฐ ๋‹‰๋„ค์ž„ ์ƒ์„ฑ ๋ฐ ์กฐํšŒ", description = "๋žœ๋ค์œผ๋กœ ์ƒ์„ฑํ•œ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€์™€ ๋‹‰๋„ค์ž„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/profile/random") public ResponseEntity> getRandomProfile() { Profile profile = createRandomProfileUseCase.createProfile(); From 81e9fb377041d1d416a32c3ab8d7b7cef360000d Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sun, 30 Mar 2025 20:34:02 +0900 Subject: [PATCH 122/163] =?UTF-8?q?=E2=9C=A8=20[Feature/room]=20change=20i?= =?UTF-8?q?sVoteMode=20to=20roomStatus=20on=20API=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: add room status field on room domain * :sparkles: feat: update room status if location input ended when getting room info And fix response field * :wrench: config: add missing dependency on application-test.yml * :bug: fix: fix to update into VOTE status when update room vote deadline --- .../adapter/in/web/LocationController.java | 3 +- .../in/dto/response/RoomDetailResponse.java | 6 +-- .../in/dto/response/RoomStatusResponse.java | 7 +++- .../room/adapter/in/web/RoomController.java | 18 +++++---- .../service/RoomCommandService.java | 15 ++++--- .../service/RoomFacadeService.java | 39 ------------------- .../application/service/RoomQueryService.java | 26 ++++++++++++- .../vote/adapter/in/web/VoteController.java | 3 +- .../adapter/in/web/RoomIntegrationTest.java | 28 +++++++------ .../service/RoomQueryServiceTest.java | 5 ++- .../src/test/resources/application-test.yml | 4 ++ .../com/kok/kokcore/room/domain/Room.java | 21 ++++++++-- .../kokcore/room/domain/vo/RoomStatus.java | 12 ++++++ .../kokcore/room/usecase/GetRoomUseCase.java | 3 +- .../room/usecase/UpdateRoomUseCase.java | 2 +- .../com/kok/kokcore/room/domain/RoomTest.java | 10 ++--- 16 files changed, 118 insertions(+), 84 deletions(-) delete mode 100644 kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index a9acdb84..0d24e0a0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -54,7 +54,8 @@ public ResponseEntity> createLocation( Pair centroid = loadCentroidUsecase.readCentroidCoordinates( locationRequest.roomId()); - updateRoomUseCase.updateRoomVoteDeadline(locationRequest.roomId(), LocalDateTime.now()); + updateRoomUseCase.startVoteIfLocationInputEnded( + locationRequest.roomId(), LocalDateTime.now()); return ResponseEntity.ok(ApiResponseDto.success( CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index 9188634b..24f2abe5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -6,15 +6,15 @@ public record RoomDetailResponse( String id, String roomName, int nonParticipantCount, - boolean isVoteMode + String roomStatus ) { - public static RoomDetailResponse of(Room room, int participantCount, boolean isVoteMode) { + public static RoomDetailResponse of(Room room, int participantCount) { return new RoomDetailResponse( room.getId(), room.getRoomName(), room.getCapacity() - participantCount, - isVoteMode + room.getStatus().name() ); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java index 6659d970..18b0f0ae 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomStatusResponse.java @@ -1,5 +1,10 @@ package com.kok.kokapi.room.adapter.in.dto.response; -public record RoomStatusResponse(boolean isVoteMode) { +import com.kok.kokcore.room.domain.Room; +public record RoomStatusResponse(String roomStatus) { + + public static RoomStatusResponse of(Room room) { + return new RoomStatusResponse(room.getStatus().name()); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index fd8cf30e..b0d95777 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -9,7 +9,6 @@ import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomParticipantsResponse; import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; -import com.kok.kokapi.room.application.service.RoomFacadeService; import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.usecase.ReadLocationUseCase; import com.kok.kokcore.room.domain.Member; @@ -34,7 +33,6 @@ @RequiredArgsConstructor public class RoomController { - private final RoomFacadeService roomFacadeService; private final GetRoomUseCase getRoomUseCase; private final CreateRoomUseCase createRoomUseCase; private final JoinRoomUseCase joinRoomUseCase; @@ -44,15 +42,18 @@ public class RoomController { @GetMapping("/rooms/{roomId}") public ResponseEntity> getRoomDetail( @PathVariable String roomId) { - RoomDetailResponse response = roomFacadeService.findByRoomId(roomId, LocalDateTime.now()); + Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + int participantsCount = getRoomUseCase.getParticipantsCount(roomId); + RoomDetailResponse response = RoomDetailResponse.of(room, participantsCount); return ResponseEntity.ok(ApiResponseDto.success(response)); } - @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒํƒœ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์•ฝ์†๋ฐฉ์ด ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ์ค‘์ด๋ฉด false๋ฅผ, ํˆฌํ‘œ ์ง„ํ–‰ ์ค‘์ด๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") + @Operation(summary = "์•ฝ์†๋ฐฉ ์ƒํƒœ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์•ฝ์†๋ฐฉ ์ƒํƒœ(LOCATION_INPUT/VOTE/VOTE_RESULT)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}/status") public ResponseEntity> getRoomStatus( @PathVariable String roomId) { - RoomStatusResponse response = roomFacadeService.getRoomStatus(roomId, LocalDateTime.now()); + Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + RoomStatusResponse response = RoomStatusResponse.of(room); return ResponseEntity.ok(ApiResponseDto.success(response)); } @@ -80,11 +81,12 @@ public ResponseEntity> createRoom( @GetMapping("/rooms/{roomId}/participants") public ResponseEntity> getParticipants( @PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId); + Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); List participants = getRoomUseCase.getParticipants(room.getId()); List locations = readLocationUseCase.readLocations(room.getId()); - RoomParticipantsResponse responses = RoomParticipantsResponse.of(room, participants, locations); + RoomParticipantsResponse responses = RoomParticipantsResponse.of(room, participants, + locations); return ResponseEntity.ok(ApiResponseDto.success(responses)); } @@ -93,7 +95,7 @@ public ResponseEntity> getParticipants( public ResponseEntity> joinRoom(@PathVariable String roomId, @Valid @RequestBody JoinRoomParticipantRequest request) { - Room room = getRoomUseCase.findRoomById(roomId); + Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); Member participant = new Member(request.nickname(), request.profile(), MemberRole.FOLLOWER); int participantCount = joinRoomUseCase.joinRoom(roomId, participant); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java index 20c6a4d7..55f41f3e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java @@ -35,19 +35,24 @@ public Room createRoom(String roomName, int capacity, Member host) { } @Override - public void updateRoomVoteDeadline(String roomId, LocalDateTime current) { - Room room = loadRoomPort.findRoomById(roomId) - .orElseThrow( - () -> new IllegalArgumentException("Cannot find room with roomId: " + roomId)); + public void startVoteIfLocationInputEnded(String roomId, LocalDateTime current) { + Room room = getRoom(roomId); if (shouldUpdateVoteDeadline(room, current)) { room.updateVoteDeadline(current); + room.startVote(); updateRoomPort.update(room); } } + private Room getRoom(String roomId) { + return loadRoomPort.findRoomById(roomId) + .orElseThrow( + () -> new IllegalArgumentException("Cannot find room with roomId: " + roomId)); + } + private boolean shouldUpdateVoteDeadline(Room room, LocalDateTime current) { List locations = readLocationPort.findLocationsByRoomId(room.getId()); int locationInputCount = locations.size(); - return room.hasLocationInputEnded(locationInputCount, current); + return room.shouldEndLocationInput(locationInputCount, current); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java deleted file mode 100644 index 810ea3ab..00000000 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomFacadeService.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.kok.kokapi.room.application.service; - -import com.kok.kokapi.room.adapter.in.dto.response.RoomDetailResponse; -import com.kok.kokapi.room.adapter.in.dto.response.RoomStatusResponse; -import com.kok.kokcore.location.domain.Location; -import com.kok.kokcore.location.usecase.ReadLocationUseCase; -import com.kok.kokcore.room.domain.Room; -import com.kok.kokcore.room.usecase.GetRoomUseCase; -import java.time.LocalDateTime; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RoomFacadeService { - - private final GetRoomUseCase getRoomUseCase; - private final ReadLocationUseCase readLocationUseCase; - - public RoomDetailResponse findByRoomId(String roomId, LocalDateTime current) { - Room room = getRoomUseCase.findRoomById(roomId); - int participantsCount = getRoomUseCase.getParticipantsCount(roomId); - boolean isVoteMode = getVoteMode(roomId, current, room); - return RoomDetailResponse.of(room, participantsCount, isVoteMode); - } - - public RoomStatusResponse getRoomStatus(String roomId, LocalDateTime current) { - Room room = getRoomUseCase.findRoomById(roomId); - boolean isVoteMode = getVoteMode(roomId, current, room); - return new RoomStatusResponse(isVoteMode); - } - - private boolean getVoteMode(String roomId, LocalDateTime current, Room room) { - List locations = readLocationUseCase.readLocations(roomId); - int locationInputCount = locations.size(); - return room.hasLocationInputEnded(locationInputCount, current); - } -} diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index 80560bc6..a727012f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -1,10 +1,14 @@ package com.kok.kokapi.room.application.service; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; import com.kok.kokcore.room.port.out.LoadRoomPort; +import com.kok.kokcore.room.port.out.UpdateRoomPort; import com.kok.kokcore.room.usecase.GetRoomUseCase; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,13 +19,30 @@ public class RoomQueryService implements GetRoomUseCase { private final LoadRoomPort loadRoomPort; private final LoadRoomParticipantPort loadRoomParticipantPort; + private final ReadLocationPort readLocationPort; + private final UpdateRoomPort updateRoomPort; @Override - public Room findRoomById(String roomId) { + public Room findRoomById(String roomId, LocalDateTime current) { + Room room = getRoom(roomId); + if (shouldEndLocationInput(room, current)) { + room.startVote(); + updateRoomPort.update(room); + } + return getRoom(roomId); + } + + private Room getRoom(String roomId) { return loadRoomPort.findRoomById(roomId) .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); } + private boolean shouldEndLocationInput(Room room, LocalDateTime current) { + List locations = readLocationPort.findLocationsByRoomId(room.getId()); + int locationInputCount = locations.size(); + return room.shouldEndLocationInput(locationInputCount, current); + } + @Override public List getParticipants(String roomId) { validate(roomId); @@ -39,7 +60,8 @@ public Member getParticipant(String roomId, String memberId) { return getParticipants(roomId).stream() .filter(member -> member.getMemberId().equals(memberId)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Member not found with id: " + memberId)); + .orElseThrow( + () -> new IllegalArgumentException("Member not found with id: " + memberId)); } @Override diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 87b5a870..9eb6952f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -12,6 +12,7 @@ import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.station.domain.entity.Station; import io.swagger.v3.oas.annotations.Operation; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -61,7 +62,7 @@ public ResponseEntity>> getMemberV @GetMapping("/vote/{roomId}/deadline") public ResponseEntity> getVoteDeadline( @PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId); + Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); VoteDeadlineResponse response = VoteDeadlineResponse.from(room); return ResponseEntity.ok(ApiResponseDto.success(response)); } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java index b1e057ec..8c4dd536 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -9,6 +9,7 @@ import com.kok.kokapi.room.adapter.in.dto.request.JoinRoomParticipantRequest; import com.kok.kokapi.room.adapter.in.dto.response.CreateRoomResponse; import com.kok.kokapi.room.adapter.in.dto.response.JoinRoomResponse; +import com.kok.kokcore.room.domain.vo.RoomStatus; import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.math.BigDecimal; @@ -33,23 +34,26 @@ Stream getRoomDetail() { inputLocation("๋ฐฉ์žฅ์ด ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse), - getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๋‹ค", createRoomResponse, 1, false), + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๋‹ค", + createRoomResponse, 1, RoomStatus.LOCATION_INPUT.name()), DynamicTest.dynamicTest("ํŒ”๋กœ์›Œ๊ฐ€ ์•ฝ์†๋ฐฉ์— ์ฐธ์—ฌํ•œ๋‹ค.", () -> joinRoomResponse.set(joinRoom(createRoomResponse.get().id(), new JoinRoomParticipantRequest("profile", "follower")))), - getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๋‹ค.", createRoomResponse, 0, false), + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๋‹ค.", + createRoomResponse, 0, RoomStatus.LOCATION_INPUT.name()), - getRoomMembers("์•ฝ์†๋ฐฉ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉด isFull์€ true์ด๊ณ , 2๋ช…์˜ ํ”„๋กœํ•„์ด ์žˆ๋‹ค", createRoomResponse, 2, - true), + getRoomMembers("์•ฝ์†๋ฐฉ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉด isFull์€ true์ด๊ณ , 2๋ช…์˜ ํ”„๋กœํ•„์ด ์žˆ๋‹ค", + createRoomResponse, 2, true), - checkVoteMode("์•„์ง ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜๊ธฐ์— voteMode๋Š” false์ด๋‹ค.", createRoomResponse, false), + checkRoomStatus("์•„์ง ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜๊ธฐ์— roomStatuts๋Š” LOCATION_INPUT์ด๋‹ค.", + createRoomResponse, RoomStatus.LOCATION_INPUT.name()), inputLocation("ํŒ”๋กœ์›Œ๊ฐ€ ์ถœ๋ฐœ์ง€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.", createRoomResponse, joinRoomResponse), - checkVoteMode("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ชจ๋“  ์ฐธ์—ฌ์ž๊ฐ€ ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ–ˆ๊ธฐ์— voteMode๋Š” true์ด๋‹ค.", - createRoomResponse, true) + checkRoomStatus("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ชจ๋“  ์ฐธ์—ฌ์ž๊ฐ€ ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ–ˆ๊ธฐ์— roomStatus๋Š” VOTE์ด๋‹ค.", + createRoomResponse, RoomStatus.VOTE.name()) ); } @@ -94,7 +98,7 @@ private static DynamicTest getRoomDetail( String message, AtomicReference createRoomResponse, int nonParticipantCount, - boolean isVoteMode + String roomStatus ) { return DynamicTest.dynamicTest(message, () -> { @@ -105,7 +109,7 @@ private static DynamicTest getRoomDetail( .then().log().all() .assertThat().statusCode(200) .body("data.nonParticipantCount", is(nonParticipantCount)) - .body("data.isVoteMode", is(isVoteMode)); + .body("data.roomStatus", is(roomStatus)); }); } @@ -143,8 +147,8 @@ private static DynamicTest inputLocation(String message, }); } - private static DynamicTest checkVoteMode(String message, - AtomicReference createRoomResponse, boolean isVoteMode) { + private static DynamicTest checkRoomStatus(String message, + AtomicReference createRoomResponse, String roomStatus) { return DynamicTest.dynamicTest(message, () -> { String roomId = createRoomResponse.get().id(); @@ -153,7 +157,7 @@ private static DynamicTest checkVoteMode(String message, .when().get("/v1/api/rooms/" + roomId + "/status") .then().log().all() .assertThat().statusCode(200) - .body("data.isVoteMode", is(isVoteMode)); + .body("data.roomStatus", is(roomStatus)); }); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java index e2f134a7..8d980e5e 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java @@ -8,6 +8,7 @@ import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; import com.kok.kokcore.room.port.out.SaveRoomPort; +import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,7 +28,7 @@ void findRoomById() { Room room = saveRoomPort.save(Room.create("room", 2, member)); // when - Room result = roomQueryService.findRoomById(room.getId()); + Room result = roomQueryService.findRoomById(room.getId(), LocalDateTime.now()); // then assertThat(result).isEqualTo(room); @@ -40,7 +41,7 @@ void cannotFindRoomById() { String roomId = "unknownId"; // when & then - assertThatThrownBy(() -> roomQueryService.findRoomById(roomId)) + assertThatThrownBy(() -> roomQueryService.findRoomById(roomId, LocalDateTime.now())) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Room not found with id: " + roomId); } diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index 53e3af3c..6a144e54 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -28,3 +28,7 @@ tmap-complex: key: mock url: mock keyname: mock +google: + places: + api: + key: mock diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 482b8151..6065345a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -1,5 +1,6 @@ package com.kok.kokcore.room.domain; +import com.kok.kokcore.room.domain.vo.RoomStatus; import java.io.Serializable; import java.time.LocalDateTime; import java.util.UUID; @@ -23,6 +24,7 @@ public class Room implements Serializable { private final LocalDateTime createdDateTime; // ๋ฐฉ ์ƒ์„ฑ์ผ์‹œ private final LocalDateTime locationInputLimitDateTime; // ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ์ผ์‹œ private LocalDateTime voteLimitDateTime; // ํˆฌํ‘œ ๋งˆ๊ฐ์ผ์‹œ + private RoomStatus status; private Room(String id, String roomName, int capacity, Member member) { this.id = id; @@ -32,6 +34,7 @@ private Room(String id, String roomName, int capacity, Member member) { this.createdDateTime = LocalDateTime.now().withNano(0); this.locationInputLimitDateTime = createdDateTime.plusHours(LOCATION_INPUT_TIME_LIMIT); this.voteLimitDateTime = locationInputLimitDateTime.plusHours(VOTE_TIME_LIMIT); + this.status = RoomStatus.LOCATION_INPUT; } public static Room create(String roomName, int capacity, Member member) { @@ -51,9 +54,13 @@ private static void validateParameter(String roomName, int capacity) { } } - public boolean hasLocationInputEnded(long locationInputCount, LocalDateTime currentTime) { - return isAllLocationInput(locationInputCount) || currentTime.isAfter( - locationInputLimitDateTime); + public boolean shouldEndLocationInput(long locationInputCount, LocalDateTime current) { + return isLocationInput() && + (isAllLocationInput(locationInputCount) || current.isAfter(locationInputLimitDateTime)); + } + + private boolean isLocationInput() { + return this.status.isLocationInput(); } private boolean isAllLocationInput(long participantCount) { @@ -67,4 +74,12 @@ public boolean isFull(int participantCount) { public void updateVoteDeadline(LocalDateTime current) { this.voteLimitDateTime = current.plusHours(VOTE_TIME_LIMIT); } + + public void startVote() { + this.status = RoomStatus.VOTE; + } + + public void closeVote() { + this.status = RoomStatus.VOTE_RESULT; + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java new file mode 100644 index 00000000..cc59f2e8 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java @@ -0,0 +1,12 @@ +package com.kok.kokcore.room.domain.vo; + +public enum RoomStatus { + + LOCATION_INPUT, + VOTE, + VOTE_RESULT; + + public boolean isLocationInput() { + return this.equals(LOCATION_INPUT); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java index 3ca756d2..2cf6ee5b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -2,11 +2,12 @@ import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import java.time.LocalDateTime; import java.util.List; public interface GetRoomUseCase { - Room findRoomById(String roomId); + Room findRoomById(String roomId, LocalDateTime current); List getParticipants(String roomId); diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java index da2aa328..202bb984 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java @@ -4,5 +4,5 @@ public interface UpdateRoomUseCase { - void updateRoomVoteDeadline(String roomId, LocalDateTime current); + void startVoteIfLocationInputEnded(String roomId, LocalDateTime current); } diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java index 95d78c50..30d55419 100644 --- a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -83,12 +83,12 @@ void createRoomWithInvalidCapacity() { @DisplayName("์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test - void hasLocationInputEndedByDeadlineExceeded() { + void shouldEndLocationInputByDeadlineExceeded() { // given Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); // when - boolean result = room.hasLocationInputEnded(1, LocalDateTime.now().plusHours(6)); + boolean result = room.shouldEndLocationInput(1, LocalDateTime.now().plusHours(6)); // then assertThat(result).isTrue(); @@ -96,12 +96,12 @@ void hasLocationInputEndedByDeadlineExceeded() { @DisplayName("๋ชจ๋“  ์ฐธ๊ฐ€์ž๊ฐ€ ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test - void hasLocationInputEndedByAllParticipantCompleted() { + void shouldEndLocationInputByAllParticipantCompleted() { // given Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); // when - boolean result = room.hasLocationInputEnded(2, LocalDateTime.now().plusHours(5)); + boolean result = room.shouldEndLocationInput(2, LocalDateTime.now().plusHours(5)); // then assertThat(result).isTrue(); @@ -114,7 +114,7 @@ void hasNotLocationInputEnded() { Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); // when - boolean result = room.hasLocationInputEnded(1, LocalDateTime.now().plusHours(5)); + boolean result = room.shouldEndLocationInput(1, LocalDateTime.now().plusHours(5)); // then assertThat(result).isFalse(); From e13ffafd7ef449f940a63dbe49d22239cd19798a Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:50:56 +0900 Subject: [PATCH 123/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20implement?= =?UTF-8?q?=20getting=20vote=20result=20API=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: fix: fix getting candidates by empty list * :sparkles: feat: implement getting vote result skeleton code and responses * :sparkles: feat: specify required usecase * :sparkles: feat: implement counting voted members * :recycle: refactor: remove save facade method and move its logic into save vote usecase * :sparkles: feat: implement getting votes by member usecase * :sparkles: feat: implement getting members by vote info * :white_check_mark: test: test for room participant query redis adapter * :white_check_mark: test: test for getting vote usecases * :white_check_mark: test: test for room domain * :recycle: refactor: give message on method name --- .../RoomParticipantQueryRedisAdapter.java | 10 +- .../in/dto/response/ResultResponse.java | 25 +++ .../in/dto/response/VoteResultResponse.java | 10 + .../in/dto/response/VotedMemberResponse.java | 13 ++ .../vote/adapter/in/web/VoteController.java | 17 +- .../persistence/VoteQueryRedisAdapter.java | 43 ++++ .../service/VoteFacadeService.java | 56 +++--- .../vote/application/service/VoteService.java | 98 ++++++++- .../RoomParticipantQueryRedisAdapterTest.java | 58 +++++- .../VoteQueryRedisAdapterTest.java | 20 ++ .../application/service/VoteServiceTest.java | 187 ++++++++++++++---- .../com/kok/kokcore/room/domain/Room.java | 14 +- .../kokcore/room/domain/vo/RoomStatus.java | 4 + .../port/out/LoadRoomParticipantPort.java | 3 + .../kokcore/vote/port/out/LoadVotePort.java | 6 + .../kokcore/vote/usecase/GetVoteUseCase.java | 10 + .../kokcore/vote/usecase/SaveVoteUseCase.java | 3 +- .../com/kok/kokcore/room/domain/RoomTest.java | 94 +++++++++ 18 files changed, 589 insertions(+), 82 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java index 0d4cb0bb..d3203f72 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapter.java @@ -6,6 +6,7 @@ import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; @@ -30,7 +31,7 @@ public Long countParticipantsById(String roomId) { @Override public List findMembersByRoomId(String roomId) { - String key = PARTICIPANT_KEY_PREFIX + roomId; + String key = buildKey(roomId); List memberJson = redisTemplate.opsForList().range(key, 0, -1); List members = new ArrayList<>(); if (memberJson != null) { @@ -45,6 +46,13 @@ public List findMembersByRoomId(String roomId) { return members; } + @Override + public Optional findByRoomIdAndMemberId(String roomId, String memberId) { + return findMembersByRoomId(roomId).stream() + .filter(member -> member.getMemberId().equals(memberId)) + .findFirst(); + } + private String buildKey(String roomId) { return PARTICIPANT_KEY_PREFIX + roomId; } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java new file mode 100644 index 00000000..46dfddd7 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java @@ -0,0 +1,25 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; + +public record ResultResponse( + long stationId, + String stationName, + String voteStatus, + int votedCount, + List members +) { + + public static ResultResponse of(Station station, Vote vote, List members) { + return new ResultResponse( + station.getId(), + station.getName(), + vote.getVoteStatus().getName(), + members.size(), + members.stream().map(VotedMemberResponse::from).toList() + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultResponse.java new file mode 100644 index 00000000..cae77f06 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultResponse.java @@ -0,0 +1,10 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +import java.util.List; + +public record VoteResultResponse( + int notVotedCount, + List results +) { + +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java new file mode 100644 index 00000000..5051580b --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java @@ -0,0 +1,13 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +import com.kok.kokcore.room.domain.Member; + +public record VotedMemberResponse( + String id, + String imageUrl +) { + + public static VotedMemberResponse from(Member member) { + return new VotedMemberResponse(member.getMemberId(), member.getProfile()); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 9eb6952f..4613a3cf 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -7,10 +7,12 @@ import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; import com.kok.kokapi.vote.adapter.in.dto.response.VoteDeadlineResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultResponse; import com.kok.kokapi.vote.application.service.VoteFacadeService; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.vote.usecase.SaveVoteUseCase; import io.swagger.v3.oas.annotations.Operation; import java.time.LocalDateTime; import java.util.List; @@ -28,6 +30,7 @@ public class VoteController { private final VoteFacadeService voteFacadeService; private final StationFacadeService stationFacadeService; private final GetRoomUseCase getRoomUseCase; + private final SaveVoteUseCase saveVoteUseCase; @Operation(summary = "ํˆฌํ‘œ ํ›„๋ณด์ง€ ๋ชฉ๋ก ์กฐํšŒ", description = "๋ฐฉ ID๊ณผ ์‚ฌ์šฉ์ž ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํˆฌํ‘œ ํ›„๋ณด์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/{memberId}/candidates") @@ -46,12 +49,12 @@ public ResponseEntity> createVote( @PathVariable String memberId, @RequestBody VoteRequest voteRequest ) { - voteFacadeService.createVote(roomId, memberId, voteRequest); + saveVoteUseCase.saveVotes(roomId, memberId, voteRequest.agreedStationIds()); return ResponseEntity.ok(ApiResponseDto.success(null)); } @Operation(summary = "์‚ฌ์šฉ์ž๋ณ„ ํˆฌํ‘œ ์ƒํƒœ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด์™€ ํˆฌํ‘œ ์ƒํƒœ(ํˆฌํ‘œ ์ „/ํˆฌํ‘œ ์™„๋ฃŒ)๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") - @GetMapping("/vote/{roomId}/status") + @GetMapping("/votes/{roomId}/status") public ResponseEntity>> getMemberVoteStatus( @PathVariable String roomId) { List responses = voteFacadeService.getMemberVoteStatus(roomId); @@ -59,11 +62,19 @@ public ResponseEntity>> getMemberV } @Operation(summary = "ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•œ ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") - @GetMapping("/vote/{roomId}/deadline") + @GetMapping("/votes/{roomId}/deadline") public ResponseEntity> getVoteDeadline( @PathVariable String roomId) { Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); VoteDeadlineResponse response = VoteDeadlineResponse.from(room); return ResponseEntity.ok(ApiResponseDto.success(response)); } + + @Operation(summary = "ํˆฌํ‘œ ๊ฒฐ๊ณผ ์กฐํšŒ", description = "๋ฐฉ ID์™€ ์‚ฌ์šฉ์ž ID์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ๊ธฐ์ค€์œผ๋กœ ํ›„๋ณด์ง€๋ณ„ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/votes/{roomId}/{memberId}") + public ResponseEntity> getVoteResult( + @PathVariable String roomId, @PathVariable String memberId) { + VoteResultResponse response = voteFacadeService.getVoteResult(roomId, memberId); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java index 5cd20a19..243201cc 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java @@ -2,14 +2,18 @@ import com.kok.kokapi.common.util.RedisExecutor; import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; import com.kok.kokcore.vote.port.out.LoadVotePort; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.stereotype.Repository; @Slf4j @@ -17,7 +21,9 @@ @RequiredArgsConstructor public class VoteQueryRedisAdapter implements LoadVotePort { + private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; + private static final int MAX_COUNT = 20; private final RedisTemplate redisTemplate; @@ -47,6 +53,39 @@ public List findAllByRoomIdAndMemberId(String roomId, String memberId) { }, List.of()); } + @Override + public int countMembersByRoomId(String roomId) { + String pattern = getMemberVoteKey(roomId, "*"); + return RedisExecutor.runOrElseGet("countMembersByRoomId", () -> { + int count = 0; + ScanOptions options = ScanOptions.scanOptions() + .match(pattern) + .count(MAX_COUNT) + .build(); + + try (Cursor cursor = redisTemplate.scan(options)) { + while (cursor.hasNext()) { + count++; + cursor.next(); + } + } + + return count; + }, 0); + } + + @Override + public List findMemberIdsByRoomIdAndStationIdAndStatus( + String roomId, long stationId, VoteStatus voteStatus) { + String key = getCandidateVoteKey(roomId, stationId, voteStatus); + Set memberIds = RedisExecutor.runOrElseGet( + "findMembersByRoomIdAndStationIdAndStatus", + () -> redisTemplate.opsForSet().members(key), + Set.of() + ); + return memberIds.stream().map(memberId -> (String) memberId).toList(); + } + private Long getStationId(Entry voteInfo) { try { return Long.valueOf(voteInfo.getKey().toString()); @@ -59,4 +98,8 @@ private Long getStationId(Entry voteInfo) { private String getMemberVoteKey(String roomId, String memberId) { return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); } + + private String getCandidateVoteKey(String roomId, long stationId, VoteStatus voteStatus) { + return String.format(CANDIDATE_VOTE_KEY_FORMAT, roomId, stationId, voteStatus.getName()); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index 285092ca..d75a819a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -4,10 +4,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.kok.kokapi.public_transportation.adapter.in.dto.response.TmapPublicTransportationParsedResponse; import com.kok.kokapi.public_transportation.application.service.TmapPublicTransportationService; -import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.ResultResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultResponse; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; @@ -15,10 +17,9 @@ import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; import com.kok.kokcore.vote.usecase.GetVoteUseCase; -import com.kok.kokcore.vote.usecase.SaveVoteUseCase; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -31,7 +32,6 @@ public class VoteFacadeService { private final GetCandidateUseCase getCandidateUseCase; private final GetStationUseCase getStationUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; - private final SaveVoteUseCase saveVoteUseCase; private final GetVoteUseCase getVoteUseCase; private final GetRoomUseCase getRoomUseCase; private final TmapPublicTransportationService tmapPublicTransportationService; @@ -39,17 +39,16 @@ public class VoteFacadeService { public List getCandidates(String roomId, String memberId, List stations) { - List responses = new ArrayList<>(); List candidates = getCandidateUseCase.saveAndGetCandidates(roomId, stations); + List responses = new ArrayList<>(); for (Candidate candidate : candidates) { Station station = getStationUseCase.getStation(candidate.getStationId()); List routes = retrieveRouteUseCase.retrieveRoutes(station); TmapPublicTransportationParsedResponse transportationParsedResponse = getTransportationParsedResponse( - roomId, - memberId, - station - ); - CandidateResponse.of(station, routes, transportationParsedResponse, List.of()); + roomId, memberId, station); + CandidateResponse response = CandidateResponse.of( + station, routes, transportationParsedResponse, List.of()); + responses.add(response); } return responses; } @@ -69,29 +68,6 @@ private TmapPublicTransportationParsedResponse getTransportationParsedResponse( } } - public void createVote(String roomId, String memberId, VoteRequest voteRequest) { - List votes = getVotes(roomId, memberId, voteRequest); - saveVoteUseCase.saveVotes(votes); - } - - private List getVotes(String roomId, String memberId, VoteRequest voteRequest) { - List votes = new ArrayList<>(); - List agreedStationIds = voteRequest.agreedStationIds(); - List candidates = getCandidateUseCase.getCandidates(roomId); - for (Candidate candidate : candidates) { - if (isAgree(agreedStationIds, candidate)) { - votes.add(new Vote(candidate, memberId, VoteStatus.AGREE)); - continue; - } - votes.add(new Vote(candidate, memberId, VoteStatus.DISAGREE)); - } - return votes; - } - - private static boolean isAgree(List agreedStationIds, Candidate candidate) { - return agreedStationIds.contains(candidate.getStationId()); - } - public List getMemberVoteStatus(String roomId) { List members = getRoomUseCase.getParticipants(roomId); List responses = new ArrayList<>(); @@ -102,4 +78,18 @@ public List getMemberVoteStatus(String roomId) { } return responses; } + + public VoteResultResponse getVoteResult(String roomId, String memberId) { + List responses = new ArrayList<>(); + Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + int votedCount = getVoteUseCase.countVotedMembers(roomId); + List votes = getVoteUseCase.getVotesByMember(roomId, memberId); + for (Vote vote : votes) { + Station station = getStationUseCase.getStation(vote.getStationId()); + List members = getVoteUseCase.getMembersByVote(vote); + ResultResponse response = ResultResponse.of(station, vote, members); + responses.add(response); + } + return new VoteResultResponse(room.getNotVotedCount(votedCount), responses); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java index e4f4c9b6..402078fd 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -1,12 +1,21 @@ package com.kok.kokapi.vote.application.service; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; +import com.kok.kokcore.room.port.out.LoadRoomPort; +import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; import com.kok.kokcore.vote.port.out.DeleteVotePort; +import com.kok.kokcore.vote.port.out.LoadCandidatePort; import com.kok.kokcore.vote.port.out.LoadVotePort; import com.kok.kokcore.vote.port.out.SaveVotePort; import com.kok.kokcore.vote.usecase.GetVoteUseCase; import com.kok.kokcore.vote.usecase.SaveVoteUseCase; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -16,19 +25,39 @@ public class VoteService implements SaveVoteUseCase, GetVoteUseCase { private final SaveVotePort saveVotePort; private final LoadVotePort loadVotePort; + private final LoadCandidatePort loadCandidatePort; private final DeleteVotePort deleteVotePort; + private final LoadRoomPort loadRoomPort; + private final LoadRoomParticipantPort loadRoomParticipantPort; @Override - public void saveVotes(List votes) { - String roomId = votes.getFirst().getRoomId(); - String memberId = votes.getFirst().getMemberId(); + public void saveVotes(String roomId, String memberId, List agreedStationIds) { + validate(roomId, memberId); initiate(roomId, memberId); + List votes = getVotes(roomId, memberId, agreedStationIds); saveVotePort.saveAllByMember(votes); for (Vote vote : votes) { saveVotePort.saveByCandidate(vote); } } + private List getVotes(String roomId, String memberId, List stationIds) { + List votes = new ArrayList<>(); + List candidates = loadCandidatePort.findByRoomId(roomId); + for (Candidate candidate : candidates) { + if (isAgree(stationIds, candidate)) { + votes.add(new Vote(candidate, memberId, VoteStatus.AGREE)); + continue; + } + votes.add(new Vote(candidate, memberId, VoteStatus.DISAGREE)); + } + return votes; + } + + private static boolean isAgree(List agreedStationIds, Candidate candidate) { + return agreedStationIds.contains(candidate.getStationId()); + } + private void initiate(String roomId, String memberId) { if (loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { List votes = loadVotePort.findAllByRoomIdAndMemberId(roomId, memberId); @@ -41,4 +70,67 @@ private void initiate(String roomId, String memberId) { public boolean isVotedByMember(String roomId, String memberId) { return loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId); } + + @Override + public int countVotedMembers(String roomId) { + validate(roomId); + return loadVotePort.countMembersByRoomId(roomId); + } + + @Override + public List getVotesByMember(String roomId, String memberId) { + validate(roomId, memberId); + validateVote(roomId, memberId); + return loadVotePort.findAllByRoomIdAndMemberId(roomId, memberId); + } + + @Override + public List getMembersByVote(Vote vote) { + List memberIds = loadVotePort.findMemberIdsByRoomIdAndStationIdAndStatus( + vote.getRoomId(), vote.getStationId(), vote.getVoteStatus()); + return getMembers(memberIds, vote.getRoomId()); + } + + private List getMembers(List memberIds, String roomId) { + return memberIds.stream() + .map(memberId -> loadRoomParticipantPort.findByRoomIdAndMemberId(roomId, memberId)) + .flatMap(Optional::stream) + .toList(); + } + + private void validate(String roomId, String memberId) { + validate(roomId); + List memberIds = loadRoomParticipantPort.findMembersByRoomId(roomId).stream() + .map(Member::getMemberId) + .toList(); + if (!memberIds.contains(memberId)) { + throw new IllegalArgumentException( + String.format("Cannot find member with id: %s, in room with id: %s", memberId, + roomId)); + } + } + + private void validate(String roomId) { + if (!loadRoomPort.isExistsByRoomId(roomId)) { + throw new IllegalArgumentException("Cannot find room with roomId: " + roomId); + } + Room room = getRoom(roomId); + if (room.isNotOnVote()) { + throw new IllegalStateException( + "Room is not on vote status but status: " + room.getStatus()); + } + } + + private Room getRoom(String roomId) { + return loadRoomPort.findRoomById(roomId) + .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); + } + + private void validateVote(String roomId, String memberId) { + if (!loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { + throw new IllegalArgumentException( + String.format("Not voted by member with id: %s, in room with id: %s", memberId, + roomId)); + } + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java index b0cdf131..08fe6876 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantQueryRedisAdapterTest.java @@ -4,6 +4,9 @@ import com.kok.kokapi.common.template.RepositoryTest; import com.kok.kokapi.fixture.MemberFixture; +import com.kok.kokcore.room.domain.Member; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,10 +15,11 @@ class RoomParticipantQueryRedisAdapterTest extends RepositoryTest { @Autowired private RoomParticipantQueryRedisAdapter roomParticipantQueryRedisAdapter; + @Autowired private RoomParticipantSaveAdapter roomParticipantSaveAdapter; - @DisplayName("๋ฐฉ์˜ ์ฐธ์—ฌ ์ธ์›์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @DisplayName("๋ฐฉ์˜ ์ฐธ์—ฌ ์ธ์› ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void getParticipantCount() { // given @@ -28,4 +32,56 @@ void getParticipantCount() { // then assertThat(participantCount).isEqualTo(1); } + + @DisplayName("๋ฐฉ์˜ ๋ชจ๋“  ์ฐธ์—ฌ์ž ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void findMembersByRoomId() { + // given + String roomId = "roomId"; + Member member1 = MemberFixture.createLeader(); + Member member2 = MemberFixture.createFollower(); + roomParticipantSaveAdapter.joinRoom(roomId, member1); + roomParticipantSaveAdapter.joinRoom(roomId, member2); + + // when + List members = roomParticipantQueryRedisAdapter.findMembersByRoomId(roomId); + + // then + assertThat(members) + .extracting(Member::getMemberId) + .containsExactlyInAnyOrder(member1.getMemberId(), member2.getMemberId()); + } + + @DisplayName("๋ฐฉ ID์™€ ๋ฉค๋ฒ„ ID๋กœ ํ•ด๋‹น ๋ฉค๋ฒ„๋ฅผ ์กฐํšŒํ•œ๋‹ค.") + @Test + void findByRoomIdAndMemberId() { + // given + String roomId = "roomId"; + Member member = MemberFixture.createLeader(); + roomParticipantSaveAdapter.joinRoom(roomId, member); + + // when + Optional result = roomParticipantQueryRedisAdapter.findByRoomIdAndMemberId( + roomId, member.getMemberId()); + + // then + assertThat(result).isPresent(); + assertThat(result.get().getMemberId()).isEqualTo(member.getMemberId()); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฉค๋ฒ„ ID๋กœ ์กฐํšŒํ•˜๋ฉด ๋นˆ Optional์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void notFoundMemberByRoomIdAndMemberId() { + // given + String roomId = "roomId"; + Member member = MemberFixture.createLeader(); + roomParticipantSaveAdapter.joinRoom(roomId, member); + + // when + Optional result = roomParticipantQueryRedisAdapter.findByRoomIdAndMemberId( + roomId, "non-existent-id"); + + // then + assertThat(result).isEmpty(); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java index e0270d00..ae2b5427 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java @@ -69,7 +69,27 @@ void findAllByRoomIdAndMemberId() { assertThat(votes).containsExactlyInAnyOrder(vote); } + @DisplayName("roomId๋กœ ํˆฌํ‘œํ•œ member ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void countMembersByRoomId() { + // given + String roomId = "roomId"; + redisTemplate.opsForHash() + .putAll(getMemberVoteKey(roomId, "1"), Map.of(1L, VoteStatus.AGREE.getName())); + redisTemplate.opsForHash() + .putAll(getMemberVoteKey(roomId, "2"), Map.of(1L, VoteStatus.AGREE.getName())); + redisTemplate.opsForHash() + .putAll(getMemberVoteKey(roomId, "3"), Map.of(1L, VoteStatus.AGREE.getName())); + + // when + int count = voteQueryRedisAdapter.countMembersByRoomId(roomId); + + // then + assertThat(count).isEqualTo(3); + } + private String getMemberVoteKey(String roomId, String memberId) { return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); } + } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java index cdeaf3e4..fd517a7b 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java @@ -1,9 +1,16 @@ package com.kok.kokapi.vote.application.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import com.kok.kokapi.common.template.ServiceTest; +import com.kok.kokapi.fixture.MemberFixture; +import com.kok.kokapi.room.adapter.out.persistence.RoomParticipantSaveAdapter; +import com.kok.kokapi.room.adapter.out.persistence.RoomSaveRedisAdapter; +import com.kok.kokapi.vote.adapter.out.persistence.CandidateCommandRedisAdapter; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.domain.vo.VoteStatus; @@ -11,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,23 +35,43 @@ class VoteServiceTest extends ServiceTest { private SaveVotePort saveVotePort; @Autowired private RedisTemplate redisTemplate; + @Autowired + private RoomSaveRedisAdapter roomSaveRedisAdapter; + @Autowired + private RoomParticipantSaveAdapter roomParticipantSaveAdapter; + @Autowired + private CandidateCommandRedisAdapter candidateCommandRedisAdapter; + + private Room room; + private Member member; + private Member member2; + private Candidate candidate; + private Candidate candidate2; + + @BeforeEach + void init() { + member = MemberFixture.createLeader(); + member2 = MemberFixture.createFollower(); + room = Room.create("room", 3, member); + room.startVote(); + roomSaveRedisAdapter.save(room); + roomParticipantSaveAdapter.joinRoom(room.getId(), member); + roomParticipantSaveAdapter.joinRoom(room.getId(), member2); + candidate = new Candidate(room.getId(), 1); + candidate2 = new Candidate(room.getId(), 2); + candidateCommandRedisAdapter.saveAll(List.of(candidate, candidate2)); + } @DisplayName("์‚ฌ์šฉ์ž ํˆฌํ‘œ ์ •๋ณด๋ฅผ ํ›„๋ณด๋ณ„/์‚ฌ์šฉ์ž๋ณ„๋กœ ๋ชจ๋‘ ์ €์žฅํ•œ๋‹ค.") @Test void saveVotes() { // given - String roomId = "roomId"; - String memberId = "memberId"; - List votes = List.of( - new Vote(new Candidate(roomId, 1), memberId, VoteStatus.AGREE), - new Vote(new Candidate(roomId, 2), memberId, VoteStatus.DISAGREE) - ); - String memberKey = getMemberVoteKey(roomId, memberId); - String agreeCandidateKey = getCandidateVoteKey(roomId, 1, VoteStatus.AGREE); - String disagreeCandidateKey = getCandidateVoteKey(roomId, 2, VoteStatus.DISAGREE); + String memberKey = getMemberVoteKey(room.getId(), member.getMemberId()); + String agreeCandidateKey = getCandidateVoteKey(room.getId(), 1, VoteStatus.AGREE); + String disagreeCandidateKey = getCandidateVoteKey(room.getId(), 2, VoteStatus.DISAGREE); // when - voteService.saveVotes(votes); + voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); // then Map storedVotes = redisTemplate.opsForHash().entries(memberKey); @@ -53,8 +81,8 @@ void saveVotes() { assertAll( () -> assertThat(storedVotes).containsEntry(1L, VoteStatus.AGREE.getName()), () -> assertThat(storedVotes).containsEntry(2L, VoteStatus.DISAGREE.getName()), - () -> assertThat(agreeMemberIds).containsExactlyInAnyOrder(memberId), - () -> assertThat(disagreeMemberIds).containsExactlyInAnyOrder(memberId) + () -> assertThat(agreeMemberIds).containsExactlyInAnyOrder(member.getMemberId()), + () -> assertThat(disagreeMemberIds).containsExactlyInAnyOrder(member.getMemberId()) ); } @@ -62,35 +90,27 @@ void saveVotes() { @Test void initiateBeforeSaveVote() { // given - String roomId = "roomId"; - String memberId = "memberId"; - Candidate candidate = new Candidate(roomId, 1); - Candidate candidate2 = new Candidate(roomId, 2); List votes = List.of( - new Vote(candidate, memberId, VoteStatus.AGREE), - new Vote(candidate2, memberId, VoteStatus.DISAGREE) + new Vote(candidate, member.getMemberId(), VoteStatus.AGREE), + new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE) ); saveVotePort.saveAllByMember(votes); for (Vote vote : votes) { saveVotePort.saveByCandidate(vote); } - List newVotes = List.of( - new Vote(candidate, memberId, VoteStatus.DISAGREE), - new Vote(candidate2, memberId, VoteStatus.DISAGREE) - ); // when - voteService.saveVotes(newVotes); + voteService.saveVotes(room.getId(), member.getMemberId(), List.of()); // then Long storedVoteCount = redisTemplate.opsForHash() - .size(getMemberVoteKey(roomId, memberId)); + .size(getMemberVoteKey(room.getId(), member.getMemberId())); Long agreeCountForStation1 = redisTemplate.opsForSet() - .size(getCandidateVoteKey(roomId, 1, VoteStatus.AGREE)); + .size(getCandidateVoteKey(room.getId(), 1, VoteStatus.AGREE)); Long disagreeCountForStation1 = redisTemplate.opsForSet() - .size(getCandidateVoteKey(roomId, 1, VoteStatus.DISAGREE)); + .size(getCandidateVoteKey(room.getId(), 1, VoteStatus.DISAGREE)); Long disagreeCountForStation2 = redisTemplate.opsForSet() - .size(getCandidateVoteKey(roomId, 2, VoteStatus.DISAGREE)); + .size(getCandidateVoteKey(room.getId(), 2, VoteStatus.DISAGREE)); assertAll( () -> assertThat(storedVoteCount).isEqualTo(2), () -> assertThat(agreeCountForStation1).isZero(), @@ -103,17 +123,15 @@ void initiateBeforeSaveVote() { @Test void isVotedByMember() { // given - String roomId = "roomId"; - String memberId = "memberId"; - Candidate candidate = new Candidate(roomId, 1); - List votes = List.of(new Vote(candidate, memberId, VoteStatus.AGREE)); + Candidate candidate = new Candidate(room.getId(), 1); + List votes = List.of(new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); saveVotePort.saveAllByMember(votes); for (Vote vote : votes) { saveVotePort.saveByCandidate(vote); } // when - boolean result = voteService.isVotedByMember(roomId, memberId); + boolean result = voteService.isVotedByMember(room.getId(), member.getMemberId()); // then assertThat(result).isTrue(); @@ -122,15 +140,112 @@ void isVotedByMember() { @DisplayName("์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œ๋ฅผ ์™„๋ฃŒํ•˜์ง€ ์•Š์•˜์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void isNotVotedByMember() { + // when + boolean result = voteService.isVotedByMember(room.getId(), member.getMemberId()); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("๋ฐฉ์— ํˆฌํ‘œ๋ฅผ ์™„๋ฃŒํ•œ ์ธ์› ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void countVotedMembers() { // given - String roomId = "roomId"; - String memberId = "memberId"; + List votes = List.of(new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); + List votes2 = List.of(new Vote(candidate, member2.getMemberId(), VoteStatus.AGREE)); + saveVotes(votes); + saveVotes(votes2); // when - boolean result = voteService.isVotedByMember(roomId, memberId); + int count = voteService.countVotedMembers(room.getId()); // then - assertThat(result).isFalse(); + assertThat(count).isEqualTo(2); + } + + @DisplayName("์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void getVotesByMember() { + // given + List votes = List.of( + new Vote(candidate, member.getMemberId(), VoteStatus.AGREE), + new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE) + ); + saveVotePort.saveAllByMember(votes); + for (Vote vote : votes) { + saveVotePort.saveByCandidate(vote); + } + + // when + List result = voteService.getVotesByMember(room.getId(), member.getMemberId()); + + // then + assertThat(result).hasSize(2) + .extracting("stationId", "voteStatus") + .containsExactlyInAnyOrder( + org.assertj.core.api.Assertions.tuple(1L, VoteStatus.AGREE), + org.assertj.core.api.Assertions.tuple(2L, VoteStatus.DISAGREE) + ); + } + + @DisplayName("ํŠน์ • ํˆฌํ‘œ์— ์ฐธ์—ฌํ•œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void getMembersByVote() { + // given + Vote vote = new Vote(candidate, member.getMemberId(), VoteStatus.AGREE); + List votes = List.of(vote); + List votes2 = List.of(new Vote(candidate, member2.getMemberId(), VoteStatus.AGREE)); + saveVotes(votes); + saveVotes(votes2); + + // when + List result = voteService.getMembersByVote(vote); + + // then + assertThat(result).hasSize(2) + .extracting(Member::getMemberId) + .containsExactlyInAnyOrder(member.getMemberId(), member2.getMemberId()); + } + + private void saveVotes(List votes) { + saveVotePort.saveAllByMember(votes); + for (Vote vote : votes) { + saveVotePort.saveByCandidate(vote); + } + } + + @DisplayName("๋ฐฉ์ด ํˆฌํ‘œ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwExceptionWhenRoomNotInVoteStatus() { + // given + Room locationInputRoom = Room.create("inputRoom", 3, member); + roomSaveRedisAdapter.save(locationInputRoom); + + // when & then + assertThatThrownBy(() -> voteService.countVotedMembers(locationInputRoom.getId())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Room is not on vote status"); + } + + @DisplayName("๋ฐฉ์— ์†ํ•˜์ง€ ์•Š์€ ๋ฉค๋ฒ„๊ฐ€ ํˆฌํ‘œํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwExceptionWhenMemberNotInRoom() { + // given + String nonParticipantId = "unknown"; + + // when & then + assertThatThrownBy(() -> voteService.saveVotes(room.getId(), nonParticipantId, List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Cannot find member with id"); + } + + @DisplayName("์•„์ง ํˆฌํ‘œํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œ ๋‚ด์—ญ ์กฐํšŒ ์‹œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwExceptionWhenGetVotesWithoutVoting() { + // when & then + assertThatThrownBy(() -> voteService.getVotesByMember(room.getId(), member.getMemberId())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Not voted by member with id"); } private String getMemberVoteKey(String roomId, String memberId) { diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 6065345a..9ab5956f 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -55,12 +55,16 @@ private static void validateParameter(String roomName, int capacity) { } public boolean shouldEndLocationInput(long locationInputCount, LocalDateTime current) { - return isLocationInput() && + return this.status.isLocationInput() && (isAllLocationInput(locationInputCount) || current.isAfter(locationInputLimitDateTime)); } - private boolean isLocationInput() { - return this.status.isLocationInput(); + public boolean isNotOnVote() { + return this.status.isLocationInput() || this.status.isVoteResultStatus(); + } + + public boolean isVoteClosed() { + return this.status.isVoteResultStatus(); } private boolean isAllLocationInput(long participantCount) { @@ -82,4 +86,8 @@ public void startVote() { public void closeVote() { this.status = RoomStatus.VOTE_RESULT; } + + public int getNotVotedCount(int votedCount) { + return capacity - votedCount; + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java index cc59f2e8..801d2ed2 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java @@ -9,4 +9,8 @@ public enum RoomStatus { public boolean isLocationInput() { return this.equals(LOCATION_INPUT); } + + public boolean isVoteResultStatus() { + return this.equals(VOTE_RESULT); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java index 6d3d404d..7ceb9260 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/port/out/LoadRoomParticipantPort.java @@ -2,10 +2,13 @@ import com.kok.kokcore.room.domain.Member; import java.util.List; +import java.util.Optional; public interface LoadRoomParticipantPort { Long countParticipantsById(String roomId); List findMembersByRoomId(String roomId); + + Optional findByRoomIdAndMemberId(String roomId, String memberId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java index eb482d54..7fb461dd 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java @@ -1,6 +1,7 @@ package com.kok.kokcore.vote.port.out; import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; import java.util.List; public interface LoadVotePort { @@ -8,4 +9,9 @@ public interface LoadVotePort { boolean isExistsByRoomIdAndMemberId(String roomId, String memberId); List findAllByRoomIdAndMemberId(String roomId, String memberId); + + int countMembersByRoomId(String roomId); + + List findMemberIdsByRoomIdAndStationIdAndStatus(String roomId, long stationId, + VoteStatus voteStatus); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java index 2be528e6..dfeeaf3c 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java @@ -1,6 +1,16 @@ package com.kok.kokcore.vote.usecase; +import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.vote.domain.Vote; +import java.util.List; + public interface GetVoteUseCase { boolean isVotedByMember(String roomId, String memberId); + + int countVotedMembers(String roomId); + + List getVotesByMember(String roomId, String memberId); + + List getMembersByVote(Vote vote); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java index 1bc14057..7680eb53 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/SaveVoteUseCase.java @@ -1,9 +1,8 @@ package com.kok.kokcore.vote.usecase; -import com.kok.kokcore.vote.domain.Vote; import java.util.List; public interface SaveVoteUseCase { - void saveVotes(List votes); + void saveVotes(String roomId, String memberId, List agreedStationIds); } diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java index 30d55419..57e0d7e7 100644 --- a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -119,4 +119,98 @@ void hasNotLocationInputEnded() { // then assertThat(result).isFalse(); } + + @DisplayName("ํ˜„์žฌ ์ƒํƒœ๊ฐ€ LOCATION_INPUT์ด๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isLocationInputStatus() { + // given + Room room = Room.create("room", 3, new Member("member", "profile", MemberRole.LEADER)); + + // when + boolean result = room.isNotOnVote(); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("ํ˜„์žฌ ์ƒํƒœ๊ฐ€ VOTE_RESULT์ด๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isVoteClosed() { + // given + Room room = Room.create("room", 3, new Member("member", "profile", MemberRole.LEADER)); + room.closeVote(); // ์ƒํƒœ๋ฅผ VOTE_RESULT๋กœ ๋ณ€๊ฒฝ + + // when + boolean result = room.isVoteClosed(); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("์ฐธ๊ฐ€ ์ธ์›์ด ๊ฝ‰ ์ฐผ์„ ๊ฒฝ์šฐ true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void isFull() { + // given + Room room = Room.create("room", 3, new Member("member", "profile", MemberRole.LEADER)); + + // when + boolean result = room.isFull(3); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„์ด ํ˜„์žฌ๋กœ๋ถ€ํ„ฐ 12์‹œ๊ฐ„ ๋’ค๋กœ ๊ฐฑ์‹ ๋œ๋‹ค.") + @Test + void updateVoteDeadline() { + // given + Room room = Room.create("room", 3, new Member("member", "profile", MemberRole.LEADER)); + LocalDateTime now = LocalDateTime.now().withNano(0); + + // when + room.updateVoteDeadline(now); + + // then + assertThat(room.getVoteLimitDateTime()).isEqualTo(now.plusHours(12)); + } + + @DisplayName("startVote ํ˜ธ์ถœ ์‹œ ์ƒํƒœ๊ฐ€ VOTE๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค.") + @Test + void startVote() { + // given + Room room = Room.create("room", 3, new Member("member", "profile", MemberRole.LEADER)); + + // when + room.startVote(); + + // then + assertThat(room.isNotOnVote()).isFalse(); + } + + @DisplayName("closeVote ํ˜ธ์ถœ ์‹œ ์ƒํƒœ๊ฐ€ VOTE_RESULT๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค.") + @Test + void closeVote() { + // given + Room room = Room.create("room", 3, new Member("member", "profile", MemberRole.LEADER)); + room.startVote(); + + // when + room.closeVote(); + + // then + assertThat(room.isVoteClosed()).isTrue(); + } + + @DisplayName("ํˆฌํ‘œํ•˜์ง€ ์•Š์€ ์ธ์› ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void getNotVotedCount() { + // given + Room room = Room.create("room", 5, new Member("member", "profile", MemberRole.LEADER)); + + // when + int notVoted = room.getNotVotedCount(2); + + // then + assertThat(notVoted).isEqualTo(3); + } } From 40b4c289b25dbda55712093d2aaf4d4cf86a016f Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:01:16 +0900 Subject: [PATCH 124/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20implement?= =?UTF-8?q?=20closing=20vote=20API=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :recycle: refactor: rename method * :sparkles: feat: implement changing room status to VOTE_RESULT API * :bug: fix: fix wrong uri --- .../adapter/in/web/LocationController.java | 3 +- .../service/RoomCommandService.java | 9 ++- .../vote/adapter/in/web/VoteController.java | 9 +++ .../service/RoomCommandServiceTest.java | 81 +++++++++++++++++++ .../room/usecase/UpdateRoomUseCase.java | 4 +- 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java index 0d24e0a0..5a592cf0 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/in/web/LocationController.java @@ -54,8 +54,7 @@ public ResponseEntity> createLocation( Pair centroid = loadCentroidUsecase.readCentroidCoordinates( locationRequest.roomId()); - updateRoomUseCase.startVoteIfLocationInputEnded( - locationRequest.roomId(), LocalDateTime.now()); + updateRoomUseCase.startVote(locationRequest.roomId(), LocalDateTime.now()); return ResponseEntity.ok(ApiResponseDto.success( CentroidResponse.of(locationRequest.roomId(), centroid.getFirst(), centroid.getSecond()) diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java index 55f41f3e..78751d9f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java @@ -35,7 +35,7 @@ public Room createRoom(String roomName, int capacity, Member host) { } @Override - public void startVoteIfLocationInputEnded(String roomId, LocalDateTime current) { + public void startVote(String roomId, LocalDateTime current) { Room room = getRoom(roomId); if (shouldUpdateVoteDeadline(room, current)) { room.updateVoteDeadline(current); @@ -44,6 +44,13 @@ public void startVoteIfLocationInputEnded(String roomId, LocalDateTime current) } } + @Override + public void closeVote(String roomId) { + Room room = getRoom(roomId); + room.closeVote(); + updateRoomPort.update(room); + } + private Room getRoom(String roomId) { return loadRoomPort.findRoomById(roomId) .orElseThrow( diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 4613a3cf..c2ffd100 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -11,6 +11,7 @@ import com.kok.kokapi.vote.application.service.VoteFacadeService; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.usecase.GetRoomUseCase; +import com.kok.kokcore.room.usecase.UpdateRoomUseCase; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.vote.usecase.SaveVoteUseCase; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +32,7 @@ public class VoteController { private final StationFacadeService stationFacadeService; private final GetRoomUseCase getRoomUseCase; private final SaveVoteUseCase saveVoteUseCase; + private final UpdateRoomUseCase updateRoomUseCase; @Operation(summary = "ํˆฌํ‘œ ํ›„๋ณด์ง€ ๋ชฉ๋ก ์กฐํšŒ", description = "๋ฐฉ ID๊ณผ ์‚ฌ์šฉ์ž ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํˆฌํ‘œ ํ›„๋ณด์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/{memberId}/candidates") @@ -70,6 +72,13 @@ public ResponseEntity> getVoteDeadline( return ResponseEntity.ok(ApiResponseDto.success(response)); } + @Operation(summary = "ํˆฌํ‘œ ์ข…๋ฃŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•˜์—ฌ ํˆฌํ‘œ ์ƒํƒœ๋ฅผ ์ข…๋ฃŒ(VOTE_RESULT)๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.") + @PostMapping("/votes/{roomId}/close") + public ResponseEntity> closeVote(@PathVariable String roomId) { + updateRoomUseCase.closeVote(roomId); + return ResponseEntity.ok(ApiResponseDto.success(null)); + } + @Operation(summary = "ํˆฌํ‘œ ๊ฒฐ๊ณผ ์กฐํšŒ", description = "๋ฐฉ ID์™€ ์‚ฌ์šฉ์ž ID์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ๊ธฐ์ค€์œผ๋กœ ํ›„๋ณด์ง€๋ณ„ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/{memberId}") public ResponseEntity> getVoteResult( diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java index df685e73..aa120b94 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java @@ -1,13 +1,24 @@ package com.kok.kokapi.room.application.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import com.kok.kokapi.centroid.adapter.out.persistence.LocationRepository; import com.kok.kokapi.common.template.ServiceTest; +import com.kok.kokapi.fixture.MemberFixture; +import com.kok.kokapi.fixture.PointFixture; +import com.kok.kokapi.room.adapter.out.persistence.RoomParticipantSaveAdapter; +import com.kok.kokapi.room.adapter.out.persistence.RoomQueryRedisAdapter; +import com.kok.kokapi.room.adapter.out.persistence.RoomSaveRedisAdapter; +import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; +import com.kok.kokcore.room.domain.vo.RoomStatus; +import java.time.LocalDateTime; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +27,28 @@ class RoomCommandServiceTest extends ServiceTest { @Autowired private RoomCommandService roomCommandService; + @Autowired + private RoomSaveRedisAdapter roomSaveRedisAdapter; + @Autowired + private RoomQueryRedisAdapter roomQueryRedisAdapter; + @Autowired + private LocationRepository locationRepository; + @Autowired + private RoomParticipantSaveAdapter roomParticipantSaveAdapter; + + private Room room; + private Member member; + private Member member2; + + @BeforeEach + void init() { + member = MemberFixture.createLeader(); + member2 = MemberFixture.createFollower(); + room = Room.create("ํˆฌํ‘œ๋ฐฉ", 2, member); + roomSaveRedisAdapter.save(room); + roomParticipantSaveAdapter.joinRoom(room.getId(), member); + roomParticipantSaveAdapter.joinRoom(room.getId(), member2); + } @DisplayName("์•ฝ์†๋ฐฉ์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @Test @@ -42,4 +75,52 @@ void createRoom() { "๋ฐฉ์žฅ ์—ญํ• ์€ Leader์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") ); } + + @DisplayName("๋ชจ๋“  ์ธ์›์ด ์ถœ๋ฐœ์ง€๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ํˆฌํ‘œ๊ฐ€ ์‹œ์ž‘๋˜๊ณ , ๋งˆ๊ฐ ์‹œ๊ฐ„์ด ๊ฐฑ์‹ ๋œ๋‹ค.") + @Test + void startVote_whenAllLocationsInput() { + // given + Location location = new Location( + room.getId(), + member.getMemberId(), + PointFixture.create(), + "์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ" + ); + Location location2 = new Location( + room.getId(), + member2.getMemberId(), + PointFixture.create(), + "์„œ์šธ์‹œ ๊ฐ•์„œ๊ตฌ" + ); + locationRepository.save(location); + locationRepository.save(location2); + LocalDateTime now = LocalDateTime.now().withNano(0); + + // when + roomCommandService.startVote(room.getId(), now); + + // then + Room updatedRoom = roomQueryRedisAdapter.findRoomById(room.getId()).get(); + + assertAll( + () -> assertThat(updatedRoom.getStatus()).isEqualTo(RoomStatus.VOTE), + () -> assertThat(updatedRoom.getVoteLimitDateTime()).isEqualTo(now.plusHours(12)) + ); + } + + @DisplayName("ํˆฌํ‘œ ์ข…๋ฃŒ ์‹œ ๋ฐฉ ์ƒํƒœ๊ฐ€ VOTE_RESULT๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค.") + @Test + void closeVote() { + // given + room.startVote(); + roomSaveRedisAdapter.update(room); + + // when + roomCommandService.closeVote(room.getId()); + + // then + Room updatedRoom = roomQueryRedisAdapter.findRoomById(room.getId()).get(); + + assertThat(updatedRoom.getStatus()).isEqualTo(RoomStatus.VOTE_RESULT); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java index 202bb984..946c0062 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java @@ -4,5 +4,7 @@ public interface UpdateRoomUseCase { - void startVoteIfLocationInputEnded(String roomId, LocalDateTime current); + void startVote(String roomId, LocalDateTime current); + + void closeVote(String roomId); } From 75471180e7c1006f6c76bbe0a262ac1876f14e65 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 20:16:19 +0900 Subject: [PATCH 125/163] :green_heart: deploy: refactor prod cd script --- .github/workflows/kok-prod-CD.yml | 84 ++++++++++++------- infra/docker-compose-blue.yml | 2 +- infra/docker-compose-env.yml | 33 ++++++++ infra/docker-compose-green.yml | 2 +- .../src/main/resources/application-prod.yml | 16 ++-- 5 files changed, 97 insertions(+), 40 deletions(-) create mode 100644 infra/docker-compose-env.yml diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 5c27a8fb..a1ba6955 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -10,31 +10,50 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Get Latest Docker Image Tag + id: latest_tag + run: | + LATEST_TAG=$(curl -s "https://hub.docker.com/v2/repositories/${{ secrets.DOCKERHUB_USERNAME }}/kok-prod/tags" | jq -r '.results[0].name') + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + - name: Create SSH key file run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem - name: Set permissions for SSH key file run: chmod 600 /tmp/NCP_KEY.pem - - name: Upload `docker-compose.yml` & `nginx.conf` to NCP + - name: Upload Compose files & nginx.conf to NCP run: | - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-prod.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/nginx/nginx.conf ${{ secrets.NCP_USER }}@${{ secrets.NCP_HOST }}:${{ secrets.COMPOSE_FILE_PATH }}/nginx.conf + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-blue.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-green.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-env.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/nginx/nginx.conf ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }}/nginx.conf - - name: Deploy to NCP (Blue-Green Auto Switch) + - name: Deploy to NCP (Blue-Green Auto + DB/Redis Check) uses: appleboy/ssh-action@v1.0.3 with: - host: ${{ secrets.NCP_HOST }} + host: ${{ secrets.NCP_PROD_HOST }} username: ${{ secrets.NCP_USER }} - password: ${{ secrets.NCP_PASSWORD }} key: ${{ secrets.NCP_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} - # ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Blue/Green ํ™˜๊ฒฝ ๊ฐ์ง€ + # .env์— ์ตœ์‹  ํƒœ๊ทธ ๊ธฐ๋ก + sed -i '/^KOK_PROD_TAG=/d' .env || true + echo "KOK_PROD_TAG=${{ env.LATEST_TAG }}" >> .env + + echo "๐Ÿ” ENV ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ..." + if ! docker compose -f docker-compose-env.yml ps | grep -q "Up"; then + echo "๐Ÿš€ ENV ์‹œ์ž‘" + docker compose -f docker-compose-env.yml up -d + else + echo "โœ… ENV ์ด๋ฏธ ์‹คํ–‰ ์ค‘" + fi + + # ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Blue/Green ํ™•์ธ CURRENT_ENV=$(grep -o 'server kok-[a-z]\+' nginx.conf | awk '{print $2}') echo "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํ™˜๊ฒฝ: $CURRENT_ENV" - + if [[ "$CURRENT_ENV" == "kok-blue" ]]; then NEW_ENV="kok-green" OLD_ENV="kok-blue" @@ -46,32 +65,33 @@ jobs: COMPOSE_FILE="docker-compose-blue.yml" NEW_PORT=8081 fi - + echo "์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์œผ๋กœ ๋ฐฐํฌ: $NEW_ENV" - - # ์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์— ์ปจํ…Œ์ด๋„ˆ ๋ฐฐํฌ - sudo docker compose -f $COMPOSE_FILE pull - sudo docker compose -f $COMPOSE_FILE up -d - - # Health Check ์‹คํ–‰ - echo "๐Ÿš€ Health Check ์‹คํ–‰ ์ค‘..." + + # ์ƒˆ ํ™˜๊ฒฝ ๋ฐฐํฌ + docker compose -f $COMPOSE_FILE pull + docker compose -f $COMPOSE_FILE up -d + + echo "๐Ÿฉบ Health Check (30์ดˆ ๋Œ€๊ธฐ)" sleep 30 - if ! curl -s http://localhost:$NEW_PORT/health_check | grep "Success"; then - echo "๐Ÿšจ Health Check ์‹คํŒจ! ๋กค๋ฐฑ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค." - sudo docker compose stop $NEW_ENV - sudo docker compose rm -f $NEW_ENV + HEALTH=$(curl -s http://localhost:$NEW_PORT/v1/api/health) + + CODE=$(echo "$HEALTH" | jq -r '.code') + DATA=$(echo "$HEALTH" | jq -r '.data') + + if [[ "$CODE" != "200" || "$DATA" != "OK" ]]; then + echo "โŒ Health Check ์‹คํŒจ (code: $CODE, data: $DATA), ๋กค๋ฐฑ!" + docker compose -f $COMPOSE_FILE stop $NEW_ENV + docker compose -f $COMPOSE_FILE rm -f $NEW_ENV exit 1 fi - - # โœ… Nginx ์„ค์ • ํŒŒ์ผ ์ž๋™ ์ˆ˜์ • (ํŠธ๋ž˜ํ”ฝ์„ ์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์œผ๋กœ ๋ณ€๊ฒฝ) - echo "โœ… ํŠธ๋ž˜ํ”ฝ์„ $NEW_ENV๋กœ ๋ณ€๊ฒฝ ์ค‘..." - sudo sed -i "s/server $OLD_ENV:8080/server $NEW_ENV:8080/" nginx.conf - - # โœ… ๋ฌด์ค‘๋‹จ์œผ๋กœ Nginx ์„ค์ • ๋ฐ˜์˜ - echo "๐Ÿ”„ Nginx ์„ค์ •์„ ๋ฌด์ค‘๋‹จ์œผ๋กœ ์ ์šฉ..." - sudo docker exec kok-nginx nginx -s reload - # โœ… ๊ธฐ์กด ํ™˜๊ฒฝ ์ค‘์ง€ ๋ฐ ์ •๋ฆฌ - echo "๐Ÿ›‘ ๊ธฐ์กด ํ™˜๊ฒฝ($OLD_ENV) ์ค‘์ง€..." - sudo docker compose stop $OLD_ENV - sudo docker compose rm -f $OLD_ENV + echo "โš™๏ธ nginx.conf ํŠธ๋ž˜ํ”ฝ์„ $NEW_ENV๋กœ ๋ณ€๊ฒฝ" + sed -i "s/server $OLD_ENV:8080/server $NEW_ENV:8080/" nginx.conf + + echo "๐Ÿ”„ Nginx ์„ค์ • reload" + docker exec kok-nginx nginx -s reload + + echo "๐Ÿงน ์ด์ „ ํ™˜๊ฒฝ($OLD_ENV) ์ •๋ฆฌ" + docker compose -f docker-compose-${OLD_ENV#kok-}.yml stop $OLD_ENV + docker compose -f docker-compose-${OLD_ENV#kok-}.yml rm -f $OLD_ENV \ No newline at end of file diff --git a/infra/docker-compose-blue.yml b/infra/docker-compose-blue.yml index f19e7401..b19433ea 100644 --- a/infra/docker-compose-blue.yml +++ b/infra/docker-compose-blue.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-blue: container_name: kok-blue - image: ${DOCKERHUB_USERNAME}/kok-prod:latest + image: ${DOCKERHUB_USERNAME}/kok-prod:${KOK_PROD_TAG} restart: always env_file: - .env diff --git a/infra/docker-compose-env.yml b/infra/docker-compose-env.yml new file mode 100644 index 00000000..fd6611ac --- /dev/null +++ b/infra/docker-compose-env.yml @@ -0,0 +1,33 @@ +services: + # MySQL ์„ค์ • + mysql: + image: mysql:8.4 + container_name: my-mysql + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_ROOT_USER: ${DB_USERNAME} + volumes: + - mysql_data:/var/lib/mysql + networks: + - mynetwork + + # Redis ์„ค์ • + redis: + image: redis:7.0 + container_name: my-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - mynetwork + +volumes: + mysql_data: + redis_data: + +networks: + mynetwork: + driver: bridge diff --git a/infra/docker-compose-green.yml b/infra/docker-compose-green.yml index 04987e06..f27f4c89 100644 --- a/infra/docker-compose-green.yml +++ b/infra/docker-compose-green.yml @@ -3,7 +3,7 @@ version: '3.8' services: kok-green: container_name: kok-green - image: ${DOCKERHUB_USERNAME}/kok-prod:latest + image: ${DOCKERHUB_USERNAME}/kok-prod:${KOK_PROD_TAG} restart: always env_file: - .env diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index fef9aa9b..23473553 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -20,13 +20,17 @@ spring: baseline-on-migrate: true data: redis: - cluster: - nodes: - - ${REDIS_HOST1}:${REDIS_PORT} - - ${REDIS_HOST2}:${REDIS_PORT} - - ${REDIS_HOST3}:${REDIS_PORT} - max-redirects: 3 + host: ${REDIS_HOST} # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. + port: ${REDIS_PORT} timeout: 5000 +# redis: +# cluster: +# nodes: +# - ${REDIS_HOST1}:${REDIS_PORT} +# - ${REDIS_HOST2}:${REDIS_PORT} +# - ${REDIS_HOST3}:${REDIS_PORT} +# max-redirects: 3 +# timeout: 5000 # sentinel: # master: ${REDIS_SENTINEL_MASTER} # nodes: From 835462e9e8307f3e799bc3958f2a0158218a8fff Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 20:22:51 +0900 Subject: [PATCH 126/163] :green_heart: deploy: refactor prod cd with nginx --- .github/workflows/kok-prod-CD.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index a1ba6955..6d52364f 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -28,6 +28,7 @@ jobs: scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-green.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-env.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/nginx/nginx.conf ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }}/nginx.conf + scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-nginx.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - name: Deploy to NCP (Blue-Green Auto + DB/Redis Check) uses: appleboy/ssh-action@v1.0.3 @@ -41,7 +42,16 @@ jobs: # .env์— ์ตœ์‹  ํƒœ๊ทธ ๊ธฐ๋ก sed -i '/^KOK_PROD_TAG=/d' .env || true echo "KOK_PROD_TAG=${{ env.LATEST_TAG }}" >> .env - + + echo "๐Ÿ” nginx ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ..." + if ! docker compose -f docker-compose-nginx.yml ps | grep -q "Up"; then + echo "๐Ÿš€ nginx ์‹œ์ž‘" + docker compose -f docker-compose-nginx.yml up -d + else + echo "โœ… nginx ์ด๋ฏธ ์‹คํ–‰ ์ค‘" + fi + + echo "๐Ÿ” ENV ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ..." if ! docker compose -f docker-compose-env.yml ps | grep -q "Up"; then echo "๐Ÿš€ ENV ์‹œ์ž‘" From 1b43f0ce22251abba03ee53bb2a5c31f15d67bf2 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 20:32:12 +0900 Subject: [PATCH 127/163] :green_heart: deploy: remove nginx.conf at CD script --- .github/workflows/kok-prod-CD.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 6d52364f..13aadc75 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -27,7 +27,6 @@ jobs: scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-blue.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-green.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-env.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/nginx/nginx.conf ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }}/nginx.conf scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-nginx.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - name: Deploy to NCP (Blue-Green Auto + DB/Redis Check) From d2f520f832a7216191443a3a86fead1f13dbfa7d Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 20:40:41 +0900 Subject: [PATCH 128/163] :green_heart: deploy: refactor health check port --- .github/workflows/kok-prod-CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 13aadc75..15a5cf33 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -83,7 +83,7 @@ jobs: echo "๐Ÿฉบ Health Check (30์ดˆ ๋Œ€๊ธฐ)" sleep 30 - HEALTH=$(curl -s http://localhost:$NEW_PORT/v1/api/health) + HEALTH=$(curl -s http://localhost:8080/v1/api/health) CODE=$(echo "$HEALTH" | jq -r '.code') DATA=$(echo "$HEALTH" | jq -r '.data') From 300248cd4ceefa3ad6bb5b6c65d22bff11cf41cb Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 20:55:07 +0900 Subject: [PATCH 129/163] :green_heart: deploy: increase health check time --- .github/workflows/kok-prod-CD.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 15a5cf33..9d8d5afb 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -81,8 +81,8 @@ jobs: docker compose -f $COMPOSE_FILE pull docker compose -f $COMPOSE_FILE up -d - echo "๐Ÿฉบ Health Check (30์ดˆ ๋Œ€๊ธฐ)" - sleep 30 + echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" + sleep 60 HEALTH=$(curl -s http://localhost:8080/v1/api/health) CODE=$(echo "$HEALTH" | jq -r '.code') From b34b265bff70fb21074c83d0e4acf8209743cd45 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 20:56:35 +0900 Subject: [PATCH 130/163] :green_heart: deploy: add health check result --- .github/workflows/kok-prod-CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 9d8d5afb..46211878 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -84,7 +84,7 @@ jobs: echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" sleep 60 HEALTH=$(curl -s http://localhost:8080/v1/api/health) - + echo "Health Check ๊ฒฐ๊ณผ: $HEALTH" CODE=$(echo "$HEALTH" | jq -r '.code') DATA=$(echo "$HEALTH" | jq -r '.data') From 09efbc0ed3c6b4110404047d67efe89efa708557 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 21:05:02 +0900 Subject: [PATCH 131/163] :green_heart: deploy: change docker network external --- infra/docker-compose-blue.yml | 3 +-- infra/docker-compose-env.yml | 8 ++++---- infra/docker-compose-green.yml | 2 +- infra/docker-compose-nginx.yml | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/infra/docker-compose-blue.yml b/infra/docker-compose-blue.yml index b19433ea..c57a7b32 100644 --- a/infra/docker-compose-blue.yml +++ b/infra/docker-compose-blue.yml @@ -13,7 +13,6 @@ services: - "8081:8080" networks: - kok-network - networks: kok-network: - driver: bridge + external: true diff --git a/infra/docker-compose-env.yml b/infra/docker-compose-env.yml index fd6611ac..933e8e94 100644 --- a/infra/docker-compose-env.yml +++ b/infra/docker-compose-env.yml @@ -11,7 +11,7 @@ services: volumes: - mysql_data:/var/lib/mysql networks: - - mynetwork + - kok-network # Redis ์„ค์ • redis: @@ -22,12 +22,12 @@ services: volumes: - redis_data:/data networks: - - mynetwork + - kok-network volumes: mysql_data: redis_data: networks: - mynetwork: - driver: bridge + kok-network: + external: true diff --git a/infra/docker-compose-green.yml b/infra/docker-compose-green.yml index f27f4c89..ec1325f3 100644 --- a/infra/docker-compose-green.yml +++ b/infra/docker-compose-green.yml @@ -16,4 +16,4 @@ services: networks: kok-network: - driver: bridge + external: true diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index b8f00e2f..42f8d7e5 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -16,4 +16,4 @@ services: networks: kok-network: - driver: bridge + external: true From e40785f692961a3deb620ce96cfaa74a1d54511e Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 21:35:57 +0900 Subject: [PATCH 132/163] :green_heart: deploy: change root user --- infra/docker-compose-env.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/docker-compose-env.yml b/infra/docker-compose-env.yml index 933e8e94..cbc88e57 100644 --- a/infra/docker-compose-env.yml +++ b/infra/docker-compose-env.yml @@ -7,7 +7,6 @@ services: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} - MYSQL_ROOT_USER: ${DB_USERNAME} volumes: - mysql_data:/var/lib/mysql networks: From 16fcba8f6f15579d090a5ab1f02f929f7c1bbc20 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 21:50:16 +0900 Subject: [PATCH 133/163] :green_heart: deploy: change nginx port --- infra/docker-compose-nginx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index 42f8d7e5..c293f100 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -10,7 +10,7 @@ services: env_file: - .env ports: - - "80:80" + - "8080:80" networks: - kok-network From f8d24b37aca7a8c375fa6b958e950152fb909263 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 22:03:11 +0900 Subject: [PATCH 134/163] :green_heart: deploy: change nginx root --- infra/docker-compose-nginx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index c293f100..07ba3613 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -6,7 +6,7 @@ services: image: nginx:latest restart: always volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx.conf:/etc/nginx/nginx.conf:ro env_file: - .env ports: From dedd1674b02ce355cfd25d093f016667f4b26f7f Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 22:22:34 +0900 Subject: [PATCH 135/163] :green_heart: deploy: add ssl config --- .github/workflows/kok-prod-CD.yml | 2 +- infra/docker-compose-nginx.yml | 15 ++++++--------- infra/nginx/nginx.conf | 20 ++++++++++++++++++-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 46211878..b8e7f592 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -83,7 +83,7 @@ jobs: echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" sleep 60 - HEALTH=$(curl -s http://localhost:8080/v1/api/health) + HEALTH=$(curl -s https://localhost:443/v1/api/health) echo "Health Check ๊ฒฐ๊ณผ: $HEALTH" CODE=$(echo "$HEALTH" | jq -r '.code') DATA=$(echo "$HEALTH" | jq -r '.data') diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index 07ba3613..97b7a435 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -1,16 +1,13 @@ -version: '3.8' - services: nginx: - container_name: kok-nginx image: nginx:latest - restart: always - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - env_file: - - .env + container_name: kok-nginx ports: - - "8080:80" + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - /etc/letsencrypt:/etc/letsencrypt:ro networks: - kok-network diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf index ae67e087..58ce38e0 100644 --- a/infra/nginx/nginx.conf +++ b/infra/nginx/nginx.conf @@ -5,11 +5,28 @@ events { http { upstream backend { server kok-blue:8080; - # ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ์—์„œ 'server kok-green:8080'์œผ๋กœ ๋ณ€๊ฒฝ๋จ + # server kok-green:8080; โ† ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ์—์„œ ๊ต์ฒด } server { listen 80; + server_name prod-api.kokokok.com; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name prod-api.kokokok.com; + + ssl_certificate /etc/letsencrypt/live/prod-api.kokokok.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/prod-api.kokokok.com/privkey.pem; location / { proxy_pass http://backend; @@ -20,4 +37,3 @@ http { } } } - From ba799f5fb86c9dbb7a5a936804677c3b2c8cfedd Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 22:27:19 +0900 Subject: [PATCH 136/163] :green_heart: deploy: change health check --- .github/workflows/kok-prod-CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index b8e7f592..509de0ea 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -83,7 +83,7 @@ jobs: echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" sleep 60 - HEALTH=$(curl -s https://localhost:443/v1/api/health) + HEALTH=$(curl -s https://prod-api.kokokok.com/v1/api/health) echo "Health Check ๊ฒฐ๊ณผ: $HEALTH" CODE=$(echo "$HEALTH" | jq -r '.code') DATA=$(echo "$HEALTH" | jq -r '.data') From fe8f41ea9f1cbc177934672de671cd0508a87c3f Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 22:30:22 +0900 Subject: [PATCH 137/163] :green_heart: deploy: change nginx path --- infra/docker-compose-nginx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index 97b7a435..5ff2df15 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -6,7 +6,7 @@ services: - "80:80" - "443:443" volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx.conf:/etc/nginx/nginx.conf:ro - /etc/letsencrypt:/etc/letsencrypt:ro networks: - kok-network From 84e6e4919028a1dd8fcbdef7e83ae71c23164032 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 22:33:46 +0900 Subject: [PATCH 138/163] :green_heart: deploy: change nginx path --- infra/docker-compose-nginx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index 5ff2df15..0b46513f 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -3,7 +3,7 @@ services: image: nginx:latest container_name: kok-nginx ports: - - "80:80" + - "8080:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro From 536c8bb974715b73ca5295a8feeb67994d5a486d Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 22:46:55 +0900 Subject: [PATCH 139/163] :green_heart: deploy: change nginx path --- infra/docker-compose-nginx.yml | 5 +++++ infra/nginx/nginx.conf | 15 +++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/infra/docker-compose-nginx.yml b/infra/docker-compose-nginx.yml index 0b46513f..24c014fe 100644 --- a/infra/docker-compose-nginx.yml +++ b/infra/docker-compose-nginx.yml @@ -1,16 +1,21 @@ +version: '3.8' + services: nginx: image: nginx:latest container_name: kok-nginx + restart: always ports: - "8080:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - /etc/letsencrypt:/etc/letsencrypt:ro + - ./certbot:/var/www/certbot networks: - kok-network networks: kok-network: external: true + diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf index 58ce38e0..651da358 100644 --- a/infra/nginx/nginx.conf +++ b/infra/nginx/nginx.conf @@ -3,10 +3,10 @@ events { } http { - upstream backend { - server kok-blue:8080; - # server kok-green:8080; โ† ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ์—์„œ ๊ต์ฒด - } + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; server { listen 80; @@ -25,11 +25,14 @@ http { listen 443 ssl; server_name prod-api.kokokok.com; - ssl_certificate /etc/letsencrypt/live/prod-api.kokokok.com/fullchain.pem; + ssl_certificate /etc/letsencrypt/live/prod-api.kokokok.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/prod-api.kokokok.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + location / { - proxy_pass http://backend; + proxy_pass http://kok-blue:8080; # โ† ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ์—์„œ ์ด ๋ถ€๋ถ„๋งŒ kok-green์œผ๋กœ ๊ต์ฒด proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 698b1510168e92e41916e9a8867f5136ccf20885 Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 23:08:03 +0900 Subject: [PATCH 140/163] :green_heart: deploy: change env name --- .github/workflows/kok-prod-CD.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 509de0ea..58eab47d 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -60,7 +60,7 @@ jobs: fi # ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Blue/Green ํ™•์ธ - CURRENT_ENV=$(grep -o 'server kok-[a-z]\+' nginx.conf | awk '{print $2}') + CURRENT_ENV=$(grep -o 'proxy_pass http://kok-[a-z]\+' nginx.conf | awk -F/ '{print $3}') echo "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํ™˜๊ฒฝ: $CURRENT_ENV" if [[ "$CURRENT_ENV" == "kok-blue" ]]; then @@ -96,7 +96,7 @@ jobs: fi echo "โš™๏ธ nginx.conf ํŠธ๋ž˜ํ”ฝ์„ $NEW_ENV๋กœ ๋ณ€๊ฒฝ" - sed -i "s/server $OLD_ENV:8080/server $NEW_ENV:8080/" nginx.conf + sed -i "s|proxy_pass http://$OLD_ENV:8080;|proxy_pass http://$NEW_ENV:8080;|" nginx.conf echo "๐Ÿ”„ Nginx ์„ค์ • reload" docker exec kok-nginx nginx -s reload From 637a11d14f9796f6b8b386e2e779e3404f7c2dab Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 23:11:28 +0900 Subject: [PATCH 141/163] :green_heart: deploy: enable swagger for test --- kok-api/src/main/resources/application-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 23473553..c73d639a 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -42,7 +42,7 @@ springdoc: default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 swagger-ui: - enabled: false + # enabled: false path: /swagger # open-api: http://data.seoul.go.kr/dataList/OA-21232/S/1/datasetView.do station: From 9d68e65d240209f4bd8a66d5de058280cc9bfabe Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 23:20:54 +0900 Subject: [PATCH 142/163] :green_heart: deploy: enable swagger for test --- kok-api/src/main/resources/application-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index c73d639a..78f34718 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -42,7 +42,7 @@ springdoc: default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 swagger-ui: - # enabled: false + enabled: true path: /swagger # open-api: http://data.seoul.go.kr/dataList/OA-21232/S/1/datasetView.do station: From dddee5dde67a2c5c324976237a898fe352f39afa Mon Sep 17 00:00:00 2001 From: minseokey Date: Tue, 1 Apr 2025 23:58:44 +0900 Subject: [PATCH 143/163] :green_heart: deploy: change nginx reload to restart --- .github/workflows/kok-prod-CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 58eab47d..1cb2df4f 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -99,7 +99,7 @@ jobs: sed -i "s|proxy_pass http://$OLD_ENV:8080;|proxy_pass http://$NEW_ENV:8080;|" nginx.conf echo "๐Ÿ”„ Nginx ์„ค์ • reload" - docker exec kok-nginx nginx -s reload + docker restart kok-nginx echo "๐Ÿงน ์ด์ „ ํ™˜๊ฒฝ($OLD_ENV) ์ •๋ฆฌ" docker compose -f docker-compose-${OLD_ENV#kok-}.yml stop $OLD_ENV From 174e9fa4a4cf635cf9f80f44898ba0c31e43c1ae Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:42:46 +0900 Subject: [PATCH 144/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20implement?= =?UTF-8?q?=20getting=20vote=20final=20result=20API=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: implement controller and response for getting vote final result * :sparkles: feat: implement getting vote deadline service * :recycle: refactor: rename keys for vote prefix * :sparkles: feat: refactor redis data structure and related logics / implement getting most voted station service and adapter * :recycle: refactor: refactor test and separate validation * :wrench: config: use UTC time * :bug: fix: save candidate if not exists before saving votes * :bug: fix: return null if no transportationParsedResponse exists * :sparkles: feat: add isKokRecommended response field on getting candidates api * :sparkles: feat: add location name response field on getting member vote status api --- .../adapter/in/web/StationController.java | 6 + .../in/dto/response/CandidateResponse.java | 34 +++- .../response/MemberVoteStatusResponse.java | 5 +- .../response/VoteResultStationResponse.java | 27 +++ .../vote/adapter/in/web/VoteController.java | 34 +++- .../CandidateCommandRedisAdapter.java | 2 +- .../CandidateQueryRedisAdapter.java | 2 +- .../persistence/VoteCommandRedisAdapter.java | 105 ++++++----- .../vote/adapter/out/persistence/VoteKey.java | 60 +++++++ .../persistence/VoteQueryRedisAdapter.java | 75 ++++---- .../service/VoteFacadeService.java | 49 +++-- .../vote/application/service/VoteService.java | 61 +++++-- .../src/main/resources/application-dev.yml | 2 + .../src/main/resources/application-prod.yml | 2 + .../CandidateCommandRedisAdapterTest.java | 2 +- .../CandidateQueryRedisAdapterTest.java | 2 +- .../VoteCommandRedisAdapterTest.java | 167 ++++++++++-------- .../VoteQueryRedisAdapterTest.java | 105 +++++++---- .../application/service/VoteServiceTest.java | 126 +++++-------- .../src/test/resources/application-test.yml | 2 + .../com/kok/kokcore/room/domain/Room.java | 8 +- .../kokcore/room/domain/vo/RoomStatus.java | 2 +- .../kokcore/vote/domain/vo/VoteStatus.java | 4 + .../kokcore/vote/port/out/DeleteVotePort.java | 20 ++- .../kokcore/vote/port/out/LoadVotePort.java | 2 + .../kokcore/vote/port/out/SaveVotePort.java | 8 +- .../kokcore/vote/usecase/GetVoteUseCase.java | 3 + 27 files changed, 581 insertions(+), 334 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultStationResponse.java create mode 100644 kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java index 17278e38..4f7866c1 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/in/web/StationController.java @@ -25,6 +25,12 @@ public class StationController { private final RetrieveRouteUseCase retrieveRouteUseCase; private final StationFacadeService stationFacadeService; + /** + * 1์ฐจ MVP ๊ธฐ์ค€ ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํšŒ API + * + * @param keyword + * @return + */ @Operation(summary = "์ถ”์ฒœ ์ง€ํ•˜์ฒ ์—ญ ๋ฆฌํ„ด", description = "Recommend subway stations based on the user's location.") @GetMapping("/stations/recommend/{roomId}") public ResponseEntity>> recommendStations( diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java index c5f08c4c..936871d3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/CandidateResponse.java @@ -4,17 +4,19 @@ import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; import java.util.List; +import java.util.Objects; public record CandidateResponse( long stationId, String stationName, List routes, - int totalTime, - int transferCount, + Integer totalTime, + Integer transferCount, + boolean isKokRecommended, List comments ) { - public static CandidateResponse of( + public static CandidateResponse recommended( Station station, List routes, TmapPublicTransportationParsedResponse transportationParsedResponse, @@ -24,8 +26,30 @@ public static CandidateResponse of( station.getId(), station.getName(), getRoutes(routes), - transportationParsedResponse.totalTime(), - transportationParsedResponse.transferCount(), + Objects.nonNull(transportationParsedResponse) ? + transportationParsedResponse.totalTime() : null, + Objects.nonNull(transportationParsedResponse) ? + transportationParsedResponse.transferCount() : null, + true, + comments + ); + } + + public static CandidateResponse custom( + Station station, + List routes, + TmapPublicTransportationParsedResponse transportationParsedResponse, + List comments //comment ๋„๋ฉ”์ธ ๊ตฌํ˜„ ์‹œ List๋กœ ๊ต์ฒด + ) { + return new CandidateResponse( + station.getId(), + station.getName(), + getRoutes(routes), + Objects.nonNull(transportationParsedResponse) ? + transportationParsedResponse.totalTime() : null, + Objects.nonNull(transportationParsedResponse) ? + transportationParsedResponse.transferCount() : null, + false, comments ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java index 24cce19f..79531a37 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/MemberVoteStatusResponse.java @@ -1,19 +1,22 @@ package com.kok.kokapi.vote.adapter.in.dto.response; +import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.room.domain.Member; public record MemberVoteStatusResponse( String memberId, String nickname, String imageUrl, + String address, boolean isVoted ) { - public static MemberVoteStatusResponse of(Member member, boolean isVoted) { + public static MemberVoteStatusResponse of(Member member, Location location, boolean isVoted) { return new MemberVoteStatusResponse( member.getMemberId(), member.getNickname(), member.getProfile(), + location.getName(), isVoted ); } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultStationResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultStationResponse.java new file mode 100644 index 00000000..83a87485 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultStationResponse.java @@ -0,0 +1,27 @@ +package com.kok.kokapi.vote.adapter.in.dto.response; + +import com.kok.kokcore.station.domain.entity.Route; +import com.kok.kokcore.station.domain.entity.Station; +import java.math.BigDecimal; +import java.util.List; + +public record VoteResultStationResponse( + long id, + String name, + BigDecimal latitude, + BigDecimal longitude, + long priority, + List routes +) { + + public static VoteResultStationResponse of(Station station, List routes) { + return new VoteResultStationResponse( + station.getId(), + station.getName(), + station.getLatitude(), + station.getLongitude(), + station.getPriority(), + routes.stream().map(Route::getName).toList() + ); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index c2ffd100..546c052c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -8,12 +8,17 @@ import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; import com.kok.kokapi.vote.adapter.in.dto.response.VoteDeadlineResponse; import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultStationResponse; import com.kok.kokapi.vote.application.service.VoteFacadeService; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.room.usecase.UpdateRoomUseCase; +import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; -import com.kok.kokcore.vote.usecase.SaveVoteUseCase; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.usecase.SystemRecommendUseCase; +import com.kok.kokcore.station.usecase.UserRecommendUseCase; +import com.kok.kokcore.vote.usecase.GetVoteUseCase; import io.swagger.v3.oas.annotations.Operation; import java.time.LocalDateTime; import java.util.List; @@ -31,16 +36,20 @@ public class VoteController { private final VoteFacadeService voteFacadeService; private final StationFacadeService stationFacadeService; private final GetRoomUseCase getRoomUseCase; - private final SaveVoteUseCase saveVoteUseCase; + private final GetVoteUseCase getVoteUseCase; + private final RetrieveRouteUseCase retrieveRouteUseCase; private final UpdateRoomUseCase updateRoomUseCase; + private final SystemRecommendUseCase systemRecommendedUseCase; + private final UserRecommendUseCase userRecommendUseCase; @Operation(summary = "ํˆฌํ‘œ ํ›„๋ณด์ง€ ๋ชฉ๋ก ์กฐํšŒ", description = "๋ฐฉ ID๊ณผ ์‚ฌ์šฉ์ž ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํˆฌํ‘œ ํ›„๋ณด์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/{memberId}/candidates") public ResponseEntity>> getCandidates( @PathVariable String roomId, @PathVariable String memberId) { - List stations = stationFacadeService.getCandidateStation(roomId); - List responses = voteFacadeService.getCandidates(roomId, memberId, - stations); + List recommendedStations = systemRecommendedUseCase.systemRecommendStation(roomId); + List customStations = userRecommendUseCase.getUserRecommendStation(roomId); + List responses = voteFacadeService.getCandidates( + roomId, memberId, recommendedStations, customStations); return ResponseEntity.ok(ApiResponseDto.success(responses)); } @@ -51,7 +60,8 @@ public ResponseEntity> createVote( @PathVariable String memberId, @RequestBody VoteRequest voteRequest ) { - saveVoteUseCase.saveVotes(roomId, memberId, voteRequest.agreedStationIds()); + List stations = stationFacadeService.getCandidateStation(roomId); + voteFacadeService.saveVotes(roomId, memberId, voteRequest.agreedStationIds(), stations); return ResponseEntity.ok(ApiResponseDto.success(null)); } @@ -63,7 +73,7 @@ public ResponseEntity>> getMemberV return ResponseEntity.ok(ApiResponseDto.success(responses)); } - @Operation(summary = "ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•œ ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @Operation(summary = "ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•œ ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„(UTC)์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/deadline") public ResponseEntity> getVoteDeadline( @PathVariable String roomId) { @@ -86,4 +96,14 @@ public ResponseEntity> getVoteResult( VoteResultResponse response = voteFacadeService.getVoteResult(roomId, memberId); return ResponseEntity.ok(ApiResponseDto.success(response)); } + + @Operation(summary = "ํˆฌํ‘œ ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•œ ์ตœ์ข… ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/votes/{roomId}/results") + public ResponseEntity> getFinalResult( + @PathVariable String roomId) { + Station station = getVoteUseCase.getVoteFinalResult(roomId); + List routes = retrieveRouteUseCase.retrieveRoutes(station); + VoteResultStationResponse response = VoteResultStationResponse.of(station, routes); + return ResponseEntity.ok(ApiResponseDto.success(response)); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java index 7f09081b..4a8890fa 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapter.java @@ -13,7 +13,7 @@ @RequiredArgsConstructor public class CandidateCommandRedisAdapter implements SaveCandidatePort { - private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + private static final String CANDIDATE_KEY_FORMAT = "candidate:%s"; private final RedisTemplate redisTemplate; diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java index e279fa46..3a425788 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapter.java @@ -16,7 +16,7 @@ @RequiredArgsConstructor public class CandidateQueryRedisAdapter implements LoadCandidatePort { - private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + private static final String CANDIDATE_KEY_FORMAT = "candidate:%s"; private final RedisTemplate redisTemplate; diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java index 31f73790..2600cfd4 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java @@ -5,9 +5,7 @@ import com.kok.kokcore.vote.port.out.DeleteVotePort; import com.kok.kokcore.vote.port.out.SaveVotePort; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -18,32 +16,44 @@ @RequiredArgsConstructor public class VoteCommandRedisAdapter implements SaveVotePort, DeleteVotePort { - private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; - private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; - private final RedisTemplate redisTemplate; @Override - public void saveByCandidate(Vote vote) { - String key = getCandidateVoteKey(vote); - RedisExecutor.runOrThrow("saveByCandidate", () -> + public void saveVoteMemberHash(List votes) { + validate(votes); + String roomId = votes.getFirst().getRoomId(); + String memberId = votes.getFirst().getMemberId(); + String memberHashKey = VoteKey.memberKey(roomId, memberId); + RedisExecutor.runOrThrow("saveVoteMemberHash", () -> { + for (Vote vote : votes) { + String field = VoteKey.stationField(vote.getStationId()); + String status = vote.getVoteStatus().getName(); + redisTemplate.opsForHash().put(memberHashKey, field, status); + } + }); + } + + @Override + public void saveVotedMemberSet(String roomId, String memberId) { + String votedMemberSetKey = VoteKey.voteKey(roomId); + RedisExecutor.runOrThrow("saveVotedMemberSet", () -> + redisTemplate.opsForSet().add(votedMemberSetKey, memberId) + ); + } + + @Override + public void saveVoteStatusSet(Vote vote) { + String key = VoteKey.voteStatusMemberSetKey(vote); + RedisExecutor.runOrThrow("saveVoteStatusSet", () -> redisTemplate.opsForSet().add(key, vote.getMemberId()) ); } @Override - public void saveAllByMember(List votes) { - validate(votes); - String roomId = votes.getFirst().getRoomId(); - String memberId = votes.getFirst().getMemberId(); - String key = getMemberVoteKey(roomId, memberId); - Map value = votes.stream() - .collect(Collectors.toMap( - Vote::getStationId, - vote -> vote.getVoteStatus().getName() - )); - RedisExecutor.runOrThrow("saveAllByMember", () -> - redisTemplate.opsForHash().putAll(key, value) + public void incrementVoteStatusCountZSet(Vote vote) { + String key = VoteKey.voteStatusCountZSetKey(vote); + RedisExecutor.runOrThrow("incrementVoteStatusCountZSet", () -> + redisTemplate.opsForZSet().incrementScore(key, vote.getStationId(), 1) ); } @@ -54,45 +64,34 @@ private void validate(List votes) { } @Override - public void deleteByCandidate(Vote vote) { - String key = getCandidateVoteKey(vote); - RedisExecutor.runOrThrow("deleteByCandidate", () -> { - Long result = redisTemplate.opsForSet().remove(key, vote.getMemberId()); - if (isNotRemoved(result)) { - log.warn( - "Failed to remove memberId from candidate set or key not found: key={}, memberId={}", - key, - vote.getMemberId() - ); - } - }); - } - - private static boolean isNotRemoved(Long removed) { - return Objects.isNull(removed) || removed == 0L; + public void removeMemberFromVoteStatusSet(Vote vote) { + String key = VoteKey.voteStatusMemberSetKey(vote); + RedisExecutor.runOrThrow("removeMemberFromVoteStatusSet", () -> + redisTemplate.opsForSet().remove(key, vote.getMemberId()) + ); } @Override - public void deleteAllByRoomIdAndMemberId(String roomId, String memberId) { - String key = getMemberVoteKey(roomId, memberId); - RedisExecutor.runOrThrow("deleteAllRoomIdAndMemberId", () -> { - Boolean result = redisTemplate.delete(key); - if (isNotRemoved(result)) { - log.warn("Key not found or already expired: {}", key); - } - }); - } - - private static boolean isNotRemoved(Boolean result) { - return result.equals(Boolean.FALSE); + public void decrementVoteCountInZSet(Vote vote) { + String key = VoteKey.voteStatusCountZSetKey(vote); + RedisExecutor.runOrThrow("decrementVoteCountInZSet", () -> + redisTemplate.opsForZSet().incrementScore(key, vote.getStationId(), -1) + ); } - private String getMemberVoteKey(String roomId, String memberId) { - return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); + @Override + public void deleteMemberVoteHash(String roomId, String memberId) { + String key = VoteKey.memberKey(roomId, memberId); + RedisExecutor.runOrThrow("deleteMemberVoteHash", () -> + redisTemplate.delete(key) + ); } - private String getCandidateVoteKey(Vote vote) { - return String.format(CANDIDATE_VOTE_KEY_FORMAT, vote.getRoomId(), vote.getStationId(), - vote.getVoteStatus().getName()); + @Override + public void removeMemberFromVotedSet(String roomId, String memberId) { + String key = VoteKey.voteKey(roomId); + RedisExecutor.runOrThrow("removeMemberFromVotedSet", () -> + redisTemplate.opsForSet().remove(key, memberId) + ); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java new file mode 100644 index 00000000..51e2fb75 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java @@ -0,0 +1,60 @@ +package com.kok.kokapi.vote.adapter.out.persistence; + +import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.vo.VoteStatus; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum VoteKey { + + VOTE_MEMBER_SET("vote:%s"), // Set + MEMBER_HASH("member:%s:%s"), // Hash + AGREE_COUNT_ZSET("agree:%s"), // ZSET + AGREE_MEMBER_SET("agree:%s:%d"), // Set per station + DISAGREE_COUNT_ZSET("disagree:%s"), // ZSET + DISAGREE_MEMBER_SET("disagree:%s:%d"); // Set per station + + private final String format; + + // ํˆฌํ‘œ ์™„๋ฃŒ์ž ๋ชฉ๋ก key + public static String voteKey(String roomId) { + return String.format(VOTE_MEMBER_SET.format, roomId); + } + + // ๋ฉค๋ฒ„๋ณ„ ํˆฌํ‘œ ์ƒ์„ธ ์ €์žฅ key + public static String memberKey(String roomId, String memberId) { + return String.format(MEMBER_HASH.format, roomId, memberId); + } + + // ์ฐฌ์„ฑ/๋ฐ˜๋Œ€ ZSET key (stationId๋ณ„ ํˆฌํ‘œ ์ˆ˜ ์ €์žฅ) + public static String voteStatusCountZSetKey(Vote vote) { + return vote.getVoteStatus().isAgree() + ? String.format(AGREE_COUNT_ZSET.format, vote.getRoomId()) + : String.format(DISAGREE_COUNT_ZSET.format, vote.getRoomId()); + } + + public static String voteStatusCountZSetKey(VoteStatus voteStatus, String roomId) { + return voteStatus.isAgree() + ? String.format(AGREE_COUNT_ZSET.format, roomId) + : String.format(DISAGREE_COUNT_ZSET.format, roomId); + } + + // ์ฐฌ์„ฑ/๋ฐ˜๋Œ€ Set key (stationId๋ณ„ memberId ์ €์žฅ) + public static String voteStatusMemberSetKey(Vote vote) { + return vote.getVoteStatus().isAgree() + ? String.format(AGREE_MEMBER_SET.format, vote.getRoomId(), vote.getStationId()) + : String.format(DISAGREE_MEMBER_SET.format, vote.getRoomId(), vote.getStationId()); + } + + public static String voteStatusMemberSetKey( + VoteStatus voteStatus, String roomId, long stationId) { + return voteStatus.isAgree() + ? String.format(AGREE_MEMBER_SET.format, roomId, stationId) + : String.format(DISAGREE_MEMBER_SET.format, roomId, stationId); + } + + // ํ•„๋“œ ์ด๋ฆ„์œผ๋กœ ์“ฐ์ผ stationId + public static String stationField(long stationId) { + return String.valueOf(stationId); + } +} diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java index 243201cc..938ee0aa 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java @@ -8,12 +8,12 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Repository; @Slf4j @@ -21,15 +21,11 @@ @RequiredArgsConstructor public class VoteQueryRedisAdapter implements LoadVotePort { - private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; - private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; - private static final int MAX_COUNT = 20; - private final RedisTemplate redisTemplate; @Override public boolean isExistsByRoomIdAndMemberId(String roomId, String memberId) { - String key = getMemberVoteKey(roomId, memberId); + String key = VoteKey.memberKey(roomId, memberId); return RedisExecutor.runOrElseGet("isExistsByRoomIdAndMemberId", () -> !redisTemplate.opsForHash().entries(key).isEmpty(), false ); @@ -37,15 +33,12 @@ public boolean isExistsByRoomIdAndMemberId(String roomId, String memberId) { @Override public List findAllByRoomIdAndMemberId(String roomId, String memberId) { - String key = getMemberVoteKey(roomId, memberId); + String key = VoteKey.memberKey(roomId, memberId); return RedisExecutor.runOrElseGet("findAllByRoomIdAndMemberId", () -> { Map voteInfos = redisTemplate.opsForHash().entries(key); - List votes = new ArrayList<>(voteInfos.size()); + List votes = new ArrayList<>(); for (Entry voteInfo : voteInfos.entrySet()) { Long stationId = getStationId(voteInfo); - if (stationId == null) { - continue; - } String voteStatus = String.valueOf(voteInfo.getValue()); votes.add(new Vote(roomId, stationId, memberId, voteStatus)); } @@ -55,51 +48,49 @@ public List findAllByRoomIdAndMemberId(String roomId, String memberId) { @Override public int countMembersByRoomId(String roomId) { - String pattern = getMemberVoteKey(roomId, "*"); return RedisExecutor.runOrElseGet("countMembersByRoomId", () -> { - int count = 0; - ScanOptions options = ScanOptions.scanOptions() - .match(pattern) - .count(MAX_COUNT) - .build(); - - try (Cursor cursor = redisTemplate.scan(options)) { - while (cursor.hasNext()) { - count++; - cursor.next(); - } - } - - return count; + String key = VoteKey.voteKey(roomId); + Long count = redisTemplate.opsForSet().size(key); + return Objects.nonNull(count) ? count.intValue() : 0; }, 0); } @Override public List findMemberIdsByRoomIdAndStationIdAndStatus( String roomId, long stationId, VoteStatus voteStatus) { - String key = getCandidateVoteKey(roomId, stationId, voteStatus); + String key = VoteKey.voteStatusMemberSetKey(voteStatus, roomId, stationId); Set memberIds = RedisExecutor.runOrElseGet( "findMembersByRoomIdAndStationIdAndStatus", - () -> redisTemplate.opsForSet().members(key), - Set.of() - ); + () -> redisTemplate.opsForSet().members(key), Set.of()); return memberIds.stream().map(memberId -> (String) memberId).toList(); } - private Long getStationId(Entry voteInfo) { - try { - return Long.valueOf(voteInfo.getKey().toString()); - } catch (NumberFormatException e) { - log.warn("Invalid stationId format in Redis: {}", voteInfo.getKey(), e); - return null; - } + @Override + public long getFirstStationIdByRoomIdAndVoteStatus(String roomId, VoteStatus voteStatus) { + return RedisExecutor.runOrElseGet("getFirstStationIdByRoomIdAndVoteStatus", () -> { + String key = VoteKey.voteStatusCountZSetKey(voteStatus, roomId); + Set> sorted = + redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0); + + if (Objects.isNull(sorted) || sorted.isEmpty()) { + return -1L; + } + + Object maxScoredStationId = sorted.iterator().next().getValue(); + return getStationId(String.valueOf(maxScoredStationId)); + }, -1L); } - private String getMemberVoteKey(String roomId, String memberId) { - return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); + private Long getStationId(Entry voteInfo) { + return getStationId(voteInfo.getKey().toString()); } - private String getCandidateVoteKey(String roomId, long stationId, VoteStatus voteStatus) { - return String.format(CANDIDATE_VOTE_KEY_FORMAT, roomId, stationId, voteStatus.getName()); + private Long getStationId(String stationId) { + try { + return Long.valueOf(stationId); + } catch (NumberFormatException e) { + log.warn("Invalid stationId format in Redis: {}", stationId, e); + throw new IllegalArgumentException("Invalid stationId format: " + stationId); + } } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index d75a819a..baaf5afd 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -8,6 +8,8 @@ import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; import com.kok.kokapi.vote.adapter.in.dto.response.ResultResponse; import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultResponse; +import com.kok.kokcore.location.domain.Location; +import com.kok.kokcore.location.usecase.ReadLocationUseCase; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.usecase.GetRoomUseCase; @@ -15,13 +17,14 @@ import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.usecase.GetStationUseCase; import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; -import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; import com.kok.kokcore.vote.usecase.GetVoteUseCase; +import com.kok.kokcore.vote.usecase.SaveVoteUseCase; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -36,18 +39,36 @@ public class VoteFacadeService { private final GetRoomUseCase getRoomUseCase; private final TmapPublicTransportationService tmapPublicTransportationService; private final ObjectMapper objectMapper; + private final SaveVoteUseCase saveVoteUseCase; + private final ReadLocationUseCase readLocationUseCase; + + public List getCandidates( + String roomId, String memberId, + List recommendedStations, + List customStations) { + List allStations = Stream.concat( + recommendedStations.stream(), customStations.stream() + ).toList(); + getCandidateUseCase.saveAndGetCandidates(roomId, allStations); - public List getCandidates(String roomId, String memberId, - List stations) { - List candidates = getCandidateUseCase.saveAndGetCandidates(roomId, stations); List responses = new ArrayList<>(); - for (Candidate candidate : candidates) { - Station station = getStationUseCase.getStation(candidate.getStationId()); + responses.addAll(createCandidateResponses(recommendedStations, roomId, memberId, true)); + responses.addAll(createCandidateResponses(customStations, roomId, memberId, false)); + + return responses; + } + + + private List createCandidateResponses( + List stations, String roomId, String memberId, boolean isRecommended) { + List responses = new ArrayList<>(); + for (Station station : stations) { List routes = retrieveRouteUseCase.retrieveRoutes(station); - TmapPublicTransportationParsedResponse transportationParsedResponse = getTransportationParsedResponse( + TmapPublicTransportationParsedResponse transportation = getTransportationParsedResponse( roomId, memberId, station); - CandidateResponse response = CandidateResponse.of( - station, routes, transportationParsedResponse, List.of()); + CandidateResponse response = isRecommended + ? CandidateResponse.recommended(station, routes, transportation, List.of()) + : CandidateResponse.custom(station, routes, transportation, List.of()); responses.add(response); } return responses; @@ -73,7 +94,9 @@ public List getMemberVoteStatus(String roomId) { List responses = new ArrayList<>(); for (Member member : members) { boolean isVoted = getVoteUseCase.isVotedByMember(roomId, member.getMemberId()); - MemberVoteStatusResponse response = MemberVoteStatusResponse.of(member, isVoted); + Location location = readLocationUseCase.readLocation(roomId, member.getMemberId()); + MemberVoteStatusResponse response = MemberVoteStatusResponse.of( + member, location, isVoted); responses.add(response); } return responses; @@ -92,4 +115,10 @@ public VoteResultResponse getVoteResult(String roomId, String memberId) { } return new VoteResultResponse(room.getNotVotedCount(votedCount), responses); } + + public void saveVotes( + String roomId, String memberId, List agreedStationIds, List stations) { + getCandidateUseCase.saveAndGetCandidates(roomId, stations); + saveVoteUseCase.saveVotes(roomId, memberId, agreedStationIds); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java index 402078fd..d93f3be9 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -4,6 +4,8 @@ import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; import com.kok.kokcore.room.port.out.LoadRoomPort; +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.port.out.RetrieveStationsPort; import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.domain.vo.VoteStatus; @@ -28,16 +30,25 @@ public class VoteService implements SaveVoteUseCase, GetVoteUseCase { private final LoadCandidatePort loadCandidatePort; private final DeleteVotePort deleteVotePort; private final LoadRoomPort loadRoomPort; + private final RetrieveStationsPort retrieveStationsPort; private final LoadRoomParticipantPort loadRoomParticipantPort; @Override public void saveVotes(String roomId, String memberId, List agreedStationIds) { validate(roomId, memberId); + validateRoomStatusIfNotOnVote(roomId); initiate(roomId, memberId); List votes = getVotes(roomId, memberId, agreedStationIds); - saveVotePort.saveAllByMember(votes); + // 1. ๋ฉค๋ฒ„์˜ ํˆฌํ‘œ ๋‚ด์šฉ Hash ์ €์žฅ + saveVotePort.saveVoteMemberHash(votes); + + // 2. ํˆฌํ‘œ ์™„๋ฃŒ Set์— ๋ฉค๋ฒ„ ์ถ”๊ฐ€ + saveVotePort.saveVotedMemberSet(roomId, memberId); + + // 3. ๊ฐ ํ›„๋ณด์— ๋Œ€ํ•œ Set/ZSet ์—…๋ฐ์ดํŠธ for (Vote vote : votes) { - saveVotePort.saveByCandidate(vote); + saveVotePort.saveVoteStatusSet(vote); + saveVotePort.incrementVoteStatusCountZSet(vote); } } @@ -61,8 +72,13 @@ private static boolean isAgree(List agreedStationIds, Candidate candidate) private void initiate(String roomId, String memberId) { if (loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { List votes = loadVotePort.findAllByRoomIdAndMemberId(roomId, memberId); - votes.forEach(deleteVotePort::deleteByCandidate); - deleteVotePort.deleteAllByRoomIdAndMemberId(roomId, memberId); + for (Vote vote : votes) { + deleteVotePort.removeMemberFromVoteStatusSet(vote); + deleteVotePort.decrementVoteCountInZSet(vote); + } + + deleteVotePort.deleteMemberVoteHash(roomId, memberId); + deleteVotePort.removeMemberFromVotedSet(roomId, memberId); } } @@ -74,6 +90,7 @@ public boolean isVotedByMember(String roomId, String memberId) { @Override public int countVotedMembers(String roomId) { validate(roomId); + validateRoomStatusIfNotOnVote(roomId); return loadVotePort.countMembersByRoomId(roomId); } @@ -98,6 +115,18 @@ private List getMembers(List memberIds, String roomId) { .toList(); } + @Override + public Station getVoteFinalResult(String roomId) { + validate(roomId); + Room room = getRoom(roomId); + validateRoomStatusIfVoteClosed(room); + long stationId = loadVotePort.getFirstStationIdByRoomIdAndVoteStatus( + roomId, VoteStatus.AGREE); + return retrieveStationsPort.retrieveStation(stationId) + .orElseThrow( + () -> new IllegalArgumentException("Station not found with id " + stationId)); + } + private void validate(String roomId, String memberId) { validate(roomId); List memberIds = loadRoomParticipantPort.findMembersByRoomId(roomId).stream() @@ -105,15 +134,18 @@ private void validate(String roomId, String memberId) { .toList(); if (!memberIds.contains(memberId)) { throw new IllegalArgumentException( - String.format("Cannot find member with id: %s, in room with id: %s", memberId, - roomId)); + String.format("Member not found with id: %s, in room with id: %s", + memberId, roomId)); } } private void validate(String roomId) { if (!loadRoomPort.isExistsByRoomId(roomId)) { - throw new IllegalArgumentException("Cannot find room with roomId: " + roomId); + throw new IllegalArgumentException("Room not found with id: " + roomId); } + } + + private void validateRoomStatusIfNotOnVote(String roomId) { Room room = getRoom(roomId); if (room.isNotOnVote()) { throw new IllegalStateException( @@ -121,16 +153,23 @@ private void validate(String roomId) { } } + private void validateVote(String roomId, String memberId) { + if (!loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { + throw new IllegalArgumentException( + String.format("Not voted by member with id: %s, in room with id: %s", + memberId, roomId)); + } + } + private Room getRoom(String roomId) { return loadRoomPort.findRoomById(roomId) .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); } - private void validateVote(String roomId, String memberId) { - if (!loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { + private static void validateRoomStatusIfVoteClosed(Room room) { + if (!room.isVoteClosed()) { throw new IllegalArgumentException( - String.format("Not voted by member with id: %s, in room with id: %s", memberId, - roomId)); + "Vote is not closed for room with id: " + room.getId()); } } } diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 85043e0f..8c4a9471 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -1,4 +1,6 @@ spring: + jackson: + time-zone: UTC datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DB_URL} diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 78f34718..30c6650e 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -1,4 +1,6 @@ spring: + jackson: + time-zone: UTC datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DB_URL} diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java index cdbf159e..1b432ded 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateCommandRedisAdapterTest.java @@ -13,7 +13,7 @@ class CandidateCommandRedisAdapterTest extends RepositoryTest { - private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + private static final String CANDIDATE_KEY_FORMAT = "candidate:%s"; @Autowired private CandidateCommandRedisAdapter candidateCommandRedisAdapter; diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java index 45f4e1ab..a88980c2 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/CandidateQueryRedisAdapterTest.java @@ -12,7 +12,7 @@ class CandidateQueryRedisAdapterTest extends RepositoryTest { - private static final String CANDIDATE_KEY_FORMAT = "vote:%s:candidates"; + private static final String CANDIDATE_KEY_FORMAT = "candidate:%s"; @Autowired private CandidateQueryRedisAdapter candidateQueryRedisAdapter; diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java index 933750be..94c1b168 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java @@ -17,122 +17,145 @@ class VoteCommandRedisAdapterTest extends RepositoryTest { - private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; - private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; - @Autowired private VoteCommandRedisAdapter voteCommandRedisAdapter; + @Autowired private RedisTemplate redisTemplate; - @DisplayName("ํ›„๋ณด๋ณ„๋กœ ์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค.") @Test - void saveVoteByCandidate() { + @DisplayName("๋ฉค๋ฒ„๋ณ„ ํˆฌํ‘œ ๋‚ด์šฉ์„ Hash๋กœ ์ €์žฅํ•œ๋‹ค.") + void saveVoteMemberHash() { // given - String existingMemberId = "memberId2"; + String roomId = "roomId"; String memberId = "memberId"; - Candidate candidate = new Candidate("roomId", 1); - Vote vote = new Vote(candidate, memberId, VoteStatus.AGREE); - String key = getCandidateVoteKey(vote); - redisTemplate.opsForSet().add(key, existingMemberId); + List votes = List.of( + new Vote(new Candidate(roomId, 1), memberId, VoteStatus.AGREE), + new Vote(new Candidate(roomId, 2), memberId, VoteStatus.DISAGREE) + ); // when - voteCommandRedisAdapter.saveByCandidate(vote); + voteCommandRedisAdapter.saveVoteMemberHash(votes); // then - Set memberIds = redisTemplate.opsForSet().members(key); - assertThat(memberIds).containsExactlyInAnyOrder(existingMemberId, memberId); + String key = VoteKey.memberKey(roomId, memberId); + Map result = redisTemplate.opsForHash().entries(key); + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.of( + "1", VoteStatus.AGREE.getName(), + "2", VoteStatus.DISAGREE.getName())); } - @DisplayName("์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•œ๋‹ค.") @Test - void saveAllByCandidateVotesOfMember() { + @DisplayName("ํˆฌํ‘œ ์™„๋ฃŒ์ž Set์— ๋ฉค๋ฒ„๋ฅผ ์ €์žฅํ•œ๋‹ค.") + void saveVotedMemberSet() { // given String roomId = "roomId"; String memberId = "memberId"; - Candidate candidate = new Candidate(roomId, 1); - Candidate candidate2 = new Candidate(roomId, 2); - Candidate candidate3 = new Candidate(roomId, 3); - List votes = List.of( - new Vote(candidate, memberId, VoteStatus.AGREE), - new Vote(candidate2, memberId, VoteStatus.DISAGREE), - new Vote(candidate3, memberId, VoteStatus.DISAGREE) - ); // when - voteCommandRedisAdapter.saveAllByMember(votes); + voteCommandRedisAdapter.saveVotedMemberSet(roomId, memberId); // then - Map result = redisTemplate.opsForHash() - .entries(getMemberVoteKey(roomId, memberId)); - assertThat(result).containsAllEntriesOf( - Map.of( - votes.get(0).getStationId(), votes.get(0).getVoteStatus().getName(), - votes.get(1).getStationId(), votes.get(1).getVoteStatus().getName(), - votes.get(2).getStationId(), votes.get(2).getVoteStatus().getName() - ) - ); + Set result = redisTemplate.opsForSet().members(VoteKey.voteKey(roomId)); + assertThat(result).containsExactlyInAnyOrder(memberId); } - @DisplayName("ํ›„๋ณด๋ณ„๋กœ ์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์‚ญ์ œํ•œ๋‹ค.") @Test - void deleteVoteByCandidate() { + @DisplayName("์ฐฌ/๋ฐ˜ Set์— ๋ฉค๋ฒ„๋ฅผ ์ €์žฅํ•œ๋‹ค.") + void saveVoteStatusSet() { // given - String memberId = "memberId"; - String memberId2 = "memberId2"; - Candidate candidate = new Candidate("roomId", 1); - Vote vote = new Vote(candidate, memberId, VoteStatus.AGREE); - String key = getCandidateVoteKey(vote); - redisTemplate.opsForSet().add(key, memberId, memberId2); + Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); + + // when + voteCommandRedisAdapter.saveVoteStatusSet(vote); + + // then + Set result = redisTemplate.opsForSet() + .members(VoteKey.voteStatusMemberSetKey(vote)); + assertThat(result).containsExactlyInAnyOrder("memberId"); + } + + @Test + @DisplayName("ZSet์— ์ฐฌ/๋ฐ˜ ๋“ํ‘œ์ˆ˜๋ฅผ 1 ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.") + void incrementVoteStatusCountZSet() { + // given + Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); + + // when + voteCommandRedisAdapter.incrementVoteStatusCountZSet(vote); + + // then + Double score = redisTemplate.opsForZSet() + .score(VoteKey.voteStatusCountZSetKey(vote), vote.getStationId()); + + assertThat(score).isEqualTo(1.0); + } + + @Test + @DisplayName("ZSet์—์„œ ๋“ํ‘œ์ˆ˜๋ฅผ 1 ๊ฐ์†Œ์‹œํ‚จ๋‹ค.") + void decrementVoteCountInZSet() { + // given + Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); + String key = VoteKey.voteStatusCountZSetKey(vote); + redisTemplate.opsForZSet().add(key, vote.getStationId(), 2.0); + + // when + voteCommandRedisAdapter.decrementVoteCountInZSet(vote); + + // then + Double score = redisTemplate.opsForZSet().score(key, vote.getStationId()); + assertThat(score).isEqualTo(1.0); + } + + @Test + @DisplayName("์ฐฌ/๋ฐ˜ Set์—์„œ ๋ฉค๋ฒ„๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.") + void removeMemberFromVoteStatusSet() { + // given + Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); + String key = VoteKey.voteStatusMemberSetKey(vote); + redisTemplate.opsForSet().add(key, "memberId"); // when - voteCommandRedisAdapter.deleteByCandidate(vote); + voteCommandRedisAdapter.removeMemberFromVoteStatusSet(vote); // then - Set memberIds = redisTemplate.opsForSet().members(key); - assertThat(memberIds).containsExactlyInAnyOrder(memberId2); + Set result = redisTemplate.opsForSet().members(key); + assertThat(result).doesNotContain("memberId"); } - @DisplayName("์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์‚ญ์ œํ•œ๋‹ค.") @Test - void deleteAllByRoomIdAndMemberId() { + @DisplayName("๋ฉค๋ฒ„ ํˆฌํ‘œ Hash๋ฅผ ์‚ญ์ œํ•œ๋‹ค.") + void deleteMemberVoteHash() { // given String roomId = "roomId"; String memberId = "memberId"; - Candidate candidate = new Candidate(roomId, 1); - Candidate candidate2 = new Candidate(roomId, 2); - Candidate candidate3 = new Candidate(roomId, 3); - String key = getMemberVoteKey(roomId, memberId); - List votes = List.of( - new Vote(candidate, memberId, VoteStatus.AGREE), - new Vote(candidate2, memberId, VoteStatus.DISAGREE), - new Vote(candidate3, memberId, VoteStatus.DISAGREE) - ); - redisTemplate.opsForHash().putAll(key, Map.of( - votes.get(0).getStationId(), votes.get(0).getVoteStatus().getName(), - votes.get(1).getStationId(), votes.get(1).getVoteStatus().getName(), - votes.get(2).getStationId(), votes.get(2).getVoteStatus().getName() - )); - Long before = redisTemplate.opsForHash().size(key); + String key = VoteKey.memberKey(roomId, memberId); + redisTemplate.opsForHash().put(key, "1", "AGREE"); // when - voteCommandRedisAdapter.deleteAllByRoomIdAndMemberId(roomId, memberId); + voteCommandRedisAdapter.deleteMemberVoteHash(roomId, memberId); // then - Long after = redisTemplate.opsForHash().size(key); assertAll( - () -> assertThat(before).isEqualTo(3), - () -> assertThat(after).isEqualTo(0), - () -> assertThat(redisTemplate.hasKey(key)).isFalse() + () -> assertThat(redisTemplate.hasKey(key)).isFalse(), + () -> assertThat(redisTemplate.opsForHash().size(key)).isZero() ); } - private String getCandidateVoteKey(Vote vote) { - return String.format(CANDIDATE_VOTE_KEY_FORMAT, vote.getRoomId(), vote.getStationId(), - vote.getVoteStatus().getName()); - } + @Test + @DisplayName("ํˆฌํ‘œ ์™„๋ฃŒ์ž Set์—์„œ ๋ฉค๋ฒ„๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.") + void removeMemberFromVotedSet() { + // given + String roomId = "roomId"; + String memberId = "memberId"; + String key = VoteKey.voteKey(roomId); + redisTemplate.opsForSet().add(key, memberId); - private String getMemberVoteKey(String roomId, String memberId) { - return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); + // when + voteCommandRedisAdapter.removeMemberFromVotedSet(roomId, memberId); + + // then + Set result = redisTemplate.opsForSet().members(key); + assertThat(result).doesNotContain(memberId); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java index ae2b5427..f327226e 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java @@ -14,21 +14,20 @@ class VoteQueryRedisAdapterTest extends RepositoryTest { - private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; - @Autowired private VoteQueryRedisAdapter voteQueryRedisAdapter; + @Autowired private RedisTemplate redisTemplate; - @DisplayName("roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ํˆฌํ‘œ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.") @Test + @DisplayName("roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ํˆฌํ‘œ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.") void isExistsByRoomIdAndMemberId() { // given - String roomId = "roomId"; - String memberId = "memberId"; - String key = getMemberVoteKey(roomId, memberId); - redisTemplate.opsForHash().putAll(key, Map.of(1L, VoteStatus.AGREE.getName())); + String roomId = "room1"; + String memberId = "memberA"; + String key = VoteKey.memberKey(roomId, memberId); + redisTemplate.opsForHash().putAll(key, Map.of("1", VoteStatus.AGREE.getName())); // when boolean result = voteQueryRedisAdapter.isExistsByRoomIdAndMemberId(roomId, memberId); @@ -37,49 +36,40 @@ void isExistsByRoomIdAndMemberId() { assertThat(result).isTrue(); } - @DisplayName("ํ•ด๋‹น roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ํˆฌํ‘œ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test + @DisplayName("ํˆฌํ‘œ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") void isNotExistsByRoomIdAndMemberId() { - // given - String roomId = "roomId"; - String memberId = "memberId"; - - // when - boolean result = voteQueryRedisAdapter.isExistsByRoomIdAndMemberId(roomId, memberId); - - // then - assertThat(result).isFalse(); + assertThat(voteQueryRedisAdapter.isExistsByRoomIdAndMemberId("roomX", "memberY")).isFalse(); } - @DisplayName("roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ๋ชจ๋“  ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค.") @Test + @DisplayName("roomId, memberId๋กœ ํˆฌํ‘œ ์ „์ฒด ๋ชฉ๋ก์„ ์กฐํšŒํ•œ๋‹ค.") void findAllByRoomIdAndMemberId() { // given - String roomId = "roomId"; - String memberId = "memberId"; - String key = getMemberVoteKey(roomId, memberId); + String roomId = "room2"; + String memberId = "memberB"; + String key = VoteKey.memberKey(roomId, memberId); Vote vote = new Vote(roomId, 1L, memberId, VoteStatus.AGREE.getName()); - redisTemplate.opsForHash() - .putAll(key, Map.of(vote.getStationId(), vote.getVoteStatus().getName())); + Vote vote2 = new Vote(roomId, 2L, memberId, VoteStatus.DISAGREE.getName()); + redisTemplate.opsForHash().putAll(key, Map.of( + "1", VoteStatus.AGREE.getName(), + "2", VoteStatus.DISAGREE.getName() + )); // when - List votes = voteQueryRedisAdapter.findAllByRoomIdAndMemberId(roomId, memberId); + List result = voteQueryRedisAdapter.findAllByRoomIdAndMemberId(roomId, memberId); // then - assertThat(votes).containsExactlyInAnyOrder(vote); + assertThat(result).hasSize(2) + .containsExactlyInAnyOrder(vote, vote2); } - @DisplayName("roomId๋กœ ํˆฌํ‘œํ•œ member ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test + @DisplayName("roomId์— ๋Œ€ํ•ด ํˆฌํ‘œ ์™„๋ฃŒํ•œ ๋ฉค๋ฒ„ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") void countMembersByRoomId() { // given - String roomId = "roomId"; - redisTemplate.opsForHash() - .putAll(getMemberVoteKey(roomId, "1"), Map.of(1L, VoteStatus.AGREE.getName())); - redisTemplate.opsForHash() - .putAll(getMemberVoteKey(roomId, "2"), Map.of(1L, VoteStatus.AGREE.getName())); - redisTemplate.opsForHash() - .putAll(getMemberVoteKey(roomId, "3"), Map.of(1L, VoteStatus.AGREE.getName())); + String roomId = "room3"; + redisTemplate.opsForSet().add(VoteKey.voteKey(roomId), "member1", "member2", "member3"); // when int count = voteQueryRedisAdapter.countMembersByRoomId(roomId); @@ -88,8 +78,53 @@ void countMembersByRoomId() { assertThat(count).isEqualTo(3); } - private String getMemberVoteKey(String roomId, String memberId) { - return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); + @Test + @DisplayName("ํŠน์ • stationId์— ๋Œ€ํ•ด ํˆฌํ‘œํ•œ memberId ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค.") + void findMemberIdsByRoomIdAndStationIdAndStatus() { + // given + String roomId = "room4"; + long stationId = 11L; + String key = VoteKey.voteStatusMemberSetKey(VoteStatus.AGREE, roomId, stationId); + redisTemplate.opsForSet().add(key, "member1", "member2"); + + // when + List result = voteQueryRedisAdapter.findMemberIdsByRoomIdAndStationIdAndStatus( + roomId, stationId, VoteStatus.AGREE); + + // then + assertThat(result).containsExactlyInAnyOrder("member1", "member2"); } + @Test + @DisplayName("์ฐฌ์„ฑ ์ˆ˜๊ฐ€ ๊ฐ€์žฅ ๋งŽ์€ stationId๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void getFirstStationIdByRoomIdAndVoteStatus() { + // given + String roomId = "room5"; + String key = VoteKey.voteStatusCountZSetKey(VoteStatus.AGREE, roomId); + redisTemplate.opsForZSet().add(key, "10", 5.0); + redisTemplate.opsForZSet().add(key, "11", 8.0); + + // when + long result = voteQueryRedisAdapter.getFirstStationIdByRoomIdAndVoteStatus( + roomId, VoteStatus.AGREE); + + // then + assertThat(result).isEqualTo(11); + } + + @Test + @DisplayName("์ฐฌ์„ฑ ํˆฌํ‘œ์ž๊ฐ€ ์•„๋ฌด๋„ ์—†์œผ๋ฉด -1์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void getFirstStationIdWhenNoVotes() { + // given + String roomId = "room6"; + String key = VoteKey.voteStatusCountZSetKey(VoteStatus.AGREE, roomId); + redisTemplate.delete(key); + + // when + long result = voteQueryRedisAdapter.getFirstStationIdByRoomIdAndVoteStatus( + roomId, VoteStatus.AGREE); + + // then + assertThat(result).isEqualTo(-1L); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java index fd517a7b..9449ea1b 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java @@ -9,12 +9,12 @@ import com.kok.kokapi.room.adapter.out.persistence.RoomParticipantSaveAdapter; import com.kok.kokapi.room.adapter.out.persistence.RoomSaveRedisAdapter; import com.kok.kokapi.vote.adapter.out.persistence.CandidateCommandRedisAdapter; +import com.kok.kokapi.vote.adapter.out.persistence.VoteKey; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.domain.vo.VoteStatus; -import com.kok.kokcore.vote.port.out.SaveVotePort; import java.util.List; import java.util.Map; import java.util.Set; @@ -26,14 +26,9 @@ class VoteServiceTest extends ServiceTest { - private static final String MEMBER_VOTE_KEY_FORMAT = "vote:%s:member:%s"; - private static final String CANDIDATE_VOTE_KEY_FORMAT = "vote:%s:candidate:%d:%s"; - @Autowired private VoteService voteService; @Autowired - private SaveVotePort saveVotePort; - @Autowired private RedisTemplate redisTemplate; @Autowired private RoomSaveRedisAdapter roomSaveRedisAdapter; @@ -65,22 +60,25 @@ void init() { @DisplayName("์‚ฌ์šฉ์ž ํˆฌํ‘œ ์ •๋ณด๋ฅผ ํ›„๋ณด๋ณ„/์‚ฌ์šฉ์ž๋ณ„๋กœ ๋ชจ๋‘ ์ €์žฅํ•œ๋‹ค.") @Test void saveVotes() { - // given - String memberKey = getMemberVoteKey(room.getId(), member.getMemberId()); - String agreeCandidateKey = getCandidateVoteKey(room.getId(), 1, VoteStatus.AGREE); - String disagreeCandidateKey = getCandidateVoteKey(room.getId(), 2, VoteStatus.DISAGREE); - // when voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); // then + String memberKey = VoteKey.memberKey(room.getId(), member.getMemberId()); + String agreeKey = VoteKey.voteStatusMemberSetKey( + new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); + String disagreeKey = VoteKey.voteStatusMemberSetKey( + new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE)); + Map storedVotes = redisTemplate.opsForHash().entries(memberKey); - Set agreeMemberIds = redisTemplate.opsForSet().members(agreeCandidateKey); - Set disagreeMemberIds = redisTemplate.opsForSet().members(disagreeCandidateKey); + Set agreeMemberIds = redisTemplate.opsForSet().members(agreeKey); + Set disagreeMemberIds = redisTemplate.opsForSet().members(disagreeKey); assertAll( - () -> assertThat(storedVotes).containsEntry(1L, VoteStatus.AGREE.getName()), - () -> assertThat(storedVotes).containsEntry(2L, VoteStatus.DISAGREE.getName()), + () -> assertThat(storedVotes).containsExactlyInAnyOrderEntriesOf(Map.of( + "1", VoteStatus.AGREE.getName(), + "2", VoteStatus.DISAGREE.getName() + )), () -> assertThat(agreeMemberIds).containsExactlyInAnyOrder(member.getMemberId()), () -> assertThat(disagreeMemberIds).containsExactlyInAnyOrder(member.getMemberId()) ); @@ -90,32 +88,30 @@ void saveVotes() { @Test void initiateBeforeSaveVote() { // given - List votes = List.of( - new Vote(candidate, member.getMemberId(), VoteStatus.AGREE), - new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE) - ); - saveVotePort.saveAllByMember(votes); - for (Vote vote : votes) { - saveVotePort.saveByCandidate(vote); - } + voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); // when voteService.saveVotes(room.getId(), member.getMemberId(), List.of()); // then - Long storedVoteCount = redisTemplate.opsForHash() - .size(getMemberVoteKey(room.getId(), member.getMemberId())); - Long agreeCountForStation1 = redisTemplate.opsForSet() - .size(getCandidateVoteKey(room.getId(), 1, VoteStatus.AGREE)); - Long disagreeCountForStation1 = redisTemplate.opsForSet() - .size(getCandidateVoteKey(room.getId(), 1, VoteStatus.DISAGREE)); - Long disagreeCountForStation2 = redisTemplate.opsForSet() - .size(getCandidateVoteKey(room.getId(), 2, VoteStatus.DISAGREE)); + String hashKey = VoteKey.memberKey(room.getId(), member.getMemberId()); + String agreeSetKey = VoteKey.voteStatusMemberSetKey( + new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); + String disagreeSetKey1 = VoteKey.voteStatusMemberSetKey( + new Vote(candidate, member.getMemberId(), VoteStatus.DISAGREE)); + String disagreeSetKey2 = VoteKey.voteStatusMemberSetKey( + new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE)); + + Long hashSize = redisTemplate.opsForHash().size(hashKey); + Long agreeSetSize = redisTemplate.opsForSet().size(agreeSetKey); + Long disagreeSetSize1 = redisTemplate.opsForSet().size(disagreeSetKey1); + Long disagreeSetSize2 = redisTemplate.opsForSet().size(disagreeSetKey2); + assertAll( - () -> assertThat(storedVoteCount).isEqualTo(2), - () -> assertThat(agreeCountForStation1).isZero(), - () -> assertThat(disagreeCountForStation1).isEqualTo(1), - () -> assertThat(disagreeCountForStation2).isEqualTo(1) + () -> assertThat(hashSize).isEqualTo(2), + () -> assertThat(agreeSetSize).isZero(), + () -> assertThat(disagreeSetSize1).isEqualTo(1), + () -> assertThat(disagreeSetSize2).isEqualTo(1) ); } @@ -123,12 +119,7 @@ void initiateBeforeSaveVote() { @Test void isVotedByMember() { // given - Candidate candidate = new Candidate(room.getId(), 1); - List votes = List.of(new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); - saveVotePort.saveAllByMember(votes); - for (Vote vote : votes) { - saveVotePort.saveByCandidate(vote); - } + voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); // when boolean result = voteService.isVotedByMember(room.getId(), member.getMemberId()); @@ -151,10 +142,8 @@ void isNotVotedByMember() { @Test void countVotedMembers() { // given - List votes = List.of(new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); - List votes2 = List.of(new Vote(candidate, member2.getMemberId(), VoteStatus.AGREE)); - saveVotes(votes); - saveVotes(votes2); + voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); + voteService.saveVotes(room.getId(), member2.getMemberId(), List.of(2L)); // when int count = voteService.countVotedMembers(room.getId()); @@ -167,61 +156,40 @@ void countVotedMembers() { @Test void getVotesByMember() { // given - List votes = List.of( - new Vote(candidate, member.getMemberId(), VoteStatus.AGREE), - new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE) - ); - saveVotePort.saveAllByMember(votes); - for (Vote vote : votes) { - saveVotePort.saveByCandidate(vote); - } + voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); // when List result = voteService.getVotesByMember(room.getId(), member.getMemberId()); // then assertThat(result).hasSize(2) - .extracting("stationId", "voteStatus") .containsExactlyInAnyOrder( - org.assertj.core.api.Assertions.tuple(1L, VoteStatus.AGREE), - org.assertj.core.api.Assertions.tuple(2L, VoteStatus.DISAGREE) + new Vote(room.getId(), 1L, member.getMemberId(), VoteStatus.AGREE.getName()), + new Vote(room.getId(), 2L, member.getMemberId(), VoteStatus.DISAGREE.getName()) ); } @DisplayName("ํŠน์ • ํˆฌํ‘œ์— ์ฐธ์—ฌํ•œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void getMembersByVote() { - // given + voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); + voteService.saveVotes(room.getId(), member2.getMemberId(), List.of(1L)); + Vote vote = new Vote(candidate, member.getMemberId(), VoteStatus.AGREE); - List votes = List.of(vote); - List votes2 = List.of(new Vote(candidate, member2.getMemberId(), VoteStatus.AGREE)); - saveVotes(votes); - saveVotes(votes2); - // when List result = voteService.getMembersByVote(vote); - // then assertThat(result).hasSize(2) .extracting(Member::getMemberId) .containsExactlyInAnyOrder(member.getMemberId(), member2.getMemberId()); } - private void saveVotes(List votes) { - saveVotePort.saveAllByMember(votes); - for (Vote vote : votes) { - saveVotePort.saveByCandidate(vote); - } - } - @DisplayName("๋ฐฉ์ด ํˆฌํ‘œ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") @Test void throwExceptionWhenRoomNotInVoteStatus() { - // given Room locationInputRoom = Room.create("inputRoom", 3, member); roomSaveRedisAdapter.save(locationInputRoom); - // when & then assertThatThrownBy(() -> voteService.countVotedMembers(locationInputRoom.getId())) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Room is not on vote status"); @@ -230,30 +198,18 @@ void throwExceptionWhenRoomNotInVoteStatus() { @DisplayName("๋ฐฉ์— ์†ํ•˜์ง€ ์•Š์€ ๋ฉค๋ฒ„๊ฐ€ ํˆฌํ‘œํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") @Test void throwExceptionWhenMemberNotInRoom() { - // given String nonParticipantId = "unknown"; - // when & then assertThatThrownBy(() -> voteService.saveVotes(room.getId(), nonParticipantId, List.of())) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Cannot find member with id"); + .hasMessageContaining("Member not found with id"); } @DisplayName("์•„์ง ํˆฌํ‘œํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œ ๋‚ด์—ญ ์กฐํšŒ ์‹œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") @Test void throwExceptionWhenGetVotesWithoutVoting() { - // when & then assertThatThrownBy(() -> voteService.getVotesByMember(room.getId(), member.getMemberId())) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Not voted by member with id"); } - - private String getMemberVoteKey(String roomId, String memberId) { - return String.format(MEMBER_VOTE_KEY_FORMAT, roomId, memberId); - } - - private String getCandidateVoteKey(String roomId, long stationId, VoteStatus status) { - return String.format(CANDIDATE_VOTE_KEY_FORMAT, roomId, stationId, status.getName()); - } } - diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index 6a144e54..69c22203 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -1,4 +1,6 @@ spring: + jackson: + time-zone: UTC profiles: active: test flyway: diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 9ab5956f..1f8d02e3 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -55,16 +55,16 @@ private static void validateParameter(String roomName, int capacity) { } public boolean shouldEndLocationInput(long locationInputCount, LocalDateTime current) { - return this.status.isLocationInput() && - (isAllLocationInput(locationInputCount) || current.isAfter(locationInputLimitDateTime)); + return this.status.isLocationInput() && (isAllLocationInput(locationInputCount) + || current.isAfter(locationInputLimitDateTime)); } public boolean isNotOnVote() { - return this.status.isLocationInput() || this.status.isVoteResultStatus(); + return this.status.isLocationInput() || this.status.isVoteResult(); } public boolean isVoteClosed() { - return this.status.isVoteResultStatus(); + return this.status.isVoteResult(); } private boolean isAllLocationInput(long participantCount) { diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java index 801d2ed2..82d6b3a2 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java @@ -10,7 +10,7 @@ public boolean isLocationInput() { return this.equals(LOCATION_INPUT); } - public boolean isVoteResultStatus() { + public boolean isVoteResult() { return this.equals(VOTE_RESULT); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java index 0af846b0..7f233e26 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java @@ -20,4 +20,8 @@ public static VoteStatus findByName(String name) { .findFirst() .orElseThrow(() -> new IllegalArgumentException("No status with name: " + name)); } + + public boolean isAgree() { + return this.equals(VoteStatus.AGREE); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java index 9609c8fd..dcfbb6c5 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java @@ -4,7 +4,23 @@ public interface DeleteVotePort { - void deleteByCandidate(Vote vote); + /** + * ํ›„๋ณด์ง€ ์ฐฌ/๋ฐ˜ Set์—์„œ memberId ์ œ๊ฑฐ {agree/disagree}:{roomId}:{stationId} + */ + void removeMemberFromVoteStatusSet(Vote vote); - void deleteAllByRoomIdAndMemberId(String roomId, String memberId); + /** + * ํ›„๋ณด์ง€ ์ฐฌ/๋ฐ˜ ZSet์˜ ํˆฌํ‘œ ์ˆ˜(score)๋ฅผ -1 {agree/disagree}:{roomId} + */ + void decrementVoteCountInZSet(Vote vote); + + /** + * member์˜ ๊ฐœ์ธ ํˆฌํ‘œ ๊ธฐ๋ก ์‚ญ์ œ member:{roomId}:{memberId} + */ + void deleteMemberVoteHash(String roomId, String memberId); + + /** + * vote ์™„๋ฃŒ Set์—์„œ memberId ์ œ๊ฑฐ ex) vote:{roomId} + */ + void removeMemberFromVotedSet(String roomId, String memberId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java index 7fb461dd..c5bbd4dd 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java @@ -14,4 +14,6 @@ public interface LoadVotePort { List findMemberIdsByRoomIdAndStationIdAndStatus(String roomId, long stationId, VoteStatus voteStatus); + + long getFirstStationIdByRoomIdAndVoteStatus(String roomId, VoteStatus voteStatus); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java index 11ae5a85..0e634c28 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java @@ -5,7 +5,11 @@ public interface SaveVotePort { - void saveByCandidate(Vote vote); + void saveVoteMemberHash(List votes); - void saveAllByMember(List votes); + void saveVoteStatusSet(Vote vote); + + void incrementVoteStatusCountZSet(Vote vote); + + void saveVotedMemberSet(String roomId, String memberId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java index dfeeaf3c..0f0e896a 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java @@ -1,6 +1,7 @@ package com.kok.kokcore.vote.usecase; import com.kok.kokcore.room.domain.Member; +import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.vote.domain.Vote; import java.util.List; @@ -8,6 +9,8 @@ public interface GetVoteUseCase { boolean isVotedByMember(String roomId, String memberId); + Station getVoteFinalResult(String roomId); + int countVotedMembers(String roomId); List getVotesByMember(String roomId, String memberId); From b384cdade0cf0b8320ebcb28efb51249eb608706 Mon Sep 17 00:00:00 2001 From: minseokey Date: Wed, 2 Apr 2025 18:10:46 +0900 Subject: [PATCH 145/163] :bug: refactor: change swagger http to https --- .../com/kok/kokapi/config/swagger/SwaggerConfig.java | 11 ++++++++++- kok-api/src/main/resources/application-dev.yml | 3 +++ kok-api/src/main/resources/application-prod.yml | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java index dcd65704..8c8def7a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java +++ b/kok-api/src/main/java/com/kok/kokapi/config/swagger/SwaggerConfig.java @@ -2,20 +2,29 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import java.util.List; import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SwaggerConfig { + @Value("${swagger.appname}") + private String applicationName; + @Bean public OpenAPI customOpenAPI() { + Server server = new Server(); + server.setUrl(applicationName); return new OpenAPI() .info(new Info() .title("KOK") .version("1.0") - .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")); + .description("API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")) + .servers(List.of(server)); } @Bean diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 8c4a9471..2c5de1c0 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -55,3 +55,6 @@ tmap-complex: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes" keyname: "appKey" + +swagger: + appname: "https://dev-api.kokokok.com" \ No newline at end of file diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 30c6650e..1c020ef9 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -69,3 +69,6 @@ tmap-complex: key: ${TMAP_KEY} url: "https://apis.openapi.sk.com/transit/routes" keyname: "appKey" + +swagger: + appname: "https://prod-api.kokokok.com" \ No newline at end of file From 0fe6975414223fb498b8d652e46d190289ba39cb Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:52:50 +0900 Subject: [PATCH 146/163] :sparkles: [Feature/station] delete code from route domain (#105) * :sparkles: feat: delete code from route * :sparkles: feat: update route name not by code but by script * :bug: fix: fix table name --- .../persistence/RoutePersistenceAdapter.java | 8 ++----- .../out/persistence/RouteRepository.java | 7 ------ kok-api/src/main/resources/db/data/name.sql | 22 +++++++++++++++++++ .../db/migration/V3__delete_station_code.sql | 1 + .../kokcore/station/domain/entity/Route.java | 7 +++--- .../station/port/out/dto/StationRouteDto.java | 2 +- .../port/out/dto/StationRouteDtos.java | 5 +++-- 7 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 kok-api/src/main/resources/db/data/name.sql create mode 100644 kok-api/src/main/resources/db/migration/V3__delete_station_code.sql diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java index 2a80312c..0d93b2f7 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RoutePersistenceAdapter.java @@ -18,16 +18,14 @@ @RequiredArgsConstructor public class RoutePersistenceAdapter implements SaveRoutePort, RetrieveRoutePort { - public static final List ROUTE_ONE_LIST = List.of("๊ฒฝ๋ถ€์„ ", "๊ฒฝ์ธ์„ ", "๊ฒฝ์›์„ ", "์žฅํ•ญ์„ "); private final RouteRepository routeRepository; private static final String INSERT_ROUTE_SQL = """ - INSERT INTO route (code, name, station_id) - VALUES (:code, :name, :station_id) + INSERT INTO route (name, station_id) + VALUES (:name, :station_id) """; private static final Function mapToParams = route -> new MapSqlParameterSource() - .addValue("code", route.getCode()) .addValue("name", route.getName()) .addValue("station_id", route.getStation().getId()); @@ -40,8 +38,6 @@ public void saveRoutes(List routes) { return; } batchInsertRoutes(routes); - int updatedCount = routeRepository.updateRouteNameToRouteOne(ROUTE_ONE_LIST); - log.debug("Successfully changed {} route name to \"1ํ˜ธ์„ \".", updatedCount); } private void batchInsertRoutes(List routes) { diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java index a758434e..0fdf9a71 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/RouteRepository.java @@ -4,15 +4,8 @@ import com.kok.kokcore.station.domain.entity.Station; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; public interface RouteRepository extends JpaRepository { List findAllByStationOrderByName(Station station); - - @Modifying(clearAutomatically = true) - @Query("UPDATE Route r SET r.name = '1ํ˜ธ์„ ' WHERE r.name IN :names") - int updateRouteNameToRouteOne(@Param("names") List names); } diff --git a/kok-api/src/main/resources/db/data/name.sql b/kok-api/src/main/resources/db/data/name.sql new file mode 100644 index 00000000..f3a9b9a6 --- /dev/null +++ b/kok-api/src/main/resources/db/data/name.sql @@ -0,0 +1,22 @@ +UPDATE route r +SET r.name = '1ํ˜ธ์„ ' +WHERE r.name IN ('๊ฒฝ๋ถ€์„ ', '๊ฒฝ์ธ์„ ', '๊ฒฝ์›์„ ', '์žฅํ•ญ์„ '); +UPDATE route r +SET r.name = '์‹ ๋ถ„๋‹น์„ ' +WHERE r.name IN ('์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ)', '์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ2)'); +UPDATE route r +SET r.name = '9ํ˜ธ์„ ' +WHERE r.name IN ('9ํ˜ธ์„ (์—ฐ์žฅ)'); +UPDATE route r +SET r.name = '7ํ˜ธ์„ ' +WHERE r.name IN ('7ํ˜ธ์„ (์ธ์ฒœ)'); +UPDATE route r +SET r.name = '์ˆ˜์ธ๋ถ„๋‹น์„ ' +WHERE r.name IN ('์ˆ˜์ธ์„ ', '๋ถ„๋‹น์„ '); +UPDATE route r +SET r.name = 'GTX' +WHERE r.name IN ('์ˆ˜๋„๊ถŒ ๊ด‘์—ญ๊ธ‰ํ–‰์ฒ ๋„'); +UPDATE route r +SET r.name = '๊ณตํ•ญ์ฒ ๋„' +WHERE r.name IN ('๊ณตํ•ญ์ฒ ๋„1ํ˜ธ์„ '); + diff --git a/kok-api/src/main/resources/db/migration/V3__delete_station_code.sql b/kok-api/src/main/resources/db/migration/V3__delete_station_code.sql new file mode 100644 index 00000000..1364f81f --- /dev/null +++ b/kok-api/src/main/resources/db/migration/V3__delete_station_code.sql @@ -0,0 +1 @@ +ALTER TABLE route DROP COLUMN code; diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java index e591e925..d0a0d520 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java @@ -9,27 +9,26 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode public class Route { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private Long code; @Column(nullable = false, length = 20) private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "station_id", nullable = false) private Station station; - public Route(Long code, String name, Station station) { - this.code = code; + public Route(String name, Station station) { this.name = name; this.station = station; } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java index 6b898913..2410e44e 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java @@ -20,6 +20,6 @@ public Station toStation() { } public Route toRouteByStation(Station station) { - return new Route(stationId, route, station); + return new Route(route, station); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java index a2ff88f8..4d37f810 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDtos.java @@ -4,6 +4,7 @@ import com.kok.kokcore.station.domain.entity.Station; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; public record StationRouteDtos( @@ -33,10 +34,10 @@ private List distinctByName() { public List toRoutesByStations(List stations) { List routes = new ArrayList<>(); for (Station station : stations) { - List routesOfStation = stationRouteDtos.stream() + Set routesOfStation = stationRouteDtos.stream() .filter(stationRouteDto -> stationRouteDto.hasName(station)) .map(stationRouteDto -> stationRouteDto.toRouteByStation(station)) - .toList(); + .collect(Collectors.toSet()); routes.addAll(routesOfStation); } return routes; From 1f7fa1addf44c2bd025ac392772433e44587a53b Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 3 Apr 2025 01:43:25 +0900 Subject: [PATCH 147/163] :bug: fix: move changing route name logic into domain due to duplicated route name saved if changing after saving on db (#106) --- kok-api/src/main/resources/db/data/name.sql | 22 ---------------- .../kokcore/station/domain/entity/Route.java | 25 ++++++++++++++++++- .../station/port/out/dto/StationRouteDto.java | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) delete mode 100644 kok-api/src/main/resources/db/data/name.sql diff --git a/kok-api/src/main/resources/db/data/name.sql b/kok-api/src/main/resources/db/data/name.sql deleted file mode 100644 index f3a9b9a6..00000000 --- a/kok-api/src/main/resources/db/data/name.sql +++ /dev/null @@ -1,22 +0,0 @@ -UPDATE route r -SET r.name = '1ํ˜ธ์„ ' -WHERE r.name IN ('๊ฒฝ๋ถ€์„ ', '๊ฒฝ์ธ์„ ', '๊ฒฝ์›์„ ', '์žฅํ•ญ์„ '); -UPDATE route r -SET r.name = '์‹ ๋ถ„๋‹น์„ ' -WHERE r.name IN ('์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ)', '์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ2)'); -UPDATE route r -SET r.name = '9ํ˜ธ์„ ' -WHERE r.name IN ('9ํ˜ธ์„ (์—ฐ์žฅ)'); -UPDATE route r -SET r.name = '7ํ˜ธ์„ ' -WHERE r.name IN ('7ํ˜ธ์„ (์ธ์ฒœ)'); -UPDATE route r -SET r.name = '์ˆ˜์ธ๋ถ„๋‹น์„ ' -WHERE r.name IN ('์ˆ˜์ธ์„ ', '๋ถ„๋‹น์„ '); -UPDATE route r -SET r.name = 'GTX' -WHERE r.name IN ('์ˆ˜๋„๊ถŒ ๊ด‘์—ญ๊ธ‰ํ–‰์ฒ ๋„'); -UPDATE route r -SET r.name = '๊ณตํ•ญ์ฒ ๋„' -WHERE r.name IN ('๊ณตํ•ญ์ฒ ๋„1ํ˜ธ์„ '); - diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java index d0a0d520..7b903eaa 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java @@ -8,6 +8,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.util.Map; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -22,13 +23,35 @@ public class Route { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false, length = 20) private String name; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "station_id", nullable = false) private Station station; - public Route(String name, Station station) { + private static final Map NORMALIZED_NAME_MAP = Map.ofEntries( + Map.entry("๊ฒฝ๋ถ€์„ ", "1ํ˜ธ์„ "), + Map.entry("๊ฒฝ์ธ์„ ", "1ํ˜ธ์„ "), + Map.entry("๊ฒฝ์›์„ ", "1ํ˜ธ์„ "), + Map.entry("์žฅํ•ญ์„ ", "1ํ˜ธ์„ "), + Map.entry("์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ)", "์‹ ๋ถ„๋‹น์„ "), + Map.entry("์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ2)", "์‹ ๋ถ„๋‹น์„ "), + Map.entry("9ํ˜ธ์„ (์—ฐ์žฅ)", "9ํ˜ธ์„ "), + Map.entry("7ํ˜ธ์„ (์ธ์ฒœ)", "7ํ˜ธ์„ "), + Map.entry("์ˆ˜์ธ์„ ", "์ˆ˜์ธ๋ถ„๋‹น์„ "), + Map.entry("๋ถ„๋‹น์„ ", "์ˆ˜์ธ๋ถ„๋‹น์„ "), + Map.entry("์ˆ˜๋„๊ถŒ ๊ด‘์—ญ๊ธ‰ํ–‰์ฒ ๋„", "GTX"), + Map.entry("๊ณตํ•ญ์ฒ ๋„1ํ˜ธ์„ ", "๊ณตํ•ญ์ฒ ๋„") + ); + + public static Route create(String rawName, Station station) { + String normalized = NORMALIZED_NAME_MAP.getOrDefault(rawName, rawName); + return new Route(normalized, station); + } + + private Route(String name, Station station) { this.name = name; this.station = station; } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java index 2410e44e..65a519cb 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/port/out/dto/StationRouteDto.java @@ -20,6 +20,6 @@ public Station toStation() { } public Route toRouteByStation(Station station) { - return new Route(route, station); + return Route.create(route, station); } } From a9047e76b73ba39f93adfb37eddc299309e4db12 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 3 Apr 2025 23:42:00 +0900 Subject: [PATCH 148/163] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20[Refactor/room]=20?= =?UTF-8?q?determine=20participant=20count=20by=20location=20input=20count?= =?UTF-8?q?=20on=20getting=20room=20detail=20API=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :recycle: refactor: determine participant count by location input count * :white_check_mark: test: fix test response --- .../adapter/out/persistence/LocationPersistenceAdapter.java | 5 +++++ .../centroid/adapter/out/persistence/LocationRepository.java | 2 ++ .../room/adapter/in/dto/response/RoomDetailResponse.java | 4 ++-- .../com/kok/kokapi/room/adapter/in/web/RoomController.java | 2 +- .../kokapi/room/application/service/RoomQueryService.java | 5 ++--- .../kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java | 4 ++-- kok-api/src/test/resources/application-test.yml | 2 ++ .../com/kok/kokcore/location/port/out/ReadLocationPort.java | 2 ++ .../java/com/kok/kokcore/room/usecase/GetRoomUseCase.java | 2 +- 9 files changed, 19 insertions(+), 9 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java index c802c299..6a93110b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationPersistenceAdapter.java @@ -44,6 +44,11 @@ public List findConvexHull(String roomId) { return locationRepository.findConvexHull(roomId); } + @Override + public long countParticipantsById(String roomId) { + return locationRepository.countByRoomId(roomId); + } + @Override @Transactional(readOnly = true) public Point findCentroidByRoomId(String roomId) { diff --git a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java index 78346a99..52e7caee 100644 --- a/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java +++ b/kok-api/src/main/java/com/kok/kokapi/centroid/adapter/out/persistence/LocationRepository.java @@ -63,4 +63,6 @@ AND NOT ST_Contains(ch.hull, ST_GeomFromText(ST_AsText(l.location_point))) ORDER BY angle """, nativeQuery = true) List findConvexHull(@Param("roomId") String roomId); + + long countByRoomId(String roomId); } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java index 24f2abe5..eb1a444f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/dto/response/RoomDetailResponse.java @@ -5,11 +5,11 @@ public record RoomDetailResponse( String id, String roomName, - int nonParticipantCount, + long nonParticipantCount, String roomStatus ) { - public static RoomDetailResponse of(Room room, int participantCount) { + public static RoomDetailResponse of(Room room, long participantCount) { return new RoomDetailResponse( room.getId(), room.getRoomName(), diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index b0d95777..b786bc67 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -43,7 +43,7 @@ public class RoomController { public ResponseEntity> getRoomDetail( @PathVariable String roomId) { Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); - int participantsCount = getRoomUseCase.getParticipantsCount(roomId); + long participantsCount = getRoomUseCase.getParticipantsCount(roomId); RoomDetailResponse response = RoomDetailResponse.of(room, participantsCount); return ResponseEntity.ok(ApiResponseDto.success(response)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index a727012f..b2ade337 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -65,8 +65,7 @@ public Member getParticipant(String roomId, String memberId) { } @Override - public int getParticipantsCount(String roomId) { - Long participantCount = loadRoomParticipantPort.countParticipantsById(roomId); - return participantCount.intValue(); + public long getParticipantsCount(String roomId) { + return readLocationPort.countParticipantsById(roomId); } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java index 8c4dd536..05de3b10 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/adapter/in/web/RoomIntegrationTest.java @@ -41,8 +41,8 @@ Stream getRoomDetail() { () -> joinRoomResponse.set(joinRoom(createRoomResponse.get().id(), new JoinRoomParticipantRequest("profile", "follower")))), - getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 0๋ช…์ด๋‹ค.", - createRoomResponse, 0, RoomStatus.LOCATION_INPUT.name()), + getRoomDetail("์•ฝ์†๋ฐฉ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด๋ณด๋ฉด ๋ฏธ์ฐธ์—ฌ์ž๋Š” 1๋ช…์ด๋‹ค.", + createRoomResponse, 1, RoomStatus.LOCATION_INPUT.name()), getRoomMembers("์•ฝ์†๋ฐฉ ํ”„๋กœํ•„ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋ฉด isFull์€ true์ด๊ณ , 2๋ช…์˜ ํ”„๋กœํ•„์ด ์žˆ๋‹ค", createRoomResponse, 2, true), diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index 69c22203..422597a7 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -34,3 +34,5 @@ google: places: api: key: mock +swagger: + appname: mock diff --git a/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java index 677abbf6..46d9fb5b 100644 --- a/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java +++ b/kok-core/src/main/java/com/kok/kokcore/location/port/out/ReadLocationPort.java @@ -13,4 +13,6 @@ public interface ReadLocationPort { List findInsideConvexHull(String roomId); List findConvexHull(String roomId); + + long countParticipantsById(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java index 2cf6ee5b..13ee6a22 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -13,5 +13,5 @@ public interface GetRoomUseCase { Member getParticipant(String roomId, String memberId); - int getParticipantsCount(String roomId); + long getParticipantsCount(String roomId); } From 98920b0dc0b906ed136de663ddabf37cb9850de5 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Fri, 4 Apr 2025 03:23:33 +0900 Subject: [PATCH 149/163] =?UTF-8?q?=E2=9C=A8=20[Feature/vote]=20fix=20key?= =?UTF-8?q?=20name=20error=20and=20add=20candidate=20count=20for=20getting?= =?UTF-8?q?=20vote=20deadline=20api=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bugs: fix: fix key name error * :sparkles: feat: add candidateCount field on response for getting vote deadline --- ...erRecommendStationCommandRedisAdapter.java | 2 +- .../in/dto/response/VoteDeadlineResponse.java | 6 +++--- .../vote/adapter/in/web/VoteController.java | 12 +++-------- .../application/service/CandidateService.java | 8 -------- .../service/VoteFacadeService.java | 20 +++++++++++++++---- .../vote/usecase/GetCandidateUseCase.java | 2 -- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java index 51d1acac..31e3446a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/adapter/out/persistence/UserRecommendStationCommandRedisAdapter.java @@ -15,7 +15,7 @@ @Repository public class UserRecommendStationCommandRedisAdapter implements SaveUserRecommendStationsPort { - private static final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStation:"; + private static final String USER_RECOMMEND_STATION_PREFIX = "userRecommendStations:"; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java index 70506c0b..4ba3ea25 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java @@ -3,9 +3,9 @@ import com.kok.kokcore.room.domain.Room; import java.time.LocalDateTime; -public record VoteDeadlineResponse(LocalDateTime endAt) { +public record VoteDeadlineResponse(int candidateCount, LocalDateTime endAt) { - public static VoteDeadlineResponse from(Room room) { - return new VoteDeadlineResponse(room.getVoteLimitDateTime()); + public static VoteDeadlineResponse of(Room room, int candidateCount) { + return new VoteDeadlineResponse(candidateCount, room.getVoteLimitDateTime()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 546c052c..9b8bc643 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -16,8 +16,6 @@ import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; -import com.kok.kokcore.station.usecase.SystemRecommendUseCase; -import com.kok.kokcore.station.usecase.UserRecommendUseCase; import com.kok.kokcore.vote.usecase.GetVoteUseCase; import io.swagger.v3.oas.annotations.Operation; import java.time.LocalDateTime; @@ -39,17 +37,12 @@ public class VoteController { private final GetVoteUseCase getVoteUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; private final UpdateRoomUseCase updateRoomUseCase; - private final SystemRecommendUseCase systemRecommendedUseCase; - private final UserRecommendUseCase userRecommendUseCase; @Operation(summary = "ํˆฌํ‘œ ํ›„๋ณด์ง€ ๋ชฉ๋ก ์กฐํšŒ", description = "๋ฐฉ ID๊ณผ ์‚ฌ์šฉ์ž ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํˆฌํ‘œ ํ›„๋ณด์ง€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/{memberId}/candidates") public ResponseEntity>> getCandidates( @PathVariable String roomId, @PathVariable String memberId) { - List recommendedStations = systemRecommendedUseCase.systemRecommendStation(roomId); - List customStations = userRecommendUseCase.getUserRecommendStation(roomId); - List responses = voteFacadeService.getCandidates( - roomId, memberId, recommendedStations, customStations); + List responses = voteFacadeService.getCandidates(roomId, memberId); return ResponseEntity.ok(ApiResponseDto.success(responses)); } @@ -78,7 +71,8 @@ public ResponseEntity>> getMemberV public ResponseEntity> getVoteDeadline( @PathVariable String roomId) { Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); - VoteDeadlineResponse response = VoteDeadlineResponse.from(room); + int candidateCount = voteFacadeService.countCandidates(roomId); + VoteDeadlineResponse response = VoteDeadlineResponse.of(room, candidateCount); return ResponseEntity.ok(ApiResponseDto.success(response)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java index 243570da..465fd7d3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java @@ -26,12 +26,4 @@ public List saveAndGetCandidates(String roomId, List station } return loadCandidatePort.findByRoomId(roomId); } - - @Override - public List getCandidates(String roomId) { - if (!loadCandidatePort.isExistsByRoomId(roomId)) { - throw new IllegalArgumentException("Cannot find candidates for roomId: " + roomId); - } - return loadCandidatePort.findByRoomId(roomId); - } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index baaf5afd..b1c1cc49 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -17,6 +17,8 @@ import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.usecase.GetStationUseCase; import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; +import com.kok.kokcore.station.usecase.SystemRecommendUseCase; +import com.kok.kokcore.station.usecase.UserRecommendUseCase; import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; import com.kok.kokcore.vote.usecase.GetVoteUseCase; @@ -41,11 +43,12 @@ public class VoteFacadeService { private final ObjectMapper objectMapper; private final SaveVoteUseCase saveVoteUseCase; private final ReadLocationUseCase readLocationUseCase; + private final SystemRecommendUseCase systemRecommendUseCase; + private final UserRecommendUseCase userRecommendUseCase; - public List getCandidates( - String roomId, String memberId, - List recommendedStations, - List customStations) { + public List getCandidates(String roomId, String memberId) { + List recommendedStations = systemRecommendUseCase.systemRecommendStation(roomId); + List customStations = userRecommendUseCase.getUserRecommendStation(roomId); List allStations = Stream.concat( recommendedStations.stream(), customStations.stream() ).toList(); @@ -121,4 +124,13 @@ public void saveVotes( getCandidateUseCase.saveAndGetCandidates(roomId, stations); saveVoteUseCase.saveVotes(roomId, memberId, agreedStationIds); } + + public int countCandidates(String roomId) { + List recommendedStations = systemRecommendUseCase.systemRecommendStation(roomId); + List customStations = userRecommendUseCase.getUserRecommendStation(roomId); + List stations = Stream.concat( + recommendedStations.stream(), customStations.stream() + ).toList(); + return getCandidateUseCase.saveAndGetCandidates(roomId, stations).size(); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java index bb64b049..a37e77f4 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java @@ -7,6 +7,4 @@ public interface GetCandidateUseCase { List saveAndGetCandidates(String roomId, List stations); - - List getCandidates(String roomId); } From eef282b125aa48d37a272bfa9f31a4d52937354a Mon Sep 17 00:00:00 2001 From: YUN YOUNG Date: Fri, 4 Apr 2025 21:20:03 +0900 Subject: [PATCH 150/163] :bug: fix: fix save Room and and add logging for redis (#108) --- .../RoomParticipantSaveAdapter.java | 16 ++++++++++++---- .../out/persistence/RoomSaveRedisAdapter.java | 18 +++++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java index 178f0408..599e056e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomParticipantSaveAdapter.java @@ -2,12 +2,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokapi.common.util.RedisExecutor; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.port.out.SaveRoomParticipantsPort; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; +@Slf4j @Repository @RequiredArgsConstructor public class RoomParticipantSaveAdapter implements SaveRoomParticipantsPort { @@ -22,12 +25,17 @@ public int joinRoom(String roomId, Member member) { try { String memberJson = objectMapper.writeValueAsString(member); - redisTemplate.opsForList().rightPush(key, memberJson); - - Long participantCount = redisTemplate.opsForList().size(key); + RedisExecutor.runOrThrow("joinRoom:push" + roomId, + () -> redisTemplate.opsForList().rightPush(key, memberJson)); + Long participantCount = RedisExecutor.runOrThrow("joinRoom:size: " + roomId, + () -> redisTemplate.opsForList().size(key)); return participantCount != null ? participantCount.intValue() : 0; } catch (JsonProcessingException e) { - throw new RuntimeException("failed to serialize member"); + log.error("[RoomParticipant] Failed to serialize member. roomId={}, member={}", roomId, member, e); + throw new RuntimeException("Failed to serialize member", e); + } catch (Exception e) { + log.error("[RoomParticipant] Redis join failed. roomId={}, member={}", roomId, member, e); + throw new RuntimeException("Failed to join room in Redis: " + e.getClass().getSimpleName(), e); } } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java index d41acc21..fe41d4c5 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java @@ -2,14 +2,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.kok.kokapi.common.util.RedisExecutor; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.port.out.SaveRoomPort; import com.kok.kokcore.room.port.out.UpdateRoomPort; import java.time.Duration; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; +@Slf4j @Repository @RequiredArgsConstructor public class RoomSaveRedisAdapter implements SaveRoomPort, UpdateRoomPort { @@ -25,9 +28,14 @@ public Room save(Room room) { String key = buildKey(room.getId()); try { String roomJson = objectMapper.writeValueAsString(room); - redisTemplate.opsForValue().set(key, roomJson, ROOM_TTL); + RedisExecutor.runOrThrow("saveRoom", + () -> redisTemplate.opsForValue().set(key, roomJson, ROOM_TTL)); } catch (JsonProcessingException e) { - throw new RuntimeException("failed to save room to Redis", e); + log.error("[Room] Failed to serialize room. roomId={}", room.getId(), e); + throw new RuntimeException("failed to serialize room object", e); + } catch (Exception e) { + log.error("[Room] Save to Redis failed. roomId={}, key={}", room.getId(), key, e); + throw new RuntimeException("failed to save room to Redis: " + e.getClass().getSimpleName(), e); } return room; } @@ -41,7 +49,11 @@ public void update(Room room) { redisTemplate.opsForValue().set(key, roomJson, currentTtl); } catch (JsonProcessingException e) { - throw new RuntimeException("failed to update room in Redis", e); + log.error("[Room] Failed to serialize room. roomId={}", room.getId(), e); + throw new RuntimeException("failed to serialize room object", e); + } catch (RuntimeException e) { + log.error("[Room] update to Redis failed. roomId={}, key={}", room.getId(), key, e); + throw new RuntimeException("failed to update room in Redis: " + e.getClass().getSimpleName(), e); } } From 9ad042ac9aecf14b4b0dac55a361f28cde776a9a Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 5 Apr 2025 00:20:46 +0900 Subject: [PATCH 151/163] =?UTF-8?q?=F0=9F=90=9B=20[Fix/Room]=20fix=20timez?= =?UTF-8?q?one=20error=20and=20add=20safety=20code=20for=20getting=20expir?= =?UTF-8?q?e=20ttl=20(#110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: feat: add route names need to be changed * :wrench: config: remove timezone configuration * :bug: fix: add checking if expire exists * :sparkles: feat: add ttl on vote * :sparkles: feat: add logging for cannot find key and initiate ttl while getExpire() * :wrench: config: add timezone configuration (UTC) * :sparkles: feat: convert to KST for LocalDateTime response --- .../kokapi/config/time/TimeZoneConfig.java | 16 +++++++++ .../out/persistence/RoomSaveRedisAdapter.java | 19 ++++++++--- .../in/dto/response/VoteDeadlineResponse.java | 7 +++- .../vote/adapter/in/web/VoteController.java | 2 +- .../persistence/VoteCommandRedisAdapter.java | 34 ++++++++++++++----- .../src/main/resources/application-dev.yml | 4 +-- .../src/main/resources/application-prod.yml | 4 +-- .../src/test/resources/application-test.yml | 2 -- .../com/kok/kokcore/room/domain/Room.java | 6 ++-- .../kokcore/station/domain/entity/Route.java | 6 ++-- .../port/out/dto/StationRouteDtosTest.java | 4 +-- 11 files changed, 75 insertions(+), 29 deletions(-) create mode 100644 kok-api/src/main/java/com/kok/kokapi/config/time/TimeZoneConfig.java diff --git a/kok-api/src/main/java/com/kok/kokapi/config/time/TimeZoneConfig.java b/kok-api/src/main/java/com/kok/kokapi/config/time/TimeZoneConfig.java new file mode 100644 index 00000000..f15afc52 --- /dev/null +++ b/kok-api/src/main/java/com/kok/kokapi/config/time/TimeZoneConfig.java @@ -0,0 +1,16 @@ +package com.kok.kokapi.config.time; + +import jakarta.annotation.PostConstruct; +import java.util.TimeZone; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TimeZoneConfig { + + @PostConstruct + public void setTimeZone() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + System.out.println("Set JVM default timezone to UTC"); + } +} + diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java index fe41d4c5..7a60342b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/out/persistence/RoomSaveRedisAdapter.java @@ -7,6 +7,7 @@ import com.kok.kokcore.room.port.out.SaveRoomPort; import com.kok.kokcore.room.port.out.UpdateRoomPort; import java.time.Duration; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -35,7 +36,8 @@ public Room save(Room room) { throw new RuntimeException("failed to serialize room object", e); } catch (Exception e) { log.error("[Room] Save to Redis failed. roomId={}, key={}", room.getId(), key, e); - throw new RuntimeException("failed to save room to Redis: " + e.getClass().getSimpleName(), e); + throw new RuntimeException( + "failed to save room to Redis: " + e.getClass().getSimpleName(), e); } return room; } @@ -45,19 +47,28 @@ public void update(Room room) { String key = buildKey(room.getId()); try { String roomJson = objectMapper.writeValueAsString(room); - Duration currentTtl = Duration.ofSeconds(redisTemplate.getExpire(key)); - + Duration currentTtl = getTTL(key); redisTemplate.opsForValue().set(key, roomJson, currentTtl); } catch (JsonProcessingException e) { log.error("[Room] Failed to serialize room. roomId={}", room.getId(), e); throw new RuntimeException("failed to serialize room object", e); } catch (RuntimeException e) { log.error("[Room] update to Redis failed. roomId={}, key={}", room.getId(), key, e); - throw new RuntimeException("failed to update room in Redis: " + e.getClass().getSimpleName(), e); + throw new RuntimeException( + "failed to update room in Redis: " + e.getClass().getSimpleName(), e); } } private String buildKey(String roomId) { return ROOM_KEY_PREFIX + ":" + roomId; } + + private Duration getTTL(String key) { + Long expireSeconds = redisTemplate.getExpire(key); + if (Objects.isNull(expireSeconds) || expireSeconds <= 0) { + log.warn("Cannot find key: {}, initiate expire TTL", key); + return ROOM_TTL; + } + return Duration.ofSeconds(expireSeconds); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java index 4ba3ea25..1d7ce83e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java @@ -1,9 +1,14 @@ package com.kok.kokapi.vote.adapter.in.dto.response; +import com.fasterxml.jackson.annotation.JsonFormat; import com.kok.kokcore.room.domain.Room; import java.time.LocalDateTime; -public record VoteDeadlineResponse(int candidateCount, LocalDateTime endAt) { +public record VoteDeadlineResponse( + int candidateCount, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul") + LocalDateTime endAt +) { public static VoteDeadlineResponse of(Room room, int candidateCount) { return new VoteDeadlineResponse(candidateCount, room.getVoteLimitDateTime()); diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 9b8bc643..4c84f360 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -66,7 +66,7 @@ public ResponseEntity>> getMemberV return ResponseEntity.ok(ApiResponseDto.success(responses)); } - @Operation(summary = "ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•œ ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„(UTC)์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @Operation(summary = "ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•œ ํˆฌํ‘œ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/deadline") public ResponseEntity> getVoteDeadline( @PathVariable String roomId) { diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java index 2600cfd4..055d4213 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java @@ -4,6 +4,7 @@ import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.port.out.DeleteVotePort; import com.kok.kokcore.vote.port.out.SaveVotePort; +import java.time.Duration; import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -16,6 +17,8 @@ @RequiredArgsConstructor public class VoteCommandRedisAdapter implements SaveVotePort, DeleteVotePort { + private static final Duration VOTE_TTL = Duration.ofDays(3); + private final RedisTemplate redisTemplate; @Override @@ -30,31 +33,35 @@ public void saveVoteMemberHash(List votes) { String status = vote.getVoteStatus().getName(); redisTemplate.opsForHash().put(memberHashKey, field, status); } + redisTemplate.expire(memberHashKey, getTTL(memberHashKey)); }); } @Override public void saveVotedMemberSet(String roomId, String memberId) { String votedMemberSetKey = VoteKey.voteKey(roomId); - RedisExecutor.runOrThrow("saveVotedMemberSet", () -> - redisTemplate.opsForSet().add(votedMemberSetKey, memberId) - ); + RedisExecutor.runOrThrow("saveVotedMemberSet", () -> { + redisTemplate.opsForSet().add(votedMemberSetKey, memberId); + redisTemplate.expire(votedMemberSetKey, getTTL(votedMemberSetKey)); + }); } @Override public void saveVoteStatusSet(Vote vote) { String key = VoteKey.voteStatusMemberSetKey(vote); - RedisExecutor.runOrThrow("saveVoteStatusSet", () -> - redisTemplate.opsForSet().add(key, vote.getMemberId()) - ); + RedisExecutor.runOrThrow("saveVoteStatusSet", () -> { + redisTemplate.opsForSet().add(key, vote.getMemberId()); + redisTemplate.expire(key, getTTL(key)); + }); } @Override public void incrementVoteStatusCountZSet(Vote vote) { String key = VoteKey.voteStatusCountZSetKey(vote); - RedisExecutor.runOrThrow("incrementVoteStatusCountZSet", () -> - redisTemplate.opsForZSet().incrementScore(key, vote.getStationId(), 1) - ); + RedisExecutor.runOrThrow("incrementVoteStatusCountZSet", () -> { + redisTemplate.opsForZSet().incrementScore(key, vote.getStationId(), 1); + redisTemplate.expire(key, getTTL(key)); + }); } private void validate(List votes) { @@ -94,4 +101,13 @@ public void removeMemberFromVotedSet(String roomId, String memberId) { redisTemplate.opsForSet().remove(key, memberId) ); } + + private Duration getTTL(String key) { + Long expireSeconds = redisTemplate.getExpire(key); + if (Objects.isNull(expireSeconds) || expireSeconds <= 0) { + log.warn("Cannot find key: {}, initiate expire TTL", key); + return VOTE_TTL; + } + return Duration.ofSeconds(expireSeconds); + } } diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 2c5de1c0..18ec1cfd 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -1,6 +1,4 @@ spring: - jackson: - time-zone: UTC datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DB_URL} @@ -57,4 +55,4 @@ tmap-complex: keyname: "appKey" swagger: - appname: "https://dev-api.kokokok.com" \ No newline at end of file + appname: "https://dev-api.kokokok.com" diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 1c020ef9..9e7f743b 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -1,6 +1,4 @@ spring: - jackson: - time-zone: UTC datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DB_URL} @@ -71,4 +69,4 @@ tmap-complex: keyname: "appKey" swagger: - appname: "https://prod-api.kokokok.com" \ No newline at end of file + appname: "https://prod-api.kokokok.com" diff --git a/kok-api/src/test/resources/application-test.yml b/kok-api/src/test/resources/application-test.yml index 422597a7..eb963837 100644 --- a/kok-api/src/test/resources/application-test.yml +++ b/kok-api/src/test/resources/application-test.yml @@ -1,6 +1,4 @@ spring: - jackson: - time-zone: UTC profiles: active: test flyway: diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 1f8d02e3..151f1c86 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -32,8 +32,10 @@ private Room(String id, String roomName, int capacity, Member member) { this.capacity = capacity; this.member = member; this.createdDateTime = LocalDateTime.now().withNano(0); - this.locationInputLimitDateTime = createdDateTime.plusHours(LOCATION_INPUT_TIME_LIMIT); - this.voteLimitDateTime = locationInputLimitDateTime.plusHours(VOTE_TIME_LIMIT); + this.locationInputLimitDateTime = createdDateTime.plusHours(LOCATION_INPUT_TIME_LIMIT) + .withNano(0); + this.voteLimitDateTime = locationInputLimitDateTime.plusHours(VOTE_TIME_LIMIT) + .withNano(0); this.status = RoomStatus.LOCATION_INPUT; } diff --git a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java index 7b903eaa..8cb2c1ce 100644 --- a/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java +++ b/kok-core/src/main/java/com/kok/kokcore/station/domain/entity/Route.java @@ -36,12 +36,14 @@ public class Route { Map.entry("๊ฒฝ์ธ์„ ", "1ํ˜ธ์„ "), Map.entry("๊ฒฝ์›์„ ", "1ํ˜ธ์„ "), Map.entry("์žฅํ•ญ์„ ", "1ํ˜ธ์„ "), + Map.entry("๊ณผ์ฒœ์„ ", "4ํ˜ธ์„ "), + Map.entry("7ํ˜ธ์„ (์ธ์ฒœ)", "7ํ˜ธ์„ "), + Map.entry("9ํ˜ธ์„ (์—ฐ์žฅ)", "9ํ˜ธ์„ "), Map.entry("์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ)", "์‹ ๋ถ„๋‹น์„ "), Map.entry("์‹ ๋ถ„๋‹น์„ (์—ฐ์žฅ2)", "์‹ ๋ถ„๋‹น์„ "), - Map.entry("9ํ˜ธ์„ (์—ฐ์žฅ)", "9ํ˜ธ์„ "), - Map.entry("7ํ˜ธ์„ (์ธ์ฒœ)", "7ํ˜ธ์„ "), Map.entry("์ˆ˜์ธ์„ ", "์ˆ˜์ธ๋ถ„๋‹น์„ "), Map.entry("๋ถ„๋‹น์„ ", "์ˆ˜์ธ๋ถ„๋‹น์„ "), + Map.entry("์•ˆ์‚ฐ์„ ", "์ˆ˜์ธ๋ถ„๋‹น์„ "), Map.entry("์ˆ˜๋„๊ถŒ ๊ด‘์—ญ๊ธ‰ํ–‰์ฒ ๋„", "GTX"), Map.entry("๊ณตํ•ญ์ฒ ๋„1ํ˜ธ์„ ", "๊ณตํ•ญ์ฒ ๋„") ); diff --git a/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java b/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java index 9e499f07..7eb33333 100644 --- a/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/station/port/out/dto/StationRouteDtosTest.java @@ -85,12 +85,12 @@ void toRoutesByStations() { List routes = stationRouteDtos.toRoutesByStations(List.of(station1, station2)); // then - List codes = routes.stream().map(Route::getCode).toList(); + List names = routes.stream().map(Route::getName).toList(); List stations = routes.stream().map(Route::getStation).distinct().toList(); assertAll( () -> assertThat(routes).hasSize(3), - () -> assertThat(codes).containsExactlyInAnyOrder(1L, 2L, 3L), + () -> assertThat(names).containsExactlyInAnyOrder("1ํ˜ธ์„ ", "2ํ˜ธ์„ ", "์‹ ๋ถ„๋‹น์„ "), () -> assertThat(stations).containsExactlyInAnyOrder(station1, station2) ); } From dfb59e7514a9bc83ce1df10fcb73e4c7c0ef02fe Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 5 Apr 2025 00:56:25 +0900 Subject: [PATCH 152/163] :wrench: config: add jackson timezone (#111) --- kok-api/src/main/resources/application-dev.yml | 2 ++ kok-api/src/main/resources/application-prod.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 18ec1cfd..cfb0d5b3 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -1,4 +1,6 @@ spring: + jackson: + time-zone: Asia/Seoul datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DB_URL} diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 9e7f743b..6b597d4c 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -1,4 +1,6 @@ spring: + jackson: + time-zone: Asia/Seoul datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DB_URL} From 0c0ee0e6e8804c39146f56d51eb1e24641d7d2ba Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 5 Apr 2025 01:30:16 +0900 Subject: [PATCH 153/163] =?UTF-8?q?=F0=9F=90=9B=20[Fix/candidate]=20remove?= =?UTF-8?q?=20candidate=20duplication=20(#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: fix: fix getting candidate with duplication * :bug: fix: fix to convert UTC to KST --- .../application/service/StationFacadeService.java | 8 +++++--- .../in/dto/response/VoteDeadlineResponse.java | 12 +++++++++--- .../kokapi/vote/adapter/in/web/VoteController.java | 3 ++- .../vote/application/service/CandidateService.java | 3 ++- .../vote/application/service/VoteFacadeService.java | 12 +++++++----- .../kokcore/vote/usecase/GetCandidateUseCase.java | 3 ++- 6 files changed, 27 insertions(+), 14 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java index a3360e52..be121c96 100644 --- a/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/station/application/service/StationFacadeService.java @@ -1,11 +1,13 @@ package com.kok.kokapi.station.application.service; import com.kok.kokapi.station.adapter.in.dto.response.RecommendedStationResponse; -import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.usecase.SystemRecommendUseCase; import com.kok.kokcore.station.usecase.UserRecommendUseCase; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -27,13 +29,13 @@ public List getCandidateStationResponse(String roomI .toList(); } - public List getCandidateStation(String roomId) { + public Set getCandidateStation(String roomId) { List recommendedStations = systemRecommendedUseCase.systemRecommendStation(roomId); List customStations = userRecommendUseCase.getUserRecommendStation(roomId); return Stream.concat( recommendedStations.stream(), customStations.stream() - ).toList(); + ).collect(Collectors.toSet()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java index 1d7ce83e..159abbdb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteDeadlineResponse.java @@ -1,16 +1,22 @@ package com.kok.kokapi.vote.adapter.in.dto.response; -import com.fasterxml.jackson.annotation.JsonFormat; import com.kok.kokcore.room.domain.Room; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; public record VoteDeadlineResponse( int candidateCount, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul") LocalDateTime endAt ) { public static VoteDeadlineResponse of(Room room, int candidateCount) { - return new VoteDeadlineResponse(candidateCount, room.getVoteLimitDateTime()); + return new VoteDeadlineResponse( + candidateCount, + room.getVoteLimitDateTime() + .atZone(ZoneOffset.UTC) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .toLocalDateTime() + ); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 4c84f360..e6b35010 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.Operation; import java.time.LocalDateTime; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -53,7 +54,7 @@ public ResponseEntity> createVote( @PathVariable String memberId, @RequestBody VoteRequest voteRequest ) { - List stations = stationFacadeService.getCandidateStation(roomId); + Set stations = stationFacadeService.getCandidateStation(roomId); voteFacadeService.saveVotes(roomId, memberId, voteRequest.agreedStationIds(), stations); return ResponseEntity.ok(ApiResponseDto.success(null)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java index 465fd7d3..a6ca8a27 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java @@ -6,6 +6,7 @@ import com.kok.kokcore.vote.port.out.SaveCandidatePort; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,7 +18,7 @@ public class CandidateService implements GetCandidateUseCase { private final LoadCandidatePort loadCandidatePort; @Override - public List saveAndGetCandidates(String roomId, List stations) { + public List saveAndGetCandidates(String roomId, Set stations) { if (!loadCandidatePort.isExistsByRoomId(roomId)) { List candidates = stations.stream() .map(station -> new Candidate(roomId, station.getId())) diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index b1c1cc49..777ccdb3 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -26,6 +26,8 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -49,9 +51,9 @@ public class VoteFacadeService { public List getCandidates(String roomId, String memberId) { List recommendedStations = systemRecommendUseCase.systemRecommendStation(roomId); List customStations = userRecommendUseCase.getUserRecommendStation(roomId); - List allStations = Stream.concat( + Set allStations = Stream.concat( recommendedStations.stream(), customStations.stream() - ).toList(); + ).collect(Collectors.toSet()); getCandidateUseCase.saveAndGetCandidates(roomId, allStations); List responses = new ArrayList<>(); @@ -120,7 +122,7 @@ public VoteResultResponse getVoteResult(String roomId, String memberId) { } public void saveVotes( - String roomId, String memberId, List agreedStationIds, List stations) { + String roomId, String memberId, List agreedStationIds, Set stations) { getCandidateUseCase.saveAndGetCandidates(roomId, stations); saveVoteUseCase.saveVotes(roomId, memberId, agreedStationIds); } @@ -128,9 +130,9 @@ public void saveVotes( public int countCandidates(String roomId) { List recommendedStations = systemRecommendUseCase.systemRecommendStation(roomId); List customStations = userRecommendUseCase.getUserRecommendStation(roomId); - List stations = Stream.concat( + Set stations = Stream.concat( recommendedStations.stream(), customStations.stream() - ).toList(); + ).collect(Collectors.toSet()); return getCandidateUseCase.saveAndGetCandidates(roomId, stations).size(); } } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java index a37e77f4..88f90b63 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetCandidateUseCase.java @@ -3,8 +3,9 @@ import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.vote.domain.Candidate; import java.util.List; +import java.util.Set; public interface GetCandidateUseCase { - List saveAndGetCandidates(String roomId, List stations); + List saveAndGetCandidates(String roomId, Set stations); } From cdb278a9654331a5104f0580eb9c5cc81b4e604a Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sat, 5 Apr 2025 01:56:29 +0900 Subject: [PATCH 154/163] :bug: fix: fix candidate-duplication (#113) --- .../service/VoteFacadeService.java | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index 777ccdb3..17561eed 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -51,32 +51,35 @@ public class VoteFacadeService { public List getCandidates(String roomId, String memberId) { List recommendedStations = systemRecommendUseCase.systemRecommendStation(roomId); List customStations = userRecommendUseCase.getUserRecommendStation(roomId); - Set allStations = Stream.concat( + Set stations = Stream.concat( recommendedStations.stream(), customStations.stream() ).collect(Collectors.toSet()); - getCandidateUseCase.saveAndGetCandidates(roomId, allStations); + getCandidateUseCase.saveAndGetCandidates(roomId, stations); List responses = new ArrayList<>(); - responses.addAll(createCandidateResponses(recommendedStations, roomId, memberId, true)); - responses.addAll(createCandidateResponses(customStations, roomId, memberId, false)); + for (Station station : stations) { + if (recommendedStations.contains(station)) { + responses.add(createCandidateResponse(station, roomId, memberId, true)); + continue; + } + if (customStations.contains(station)) { + responses.add(createCandidateResponse(station, roomId, memberId, false)); + } + } return responses; } - private List createCandidateResponses( - List stations, String roomId, String memberId, boolean isRecommended) { - List responses = new ArrayList<>(); - for (Station station : stations) { - List routes = retrieveRouteUseCase.retrieveRoutes(station); - TmapPublicTransportationParsedResponse transportation = getTransportationParsedResponse( - roomId, memberId, station); - CandidateResponse response = isRecommended - ? CandidateResponse.recommended(station, routes, transportation, List.of()) - : CandidateResponse.custom(station, routes, transportation, List.of()); - responses.add(response); - } - return responses; + private CandidateResponse createCandidateResponse( + Station station, String roomId, String memberId, boolean isRecommended) { + List routes = retrieveRouteUseCase.retrieveRoutes(station); + TmapPublicTransportationParsedResponse transportation = getTransportationParsedResponse( + roomId, memberId, station); + CandidateResponse response = isRecommended + ? CandidateResponse.recommended(station, routes, transportation, List.of()) + : CandidateResponse.custom(station, routes, transportation, List.of()); + return response; } private TmapPublicTransportationParsedResponse getTransportationParsedResponse( From 925b26f454b3aaa2b68e051c5bb7885faf4857da Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:15:28 +0900 Subject: [PATCH 155/163] [Feature/vote] fix getting current result api due to change of UI (#117) * :recycle: refactor: remove voteStatus from domain and business policy due to only need agreed votes * :recycle: refactor: refactor deleteVotePort * :recycle: refactor: rename key for votes and save vote port methods * :recycle: refactor: rename load vote port method * :sparkles: feat: implement finding voted station Ids order by voteCount port * :recycle: refactor: remove unused method * :sparkles: feat: implement vote result domain and sorting by priority if vote count is same * :sparkles: feat: implement getting vote results usecase * :sparkles: feat: implement initiating vote Count per candidate when saving candidates * :bug: fix: fix wrong sorting * :sparkles: feat: implement tagging TOP/CLOSE on voteResults domain * :sparkles: feat: implement getting members in memberIds by roomId * :sparkles: feat: implement getting current vote result controller and facade service * :bug: fix: fix to get final result from voteResults which is sorted by priority if votedCount is Same * :bug: fix: fix to update tag when same or more than 60% * :recycle: refactor: add script for changing priority column default value from 0 to 1 --- .../application/service/RoomQueryService.java | 7 + .../in/dto/response/ResultResponse.java | 20 ++- ...se.java => VoteCurrentResultResponse.java} | 2 +- .../in/dto/response/VotedMemberResponse.java | 3 +- .../vote/adapter/in/web/VoteController.java | 16 +- .../persistence/VoteCommandRedisAdapter.java | 73 +++++----- .../vote/adapter/out/persistence/VoteKey.java | 55 +++---- .../persistence/VoteQueryRedisAdapter.java | 55 ++++--- .../application/service/CandidateService.java | 9 ++ .../service/VoteFacadeService.java | 27 ++-- .../vote/application/service/VoteService.java | 113 +++++++-------- .../migration/V4__change_priority_default.sql | 2 + .../VoteCommandRedisAdapterTest.java | 126 ++++++++++------ .../VoteQueryRedisAdapterTest.java | 65 +++++---- .../application/service/VoteServiceTest.java | 137 +++++++----------- .../com/kok/kokcore/vote/VoteResultsTest.java | 81 +++++++++++ .../com/kok/kokcore/room/domain/Room.java | 7 + .../kokcore/room/usecase/GetRoomUseCase.java | 2 + .../com/kok/kokcore/vote/VoteResults.java | 41 ++++++ .../com/kok/kokcore/vote/domain/Vote.java | 13 +- .../kok/kokcore/vote/domain/VoteResult.java | 44 ++++++ .../kok/kokcore/vote/domain/vo/ResultTag.java | 8 + .../kokcore/vote/domain/vo/VoteStatus.java | 27 ---- .../kokcore/vote/port/out/DeleteVotePort.java | 14 +- .../kokcore/vote/port/out/LoadVotePort.java | 10 +- .../kokcore/vote/port/out/SaveVotePort.java | 12 +- .../kokcore/vote/usecase/GetVoteUseCase.java | 8 +- 27 files changed, 574 insertions(+), 403 deletions(-) rename kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/{VoteResultResponse.java => VoteCurrentResultResponse.java} (77%) create mode 100644 kok-api/src/main/resources/db/migration/V4__change_priority_default.sql create mode 100644 kok-api/src/test/java/com/kok/kokcore/vote/VoteResultsTest.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/VoteResults.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/domain/VoteResult.java create mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/ResultTag.java delete mode 100644 kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index b2ade337..6d81aafb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -68,4 +68,11 @@ public Member getParticipant(String roomId, String memberId) { public long getParticipantsCount(String roomId) { return readLocationPort.countParticipantsById(roomId); } + + @Override + public List getParticipantsByRoomIdInMemberIds(String roomId, List memberIds) { + return getParticipants(roomId).stream() + .filter(member -> memberIds.contains(member.getMemberId())) + .toList(); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java index 46dfddd7..983765cb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/ResultResponse.java @@ -2,24 +2,30 @@ import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.station.domain.entity.Station; -import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.domain.VoteResult; import java.util.List; public record ResultResponse( long stationId, String stationName, - String voteStatus, int votedCount, - List members + List members, + String resultTag ) { - public static ResultResponse of(Station station, Vote vote, List members) { + public static ResultResponse of(Station station, VoteResult voteResult, List members) { return new ResultResponse( station.getId(), station.getName(), - vote.getVoteStatus().getName(), - members.size(), - members.stream().map(VotedMemberResponse::from).toList() + voteResult.getVotedCount(), + getVotedMemberResponses(members), + voteResult.getResultTag().name() ); } + + private static List getVotedMemberResponses(List members) { + return members.stream() + .map(VotedMemberResponse::from) + .toList(); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteCurrentResultResponse.java similarity index 77% rename from kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultResponse.java rename to kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteCurrentResultResponse.java index cae77f06..7d42805b 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteResultResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VoteCurrentResultResponse.java @@ -2,7 +2,7 @@ import java.util.List; -public record VoteResultResponse( +public record VoteCurrentResultResponse( int notVotedCount, List results ) { diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java index 5051580b..a102f0bb 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/dto/response/VotedMemberResponse.java @@ -3,11 +3,10 @@ import com.kok.kokcore.room.domain.Member; public record VotedMemberResponse( - String id, String imageUrl ) { public static VotedMemberResponse from(Member member) { - return new VotedMemberResponse(member.getMemberId(), member.getProfile()); + return new VotedMemberResponse(member.getProfile()); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index e6b35010..459d0359 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -6,8 +6,8 @@ import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.VoteCurrentResultResponse; import com.kok.kokapi.vote.adapter.in.dto.response.VoteDeadlineResponse; -import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultResponse; import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultStationResponse; import com.kok.kokapi.vote.application.service.VoteFacadeService; import com.kok.kokcore.room.domain.Room; @@ -20,7 +20,6 @@ import io.swagger.v3.oas.annotations.Operation; import java.time.LocalDateTime; import java.util.List; -import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -54,8 +53,7 @@ public ResponseEntity> createVote( @PathVariable String memberId, @RequestBody VoteRequest voteRequest ) { - Set stations = stationFacadeService.getCandidateStation(roomId); - voteFacadeService.saveVotes(roomId, memberId, voteRequest.agreedStationIds(), stations); + voteFacadeService.saveVotes(roomId, memberId, voteRequest.agreedStationIds()); return ResponseEntity.ok(ApiResponseDto.success(null)); } @@ -84,11 +82,11 @@ public ResponseEntity> closeVote(@PathVariable String roomI return ResponseEntity.ok(ApiResponseDto.success(null)); } - @Operation(summary = "ํˆฌํ‘œ ๊ฒฐ๊ณผ ์กฐํšŒ", description = "๋ฐฉ ID์™€ ์‚ฌ์šฉ์ž ID์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ๊ธฐ์ค€์œผ๋กœ ํ›„๋ณด์ง€๋ณ„ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") - @GetMapping("/votes/{roomId}/{memberId}") - public ResponseEntity> getVoteResult( - @PathVariable String roomId, @PathVariable String memberId) { - VoteResultResponse response = voteFacadeService.getVoteResult(roomId, memberId); + @Operation(summary = "ํˆฌํ‘œ ํ˜„ํ™ฉ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•ด ํ˜„์žฌ๊นŒ์ง€ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/votes/{roomId}/results/current") + public ResponseEntity> getVoteResult( + @PathVariable String roomId) { + VoteCurrentResultResponse response = voteFacadeService.getVoteCurrentResult(roomId); return ResponseEntity.ok(ApiResponseDto.success(response)); } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java index 055d4213..b1a23734 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapter.java @@ -1,7 +1,6 @@ package com.kok.kokapi.vote.adapter.out.persistence; import com.kok.kokapi.common.util.RedisExecutor; -import com.kok.kokcore.vote.domain.Vote; import com.kok.kokcore.vote.port.out.DeleteVotePort; import com.kok.kokcore.vote.port.out.SaveVotePort; import java.time.Duration; @@ -22,24 +21,20 @@ public class VoteCommandRedisAdapter implements SaveVotePort, DeleteVotePort { private final RedisTemplate redisTemplate; @Override - public void saveVoteMemberHash(List votes) { - validate(votes); - String roomId = votes.getFirst().getRoomId(); - String memberId = votes.getFirst().getMemberId(); - String memberHashKey = VoteKey.memberKey(roomId, memberId); + public void saveVotedStationsByRoomIdAndMemberId(List stationIds, String roomId, + String memberId) { + String memberKey = VoteKey.votedStationsByMemberKey(roomId, memberId); RedisExecutor.runOrThrow("saveVoteMemberHash", () -> { - for (Vote vote : votes) { - String field = VoteKey.stationField(vote.getStationId()); - String status = vote.getVoteStatus().getName(); - redisTemplate.opsForHash().put(memberHashKey, field, status); + for (Long stationId : stationIds) { + redisTemplate.opsForSet().add(memberKey, stationId); } - redisTemplate.expire(memberHashKey, getTTL(memberHashKey)); + redisTemplate.expire(memberKey, getTTL(memberKey)); }); } @Override - public void saveVotedMemberSet(String roomId, String memberId) { - String votedMemberSetKey = VoteKey.voteKey(roomId); + public void saveVotedMemberByRoomId(String roomId, String memberId) { + String votedMemberSetKey = VoteKey.voteCompletedMembersKey(roomId); RedisExecutor.runOrThrow("saveVotedMemberSet", () -> { redisTemplate.opsForSet().add(votedMemberSetKey, memberId); redisTemplate.expire(votedMemberSetKey, getTTL(votedMemberSetKey)); @@ -47,56 +42,64 @@ public void saveVotedMemberSet(String roomId, String memberId) { } @Override - public void saveVoteStatusSet(Vote vote) { - String key = VoteKey.voteStatusMemberSetKey(vote); + public void initiateVoteCountByRoomIdAndStationIds(String roomId, List stationIds) { + String key = VoteKey.votedCountOfStationKey(roomId); + RedisExecutor.runOrThrow("initiateVoteCountByRoomIdAndStationIds", () -> { + for (Long stationId : stationIds) { + redisTemplate.opsForZSet().addIfAbsent(key, stationId, 0); + redisTemplate.expire(key, getTTL(key)); + } + }); + } + + @Override + public void saveVotedMemberByRoomIdAndStationId(String memberId, String roomId, + long stationId) { + String key = VoteKey.votedMembersOfStationKey(roomId, stationId); RedisExecutor.runOrThrow("saveVoteStatusSet", () -> { - redisTemplate.opsForSet().add(key, vote.getMemberId()); + redisTemplate.opsForSet().add(key, memberId); redisTemplate.expire(key, getTTL(key)); }); } @Override - public void incrementVoteStatusCountZSet(Vote vote) { - String key = VoteKey.voteStatusCountZSetKey(vote); + public void increaseVotedCountByRoomIdAndStationId(String roomId, long stationId) { + String key = VoteKey.votedCountOfStationKey(roomId); RedisExecutor.runOrThrow("incrementVoteStatusCountZSet", () -> { - redisTemplate.opsForZSet().incrementScore(key, vote.getStationId(), 1); + redisTemplate.opsForZSet().incrementScore(key, stationId, 1); redisTemplate.expire(key, getTTL(key)); }); } - private void validate(List votes) { - if (Objects.isNull(votes) || votes.isEmpty()) { - throw new IllegalArgumentException("No votes to save"); - } - } - @Override - public void removeMemberFromVoteStatusSet(Vote vote) { - String key = VoteKey.voteStatusMemberSetKey(vote); + public void deleteVotedMemberByRoomIdAndStationId( + String memberId, String roomId, long stationId + ) { + String key = VoteKey.votedMembersOfStationKey(roomId, stationId); RedisExecutor.runOrThrow("removeMemberFromVoteStatusSet", () -> - redisTemplate.opsForSet().remove(key, vote.getMemberId()) + redisTemplate.opsForSet().remove(key, memberId) ); } @Override - public void decrementVoteCountInZSet(Vote vote) { - String key = VoteKey.voteStatusCountZSetKey(vote); + public void decreaseVotedCountByRoomIdAndStationId(String roomId, long stationId) { + String key = VoteKey.votedCountOfStationKey(roomId); RedisExecutor.runOrThrow("decrementVoteCountInZSet", () -> - redisTemplate.opsForZSet().incrementScore(key, vote.getStationId(), -1) + redisTemplate.opsForZSet().incrementScore(key, stationId, -1) ); } @Override - public void deleteMemberVoteHash(String roomId, String memberId) { - String key = VoteKey.memberKey(roomId, memberId); + public void deleteVotedStationsByRoomIdAndMemberId(String roomId, String memberId) { + String key = VoteKey.votedStationsByMemberKey(roomId, memberId); RedisExecutor.runOrThrow("deleteMemberVoteHash", () -> redisTemplate.delete(key) ); } @Override - public void removeMemberFromVotedSet(String roomId, String memberId) { - String key = VoteKey.voteKey(roomId); + public void deleteVotedMemberByRoomId(String roomId, String memberId) { + String key = VoteKey.voteCompletedMembersKey(roomId); RedisExecutor.runOrThrow("removeMemberFromVotedSet", () -> redisTemplate.opsForSet().remove(key, memberId) ); diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java index 51e2fb75..f589cc99 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteKey.java @@ -1,60 +1,41 @@ package com.kok.kokapi.vote.adapter.out.persistence; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public enum VoteKey { - VOTE_MEMBER_SET("vote:%s"), // Set - MEMBER_HASH("member:%s:%s"), // Hash - AGREE_COUNT_ZSET("agree:%s"), // ZSET - AGREE_MEMBER_SET("agree:%s:%d"), // Set per station - DISAGREE_COUNT_ZSET("disagree:%s"), // ZSET - DISAGREE_MEMBER_SET("disagree:%s:%d"); // Set per station + VOTE_COMPLETED_MEMBERS_SET("votedDoneMembers:%s"), // Set + VOTED_STATIONS_BY_MEMBER_SET("memberVoted:%s:%s"), // Set + VOTED_COUNT_OF_STATION_ZSET("votedCount:%s"), // ZSET + VOTED_MEMBERS_OF_STATION_SET("votedMembers:%s:%d") // Set per station + ; private final String format; // ํˆฌํ‘œ ์™„๋ฃŒ์ž ๋ชฉ๋ก key - public static String voteKey(String roomId) { - return String.format(VOTE_MEMBER_SET.format, roomId); + public static String voteCompletedMembersKey(String roomId) { + return String.format(VOTE_COMPLETED_MEMBERS_SET.format, roomId); } // ๋ฉค๋ฒ„๋ณ„ ํˆฌํ‘œ ์ƒ์„ธ ์ €์žฅ key - public static String memberKey(String roomId, String memberId) { - return String.format(MEMBER_HASH.format, roomId, memberId); + public static String votedStationsByMemberKey(String roomId, String memberId) { + return String.format(VOTED_STATIONS_BY_MEMBER_SET.format, roomId, memberId); } - // ์ฐฌ์„ฑ/๋ฐ˜๋Œ€ ZSET key (stationId๋ณ„ ํˆฌํ‘œ ์ˆ˜ ์ €์žฅ) - public static String voteStatusCountZSetKey(Vote vote) { - return vote.getVoteStatus().isAgree() - ? String.format(AGREE_COUNT_ZSET.format, vote.getRoomId()) - : String.format(DISAGREE_COUNT_ZSET.format, vote.getRoomId()); + // stationId๋ณ„ ํˆฌํ‘œ ์ˆ˜ ์ €์žฅ key + public static String votedCountOfStationKey(String roomId) { + return String.format(VOTED_COUNT_OF_STATION_ZSET.format, roomId); } - public static String voteStatusCountZSetKey(VoteStatus voteStatus, String roomId) { - return voteStatus.isAgree() - ? String.format(AGREE_COUNT_ZSET.format, roomId) - : String.format(DISAGREE_COUNT_ZSET.format, roomId); + // stationId๋ณ„ memberId ์ €์žฅ key + public static String votedMembersOfStationKey(Vote vote) { + return String.format(VOTED_MEMBERS_OF_STATION_SET.format, vote.getRoomId(), + vote.getStationId()); } - // ์ฐฌ์„ฑ/๋ฐ˜๋Œ€ Set key (stationId๋ณ„ memberId ์ €์žฅ) - public static String voteStatusMemberSetKey(Vote vote) { - return vote.getVoteStatus().isAgree() - ? String.format(AGREE_MEMBER_SET.format, vote.getRoomId(), vote.getStationId()) - : String.format(DISAGREE_MEMBER_SET.format, vote.getRoomId(), vote.getStationId()); - } - - public static String voteStatusMemberSetKey( - VoteStatus voteStatus, String roomId, long stationId) { - return voteStatus.isAgree() - ? String.format(AGREE_MEMBER_SET.format, roomId, stationId) - : String.format(DISAGREE_MEMBER_SET.format, roomId, stationId); - } - - // ํ•„๋“œ ์ด๋ฆ„์œผ๋กœ ์“ฐ์ผ stationId - public static String stationField(long stationId) { - return String.valueOf(stationId); + public static String votedMembersOfStationKey(String roomId, long stationId) { + return String.format(VOTED_MEMBERS_OF_STATION_SET.format, roomId, stationId); } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java index 938ee0aa..950489ed 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapter.java @@ -2,18 +2,16 @@ import com.kok.kokapi.common.util.RedisExecutor; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; import com.kok.kokcore.vote.port.out.LoadVotePort; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.stereotype.Repository; @Slf4j @@ -25,40 +23,37 @@ public class VoteQueryRedisAdapter implements LoadVotePort { @Override public boolean isExistsByRoomIdAndMemberId(String roomId, String memberId) { - String key = VoteKey.memberKey(roomId, memberId); + String key = VoteKey.votedStationsByMemberKey(roomId, memberId); return RedisExecutor.runOrElseGet("isExistsByRoomIdAndMemberId", () -> - !redisTemplate.opsForHash().entries(key).isEmpty(), false + !redisTemplate.opsForSet().members(key).isEmpty(), false ); } @Override public List findAllByRoomIdAndMemberId(String roomId, String memberId) { - String key = VoteKey.memberKey(roomId, memberId); + String key = VoteKey.votedStationsByMemberKey(roomId, memberId); return RedisExecutor.runOrElseGet("findAllByRoomIdAndMemberId", () -> { - Map voteInfos = redisTemplate.opsForHash().entries(key); + Set stationIds = redisTemplate.opsForSet().members(key); List votes = new ArrayList<>(); - for (Entry voteInfo : voteInfos.entrySet()) { - Long stationId = getStationId(voteInfo); - String voteStatus = String.valueOf(voteInfo.getValue()); - votes.add(new Vote(roomId, stationId, memberId, voteStatus)); + for (Object stationId : stationIds) { + votes.add(new Vote(roomId, getStationId(stationId), memberId)); } return votes; }, List.of()); } @Override - public int countMembersByRoomId(String roomId) { + public int countVotedMembersByRoomId(String roomId) { return RedisExecutor.runOrElseGet("countMembersByRoomId", () -> { - String key = VoteKey.voteKey(roomId); + String key = VoteKey.voteCompletedMembersKey(roomId); Long count = redisTemplate.opsForSet().size(key); return Objects.nonNull(count) ? count.intValue() : 0; }, 0); } @Override - public List findMemberIdsByRoomIdAndStationIdAndStatus( - String roomId, long stationId, VoteStatus voteStatus) { - String key = VoteKey.voteStatusMemberSetKey(voteStatus, roomId, stationId); + public List findMemberIdsByRoomIdAndStationId(String roomId, long stationId) { + String key = VoteKey.votedMembersOfStationKey(roomId, stationId); Set memberIds = RedisExecutor.runOrElseGet( "findMembersByRoomIdAndStationIdAndStatus", () -> redisTemplate.opsForSet().members(key), Set.of()); @@ -66,9 +61,9 @@ public List findMemberIdsByRoomIdAndStationIdAndStatus( } @Override - public long getFirstStationIdByRoomIdAndVoteStatus(String roomId, VoteStatus voteStatus) { + public long findFirstStationIdByRoomIdOrderByVotedCount(String roomId) { return RedisExecutor.runOrElseGet("getFirstStationIdByRoomIdAndVoteStatus", () -> { - String key = VoteKey.voteStatusCountZSetKey(voteStatus, roomId); + String key = VoteKey.votedCountOfStationKey(roomId); Set> sorted = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 0); @@ -77,17 +72,31 @@ public long getFirstStationIdByRoomIdAndVoteStatus(String roomId, VoteStatus vot } Object maxScoredStationId = sorted.iterator().next().getValue(); - return getStationId(String.valueOf(maxScoredStationId)); + return getStationId(maxScoredStationId); }, -1L); } - private Long getStationId(Entry voteInfo) { - return getStationId(voteInfo.getKey().toString()); + @Override + public List findStationIdsByRoomIdOrderByVotedCount(String roomId) { + return RedisExecutor.runOrElseGet("getStationIdsByRoomIdOrderByVotedCount", () -> { + String key = VoteKey.votedCountOfStationKey(roomId); + Set> sorted = + redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, -1); + + if (Objects.isNull(sorted) || sorted.isEmpty()) { + return List.of(); + } + + return sorted.stream() + .map(TypedTuple::getValue) + .map(this::getStationId) + .toList(); + }, List.of()); } - private Long getStationId(String stationId) { + private Long getStationId(Object stationId) { try { - return Long.valueOf(stationId); + return Long.valueOf(String.valueOf(stationId)); } catch (NumberFormatException e) { log.warn("Invalid stationId format in Redis: {}", stationId, e); throw new IllegalArgumentException("Invalid stationId format: " + stationId); diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java index a6ca8a27..11ce85ff 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/CandidateService.java @@ -4,6 +4,7 @@ import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.port.out.LoadCandidatePort; import com.kok.kokcore.vote.port.out.SaveCandidatePort; +import com.kok.kokcore.vote.port.out.SaveVotePort; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; import java.util.List; import java.util.Set; @@ -16,6 +17,7 @@ public class CandidateService implements GetCandidateUseCase { private final SaveCandidatePort saveCandidatePort; private final LoadCandidatePort loadCandidatePort; + private final SaveVotePort saveVotePort; @Override public List saveAndGetCandidates(String roomId, Set stations) { @@ -24,7 +26,14 @@ public List saveAndGetCandidates(String roomId, Set stations .map(station -> new Candidate(roomId, station.getId())) .toList(); saveCandidatePort.saveAll(candidates); + saveVotePort.initiateVoteCountByRoomIdAndStationIds(roomId, getStationIds(candidates)); } return loadCandidatePort.findByRoomId(roomId); } + + private List getStationIds(List candidates) { + return candidates.stream() + .map(Candidate::getStationId) + .toList(); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index 17561eed..7db3120a 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -7,7 +7,7 @@ import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; import com.kok.kokapi.vote.adapter.in.dto.response.ResultResponse; -import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultResponse; +import com.kok.kokapi.vote.adapter.in.dto.response.VoteCurrentResultResponse; import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.usecase.ReadLocationUseCase; import com.kok.kokcore.room.domain.Member; @@ -19,7 +19,8 @@ import com.kok.kokcore.station.usecase.RetrieveRouteUseCase; import com.kok.kokcore.station.usecase.SystemRecommendUseCase; import com.kok.kokcore.station.usecase.UserRecommendUseCase; -import com.kok.kokcore.vote.domain.Vote; +import com.kok.kokcore.vote.VoteResults; +import com.kok.kokcore.vote.domain.VoteResult; import com.kok.kokcore.vote.usecase.GetCandidateUseCase; import com.kok.kokcore.vote.usecase.GetVoteUseCase; import com.kok.kokcore.vote.usecase.SaveVoteUseCase; @@ -37,7 +38,6 @@ public class VoteFacadeService { private final GetCandidateUseCase getCandidateUseCase; - private final GetStationUseCase getStationUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; private final GetVoteUseCase getVoteUseCase; private final GetRoomUseCase getRoomUseCase; @@ -47,6 +47,7 @@ public class VoteFacadeService { private final ReadLocationUseCase readLocationUseCase; private final SystemRecommendUseCase systemRecommendUseCase; private final UserRecommendUseCase userRecommendUseCase; + private final GetStationUseCase getStationUseCase; public List getCandidates(String roomId, String memberId) { List recommendedStations = systemRecommendUseCase.systemRecommendStation(roomId); @@ -110,23 +111,21 @@ public List getMemberVoteStatus(String roomId) { return responses; } - public VoteResultResponse getVoteResult(String roomId, String memberId) { + public VoteCurrentResultResponse getVoteCurrentResult(String roomId) { List responses = new ArrayList<>(); Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); int votedCount = getVoteUseCase.countVotedMembers(roomId); - List votes = getVoteUseCase.getVotesByMember(roomId, memberId); - for (Vote vote : votes) { - Station station = getStationUseCase.getStation(vote.getStationId()); - List members = getVoteUseCase.getMembersByVote(vote); - ResultResponse response = ResultResponse.of(station, vote, members); - responses.add(response); + VoteResults voteResults = getVoteUseCase.getVoteResultsByRoomId(roomId); + for (VoteResult voteResult : voteResults.getVoteResults()) { + Station station = getStationUseCase.getStation(voteResult.getStationId()); + List members = getRoomUseCase.getParticipantsByRoomIdInMemberIds( + roomId, voteResult.getMemberIds()); + responses.add(ResultResponse.of(station, voteResult, members)); } - return new VoteResultResponse(room.getNotVotedCount(votedCount), responses); + return new VoteCurrentResultResponse(room.getNotVotedCount(votedCount), responses); } - public void saveVotes( - String roomId, String memberId, List agreedStationIds, Set stations) { - getCandidateUseCase.saveAndGetCandidates(roomId, stations); + public void saveVotes(String roomId, String memberId, List agreedStationIds) { saveVoteUseCase.saveVotes(roomId, memberId, agreedStationIds); } diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java index d93f3be9..7a8e7868 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -6,18 +6,16 @@ import com.kok.kokcore.room.port.out.LoadRoomPort; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.port.out.RetrieveStationsPort; -import com.kok.kokcore.vote.domain.Candidate; +import com.kok.kokcore.vote.VoteResults; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; +import com.kok.kokcore.vote.domain.VoteResult; import com.kok.kokcore.vote.port.out.DeleteVotePort; -import com.kok.kokcore.vote.port.out.LoadCandidatePort; import com.kok.kokcore.vote.port.out.LoadVotePort; import com.kok.kokcore.vote.port.out.SaveVotePort; import com.kok.kokcore.vote.usecase.GetVoteUseCase; import com.kok.kokcore.vote.usecase.SaveVoteUseCase; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -25,9 +23,10 @@ @RequiredArgsConstructor public class VoteService implements SaveVoteUseCase, GetVoteUseCase { + private static final int MINIMUM_VOTED_RATIO = 60; + private final SaveVotePort saveVotePort; private final LoadVotePort loadVotePort; - private final LoadCandidatePort loadCandidatePort; private final DeleteVotePort deleteVotePort; private final LoadRoomPort loadRoomPort; private final RetrieveStationsPort retrieveStationsPort; @@ -38,47 +37,36 @@ public void saveVotes(String roomId, String memberId, List agreedStationId validate(roomId, memberId); validateRoomStatusIfNotOnVote(roomId); initiate(roomId, memberId); - List votes = getVotes(roomId, memberId, agreedStationIds); - // 1. ๋ฉค๋ฒ„์˜ ํˆฌํ‘œ ๋‚ด์šฉ Hash ์ €์žฅ - saveVotePort.saveVoteMemberHash(votes); - // 2. ํˆฌํ‘œ ์™„๋ฃŒ Set์— ๋ฉค๋ฒ„ ์ถ”๊ฐ€ - saveVotePort.saveVotedMemberSet(roomId, memberId); + // 1. ๋ฉค๋ฒ„๊ฐ€ ํˆฌํ‘œํ•œ stationId set ์ €์žฅ + saveVotePort.saveVotedStationsByRoomIdAndMemberId(agreedStationIds, roomId, memberId); - // 3. ๊ฐ ํ›„๋ณด์— ๋Œ€ํ•œ Set/ZSet ์—…๋ฐ์ดํŠธ - for (Vote vote : votes) { - saveVotePort.saveVoteStatusSet(vote); - saveVotePort.incrementVoteStatusCountZSet(vote); + // 2. ๊ฐ ํ›„๋ณด์— ๋Œ€ํ•œ ํˆฌํ‘œ์ž Set/ ํˆฌํ‘œ ์ˆ˜ ZSet ๊ฐฑ์‹  + for (Long stationId : agreedStationIds) { + saveVotePort.saveVotedMemberByRoomIdAndStationId(memberId, roomId, stationId); + saveVotePort.increaseVotedCountByRoomIdAndStationId(roomId, stationId); } - } - private List getVotes(String roomId, String memberId, List stationIds) { - List votes = new ArrayList<>(); - List candidates = loadCandidatePort.findByRoomId(roomId); - for (Candidate candidate : candidates) { - if (isAgree(stationIds, candidate)) { - votes.add(new Vote(candidate, memberId, VoteStatus.AGREE)); - continue; - } - votes.add(new Vote(candidate, memberId, VoteStatus.DISAGREE)); - } - return votes; - } - - private static boolean isAgree(List agreedStationIds, Candidate candidate) { - return agreedStationIds.contains(candidate.getStationId()); + // 3. ํˆฌํ‘œ ์™„๋ฃŒ Set์— ๋ฉค๋ฒ„ ์ถ”๊ฐ€ + saveVotePort.saveVotedMemberByRoomId(roomId, memberId); } private void initiate(String roomId, String memberId) { if (loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { List votes = loadVotePort.findAllByRoomIdAndMemberId(roomId, memberId); + // 1. ๊ฐ ํ›„๋ณด์— ๋Œ€ํ•œ ํˆฌํ‘œ์ž Set/ ํˆฌํ‘œ ์ˆ˜ ZSet ๊ฐฑ์‹  for (Vote vote : votes) { - deleteVotePort.removeMemberFromVoteStatusSet(vote); - deleteVotePort.decrementVoteCountInZSet(vote); + deleteVotePort.deleteVotedMemberByRoomIdAndStationId( + vote.getMemberId(), vote.getRoomId(), vote.getStationId()); + deleteVotePort.decreaseVotedCountByRoomIdAndStationId( + vote.getRoomId(), vote.getStationId()); } - deleteVotePort.deleteMemberVoteHash(roomId, memberId); - deleteVotePort.removeMemberFromVotedSet(roomId, memberId); + // 2. ๋ฉค๋ฒ„๊ฐ€ ํˆฌํ‘œํ•œ stationId set ์ œ๊ฑฐ + deleteVotePort.deleteVotedStationsByRoomIdAndMemberId(roomId, memberId); + + //3. ํˆฌํ‘œ ์™„๋ฃŒ set์—์„œ ๋ฉค๋ฒ„ ์ œ๊ฑฐ + deleteVotePort.deleteVotedMemberByRoomId(roomId, memberId); } } @@ -91,28 +79,33 @@ public boolean isVotedByMember(String roomId, String memberId) { public int countVotedMembers(String roomId) { validate(roomId); validateRoomStatusIfNotOnVote(roomId); - return loadVotePort.countMembersByRoomId(roomId); + return loadVotePort.countVotedMembersByRoomId(roomId); } @Override - public List getVotesByMember(String roomId, String memberId) { - validate(roomId, memberId); - validateVote(roomId, memberId); - return loadVotePort.findAllByRoomIdAndMemberId(roomId, memberId); - } - - @Override - public List getMembersByVote(Vote vote) { - List memberIds = loadVotePort.findMemberIdsByRoomIdAndStationIdAndStatus( - vote.getRoomId(), vote.getStationId(), vote.getVoteStatus()); - return getMembers(memberIds, vote.getRoomId()); + public VoteResults getVoteResultsByRoomId(String roomId) { + validate(roomId); + validateRoomStatusIfNotOnVote(roomId); + Room room = getRoom(roomId); + List stationIds = loadVotePort.findStationIdsByRoomIdOrderByVotedCount(roomId); + VoteResults voteResults = getVoteResults(room, stationIds); + int votedCount = loadVotePort.countVotedMembersByRoomId(roomId); + if (room.getVotedRatio(votedCount) >= MINIMUM_VOTED_RATIO) { + voteResults.applyResultTag(); + } + return voteResults; } - private List getMembers(List memberIds, String roomId) { - return memberIds.stream() - .map(memberId -> loadRoomParticipantPort.findByRoomIdAndMemberId(roomId, memberId)) - .flatMap(Optional::stream) - .toList(); + private VoteResults getVoteResults(Room room, List stationIds) { + List voteResults = new ArrayList<>(); + for (Long stationId : stationIds) { + List memberIds = loadVotePort.findMemberIdsByRoomIdAndStationId( + room.getId(), stationId); + Station station = getStation(stationId); + voteResults.add( + new VoteResult(room.getId(), stationId, memberIds, station.getPriority())); + } + return new VoteResults(voteResults); } @Override @@ -120,11 +113,9 @@ public Station getVoteFinalResult(String roomId) { validate(roomId); Room room = getRoom(roomId); validateRoomStatusIfVoteClosed(room); - long stationId = loadVotePort.getFirstStationIdByRoomIdAndVoteStatus( - roomId, VoteStatus.AGREE); - return retrieveStationsPort.retrieveStation(stationId) - .orElseThrow( - () -> new IllegalArgumentException("Station not found with id " + stationId)); + List stationIds = loadVotePort.findStationIdsByRoomIdOrderByVotedCount(roomId); + VoteResults voteResults = getVoteResults(room, stationIds); + return getStation(voteResults.getFinalResult().getStationId()); } private void validate(String roomId, String memberId) { @@ -153,12 +144,10 @@ private void validateRoomStatusIfNotOnVote(String roomId) { } } - private void validateVote(String roomId, String memberId) { - if (!loadVotePort.isExistsByRoomIdAndMemberId(roomId, memberId)) { - throw new IllegalArgumentException( - String.format("Not voted by member with id: %s, in room with id: %s", - memberId, roomId)); - } + private Station getStation(long stationId) { + return retrieveStationsPort.retrieveStation(stationId) + .orElseThrow( + () -> new IllegalArgumentException("Station not found with id " + stationId)); } private Room getRoom(String roomId) { diff --git a/kok-api/src/main/resources/db/migration/V4__change_priority_default.sql b/kok-api/src/main/resources/db/migration/V4__change_priority_default.sql new file mode 100644 index 00000000..9ddd7d1e --- /dev/null +++ b/kok-api/src/main/resources/db/migration/V4__change_priority_default.sql @@ -0,0 +1,2 @@ +ALTER TABLE station + MODIFY COLUMN priority BIGINT NOT NULL DEFAULT 1; diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java index 94c1b168..bda8e19b 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteCommandRedisAdapterTest.java @@ -6,9 +6,7 @@ import com.kok.kokapi.common.template.RepositoryTest; import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; import java.util.List; -import java.util.Map; import java.util.Set; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -24,83 +22,86 @@ class VoteCommandRedisAdapterTest extends RepositoryTest { private RedisTemplate redisTemplate; @Test - @DisplayName("๋ฉค๋ฒ„๋ณ„ ํˆฌํ‘œ ๋‚ด์šฉ์„ Hash๋กœ ์ €์žฅํ•œ๋‹ค.") - void saveVoteMemberHash() { + @DisplayName("๋ฉค๋ฒ„๋ณ„๋กœ ํˆฌํ‘œํ•œ ๊ณณ์„ Set์œผ๋กœ ์ €์žฅํ•œ๋‹ค.") + void saveVotes() { // given String roomId = "roomId"; String memberId = "memberId"; - List votes = List.of( - new Vote(new Candidate(roomId, 1), memberId, VoteStatus.AGREE), - new Vote(new Candidate(roomId, 2), memberId, VoteStatus.DISAGREE) - ); + List stationIds = List.of(1L, 2L); // when - voteCommandRedisAdapter.saveVoteMemberHash(votes); + voteCommandRedisAdapter.saveVotedStationsByRoomIdAndMemberId(stationIds, roomId, memberId); // then - String key = VoteKey.memberKey(roomId, memberId); - Map result = redisTemplate.opsForHash().entries(key); - assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.of( - "1", VoteStatus.AGREE.getName(), - "2", VoteStatus.DISAGREE.getName())); + String key = VoteKey.votedStationsByMemberKey(roomId, memberId); + Set result = redisTemplate.opsForSet().members(key); + assertThat(result).containsExactlyInAnyOrder(1, 2); } @Test @DisplayName("ํˆฌํ‘œ ์™„๋ฃŒ์ž Set์— ๋ฉค๋ฒ„๋ฅผ ์ €์žฅํ•œ๋‹ค.") - void saveVotedMemberSet() { + void saveVotedMemberByRoomId() { // given String roomId = "roomId"; String memberId = "memberId"; // when - voteCommandRedisAdapter.saveVotedMemberSet(roomId, memberId); + voteCommandRedisAdapter.saveVotedMemberByRoomId(roomId, memberId); // then - Set result = redisTemplate.opsForSet().members(VoteKey.voteKey(roomId)); + Set result = redisTemplate.opsForSet() + .members(VoteKey.voteCompletedMembersKey(roomId)); assertThat(result).containsExactlyInAnyOrder(memberId); } @Test - @DisplayName("์ฐฌ/๋ฐ˜ Set์— ๋ฉค๋ฒ„๋ฅผ ์ €์žฅํ•œ๋‹ค.") - void saveVoteStatusSet() { + @DisplayName("ํŠน์ • ํ›„๋ณด์ง€์— ํˆฌํ‘œํ•œ ๋ฉค๋ฒ„๋ฅผ ์ €์žฅํ•œ๋‹ค.") + void saveVotedMemberByRoomIdByRoomIdAndStationId() { // given - Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); + String roomId = "roomId"; + long stationId = 100; + String memberId = "memberId"; + Vote vote = new Vote(new Candidate(roomId, stationId), memberId); // when - voteCommandRedisAdapter.saveVoteStatusSet(vote); + voteCommandRedisAdapter.saveVotedMemberByRoomIdAndStationId(memberId, roomId, stationId); // then Set result = redisTemplate.opsForSet() - .members(VoteKey.voteStatusMemberSetKey(vote)); + .members(VoteKey.votedMembersOfStationKey(vote)); assertThat(result).containsExactlyInAnyOrder("memberId"); } @Test - @DisplayName("ZSet์— ์ฐฌ/๋ฐ˜ ๋“ํ‘œ์ˆ˜๋ฅผ 1 ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.") + @DisplayName("ํŠน์ • ํ›„๋ณด์ง€์˜ ๋“ํ‘œ์ˆ˜๋ฅผ 1 ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.") void incrementVoteStatusCountZSet() { // given - Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); + String roomId = "roomId"; + long stationId = 100; + Vote vote = new Vote(new Candidate(roomId, stationId), "memberId"); // when - voteCommandRedisAdapter.incrementVoteStatusCountZSet(vote); + voteCommandRedisAdapter.increaseVotedCountByRoomIdAndStationId(roomId, stationId); // then Double score = redisTemplate.opsForZSet() - .score(VoteKey.voteStatusCountZSetKey(vote), vote.getStationId()); + .score(VoteKey.votedCountOfStationKey(roomId), vote.getStationId()); assertThat(score).isEqualTo(1.0); } @Test @DisplayName("ZSet์—์„œ ๋“ํ‘œ์ˆ˜๋ฅผ 1 ๊ฐ์†Œ์‹œํ‚จ๋‹ค.") - void decrementVoteCountInZSet() { + void decreaseVotedCountByRoomIdAndStationId() { // given - Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); - String key = VoteKey.voteStatusCountZSetKey(vote); + String roomId = "roomId"; + long stationId = 100; + Vote vote = new Vote(new Candidate(roomId, stationId), "memberId"); + String key = VoteKey.votedCountOfStationKey(roomId); redisTemplate.opsForZSet().add(key, vote.getStationId(), 2.0); // when - voteCommandRedisAdapter.decrementVoteCountInZSet(vote); + voteCommandRedisAdapter.decreaseVotedCountByRoomIdAndStationId(roomId, stationId); // then Double score = redisTemplate.opsForZSet().score(key, vote.getStationId()); @@ -108,15 +109,18 @@ void decrementVoteCountInZSet() { } @Test - @DisplayName("์ฐฌ/๋ฐ˜ Set์—์„œ ๋ฉค๋ฒ„๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.") - void removeMemberFromVoteStatusSet() { + @DisplayName("ํ›„๋ณด์ง€์— ํˆฌํ‘œํ•œ ๋ฉค๋ฒ„ Set์—์„œ ๋ฉค๋ฒ„๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.") + void deleteVotedMemberByRoomIdAndStationId() { // given - Vote vote = new Vote(new Candidate("roomId", 100), "memberId", VoteStatus.AGREE); - String key = VoteKey.voteStatusMemberSetKey(vote); - redisTemplate.opsForSet().add(key, "memberId"); + String roomId = "roomId"; + String memberId = "memberId"; + long stationId = 100; + Vote vote = new Vote(new Candidate(roomId, stationId), memberId); + String key = VoteKey.votedMembersOfStationKey(vote); + redisTemplate.opsForSet().add(key, memberId); // when - voteCommandRedisAdapter.removeMemberFromVoteStatusSet(vote); + voteCommandRedisAdapter.deleteVotedMemberByRoomIdAndStationId(memberId, roomId, stationId); // then Set result = redisTemplate.opsForSet().members(key); @@ -124,16 +128,16 @@ void removeMemberFromVoteStatusSet() { } @Test - @DisplayName("๋ฉค๋ฒ„ ํˆฌํ‘œ Hash๋ฅผ ์‚ญ์ œํ•œ๋‹ค.") - void deleteMemberVoteHash() { + @DisplayName("๋ฉค๋ฒ„๊ฐ€ ํˆฌํ‘œํ•œ ํ›„๋ณด์ง€ Set์„ ์‚ญ์ œํ•œ๋‹ค.") + void deleteVotedStationsByRoomIdAndMemberId() { // given String roomId = "roomId"; String memberId = "memberId"; - String key = VoteKey.memberKey(roomId, memberId); + String key = VoteKey.votedStationsByMemberKey(roomId, memberId); redisTemplate.opsForHash().put(key, "1", "AGREE"); // when - voteCommandRedisAdapter.deleteMemberVoteHash(roomId, memberId); + voteCommandRedisAdapter.deleteVotedStationsByRoomIdAndMemberId(roomId, memberId); // then assertAll( @@ -144,18 +148,54 @@ void deleteMemberVoteHash() { @Test @DisplayName("ํˆฌํ‘œ ์™„๋ฃŒ์ž Set์—์„œ ๋ฉค๋ฒ„๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.") - void removeMemberFromVotedSet() { + void deleteVotedMemberByRoomId() { // given String roomId = "roomId"; String memberId = "memberId"; - String key = VoteKey.voteKey(roomId); + String key = VoteKey.voteCompletedMembersKey(roomId); redisTemplate.opsForSet().add(key, memberId); // when - voteCommandRedisAdapter.removeMemberFromVotedSet(roomId, memberId); + voteCommandRedisAdapter.deleteVotedMemberByRoomId(roomId, memberId); // then Set result = redisTemplate.opsForSet().members(key); assertThat(result).doesNotContain(memberId); } + + @Test + @DisplayName("์ฃผ์–ด์ง„ stationId์— ๋Œ€ํ•ด ๋“ํ‘œ ์ˆ˜๊ฐ€ 0์œผ๋กœ Zset์„ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.") + void initiateVoteCountByRoomIdAndStationIds() { + // given + String roomId = "roomId"; + List stationIds = List.of(1L, 2L, 3L); + String key = VoteKey.votedCountOfStationKey(roomId); + + // when + voteCommandRedisAdapter.initiateVoteCountByRoomIdAndStationIds(roomId, stationIds); + + // then + assertAll( + () -> assertThat(redisTemplate.opsForZSet().score(key, 1L)).isEqualTo(0.0), + () -> assertThat(redisTemplate.opsForZSet().score(key, 2L)).isEqualTo(0.0), + () -> assertThat(redisTemplate.opsForZSet().score(key, 3L)).isEqualTo(0.0) + ); + } + + @Test + @DisplayName("์ด๋ฏธ ๋“ํ‘œ ์ˆ˜๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ ZSet ์ดˆ๊ธฐํ™” ์‹œ ๊ฐ’์„ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š”๋‹ค.") + void doesNotInitiateVoteCountDoesNotOverrideExistingScoreIfPresent() { + // given + String roomId = "roomId"; + long stationId = 100; + String key = VoteKey.votedCountOfStationKey(roomId); + redisTemplate.opsForZSet().add(key, stationId, 5.0); + + // when + voteCommandRedisAdapter.initiateVoteCountByRoomIdAndStationIds(roomId, List.of(stationId)); + + // then + Double score = redisTemplate.opsForZSet().score(key, stationId); + assertThat(score).isEqualTo(5.0); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java index f327226e..ac737735 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/adapter/out/persistence/VoteQueryRedisAdapterTest.java @@ -4,9 +4,7 @@ import com.kok.kokapi.common.template.RepositoryTest; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,10 +22,10 @@ class VoteQueryRedisAdapterTest extends RepositoryTest { @DisplayName("roomId์™€ memberId ์กฐํ•ฉ์œผ๋กœ ํˆฌํ‘œ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.") void isExistsByRoomIdAndMemberId() { // given - String roomId = "room1"; - String memberId = "memberA"; - String key = VoteKey.memberKey(roomId, memberId); - redisTemplate.opsForHash().putAll(key, Map.of("1", VoteStatus.AGREE.getName())); + String roomId = "room"; + String memberId = "member"; + String key = VoteKey.votedStationsByMemberKey(roomId, memberId); + redisTemplate.opsForSet().add(key, 1); // when boolean result = voteQueryRedisAdapter.isExistsByRoomIdAndMemberId(roomId, memberId); @@ -48,13 +46,10 @@ void findAllByRoomIdAndMemberId() { // given String roomId = "room2"; String memberId = "memberB"; - String key = VoteKey.memberKey(roomId, memberId); - Vote vote = new Vote(roomId, 1L, memberId, VoteStatus.AGREE.getName()); - Vote vote2 = new Vote(roomId, 2L, memberId, VoteStatus.DISAGREE.getName()); - redisTemplate.opsForHash().putAll(key, Map.of( - "1", VoteStatus.AGREE.getName(), - "2", VoteStatus.DISAGREE.getName() - )); + String key = VoteKey.votedStationsByMemberKey(roomId, memberId); + Vote vote = new Vote(roomId, 1L, memberId); + Vote vote2 = new Vote(roomId, 2L, memberId); + redisTemplate.opsForSet().add(key, 1, 2); // when List result = voteQueryRedisAdapter.findAllByRoomIdAndMemberId(roomId, memberId); @@ -66,13 +61,14 @@ void findAllByRoomIdAndMemberId() { @Test @DisplayName("roomId์— ๋Œ€ํ•ด ํˆฌํ‘œ ์™„๋ฃŒํ•œ ๋ฉค๋ฒ„ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - void countMembersByRoomId() { + void countVotedMembersByRoomId() { // given String roomId = "room3"; - redisTemplate.opsForSet().add(VoteKey.voteKey(roomId), "member1", "member2", "member3"); + redisTemplate.opsForSet() + .add(VoteKey.voteCompletedMembersKey(roomId), "member1", "member2", "member3"); // when - int count = voteQueryRedisAdapter.countMembersByRoomId(roomId); + int count = voteQueryRedisAdapter.countVotedMembersByRoomId(roomId); // then assertThat(count).isEqualTo(3); @@ -80,16 +76,16 @@ void countMembersByRoomId() { @Test @DisplayName("ํŠน์ • stationId์— ๋Œ€ํ•ด ํˆฌํ‘œํ•œ memberId ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•œ๋‹ค.") - void findMemberIdsByRoomIdAndStationIdAndStatus() { + void findMemberIdsByRoomIdAndStationId() { // given String roomId = "room4"; long stationId = 11L; - String key = VoteKey.voteStatusMemberSetKey(VoteStatus.AGREE, roomId, stationId); + String key = VoteKey.votedMembersOfStationKey(roomId, stationId); redisTemplate.opsForSet().add(key, "member1", "member2"); // when - List result = voteQueryRedisAdapter.findMemberIdsByRoomIdAndStationIdAndStatus( - roomId, stationId, VoteStatus.AGREE); + List result = voteQueryRedisAdapter.findMemberIdsByRoomIdAndStationId(roomId, + stationId); // then assertThat(result).containsExactlyInAnyOrder("member1", "member2"); @@ -97,16 +93,15 @@ void findMemberIdsByRoomIdAndStationIdAndStatus() { @Test @DisplayName("์ฐฌ์„ฑ ์ˆ˜๊ฐ€ ๊ฐ€์žฅ ๋งŽ์€ stationId๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - void getFirstStationIdByRoomIdAndVoteStatus() { + void findFirstStationIdByRoomIdOrderByVotedCount() { // given String roomId = "room5"; - String key = VoteKey.voteStatusCountZSetKey(VoteStatus.AGREE, roomId); + String key = VoteKey.votedCountOfStationKey(roomId); redisTemplate.opsForZSet().add(key, "10", 5.0); redisTemplate.opsForZSet().add(key, "11", 8.0); // when - long result = voteQueryRedisAdapter.getFirstStationIdByRoomIdAndVoteStatus( - roomId, VoteStatus.AGREE); + long result = voteQueryRedisAdapter.findFirstStationIdByRoomIdOrderByVotedCount(roomId); // then assertThat(result).isEqualTo(11); @@ -117,14 +112,30 @@ void getFirstStationIdByRoomIdAndVoteStatus() { void getFirstStationIdWhenNoVotes() { // given String roomId = "room6"; - String key = VoteKey.voteStatusCountZSetKey(VoteStatus.AGREE, roomId); + String key = VoteKey.votedCountOfStationKey(roomId); redisTemplate.delete(key); // when - long result = voteQueryRedisAdapter.getFirstStationIdByRoomIdAndVoteStatus( - roomId, VoteStatus.AGREE); + long result = voteQueryRedisAdapter.findFirstStationIdByRoomIdOrderByVotedCount(roomId); // then assertThat(result).isEqualTo(-1L); } + + @Test + @DisplayName("roomId์— ๋Œ€ํ•œ stationId๋“ค์„ ์ฐฌ์„ฑ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์กฐํšŒํ•œ๋‹ค.") + void findStationIdsByRoomIdOrderByVotedCount() { + // given + String roomId = "room7"; + String key = VoteKey.votedCountOfStationKey(roomId); + redisTemplate.opsForZSet().add(key, "10", 5.0); + redisTemplate.opsForZSet().add(key, "11", 8.0); + redisTemplate.opsForZSet().add(key, "12", 2.0); + + // when + List result = voteQueryRedisAdapter.findStationIdsByRoomIdOrderByVotedCount(roomId); + + // then + assertThat(result).containsExactly(11L, 10L, 12L); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java index 9449ea1b..fb131213 100644 --- a/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/vote/application/service/VoteServiceTest.java @@ -8,15 +8,17 @@ import com.kok.kokapi.fixture.MemberFixture; import com.kok.kokapi.room.adapter.out.persistence.RoomParticipantSaveAdapter; import com.kok.kokapi.room.adapter.out.persistence.RoomSaveRedisAdapter; +import com.kok.kokapi.station.adapter.out.persistence.StationRepository; import com.kok.kokapi.vote.adapter.out.persistence.CandidateCommandRedisAdapter; import com.kok.kokapi.vote.adapter.out.persistence.VoteKey; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; +import com.kok.kokcore.station.domain.entity.Station; +import com.kok.kokcore.vote.VoteResults; import com.kok.kokcore.vote.domain.Candidate; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; +import java.math.BigDecimal; import java.util.List; -import java.util.Map; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -36,15 +38,23 @@ class VoteServiceTest extends ServiceTest { private RoomParticipantSaveAdapter roomParticipantSaveAdapter; @Autowired private CandidateCommandRedisAdapter candidateCommandRedisAdapter; + @Autowired + private StationRepository stationRepository; private Room room; private Member member; private Member member2; private Candidate candidate; private Candidate candidate2; + private Station station; + private Station station2; @BeforeEach void init() { + station = stationRepository.save( + new Station("station", BigDecimal.valueOf(33), BigDecimal.valueOf(127), 3)); + station2 = stationRepository.save( + new Station("station2", BigDecimal.valueOf(32), BigDecimal.valueOf(128), 2)); member = MemberFixture.createLeader(); member2 = MemberFixture.createFollower(); room = Room.create("room", 3, member); @@ -52,8 +62,8 @@ void init() { roomSaveRedisAdapter.save(room); roomParticipantSaveAdapter.joinRoom(room.getId(), member); roomParticipantSaveAdapter.joinRoom(room.getId(), member2); - candidate = new Candidate(room.getId(), 1); - candidate2 = new Candidate(room.getId(), 2); + candidate = new Candidate(room.getId(), station.getId()); + candidate2 = new Candidate(room.getId(), station2.getId()); candidateCommandRedisAdapter.saveAll(List.of(candidate, candidate2)); } @@ -64,24 +74,15 @@ void saveVotes() { voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); // then - String memberKey = VoteKey.memberKey(room.getId(), member.getMemberId()); - String agreeKey = VoteKey.voteStatusMemberSetKey( - new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); - String disagreeKey = VoteKey.voteStatusMemberSetKey( - new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE)); + String memberKey = VoteKey.votedStationsByMemberKey(room.getId(), member.getMemberId()); + String votedMembersKey = VoteKey.votedMembersOfStationKey( + new Vote(candidate, member.getMemberId())); - Map storedVotes = redisTemplate.opsForHash().entries(memberKey); - Set agreeMemberIds = redisTemplate.opsForSet().members(agreeKey); - Set disagreeMemberIds = redisTemplate.opsForSet().members(disagreeKey); + Set storedVotes = redisTemplate.opsForSet().members(memberKey); + Set votedMemberIds = redisTemplate.opsForSet().members(votedMembersKey); - assertAll( - () -> assertThat(storedVotes).containsExactlyInAnyOrderEntriesOf(Map.of( - "1", VoteStatus.AGREE.getName(), - "2", VoteStatus.DISAGREE.getName() - )), - () -> assertThat(agreeMemberIds).containsExactlyInAnyOrder(member.getMemberId()), - () -> assertThat(disagreeMemberIds).containsExactlyInAnyOrder(member.getMemberId()) - ); + assertAll(() -> assertThat(storedVotes).containsExactlyInAnyOrder(1), + () -> assertThat(votedMemberIds).containsExactlyInAnyOrder(member.getMemberId())); } @DisplayName("์‚ฌ์šฉ์ž ํˆฌํ‘œ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์ „์— ์ด์ „ ํˆฌํ‘œ ๋‚ด์—ญ์„ ์‚ญ์ œํ•œ๋‹ค.") @@ -91,28 +92,18 @@ void initiateBeforeSaveVote() { voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); // when - voteService.saveVotes(room.getId(), member.getMemberId(), List.of()); + voteService.saveVotes(room.getId(), member.getMemberId(), List.of(2L, 3L)); // then - String hashKey = VoteKey.memberKey(room.getId(), member.getMemberId()); - String agreeSetKey = VoteKey.voteStatusMemberSetKey( - new Vote(candidate, member.getMemberId(), VoteStatus.AGREE)); - String disagreeSetKey1 = VoteKey.voteStatusMemberSetKey( - new Vote(candidate, member.getMemberId(), VoteStatus.DISAGREE)); - String disagreeSetKey2 = VoteKey.voteStatusMemberSetKey( - new Vote(candidate2, member.getMemberId(), VoteStatus.DISAGREE)); - - Long hashSize = redisTemplate.opsForHash().size(hashKey); - Long agreeSetSize = redisTemplate.opsForSet().size(agreeSetKey); - Long disagreeSetSize1 = redisTemplate.opsForSet().size(disagreeSetKey1); - Long disagreeSetSize2 = redisTemplate.opsForSet().size(disagreeSetKey2); + String memberKey = VoteKey.votedStationsByMemberKey(room.getId(), member.getMemberId()); + String votedMembersKey = VoteKey.votedMembersOfStationKey( + new Vote(room.getId(), 1L, member.getMemberId())); - assertAll( - () -> assertThat(hashSize).isEqualTo(2), - () -> assertThat(agreeSetSize).isZero(), - () -> assertThat(disagreeSetSize1).isEqualTo(1), - () -> assertThat(disagreeSetSize2).isEqualTo(1) - ); + Long memberSize = redisTemplate.opsForSet().size(memberKey); + Long votedMembersSize = redisTemplate.opsForSet().size(votedMembersKey); + + assertAll(() -> assertThat(memberSize).isEqualTo(2), + () -> assertThat(votedMembersSize).isZero()); } @DisplayName("์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œ๋ฅผ ์™„๋ฃŒํ–ˆ์œผ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @@ -152,47 +143,15 @@ void countVotedMembers() { assertThat(count).isEqualTo(2); } - @DisplayName("์‚ฌ์šฉ์ž์˜ ํˆฌํ‘œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void getVotesByMember() { - // given - voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); - - // when - List result = voteService.getVotesByMember(room.getId(), member.getMemberId()); - - // then - assertThat(result).hasSize(2) - .containsExactlyInAnyOrder( - new Vote(room.getId(), 1L, member.getMemberId(), VoteStatus.AGREE.getName()), - new Vote(room.getId(), 2L, member.getMemberId(), VoteStatus.DISAGREE.getName()) - ); - } - - @DisplayName("ํŠน์ • ํˆฌํ‘œ์— ์ฐธ์—ฌํ•œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") - @Test - void getMembersByVote() { - voteService.saveVotes(room.getId(), member.getMemberId(), List.of(1L)); - voteService.saveVotes(room.getId(), member2.getMemberId(), List.of(1L)); - - Vote vote = new Vote(candidate, member.getMemberId(), VoteStatus.AGREE); - - List result = voteService.getMembersByVote(vote); - - assertThat(result).hasSize(2) - .extracting(Member::getMemberId) - .containsExactlyInAnyOrder(member.getMemberId(), member2.getMemberId()); - } - @DisplayName("๋ฐฉ์ด ํˆฌํ‘œ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") @Test void throwExceptionWhenRoomNotInVoteStatus() { Room locationInputRoom = Room.create("inputRoom", 3, member); roomSaveRedisAdapter.save(locationInputRoom); - assertThatThrownBy(() -> voteService.countVotedMembers(locationInputRoom.getId())) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Room is not on vote status"); + assertThatThrownBy( + () -> voteService.countVotedMembers(locationInputRoom.getId())).isInstanceOf( + IllegalStateException.class).hasMessageContaining("Room is not on vote status"); } @DisplayName("๋ฐฉ์— ์†ํ•˜์ง€ ์•Š์€ ๋ฉค๋ฒ„๊ฐ€ ํˆฌํ‘œํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") @@ -200,16 +159,30 @@ void throwExceptionWhenRoomNotInVoteStatus() { void throwExceptionWhenMemberNotInRoom() { String nonParticipantId = "unknown"; - assertThatThrownBy(() -> voteService.saveVotes(room.getId(), nonParticipantId, List.of())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Member not found with id"); + assertThatThrownBy( + () -> voteService.saveVotes(room.getId(), nonParticipantId, List.of())).isInstanceOf( + IllegalArgumentException.class).hasMessageContaining("Member not found with id"); } - @DisplayName("์•„์ง ํˆฌํ‘œํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ํˆฌํ‘œ ๋‚ด์—ญ ์กฐํšŒ ์‹œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @DisplayName("roomId๋กœ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•˜๋ฉด ์ฐฌ์„ฑ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ ํ›„๋ณด ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.") @Test - void throwExceptionWhenGetVotesWithoutVoting() { - assertThatThrownBy(() -> voteService.getVotesByMember(room.getId(), member.getMemberId())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Not voted by member with id"); + void getVoteResultsByRoomId() { + // given + voteService.saveVotes(room.getId(), member.getMemberId(), + List.of(candidate2.getStationId())); + voteService.saveVotes(room.getId(), member2.getMemberId(), + List.of(candidate.getStationId(), candidate2.getStationId())); + + // when + VoteResults results = voteService.getVoteResultsByRoomId(room.getId()); + + // then + assertAll( + () -> assertThat(results.getVoteResults()).hasSize(2), + () -> assertThat(results.getVoteResults().getFirst().getCandidate().getStationId()) + .isEqualTo(station2.getId()), + () -> assertThat(results.getVoteResults().getLast().getCandidate().getStationId()) + .isEqualTo(station.getId()) + ); } } diff --git a/kok-api/src/test/java/com/kok/kokcore/vote/VoteResultsTest.java b/kok-api/src/test/java/com/kok/kokcore/vote/VoteResultsTest.java new file mode 100644 index 00000000..52b0d278 --- /dev/null +++ b/kok-api/src/test/java/com/kok/kokcore/vote/VoteResultsTest.java @@ -0,0 +1,81 @@ +package com.kok.kokcore.vote; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.kok.kokcore.vote.domain.VoteResult; +import com.kok.kokcore.vote.domain.vo.ResultTag; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class VoteResultsTest { + + @DisplayName("์ฃผ์–ด์ง„ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ํˆฌํ‘œ ์ˆ˜๊ฐ€ ๊ฐ™๋‹ค๋ฉด, ๊ฐ€์ค‘์น˜ ์ˆœ์œผ๋กœ ์ •๋ ฌํ•œ๋‹ค.") + @Test + void orderByPriorityIfVoteCountSame() { + VoteResult voteResult = new VoteResult("roomId", 1, List.of("1"), 3); + VoteResult voteResult2 = new VoteResult("roomId", 1, List.of("1"), 4); + VoteResults voteResults = new VoteResults(List.of(voteResult, voteResult2)); + + assertThat(voteResults.getVoteResults()).containsExactly(voteResult2, voteResult); + } + + @DisplayName("์ตœ๊ณ  ๋“ํ‘œ ํ›„๋ณด์ง€๊ฐ€ 1๊ฐœ์ธ ๊ฒฝ์šฐ TOP ํƒœ๊ทธ๊ฐ€ ๋ถ€์—ฌ๋œ๋‹ค.") + @Test + void applyResultTagWithSingleTopVoted() { + // given + VoteResult voteResult = new VoteResult("roomId", 1, List.of("a", "b", "c"), 1); + VoteResult voteResult2 = new VoteResult("roomId", 2, List.of("a", "b"), 2); + VoteResult voteResult3 = new VoteResult("roomId", 3, List.of("a"), 3); + VoteResults voteResults = new VoteResults(List.of(voteResult, voteResult2, voteResult3)); + + // when + voteResults.applyResultTag(); + + // then + assertAll( + () -> assertThat(voteResults.getVoteResults().get(0).getVotedCount()) + .isEqualTo(3), + () -> assertThat(voteResults.getVoteResults().get(0).getResultTag()) + .isEqualTo(ResultTag.TOP), + () -> assertThat(voteResults.getVoteResults().get(1).getVotedCount()) + .isEqualTo(2), + () -> assertThat(voteResults.getVoteResults().get(1).getResultTag()) + .isEqualTo(ResultTag.NONE), + () -> assertThat(voteResults.getVoteResults().get(2).getVotedCount()) + .isEqualTo(1), + () -> assertThat(voteResults.getVoteResults().get(2).getResultTag()) + .isEqualTo(ResultTag.NONE) + ); + } + + @DisplayName("์ตœ๊ณ  ๋“ํ‘œ ํ›„๋ณด์ง€๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ด๋ฉด ๋ชจ๋‘์—๊ฒŒ CLOSE ํƒœ๊ทธ๊ฐ€ ๋ถ€์—ฌ๋œ๋‹ค.") + @Test + void applyResultTagWithTiedTopVotes() { + // given + VoteResult close = new VoteResult("roomId", 1, List.of("a", "b"), 1); + VoteResult close2 = new VoteResult("roomId", 2, List.of("a", "b"), 2); + VoteResult low = new VoteResult("roomId", 3, List.of("a"), 3); + VoteResults voteResults = new VoteResults(List.of(close, close2, low)); + + // when + voteResults.applyResultTag(); + + // then + assertAll( + () -> assertThat(voteResults.getVoteResults().get(0).getVotedCount()) + .isEqualTo(2), + () -> assertThat(voteResults.getVoteResults().get(0).getResultTag()) + .isEqualTo(ResultTag.CLOSE), + () -> assertThat(voteResults.getVoteResults().get(1).getVotedCount()) + .isEqualTo(2), + () -> assertThat(voteResults.getVoteResults().get(1).getResultTag()) + .isEqualTo(ResultTag.CLOSE), + () -> assertThat(voteResults.getVoteResults().get(2).getVotedCount()) + .isEqualTo(1), + () -> assertThat(voteResults.getVoteResults().get(2).getResultTag()) + .isEqualTo(ResultTag.NONE) + ); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 151f1c86..4f56b6fc 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -92,4 +92,11 @@ public void closeVote() { public int getNotVotedCount(int votedCount) { return capacity - votedCount; } + + public int getVotedRatio(int votedCount) { + if (capacity == 0) { + return 0; + } + return (int) ((double) votedCount / capacity * 100); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java index 13ee6a22..87122814 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -14,4 +14,6 @@ public interface GetRoomUseCase { Member getParticipant(String roomId, String memberId); long getParticipantsCount(String roomId); + + List getParticipantsByRoomIdInMemberIds(String roomId, List memberIds); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/VoteResults.java b/kok-core/src/main/java/com/kok/kokcore/vote/VoteResults.java new file mode 100644 index 00000000..bbfa7c1c --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/VoteResults.java @@ -0,0 +1,41 @@ +package com.kok.kokcore.vote; + +import com.kok.kokcore.vote.domain.VoteResult; +import java.util.Comparator; +import java.util.List; +import lombok.Getter; + +@Getter +public class VoteResults { + + private final List voteResults; + + public VoteResults(List voteResults) { + this.voteResults = voteResults.stream() + .sorted(Comparator + .comparing(VoteResult::getVotedCount, Comparator.reverseOrder()) + .thenComparing(VoteResult::getPriority, Comparator.reverseOrder())) + .toList(); + } + + public void applyResultTag() { + int topVotedCount = voteResults.getFirst().getVotedCount(); + + List topResults = voteResults.stream() + .filter(voteResult -> voteResult.getVotedCount() == topVotedCount) + .toList(); + + if (topResults.size() == 1) { + topResults.getFirst().markTop(); + return; + } + + for (VoteResult topResult : topResults) { + topResult.markClose(); + } + } + + public VoteResult getFinalResult() { + return voteResults.getFirst(); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java index 3d4cc911..36f26e11 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/Vote.java @@ -1,6 +1,5 @@ package com.kok.kokcore.vote.domain; -import com.kok.kokcore.vote.domain.vo.VoteStatus; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -10,20 +9,14 @@ public class Vote { private final Candidate candidate; private final String memberId; - private final VoteStatus voteStatus; - public Vote(Candidate candidate, String memberId, VoteStatus voteStatus) { + public Vote(Candidate candidate, String memberId) { this.candidate = candidate; this.memberId = memberId; - this.voteStatus = voteStatus; } - public Vote(String roomId, String memberId, long stationId) { - this(new Candidate(roomId, stationId), memberId, VoteStatus.DISAGREE); - } - - public Vote(String roomId, Long stationId, String memberId, String voteStatus) { - this(new Candidate(roomId, stationId), memberId, VoteStatus.findByName(voteStatus)); + public Vote(String roomId, long stationId, String memberId) { + this(new Candidate(roomId, stationId), memberId); } public String getRoomId() { diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/VoteResult.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/VoteResult.java new file mode 100644 index 00000000..222026e9 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/VoteResult.java @@ -0,0 +1,44 @@ +package com.kok.kokcore.vote.domain; + +import com.kok.kokcore.vote.domain.vo.ResultTag; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +public class VoteResult { + + private final Candidate candidate; + private final List memberIds; + private final long priority; + private ResultTag resultTag; + + public VoteResult(String roomId, long stationId, List memberIds, long priority) { + this(new Candidate(roomId, stationId), memberIds, priority, ResultTag.NONE); + } + + public VoteResult( + Candidate candidate, List memberIds, long priority, ResultTag resultTag) { + this.candidate = candidate; + this.memberIds = memberIds; + this.priority = priority; + this.resultTag = resultTag; + } + + public int getVotedCount() { + return memberIds.size(); + } + + public void markTop() { + resultTag = ResultTag.TOP; + } + + public void markClose() { + resultTag = ResultTag.CLOSE; + } + + public long getStationId() { + return candidate.getStationId(); + } +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/ResultTag.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/ResultTag.java new file mode 100644 index 00000000..aab2fb30 --- /dev/null +++ b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/ResultTag.java @@ -0,0 +1,8 @@ +package com.kok.kokcore.vote.domain.vo; + +public enum ResultTag { + + TOP, + CLOSE, + NONE; +} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java b/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java deleted file mode 100644 index 7f233e26..00000000 --- a/kok-core/src/main/java/com/kok/kokcore/vote/domain/vo/VoteStatus.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.kok.kokcore.vote.domain.vo; - -import java.util.Arrays; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Getter -public enum VoteStatus { - - AGREE("agree"), - DISAGREE("disagree"), - ; - - private final String name; - - public static VoteStatus findByName(String name) { - return Arrays.stream(values()) - .filter(voteStatus -> voteStatus.getName().equalsIgnoreCase(name)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No status with name: " + name)); - } - - public boolean isAgree() { - return this.equals(VoteStatus.AGREE); - } -} diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java index dcfbb6c5..33ca6e4f 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/DeleteVotePort.java @@ -1,26 +1,24 @@ package com.kok.kokcore.vote.port.out; -import com.kok.kokcore.vote.domain.Vote; - public interface DeleteVotePort { /** - * ํ›„๋ณด์ง€ ์ฐฌ/๋ฐ˜ Set์—์„œ memberId ์ œ๊ฑฐ {agree/disagree}:{roomId}:{stationId} + * ํˆฌํ‘œํ•œ ๋ฉค๋ฒ„ Set์—์„œ memberId ์ œ๊ฑฐ votedMember:{roomId}:{stationId} */ - void removeMemberFromVoteStatusSet(Vote vote); + void deleteVotedMemberByRoomIdAndStationId(String memberId, String roomId, long stationId); /** - * ํ›„๋ณด์ง€ ์ฐฌ/๋ฐ˜ ZSet์˜ ํˆฌํ‘œ ์ˆ˜(score)๋ฅผ -1 {agree/disagree}:{roomId} + * ํ›„๋ณด์ง€ ํˆฌํ‘œ ์ˆ˜ ZSet์˜ ํˆฌํ‘œ ์ˆ˜(score)๋ฅผ -1 votedCount:{roomId} */ - void decrementVoteCountInZSet(Vote vote); + void decreaseVotedCountByRoomIdAndStationId(String roomId, long stationId); /** * member์˜ ๊ฐœ์ธ ํˆฌํ‘œ ๊ธฐ๋ก ์‚ญ์ œ member:{roomId}:{memberId} */ - void deleteMemberVoteHash(String roomId, String memberId); + void deleteVotedStationsByRoomIdAndMemberId(String roomId, String memberId); /** * vote ์™„๋ฃŒ Set์—์„œ memberId ์ œ๊ฑฐ ex) vote:{roomId} */ - void removeMemberFromVotedSet(String roomId, String memberId); + void deleteVotedMemberByRoomId(String roomId, String memberId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java index c5bbd4dd..5e4f3321 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/LoadVotePort.java @@ -1,7 +1,6 @@ package com.kok.kokcore.vote.port.out; import com.kok.kokcore.vote.domain.Vote; -import com.kok.kokcore.vote.domain.vo.VoteStatus; import java.util.List; public interface LoadVotePort { @@ -10,10 +9,11 @@ public interface LoadVotePort { List findAllByRoomIdAndMemberId(String roomId, String memberId); - int countMembersByRoomId(String roomId); + int countVotedMembersByRoomId(String roomId); - List findMemberIdsByRoomIdAndStationIdAndStatus(String roomId, long stationId, - VoteStatus voteStatus); + List findMemberIdsByRoomIdAndStationId(String roomId, long stationId); - long getFirstStationIdByRoomIdAndVoteStatus(String roomId, VoteStatus voteStatus); + long findFirstStationIdByRoomIdOrderByVotedCount(String roomId); + + List findStationIdsByRoomIdOrderByVotedCount(String roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java index 0e634c28..17107f58 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/port/out/SaveVotePort.java @@ -1,15 +1,17 @@ package com.kok.kokcore.vote.port.out; -import com.kok.kokcore.vote.domain.Vote; import java.util.List; public interface SaveVotePort { - void saveVoteMemberHash(List votes); + void saveVotedStationsByRoomIdAndMemberId(List stationIds, String roomId, + String memberId); - void saveVoteStatusSet(Vote vote); + void saveVotedMemberByRoomIdAndStationId(String memberId, String roomId, long stationId); - void incrementVoteStatusCountZSet(Vote vote); + void increaseVotedCountByRoomIdAndStationId(String roomId, long stationId); - void saveVotedMemberSet(String roomId, String memberId); + void saveVotedMemberByRoomId(String roomId, String memberId); + + void initiateVoteCountByRoomIdAndStationIds(String roomId, List stationIds); } diff --git a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java index 0f0e896a..990f61ca 100644 --- a/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/vote/usecase/GetVoteUseCase.java @@ -1,9 +1,7 @@ package com.kok.kokcore.vote.usecase; -import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.station.domain.entity.Station; -import com.kok.kokcore.vote.domain.Vote; -import java.util.List; +import com.kok.kokcore.vote.VoteResults; public interface GetVoteUseCase { @@ -13,7 +11,5 @@ public interface GetVoteUseCase { int countVotedMembers(String roomId); - List getVotesByMember(String roomId, String memberId); - - List getMembersByVote(Vote vote); + VoteResults getVoteResultsByRoomId(String roomId); } From 9b9cf9be6c36c4faec3cdeb4632ff05eaa3ce8b1 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:06:25 +0900 Subject: [PATCH 156/163] [Feature/vote] remove closing vote api and close vote when all members voted (#118) * :recycle: refactor: seperate updating room status usecase from get room usecase * :recycle: refactor: close vote when all member voted --- .../room/adapter/in/web/RoomController.java | 11 ++--- .../service/RoomCommandService.java | 37 +++++++++++++--- .../application/service/RoomQueryService.java | 17 +------ .../vote/adapter/in/web/VoteController.java | 13 +----- .../service/VoteFacadeService.java | 5 ++- .../service/RoomCommandServiceTest.java | 44 +++++++++++++++++++ .../service/RoomQueryServiceTest.java | 5 +-- .../com/kok/kokcore/room/domain/Room.java | 9 ++++ .../kokcore/room/domain/vo/RoomStatus.java | 4 ++ .../kokcore/room/usecase/GetRoomUseCase.java | 3 +- .../room/usecase/UpdateRoomUseCase.java | 3 ++ .../com/kok/kokcore/room/domain/RoomTest.java | 39 ++++++++++++++++ 12 files changed, 146 insertions(+), 44 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java index b786bc67..b36ced5d 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/adapter/in/web/RoomController.java @@ -17,6 +17,7 @@ import com.kok.kokcore.room.usecase.CreateRoomUseCase; import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.room.usecase.JoinRoomUseCase; +import com.kok.kokcore.room.usecase.UpdateRoomUseCase; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import java.time.LocalDateTime; @@ -37,12 +38,13 @@ public class RoomController { private final CreateRoomUseCase createRoomUseCase; private final JoinRoomUseCase joinRoomUseCase; private final ReadLocationUseCase readLocationUseCase; + private final UpdateRoomUseCase updateRoomUseCase; @Operation(summary = "์•ฝ์†๋ฐฉ ์กฐํšŒ", description = "์•ฝ์†๋ฐฉ ID๋ฅผ ํ†ตํ•ด ์•ฝ์†๋ฐฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ํˆฌํ‘œ ๋ชจ๋“œ์— ๋Œ€ํ•œ ๊ฐ’์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/rooms/{roomId}") public ResponseEntity> getRoomDetail( @PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + Room room = updateRoomUseCase.updateRoomStatus(roomId, LocalDateTime.now()); long participantsCount = getRoomUseCase.getParticipantsCount(roomId); RoomDetailResponse response = RoomDetailResponse.of(room, participantsCount); return ResponseEntity.ok(ApiResponseDto.success(response)); @@ -52,7 +54,7 @@ public ResponseEntity> getRoomDetail( @GetMapping("/rooms/{roomId}/status") public ResponseEntity> getRoomStatus( @PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + Room room = updateRoomUseCase.updateRoomStatus(roomId, LocalDateTime.now()); RoomStatusResponse response = RoomStatusResponse.of(room); return ResponseEntity.ok(ApiResponseDto.success(response)); } @@ -81,7 +83,7 @@ public ResponseEntity> createRoom( @GetMapping("/rooms/{roomId}/participants") public ResponseEntity> getParticipants( @PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + Room room = updateRoomUseCase.updateRoomStatus(roomId, LocalDateTime.now()); List participants = getRoomUseCase.getParticipants(room.getId()); List locations = readLocationUseCase.readLocations(room.getId()); @@ -94,8 +96,7 @@ public ResponseEntity> getParticipants( @PostMapping("/rooms/{roomId}/join") public ResponseEntity> joinRoom(@PathVariable String roomId, @Valid @RequestBody JoinRoomParticipantRequest request) { - - Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + Room room = updateRoomUseCase.updateRoomStatus(roomId, LocalDateTime.now()); Member participant = new Member(request.nickname(), request.profile(), MemberRole.FOLLOWER); int participantCount = joinRoomUseCase.joinRoom(roomId, participant); diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java index 78751d9f..7ffb679f 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomCommandService.java @@ -10,6 +10,7 @@ import com.kok.kokcore.room.usecase.CreateRoomUseCase; import com.kok.kokcore.room.usecase.JoinRoomUseCase; import com.kok.kokcore.room.usecase.UpdateRoomUseCase; +import com.kok.kokcore.vote.port.out.LoadVotePort; import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -23,6 +24,7 @@ public class RoomCommandService implements CreateRoomUseCase, UpdateRoomUseCase private final LoadRoomPort loadRoomPort; private final ReadLocationPort readLocationPort; private final UpdateRoomPort updateRoomPort; + private final LoadVotePort loadVotePort; private final JoinRoomUseCase joinRoomUseCase; @Override @@ -44,6 +46,12 @@ public void startVote(String roomId, LocalDateTime current) { } } + private boolean shouldUpdateVoteDeadline(Room room, LocalDateTime current) { + List locations = readLocationPort.findLocationsByRoomId(room.getId()); + int locationInputCount = locations.size(); + return room.shouldEndLocationInput(locationInputCount, current); + } + @Override public void closeVote(String roomId) { Room room = getRoom(roomId); @@ -51,15 +59,34 @@ public void closeVote(String roomId) { updateRoomPort.update(room); } - private Room getRoom(String roomId) { - return loadRoomPort.findRoomById(roomId) - .orElseThrow( - () -> new IllegalArgumentException("Cannot find room with roomId: " + roomId)); + @Override + public Room updateRoomStatus(String roomId, LocalDateTime current) { + Room room = getRoom(roomId); + if (shouldEndLocationInput(room, current)) { + room.startVote(); + } + if (shouldEndVote(room, current)) { + room.closeVote(); + room.updateVoteDeadline(current); + } + updateRoomPort.update(room); + return room; } - private boolean shouldUpdateVoteDeadline(Room room, LocalDateTime current) { + private boolean shouldEndLocationInput(Room room, LocalDateTime current) { List locations = readLocationPort.findLocationsByRoomId(room.getId()); int locationInputCount = locations.size(); return room.shouldEndLocationInput(locationInputCount, current); } + + private boolean shouldEndVote(Room room, LocalDateTime current) { + int votedCount = loadVotePort.countVotedMembersByRoomId(room.getId()); + return room.shouldEndVote(votedCount, current); + } + + private Room getRoom(String roomId) { + return loadRoomPort.findRoomById(roomId) + .orElseThrow( + () -> new IllegalArgumentException("Cannot find room with roomId: " + roomId)); + } } diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java index 6d81aafb..b5761d27 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RoomQueryService.java @@ -1,14 +1,11 @@ package com.kok.kokapi.room.application.service; -import com.kok.kokcore.location.domain.Location; import com.kok.kokcore.location.port.out.ReadLocationPort; import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.port.out.LoadRoomParticipantPort; import com.kok.kokcore.room.port.out.LoadRoomPort; -import com.kok.kokcore.room.port.out.UpdateRoomPort; import com.kok.kokcore.room.usecase.GetRoomUseCase; -import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,15 +17,9 @@ public class RoomQueryService implements GetRoomUseCase { private final LoadRoomPort loadRoomPort; private final LoadRoomParticipantPort loadRoomParticipantPort; private final ReadLocationPort readLocationPort; - private final UpdateRoomPort updateRoomPort; @Override - public Room findRoomById(String roomId, LocalDateTime current) { - Room room = getRoom(roomId); - if (shouldEndLocationInput(room, current)) { - room.startVote(); - updateRoomPort.update(room); - } + public Room findRoomById(String roomId) { return getRoom(roomId); } @@ -37,12 +28,6 @@ private Room getRoom(String roomId) { .orElseThrow(() -> new IllegalArgumentException("Room not found with id: " + roomId)); } - private boolean shouldEndLocationInput(Room room, LocalDateTime current) { - List locations = readLocationPort.findLocationsByRoomId(room.getId()); - int locationInputCount = locations.size(); - return room.shouldEndLocationInput(locationInputCount, current); - } - @Override public List getParticipants(String roomId) { validate(roomId); diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java index 459d0359..1f2e7d7e 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/adapter/in/web/VoteController.java @@ -2,7 +2,6 @@ import com.kok.kokapi.common.response.ApiResponseDto; import com.kok.kokapi.config.annotion.V1Controller; -import com.kok.kokapi.station.application.service.StationFacadeService; import com.kok.kokapi.vote.adapter.in.dto.request.VoteRequest; import com.kok.kokapi.vote.adapter.in.dto.response.CandidateResponse; import com.kok.kokapi.vote.adapter.in.dto.response.MemberVoteStatusResponse; @@ -11,7 +10,6 @@ import com.kok.kokapi.vote.adapter.in.dto.response.VoteResultStationResponse; import com.kok.kokapi.vote.application.service.VoteFacadeService; import com.kok.kokcore.room.domain.Room; -import com.kok.kokcore.room.usecase.GetRoomUseCase; import com.kok.kokcore.room.usecase.UpdateRoomUseCase; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; @@ -32,8 +30,6 @@ public class VoteController { private final VoteFacadeService voteFacadeService; - private final StationFacadeService stationFacadeService; - private final GetRoomUseCase getRoomUseCase; private final GetVoteUseCase getVoteUseCase; private final RetrieveRouteUseCase retrieveRouteUseCase; private final UpdateRoomUseCase updateRoomUseCase; @@ -69,19 +65,12 @@ public ResponseEntity>> getMemberV @GetMapping("/votes/{roomId}/deadline") public ResponseEntity> getVoteDeadline( @PathVariable String roomId) { - Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + Room room = updateRoomUseCase.updateRoomStatus(roomId, LocalDateTime.now()); int candidateCount = voteFacadeService.countCandidates(roomId); VoteDeadlineResponse response = VoteDeadlineResponse.of(room, candidateCount); return ResponseEntity.ok(ApiResponseDto.success(response)); } - @Operation(summary = "ํˆฌํ‘œ ์ข…๋ฃŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•˜์—ฌ ํˆฌํ‘œ ์ƒํƒœ๋ฅผ ์ข…๋ฃŒ(VOTE_RESULT)๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.") - @PostMapping("/votes/{roomId}/close") - public ResponseEntity> closeVote(@PathVariable String roomId) { - updateRoomUseCase.closeVote(roomId); - return ResponseEntity.ok(ApiResponseDto.success(null)); - } - @Operation(summary = "ํˆฌํ‘œ ํ˜„ํ™ฉ ์กฐํšŒ", description = "๋ฐฉ ID์— ๋Œ€ํ•ด ํ˜„์žฌ๊นŒ์ง€ ํˆฌํ‘œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("/votes/{roomId}/results/current") public ResponseEntity> getVoteResult( diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java index 7db3120a..d12d6a5c 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteFacadeService.java @@ -13,6 +13,7 @@ import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.usecase.GetRoomUseCase; +import com.kok.kokcore.room.usecase.UpdateRoomUseCase; import com.kok.kokcore.station.domain.entity.Route; import com.kok.kokcore.station.domain.entity.Station; import com.kok.kokcore.station.usecase.GetStationUseCase; @@ -48,6 +49,7 @@ public class VoteFacadeService { private final SystemRecommendUseCase systemRecommendUseCase; private final UserRecommendUseCase userRecommendUseCase; private final GetStationUseCase getStationUseCase; + private final UpdateRoomUseCase updateRoomUseCase; public List getCandidates(String roomId, String memberId) { List recommendedStations = systemRecommendUseCase.systemRecommendStation(roomId); @@ -113,7 +115,7 @@ public List getMemberVoteStatus(String roomId) { public VoteCurrentResultResponse getVoteCurrentResult(String roomId) { List responses = new ArrayList<>(); - Room room = getRoomUseCase.findRoomById(roomId, LocalDateTime.now()); + Room room = updateRoomUseCase.updateRoomStatus(roomId, LocalDateTime.now()); int votedCount = getVoteUseCase.countVotedMembers(roomId); VoteResults voteResults = getVoteUseCase.getVoteResultsByRoomId(roomId); for (VoteResult voteResult : voteResults.getVoteResults()) { @@ -127,6 +129,7 @@ public VoteCurrentResultResponse getVoteCurrentResult(String roomId) { public void saveVotes(String roomId, String memberId, List agreedStationIds) { saveVoteUseCase.saveVotes(roomId, memberId, agreedStationIds); + updateRoomUseCase.updateRoomStatus(roomId, LocalDateTime.now()); } public int countCandidates(String roomId) { diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java index aa120b94..673d276d 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomCommandServiceTest.java @@ -123,4 +123,48 @@ void closeVote() { assertThat(updatedRoom.getStatus()).isEqualTo(RoomStatus.VOTE_RESULT); } + + @DisplayName("๋ชจ๋“  ์ธ์›์ด ์ถœ๋ฐœ์ง€๋ฅผ ์ž…๋ ฅํ•˜๋ฉด updateRoomStatus๋ฅผ ํ†ตํ•ด ์ƒํƒœ๊ฐ€ VOTE๋กœ ์ „ํ™˜๋œ๋‹ค.") + @Test + void updateRoomStatusChangesToVote() { + // given + Location location1 = new Location(room.getId(), member.getMemberId(), PointFixture.create(), + "์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ"); + Location location2 = new Location(room.getId(), member2.getMemberId(), + PointFixture.create(), "์„œ์šธ์‹œ ๊ฐ•์„œ๊ตฌ"); + locationRepository.save(location1); + locationRepository.save(location2); + + // when + Room updatedRoom = roomCommandService.updateRoomStatus(room.getId(), LocalDateTime.now()); + + // then + assertThat(updatedRoom.getStatus()).isEqualTo(RoomStatus.VOTE); + } + + @DisplayName("๋ชจ๋“  ์ธ์›์ด ํˆฌํ‘œ๋ฅผ ์™„๋ฃŒํ•˜๋ฉด updateRoomStatus๋ฅผ ํ†ตํ•ด ์ƒํƒœ๊ฐ€ VOTE_RESULT๋กœ ์ „ํ™˜๋œ๋‹ค.") + @Test + void updateRoomStatusChangesToVoteResult() { + // given + LocalDateTime now = LocalDateTime.now().withNano(0); + + Location location1 = new Location(room.getId(), member.getMemberId(), PointFixture.create(), + "์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ"); + Location location2 = new Location(room.getId(), member2.getMemberId(), + PointFixture.create(), "์„œ์šธ์‹œ ๊ฐ•์„œ๊ตฌ"); + locationRepository.save(location1); + locationRepository.save(location2); + roomCommandService.updateRoomStatus(room.getId(), now); + + Room voteStartedRoom = roomQueryRedisAdapter.findRoomById(room.getId()).get(); + voteStartedRoom.updateVoteDeadline(now); + voteStartedRoom.startVote(); + roomSaveRedisAdapter.update(voteStartedRoom); + + // when + Room updatedRoom = roomCommandService.updateRoomStatus(room.getId(), now.plusHours(13)); + + // then + assertThat(updatedRoom.getStatus()).isEqualTo(RoomStatus.VOTE_RESULT); + } } diff --git a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java index 8d980e5e..e2f134a7 100644 --- a/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java +++ b/kok-api/src/test/java/com/kok/kokapi/room/application/service/RoomQueryServiceTest.java @@ -8,7 +8,6 @@ import com.kok.kokcore.room.domain.Room; import com.kok.kokcore.room.domain.vo.MemberRole; import com.kok.kokcore.room.port.out.SaveRoomPort; -import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -28,7 +27,7 @@ void findRoomById() { Room room = saveRoomPort.save(Room.create("room", 2, member)); // when - Room result = roomQueryService.findRoomById(room.getId(), LocalDateTime.now()); + Room result = roomQueryService.findRoomById(room.getId()); // then assertThat(result).isEqualTo(room); @@ -41,7 +40,7 @@ void cannotFindRoomById() { String roomId = "unknownId"; // when & then - assertThatThrownBy(() -> roomQueryService.findRoomById(roomId, LocalDateTime.now())) + assertThatThrownBy(() -> roomQueryService.findRoomById(roomId)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Room not found with id: " + roomId); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index 4f56b6fc..a1333d55 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -99,4 +99,13 @@ public int getVotedRatio(int votedCount) { } return (int) ((double) votedCount / capacity * 100); } + + public boolean shouldEndVote(int votedCount, LocalDateTime current) { + return this.status.isVote() && (isAllVoted(votedCount) + || current.isAfter(voteLimitDateTime)); + } + + private boolean isAllVoted(int votedCount) { + return votedCount == capacity; + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java index 82d6b3a2..b3a14a43 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/vo/RoomStatus.java @@ -13,4 +13,8 @@ public boolean isLocationInput() { public boolean isVoteResult() { return this.equals(VOTE_RESULT); } + + public boolean isVote() { + return this.equals(VOTE); + } } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java index 87122814..6b27b1d8 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/GetRoomUseCase.java @@ -2,12 +2,11 @@ import com.kok.kokcore.room.domain.Member; import com.kok.kokcore.room.domain.Room; -import java.time.LocalDateTime; import java.util.List; public interface GetRoomUseCase { - Room findRoomById(String roomId, LocalDateTime current); + Room findRoomById(String roomId); List getParticipants(String roomId); diff --git a/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java index 946c0062..82430e29 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/usecase/UpdateRoomUseCase.java @@ -1,5 +1,6 @@ package com.kok.kokcore.room.usecase; +import com.kok.kokcore.room.domain.Room; import java.time.LocalDateTime; public interface UpdateRoomUseCase { @@ -7,4 +8,6 @@ public interface UpdateRoomUseCase { void startVote(String roomId, LocalDateTime current); void closeVote(String roomId); + + Room updateRoomStatus(String roomId, LocalDateTime current); } diff --git a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java index 57e0d7e7..765eeb4c 100644 --- a/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java +++ b/kok-core/src/test/java/com/kok/kokcore/room/domain/RoomTest.java @@ -213,4 +213,43 @@ void getNotVotedCount() { // then assertThat(notVoted).isEqualTo(3); } + + @DisplayName("์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void shouldEndVoteByDeadlineExceeded() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.shouldEndVote(1, LocalDateTime.now().plusHours(18)); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("๋ชจ๋“  ์ฐธ๊ฐ€์ž๊ฐ€ ํˆฌํ‘œ๋ฅผ ์™„๋ฃŒํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void shouldEndVoteByAllParticipantCompleted() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.shouldEndVote(2, LocalDateTime.now().plusHours(17)); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("์ถœ๋ฐœ์ง€ ์ž…๋ ฅ ๋งˆ๊ฐ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜์ง€๋„, ์ถœ๋ฐœ์ง€ ์ž…๋ ฅ์„ ์™„๋ฃŒํ•˜์ง€๋„ ์•Š์•˜์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void hasNotVoteEnded() { + // given + Room room = Room.create("room", 2, new Member("member", "profile.svg", MemberRole.LEADER)); + + // when + boolean result = room.shouldEndVote(1, LocalDateTime.now().plusHours(17)); + + // then + assertThat(result).isFalse(); + } } From f86f81a5c39cc4fb5911b394a870de84d36e16c6 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:36:05 +0900 Subject: [PATCH 157/163] :bug: fix: fix room status validation error when get current vote result (#119) --- .../kok/kokapi/vote/application/service/VoteService.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java index 7a8e7868..f2a04c16 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -85,8 +85,8 @@ public int countVotedMembers(String roomId) { @Override public VoteResults getVoteResultsByRoomId(String roomId) { validate(roomId); - validateRoomStatusIfNotOnVote(roomId); Room room = getRoom(roomId); + validateRoomStatusIfBeforeVote(room); List stationIds = loadVotePort.findStationIdsByRoomIdOrderByVotedCount(roomId); VoteResults voteResults = getVoteResults(room, stationIds); int votedCount = loadVotePort.countVotedMembersByRoomId(roomId); @@ -96,6 +96,13 @@ public VoteResults getVoteResultsByRoomId(String roomId) { return voteResults; } + private void validateRoomStatusIfBeforeVote(Room room) { + if (room.isNotOnVote()) { + throw new IllegalStateException( + "Room is not on vote yet, but status: " + room.getStatus()); + } + } + private VoteResults getVoteResults(Room room, List stationIds) { List voteResults = new ArrayList<>(); for (Long stationId : stationIds) { From ec54385b815a821449d6c3703296a25de8bd9f86 Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 17 Apr 2025 23:02:32 +0900 Subject: [PATCH 158/163] :bug: fix: fix room status validation error when get current vote result (#120) --- .../kok/kokapi/vote/application/service/VoteService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java index f2a04c16..f2941e42 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -78,15 +78,15 @@ public boolean isVotedByMember(String roomId, String memberId) { @Override public int countVotedMembers(String roomId) { validate(roomId); - validateRoomStatusIfNotOnVote(roomId); + validateRoomStatusIfBeforeVote(roomId); return loadVotePort.countVotedMembersByRoomId(roomId); } @Override public VoteResults getVoteResultsByRoomId(String roomId) { validate(roomId); + validateRoomStatusIfBeforeVote(roomId); Room room = getRoom(roomId); - validateRoomStatusIfBeforeVote(room); List stationIds = loadVotePort.findStationIdsByRoomIdOrderByVotedCount(roomId); VoteResults voteResults = getVoteResults(room, stationIds); int votedCount = loadVotePort.countVotedMembersByRoomId(roomId); @@ -96,7 +96,8 @@ public VoteResults getVoteResultsByRoomId(String roomId) { return voteResults; } - private void validateRoomStatusIfBeforeVote(Room room) { + private void validateRoomStatusIfBeforeVote(String roomId) { + Room room = getRoom(roomId); if (room.isNotOnVote()) { throw new IllegalStateException( "Room is not on vote yet, but status: " + room.getStatus()); From df44a91723a02d5b0e570cf211a5ec7c1ffd461e Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Thu, 17 Apr 2025 23:18:44 +0900 Subject: [PATCH 159/163] :bug: fix: fix room status validation error to check if vote not started instead not on vote (#121) --- .../com/kok/kokapi/vote/application/service/VoteService.java | 2 +- kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java index f2941e42..65596b15 100644 --- a/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java +++ b/kok-api/src/main/java/com/kok/kokapi/vote/application/service/VoteService.java @@ -98,7 +98,7 @@ public VoteResults getVoteResultsByRoomId(String roomId) { private void validateRoomStatusIfBeforeVote(String roomId) { Room room = getRoom(roomId); - if (room.isNotOnVote()) { + if (room.isBeforeVote()) { throw new IllegalStateException( "Room is not on vote yet, but status: " + room.getStatus()); } diff --git a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java index a1333d55..5d514f8d 100644 --- a/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java +++ b/kok-core/src/main/java/com/kok/kokcore/room/domain/Room.java @@ -61,6 +61,10 @@ public boolean shouldEndLocationInput(long locationInputCount, LocalDateTime cur || current.isAfter(locationInputLimitDateTime)); } + public boolean isBeforeVote() { + return this.status.isLocationInput(); + } + public boolean isNotOnVote() { return this.status.isLocationInput() || this.status.isVoteResult(); } From 1bdfe402eca6ea1563de68c4d033f31ee28136bb Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sun, 4 May 2025 23:34:36 +0900 Subject: [PATCH 160/163] [Deploy/dev] Dev Server migration (#123) * :gear: chore: remove redis container from docker-compose-dev * :construction_worker: build: change dev CD workflow trigger temporarily * :construction_worker: build: update environment arguments * :construction_worker: build: update environment arguments * :construction_worker: build: change dev CD workflow trigger temporarily * :construction_worker: build: update environment arguments * :construction_worker: build: add dockerhub username on .env * :construction_worker: build: remove adding dockerhub username on .env from CD Workflow * :gear: config: remove baseline migration version * :gear: config: port forward from 80 to 8080 on dev container * :gear: config: port forward from 8080 to 8080 on dev container * :construction_worker: build: remove temporal CD workflow trigger --- .github/workflows/kok-dev-CD.yml | 15 ++++++++------- infra/docker-compose-dev.yml | 14 -------------- kok-api/src/main/resources/application-dev.yml | 1 - 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/.github/workflows/kok-dev-CD.yml b/.github/workflows/kok-dev-CD.yml index 60225dc8..e3880ceb 100644 --- a/.github/workflows/kok-dev-CD.yml +++ b/.github/workflows/kok-dev-CD.yml @@ -21,26 +21,27 @@ jobs: - name: Create SSH key file - run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem + run: echo "${{ secrets.AWS_KEY }}" > /tmp/AWS_KEY.pem - name: Set permissions for SSH key file - run: chmod 400 /tmp/NCP_KEY.pem + run: chmod 400 /tmp/AWS_KEY.pem - - name: Upload `docker-compose.yml` to NCP + - name: Upload `docker-compose.yml` to AWS run: | - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-dev.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/AWS_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-dev.yml ${{ secrets.AWS_USER }}@${{ secrets.AWS_DEV_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - name: Deploy to Dev Server uses: appleboy/ssh-action@v1.0.3 with: - host: ${{ secrets.NCP_HOST }} - username: ${{ secrets.NCP_USER }} - key: ${{ secrets.NCP_KEY }} + host: ${{ secrets.AWS_DEV_HOST }} + username: ${{ secrets.AWS_USER }} + key: ${{ secrets.AWS_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} sed -i '/^KOK_DEV_TAG = /d' .env echo "KOK_DEV_TAG = ${{ env.LATEST_TAG }}" >> .env + sudo docker compose -f docker-compose-dev.yml pull sudo docker compose -f docker-compose-dev.yml down sudo docker compose -f docker-compose-dev.yml up -d diff --git a/infra/docker-compose-dev.yml b/infra/docker-compose-dev.yml index e7f753b7..8056385c 100644 --- a/infra/docker-compose-dev.yml +++ b/infra/docker-compose-dev.yml @@ -14,20 +14,6 @@ services: networks: - kok-network - # Redis ์„ค์ • - redis: - image: redis:7.0 - container_name: redis-dev - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - kok-network - -volumes: - redis_data: - networks: kok-network: driver: bridge diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index cfb0d5b3..9791980d 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -20,7 +20,6 @@ spring: enabled: true locations: classpath:db/migration baseline-on-migrate: true - baseline-version: 1 # V1 ์ดํ•˜์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์ด๋ฏธ ์ ์šฉ. data: redis: host: ${REDIS_HOST} # redis๋„ ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ. From 5e5c8e9d7149d48ae5f57b08fdc065357f2c6fae Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Sun, 4 May 2025 23:35:54 +0900 Subject: [PATCH 161/163] =?UTF-8?q?=F0=9F=9A=80=20[Deploy]=20migrate=20pro?= =?UTF-8?q?d=20server=20(#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :gear: config: change configuration name from ncp to aws * :gear: config: disable swagger springdocs on prod * :construction_worker: build: seperate deploy.sh & change workflow due to using nginx not by container --- .github/workflows/kok-prod-CD.yml | 90 +++---------------- infra/deploy.sh | 57 ++++++++++++ .../service/RandomProfileService.java | 2 +- .../src/main/resources/application-dev.yml | 2 +- .../src/main/resources/application-prod.yml | 5 +- 5 files changed, 74 insertions(+), 82 deletions(-) create mode 100644 infra/deploy.sh diff --git a/.github/workflows/kok-prod-CD.yml b/.github/workflows/kok-prod-CD.yml index 1cb2df4f..59ab5bb2 100644 --- a/.github/workflows/kok-prod-CD.yml +++ b/.github/workflows/kok-prod-CD.yml @@ -17,90 +17,24 @@ jobs: echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV - name: Create SSH key file - run: echo "${{ secrets.NCP_KEY }}" > /tmp/NCP_KEY.pem + run: echo "${{ secrets.AWS_KEY }}" > /tmp/AWS_KEY.pem - name: Set permissions for SSH key file - run: chmod 600 /tmp/NCP_KEY.pem + run: chmod 600 /tmp/AWS_KEY.pem - - name: Upload Compose files & nginx.conf to NCP + - name: Upload Compose files & deploy.sh to AWS run: | - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-blue.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-green.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-env.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - scp -i /tmp/NCP_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-nginx.yml ${{ secrets.NCP_USER }}@${{ secrets.NCP_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/AWS_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-blue.yml ${{ secrets.AWS_USER }}@${{ secrets.AWS_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/AWS_KEY.pem -o StrictHostKeyChecking=no infra/docker-compose-green.yml ${{ secrets.AWS_USER }}@${{ secrets.AWS_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} + scp -i /tmp/AWS_KEY.pem -o StrictHostKeyChecking=no infra/deploy.sh ${{ secrets.AWS_USER }}@${{ secrets.AWS_PROD_HOST }}:${{ secrets.COMPOSE_FILE_PATH }} - - name: Deploy to NCP (Blue-Green Auto + DB/Redis Check) + - name: Deploy to AWS via deploy.sh uses: appleboy/ssh-action@v1.0.3 with: - host: ${{ secrets.NCP_PROD_HOST }} - username: ${{ secrets.NCP_USER }} - key: ${{ secrets.NCP_KEY }} + host: ${{ secrets.AWS_PROD_HOST }} + username: ${{ secrets.AWS_USER }} + key: ${{ secrets.AWS_KEY }} script: | cd ${{ secrets.COMPOSE_FILE_PATH }} - - # .env์— ์ตœ์‹  ํƒœ๊ทธ ๊ธฐ๋ก - sed -i '/^KOK_PROD_TAG=/d' .env || true - echo "KOK_PROD_TAG=${{ env.LATEST_TAG }}" >> .env - - echo "๐Ÿ” nginx ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ..." - if ! docker compose -f docker-compose-nginx.yml ps | grep -q "Up"; then - echo "๐Ÿš€ nginx ์‹œ์ž‘" - docker compose -f docker-compose-nginx.yml up -d - else - echo "โœ… nginx ์ด๋ฏธ ์‹คํ–‰ ์ค‘" - fi - - - echo "๐Ÿ” ENV ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ..." - if ! docker compose -f docker-compose-env.yml ps | grep -q "Up"; then - echo "๐Ÿš€ ENV ์‹œ์ž‘" - docker compose -f docker-compose-env.yml up -d - else - echo "โœ… ENV ์ด๋ฏธ ์‹คํ–‰ ์ค‘" - fi - - # ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Blue/Green ํ™•์ธ - CURRENT_ENV=$(grep -o 'proxy_pass http://kok-[a-z]\+' nginx.conf | awk -F/ '{print $3}') - echo "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํ™˜๊ฒฝ: $CURRENT_ENV" - - if [[ "$CURRENT_ENV" == "kok-blue" ]]; then - NEW_ENV="kok-green" - OLD_ENV="kok-blue" - COMPOSE_FILE="docker-compose-green.yml" - NEW_PORT=8082 - else - NEW_ENV="kok-blue" - OLD_ENV="kok-green" - COMPOSE_FILE="docker-compose-blue.yml" - NEW_PORT=8081 - fi - - echo "์ƒˆ๋กœ์šด ํ™˜๊ฒฝ์œผ๋กœ ๋ฐฐํฌ: $NEW_ENV" - - # ์ƒˆ ํ™˜๊ฒฝ ๋ฐฐํฌ - docker compose -f $COMPOSE_FILE pull - docker compose -f $COMPOSE_FILE up -d - - echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" - sleep 60 - HEALTH=$(curl -s https://prod-api.kokokok.com/v1/api/health) - echo "Health Check ๊ฒฐ๊ณผ: $HEALTH" - CODE=$(echo "$HEALTH" | jq -r '.code') - DATA=$(echo "$HEALTH" | jq -r '.data') - - if [[ "$CODE" != "200" || "$DATA" != "OK" ]]; then - echo "โŒ Health Check ์‹คํŒจ (code: $CODE, data: $DATA), ๋กค๋ฐฑ!" - docker compose -f $COMPOSE_FILE stop $NEW_ENV - docker compose -f $COMPOSE_FILE rm -f $NEW_ENV - exit 1 - fi - - echo "โš™๏ธ nginx.conf ํŠธ๋ž˜ํ”ฝ์„ $NEW_ENV๋กœ ๋ณ€๊ฒฝ" - sed -i "s|proxy_pass http://$OLD_ENV:8080;|proxy_pass http://$NEW_ENV:8080;|" nginx.conf - - echo "๐Ÿ”„ Nginx ์„ค์ • reload" - docker restart kok-nginx - - echo "๐Ÿงน ์ด์ „ ํ™˜๊ฒฝ($OLD_ENV) ์ •๋ฆฌ" - docker compose -f docker-compose-${OLD_ENV#kok-}.yml stop $OLD_ENV - docker compose -f docker-compose-${OLD_ENV#kok-}.yml rm -f $OLD_ENV \ No newline at end of file + chmod +x deploy.sh + ./deploy.sh "${{ env.LATEST_TAG }}" diff --git a/infra/deploy.sh b/infra/deploy.sh new file mode 100644 index 00000000..feb1907d --- /dev/null +++ b/infra/deploy.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +LATEST_TAG=$1 + +# .env ์ตœ์‹  ํƒœ๊ทธ ์—…๋ฐ์ดํŠธ +sed -i '/^KOK_PROD_TAG=/d' .env || true +echo "KOK_PROD_TAG=$LATEST_TAG" >> .env + +# ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Blue/Green ํ™•์ธ +CURRENT_PORT=$(grep -o 'localhost:[0-9]\+' /etc/nginx/conf.d/service-url.inc | awk -F: '{print $2}') + +if [ "$CURRENT_PORT" == "8081" ]; then + CURRENT_ENV="kok-blue" + NEW_ENV="kok-green" + NEW_PORT=8082 + COMPOSE_FILE="docker-compose-green.yml" + NEW_SERVICE_URL_PATH="/etc/nginx/conf.d/service-url-green.inc" +elif [ "$CURRENT_PORT" == "8082" ]; then + CURRENT_ENV="kok-green" + NEW_ENV="kok-blue" + NEW_PORT=8081 + COMPOSE_FILE="docker-compose-blue.yml" + NEW_SERVICE_URL_PATH="/etc/nginx/conf.d/service-url-blue.inc" +else + echo "โŒ ํ˜„์žฌ service-url.inc์— ์•Œ ์ˆ˜ ์—†๋Š” ํฌํŠธ๊ฐ’์ด ์žˆ์Šต๋‹ˆ๋‹ค: $CURRENT_PORT" + exit 1 +fi + +echo "ํ˜„์žฌ ํ™˜๊ฒฝ: $CURRENT_ENV โ†’ ์ƒˆ ํ™˜๊ฒฝ: $NEW_ENV" + +# ์ƒˆ ํ™˜๊ฒฝ ๋ฐฐํฌ +docker compose -f $COMPOSE_FILE pull +docker compose -f $COMPOSE_FILE up -d + +echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" +sleep 60 +HEALTH=$(curl -s https://prod-api.kokokok.com/v1/api/health) +echo "Health Check ๊ฒฐ๊ณผ: $HEALTH" +CODE=$(echo "$HEALTH" | jq -r '.code') +DATA=$(echo "$HEALTH" | jq -r '.data') + +if [[ "$CODE" != "200" || "$DATA" != "OK" ]]; then + echo "โŒ Health Check ์‹คํŒจ (code: $CODE, data: $DATA), ๋กค๋ฐฑ!" + docker compose -f $COMPOSE_FILE stop $NEW_ENV + docker compose -f $COMPOSE_FILE rm -f $NEW_ENV + exit 1 +fi + +echo "โš™๏ธ service-url.inc ๊ต์ฒด: $NEW_SERVICE_URL_PATH โ†’ /etc/nginx/conf.d/service-url.inc" +sudo cp $NEW_SERVICE_URL_PATH /etc/nginx/conf.d/service-url.inc + +echo "๐Ÿ”„ Nginx ์„ค์ • reload" +sudo nginx -t && sudo systemctl reload nginx + +echo "๐Ÿงน ์ด์ „ ํ™˜๊ฒฝ($CURRENT_ENV) ์ •๋ฆฌ" +docker compose -f docker-compose-${CURRENT_ENV#kok-}.yml stop $CURRENT_ENV +docker compose -f docker-compose-${CURRENT_ENV#kok-}.yml rm -f $CURRENT_ENV diff --git a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java index 1fd2142b..03b5a2d6 100644 --- a/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java +++ b/kok-api/src/main/java/com/kok/kokapi/room/application/service/RandomProfileService.java @@ -12,7 +12,7 @@ @RequiredArgsConstructor public class RandomProfileService implements CreateRandomProfileUseCase { - @Value("${ncp.object-storage-url}") + @Value("${aws.object-storage-url}") private String objectStorageUrl; private static final List ADJECTIVES = List.of( diff --git a/kok-api/src/main/resources/application-dev.yml b/kok-api/src/main/resources/application-dev.yml index 9791980d..9243fda5 100644 --- a/kok-api/src/main/resources/application-dev.yml +++ b/kok-api/src/main/resources/application-dev.yml @@ -40,7 +40,7 @@ station: start-idx: 1 end-idx: 1000 -ncp: +aws: object-storage-url: ${OBJECT_STORAGE_URL} google: places: diff --git a/kok-api/src/main/resources/application-prod.yml b/kok-api/src/main/resources/application-prod.yml index 6b597d4c..16bb00e4 100644 --- a/kok-api/src/main/resources/application-prod.yml +++ b/kok-api/src/main/resources/application-prod.yml @@ -44,8 +44,9 @@ springdoc: default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 swagger-ui: - enabled: true + enabled: false path: /swagger + # open-api: http://data.seoul.go.kr/dataList/OA-21232/S/1/datasetView.do station: base-url: http://openapi.seoul.go.kr:8088 @@ -55,7 +56,7 @@ station: start-idx: 1 end-idx: 1000 -ncp: +aws: object-storage-url: ${OBJECT_STORAGE_URL} google: places: From 2ef9fdb1a6d134c7dd1542bb49811a9a9adfe2fd Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Mon, 5 May 2025 01:49:59 +0900 Subject: [PATCH 162/163] :construction_worker: build: fix health check path (#126) --- infra/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/deploy.sh b/infra/deploy.sh index feb1907d..2d887a98 100644 --- a/infra/deploy.sh +++ b/infra/deploy.sh @@ -34,7 +34,7 @@ docker compose -f $COMPOSE_FILE up -d echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" sleep 60 -HEALTH=$(curl -s https://prod-api.kokokok.com/v1/api/health) +HEALTH=$(curl -s http://localhost:$NEW_PORT/v1/api/health) echo "Health Check ๊ฒฐ๊ณผ: $HEALTH" CODE=$(echo "$HEALTH" | jq -r '.code') DATA=$(echo "$HEALTH" | jq -r '.data') From 0f19740e9843036a943eb40c9adbd9f7a2b5be5c Mon Sep 17 00:00:00 2001 From: linirini <101927543+linirini@users.noreply.github.com> Date: Mon, 5 May 2025 16:04:51 +0900 Subject: [PATCH 163/163] :construction_worker: build: fix service url from localhost to exact ip (#127) --- infra/deploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/deploy.sh b/infra/deploy.sh index 2d887a98..6fb5bb85 100644 --- a/infra/deploy.sh +++ b/infra/deploy.sh @@ -7,7 +7,7 @@ sed -i '/^KOK_PROD_TAG=/d' .env || true echo "KOK_PROD_TAG=$LATEST_TAG" >> .env # ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Blue/Green ํ™•์ธ -CURRENT_PORT=$(grep -o 'localhost:[0-9]\+' /etc/nginx/conf.d/service-url.inc | awk -F: '{print $2}') +CURRENT_PORT=$(grep -o '127.0.0.1:[0-9]\+' /etc/nginx/conf.d/service-url.inc | awk -F: '{print $2}') if [ "$CURRENT_PORT" == "8081" ]; then CURRENT_ENV="kok-blue" @@ -34,7 +34,7 @@ docker compose -f $COMPOSE_FILE up -d echo "๐Ÿฉบ Health Check (60์ดˆ ๋Œ€๊ธฐ)" sleep 60 -HEALTH=$(curl -s http://localhost:$NEW_PORT/v1/api/health) +HEALTH=$(curl -s http://127.0.0.1:$NEW_PORT/v1/api/health) echo "Health Check ๊ฒฐ๊ณผ: $HEALTH" CODE=$(echo "$HEALTH" | jq -r '.code') DATA=$(echo "$HEALTH" | jq -r '.data')