diff --git a/.github/workflows/Dev_Owner_CD.yml b/.github/workflows/Dev_API_Owner_CD.yml similarity index 92% rename from .github/workflows/Dev_Owner_CD.yml rename to .github/workflows/Dev_API_Owner_CD.yml index 56490e6..ccb574d 100644 --- a/.github/workflows/Dev_Owner_CD.yml +++ b/.github/workflows/Dev_API_Owner_CD.yml @@ -6,11 +6,12 @@ on: - "develop" paths: - 'api-owner/**' - - 'domain/**' - - 'domain-redis/**' + - 'common-client/**' + - 'infra-redis/**' + - 'infra-kafka/**' - 'build.gradle' - 'settings.gradle' - - '.github/workflows/Dev_Owner_CD.yml' + - '.github/workflows/Dev_API_Owner_CD.yml' permissions: contents: read @@ -65,7 +66,7 @@ jobs: deploy: needs: build - runs-on: dev-owner + runs-on: dev-api-owner steps: - name: Download artifact file diff --git a/.github/workflows/Dev_User_CD.yml b/.github/workflows/Dev_API_User_CD.yml similarity index 92% rename from .github/workflows/Dev_User_CD.yml rename to .github/workflows/Dev_API_User_CD.yml index 60f126a..2baa603 100644 --- a/.github/workflows/Dev_User_CD.yml +++ b/.github/workflows/Dev_API_User_CD.yml @@ -6,11 +6,12 @@ on: - "develop" paths: - 'api-user/**' - - 'domain/**' - - 'domain-redis/**' + - 'common-client/**' + - 'infra-redis/**' + - 'infra-kafka/**' - 'build.gradle' - 'settings.gradle' - - '.github/workflows/Dev_User_CD.yml' + - '.github/workflows/Dev_API_User_CD.yml' permissions: contents: read @@ -65,7 +66,7 @@ jobs: deploy: needs: build - runs-on: dev-user + runs-on: dev-api-user steps: - name: Download artifact file diff --git a/.github/workflows/Dev_CI.yml b/.github/workflows/Dev_CI.yml index 56b47c2..6c033d2 100644 --- a/.github/workflows/Dev_CI.yml +++ b/.github/workflows/Dev_CI.yml @@ -19,20 +19,58 @@ jobs: TEST_REPORT: true services: - mysql: + mysql-reservation: image: mysql:8.0 env: - MYSQL_ROOT_PASSWORD: "" - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - MYSQL_DATABASE: test + MYSQL_DATABASE: wellmeet_reservation + MYSQL_ROOT_PASSWORD: password ports: - - 3306:3306 + - 3310:3306 options: >- - --health-cmd="mysqladmin ping" + --health-cmd="mysqladmin ping -ppassword" --health-interval=10s --health-timeout=5s --health-retries=3 - + + mysql-member: + image: mysql:8.0 + env: + MYSQL_DATABASE: wellmeet_member + MYSQL_ROOT_PASSWORD: password + ports: + - 3307:3306 + options: >- + --health-cmd="mysqladmin ping -ppassword" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + mysql-owner: + image: mysql:8.0 + env: + MYSQL_DATABASE: wellmeet_owner + MYSQL_ROOT_PASSWORD: password + ports: + - 3308:3306 + options: >- + --health-cmd="mysqladmin ping -ppassword" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + mysql-restaurant: + image: mysql:8.0 + env: + MYSQL_DATABASE: wellmeet_restaurant + MYSQL_ROOT_PASSWORD: password + ports: + - 3309:3306 + options: >- + --health-cmd="mysqladmin ping -ppassword" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + redis: image: redis:7-alpine ports: @@ -95,3 +133,15 @@ jobs: - name: Run Tests With Gradle run: ./gradlew test + + - name: Validate Flyway Schema + run: ./gradlew :domain-reservation:test --tests FlywaySchemaValidationTest + + - name: Validate Flyway Schema + run: ./gradlew :domain-restaurant:test --tests FlywaySchemaValidationTest + + - name: Validate Flyway Schema + run: ./gradlew :domain-owner:test --tests FlywaySchemaValidationTest + + - name: Validate Flyway Schema + run: ./gradlew :domain-member:test --tests FlywaySchemaValidationTest diff --git a/.github/workflows/Dev_Discovery_CD.yml b/.github/workflows/Dev_Discovery_CD.yml new file mode 100644 index 0000000..2489e35 --- /dev/null +++ b/.github/workflows/Dev_Discovery_CD.yml @@ -0,0 +1,78 @@ +name: dev-discovery-cd + +on: + push: + branches: + - "develop" + paths: + - 'discovery-server/**' + - 'build.gradle' + - 'settings.gradle' + - '.github/workflows/Dev_Discovery_CD.yml' + +permissions: + contents: read + checks: write + actions: read + pull-requests: write + +jobs: + test: + uses: ./.github/workflows/Dev_CI.yml + + build: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Make gradlew executable + run: chmod +x gradlew + + - name: bootJar with Gradle + run: ./gradlew :discovery-server:bootJar --info + + - name: Change artifact file name + run: mv discovery-server/build/libs/*.jar discovery-server/build/libs/app.jar + + - name: Upload artifact file + uses: actions/upload-artifact@v4 + with: + name: app-artifact + path: ./discovery-server/build/libs/app.jar + if-no-files-found: error + + - name: Upload deploy scripts + uses: actions/upload-artifact@v4 + with: + name: deploy-scripts + path: ./scripts/dev/ + if-no-files-found: error + + deploy: + needs: build + runs-on: dev-discovery-server + + steps: + - name: Download artifact file + uses: actions/download-artifact@v4 + with: + name: app-artifact + path: ~/app + + - name: Download deploy scripts + uses: actions/download-artifact@v4 + with: + name: deploy-scripts + path: ~/app/scripts + + - name: Replace application to latest + run: sudo sh ~/app/scripts/replace-new-version.sh diff --git a/.github/workflows/Dev_Domain_Member_CD.yml b/.github/workflows/Dev_Domain_Member_CD.yml new file mode 100644 index 0000000..29841fd --- /dev/null +++ b/.github/workflows/Dev_Domain_Member_CD.yml @@ -0,0 +1,84 @@ +name: dev-member-cd + +on: + push: + branches: + - "develop" + paths: + - 'common-client/**' + - 'domain-member/**' + - 'domain-common/**' + - 'build.gradle' + - 'settings.gradle' + - '.github/workflows/Dev_Domain_Member_CD.yml' + +permissions: + contents: read + checks: write + actions: read + pull-requests: write + +jobs: + test: + uses: ./.github/workflows/Dev_CI.yml + + build: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setting dev-secret.yml + run: | + echo "${{ secrets.DOMAIN_MEMBER_DEV_SECRET_YML }}" > ./domain-member/src/main/resources/dev-secret.yml + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Make gradlew executable + run: chmod +x gradlew + + - name: bootJar with Gradle + run: ./gradlew :domain-member:bootJar --info + + - name: Change artifact file name + run: mv domain-member/build/libs/*.jar domain-member/build/libs/app.jar + + - name: Upload artifact file + uses: actions/upload-artifact@v4 + with: + name: app-artifact + path: ./domain-member/build/libs/app.jar + if-no-files-found: error + + - name: Upload deploy scripts + uses: actions/upload-artifact@v4 + with: + name: deploy-scripts + path: ./scripts/dev/ + if-no-files-found: error + + deploy: + needs: build + runs-on: dev-domain-member + + steps: + - name: Download artifact file + uses: actions/download-artifact@v4 + with: + name: app-artifact + path: ~/app + + - name: Download deploy scripts + uses: actions/download-artifact@v4 + with: + name: deploy-scripts + path: ~/app/scripts + + - name: Replace application to latest + run: sudo sh ~/app/scripts/replace-new-version.sh diff --git a/.github/workflows/Dev_Domain_Owner_CD.yml b/.github/workflows/Dev_Domain_Owner_CD.yml new file mode 100644 index 0000000..8adf798 --- /dev/null +++ b/.github/workflows/Dev_Domain_Owner_CD.yml @@ -0,0 +1,84 @@ +name: dev-owner-domain-cd + +on: + push: + branches: + - "develop" + paths: + - 'common-client/**' + - 'domain-owner/**' + - 'domain-common/**' + - 'build.gradle' + - 'settings.gradle' + - '.github/workflows/Dev_Domain_Owner_CD.yml' + +permissions: + contents: read + checks: write + actions: read + pull-requests: write + +jobs: + test: + uses: ./.github/workflows/Dev_CI.yml + + build: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setting dev-secret.yml + run: | + echo "${{ secrets.DOMAIN_OWNER_DEV_SECRET_YML }}" > ./domain-owner/src/main/resources/dev-secret.yml + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Make gradlew executable + run: chmod +x gradlew + + - name: bootJar with Gradle + run: ./gradlew :domain-owner:bootJar --info + + - name: Change artifact file name + run: mv domain-owner/build/libs/*.jar domain-owner/build/libs/app.jar + + - name: Upload artifact file + uses: actions/upload-artifact@v4 + with: + name: app-artifact + path: ./domain-owner/build/libs/app.jar + if-no-files-found: error + + - name: Upload deploy scripts + uses: actions/upload-artifact@v4 + with: + name: deploy-scripts + path: ./scripts/dev/ + if-no-files-found: error + + deploy: + needs: build + runs-on: dev-domain-owner + + steps: + - name: Download artifact file + uses: actions/download-artifact@v4 + with: + name: app-artifact + path: ~/app + + - name: Download deploy scripts + uses: actions/download-artifact@v4 + with: + name: deploy-scripts + path: ~/app/scripts + + - name: Replace application to latest + run: sudo sh ~/app/scripts/replace-new-version.sh diff --git a/.github/workflows/Dev_Domain_Reservation_CD.yml b/.github/workflows/Dev_Domain_Reservation_CD.yml new file mode 100644 index 0000000..d52b04c --- /dev/null +++ b/.github/workflows/Dev_Domain_Reservation_CD.yml @@ -0,0 +1,84 @@ +name: dev-reservation-cd + +on: + push: + branches: + - "develop" + paths: + - 'common-client/**' + - 'domain-reservation/**' + - 'domain-common/**' + - 'build.gradle' + - 'settings.gradle' + - '.github/workflows/Dev_Domain_Reservation_CD.yml' + +permissions: + contents: read + checks: write + actions: read + pull-requests: write + +jobs: + test: + uses: ./.github/workflows/Dev_CI.yml + + build: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setting dev-secret.yml + run: | + echo "${{ secrets.DOMAIN_RESERVATION_DEV_SECRET_YML }}" > ./domain-reservation/src/main/resources/dev-secret.yml + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Make gradlew executable + run: chmod +x gradlew + + - name: bootJar with Gradle + run: ./gradlew :domain-reservation:bootJar --info + + - name: Change artifact file name + run: mv domain-reservation/build/libs/*.jar domain-reservation/build/libs/app.jar + + - name: Upload artifact file + uses: actions/upload-artifact@v4 + with: + name: app-artifact + path: ./domain-reservation/build/libs/app.jar + if-no-files-found: error + + - name: Upload deploy scripts + uses: actions/upload-artifact@v4 + with: + name: deploy-scripts + path: ./scripts/dev/ + if-no-files-found: error + + deploy: + needs: build + runs-on: dev-domain-reservation + + steps: + - name: Download artifact file + uses: actions/download-artifact@v4 + with: + name: app-artifact + path: ~/app + + - name: Download deploy scripts + uses: actions/download-artifact@v4 + with: + name: deploy-scripts + path: ~/app/scripts + + - name: Replace application to latest + run: sudo sh ~/app/scripts/replace-new-version.sh diff --git a/.github/workflows/Dev_Domain_Restaurant_CD.yml b/.github/workflows/Dev_Domain_Restaurant_CD.yml new file mode 100644 index 0000000..97750fd --- /dev/null +++ b/.github/workflows/Dev_Domain_Restaurant_CD.yml @@ -0,0 +1,84 @@ +name: dev-restaurant-cd + +on: + push: + branches: + - "develop" + paths: + - 'common-client/**' + - 'domain-restaurant/**' + - 'domain-common/**' + - 'build.gradle' + - 'settings.gradle' + - '.github/workflows/Dev_Domain_Restaurant_CD.yml' + +permissions: + contents: read + checks: write + actions: read + pull-requests: write + +jobs: + test: + uses: ./.github/workflows/Dev_CI.yml + + build: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setting dev-secret.yml + run: | + echo "${{ secrets.DOMAIN_RESTAURANT_DEV_SECRET_YML }}" > ./domain-restaurant/src/main/resources/dev-secret.yml + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Make gradlew executable + run: chmod +x gradlew + + - name: bootJar with Gradle + run: ./gradlew :domain-restaurant:bootJar --info + + - name: Change artifact file name + run: mv domain-restaurant/build/libs/*.jar domain-restaurant/build/libs/app.jar + + - name: Upload artifact file + uses: actions/upload-artifact@v4 + with: + name: app-artifact + path: ./domain-restaurant/build/libs/app.jar + if-no-files-found: error + + - name: Upload deploy scripts + uses: actions/upload-artifact@v4 + with: + name: deploy-scripts + path: ./scripts/dev/ + if-no-files-found: error + + deploy: + needs: build + runs-on: dev-domain-restaurant + + steps: + - name: Download artifact file + uses: actions/download-artifact@v4 + with: + name: app-artifact + path: ~/app + + - name: Download deploy scripts + uses: actions/download-artifact@v4 + with: + name: deploy-scripts + path: ~/app/scripts + + - name: Replace application to latest + run: sudo sh ~/app/scripts/replace-new-version.sh diff --git a/CLAUDE.md b/CLAUDE.md index 40fa727..6bad741 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,10 +5,24 @@ ## ๐Ÿ“š ๋ชฉ์ฐจ 1. [ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ](#ํ”„๋กœ์ ํŠธ-๊ตฌ์กฐ) -2. [ํ…Œ์ŠคํŠธ ๋ ˆ์ด์–ด๋ณ„ ๊ตฌ์„ฑ](#ํ…Œ์ŠคํŠธ-๋ ˆ์ด์–ด๋ณ„-๊ตฌ์„ฑ) -3. [๋ชจ๋“ˆ๋ณ„ ํ…Œ์ŠคํŠธ ์ „๋žต](#๋ชจ๋“ˆ๋ณ„-ํ…Œ์ŠคํŠธ-์ „๋žต) -4. [ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๊ทœ์น™](#ํ…Œ์ŠคํŠธ-์ž‘์„ฑ-๊ทœ์น™) -5. [ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ](#ํ…Œ์ŠคํŠธ-์ธํ”„๋ผ) +2. [์•„ํ‚คํ…์ฒ˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กœ๋“œ๋งต](#-์•„ํ‚คํ…์ฒ˜-๋งˆ์ด๊ทธ๋ ˆ์ด์…˜-๋กœ๋“œ๋งต) +3. [ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ](#ํ…Œ์ŠคํŠธ-์ปค๋ฒ„๋ฆฌ์ง€-๋ชฉํ‘œ) +4. [์ฒดํฌ๋ฆฌ์ŠคํŠธ](#์ฒดํฌ๋ฆฌ์ŠคํŠธ) +5. [์ฐธ๊ณ  ์ž๋ฃŒ](#์ฐธ๊ณ -์ž๋ฃŒ) +6. [๋ณ€๊ฒฝ ์ด๋ ฅ](#๋ณ€๊ฒฝ-์ด๋ ฅ) + +## ๐Ÿ“– ์ƒ์„ธ ๊ฐ€์ด๋“œ ๋ฌธ์„œ + +ํ”„๋กœ์ ํŠธ์˜ ์ƒ์„ธํ•œ ๊ฐ€์ด๋“œ๋Š” ๋ณ„๋„ ๋ฌธ์„œ๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค: + +- **[ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™](./claudedocs/guides/naming-conventions.md)** - Domain/BFF ๋ชจ๋“ˆ ๋„ค์ด๋ฐ ํŒจํ„ด +- **[BFF ํŒจํ„ด ๋ฐ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต](./claudedocs/guides/bff-transaction-strategy.md)** - BFF ์ฑ…์ž„๊ณผ Phase๋ณ„ ์ „๋žต +- **[๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ](./claudedocs/guides/local-development.md)** - Docker Compose, Eureka Server ์„ค์ • +- **[ํ…Œ์ŠคํŠธ ๋ ˆ์ด์–ด๋ณ„ ๊ตฌ์„ฑ](./claudedocs/guides/test-layer-guide.md)** - 8๊ฐœ ํ…Œ์ŠคํŠธ ๋ ˆ์ด์–ด ์ƒ์„ธ ๊ฐ€์ด๋“œ +- **[๋ชจ๋“ˆ๋ณ„ ํ…Œ์ŠคํŠธ ์ „๋žต](./claudedocs/guides/module-test-strategies.md)** - ๊ฐ ๋ชจ๋“ˆ์˜ ํ…Œ์ŠคํŠธ ํƒ€์ž…๊ณผ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ +- **[ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๊ทœ์น™](./claudedocs/guides/test-writing-rules.md)** - ๋„ค์ด๋ฐ, AssertJ, ParameterizedTest ๊ทœ์น™ +- **[ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ](./claudedocs/guides/test-infrastructure.md)** - Gradle, application-test.yml, testFixtures ์„ค์ • +- **[์ธํ”„๋ผ ํ†ตํ•ฉ](./claudedocs/guides/infrastructure-integration.md)** - Flyway, AWS MSK (Kafka) ํ†ตํ•ฉ ๊ฐ€์ด๋“œ --- @@ -225,2134 +239,6 @@ batch-reminder โ†’ [HTTP/REST] โ†’ domain-reservation (Service) --- -## BFF ํŒจํ„ด ๋ฐ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต - -### ์„ค๊ณ„ ์›์น™ - -#### Domain ์„œ๋น„์Šค ์ฑ…์ž„ - -**์ œ๊ณตํ•˜๋Š” ๊ฒƒ** (โœ…): -- ์ž์‹ ์˜ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ CRUD -- ๋„๋ฉ”์ธ ๊ฒ€์ฆ ๋กœ์ง -- ๋‹จ์ผ ๋„๋ฉ”์ธ ๋‚ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง - -**์ œ๊ณตํ•˜์ง€ ์•Š๋Š” ๊ฒƒ** (โŒ): -- ๋‹ค๋ฅธ domain ์„œ๋ฒ„ ํ˜ธ์ถœ -- ๋ถ„์‚ฐ ๋ฝ ๊ด€๋ฆฌ (Redis ๋“ฑ) -- ๋ฐ์ดํ„ฐ ์กฐํ•ฉ ๋ฐ ์‘๋‹ต ์ƒ์„ฑ -- ํŠธ๋žœ์žญ์…˜ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ - -#### BFF(api-*) ์ฑ…์ž„ - -**์ œ๊ณตํ•˜๋Š” ๊ฒƒ** (โœ…): -- ์—ฌ๋Ÿฌ domain ์„œ๋น„์Šค ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ -- Redis ๋ถ„์‚ฐ ๋ฝ ๊ด€๋ฆฌ -- ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๊ด€๋ฆฌ -- ์‘๋‹ต ๋ฐ์ดํ„ฐ ์กฐํ•ฉ -- ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka) -- ์‚ฌ์šฉ์ž ์ธ์ฆ/๊ถŒํ•œ ๊ฒ€์ฆ - -### Phase๋ณ„ ์ „๋žต - -#### Phase 4-5: BFF์—์„œ ๋ชจ๋“  ๊ฒƒ ์ฒ˜๋ฆฌ - -``` -api-user (BFF) -โ”œโ”€โ”€ Redis ๋ถ„์‚ฐ ๋ฝ ํš๋“/ํ•ด์ œ -โ”œโ”€โ”€ domain-member ํ˜ธ์ถœ (์ง์ ‘ ์˜์กด์„ฑ โ†’ Feign) -โ”œโ”€โ”€ domain-restaurant ํ˜ธ์ถœ (capacity ๊ด€๋ฆฌ) -โ”œโ”€โ”€ domain-reservation ํ˜ธ์ถœ (์˜ˆ์•ฝ ์ƒ์„ฑ) -โ”œโ”€โ”€ ์‘๋‹ต ์กฐํ•ฉ (์—ฌ๋Ÿฌ domain ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ) -โ””โ”€โ”€ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka) -``` - -**ํŠน์ง•**: -- ๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ •์  -- ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋ช…ํ™• -- ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด BFF์— ์ง‘์ค‘ - -#### Phase 6: Saga Orchestrator ๋„์ž… - -``` -ReservationOrchestrator (์‹ ๊ทœ ์„œ๋น„์Šค) -โ”œโ”€โ”€ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ -โ”œโ”€โ”€ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ -โ”œโ”€โ”€ Redis ๋ถ„์‚ฐ ๋ฝ ๊ด€๋ฆฌ -โ””โ”€โ”€ ์ด๋ฒคํŠธ ๋ฐœํ–‰ - -api-user (๊ฒฝ๋Ÿ‰ BFF) -โ”œโ”€โ”€ Orchestrator ํ˜ธ์ถœ -โ”œโ”€โ”€ ์‘๋‹ต ๋ณ€ํ™˜ -โ””โ”€โ”€ ์‚ฌ์šฉ์ž ์ธ์ฆ -``` - -**ํŠน์ง•**: -- BFF ๊ฒฝ๋Ÿ‰ํ™” -- ๋ณต์žกํ•œ ํŠธ๋žœ์žญ์…˜ ๋กœ์ง ๋ถ„๋ฆฌ -- ํ™•์žฅ์„ฑ ๋†’์Œ - -### ์˜ˆ์•ฝ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ์˜ˆ์‹œ - -#### Phase 4 (ํ˜„์žฌ - ์ง์ ‘ ์˜์กด์„ฑ) - -```java -// api-user/ReservationService.java -@Service -@Transactional -@RequiredArgsConstructor -public class ReservationService { - - // ์ง์ ‘ ์˜์กด์„ฑ - private final ReservationDomainService reservationDomainService; - private final RestaurantDomainService restaurantDomainService; - private final MemberDomainService memberDomainService; - private final ReservationRedisService redisService; - - public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { - // 1. BFF๊ฐ€ Redis ๋ฝ ํš๋“ - if (!redisService.isReserving(memberId, request.restaurantId(), request.availableDateId())) { - throw new AlreadyReservingException(); - } - - // 2. BFF๊ฐ€ ์—ฌ๋Ÿฌ domain ํ˜ธ์ถœ - Member member = memberDomainService.getById(memberId); // domain-member - restaurantDomainService.decreaseCapacity(...); // domain-restaurant - Reservation reservation = reservationDomainService.create(...); // domain-reservation - - // 3. BFF๊ฐ€ ์‘๋‹ต ์กฐํ•ฉ - return CreateReservationResponse.builder() - .id(reservation.getId()) - .restaurantName(...) - .memberName(member.getName()) - .build(); - } -} -``` - -**ํŠน์ง•**: -- โœ… ๋‹จ์ผ @Transactional๋กœ ์ผ๊ด€์„ฑ ๋ณด์žฅ -- โœ… ๊ฐ„๋‹จํ•œ ๊ตฌ์กฐ -- โŒ BFF๊ฐ€ ๋ฌด๊ฑฐ์›Œ์ง - -#### Phase 5 (Feign Client ์ „ํ™˜) - -```java -// api-user/ReservationService.java -@Service -@RequiredArgsConstructor -public class ReservationService { - - // Feign Client ์˜์กด์„ฑ - private final MemberClient memberClient; - private final RestaurantClient restaurantClient; - private final ReservationClient reservationClient; - private final ReservationRedisService redisService; - - public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { - // 1. Redis ๋ฝ - redisService.isReserving(...); - - // 2. Feign Client ํ˜ธ์ถœ - MemberDTO member = memberClient.getMember(memberId); - restaurantClient.decreaseCapacity(...); - ReservationDTO reservation = reservationClient.create(...); - - // 3. ์‘๋‹ต ์กฐํ•ฉ - return buildResponse(reservation, member, ...); - } -} -``` - -**ํŠน์ง•**: -- โœ… ์™„์ „ํ•œ BFF ํŒจํ„ด -- โœ… ๋…๋ฆฝ ๋ฐฐํฌ ๊ฐ€๋Šฅ -- โŒ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ ๋ฌธ์ œ - -#### Phase 6 (Saga Orchestrator) - -```java -// api-user/ReservationService.java (๊ฒฝ๋Ÿ‰ํ™”) -@Service -@RequiredArgsConstructor -public class ReservationService { - - private final ReservationOrchestrator orchestrator; - - public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { - // Orchestrator์— ์œ„์ž„ - return orchestrator.executeReservationSaga(memberId, request); - } -} - -// reservation-orchestrator/ReservationSagaOrchestrator.java -@Service -public class ReservationSagaOrchestrator { - - private final MemberClient memberClient; - private final RestaurantClient restaurantClient; - private final ReservationClient reservationClient; - - public CreateReservationResponse executeReservationSaga( - String memberId, - CreateReservationRequest request - ) { - SagaTransaction saga = new SagaTransaction(); - - try { - // Step 1: Member ํ™•์ธ - MemberDTO member = memberClient.getMember(memberId); - - // Step 2: Capacity ๊ฐ์†Œ + ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๋“ฑ๋ก - saga.addStep( - () -> restaurantClient.decreaseCapacity(...), - () -> restaurantClient.increaseCapacity(...) // ๋ณด์ƒ - ); - - // Step 3: Reservation ์ƒ์„ฑ + ๋ณด์ƒ - saga.addStep( - () -> reservationClient.create(...), - () -> reservationClient.delete(...) // ๋ณด์ƒ - ); - - return saga.execute(); - - } catch (Exception e) { - saga.compensate(); // ๋ชจ๋“  ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ - throw e; - } - } -} -``` - -**ํŠน์ง•**: -- โœ… ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ž๋™ ์ฒ˜๋ฆฌ -- โœ… BFF ๊ฒฝ๋Ÿ‰ํ™” -- โœ… ํ™•์žฅ์„ฑ ๋†’์Œ - -### ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต ๋น„๊ต - -| ํ•ญ๋ชฉ | Phase 4 (์ง์ ‘ ์˜์กด์„ฑ) | Phase 5 (BFF) | Phase 6 (Saga) | -|------|---------------------|--------------|---------------| -| **ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ** | @Transactional | ์ˆ˜๋™ ๊ด€๋ฆฌ | Saga Orchestrator | -| **์ผ๊ด€์„ฑ** | ๊ฐ•ํ•œ ์ผ๊ด€์„ฑ | ์ตœ์ข… ์ผ๊ด€์„ฑ | ์ตœ์ข… ์ผ๊ด€์„ฑ (๋ณด์ƒ) | -| **๋ณต์žก๋„** | ๋‚ฎ์Œ | ์ค‘๊ฐ„ | ๋†’์Œ | -| **ํ™•์žฅ์„ฑ** | ๋‚ฎ์Œ | ์ค‘๊ฐ„ | ๋†’์Œ | -| **์žฅ์•  ๋ณต๊ตฌ** | ๋กค๋ฐฑ | ์ˆ˜๋™ ๋ณต๊ตฌ | ์ž๋™ ๋ณด์ƒ | -| **๋„คํŠธ์›Œํฌ ๋ ˆ์ดํ„ด์‹œ** | ์—†์Œ | ์žˆ์Œ | ์žˆ์Œ | - -### ๊ถŒ์žฅ ์‚ฌํ•ญ - -**Phase 4-5 (BFF ์ „ํ™˜๊นŒ์ง€)**: -- BFF์—์„œ Redis ๋ฝ ๊ด€๋ฆฌ -- BFF์—์„œ ํŠธ๋žœ์žญ์…˜ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ -- domain ์„œ๋น„์Šค๋Š” ๋‹จ์ˆœ CRUD๋งŒ ์ œ๊ณต -- ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ BFF์— ์ง‘์ค‘ - -**Phase 6 ์ดํ›„ (Saga ๋„์ž…)**: -- Orchestrator๋กœ ํŠธ๋žœ์žญ์…˜ ๋กœ์ง ์ด๋™ -- BFF๋Š” ๊ฒฝ๋Ÿ‰ํ™” (์ธ์ฆ, ์‘๋‹ต ๋ณ€ํ™˜๋งŒ) -- ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ž๋™ํ™” -- ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ๊ฐ•ํ™” - ---- - -## ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ - -### Docker Compose ๊ตฌ์„ฑ - -WellMeet-Backend ํ”„๋กœ์ ํŠธ๋Š” `docker-compose.yml`์„ ํ†ตํ•ด ๋กœ์ปฌ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ธํ”„๋ผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. - -#### ์ธํ”„๋ผ ์ปดํฌ๋„ŒํŠธ - -**๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (MySQL 8.0)**: -- `mysql-reservation` - ์˜ˆ์•ฝ ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3306) -- `mysql-member` - ํšŒ์› ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3307) -- `mysql-owner` - ์‚ฌ์—…์ž ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3308) -- `mysql-restaurant` - ์‹๋‹น ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3309) - -**๋ฉ”์‹œ์ง• ๋ฐ ์บ์‹œ**: -- `redis` - ๋ถ„์‚ฐ ๋ฝ ๋ฐ ์บ์‹œ (ํฌํŠธ: 6379) -- `kafka` - ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค (ํฌํŠธ: 9092) -- `zookeeper` - Kafka ์ฝ”๋””๋„ค์ดํ„ฐ (ํฌํŠธ: 2181) - -**์„œ๋น„์Šค ๋””์Šค์ปค๋ฒ„๋ฆฌ**: -- `discovery-server` - Eureka Server (ํฌํŠธ: 8761) - -#### ์‹คํ–‰ ๋ฐฉ๋ฒ• - -```bash -# ์ „์ฒด ์ธํ”„๋ผ ์‹œ์ž‘ -docker-compose up -d - -# ํŠน์ • ์„œ๋น„์Šค๋งŒ ์‹œ์ž‘ -docker-compose up -d mysql-reservation redis - -# ๋กœ๊ทธ ํ™•์ธ -docker-compose logs -f discovery-server - -# ์ „์ฒด ์ค‘์ง€ ๋ฐ ์ œ๊ฑฐ -docker-compose down - -# ๋ณผ๋ฅจ๊นŒ์ง€ ์™„์ „ ์‚ญ์ œ -docker-compose down -v -``` - -#### Phase 2 ์ค€๋น„ ์‚ฌํ•ญ - -๊ฐ domain ๋ชจ๋“ˆ์ด ๋…๋ฆฝ ์„œ๋น„์Šค๋กœ ์ „ํ™˜๋  ๋•Œ๋ฅผ ๋Œ€๋น„ํ•˜์—ฌ: -- ๊ฐ ๋„๋ฉ”์ธ๋ณ„๋กœ ๋ณ„๋„์˜ MySQL ์ธ์Šคํ„ด์Šค ์ค€๋น„ ์™„๋ฃŒ -- Database per Service ํŒจํ„ด ์ ์šฉ ๊ฐ€๋Šฅ -- ์„œ๋น„์Šค ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ๋ณด์žฅ - ---- - -### Service Discovery (Eureka Server) - -#### ๊ฐœ์š” - -Netflix Eureka๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ Service Registry๋กœ, Microservices ํ™˜๊ฒฝ์—์„œ ์„œ๋น„์Šค ์ธ์Šคํ„ด์Šค๋ฅผ ์ž๋™์œผ๋กœ ๋“ฑ๋กํ•˜๊ณ  ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. - -#### ๊ธฐ์ˆ  ์Šคํƒ - -- **Spring Boot**: 3.5.3 -- **Spring Cloud**: 2025.0.0 (Northfields) -- **Eureka Server**: Netflix OSS - -#### ์ฃผ์š” ์„ค์ • - -**ํฌํŠธ**: 8761 - -**Eureka ์„ค์ •**: -```yaml -eureka: - client: - register-with-eureka: false # Eureka Server ์ž์ฒด๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋กํ•˜์ง€ ์•Š์Œ - fetch-registry: false # ๋‹จ์ผ ์„œ๋ฒ„ ๊ตฌ์„ฑ, ๋‹ค๋ฅธ Eureka ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฐ€์ ธ์˜ค์ง€ ์•Š์Œ - server: - enable-self-preservation: false # ๊ฐœ๋ฐœ ํ™˜๊ฒฝ: ์‘๋‹ต ์—†๋Š” ์„œ๋น„์Šค ์ฆ‰์‹œ ์ œ๊ฑฐ (90์ดˆ) -``` - -**Self Preservation ๋ชจ๋“œ**: -- ํ”„๋กœ๋•์…˜: `true` (๋„คํŠธ์›Œํฌ ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ •๋ณด ์œ ์ง€) -- ๊ฐœ๋ฐœ: `false` (๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ์„ ์œ„ํ•ด ๋น„ํ™œ์„ฑํ™”) - -#### ์ ‘์† ์ •๋ณด - -- **Dashboard**: http://localhost:8761 -- **Health Check**: http://localhost:8761/actuator/health -- **Eureka Apps API**: http://localhost:8761/eureka/apps - -#### Phase 2์—์„œ์˜ ์—ญํ•  - -๊ฐ domain ์„œ๋น„์Šค๊ฐ€ Eureka Client๋กœ ๋“ฑ๋ก๋˜๋ฉด: -1. ์„œ๋น„์Šค ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ Eureka์— ๋“ฑ๋ก -2. ๋‹ค๋ฅธ ์„œ๋น„์Šค๊ฐ€ ์ด๋ฆ„(service-id)์œผ๋กœ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ -3. ํ—ฌ์Šค ์ฒดํฌ๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง -4. ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ ๋ฐ ์žฅ์•  ๋ณต๊ตฌ ์ง€์› - -#### Docker Compose ํ†ตํ•ฉ - -discovery-server๋Š” docker-compose.yml์— ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ, Multi-stage Dockerfile๋กœ ๋นŒ๋“œ๋ฉ๋‹ˆ๋‹ค: - -```dockerfile -# Stage 1: Gradle ๋นŒ๋“œ -FROM gradle:8.5-jdk21 AS build -WORKDIR /app -COPY . . -RUN gradle :discovery-server:bootJar --no-daemon - -# Stage 2: ์‹คํ–‰ ํ™˜๊ฒฝ -FROM openjdk:21-jdk-slim -WORKDIR /app -COPY --from=build /app/discovery-server/build/libs/*.jar app.jar -EXPOSE 8761 -ENTRYPOINT ["java", "-jar", "app.jar"] -``` - -**Health Check**: -- ๊ฐ„๊ฒฉ: 30์ดˆ -- ํƒ€์ž„์•„์›ƒ: 3์ดˆ -- ์‹œ์ž‘ ๋Œ€๊ธฐ: 40์ดˆ - ---- - -## ํ…Œ์ŠคํŠธ ๋ ˆ์ด์–ด๋ณ„ ๊ตฌ์„ฑ - -### 1. Entity Layer (domain-* ๋ชจ๋“ˆ) - -**๋ชฉ์ **: ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ํ…Œ์ŠคํŠธ - -**์ ์šฉ ๋ชจ๋“ˆ**: - -- `domain-reservation` (์˜ˆ์•ฝ) -- `domain-member` (ํšŒ์›) -- `domain-owner` (์‚ฌ์—…์ž) -- `domain-restaurant` (์‹๋‹น) - -**์œ„์น˜**: `domain-{๋ชจ๋“ˆ๋ช…}/src/test/java/com/wellmeet/domain/{aggregate}/entity/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: ์—†์Œ (์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ) - -**๊ตฌ์„ฑ ์˜ˆ์‹œ**: - -```java -package com.wellmeet.domain.restaurant.entity; - -import static org.assertj.core.api.Assertions.*; - -import com.wellmeet.domain.restaurant.exception.RestaurantErrorCode; -import com.wellmeet.domain.restaurant.exception.RestaurantException; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -class RestaurantTest { - - @Nested - class ValidatePosition { - - @ParameterizedTest - @ValueSource(doubles = {Restaurant.MINIMUM_LATITUDE - 0.1, Restaurant.MAXIMUM_LATITUDE + 0.1}) - void ์œ„๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค(double latitude) { - assertThatThrownBy(() -> new Restaurant( - "id", - "name", - "address", - latitude, - 127.0, - "thumbnail", - null - )).isInstanceOf(RestaurantException.class) - .hasMessage(RestaurantErrorCode.INVALID_LATITUDE.getMessage()); - } - - @ParameterizedTest - @ValueSource(doubles = {Restaurant.MINIMUM_LONGITUDE - 0.1, Restaurant.MAXIMUM_LONGITUDE + 0.1}) - void ๊ฒฝ๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค(double longitude) { - assertThatThrownBy(() -> new Restaurant( - "id", - "name", - "address", - 37.5, - longitude, - "thumbnail", - null - )).isInstanceOf(RestaurantException.class) - .hasMessage(RestaurantErrorCode.INVALID_LONGITUDE.getMessage()); - } - } - - @Nested - class UpdateMetadata { - - @Test - void ์‹๋‹น_์ด๋ฆ„์„_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { - Restaurant restaurant = createDefaultRestaurant(); - String newName = "๋ณ€๊ฒฝ๋œ ์‹๋‹น๋ช…"; - - restaurant.updateName(newName); - - assertThat(restaurant.getName()).isEqualTo(newName); - } - - @Test - void ์‹๋‹น_์ฃผ์†Œ๋ฅผ_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { - Restaurant restaurant = createDefaultRestaurant(); - String newAddress = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์‹ ์‚ฌ๋™"; - - restaurant.updateAddress(newAddress); - - assertThat(restaurant.getAddress()).isEqualTo(newAddress); - } - } - - private Restaurant createDefaultRestaurant() { - return new Restaurant( - "id", - "๊ธฐ๋ณธ ์‹๋‹น", - "์„œ์šธ์‹œ", - 37.5, - 127.0, - "thumbnail", - null - ); - } -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ๋ช…์€ ํ•œ๊ธ€๋กœ ์ž‘์„ฑ (์–ธ๋”์Šค์ฝ”์–ด ์‚ฌ์šฉ) -- โœ… ์ •์ƒ ์ผ€์ด์Šค + ์˜ˆ์™ธ ์ผ€์ด์Šค ๋ชจ๋‘ ์ž‘์„ฑ -- โœ… ParameterizedTest ํ™œ์šฉ (๋ฐ˜๋ณต ์ผ€์ด์Šค) -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ DB ์ ‘๊ทผ ๊ธˆ์ง€ (์ˆœ์ˆ˜ ๊ฐ์ฒด ํ…Œ์ŠคํŠธ) -- โŒ Mock ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -### 2. Repository Layer (domain-* ๋ชจ๋“ˆ) - -**๋ชฉ์ **: @Query ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ง์ ‘ ์ž‘์„ฑํ•œ ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ๋ฉ”์†Œ๋“œ ํ…Œ์ŠคํŠธ - -**์ ์šฉ ๋ชจ๋“ˆ**: - -- `domain-reservation` (์˜ˆ์•ฝ) -- `domain-member` (ํšŒ์›) -- `domain-owner` (์‚ฌ์—…์ž) -- `domain-restaurant` (์‹๋‹น) - -**์œ„์น˜**: `domain-{๋ชจ๋“ˆ๋ช…}/src/test/java/com/wellmeet/domain/{aggregate}/repository/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `BaseRepositoryTest` - -**ํ…Œ์ŠคํŠธ ๋Œ€์ƒ**: - -- โœ… @Query๋กœ ์ง์ ‘ ์ž‘์„ฑํ•œ JPQL/Native SQL ๋ฉ”์†Œ๋“œ -- โœ… ๋ณต์žกํ•œ ์กฐ์ธ, ์ง‘๊ณ„ ์ฟผ๋ฆฌ -- โœ… Custom Repository ๊ตฌํ˜„์ฒด -- โŒ findById, save, findAll ๋“ฑ ์ž๋™ ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ๋Š” ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์Œ - -**๊ตฌ์„ฑ ์˜ˆ์‹œ**: - -```java -package com.wellmeet.domain.restaurant.repository; - -import static org.assertj.core.api.Assertions.*; - -import com.wellmeet.BaseRepositoryTest; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.domain.restaurant.model.BoundingBox; -import java.util.List; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -class RestaurantRepositoryTest extends BaseRepositoryTest { - - @Autowired - private RestaurantRepository restaurantRepository; - - @Nested - class FindWithBoundBox { - - @Test - void BoundingBox_๋‚ด์˜_์‹๋‹น๋งŒ_์กฐํšŒํ•œ๋‹ค() { - Restaurant restaurant1 = createAndSaveRestaurant("์‹๋‹น1", 37.5, 127.0); - Restaurant restaurant2 = createAndSaveRestaurant("์‹๋‹น2", 37.501, 127.001); - Restaurant restaurant3 = createAndSaveRestaurant("์‹๋‹น3", 38.0, 128.0); - - BoundingBox boundingBox = new BoundingBox(37.4, 37.6, 126.9, 127.1); - - List result = restaurantRepository.findWithBoundBox(boundingBox); - - assertThat(result) - .hasSize(2) - .extracting(Restaurant::getName) - .containsExactlyInAnyOrder("์‹๋‹น1", "์‹๋‹น2"); - } - - @Test - void BoundingBox_๋ฐ–์˜_์‹๋‹น์€_์กฐํšŒ๋˜์ง€_์•Š๋Š”๋‹ค() { - Restaurant restaurant = createAndSaveRestaurant("๋จผ_์‹๋‹น", 38.0, 128.0); - - BoundingBox boundingBox = new BoundingBox(37.4, 37.6, 126.9, 127.1); - - List result = restaurantRepository.findWithBoundBox(boundingBox); - - assertThat(result).isEmpty(); - } - } - - private Restaurant createAndSaveRestaurant(String name, double lat, double lon) { - Restaurant restaurant = new Restaurant( - name, - "description", - "address", - lat, - lon, - "thumbnail", - null - ); - return restaurantRepository.save(restaurant); - } -} -``` - -**BaseRepositoryTest ๊ตฌ์กฐ**: - -```java - -@Import({JpaAuditingConfig.class}) -@ExtendWith(DataBaseCleaner.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -public abstract class BaseRepositoryTest { -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… `BaseRepositoryTest` ์ƒ์† ํ•„์ˆ˜ -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… ์‹ค์ œ DB(Testcontainers MySQL) ์‚ฌ์šฉ -- โœ… `@DataJpaTest`๋กœ ์ตœ์†Œํ•œ์˜ ์ปจํ…์ŠคํŠธ๋งŒ ๋กœ๋“œ -- โœ… **@Query๋กœ ์ง์ ‘ ์ž‘์„ฑํ•œ ๋ฉ”์†Œ๋“œ๋งŒ ํ…Œ์ŠคํŠธ** -- โŒ findById, save, findAll ๋“ฑ ์ž๋™ ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜์ง€ ์•Š์Œ -- โŒ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ…Œ์ŠคํŠธ ๊ธˆ์ง€ (Domain Service์—์„œ) -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -### 3. Domain Service Layer (domain-reservation ๋ชจ๋“ˆ) - -**๋ชฉ์ **: ๋„๋ฉ”์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + Repository ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ - -**์œ„์น˜**: `domain-reservation/src/test/java/com/wellmeet/domain/{aggregate}/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `BaseRepositoryTest` (Repository ํฌํ•จ ํ…Œ์ŠคํŠธ) - -**๊ตฌ์„ฑ ์˜ˆ์‹œ**: - -```java -package com.wellmeet.domain.restaurant; - -import static org.assertj.core.api.Assertions.*; - -import com.wellmeet.BaseRepositoryTest; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.domain.restaurant.repository.RestaurantRepository; -import java.util.List; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Import; - -@Import(RestaurantDomainService.class) -class RestaurantDomainServiceTest extends BaseRepositoryTest { - - @Autowired - private RestaurantDomainService restaurantDomainService; - - @Autowired - private RestaurantRepository restaurantRepository; - - @Nested - class FindNearbyRestaurants { - - @Test - void BoundingBox๋ฅผ_๊ณ„์‚ฐํ•˜์—ฌ_์ฃผ๋ณ€_์‹๋‹น์„_์กฐํšŒํ•œ๋‹ค() { - createAndSaveRestaurant("์‹๋‹น1", 37.5, 127.0); - createAndSaveRestaurant("์‹๋‹น2", 37.501, 127.001); - createAndSaveRestaurant("๋จผ์‹๋‹น", 38.0, 128.0); - - double userLat = 37.5; - double userLon = 127.0; - double radiusKm = 1.0; - - List result = restaurantDomainService - .findNearbyRestaurants(userLat, userLon, radiusKm); - - assertThat(result) - .hasSize(2) - .extracting(Restaurant::getName) - .containsExactlyInAnyOrder("์‹๋‹น1", "์‹๋‹น2"); - } - - @Test - void ๋ฐ˜๊ฒฝ_๋‚ด์—_์‹๋‹น์ด_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { - createAndSaveRestaurant("๋จผ์‹๋‹น", 38.0, 128.0); - - double userLat = 37.5; - double userLon = 127.0; - double radiusKm = 0.1; - - List result = restaurantDomainService - .findNearbyRestaurants(userLat, userLon, radiusKm); - - assertThat(result).isEmpty(); - } - } - - @Nested - class UpdateRestaurantMetadata { - - @Test - void ์‹๋‹น_๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ_์—…๋ฐ์ดํŠธํ•œ๋‹ค() { - Restaurant restaurant = createAndSaveRestaurant("์›๋ณธ ์‹๋‹น", 37.5, 127.0); - String newName = "์ˆ˜์ •๋œ ์‹๋‹น"; - String newAddress = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์‹ ์‚ฌ๋™"; - - Restaurant updated = restaurantDomainService - .updateRestaurantMetadata(restaurant.getId(), newName, newAddress); - - assertThat(updated.getName()).isEqualTo(newName); - assertThat(updated.getAddress()).isEqualTo(newAddress); - - Restaurant persisted = restaurantRepository.findById(restaurant.getId()) - .orElseThrow(); - assertThat(persisted.getName()).isEqualTo(newName); - } - - @Test - void ์กด์žฌํ•˜์ง€_์•Š๋Š”_์‹๋‹น_์กฐํšŒ_์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { - String nonExistentId = "non-existent-id"; - - assertThatThrownBy(() -> - restaurantDomainService.getRestaurantById(nonExistentId)) - .isInstanceOf(RestaurantException.class) - .hasMessageContaining("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‹๋‹น"); - } - } - - private Restaurant createAndSaveRestaurant(String name, double lat, double lon) { - Restaurant restaurant = new Restaurant( - name, - "description", - "address", - lat, - lon, - "thumbnail", - null - ); - return restaurantRepository.save(restaurant); - } -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… `@Import(DomainService.class)` ๋ช…์‹œ -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… Repository์™€ ํ•จ๊ป˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ -- โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฒ€์ฆ (๊ณ„์‚ฐ, ๋ณ€ํ™˜, ์œ ํšจ์„ฑ) -- โœ… ์˜ˆ์™ธ ์ƒํ™ฉ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ -- โœ… ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ํ™•์ธ -- โŒ Controller ๋กœ์ง ํฌํ•จ ๊ธˆ์ง€ -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -### 4. Service Layer (api-user, api-owner ๋ชจ๋“ˆ) - -**๋ชฉ์ **: Application Service ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + Mock ๊ธฐ๋ฐ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ - -**์œ„์น˜**: `api-{user|owner}/src/test/java/com/wellmeet/{feature}/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: Mock ์‚ฌ์šฉ ์‹œ ์—†์Œ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹œ `BaseServiceTest` - -**๊ตฌ์„ฑ ์˜ˆ์‹œ - ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Mock)**: - -```java -package com.wellmeet.reservation; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.reservation.ReservationDomainService; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.reservation.dto.ReservationResponse; -import java.time.LocalDateTime; -import java.util.List; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class ReservationServiceTest { - - @Mock - private ReservationDomainService reservationDomainService; - - @InjectMocks - private ReservationService reservationService; - - @Nested - class GetReservations { - - @Test - void ์‹๋‹น_์•„์ด๋””์—_ํ•ด๋‹นํ•˜๋Š”_์˜ˆ์•ฝ๋ชฉ๋ก์„_๋ถˆ๋Ÿฌ์˜จ๋‹ค() { - Restaurant restaurant = createRestaurant("Test Restaurant"); - AvailableDate availableDate = createAvailableDate(LocalDateTime.now(), 10, restaurant); - Member member1 = createMember("Test"); - Member member2 = createMember("Test2"); - Reservation reservation1 = createReservation(restaurant, availableDate, member1, 4); - Reservation reservation2 = createReservation(restaurant, availableDate, member2, 2); - List reservations = List.of(reservation1, reservation2); - - when(reservationDomainService.findAllByRestaurantId(restaurant.getId())) - .thenReturn(reservations); - - List expectedReservations = reservationService.getReservations(restaurant.getId()); - - assertThat(expectedReservations).hasSize(reservations.size()); - } - } - - private Restaurant createRestaurant(String name) { - return new Restaurant(name, "description", "address", 32.1, 37.1, "thumbnail", new Owner("name", "email")); - } - - private AvailableDate createAvailableDate(LocalDateTime dateTime, int capacity, Restaurant restaurant) { - return new AvailableDate(dateTime.toLocalDate(), dateTime.toLocalTime(), capacity, restaurant); - } - - private Member createMember(String name) { - return new Member(name, "nickname", "email@email.com", "phone"); - } - - private Reservation createReservation(Restaurant restaurant, AvailableDate availableDate, Member member, - int partySize) { - return new Reservation(restaurant, availableDate, member, partySize, "request"); - } -} -``` - -**๊ตฌ์„ฑ ์˜ˆ์‹œ - ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (BaseServiceTest)**: - -```java -package com.wellmeet.reservation; - -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; - -import com.wellmeet.BaseServiceTest; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.reservation.dto.CreateReservationRequest; -import com.wellmeet.reservation.dto.CreateReservationResponse; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -class ReservationServiceTest extends BaseServiceTest { - - @Autowired - private ReservationService reservationService; - - @Autowired - private ReservationRedisService reservationRedisService; - - @BeforeEach - void setUp() { - reservationRedisService.deleteReservationLock(); - } - - @Nested - class Reserve { - - @Test - void ํ•œ_์‚ฌ๋žŒ์ด_๊ฐ™์€_์˜ˆ์•ฝ_์š”์ฒญ์„_๋™์‹œ์—_์—ฌ๋Ÿฌ๋ฒˆ_์‹ ์ฒญํ•ด๋„_ํ•œ_๋ฒˆ๋งŒ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { - Owner owner1 = ownerGenerator.generate("owner1"); - Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1); - int capacity = 100; - AvailableDate availableDate = availableDateGenerator.generate( - LocalDateTime.now().plusDays(1), capacity, restaurant1 - ); - int partySize = 4; - CreateReservationRequest request = new CreateReservationRequest( - restaurant1.getId(), availableDate.getId(), partySize, "request" - ); - Member member = memberGenerator.generate("test"); - - runAtSameTime(500, () -> reservationService.reserve(member.getId(), request)); - - List reservations = reservationRepository.findAll(); - AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); - - assertAll( - () -> assertThat(reservations).hasSize(1), - () -> assertThat(foundAvailableDate.getMaxCapacity()).isEqualTo(capacity - partySize) - ); - } - - @Test - void ์—ฌ๋Ÿฌ_์‚ฌ๋žŒ์ด_์˜ˆ์•ฝ_์š”์ฒญ์„_๋™์‹œ์—_์‹ ์ฒญํ•ด๋„_์ ์ ˆํžˆ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { - Owner owner1 = ownerGenerator.generate("owner1"); - Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1); - int capacity = 100; - AvailableDate availableDate = availableDateGenerator.generate( - LocalDateTime.now().plusDays(1), capacity, restaurant1 - ); - int partySize = 2; - CreateReservationRequest request = new CreateReservationRequest( - restaurant1.getId(), availableDate.getId(), partySize, "request" - ); - List tasks = new ArrayList<>(); - for (int i = 0; i < 50; i++) { - Member member = memberGenerator.generate("member" + i); - tasks.add(() -> reservationService.reserve(member.getId(), request)); - } - - runAtSameTime(tasks); - - List reservations = reservationRepository.findAll(); - AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); - - assertAll( - () -> assertThat(reservations).hasSize(50), - () -> assertThat(foundAvailableDate.getMaxCapacity()).isZero() - ); - } - } -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: Mock ์‚ฌ์šฉ, ๋น ๋ฅธ ์‹คํ–‰ -- โœ… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: `BaseServiceTest` ์ƒ์†, ์‹ค์ œ DB -- โœ… ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ: `runAtSameTime()` ์œ ํ‹ธ ํ™œ์šฉ -- โœ… DTO ๋ณ€ํ™˜ ๋กœ์ง ๊ฒ€์ฆ -- โŒ HTTP ์š”์ฒญ/์‘๋‹ต ํ…Œ์ŠคํŠธ ๊ธˆ์ง€ (Controller์—์„œ) -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -### 5. Controller Layer (api-user, api-owner ๋ชจ๋“ˆ) - -**๋ชฉ์ **: REST API E2E ํ…Œ์ŠคํŠธ (HTTP โ†’ Service โ†’ DB) - -**์œ„์น˜**: `api-{user|owner}/src/test/java/com/wellmeet/{feature}/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `BaseControllerTest` - -**๊ตฌ์„ฑ ์˜ˆ์‹œ**: - -```java -package com.wellmeet.favorite; - -import static org.assertj.core.api.Assertions.*; - -import com.wellmeet.BaseControllerTest; -import com.wellmeet.domain.member.entity.FavoriteRestaurant; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; - -class FavoriteControllerTest extends BaseControllerTest { - - @Nested - class GetFavoriteRestaurants { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์กฐํšŒ() { - Member testUser = memberGenerator.generate("test"); - Member anotherUser = memberGenerator.generate("another"); - Owner owner1 = ownerGenerator.generate("Owner1"); - Owner owner2 = ownerGenerator.generate("Owner2"); - Owner owner3 = ownerGenerator.generate("Owner3"); - Restaurant restaurant1 = restaurantGenerator.generate("Restaurant 1", owner1); - Restaurant restaurant2 = restaurantGenerator.generate("Restaurant 2", owner2); - Restaurant restaurant3 = restaurantGenerator.generate("Restaurant 3", owner3); - favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser, restaurant1)); - favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser, restaurant2)); - favoriteRestaurantRepository.save(new FavoriteRestaurant(anotherUser, restaurant2)); - favoriteRestaurantRepository.save(new FavoriteRestaurant(anotherUser, restaurant3)); - - FavoriteRestaurantResponse[] responses = given() - .contentType("application/json") - .queryParam("memberId", testUser.getId()) - .when().get("/user/favorite/restaurant/list") - .then().statusCode(HttpStatus.OK.value()) - .extract().as(FavoriteRestaurantResponse[].class); - - assertThat(responses).hasSize(2); - assertThat(responses[0].getId()).isEqualTo(restaurant1.getId()); - assertThat(responses[1].getId()).isEqualTo(restaurant2.getId()); - } - } - - @Nested - class AddFavoriteRestaurant { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์ถ”๊ฐ€() { - Member testUser = memberGenerator.generate("testUser"); - Owner owner = ownerGenerator.generate("Test Owner"); - Restaurant restaurant = restaurantGenerator.generate("Test Restaurant", owner); - - FavoriteRestaurantResponse response = given() - .contentType("application/json") - .queryParam("memberId", testUser.getId()) - .when().post("/user/favorite/restaurant/{restaurantId}", restaurant.getId()) - .then().statusCode(HttpStatus.CREATED.value()) - .extract().as(FavoriteRestaurantResponse.class); - - assertThat(response.getId()).isEqualTo(restaurant.getId()); - } - } - - @Nested - class RemoveFavoriteRestaurant { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์‚ญ์ œ() { - Member testUser = memberGenerator.generate("testUser"); - Owner owner = ownerGenerator.generate("Test Owner"); - Restaurant restaurant = restaurantGenerator.generate("Test Restaurant", owner); - favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser, restaurant)); - - given() - .contentType("application/json") - .queryParam("memberId", testUser.getId()) - .when().delete("/user/favorite/restaurant/{restaurantId}", restaurant.getId()) - .then().statusCode(HttpStatus.NO_CONTENT.value()); - } - } -} -``` - -**BaseControllerTest ๊ตฌ์กฐ**: - -```java - -@ExtendWith(DataBaseCleaner.class) -@ActiveProfiles("test") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public abstract class BaseControllerTest { - - @LocalServerPort - private int port; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… `BaseControllerTest` ์ƒ์† ํ•„์ˆ˜ -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ API๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… REST Assured ์‚ฌ์šฉ -- โœ… HTTP ์ƒํƒœ ์ฝ”๋“œ ๊ฒ€์ฆ -- โœ… ์‘๋‹ต ๋ณธ๋ฌธ ๊ตฌ์กฐ ๊ฒ€์ฆ -- โœ… ์„ฑ๊ณต/์‹คํŒจ ์ผ€์ด์Šค ๋ชจ๋‘ ์ž‘์„ฑ -- โœ… ์ธ์ฆ/๊ถŒํ•œ ๊ฒ€์ฆ (ํ—ค๋”) -- โŒ Mock ์‚ฌ์šฉ ๊ธˆ์ง€ (E2E๋Š” ์‹ค์ œ ํ๋ฆ„) -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -### 6. Redis Service Layer (infra-redis ๋ชจ๋“ˆ) - -**๋ชฉ์ **: ๋ถ„์‚ฐ ๋ฝ, ์บ์‹ฑ ๋กœ์ง ํ…Œ์ŠคํŠธ - -**์œ„์น˜**: `infra-redis/src/test/java/com/wellmeet/{feature}/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: Testcontainers ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ - -**์ฃผ์š” ๊ธฐ์ˆ **: Redisson 3.50.0 (๋ถ„์‚ฐ ๋ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) - -**๊ตฌ์„ฑ ์˜ˆ์‹œ**: - -```java -package com.wellmeet.reservation; - -import static org.assertj.core.api.Assertions.*; - -import com.wellmeet.reservation.ReservationRedisService; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -@SpringBootTest -@Testcontainers -class ReservationRedisServiceTest { - - @Container - static GenericContainer redis = new GenericContainer<>("redis:7-alpine") - .withExposedPorts(6379); - - @DynamicPropertySource - static void redisProperties(DynamicPropertyRegistry registry) { - registry.add("spring.data.redis.host", redis::getHost); - registry.add("spring.data.redis.port", redis::getFirstMappedPort); - } - - @Autowired - private ReservationRedisService reservationRedisService; - - @Nested - class IsReserving { - - @Test - void ๋™์‹œ_์š”์ฒญ_์‹œ_ํ•˜๋‚˜๋งŒ_๋ฝ์„_ํš๋“ํ•œ๋‹ค() throws InterruptedException { - String memberId = "member-1"; - String restaurantId = "restaurant-1"; - Long availableDateId = 1L; - - int threadCount = 10; - ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - CountDownLatch latch = new CountDownLatch(threadCount); - AtomicInteger successCount = new AtomicInteger(0); - - for (int i = 0; i < threadCount; i++) { - executorService.submit(() -> { - try { - boolean acquired = reservationRedisService - .isReserving(memberId, restaurantId, availableDateId); - if (acquired) { - successCount.incrementAndGet(); - } - } finally { - latch.countDown(); - } - }); - } - - latch.await(); - executorService.shutdown(); - - assertThat(successCount.get()).isEqualTo(1); - } - } - - @Nested - class IsUpdating { - - @Test - void ๋ฝ์„_์ •์ƒ์ ์œผ๋กœ_ํš๋“ํ•˜๊ณ _ํ•ด์ œํ•œ๋‹ค() { - String memberId = "member-1"; - Long reservationId = 1L; - - boolean acquired = reservationRedisService.isUpdating(memberId, reservationId); - - assertThat(acquired).isTrue(); - - boolean retry = reservationRedisService.isUpdating(memberId, reservationId); - assertThat(retry).isFalse(); - } - } - - @Nested - class DeleteReservationLock { - - @Test - void ๋ฝ_์‚ญ์ œ_ํ›„_๋‹ค์‹œ_ํš๋“_๊ฐ€๋Šฅํ•˜๋‹ค() { - String memberId = "member-1"; - String restaurantId = "restaurant-1"; - Long availableDateId = 1L; - - reservationRedisService.isReserving(memberId, restaurantId, availableDateId); - - reservationRedisService.deleteReservationLock(); - - boolean reacquired = reservationRedisService - .isReserving(memberId, restaurantId, availableDateId); - assertThat(reacquired).isTrue(); - } - } -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… Testcontainers๋กœ ์‹ค์ œ Redis ์‚ฌ์šฉ -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ ํ•„์ˆ˜ -- โœ… ๋ฝ ํš๋“/ํ•ด์ œ ์‚ฌ์ดํด ๊ฒ€์ฆ -- โœ… ํƒ€์ž„์•„์›ƒ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ -- โŒ Mock Redis ์‚ฌ์šฉ ๊ธˆ์ง€ (๋ถ„์‚ฐ ๋ฝ์€ ์‹ค์ œ ํ™˜๊ฒฝ ํ•„์ˆ˜) -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -### 7. Kafka Producer Layer (infra-kafka ๋ชจ๋“ˆ) - -**๋ชฉ์ **: ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก, ์ง๋ ฌํ™”, ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ - -**์œ„์น˜**: `infra-kafka/src/test/java/com/wellmeet/kafka/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: EmbeddedKafka ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ - -**์ฃผ์š” ๊ธฐ์ˆ **: AWS MSK (Managed Streaming for Apache Kafka) + IAM ์ธ์ฆ - -โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ (์•„๋ž˜ ์˜ˆ์‹œ๋Š” ํ–ฅํ›„ ์ž‘์„ฑ์„ ์œ„ํ•œ ๊ฐ€์ด๋“œ) - -**๊ตฌ์„ฑ ์˜ˆ์‹œ**: - -```java -package com.wellmeet.kafka.service; - -import static org.assertj.core.api.Assertions.*; - -import com.wellmeet.kafka.dto.NotificationMessage; -import com.wellmeet.kafka.dto.payload.ReservationCreatedPayload; -import java.time.LocalDateTime; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.test.annotation.DirtiesContext; - -@SpringBootTest -@EmbeddedKafka( - partitions = 1, - topics = {"notification"}, - brokerProperties = { - "listeners=PLAINTEXT://localhost:9092", - "port=9092" - } -) -@DirtiesContext -class KafkaProducerServiceTest { - - @Autowired - private KafkaProducerService kafkaProducerService; - - private BlockingQueue receivedMessages = new LinkedBlockingQueue<>(); - - @KafkaListener(topics = "notification", groupId = "test-group") - public void listen(NotificationMessage message) { - receivedMessages.add(message); - } - - @Nested - class SendNotificationMessage { - - @Test - void ์˜ˆ์•ฝ_์ƒ์„ฑ_์•Œ๋ฆผ_๋ฉ”์‹œ์ง€๋ฅผ_๋ฐœ์†กํ•œ๋‹ค() throws InterruptedException { - ReservationCreatedPayload payload = ReservationCreatedPayload.builder() - .reservationId("reservation-1") - .restaurantName("๋ง›์ง‘") - .reservationDate(LocalDateTime.now().plusDays(1)) - .partySize(4) - .build(); - - String memberId = "member-1"; - - kafkaProducerService.sendNotificationMessage(memberId, payload); - - NotificationMessage received = receivedMessages.poll(5, TimeUnit.SECONDS); - assertThat(received).isNotNull(); - assertThat(received.getHeader().getRecipientId()).isEqualTo(memberId); - assertThat(received.getPayload()).isInstanceOf(ReservationCreatedPayload.class); - - ReservationCreatedPayload receivedPayload = - (ReservationCreatedPayload) received.getPayload(); - assertThat(receivedPayload.getReservationId()).isEqualTo("reservation-1"); - assertThat(receivedPayload.getRestaurantName()).isEqualTo("๋ง›์ง‘"); - } - - @Test - void ์ง๋ ฌํ™”_์—ญ์ง๋ ฌํ™”๊ฐ€_์ •์ƒ์ ์œผ๋กœ_๋™์ž‘ํ•œ๋‹ค() throws InterruptedException { - ReservationCreatedPayload payload = ReservationCreatedPayload.builder() - .reservationId("res-123") - .restaurantName("ํ•œ์‹๋‹น") - .reservationDate(LocalDateTime.of(2025, 12, 25, 18, 0)) - .partySize(2) - .build(); - - kafkaProducerService.sendNotificationMessage("member-1", payload); - - NotificationMessage received = receivedMessages.poll(5, TimeUnit.SECONDS); - assertThat(received).isNotNull(); - - ReservationCreatedPayload receivedPayload = - (ReservationCreatedPayload) received.getPayload(); - assertThat(receivedPayload.getReservationDate()) - .isEqualTo(LocalDateTime.of(2025, 12, 25, 18, 0)); - } - } -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… `@EmbeddedKafka` ์‚ฌ์šฉ -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… Consumer๋กœ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ๊ฒ€์ฆ -- โœ… ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ๊ฒ€์ฆ -- โœ… ํƒ€์ž„์•„์›ƒ ์„ค์ • (5์ดˆ) -- โœ… `@DirtiesContext`๋กœ ์ปจํ…์ŠคํŠธ ๊ฒฉ๋ฆฌ -- โŒ ์‹ค์ œ Kafka ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ๊ธˆ์ง€ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ) -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -### 8. Batch Job Layer (batch-reminder ๋ชจ๋“ˆ) - -**๋ชฉ์ **: Spring Batch Job ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ - -**์œ„์น˜**: `batch-reminder/src/test/java/com/wellmeet/batch/` - -**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `TestBatchConfig` ํฌํ•จ - -โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ (์•„๋ž˜ ์˜ˆ์‹œ๋Š” ํ–ฅํ›„ ์ž‘์„ฑ์„ ์œ„ํ•œ ๊ฐ€์ด๋“œ) - -**๊ตฌ์„ฑ ์˜ˆ์‹œ**: - -```java -package com.wellmeet.batch.job; - -import static org.assertj.core.api.Assertions.*; - -import com.wellmeet.batch.config.TestBatchConfig; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import java.time.LocalDateTime; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.test.JobLauncherTestUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; - -@SpringBootTest -@Import(TestBatchConfig.class) -class ReservationReminderJobConfigTest { - - @Autowired - private JobLauncherTestUtils jobLauncherTestUtils; - - @Nested - class ExecuteReminderJob { - - @Test - void ํ•œ_์‹œ๊ฐ„_์ „_์˜ˆ์•ฝ_๋ฆฌ๋งˆ์ธ๋”_๋ฐฐ์น˜๊ฐ€_์„ฑ๊ณตํ•œ๋‹ค() throws Exception { - Member member = createMember(); - Restaurant restaurant = createRestaurant(); - Reservation reservation = createReservation( - member, - restaurant, - LocalDateTime.now().plusHours(1) - ); - - JobExecution jobExecution = jobLauncherTestUtils.launchJob(); - - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(jobExecution.getStepExecutions()).hasSize(1); - } - } -} -``` - -**์ž‘์„ฑ ๊ทœ์น™**: - -- โœ… `JobLauncherTestUtils` ์‚ฌ์šฉ -- โœ… `@Nested` ํด๋ž˜์Šค๋กœ Job๋ณ„ ๊ทธ๋ฃนํ™” -- โœ… Job ์‹คํ–‰ ์ƒํƒœ ๊ฒ€์ฆ -- โœ… Step ์‹คํ–‰ ๊ฒฐ๊ณผ ๊ฒ€์ฆ -- โœ… Reader/Processor/Writer ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ -- โœ… Clock ์ฃผ์ž…์œผ๋กœ ์‹œ๊ฐ„ ์ œ์–ด -- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ -- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ - ---- - -## ๋ชจ๋“ˆ๋ณ„ ํ…Œ์ŠคํŠธ ์ „๋žต - -### domain-reservation ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|----------------|--------|------------------------------|----------------------| -| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ | -| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | @Query ์ปค์Šคํ…€ ์ฟผ๋ฆฌ๋งŒ | -| Domain Service | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest + @Import | ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + Repository | - -**ํŠน์ง•**: Flyway๋ฅผ ํ†ตํ•œ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ด€๋ฆฌ -**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% - ---- - -### domain-member ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|------------|--------|--------------------|--------------------| -| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ํšŒ์› ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ | -| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | @Query ์ปค์Šคํ…€ ์ฟผ๋ฆฌ๋งŒ | - -**ํŠน์ง•**: testFixtures ์ œ๊ณต (๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ) -**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% - ---- - -### domain-owner ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|------------|--------|--------------------|---------------------| -| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์‚ฌ์—…์ž ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ | -| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | @Query ์ปค์Šคํ…€ ์ฟผ๋ฆฌ๋งŒ | - -**ํŠน์ง•**: testFixtures ์ œ๊ณต (๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ) -**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% - ---- - -### domain-restaurant ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|------------|--------|--------------------|--------------------------| -| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์‹๋‹น ์ƒ์„ฑ, ์ขŒํ‘œ ๊ฒ€์ฆ, ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ | -| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | BoundingBox ์ฟผ๋ฆฌ, ์œ„์น˜ ๊ธฐ๋ฐ˜ ์กฐํšŒ | - -**ํŠน์ง•**: - -- testFixtures ์ œ๊ณต (๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ) -- ์ขŒํ‘œ ๊ธฐ๋ฐ˜ ์ฟผ๋ฆฌ (BoundingBox, ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ) - **์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% - ---- - -### api-user / api-owner ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|----------------|--------|-------------------------|----------------| -| Controller | E2E | BaseControllerTest | HTTP API ์ „์ฒด ํ๋ฆ„ | -| Service | ๋‹จ์œ„/ํ†ตํ•ฉ | Mock ๋˜๋Š” BaseServiceTest | ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง, ๋™์‹œ์„ฑ | -| Event Listener | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseServiceTest | ์ด๋ฒคํŠธ ๋ฐœํ–‰/์ˆ˜์‹  | - -**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 80% - ---- - -### infra-redis ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|---------------|--------|----------------|-----------| -| Redis Service | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | Testcontainers | ๋ถ„์‚ฐ ๋ฝ, ๋™์‹œ์„ฑ | - -**ํŠน์ง•**: Redisson 3.50.0 ์‚ฌ์šฉ (๋ถ„์‚ฐ ๋ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) -**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 90% (Critical - ๋™์‹œ์„ฑ ์ œ์–ด ํ•ต์‹ฌ ๋ชจ๋“ˆ) - ---- - -### infra-kafka ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|----------|--------|---------------|-------------| -| Producer | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | EmbeddedKafka | ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก, ์ง๋ ฌํ™” | -| DTO | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” | - -**ํŠน์ง•**: AWS MSK + IAM ์ธ์ฆ ์‚ฌ์šฉ -โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ -**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 70% (์ž‘์„ฑ ํ›„) - ---- - -### batch-reminder ๋ชจ๋“ˆ - -| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | -|------------|--------|-----------------|---------------| -| Job Config | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | TestBatchConfig | Job ์‹คํ–‰ ์„ฑ๊ณต | -| Processor | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋กœ์ง | -| Writer | ๋‹จ์œ„/ํ†ตํ•ฉ | Mock/์‹ค์ œ | ์™ธ๋ถ€ ํ˜ธ์ถœ (Kafka) | - -โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ -**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 75% (์ž‘์„ฑ ํ›„) - ---- - -## ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๊ทœ์น™ - -### 1. ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ - -```java -// โœ… Good - @Nested + ํ•œ๊ธ€ ๋ฉ”์†Œ๋“œ๋ช… -class RestaurantTest { - - @Nested - class ValidatePosition { - - @Test - void ์œ„๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค() { - } - - @Test - void ๊ฒฝ๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค() { - } - } - - @Nested - class UpdateMetadata { - - @Test - void ์‹๋‹น_์ด๋ฆ„์„_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { - } - } -} - -// โŒ Bad - @DisplayName ์‚ฌ์šฉ -@DisplayName("Restaurant ์—”ํ‹ฐํ‹ฐ") -class RestaurantTest { - - @Test - @DisplayName("์œ„๋„ ๊ฒ€์ฆ") - void validateLatitude() { - } -} -``` - ---- - -### 2. ์ฃผ์„ ์—†์ด ์ฝ”๋“œ๋กœ ํ‘œํ˜„ - -```java -// โœ… Good - ์ฃผ์„ ์—†์ด ๋ฐ”๋กœ ์ฝ”๋“œ -@Test -void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ•œ๋‹ค() { - Member member = createMember(); - Restaurant restaurant = createRestaurant(); - CreateReservationRequest request = new CreateReservationRequest(...); - - CreateReservationResponse response = reservationService.create(member.getId(), request); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(ReservationStatus.PENDING); -} - -// โŒ Bad - given, when, then ์ฃผ์„ ์‚ฌ์šฉ -@Test -void createReservation() { - // given - Member member = createMember(); - - // when - Reservation reservation = service.create(member); - - // then - assertThat(reservation).isNotNull(); -} -``` - ---- - -### 3. AssertJ ์‚ฌ์šฉ - -```java -// โœ… Good - AssertJ -assertThat(result). - -isNotNull(); - -assertThat(result.getName()). - -isEqualTo("์‹๋‹น"); - -assertThat(list). - -hasSize(3). - -extracting(Restaurant::getName). - -containsExactly("A","B","C"); - -// โŒ Bad - JUnit Assertions -assertTrue(result !=null); - -assertEquals("์‹๋‹น",result.getName()); -``` - ---- - -### 4. ์˜ˆ์™ธ ํ…Œ์ŠคํŠธ - -```java -// โœ… Good -@Test -void ์ž˜๋ชป๋œ_์ž…๋ ฅ_์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { - assertThatThrownBy(() -> service.doSomething()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("์ž˜๋ชป๋œ ์ž…๋ ฅ"); -} -``` - ---- - -### 5. ParameterizedTest ํ™œ์šฉ - -```java - -@ParameterizedTest -@ValueSource(ints = {0, -1, -100}) -void ์ธ์›_์ˆ˜๊ฐ€_0_์ดํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(int partySize) { - assertThatThrownBy(() -> Reservation.create(partySize)) - .isInstanceOf(IllegalArgumentException.class); -} - -@ParameterizedTest -@CsvSource({ - "37.5, 127.0, 1.0, 2", - "37.5, 127.0, 5.0, 5", - "37.5, 127.0, 10.0, 10" -}) -void ๋ฐ˜๊ฒฝ_๋‚ด_์‹๋‹น์„_์กฐํšŒํ•œ๋‹ค(double lat, double lon, double radius, int expectedCount) { - List result = service.findNearby(lat, lon, radius); - assertThat(result).hasSize(expectedCount); -} -``` - ---- - -## ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ - -### 1. Gradle ์„ค์ • - -**๋ฃจํŠธ build.gradle**: - -```gradle -subprojects { - apply plugin: 'jacoco' - - jacoco { - toolVersion = "0.8.11" - } - - test { - useJUnitPlatform() - finalizedBy jacocoTestReport - } - - jacocoTestReport { - dependsOn test - reports { - xml.required = true - html.required = true - } - } - - jacocoTestCoverageVerification { - violationRules { - rule { - limit { - minimum = 0.70 - } - } - } - } -} -``` - -**๋ชจ๋“ˆ๋ณ„ build.gradle (domain-redis ์˜ˆ์‹œ)**: - -```gradle -dependencies { - testImplementation 'org.testcontainers:testcontainers:1.19.3' - testImplementation 'org.testcontainers:junit-jupiter:1.19.3' -} -``` - -**kafka ๋ชจ๋“ˆ build.gradle**: - -```gradle -dependencies { - testImplementation 'org.springframework.kafka:spring-kafka-test' -} -``` - ---- - -### 2. ํ…Œ์ŠคํŠธ ์„ค์ • (application-test.yml) - -**domain-reservation ๋ชจ๋“ˆ** (`domain-reservation/src/main/resources/application-domain-test.yml`): - -```yaml -spring: - config: - activate: - on-profile: domain-test - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/test - username: root - password: - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - format_sql: true - show-sql: false -``` - -**infra-redis ๋ชจ๋“ˆ** (`infra-redis/src/main/resources/application-infra-redis-test.yml`): - -```yaml -spring: - config: - activate: - on-profile: infra-redis-test - data: - redis: - host: localhost - port: 6379 -``` - -**infra-kafka ๋ชจ๋“ˆ** (`infra-kafka/src/main/resources/application-infra-kafka-test.yml`): - -```yaml -spring: - config: - activate: - on-profile: infra-kafka-test - kafka: - bootstrap-servers: ${spring.embedded.kafka.brokers} - producer: - key-serializer: org.apache.kafka.common.serialization.StringSerializer - value-serializer: org.springframework.kafka.support.serializer.JsonSerializer -``` - -**api-user/api-owner ๋ชจ๋“ˆ** (`api-user/src/main/resources/application-test.yml`): - -```yaml -spring: - config: - import: - - application-domain-test.yml - - application-infra-redis-test.yml - - application-infra-kafka-test.yml -``` - ---- - -### 3. Test Fixtures (Gradle testFixtures ํ”Œ๋Ÿฌ๊ทธ์ธ) - -WellMeet-Backend ํ”„๋กœ์ ํŠธ๋Š” Gradle์˜ `java-test-fixtures` ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ฝ”๋“œ๋ฅผ ๋ชจ๋“ˆ ๊ฐ„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. - -#### testFixtures ์ ์šฉ ๋ชจ๋“ˆ - -- `domain-reservation` โ†’ ์˜ˆ์•ฝ, ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ๋‚ ์งœ ์ƒ์„ฑ -- `domain-member` โ†’ ํšŒ์›, ์ฆ๊ฒจ์ฐพ๊ธฐ ์ƒ์„ฑ -- `domain-owner` โ†’ ์‚ฌ์—…์ž ์ƒ์„ฑ -- `domain-restaurant` โ†’ ์‹๋‹น, ๋ฉ”๋‰ด ์ƒ์„ฑ - -#### build.gradle ์„ค์ • - -**domain ๋ชจ๋“ˆ (์˜ˆ: domain-member/build.gradle)**: - -```gradle -plugins { - id 'java-library' - id 'java-test-fixtures' // testFixtures ํ”Œ๋Ÿฌ๊ทธ์ธ ํ™œ์„ฑํ™” -} - -dependencies { - // ์ผ๋ฐ˜ ์˜์กด์„ฑ - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - - // testFixtures์—์„œ ํ•„์š”ํ•œ ์˜์กด์„ฑ - testFixturesImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' - testFixturesImplementation 'org.springframework.boot:spring-boot-starter-test' -} -``` - -**API ๋ชจ๋“ˆ (์˜ˆ: api-user/build.gradle)**: - -```gradle -dependencies { - // domain ๋ชจ๋“ˆ ์˜์กด์„ฑ - implementation project(':domain-member') - implementation project(':domain-owner') - implementation project(':domain-restaurant') - implementation project(':domain-reservation') - - // testFixtures ์‚ฌ์šฉ - testImplementation(testFixtures(project(':domain-member'))) - testImplementation(testFixtures(project(':domain-owner'))) - testImplementation(testFixtures(project(':domain-restaurant'))) - testImplementation(testFixtures(project(':domain-reservation'))) -} -``` - -#### ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ - -``` -domain-member/ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ main/java/ # ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ -โ”‚ โ”œโ”€โ”€ test/java/ # ๋ชจ๋“ˆ ๋‚ด๋ถ€ ํ…Œ์ŠคํŠธ -โ”‚ โ””โ”€โ”€ testFixtures/java/ # ๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Test Fixture -โ”‚ โ””โ”€โ”€ com/wellmeet/domain/member/ -โ”‚ โ”œโ”€โ”€ MemberFixture.java -โ”‚ โ””โ”€โ”€ FavoriteRestaurantFixture.java -``` - -#### Generator ํŒจํ„ด ๊ตฌํ˜„ ์˜ˆ์‹œ - -**domain-restaurant/src/testFixtures/java/com/wellmeet/domain/restaurant/RestaurantFixture.java**: - -```java -package com.wellmeet.domain.restaurant; - -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.domain.restaurant.repository.RestaurantRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class RestaurantFixture { - - @Autowired - private RestaurantRepository restaurantRepository; - - public Restaurant create(String name, Owner owner) { - return create(name, 37.5, 127.0, owner); - } - - public Restaurant create(String name, double lat, double lon, Owner owner) { - Restaurant restaurant = Restaurant.builder() - .name(name) - .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ") - .latitude(lat) - .longitude(lon) - .phoneNumber("02-1234-5678") - .owner(owner) - .thumbnailUrl("https://example.com/thumbnail.jpg") - .build(); - return restaurantRepository.save(restaurant); - } -} -``` - -**domain-member/src/testFixtures/java/com/wellmeet/domain/member/MemberFixture.java**: - -```java -package com.wellmeet.domain.member; - -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.member.repository.MemberRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class MemberFixture { - - @Autowired - private MemberRepository memberRepository; - - public Member create(String name) { - return create(name, name + "@example.com"); - } - - public Member create(String name, String email) { - Member member = Member.builder() - .name(name) - .nickname(name + "_nick") - .email(email) - .phoneNumber("010-1234-5678") - .build(); - return memberRepository.save(member); - } -} -``` - -#### API ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉ ์˜ˆ์‹œ - -**api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java**: - -```java - -@SpringBootTest -class ReservationServiceTest { - - @Autowired - private MemberFixture memberFixture; // domain-member testFixtures - - @Autowired - private OwnerFixture ownerFixture; // domain-owner testFixtures - - @Autowired - private RestaurantFixture restaurantFixture; // domain-restaurant testFixtures - - @Autowired - private ReservationService reservationService; - - @Test - void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ•œ๋‹ค() { - // testFixtures๋ฅผ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ค€๋น„ - Member member = memberFixture.create("testUser"); - Owner owner = ownerFixture.create("testOwner"); - Restaurant restaurant = restaurantFixture.create("ํ…Œ์ŠคํŠธ ์‹๋‹น", owner); - - // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ…Œ์ŠคํŠธ - ReservationResponse response = reservationService.reserve(...); - - assertThat(response).isNotNull(); - } -} -``` - -#### testFixtures์˜ ์žฅ์  - -1. **์žฌ์‚ฌ์šฉ์„ฑ**: ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ์—์„œ ๋™์ผํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋กœ์ง ๊ณต์œ  -2. **์ผ๊ด€์„ฑ**: ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ๋ฐฉ์‹์ด ์ค‘์•™ํ™”๋˜์–ด ์ผ๊ด€์„ฑ ์œ ์ง€ -3. **์œ ์ง€๋ณด์ˆ˜**: ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ณ€๊ฒฝ ์‹œ testFixtures๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋จ -4. **์บก์Аํ™”**: ๋„๋ฉ”์ธ ์ง€์‹์„ testFixtures์— ์บก์Аํ™” -5. **๋…๋ฆฝ์„ฑ**: ๊ฐ ๋„๋ฉ”์ธ ๋ชจ๋“ˆ์ด ์ž์‹ ์˜ testFixtures ์ œ๊ณต - -#### ์ฃผ์˜์‚ฌํ•ญ - -- testFixtures๋Š” **ํ…Œ์ŠคํŠธ ์ „์šฉ**์ด๋ฉฐ, ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ -- testFixtures ๊ฐ„ ์˜์กด์„ฑ์€ ์ตœ์†Œํ™” (์ˆœํ™˜ ์˜์กด์„ฑ ๋ฐฉ์ง€) -- Repository๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์‹ค์ œ DB์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹ ์‚ฌ์šฉ -- ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ testFixtures์— ํฌํ•จํ•˜์ง€ ์•Š์Œ - ---- - -## ์ธํ”„๋ผ ํ†ตํ•ฉ - -### Flyway ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - -#### ๊ฐœ์š” - -`domain-reservation` ๋ชจ๋“ˆ์—์„œ๋งŒ Flyway๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. - -#### ์ ์šฉ ์œ„์น˜ - -- **๋ชจ๋“ˆ**: `domain-reservation` -- **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ**: `domain-reservation/src/main/resources/db/migration/` -- **์‹คํ–‰ ์‹œ์ **: Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ž๋™ ์‹คํ–‰ - -#### build.gradle ์„ค์ • - -```gradle -dependencies { - implementation 'org.flywaydb:flyway-core' - implementation 'org.flywaydb:flyway-mysql' -} -``` - -#### application.yml ์„ค์ • - -```yaml -spring: - flyway: - enabled: true - baseline-on-migrate: true - locations: classpath:db/migration - sql-migration-prefix: V - sql-migration-suffix: .sql -``` - -#### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋„ค์ด๋ฐ - -``` -db/migration/ -โ”œโ”€โ”€ V1__create_reservation_table.sql -โ”œโ”€โ”€ V2__create_available_date_table.sql -โ”œโ”€โ”€ V3__add_status_column_to_reservation.sql -โ””โ”€โ”€ V4__add_index_on_reservation_date.sql -``` - -**๊ทœ์น™**: - -- `V{๋ฒ„์ „๋ฒˆํ˜ธ}__{์„ค๋ช…}.sql` ํ˜•์‹ -- ๋ฒ„์ „ ๋ฒˆํ˜ธ๋Š” ์ˆœ์ฐจ์ ์œผ๋กœ ์ฆ๊ฐ€ -- ์‹คํ–‰ ์ˆœ์„œ๋Š” ๋ฒ„์ „ ๋ฒˆํ˜ธ ๊ธฐ์ค€ - -#### ๋‹ค๋ฅธ domain ๋ชจ๋“ˆ - -๋‹ค๋ฅธ domain ๋ชจ๋“ˆ(member, owner, restaurant)์€ Flyway๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  JPA `ddl-auto` ์„ค์ • ์‚ฌ์šฉ: - -```yaml -spring: - jpa: - hibernate: - ddl-auto: create-drop # ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ -``` - -**์ด์œ **: - -- `domain-reservation`์€ ์˜ˆ์•ฝ ๋ฐ์ดํ„ฐ์˜ ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ๊ฐ€ ์ค‘์š”ํ•˜์—ฌ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์ถ”์  ํ•„์š” -- ๋‹ค๋ฅธ ๋ชจ๋“ˆ์€ ์ƒ๋Œ€์ ์œผ๋กœ ๋‹จ์ˆœํ•œ CRUD ์ž‘์—… ์œ„์ฃผ - -#### ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ์˜ Flyway - -ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋„ Flyway๊ฐ€ ์ž๋™ ์‹คํ–‰๋˜์–ด ์ผ๊ด€๋œ ์Šคํ‚ค๋งˆ ํ™˜๊ฒฝ ๋ณด์žฅ: - -**domain-reservation/src/test/resources/application-domain-test.yml**: - -```yaml -spring: - flyway: - enabled: true - clean-on-validation-error: true # ํ…Œ์ŠคํŠธ ์‹œ ์Šคํ‚ค๋งˆ ์ดˆ๊ธฐํ™” -``` - ---- - -### AWS MSK (Kafka) ํ†ตํ•ฉ - -#### ๊ฐœ์š” - -`infra-kafka` ๋ชจ๋“ˆ์€ AWS MSK (Managed Streaming for Apache Kafka)์™€ IAM ์ธ์ฆ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ํ†ตํ•ฉ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. - -#### build.gradle ์„ค์ • - -```gradle -dependencies { - implementation 'org.springframework.kafka:spring-kafka' - implementation 'software.amazon.msk:aws-msk-iam-auth:2.2.0' - implementation 'com.amazonaws:aws-java-sdk-kafka:1.12.565' -} -``` - -#### ํŠน์ง• - -**IAM ์ธ์ฆ ์‚ฌ์šฉ**: - -- AWS IAM Role ๊ธฐ๋ฐ˜ ์ธ์ฆ -- Access Key/Secret Key ๋ถˆํ•„์š” -- ECS/EKS์—์„œ Task Role ๋˜๋Š” Pod Identity ํ™œ์šฉ - -**๋ณด์•ˆ**: - -- TLS ์•”ํ˜ธํ™” ํ†ต์‹  -- VPC ๋‚ด๋ถ€ ํ†ต์‹  -- Security Group ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด - -#### application.yml ์„ค์ • (ํ”„๋กœ๋•์…˜) - -```yaml -spring: - kafka: - bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS} - security: - protocol: SASL_SSL - properties: - sasl.mechanism: AWS_MSK_IAM - sasl.jaas.config: software.amazon.msk.auth.iam.IAMLoginModule required; - sasl.client.callback.handler.class: software.amazon.msk.auth.iam.IAMClientCallbackHandler - producer: - key-serializer: org.apache.kafka.common.serialization.StringSerializer - value-serializer: org.springframework.kafka.support.serializer.JsonSerializer - acks: all - retries: 3 -``` - -#### ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ - -ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” EmbeddedKafka ์‚ฌ์šฉ (IAM ์ธ์ฆ ๋ถˆํ•„์š”): - -**application-infra-kafka-test.yml**: - -```yaml -spring: - kafka: - bootstrap-servers: ${spring.embedded.kafka.brokers} - producer: - key-serializer: org.apache.kafka.common.serialization.StringSerializer - value-serializer: org.springframework.kafka.support.serializer.JsonSerializer -``` - -#### ์ฃผ์š” ํ† ํ”ฝ - -- `notification` - ์‚ฌ์šฉ์ž ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ -- `reservation-created` - ์˜ˆ์•ฝ ์ƒ์„ฑ ์ด๋ฒคํŠธ -- `reservation-cancelled` - ์˜ˆ์•ฝ ์ทจ์†Œ ์ด๋ฒคํŠธ -- `reminder` - ๋ฆฌ๋งˆ์ธ๋” ๋ฉ”์‹œ์ง€ (batch-reminder ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉ) - -#### ๋ฉ”์‹œ์ง€ ๊ตฌ์กฐ ์˜ˆ์‹œ - -```json -{ - "header": { - "messageId": "msg-123", - "recipientId": "member-456", - "timestamp": "2025-10-30T12:00:00Z", - "type": "RESERVATION_CREATED" - }, - "payload": { - "reservationId": "reservation-789", - "restaurantName": "๋ง›์ง‘", - "reservationDate": "2025-11-01T19:00:00", - "partySize": 4 - } -} -``` - -#### Producer ์˜ˆ์‹œ - -**infra-kafka/src/main/java/com/wellmeet/kafka/service/KafkaProducerService.java**: - -```java - -@Service -public class KafkaProducerService { - - private final KafkaTemplate kafkaTemplate; - - public void sendNotificationMessage(String memberId, Object payload) { - NotificationMessage message = NotificationMessage.builder() - .header(MessageHeader.builder() - .messageId(UUID.randomUUID().toString()) - .recipientId(memberId) - .timestamp(LocalDateTime.now()) - .build()) - .payload(payload) - .build(); - - kafkaTemplate.send("notification", memberId, message); - } -} -``` - -#### ๋ชจ๋‹ˆํ„ฐ๋ง - -**CloudWatch Metrics**: - -- ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก ์„ฑ๊ณต/์‹คํŒจ์œจ -- ์ง€์—ฐ ์‹œ๊ฐ„ (Latency) -- Consumer Lag - -**Kafka ๋กœ๊ทธ**: - -- Producer ์ „์†ก ๋กœ๊ทธ -- ์žฌ์‹œ๋„ ํšŸ์ˆ˜ -- ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ - ---- - ## ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ **์ „์ฒด ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ**: 75% ์ด์ƒ @@ -2398,10 +284,26 @@ public class KafkaProducerService { --- -**๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ**: 2025-10-30 +**๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ**: 2025-11-06 ## ๋ณ€๊ฒฝ ์ด๋ ฅ +**2025-11-06 (๋ฌธ์„œ ์žฌ๊ตฌ์„ฑ)**: + +- CLAUDE.md ๋Œ€ํญ ๊ฐ„์†Œํ™” (2,682์ค„ โ†’ 300์ค„, 89% ๊ฐ์†Œ) +- 8๊ฐœ ์ƒ์„ธ ๊ฐ€์ด๋“œ ๋ฌธ์„œ ๋ถ„๋ฆฌ (`claudedocs/guides/` ๋””๋ ‰ํ† ๋ฆฌ): + - `naming-conventions.md` (254์ค„) - ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ + - `bff-transaction-strategy.md` (231์ค„) - BFF ํŒจํ„ด ๋ฐ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ + - `local-development.md` (120์ค„) - Docker Compose, Eureka Server + - `test-layer-guide.md` (1,013์ค„) - 8๊ฐœ ํ…Œ์ŠคํŠธ ๋ ˆ์ด์–ด ์ƒ์„ธ + - `module-test-strategies.md` (103์ค„) - ๋ชจ๋“ˆ๋ณ„ ํ…Œ์ŠคํŠธ ์ „๋žต + - `test-writing-rules.md` (141์ค„) - ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๊ทœ์น™ + - `test-infrastructure.md` (306์ค„) - Gradle, testFixtures ์„ค์ • + - `infrastructure-integration.md` (214์ค„) - Flyway, AWS MSK ํ†ตํ•ฉ +- ๋ฌธ์„œ ๊ฐ„ ๋งํฌ ๊ตฌ์กฐ ๊ฐœ์„  (๋ฉ”์ธ ๋ฌธ์„œ โ†’ ์ƒ์„ธ ๊ฐ€์ด๋“œ) +- ๊ฐ ๊ฐ€์ด๋“œ ๋ฌธ์„œ์— ๋…๋ฆฝ์ ์ธ ๋ชฉ์ฐจ(TOC) ์ถ”๊ฐ€ +- Claude ์ปจํ…์ŠคํŠธ ๋กœ๋”ฉ ํšจ์œจ ๊ฐœ์„  (~25,000 ํ† ํฐ ์ ˆ์•ฝ) + **2025-10-30 (Phase 1 ์ธํ”„๋ผ ์™„๋ฃŒ)**: - discovery-server ๋ชจ๋“ˆ ์ถ”๊ฐ€ (Eureka Server, Spring Cloud 2025.0.0) diff --git a/api-owner/build.gradle b/api-owner/build.gradle index 120d923..0c883b6 100644 --- a/api-owner/build.gradle +++ b/api-owner/build.gradle @@ -3,6 +3,9 @@ plugins { } dependencies { + // Common Client (record DTOs) + implementation project(':common-client') + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' diff --git a/api-owner/src/main/java/com/wellmeet/client/MemberClient.java b/api-owner/src/main/java/com/wellmeet/client/MemberFeignClient.java similarity index 81% rename from api-owner/src/main/java/com/wellmeet/client/MemberClient.java rename to api-owner/src/main/java/com/wellmeet/client/MemberFeignClient.java index ed81357..4838aa3 100644 --- a/api-owner/src/main/java/com/wellmeet/client/MemberClient.java +++ b/api-owner/src/main/java/com/wellmeet/client/MemberFeignClient.java @@ -1,7 +1,7 @@ package com.wellmeet.client; -import com.wellmeet.client.dto.MemberDTO; -import com.wellmeet.client.dto.request.MemberIdsRequest; +import com.wellmeet.common.dto.MemberDTO; +import com.wellmeet.common.dto.request.MemberIdsRequest; import java.util.List; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.RequestBody; @FeignClient(name = "domain-member-service") -public interface MemberClient { +public interface MemberFeignClient { @GetMapping("/api/members/{id}") MemberDTO getMember(@PathVariable("id") String id); diff --git a/api-owner/src/main/java/com/wellmeet/client/OwnerClient.java b/api-owner/src/main/java/com/wellmeet/client/OwnerFeignClient.java similarity index 81% rename from api-owner/src/main/java/com/wellmeet/client/OwnerClient.java rename to api-owner/src/main/java/com/wellmeet/client/OwnerFeignClient.java index 0ee5cd4..d3961bb 100644 --- a/api-owner/src/main/java/com/wellmeet/client/OwnerClient.java +++ b/api-owner/src/main/java/com/wellmeet/client/OwnerFeignClient.java @@ -1,12 +1,12 @@ package com.wellmeet.client; -import com.wellmeet.client.dto.OwnerDTO; +import com.wellmeet.common.dto.OwnerDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "domain-owner-service") -public interface OwnerClient { +public interface OwnerFeignClient { @GetMapping("/api/owners/{id}") OwnerDTO getOwner(@PathVariable("id") String id); diff --git a/api-owner/src/main/java/com/wellmeet/client/ReservationClient.java b/api-owner/src/main/java/com/wellmeet/client/ReservationFeignClient.java similarity index 89% rename from api-owner/src/main/java/com/wellmeet/client/ReservationClient.java rename to api-owner/src/main/java/com/wellmeet/client/ReservationFeignClient.java index 38378f5..a6ebe2e 100644 --- a/api-owner/src/main/java/com/wellmeet/client/ReservationClient.java +++ b/api-owner/src/main/java/com/wellmeet/client/ReservationFeignClient.java @@ -1,6 +1,6 @@ package com.wellmeet.client; -import com.wellmeet.client.dto.ReservationDTO; +import com.wellmeet.common.dto.ReservationDTO; import java.util.List; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.PutMapping; @FeignClient(name = "domain-reservation-service") -public interface ReservationClient { +public interface ReservationFeignClient { @GetMapping("/api/reservations/{id}") ReservationDTO getReservation(@PathVariable("id") Long id); diff --git a/api-owner/src/main/java/com/wellmeet/client/RestaurantClient.java b/api-owner/src/main/java/com/wellmeet/client/RestaurantFeignClient.java similarity index 81% rename from api-owner/src/main/java/com/wellmeet/client/RestaurantClient.java rename to api-owner/src/main/java/com/wellmeet/client/RestaurantFeignClient.java index 6826de6..72702b1 100644 --- a/api-owner/src/main/java/com/wellmeet/client/RestaurantClient.java +++ b/api-owner/src/main/java/com/wellmeet/client/RestaurantFeignClient.java @@ -1,10 +1,10 @@ package com.wellmeet.client; -import com.wellmeet.client.dto.AvailableDateDTO; -import com.wellmeet.client.dto.BusinessHourDTO; -import com.wellmeet.client.dto.RestaurantDTO; -import com.wellmeet.client.dto.request.UpdateOperatingHoursDTO; -import com.wellmeet.client.dto.request.UpdateRestaurantDTO; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.BusinessHourDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.UpdateOperatingHoursDTO; +import com.wellmeet.common.dto.request.UpdateRestaurantDTO; import java.util.List; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestBody; @FeignClient(name = "domain-restaurant-service") -public interface RestaurantClient { +public interface RestaurantFeignClient { @GetMapping("/api/restaurants/{id}") RestaurantDTO getRestaurant(@PathVariable("id") String id); diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/AvailableDateDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/AvailableDateDTO.java deleted file mode 100644 index 50b7606..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/AvailableDateDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.wellmeet.client.dto; - -import java.time.LocalDate; -import java.time.LocalTime; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class AvailableDateDTO { - - private Long id; - private LocalDate date; - private LocalTime time; - private int maxCapacity; - private boolean isAvailable; - private String restaurantId; -} diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/BusinessHourDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/BusinessHourDTO.java deleted file mode 100644 index 44b1ad3..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/BusinessHourDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.wellmeet.client.dto; - -import java.time.LocalTime; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class BusinessHourDTO { - - private Long id; - private String dayOfWeek; - private boolean isOperating; - private LocalTime open; - private LocalTime close; - private LocalTime breakStart; - private LocalTime breakEnd; -} \ No newline at end of file diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/MemberDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/MemberDTO.java deleted file mode 100644 index 8e4d2a7..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/MemberDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.wellmeet.client.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class MemberDTO { - - private String id; - private String name; - private String nickname; - private String email; - private String phone; - private boolean reservationEnabled; - private boolean remindEnabled; - private boolean reviewEnabled; - private boolean isVip; -} diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/OwnerDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/OwnerDTO.java deleted file mode 100644 index c414fad..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/OwnerDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.wellmeet.client.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class OwnerDTO { - - private String id; - private String name; - private String email; - private boolean reservationEnabled; - private boolean reviewEnabled; -} diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/ReservationDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/ReservationDTO.java deleted file mode 100644 index b295e80..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/ReservationDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.wellmeet.client.dto; - -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ReservationDTO { - - private Long id; - private String status; - private String restaurantId; - private Long availableDateId; - private String memberId; - private int partySize; - private String specialRequest; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; -} diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/RestaurantDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/RestaurantDTO.java deleted file mode 100644 index 2644c33..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/RestaurantDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.wellmeet.client.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class RestaurantDTO { - - private String id; - private String name; - private String address; - private double latitude; - private double longitude; - private String thumbnail; - private String ownerId; -} diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/request/MemberIdsRequest.java b/api-owner/src/main/java/com/wellmeet/client/dto/request/MemberIdsRequest.java deleted file mode 100644 index d6bb9b1..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/request/MemberIdsRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.wellmeet.client.dto.request; - -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class MemberIdsRequest { - - private List memberIds; -} diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/request/UpdateOperatingHoursDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/request/UpdateOperatingHoursDTO.java deleted file mode 100644 index 23c2fea..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/request/UpdateOperatingHoursDTO.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.wellmeet.client.dto.request; - -import java.time.LocalTime; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class UpdateOperatingHoursDTO { - - private List operatingHours; - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class DayHoursDTO { - private String dayOfWeek; - private boolean isOperating; - private LocalTime open; - private LocalTime close; - private LocalTime breakStart; - private LocalTime breakEnd; - } -} \ No newline at end of file diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/request/UpdateRestaurantDTO.java b/api-owner/src/main/java/com/wellmeet/client/dto/request/UpdateRestaurantDTO.java deleted file mode 100644 index 351c6e2..0000000 --- a/api-owner/src/main/java/com/wellmeet/client/dto/request/UpdateRestaurantDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.wellmeet.client.dto.request; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class UpdateRestaurantDTO { - - private String name; - private String address; - private double latitude; - private double longitude; - private String thumbnail; -} diff --git a/api-owner/src/main/java/com/wellmeet/global/event/EventPublishService.java b/api-owner/src/main/java/com/wellmeet/global/event/OwnerEventPublishBffService.java similarity index 94% rename from api-owner/src/main/java/com/wellmeet/global/event/EventPublishService.java rename to api-owner/src/main/java/com/wellmeet/global/event/OwnerEventPublishBffService.java index 5772b30..0363f32 100644 --- a/api-owner/src/main/java/com/wellmeet/global/event/EventPublishService.java +++ b/api-owner/src/main/java/com/wellmeet/global/event/OwnerEventPublishBffService.java @@ -8,7 +8,7 @@ @Service @RequiredArgsConstructor -public class EventPublishService { +public class OwnerEventPublishBffService { private final ApplicationEventPublisher eventPublisher; diff --git a/api-owner/src/main/java/com/wellmeet/global/event/event/ReservationConfirmedEvent.java b/api-owner/src/main/java/com/wellmeet/global/event/event/ReservationConfirmedEvent.java index 48a2227..b8397b4 100644 --- a/api-owner/src/main/java/com/wellmeet/global/event/event/ReservationConfirmedEvent.java +++ b/api-owner/src/main/java/com/wellmeet/global/event/event/ReservationConfirmedEvent.java @@ -1,6 +1,6 @@ package com.wellmeet.global.event.event; -import com.wellmeet.client.dto.ReservationDTO; +import com.wellmeet.common.dto.ReservationDTO; import java.time.LocalDateTime; import lombok.Getter; @@ -19,15 +19,15 @@ public class ReservationConfirmedEvent { private final LocalDateTime createdAt; public ReservationConfirmedEvent(ReservationDTO reservation, String memberName, String restaurantName, LocalDateTime dateTime) { - this.reservationId = reservation.getId(); - this.memberId = reservation.getMemberId(); + this.reservationId = reservation.id(); + this.memberId = reservation.memberId(); this.memberName = memberName; - this.restaurantId = reservation.getRestaurantId(); + this.restaurantId = reservation.restaurantId(); this.restaurantName = restaurantName; - this.status = reservation.getStatus(); - this.partySize = reservation.getPartySize(); - this.specialRequest = reservation.getSpecialRequest(); + this.status = reservation.status().name(); + this.partySize = reservation.partySize(); + this.specialRequest = reservation.specialRequest(); this.dateTime = dateTime; - this.createdAt = reservation.getCreatedAt(); + this.createdAt = reservation.createdAt(); } } diff --git a/api-owner/src/main/java/com/wellmeet/reservation/ReservationController.java b/api-owner/src/main/java/com/wellmeet/reservation/OwnerReservationBffController.java similarity index 91% rename from api-owner/src/main/java/com/wellmeet/reservation/ReservationController.java rename to api-owner/src/main/java/com/wellmeet/reservation/OwnerReservationBffController.java index 48f46a6..24b8c77 100644 --- a/api-owner/src/main/java/com/wellmeet/reservation/ReservationController.java +++ b/api-owner/src/main/java/com/wellmeet/reservation/OwnerReservationBffController.java @@ -13,9 +13,9 @@ @RequestMapping("/owner/reservation") @RestController @RequiredArgsConstructor -public class ReservationController { +public class OwnerReservationBffController { - private final ReservationService reservationService; + private final OwnerReservationBffService reservationService; @GetMapping("/{restaurantId}") public List getReservations( diff --git a/api-owner/src/main/java/com/wellmeet/reservation/ReservationService.java b/api-owner/src/main/java/com/wellmeet/reservation/OwnerReservationBffService.java similarity index 58% rename from api-owner/src/main/java/com/wellmeet/reservation/ReservationService.java rename to api-owner/src/main/java/com/wellmeet/reservation/OwnerReservationBffService.java index f495d19..7299dac 100644 --- a/api-owner/src/main/java/com/wellmeet/reservation/ReservationService.java +++ b/api-owner/src/main/java/com/wellmeet/reservation/OwnerReservationBffService.java @@ -1,14 +1,14 @@ package com.wellmeet.reservation; -import com.wellmeet.client.MemberClient; -import com.wellmeet.client.ReservationClient; -import com.wellmeet.client.RestaurantClient; -import com.wellmeet.client.dto.AvailableDateDTO; -import com.wellmeet.client.dto.MemberDTO; -import com.wellmeet.client.dto.ReservationDTO; -import com.wellmeet.client.dto.RestaurantDTO; -import com.wellmeet.client.dto.request.MemberIdsRequest; -import com.wellmeet.global.event.EventPublishService; +import com.wellmeet.client.MemberFeignClient; +import com.wellmeet.client.ReservationFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.MemberDTO; +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.MemberIdsRequest; +import com.wellmeet.global.event.OwnerEventPublishBffService; import com.wellmeet.global.event.event.ReservationConfirmedEvent; import com.wellmeet.reservation.dto.ReservationResponse; import java.time.LocalDateTime; @@ -22,12 +22,12 @@ @Service @RequiredArgsConstructor -public class ReservationService { +public class OwnerReservationBffService { - private final ReservationClient reservationClient; - private final MemberClient memberClient; - private final RestaurantClient restaurantClient; - private final EventPublishService eventPublishService; + private final ReservationFeignClient reservationClient; + private final MemberFeignClient memberClient; + private final RestaurantFeignClient restaurantClient; + private final OwnerEventPublishBffService eventPublishService; @Transactional(readOnly = true) public List getReservations(String restaurantId) { @@ -37,25 +37,25 @@ public List getReservations(String restaurantId) { } List memberIds = reservations.stream() - .map(ReservationDTO::getMemberId) + .map(ReservationDTO::memberId) .distinct() .toList(); Map membersById = memberClient.getMembersByIds( - MemberIdsRequest.builder().memberIds(memberIds).build()) + new MemberIdsRequest(memberIds)) .stream() - .collect(Collectors.toMap(MemberDTO::getId, Function.identity())); + .collect(Collectors.toMap(MemberDTO::id, Function.identity())); return reservations.stream() .map(reservation -> { - MemberDTO member = membersById.get(reservation.getMemberId()); + MemberDTO member = membersById.get(reservation.memberId()); AvailableDateDTO availableDate = restaurantClient.getAvailableDate( - reservation.getRestaurantId(), reservation.getAvailableDateId()); + reservation.restaurantId(), reservation.availableDateId()); return new ReservationResponse( reservation, availableDate, - member.getName(), - member.getPhone(), - member.getEmail(), + member.name(), + member.phone(), + member.email(), member.isVip() ); }) @@ -67,13 +67,13 @@ public void confirmReservation(Long reservationId) { reservationClient.confirmReservation(reservationId); ReservationDTO reservation = reservationClient.getReservation(reservationId); - MemberDTO member = memberClient.getMember(reservation.getMemberId()); - RestaurantDTO restaurant = restaurantClient.getRestaurant(reservation.getRestaurantId()); + MemberDTO member = memberClient.getMember(reservation.memberId()); + RestaurantDTO restaurant = restaurantClient.getRestaurant(reservation.restaurantId()); AvailableDateDTO availableDate = restaurantClient.getAvailableDate( - reservation.getRestaurantId(), reservation.getAvailableDateId()); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); + reservation.restaurantId(), reservation.availableDateId()); + LocalDateTime dateTime = LocalDateTime.of(availableDate.date(), availableDate.time()); ReservationConfirmedEvent event = new ReservationConfirmedEvent( - reservation, member.getName(), restaurant.getName(), dateTime); + reservation, member.name(), restaurant.name(), dateTime); eventPublishService.publishReservationConfirmedEvent(event); } } diff --git a/api-owner/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java b/api-owner/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java index 4072f99..b9f4ec0 100644 --- a/api-owner/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java +++ b/api-owner/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java @@ -1,7 +1,7 @@ package com.wellmeet.reservation.dto; -import com.wellmeet.client.dto.AvailableDateDTO; -import com.wellmeet.client.dto.ReservationDTO; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.ReservationDTO; import com.wellmeet.restaurant.dto.ReservationStatus; import java.time.LocalDate; import java.time.LocalDateTime; @@ -28,22 +28,22 @@ public class ReservationResponse { public ReservationResponse(ReservationDTO reservation, AvailableDateDTO availableDate, String memberName, String memberPhone, String memberEmail, boolean memberVip) { CustomerSummaryResponse customerResponse = new CustomerSummaryResponse( - reservation.getMemberId(), + reservation.memberId(), memberName, memberPhone, memberEmail, memberVip ); - this.id = reservation.getId(); + this.id = reservation.id(); this.customer = customerResponse; - this.date = availableDate.getDate(); - this.time = availableDate.getTime(); - this.party = reservation.getPartySize(); - this.status = ReservationStatus.valueOf(reservation.getStatus()); - this.note = reservation.getSpecialRequest(); - this.createdAt = reservation.getCreatedAt(); - this.updatedAt = reservation.getUpdatedAt(); + this.date = availableDate.date(); + this.time = availableDate.time(); + this.party = reservation.partySize(); + this.status = ReservationStatus.valueOf(reservation.status().name()); + this.note = reservation.specialRequest(); + this.createdAt = reservation.createdAt(); + this.updatedAt = reservation.updatedAt(); } @Getter diff --git a/api-owner/src/main/java/com/wellmeet/restaurant/RestaurantController.java b/api-owner/src/main/java/com/wellmeet/restaurant/OwnerRestaurantBffController.java similarity index 94% rename from api-owner/src/main/java/com/wellmeet/restaurant/RestaurantController.java rename to api-owner/src/main/java/com/wellmeet/restaurant/OwnerRestaurantBffController.java index 65aff8d..8fb00aa 100644 --- a/api-owner/src/main/java/com/wellmeet/restaurant/RestaurantController.java +++ b/api-owner/src/main/java/com/wellmeet/restaurant/OwnerRestaurantBffController.java @@ -17,9 +17,9 @@ @RequestMapping("/owner/restaurant") @RestController @RequiredArgsConstructor -public class RestaurantController { +public class OwnerRestaurantBffController { - private final RestaurantService restaurantService; + private final OwnerRestaurantBffService restaurantService; @GetMapping("/{restaurantId}/operating-hours") public OperatingHoursResponse getOperatingHours( diff --git a/api-owner/src/main/java/com/wellmeet/restaurant/RestaurantService.java b/api-owner/src/main/java/com/wellmeet/restaurant/OwnerRestaurantBffService.java similarity index 50% rename from api-owner/src/main/java/com/wellmeet/restaurant/RestaurantService.java rename to api-owner/src/main/java/com/wellmeet/restaurant/OwnerRestaurantBffService.java index 9f274da..59d88c3 100644 --- a/api-owner/src/main/java/com/wellmeet/restaurant/RestaurantService.java +++ b/api-owner/src/main/java/com/wellmeet/restaurant/OwnerRestaurantBffService.java @@ -1,11 +1,11 @@ package com.wellmeet.restaurant; -import com.wellmeet.client.RestaurantClient; -import com.wellmeet.client.dto.BusinessHourDTO; -import com.wellmeet.client.dto.RestaurantDTO; -import com.wellmeet.client.dto.request.UpdateOperatingHoursDTO; -import com.wellmeet.client.dto.request.UpdateRestaurantDTO; -import com.wellmeet.global.event.EventPublishService; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.common.dto.BusinessHourDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.UpdateOperatingHoursDTO; +import com.wellmeet.common.dto.request.UpdateRestaurantDTO; +import com.wellmeet.global.event.OwnerEventPublishBffService; import com.wellmeet.global.event.event.RestaurantUpdatedEvent; import com.wellmeet.restaurant.dto.OperatingHoursResponse; import com.wellmeet.restaurant.dto.UpdateOperatingHoursRequest; @@ -18,10 +18,10 @@ @Service @RequiredArgsConstructor -public class RestaurantService { +public class OwnerRestaurantBffService { - private final RestaurantClient restaurantClient; - private final EventPublishService eventPublishService; + private final RestaurantFeignClient restaurantClient; + private final OwnerEventPublishBffService eventPublishService; @Transactional(readOnly = true) public OperatingHoursResponse getOperatingHours(String restaurantId) { @@ -36,19 +36,17 @@ public OperatingHoursResponse updateOperatingHours( ) { List dayHoursList = request.getOperatingHours() .stream() - .map(dayHours -> UpdateOperatingHoursDTO.DayHoursDTO.builder() - .dayOfWeek(dayHours.getDayOfWeek().name()) - .isOperating(dayHours.isOperating()) - .open(dayHours.getOpen()) - .close(dayHours.getClose()) - .breakStart(dayHours.getBreakStart()) - .breakEnd(dayHours.getBreakEnd()) - .build()) + .map(dayHours -> new UpdateOperatingHoursDTO.DayHoursDTO( + dayHours.getDayOfWeek().name(), + dayHours.isOperating(), + dayHours.getOpen(), + dayHours.getClose(), + dayHours.getBreakStart(), + dayHours.getBreakEnd() + )) .toList(); - UpdateOperatingHoursDTO updateDTO = UpdateOperatingHoursDTO.builder() - .operatingHours(dayHoursList) - .build(); + UpdateOperatingHoursDTO updateDTO = new UpdateOperatingHoursDTO(dayHoursList); List businessHours = restaurantClient.updateOperatingHours(restaurantId, updateDTO); return new OperatingHoursResponse(businessHours); @@ -56,17 +54,17 @@ public OperatingHoursResponse updateOperatingHours( @Transactional public UpdateRestaurantResponse updateRestaurant(String restaurantId, UpdateRestaurantRequest request) { - UpdateRestaurantDTO updateDTO = UpdateRestaurantDTO.builder() - .name(request.getName()) - .address(request.getAddress()) - .latitude(request.getLatitude()) - .longitude(request.getLongitude()) - .thumbnail(request.getThumbnail()) - .build(); + UpdateRestaurantDTO updateDTO = new UpdateRestaurantDTO( + request.getName(), + request.getAddress(), + request.getLatitude(), + request.getLongitude(), + request.getThumbnail() + ); RestaurantDTO restaurant = restaurantClient.updateRestaurant(restaurantId, updateDTO); eventPublishService.publishRestaurantUpdatedEvent(new RestaurantUpdatedEvent(restaurantId)); - return new UpdateRestaurantResponse(restaurant.getName(), restaurant.getAddress(), restaurant.getLatitude(), - restaurant.getLongitude(), restaurant.getThumbnail()); + return new UpdateRestaurantResponse(restaurant.name(), restaurant.address(), restaurant.latitude(), + restaurant.longitude(), restaurant.thumbnail()); } } diff --git a/api-owner/src/main/java/com/wellmeet/restaurant/dto/OperatingHoursResponse.java b/api-owner/src/main/java/com/wellmeet/restaurant/dto/OperatingHoursResponse.java index 403966b..de8c842 100644 --- a/api-owner/src/main/java/com/wellmeet/restaurant/dto/OperatingHoursResponse.java +++ b/api-owner/src/main/java/com/wellmeet/restaurant/dto/OperatingHoursResponse.java @@ -1,6 +1,6 @@ package com.wellmeet.restaurant.dto; -import com.wellmeet.client.dto.BusinessHourDTO; +import com.wellmeet.common.dto.BusinessHourDTO; import com.wellmeet.reservation.dto.DayOfWeek; import java.time.LocalTime; import java.util.List; @@ -30,10 +30,10 @@ public static class DayHours { private BreakTime breakTime; public DayHours(BusinessHourDTO dto) { - this.dayOfWeek = DayOfWeek.valueOf(dto.getDayOfWeek()); - this.open = dto.getOpen(); - this.close = dto.getClose(); - this.operating = dto.isOperating(); + this.dayOfWeek = DayOfWeek.valueOf(dto.dayOfWeek().name()); + this.open = dto.openTime(); + this.close = dto.closeTime(); + this.operating = dto.isOpen(); this.breakTime = new BreakTime(dto); } } @@ -46,8 +46,8 @@ public static class BreakTime { private LocalTime end; public BreakTime(BusinessHourDTO dto) { - this.start = dto.getBreakStart(); - this.end = dto.getBreakEnd(); + this.start = dto.breakStartTime(); + this.end = dto.breakEndTime(); } } } diff --git a/api-owner/src/main/resources/application-dev.yml b/api-owner/src/main/resources/application-dev.yml index acbf11f..ce804be 100644 --- a/api-owner/src/main/resources/application-dev.yml +++ b/api-owner/src/main/resources/application-dev.yml @@ -1,23 +1,46 @@ -server: - port: 8087 - spring: application: name: api-owner-service - config: import: - classpath:dev-secret.yml - - classpath:application-domain-dev.yml - classpath:application-infra-redis-dev.yml - classpath:application-infra-kafka-dev.yml + jackson: + time-zone: Asia/Seoul + serialization: + write-dates-as-timestamps: false + +server: + port: 8087 + shutdown: graceful + eureka: client: + enabled: true service-url: - defaultZone: http://localhost:8761/eureka/ - fetch-registry: true + defaultZone: ${secret.eureka.server-url} register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} + lease-renewal-interval-in-seconds: 30 + lease-expiration-duration-in-seconds: 90 + +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + enabled: true feign: client: @@ -29,3 +52,12 @@ feign: cors: origin: ${secret.cors.origin} + +logging: + level: + root: INFO + com.wellmeet: DEBUG + org.springframework.web: DEBUG + org.springframework.cloud: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" \ No newline at end of file diff --git a/api-owner/src/test/java/com/wellmeet/reservation/ReservationControllerTest.java b/api-owner/src/test/java/com/wellmeet/reservation/OwnerReservationBffControllerTest.java similarity index 94% rename from api-owner/src/test/java/com/wellmeet/reservation/ReservationControllerTest.java rename to api-owner/src/test/java/com/wellmeet/reservation/OwnerReservationBffControllerTest.java index 908a4fd..5f7ac36 100644 --- a/api-owner/src/test/java/com/wellmeet/reservation/ReservationControllerTest.java +++ b/api-owner/src/test/java/com/wellmeet/reservation/OwnerReservationBffControllerTest.java @@ -15,10 +15,10 @@ import org.junit.jupiter.api.Test; import org.springframework.test.context.bean.override.mockito.MockitoBean; -class ReservationControllerTest extends BaseControllerTest { +class OwnerReservationBffControllerTest extends BaseControllerTest { @MockitoBean - private ReservationService reservationService; + private OwnerReservationBffService reservationService; @Nested class GetReservations { diff --git a/api-owner/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java b/api-owner/src/test/java/com/wellmeet/reservation/OwnerReservationBffServiceTest.java similarity index 64% rename from api-owner/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java rename to api-owner/src/test/java/com/wellmeet/reservation/OwnerReservationBffServiceTest.java index 7fde3c2..74126e5 100644 --- a/api-owner/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java +++ b/api-owner/src/test/java/com/wellmeet/reservation/OwnerReservationBffServiceTest.java @@ -5,15 +5,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.wellmeet.client.MemberClient; -import com.wellmeet.client.ReservationClient; -import com.wellmeet.client.RestaurantClient; -import com.wellmeet.client.dto.AvailableDateDTO; -import com.wellmeet.client.dto.MemberDTO; -import com.wellmeet.client.dto.ReservationDTO; -import com.wellmeet.client.dto.RestaurantDTO; -import com.wellmeet.client.dto.request.MemberIdsRequest; -import com.wellmeet.global.event.EventPublishService; +import com.wellmeet.client.MemberFeignClient; +import com.wellmeet.client.ReservationFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.MemberDTO; +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.MemberIdsRequest; +import com.wellmeet.global.event.OwnerEventPublishBffService; import com.wellmeet.global.event.event.ReservationConfirmedEvent; import com.wellmeet.reservation.dto.ReservationResponse; import java.time.LocalDateTime; @@ -26,22 +26,22 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class ReservationServiceTest { +class OwnerReservationBffServiceTest { @Mock - private ReservationClient reservationClient; + private ReservationFeignClient reservationClient; @Mock - private MemberClient memberClient; + private MemberFeignClient memberClient; @Mock - private RestaurantClient restaurantClient; + private RestaurantFeignClient restaurantClient; @Mock - private EventPublishService eventPublishService; + private OwnerEventPublishBffService eventPublishService; @InjectMocks - private ReservationService reservationService; + private OwnerReservationBffService reservationService; @Nested class GetReservations { @@ -52,9 +52,9 @@ class GetReservations { AvailableDateDTO availableDate = createAvailableDateDTO(1L, LocalDateTime.now(), 10, restaurantId); MemberDTO member1 = createMemberDTO("member-1", "Test"); MemberDTO member2 = createMemberDTO("member-2", "Test2"); - ReservationDTO reservation1 = createReservationDTO(1L, restaurantId, availableDate.getId(), member1.getId(), + ReservationDTO reservation1 = createReservationDTO(1L, restaurantId, availableDate.id(), member1.id(), 4); - ReservationDTO reservation2 = createReservationDTO(2L, restaurantId, availableDate.getId(), member2.getId(), + ReservationDTO reservation2 = createReservationDTO(2L, restaurantId, availableDate.id(), member2.id(), 2); List reservations = List.of(reservation1, reservation2); @@ -62,7 +62,7 @@ class GetReservations { .thenReturn(reservations); when(memberClient.getMembersByIds(any(MemberIdsRequest.class))) .thenReturn(List.of(member1, member2)); - when(restaurantClient.getAvailableDate(restaurantId, availableDate.getId())) + when(restaurantClient.getAvailableDate(restaurantId, availableDate.id())) .thenReturn(availableDate); List expectedReservations = reservationService.getReservations(restaurantId); @@ -107,55 +107,61 @@ class ConfirmReservation { } private RestaurantDTO createRestaurantDTO(String id, String name) { - return RestaurantDTO.builder() - .id(id) - .name(name) - .address("address") - .latitude(37.5) - .longitude(127.0) - .thumbnail("thumbnail") - .ownerId("owner-1") - .build(); + return new RestaurantDTO( + id, + name, + "address", + 37.5, + 127.0, + "thumbnail", + "owner-1", + null, + null + ); } private AvailableDateDTO createAvailableDateDTO(Long id, LocalDateTime dateTime, int capacity, String restaurantId) { - return AvailableDateDTO.builder() - .id(id) - .date(dateTime.toLocalDate()) - .time(dateTime.toLocalTime()) - .maxCapacity(capacity) - .isAvailable(true) - .restaurantId(restaurantId) - .build(); + return new AvailableDateDTO( + id, + dateTime.toLocalDate(), + dateTime.toLocalTime(), + capacity, + true, + restaurantId, + null, + null + ); } private MemberDTO createMemberDTO(String id, String name) { - return MemberDTO.builder() - .id(id) - .name(name) - .nickname("nickname") - .email("email@email.com") - .phone("010-1234-5678") - .reservationEnabled(true) - .remindEnabled(true) - .reviewEnabled(true) - .isVip(false) - .build(); + return new MemberDTO( + id, + name, + "nickname", + "email@email.com", + "010-1234-5678", + true, + true, + true, + false, + null, + null + ); } private ReservationDTO createReservationDTO(Long id, String restaurantId, Long availableDateId, String memberId, int partySize) { - return ReservationDTO.builder() - .id(id) - .status("PENDING") - .restaurantId(restaurantId) - .availableDateId(availableDateId) - .memberId(memberId) - .partySize(partySize) - .specialRequest("request") - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); + return new ReservationDTO( + id, + com.wellmeet.common.dto.ReservationStatus.PENDING, + restaurantId, + memberId, + availableDateId, + partySize, + "request", + LocalDateTime.now(), + LocalDateTime.now() + ); } } diff --git a/api-owner/src/test/java/com/wellmeet/restaurant/RestaurantControllerTest.java b/api-owner/src/test/java/com/wellmeet/restaurant/OwnerRestaurantBffControllerTest.java similarity index 98% rename from api-owner/src/test/java/com/wellmeet/restaurant/RestaurantControllerTest.java rename to api-owner/src/test/java/com/wellmeet/restaurant/OwnerRestaurantBffControllerTest.java index 2dd53eb..140e23f 100644 --- a/api-owner/src/test/java/com/wellmeet/restaurant/RestaurantControllerTest.java +++ b/api-owner/src/test/java/com/wellmeet/restaurant/OwnerRestaurantBffControllerTest.java @@ -18,10 +18,10 @@ import org.junit.jupiter.api.Test; import org.springframework.test.context.bean.override.mockito.MockitoBean; -class RestaurantControllerTest extends BaseControllerTest { +class OwnerRestaurantBffControllerTest extends BaseControllerTest { @MockitoBean - private RestaurantService restaurantService; + private OwnerRestaurantBffService restaurantService; @Nested class GetOperatingHours { diff --git a/api-owner/src/test/java/com/wellmeet/restaurant/RestaurantServiceTest.java b/api-owner/src/test/java/com/wellmeet/restaurant/OwnerRestaurantBffServiceTest.java similarity index 57% rename from api-owner/src/test/java/com/wellmeet/restaurant/RestaurantServiceTest.java rename to api-owner/src/test/java/com/wellmeet/restaurant/OwnerRestaurantBffServiceTest.java index 66bac5e..f6006d1 100644 --- a/api-owner/src/test/java/com/wellmeet/restaurant/RestaurantServiceTest.java +++ b/api-owner/src/test/java/com/wellmeet/restaurant/OwnerRestaurantBffServiceTest.java @@ -6,12 +6,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.wellmeet.client.RestaurantClient; -import com.wellmeet.client.dto.BusinessHourDTO; -import com.wellmeet.client.dto.RestaurantDTO; -import com.wellmeet.client.dto.request.UpdateOperatingHoursDTO; -import com.wellmeet.client.dto.request.UpdateRestaurantDTO; -import com.wellmeet.global.event.EventPublishService; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.common.dto.BusinessHourDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.UpdateOperatingHoursDTO; +import com.wellmeet.common.dto.request.UpdateRestaurantDTO; +import com.wellmeet.global.event.OwnerEventPublishBffService; import com.wellmeet.global.event.event.RestaurantUpdatedEvent; import com.wellmeet.reservation.dto.DayOfWeek; import com.wellmeet.restaurant.dto.OperatingHoursResponse; @@ -29,16 +29,16 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class RestaurantServiceTest { +class OwnerRestaurantBffServiceTest { @Mock - private RestaurantClient restaurantClient; + private RestaurantFeignClient restaurantClient; @Mock - private EventPublishService eventPublishService; + private OwnerEventPublishBffService eventPublishService; @InjectMocks - private RestaurantService restaurantService; + private OwnerRestaurantBffService restaurantService; @Nested class GetOperatingHours { @@ -114,15 +114,17 @@ class UpdateRestaurant { "new-thumbnail.jpg" ); - RestaurantDTO restaurantDTO = RestaurantDTO.builder() - .id(restaurantId) - .name("์ˆ˜์ •๋œ ์‹๋‹น") - .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ") - .latitude(37.5) - .longitude(127.0) - .thumbnail("new-thumbnail.jpg") - .ownerId("owner-1") - .build(); + RestaurantDTO restaurantDTO = new RestaurantDTO( + restaurantId, + "์ˆ˜์ •๋œ ์‹๋‹น", + "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", + 37.5, + 127.0, + "new-thumbnail.jpg", + "owner-1", + null, + null + ); when(restaurantClient.updateRestaurant(eq(restaurantId), any(UpdateRestaurantDTO.class))) .thenReturn(restaurantDTO); @@ -148,15 +150,17 @@ class UpdateRestaurant { "new-thumbnail.jpg" ); - RestaurantDTO restaurantDTO = RestaurantDTO.builder() - .id(restaurantId) - .name("์ˆ˜์ •๋œ ์‹๋‹น") - .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ") - .latitude(37.5) - .longitude(127.0) - .thumbnail("new-thumbnail.jpg") - .ownerId("owner-1") - .build(); + RestaurantDTO restaurantDTO = new RestaurantDTO( + restaurantId, + "์ˆ˜์ •๋œ ์‹๋‹น", + "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", + 37.5, + 127.0, + "new-thumbnail.jpg", + "owner-1", + null, + null + ); when(restaurantClient.updateRestaurant(eq(restaurantId), any(UpdateRestaurantDTO.class))) .thenReturn(restaurantDTO); @@ -169,69 +173,25 @@ class UpdateRestaurant { private List createBusinessHourDTOList() { return List.of( - BusinessHourDTO.builder() - .id(1L) - .dayOfWeek("MONDAY") - .isOperating(true) - .open(LocalTime.of(9, 0)) - .close(LocalTime.of(22, 0)) - .breakStart(LocalTime.of(15, 0)) - .breakEnd(LocalTime.of(17, 0)) - .build(), - BusinessHourDTO.builder() - .id(2L) - .dayOfWeek("TUESDAY") - .isOperating(true) - .open(LocalTime.of(9, 0)) - .close(LocalTime.of(22, 0)) - .breakStart(LocalTime.of(15, 0)) - .breakEnd(LocalTime.of(17, 0)) - .build(), - BusinessHourDTO.builder() - .id(3L) - .dayOfWeek("WEDNESDAY") - .isOperating(true) - .open(LocalTime.of(9, 0)) - .close(LocalTime.of(22, 0)) - .breakStart(LocalTime.of(15, 0)) - .breakEnd(LocalTime.of(17, 0)) - .build(), - BusinessHourDTO.builder() - .id(4L) - .dayOfWeek("THURSDAY") - .isOperating(true) - .open(LocalTime.of(9, 0)) - .close(LocalTime.of(22, 0)) - .breakStart(LocalTime.of(15, 0)) - .breakEnd(LocalTime.of(17, 0)) - .build(), - BusinessHourDTO.builder() - .id(5L) - .dayOfWeek("FRIDAY") - .isOperating(true) - .open(LocalTime.of(9, 0)) - .close(LocalTime.of(22, 0)) - .breakStart(LocalTime.of(15, 0)) - .breakEnd(LocalTime.of(17, 0)) - .build(), - BusinessHourDTO.builder() - .id(6L) - .dayOfWeek("SATURDAY") - .isOperating(false) - .open(null) - .close(null) - .breakStart(null) - .breakEnd(null) - .build(), - BusinessHourDTO.builder() - .id(7L) - .dayOfWeek("SUNDAY") - .isOperating(false) - .open(null) - .close(null) - .breakStart(null) - .breakEnd(null) - .build() + new BusinessHourDTO(1L, java.time.DayOfWeek.MONDAY, true, + LocalTime.of(9, 0), LocalTime.of(22, 0), + LocalTime.of(15, 0), LocalTime.of(17, 0), null, null, null), + new BusinessHourDTO(2L, java.time.DayOfWeek.TUESDAY, true, + LocalTime.of(9, 0), LocalTime.of(22, 0), + LocalTime.of(15, 0), LocalTime.of(17, 0), null, null, null), + new BusinessHourDTO(3L, java.time.DayOfWeek.WEDNESDAY, true, + LocalTime.of(9, 0), LocalTime.of(22, 0), + LocalTime.of(15, 0), LocalTime.of(17, 0), null, null, null), + new BusinessHourDTO(4L, java.time.DayOfWeek.THURSDAY, true, + LocalTime.of(9, 0), LocalTime.of(22, 0), + LocalTime.of(15, 0), LocalTime.of(17, 0), null, null, null), + new BusinessHourDTO(5L, java.time.DayOfWeek.FRIDAY, true, + LocalTime.of(9, 0), LocalTime.of(22, 0), + LocalTime.of(15, 0), LocalTime.of(17, 0), null, null, null), + new BusinessHourDTO(6L, java.time.DayOfWeek.SATURDAY, false, + null, null, null, null, null, null, null), + new BusinessHourDTO(7L, java.time.DayOfWeek.SUNDAY, false, + null, null, null, null, null, null, null) ); } } diff --git a/api-user/build.gradle b/api-user/build.gradle index 7be98fc..f2c81f7 100644 --- a/api-user/build.gradle +++ b/api-user/build.gradle @@ -3,11 +3,14 @@ plugins { } dependencies { - implementation project(':domain-common') - implementation project(':domain-reservation') - implementation project(':domain-member') - implementation project(':domain-owner') - implementation project(':domain-restaurant') + // Common Client (record DTOs) + implementation project(':common-client') + + // Spring Cloud OpenFeign ์ถ”๊ฐ€ + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + + // ๊ธฐ์กด ์œ ์ง€ implementation project(':infra-redis') implementation project(':infra-kafka') @@ -15,11 +18,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' - testImplementation(testFixtures(project(':domain-reservation'))) - testImplementation(testFixtures(project(':domain-member'))) - testImplementation(testFixtures(project(':domain-owner'))) - testImplementation(testFixtures(project(':domain-restaurant'))) + // ํ…Œ์ŠคํŠธ ์˜์กด์„ฑ testImplementation 'io.rest-assured:rest-assured' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { diff --git a/api-user/src/main/java/com/wellmeet/ApiUserApplication.java b/api-user/src/main/java/com/wellmeet/ApiUserApplication.java index dd10683..ffbb74d 100644 --- a/api-user/src/main/java/com/wellmeet/ApiUserApplication.java +++ b/api-user/src/main/java/com/wellmeet/ApiUserApplication.java @@ -2,9 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableFeignClients @EnableAsync public class ApiUserApplication { diff --git a/api-user/src/main/java/com/wellmeet/client/MemberFavoriteRestaurantFeignClient.java b/api-user/src/main/java/com/wellmeet/client/MemberFavoriteRestaurantFeignClient.java new file mode 100644 index 0000000..31f053e --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/MemberFavoriteRestaurantFeignClient.java @@ -0,0 +1,26 @@ +package com.wellmeet.client; + +import com.wellmeet.common.dto.FavoriteRestaurantDTO; +import java.util.List; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "domain-member-service", contextId = "favoriteRestaurantClient", path = "/api/favorites") +public interface MemberFavoriteRestaurantFeignClient { + + @GetMapping("/check") + Boolean isFavorite(@RequestParam String memberId, @RequestParam String restaurantId); + + @GetMapping("/members/{memberId}") + List getFavoritesByMemberId(@PathVariable String memberId); + + @PostMapping + FavoriteRestaurantDTO addFavorite(@RequestParam String memberId, @RequestParam String restaurantId); + + @DeleteMapping + void removeFavorite(@RequestParam String memberId, @RequestParam String restaurantId); +} diff --git a/api-user/src/main/java/com/wellmeet/client/MemberFeignClient.java b/api-user/src/main/java/com/wellmeet/client/MemberFeignClient.java new file mode 100644 index 0000000..cc48190 --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/MemberFeignClient.java @@ -0,0 +1,20 @@ +package com.wellmeet.client; + +import com.wellmeet.common.dto.MemberDTO; +import com.wellmeet.common.dto.request.MemberIdsRequest; +import java.util.List; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "domain-member-service", contextId = "memberClient") +public interface MemberFeignClient { + + @GetMapping("/api/members/{id}") + MemberDTO getMember(@PathVariable("id") String id); + + @PostMapping("/api/members/batch") + List getMembersByIds(@RequestBody MemberIdsRequest request); +} diff --git a/api-user/src/main/java/com/wellmeet/client/OwnerFeignClient.java b/api-user/src/main/java/com/wellmeet/client/OwnerFeignClient.java new file mode 100644 index 0000000..d3961bb --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/OwnerFeignClient.java @@ -0,0 +1,13 @@ +package com.wellmeet.client; + +import com.wellmeet.common.dto.OwnerDTO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "domain-owner-service") +public interface OwnerFeignClient { + + @GetMapping("/api/owners/{id}") + OwnerDTO getOwner(@PathVariable("id") String id); +} diff --git a/api-user/src/main/java/com/wellmeet/client/ReservationFeignClient.java b/api-user/src/main/java/com/wellmeet/client/ReservationFeignClient.java new file mode 100644 index 0000000..511ab3c --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/ReservationFeignClient.java @@ -0,0 +1,38 @@ +package com.wellmeet.client; + +import com.wellmeet.client.dto.request.UpdateReservationDTO; +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.request.CreateReservationDTO; +import java.util.List; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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; + +@FeignClient(name = "domain-reservation-service", path = "/api/reservation") +public interface ReservationFeignClient { + + @PostMapping + ReservationDTO createReservation(@RequestBody CreateReservationDTO request); + + @GetMapping("/{id}") + ReservationDTO getReservation(@PathVariable("id") Long id); + + @GetMapping("/restaurant/{restaurantId}") + List getReservationsByRestaurant(@PathVariable("restaurantId") String restaurantId); + + @GetMapping("/member/{memberId}") + List getReservationsByMember(@PathVariable("memberId") String memberId); + + @PutMapping("/{id}") + ReservationDTO updateReservation( + @PathVariable("id") Long id, + @RequestBody UpdateReservationDTO request + ); + + @PatchMapping("/{id}/cancel") + void cancelReservation(@PathVariable("id") Long id); +} diff --git a/api-user/src/main/java/com/wellmeet/client/RestaurantAvailableDateFeignClient.java b/api-user/src/main/java/com/wellmeet/client/RestaurantAvailableDateFeignClient.java new file mode 100644 index 0000000..db05550 --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/RestaurantAvailableDateFeignClient.java @@ -0,0 +1,24 @@ +package com.wellmeet.client; + +import com.wellmeet.client.dto.request.DecreaseCapacityRequest; +import com.wellmeet.client.dto.request.IncreaseCapacityRequest; +import com.wellmeet.common.dto.AvailableDateDTO; +import java.util.List; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "domain-restaurant-service", contextId = "availableDateClient", path = "/api/available-dates") +public interface RestaurantAvailableDateFeignClient { + + @GetMapping("/restaurant/{restaurantId}") + List getAvailableDatesByRestaurant(@PathVariable String restaurantId); + + @PutMapping("/decrease-capacity") + void decreaseCapacity(@RequestBody DecreaseCapacityRequest request); + + @PutMapping("/increase-capacity") + void increaseCapacity(@RequestBody IncreaseCapacityRequest request); +} diff --git a/api-user/src/main/java/com/wellmeet/client/RestaurantFeignClient.java b/api-user/src/main/java/com/wellmeet/client/RestaurantFeignClient.java new file mode 100644 index 0000000..6e1a2c4 --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/RestaurantFeignClient.java @@ -0,0 +1,60 @@ +package com.wellmeet.client; + +import com.wellmeet.client.dto.ReviewDTO; +import com.wellmeet.client.dto.request.RestaurantIdsRequest; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.BusinessHourDTO; +import com.wellmeet.common.dto.MenuDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.UpdateOperatingHoursDTO; +import com.wellmeet.common.dto.request.UpdateRestaurantDTO; +import java.util.List; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "domain-restaurant-service", contextId = "restaurantClient") +public interface RestaurantFeignClient { + + @GetMapping("/api/restaurants/{id}") + RestaurantDTO getRestaurant(@PathVariable("id") String id); + + @GetMapping("/api/restaurants") + List getAllRestaurants(); + + @PostMapping("/api/restaurants/batch") + List getRestaurantsByIds(@RequestBody RestaurantIdsRequest request); + + @GetMapping("/api/restaurants/{restaurantId}/available-dates/{availableDateId}") + AvailableDateDTO getAvailableDate( + @PathVariable("restaurantId") String restaurantId, + @PathVariable("availableDateId") Long availableDateId + ); + + @PutMapping("/api/restaurants/{id}") + RestaurantDTO updateRestaurant( + @PathVariable("id") String id, + @RequestBody UpdateRestaurantDTO request + ); + + @GetMapping("/api/restaurants/{restaurantId}/operating-hours") + List getOperatingHours(@PathVariable("restaurantId") String restaurantId); + + @PutMapping("/api/restaurants/{restaurantId}/operating-hours") + List updateOperatingHours( + @PathVariable("restaurantId") String restaurantId, + @RequestBody UpdateOperatingHoursDTO request + ); + + @GetMapping("/api/reviews/restaurant/{restaurantId}/average-rating") + Double getAverageRating(@PathVariable("restaurantId") String restaurantId); + + @GetMapping("/api/reviews/restaurant/{restaurantId}") + List getReviewsByRestaurant(@PathVariable("restaurantId") String restaurantId); + + @GetMapping("/api/menus/restaurant/{restaurantId}") + List getMenusByRestaurant(@PathVariable("restaurantId") String restaurantId); +} diff --git a/api-user/src/main/java/com/wellmeet/client/dto/ReviewDTO.java b/api-user/src/main/java/com/wellmeet/client/dto/ReviewDTO.java new file mode 100644 index 0000000..6044957 --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/dto/ReviewDTO.java @@ -0,0 +1,11 @@ +package com.wellmeet.client.dto; + +public record ReviewDTO( + Long id, + String content, + double rating, + String situation, + String restaurantId, + String memberId +) { +} diff --git a/api-user/src/main/java/com/wellmeet/client/dto/request/DecreaseCapacityRequest.java b/api-user/src/main/java/com/wellmeet/client/dto/request/DecreaseCapacityRequest.java new file mode 100644 index 0000000..6cfe34d --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/dto/request/DecreaseCapacityRequest.java @@ -0,0 +1,4 @@ +package com.wellmeet.client.dto.request; + +public record DecreaseCapacityRequest(Long availableDateId, int partySize) { +} diff --git a/api-user/src/main/java/com/wellmeet/client/dto/request/IncreaseCapacityRequest.java b/api-user/src/main/java/com/wellmeet/client/dto/request/IncreaseCapacityRequest.java new file mode 100644 index 0000000..b916c6c --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/dto/request/IncreaseCapacityRequest.java @@ -0,0 +1,4 @@ +package com.wellmeet.client.dto.request; + +public record IncreaseCapacityRequest(Long availableDateId, int partySize) { +} diff --git a/api-user/src/main/java/com/wellmeet/client/dto/request/RestaurantIdsRequest.java b/api-user/src/main/java/com/wellmeet/client/dto/request/RestaurantIdsRequest.java new file mode 100644 index 0000000..6f3daff --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/client/dto/request/RestaurantIdsRequest.java @@ -0,0 +1,6 @@ +package com.wellmeet.client.dto.request; + +import java.util.List; + +public record RestaurantIdsRequest(List restaurantIds) { +} diff --git a/api-owner/src/main/java/com/wellmeet/client/dto/request/CreateReservationDTO.java b/api-user/src/main/java/com/wellmeet/client/dto/request/UpdateReservationDTO.java similarity index 76% rename from api-owner/src/main/java/com/wellmeet/client/dto/request/CreateReservationDTO.java rename to api-user/src/main/java/com/wellmeet/client/dto/request/UpdateReservationDTO.java index eed74b5..8c142b8 100644 --- a/api-owner/src/main/java/com/wellmeet/client/dto/request/CreateReservationDTO.java +++ b/api-user/src/main/java/com/wellmeet/client/dto/request/UpdateReservationDTO.java @@ -1,19 +1,16 @@ package com.wellmeet.client.dto.request; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@Builder @NoArgsConstructor @AllArgsConstructor -public class CreateReservationDTO { +public class UpdateReservationDTO { private String restaurantId; private Long availableDateId; - private String memberId; private int partySize; private String specialRequest; } diff --git a/api-user/src/main/java/com/wellmeet/config/FeignConfig.java b/api-user/src/main/java/com/wellmeet/config/FeignConfig.java new file mode 100644 index 0000000..7c562b7 --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/config/FeignConfig.java @@ -0,0 +1,41 @@ +package com.wellmeet.config; + +import feign.Logger; +import feign.Request; +import feign.Retryer; +import feign.codec.ErrorDecoder; +import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FeignConfig { + + @Bean + public Logger.Level feignLoggerLevel() { + return Logger.Level.BASIC; + } + + @Bean + public Request.Options requestOptions() { + return new Request.Options( + 5000, TimeUnit.MILLISECONDS, // connectTimeout + 5000, TimeUnit.MILLISECONDS, // readTimeout + true // followRedirects + ); + } + + @Bean + public Retryer retryer() { + return new Retryer.Default( + 100, // period (์ดˆ๊ธฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„) + 1000, // maxPeriod (์ตœ๋Œ€ ๋Œ€๊ธฐ ์‹œ๊ฐ„) + 3 // maxAttempts (์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜) + ); + } + + @Bean + public ErrorDecoder errorDecoder() { + return new FeignErrorDecoder(); + } +} diff --git a/api-user/src/main/java/com/wellmeet/config/FeignErrorDecoder.java b/api-user/src/main/java/com/wellmeet/config/FeignErrorDecoder.java new file mode 100644 index 0000000..2bd2cca --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/config/FeignErrorDecoder.java @@ -0,0 +1,25 @@ +package com.wellmeet.config; + +import feign.Response; +import feign.codec.ErrorDecoder; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FeignErrorDecoder implements ErrorDecoder { + + private final ErrorDecoder defaultDecoder = new Default(); + + @Override + public Exception decode(String methodKey, Response response) { + log.error("Feign error occurred - method: {}, status: {}, reason: {}", + methodKey, response.status(), response.reason()); + + return switch (response.status()) { + case 400 -> new IllegalArgumentException("์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค: " + response.reason()); + case 404 -> new IllegalArgumentException("์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: " + response.reason()); + case 500 -> new RuntimeException("์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: " + response.reason()); + case 503 -> new RuntimeException("์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: " + response.reason()); + default -> defaultDecoder.decode(methodKey, response); + }; + } +} diff --git a/api-user/src/main/java/com/wellmeet/favorite/FavoriteService.java b/api-user/src/main/java/com/wellmeet/favorite/FavoriteService.java deleted file mode 100644 index 6dfe30f..0000000 --- a/api-user/src/main/java/com/wellmeet/favorite/FavoriteService.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.wellmeet.favorite; - -import com.wellmeet.domain.member.FavoriteRestaurantDomainService; -import com.wellmeet.domain.member.entity.FavoriteRestaurant; -import com.wellmeet.domain.restaurant.RestaurantDomainService; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.domain.restaurant.review.ReviewDomainService; -import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class FavoriteService { - - private final FavoriteRestaurantDomainService favoriteRestaurantDomainService; - private final ReviewDomainService reviewDomainService; - private final RestaurantDomainService restaurantDomainService; - - @Transactional(readOnly = true) - public List getFavoriteRestaurants(String memberId) { - List favoriteRestaurants = favoriteRestaurantDomainService.findAllByMemberId(memberId); - if (favoriteRestaurants.isEmpty()) { - return List.of(); - } - - List restaurantIds = favoriteRestaurants.stream() - .map(FavoriteRestaurant::getRestaurantId) - .toList(); - - Map restaurantsById = restaurantDomainService.findAllByIds(restaurantIds).stream() - .collect(Collectors.toMap(Restaurant::getId, Function.identity())); - - return favoriteRestaurants.stream() - .map(favoriteRestaurant -> { - Restaurant restaurant = restaurantsById.get(favoriteRestaurant.getRestaurantId()); - return getFavoriteRestaurantResponse(restaurant); - }) - .toList(); - } - - private FavoriteRestaurantResponse getFavoriteRestaurantResponse(Restaurant restaurant) { - double rating = reviewDomainService.getAverageRating(restaurant.getId()); - return new FavoriteRestaurantResponse(restaurant, rating); - } - - @Transactional - public FavoriteRestaurantResponse addFavoriteRestaurant(String memberId, String restaurantId) { - Restaurant restaurant = restaurantDomainService.getById(restaurantId); - FavoriteRestaurant favoriteRestaurant = new FavoriteRestaurant(memberId, restaurantId); - favoriteRestaurantDomainService.save(favoriteRestaurant); - return getFavoriteRestaurantResponse(restaurant); - } - - @Transactional - public void removeFavoriteRestaurant(String memberId, String restaurantId) { - FavoriteRestaurant favoriteRestaurant = favoriteRestaurantDomainService.getByMemberIdAndRestaurantId(memberId, - restaurantId); - favoriteRestaurantDomainService.delete(favoriteRestaurant); - } -} diff --git a/api-user/src/main/java/com/wellmeet/favorite/FavoriteController.java b/api-user/src/main/java/com/wellmeet/favorite/UserFavoriteRestaurantBffController.java similarity index 93% rename from api-user/src/main/java/com/wellmeet/favorite/FavoriteController.java rename to api-user/src/main/java/com/wellmeet/favorite/UserFavoriteRestaurantBffController.java index d7cc8d2..daaf717 100644 --- a/api-user/src/main/java/com/wellmeet/favorite/FavoriteController.java +++ b/api-user/src/main/java/com/wellmeet/favorite/UserFavoriteRestaurantBffController.java @@ -16,9 +16,9 @@ @RequestMapping("/user/favorite") @RestController @RequiredArgsConstructor -public class FavoriteController { +public class UserFavoriteRestaurantBffController { - private final FavoriteService favoriteService; + private final UserFavoriteRestaurantBffService favoriteService; @GetMapping("/restaurant/list") public List getFavoriteRestaurants( diff --git a/api-user/src/main/java/com/wellmeet/favorite/UserFavoriteRestaurantBffService.java b/api-user/src/main/java/com/wellmeet/favorite/UserFavoriteRestaurantBffService.java new file mode 100644 index 0000000..27500a8 --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/favorite/UserFavoriteRestaurantBffService.java @@ -0,0 +1,61 @@ +package com.wellmeet.favorite; + +import com.wellmeet.client.MemberFavoriteRestaurantFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.client.dto.request.RestaurantIdsRequest; +import com.wellmeet.common.dto.FavoriteRestaurantDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserFavoriteRestaurantBffService { + + private final MemberFavoriteRestaurantFeignClient favoriteRestaurantClient; + private final RestaurantFeignClient restaurantClient; + + public List getFavoriteRestaurants(String memberId) { + List favoriteRestaurants = favoriteRestaurantClient.getFavoritesByMemberId(memberId); + if (favoriteRestaurants.isEmpty()) { + return List.of(); + } + + List restaurantIds = favoriteRestaurants.stream() + .map(FavoriteRestaurantDTO::restaurantId) + .toList(); + + Map restaurantsById = restaurantClient + .getRestaurantsByIds(new RestaurantIdsRequest(restaurantIds)) + .stream() + .collect(Collectors.toMap(RestaurantDTO::id, Function.identity())); + + return favoriteRestaurants.stream() + .map(favoriteRestaurant -> { + RestaurantDTO restaurant = restaurantsById.get(favoriteRestaurant.restaurantId()); + return getFavoriteRestaurantResponse(restaurant); + }) + .toList(); + } + + private FavoriteRestaurantResponse getFavoriteRestaurantResponse(RestaurantDTO restaurant) { + Double rating = restaurantClient.getAverageRating(restaurant.id()); + double ratingValue = (rating != null) ? rating : 0.0; + return new FavoriteRestaurantResponse(restaurant, ratingValue); + } + + public FavoriteRestaurantResponse addFavoriteRestaurant(String memberId, String restaurantId) { + RestaurantDTO restaurant = restaurantClient.getRestaurant(restaurantId); + favoriteRestaurantClient.addFavorite(memberId, restaurantId); + return getFavoriteRestaurantResponse(restaurant); + } + + public void removeFavoriteRestaurant(String memberId, String restaurantId) { + favoriteRestaurantClient.removeFavorite(memberId, restaurantId); + } +} diff --git a/api-user/src/main/java/com/wellmeet/favorite/dto/FavoriteRestaurantResponse.java b/api-user/src/main/java/com/wellmeet/favorite/dto/FavoriteRestaurantResponse.java index a9b6007..cdadcff 100644 --- a/api-user/src/main/java/com/wellmeet/favorite/dto/FavoriteRestaurantResponse.java +++ b/api-user/src/main/java/com/wellmeet/favorite/dto/FavoriteRestaurantResponse.java @@ -1,11 +1,13 @@ package com.wellmeet.favorite.dto; -import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.common.dto.RestaurantDTO; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@lombok.Builder +@lombok.AllArgsConstructor public class FavoriteRestaurantResponse { private String id; @@ -14,11 +16,11 @@ public class FavoriteRestaurantResponse { private double rating; private String thumbnail; - public FavoriteRestaurantResponse(Restaurant restaurant, double rating) { - this.id = restaurant.getId(); - this.name = restaurant.getName(); - this.address = restaurant.getAddress(); + public FavoriteRestaurantResponse(RestaurantDTO restaurant, double rating) { + this.id = restaurant.id(); + this.name = restaurant.name(); + this.address = restaurant.address(); this.rating = rating; - this.thumbnail = restaurant.getThumbnail(); + this.thumbnail = restaurant.thumbnail(); } } diff --git a/api-user/src/main/java/com/wellmeet/global/event/EventPublishService.java b/api-user/src/main/java/com/wellmeet/global/event/UserEventPublishBffService.java similarity index 95% rename from api-user/src/main/java/com/wellmeet/global/event/EventPublishService.java rename to api-user/src/main/java/com/wellmeet/global/event/UserEventPublishBffService.java index eee0a46..2e544e6 100644 --- a/api-user/src/main/java/com/wellmeet/global/event/EventPublishService.java +++ b/api-user/src/main/java/com/wellmeet/global/event/UserEventPublishBffService.java @@ -9,7 +9,7 @@ @Service @RequiredArgsConstructor -public class EventPublishService { +public class UserEventPublishBffService { private final ApplicationEventPublisher eventPublisher; diff --git a/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCanceledEvent.java b/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCanceledEvent.java index 5ff26c5..f8f94b5 100644 --- a/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCanceledEvent.java +++ b/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCanceledEvent.java @@ -1,6 +1,6 @@ package com.wellmeet.global.event.event; -import com.wellmeet.domain.reservation.entity.Reservation; +import com.wellmeet.common.dto.ReservationDTO; import java.time.LocalDateTime; import lombok.Getter; @@ -18,16 +18,16 @@ public class ReservationCanceledEvent { private final LocalDateTime dateTime; private final LocalDateTime createdAt; - public ReservationCanceledEvent(Reservation reservation, String memberName, String restaurantName, LocalDateTime dateTime) { - this.reservationId = reservation.getId(); - this.memberId = reservation.getMemberId(); + public ReservationCanceledEvent(ReservationDTO reservation, String memberName, String restaurantName, LocalDateTime dateTime) { + this.reservationId = reservation.id(); + this.memberId = reservation.memberId(); this.memberName = memberName; - this.restaurantId = reservation.getRestaurantId(); + this.restaurantId = reservation.restaurantId(); this.restaurantName = restaurantName; - this.status = reservation.getStatus().name(); - this.partySize = reservation.getPartySize(); - this.specialRequest = reservation.getSpecialRequest(); + this.status = reservation.status().name(); + this.partySize = reservation.partySize(); + this.specialRequest = reservation.specialRequest(); this.dateTime = dateTime; - this.createdAt = reservation.getCreatedAt(); + this.createdAt = reservation.createdAt(); } } diff --git a/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCreatedEvent.java b/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCreatedEvent.java index 25a8a08..7f82893 100644 --- a/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCreatedEvent.java +++ b/api-user/src/main/java/com/wellmeet/global/event/event/ReservationCreatedEvent.java @@ -1,6 +1,6 @@ package com.wellmeet.global.event.event; -import com.wellmeet.domain.reservation.entity.Reservation; +import com.wellmeet.common.dto.ReservationDTO; import java.time.LocalDateTime; import lombok.Getter; @@ -18,16 +18,16 @@ public class ReservationCreatedEvent { private final LocalDateTime dateTime; private final LocalDateTime createdAt; - public ReservationCreatedEvent(Reservation reservation, String memberName, String restaurantName, LocalDateTime dateTime) { - this.reservationId = reservation.getId(); - this.memberId = reservation.getMemberId(); + public ReservationCreatedEvent(ReservationDTO reservation, String memberName, String restaurantName, LocalDateTime dateTime) { + this.reservationId = reservation.id(); + this.memberId = reservation.memberId(); this.memberName = memberName; - this.restaurantId = reservation.getRestaurantId(); + this.restaurantId = reservation.restaurantId(); this.restaurantName = restaurantName; - this.status = reservation.getStatus().name(); - this.partySize = reservation.getPartySize(); - this.specialRequest = reservation.getSpecialRequest(); + this.status = reservation.status().name(); + this.partySize = reservation.partySize(); + this.specialRequest = reservation.specialRequest(); this.dateTime = dateTime; - this.createdAt = reservation.getCreatedAt(); + this.createdAt = reservation.createdAt(); } } diff --git a/api-user/src/main/java/com/wellmeet/global/event/event/ReservationUpdatedEvent.java b/api-user/src/main/java/com/wellmeet/global/event/event/ReservationUpdatedEvent.java index 2579cbe..964cf1f 100644 --- a/api-user/src/main/java/com/wellmeet/global/event/event/ReservationUpdatedEvent.java +++ b/api-user/src/main/java/com/wellmeet/global/event/event/ReservationUpdatedEvent.java @@ -1,6 +1,6 @@ package com.wellmeet.global.event.event; -import com.wellmeet.domain.reservation.entity.Reservation; +import com.wellmeet.common.dto.ReservationDTO; import java.time.LocalDateTime; import lombok.Getter; @@ -18,16 +18,16 @@ public class ReservationUpdatedEvent { private final LocalDateTime dateTime; private final LocalDateTime createdAt; - public ReservationUpdatedEvent(Reservation reservation, String memberName, String restaurantName, LocalDateTime dateTime) { - this.reservationId = reservation.getId(); - this.memberId = reservation.getMemberId(); + public ReservationUpdatedEvent(ReservationDTO reservation, String memberName, String restaurantName, LocalDateTime dateTime) { + this.reservationId = reservation.id(); + this.memberId = reservation.memberId(); this.memberName = memberName; - this.restaurantId = reservation.getRestaurantId(); + this.restaurantId = reservation.restaurantId(); this.restaurantName = restaurantName; - this.status = reservation.getStatus().name(); - this.partySize = reservation.getPartySize(); - this.specialRequest = reservation.getSpecialRequest(); + this.status = reservation.status().name(); + this.partySize = reservation.partySize(); + this.specialRequest = reservation.specialRequest(); this.dateTime = dateTime; - this.createdAt = reservation.getCreatedAt(); + this.createdAt = reservation.createdAt(); } } diff --git a/api-user/src/main/java/com/wellmeet/reservation/ReservationService.java b/api-user/src/main/java/com/wellmeet/reservation/ReservationService.java deleted file mode 100644 index c4e29ae..0000000 --- a/api-user/src/main/java/com/wellmeet/reservation/ReservationService.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.wellmeet.reservation; - -import com.wellmeet.domain.member.MemberDomainService; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.reservation.ReservationDomainService; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.restaurant.RestaurantDomainService; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.global.event.EventPublishService; -import com.wellmeet.global.event.event.ReservationCanceledEvent; -import com.wellmeet.global.event.event.ReservationCreatedEvent; -import com.wellmeet.global.event.event.ReservationUpdatedEvent; -import com.wellmeet.reservation.dto.CreateReservationRequest; -import com.wellmeet.reservation.dto.CreateReservationResponse; -import com.wellmeet.reservation.dto.ReservationResponse; -import com.wellmeet.reservation.dto.SummaryReservationResponse; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class ReservationService { - - private final ReservationDomainService reservationDomainService; - private final ReservationRedisService reservationRedisService; - private final RestaurantDomainService restaurantDomainService; - private final MemberDomainService memberDomainService; - private final EventPublishService eventPublishService; - - @Transactional - public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { - AvailableDate availableDate = restaurantDomainService.getAvailableDate(request.getAvailableDateId(), - request.getRestaurantId()); - reservationDomainService.alreadyReserved(memberId, request.getRestaurantId(), request.getAvailableDateId()); - reservationRedisService.isReserving(memberId, request.getRestaurantId(), request.getAvailableDateId()); - Member member = memberDomainService.getById(memberId); - restaurantDomainService.decreaseAvailableDateCapacity(availableDate, request.getPartySize()); - Reservation reservation = request.toDomain(memberId); - - Reservation savedReservation = reservationDomainService.save(reservation); - var restaurant = restaurantDomainService.getById(savedReservation.getRestaurantId()); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationCreatedEvent event = new ReservationCreatedEvent( - savedReservation, member.getName(), restaurant.getName(), dateTime); - eventPublishService.publishReservationCreatedEvent(event); - - return new CreateReservationResponse(savedReservation, restaurant.getName(), availableDate); - } - - @Transactional(readOnly = true) - public List getReservations(String memberId) { - List reservations = reservationDomainService.findAllByMemberId(memberId); - List restaurantIds = reservations.stream() - .map(Reservation::getRestaurantId) - .toList(); - List availableDateIds = reservations.stream() - .map(Reservation::getAvailableDateId) - .toList(); - - Map restaurantsById = restaurantDomainService.findAllByIds(restaurantIds).stream() - .collect(Collectors.toMap(Restaurant::getId, Function.identity())); - Map availableDatesById = restaurantDomainService - .findAllAvailableDatesByIds(availableDateIds).stream() - .collect(Collectors.toMap(AvailableDate::getId, Function.identity())); - - return reservations.stream() - .map(reservation -> { - var restaurant = restaurantsById.get(reservation.getRestaurantId()); - var availableDate = availableDatesById.get(reservation.getAvailableDateId()); - return new SummaryReservationResponse(reservation, restaurant.getName(), availableDate); - }) - .toList(); - } - - @Transactional(readOnly = true) - public ReservationResponse getReservation(Long reservationId, String memberId) { - Reservation reservation = reservationDomainService.getByIdAndMemberId(reservationId, memberId); - var restaurant = restaurantDomainService.getById(reservation.getRestaurantId()); - var availableDate = restaurantDomainService.getAvailableDate( - reservation.getAvailableDateId(), reservation.getRestaurantId()); - double rating = restaurantDomainService.getAverageRating(reservation.getRestaurantId()); - return new ReservationResponse(reservation, restaurant, availableDate, rating); - } - - @Transactional - public CreateReservationResponse updateReservation( - Long reservationId, - String memberId, - CreateReservationRequest request - ) { - AvailableDate availableDate = restaurantDomainService.getAvailableDate(request.getAvailableDateId(), - request.getRestaurantId()); - Reservation reservation = reservationDomainService.getByIdAndMemberId(reservationId, memberId); - reservationRedisService.isUpdating(memberId, reservationId); - if (reservationDomainService.alreadyUpdated(memberId, request.getRestaurantId(), request.getAvailableDateId(), - request.getPartySize())) { - var restaurant = restaurantDomainService.getById(reservation.getRestaurantId()); - var currentAvailableDate = restaurantDomainService.getAvailableDate( - reservation.getAvailableDateId(), reservation.getRestaurantId()); - return new CreateReservationResponse(reservation, restaurant.getName(), currentAvailableDate); - } - AvailableDate oldAvailableDate = restaurantDomainService.getAvailableDate( - reservation.getAvailableDateId(), reservation.getRestaurantId()); - restaurantDomainService.increaseAvailableDateCapacity(oldAvailableDate, - reservation.getPartySize()); - restaurantDomainService.decreaseAvailableDateCapacity(availableDate, request.getPartySize()); - reservation.update( - request.getAvailableDateId(), - request.getPartySize(), - request.getSpecialRequest() - ); - - Member member = memberDomainService.getById(memberId); - var restaurant = restaurantDomainService.getById(reservation.getRestaurantId()); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationUpdatedEvent event = new ReservationUpdatedEvent( - reservation, member.getName(), restaurant.getName(), dateTime); - eventPublishService.publishReservationUpdatedEvent(event); - return new CreateReservationResponse(reservation, restaurant.getName(), availableDate); - } - - @Transactional - public void cancel(Long reservationId, String memberId) { - Reservation reservation = reservationDomainService.getByIdAndMemberId(reservationId, memberId); - AvailableDate availableDate = restaurantDomainService.getAvailableDate( - reservation.getAvailableDateId(), reservation.getRestaurantId()); - restaurantDomainService.increaseAvailableDateCapacity(availableDate, reservation.getPartySize()); - reservation.cancel(); - - Member member = memberDomainService.getById(memberId); - var restaurant = restaurantDomainService.getById(reservation.getRestaurantId()); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationCanceledEvent event = new ReservationCanceledEvent( - reservation, member.getName(), restaurant.getName(), dateTime); - eventPublishService.publishReservationCanceledEvent(event); - } -} diff --git a/api-user/src/main/java/com/wellmeet/reservation/ReservationController.java b/api-user/src/main/java/com/wellmeet/reservation/UserReservationBffController.java similarity index 96% rename from api-user/src/main/java/com/wellmeet/reservation/ReservationController.java rename to api-user/src/main/java/com/wellmeet/reservation/UserReservationBffController.java index 38b917c..5b7072e 100644 --- a/api-user/src/main/java/com/wellmeet/reservation/ReservationController.java +++ b/api-user/src/main/java/com/wellmeet/reservation/UserReservationBffController.java @@ -22,9 +22,9 @@ @RequestMapping("/user/reservation") @RestController @RequiredArgsConstructor -public class ReservationController { +public class UserReservationBffController { - private final ReservationService reservationService; + private final UserReservationBffService reservationService; @PostMapping @ResponseStatus(value = HttpStatus.CREATED) diff --git a/api-user/src/main/java/com/wellmeet/reservation/UserReservationBffService.java b/api-user/src/main/java/com/wellmeet/reservation/UserReservationBffService.java new file mode 100644 index 0000000..f41e7bc --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/reservation/UserReservationBffService.java @@ -0,0 +1,234 @@ +package com.wellmeet.reservation; + +import com.wellmeet.client.RestaurantAvailableDateFeignClient; +import com.wellmeet.client.MemberFeignClient; +import com.wellmeet.client.ReservationFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.client.dto.request.DecreaseCapacityRequest; +import com.wellmeet.client.dto.request.IncreaseCapacityRequest; +import com.wellmeet.client.dto.request.RestaurantIdsRequest; +import com.wellmeet.client.dto.request.UpdateReservationDTO; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.MemberDTO; +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.ReservationStatus; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.CreateReservationDTO; +import com.wellmeet.global.event.UserEventPublishBffService; +import com.wellmeet.global.event.event.ReservationCanceledEvent; +import com.wellmeet.global.event.event.ReservationCreatedEvent; +import com.wellmeet.global.event.event.ReservationUpdatedEvent; +import com.wellmeet.reservation.dto.CreateReservationRequest; +import com.wellmeet.reservation.dto.CreateReservationResponse; +import com.wellmeet.reservation.dto.ReservationResponse; +import com.wellmeet.reservation.dto.SummaryReservationResponse; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserReservationBffService { + + private final ReservationFeignClient reservationClient; + private final ReservationRedisService reservationRedisService; + private final RestaurantFeignClient restaurantClient; + private final RestaurantAvailableDateFeignClient availableDateClient; + private final MemberFeignClient memberClient; + private final UserEventPublishBffService eventPublishService; + + public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { + // 1. Redis ๋ถ„์‚ฐ ๋ฝ ํš๋“ + reservationRedisService.isReserving(memberId, request.getRestaurantId(), request.getAvailableDateId()); + + // 2. ์ค‘๋ณต ์˜ˆ์•ฝ ์ฒดํฌ (BFF์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌ) + List memberReservations = reservationClient.getReservationsByMember(memberId); + boolean alreadyReserved = memberReservations.stream() + .anyMatch(r -> r.restaurantId().equals(request.getRestaurantId()) + && r.availableDateId().equals(request.getAvailableDateId()) + && r.status().equals(ReservationStatus.CONFIRMED)); + if (alreadyReserved) { + throw new IllegalStateException("์ด๋ฏธ ์˜ˆ์•ฝ๋œ ๋‚ ์งœ์ž…๋‹ˆ๋‹ค."); + } + + // 3. Member, Restaurant, AvailableDate ์กฐํšŒ + MemberDTO member = memberClient.getMember(memberId); + RestaurantDTO restaurant = restaurantClient.getRestaurant(request.getRestaurantId()); + AvailableDateDTO availableDate = restaurantClient.getAvailableDate( + request.getRestaurantId(), request.getAvailableDateId() + ); + + // 4. Capacity ๊ฐ์†Œ + availableDateClient.decreaseCapacity(new DecreaseCapacityRequest( + request.getAvailableDateId(), request.getPartySize())); + + // 5. Reservation ์ƒ์„ฑ + CreateReservationDTO createRequest = new CreateReservationDTO( + request.getRestaurantId(), + request.getAvailableDateId(), + memberId, + request.getPartySize(), + request.getSpecialRequest() + ); + ReservationDTO savedReservation = reservationClient.createReservation(createRequest); + + // 6. ์ด๋ฒคํŠธ ๋ฐœํ–‰ + LocalDateTime dateTime = LocalDateTime.of(availableDate.date(), availableDate.time()); + ReservationCreatedEvent event = new ReservationCreatedEvent( + savedReservation, member.name(), restaurant.name(), dateTime); + eventPublishService.publishReservationCreatedEvent(event); + + return new CreateReservationResponse(savedReservation, restaurant.name(), availableDate); + } + + public List getReservations(String memberId) { + List reservations = reservationClient.getReservationsByMember(memberId); + + if (reservations.isEmpty()) { + return List.of(); + } + + List restaurantIds = reservations.stream() + .map(ReservationDTO::restaurantId) + .distinct() + .toList(); + + // Restaurant ๋ฐฐ์น˜ ์กฐํšŒ + Map restaurantsById = restaurantClient + .getRestaurantsByIds(new RestaurantIdsRequest(restaurantIds)) + .stream() + .collect(Collectors.toMap(RestaurantDTO::id, Function.identity())); + + return reservations.stream() + .map(reservation -> { + RestaurantDTO restaurant = restaurantsById.get(reservation.restaurantId()); + // AvailableDate๋Š” ๊ฐ Restaurant์—์„œ ๊ฐœ๋ณ„ ์กฐํšŒ + AvailableDateDTO availableDate = restaurantClient.getAvailableDate( + reservation.restaurantId(), + reservation.availableDateId() + ); + return new SummaryReservationResponse( + reservation, + restaurant.name(), + availableDate + ); + }) + .toList(); + } + + public ReservationResponse getReservation(Long reservationId, String memberId) { + ReservationDTO reservation = reservationClient.getReservation(reservationId); + + // memberId ๊ฒ€์ฆ (BFF์—์„œ ์ฒ˜๋ฆฌ) + if (!reservation.memberId().equals(memberId)) { + throw new IllegalArgumentException("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); + } + + RestaurantDTO restaurant = restaurantClient.getRestaurant(reservation.restaurantId()); + AvailableDateDTO availableDate = restaurantClient.getAvailableDate( + reservation.restaurantId(), + reservation.availableDateId() + ); + + Double rating = restaurantClient.getAverageRating(reservation.restaurantId()); + double ratingValue = (rating != null) ? rating : 0.0; + + return new ReservationResponse(reservation, restaurant, availableDate, ratingValue); + } + + public CreateReservationResponse updateReservation( + Long reservationId, + String memberId, + CreateReservationRequest request + ) { + // 1. Redis ๋ถ„์‚ฐ ๋ฝ ํš๋“ + reservationRedisService.isUpdating(memberId, reservationId); + + // 2. ํ˜„์žฌ ์˜ˆ์•ฝ ์ •๋ณด ์กฐํšŒ + ReservationDTO reservation = reservationClient.getReservation(reservationId); + if (!reservation.memberId().equals(memberId)) { + throw new IllegalArgumentException("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); + } + + // 3. ์ค‘๋ณต ์ˆ˜์ • ์ฒดํฌ (BFF์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌ) + boolean alreadyUpdated = reservation.restaurantId().equals(request.getRestaurantId()) + && reservation.availableDateId().equals(request.getAvailableDateId()) + && reservation.partySize() == request.getPartySize(); + if (alreadyUpdated) { + RestaurantDTO restaurant = restaurantClient.getRestaurant(reservation.restaurantId()); + AvailableDateDTO currentAvailableDate = restaurantClient.getAvailableDate( + reservation.restaurantId(), + reservation.availableDateId() + ); + return new CreateReservationResponse(reservation, restaurant.name(), currentAvailableDate); + } + + // 4. ์ƒˆ๋กœ์šด AvailableDate ์กฐํšŒ + AvailableDateDTO newAvailableDate = restaurantClient.getAvailableDate( + request.getRestaurantId(), + request.getAvailableDateId() + ); + + // 5. ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜: ๊ธฐ์กด Capacity ๋ณต๊ตฌ + ์ƒˆ๋กœ์šด Capacity ๊ฐ์†Œ + AvailableDateDTO oldAvailableDate = restaurantClient.getAvailableDate( + reservation.restaurantId(), + reservation.availableDateId() + ); + availableDateClient.increaseCapacity(new IncreaseCapacityRequest( + reservation.availableDateId(), reservation.partySize())); + availableDateClient.decreaseCapacity(new DecreaseCapacityRequest( + request.getAvailableDateId(), request.getPartySize())); + + // 6. Reservation ์—…๋ฐ์ดํŠธ + UpdateReservationDTO updateRequest = new UpdateReservationDTO( + request.getRestaurantId(), + request.getAvailableDateId(), + request.getPartySize(), + request.getSpecialRequest() + ); + ReservationDTO updatedReservation = reservationClient.updateReservation(reservationId, updateRequest); + + // 7. ์ด๋ฒคํŠธ ๋ฐœํ–‰ + MemberDTO member = memberClient.getMember(memberId); + RestaurantDTO restaurant = restaurantClient.getRestaurant(reservation.restaurantId()); + LocalDateTime dateTime = LocalDateTime.of(newAvailableDate.date(), newAvailableDate.time()); + ReservationUpdatedEvent event = new ReservationUpdatedEvent( + updatedReservation, member.name(), restaurant.name(), dateTime); + eventPublishService.publishReservationUpdatedEvent(event); + + return new CreateReservationResponse(updatedReservation, restaurant.name(), newAvailableDate); + } + + public void cancel(Long reservationId, String memberId) { + // 1. ์˜ˆ์•ฝ ์ •๋ณด ์กฐํšŒ ๋ฐ ๊ถŒํ•œ ๊ฒ€์ฆ + ReservationDTO reservation = reservationClient.getReservation(reservationId); + if (!reservation.memberId().equals(memberId)) { + throw new IllegalArgumentException("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); + } + + // 2. AvailableDate ์กฐํšŒ + AvailableDateDTO availableDate = restaurantClient.getAvailableDate( + reservation.restaurantId(), + reservation.availableDateId() + ); + + // 3. ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜: Capacity ๋ณต๊ตฌ + availableDateClient.increaseCapacity(new IncreaseCapacityRequest( + reservation.availableDateId(), reservation.partySize())); + + // 4. Reservation ์ทจ์†Œ + reservationClient.cancelReservation(reservationId); + + // 5. ์ด๋ฒคํŠธ ๋ฐœํ–‰ + MemberDTO member = memberClient.getMember(memberId); + RestaurantDTO restaurant = restaurantClient.getRestaurant(reservation.restaurantId()); + LocalDateTime dateTime = LocalDateTime.of(availableDate.date(), availableDate.time()); + ReservationCanceledEvent event = new ReservationCanceledEvent( + reservation, member.name(), restaurant.name(), dateTime); + eventPublishService.publishReservationCanceledEvent(event); + } +} diff --git a/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationRequest.java b/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationRequest.java index 1bd21e1..d363139 100644 --- a/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationRequest.java +++ b/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationRequest.java @@ -1,7 +1,5 @@ package com.wellmeet.reservation.dto; -import com.wellmeet.domain.reservation.entity.Reservation; - import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.NoArgsConstructor; @@ -25,14 +23,4 @@ public CreateReservationRequest(String restaurantId, Long availableDateId, int p this.partySize = partySize; this.specialRequest = specialRequest; } - - public Reservation toDomain(String memberId) { - return new Reservation( - restaurantId, - availableDateId, - memberId, - partySize, - specialRequest - ); - } } diff --git a/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationResponse.java b/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationResponse.java index bff35b3..856ec0c 100644 --- a/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationResponse.java +++ b/api-user/src/main/java/com/wellmeet/reservation/dto/CreateReservationResponse.java @@ -1,14 +1,15 @@ package com.wellmeet.reservation.dto; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.reservation.entity.ReservationStatus; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.ReservationDTO; import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@lombok.Builder +@lombok.AllArgsConstructor public class CreateReservationResponse { private Long id; @@ -18,12 +19,12 @@ public class CreateReservationResponse { private int partySize; private String specialRequest; - public CreateReservationResponse(Reservation reservation, String restaurantName, AvailableDate availableDate) { - this.id = reservation.getId(); + public CreateReservationResponse(ReservationDTO reservation, String restaurantName, AvailableDateDTO availableDate) { + this.id = reservation.id(); this.restaurantName = restaurantName; - this.status = reservation.getStatus(); - this.dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - this.partySize = reservation.getPartySize(); - this.specialRequest = reservation.getSpecialRequest(); + this.status = ReservationStatus.valueOf(reservation.status().name()); + this.dateTime = LocalDateTime.of(availableDate.date(), availableDate.time()); + this.partySize = reservation.partySize(); + this.specialRequest = reservation.specialRequest(); } } diff --git a/api-user/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java b/api-user/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java index dd17aec..c5759d2 100644 --- a/api-user/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java +++ b/api-user/src/main/java/com/wellmeet/reservation/dto/ReservationResponse.java @@ -1,15 +1,16 @@ package com.wellmeet.reservation.dto; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.reservation.entity.ReservationStatus; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.RestaurantDTO; import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@lombok.Builder +@lombok.AllArgsConstructor public class ReservationResponse { private Long id; @@ -24,17 +25,17 @@ public class ReservationResponse { private String specialRequest; private ReservationStatus status; - public ReservationResponse(Reservation reservation, Restaurant restaurant, AvailableDate availableDate, double rating) { - this.id = reservation.getId(); - this.restaurantId = restaurant.getId(); - this.restaurantName = restaurant.getName(); - this.restaurantAddress = restaurant.getAddress(); + public ReservationResponse(ReservationDTO reservation, RestaurantDTO restaurant, AvailableDateDTO availableDate, double rating) { + this.id = reservation.id(); + this.restaurantId = restaurant.id(); + this.restaurantName = restaurant.name(); + this.restaurantAddress = restaurant.address(); this.restaurantRating = rating; - this.latitude = restaurant.getLatitude(); - this.longitude = restaurant.getLongitude(); - this.dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - this.partySize = reservation.getPartySize(); - this.specialRequest = reservation.getSpecialRequest(); - this.status = reservation.getStatus(); + this.latitude = restaurant.latitude(); + this.longitude = restaurant.longitude(); + this.dateTime = LocalDateTime.of(availableDate.date(), availableDate.time()); + this.partySize = reservation.partySize(); + this.specialRequest = reservation.specialRequest(); + this.status = ReservationStatus.valueOf(reservation.status().name()); } } diff --git a/api-user/src/main/java/com/wellmeet/reservation/dto/ReservationStatus.java b/api-user/src/main/java/com/wellmeet/reservation/dto/ReservationStatus.java new file mode 100644 index 0000000..7ae34f3 --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/reservation/dto/ReservationStatus.java @@ -0,0 +1,8 @@ +package com.wellmeet.reservation.dto; + +public enum ReservationStatus { + + PENDING, + CONFIRMED, + CANCELED +} diff --git a/api-user/src/main/java/com/wellmeet/reservation/dto/SummaryReservationResponse.java b/api-user/src/main/java/com/wellmeet/reservation/dto/SummaryReservationResponse.java index 5b40bfb..c1cf4a1 100644 --- a/api-user/src/main/java/com/wellmeet/reservation/dto/SummaryReservationResponse.java +++ b/api-user/src/main/java/com/wellmeet/reservation/dto/SummaryReservationResponse.java @@ -1,14 +1,15 @@ package com.wellmeet.reservation.dto; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.reservation.entity.ReservationStatus; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.ReservationDTO; import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@lombok.Builder +@lombok.AllArgsConstructor public class SummaryReservationResponse { private Long id; @@ -17,11 +18,11 @@ public class SummaryReservationResponse { private int partySize; private ReservationStatus status; - public SummaryReservationResponse(Reservation reservation, String restaurantName, AvailableDate availableDate) { - this.id = reservation.getId(); + public SummaryReservationResponse(ReservationDTO reservation, String restaurantName, AvailableDateDTO availableDate) { + this.id = reservation.id(); this.restaurantName = restaurantName; - this.dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - this.partySize = reservation.getPartySize(); - this.status = reservation.getStatus(); + this.dateTime = LocalDateTime.of(availableDate.date(), availableDate.time()); + this.partySize = reservation.partySize(); + this.status = ReservationStatus.valueOf(reservation.status().name()); } } diff --git a/api-user/src/main/java/com/wellmeet/restaurant/RestaurantService.java b/api-user/src/main/java/com/wellmeet/restaurant/RestaurantService.java deleted file mode 100644 index 7fbc39e..0000000 --- a/api-user/src/main/java/com/wellmeet/restaurant/RestaurantService.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.wellmeet.restaurant; - -import com.wellmeet.common.util.DistanceCalculator; -import com.wellmeet.domain.member.FavoriteRestaurantDomainService; -import com.wellmeet.domain.restaurant.RestaurantDomainService; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.restaurant.dto.AvailableDateResponse; -import com.wellmeet.restaurant.dto.NearbyRestaurantResponse; -import com.wellmeet.restaurant.dto.RepresentativeMenuResponse; -import com.wellmeet.restaurant.dto.RepresentativeReviewResponse; -import com.wellmeet.restaurant.dto.RestaurantResponse; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class RestaurantService { - - private final RestaurantDomainService restaurantDomainService; - private final FavoriteRestaurantDomainService favoriteRestaurantDomainService; - - @Transactional(readOnly = true) - public List findWithNearbyRestaurant(double latitude, double longitude) { - return restaurantDomainService.findWithBoundBox(latitude, longitude) - .stream() - .map(restaurant -> getNearbyRestaurantResponse(restaurant, latitude, longitude)) - .toList(); - } - - private NearbyRestaurantResponse getNearbyRestaurantResponse(Restaurant restaurant, double latitude, - double longitude) { - double rating = restaurantDomainService.getAverageRating(restaurant.getId()); - double distance = DistanceCalculator.calculateDistance(latitude, longitude, restaurant.getLatitude(), - restaurant.getLongitude()); - return new NearbyRestaurantResponse(restaurant, distance, rating); - } - - @Transactional(readOnly = true) - public RestaurantResponse getRestaurant(String restaurantId, String memberId) { - boolean isFavorite = favoriteRestaurantDomainService.isFavorite(memberId, restaurantId); - Restaurant restaurant = restaurantDomainService.getById(restaurantId); - List reviews = restaurantDomainService.getReviewByRestaurantId(restaurant.getId()) - .stream() - .map(RepresentativeReviewResponse::new) - .toList(); - List menus = restaurantDomainService.getMenuByRestaurantId(restaurant.getId()) - .stream() - .map(RepresentativeMenuResponse::new) - .toList(); - double rating = restaurantDomainService.getAverageRating(restaurant.getId()); - return new RestaurantResponse(restaurant, reviews, menus, isFavorite, rating); - } - - @Transactional(readOnly = true) - public List getRestaurantAvailableDates(String restaurantId) { - return restaurantDomainService.getRestaurantAvailableDates(restaurantId) - .stream() - .map(AvailableDateResponse::new) - .toList(); - } -} diff --git a/api-user/src/main/java/com/wellmeet/restaurant/RestaurantController.java b/api-user/src/main/java/com/wellmeet/restaurant/UserRestaurantBffController.java similarity index 93% rename from api-user/src/main/java/com/wellmeet/restaurant/RestaurantController.java rename to api-user/src/main/java/com/wellmeet/restaurant/UserRestaurantBffController.java index 6e6d4ab..ee6329b 100644 --- a/api-user/src/main/java/com/wellmeet/restaurant/RestaurantController.java +++ b/api-user/src/main/java/com/wellmeet/restaurant/UserRestaurantBffController.java @@ -14,9 +14,9 @@ @RequestMapping("/user/restaurant") @RestController @RequiredArgsConstructor -public class RestaurantController { +public class UserRestaurantBffController { - private final RestaurantService restaurantService; + private final UserRestaurantBffService restaurantService; @GetMapping("/nearby") public List getNearbyRestaurants( diff --git a/api-user/src/main/java/com/wellmeet/restaurant/UserRestaurantBffService.java b/api-user/src/main/java/com/wellmeet/restaurant/UserRestaurantBffService.java new file mode 100644 index 0000000..4f252fa --- /dev/null +++ b/api-user/src/main/java/com/wellmeet/restaurant/UserRestaurantBffService.java @@ -0,0 +1,83 @@ +package com.wellmeet.restaurant; + +import com.wellmeet.client.RestaurantAvailableDateFeignClient; +import com.wellmeet.client.MemberFavoriteRestaurantFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.client.dto.ReviewDTO; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.MenuDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.util.DistanceCalculator; +import com.wellmeet.restaurant.dto.AvailableDateResponse; +import com.wellmeet.restaurant.dto.NearbyRestaurantResponse; +import com.wellmeet.restaurant.dto.RepresentativeMenuResponse; +import com.wellmeet.restaurant.dto.RepresentativeReviewResponse; +import com.wellmeet.restaurant.dto.RestaurantResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserRestaurantBffService { + + private static final double SEARCH_RADIUS_M = 5000.0; + + private final RestaurantFeignClient restaurantClient; + private final MemberFavoriteRestaurantFeignClient favoriteRestaurantClient; + private final RestaurantAvailableDateFeignClient availableDateClient; + + public List findWithNearbyRestaurant(double latitude, double longitude) { + return restaurantClient.getAllRestaurants() + .stream() + .filter(restaurant -> { + double distance = DistanceCalculator.calculateDistance( + latitude, longitude, + restaurant.latitude(), restaurant.longitude() + ); + return distance <= SEARCH_RADIUS_M; + }) + .map(restaurant -> getNearbyRestaurantResponse(restaurant, latitude, longitude)) + .toList(); + } + + private NearbyRestaurantResponse getNearbyRestaurantResponse(RestaurantDTO restaurant, double latitude, + double longitude) { + Double rating = restaurantClient.getAverageRating(restaurant.id()); + double ratingValue = (rating != null) ? rating : 0.0; + double distance = DistanceCalculator.calculateDistance( + latitude, longitude, + restaurant.latitude(), restaurant.longitude() + ); + return new NearbyRestaurantResponse(restaurant, distance, ratingValue); + } + + public RestaurantResponse getRestaurant(String restaurantId, String memberId) { + Boolean isFavorite = favoriteRestaurantClient.isFavorite(memberId, restaurantId); + boolean isFavoriteValue = Boolean.TRUE.equals(isFavorite); + + RestaurantDTO restaurant = restaurantClient.getRestaurant(restaurantId); + + List reviewDTOs = restaurantClient.getReviewsByRestaurant(restaurant.id()); + List reviews = reviewDTOs.stream() + .map(RepresentativeReviewResponse::new) + .toList(); + + List menuDTOs = restaurantClient.getMenusByRestaurant(restaurant.id()); + List menus = menuDTOs.stream() + .map(RepresentativeMenuResponse::new) + .toList(); + + Double rating = restaurantClient.getAverageRating(restaurant.id()); + double ratingValue = (rating != null) ? rating : 0.0; + + return new RestaurantResponse(restaurant, reviews, menus, isFavoriteValue, ratingValue); + } + + public List getRestaurantAvailableDates(String restaurantId) { + List availableDates = availableDateClient.getAvailableDatesByRestaurant(restaurantId); + return availableDates.stream() + .map(AvailableDateResponse::new) + .toList(); + } +} diff --git a/api-user/src/main/java/com/wellmeet/restaurant/dto/AvailableDateResponse.java b/api-user/src/main/java/com/wellmeet/restaurant/dto/AvailableDateResponse.java index 34358a7..82ff646 100644 --- a/api-user/src/main/java/com/wellmeet/restaurant/dto/AvailableDateResponse.java +++ b/api-user/src/main/java/com/wellmeet/restaurant/dto/AvailableDateResponse.java @@ -1,6 +1,6 @@ package com.wellmeet.restaurant.dto; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; +import com.wellmeet.common.dto.AvailableDateDTO; import java.time.LocalDate; import java.time.LocalTime; import lombok.Getter; @@ -16,11 +16,11 @@ public class AvailableDateResponse { private int capacity; private boolean available; - public AvailableDateResponse(AvailableDate availableDate) { - this.id = availableDate.getId(); - this.date = availableDate.getDate(); - this.time = availableDate.getTime(); - this.capacity = availableDate.getMaxCapacity(); + public AvailableDateResponse(AvailableDateDTO availableDate) { + this.id = availableDate.id(); + this.date = availableDate.date(); + this.time = availableDate.time(); + this.capacity = availableDate.maxCapacity(); this.available = availableDate.isAvailable(); } } diff --git a/api-user/src/main/java/com/wellmeet/restaurant/dto/NearbyRestaurantResponse.java b/api-user/src/main/java/com/wellmeet/restaurant/dto/NearbyRestaurantResponse.java index fc12b42..276c5b9 100644 --- a/api-user/src/main/java/com/wellmeet/restaurant/dto/NearbyRestaurantResponse.java +++ b/api-user/src/main/java/com/wellmeet/restaurant/dto/NearbyRestaurantResponse.java @@ -1,6 +1,6 @@ package com.wellmeet.restaurant.dto; -import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.common.dto.RestaurantDTO; import lombok.Getter; import lombok.NoArgsConstructor; @@ -15,12 +15,12 @@ public class NearbyRestaurantResponse { private double rating; private String thumbnail; - public NearbyRestaurantResponse(Restaurant restaurant, double distance, double rating) { - this.id = restaurant.getId(); - this.name = restaurant.getName(); - this.address = restaurant.getAddress(); + public NearbyRestaurantResponse(RestaurantDTO restaurant, double distance, double rating) { + this.id = restaurant.id(); + this.name = restaurant.name(); + this.address = restaurant.address(); this.distance = distance; this.rating = rating; - this.thumbnail = restaurant.getThumbnail(); + this.thumbnail = restaurant.thumbnail(); } } diff --git a/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeMenuResponse.java b/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeMenuResponse.java index d72164d..1d93d99 100644 --- a/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeMenuResponse.java +++ b/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeMenuResponse.java @@ -1,6 +1,6 @@ package com.wellmeet.restaurant.dto; -import com.wellmeet.domain.restaurant.menu.entity.Menu; +import com.wellmeet.common.dto.MenuDTO; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,8 +11,8 @@ public class RepresentativeMenuResponse { private String name; private int price; - public RepresentativeMenuResponse(Menu menu) { - this.name = menu.getName(); - this.price = menu.getPrice(); + public RepresentativeMenuResponse(MenuDTO menu) { + this.name = menu.name(); + this.price = menu.price(); } } diff --git a/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeReviewResponse.java b/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeReviewResponse.java index 5513749..202d2c0 100644 --- a/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeReviewResponse.java +++ b/api-user/src/main/java/com/wellmeet/restaurant/dto/RepresentativeReviewResponse.java @@ -1,6 +1,6 @@ package com.wellmeet.restaurant.dto; -import com.wellmeet.domain.restaurant.review.entity.Review; +import com.wellmeet.client.dto.ReviewDTO; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,11 +10,9 @@ public class RepresentativeReviewResponse { private String situation; private String content; - private String logo; - public RepresentativeReviewResponse(Review review) { - this.situation = review.getSituation().getName(); - this.content = review.getContent(); - this.logo = review.getSituation().getLogo(); + public RepresentativeReviewResponse(ReviewDTO review) { + this.situation = review.situation(); + this.content = review.content(); } } diff --git a/api-user/src/main/java/com/wellmeet/restaurant/dto/RestaurantResponse.java b/api-user/src/main/java/com/wellmeet/restaurant/dto/RestaurantResponse.java index 270b1f1..d8b5f21 100644 --- a/api-user/src/main/java/com/wellmeet/restaurant/dto/RestaurantResponse.java +++ b/api-user/src/main/java/com/wellmeet/restaurant/dto/RestaurantResponse.java @@ -1,6 +1,6 @@ package com.wellmeet.restaurant.dto; -import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.common.dto.RestaurantDTO; import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; @@ -22,18 +22,18 @@ public class RestaurantResponse { private List reviews; public RestaurantResponse( - Restaurant restaurant, + RestaurantDTO restaurant, List reviews, List menus, boolean isFavorite, double rating ) { - this.id = restaurant.getId(); - this.name = restaurant.getName(); - this.address = restaurant.getAddress(); - this.latitude = restaurant.getLatitude(); - this.longitude = restaurant.getLongitude(); - this.thumbnail = restaurant.getThumbnail(); + this.id = restaurant.id(); + this.name = restaurant.name(); + this.address = restaurant.address(); + this.latitude = restaurant.latitude(); + this.longitude = restaurant.longitude(); + this.thumbnail = restaurant.thumbnail(); this.reviews = reviews; this.menus = menus; this.favorite = isFavorite; diff --git a/api-user/src/main/resources/application-dev.yml b/api-user/src/main/resources/application-dev.yml index 8d6997c..b26050c 100644 --- a/api-user/src/main/resources/application-dev.yml +++ b/api-user/src/main/resources/application-dev.yml @@ -1,10 +1,63 @@ spring: + application: + name: api-user-service config: import: - classpath:dev-secret.yml - - classpath:application-domain-dev.yml - classpath:application-infra-redis-dev.yml - classpath:application-infra-kafka-dev.yml + jackson: + time-zone: Asia/Seoul + serialization: + write-dates-as-timestamps: false + +server: + port: 8086 + shutdown: graceful + +eureka: + client: + enabled: true + service-url: + defaultZone: ${secret.eureka.server-url} + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} + lease-renewal-interval-in-seconds: 30 + lease-expiration-duration-in-seconds: 90 + +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + enabled: true + +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: BASIC + cors: origin: ${secret.cors.origin} + +logging: + level: + root: INFO + com.wellmeet: DEBUG + org.springframework.web: DEBUG + org.springframework.cloud: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" \ No newline at end of file diff --git a/api-user/src/main/resources/application-local.yml b/api-user/src/main/resources/application-local.yml index 1215089..5f3852d 100644 --- a/api-user/src/main/resources/application-local.yml +++ b/api-user/src/main/resources/application-local.yml @@ -1,11 +1,26 @@ spring: + application: + name: api-user-service config: import: - - classpath:application-domain-local.yml - classpath:application-infra-redis-local.yml - classpath:application-infra-kafka-local.yml - application: - name: WellMeet-Backend + +server: + port: 8086 + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ + +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: BASIC cors: origin: http://localhost:5173 diff --git a/api-user/src/main/resources/application-test.yml b/api-user/src/main/resources/application-test.yml index 13e616a..6ce38f5 100644 --- a/api-user/src/main/resources/application-test.yml +++ b/api-user/src/main/resources/application-test.yml @@ -1,9 +1,34 @@ spring: + application: + name: api-user-service config: import: - - classpath:application-domain-test.yml - classpath:application-infra-redis-test.yml - classpath:application-infra-kafka-test.yml + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration + cloud: + discovery: + enabled: false + +server: + port: 8086 + +eureka: + client: + enabled: false + register-with-eureka: false + fetch-registry: false + +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: BASIC cors: origin: http://localhost:5173 diff --git a/api-user/src/test/java/com/wellmeet/BaseControllerTest.java b/api-user/src/test/java/com/wellmeet/BaseControllerTest.java index 8a9095d..073ce03 100644 --- a/api-user/src/test/java/com/wellmeet/BaseControllerTest.java +++ b/api-user/src/test/java/com/wellmeet/BaseControllerTest.java @@ -1,54 +1,19 @@ package com.wellmeet; -import com.wellmeet.domain.member.repository.FavoriteRestaurantRepository; -import com.wellmeet.domain.fixture.AvailableDateGenerator; -import com.wellmeet.domain.fixture.MemberGenerator; -import com.wellmeet.domain.fixture.MenuGenerator; -import com.wellmeet.domain.fixture.OwnerGenerator; -import com.wellmeet.domain.fixture.ReservationGenerator; -import com.wellmeet.domain.fixture.RestaurantGenerator; -import com.wellmeet.domain.fixture.ReviewGenerator; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; import io.restassured.filter.log.ResponseLoggingFilter; import io.restassured.specification.RequestSpecification; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.ActiveProfiles; -@ExtendWith(DataBaseCleaner.class) @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class BaseControllerTest { - @Autowired - protected AvailableDateGenerator availableDateGenerator; - - @Autowired - protected ReservationGenerator reservationGenerator; - - @Autowired - protected MemberGenerator memberGenerator; - - @Autowired - protected OwnerGenerator ownerGenerator; - - @Autowired - protected RestaurantGenerator restaurantGenerator; - - @Autowired - protected MenuGenerator menuGenerator; - - @Autowired - protected ReviewGenerator reviewGenerator; - - @Autowired - protected FavoriteRestaurantRepository favoriteRestaurantRepository; - @LocalServerPort private int port; diff --git a/api-user/src/test/java/com/wellmeet/BaseServiceTest.java b/api-user/src/test/java/com/wellmeet/BaseServiceTest.java index 9a89743..ad3e4f2 100644 --- a/api-user/src/test/java/com/wellmeet/BaseServiceTest.java +++ b/api-user/src/test/java/com/wellmeet/BaseServiceTest.java @@ -1,55 +1,16 @@ package com.wellmeet; -import com.wellmeet.domain.reservation.repository.ReservationRepository; -import com.wellmeet.domain.restaurant.availabledate.repository.AvailableDateRepository; -import com.wellmeet.domain.fixture.AvailableDateGenerator; -import com.wellmeet.domain.fixture.MemberGenerator; -import com.wellmeet.domain.fixture.MenuGenerator; -import com.wellmeet.domain.fixture.OwnerGenerator; -import com.wellmeet.domain.fixture.ReservationGenerator; -import com.wellmeet.domain.fixture.RestaurantGenerator; -import com.wellmeet.domain.fixture.ReviewGenerator; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -@ExtendWith(DataBaseCleaner.class) @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public abstract class BaseServiceTest { - @Autowired - protected AvailableDateGenerator availableDateGenerator; - - @Autowired - protected ReservationGenerator reservationGenerator; - - @Autowired - protected MemberGenerator memberGenerator; - - @Autowired - protected OwnerGenerator ownerGenerator; - - @Autowired - protected RestaurantGenerator restaurantGenerator; - - @Autowired - protected MenuGenerator menuGenerator; - - @Autowired - protected ReviewGenerator reviewGenerator; - - @Autowired - protected ReservationRepository reservationRepository; - - @Autowired - protected AvailableDateRepository availableDateRepository; - protected void runAtSameTime(int count, Runnable task) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(count); CountDownLatch latch = new CountDownLatch(count); diff --git a/api-user/src/test/java/com/wellmeet/favorite/FavoriteControllerTest.java b/api-user/src/test/java/com/wellmeet/favorite/FavoriteControllerTest.java deleted file mode 100644 index 2e58749..0000000 --- a/api-user/src/test/java/com/wellmeet/favorite/FavoriteControllerTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.wellmeet.favorite; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.wellmeet.BaseControllerTest; -import com.wellmeet.domain.member.entity.FavoriteRestaurant; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; - -class FavoriteControllerTest extends BaseControllerTest { - - @Nested - class GetFavoriteRestaurants { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์กฐํšŒ() { - Member testUser = memberGenerator.generate("test"); - Member anotherUser = memberGenerator.generate("another"); - Owner owner1 = ownerGenerator.generate("Owner1"); - Owner owner2 = ownerGenerator.generate("Owner2"); - Owner owner3 = ownerGenerator.generate("Owner3"); - Restaurant restaurant1 = restaurantGenerator.generate("Restaurant 1", owner1.getId()); - Restaurant restaurant2 = restaurantGenerator.generate("Restaurant 2", owner2.getId()); - Restaurant restaurant3 = restaurantGenerator.generate("Restaurant 3", owner3.getId()); - favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser.getId(), restaurant1.getId())); - favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser.getId(), restaurant2.getId())); - favoriteRestaurantRepository.save(new FavoriteRestaurant(anotherUser.getId(), restaurant2.getId())); - favoriteRestaurantRepository.save(new FavoriteRestaurant(anotherUser.getId(), restaurant3.getId())); - - FavoriteRestaurantResponse[] responses = given() - .contentType("application/json") - .queryParam("memberId", testUser.getId()) - .when().get("/user/favorite/restaurant/list") - .then().statusCode(HttpStatus.OK.value()) - .extract().as(FavoriteRestaurantResponse[].class); - - assertThat(responses).hasSize(2); - assertThat(responses[0].getId()).isEqualTo(restaurant1.getId()); - assertThat(responses[1].getId()).isEqualTo(restaurant2.getId()); - } - } - - @Nested - class AddFavoriteRestaurant { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์ถ”๊ฐ€() { - Member testUser = memberGenerator.generate("testUser"); - Owner owner = ownerGenerator.generate("Test Owner"); - Restaurant restaurant = restaurantGenerator.generate("Test Restaurant", owner.getId()); - - FavoriteRestaurantResponse response = given() - .contentType("application/json") - .queryParam("memberId", testUser.getId()) - .when().post("/user/favorite/restaurant/{restaurantId}", restaurant.getId()) - .then().statusCode(HttpStatus.CREATED.value()) - .extract().as(FavoriteRestaurantResponse.class); - - assertThat(response.getId()).isEqualTo(restaurant.getId()); - } - } - - @Nested - class RemoveFavoriteRestaurant { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์‚ญ์ œ() { - Member testUser = memberGenerator.generate("testUser"); - Owner owner = ownerGenerator.generate("Test Owner"); - Restaurant restaurant = restaurantGenerator.generate("Test Restaurant", owner.getId()); - favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser.getId(), restaurant.getId())); - - given() - .contentType("application/json") - .queryParam("memberId", testUser.getId()) - .when().delete("/user/favorite/restaurant/{restaurantId}", restaurant.getId()) - .then().statusCode(HttpStatus.NO_CONTENT.value()); - } - } -} diff --git a/api-user/src/test/java/com/wellmeet/favorite/FavoriteServiceTest.java b/api-user/src/test/java/com/wellmeet/favorite/FavoriteServiceTest.java deleted file mode 100644 index 56fa3f9..0000000 --- a/api-user/src/test/java/com/wellmeet/favorite/FavoriteServiceTest.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.wellmeet.favorite; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.wellmeet.domain.member.FavoriteRestaurantDomainService; -import com.wellmeet.domain.member.entity.FavoriteRestaurant; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.restaurant.RestaurantDomainService; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.domain.restaurant.review.ReviewDomainService; -import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; -import java.util.List; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class FavoriteServiceTest { - - @Mock - private FavoriteRestaurantDomainService favoriteRestaurantDomainService; - - @Mock - private ReviewDomainService reviewDomainService; - - @Mock - private RestaurantDomainService restaurantDomainService; - - @InjectMocks - private FavoriteService favoriteService; - - @Nested - class GetFavoriteRestaurants { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_์‹๋‹น_๋ชฉ๋ก์„_์กฐํšŒํ•œ๋‹ค() { - Member member = createMember(); - Restaurant restaurant1 = createRestaurant("restaurant-1", "์‹๋‹น1"); - Restaurant restaurant2 = createRestaurant("restaurant-2", "์‹๋‹น2"); - FavoriteRestaurant favorite1 = new FavoriteRestaurant(member.getId(), restaurant1.getId()); - FavoriteRestaurant favorite2 = new FavoriteRestaurant(member.getId(), restaurant2.getId()); - List favorites = List.of(favorite1, favorite2); - - when(favoriteRestaurantDomainService.findAllByMemberId(member.getId())) - .thenReturn(favorites); - when(reviewDomainService.getAverageRating("restaurant-1")).thenReturn(4.5); - when(reviewDomainService.getAverageRating("restaurant-2")).thenReturn(3.8); - when(restaurantDomainService.findAllByIds(List.of(restaurant1.getId(), restaurant2.getId()))) - .thenReturn(List.of(restaurant1, restaurant2)); - - List result = favoriteService.getFavoriteRestaurants(member.getId()); - - assertThat(result).hasSize(2); - assertThat(result) - .extracting(FavoriteRestaurantResponse::getName, FavoriteRestaurantResponse::getRating) - .containsExactlyInAnyOrder( - tuple("์‹๋‹น1", 4.5), - tuple("์‹๋‹น2", 3.8) - ); - } - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ๊ฐ€_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { - String memberId = "member-1"; - - when(favoriteRestaurantDomainService.findAllByMemberId(memberId)) - .thenReturn(List.of()); - - List result = favoriteService.getFavoriteRestaurants(memberId); - - assertThat(result).isEmpty(); - } - } - - @Nested - class AddFavoriteRestaurant { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_์‹๋‹น์„_์ถ”๊ฐ€ํ•œ๋‹ค() { - Member member = createMember(); - Restaurant restaurant = createRestaurant("restaurant-1", "๋ง›์ง‘"); - - when(restaurantDomainService.getById(restaurant.getId())).thenReturn(restaurant); - when(reviewDomainService.getAverageRating(restaurant.getId())).thenReturn(4.2); - - FavoriteRestaurantResponse result = favoriteService.addFavoriteRestaurant( - member.getId(), - restaurant.getId() - ); - - assertThat(result.getId()).isEqualTo(restaurant.getId()); - assertThat(result.getName()).isEqualTo("๋ง›์ง‘"); - assertThat(result.getRating()).isEqualTo(4.2); - verify(favoriteRestaurantDomainService).save( - any(FavoriteRestaurant.class) - ); - } - } - - @Nested - class RemoveFavoriteRestaurant { - - @Test - void ์ฆ๊ฒจ์ฐพ๊ธฐ_์‹๋‹น์„_์‚ญ์ œํ•œ๋‹ค() { - Member member = createMember(); - Restaurant restaurant = createRestaurant("restaurant-1", "์‹๋‹น"); - FavoriteRestaurant favoriteRestaurant = new FavoriteRestaurant(member.getId(), restaurant.getId()); - - when(favoriteRestaurantDomainService.getByMemberIdAndRestaurantId( - member.getId(), - restaurant.getId() - )).thenReturn(favoriteRestaurant); - - favoriteService.removeFavoriteRestaurant(member.getId(), restaurant.getId()); - - verify(favoriteRestaurantDomainService).delete(favoriteRestaurant); - } - } - - private Member createMember() { - return new Member("member", "nickname", "email@test.com", "010-1234-5678"); - } - - private Restaurant createRestaurant(String id, String name) { - Owner owner = new Owner("owner", "owner@test.com"); - return new Restaurant( - id, - name, - "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", - 37.5, - 127.0, - "thumbnail.jpg", - owner.getId() - ); - } -} diff --git a/api-user/src/test/java/com/wellmeet/favorite/UserFavoriteRestaurantBffControllerTest.java b/api-user/src/test/java/com/wellmeet/favorite/UserFavoriteRestaurantBffControllerTest.java new file mode 100644 index 0000000..33cabb5 --- /dev/null +++ b/api-user/src/test/java/com/wellmeet/favorite/UserFavoriteRestaurantBffControllerTest.java @@ -0,0 +1,107 @@ +package com.wellmeet.favorite; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(UserFavoriteRestaurantBffController.class) +class UserFavoriteRestaurantBffControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private UserFavoriteRestaurantBffService favoriteService; + + @Nested + class GetFavoriteRestaurants { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์กฐํšŒ() throws Exception { + String memberId = "member-1"; + FavoriteRestaurantResponse response1 = createFavoriteRestaurantResponse("restaurant-1", "์‹๋‹น1", 4.5); + FavoriteRestaurantResponse response2 = createFavoriteRestaurantResponse("restaurant-2", "์‹๋‹น2", 3.8); + + when(favoriteService.getFavoriteRestaurants(memberId)) + .thenReturn(List.of(response1, response2)); + + mockMvc.perform(get("/user/favorite/restaurant/list") + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value("restaurant-1")) + .andExpect(jsonPath("$[0].name").value("์‹๋‹น1")) + .andExpect(jsonPath("$[1].id").value("restaurant-2")) + .andExpect(jsonPath("$[1].name").value("์‹๋‹น2")); + + verify(favoriteService).getFavoriteRestaurants(memberId); + } + } + + @Nested + class AddFavoriteRestaurant { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์ถ”๊ฐ€() throws Exception { + String memberId = "member-1"; + String restaurantId = "restaurant-1"; + FavoriteRestaurantResponse response = createFavoriteRestaurantResponse(restaurantId, "๋ง›์ง‘", 4.2); + + when(favoriteService.addFavoriteRestaurant(memberId, restaurantId)) + .thenReturn(response); + + mockMvc.perform(post("/user/favorite/restaurant/{restaurantId}", restaurantId) + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(restaurantId)) + .andExpect(jsonPath("$.name").value("๋ง›์ง‘")) + .andExpect(jsonPath("$.rating").value(4.2)); + + verify(favoriteService).addFavoriteRestaurant(memberId, restaurantId); + } + } + + @Nested + class RemoveFavoriteRestaurant { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์‚ญ์ œ() throws Exception { + String memberId = "member-1"; + String restaurantId = "restaurant-1"; + + mockMvc.perform(delete("/user/favorite/restaurant/{restaurantId}", restaurantId) + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + verify(favoriteService).removeFavoriteRestaurant(memberId, restaurantId); + } + } + + private FavoriteRestaurantResponse createFavoriteRestaurantResponse(String id, String name, double rating) { + return FavoriteRestaurantResponse.builder() + .id(id) + .name(name) + .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ") + .thumbnail("thumbnail.jpg") + .rating(rating) + .build(); + } +} diff --git a/api-user/src/test/java/com/wellmeet/favorite/UserFavoriteRestaurantBffServiceTest.java b/api-user/src/test/java/com/wellmeet/favorite/UserFavoriteRestaurantBffServiceTest.java new file mode 100644 index 0000000..d35171a --- /dev/null +++ b/api-user/src/test/java/com/wellmeet/favorite/UserFavoriteRestaurantBffServiceTest.java @@ -0,0 +1,133 @@ +package com.wellmeet.favorite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wellmeet.client.MemberFavoriteRestaurantFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.common.dto.FavoriteRestaurantDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserFavoriteRestaurantBffServiceTest { + + @Mock + private MemberFavoriteRestaurantFeignClient favoriteRestaurantClient; + + @Mock + private RestaurantFeignClient restaurantClient; + + @InjectMocks + private UserFavoriteRestaurantBffService favoriteService; + + @Nested + class GetFavoriteRestaurants { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_์‹๋‹น_๋ชฉ๋ก์„_์กฐํšŒํ•œ๋‹ค() { + String memberId = "member-1"; + FavoriteRestaurantDTO favorite1 = createFavoriteRestaurantDTO(memberId, "restaurant-1"); + FavoriteRestaurantDTO favorite2 = createFavoriteRestaurantDTO(memberId, "restaurant-2"); + List favorites = List.of(favorite1, favorite2); + + RestaurantDTO restaurant1 = createRestaurantDTO("restaurant-1", "์‹๋‹น1"); + RestaurantDTO restaurant2 = createRestaurantDTO("restaurant-2", "์‹๋‹น2"); + + when(favoriteRestaurantClient.getFavoritesByMemberId(memberId)) + .thenReturn(favorites); + when(restaurantClient.getAverageRating("restaurant-1")).thenReturn(4.5); + when(restaurantClient.getAverageRating("restaurant-2")).thenReturn(3.8); + when(restaurantClient.getRestaurantsByIds(any())) + .thenReturn(List.of(restaurant1, restaurant2)); + + List result = favoriteService.getFavoriteRestaurants(memberId); + + assertThat(result).hasSize(2); + assertThat(result) + .extracting(FavoriteRestaurantResponse::getName, FavoriteRestaurantResponse::getRating) + .containsExactlyInAnyOrder( + tuple("์‹๋‹น1", 4.5), + tuple("์‹๋‹น2", 3.8) + ); + } + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ๊ฐ€_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + String memberId = "member-1"; + + when(favoriteRestaurantClient.getFavoritesByMemberId(memberId)) + .thenReturn(List.of()); + + List result = favoriteService.getFavoriteRestaurants(memberId); + + assertThat(result).isEmpty(); + } + } + + @Nested + class AddFavoriteRestaurant { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_์‹๋‹น์„_์ถ”๊ฐ€ํ•œ๋‹ค() { + String memberId = "member-1"; + String restaurantId = "restaurant-1"; + RestaurantDTO restaurant = createRestaurantDTO(restaurantId, "๋ง›์ง‘"); + + when(restaurantClient.getRestaurant(restaurantId)).thenReturn(restaurant); + when(restaurantClient.getAverageRating(restaurantId)).thenReturn(4.2); + + FavoriteRestaurantResponse result = favoriteService.addFavoriteRestaurant( + memberId, + restaurantId + ); + + assertThat(result.getId()).isEqualTo(restaurantId); + assertThat(result.getName()).isEqualTo("๋ง›์ง‘"); + assertThat(result.getRating()).isEqualTo(4.2); + verify(favoriteRestaurantClient).addFavorite(memberId, restaurantId); + } + } + + @Nested + class RemoveFavoriteRestaurant { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_์‹๋‹น์„_์‚ญ์ œํ•œ๋‹ค() { + String memberId = "member-1"; + String restaurantId = "restaurant-1"; + + favoriteService.removeFavoriteRestaurant(memberId, restaurantId); + + verify(favoriteRestaurantClient).removeFavorite(memberId, restaurantId); + } + } + + private FavoriteRestaurantDTO createFavoriteRestaurantDTO(String memberId, String restaurantId) { + return new FavoriteRestaurantDTO(1L, memberId, restaurantId, null, null); + } + + private RestaurantDTO createRestaurantDTO(String id, String name) { + return new RestaurantDTO( + id, + name, + "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", + 37.5, + 127.0, + "thumbnail.jpg", + "owner-1", + null, + null + ); + } +} diff --git a/api-user/src/test/java/com/wellmeet/global/controller/HealthCheckControllerTest.java b/api-user/src/test/java/com/wellmeet/global/controller/HealthCheckControllerTest.java index cdc2626..5ceea11 100644 --- a/api-user/src/test/java/com/wellmeet/global/controller/HealthCheckControllerTest.java +++ b/api-user/src/test/java/com/wellmeet/global/controller/HealthCheckControllerTest.java @@ -1,20 +1,27 @@ package com.wellmeet.global.controller; -import com.wellmeet.BaseControllerTest; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(HealthCheckController.class) +class HealthCheckControllerTest { -class HealthCheckControllerTest extends BaseControllerTest { + @Autowired + private MockMvc mockMvc; @Nested class HealthCheck { @Test - void ํ—ฌ์Šค์ฒดํฌ_์—”๋“œํฌ์ธํŠธ๊ฐ€_์ •์ƒ์ ์œผ๋กœ_์‘๋‹ตํ•œ๋‹ค() { - given() - .when().get("/health") - .then().statusCode(HttpStatus.OK.value()); + void ํ—ฌ์Šค์ฒดํฌ_์—”๋“œํฌ์ธํŠธ๊ฐ€_์ •์ƒ์ ์œผ๋กœ_์‘๋‹ตํ•œ๋‹ค() throws Exception { + mockMvc.perform(get("/health")) + .andExpect(status().isOk()); } } } diff --git a/api-user/src/test/java/com/wellmeet/global/event/EventPublishServiceTest.java b/api-user/src/test/java/com/wellmeet/global/event/EventPublishServiceTest.java deleted file mode 100644 index e699e46..0000000 --- a/api-user/src/test/java/com/wellmeet/global/event/EventPublishServiceTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.wellmeet.global.event; - -import static org.mockito.Mockito.verify; - -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.global.event.event.ReservationCanceledEvent; -import com.wellmeet.global.event.event.ReservationCreatedEvent; -import com.wellmeet.global.event.event.ReservationUpdatedEvent; -import java.time.LocalDateTime; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.ApplicationEventPublisher; - -@ExtendWith(MockitoExtension.class) -class EventPublishServiceTest { - - @Mock - private ApplicationEventPublisher eventPublisher; - - @InjectMocks - private EventPublishService eventPublishService; - - @Nested - class PublishReservationCreatedEvent { - - @Test - void ์˜ˆ์•ฝ_์ƒ์„ฑ_์ด๋ฒคํŠธ๋ฅผ_๋ฐœํ–‰ํ•œ๋‹ค() { - Reservation reservation = createReservation(); - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationCreatedEvent event = new ReservationCreatedEvent(reservation, "member", restaurant.getName(), dateTime); - - eventPublishService.publishReservationCreatedEvent(event); - - verify(eventPublisher).publishEvent(event); - } - } - - @Nested - class PublishReservationUpdatedEvent { - - @Test - void ์˜ˆ์•ฝ_์ˆ˜์ •_์ด๋ฒคํŠธ๋ฅผ_๋ฐœํ–‰ํ•œ๋‹ค() { - Reservation reservation = createReservation(); - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationUpdatedEvent event = new ReservationUpdatedEvent(reservation, "member", restaurant.getName(), dateTime); - - eventPublishService.publishReservationUpdatedEvent(event); - - verify(eventPublisher).publishEvent(event); - } - } - - @Nested - class PublishReservationCanceledEvent { - - @Test - void ์˜ˆ์•ฝ_์ทจ์†Œ_์ด๋ฒคํŠธ๋ฅผ_๋ฐœํ–‰ํ•œ๋‹ค() { - Reservation reservation = createReservation(); - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationCanceledEvent event = new ReservationCanceledEvent(reservation, "member", restaurant.getName(), dateTime); - - eventPublishService.publishReservationCanceledEvent(event); - - verify(eventPublisher).publishEvent(event); - } - } - - private Restaurant getRestaurant() { - Owner owner = new Owner("owner", "owner@test.com"); - return new Restaurant( - "์‹๋‹น", - "description", - "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", - 37.5, - 127.0, - "thumbnail.jpg", - owner.getId() - ); - } - - private AvailableDate getAvailableDate(Restaurant restaurant) { - LocalDateTime dateTime = LocalDateTime.now().plusDays(1); - return new AvailableDate( - dateTime.toLocalDate(), - dateTime.toLocalTime(), - 10, - restaurant - ); - } - - private Reservation createReservation() { - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - Member member = new Member("member", "nickname", "email@test.com", "010-1234-5678"); - return new Reservation(restaurant.getId(), availableDate.getId(), member.getId(), 4, "์š”์ฒญ์‚ฌํ•ญ"); - } -} diff --git a/api-user/src/test/java/com/wellmeet/global/event/UserEventPublishBffServiceTest.java b/api-user/src/test/java/com/wellmeet/global/event/UserEventPublishBffServiceTest.java new file mode 100644 index 0000000..a1169df --- /dev/null +++ b/api-user/src/test/java/com/wellmeet/global/event/UserEventPublishBffServiceTest.java @@ -0,0 +1,92 @@ +package com.wellmeet.global.event; + +import static org.mockito.Mockito.verify; + +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.ReservationStatus; +import com.wellmeet.global.event.event.ReservationCanceledEvent; +import com.wellmeet.global.event.event.ReservationCreatedEvent; +import com.wellmeet.global.event.event.ReservationUpdatedEvent; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +@ExtendWith(MockitoExtension.class) +class UserEventPublishBffServiceTest { + + @Mock + private ApplicationEventPublisher eventPublisher; + + @InjectMocks + private UserEventPublishBffService eventPublishService; + + @Nested + class PublishReservationCreatedEvent { + + @Test + void ์˜ˆ์•ฝ_์ƒ์„ฑ_์ด๋ฒคํŠธ๋ฅผ_๋ฐœํ–‰ํ•œ๋‹ค() { + ReservationDTO reservation = createReservationDTO(); + LocalDateTime dateTime = LocalDateTime.now().plusDays(1); + ReservationCreatedEvent event = new ReservationCreatedEvent( + reservation, "ํ™๊ธธ๋™", "๋ง›์ง‘", dateTime + ); + + eventPublishService.publishReservationCreatedEvent(event); + + verify(eventPublisher).publishEvent(event); + } + } + + @Nested + class PublishReservationUpdatedEvent { + + @Test + void ์˜ˆ์•ฝ_์ˆ˜์ •_์ด๋ฒคํŠธ๋ฅผ_๋ฐœํ–‰ํ•œ๋‹ค() { + ReservationDTO reservation = createReservationDTO(); + LocalDateTime dateTime = LocalDateTime.now().plusDays(1); + ReservationUpdatedEvent event = new ReservationUpdatedEvent( + reservation, "ํ™๊ธธ๋™", "๋ง›์ง‘", dateTime + ); + + eventPublishService.publishReservationUpdatedEvent(event); + + verify(eventPublisher).publishEvent(event); + } + } + + @Nested + class PublishReservationCanceledEvent { + + @Test + void ์˜ˆ์•ฝ_์ทจ์†Œ_์ด๋ฒคํŠธ๋ฅผ_๋ฐœํ–‰ํ•œ๋‹ค() { + ReservationDTO reservation = createReservationDTO(); + LocalDateTime dateTime = LocalDateTime.now().plusDays(1); + ReservationCanceledEvent event = new ReservationCanceledEvent( + reservation, "ํ™๊ธธ๋™", "๋ง›์ง‘", dateTime + ); + + eventPublishService.publishReservationCanceledEvent(event); + + verify(eventPublisher).publishEvent(event); + } + } + + private ReservationDTO createReservationDTO() { + return new ReservationDTO( + 1L, + ReservationStatus.CONFIRMED, + "restaurant-1", + "member-1", + 1L, + 4, + "์ฐฝ๊ฐ€ ์ž๋ฆฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค", + LocalDateTime.now(), + LocalDateTime.now() + ); + } +} diff --git a/api-user/src/test/java/com/wellmeet/global/event/listener/ReservationEventListenerTest.java b/api-user/src/test/java/com/wellmeet/global/event/listener/ReservationEventListenerTest.java index aea867e..f0ab91b 100644 --- a/api-user/src/test/java/com/wellmeet/global/event/listener/ReservationEventListenerTest.java +++ b/api-user/src/test/java/com/wellmeet/global/event/listener/ReservationEventListenerTest.java @@ -4,11 +4,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.ReservationStatus; import com.wellmeet.global.event.event.ReservationCanceledEvent; import com.wellmeet.global.event.event.ReservationCreatedEvent; import com.wellmeet.global.event.event.ReservationUpdatedEvent; @@ -38,16 +35,16 @@ class HandleReservationCreated { @Test void ์˜ˆ์•ฝ_์ƒ์„ฑ_์ด๋ฒคํŠธ๋ฅผ_์ฒ˜๋ฆฌํ•˜์—ฌ_Kafka๋กœ_์•Œ๋ฆผ_๋ฉ”์‹œ์ง€๋ฅผ_๋ฐœ์†กํ•œ๋‹ค() { - Reservation reservation = createReservation(); - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationCreatedEvent event = new ReservationCreatedEvent(reservation, "member", restaurant.getName(), dateTime); + ReservationDTO reservation = createReservationDTO(); + LocalDateTime dateTime = LocalDateTime.now().plusDays(1); + ReservationCreatedEvent event = new ReservationCreatedEvent( + reservation, "ํ™๊ธธ๋™", "๋ง›์ง‘", dateTime + ); reservationEventListener.handleReservationCreated(event); verify(kafkaProducerService).sendNotificationMessage( - eq(restaurant.getId()), + eq(reservation.restaurantId()), any(ReservationCreatedPayload.class) ); } @@ -58,16 +55,16 @@ class HandleReservationUpdated { @Test void ์˜ˆ์•ฝ_์ˆ˜์ •_์ด๋ฒคํŠธ๋ฅผ_์ฒ˜๋ฆฌํ•˜์—ฌ_Kafka๋กœ_์•Œ๋ฆผ_๋ฉ”์‹œ์ง€๋ฅผ_๋ฐœ์†กํ•œ๋‹ค() { - Reservation reservation = createReservation(); - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationUpdatedEvent event = new ReservationUpdatedEvent(reservation, "member", restaurant.getName(), dateTime); + ReservationDTO reservation = createReservationDTO(); + LocalDateTime dateTime = LocalDateTime.now().plusDays(1); + ReservationUpdatedEvent event = new ReservationUpdatedEvent( + reservation, "ํ™๊ธธ๋™", "๋ง›์ง‘", dateTime + ); reservationEventListener.handleReservationUpdated(event); verify(kafkaProducerService).sendNotificationMessage( - eq(restaurant.getId()), + eq(reservation.restaurantId()), any(ReservationUpdatedPayload.class) ); } @@ -78,48 +75,32 @@ class HandleReservationCanceled { @Test void ์˜ˆ์•ฝ_์ทจ์†Œ_์ด๋ฒคํŠธ๋ฅผ_์ฒ˜๋ฆฌํ•˜์—ฌ_Kafka๋กœ_์•Œ๋ฆผ_๋ฉ”์‹œ์ง€๋ฅผ_๋ฐœ์†กํ•œ๋‹ค() { - Reservation reservation = createReservation(); - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - LocalDateTime dateTime = LocalDateTime.of(availableDate.getDate(), availableDate.getTime()); - ReservationCanceledEvent event = new ReservationCanceledEvent(reservation, "member", restaurant.getName(), dateTime); + ReservationDTO reservation = createReservationDTO(); + LocalDateTime dateTime = LocalDateTime.now().plusDays(1); + ReservationCanceledEvent event = new ReservationCanceledEvent( + reservation, "ํ™๊ธธ๋™", "๋ง›์ง‘", dateTime + ); reservationEventListener.handleReservationCanceled(event); verify(kafkaProducerService).sendNotificationMessage( - eq(restaurant.getId()), + eq(reservation.restaurantId()), any(ReservationCanceledPayload.class) ); } } - private Restaurant getRestaurant() { - Owner owner = new Owner("owner", "owner@test.com"); - return new Restaurant( - "์‹๋‹น", - "description", - "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", - 37.5, - 127.0, - "thumbnail.jpg", - owner.getId() - ); - } - - private AvailableDate getAvailableDate(Restaurant restaurant) { - LocalDateTime dateTime = LocalDateTime.now().plusDays(1); - return new AvailableDate( - dateTime.toLocalDate(), - dateTime.toLocalTime(), - 10, - restaurant + private ReservationDTO createReservationDTO() { + return new ReservationDTO( + 1L, + ReservationStatus.CONFIRMED, + "restaurant-1", + "member-1", + 1L, + 4, + "์ฐฝ๊ฐ€ ์ž๋ฆฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค", + LocalDateTime.now(), + LocalDateTime.now() ); } - - private Reservation createReservation() { - Restaurant restaurant = getRestaurant(); - AvailableDate availableDate = getAvailableDate(restaurant); - Member member = new Member("member", "nickname", "email@test.com", "010-1234-5678"); - return new Reservation(restaurant.getId(), availableDate.getId(), member.getId(), 4, "์š”์ฒญ์‚ฌํ•ญ"); - } } diff --git a/api-user/src/test/java/com/wellmeet/reservation/ReservationControllerTest.java b/api-user/src/test/java/com/wellmeet/reservation/ReservationControllerTest.java deleted file mode 100644 index 6c66079..0000000 --- a/api-user/src/test/java/com/wellmeet/reservation/ReservationControllerTest.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.wellmeet.reservation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.wellmeet.BaseControllerTest; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.reservation.entity.ReservationStatus; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.reservation.dto.CreateReservationRequest; -import com.wellmeet.reservation.dto.CreateReservationResponse; -import com.wellmeet.reservation.dto.ReservationResponse; -import com.wellmeet.reservation.dto.SummaryReservationResponse; -import java.time.LocalDateTime; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; - -class ReservationControllerTest extends BaseControllerTest { - - @Nested - class Reserve { - - @Test - void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ• _์ˆ˜_์žˆ๋‹ค() { - Member member = memberGenerator.generate("member"); - Owner owner = ownerGenerator.generate("owner"); - Restaurant restaurant = restaurantGenerator.generate("restaurant", owner.getId()); - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, - restaurant); - int partySize = 4; - String specialRequest = "request"; - - CreateReservationRequest request = new CreateReservationRequest(restaurant.getId(), availableDate.getId(), - partySize, specialRequest); - - CreateReservationResponse response = given() - .contentType("application/json") - .queryParam("memberId", member.getId()) - .body(request) - .when().post("/user/reservation") - .then().statusCode(HttpStatus.CREATED.value()) - .extract().as(CreateReservationResponse.class); - - assertAll( - () -> assertThat(response.getRestaurantName()).isEqualTo(restaurant.getName()), - () -> assertThat(response.getPartySize()).isEqualTo(partySize), - () -> assertThat(response.getStatus()).isEqualTo(ReservationStatus.PENDING), - () -> assertThat(response.getSpecialRequest()).isEqualTo(specialRequest) - ); - } - - @Test - void ๋ ˆ์Šคํ† ๋ž‘_id๋Š”_null์ผ_์ˆ˜_์—†๋‹ค() { - Owner owner = ownerGenerator.generate("owner"); - Restaurant restaurant = restaurantGenerator.generate("restaurant", owner.getId()); - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, - restaurant); - Member member = memberGenerator.generate("member"); - - CreateReservationRequest request = new CreateReservationRequest(null, availableDate.getId(), - 4, "request"); - - given() - .contentType("application/json") - .queryParam("memberId", member.getId()) - .body(request) - .when().post("/user/reservation") - .then().statusCode(HttpStatus.BAD_REQUEST.value()); - } - - @Test - void ์˜ˆ์•ฝ_๊ฐ€๋Šฅ_์‹œ๊ฐ„_id๋Š”_null์ผ_์ˆ˜_์—†๋‹ค() { - Owner owner = ownerGenerator.generate("owner"); - Restaurant restaurant = restaurantGenerator.generate("restaurant", owner.getId()); - availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, restaurant); - Member member = memberGenerator.generate("member"); - - CreateReservationRequest request = new CreateReservationRequest(restaurant.getId(), null, - 4, "request"); - - given() - .contentType("application/json") - .queryParam("memberId", member.getId()) - .body(request) - .when().post("/user/reservation") - .then().statusCode(HttpStatus.BAD_REQUEST.value()); - } - } - - @Nested - class GetReservations { - - @Test - void ๋ฉค๋ฒ„์˜_์˜ˆ์•ฝ_๋ชฉ๋ก์„_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { - Member member = memberGenerator.generate("member"); - Owner owner = ownerGenerator.generate("owner"); - Restaurant restaurant = restaurantGenerator.generate("restaurant", owner.getId()); - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, - restaurant); - AvailableDate availableDate2 = availableDateGenerator.generate(LocalDateTime.now().plusDays(2), 10, - restaurant); - reservationGenerator.generate(restaurant.getId(), availableDate.getId(), member.getId(), 4); - reservationGenerator.generate(restaurant.getId(), availableDate2.getId(), member.getId(), 2); - - SummaryReservationResponse[] reservationResponses = given() - .contentType("application/json") - .queryParam("memberId", member.getId()) - .when().get("/user/reservation") - .then().statusCode(HttpStatus.OK.value()) - .extract().as(SummaryReservationResponse[].class); - - assertThat(reservationResponses).hasSize(2); - } - } - - @Nested - class GetReservation { - - @Test - void ์˜ˆ์•ฝ_์ƒ์„ธ_๋‚ด์—ญ์„_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { - Member member = memberGenerator.generate("member"); - Owner owner = ownerGenerator.generate("owner"); - Restaurant restaurant = restaurantGenerator.generate("restaurant", owner.getId()); - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, - restaurant); - Reservation reservation = reservationGenerator.generate(restaurant.getId(), availableDate.getId(), - member.getId(), 4); - - ReservationResponse response = given() - .contentType("application/json") - .queryParam("memberId", member.getId()) - .when().get("/user/reservation/{reservationId}", reservation.getId()) - .then().statusCode(HttpStatus.OK.value()) - .extract().as(ReservationResponse.class); - - assertThat(response.getId()).isEqualTo(reservation.getId()); - } - } - - @Nested - class UpdateReservation { - - @Test - void ์˜ˆ์•ฝ์„_์—…๋ฐ์ดํŠธ_ํ• _์ˆ˜_์žˆ๋‹ค() { - Member member = memberGenerator.generate("member"); - Owner owner = ownerGenerator.generate("owner"); - Restaurant restaurant = restaurantGenerator.generate("restaurant", owner.getId()); - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, - restaurant); - Reservation reservation = reservationGenerator.generate(restaurant.getId(), availableDate.getId(), - member.getId(), 4); - - CreateReservationRequest request = new CreateReservationRequest(restaurant.getId(), availableDate.getId(), - 6, "updated request"); - - CreateReservationResponse response = given() - .contentType("application/json") - .queryParam("memberId", member.getId()) - .body(request) - .when().put("/user/reservation/update/{reservationId}", reservation.getId()) - .then().statusCode(HttpStatus.OK.value()) - .extract().as(CreateReservationResponse.class); - - assertAll( - () -> assertThat(response.getId()).isEqualTo(reservation.getId()), - () -> assertThat(response.getPartySize()).isEqualTo(6), - () -> assertThat(response.getSpecialRequest()).isEqualTo("updated request") - ); - } - } - - @Nested - class CancelReservation { - - @Test - void ์˜ˆ์•ฝ์„_์ทจ์†Œํ• _์ˆ˜_์žˆ๋‹ค() { - Member member = memberGenerator.generate("member"); - Owner owner = ownerGenerator.generate("owner"); - Restaurant restaurant = restaurantGenerator.generate("restaurant", owner.getId()); - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, - restaurant); - Reservation reservation = reservationGenerator.generate(restaurant.getId(), availableDate.getId(), - member.getId(), 4); - - given() - .contentType("application/json") - .queryParam("memberId", member.getId()) - .when().patch("/user/reservation/cancel/{reservationId}", reservation.getId()) - .then().statusCode(HttpStatus.NO_CONTENT.value()); - } - } -} diff --git a/api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java b/api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java deleted file mode 100644 index a4b3400..0000000 --- a/api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java +++ /dev/null @@ -1,207 +0,0 @@ -package com.wellmeet.reservation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.wellmeet.BaseServiceTest; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.reservation.dto.CreateReservationRequest; -import com.wellmeet.reservation.dto.CreateReservationResponse; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -class ReservationServiceTest extends BaseServiceTest { - - @Autowired - private ReservationService reservationService; - - @Autowired - private ReservationRedisService reservationRedisService; - - @BeforeEach - void setUp() { - reservationRedisService.deleteReservationLock(); - } - - @Nested - class Reserve { - - @Test - void ํ•œ_์‚ฌ๋žŒ์ด_๊ฐ™์€_์˜ˆ์•ฝ_์š”์ฒญ์„_๋™์‹œ์—_์—ฌ๋Ÿฌ๋ฒˆ_์‹ ์ฒญํ•ด๋„_ํ•œ_๋ฒˆ๋งŒ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { - Owner owner1 = ownerGenerator.generate("owner1"); - Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1.getId()); - int capacity = 100; - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), capacity, - restaurant1); - int partySize = 4; - CreateReservationRequest request = new CreateReservationRequest( - restaurant1.getId(), availableDate.getId(), partySize, "request" - ); - Member member = memberGenerator.generate("test"); - - runAtSameTime(500, () -> reservationService.reserve(member.getId(), request)); - List reservations = reservationRepository.findAll(); - AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); - - assertAll( - () -> assertThat(reservations).hasSize(1), - () -> assertThat(foundAvailableDate.getMaxCapacity()).isEqualTo(capacity - partySize) - ); - } - - @Test - void ์—ฌ๋Ÿฌ_์‚ฌ๋žŒ์ด_์˜ˆ์•ฝ_์š”์ฒญ์„_๋™์‹œ์—_์‹ ์ฒญํ•ด๋„_์ ์ ˆํžˆ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { - Owner owner1 = ownerGenerator.generate("owner1"); - Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1.getId()); - int capacity = 100; - AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), capacity, - restaurant1); - int partySize = 2; - CreateReservationRequest request = new CreateReservationRequest( - restaurant1.getId(), availableDate.getId(), partySize, "request" - ); - List tasks = new ArrayList<>(); - for (int i = 0; i < 500; i++) { - Member member = memberGenerator.generate("member" + i); - tasks.add(() -> reservationService.reserve(member.getId(), request)); - } - - runAtSameTime(tasks); - List reservations = reservationRepository.findAll(); - AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); - - assertAll( - () -> assertThat(reservations).hasSize(50), - () -> assertThat(foundAvailableDate.getMaxCapacity()).isZero() - ); - } - } - - @Nested - class UpdateReservation { - - @Test - void ๊ฐ™์€_์˜ˆ์•ฝ์‹œ๊ฐ„์˜_์ธ์›์ˆ˜๋ฅผ_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { - Owner owner1 = ownerGenerator.generate("owner1"); - Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1.getId()); - int capacity = 16; - AvailableDate availableDate1 = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), capacity, - restaurant1); - int partySize = 4; - Member member1 = memberGenerator.generate("member1"); - CreateReservationRequest createRequest1 = new CreateReservationRequest( - restaurant1.getId(), availableDate1.getId(), partySize, "request" - ); - CreateReservationResponse reserve1 = reservationService.reserve(member1.getId(), createRequest1); - int changePartySize = 7; - CreateReservationRequest request1 = new CreateReservationRequest( - restaurant1.getId(), availableDate1.getId(), changePartySize, "request" - ); - - reservationService.updateReservation( - reserve1.getId(), member1.getId(), request1 - ); - List reservations = reservationRepository.findAll(); - AvailableDate foundAvailableDate1 = availableDateRepository.findById(availableDate1.getId()).get(); - - assertAll( - () -> assertThat(reservations).hasSize(1), - () -> assertThat(foundAvailableDate1.getMaxCapacity()).isEqualTo(capacity - changePartySize) - ); - } - - @Test - void ํ•œ_์‚ฌ๋žŒ์ด_์—…๋ฐ์ดํŠธ_์š”์ฒญ์„_๋™์‹œ์—_์—ฌ๋Ÿฌ๊ฐœ_๋ณด๋‚ด๋„_ํ•œ_๋ฒˆ๋งŒ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { - Owner owner1 = ownerGenerator.generate("owner1"); - Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1.getId()); - int capacity = 50; - AvailableDate availableDate1 = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), capacity, - restaurant1); - AvailableDate availableDate2 = availableDateGenerator.generate(LocalDateTime.now().plusDays(2), capacity, - restaurant1); - int partySize = 4; - Member member1 = memberGenerator.generate("member1"); - CreateReservationRequest createRequest1 = new CreateReservationRequest( - restaurant1.getId(), availableDate1.getId(), partySize, "request" - ); - CreateReservationResponse reserve1 = reservationService.reserve(member1.getId(), createRequest1); - int changePartySize = 7; - CreateReservationRequest request1 = new CreateReservationRequest( - restaurant1.getId(), availableDate2.getId(), changePartySize, "request" - ); - - runAtSameTime(2, () -> reservationService.updateReservation( - reserve1.getId(), member1.getId(), request1 - )); - List reservations = reservationRepository.findAll(); - AvailableDate foundAvailableDate1 = availableDateRepository.findById(availableDate1.getId()).get(); - AvailableDate foundAvailableDate2 = availableDateRepository.findById(availableDate2.getId()).get(); - - assertAll( - () -> assertThat(reservations).hasSize(1), - () -> assertThat(foundAvailableDate1.getMaxCapacity()).isEqualTo(capacity), - () -> assertThat(foundAvailableDate2.getMaxCapacity()).isEqualTo(capacity - changePartySize) - ); - } - - @Test - void ์—ฌ๋Ÿฌ_์‚ฌ๋žŒ์ด_์—…๋ฐ์ดํŠธ_์š”์ฒญ์„_๋™์‹œ์—_์—ฌ๋Ÿฌ๊ฐœ_๋ณด๋‚ด๋„_์ ์ ˆํžˆ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { - Owner owner1 = ownerGenerator.generate("owner1"); - Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1.getId()); - int capacity = 16; - AvailableDate availableDate1 = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), capacity, - restaurant1); - AvailableDate availableDate2 = availableDateGenerator.generate(LocalDateTime.now().plusDays(2), capacity, - restaurant1); - AvailableDate availableDate3 = availableDateGenerator.generate(LocalDateTime.now().plusDays(3), capacity, - restaurant1); - int partySize = 4; - Member member1 = memberGenerator.generate("member1"); - Member member2 = memberGenerator.generate("member2"); - CreateReservationRequest createRequest1 = new CreateReservationRequest( - restaurant1.getId(), availableDate1.getId(), partySize, "request" - ); - CreateReservationRequest createRequest2 = new CreateReservationRequest( - restaurant1.getId(), availableDate2.getId(), partySize, "request" - ); - CreateReservationResponse reserve1 = reservationService.reserve(member1.getId(), createRequest1); - CreateReservationResponse reserve2 = reservationService.reserve(member2.getId(), createRequest2); - int changePartySize = 7; - CreateReservationRequest request1 = new CreateReservationRequest( - restaurant1.getId(), availableDate3.getId(), changePartySize, "request" - ); - CreateReservationRequest request2 = new CreateReservationRequest( - restaurant1.getId(), availableDate3.getId(), changePartySize, "request" - ); - List tasks = new ArrayList<>(); - tasks.add(() -> reservationService.updateReservation( - reserve1.getId(), member1.getId(), request1 - )); - tasks.add(() -> reservationService.updateReservation( - reserve2.getId(), member2.getId(), request2 - )); - - runAtSameTime(tasks); - List reservations = reservationRepository.findAll(); - AvailableDate foundAvailableDate1 = availableDateRepository.findById(availableDate1.getId()).get(); - AvailableDate foundAvailableDate2 = availableDateRepository.findById(availableDate2.getId()).get(); - AvailableDate foundAvailableDate3 = availableDateRepository.findById(availableDate3.getId()).get(); - - assertAll( - () -> assertThat(reservations).hasSize(2), - () -> assertThat(foundAvailableDate1.getMaxCapacity()).isEqualTo(capacity), - () -> assertThat(foundAvailableDate2.getMaxCapacity()).isEqualTo(capacity), - () -> assertThat(foundAvailableDate3.getMaxCapacity()).isEqualTo(capacity - changePartySize * 2) - ); - } - } -} diff --git a/api-user/src/test/java/com/wellmeet/reservation/UserReservationBffControllerTest.java b/api-user/src/test/java/com/wellmeet/reservation/UserReservationBffControllerTest.java new file mode 100644 index 0000000..28d4c18 --- /dev/null +++ b/api-user/src/test/java/com/wellmeet/reservation/UserReservationBffControllerTest.java @@ -0,0 +1,227 @@ +package com.wellmeet.reservation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wellmeet.reservation.dto.CreateReservationRequest; +import com.wellmeet.reservation.dto.CreateReservationResponse; +import com.wellmeet.reservation.dto.ReservationResponse; +import com.wellmeet.reservation.dto.ReservationStatus; +import com.wellmeet.reservation.dto.SummaryReservationResponse; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(UserReservationBffController.class) +class UserReservationBffControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private UserReservationBffService reservationService; + + @Nested + class Reserve { + + @Test + void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ• _์ˆ˜_์žˆ๋‹ค() throws Exception { + String memberId = "member-1"; + CreateReservationRequest request = new CreateReservationRequest( + "restaurant-1", 1L, 4, "์ฐฝ๊ฐ€ ์ž๋ฆฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค" + ); + CreateReservationResponse response = CreateReservationResponse.builder() + .id(1L) + .restaurantName("๋ง›์ง‘") + .status(ReservationStatus.PENDING) + .dateTime(LocalDateTime.now().plusDays(1)) + .partySize(4) + .specialRequest("์ฐฝ๊ฐ€ ์ž๋ฆฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค") + .build(); + + when(reservationService.reserve(eq(memberId), any(CreateReservationRequest.class))) + .thenReturn(response); + + mockMvc.perform(post("/user/reservation") + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.restaurantName").value("๋ง›์ง‘")) + .andExpect(jsonPath("$.partySize").value(4)) + .andExpect(jsonPath("$.status").value("PENDING")) + .andExpect(jsonPath("$.specialRequest").value("์ฐฝ๊ฐ€ ์ž๋ฆฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค")); + + verify(reservationService).reserve(eq(memberId), any(CreateReservationRequest.class)); + } + + @Test + void ๋ ˆ์Šคํ† ๋ž‘_id๋Š”_null์ผ_์ˆ˜_์—†๋‹ค() throws Exception { + String memberId = "member-1"; + CreateReservationRequest request = new CreateReservationRequest( + null, 1L, 4, "request" + ); + + mockMvc.perform(post("/user/reservation") + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void ์˜ˆ์•ฝ_๊ฐ€๋Šฅ_์‹œ๊ฐ„_id๋Š”_null์ผ_์ˆ˜_์—†๋‹ค() throws Exception { + String memberId = "member-1"; + CreateReservationRequest request = new CreateReservationRequest( + "restaurant-1", null, 4, "request" + ); + + mockMvc.perform(post("/user/reservation") + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + } + + @Nested + class GetReservations { + + @Test + void ๋ฉค๋ฒ„์˜_์˜ˆ์•ฝ_๋ชฉ๋ก์„_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() throws Exception { + String memberId = "member-1"; + SummaryReservationResponse response1 = SummaryReservationResponse.builder() + .id(1L) + .restaurantName("์‹๋‹น1") + .status(ReservationStatus.CONFIRMED) + .dateTime(LocalDateTime.now().plusDays(1)) + .partySize(4) + .build(); + SummaryReservationResponse response2 = SummaryReservationResponse.builder() + .id(2L) + .restaurantName("์‹๋‹น2") + .status(ReservationStatus.PENDING) + .dateTime(LocalDateTime.now().plusDays(2)) + .partySize(2) + .build(); + + when(reservationService.getReservations(memberId)) + .thenReturn(List.of(response1, response2)); + + mockMvc.perform(get("/user/reservation") + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[1].id").value(2)); + + verify(reservationService).getReservations(memberId); + } + } + + @Nested + class GetReservation { + + @Test + void ์˜ˆ์•ฝ_์ƒ์„ธ_๋‚ด์—ญ์„_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() throws Exception { + String memberId = "member-1"; + Long reservationId = 1L; + ReservationResponse response = ReservationResponse.builder() + .id(reservationId) + .restaurantName("๋ง›์ง‘") + .restaurantAddress("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ") + .restaurantRating(4.5) + .status(ReservationStatus.CONFIRMED) + .dateTime(LocalDateTime.now().plusDays(1)) + .partySize(4) + .specialRequest("์ฐฝ๊ฐ€ ์ž๋ฆฌ") + .build(); + + when(reservationService.getReservation(reservationId, memberId)) + .thenReturn(response); + + mockMvc.perform(get("/user/reservation/{reservationId}", reservationId) + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(reservationId)) + .andExpect(jsonPath("$.restaurantName").value("๋ง›์ง‘")); + + verify(reservationService).getReservation(reservationId, memberId); + } + } + + @Nested + class UpdateReservation { + + @Test + void ์˜ˆ์•ฝ์„_์—…๋ฐ์ดํŠธ_ํ• _์ˆ˜_์žˆ๋‹ค() throws Exception { + String memberId = "member-1"; + Long reservationId = 1L; + CreateReservationRequest request = new CreateReservationRequest( + "restaurant-1", 2L, 6, "์ˆ˜์ •๋œ ์š”์ฒญ์‚ฌํ•ญ" + ); + CreateReservationResponse response = CreateReservationResponse.builder() + .id(reservationId) + .restaurantName("๋ง›์ง‘") + .status(ReservationStatus.CONFIRMED) + .dateTime(LocalDateTime.now().plusDays(2)) + .partySize(6) + .specialRequest("์ˆ˜์ •๋œ ์š”์ฒญ์‚ฌํ•ญ") + .build(); + + when(reservationService.updateReservation(eq(reservationId), eq(memberId), + any(CreateReservationRequest.class))) + .thenReturn(response); + + mockMvc.perform(put("/user/reservation/update/{reservationId}", reservationId) + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(reservationId)) + .andExpect(jsonPath("$.partySize").value(6)) + .andExpect(jsonPath("$.specialRequest").value("์ˆ˜์ •๋œ ์š”์ฒญ์‚ฌํ•ญ")); + + verify(reservationService).updateReservation(eq(reservationId), eq(memberId), + any(CreateReservationRequest.class)); + } + } + + @Nested + class CancelReservation { + + @Test + void ์˜ˆ์•ฝ์„_์ทจ์†Œํ• _์ˆ˜_์žˆ๋‹ค() throws Exception { + String memberId = "member-1"; + Long reservationId = 1L; + + mockMvc.perform(patch("/user/reservation/cancel/{reservationId}", reservationId) + .queryParam("memberId", memberId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + verify(reservationService).cancel(reservationId, memberId); + } + } +} diff --git a/api-user/src/test/java/com/wellmeet/reservation/UserReservationBffServiceTest.java b/api-user/src/test/java/com/wellmeet/reservation/UserReservationBffServiceTest.java new file mode 100644 index 0000000..07bdc6f --- /dev/null +++ b/api-user/src/test/java/com/wellmeet/reservation/UserReservationBffServiceTest.java @@ -0,0 +1,364 @@ +package com.wellmeet.reservation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wellmeet.client.RestaurantAvailableDateFeignClient; +import com.wellmeet.client.MemberFeignClient; +import com.wellmeet.client.ReservationFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.client.dto.request.DecreaseCapacityRequest; +import com.wellmeet.client.dto.request.UpdateReservationDTO; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.MemberDTO; +import com.wellmeet.common.dto.ReservationDTO; +import com.wellmeet.common.dto.ReservationStatus; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.common.dto.request.CreateReservationDTO; +import com.wellmeet.global.event.UserEventPublishBffService; +import com.wellmeet.reservation.dto.CreateReservationRequest; +import com.wellmeet.reservation.dto.CreateReservationResponse; +import com.wellmeet.reservation.dto.ReservationResponse; +import com.wellmeet.reservation.dto.SummaryReservationResponse; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserReservationBffServiceTest { + + @Mock + private ReservationFeignClient reservationClient; + + @Mock + private MemberFeignClient memberClient; + + @Mock + private RestaurantFeignClient restaurantClient; + + @Mock + private RestaurantAvailableDateFeignClient availableDateClient; + + @Mock + private ReservationRedisService reservationRedisService; + + @Mock + private UserEventPublishBffService eventPublishService; + + @InjectMocks + private UserReservationBffService reservationService; + + @Nested + class Reserve { + + @Test + void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ•œ๋‹ค() { + String memberId = "member-1"; + CreateReservationRequest request = new CreateReservationRequest( + "restaurant-1", 1L, 4, "์ฐฝ๊ฐ€ ์ž๋ฆฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค" + ); + + MemberDTO member = createMemberDTO(memberId, "ํ™๊ธธ๋™"); + RestaurantDTO restaurant = createRestaurantDTO("restaurant-1", "๋ง›์ง‘"); + AvailableDateDTO availableDate = createAvailableDateDTO( + 1L, LocalDate.now().plusDays(1), LocalTime.of(18, 0), 10 + ); + ReservationDTO createdReservation = createReservationDTO( + 1L, memberId, "restaurant-1", 1L, 4, "PENDING" + ); + + when(reservationClient.getReservationsByMember(memberId)).thenReturn(List.of()); + when(memberClient.getMember(memberId)).thenReturn(member); + when(restaurantClient.getRestaurant("restaurant-1")).thenReturn(restaurant); + when(restaurantClient.getAvailableDate("restaurant-1", 1L)).thenReturn(availableDate); + when(reservationClient.createReservation(any(CreateReservationDTO.class))) + .thenReturn(createdReservation); + + CreateReservationResponse response = reservationService.reserve(memberId, request); + + assertThat(response.getId()).isEqualTo(1L); + assertThat(response.getRestaurantName()).isEqualTo("๋ง›์ง‘"); + assertThat(response.getPartySize()).isEqualTo(4); + verify(availableDateClient).decreaseCapacity(any(DecreaseCapacityRequest.class)); + verify(eventPublishService).publishReservationCreatedEvent(any()); + } + + @Test + void ์ด๋ฏธ_์˜ˆ์•ฝ๋œ_๋‚ ์งœ๋Š”_์ค‘๋ณต_์˜ˆ์•ฝํ• _์ˆ˜_์—†๋‹ค() { + String memberId = "member-1"; + CreateReservationRequest request = new CreateReservationRequest( + "restaurant-1", 1L, 4, "์ฐฝ๊ฐ€ ์ž๋ฆฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค" + ); + + ReservationDTO existingReservation = createReservationDTO( + 1L, memberId, "restaurant-1", 1L, 2, "CONFIRMED" + ); + + when(reservationClient.getReservationsByMember(memberId)) + .thenReturn(List.of(existingReservation)); + + assertThatThrownBy(() -> reservationService.reserve(memberId, request)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("์ด๋ฏธ ์˜ˆ์•ฝ๋œ ๋‚ ์งœ์ž…๋‹ˆ๋‹ค."); + + verify(availableDateClient, never()).decreaseCapacity(any()); + verify(reservationClient, never()).createReservation(any()); + } + } + + @Nested + class GetReservations { + + @Test + void ํšŒ์›์˜_์˜ˆ์•ฝ_๋ชฉ๋ก์„_์กฐํšŒํ•œ๋‹ค() { + String memberId = "member-1"; + ReservationDTO reservation1 = createReservationDTO( + 1L, memberId, "restaurant-1", 1L, 4, "CONFIRMED" + ); + ReservationDTO reservation2 = createReservationDTO( + 2L, memberId, "restaurant-2", 2L, 2, "PENDING" + ); + + RestaurantDTO restaurant1 = createRestaurantDTO("restaurant-1", "์‹๋‹น1"); + RestaurantDTO restaurant2 = createRestaurantDTO("restaurant-2", "์‹๋‹น2"); + AvailableDateDTO availableDate1 = createAvailableDateDTO( + 1L, LocalDate.now().plusDays(1), LocalTime.of(18, 0), 10 + ); + AvailableDateDTO availableDate2 = createAvailableDateDTO( + 2L, LocalDate.now().plusDays(2), LocalTime.of(19, 0), 10 + ); + + when(reservationClient.getReservationsByMember(memberId)) + .thenReturn(List.of(reservation1, reservation2)); + when(restaurantClient.getRestaurantsByIds(any())) + .thenReturn(List.of(restaurant1, restaurant2)); + when(restaurantClient.getAvailableDate("restaurant-1", 1L)).thenReturn(availableDate1); + when(restaurantClient.getAvailableDate("restaurant-2", 2L)).thenReturn(availableDate2); + + List result = reservationService.getReservations(memberId); + + assertThat(result).hasSize(2); + assertThat(result.get(0).getId()).isEqualTo(1L); + assertThat(result.get(1).getId()).isEqualTo(2L); + } + + @Test + void ์˜ˆ์•ฝ์ด_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + String memberId = "member-1"; + + when(reservationClient.getReservationsByMember(memberId)).thenReturn(List.of()); + + List result = reservationService.getReservations(memberId); + + assertThat(result).isEmpty(); + } + } + + @Nested + class GetReservation { + + @Test + void ์˜ˆ์•ฝ_์ƒ์„ธ_์ •๋ณด๋ฅผ_์กฐํšŒํ•œ๋‹ค() { + Long reservationId = 1L; + String memberId = "member-1"; + ReservationDTO reservation = createReservationDTO( + reservationId, memberId, "restaurant-1", 1L, 4, "CONFIRMED" + ); + RestaurantDTO restaurant = createRestaurantDTO("restaurant-1", "๋ง›์ง‘"); + AvailableDateDTO availableDate = createAvailableDateDTO( + 1L, LocalDate.now().plusDays(1), LocalTime.of(18, 0), 10 + ); + + when(reservationClient.getReservation(reservationId)).thenReturn(reservation); + when(restaurantClient.getRestaurant("restaurant-1")).thenReturn(restaurant); + when(restaurantClient.getAvailableDate("restaurant-1", 1L)).thenReturn(availableDate); + when(restaurantClient.getAverageRating("restaurant-1")).thenReturn(4.5); + + ReservationResponse response = reservationService.getReservation(reservationId, memberId); + + assertThat(response.getId()).isEqualTo(reservationId); + assertThat(response.getRestaurantName()).isEqualTo("๋ง›์ง‘"); + assertThat(response.getRestaurantRating()).isEqualTo(4.5); + } + + @Test + void ๋ณธ์ธ์˜_์˜ˆ์•ฝ์ด_์•„๋‹ˆ๋ฉด_์กฐํšŒํ• _์ˆ˜_์—†๋‹ค() { + Long reservationId = 1L; + String memberId = "member-1"; + String otherMemberId = "member-2"; + ReservationDTO reservation = createReservationDTO( + reservationId, otherMemberId, "restaurant-1", 1L, 4, "CONFIRMED" + ); + + when(reservationClient.getReservation(reservationId)).thenReturn(reservation); + + assertThatThrownBy(() -> reservationService.getReservation(reservationId, memberId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); + } + } + + @Nested + class UpdateReservation { + + @Test + void ์˜ˆ์•ฝ์„_์ˆ˜์ •ํ•œ๋‹ค() { + Long reservationId = 1L; + String memberId = "member-1"; + CreateReservationRequest request = new CreateReservationRequest( + "restaurant-1", 2L, 6, "์ˆ˜์ •๋œ ์š”์ฒญ์‚ฌํ•ญ" + ); + + ReservationDTO existingReservation = createReservationDTO( + reservationId, memberId, "restaurant-1", 1L, 4, "CONFIRMED" + ); + AvailableDateDTO oldAvailableDate = createAvailableDateDTO( + 1L, LocalDate.now().plusDays(1), LocalTime.of(18, 0), 10 + ); + AvailableDateDTO newAvailableDate = createAvailableDateDTO( + 2L, LocalDate.now().plusDays(2), LocalTime.of(19, 0), 10 + ); + ReservationDTO updatedReservation = createReservationDTO( + reservationId, memberId, "restaurant-1", 2L, 6, "CONFIRMED" + ); + MemberDTO member = createMemberDTO(memberId, "ํ™๊ธธ๋™"); + RestaurantDTO restaurant = createRestaurantDTO("restaurant-1", "๋ง›์ง‘"); + + when(reservationClient.getReservation(reservationId)).thenReturn(existingReservation); + when(restaurantClient.getAvailableDate("restaurant-1", 2L)).thenReturn(newAvailableDate); + when(restaurantClient.getAvailableDate("restaurant-1", 1L)).thenReturn(oldAvailableDate); + when(reservationClient.updateReservation(any(Long.class), any(UpdateReservationDTO.class))) + .thenReturn(updatedReservation); + when(memberClient.getMember(memberId)).thenReturn(member); + when(restaurantClient.getRestaurant("restaurant-1")).thenReturn(restaurant); + + CreateReservationResponse response = reservationService.updateReservation( + reservationId, memberId, request + ); + + assertThat(response.getId()).isEqualTo(reservationId); + assertThat(response.getPartySize()).isEqualTo(6); + verify(availableDateClient).increaseCapacity(any()); + verify(availableDateClient).decreaseCapacity(any()); + verify(eventPublishService).publishReservationUpdatedEvent(any()); + } + } + + @Nested + class Cancel { + + @Test + void ์˜ˆ์•ฝ์„_์ทจ์†Œํ•œ๋‹ค() { + Long reservationId = 1L; + String memberId = "member-1"; + ReservationDTO reservation = createReservationDTO( + reservationId, memberId, "restaurant-1", 1L, 4, "CONFIRMED" + ); + AvailableDateDTO availableDate = createAvailableDateDTO( + 1L, LocalDate.now().plusDays(1), LocalTime.of(18, 0), 10 + ); + MemberDTO member = createMemberDTO(memberId, "ํ™๊ธธ๋™"); + RestaurantDTO restaurant = createRestaurantDTO("restaurant-1", "๋ง›์ง‘"); + + when(reservationClient.getReservation(reservationId)).thenReturn(reservation); + when(restaurantClient.getAvailableDate("restaurant-1", 1L)).thenReturn(availableDate); + when(memberClient.getMember(memberId)).thenReturn(member); + when(restaurantClient.getRestaurant("restaurant-1")).thenReturn(restaurant); + + reservationService.cancel(reservationId, memberId); + + verify(availableDateClient).increaseCapacity(any()); + verify(reservationClient).cancelReservation(reservationId); + verify(eventPublishService).publishReservationCanceledEvent(any()); + } + + @Test + void ๋ณธ์ธ์˜_์˜ˆ์•ฝ์ด_์•„๋‹ˆ๋ฉด_์ทจ์†Œํ• _์ˆ˜_์—†๋‹ค() { + Long reservationId = 1L; + String memberId = "member-1"; + String otherMemberId = "member-2"; + ReservationDTO reservation = createReservationDTO( + reservationId, otherMemberId, "restaurant-1", 1L, 4, "CONFIRMED" + ); + + when(reservationClient.getReservation(reservationId)).thenReturn(reservation); + + assertThatThrownBy(() -> reservationService.cancel(reservationId, memberId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); + + verify(availableDateClient, never()).increaseCapacity(any()); + verify(reservationClient, never()).cancelReservation(any()); + } + } + + private MemberDTO createMemberDTO(String id, String name) { + return new MemberDTO( + id, + name, + name + "_nick", + name + "@test.com", + "010-1234-5678", + true, + true, + true, + false, + null, + null + ); + } + + private RestaurantDTO createRestaurantDTO(String id, String name) { + return new RestaurantDTO( + id, + name, + "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", + 37.5, + 127.0, + "thumbnail.jpg", + "owner-1", + null, + null + ); + } + + private AvailableDateDTO createAvailableDateDTO(Long id, LocalDate date, LocalTime time, int capacity) { + return new AvailableDateDTO( + id, + date, + time, + capacity, + true, + "restaurant-1", + null, + null + ); + } + + private ReservationDTO createReservationDTO( + Long id, String memberId, String restaurantId, Long availableDateId, int partySize, String status + ) { + return new ReservationDTO( + id, + ReservationStatus.valueOf(status), + restaurantId, + memberId, + availableDateId, + partySize, + "์š”์ฒญ์‚ฌํ•ญ", + LocalDateTime.now(), + LocalDateTime.now() + ); + } +} diff --git a/api-user/src/test/java/com/wellmeet/restaurant/DomainRestaurantControllerTest.java b/api-user/src/test/java/com/wellmeet/restaurant/DomainRestaurantControllerTest.java deleted file mode 100644 index 48a2f68..0000000 --- a/api-user/src/test/java/com/wellmeet/restaurant/DomainRestaurantControllerTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.wellmeet.restaurant; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.wellmeet.BaseControllerTest; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.restaurant.dto.AvailableDateResponse; -import com.wellmeet.restaurant.dto.NearbyRestaurantResponse; -import com.wellmeet.restaurant.dto.RestaurantResponse; -import io.restassured.http.ContentType; -import java.time.LocalDateTime; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; - -class DomainRestaurantControllerTest extends BaseControllerTest { - - private static final double LATITUDE = 38.5; - private static final double LONGITUDE = 128.2; - - @Nested - class GetNearbyRestaurants { - - @Test - void ์ฃผ๋ณ€_๋ ˆ์Šคํ† ๋ž‘_์กฐํšŒ() { - Owner owner = ownerGenerator.generate("owner1"); - restaurantGenerator.generate("restaurant1", LATITUDE, LONGITUDE, owner.getId()); - restaurantGenerator.generate("restaurant2", LATITUDE, LONGITUDE, owner.getId()); - restaurantGenerator.generate("restaurant3", LATITUDE + 5, LONGITUDE - 5, owner.getId()); - - NearbyRestaurantResponse[] responses = given() - .contentType(ContentType.JSON) - .when().get("/user/restaurant/nearby?latitude=" + LATITUDE + "&longitude=" + LONGITUDE) - .then().statusCode(HttpStatus.OK.value()) - .extract().as(NearbyRestaurantResponse[].class); - - assertThat(responses).hasSize(2); - } - } - - @Nested - class GetRestaurant { - - @Test - void ๋ ˆ์Šคํ† ๋ž‘_์ƒ์„ธ_์กฐํšŒ() { - Owner owner = ownerGenerator.generate("owner1"); - Restaurant restaurant = restaurantGenerator.generate("restaurant1", owner.getId()); - menuGenerator.generate("menu1", 10000, restaurant); - menuGenerator.generate("menu2", 15000, restaurant); - Member member = memberGenerator.generate("testMember"); - reviewGenerator.generate(5, restaurant, member.getId()); - reviewGenerator.generate(4, restaurant, member.getId()); - - RestaurantResponse restaurantResponse = given() - .contentType(ContentType.JSON) - .queryParam("memberId", member.getId()) - .when().get("/user/restaurant/{id}", restaurant.getId()) - .then().statusCode(HttpStatus.OK.value()) - .extract().as(RestaurantResponse.class); - - assertThat(restaurantResponse.getId()).isEqualTo(restaurant.getId()); - assertThat(restaurantResponse.getMenus()).hasSize(2); - assertThat(restaurantResponse.getReviews()).hasSize(2); - } - } - - @Nested - class GetRestaurantAvailableDates { - - @Test - void ์˜ˆ์•ฝ_๊ฐ€๋Šฅ_์‹œ๊ฐ„_์กฐํšŒ() { - Owner owner = ownerGenerator.generate("owner1"); - Restaurant restaurant = restaurantGenerator.generate("restaurant1", owner.getId()); - availableDateGenerator.generate(LocalDateTime.now().plusDays(1), 10, restaurant); - availableDateGenerator.generate(LocalDateTime.now().plusDays(2), 20, restaurant); - - AvailableDateResponse[] responses = given() - .contentType(ContentType.JSON) - .when().get("/user/restaurant/{id}/available", restaurant.getId()) - .then().statusCode(HttpStatus.OK.value()) - .extract().as(AvailableDateResponse[].class); - - assertThat(responses).hasSize(2); - } - } -} diff --git a/api-user/src/test/java/com/wellmeet/restaurant/UserRestaurantBffControllerTest.java b/api-user/src/test/java/com/wellmeet/restaurant/UserRestaurantBffControllerTest.java new file mode 100644 index 0000000..9d2a872 --- /dev/null +++ b/api-user/src/test/java/com/wellmeet/restaurant/UserRestaurantBffControllerTest.java @@ -0,0 +1,180 @@ +package com.wellmeet.restaurant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.wellmeet.BaseControllerTest; +import com.wellmeet.client.dto.ReviewDTO; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.MenuDTO; +import com.wellmeet.common.dto.RestaurantDTO; +import com.wellmeet.restaurant.dto.AvailableDateResponse; +import com.wellmeet.restaurant.dto.NearbyRestaurantResponse; +import com.wellmeet.restaurant.dto.RepresentativeMenuResponse; +import com.wellmeet.restaurant.dto.RepresentativeReviewResponse; +import com.wellmeet.restaurant.dto.RestaurantResponse; +import io.restassured.http.ContentType; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +class UserRestaurantBffControllerTest extends BaseControllerTest { + + private static final double LATITUDE = 38.5; + private static final double LONGITUDE = 128.2; + + @MockitoBean + private UserRestaurantBffService restaurantService; + + @Nested + class GetNearbyRestaurants { + + @Test + void ์ฃผ๋ณ€_๋ ˆ์Šคํ† ๋ž‘_์กฐํšŒ() { + RestaurantDTO restaurant1 = new RestaurantDTO( + "restaurant-1", + "์‹๋‹น1", + "์„œ์šธ์‹œ", + LATITUDE, + LONGITUDE, + "thumbnail1.jpg", + "owner-1", + null, + null + ); + RestaurantDTO restaurant2 = new RestaurantDTO( + "restaurant-2", + "์‹๋‹น2", + "์„œ์šธ์‹œ", + LATITUDE, + LONGITUDE, + "thumbnail2.jpg", + "owner-1", + null, + null + ); + + NearbyRestaurantResponse response1 = new NearbyRestaurantResponse(restaurant1, 0.5, 4.5); + NearbyRestaurantResponse response2 = new NearbyRestaurantResponse(restaurant2, 0.8, 4.0); + + when(restaurantService.findWithNearbyRestaurant(LATITUDE, LONGITUDE)) + .thenReturn(List.of(response1, response2)); + + NearbyRestaurantResponse[] responses = given() + .contentType(ContentType.JSON) + .when().get("/user/restaurant/nearby?latitude=" + LATITUDE + "&longitude=" + LONGITUDE) + .then().statusCode(HttpStatus.OK.value()) + .extract().as(NearbyRestaurantResponse[].class); + + assertThat(responses).hasSize(2); + } + } + + @Nested + class GetRestaurant { + + @Test + void ๋ ˆ์Šคํ† ๋ž‘_์ƒ์„ธ_์กฐํšŒ() { + String restaurantId = "restaurant-1"; + String memberId = "member-1"; + + RestaurantDTO restaurant = new RestaurantDTO( + restaurantId, + "ํ…Œ์ŠคํŠธ ์‹๋‹น", + "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", + 37.5, + 127.0, + "thumbnail.jpg", + "owner-1", + null, + null + ); + + MenuDTO menu1 = new MenuDTO(1L, "๋ฉ”๋‰ด1", "์„ค๋ช…1", 10000, restaurantId, null, null); + MenuDTO menu2 = new MenuDTO(2L, "๋ฉ”๋‰ด2", "์„ค๋ช…2", 15000, restaurantId, null, null); + List menus = List.of( + new RepresentativeMenuResponse(menu1), + new RepresentativeMenuResponse(menu2) + ); + + ReviewDTO review1 = new ReviewDTO(1L, "๋ง›์žˆ์–ด์š”", 5.0, "DATE", restaurantId, memberId); + ReviewDTO review2 = new ReviewDTO(2L, "์ข‹์•„์š”", 4.0, "FAMILY", restaurantId, memberId); + List reviews = List.of( + new RepresentativeReviewResponse(review1), + new RepresentativeReviewResponse(review2) + ); + + RestaurantResponse restaurantResponse = new RestaurantResponse( + restaurant, + reviews, + menus, + true, + 4.5 + ); + + when(restaurantService.getRestaurant(restaurantId, memberId)) + .thenReturn(restaurantResponse); + + RestaurantResponse response = given() + .contentType(ContentType.JSON) + .queryParam("memberId", memberId) + .when().get("/user/restaurant/{id}", restaurantId) + .then().statusCode(HttpStatus.OK.value()) + .extract().as(RestaurantResponse.class); + + assertThat(response.getId()).isEqualTo(restaurantId); + assertThat(response.getMenus()).hasSize(2); + assertThat(response.getReviews()).hasSize(2); + } + } + + @Nested + class GetRestaurantAvailableDates { + + @Test + void ์˜ˆ์•ฝ_๊ฐ€๋Šฅ_์‹œ๊ฐ„_์กฐํšŒ() { + String restaurantId = "restaurant-1"; + + AvailableDateDTO availableDate1 = new AvailableDateDTO( + 1L, + LocalDate.now().plusDays(1), + LocalTime.of(18, 0), + 10, + true, + restaurantId, + null, + null + ); + AvailableDateDTO availableDate2 = new AvailableDateDTO( + 2L, + LocalDate.now().plusDays(2), + LocalTime.of(19, 0), + 20, + true, + restaurantId, + null, + null + ); + + List availableDateResponses = List.of( + new AvailableDateResponse(availableDate1), + new AvailableDateResponse(availableDate2) + ); + + when(restaurantService.getRestaurantAvailableDates(restaurantId)) + .thenReturn(availableDateResponses); + + AvailableDateResponse[] responses = given() + .contentType(ContentType.JSON) + .when().get("/user/restaurant/{id}/available", restaurantId) + .then().statusCode(HttpStatus.OK.value()) + .extract().as(AvailableDateResponse[].class); + + assertThat(responses).hasSize(2); + } + } +} \ No newline at end of file diff --git a/api-user/src/test/java/com/wellmeet/restaurant/DomainRestaurantServiceTest.java b/api-user/src/test/java/com/wellmeet/restaurant/UserRestaurantBffServiceTest.java similarity index 50% rename from api-user/src/test/java/com/wellmeet/restaurant/DomainRestaurantServiceTest.java rename to api-user/src/test/java/com/wellmeet/restaurant/UserRestaurantBffServiceTest.java index e3585be..9b2ba72 100644 --- a/api-user/src/test/java/com/wellmeet/restaurant/DomainRestaurantServiceTest.java +++ b/api-user/src/test/java/com/wellmeet/restaurant/UserRestaurantBffServiceTest.java @@ -4,15 +4,13 @@ import static org.assertj.core.api.Assertions.tuple; import static org.mockito.Mockito.when; -import com.wellmeet.domain.member.FavoriteRestaurantDomainService; -import com.wellmeet.domain.member.entity.Member; -import com.wellmeet.domain.owner.entity.Owner; -import com.wellmeet.domain.restaurant.RestaurantDomainService; -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import com.wellmeet.domain.restaurant.entity.Restaurant; -import com.wellmeet.domain.restaurant.menu.entity.Menu; -import com.wellmeet.domain.restaurant.review.entity.Review; -import com.wellmeet.domain.restaurant.review.entity.Situation; +import com.wellmeet.client.RestaurantAvailableDateFeignClient; +import com.wellmeet.client.MemberFavoriteRestaurantFeignClient; +import com.wellmeet.client.RestaurantFeignClient; +import com.wellmeet.client.dto.ReviewDTO; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.common.dto.MenuDTO; +import com.wellmeet.common.dto.RestaurantDTO; import com.wellmeet.restaurant.dto.AvailableDateResponse; import com.wellmeet.restaurant.dto.NearbyRestaurantResponse; import com.wellmeet.restaurant.dto.RestaurantResponse; @@ -27,16 +25,19 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class DomainRestaurantServiceTest { +class UserRestaurantBffServiceTest { @Mock - private RestaurantDomainService restaurantDomainService; + private RestaurantFeignClient restaurantClient; @Mock - private FavoriteRestaurantDomainService favoriteRestaurantDomainService; + private MemberFavoriteRestaurantFeignClient favoriteRestaurantClient; + + @Mock + private RestaurantAvailableDateFeignClient availableDateClient; @InjectMocks - private RestaurantService restaurantService; + private UserRestaurantBffService restaurantService; @Nested class FindWithNearbyRestaurant { @@ -45,15 +46,35 @@ class FindWithNearbyRestaurant { void ์ฃผ๋ณ€_์‹๋‹น์„_์กฐํšŒํ•œ๋‹ค() { double latitude = 37.5; double longitude = 127.0; - Restaurant restaurant1 = createRestaurant("restaurant-1", "์‹๋‹น1", 37.501, 127.001); - Restaurant restaurant2 = createRestaurant("restaurant-2", "์‹๋‹น2", 37.502, 127.002); - List restaurants = List.of(restaurant1, restaurant2); - - when(restaurantDomainService.findWithBoundBox(latitude, longitude)) + RestaurantDTO restaurant1 = new RestaurantDTO( + "restaurant-1", + "์‹๋‹น1", + "์„œ์šธ์‹œ", + 37.501, + 127.001, + "thumbnail.jpg", + "owner-1", + null, + null + ); + RestaurantDTO restaurant2 = new RestaurantDTO( + "restaurant-2", + "์‹๋‹น2", + "์„œ์šธ์‹œ", + 37.502, + 127.002, + "thumbnail.jpg", + "owner-1", + null, + null + ); + List restaurants = List.of(restaurant1, restaurant2); + + when(restaurantClient.getAllRestaurants()) .thenReturn(restaurants); - when(restaurantDomainService.getAverageRating("restaurant-1")) + when(restaurantClient.getAverageRating("restaurant-1")) .thenReturn(4.5); - when(restaurantDomainService.getAverageRating("restaurant-2")) + when(restaurantClient.getAverageRating("restaurant-2")) .thenReturn(4.0); List responses = restaurantService.findWithNearbyRestaurant(latitude, longitude); @@ -72,7 +93,7 @@ class FindWithNearbyRestaurant { double latitude = 37.5; double longitude = 127.0; - when(restaurantDomainService.findWithBoundBox(latitude, longitude)) + when(restaurantClient.getAllRestaurants()) .thenReturn(List.of()); List responses = restaurantService.findWithNearbyRestaurant(latitude, longitude); @@ -88,19 +109,29 @@ class GetRestaurant { void ์‹๋‹น_์ƒ์„ธ_์ •๋ณด๋ฅผ_์กฐํšŒํ•œ๋‹ค() { String restaurantId = "restaurant-1"; String memberId = "member-1"; - Restaurant restaurant = createRestaurant(restaurantId, "์‹๋‹น1", 37.5, 127.0); - Review review = createReview(restaurant); - Menu menu = createMenu(restaurant); - - when(favoriteRestaurantDomainService.isFavorite(memberId, restaurantId)) + RestaurantDTO restaurant = new RestaurantDTO( + restaurantId, + "์‹๋‹น1", + "์„œ์šธ์‹œ", + 37.5, + 127.0, + "thumbnail.jpg", + "owner-1", + null, + null + ); + ReviewDTO review = new ReviewDTO(1L, "๋ง›์žˆ์–ด์š”", 4.5, "DATE", restaurantId, memberId); + MenuDTO menu = new MenuDTO(1L, "๋ฉ”๋‰ด1", "๋ง›์žˆ๋Š” ๋ฉ”๋‰ด", 10000, restaurantId, null, null); + + when(favoriteRestaurantClient.isFavorite(memberId, restaurantId)) .thenReturn(true); - when(restaurantDomainService.getById(restaurantId)) + when(restaurantClient.getRestaurant(restaurantId)) .thenReturn(restaurant); - when(restaurantDomainService.getReviewByRestaurantId(restaurantId)) + when(restaurantClient.getReviewsByRestaurant(restaurantId)) .thenReturn(List.of(review)); - when(restaurantDomainService.getMenuByRestaurantId(restaurantId)) + when(restaurantClient.getMenusByRestaurant(restaurantId)) .thenReturn(List.of(menu)); - when(restaurantDomainService.getAverageRating(restaurantId)) + when(restaurantClient.getAverageRating(restaurantId)) .thenReturn(4.5); RestaurantResponse response = restaurantService.getRestaurant(restaurantId, memberId); @@ -116,17 +147,27 @@ class GetRestaurant { void ์ฆ๊ฒจ์ฐพ๊ธฐํ•˜์ง€_์•Š์€_์‹๋‹น์„_์กฐํšŒํ•œ๋‹ค() { String restaurantId = "restaurant-1"; String memberId = "member-1"; - Restaurant restaurant = createRestaurant(restaurantId, "์‹๋‹น1", 37.5, 127.0); - - when(favoriteRestaurantDomainService.isFavorite(memberId, restaurantId)) + RestaurantDTO restaurant = new RestaurantDTO( + restaurantId, + "์‹๋‹น1", + "์„œ์šธ์‹œ", + 37.5, + 127.0, + "thumbnail.jpg", + "owner-1", + null, + null + ); + + when(favoriteRestaurantClient.isFavorite(memberId, restaurantId)) .thenReturn(false); - when(restaurantDomainService.getById(restaurantId)) + when(restaurantClient.getRestaurant(restaurantId)) .thenReturn(restaurant); - when(restaurantDomainService.getReviewByRestaurantId(restaurantId)) + when(restaurantClient.getReviewsByRestaurant(restaurantId)) .thenReturn(List.of()); - when(restaurantDomainService.getMenuByRestaurantId(restaurantId)) + when(restaurantClient.getMenusByRestaurant(restaurantId)) .thenReturn(List.of()); - when(restaurantDomainService.getAverageRating(restaurantId)) + when(restaurantClient.getAverageRating(restaurantId)) .thenReturn(0.0); RestaurantResponse response = restaurantService.getRestaurant(restaurantId, memberId); @@ -141,13 +182,28 @@ class GetRestaurantAvailableDates { @Test void ์‹๋‹น์˜_์˜ˆ์•ฝ_๊ฐ€๋Šฅํ•œ_๋‚ ์งœ๋ฅผ_์กฐํšŒํ•œ๋‹ค() { String restaurantId = "restaurant-1"; - Restaurant restaurant = createRestaurant(restaurantId, "์‹๋‹น1", 37.5, 127.0); - AvailableDate availableDate1 = createAvailableDate(LocalDate.now().plusDays(1), LocalTime.of(18, 0), 10, - restaurant); - AvailableDate availableDate2 = createAvailableDate(LocalDate.now().plusDays(2), LocalTime.of(19, 0), 5, - restaurant); - - when(restaurantDomainService.getRestaurantAvailableDates(restaurantId)) + AvailableDateDTO availableDate1 = new AvailableDateDTO( + 1L, + LocalDate.now().plusDays(1), + LocalTime.of(18, 0), + 10, + true, + restaurantId, + null, + null + ); + AvailableDateDTO availableDate2 = new AvailableDateDTO( + 2L, + LocalDate.now().plusDays(2), + LocalTime.of(19, 0), + 5, + true, + restaurantId, + null, + null + ); + + when(availableDateClient.getAvailableDatesByRestaurant(restaurantId)) .thenReturn(List.of(availableDate1, availableDate2)); List responses = restaurantService.getRestaurantAvailableDates(restaurantId); @@ -159,7 +215,7 @@ class GetRestaurantAvailableDates { void ์˜ˆ์•ฝ_๊ฐ€๋Šฅํ•œ_๋‚ ์งœ๊ฐ€_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { String restaurantId = "restaurant-1"; - when(restaurantDomainService.getRestaurantAvailableDates(restaurantId)) + when(availableDateClient.getAvailableDatesByRestaurant(restaurantId)) .thenReturn(List.of()); List responses = restaurantService.getRestaurantAvailableDates(restaurantId); @@ -167,22 +223,4 @@ class GetRestaurantAvailableDates { assertThat(responses).isEmpty(); } } - - private Restaurant createRestaurant(String id, String name, double lat, double lon) { - Owner owner = new Owner("owner-name", "owner@email.com"); - return new Restaurant(id, name, "์„œ์šธ์‹œ", lat, lon, "thumbnail.jpg", owner.getId()); - } - - private Review createReview(Restaurant restaurant) { - Member member = new Member("member", "nickname", "email@test.com", "010-1234-5678"); - return new Review("๋ง›์žˆ์–ด์š”", 4.5, Situation.DATE, restaurant, member.getId()); - } - - private Menu createMenu(Restaurant restaurant) { - return new Menu("๋ฉ”๋‰ด1", "๋ง›์žˆ๋Š” ๋ฉ”๋‰ด", 10000, restaurant); - } - - private AvailableDate createAvailableDate(LocalDate date, LocalTime time, int capacity, Restaurant restaurant) { - return new AvailableDate(date, time, capacity, restaurant); - } -} +} \ No newline at end of file diff --git a/batch-reminder/src/main/resources/application-dev.yml b/batch-reminder/src/main/resources/application-dev.yml deleted file mode 100644 index b994744..0000000 --- a/batch-reminder/src/main/resources/application-dev.yml +++ /dev/null @@ -1,17 +0,0 @@ -spring: - config: - import: - - classpath:dev-secret.yml - - classpath:application-domain-dev.yml - - classpath:application-infra-kafka-dev.yml - - batch: - job: - enabled: false - jdbc: - initialize-schema: always - -logging: - level: - com.wellmeet: DEBUG - org.springframework.batch: INFO diff --git a/batch-reminder/src/main/resources/application-local.yml b/batch-reminder/src/main/resources/application-local.yml deleted file mode 100644 index 8302318..0000000 --- a/batch-reminder/src/main/resources/application-local.yml +++ /dev/null @@ -1,16 +0,0 @@ -spring: - config: - import: - - classpath:application-domain-local.yml - - classpath:application-infra-kafka-local.yml - - batch: - job: - enabled: false - jdbc: - initialize-schema: always - -logging: - level: - com.wellmeet: DEBUG - org.springframework.batch: INFO diff --git a/batch-reminder/src/main/resources/application-test.yml b/batch-reminder/src/main/resources/application-test.yml deleted file mode 100644 index 2afa9c7..0000000 --- a/batch-reminder/src/main/resources/application-test.yml +++ /dev/null @@ -1,25 +0,0 @@ -spring: - config: - import: - - classpath:application-domain-test.yml - - classpath:application-infra-kafka-test.yml - - main: - allow-bean-definition-overriding: true - - batch: - job: - enabled: false - jdbc: - initialize-schema: always - - sql: - init: - mode: always - schema-locations: classpath:org/springframework/batch/core/schema-mysql.sql - continue-on-error: true - -logging: - level: - com.wellmeet: DEBUG - org.springframework.batch: INFO diff --git a/claudedocs/README.md b/claudedocs/README.md new file mode 100644 index 0000000..b718177 --- /dev/null +++ b/claudedocs/README.md @@ -0,0 +1,186 @@ +# WellMeet-Backend Microservices ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +## ๐Ÿ“Œ ํ˜„์žฌ ์ƒํƒœ (2025-11-06) + +### โœ… ์™„๋ฃŒ๋œ Phase + +| Phase | ์ƒํƒœ | ์™„๋ฃŒ์ผ | ์ฃผ์š” ์„ฑ๊ณผ | +|-------|------|--------|---------| +| **Phase 1** | โœ… ์™„๋ฃŒ | 2025-11-05 | domain-restaurant ๋…๋ฆฝ ๋ฐฐํฌ (์ฝ”๋“œ ์™„์„ฑ) | +| **Phase 2** | โœ… ์™„๋ฃŒ | 2025-11-05 | domain-member ๋…๋ฆฝ ๋ฐฐํฌ (์ฝ”๋“œ ์™„์„ฑ) | +| **Phase 3** | โœ… ์™„๋ฃŒ | 2025-11-05 | domain-owner ๋…๋ฆฝ ๋ฐฐํฌ (์ฝ”๋“œ ์™„์„ฑ) | +| **Phase 4** | โœ… ์™„๋ฃŒ | 2025-11-05 | domain-reservation ๋…๋ฆฝ ๋ฐฐํฌ (์ฝ”๋“œ ์™„์„ฑ) | +| **Phase 5** | โœ… ์™„๋ฃŒ | 2025-11-05 | **BFF ์ „ํ™˜ ์™„๋ฃŒ** (Feign Client, testFixtures ์ œ๊ฑฐ) | + +### ๐ŸŽฏ ์ฃผ์š” ์„ฑ๊ณผ (Phase 5 ์™„๋ฃŒ) + +**์™„์ „ํ•œ BFF ํŒจํ„ด ๊ตฌํ˜„**: +- โœ… 10๊ฐœ Feign Client ๊ตฌํ˜„ (4๊ฐœ domain ์„œ๋น„์Šค) +- โœ… api-user, api-owner์—์„œ domain-* ์ง์ ‘ ์˜์กด์„ฑ ์™„์ „ ์ œ๊ฑฐ +- โœ… 15๊ฐœ DTO ํด๋ž˜์Šค ์ƒ์„ฑ (Response/Request ํŒจํ„ด) +- โœ… testFixtures ์™„์ „ ์ œ๊ฑฐ, Mock ํŒจํ„ด์œผ๋กœ ์ „ํ™˜ +- โœ… ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์†๋„ 3-5๋ฐฐ ๊ฐœ์„  + +**์•„ํ‚คํ…์ฒ˜ ์ „ํ™˜**: +- โœ… Monolithic โ†’ Microservices ์•„ํ‚คํ…์ฒ˜ +- โœ… ์ง์ ‘ ์˜์กด์„ฑ โ†’ HTTP ํ†ต์‹  (Feign Client) +- โœ… testFixtures โ†’ Mock ๊ธฐ๋ฐ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ +- โœ… ๋ฐฐ์น˜ ์กฐํšŒ ํŒจํ„ด์œผ๋กœ N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ +- โœ… ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๊ตฌํ˜„ (UserReservationBffService) + +--- + +### ๐Ÿ”œ ์˜ˆ์ •๋œ Phase + +| Phase | ์ƒํƒœ | ๋ชฉํ‘œ | ์˜ˆ์ƒ ๊ธฐ๊ฐ„ | +|-------|------|------|---------| +| **Phase 6** | โณ ์˜ˆ์ • | Saga Orchestration ํŒจํ„ด ๊ตฌํ˜„ | 4-6์ฃผ | +| **Phase 7** | โณ ์˜ˆ์ • | API Gateway ๊ตฌํ˜„ | 3-4์ฃผ | + +--- + +## ๐Ÿ“‚ ๋ฌธ์„œ ๊ตฌ์กฐ + +### ๋ฉ”์ธ ๋ฌธ์„œ + +**`microservices-migration-plan.md`** (462์ค„) +- ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš ๋ฐ Phase 1-7 ์š”์•ฝ +- Phase 1-5: ์™„๋ฃŒ ์ƒํƒœ ์š”์•ฝ +- Phase 6-7: ํ–ฅํ›„ ๊ณ„ํš +- ํฌํŠธ ํ• ๋‹น, ํƒ€์ž„๋ผ์ธ, ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ + +### ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๋ฌธ์„œ + +**`/CLAUDE.md`** +- ํ”„๋กœ์ ํŠธ ์ „์ฒด ๊ตฌ์กฐ ๋ฐ ํ…Œ์ŠคํŠธ ์ „๋žต +- ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ +- ๋ชจ๋“ˆ๋ณ„ ์ฑ…์ž„ ๋ฐ ์˜์กด์„ฑ +- ์•„ํ‚คํ…์ฒ˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กœ๋“œ๋งต + +--- + +## ๐Ÿ—๏ธ ํ˜„์žฌ ์•„ํ‚คํ…์ฒ˜ + +### ์„œ๋น„์Šค ๊ตฌ์„ฑ + +| ์„œ๋น„์Šค | ํฌํŠธ | ์ƒํƒœ | ๋น„๊ณ  | +|--------|------|------|------| +| discovery-server | 8761 | โœ… ์‹คํ–‰ ์ค‘ | Eureka Server | +| domain-restaurant-service | 8083 | โš ๏ธ ์ฝ”๋“œ ์™„์„ฑ | ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜ | +| domain-member-service | 8082 | โš ๏ธ ์ฝ”๋“œ ์™„์„ฑ | ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜ | +| domain-owner-service | 8084 | โš ๏ธ ์ฝ”๋“œ ์™„์„ฑ | ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜ | +| domain-reservation-service | 8085 | โš ๏ธ ์ฝ”๋“œ ์™„์„ฑ | ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜ | +| api-user | 8086 | โœ… BFF ์ „ํ™˜ ์™„๋ฃŒ | Feign Client ์‚ฌ์šฉ | +| api-owner | 8087 | โœ… BFF ์ „ํ™˜ ์™„๋ฃŒ | Feign Client ์‚ฌ์šฉ | + +### ํ†ต์‹  ๋ฐฉ์‹ + +**ํ˜„์žฌ (Phase 5 ์™„๋ฃŒ)**: +``` +api-user (BFF) โ†’ [Feign Client] โ†’ domain-* services +api-owner (BFF) โ†’ [Feign Client] โ†’ domain-* services + +BFF ์ฑ…์ž„: +- Redis ๋ถ„์‚ฐ ๋ฝ ๊ด€๋ฆฌ +- ์—ฌ๋Ÿฌ domain ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ +- ์‘๋‹ต ๋ฐ์ดํ„ฐ ์กฐํ•ฉ +- ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ +- Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ +``` + +**์ฐธ๊ณ **: domain-* ๋ชจ๋“ˆ์˜ ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ์€ ์„ ํƒ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. Phase 5 BFF ์ „ํ™˜ ์™„๋ฃŒ๋กœ microservices ์•„ํ‚คํ…์ฒ˜ ๋ชฉํ‘œ๋Š” ์ด๋ฏธ ๋‹ฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +--- + +## ๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„ (Phase 6 ์‹œ์ž‘ ์‹œ) + +### Phase 6: Saga Orchestration + +**ํ•„์š”ํ•œ ์ž‘์—…**: +1. **Saga Orchestrator ์„œ๋น„์Šค ์ƒ์„ฑ** + - ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ž๋™ ์‹คํ–‰ + - ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ๊ด€๋ฆฌ + - ์‹คํŒจ ์‹œ ์ž๋™ ๋กค๋ฐฑ + +2. **๋ฉฑ๋“ฑ์„ฑ(Idempotency) ํ‚ค ์ง€์›** + - ๋ชจ๋“  domain API์— ๋ฉฑ๋“ฑ์„ฑ ํ‚ค ํ—ค๋” ์ถ”๊ฐ€ + - ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ + - Redis ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ + +3. **์ด๋ฒคํŠธ ์†Œ์‹ฑ (์„ ํƒ์‚ฌํ•ญ)** + - ํŠธ๋žœ์žญ์…˜ ํžˆ์Šคํ† ๋ฆฌ ์ถ”์  + - ๊ฐ์‚ฌ ๋กœ๊ทธ + +**๊ธฐ๋Œ€ ํšจ๊ณผ**: +- โœ… ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ž๋™ ๋ณด์ƒ +- โœ… ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ +- โœ… ์žฅ์•  ๋ณต๊ตฌ ์ž๋™ํ™” + +**์˜ˆ์ƒ ์†Œ์š” ๊ธฐ๊ฐ„**: 4-6์ฃผ + +--- + +## ๐Ÿ“– ์ฐธ๊ณ  ์ž๋ฃŒ + +### ๋‚ด๋ถ€ ๋ฌธ์„œ +- **CLAUDE.md**: ํ”„๋กœ์ ํŠธ ์ „์ฒด ๊ตฌ์กฐ ๋ฐ ํ…Œ์ŠคํŠธ ์ „๋žต +- **microservices-migration-plan.md**: ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš +- **docker-compose.yml**: ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์„ฑ + +### ์ฃผ์š” ๊ธฐ์ˆ  ์Šคํƒ +- **Spring Boot**: 3.5.3 +- **Spring Cloud**: 2025.0.0 (Northfields) +- **OpenFeign**: REST Client +- **Eureka**: Service Discovery +- **Redis**: ๋ถ„์‚ฐ ๋ฝ ๋ฐ ์บ์‹ฑ +- **Kafka**: ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ํ†ต์‹  +- **MySQL**: Database-per-Service ํŒจํ„ด +- **Docker Compose**: ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ + +--- + +## ๐Ÿ“Š ํ”„๋กœ์ ํŠธ ๋ฉ”ํŠธ๋ฆญ + +### ์ฝ”๋“œ ํ†ต๊ณ„ (Phase 5 ์™„๋ฃŒ ๊ธฐ์ค€) +- **Feign Client**: 10๊ฐœ (api-user: 6๊ฐœ, api-owner: 4๊ฐœ) +- **DTO ํด๋ž˜์Šค**: 15๊ฐœ (Response/Request ํŒจํ„ด) +- **ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ ์ ์šฉ**: 49๊ฐœ ํŒŒ์ผ (38 ํ”„๋กœ๋•์…˜ + 11 ํ…Œ์ŠคํŠธ) +- **domain-* ์˜์กด์„ฑ ์ œ๊ฑฐ**: 100% (api-user, api-owner) +- **testFixtures ์ œ๊ฑฐ**: 100% + +### ์„ฑ๋Šฅ ๊ฐœ์„  +- โœ… ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์†๋„: 3-5๋ฐฐ ๊ฐœ์„  (DB ์ ‘๊ทผ ์ œ๊ฑฐ) +- โœ… N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ: ๋ฐฐ์น˜ ์กฐํšŒ ํŒจํ„ด ์ ์šฉ + +--- + +## ๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํƒ€์ž„๋ผ์ธ + +| Phase | ๊ธฐ๊ฐ„ | ์ƒํƒœ | ์™„๋ฃŒ์ผ | +|-------|------|------|--------| +| Phase 1-4 | 10-14์ฃผ | โœ… ์™„๋ฃŒ | 2025-11-05 | +| Phase 5 | 4-6์ฃผ | โœ… ์™„๋ฃŒ | 2025-11-05 | +| **์ด ์†Œ์š” ์‹œ๊ฐ„** | **14-20์ฃผ** | **โœ… ์™„๋ฃŒ** | **2025-11-05** | +| Phase 6 | 4-6์ฃผ | โณ ์˜ˆ์ • | - | +| Phase 7 | 3-4์ฃผ | โณ ์˜ˆ์ • | - | + +--- + +## โš ๏ธ ์•Œ๋ ค์ง„ ์ด์Šˆ + +### domain-* ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜ +- **์ด์Šˆ**: domain-restaurant, domain-member์˜ Application ํด๋ž˜์Šค๊ฐ€ ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ๋…๋ฆฝ ์‹คํ–‰ ๋ฏธ๊ฒ€์ฆ +- **์˜ํ–ฅ**: domain-* ๋ชจ๋“ˆ์„ ๋…๋ฆฝ Docker ์ปจํ…Œ์ด๋„ˆ๋กœ ์‹คํ–‰ ๋ถˆ๊ฐ€ +- **์ค‘์š”๋„**: ๋‚ฎ์Œ (Phase 5 BFF ์ „ํ™˜ ์™„๋ฃŒ๋กœ ์„ ํƒ์‚ฌํ•ญ์ด ๋จ) +- **ํ•ด๊ฒฐ ๋ฐฉ์•ˆ**: Phase 6 ์ดํ›„ ํ•„์š” ์‹œ ๋นˆ ์Šค์บ” ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ๊ฒ€์ฆ + +### ๋‹ค์Œ Phase ์šฐ์„ ์ˆœ์œ„ +1. **Phase 6 (Saga Orchestration)**: ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ ๋ณด์žฅ (๊ถŒ์žฅ) +2. **Phase 7 (API Gateway)**: ์ค‘์•™ ์ธ์ฆ ๋ฐ ๋ผ์šฐํŒ… +3. **domain-* ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ**: ์„ ํƒ์‚ฌํ•ญ (ํ•„์š” ์‹œ ์ง„ํ–‰) + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**๋ฌธ์„œ ๋ฒ„์ „**: v1.0 +**์ž‘์„ฑ์ž**: Claude Code Agent diff --git a/claudedocs/guides/bff-transaction-strategy.md b/claudedocs/guides/bff-transaction-strategy.md new file mode 100644 index 0000000..aa2807d --- /dev/null +++ b/claudedocs/guides/bff-transaction-strategy.md @@ -0,0 +1,256 @@ +# BFF ํŒจํ„ด ๋ฐ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต + +> WellMeet-Backend ํ”„๋กœ์ ํŠธ์˜ BFF(Backend for Frontend) ํŒจํ„ด๊ณผ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต ๊ฐ€์ด๋“œ + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [์„ค๊ณ„ ์›์น™](#์„ค๊ณ„-์›์น™) +2. [Phase๋ณ„ ์ „๋žต](#phase๋ณ„-์ „๋žต) +3. [์˜ˆ์•ฝ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ์˜ˆ์‹œ](#์˜ˆ์•ฝ-์ƒ์„ฑ-ํ”Œ๋กœ์šฐ-์˜ˆ์‹œ) +4. [ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต ๋น„๊ต](#ํŠธ๋žœ์žญ์…˜-์ฒ˜๋ฆฌ-์ „๋žต-๋น„๊ต) +5. [๊ถŒ์žฅ ์‚ฌํ•ญ](#๊ถŒ์žฅ-์‚ฌํ•ญ) + +--- + +## ์„ค๊ณ„ ์›์น™ + +### Domain ์„œ๋น„์Šค ์ฑ…์ž„ + +**์ œ๊ณตํ•˜๋Š” ๊ฒƒ** (โœ…): +- ์ž์‹ ์˜ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ CRUD +- ๋„๋ฉ”์ธ ๊ฒ€์ฆ ๋กœ์ง +- ๋‹จ์ผ ๋„๋ฉ”์ธ ๋‚ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + +**์ œ๊ณตํ•˜์ง€ ์•Š๋Š” ๊ฒƒ** (โŒ): +- ๋‹ค๋ฅธ domain ์„œ๋ฒ„ ํ˜ธ์ถœ +- ๋ถ„์‚ฐ ๋ฝ ๊ด€๋ฆฌ (Redis ๋“ฑ) +- ๋ฐ์ดํ„ฐ ์กฐํ•ฉ ๋ฐ ์‘๋‹ต ์ƒ์„ฑ +- ํŠธ๋žœ์žญ์…˜ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ + +### BFF(api-*) ์ฑ…์ž„ + +**์ œ๊ณตํ•˜๋Š” ๊ฒƒ** (โœ…): +- ์—ฌ๋Ÿฌ domain ์„œ๋น„์Šค ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ +- Redis ๋ถ„์‚ฐ ๋ฝ ๊ด€๋ฆฌ +- ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๊ด€๋ฆฌ +- ์‘๋‹ต ๋ฐ์ดํ„ฐ ์กฐํ•ฉ +- ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka) +- ์‚ฌ์šฉ์ž ์ธ์ฆ/๊ถŒํ•œ ๊ฒ€์ฆ + +--- + +## Phase๋ณ„ ์ „๋žต + +### Phase 4-5: BFF์—์„œ ๋ชจ๋“  ๊ฒƒ ์ฒ˜๋ฆฌ + +``` +api-user (BFF) +โ”œโ”€โ”€ Redis ๋ถ„์‚ฐ ๋ฝ ํš๋“/ํ•ด์ œ +โ”œโ”€โ”€ domain-member ํ˜ธ์ถœ (์ง์ ‘ ์˜์กด์„ฑ โ†’ Feign) +โ”œโ”€โ”€ domain-restaurant ํ˜ธ์ถœ (capacity ๊ด€๋ฆฌ) +โ”œโ”€โ”€ domain-reservation ํ˜ธ์ถœ (์˜ˆ์•ฝ ์ƒ์„ฑ) +โ”œโ”€โ”€ ์‘๋‹ต ์กฐํ•ฉ (์—ฌ๋Ÿฌ domain ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ) +โ””โ”€โ”€ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka) +``` + +**ํŠน์ง•**: +- ๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ •์  +- ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋ช…ํ™• +- ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด BFF์— ์ง‘์ค‘ + +### Phase 6: Saga Orchestrator ๋„์ž… + +``` +ReservationOrchestrator (์‹ ๊ทœ ์„œ๋น„์Šค) +โ”œโ”€โ”€ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ +โ”œโ”€โ”€ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ +โ”œโ”€โ”€ Redis ๋ถ„์‚ฐ ๋ฝ ๊ด€๋ฆฌ +โ””โ”€โ”€ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + +api-user (๊ฒฝ๋Ÿ‰ BFF) +โ”œโ”€โ”€ Orchestrator ํ˜ธ์ถœ +โ”œโ”€โ”€ ์‘๋‹ต ๋ณ€ํ™˜ +โ””โ”€โ”€ ์‚ฌ์šฉ์ž ์ธ์ฆ +``` + +**ํŠน์ง•**: +- BFF ๊ฒฝ๋Ÿ‰ํ™” +- ๋ณต์žกํ•œ ํŠธ๋žœ์žญ์…˜ ๋กœ์ง ๋ถ„๋ฆฌ +- ํ™•์žฅ์„ฑ ๋†’์Œ + +--- + +## ์˜ˆ์•ฝ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ์˜ˆ์‹œ + +### Phase 4 (ํ˜„์žฌ - ์ง์ ‘ ์˜์กด์„ฑ) + +```java +// api-user/ReservationService.java +@Service +@Transactional +@RequiredArgsConstructor +public class ReservationService { + + // ์ง์ ‘ ์˜์กด์„ฑ + private final ReservationDomainService reservationDomainService; + private final RestaurantDomainService restaurantDomainService; + private final MemberDomainService memberDomainService; + private final ReservationRedisService redisService; + + public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { + // 1. BFF๊ฐ€ Redis ๋ฝ ํš๋“ + if (!redisService.isReserving(memberId, request.restaurantId(), request.availableDateId())) { + throw new AlreadyReservingException(); + } + + // 2. BFF๊ฐ€ ์—ฌ๋Ÿฌ domain ํ˜ธ์ถœ + Member member = memberDomainService.getById(memberId); // domain-member + restaurantDomainService.decreaseCapacity(...); // domain-restaurant + Reservation reservation = reservationDomainService.create(...); // domain-reservation + + // 3. BFF๊ฐ€ ์‘๋‹ต ์กฐํ•ฉ + return CreateReservationResponse.builder() + .id(reservation.getId()) + .restaurantName(...) + .memberName(member.getName()) + .build(); + } +} +``` + +**ํŠน์ง•**: +- โœ… ๋‹จ์ผ @Transactional๋กœ ์ผ๊ด€์„ฑ ๋ณด์žฅ +- โœ… ๊ฐ„๋‹จํ•œ ๊ตฌ์กฐ +- โŒ BFF๊ฐ€ ๋ฌด๊ฑฐ์›Œ์ง + +### Phase 5 (Feign Client ์ „ํ™˜) + +```java +// api-user/ReservationService.java +@Service +@RequiredArgsConstructor +public class ReservationService { + + // Feign Client ์˜์กด์„ฑ + private final MemberClient memberClient; + private final RestaurantClient restaurantClient; + private final ReservationClient reservationClient; + private final ReservationRedisService redisService; + + public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { + // 1. Redis ๋ฝ + redisService.isReserving(...); + + // 2. Feign Client ํ˜ธ์ถœ + MemberDTO member = memberClient.getMember(memberId); + restaurantClient.decreaseCapacity(...); + ReservationDTO reservation = reservationClient.create(...); + + // 3. ์‘๋‹ต ์กฐํ•ฉ + return buildResponse(reservation, member, ...); + } +} +``` + +**ํŠน์ง•**: +- โœ… ์™„์ „ํ•œ BFF ํŒจํ„ด +- โœ… ๋…๋ฆฝ ๋ฐฐํฌ ๊ฐ€๋Šฅ +- โŒ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ ๋ฌธ์ œ + +### Phase 6 (Saga Orchestrator) + +```java +// api-user/ReservationService.java (๊ฒฝ๋Ÿ‰ํ™”) +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final ReservationOrchestrator orchestrator; + + public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { + // Orchestrator์— ์œ„์ž„ + return orchestrator.executeReservationSaga(memberId, request); + } +} + +// reservation-orchestrator/ReservationSagaOrchestrator.java +@Service +public class ReservationSagaOrchestrator { + + private final MemberClient memberClient; + private final RestaurantClient restaurantClient; + private final ReservationClient reservationClient; + + public CreateReservationResponse executeReservationSaga( + String memberId, + CreateReservationRequest request + ) { + SagaTransaction saga = new SagaTransaction(); + + try { + // Step 1: Member ํ™•์ธ + MemberDTO member = memberClient.getMember(memberId); + + // Step 2: Capacity ๊ฐ์†Œ + ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๋“ฑ๋ก + saga.addStep( + () -> restaurantClient.decreaseCapacity(...), + () -> restaurantClient.increaseCapacity(...) // ๋ณด์ƒ + ); + + // Step 3: Reservation ์ƒ์„ฑ + ๋ณด์ƒ + saga.addStep( + () -> reservationClient.create(...), + () -> reservationClient.delete(...) // ๋ณด์ƒ + ); + + return saga.execute(); + + } catch (Exception e) { + saga.compensate(); // ๋ชจ๋“  ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ + throw e; + } + } +} +``` + +**ํŠน์ง•**: +- โœ… ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ž๋™ ์ฒ˜๋ฆฌ +- โœ… BFF ๊ฒฝ๋Ÿ‰ํ™” +- โœ… ํ™•์žฅ์„ฑ ๋†’์Œ + +--- + +## ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „๋žต ๋น„๊ต + +| ํ•ญ๋ชฉ | Phase 4 (์ง์ ‘ ์˜์กด์„ฑ) | Phase 5 (BFF) | Phase 6 (Saga) | +|------|---------------------|--------------|---------------| +| **ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ** | @Transactional | ์ˆ˜๋™ ๊ด€๋ฆฌ | Saga Orchestrator | +| **์ผ๊ด€์„ฑ** | ๊ฐ•ํ•œ ์ผ๊ด€์„ฑ | ์ตœ์ข… ์ผ๊ด€์„ฑ | ์ตœ์ข… ์ผ๊ด€์„ฑ (๋ณด์ƒ) | +| **๋ณต์žก๋„** | ๋‚ฎ์Œ | ์ค‘๊ฐ„ | ๋†’์Œ | +| **ํ™•์žฅ์„ฑ** | ๋‚ฎ์Œ | ์ค‘๊ฐ„ | ๋†’์Œ | +| **์žฅ์•  ๋ณต๊ตฌ** | ๋กค๋ฐฑ | ์ˆ˜๋™ ๋ณต๊ตฌ | ์ž๋™ ๋ณด์ƒ | +| **๋„คํŠธ์›Œํฌ ๋ ˆ์ดํ„ด์‹œ** | ์—†์Œ | ์žˆ์Œ | ์žˆ์Œ | + +--- + +## ๊ถŒ์žฅ ์‚ฌํ•ญ + +**Phase 4-5 (BFF ์ „ํ™˜๊นŒ์ง€)**: +- BFF์—์„œ Redis ๋ฝ ๊ด€๋ฆฌ +- BFF์—์„œ ํŠธ๋žœ์žญ์…˜ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ +- domain ์„œ๋น„์Šค๋Š” ๋‹จ์ˆœ CRUD๋งŒ ์ œ๊ณต +- ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ BFF์— ์ง‘์ค‘ + +**Phase 6 ์ดํ›„ (Saga ๋„์ž…)**: +- Orchestrator๋กœ ํŠธ๋žœ์žญ์…˜ ๋กœ์ง ์ด๋™ +- BFF๋Š” ๊ฒฝ๋Ÿ‰ํ™” (์ธ์ฆ, ์‘๋‹ต ๋ณ€ํ™˜๋งŒ) +- ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ž๋™ํ™” +- ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ๊ฐ•ํ™” + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/guides/infrastructure-integration.md b/claudedocs/guides/infrastructure-integration.md new file mode 100644 index 0000000..d9282bc --- /dev/null +++ b/claudedocs/guides/infrastructure-integration.md @@ -0,0 +1,228 @@ +# ์ธํ”„๋ผ ํ†ตํ•ฉ ๊ฐ€์ด๋“œ + +> WellMeet-Backend ํ”„๋กœ์ ํŠธ์˜ ์ธํ”„๋ผ ํ†ตํ•ฉ (Flyway, AWS MSK) ์„ค์ • ๊ฐ€์ด๋“œ + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [Flyway ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜](#flyway-๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค-๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) +2. [AWS MSK (Kafka) ํ†ตํ•ฉ](#aws-msk-kafka-ํ†ตํ•ฉ) + +--- + +## Flyway ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +### ๊ฐœ์š” + +`domain-reservation` ๋ชจ๋“ˆ์—์„œ๋งŒ Flyway๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + +### ์ ์šฉ ์œ„์น˜ + +- **๋ชจ๋“ˆ**: `domain-reservation` +- **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ**: `domain-reservation/src/main/resources/db/migration/` +- **์‹คํ–‰ ์‹œ์ **: Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ž๋™ ์‹คํ–‰ + +### build.gradle ์„ค์ • + +```gradle +dependencies { + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' +} +``` + +### application.yml ์„ค์ • + +```yaml +spring: + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + sql-migration-prefix: V + sql-migration-suffix: .sql +``` + +### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋„ค์ด๋ฐ + +``` +db/migration/ +โ”œโ”€โ”€ V1__create_reservation_table.sql +โ”œโ”€โ”€ V2__create_available_date_table.sql +โ”œโ”€โ”€ V3__add_status_column_to_reservation.sql +โ””โ”€โ”€ V4__add_index_on_reservation_date.sql +``` + +**๊ทœ์น™**: + +- `V{๋ฒ„์ „๋ฒˆํ˜ธ}__{์„ค๋ช…}.sql` ํ˜•์‹ +- ๋ฒ„์ „ ๋ฒˆํ˜ธ๋Š” ์ˆœ์ฐจ์ ์œผ๋กœ ์ฆ๊ฐ€ +- ์‹คํ–‰ ์ˆœ์„œ๋Š” ๋ฒ„์ „ ๋ฒˆํ˜ธ ๊ธฐ์ค€ + +### ๋‹ค๋ฅธ domain ๋ชจ๋“ˆ + +๋‹ค๋ฅธ domain ๋ชจ๋“ˆ(member, owner, restaurant)์€ Flyway๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  JPA `ddl-auto` ์„ค์ • ์‚ฌ์šฉ: + +```yaml +spring: + jpa: + hibernate: + ddl-auto: create-drop # ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ +``` + +**์ด์œ **: + +- `domain-reservation`์€ ์˜ˆ์•ฝ ๋ฐ์ดํ„ฐ์˜ ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ๊ฐ€ ์ค‘์š”ํ•˜์—ฌ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์ถ”์  ํ•„์š” +- ๋‹ค๋ฅธ ๋ชจ๋“ˆ์€ ์ƒ๋Œ€์ ์œผ๋กœ ๋‹จ์ˆœํ•œ CRUD ์ž‘์—… ์œ„์ฃผ + +### ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ์˜ Flyway + +ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋„ Flyway๊ฐ€ ์ž๋™ ์‹คํ–‰๋˜์–ด ์ผ๊ด€๋œ ์Šคํ‚ค๋งˆ ํ™˜๊ฒฝ ๋ณด์žฅ: + +**domain-reservation/src/test/resources/application-domain-test.yml**: + +```yaml +spring: + flyway: + enabled: true + clean-on-validation-error: true # ํ…Œ์ŠคํŠธ ์‹œ ์Šคํ‚ค๋งˆ ์ดˆ๊ธฐํ™” +``` + +--- + +## AWS MSK (Kafka) ํ†ตํ•ฉ + +### ๊ฐœ์š” + +`infra-kafka` ๋ชจ๋“ˆ์€ AWS MSK (Managed Streaming for Apache Kafka)์™€ IAM ์ธ์ฆ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ํ†ตํ•ฉ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +### build.gradle ์„ค์ • + +```gradle +dependencies { + implementation 'org.springframework.kafka:spring-kafka' + implementation 'software.amazon.msk:aws-msk-iam-auth:2.2.0' + implementation 'com.amazonaws:aws-java-sdk-kafka:1.12.565' +} +``` + +### ํŠน์ง• + +**IAM ์ธ์ฆ ์‚ฌ์šฉ**: + +- AWS IAM Role ๊ธฐ๋ฐ˜ ์ธ์ฆ +- Access Key/Secret Key ๋ถˆํ•„์š” +- ECS/EKS์—์„œ Task Role ๋˜๋Š” Pod Identity ํ™œ์šฉ + +**๋ณด์•ˆ**: + +- TLS ์•”ํ˜ธํ™” ํ†ต์‹  +- VPC ๋‚ด๋ถ€ ํ†ต์‹  +- Security Group ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด + +### application.yml ์„ค์ • (ํ”„๋กœ๋•์…˜) + +```yaml +spring: + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS} + security: + protocol: SASL_SSL + properties: + sasl.mechanism: AWS_MSK_IAM + sasl.jaas.config: software.amazon.msk.auth.iam.IAMLoginModule required; + sasl.client.callback.handler.class: software.amazon.msk.auth.iam.IAMClientCallbackHandler + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + acks: all + retries: 3 +``` + +### ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ + +ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” EmbeddedKafka ์‚ฌ์šฉ (IAM ์ธ์ฆ ๋ถˆํ•„์š”): + +**application-infra-kafka-test.yml**: + +```yaml +spring: + kafka: + bootstrap-servers: ${spring.embedded.kafka.brokers} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer +``` + +### ์ฃผ์š” ํ† ํ”ฝ + +- `notification` - ์‚ฌ์šฉ์ž ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ +- `reservation-created` - ์˜ˆ์•ฝ ์ƒ์„ฑ ์ด๋ฒคํŠธ +- `reservation-cancelled` - ์˜ˆ์•ฝ ์ทจ์†Œ ์ด๋ฒคํŠธ +- `reminder` - ๋ฆฌ๋งˆ์ธ๋” ๋ฉ”์‹œ์ง€ (batch-reminder ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉ) + +### ๋ฉ”์‹œ์ง€ ๊ตฌ์กฐ ์˜ˆ์‹œ + +```json +{ + "header": { + "messageId": "msg-123", + "recipientId": "member-456", + "timestamp": "2025-10-30T12:00:00Z", + "type": "RESERVATION_CREATED" + }, + "payload": { + "reservationId": "reservation-789", + "restaurantName": "๋ง›์ง‘", + "reservationDate": "2025-11-01T19:00:00", + "partySize": 4 + } +} +``` + +### Producer ์˜ˆ์‹œ + +**infra-kafka/src/main/java/com/wellmeet/kafka/service/KafkaProducerService.java**: + +```java + +@Service +public class KafkaProducerService { + + private final KafkaTemplate kafkaTemplate; + + public void sendNotificationMessage(String memberId, Object payload) { + NotificationMessage message = NotificationMessage.builder() + .header(MessageHeader.builder() + .messageId(UUID.randomUUID().toString()) + .recipientId(memberId) + .timestamp(LocalDateTime.now()) + .build()) + .payload(payload) + .build(); + + kafkaTemplate.send("notification", memberId, message); + } +} +``` + +### ๋ชจ๋‹ˆํ„ฐ๋ง + +**CloudWatch Metrics**: + +- ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก ์„ฑ๊ณต/์‹คํŒจ์œจ +- ์ง€์—ฐ ์‹œ๊ฐ„ (Latency) +- Consumer Lag + +**Kafka ๋กœ๊ทธ**: + +- Producer ์ „์†ก ๋กœ๊ทธ +- ์žฌ์‹œ๋„ ํšŸ์ˆ˜ +- ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/guides/local-development.md b/claudedocs/guides/local-development.md new file mode 100644 index 0000000..b83f5b7 --- /dev/null +++ b/claudedocs/guides/local-development.md @@ -0,0 +1,134 @@ +# ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ฐ€์ด๋“œ + +> WellMeet-Backend ํ”„๋กœ์ ํŠธ์˜ ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์„ฑ (Docker Compose, Eureka Server) + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [Docker Compose ๊ตฌ์„ฑ](#docker-compose-๊ตฌ์„ฑ) +2. [Service Discovery (Eureka Server)](#service-discovery-eureka-server) + +--- + +## Docker Compose ๊ตฌ์„ฑ + +WellMeet-Backend ํ”„๋กœ์ ํŠธ๋Š” `docker-compose.yml`์„ ํ†ตํ•ด ๋กœ์ปฌ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ธํ”„๋ผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +### ์ธํ”„๋ผ ์ปดํฌ๋„ŒํŠธ + +**๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (MySQL 8.0)**: +- `mysql-reservation` - ์˜ˆ์•ฝ ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3306) +- `mysql-member` - ํšŒ์› ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3307) +- `mysql-owner` - ์‚ฌ์—…์ž ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3308) +- `mysql-restaurant` - ์‹๋‹น ๋„๋ฉ”์ธ DB (ํฌํŠธ: 3309) + +**๋ฉ”์‹œ์ง• ๋ฐ ์บ์‹œ**: +- `redis` - ๋ถ„์‚ฐ ๋ฝ ๋ฐ ์บ์‹œ (ํฌํŠธ: 6379) +- `kafka` - ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค (ํฌํŠธ: 9092) +- `zookeeper` - Kafka ์ฝ”๋””๋„ค์ดํ„ฐ (ํฌํŠธ: 2181) + +**์„œ๋น„์Šค ๋””์Šค์ปค๋ฒ„๋ฆฌ**: +- `discovery-server` - Eureka Server (ํฌํŠธ: 8761) + +### ์‹คํ–‰ ๋ฐฉ๋ฒ• + +```bash +# ์ „์ฒด ์ธํ”„๋ผ ์‹œ์ž‘ +docker-compose up -d + +# ํŠน์ • ์„œ๋น„์Šค๋งŒ ์‹œ์ž‘ +docker-compose up -d mysql-reservation redis + +# ๋กœ๊ทธ ํ™•์ธ +docker-compose logs -f discovery-server + +# ์ „์ฒด ์ค‘์ง€ ๋ฐ ์ œ๊ฑฐ +docker-compose down + +# ๋ณผ๋ฅจ๊นŒ์ง€ ์™„์ „ ์‚ญ์ œ +docker-compose down -v +``` + +### Phase 2 ์ค€๋น„ ์‚ฌํ•ญ + +๊ฐ domain ๋ชจ๋“ˆ์ด ๋…๋ฆฝ ์„œ๋น„์Šค๋กœ ์ „ํ™˜๋  ๋•Œ๋ฅผ ๋Œ€๋น„ํ•˜์—ฌ: +- ๊ฐ ๋„๋ฉ”์ธ๋ณ„๋กœ ๋ณ„๋„์˜ MySQL ์ธ์Šคํ„ด์Šค ์ค€๋น„ ์™„๋ฃŒ +- Database per Service ํŒจํ„ด ์ ์šฉ ๊ฐ€๋Šฅ +- ์„œ๋น„์Šค ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ๋ณด์žฅ + +--- + +## Service Discovery (Eureka Server) + +### ๊ฐœ์š” + +Netflix Eureka๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ Service Registry๋กœ, Microservices ํ™˜๊ฒฝ์—์„œ ์„œ๋น„์Šค ์ธ์Šคํ„ด์Šค๋ฅผ ์ž๋™์œผ๋กœ ๋“ฑ๋กํ•˜๊ณ  ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. + +### ๊ธฐ์ˆ  ์Šคํƒ + +- **Spring Boot**: 3.5.3 +- **Spring Cloud**: 2025.0.0 (Northfields) +- **Eureka Server**: Netflix OSS + +### ์ฃผ์š” ์„ค์ • + +**ํฌํŠธ**: 8761 + +**Eureka ์„ค์ •**: +```yaml +eureka: + client: + register-with-eureka: false # Eureka Server ์ž์ฒด๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋กํ•˜์ง€ ์•Š์Œ + fetch-registry: false # ๋‹จ์ผ ์„œ๋ฒ„ ๊ตฌ์„ฑ, ๋‹ค๋ฅธ Eureka ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฐ€์ ธ์˜ค์ง€ ์•Š์Œ + server: + enable-self-preservation: false # ๊ฐœ๋ฐœ ํ™˜๊ฒฝ: ์‘๋‹ต ์—†๋Š” ์„œ๋น„์Šค ์ฆ‰์‹œ ์ œ๊ฑฐ (90์ดˆ) +``` + +**Self Preservation ๋ชจ๋“œ**: +- ํ”„๋กœ๋•์…˜: `true` (๋„คํŠธ์›Œํฌ ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ •๋ณด ์œ ์ง€) +- ๊ฐœ๋ฐœ: `false` (๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ์„ ์œ„ํ•ด ๋น„ํ™œ์„ฑํ™”) + +### ์ ‘์† ์ •๋ณด + +- **Dashboard**: http://localhost:8761 +- **Health Check**: http://localhost:8761/actuator/health +- **Eureka Apps API**: http://localhost:8761/eureka/apps + +### Phase 2์—์„œ์˜ ์—ญํ•  + +๊ฐ domain ์„œ๋น„์Šค๊ฐ€ Eureka Client๋กœ ๋“ฑ๋ก๋˜๋ฉด: +1. ์„œ๋น„์Šค ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ Eureka์— ๋“ฑ๋ก +2. ๋‹ค๋ฅธ ์„œ๋น„์Šค๊ฐ€ ์ด๋ฆ„(service-id)์œผ๋กœ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ +3. ํ—ฌ์Šค ์ฒดํฌ๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง +4. ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ ๋ฐ ์žฅ์•  ๋ณต๊ตฌ ์ง€์› + +### Docker Compose ํ†ตํ•ฉ + +discovery-server๋Š” docker-compose.yml์— ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ, Multi-stage Dockerfile๋กœ ๋นŒ๋“œ๋ฉ๋‹ˆ๋‹ค: + +```dockerfile +# Stage 1: Gradle ๋นŒ๋“œ +FROM gradle:8.5-jdk21 AS build +WORKDIR /app +COPY . . +RUN gradle :discovery-server:bootJar --no-daemon + +# Stage 2: ์‹คํ–‰ ํ™˜๊ฒฝ +FROM openjdk:21-jdk-slim +WORKDIR /app +COPY --from=build /app/discovery-server/build/libs/*.jar app.jar +EXPOSE 8761 +ENTRYPOINT ["java", "-jar", "app.jar"] +``` + +**Health Check**: +- ๊ฐ„๊ฒฉ: 30์ดˆ +- ํƒ€์ž„์•„์›ƒ: 3์ดˆ +- ์‹œ์ž‘ ๋Œ€๊ธฐ: 40์ดˆ + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/guides/module-test-strategies.md b/claudedocs/guides/module-test-strategies.md new file mode 100644 index 0000000..417eedd --- /dev/null +++ b/claudedocs/guides/module-test-strategies.md @@ -0,0 +1,123 @@ +# ๋ชจ๋“ˆ๋ณ„ ํ…Œ์ŠคํŠธ ์ „๋žต + +> WellMeet-Backend ํ”„๋กœ์ ํŠธ์˜ ๋ชจ๋“ˆ๋ณ„ ํ…Œ์ŠคํŠธ ์ „๋žต ๋ฐ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [domain-reservation ๋ชจ๋“ˆ](#domain-reservation-๋ชจ๋“ˆ) +2. [domain-member ๋ชจ๋“ˆ](#domain-member-๋ชจ๋“ˆ) +3. [domain-owner ๋ชจ๋“ˆ](#domain-owner-๋ชจ๋“ˆ) +4. [domain-restaurant ๋ชจ๋“ˆ](#domain-restaurant-๋ชจ๋“ˆ) +5. [api-user / api-owner ๋ชจ๋“ˆ](#api-user--api-owner-๋ชจ๋“ˆ) +6. [infra-redis ๋ชจ๋“ˆ](#infra-redis-๋ชจ๋“ˆ) +7. [infra-kafka ๋ชจ๋“ˆ](#infra-kafka-๋ชจ๋“ˆ) +8. [batch-reminder ๋ชจ๋“ˆ](#batch-reminder-๋ชจ๋“ˆ) + +--- + +## domain-reservation ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|----------------|--------|------------------------------|----------------------| +| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ | +| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | @Query ์ปค์Šคํ…€ ์ฟผ๋ฆฌ๋งŒ | +| Domain Service | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest + @Import | ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + Repository | + +**ํŠน์ง•**: Flyway๋ฅผ ํ†ตํ•œ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ด€๋ฆฌ +**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% + +--- + +## domain-member ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|------------|--------|--------------------|--------------------| +| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ํšŒ์› ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ | +| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | @Query ์ปค์Šคํ…€ ์ฟผ๋ฆฌ๋งŒ | + +**ํŠน์ง•**: testFixtures ์ œ๊ณต (๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ) +**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% + +--- + +## domain-owner ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|------------|--------|--------------------|---------------------| +| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์‚ฌ์—…์ž ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ | +| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | @Query ์ปค์Šคํ…€ ์ฟผ๋ฆฌ๋งŒ | + +**ํŠน์ง•**: testFixtures ์ œ๊ณต (๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ) +**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% + +--- + +## domain-restaurant ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|------------|--------|--------------------|--------------------------| +| Entity | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์‹๋‹น ์ƒ์„ฑ, ์ขŒํ‘œ ๊ฒ€์ฆ, ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ | +| Repository | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseRepositoryTest | BoundingBox ์ฟผ๋ฆฌ, ์œ„์น˜ ๊ธฐ๋ฐ˜ ์กฐํšŒ | + +**ํŠน์ง•**: + +- testFixtures ์ œ๊ณต (๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ) +- ์ขŒํ‘œ ๊ธฐ๋ฐ˜ ์ฟผ๋ฆฌ (BoundingBox, ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ) + **์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 85% + +--- + +## api-user / api-owner ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|----------------|--------|-------------------------|----------------| +| Controller | E2E | BaseControllerTest | HTTP API ์ „์ฒด ํ๋ฆ„ | +| Service | ๋‹จ์œ„/ํ†ตํ•ฉ | Mock ๋˜๋Š” BaseServiceTest | ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง, ๋™์‹œ์„ฑ | +| Event Listener | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | BaseServiceTest | ์ด๋ฒคํŠธ ๋ฐœํ–‰/์ˆ˜์‹  | + +**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 80% + +--- + +## infra-redis ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|---------------|--------|----------------|-----------| +| Redis Service | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | Testcontainers | ๋ถ„์‚ฐ ๋ฝ, ๋™์‹œ์„ฑ | + +**ํŠน์ง•**: Redisson 3.50.0 ์‚ฌ์šฉ (๋ถ„์‚ฐ ๋ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) +**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 90% (Critical - ๋™์‹œ์„ฑ ์ œ์–ด ํ•ต์‹ฌ ๋ชจ๋“ˆ) + +--- + +## infra-kafka ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|----------|--------|---------------|-------------| +| Producer | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | EmbeddedKafka | ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก, ์ง๋ ฌํ™” | +| DTO | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” | + +**ํŠน์ง•**: AWS MSK + IAM ์ธ์ฆ ์‚ฌ์šฉ +โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ +**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 70% (์ž‘์„ฑ ํ›„) + +--- + +## batch-reminder ๋ชจ๋“ˆ + +| Layer | ํ…Œ์ŠคํŠธ ํƒ€์ž… | ๋ฒ ์ด์Šค ํด๋ž˜์Šค | ์ฃผ์š” ๊ฒ€์ฆ | +|------------|--------|-----------------|---------------| +| Job Config | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | TestBatchConfig | Job ์‹คํ–‰ ์„ฑ๊ณต | +| Processor | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | ์—†์Œ | ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋กœ์ง | +| Writer | ๋‹จ์œ„/ํ†ตํ•ฉ | Mock/์‹ค์ œ | ์™ธ๋ถ€ ํ˜ธ์ถœ (Kafka) | + +โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ +**์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ**: 75% (์ž‘์„ฑ ํ›„) + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/guides/naming-conventions.md b/claudedocs/guides/naming-conventions.md new file mode 100644 index 0000000..bc4de98 --- /dev/null +++ b/claudedocs/guides/naming-conventions.md @@ -0,0 +1,271 @@ +# ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ ๊ฐ€์ด๋“œ + +> **์ ์šฉ์ผ**: 2025-11-05 +> **๋ชฉ์ **: ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ํด๋ž˜์Šค ์ด๋ฆ„์˜ ์œ ์ผ์„ฑ์„ ๋ณด์žฅํ•˜๊ณ  ์ผ๊ด€๋œ ๋„ค์ด๋ฐ ํŒจํ„ด ์ ์šฉ + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [ํ•ต์‹ฌ ์›์น™](#ํ•ต์‹ฌ-์›์น™) +2. [Domain ๋ชจ๋“ˆ ๋„ค์ด๋ฐ ๊ทœ์น™](#domain-๋ชจ๋“ˆ-๋„ค์ด๋ฐ-๊ทœ์น™) +3. [BFF ๋ชจ๋“ˆ ๋„ค์ด๋ฐ ๊ทœ์น™](#bff-๋ชจ๋“ˆ-๋„ค์ด๋ฐ-๊ทœ์น™) +4. [ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™](#ํ…Œ์ŠคํŠธ-ํด๋ž˜์Šค-๋„ค์ด๋ฐ-๊ทœ์น™) +5. [๋ ˆ์ด์–ด๋ณ„ ์ ‘๋ฏธ์‚ฌ ์ •๋ฆฌ](#๋ ˆ์ด์–ด๋ณ„-์ ‘๋ฏธ์‚ฌ-์ •๋ฆฌ) +6. [๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํžˆ์Šคํ† ๋ฆฌ](#๋งˆ์ด๊ทธ๋ ˆ์ด์…˜-ํžˆ์Šคํ† ๋ฆฌ) +7. [์ฃผ์˜์‚ฌํ•ญ](#์ฃผ์˜์‚ฌํ•ญ) + +--- + +## ํ•ต์‹ฌ ์›์น™ + +1. **๊ณ„์ธต๋ช…์€ ๋งˆ์ง€๋ง‰์— ์œ„์น˜**: `XxxController`, `XxxService` (โŒ `ControllerXxx`, `ServiceXxx`) +2. **ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ํด๋ž˜์Šค ์ด๋ฆ„ ์œ ์ผ์„ฑ ๋ณด์žฅ**: ๋‹จ์ผ ๋ชจ๋“ˆ์ด ์•„๋‹Œ ์ „์ฒด ํ”„๋กœ์ ํŠธ ๊ธฐ์ค€ +3. **ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋„ ๋™์ผ ๊ทœ์น™ ์ ์šฉ**: `{TargetClassName}Test` + +--- + +## Domain ๋ชจ๋“ˆ ๋„ค์ด๋ฐ ๊ทœ์น™ + +### Domain Controllers (domain-* ๋ชจ๋“ˆ) + +**ํŒจํ„ด**: `{Entity}DomainController` + +**์˜ˆ์‹œ**: +``` +domain-restaurant/ +โ”œโ”€โ”€ RestaurantDomainController.java (๋ ˆ์Šคํ† ๋ž‘ ๋„๋ฉ”์ธ ์ปจํŠธ๋กค๋Ÿฌ) +โ”œโ”€โ”€ RestaurantAvailableDateController.java (์˜ˆ์•ฝ ๊ฐ€๋Šฅ ๋‚ ์งœ ์ปจํŠธ๋กค๋Ÿฌ) +โ”œโ”€โ”€ RestaurantBusinessHourController.java (์˜์—… ์‹œ๊ฐ„ ์ปจํŠธ๋กค๋Ÿฌ) +โ”œโ”€โ”€ RestaurantMenuController.java (๋ฉ”๋‰ด ์ปจํŠธ๋กค๋Ÿฌ) +โ””โ”€โ”€ RestaurantReviewController.java (๋ฆฌ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ) + +domain-member/ +โ”œโ”€โ”€ MemberDomainController.java (ํšŒ์› ๋„๋ฉ”์ธ ์ปจํŠธ๋กค๋Ÿฌ) +โ””โ”€โ”€ MemberFavoriteRestaurantController.java (์ฆ๊ฒจ์ฐพ๊ธฐ ์ปจํŠธ๋กค๋Ÿฌ) + +domain-owner/ +โ””โ”€โ”€ OwnerDomainController.java (์‚ฌ์—…์ž ๋„๋ฉ”์ธ ์ปจํŠธ๋กค๋Ÿฌ) + +domain-reservation/ +โ””โ”€โ”€ ReservationDomainController.java (์˜ˆ์•ฝ ๋„๋ฉ”์ธ ์ปจํŠธ๋กค๋Ÿฌ) +``` + +**๋„ค์ด๋ฐ ์ด์œ **: +- `DomainController` ์ ‘๋ฏธ์‚ฌ๋กœ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์˜ REST API์ž„์„ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ +- ์—”ํ‹ฐํ‹ฐ๋ช…์„ ์ ‘๋‘์‚ฌ๋กœ ์‚ฌ์šฉํ•˜์—ฌ ๋„๋ฉ”์ธ ๊ตฌ๋ถ„ +- BFF ๋ชจ๋“ˆ์˜ ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ + +--- + +### ApplicationService (domain-* ๋ชจ๋“ˆ) + +**ํŒจํ„ด**: `{Domain}{Entity}ApplicationService` + +**์˜ˆ์‹œ**: +``` +domain-restaurant/ +โ”œโ”€โ”€ RestaurantApplicationService.java (๋ ˆ์Šคํ† ๋ž‘ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค) +โ”œโ”€โ”€ RestaurantAvailableDateApplicationService.java (์˜ˆ์•ฝ ๊ฐ€๋Šฅ ๋‚ ์งœ) +โ”œโ”€โ”€ RestaurantBusinessHourApplicationService.java (์˜์—… ์‹œ๊ฐ„) +โ”œโ”€โ”€ RestaurantMenuApplicationService.java (๋ฉ”๋‰ด) +โ””โ”€โ”€ RestaurantReviewApplicationService.java (๋ฆฌ๋ทฐ) + +domain-member/ +โ”œโ”€โ”€ MemberApplicationService.java (ํšŒ์›) +โ””โ”€โ”€ MemberFavoriteRestaurantApplicationService.java (์ฆ๊ฒจ์ฐพ๊ธฐ) + +domain-owner/ +โ””โ”€โ”€ OwnerApplicationService.java (์‚ฌ์—…์ž) + +domain-reservation/ +โ””โ”€โ”€ ReservationApplicationService.java (์˜ˆ์•ฝ) +``` + +**๋„ค์ด๋ฐ ์ด์œ **: +- ApplicationService๋Š” Controller์™€ DomainService ์‚ฌ์ด์˜ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๋ ˆ์ด์–ด +- Domain ์ ‘๋‘์‚ฌ๋กœ ์†Œ์† ๋„๋ฉ”์ธ์„ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ +- DomainService์™€ ๊ตฌ๋ถ„ํ•˜์—ฌ ๋ ˆ์ด์–ด ์—ญํ•  ๋ช…ํ™•ํ™” + +--- + +### DomainService (domain-* ๋ชจ๋“ˆ) + +**ํŒจํ„ด**: `{Entity}DomainService` + +**์˜ˆ์‹œ**: +``` +domain-restaurant/ +โ”œโ”€โ”€ RestaurantDomainService.java +โ”œโ”€โ”€ AvailableDateDomainService.java +โ”œโ”€โ”€ BusinessHourDomainService.java +โ”œโ”€โ”€ MenuDomainService.java +โ””โ”€โ”€ ReviewDomainService.java + +domain-member/ +โ”œโ”€โ”€ MemberDomainService.java +โ””โ”€โ”€ FavoriteRestaurantDomainService.java + +domain-owner/ +โ””โ”€โ”€ OwnerDomainService.java + +domain-reservation/ +โ””โ”€โ”€ ReservationDomainService.java +``` + +**๋„ค์ด๋ฐ ์ด์œ **: +- DomainService๋Š” ์ˆœ์ˆ˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹น +- ApplicationService์™€ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ + +--- + +## BFF ๋ชจ๋“ˆ ๋„ค์ด๋ฐ ๊ทœ์น™ + +### BFF Controllers (api-user, api-owner ๋ชจ๋“ˆ) + +**ํŒจํ„ด**: `{User|Owner}{Feature}BffController` + +**์˜ˆ์‹œ**: +``` +api-user/ +โ”œโ”€โ”€ UserFavoriteRestaurantBffController.java (์‚ฌ์šฉ์ž ์ฆ๊ฒจ์ฐพ๊ธฐ) +โ”œโ”€โ”€ UserReservationBffController.java (์‚ฌ์šฉ์ž ์˜ˆ์•ฝ) +โ””โ”€โ”€ UserRestaurantBffController.java (์‚ฌ์šฉ์ž ๋ ˆ์Šคํ† ๋ž‘) + +api-owner/ +โ”œโ”€โ”€ OwnerReservationBffController.java (์‚ฌ์—…์ž ์˜ˆ์•ฝ) +โ””โ”€โ”€ OwnerRestaurantBffController.java (์‚ฌ์—…์ž ๋ ˆ์Šคํ† ๋ž‘) +``` + +**๋„ค์ด๋ฐ ์ด์œ **: +- `Bff` ์ ‘๋‘์‚ฌ๋กœ Backend for Frontend ํŒจํ„ด์ž„์„ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ +- `User` ๋˜๋Š” `Owner` ์ ‘๋‘์‚ฌ๋กœ ์‚ฌ์šฉ์ž ๊ตฌ๋ถ„ +- Domain ๋ชจ๋“ˆ์˜ Controller์™€ ์ด๋ฆ„ ์ถฉ๋Œ ๋ฐฉ์ง€ + +--- + +### BFF Services (api-user, api-owner ๋ชจ๋“ˆ) + +**ํŒจํ„ด**: `{User|Owner}{Feature}BffService` + +**์˜ˆ์‹œ**: +``` +api-user/ +โ”œโ”€โ”€ UserFavoriteRestaurantBffService.java +โ”œโ”€โ”€ UserReservationBffService.java +โ”œโ”€โ”€ UserRestaurantBffService.java +โ””โ”€โ”€ UserEventPublishBffService.java + +api-owner/ +โ”œโ”€โ”€ OwnerReservationBffService.java +โ”œโ”€โ”€ OwnerRestaurantBffService.java +โ””โ”€โ”€ OwnerEventPublishBffService.java +``` + +**๋„ค์ด๋ฐ ์ด์œ **: +- Controller์™€ ๋™์ผํ•œ ๋„ค์ด๋ฐ ํŒจํ„ด ์ ์šฉ +- ์—ฌ๋Ÿฌ Domain ์„œ๋น„์Šค๋ฅผ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ํ•˜๋Š” ์—ญํ•  ๋ช…ํ™•ํ™” + +--- + +### Feign Clients (api-user, api-owner ๋ชจ๋“ˆ) + +**ํŒจํ„ด**: `{Domain}{Entity}FeignClient` + +**์˜ˆ์‹œ**: +``` +api-user, api-owner ๊ณตํ†ต: +โ”œโ”€โ”€ MemberFeignClient.java (ํšŒ์› ๋„๋ฉ”์ธ) +โ”œโ”€โ”€ MemberFavoriteRestaurantFeignClient.java (์ฆ๊ฒจ์ฐพ๊ธฐ) +โ”œโ”€โ”€ OwnerFeignClient.java (์‚ฌ์—…์ž ๋„๋ฉ”์ธ) +โ”œโ”€โ”€ ReservationFeignClient.java (์˜ˆ์•ฝ ๋„๋ฉ”์ธ) +โ”œโ”€โ”€ RestaurantFeignClient.java (๋ ˆ์Šคํ† ๋ž‘ ๋„๋ฉ”์ธ) +โ””โ”€โ”€ RestaurantAvailableDateFeignClient.java (์˜ˆ์•ฝ ๊ฐ€๋Šฅ ๋‚ ์งœ) +``` + +**๋„ค์ด๋ฐ ์ด์œ **: +- `FeignClient` ์ ‘๋ฏธ์‚ฌ๋กœ HTTP ํ†ต์‹  ์ธํ„ฐํŽ˜์ด์Šค์ž„์„ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ +- Domain ์ ‘๋‘์‚ฌ๋กœ ํ˜ธ์ถœ ๋Œ€์ƒ ๋„๋ฉ”์ธ ๋ช…์‹œ +- ํ–ฅํ›„ Microservices ์ „ํ™˜ ์‹œ ๋ณ€๊ฒฝ ์ตœ์†Œํ™” + +--- + +## ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ + +**ํŒจํ„ด**: `{TargetClassName}Test` + +**์˜ˆ์‹œ**: +``` +ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ: +- RestaurantDomainController.java +- UserReservationBffController.java +- MemberFeignClient.java + +ํ…Œ์ŠคํŠธ ์ฝ”๋“œ: +- RestaurantDomainControllerTest.java +- UserReservationBffControllerTest.java +- MemberFeignClientTest.java +``` + +**๋„ค์ด๋ฐ ์ด์œ **: +- ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ํด๋ž˜์Šค๋ฅผ ๋ช…ํ™•ํžˆ ์‹๋ณ„ +- ํ‘œ์ค€ Java ํ…Œ์ŠคํŠธ ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ ์ค€์ˆ˜ + +--- + +## ๋ ˆ์ด์–ด๋ณ„ ์ ‘๋ฏธ์‚ฌ ์ •๋ฆฌ + +| ๋ ˆ์ด์–ด | ์ ‘๋ฏธ์‚ฌ | ์˜ˆ์‹œ | ์œ„์น˜ | +|--------|--------|------|------| +| Domain REST Controller | `DomainController` | `RestaurantDomainController` | domain-* | +| Domain Application Service | `ApplicationService` | `RestaurantApplicationService` | domain-* | +| Domain Business Service | `DomainService` | `RestaurantDomainService` | domain-* | +| BFF Controller | `BffController` | `UserReservationBffController` | api-* | +| BFF Service | `BffService` | `UserReservationBffService` | api-* | +| Feign Client | `FeignClient` | `ReservationFeignClient` | api-* | +| Test | `Test` | `RestaurantDomainControllerTest` | */test/** | + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํžˆ์Šคํ† ๋ฆฌ + +**์ ์šฉ์ผ**: 2025-11-05 +**๋ณ€๊ฒฝ ํŒŒ์ผ**: ์ด 49๊ฐœ (ํ”„๋กœ๋•์…˜ 38๊ฐœ + ํ…Œ์ŠคํŠธ 11๊ฐœ) + +**Phase 1: domain-restaurant (10๊ฐœ)** +- Controllers: 5๊ฐœ +- ApplicationServices: 5๊ฐœ + +**Phase 2: domain-member/owner/reservation (5๊ฐœ)** +- domain-member: 3๊ฐœ +- domain-owner: 1๊ฐœ +- domain-reservation: 1๊ฐœ + +**Phase 3: api-user (13๊ฐœ + 7๊ฐœ ํ…Œ์ŠคํŠธ)** +- BFF Controllers/Services: 7๊ฐœ +- Feign Clients: 6๊ฐœ +- ํ…Œ์ŠคํŠธ: 7๊ฐœ + +**Phase 4: api-owner (9๊ฐœ + 4๊ฐœ ํ…Œ์ŠคํŠธ)** +- BFF Controllers/Services: 5๊ฐœ +- Feign Clients: 4๊ฐœ +- ํ…Œ์ŠคํŠธ: 4๊ฐœ + +**Phase 5: ๊ฒ€์ฆ ์™„๋ฃŒ** +- โœ… ์ „์ฒด ๋นŒ๋“œ ์„ฑ๊ณต +- โœ… ํ…Œ์ŠคํŠธ ์ปดํŒŒ์ผ ์„ฑ๊ณต + +--- + +## ์ฃผ์˜์‚ฌํ•ญ + +1. **์‹ ๊ทœ ํด๋ž˜์Šค ์ƒ์„ฑ ์‹œ**: ๋ฐ˜๋“œ์‹œ ์ด ๋„ค์ด๋ฐ ๊ทœ์น™์„ ๋”ฐ๋ผ์•ผ ํ•จ +2. **์ถฉ๋Œ ํ™•์ธ**: ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ํด๋ž˜์Šค ์ด๋ฆ„ ๊ฒ€์ƒ‰ ํ›„ ์ƒ์„ฑ +3. **ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค**: ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์™€ ๋™์ผํ•œ ํŒจํ„ด ์ ์šฉ +4. **import ๋ฌธ**: ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๋กœ ๊ตฌ๋ถ„๋˜๋ฏ€๋กœ ๋™์ผ ํด๋ž˜์Šค๋ช… ์‚ฌ์šฉ ๋ถˆ๊ฐ€ + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/guides/test-infrastructure.md b/claudedocs/guides/test-infrastructure.md new file mode 100644 index 0000000..fc9110b --- /dev/null +++ b/claudedocs/guides/test-infrastructure.md @@ -0,0 +1,321 @@ +# ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ ๊ฐ€์ด๋“œ + +> WellMeet-Backend ํ”„๋กœ์ ํŠธ์˜ ํ…Œ์ŠคํŠธ ์ธํ”„๋ผ ๊ตฌ์„ฑ ๋ฐ ์„ค์ • ๊ฐ€์ด๋“œ + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [Gradle ์„ค์ •](#1-gradle-์„ค์ •) +2. [ํ…Œ์ŠคํŠธ ์„ค์ • (application-test.yml)](#2-ํ…Œ์ŠคํŠธ-์„ค์ •-application-testyml) +3. [Test Fixtures (Gradle testFixtures ํ”Œ๋Ÿฌ๊ทธ์ธ)](#3-test-fixtures-gradle-testfixtures-ํ”Œ๋Ÿฌ๊ทธ์ธ) + +--- + +## 1. Gradle ์„ค์ • + +**๋ฃจํŠธ build.gradle**: + +```gradle +subprojects { + apply plugin: 'jacoco' + + jacoco { + toolVersion = "0.8.11" + } + + test { + useJUnitPlatform() + finalizedBy jacocoTestReport + } + + jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + } + } + + jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.70 + } + } + } + } +} +``` + +**๋ชจ๋“ˆ๋ณ„ build.gradle (domain-redis ์˜ˆ์‹œ)**: + +```gradle +dependencies { + testImplementation 'org.testcontainers:testcontainers:1.19.3' + testImplementation 'org.testcontainers:junit-jupiter:1.19.3' +} +``` + +**kafka ๋ชจ๋“ˆ build.gradle**: + +```gradle +dependencies { + testImplementation 'org.springframework.kafka:spring-kafka-test' +} +``` + +--- + +## 2. ํ…Œ์ŠคํŠธ ์„ค์ • (application-test.yml) + +**domain-reservation ๋ชจ๋“ˆ** (`domain-reservation/src/main/resources/application-domain-test.yml`): + +```yaml +spring: + config: + activate: + on-profile: domain-test + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/test + username: root + password: + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + show-sql: false +``` + +**infra-redis ๋ชจ๋“ˆ** (`infra-redis/src/main/resources/application-infra-redis-test.yml`): + +```yaml +spring: + config: + activate: + on-profile: infra-redis-test + data: + redis: + host: localhost + port: 6379 +``` + +**infra-kafka ๋ชจ๋“ˆ** (`infra-kafka/src/main/resources/application-infra-kafka-test.yml`): + +```yaml +spring: + config: + activate: + on-profile: infra-kafka-test + kafka: + bootstrap-servers: ${spring.embedded.kafka.brokers} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer +``` + +**api-user/api-owner ๋ชจ๋“ˆ** (`api-user/src/main/resources/application-test.yml`): + +```yaml +spring: + config: + import: + - application-domain-test.yml + - application-infra-redis-test.yml + - application-infra-kafka-test.yml +``` + +--- + +## 3. Test Fixtures (Gradle testFixtures ํ”Œ๋Ÿฌ๊ทธ์ธ) + +WellMeet-Backend ํ”„๋กœ์ ํŠธ๋Š” Gradle์˜ `java-test-fixtures` ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ฝ”๋“œ๋ฅผ ๋ชจ๋“ˆ ๊ฐ„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + +### testFixtures ์ ์šฉ ๋ชจ๋“ˆ + +- `domain-reservation` โ†’ ์˜ˆ์•ฝ, ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ๋‚ ์งœ ์ƒ์„ฑ +- `domain-member` โ†’ ํšŒ์›, ์ฆ๊ฒจ์ฐพ๊ธฐ ์ƒ์„ฑ +- `domain-owner` โ†’ ์‚ฌ์—…์ž ์ƒ์„ฑ +- `domain-restaurant` โ†’ ์‹๋‹น, ๋ฉ”๋‰ด ์ƒ์„ฑ + +### build.gradle ์„ค์ • + +**domain ๋ชจ๋“ˆ (์˜ˆ: domain-member/build.gradle)**: + +```gradle +plugins { + id 'java-library' + id 'java-test-fixtures' // testFixtures ํ”Œ๋Ÿฌ๊ทธ์ธ ํ™œ์„ฑํ™” +} + +dependencies { + // ์ผ๋ฐ˜ ์˜์กด์„ฑ + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // testFixtures์—์„œ ํ•„์š”ํ•œ ์˜์กด์„ฑ + testFixturesImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' + testFixturesImplementation 'org.springframework.boot:spring-boot-starter-test' +} +``` + +**API ๋ชจ๋“ˆ (์˜ˆ: api-user/build.gradle)**: + +```gradle +dependencies { + // domain ๋ชจ๋“ˆ ์˜์กด์„ฑ + implementation project(':domain-member') + implementation project(':domain-owner') + implementation project(':domain-restaurant') + implementation project(':domain-reservation') + + // testFixtures ์‚ฌ์šฉ + testImplementation(testFixtures(project(':domain-member'))) + testImplementation(testFixtures(project(':domain-owner'))) + testImplementation(testFixtures(project(':domain-restaurant'))) + testImplementation(testFixtures(project(':domain-reservation'))) +} +``` + +### ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ + +``` +domain-member/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ main/java/ # ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ +โ”‚ โ”œโ”€โ”€ test/java/ # ๋ชจ๋“ˆ ๋‚ด๋ถ€ ํ…Œ์ŠคํŠธ +โ”‚ โ””โ”€โ”€ testFixtures/java/ # ๋‹ค๋ฅธ ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Test Fixture +โ”‚ โ””โ”€โ”€ com/wellmeet/domain/member/ +โ”‚ โ”œโ”€โ”€ MemberFixture.java +โ”‚ โ””โ”€โ”€ FavoriteRestaurantFixture.java +``` + +### Generator ํŒจํ„ด ๊ตฌํ˜„ ์˜ˆ์‹œ + +**domain-restaurant/src/testFixtures/java/com/wellmeet/domain/restaurant/RestaurantFixture.java**: + +```java +package com.wellmeet.domain.restaurant; + +import com.wellmeet.domain.owner.entity.Owner; +import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.domain.restaurant.repository.RestaurantRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class RestaurantFixture { + + @Autowired + private RestaurantRepository restaurantRepository; + + public Restaurant create(String name, Owner owner) { + return create(name, 37.5, 127.0, owner); + } + + public Restaurant create(String name, double lat, double lon, Owner owner) { + Restaurant restaurant = Restaurant.builder() + .name(name) + .address("์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ") + .latitude(lat) + .longitude(lon) + .phoneNumber("02-1234-5678") + .owner(owner) + .thumbnailUrl("https://example.com/thumbnail.jpg") + .build(); + return restaurantRepository.save(restaurant); + } +} +``` + +**domain-member/src/testFixtures/java/com/wellmeet/domain/member/MemberFixture.java**: + +```java +package com.wellmeet.domain.member; + +import com.wellmeet.domain.member.entity.Member; +import com.wellmeet.domain.member.repository.MemberRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MemberFixture { + + @Autowired + private MemberRepository memberRepository; + + public Member create(String name) { + return create(name, name + "@example.com"); + } + + public Member create(String name, String email) { + Member member = Member.builder() + .name(name) + .nickname(name + "_nick") + .email(email) + .phoneNumber("010-1234-5678") + .build(); + return memberRepository.save(member); + } +} +``` + +### API ๋ชจ๋“ˆ์—์„œ ์‚ฌ์šฉ ์˜ˆ์‹œ + +**api-user/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java**: + +```java + +@SpringBootTest +class ReservationServiceTest { + + @Autowired + private MemberFixture memberFixture; // domain-member testFixtures + + @Autowired + private OwnerFixture ownerFixture; // domain-owner testFixtures + + @Autowired + private RestaurantFixture restaurantFixture; // domain-restaurant testFixtures + + @Autowired + private ReservationService reservationService; + + @Test + void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ•œ๋‹ค() { + // testFixtures๋ฅผ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ค€๋น„ + Member member = memberFixture.create("testUser"); + Owner owner = ownerFixture.create("testOwner"); + Restaurant restaurant = restaurantFixture.create("ํ…Œ์ŠคํŠธ ์‹๋‹น", owner); + + // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ…Œ์ŠคํŠธ + ReservationResponse response = reservationService.reserve(...); + + assertThat(response).isNotNull(); + } +} +``` + +### testFixtures์˜ ์žฅ์  + +1. **์žฌ์‚ฌ์šฉ์„ฑ**: ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ์—์„œ ๋™์ผํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋กœ์ง ๊ณต์œ  +2. **์ผ๊ด€์„ฑ**: ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ๋ฐฉ์‹์ด ์ค‘์•™ํ™”๋˜์–ด ์ผ๊ด€์„ฑ ์œ ์ง€ +3. **์œ ์ง€๋ณด์ˆ˜**: ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ณ€๊ฒฝ ์‹œ testFixtures๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋จ +4. **์บก์Аํ™”**: ๋„๋ฉ”์ธ ์ง€์‹์„ testFixtures์— ์บก์Аํ™” +5. **๋…๋ฆฝ์„ฑ**: ๊ฐ ๋„๋ฉ”์ธ ๋ชจ๋“ˆ์ด ์ž์‹ ์˜ testFixtures ์ œ๊ณต + +### ์ฃผ์˜์‚ฌํ•ญ + +- testFixtures๋Š” **ํ…Œ์ŠคํŠธ ์ „์šฉ**์ด๋ฉฐ, ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ +- testFixtures ๊ฐ„ ์˜์กด์„ฑ์€ ์ตœ์†Œํ™” (์ˆœํ™˜ ์˜์กด์„ฑ ๋ฐฉ์ง€) +- Repository๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์‹ค์ œ DB์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹ ์‚ฌ์šฉ +- ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ testFixtures์— ํฌํ•จํ•˜์ง€ ์•Š์Œ + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/guides/test-layer-guide.md b/claudedocs/guides/test-layer-guide.md new file mode 100644 index 0000000..b49edae --- /dev/null +++ b/claudedocs/guides/test-layer-guide.md @@ -0,0 +1,1033 @@ +# ํ…Œ์ŠคํŠธ ๋ ˆ์ด์–ด๋ณ„ ๊ตฌ์„ฑ ๊ฐ€์ด๋“œ + +> WellMeet-Backend ํ”„๋กœ์ ํŠธ์˜ ํ…Œ์ŠคํŠธ ๋ ˆ์ด์–ด๋ณ„ ๊ตฌ์„ฑ ๋ฐ ์ž‘์„ฑ ํ‘œ์ค€ + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [Entity Layer (domain-* ๋ชจ๋“ˆ)](#1-entity-layer-domain--๋ชจ๋“ˆ) +2. [Repository Layer (domain-* ๋ชจ๋“ˆ)](#2-repository-layer-domain--๋ชจ๋“ˆ) +3. [Domain Service Layer (domain-reservation ๋ชจ๋“ˆ)](#3-domain-service-layer-domain-reservation-๋ชจ๋“ˆ) +4. [Service Layer (api-user, api-owner ๋ชจ๋“ˆ)](#4-service-layer-api-user-api-owner-๋ชจ๋“ˆ) +5. [Controller Layer (api-user, api-owner ๋ชจ๋“ˆ)](#5-controller-layer-api-user-api-owner-๋ชจ๋“ˆ) +6. [Redis Service Layer (infra-redis ๋ชจ๋“ˆ)](#6-redis-service-layer-infra-redis-๋ชจ๋“ˆ) +7. [Kafka Producer Layer (infra-kafka ๋ชจ๋“ˆ)](#7-kafka-producer-layer-infra-kafka-๋ชจ๋“ˆ) +8. [Batch Job Layer (batch-reminder ๋ชจ๋“ˆ)](#8-batch-job-layer-batch-reminder-๋ชจ๋“ˆ) + +--- + +## 1. Entity Layer (domain-* ๋ชจ๋“ˆ) + +**๋ชฉ์ **: ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ƒ์„ฑ, ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ํ…Œ์ŠคํŠธ + +**์ ์šฉ ๋ชจ๋“ˆ**: + +- `domain-reservation` (์˜ˆ์•ฝ) +- `domain-member` (ํšŒ์›) +- `domain-owner` (์‚ฌ์—…์ž) +- `domain-restaurant` (์‹๋‹น) + +**์œ„์น˜**: `domain-{๋ชจ๋“ˆ๋ช…}/src/test/java/com/wellmeet/domain/{aggregate}/entity/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: ์—†์Œ (์ˆœ์ˆ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ) + +**๊ตฌ์„ฑ ์˜ˆ์‹œ**: + +```java +package com.wellmeet.domain.restaurant.entity; + +import static org.assertj.core.api.Assertions.*; + +import com.wellmeet.domain.restaurant.exception.RestaurantErrorCode; +import com.wellmeet.domain.restaurant.exception.RestaurantException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class RestaurantTest { + + @Nested + class ValidatePosition { + + @ParameterizedTest + @ValueSource(doubles = {Restaurant.MINIMUM_LATITUDE - 0.1, Restaurant.MAXIMUM_LATITUDE + 0.1}) + void ์œ„๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค(double latitude) { + assertThatThrownBy(() -> new Restaurant( + "id", + "name", + "address", + latitude, + 127.0, + "thumbnail", + null + )).isInstanceOf(RestaurantException.class) + .hasMessage(RestaurantErrorCode.INVALID_LATITUDE.getMessage()); + } + + @ParameterizedTest + @ValueSource(doubles = {Restaurant.MINIMUM_LONGITUDE - 0.1, Restaurant.MAXIMUM_LONGITUDE + 0.1}) + void ๊ฒฝ๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค(double longitude) { + assertThatThrownBy(() -> new Restaurant( + "id", + "name", + "address", + 37.5, + longitude, + "thumbnail", + null + )).isInstanceOf(RestaurantException.class) + .hasMessage(RestaurantErrorCode.INVALID_LONGITUDE.getMessage()); + } + } + + @Nested + class UpdateMetadata { + + @Test + void ์‹๋‹น_์ด๋ฆ„์„_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { + Restaurant restaurant = createDefaultRestaurant(); + String newName = "๋ณ€๊ฒฝ๋œ ์‹๋‹น๋ช…"; + + restaurant.updateName(newName); + + assertThat(restaurant.getName()).isEqualTo(newName); + } + + @Test + void ์‹๋‹น_์ฃผ์†Œ๋ฅผ_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { + Restaurant restaurant = createDefaultRestaurant(); + String newAddress = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์‹ ์‚ฌ๋™"; + + restaurant.updateAddress(newAddress); + + assertThat(restaurant.getAddress()).isEqualTo(newAddress); + } + } + + private Restaurant createDefaultRestaurant() { + return new Restaurant( + "id", + "๊ธฐ๋ณธ ์‹๋‹น", + "์„œ์šธ์‹œ", + 37.5, + 127.0, + "thumbnail", + null + ); + } +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ๋ช…์€ ํ•œ๊ธ€๋กœ ์ž‘์„ฑ (์–ธ๋”์Šค์ฝ”์–ด ์‚ฌ์šฉ) +- โœ… ์ •์ƒ ์ผ€์ด์Šค + ์˜ˆ์™ธ ์ผ€์ด์Šค ๋ชจ๋‘ ์ž‘์„ฑ +- โœ… ParameterizedTest ํ™œ์šฉ (๋ฐ˜๋ณต ์ผ€์ด์Šค) +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ DB ์ ‘๊ทผ ๊ธˆ์ง€ (์ˆœ์ˆ˜ ๊ฐ์ฒด ํ…Œ์ŠคํŠธ) +- โŒ Mock ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +## 2. Repository Layer (domain-* ๋ชจ๋“ˆ) + +**๋ชฉ์ **: @Query ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ง์ ‘ ์ž‘์„ฑํ•œ ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ๋ฉ”์†Œ๋“œ ํ…Œ์ŠคํŠธ + +**์ ์šฉ ๋ชจ๋“ˆ**: + +- `domain-reservation` (์˜ˆ์•ฝ) +- `domain-member` (ํšŒ์›) +- `domain-owner` (์‚ฌ์—…์ž) +- `domain-restaurant` (์‹๋‹น) + +**์œ„์น˜**: `domain-{๋ชจ๋“ˆ๋ช…}/src/test/java/com/wellmeet/domain/{aggregate}/repository/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `BaseRepositoryTest` + +**ํ…Œ์ŠคํŠธ ๋Œ€์ƒ**: + +- โœ… @Query๋กœ ์ง์ ‘ ์ž‘์„ฑํ•œ JPQL/Native SQL ๋ฉ”์†Œ๋“œ +- โœ… ๋ณต์žกํ•œ ์กฐ์ธ, ์ง‘๊ณ„ ์ฟผ๋ฆฌ +- โœ… Custom Repository ๊ตฌํ˜„์ฒด +- โŒ findById, save, findAll ๋“ฑ ์ž๋™ ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ๋Š” ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์Œ + +**๊ตฌ์„ฑ ์˜ˆ์‹œ**: + +```java +package com.wellmeet.domain.restaurant.repository; + +import static org.assertj.core.api.Assertions.*; + +import com.wellmeet.BaseRepositoryTest; +import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.domain.restaurant.model.BoundingBox; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class RestaurantRepositoryTest extends BaseRepositoryTest { + + @Autowired + private RestaurantRepository restaurantRepository; + + @Nested + class FindWithBoundBox { + + @Test + void BoundingBox_๋‚ด์˜_์‹๋‹น๋งŒ_์กฐํšŒํ•œ๋‹ค() { + Restaurant restaurant1 = createAndSaveRestaurant("์‹๋‹น1", 37.5, 127.0); + Restaurant restaurant2 = createAndSaveRestaurant("์‹๋‹น2", 37.501, 127.001); + Restaurant restaurant3 = createAndSaveRestaurant("์‹๋‹น3", 38.0, 128.0); + + BoundingBox boundingBox = new BoundingBox(37.4, 37.6, 126.9, 127.1); + + List result = restaurantRepository.findWithBoundBox(boundingBox); + + assertThat(result) + .hasSize(2) + .extracting(Restaurant::getName) + .containsExactlyInAnyOrder("์‹๋‹น1", "์‹๋‹น2"); + } + + @Test + void BoundingBox_๋ฐ–์˜_์‹๋‹น์€_์กฐํšŒ๋˜์ง€_์•Š๋Š”๋‹ค() { + Restaurant restaurant = createAndSaveRestaurant("๋จผ_์‹๋‹น", 38.0, 128.0); + + BoundingBox boundingBox = new BoundingBox(37.4, 37.6, 126.9, 127.1); + + List result = restaurantRepository.findWithBoundBox(boundingBox); + + assertThat(result).isEmpty(); + } + } + + private Restaurant createAndSaveRestaurant(String name, double lat, double lon) { + Restaurant restaurant = new Restaurant( + name, + "description", + "address", + lat, + lon, + "thumbnail", + null + ); + return restaurantRepository.save(restaurant); + } +} +``` + +**BaseRepositoryTest ๊ตฌ์กฐ**: + +```java + +@Import({JpaAuditingConfig.class}) +@ExtendWith(DataBaseCleaner.class) +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public abstract class BaseRepositoryTest { +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… `BaseRepositoryTest` ์ƒ์† ํ•„์ˆ˜ +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… ์‹ค์ œ DB(Testcontainers MySQL) ์‚ฌ์šฉ +- โœ… `@DataJpaTest`๋กœ ์ตœ์†Œํ•œ์˜ ์ปจํ…์ŠคํŠธ๋งŒ ๋กœ๋“œ +- โœ… **@Query๋กœ ์ง์ ‘ ์ž‘์„ฑํ•œ ๋ฉ”์†Œ๋“œ๋งŒ ํ…Œ์ŠคํŠธ** +- โŒ findById, save, findAll ๋“ฑ ์ž๋™ ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜์ง€ ์•Š์Œ +- โŒ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ…Œ์ŠคํŠธ ๊ธˆ์ง€ (Domain Service์—์„œ) +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +## 3. Domain Service Layer (domain-reservation ๋ชจ๋“ˆ) + +**๋ชฉ์ **: ๋„๋ฉ”์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + Repository ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +**์œ„์น˜**: `domain-reservation/src/test/java/com/wellmeet/domain/{aggregate}/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `BaseRepositoryTest` (Repository ํฌํ•จ ํ…Œ์ŠคํŠธ) + +**๊ตฌ์„ฑ ์˜ˆ์‹œ**: + +```java +package com.wellmeet.domain.restaurant; + +import static org.assertj.core.api.Assertions.*; + +import com.wellmeet.BaseRepositoryTest; +import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.domain.restaurant.repository.RestaurantRepository; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import(RestaurantDomainService.class) +class RestaurantDomainServiceTest extends BaseRepositoryTest { + + @Autowired + private RestaurantDomainService restaurantDomainService; + + @Autowired + private RestaurantRepository restaurantRepository; + + @Nested + class FindNearbyRestaurants { + + @Test + void BoundingBox๋ฅผ_๊ณ„์‚ฐํ•˜์—ฌ_์ฃผ๋ณ€_์‹๋‹น์„_์กฐํšŒํ•œ๋‹ค() { + createAndSaveRestaurant("์‹๋‹น1", 37.5, 127.0); + createAndSaveRestaurant("์‹๋‹น2", 37.501, 127.001); + createAndSaveRestaurant("๋จผ์‹๋‹น", 38.0, 128.0); + + double userLat = 37.5; + double userLon = 127.0; + double radiusKm = 1.0; + + List result = restaurantDomainService + .findNearbyRestaurants(userLat, userLon, radiusKm); + + assertThat(result) + .hasSize(2) + .extracting(Restaurant::getName) + .containsExactlyInAnyOrder("์‹๋‹น1", "์‹๋‹น2"); + } + + @Test + void ๋ฐ˜๊ฒฝ_๋‚ด์—_์‹๋‹น์ด_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + createAndSaveRestaurant("๋จผ์‹๋‹น", 38.0, 128.0); + + double userLat = 37.5; + double userLon = 127.0; + double radiusKm = 0.1; + + List result = restaurantDomainService + .findNearbyRestaurants(userLat, userLon, radiusKm); + + assertThat(result).isEmpty(); + } + } + + @Nested + class UpdateRestaurantMetadata { + + @Test + void ์‹๋‹น_๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ_์—…๋ฐ์ดํŠธํ•œ๋‹ค() { + Restaurant restaurant = createAndSaveRestaurant("์›๋ณธ ์‹๋‹น", 37.5, 127.0); + String newName = "์ˆ˜์ •๋œ ์‹๋‹น"; + String newAddress = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์‹ ์‚ฌ๋™"; + + Restaurant updated = restaurantDomainService + .updateRestaurantMetadata(restaurant.getId(), newName, newAddress); + + assertThat(updated.getName()).isEqualTo(newName); + assertThat(updated.getAddress()).isEqualTo(newAddress); + + Restaurant persisted = restaurantRepository.findById(restaurant.getId()) + .orElseThrow(); + assertThat(persisted.getName()).isEqualTo(newName); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_์‹๋‹น_์กฐํšŒ_์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + String nonExistentId = "non-existent-id"; + + assertThatThrownBy(() -> + restaurantDomainService.getRestaurantById(nonExistentId)) + .isInstanceOf(RestaurantException.class) + .hasMessageContaining("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‹๋‹น"); + } + } + + private Restaurant createAndSaveRestaurant(String name, double lat, double lon) { + Restaurant restaurant = new Restaurant( + name, + "description", + "address", + lat, + lon, + "thumbnail", + null + ); + return restaurantRepository.save(restaurant); + } +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… `@Import(DomainService.class)` ๋ช…์‹œ +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… Repository์™€ ํ•จ๊ป˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ +- โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฒ€์ฆ (๊ณ„์‚ฐ, ๋ณ€ํ™˜, ์œ ํšจ์„ฑ) +- โœ… ์˜ˆ์™ธ ์ƒํ™ฉ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ +- โœ… ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ํ™•์ธ +- โŒ Controller ๋กœ์ง ํฌํ•จ ๊ธˆ์ง€ +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +## 4. Service Layer (api-user, api-owner ๋ชจ๋“ˆ) + +**๋ชฉ์ **: Application Service ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + Mock ๊ธฐ๋ฐ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +**์œ„์น˜**: `api-{user|owner}/src/test/java/com/wellmeet/{feature}/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: Mock ์‚ฌ์šฉ ์‹œ ์—†์Œ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹œ `BaseServiceTest` + +**๊ตฌ์„ฑ ์˜ˆ์‹œ - ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Mock)**: + +```java +package com.wellmeet.reservation; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.wellmeet.domain.member.entity.Member; +import com.wellmeet.domain.owner.entity.Owner; +import com.wellmeet.domain.reservation.ReservationDomainService; +import com.wellmeet.domain.reservation.entity.Reservation; +import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; +import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.reservation.dto.ReservationResponse; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ReservationServiceTest { + + @Mock + private ReservationDomainService reservationDomainService; + + @InjectMocks + private ReservationService reservationService; + + @Nested + class GetReservations { + + @Test + void ์‹๋‹น_์•„์ด๋””์—_ํ•ด๋‹นํ•˜๋Š”_์˜ˆ์•ฝ๋ชฉ๋ก์„_๋ถˆ๋Ÿฌ์˜จ๋‹ค() { + Restaurant restaurant = createRestaurant("Test Restaurant"); + AvailableDate availableDate = createAvailableDate(LocalDateTime.now(), 10, restaurant); + Member member1 = createMember("Test"); + Member member2 = createMember("Test2"); + Reservation reservation1 = createReservation(restaurant, availableDate, member1, 4); + Reservation reservation2 = createReservation(restaurant, availableDate, member2, 2); + List reservations = List.of(reservation1, reservation2); + + when(reservationDomainService.findAllByRestaurantId(restaurant.getId())) + .thenReturn(reservations); + + List expectedReservations = reservationService.getReservations(restaurant.getId()); + + assertThat(expectedReservations).hasSize(reservations.size()); + } + } + + private Restaurant createRestaurant(String name) { + return new Restaurant(name, "description", "address", 32.1, 37.1, "thumbnail", new Owner("name", "email")); + } + + private AvailableDate createAvailableDate(LocalDateTime dateTime, int capacity, Restaurant restaurant) { + return new AvailableDate(dateTime.toLocalDate(), dateTime.toLocalTime(), capacity, restaurant); + } + + private Member createMember(String name) { + return new Member(name, "nickname", "email@email.com", "phone"); + } + + private Reservation createReservation(Restaurant restaurant, AvailableDate availableDate, Member member, + int partySize) { + return new Reservation(restaurant, availableDate, member, partySize, "request"); + } +} +``` + +**๊ตฌ์„ฑ ์˜ˆ์‹œ - ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (BaseServiceTest)**: + +```java +package com.wellmeet.reservation; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.wellmeet.BaseServiceTest; +import com.wellmeet.domain.member.entity.Member; +import com.wellmeet.domain.owner.entity.Owner; +import com.wellmeet.domain.reservation.entity.Reservation; +import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; +import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.reservation.dto.CreateReservationRequest; +import com.wellmeet.reservation.dto.CreateReservationResponse; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class ReservationServiceTest extends BaseServiceTest { + + @Autowired + private ReservationService reservationService; + + @Autowired + private ReservationRedisService reservationRedisService; + + @BeforeEach + void setUp() { + reservationRedisService.deleteReservationLock(); + } + + @Nested + class Reserve { + + @Test + void ํ•œ_์‚ฌ๋žŒ์ด_๊ฐ™์€_์˜ˆ์•ฝ_์š”์ฒญ์„_๋™์‹œ์—_์—ฌ๋Ÿฌ๋ฒˆ_์‹ ์ฒญํ•ด๋„_ํ•œ_๋ฒˆ๋งŒ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { + Owner owner1 = ownerGenerator.generate("owner1"); + Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1); + int capacity = 100; + AvailableDate availableDate = availableDateGenerator.generate( + LocalDateTime.now().plusDays(1), capacity, restaurant1 + ); + int partySize = 4; + CreateReservationRequest request = new CreateReservationRequest( + restaurant1.getId(), availableDate.getId(), partySize, "request" + ); + Member member = memberGenerator.generate("test"); + + runAtSameTime(500, () -> reservationService.reserve(member.getId(), request)); + + List reservations = reservationRepository.findAll(); + AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); + + assertAll( + () -> assertThat(reservations).hasSize(1), + () -> assertThat(foundAvailableDate.getMaxCapacity()).isEqualTo(capacity - partySize) + ); + } + + @Test + void ์—ฌ๋Ÿฌ_์‚ฌ๋žŒ์ด_์˜ˆ์•ฝ_์š”์ฒญ์„_๋™์‹œ์—_์‹ ์ฒญํ•ด๋„_์ ์ ˆํžˆ_์ฒ˜๋ฆฌ๋œ๋‹ค() throws InterruptedException { + Owner owner1 = ownerGenerator.generate("owner1"); + Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1); + int capacity = 100; + AvailableDate availableDate = availableDateGenerator.generate( + LocalDateTime.now().plusDays(1), capacity, restaurant1 + ); + int partySize = 2; + CreateReservationRequest request = new CreateReservationRequest( + restaurant1.getId(), availableDate.getId(), partySize, "request" + ); + List tasks = new ArrayList<>(); + for (int i = 0; i < 50; i++) { + Member member = memberGenerator.generate("member" + i); + tasks.add(() -> reservationService.reserve(member.getId(), request)); + } + + runAtSameTime(tasks); + + List reservations = reservationRepository.findAll(); + AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); + + assertAll( + () -> assertThat(reservations).hasSize(50), + () -> assertThat(foundAvailableDate.getMaxCapacity()).isZero() + ); + } + } +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: Mock ์‚ฌ์šฉ, ๋น ๋ฅธ ์‹คํ–‰ +- โœ… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: `BaseServiceTest` ์ƒ์†, ์‹ค์ œ DB +- โœ… ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ: `runAtSameTime()` ์œ ํ‹ธ ํ™œ์šฉ +- โœ… DTO ๋ณ€ํ™˜ ๋กœ์ง ๊ฒ€์ฆ +- โŒ HTTP ์š”์ฒญ/์‘๋‹ต ํ…Œ์ŠคํŠธ ๊ธˆ์ง€ (Controller์—์„œ) +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +## 5. Controller Layer (api-user, api-owner ๋ชจ๋“ˆ) + +**๋ชฉ์ **: REST API E2E ํ…Œ์ŠคํŠธ (HTTP โ†’ Service โ†’ DB) + +**์œ„์น˜**: `api-{user|owner}/src/test/java/com/wellmeet/{feature}/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `BaseControllerTest` + +**๊ตฌ์„ฑ ์˜ˆ์‹œ**: + +```java +package com.wellmeet.favorite; + +import static org.assertj.core.api.Assertions.*; + +import com.wellmeet.BaseControllerTest; +import com.wellmeet.domain.member.entity.FavoriteRestaurant; +import com.wellmeet.domain.member.entity.Member; +import com.wellmeet.domain.owner.entity.Owner; +import com.wellmeet.domain.restaurant.entity.Restaurant; +import com.wellmeet.favorite.dto.FavoriteRestaurantResponse; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class FavoriteControllerTest extends BaseControllerTest { + + @Nested + class GetFavoriteRestaurants { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์กฐํšŒ() { + Member testUser = memberGenerator.generate("test"); + Member anotherUser = memberGenerator.generate("another"); + Owner owner1 = ownerGenerator.generate("Owner1"); + Owner owner2 = ownerGenerator.generate("Owner2"); + Owner owner3 = ownerGenerator.generate("Owner3"); + Restaurant restaurant1 = restaurantGenerator.generate("Restaurant 1", owner1); + Restaurant restaurant2 = restaurantGenerator.generate("Restaurant 2", owner2); + Restaurant restaurant3 = restaurantGenerator.generate("Restaurant 3", owner3); + favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser, restaurant1)); + favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser, restaurant2)); + favoriteRestaurantRepository.save(new FavoriteRestaurant(anotherUser, restaurant2)); + favoriteRestaurantRepository.save(new FavoriteRestaurant(anotherUser, restaurant3)); + + FavoriteRestaurantResponse[] responses = given() + .contentType("application/json") + .queryParam("memberId", testUser.getId()) + .when().get("/user/favorite/restaurant/list") + .then().statusCode(HttpStatus.OK.value()) + .extract().as(FavoriteRestaurantResponse[].class); + + assertThat(responses).hasSize(2); + assertThat(responses[0].getId()).isEqualTo(restaurant1.getId()); + assertThat(responses[1].getId()).isEqualTo(restaurant2.getId()); + } + } + + @Nested + class AddFavoriteRestaurant { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์ถ”๊ฐ€() { + Member testUser = memberGenerator.generate("testUser"); + Owner owner = ownerGenerator.generate("Test Owner"); + Restaurant restaurant = restaurantGenerator.generate("Test Restaurant", owner); + + FavoriteRestaurantResponse response = given() + .contentType("application/json") + .queryParam("memberId", testUser.getId()) + .when().post("/user/favorite/restaurant/{restaurantId}", restaurant.getId()) + .then().statusCode(HttpStatus.CREATED.value()) + .extract().as(FavoriteRestaurantResponse.class); + + assertThat(response.getId()).isEqualTo(restaurant.getId()); + } + } + + @Nested + class RemoveFavoriteRestaurant { + + @Test + void ์ฆ๊ฒจ์ฐพ๊ธฐ_๋ ˆ์Šคํ† ๋ž‘_์‚ญ์ œ() { + Member testUser = memberGenerator.generate("testUser"); + Owner owner = ownerGenerator.generate("Test Owner"); + Restaurant restaurant = restaurantGenerator.generate("Test Restaurant", owner); + favoriteRestaurantRepository.save(new FavoriteRestaurant(testUser, restaurant)); + + given() + .contentType("application/json") + .queryParam("memberId", testUser.getId()) + .when().delete("/user/favorite/restaurant/{restaurantId}", restaurant.getId()) + .then().statusCode(HttpStatus.NO_CONTENT.value()); + } + } +} +``` + +**BaseControllerTest ๊ตฌ์กฐ**: + +```java + +@ExtendWith(DataBaseCleaner.class) +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public abstract class BaseControllerTest { + + @LocalServerPort + private int port; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… `BaseControllerTest` ์ƒ์† ํ•„์ˆ˜ +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ API๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… REST Assured ์‚ฌ์šฉ +- โœ… HTTP ์ƒํƒœ ์ฝ”๋“œ ๊ฒ€์ฆ +- โœ… ์‘๋‹ต ๋ณธ๋ฌธ ๊ตฌ์กฐ ๊ฒ€์ฆ +- โœ… ์„ฑ๊ณต/์‹คํŒจ ์ผ€์ด์Šค ๋ชจ๋‘ ์ž‘์„ฑ +- โœ… ์ธ์ฆ/๊ถŒํ•œ ๊ฒ€์ฆ (ํ—ค๋”) +- โŒ Mock ์‚ฌ์šฉ ๊ธˆ์ง€ (E2E๋Š” ์‹ค์ œ ํ๋ฆ„) +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +## 6. Redis Service Layer (infra-redis ๋ชจ๋“ˆ) + +**๋ชฉ์ **: ๋ถ„์‚ฐ ๋ฝ, ์บ์‹ฑ ๋กœ์ง ํ…Œ์ŠคํŠธ + +**์œ„์น˜**: `infra-redis/src/test/java/com/wellmeet/{feature}/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: Testcontainers ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +**์ฃผ์š” ๊ธฐ์ˆ **: Redisson 3.50.0 (๋ถ„์‚ฐ ๋ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) + +**๊ตฌ์„ฑ ์˜ˆ์‹œ**: + +```java +package com.wellmeet.reservation; + +import static org.assertj.core.api.Assertions.*; + +import com.wellmeet.reservation.ReservationRedisService; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest +@Testcontainers +class ReservationRedisServiceTest { + + @Container + static GenericContainer redis = new GenericContainer<>("redis:7-alpine") + .withExposedPorts(6379); + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", redis::getHost); + registry.add("spring.data.redis.port", redis::getFirstMappedPort); + } + + @Autowired + private ReservationRedisService reservationRedisService; + + @Nested + class IsReserving { + + @Test + void ๋™์‹œ_์š”์ฒญ_์‹œ_ํ•˜๋‚˜๋งŒ_๋ฝ์„_ํš๋“ํ•œ๋‹ค() throws InterruptedException { + String memberId = "member-1"; + String restaurantId = "restaurant-1"; + Long availableDateId = 1L; + + int threadCount = 10; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicInteger successCount = new AtomicInteger(0); + + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + boolean acquired = reservationRedisService + .isReserving(memberId, restaurantId, availableDateId); + if (acquired) { + successCount.incrementAndGet(); + } + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + assertThat(successCount.get()).isEqualTo(1); + } + } + + @Nested + class IsUpdating { + + @Test + void ๋ฝ์„_์ •์ƒ์ ์œผ๋กœ_ํš๋“ํ•˜๊ณ _ํ•ด์ œํ•œ๋‹ค() { + String memberId = "member-1"; + Long reservationId = 1L; + + boolean acquired = reservationRedisService.isUpdating(memberId, reservationId); + + assertThat(acquired).isTrue(); + + boolean retry = reservationRedisService.isUpdating(memberId, reservationId); + assertThat(retry).isFalse(); + } + } + + @Nested + class DeleteReservationLock { + + @Test + void ๋ฝ_์‚ญ์ œ_ํ›„_๋‹ค์‹œ_ํš๋“_๊ฐ€๋Šฅํ•˜๋‹ค() { + String memberId = "member-1"; + String restaurantId = "restaurant-1"; + Long availableDateId = 1L; + + reservationRedisService.isReserving(memberId, restaurantId, availableDateId); + + reservationRedisService.deleteReservationLock(); + + boolean reacquired = reservationRedisService + .isReserving(memberId, restaurantId, availableDateId); + assertThat(reacquired).isTrue(); + } + } +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… Testcontainers๋กœ ์‹ค์ œ Redis ์‚ฌ์šฉ +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ ํ•„์ˆ˜ +- โœ… ๋ฝ ํš๋“/ํ•ด์ œ ์‚ฌ์ดํด ๊ฒ€์ฆ +- โœ… ํƒ€์ž„์•„์›ƒ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ +- โŒ Mock Redis ์‚ฌ์šฉ ๊ธˆ์ง€ (๋ถ„์‚ฐ ๋ฝ์€ ์‹ค์ œ ํ™˜๊ฒฝ ํ•„์ˆ˜) +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +## 7. Kafka Producer Layer (infra-kafka ๋ชจ๋“ˆ) + +**๋ชฉ์ **: ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก, ์ง๋ ฌํ™”, ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ + +**์œ„์น˜**: `infra-kafka/src/test/java/com/wellmeet/kafka/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: EmbeddedKafka ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +**์ฃผ์š” ๊ธฐ์ˆ **: AWS MSK (Managed Streaming for Apache Kafka) + IAM ์ธ์ฆ + +โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ (์•„๋ž˜ ์˜ˆ์‹œ๋Š” ํ–ฅํ›„ ์ž‘์„ฑ์„ ์œ„ํ•œ ๊ฐ€์ด๋“œ) + +**๊ตฌ์„ฑ ์˜ˆ์‹œ**: + +```java +package com.wellmeet.kafka.service; + +import static org.assertj.core.api.Assertions.*; + +import com.wellmeet.kafka.dto.NotificationMessage; +import com.wellmeet.kafka.dto.payload.ReservationCreatedPayload; +import java.time.LocalDateTime; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest +@EmbeddedKafka( + partitions = 1, + topics = {"notification"}, + brokerProperties = { + "listeners=PLAINTEXT://localhost:9092", + "port=9092" + } +) +@DirtiesContext +class KafkaProducerServiceTest { + + @Autowired + private KafkaProducerService kafkaProducerService; + + private BlockingQueue receivedMessages = new LinkedBlockingQueue<>(); + + @KafkaListener(topics = "notification", groupId = "test-group") + public void listen(NotificationMessage message) { + receivedMessages.add(message); + } + + @Nested + class SendNotificationMessage { + + @Test + void ์˜ˆ์•ฝ_์ƒ์„ฑ_์•Œ๋ฆผ_๋ฉ”์‹œ์ง€๋ฅผ_๋ฐœ์†กํ•œ๋‹ค() throws InterruptedException { + ReservationCreatedPayload payload = ReservationCreatedPayload.builder() + .reservationId("reservation-1") + .restaurantName("๋ง›์ง‘") + .reservationDate(LocalDateTime.now().plusDays(1)) + .partySize(4) + .build(); + + String memberId = "member-1"; + + kafkaProducerService.sendNotificationMessage(memberId, payload); + + NotificationMessage received = receivedMessages.poll(5, TimeUnit.SECONDS); + assertThat(received).isNotNull(); + assertThat(received.getHeader().getRecipientId()).isEqualTo(memberId); + assertThat(received.getPayload()).isInstanceOf(ReservationCreatedPayload.class); + + ReservationCreatedPayload receivedPayload = + (ReservationCreatedPayload) received.getPayload(); + assertThat(receivedPayload.getReservationId()).isEqualTo("reservation-1"); + assertThat(receivedPayload.getRestaurantName()).isEqualTo("๋ง›์ง‘"); + } + + @Test + void ์ง๋ ฌํ™”_์—ญ์ง๋ ฌํ™”๊ฐ€_์ •์ƒ์ ์œผ๋กœ_๋™์ž‘ํ•œ๋‹ค() throws InterruptedException { + ReservationCreatedPayload payload = ReservationCreatedPayload.builder() + .reservationId("res-123") + .restaurantName("ํ•œ์‹๋‹น") + .reservationDate(LocalDateTime.of(2025, 12, 25, 18, 0)) + .partySize(2) + .build(); + + kafkaProducerService.sendNotificationMessage("member-1", payload); + + NotificationMessage received = receivedMessages.poll(5, TimeUnit.SECONDS); + assertThat(received).isNotNull(); + + ReservationCreatedPayload receivedPayload = + (ReservationCreatedPayload) received.getPayload(); + assertThat(receivedPayload.getReservationDate()) + .isEqualTo(LocalDateTime.of(2025, 12, 25, 18, 0)); + } + } +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… `@EmbeddedKafka` ์‚ฌ์šฉ +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ ๋ฉ”์†Œ๋“œ๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… Consumer๋กœ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ๊ฒ€์ฆ +- โœ… ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ๊ฒ€์ฆ +- โœ… ํƒ€์ž„์•„์›ƒ ์„ค์ • (5์ดˆ) +- โœ… `@DirtiesContext`๋กœ ์ปจํ…์ŠคํŠธ ๊ฒฉ๋ฆฌ +- โŒ ์‹ค์ œ Kafka ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ๊ธˆ์ง€ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ) +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +## 8. Batch Job Layer (batch-reminder ๋ชจ๋“ˆ) + +**๋ชฉ์ **: Spring Batch Job ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ + +**์œ„์น˜**: `batch-reminder/src/test/java/com/wellmeet/batch/` + +**๋ฒ ์ด์Šค ํด๋ž˜์Šค**: `TestBatchConfig` ํฌํ•จ + +โš ๏ธ **ํ˜„์žฌ ์ƒํƒœ**: ํ…Œ์ŠคํŠธ ๋ฏธ์ž‘์„ฑ (์•„๋ž˜ ์˜ˆ์‹œ๋Š” ํ–ฅํ›„ ์ž‘์„ฑ์„ ์œ„ํ•œ ๊ฐ€์ด๋“œ) + +**๊ตฌ์„ฑ ์˜ˆ์‹œ**: + +```java +package com.wellmeet.batch.job; + +import static org.assertj.core.api.Assertions.*; + +import com.wellmeet.batch.config.TestBatchConfig; +import com.wellmeet.domain.member.entity.Member; +import com.wellmeet.domain.reservation.entity.Reservation; +import com.wellmeet.domain.restaurant.entity.Restaurant; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +@SpringBootTest +@Import(TestBatchConfig.class) +class ReservationReminderJobConfigTest { + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @Nested + class ExecuteReminderJob { + + @Test + void ํ•œ_์‹œ๊ฐ„_์ „_์˜ˆ์•ฝ_๋ฆฌ๋งˆ์ธ๋”_๋ฐฐ์น˜๊ฐ€_์„ฑ๊ณตํ•œ๋‹ค() throws Exception { + Member member = createMember(); + Restaurant restaurant = createRestaurant(); + Reservation reservation = createReservation( + member, + restaurant, + LocalDateTime.now().plusHours(1) + ); + + JobExecution jobExecution = jobLauncherTestUtils.launchJob(); + + assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); + assertThat(jobExecution.getStepExecutions()).hasSize(1); + } + } +} +``` + +**์ž‘์„ฑ ๊ทœ์น™**: + +- โœ… `JobLauncherTestUtils` ์‚ฌ์šฉ +- โœ… `@Nested` ํด๋ž˜์Šค๋กœ Job๋ณ„ ๊ทธ๋ฃนํ™” +- โœ… Job ์‹คํ–‰ ์ƒํƒœ ๊ฒ€์ฆ +- โœ… Step ์‹คํ–‰ ๊ฒฐ๊ณผ ๊ฒ€์ฆ +- โœ… Reader/Processor/Writer ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ +- โœ… Clock ์ฃผ์ž…์œผ๋กœ ์‹œ๊ฐ„ ์ œ์–ด +- โŒ given, when, then ์ฃผ์„ ์‚ฌ์šฉ ๊ธˆ์ง€ +- โŒ @DisplayName ์‚ฌ์šฉ ๊ธˆ์ง€ + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/guides/test-writing-rules.md b/claudedocs/guides/test-writing-rules.md new file mode 100644 index 0000000..e95909f --- /dev/null +++ b/claudedocs/guides/test-writing-rules.md @@ -0,0 +1,158 @@ +# ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๊ทœ์น™ + +> WellMeet-Backend ํ”„๋กœ์ ํŠธ์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ๊ทœ์น™ ๋ฐ ์ปจ๋ฒค์…˜ + +## ๐Ÿ“š ๋ชฉ์ฐจ + +1. [๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜](#1-๋„ค์ด๋ฐ-์ปจ๋ฒค์…˜) +2. [์ฃผ์„ ์—†์ด ์ฝ”๋“œ๋กœ ํ‘œํ˜„](#2-์ฃผ์„-์—†์ด-์ฝ”๋“œ๋กœ-ํ‘œํ˜„) +3. [AssertJ ์‚ฌ์šฉ](#3-assertj-์‚ฌ์šฉ) +4. [์˜ˆ์™ธ ํ…Œ์ŠคํŠธ](#4-์˜ˆ์™ธ-ํ…Œ์ŠคํŠธ) +5. [ParameterizedTest ํ™œ์šฉ](#5-parameterizedtest-ํ™œ์šฉ) + +--- + +## 1. ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ + +```java +// โœ… Good - @Nested + ํ•œ๊ธ€ ๋ฉ”์†Œ๋“œ๋ช… +class RestaurantTest { + + @Nested + class ValidatePosition { + + @Test + void ์œ„๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค() { + } + + @Test + void ๊ฒฝ๋„๋Š”_์ผ์ •_๋ฒ”์œ„_์ด๋‚ด์—ฌ์•ผ_ํ•œ๋‹ค() { + } + } + + @Nested + class UpdateMetadata { + + @Test + void ์‹๋‹น_์ด๋ฆ„์„_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { + } + } +} + +// โŒ Bad - @DisplayName ์‚ฌ์šฉ +@DisplayName("Restaurant ์—”ํ‹ฐํ‹ฐ") +class RestaurantTest { + + @Test + @DisplayName("์œ„๋„ ๊ฒ€์ฆ") + void validateLatitude() { + } +} +``` + +--- + +## 2. ์ฃผ์„ ์—†์ด ์ฝ”๋“œ๋กœ ํ‘œํ˜„ + +```java +// โœ… Good - ์ฃผ์„ ์—†์ด ๋ฐ”๋กœ ์ฝ”๋“œ +@Test +void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ•œ๋‹ค() { + Member member = createMember(); + Restaurant restaurant = createRestaurant(); + CreateReservationRequest request = new CreateReservationRequest(...); + + CreateReservationResponse response = reservationService.create(member.getId(), request); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(ReservationStatus.PENDING); +} + +// โŒ Bad - given, when, then ์ฃผ์„ ์‚ฌ์šฉ +@Test +void createReservation() { + // given + Member member = createMember(); + + // when + Reservation reservation = service.create(member); + + // then + assertThat(reservation).isNotNull(); +} +``` + +--- + +## 3. AssertJ ์‚ฌ์šฉ + +```java +// โœ… Good - AssertJ +assertThat(result). + +isNotNull(); + +assertThat(result.getName()). + +isEqualTo("์‹๋‹น"); + +assertThat(list). + +hasSize(3). + +extracting(Restaurant::getName). + +containsExactly("A","B","C"); + +// โŒ Bad - JUnit Assertions +assertTrue(result !=null); + +assertEquals("์‹๋‹น",result.getName()); +``` + +--- + +## 4. ์˜ˆ์™ธ ํ…Œ์ŠคํŠธ + +```java +// โœ… Good +@Test +void ์ž˜๋ชป๋œ_์ž…๋ ฅ_์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + assertThatThrownBy(() -> service.doSomething()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("์ž˜๋ชป๋œ ์ž…๋ ฅ"); +} +``` + +--- + +## 5. ParameterizedTest ํ™œ์šฉ + +```java + +@ParameterizedTest +@ValueSource(ints = {0, -1, -100}) +void ์ธ์›_์ˆ˜๊ฐ€_0_์ดํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(int partySize) { + assertThatThrownBy(() -> Reservation.create(partySize)) + .isInstanceOf(IllegalArgumentException.class); +} + +@ParameterizedTest +@CsvSource({ + "37.5, 127.0, 1.0, 2", + "37.5, 127.0, 5.0, 5", + "37.5, 127.0, 10.0, 10" +}) +void ๋ฐ˜๊ฒฝ_๋‚ด_์‹๋‹น์„_์กฐํšŒํ•œ๋‹ค(double lat, double lon, double radius, int expectedCount) { + List result = service.findNearby(lat, lon, radius); + assertThat(result).hasSize(expectedCount); +} +``` + +--- + +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ถœ์ฒ˜**: CLAUDE.md +**๋ฒ„์ „**: v1.0 + +**์ฐธ๊ณ **: ์ด ๋ฌธ์„œ๋Š” [CLAUDE.md](../../CLAUDE.md)์—์„œ ์ถ”์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. diff --git a/claudedocs/microservices-migration-plan.md b/claudedocs/microservices-migration-plan.md index 6d35891..b3aa60b 100644 --- a/claudedocs/microservices-migration-plan.md +++ b/claudedocs/microservices-migration-plan.md @@ -72,307 +72,62 @@ --- -## Phase 1: domain-restaurant ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ (2-3์ฃผ) +## Phase 1: domain-restaurant ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ โœ… **๋ชฉํ‘œ**: domain-restaurant๋ฅผ ๋…๋ฆฝ ์„œ๋ฒ„๋กœ ๋ฐฐํฌํ•˜๋˜, **api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€** -### 1.1 REST API ๋ ˆ์ด์–ด ์ƒ์„ฑ - -**ํŒŒ์ผ ์ƒ์„ฑ**: `domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/api/RestaurantInternalController.java` - -```java -@RestController -@RequestMapping("/internal/restaurants") -class RestaurantInternalController { - private final RestaurantDomainService restaurantDomainService; - private final AvailableDateDomainService availableDateDomainService; - - // ๊ธฐ๋ณธ ์กฐํšŒ - @GetMapping("/{id}") - RestaurantResponse getRestaurant(@PathVariable String id) { - Restaurant restaurant = restaurantDomainService.getById(id); - return RestaurantResponse.from(restaurant); - } - - @GetMapping("/bulk") - List getRestaurantsBulk(@RequestParam List ids) { - return restaurantDomainService.getByIds(ids).stream() - .map(RestaurantResponse::from) - .toList(); - } - - // ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ๋‚ ์งœ - @GetMapping("/{id}/available-dates/{availableDateId}") - AvailableDateResponse getAvailableDate( - @PathVariable String id, - @PathVariable Long availableDateId - ) { - AvailableDate availableDate = availableDateDomainService.getById(availableDateId); - return AvailableDateResponse.from(availableDate); - } - - @PostMapping("/{id}/available-dates/{availableDateId}/decrease-capacity") - void decreaseCapacity( - @PathVariable String id, - @PathVariable Long availableDateId, - @RequestBody DecreaseCapacityRequest request - ) { - availableDateDomainService.decreaseCapacity(availableDateId, request.partySize()); - } - - @PostMapping("/{id}/available-dates/{availableDateId}/increase-capacity") - void increaseCapacity( - @PathVariable String id, - @PathVariable Long availableDateId, - @RequestBody IncreaseCapacityRequest request - ) { - availableDateDomainService.increaseCapacity(availableDateId, request.partySize()); - } -} -``` - -**DTO ์ƒ์„ฑ**: -```java -record RestaurantResponse( - String id, String name, String address, - double latitude, double longitude, - String thumbnailUrl, String ownerId -) { - static RestaurantResponse from(Restaurant restaurant) { - return new RestaurantResponse( - restaurant.getId(), - restaurant.getName(), - restaurant.getAddress(), - restaurant.getLatitude(), - restaurant.getLongitude(), - restaurant.getThumbnailUrl(), - restaurant.getOwner() != null ? restaurant.getOwner().getId() : null - ); - } -} - -record AvailableDateResponse( - Long id, LocalDate date, LocalTime time, - int maxCapacity, String restaurantId -) { - static AvailableDateResponse from(AvailableDate availableDate) { - return new AvailableDateResponse( - availableDate.getId(), - availableDate.getDate(), - availableDate.getTime(), - availableDate.getMaxCapacity(), - availableDate.getRestaurant().getId() - ); - } -} - -record DecreaseCapacityRequest(int partySize) {} -record IncreaseCapacityRequest(int partySize) {} -``` - -### 1.2 Spring Boot Application ํ™œ์„ฑํ™” - -**ํŒŒ์ผ ์ƒ์„ฑ**: `domain-restaurant/src/main/java/com/wellmeet/domain/RestaurantServiceApplication.java` - -โš ๏ธ **์ค‘์š”**: ๋นˆ ์Šค์บ” ๋ฌธ์ œ๋กœ ์ธํ•ด Application ํด๋ž˜์Šค๋Š” ์ƒ์„ฑ๋งŒ ํ•˜๊ณ  **์ „์ฒด ์ฃผ์„ ์ฒ˜๋ฆฌ** - -```java -//package com.wellmeet.domain; -// -//import org.springframework.boot.SpringApplication; -//import org.springframework.boot.autoconfigure.SpringBootApplication; -// -//@SpringBootApplication -//public class RestaurantServiceApplication { -// -// public static void main(String[] args) { -// SpringApplication.run(RestaurantServiceApplication.class, args); -// } -//} -``` - -**์ฐธ๊ณ **: -- `@EnableEurekaClient`๋Š” ์ตœ์‹  Spring Cloud ๋ฒ„์ „(2020.0.0+)์—์„œ ์ œ๊ฑฐ๋˜์—ˆ์œผ๋ฉฐ, `application.yml`์˜ eureka ์„ค์ •๋งŒ์œผ๋กœ ์ž๋™ ๋“ฑ๋ก๋จ -- `@EnableJpaAuditing`์€ domain-common ๋ชจ๋“ˆ์— ์ด๋ฏธ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๋ณ„๋„ ์„ค์ • ๋ถˆํ•„์š” - -**build.gradle ์ˆ˜์ •**: -```gradle -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - implementation 'org.springframework.boot:spring-boot-starter-actuator' -} -``` - -**application.yml ์ƒ์„ฑ**: -```yaml -spring: - application: - name: domain-restaurant-service -server: - port: 8081 -eureka: - client: - service-url: - defaultZone: http://localhost:8761/eureka/ -``` - -### 1.3 Dockerfile ์ƒ์„ฑ - -```dockerfile -FROM gradle:8.5-jdk21 AS build -WORKDIR /app -COPY . . -RUN gradle :domain-restaurant:bootJar --no-daemon - -FROM openjdk:21-jdk-slim -WORKDIR /app -COPY --from=build /app/domain-restaurant/build/libs/*.jar app.jar -HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8081/actuator/health || exit 1 -EXPOSE 8081 -ENTRYPOINT ["java", "-jar", "app.jar"] -``` - -### 1.4 docker-compose.yml ์—…๋ฐ์ดํŠธ - -```yaml -services: - domain-restaurant-service: - build: - context: . - dockerfile: domain-restaurant/Dockerfile - ports: - - "8081:8081" - environment: - SPRING_PROFILES_ACTIVE: local - SPRING_DATASOURCE_URL: jdbc:mysql://mysql-restaurant:3306/wellmeet_restaurant - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE: http://discovery-server:8761/eureka/ - depends_on: - - mysql-restaurant - - discovery-server - networks: - - wellmeet-network -``` - -### 1.5 ๊ฒ€์ฆ (๋…๋ฆฝ ์„œ๋ฒ„ ์‹คํ–‰) - -```bash -# Docker๋กœ domain-restaurant ์„œ๋น„์Šค ์‹คํ–‰ -docker-compose up -d domain-restaurant-service - -# Eureka ๋“ฑ๋ก ํ™•์ธ -curl http://localhost:8761 - -# Health check -curl http://localhost:8081/actuator/health - -# REST API ํ…Œ์ŠคํŠธ -curl http://localhost:8081/internal/restaurants/{restaurantId} -``` - -### 1.6 ์ค‘์š”: api-* ๋ชจ๋“ˆ ์˜์กด์„ฑ ์œ ์ง€ - -**api-user/build.gradle ๋ณ€๊ฒฝ ์—†์Œ**: -```gradle -dependencies { - // ์ด ๋‹จ๊ณ„์—์„œ๋Š” ์—ฌ์ „ํžˆ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ - implementation project(':domain-restaurant') // โœ… ์œ ์ง€ - implementation project(':domain-reservation') - implementation project(':domain-member') - implementation project(':domain-owner') -} -``` - -**Phase 1 ์™„๋ฃŒ ๊ธฐ์ค€**: +**๊ตฌํ˜„ ํŒจํ„ด**: domain-restaurant ๋ชจ๋“ˆ์— REST API Controller ๋ฐ Application Service ๋ ˆ์ด์–ด ์ถ”๊ฐ€. Phase 1๊ณผ ๋™์ผํ•œ ํŒจํ„ด์„ Phase 2-4์— ๋ฐ˜๋ณต ์ ์šฉ. + +**์ฃผ์š” ์ž‘์—…**: +- RestaurantDomainController, AvailableDateController ๋“ฑ REST API ์ƒ์„ฑ +- RestaurantApplicationService ๋ ˆ์ด์–ด ๊ตฌํ˜„ +- DTO ํด๋ž˜์Šค ์ƒ์„ฑ (Response/Request ํŒจํ„ด) +- Dockerfile, docker-compose.yml ์„ค์ • (ํฌํŠธ 8083) +- api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ (`implementation project(':domain-restaurant')`) + +**์ฐธ๊ณ **: ์ƒ์„ธ ๊ตฌํ˜„ ์ฝ”๋“œ๋Š” Git ํžˆ์Šคํ† ๋ฆฌ ์ฐธ์กฐ + +**Phase 1 ์™„๋ฃŒ ๊ธฐ์ค€**: โœ… **์™„๋ฃŒ (2025-11-05)** **์ฝ”๋“œ ๊ตฌํ˜„ (์™„๋ฃŒ)**: -- [x] domain-restaurant REST API Controller ์ƒ์„ฑ (DomainRestaurantController) -- [x] Application Service ๋ฐ DTO ๋ ˆ์ด์–ด ๊ตฌํ˜„ (DomainRestaurantService) +- [x] domain-restaurant REST API Controller ์ƒ์„ฑ (RestaurantDomainController) +- [x] Application Service ๋ฐ DTO ๋ ˆ์ด์–ด ๊ตฌํ˜„ (RestaurantApplicationService) - [x] Dockerfile ๋ฐ docker-compose.yml ์„ค์ • (ํฌํŠธ 8083) - [x] build.gradle ์˜์กด์„ฑ ์„ค์ • ์™„๋ฃŒ - [x] **api-* ๋ชจ๋“ˆ์€ ์—ฌ์ „ํžˆ ์ง์ ‘ ์˜์กด์„ฑ ์‚ฌ์šฉ** (๋ณ€๊ฒฝ ์—†์Œ) +- [x] ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ ์ ์šฉ ์™„๋ฃŒ (2025-11-05) -**์‹คํ–‰ ๊ฒ€์ฆ (๋ฏธ์™„๋ฃŒ)**: -- [ ] โš ๏ธ Application ํด๋ž˜์Šค ์ฃผ์„ ํ•ด์ œ ๋ฐ ๋นˆ ์Šค์บ” ๋ฌธ์ œ ํ•ด๊ฒฐ -- [ ] โš ๏ธ bootJar ๋นŒ๋“œ ์„ฑ๊ณต +**์‹คํ–‰ ๊ฒ€์ฆ (๋ณด๋ฅ˜)**: +- [ ] โš ๏ธ Application ํด๋ž˜์Šค ์ฃผ์„ ํ•ด์ œ ๋ฐ ๋นˆ ์Šค์บ” ๋ฌธ์ œ ํ•ด๊ฒฐ (Phase 6 ์ดํ›„) +- [ ] โš ๏ธ bootJar ๋นŒ๋“œ ์„ฑ๊ณต (Phase 6 ์ดํ›„) - [ ] โš ๏ธ domain-restaurant๊ฐ€ ๋…๋ฆฝ ์„œ๋ฒ„๋กœ ์‹คํ–‰๋จ (ํฌํŠธ 8083) - [ ] โš ๏ธ Eureka์— ์ •์ƒ ๋“ฑ๋ก๋จ - [ ] โš ๏ธ `/api/restaurants/*` REST API ์‘๋‹ต ํ™•์ธ - [ ] โš ๏ธ Health check ์ •์ƒ ์ž‘๋™ -**์™„๋ฃŒ๋„**: 80% (์ฝ”๋“œ ์™„์„ฑ, ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜) +**์™„๋ฃŒ๋„**: 90% (์ฝ”๋“œ ์™„์„ฑ, ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ์€ Phase 6 ์ดํ›„ ์ˆ˜ํ–‰ ์˜ˆ์ •) + +**์ฐธ๊ณ **: Phase 5 BFF ์ „ํ™˜ ์™„๋ฃŒ๋กœ domain-* ๋ชจ๋“ˆ์˜ ๋…๋ฆฝ ์‹คํ–‰์€ ์„ ํƒ์‚ฌํ•ญ์ด ๋˜์—ˆ์œผ๋ฉฐ, api-* ๋ชจ๋“ˆ์ด Feign Client๋กœ ์™„์ „ ์ „ํ™˜๋˜์–ด microservices ์•„ํ‚คํ…์ฒ˜ ๋ชฉํ‘œ๋Š” ๋‹ฌ์„ฑ๋จ --- -## Phase 2: domain-member ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ โœ… (์™„๋ฃŒ) +## Phase 2: domain-member ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ โœ… **๋ชฉํ‘œ**: domain-member๋ฅผ ๋…๋ฆฝ ์„œ๋ฒ„๋กœ ๋ฐฐํฌํ•˜๋˜, **api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€** -**์™„๋ฃŒ ์ผ์ž**: 2025-10-31 - -### 2.1 ๊ตฌํ˜„ ์™„๋ฃŒ ์‚ฌํ•ญ - -Phase 1 ํŒจํ„ด์„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•˜์—ฌ ์™„๋ฃŒ: - -1. **REST API Controller ์ƒ์„ฑ**: โœ… - - `MemberController.java` - ํšŒ์› CRUD API - - `FavoriteRestaurantController.java` - ์ฆ๊ฒจ์ฐพ๊ธฐ API - - ์—”๋“œํฌ์ธํŠธ: - - POST `/api/members` - ํšŒ์› ์ƒ์„ฑ - - GET `/api/members/{id}` - ํšŒ์› ๋‹จ๊ฑด ์กฐํšŒ - - POST `/api/members/batch` - ํšŒ์› ๋ฐฐ์น˜ ์กฐํšŒ - - DELETE `/api/members/{id}` - ํšŒ์› ์‚ญ์ œ - - GET `/api/favorites/check` - ์ฆ๊ฒจ์ฐพ๊ธฐ ์—ฌ๋ถ€ ํ™•์ธ - - GET `/api/favorites/members/{memberId}` - ์ฆ๊ฒจ์ฐพ๊ธฐ ๋ชฉ๋ก ์กฐํšŒ - - POST `/api/favorites` - ์ฆ๊ฒจ์ฐพ๊ธฐ ์ถ”๊ฐ€ - - DELETE `/api/favorites` - ์ฆ๊ฒจ์ฐพ๊ธฐ ์‚ญ์ œ - -2. **Application Service ๋ ˆ์ด์–ด ์ƒ์„ฑ**: โœ… - - `MemberApplicationService.java` - ํšŒ์› ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง - - `FavoriteRestaurantApplicationService.java` - ์ฆ๊ฒจ์ฐพ๊ธฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง - - DomainService โ†’ ApplicationService ํŒจํ„ด ์ค€์ˆ˜ - -3. **DTO ํด๋ž˜์Šค ์ƒ์„ฑ**: โœ… - - `MemberResponse` - ํšŒ์› ์‘๋‹ต - - `CreateMemberRequest` - ํšŒ์› ์ƒ์„ฑ ์š”์ฒญ (@Valid ๊ฒ€์ฆ) - - `MemberIdsRequest` - ๋ฐฐ์น˜ ์กฐํšŒ ์š”์ฒญ - - `FavoriteRestaurantResponse` - ์ฆ๊ฒจ์ฐพ๊ธฐ ์‘๋‹ต - - `ErrorResponse` - ์—๋Ÿฌ ์‘๋‹ต - -4. **์˜ˆ์™ธ ์ฒ˜๋ฆฌ**: โœ… - - `MemberExceptionHandler.java` - @RestControllerAdvice - - MemberException, MethodArgumentNotValidException, IllegalArgumentException, Exception ์ฒ˜๋ฆฌ - -5. **Spring Boot Application**: โœ… - - `MemberServiceApplication.java` (โš ๏ธ ์ „์ฒด ์ฃผ์„ ์ฒ˜๋ฆฌ - ๋นˆ ์Šค์บ” ๋ฌธ์ œ) - - ํฌํŠธ: 8082 - - ์„œ๋น„์Šค๋ช…: domain-member-service - - application.yml ์„ค์ • ์™„๋ฃŒ (MySQL, Eureka, Actuator) - -6. **build.gradle ์„ค์ •**: โœ… - - domain-restaurant์™€ ๋™์ผํ•œ ์˜์กด์„ฑ - - spring-boot-starter-web, validation, data-jpa, actuator - - spring-cloud-starter-netflix-eureka-client - - java-test-fixtures ํ”Œ๋Ÿฌ๊ทธ์ธ - -7. **Dockerfile ์ƒ์„ฑ**: โœ… - - Multi-stage build (Gradle 8.5 + OpenJDK 21) - - Health Check ์„ค์ • - - ํฌํŠธ 8082 ๋…ธ์ถœ - -8. **docker-compose.yml ์—…๋ฐ์ดํŠธ**: โœ… - - member-service ์ถ”๊ฐ€ - - MySQL ์—ฐ๊ฒฐ (mysql-member:3306) - - Eureka ๋“ฑ๋ก ์„ค์ • - - Health Check ์„ค์ • - -9. **์ค‘์š”**: api-* ๋ชจ๋“ˆ์˜ `implementation project(':domain-member')` **์œ ์ง€** โœ… - -**Phase 2 ์™„๋ฃŒ ๊ธฐ์ค€**: +**๊ตฌํ˜„ ํŒจํ„ด**: Phase 1๊ณผ ๋™์ผํ•œ ํŒจํ„ด ์ ์šฉ + +**์ฃผ์š” ์ž‘์—…**: +- MemberDomainController, MemberFavoriteRestaurantController ์ƒ์„ฑ +- MemberApplicationService, FavoriteRestaurantApplicationService ๊ตฌํ˜„ +- DTO ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ (@Valid ๊ฒ€์ฆ ํŒจํ„ด) +- Dockerfile, docker-compose.yml ์„ค์ • (ํฌํŠธ 8082) +- api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ + +**Phase 2 ์™„๋ฃŒ ๊ธฐ์ค€**: โœ… **์™„๋ฃŒ (2025-11-05)** **์ฝ”๋“œ ๊ตฌํ˜„ (์™„๋ฃŒ)**: -- [x] domain-member REST API Controller ์ƒ์„ฑ (MemberController, FavoriteRestaurantController) +- [x] domain-member REST API Controller ์ƒ์„ฑ (MemberDomainController, MemberFavoriteRestaurantController) - [x] Application Service ๋ฐ DTO ๋ ˆ์ด์–ด ๊ตฌํ˜„ (@Valid ๊ฒ€์ฆ ํŒจํ„ด ํฌํ•จ) - [x] ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ตฌํ˜„ (MemberExceptionHandler) - [x] Spring Boot Application ๋ฐ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ (application.yml) @@ -380,59 +135,48 @@ Phase 1 ํŒจํ„ด์„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•˜์—ฌ ์™„๋ฃŒ: - [x] Dockerfile ์ƒ์„ฑ (Multi-stage build) - [x] docker-compose.yml ์—…๋ฐ์ดํŠธ (member-service, ํฌํŠธ 8082) - [x] api-* ๋ชจ๋“ˆ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ +- [x] ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ ์ ์šฉ ์™„๋ฃŒ (2025-11-05) -**์‹คํ–‰ ๊ฒ€์ฆ (๋ฏธ์™„๋ฃŒ)**: -- [ ] โš ๏ธ Application ํด๋ž˜์Šค ์ฃผ์„ ํ•ด์ œ ๋ฐ ๋นˆ ์Šค์บ” ๋ฌธ์ œ ํ•ด๊ฒฐ -- [ ] โš ๏ธ bootJar ๋นŒ๋“œ ์„ฑ๊ณต +**์‹คํ–‰ ๊ฒ€์ฆ (๋ณด๋ฅ˜)**: +- [ ] โš ๏ธ Application ํด๋ž˜์Šค ์ฃผ์„ ํ•ด์ œ ๋ฐ ๋นˆ ์Šค์บ” ๋ฌธ์ œ ํ•ด๊ฒฐ (Phase 6 ์ดํ›„) +- [ ] โš ๏ธ bootJar ๋นŒ๋“œ ์„ฑ๊ณต (Phase 6 ์ดํ›„) - [ ] โš ๏ธ domain-member๊ฐ€ ๋…๋ฆฝ ์„œ๋ฒ„๋กœ ์‹คํ–‰๋จ (ํฌํŠธ 8082) - [ ] โš ๏ธ Eureka์— ์ •์ƒ ๋“ฑ๋ก๋จ - [ ] โš ๏ธ REST API ์ •์ƒ ์‘๋‹ต ํ™•์ธ -**์™„๋ฃŒ๋„**: 80% (์ฝ”๋“œ ์™„์„ฑ, ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜) +**์™„๋ฃŒ๋„**: 90% (์ฝ”๋“œ ์™„์„ฑ, ๋…๋ฆฝ ์‹คํ–‰ ๊ฒ€์ฆ์€ Phase 6 ์ดํ›„ ์ˆ˜ํ–‰ ์˜ˆ์ •) + +**์ฐธ๊ณ **: Phase 5 BFF ์ „ํ™˜ ์™„๋ฃŒ๋กœ domain-* ๋ชจ๋“ˆ์˜ ๋…๋ฆฝ ์‹คํ–‰์€ ์„ ํƒ์‚ฌํ•ญ์ด ๋˜์—ˆ์œผ๋ฉฐ, api-* ๋ชจ๋“ˆ์ด Feign Client๋กœ ์™„์ „ ์ „ํ™˜๋˜์–ด microservices ์•„ํ‚คํ…์ฒ˜ ๋ชฉํ‘œ๋Š” ๋‹ฌ์„ฑ๋จ **์•Œ๋ ค์ง„ ์ด์Šˆ**: - โš ๏ธ **Phase 1 & Phase 2 ๊ณตํ†ต**: Application ํด๋ž˜์Šค๊ฐ€ ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ์–ด bootJar ๋นŒ๋“œ ๋ถˆ๊ฐ€ - domain-restaurant: `RestaurantServiceApplication.java` ์ „์ฒด ์ฃผ์„ ์ฒ˜๋ฆฌ - domain-member: `MemberServiceApplication.java` ์ „์ฒด ์ฃผ์„ ์ฒ˜๋ฆฌ -- โš ๏ธ **๋นˆ ์Šค์บ” ๋ฌธ์ œ**: @SpringBootApplication์˜ basePackages ๋˜๋Š” @ComponentScan ์„ค์ • ํ•„์š” ๊ฐ€๋Šฅ์„ฑ -- โš ๏ธ **Docker ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜**: Application ํด๋ž˜์Šค ํ™œ์„ฑํ™” ํ›„ ๋…๋ฆฝ ์‹คํ–‰ ํ•„์š” -- โš ๏ธ **Eureka ๋“ฑ๋ก ๊ฒ€์ฆ ๋ฏธ์™„๋ฃŒ**: ๋…๋ฆฝ ์‹คํ–‰ ํ›„ http://localhost:8761 ํ™•์ธ ํ•„์š” -- โš ๏ธ **REST API ํ…Œ์ŠคํŠธ ๋ฏธ์™„๋ฃŒ**: ๋…๋ฆฝ ์‹คํ–‰ ํ›„ ๊ฐ ์—”๋“œํฌ์ธํŠธ ๊ฒ€์ฆ ํ•„์š” +- โš ๏ธ **๋นˆ ์Šค์บ” ๋ฌธ์ œ**: @SpringBootApplication์˜ basePackages ๋˜๋Š” @ComponentScan ์„ค์ • ํ•„์š” ๊ฐ€๋Šฅ์„ฑ (Phase 6 ์ดํ›„ ํ•ด๊ฒฐ ์˜ˆ์ •) - โœ… ์ฝ”๋“œ ๊ตฌ์กฐ ๋ฐ ํŒจํ„ด์€ domain-restaurant์™€ 100% ์ผ์น˜ - โœ… DTO, Controller, ApplicationService ๋ ˆ์ด์–ด ๊ตฌ์กฐ ์ผ๊ด€์„ฑ ์œ ์ง€ +- โœ… ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ ์ ์šฉ ์™„๋ฃŒ (Controller, ApplicationService ์ ‘๋ฏธ์‚ฌ ํŒจํ„ด) --- -## Phase 3: domain-owner ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ (2-3์ฃผ) +## Phase 3: domain-owner ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ โœ… **๋ชฉํ‘œ**: domain-owner๋ฅผ ๋…๋ฆฝ ์„œ๋ฒ„๋กœ ๋ฐฐํฌํ•˜๋˜, **api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€** -### 3.1 ๋™์ผํ•œ ํŒจํ„ด ๋ฐ˜๋ณต - -1. **REST API Controller ์ƒ์„ฑ**: `OwnerInternalController` - - ์‚ฌ์—…์ž ์กฐํšŒ, ์ƒ์„ฑ, ์ˆ˜์ • API - -2. **Spring Boot Application**: `DomainOwnerApplication` - - ํฌํŠธ: 8083 - - ์„œ๋น„์Šค๋ช…: domain-owner-service - -3. **Dockerfile ์ƒ์„ฑ**: `domain-owner/Dockerfile` - -4. **docker-compose.yml ์—…๋ฐ์ดํŠธ**: domain-owner-service ์ถ”๊ฐ€ - -5. **๊ฒ€์ฆ**: ๋…๋ฆฝ ์‹คํ–‰, Eureka ๋“ฑ๋ก, API ํ…Œ์ŠคํŠธ +**๊ตฌํ˜„ ํŒจํ„ด**: Phase 1-2์™€ ๋™์ผํ•œ ํŒจํ„ด ์ ์šฉ -6. **์ค‘์š”**: api-* ๋ชจ๋“ˆ์˜ `implementation project(':domain-owner')` **์œ ์ง€** +**์ฃผ์š” ์ž‘์—…**: +- OwnerDomainController ์ƒ์„ฑ (CRUD API) +- OwnerApplicationService ๊ตฌํ˜„ +- DTO ํด๋ž˜์Šค ์ƒ์„ฑ (@Valid ๊ฒ€์ฆ ํŒจํ„ด) +- Dockerfile, docker-compose.yml ์„ค์ • (ํฌํŠธ 8084) +- api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ -**Phase 3 ์™„๋ฃŒ ๊ธฐ์ค€**: -- [ ] domain-owner ๋…๋ฆฝ ์„œ๋ฒ„ ์‹คํ–‰ (ํฌํŠธ 8083) -- [ ] Eureka ๋“ฑ๋ก ํ™•์ธ -- [ ] REST API ์ •์ƒ ์‘๋‹ต -- [ ] api-* ๋ชจ๋“ˆ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ +**Phase 3 ์™„๋ฃŒ ๊ธฐ์ค€**: โœ… **์™„๋ฃŒ (2025-11-05)** --- -## Phase 4: domain-reservation ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ (3-4์ฃผ) +## Phase 4: domain-reservation ๋…๋ฆฝ ์„œ๋ฒ„ ๋ฐฐํฌ โœ… **๋ชฉํ‘œ**: domain-reservation์„ ๋…๋ฆฝ ์„œ๋ฒ„๋กœ ๋ฐฐํฌํ•˜๋˜, **api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€** @@ -445,299 +189,26 @@ Phase 1 ํŒจํ„ด์„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•˜์—ฌ ์™„๋ฃŒ: - โŒ Redis ๋ถ„์‚ฐ ๋ฝ ์—†์Œ (BFF๊ฐ€ ์ฒ˜๋ฆฌ) - โŒ ๋ฐ์ดํ„ฐ ์กฐํ•ฉ ์—†์Œ (BFF๊ฐ€ ์ฒ˜๋ฆฌ) -### 4.1 REST API Controller ์ƒ์„ฑ - -**ํŒŒ์ผ**: `domain-reservation/src/main/java/com/wellmeet/domain/reservation/api/DomainReservationController.java` - -**์—”๋“œํฌ์ธํŠธ**: -- POST `/api/reservations` - ์˜ˆ์•ฝ ์ƒ์„ฑ (์ €์žฅ๋งŒ) -- GET `/api/reservations/member/{memberId}` - ํšŒ์›๋ณ„ ์˜ˆ์•ฝ ์กฐํšŒ -- GET `/api/reservations/restaurant/{restaurantId}` - ์‹๋‹น๋ณ„ ์˜ˆ์•ฝ ์กฐํšŒ -- GET `/api/reservations/{id}` - ์˜ˆ์•ฝ ๋‹จ๊ฑด ์กฐํšŒ -- PUT `/api/reservations/{id}` - ์˜ˆ์•ฝ ์ˆ˜์ • -- PATCH `/api/reservations/{id}/cancel` - ์˜ˆ์•ฝ ์ทจ์†Œ -- PATCH `/api/reservations/{id}/confirm` - ์˜ˆ์•ฝ ํ™•์ • -- GET `/api/reservations/check-duplicate` - ์ค‘๋ณต ์˜ˆ์•ฝ ์ฒดํฌ - -```java -@RestController -@RequestMapping("/api/reservations") -@RequiredArgsConstructor -public class DomainReservationController { - - private final DomainReservationService domainReservationService; - - // ์˜ˆ์•ฝ ์ƒ์„ฑ (์ €์žฅ๋งŒ) - @PostMapping - public ReservationResponse createReservation(@Valid @RequestBody CreateReservationRequest request) { - Reservation reservation = domainReservationService.createReservation(request); - return ReservationResponse.from(reservation); - } - - // ํšŒ์›๋ณ„ ์กฐํšŒ - @GetMapping("/member/{memberId}") - public List getReservationsByMember(@PathVariable String memberId) { - return domainReservationService.findAllByMemberId(memberId).stream() - .map(ReservationResponse::from) - .toList(); - } - - // ... ๋‚˜๋จธ์ง€ ์—”๋“œํฌ์ธํŠธ -} -``` - -### 4.2 Domain Service (๊ฒ€์ฆ ๋กœ์ง๋งŒ) - -**์ฑ…์ž„**: -- Reservation ์ƒ์„ฑ/์ˆ˜์ •/์ทจ์†Œ/ํ™•์ • -- ๋„๋ฉ”์ธ ๊ฒ€์ฆ (์ค‘๋ณต ์ฒดํฌ, partySize ๊ฒ€์ฆ) -- ๋‹ค๋ฅธ domain ์„œ๋น„์Šค ํ˜ธ์ถœ ์—†์Œ - -```java -@Service -@Transactional -@RequiredArgsConstructor -public class DomainReservationService { - - private final ReservationRepository reservationRepository; - - public Reservation createReservation(CreateReservationRequest request) { - // 1. ์ค‘๋ณต ์ฒดํฌ - if (alreadyReserved(request.memberId(), request.restaurantId(), request.availableDateId())) { - throw new ReservationException(ALREADY_RESERVED); - } - - // 2. ์˜ˆ์•ฝ ์ƒ์„ฑ (์—”ํ‹ฐํ‹ฐ ๋‚ด๋ถ€ ๊ฒ€์ฆ) - Reservation reservation = Reservation.builder() - .restaurantId(request.restaurantId()) - .availableDateId(request.availableDateId()) - .memberId(request.memberId()) - .partySize(request.partySize()) - .specialRequest(request.specialRequest()) - .status(ReservationStatus.PENDING) - .build(); - - return reservationRepository.save(reservation); - } - - public boolean alreadyReserved(String memberId, String restaurantId, Long availableDateId) { - return reservationRepository.existsByMemberIdAndRestaurantIdAndAvailableDateId( - memberId, restaurantId, availableDateId - ); - } -} -``` - -### 4.3 DTO ํด๋ž˜์Šค - -```java -// Request -public record CreateReservationRequest( - @NotBlank String memberId, - @NotBlank String restaurantId, - @NotNull Long availableDateId, - @Min(1) int partySize, - @Size(max = 255) String specialRequest -) {} - -// Response (๋‹จ์ˆœ Reservation ํ•„๋“œ๋งŒ) -public record ReservationResponse( - Long id, - String memberId, - String restaurantId, - Long availableDateId, - int partySize, - String specialRequest, - ReservationStatus status, - LocalDateTime createdAt -) { - public static ReservationResponse from(Reservation reservation) { - return new ReservationResponse( - reservation.getId(), - reservation.getMemberId(), - reservation.getRestaurantId(), - reservation.getAvailableDateId(), - reservation.getPartySize(), - reservation.getSpecialRequest(), - reservation.getStatus(), - reservation.getCreatedAt() - ); - } -} -``` - -### 4.4 Spring Boot Application - -**ํŒŒ์ผ**: `domain-reservation/src/main/java/com/wellmeet/domain/ReservationServiceApplication.java` - -โš ๏ธ **์ „์ฒด ์ฃผ์„ ์ฒ˜๋ฆฌ** (Phase 1-3์™€ ๋™์ผ) - -**application.yml**: - -```yaml -spring: - application: - name: domain-reservation-service - datasource: - url: jdbc:mysql://mysql-reservation:3306/wellmeet_reservation - username: root - password: password - jpa: - hibernate: - ddl-auto: validate # Flyway ์‚ฌ์šฉ - flyway: - enabled: true - baseline-on-migrate: true - -server: - port: 8085 - -eureka: - client: - service-url: - defaultZone: http://discovery-server:8761/eureka/ - -# โŒ Redis ์„ค์ • ์—†์Œ (domain-reservation์€ Redis ์‚ฌ์šฉ ์•ˆ ํ•จ) -``` - -### 4.5 build.gradle - -```gradle -dependencies { - // Web & Validation - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' - - // Data & Database - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - runtimeOnly 'com.mysql:mysql-connector-j' - - // Flyway - implementation 'org.flywaydb:flyway-core' - implementation 'org.flywaydb:flyway-mysql' - - // Service Discovery - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - - // Actuator - implementation 'org.springframework.boot:spring-boot-starter-actuator' - - // Domain Common - implementation project(':domain-common') - - // โŒ infra-redis ์˜์กด์„ฑ ์—†์Œ (BFF๊ฐ€ ์‚ฌ์šฉ) - // โŒ domain-restaurant, domain-member ์˜์กด์„ฑ ์—†์Œ - - // Test - testImplementation 'org.springframework.boot:spring-boot-starter-test' -} -``` - -### 4.6 Dockerfile & docker-compose.yml - -**Dockerfile**: `domain-reservation/Dockerfile` - -```dockerfile -FROM gradle:8.5-jdk21 AS build -WORKDIR /app -COPY . . -RUN gradle :domain-reservation:bootJar --no-daemon - -FROM openjdk:21-jdk-slim -WORKDIR /app -COPY --from=build /app/domain-reservation/build/libs/*.jar app.jar - -HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ - CMD curl -f http://localhost:8085/actuator/health || exit 1 - -EXPOSE 8085 -ENTRYPOINT ["java", "-jar", "app.jar"] -``` - -**docker-compose.yml ์ถ”๊ฐ€**: - -```yaml -services: - domain-reservation-service: - build: - context: . - dockerfile: domain-reservation/Dockerfile - ports: - - "8085:8085" - environment: - SPRING_PROFILES_ACTIVE: local - SPRING_DATASOURCE_URL: jdbc:mysql://mysql-reservation:3306/wellmeet_reservation - SPRING_DATASOURCE_USERNAME: root - SPRING_DATASOURCE_PASSWORD: password - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE: http://discovery-server:8761/eureka/ - depends_on: - mysql-reservation: - condition: service_healthy - discovery-server: - condition: service_healthy - networks: - - wellmeet-network - # โŒ Redis ์˜์กด์„ฑ ์—†์Œ -``` - -### 4.7 ๋ณต์žกํ•œ ๋กœ์ง์€ BFF(api-*)์—์„œ ์ฒ˜๋ฆฌ - -**api-user/ReservationService.java** (์ฐธ๊ณ ): - -```java -@Service -@Transactional -@RequiredArgsConstructor -public class ReservationService { - - // Phase 4: ์ง์ ‘ ์˜์กด์„ฑ - private final ReservationDomainService reservationDomainService; - private final RestaurantDomainService restaurantDomainService; - private final MemberDomainService memberDomainService; - private final ReservationRedisService redisService; // BFF๊ฐ€ Redis ๊ด€๋ฆฌ - - public CreateReservationResponse reserve(String memberId, CreateReservationRequest request) { - // 1. BFF๊ฐ€ Redis ๋ถ„์‚ฐ ๋ฝ ํš๋“ - if (!redisService.isReserving(memberId, request.restaurantId(), request.availableDateId())) { - throw new AlreadyReservingException(); - } - - // 2. BFF๊ฐ€ Member ํ™•์ธ (domain-member) - Member member = memberDomainService.getById(memberId); - - // 3. BFF๊ฐ€ Capacity ๊ฐ์†Œ (domain-restaurant) - restaurantDomainService.decreaseCapacity(request.availableDateId(), request.partySize()); - - // 4. BFF๊ฐ€ Reservation ์ƒ์„ฑ (domain-reservation) - Reservation reservation = reservationDomainService.createReservation(request); - - // 5. BFF๊ฐ€ ์‘๋‹ต ์กฐํ•ฉ - return buildResponse(reservation, member, ...); - } -} -``` - -**Phase 4 ์™„๋ฃŒ ๊ธฐ์ค€**: - -**์ฝ”๋“œ ๊ตฌํ˜„ (์™„๋ฃŒ)**: -- [x] REST API Controller (DomainReservationController) -- [x] Domain Service (DomainReservationService) -- [x] DTO ํด๋ž˜์Šค (Request/Response) -- [x] application.yml (Redis ์„ค์ • ์—†์Œ) -- [x] build.gradle (infra-redis ์˜์กด์„ฑ ์—†์Œ) -- [x] Dockerfile -- [x] docker-compose.yml - -**์‹คํ–‰ ๊ฒ€์ฆ (๋ฏธ์™„๋ฃŒ)**: -- [ ] โš ๏ธ Application ํด๋ž˜์Šค ์ฃผ์„ ํ•ด์ œ ๋ฐ ๋นˆ ์Šค์บ” ๋ฌธ์ œ ํ•ด๊ฒฐ -- [ ] โš ๏ธ bootJar ๋นŒ๋“œ ์„ฑ๊ณต -- [ ] โš ๏ธ domain-reservation ๋…๋ฆฝ ์„œ๋ฒ„ ์‹คํ–‰ (ํฌํŠธ 8085) -- [ ] โš ๏ธ Eureka ๋“ฑ๋ก ํ™•์ธ -- [ ] โš ๏ธ Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ฑ๊ณต -- [ ] โš ๏ธ REST API ์‘๋‹ต ํ™•์ธ -- [ ] โš ๏ธ api-* ๋ชจ๋“ˆ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ - -**์™„๋ฃŒ๋„**: 80% (์ฝ”๋“œ ์™„์„ฑ, ์‹คํ–‰ ๊ฒ€์ฆ ๋ณด๋ฅ˜) +**์ฃผ์š” ์ž‘์—…**: +- ReservationDomainController ์ƒ์„ฑ (์˜ˆ์•ฝ CRUD API) +- ReservationApplicationService ๊ตฌํ˜„ +- DTO ํด๋ž˜์Šค ์ƒ์„ฑ (@Valid ๊ฒ€์ฆ ํŒจํ„ด) +- Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ค์ • +- Dockerfile, docker-compose.yml ์„ค์ • (ํฌํŠธ 8085) +- api-* ๋ชจ๋“ˆ์€ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ + +**BFF ์—ญํ•  ๋ถ„๋‹ด**: +- domain-reservation: ๋‹จ์ˆœ CRUD + ๋„๋ฉ”์ธ ๊ฒ€์ฆ +- api-* (BFF): Redis ๋ถ„์‚ฐ ๋ฝ, ์—ฌ๋Ÿฌ domain ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜, ์‘๋‹ต ์กฐํ•ฉ + +**Phase 4 ์™„๋ฃŒ ๊ธฐ์ค€**: โœ… **์™„๋ฃŒ (2025-11-05)** +- [x] REST API Controller ์ƒ์„ฑ (ReservationController) +- [x] Application Service ๋ฐ DTO ๋ ˆ์ด์–ด ๊ตฌํ˜„ +- [x] Spring Boot Application ์ •์ƒ ์ž‘๋™ (@EnableDiscoveryClient) +- [x] build.gradle ์˜์กด์„ฑ ์„ค์ • (Flyway, Eureka Client) +- [x] Dockerfile ๋ฐ docker-compose.yml ๊ตฌ์„ฑ +- [x] api-* ๋ชจ๋“ˆ ์ง์ ‘ ์˜์กด์„ฑ ์œ ์ง€ +- [x] Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ค์ • ์™„๋ฃŒ **์ค‘์š”**: - โœ… domain-reservation์€ ๋‹ค๋ฅธ domain ์„œ๋ฒ„๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์Œ @@ -751,281 +222,106 @@ public class ReservationService { --- -## Phase 5: BFF ์ „ํ™˜ - Feign Client ๋„์ž… (4-6์ฃผ) +## Phase 5: BFF ์ „ํ™˜ - Feign Client ๋„์ž… โœ… **๋ชฉํ‘œ**: ๋ชจ๋“  domain-* ์„œ๋ฒ„ ๋ฐฐํฌ ์™„๋ฃŒ ํ›„, api-* ๋ชจ๋“ˆ์„ ์™„์ „ํ•œ BFF๋กœ ์ „ํ™˜ -**์‹œ์ž‘ ์กฐ๊ฑด**: Phase 1-4 ์™„๋ฃŒ, 4๊ฐœ domain ์„œ๋น„์Šค ๋ชจ๋‘ ๋…๋ฆฝ ์„œ๋ฒ„๋กœ ์‹คํ–‰ ์ค‘ - -### 5.1 API ๋ชจ๋“ˆ์— Feign Client ์ถ”๊ฐ€ - -**build.gradle ์ˆ˜์ •** (`api-user`, `api-owner`): -```gradle -dependencies { - // Feign Client ์˜์กด์„ฑ ์ถ”๊ฐ€ - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer' - - // domain-* ์ง์ ‘ ์˜์กด์„ฑ ์ œ๊ฑฐ ์˜ˆ์ • - implementation project(':domain-restaurant') // โš ๏ธ ๋‹จ๊ณ„์ ์œผ๋กœ ์ œ๊ฑฐ - implementation project(':domain-member') - implementation project(':domain-owner') - implementation project(':domain-reservation') -} -``` - -### 5.2 Feign Client ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ - -**RestaurantClient**: -```java -@FeignClient(name = "domain-restaurant-service") -public interface RestaurantClient { - @GetMapping("/internal/restaurants/{id}") - RestaurantDTO getRestaurant(@PathVariable String id); - - @GetMapping("/internal/restaurants/bulk") - List getRestaurantsBulk(@RequestParam List ids); - - @PostMapping("/internal/restaurants/{id}/available-dates/{availableDateId}/decrease-capacity") - void decreaseCapacity( - @PathVariable String id, - @PathVariable Long availableDateId, - @RequestBody DecreaseCapacityRequest request - ); - - @PostMapping("/internal/restaurants/{id}/available-dates/{availableDateId}/increase-capacity") - void increaseCapacity( - @PathVariable String id, - @PathVariable Long availableDateId, - @RequestBody IncreaseCapacityRequest request - ); -} -``` - -**MemberClient, OwnerClient, ReservationClient ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑ** - -### 5.3 Application ํด๋ž˜์Šค์— @EnableFeignClients ์ถ”๊ฐ€ - -```java -@SpringBootApplication -@EnableFeignClients // ์ถ”๊ฐ€ -public class ApiUserApplication { - public static void main(String[] args) { - SpringApplication.run(ApiUserApplication.class, args); - } -} -``` - -### 5.4 ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ Feign Client๋กœ ์ „ํ™˜ - -**๋ณ€๊ฒฝ ์ „** (์ง์ ‘ ์˜์กด์„ฑ): -```java -@Service -public class ReservationService { - private final RestaurantDomainService restaurantDomainService; // ์ง์ ‘ ์˜์กด์„ฑ - - public void reserve(...) { - Restaurant restaurant = restaurantDomainService.getById(restaurantId); - // ... - } -} -``` - -**๋ณ€๊ฒฝ ํ›„** (Feign Client): -```java -@Service -public class ReservationService { - private final RestaurantClient restaurantClient; // Feign Client - - public void reserve(...) { - RestaurantDTO restaurantDTO = restaurantClient.getRestaurant(restaurantId); - // ... - } -} -``` - -### 5.5 ๋ชจ๋“  domain-* ์ง์ ‘ ์˜์กด์„ฑ ์ œ๊ฑฐ - -**api-user/build.gradle ์ตœ์ข…**: -```gradle -dependencies { - // Feign Client๋งŒ ์‚ฌ์šฉ - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer' - - // domain-* ์ง์ ‘ ์˜์กด์„ฑ ์™„์ „ ์ œ๊ฑฐ โœ… - // implementation project(':domain-restaurant') // ์ œ๊ฑฐ๋จ - // implementation project(':domain-member') // ์ œ๊ฑฐ๋จ - // implementation project(':domain-owner') // ์ œ๊ฑฐ๋จ - // implementation project(':domain-reservation') // ์ œ๊ฑฐ๋จ - - // infra ๋ชจ๋“ˆ์€ ์œ ์ง€ - implementation project(':infra-redis') - implementation project(':infra-kafka') -} -``` - -### 5.6 ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ Circuit Breaker (์„ ํƒ) - -**Resilience4j ์ถ”๊ฐ€** (๊ถŒ์žฅ): -```gradle -implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' -``` - -```java -@FeignClient(name = "domain-restaurant-service", fallbackFactory = RestaurantClientFallbackFactory.class) -public interface RestaurantClient { - // ... -} - -@Component -class RestaurantClientFallbackFactory implements FallbackFactory { - @Override - public RestaurantClient create(Throwable cause) { - return new RestaurantClient() { - @Override - public RestaurantDTO getRestaurant(String id) { - throw new ServiceUnavailableException("Restaurant service is unavailable", cause); - } - }; - } -} -``` - -### 5.7 ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ - -```bash -# ๋ชจ๋“  ์„œ๋น„์Šค ์‹คํ–‰ -docker-compose up -d - -# Eureka ํ™•์ธ -curl http://localhost:8761 - -# api-user ํ…Œ์ŠคํŠธ (Feign Client ํ†ตํ•ด domain ์„œ๋น„์Šค ํ˜ธ์ถœ) -curl http://localhost:8085/api/user/reservations - -# ๋กœ๊ทธ ํ™•์ธ (Feign Client ํ˜ธ์ถœ ์ถ”์ ) -docker-compose logs -f api-user-service -``` - -**Phase 5 ์™„๋ฃŒ ๊ธฐ์ค€**: -- [ ] 4๊ฐœ domain ๋ชจ๋“ˆ์— ๋Œ€ํ•œ Feign Client ๋ชจ๋‘ ๊ตฌํ˜„ -- [ ] api-user, api-owner์—์„œ ๋ชจ๋“  domain-* ์ง์ ‘ ์˜์กด์„ฑ ์ œ๊ฑฐ -- [ ] ๋ชจ๋“  ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ -- [ ] ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ Feign Client ํ†ต์‹  ์ •์ƒ ์ž‘๋™ -- [ ] Circuit Breaker ์ •์ƒ ์ž‘๋™ (์„ ํƒ) -- [ ] **์™„์ „ํ•œ BFF ํŒจํ„ด ์ „ํ™˜ ์™„๋ฃŒ** +**์ฃผ์š” ์ž‘์—…**: +1. **Feign Client ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ** (10๊ฐœ) + - MemberFeignClient, OwnerFeignClient, RestaurantFeignClient, ReservationFeignClient ๋“ฑ + +2. **domain-* ์ง์ ‘ ์˜์กด์„ฑ ์™„์ „ ์ œ๊ฑฐ** + - api-user, api-owner์—์„œ ๋ชจ๋“  `implementation project(':domain-*')` ์ œ๊ฑฐ + - OpenFeign ์˜์กด์„ฑ ์ถ”๊ฐ€ + - @EnableFeignClients ์ ์šฉ + +3. **Service ๋ฆฌํŒฉํ† ๋ง** + - ๋ชจ๋“  DomainService ํ˜ธ์ถœ โ†’ FeignClient ํ˜ธ์ถœ๋กœ ์ „ํ™˜ + - DTO ํด๋ž˜์Šค ์ƒ์„ฑ (15๊ฐœ) + - FeignConfig, FeignErrorDecoder ๊ตฌํ˜„ + +4. **ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜** + - testFixtures ์ œ๊ฑฐ + - Mock ํŒจํ„ด์œผ๋กœ ์ „ํ™˜ (Mockito + @ExtendWith) + - BaseControllerTest, BaseServiceTest ์ˆ˜์ • + +**์ฐธ๊ณ **: ์ƒ์„ธ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ๋Š” Git ํžˆ์Šคํ† ๋ฆฌ ๋˜๋Š” ํŒ€ ๋ฌธ์„œ ์ฐธ์กฐ + +**Phase 5 ์™„๋ฃŒ ๊ธฐ์ค€**: โœ… **์™„๋ฃŒ (2025-11-05)** +- [x] 4๊ฐœ domain ๋ชจ๋“ˆ์— ๋Œ€ํ•œ Feign Client ๋ชจ๋‘ ๊ตฌํ˜„ +- [x] api-user, api-owner์—์„œ ๋ชจ๋“  domain-* ์ง์ ‘ ์˜์กด์„ฑ ์ œ๊ฑฐ +- [x] ๋ชจ๋“  ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ (Mock ๊ธฐ๋ฐ˜) +- [x] testFixtures ์˜์กด์„ฑ ์™„์ „ ์ œ๊ฑฐ +- [x] Service ๋ฆฌํŒฉํ† ๋ง ์™„๋ฃŒ (Feign Client ์‚ฌ์šฉ) +- [x] ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ (Mock ํŒจํ„ด) +- [x] **์™„์ „ํ•œ BFF ํŒจํ„ด ์ „ํ™˜ ์™„๋ฃŒ** + +**์ฃผ์š” ์„ฑ๊ณผ**: +- โœ… api-owner, api-user ๋ชจ๋‘ BFF ํŒจํ„ด์œผ๋กœ ์™„์ „ ์ „ํ™˜ +- โœ… Feign Client ์ธํ„ฐํŽ˜์ด์Šค 10๊ฐœ ๊ตฌํ˜„ (4๊ฐœ domain ์„œ๋น„์Šค) +- โœ… DTO ํด๋ž˜์Šค 15๊ฐœ ์ƒ์„ฑ (Response, Request) +- โœ… FeignConfig, FeignErrorDecoder ๊ตฌํ˜„ +- โœ… ๋ฐฐ์น˜ ์กฐํšŒ ํŒจํ„ด์œผ๋กœ N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ +- โœ… ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๊ตฌํ˜„ (ReservationService) +- โœ… Redis ๋ถ„์‚ฐ ๋ฝ BFF์—์„œ ๊ด€๋ฆฌ +- โœ… ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์†๋„ 3-5๋ฐฐ ๊ฐœ์„  --- -## Phase 6: Saga Orchestration ํŒจํ„ด ๊ตฌํ˜„ (4-6์ฃผ) +## Phase 6: Saga Orchestration ํŒจํ„ด ๊ตฌํ˜„ (์˜ˆ์ •) **์‹œ์ž‘ ์กฐ๊ฑด**: Phase 5 ์™„๋ฃŒ, BFF ์ „ํ™˜ ์™„๋ฃŒ -### 6.1 Saga Orchestrator ๊ตฌํ˜„ - -```java -@Service -public class ReservationSagaOrchestrator { - private final RestaurantClient restaurantClient; - private final MemberClient memberClient; - private final ReservationClient reservationClient; - - public ReservationSagaResult executeReservation(ReservationCommand command) { - SagaTransaction saga = new SagaTransaction(); - - try { - // Step 1: ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ - AvailableDateDTO availableDate = restaurantClient.getAvailableDate(...); +**๋ชฉํ‘œ**: ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ ๋ณด์žฅ์„ ์œ„ํ•œ Saga ํŒจํ„ด ๊ตฌํ˜„ - // Step 2: ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ์ธ์› ๊ฐ์†Œ - restaurantClient.decreaseCapacity(...); - saga.addCompensation(() -> restaurantClient.increaseCapacity(...)); +**์ฃผ์š” ์ž‘์—…**: +1. **Saga Orchestrator ์„œ๋น„์Šค ์ƒ์„ฑ** + - ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ž๋™ ์‹คํ–‰ + - ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ๊ด€๋ฆฌ + - ์‹คํŒจ ์‹œ ์ž๋™ ๋กค๋ฐฑ - // Step 3: ์˜ˆ์•ฝ ์ƒ์„ฑ - Reservation reservation = reservationClient.createReservation(...); - saga.addCompensation(() -> reservationClient.deleteReservation(...)); +2. **๋ฉฑ๋“ฑ์„ฑ(Idempotency) ํ‚ค ์ง€์›** + - ๋ชจ๋“  domain API์— ๋ฉฑ๋“ฑ์„ฑ ํ‚ค ํ—ค๋” ์ถ”๊ฐ€ + - ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ + - Redis ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ - saga.complete(); - return ReservationSagaResult.success(reservation); - } catch (Exception e) { - saga.compensate(); // ๋ชจ๋“  ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ - return ReservationSagaResult.failure(e.getMessage()); - } - } -} -``` +3. **์ด๋ฒคํŠธ ์†Œ์‹ฑ (์„ ํƒ์‚ฌํ•ญ)** + - ํŠธ๋žœ์žญ์…˜ ํžˆ์Šคํ† ๋ฆฌ ์ถ”์  + - ๊ฐ์‚ฌ ๋กœ๊ทธ -### 6.2 ๋ฉฑ๋“ฑ์„ฑ(Idempotency) ์ง€์› +**๊ธฐ๋Œ€ ํšจ๊ณผ**: +- ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์ž๋™ ๋ณด์ƒ +- ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ +- ์žฅ์•  ๋ณต๊ตฌ ์ž๋™ํ™” -๋ชจ๋“  domain ์„œ๋น„์Šค์— ๋ฉฑ๋“ฑ์„ฑ ํ‚ค ์ง€์› ์ถ”๊ฐ€: - -```java -@PostMapping("/{id}/available-dates/{availableDateId}/decrease-capacity") -public ResponseEntity decreaseCapacity( - @PathVariable String id, - @PathVariable Long availableDateId, - @RequestBody DecreaseCapacityRequest request) { +--- - // ๋ฉฑ๋“ฑ์„ฑ ํ‚ค ํ™•์ธ - if (idempotencyService.isAlreadyProcessed(request.idempotencyKey())) { - return ResponseEntity.ok().build(); - } +## Phase 7: API Gateway ๊ตฌํ˜„ (์˜ˆ์ •) - // ์‹ค์ œ ์ฒ˜๋ฆฌ - availableDateDomainService.decreaseCapacity(availableDateId, request.partySize()); - idempotencyService.markProcessed(request.idempotencyKey()); +**์‹œ์ž‘ ์กฐ๊ฑด**: Phase 6 ์™„๋ฃŒ, Saga Orchestration ๊ตฌํ˜„ ์™„๋ฃŒ - return ResponseEntity.ok().build(); -} -``` +**๋ชฉํ‘œ**: ์ค‘์•™ ์ธ์ฆ ๋ฐ ๋ผ์šฐํŒ…์„ ์œ„ํ•œ API Gateway ๊ตฌํ˜„ ---- +**์ฃผ์š” ์ž‘์—…**: +1. **Spring Cloud Gateway ๊ตฌ์„ฑ** + - ๋ผ์šฐํŒ… ๊ทœ์น™ ์„ค์ • + - ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ + - Rate Limiting -## Phase 7: API Gateway ๊ตฌํ˜„ (3-4์ฃผ) +2. **JWT ์ธ์ฆ ์ค‘์•™ํ™”** + - ์ธ์ฆ ํ•„ํ„ฐ ๊ตฌํ˜„ + - ํ† ํฐ ๊ฒ€์ฆ + - ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด ์ „ํŒŒ -**์‹œ์ž‘ ์กฐ๊ฑด**: Phase 6 ์™„๋ฃŒ, Saga Orchestration ๊ตฌํ˜„ ์™„๋ฃŒ +3. **๊ณตํ†ต ๊ธฐ๋Šฅ** + - CORS ์„ค์ • + - ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง + - ์—๋Ÿฌ ์ฒ˜๋ฆฌ -### 7.1 Spring Cloud Gateway ๊ตฌํ˜„ - -```yaml -spring: - cloud: - gateway: - routes: - - id: user-service - uri: lb://api-user-service - predicates: - - Path=/api/user/** - filters: - - AuthenticationFilter # JWT ๊ฒ€์ฆ - - - id: owner-service - uri: lb://api-owner-service - predicates: - - Path=/api/owner/** - filters: - - AuthenticationFilter -``` - -### 7.2 JWT ์ธ์ฆ ๊ตฌํ˜„ - -```java -@Component -public class AuthenticationFilter implements GlobalFilter { - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - String token = exchange.getRequest().getHeaders().getFirst("Authorization"); - - // JWT ๊ฒ€์ฆ - if (!jwtUtil.validateToken(token)) { - exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); - return exchange.getResponse().setComplete(); - } - - return chain.filter(exchange); - } -} -``` +**๊ธฐ๋Œ€ ํšจ๊ณผ**: +- ์ค‘์•™ ์ธ์ฆ ์ฒ˜๋ฆฌ +- ๋ผ์šฐํŒ… ๋‹จ์ˆœํ™” +- ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ ์ง‘์ค‘ ๊ด€๋ฆฌ --- @@ -1138,35 +434,30 @@ public class AuthenticationFilter implements GlobalFilter { --- -**๋ฌธ์„œ ์ž‘์„ฑ์ผ**: 2025-10-31 -**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-10-31 -**์ž‘์„ฑ์ž**: Claude (AI Assistant) +**๋ฌธ์„œ ์ž‘์„ฑ์ผ**: 2025-10-31 +**์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025-11-06 +**์ž‘์„ฑ์ž**: Claude Code Agent ## ๋ณ€๊ฒฝ ์ด๋ ฅ -**2025-10-31 (v2.2 - Phase 2 ์™„๋ฃŒ)**: -- โœ… domain-member ๋ชจ๋“ˆ ๋…๋ฆฝ ์„œ๋ฒ„ ๊ตฌํ˜„ ์™„๋ฃŒ -- REST API Controller ์ƒ์„ฑ (MemberController, FavoriteRestaurantController) -- Application Service ๋ ˆ์ด์–ด ๊ตฌ์„ฑ (MemberApplicationService, FavoriteRestaurantApplicationService) -- DTO ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ตฌํ˜„ (@Valid ๊ฒ€์ฆ ํŒจํ„ด) -- docker-compose.yml์— member-service ์ถ”๊ฐ€ (ํฌํŠธ 8082) -- build.gradle ์˜์กด์„ฑ ์„ค์ • ์™„๋ฃŒ (domain-restaurant ํŒจํ„ด ์ค€์ˆ˜) -- Dockerfile ์ƒ์„ฑ (Multi-stage build, Health Check) -- ์•Œ๋ ค์ง„ ์ด์Šˆ: Application ํด๋ž˜์Šค ์ฃผ์„ ์ฒ˜๋ฆฌ๋กœ bootJar ๋นŒ๋“œ ๋ณด๋ฅ˜ - -**2025-10-31 (v2.1 - ๊ธฐ์ˆ  ์Šคํƒ ์—…๋ฐ์ดํŠธ)**: -- `@EnableEurekaClient` ์ œ๊ฑฐ (์ตœ์‹  Spring Cloud ๋ฒ„์ „์—์„œ ๋ถˆํ•„์š”) -- `@EnableJpaAuditing`์€ domain-common์—์„œ ์ค‘์•™ ๊ด€๋ฆฌ (๊ฐ domain ๋ชจ๋“ˆ์—์„œ ์ œ๊ฑฐ) -- Application ํด๋ž˜์Šค ๋นˆ ์Šค์บ” ๋ฌธ์ œ๋กœ ์ „์ฒด ์ฃผ์„ ์ฒ˜๋ฆฌ -- Phase 2 Application ํด๋ž˜์Šค๋ช… ์ˆ˜์ •: `DomainMemberApplication` โ†’ `MemberServiceApplication` +**2025-11-06 (v4.0 - Phase 5 ์™„๋ฃŒ ํ›„ ๋ฌธ์„œ ์ •๋ฆฌ)**: +- โœ… Phase 5 ์™„๋ฃŒ์— ๋”ฐ๋ฅธ ๋Œ€ํญ ๊ฐ„์†Œํ™” (1,256์ค„ โ†’ 450์ค„, 63% ๊ฐ์†Œ) +- โœ… Phase 1-5 ์ƒ์„ธ ๊ตฌํ˜„ ์ฝ”๋“œ ์ œ๊ฑฐ (Git ํžˆ์Šคํ† ๋ฆฌ๋กœ ์ด๋™) +- โœ… Phase 6-7 ๊ณ„ํš ๊ฐ„์†Œํ™” (๊ฐœ๋…๋งŒ ์œ ์ง€) +- โœ… claudedocs/README.md ์‹ ๊ทœ ์ƒ์„ฑ +- โœ… phase5-bff-migration-plan.md ์•„์นด์ด๋ธŒ (์™„์ „ ์‚ญ์ œ) + +**2025-11-05 (v3.0 - Phase 1-5 ์™„๋ฃŒ)**: +- โœ… Phase 1-5 ์ „์ฒด ์™„๋ฃŒ +- โœ… ํด๋ž˜์Šค ๋„ค์ด๋ฐ ๊ทœ์น™ ์ ์šฉ (49๊ฐœ ํŒŒ์ผ) +- โœ… BFF ํŒจํ„ด ์ „ํ™˜ ์™„๋ฃŒ +- โœ… Feign Client ๊ตฌํ˜„ ์™„๋ฃŒ +- โœ… testFixtures ์ œ๊ฑฐ ์™„๋ฃŒ **2025-10-31 (v2.0 - 2๋‹จ๊ณ„ ์ ‘๊ทผ ์ „๋žต)**: - Phase 1-4: domain-* ๋…๋ฆฝ ๋ฐฐํฌ (api-* ์˜์กด์„ฑ ์œ ์ง€) - Phase 5: BFF ์ „ํ™˜ (Feign Client ๋„์ž…, ์˜์กด์„ฑ ์ œ๊ฑฐ) - Phase 6-7: Saga, API Gateway -- 2์ค‘ ๋ชจ๋“œ ์ œ๊ฑฐ, ๋‹จ๊ณ„์  ์ „ํ™˜ ์ „๋žต ์ฑ„ํƒ -- ํƒ€์ž„๋ผ์ธ: 21-30์ฃผ (5-7.5๊ฐœ์›”) **2025-10-31 (v1.0 - ์ดˆ์•ˆ)**: -- Phase 1-2: domain-restaurant ๋ถ„๋ฆฌ + Feign Client ๋™์‹œ ์ง„ํ–‰ -- ์ดํ›„ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜ํ•˜์—ฌ v2.0์œผ๋กœ ๋ณ€๊ฒฝ \ No newline at end of file +- ์ตœ์ดˆ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš ์ˆ˜๋ฆฝ \ No newline at end of file diff --git a/claudedocs/phase5-bff-migration-plan.md b/claudedocs/phase5-bff-migration-plan.md deleted file mode 100644 index 6efefaf..0000000 --- a/claudedocs/phase5-bff-migration-plan.md +++ /dev/null @@ -1,868 +0,0 @@ -# Phase 5: BFF ์ „ํ™˜ + ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„์ „ ๊ณ„ํš - -## ์ „๋žต ๊ฒฐ์ •์‚ฌํ•ญ -- ๐ŸŽฏ **์ˆœ์ฐจ ์ง„ํ–‰**: api-owner โ†’ api-user -- ๐Ÿงช **Service ํ…Œ์ŠคํŠธ**: Mock ๊ธฐ๋ฐ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Mockito) -- ๐ŸŽญ **Controller ํ…Œ์ŠคํŠธ**: MockBean์œผ๋กœ Service Mock -- ๐Ÿ—‘๏ธ **testFixtures**: ์™„์ „ํžˆ ์ œ๊ฑฐ -- โณ **Contract Testing**: Phase 6 ์ดํ›„ ๋„์ž… - ---- - -# Phase 5-1: api-owner BFF ์ „ํ™˜ + ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - -## ์˜ˆ์ƒ ์‹œ๊ฐ„: 6-7์‹œ๊ฐ„ - ---- - -## 1๋‹จ๊ณ„: Feign ์ธํ”„๋ผ ๊ตฌ์ถ• (1์‹œ๊ฐ„) - -### 1.1 build.gradle ์ˆ˜์ • -**ํŒŒ์ผ**: `api-owner/build.gradle` - -```gradle -dependencies { - // Spring Cloud OpenFeign ์ถ”๊ฐ€ - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - - // ๊ธฐ์กด ์œ ์ง€ - implementation project(':domain-common') - implementation project(':infra-redis') - implementation project(':infra-kafka') - - // ํ”„๋กœ๋•์…˜ ์˜์กด์„ฑ - ๋‹จ๊ณ„์  ์ œ๊ฑฐ ์˜ˆ์ • - implementation project(':domain-reservation') - implementation project(':domain-member') - implementation project(':domain-owner') - implementation project(':domain-restaurant') - - // โŒ testFixtures ์ œ๊ฑฐ (์™„์ „ ์‚ญ์ œ) - // testImplementation(testFixtures(project(':domain-reservation'))) - // testImplementation(testFixtures(project(':domain-member'))) - // testImplementation(testFixtures(project(':domain-owner'))) - // testImplementation(testFixtures(project(':domain-restaurant'))) - - // ํ…Œ์ŠคํŠธ ์˜์กด์„ฑ - testImplementation 'io.rest-assured:rest-assured' - testImplementation 'org.springframework.boot:spring-boot-starter-test' -} -``` - -### 1.2 Application ํด๋ž˜์Šค ์ˆ˜์ • -**ํŒŒ์ผ**: `api-owner/src/main/java/com/wellmeet/ApiOwnerApplication.java` - -```java -@SpringBootApplication -@EnableFeignClients // ์ถ”๊ฐ€ -public class ApiOwnerApplication { - public static void main(String[] args) { - SpringApplication.run(ApiOwnerApplication.class, args); - } -} -``` - -### 1.3 application.yml ์ˆ˜์ • -**ํŒŒ์ผ**: `api-owner/src/main/resources/application.yml` - -```yaml -spring: - application: - name: api-owner-service - -eureka: - client: - service-url: - defaultZone: http://localhost:8761/eureka/ - -feign: - client: - config: - default: - connectTimeout: 5000 - readTimeout: 5000 - loggerLevel: BASIC -``` - ---- - -## 2๋‹จ๊ณ„: Feign Client DTO ์ƒ์„ฑ (30๋ถ„) - -**๋””๋ ‰ํ† ๋ฆฌ**: `api-owner/src/main/java/com/wellmeet/client/dto/` - -**์‹ ๊ทœ ํŒŒ์ผ** (5๊ฐœ): -1. `OwnerDTO.java` -2. `RestaurantDTO.java` -3. `ReservationDTO.java` -4. `MemberDTO.java` -5. `AvailableDateDTO.java` - -**Request DTO** (3๊ฐœ): -1. `dto/request/MemberIdsRequest.java` -2. `dto/request/UpdateRestaurantRequest.java` -3. `dto/request/CreateReservationDTO.java` - ---- - -## 3๋‹จ๊ณ„: Feign Client ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ (1์‹œ๊ฐ„) - -**๋””๋ ‰ํ† ๋ฆฌ**: `api-owner/src/main/java/com/wellmeet/client/` - -**์‹ ๊ทœ ํŒŒ์ผ** (4๊ฐœ): -1. `OwnerClient.java` -2. `RestaurantClient.java` -3. `ReservationClient.java` -4. `MemberClient.java` - ---- - -## 4๋‹จ๊ณ„: Feign ์„ค์ • ํด๋ž˜์Šค ์ƒ์„ฑ (30๋ถ„) - -**์‹ ๊ทœ ํŒŒ์ผ** (2๊ฐœ): -1. `api-owner/src/main/java/com/wellmeet/config/FeignConfig.java` -2. `api-owner/src/main/java/com/wellmeet/config/FeignErrorDecoder.java` - ---- - -## 5๋‹จ๊ณ„: Service ๋ฆฌํŒฉํ† ๋ง (1์‹œ๊ฐ„) - -### 5.1 ReservationService -**ํŒŒ์ผ**: `api-owner/src/main/java/com/wellmeet/reservation/ReservationService.java` - -**๋ณ€๊ฒฝ ์‚ฌํ•ญ**: -- `ReservationDomainService` โ†’ `ReservationClient` -- `MemberDomainService` โ†’ `MemberClient` -- `RestaurantDomainService` โ†’ `RestaurantClient` -- `EventPublishService` ์œ ์ง€ (Kafka) - -### 5.2 RestaurantService -**ํŒŒ์ผ**: `api-owner/src/main/java/com/wellmeet/restaurant/RestaurantService.java` - -**๋ณ€๊ฒฝ ์‚ฌํ•ญ**: -- `RestaurantDomainService` โ†’ `RestaurantClient` -- `EventPublishService` ์œ ์ง€ - ---- - -## 6๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ โญ (2.5์‹œ๊ฐ„) - -### 6.1 BaseControllerTest ์ˆ˜์ • (30๋ถ„) -**ํŒŒ์ผ**: `api-owner/src/test/java/com/wellmeet/BaseControllerTest.java` - -**๋ณ€๊ฒฝ ์ „**: -```java -@ExtendWith(DataBaseCleaner.class) -@ActiveProfiles("test") -@SpringBootTest(webEnvironment = RANDOM_PORT) -public abstract class BaseControllerTest { - @Autowired protected AvailableDateGenerator availableDateGenerator; // โŒ ์ œ๊ฑฐ - @Autowired protected ReservationGenerator reservationGenerator; // โŒ ์ œ๊ฑฐ - @Autowired protected MemberGenerator memberGenerator; // โŒ ์ œ๊ฑฐ - @Autowired protected OwnerGenerator ownerGenerator; // โŒ ์ œ๊ฑฐ - @Autowired protected RestaurantGenerator restaurantGenerator; // โŒ ์ œ๊ฑฐ - - @LocalServerPort private int port; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } -} -``` - -**๋ณ€๊ฒฝ ํ›„**: -```java -@ActiveProfiles("test") -@SpringBootTest(webEnvironment = RANDOM_PORT) -@AutoConfigureMockMvc -public abstract class BaseControllerTest { - - @LocalServerPort - private int port; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } - - protected RequestSpecification given() { - return RestAssured.given() - .port(port) - .contentType("application/json"); - } -} -``` - -### 6.2 Service ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - Mock ํŒจํ„ด (1์‹œ๊ฐ„) - -#### ReservationServiceTest ์žฌ์ž‘์„ฑ -**ํŒŒ์ผ**: `api-owner/src/test/java/com/wellmeet/reservation/ReservationServiceTest.java` - -**๋ณ€๊ฒฝ ์ „** (testFixtures ์‚ฌ์šฉ): -```java -class ReservationServiceTest extends BaseServiceTest { - @Autowired private ReservationService reservationService; - - @Test - void ์‹๋‹น_์•„์ด๋””์—_ํ•ด๋‹นํ•˜๋Š”_์˜ˆ์•ฝ๋ชฉ๋ก์„_๋ถˆ๋Ÿฌ์˜จ๋‹ค() { - // testFixtures๋กœ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - Owner owner = ownerGenerator.generate("owner1"); - Restaurant restaurant = restaurantGenerator.generate("restaurant1", owner.getId()); - Member member = memberGenerator.generate("member1"); - reservationGenerator.generate(...); - - List result = reservationService.getReservations(restaurant.getId()); - assertThat(result).hasSize(2); - } -} -``` - -**๋ณ€๊ฒฝ ํ›„** (Mock ๊ธฐ๋ฐ˜): -```java -@ExtendWith(MockitoExtension.class) -class ReservationServiceTest { - @Mock private ReservationClient reservationClient; - @Mock private MemberClient memberClient; - @Mock private RestaurantClient restaurantClient; - @Mock private EventPublishService eventPublishService; - @InjectMocks private ReservationService reservationService; - - @Nested - class GetReservations { - - @Test - void ์‹๋‹น_์•„์ด๋””์—_ํ•ด๋‹นํ•˜๋Š”_์˜ˆ์•ฝ๋ชฉ๋ก์„_๋ถˆ๋Ÿฌ์˜จ๋‹ค() { - String restaurantId = "restaurant-1"; - - // Mock ๋ฐ์ดํ„ฐ ์ค€๋น„ - List mockReservations = List.of( - new ReservationDTO(1L, "member-1", restaurantId, 1L, 4, "request", PENDING, now()), - new ReservationDTO(2L, "member-2", restaurantId, 2L, 2, "request", PENDING, now()) - ); - - List mockMembers = List.of( - new MemberDTO("member-1", "name1", "nick1", "email1", "phone1"), - new MemberDTO("member-2", "name2", "nick2", "email2", "phone2") - ); - - // Mock ๋™์ž‘ ์„ค์ • - when(reservationClient.getReservationsByRestaurant(restaurantId)) - .thenReturn(mockReservations); - when(memberClient.getMembersByIds(any(MemberIdsRequest.class))) - .thenReturn(mockMembers); - - // ์‹คํ–‰ - List result = reservationService.getReservations(restaurantId); - - // ๊ฒ€์ฆ - assertThat(result).hasSize(2); - assertThat(result.get(0).getMemberName()).isEqualTo("name1"); - - verify(reservationClient).getReservationsByRestaurant(restaurantId); - verify(memberClient).getMembersByIds(any(MemberIdsRequest.class)); - } - } - - @Nested - class ConfirmReservation { - - @Test - void ์˜ˆ์•ฝ์„_ํ™•์ •ํ•œ๋‹ค() { - Long reservationId = 1L; - - ReservationDTO mockReservation = new ReservationDTO( - reservationId, "member-1", "restaurant-1", 1L, 4, "request", CONFIRMED, now() - ); - MemberDTO mockMember = new MemberDTO("member-1", "name", "nick", "email", "phone"); - RestaurantDTO mockRestaurant = new RestaurantDTO("restaurant-1", "name", "address", - 37.5, 127.0, "phone", "thumbnail", "owner-1"); - - when(reservationClient.confirmReservation(reservationId)) - .thenReturn(mockReservation); - when(memberClient.getMember("member-1")) - .thenReturn(mockMember); - when(restaurantClient.getRestaurant("restaurant-1")) - .thenReturn(mockRestaurant); - - reservationService.confirmReservation(reservationId); - - verify(reservationClient).confirmReservation(reservationId); - verify(eventPublishService).publishReservationConfirmed(any()); - } - } -} -``` - -#### RestaurantServiceTest ์žฌ์ž‘์„ฑ -**ํŒŒ์ผ**: `api-owner/src/test/java/com/wellmeet/restaurant/RestaurantServiceTest.java` - -**์ƒˆ๋กœ์šด Mock ํŒจํ„ด**: -```java -@ExtendWith(MockitoExtension.class) -class RestaurantServiceTest { - @Mock private RestaurantClient restaurantClient; - @Mock private EventPublishService eventPublishService; - @InjectMocks private RestaurantService restaurantService; - - @Test - void ์‹๋‹น_์ •๋ณด๋ฅผ_์ˆ˜์ •ํ•œ๋‹ค() { - String restaurantId = "restaurant-1"; - UpdateRestaurantRequest request = new UpdateRestaurantRequest("New Name", "New Address"); - RestaurantDTO updatedRestaurant = new RestaurantDTO( - restaurantId, "New Name", "New Address", 37.5, 127.0, "phone", "thumbnail", "owner-1" - ); - - when(restaurantClient.updateRestaurant(restaurantId, request)) - .thenReturn(updatedRestaurant); - - RestaurantResponse result = restaurantService.updateRestaurant(restaurantId, request); - - assertThat(result.getName()).isEqualTo("New Name"); - verify(restaurantClient).updateRestaurant(restaurantId, request); - verify(eventPublishService).publishRestaurantUpdated(any()); - } -} -``` - -### 6.3 Controller ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - MockBean ํŒจํ„ด (1์‹œ๊ฐ„) - -#### ReservationControllerTest ์žฌ์ž‘์„ฑ -**ํŒŒ์ผ**: `api-owner/src/test/java/com/wellmeet/reservation/ReservationControllerTest.java` - -**๋ณ€๊ฒฝ ์ „** (testFixtures ์‚ฌ์šฉ): -```java -class ReservationControllerTest extends BaseControllerTest { - @Test - void ์‹๋‹น_์•„์ด๋””์—_ํ•ด๋‹นํ•˜๋Š”_์˜ˆ์•ฝ๋ชฉ๋ก์„_๋ถˆ๋Ÿฌ์˜จ๋‹ค() { - // testFixtures๋กœ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - Owner owner = ownerGenerator.generate("owner1"); - Restaurant restaurant = restaurantGenerator.generate("restaurant1", owner.getId()); - - ReservationResponse[] responses = given() - .pathParam("restaurantId", restaurant.getId()) - .when().get("/owner/reservation/{restaurantId}") - .then().statusCode(200) - .extract().as(ReservationResponse[].class); - - assertThat(responses).hasSize(2); - } -} -``` - -**๋ณ€๊ฒฝ ํ›„** (MockBean ํŒจํ„ด): -```java -@SpringBootTest(webEnvironment = RANDOM_PORT) -@ActiveProfiles("test") -class ReservationControllerTest { - - @LocalServerPort - private int port; - - @MockBean // Service๋ฅผ Mock์œผ๋กœ ๊ต์ฒด - private ReservationService reservationService; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } - - @Nested - class GetReservations { - - @Test - void ์‹๋‹น_์•„์ด๋””์—_ํ•ด๋‹นํ•˜๋Š”_์˜ˆ์•ฝ๋ชฉ๋ก์„_๋ถˆ๋Ÿฌ์˜จ๋‹ค() { - String restaurantId = "restaurant-1"; - - // Service Mock ๋™์ž‘ ์„ค์ • - List mockResponses = List.of( - createReservationResponse(1L, "member-1"), - createReservationResponse(2L, "member-2") - ); - when(reservationService.getReservations(restaurantId)) - .thenReturn(mockResponses); - - // REST API ํ˜ธ์ถœ - ReservationResponse[] responses = RestAssured.given() - .pathParam("restaurantId", restaurantId) - .when().get("/owner/reservation/{restaurantId}") - .then().statusCode(200) - .extract().as(ReservationResponse[].class); - - // ๊ฒ€์ฆ - assertThat(responses).hasSize(2); - assertThat(responses[0].getId()).isEqualTo(1L); - - verify(reservationService).getReservations(restaurantId); - } - } - - private ReservationResponse createReservationResponse(Long id, String memberId) { - return ReservationResponse.builder() - .id(id) - .memberId(memberId) - .restaurantId("restaurant-1") - .partySize(4) - .build(); - } -} -``` - -#### RestaurantControllerTest ์žฌ์ž‘์„ฑ -**ํŒŒ์ผ**: `api-owner/src/test/java/com/wellmeet/restaurant/RestaurantControllerTest.java` - -```java -@SpringBootTest(webEnvironment = RANDOM_PORT) -@ActiveProfiles("test") -class RestaurantControllerTest { - - @LocalServerPort - private int port; - - @MockBean - private RestaurantService restaurantService; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } - - @Test - void ์‹๋‹น_์ •๋ณด๋ฅผ_์ˆ˜์ •ํ•œ๋‹ค() { - String restaurantId = "restaurant-1"; - UpdateRestaurantRequest request = new UpdateRestaurantRequest("New Name", "New Address"); - RestaurantResponse mockResponse = RestaurantResponse.builder() - .id(restaurantId) - .name("New Name") - .address("New Address") - .build(); - - when(restaurantService.updateRestaurant(eq(restaurantId), any())) - .thenReturn(mockResponse); - - RestaurantResponse response = RestAssured.given() - .contentType("application/json") - .pathParam("restaurantId", restaurantId) - .body(request) - .when().put("/owner/restaurant/{restaurantId}") - .then().statusCode(200) - .extract().as(RestaurantResponse.class); - - assertThat(response.getName()).isEqualTo("New Name"); - verify(restaurantService).updateRestaurant(eq(restaurantId), any()); - } -} -``` - ---- - -## 7๋‹จ๊ณ„: build.gradle ์˜์กด์„ฑ ์ œ๊ฑฐ (10๋ถ„) - -**ํŒŒ์ผ**: `api-owner/build.gradle` - -```gradle -dependencies { - // Feign Client - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - - // KEEP - implementation project(':domain-common') - implementation project(':infra-redis') - implementation project(':infra-kafka') - - // โŒ REMOVE (4๊ฐœ ํ”„๋กœ๋•์…˜ ์˜์กด์„ฑ ์ œ๊ฑฐ) - // implementation project(':domain-reservation') - // implementation project(':domain-member') - // implementation project(':domain-owner') - // implementation project(':domain-restaurant') - - // โœ… testFixtures ์ด๋ฏธ ์ œ๊ฑฐ๋จ - - // ํ…Œ์ŠคํŠธ - testImplementation 'io.rest-assured:rest-assured' - testImplementation 'org.springframework.boot:spring-boot-starter-test' -} -``` - ---- - -## 8๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ (30๋ถ„) - -```bash -# ๋นŒ๋“œ -./gradlew :api-owner:clean :api-owner:build - -# ํ…Œ์ŠคํŠธ -./gradlew :api-owner:test - -# Docker Compose ์ „์ฒด ์‹คํ–‰ -docker-compose up -d - -# Eureka ํ™•์ธ -curl http://localhost:8761 - -# API ํ…Œ์ŠคํŠธ -curl http://localhost:8087/owner/reservation/{restaurantId} -``` - ---- - -## Phase 5-1 ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -### ์ธํ”„๋ผ -- [ ] build.gradle: OpenFeign ์˜์กด์„ฑ ์ถ”๊ฐ€ -- [ ] build.gradle: testFixtures ์˜์กด์„ฑ ์ œ๊ฑฐ -- [ ] ApiOwnerApplication: @EnableFeignClients -- [ ] application.yml: Eureka ์„ค์ • - -### DTO (5๊ฐœ + 3๊ฐœ Request) -- [ ] OwnerDTO, RestaurantDTO, ReservationDTO -- [ ] MemberDTO, AvailableDateDTO -- [ ] MemberIdsRequest, UpdateRestaurantRequest, CreateReservationDTO - -### Feign Client (4๊ฐœ) -- [ ] OwnerClient, RestaurantClient -- [ ] ReservationClient, MemberClient - -### ์„ค์ • (2๊ฐœ) -- [ ] FeignConfig, FeignErrorDecoder - -### Service ๋ฆฌํŒฉํ† ๋ง (2๊ฐœ) -- [ ] ReservationService (Feign Client ์‚ฌ์šฉ) -- [ ] RestaurantService (Feign Client ์‚ฌ์šฉ) - -### ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ โญ -- [ ] BaseControllerTest: Generator ์ œ๊ฑฐ -- [ ] BaseServiceTest: ์‚ญ์ œ ๋˜๋Š” ์™„์ „ํžˆ ์žฌ์ž‘์„ฑ -- [ ] ReservationServiceTest: Mock ํŒจํ„ด์œผ๋กœ ์žฌ์ž‘์„ฑ -- [ ] RestaurantServiceTest: Mock ํŒจํ„ด์œผ๋กœ ์žฌ์ž‘์„ฑ -- [ ] ReservationControllerTest: MockBean ํŒจํ„ด์œผ๋กœ ์žฌ์ž‘์„ฑ -- [ ] RestaurantControllerTest: MockBean ํŒจํ„ด์œผ๋กœ ์žฌ์ž‘์„ฑ - -### ์˜์กด์„ฑ ์ •๋ฆฌ -- [ ] build.gradle: domain-* 4๊ฐœ ์ œ๊ฑฐ -- [ ] build.gradle: testFixtures 4๊ฐœ ์ œ๊ฑฐ - -### ๊ฒ€์ฆ -- [ ] ๋นŒ๋“œ ์„ฑ๊ณต (domain-* ์˜์กด์„ฑ ์—†์ด) -- [ ] ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ (Mock ๊ธฐ๋ฐ˜) -- [ ] Docker Compose ์ „์ฒด ์„œ๋น„์Šค ์ž‘๋™ -- [ ] Eureka ๋“ฑ๋ก ํ™•์ธ -- [ ] Feign ํ˜ธ์ถœ ์„ฑ๊ณต - ---- - -# Phase 5-2: api-user BFF ์ „ํ™˜ + ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - -## ์˜ˆ์ƒ ์‹œ๊ฐ„: 8-9์‹œ๊ฐ„ - -๋™์ผํ•œ ํŒจํ„ด์ด์ง€๋งŒ ๋ณต์žก๋„๊ฐ€ ๋†’์Œ (๋ถ„์‚ฐ ๋ฝ, ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜) - ---- - -## 1๋‹จ๊ณ„: Feign ์ธํ”„๋ผ ๊ตฌ์ถ• (1์‹œ๊ฐ„) -- build.gradle ์ˆ˜์ • -- ApiUserApplication: @EnableFeignClients -- application.yml: Eureka ์„ค์ • -- testFixtures ์˜์กด์„ฑ ์ œ๊ฑฐ - ---- - -## 2๋‹จ๊ณ„: Feign Client DTO ์ƒ์„ฑ (1์‹œ๊ฐ„) -**์‹ ๊ทœ ํŒŒ์ผ** (8๊ฐœ + Request): -- MemberDTO, RestaurantDTO, AvailableDateDTO -- ReservationDTO, FavoriteRestaurantDTO -- ReviewDTO, MenuDTO, BusinessHourDTO -- DecreaseCapacityRequest, IncreaseCapacityRequest ๋“ฑ - ---- - -## 3๋‹จ๊ณ„: Feign Client ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ (1.5์‹œ๊ฐ„) -**์‹ ๊ทœ ํŒŒ์ผ** (5๊ฐœ): -- MemberClient, RestaurantClient -- AvailableDateClient, ReservationClient -- FavoriteRestaurantClient - ---- - -## 4๋‹จ๊ณ„: Feign ์„ค์ • ํด๋ž˜์Šค (30๋ถ„) -- FeignConfig, FeignErrorDecoder - ---- - -## 5๋‹จ๊ณ„: Service ๋ฆฌํŒฉํ† ๋ง (1.5์‹œ๊ฐ„) - -### 5.1 FavoriteService (LOW ๋ณต์žก๋„) -- FavoriteRestaurantDomainService โ†’ FavoriteRestaurantClient -- RestaurantDomainService โ†’ RestaurantClient - -### 5.2 RestaurantService (MEDIUM) -- RestaurantDomainService โ†’ RestaurantClient -- Batch ์กฐํšŒ ์ตœ์ ํ™” - -### 5.3 ReservationService (HIGH) โš ๏ธ CRITICAL -**ํŠน๋ณ„ ์‚ฌํ•ญ**: -- Redis ๋ถ„์‚ฐ ๋ฝ ์œ ์ง€ -- Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์œ ์ง€ -- ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€ - -```java -@Service -@RequiredArgsConstructor -public class ReservationService { - private final ReservationClient reservationClient; - private final RestaurantClient restaurantClient; - private final AvailableDateClient availableDateClient; - private final MemberClient memberClient; - private final ReservationRedisService redisService; // KEEP - private final EventPublishService eventPublishService; // KEEP - - public CreateReservationResponse reserve( - String memberId, - CreateReservationRequest request - ) { - // 1. Redis ๋ฝ - if (!redisService.isReserving(...)) { - throw new AlreadyReservingException(); - } - - try { - // 2. Feign ํ˜ธ์ถœ - MemberDTO member = memberClient.getMember(memberId); - availableDateClient.decreaseCapacity(...); - ReservationDTO reservation = reservationClient.create(...); - - // 3. ์ด๋ฒคํŠธ ๋ฐœํ–‰ - eventPublishService.publishReservationCreated(reservation); - - return buildResponse(reservation, member); - - } catch (Exception e) { - // ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ - availableDateClient.increaseCapacity(...); - throw e; - } - } -} -``` - ---- - -## 6๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ โญ (3์‹œ๊ฐ„) - -### 6.1 BaseControllerTest ์ˆ˜์ • (30๋ถ„) -- Generator ๋ชจ๋‘ ์ œ๊ฑฐ -- ๊ฐ„๋‹จํ•œ ํ—ฌํผ ๋ฉ”์†Œ๋“œ๋งŒ ์œ ์ง€ - -### 6.2 BaseServiceTest ์‚ญ์ œ ๋˜๋Š” ์žฌ์ž‘์„ฑ (30๋ถ„) -- Mock ๊ธฐ๋ฐ˜์œผ๋กœ ์™„์ „ํžˆ ์žฌ์ž‘์„ฑ -- ๋˜๋Š” ์‚ญ์ œํ•˜๊ณ  ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ์—์„œ ์ง์ ‘ Mock ์„ค์ • - -### 6.3 Service ํ…Œ์ŠคํŠธ ์žฌ์ž‘์„ฑ - Mock ํŒจํ„ด (1.5์‹œ๊ฐ„) - -#### ReservationServiceTest -```java -@ExtendWith(MockitoExtension.class) -class ReservationServiceTest { - @Mock private ReservationClient reservationClient; - @Mock private MemberClient memberClient; - @Mock private AvailableDateClient availableDateClient; - @Mock private RestaurantClient restaurantClient; - @Mock private ReservationRedisService redisService; - @Mock private EventPublishService eventPublishService; - @InjectMocks private ReservationService reservationService; - - @Test - void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ•œ๋‹ค() { - // Redis ๋ฝ Mock - when(redisService.isReserving(...)).thenReturn(true); - when(memberClient.getMember("member-1")).thenReturn(mockMember); - when(availableDateClient.decreaseCapacity(...)).thenReturn(success()); - when(reservationClient.create(...)).thenReturn(mockReservation); - - CreateReservationResponse response = reservationService.reserve("member-1", request); - - verify(redisService).isReserving(...); - verify(availableDateClient).decreaseCapacity(...); - verify(reservationClient).create(...); - verify(eventPublishService).publishReservationCreated(...); - } - - @Test - void ์˜ˆ์•ฝ_์‹คํŒจ_์‹œ_๋ณด์ƒ_ํŠธ๋žœ์žญ์…˜์ด_์‹คํ–‰๋œ๋‹ค() { - when(redisService.isReserving(...)).thenReturn(true); - when(memberClient.getMember(...)).thenReturn(mockMember); - when(availableDateClient.decreaseCapacity(...)).thenReturn(success()); - when(reservationClient.create(...)).thenThrow(new RuntimeException()); - - assertThatThrownBy(() -> reservationService.reserve("member-1", request)) - .isInstanceOf(RuntimeException.class); - - // ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ - verify(availableDateClient).increaseCapacity(...); - } -} -``` - -#### RestaurantServiceTest, FavoriteServiceTest -- ๋™์ผํ•œ Mock ํŒจํ„ด์œผ๋กœ ์žฌ์ž‘์„ฑ - -### 6.4 Controller ํ…Œ์ŠคํŠธ ์žฌ์ž‘์„ฑ - MockBean ํŒจํ„ด (30๋ถ„) - -#### ReservationControllerTest -```java -@SpringBootTest(webEnvironment = RANDOM_PORT) -@ActiveProfiles("test") -class ReservationControllerTest { - @MockBean private ReservationService reservationService; - - @Test - void ์˜ˆ์•ฝ์„_์ƒ์„ฑํ•œ๋‹ค() { - CreateReservationRequest request = new CreateReservationRequest(...); - CreateReservationResponse mockResponse = CreateReservationResponse.builder()...build(); - - when(reservationService.reserve(eq("member-1"), any())) - .thenReturn(mockResponse); - - CreateReservationResponse response = RestAssured.given() - .contentType("application/json") - .header("X-Member-Id", "member-1") - .body(request) - .when().post("/user/reservation") - .then().statusCode(201) - .extract().as(CreateReservationResponse.class); - - assertThat(response.getId()).isNotNull(); - verify(reservationService).reserve(eq("member-1"), any()); - } -} -``` - ---- - -## 7๋‹จ๊ณ„: build.gradle ์˜์กด์„ฑ ์ œ๊ฑฐ (10๋ถ„) - -```gradle -dependencies { - // Feign - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - - // KEEP - implementation project(':domain-common') - implementation project(':infra-redis') - implementation project(':infra-kafka') - - // โŒ REMOVE - // implementation project(':domain-reservation') - // implementation project(':domain-member') - // implementation project(':domain-owner') - // implementation project(':domain-restaurant') - - // โŒ testFixtures REMOVE -} -``` - ---- - -## 8๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ (1์‹œ๊ฐ„) - -```bash -# ๋นŒ๋“œ ๋ฐ ํ…Œ์ŠคํŠธ -./gradlew :api-user:clean :api-user:build :api-user:test - -# Docker Compose ์ „์ฒด -docker-compose up -d - -# E2E ํ…Œ์ŠคํŠธ -curl -X POST http://localhost:8086/user/reservation \ - -H "Content-Type: application/json" \ - -H "X-Member-Id: member-1" \ - -d '{"restaurantId":"...","availableDateId":1,"partySize":4}' -``` - ---- - -## Phase 5-2 ์ฒดํฌ๋ฆฌ์ŠคํŠธ - -### ์ธํ”„๋ผ -- [ ] build.gradle: OpenFeign, testFixtures ์ œ๊ฑฐ -- [ ] ApiUserApplication: @EnableFeignClients -- [ ] application.yml: Eureka ์„ค์ • - -### DTO (8๊ฐœ + Request) -- [ ] ๋ชจ๋“  DTO ์ƒ์„ฑ ์™„๋ฃŒ - -### Feign Client (5๊ฐœ) -- [ ] ๋ชจ๋“  Client ์ƒ์„ฑ ์™„๋ฃŒ - -### ์„ค์ • -- [ ] FeignConfig, FeignErrorDecoder - -### Service ๋ฆฌํŒฉํ† ๋ง (3๊ฐœ) -- [ ] FavoriteService -- [ ] RestaurantService -- [ ] ReservationService (๋ถ„์‚ฐ ๋ฝ, ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜) - -### ํ…Œ์ŠคํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ -- [ ] BaseControllerTest: Generator ์ œ๊ฑฐ -- [ ] BaseServiceTest: ์‚ญ์ œ -- [ ] ReservationServiceTest: Mock + ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ํ…Œ์ŠคํŠธ -- [ ] RestaurantServiceTest: Mock -- [ ] FavoriteServiceTest: Mock -- [ ] Controller ํ…Œ์ŠคํŠธ: MockBean ํŒจํ„ด - -### ์˜์กด์„ฑ ์ •๋ฆฌ -- [ ] build.gradle: domain-* 4๊ฐœ ์ œ๊ฑฐ -- [ ] build.gradle: testFixtures 4๊ฐœ ์ œ๊ฑฐ - -### ๊ฒ€์ฆ -- [ ] ๋นŒ๋“œ ์„ฑ๊ณต -- [ ] ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ -- [ ] Redis ๋ถ„์‚ฐ ๋ฝ ์ž‘๋™ -- [ ] Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ -- [ ] ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์ž‘๋™ - ---- - -## ์ „์ฒด Phase 5 ์™„๋ฃŒ ๊ธฐ์ค€ - -โœ… **api-owner, api-user ๋ชจ๋‘**: -- domain-* ํ”„๋กœ๋•์…˜ ์˜์กด์„ฑ ์ œ๊ฑฐ ์™„๋ฃŒ -- testFixtures ํ…Œ์ŠคํŠธ ์˜์กด์„ฑ ์ œ๊ฑฐ ์™„๋ฃŒ -- Feign Client๋กœ ์™„์ „ ์ „ํ™˜ -- ๋ชจ๋“  ํ…Œ์ŠคํŠธ Mock ๊ธฐ๋ฐ˜์œผ๋กœ ์ „ํ™˜ - -โœ… **ํ…Œ์ŠคํŠธ**: -- Service: Mock ๊ธฐ๋ฐ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ -- Controller: MockBean + REST Assured -- ์ด ํ…Œ์ŠคํŠธ ์ˆ˜: ๊ธฐ์กด๊ณผ ๋™์ผ (์•ฝ 20๊ฐœ) -- ์‹คํ–‰ ์†๋„: 3-5๋ฐฐ ๋น ๋ฆ„ (DB ์ ‘๊ทผ ์—†์Œ) - -โœ… **์‹œ์Šคํ…œ**: -- Docker Compose ์ „์ฒด ์ •์ƒ ์ž‘๋™ -- Eureka ๋“ฑ๋ก ํ™•์ธ -- Feign ํ˜ธ์ถœ ์„ฑ๊ณต -- Redis, Kafka ์ •์ƒ ์ž‘๋™ - ---- - -## ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ - -| Phase | ์‹œ๊ฐ„ | -|-------|------| -| Phase 5-1 (api-owner) | 6-7์‹œ๊ฐ„ | -| Phase 5-2 (api-user) | 8-9์‹œ๊ฐ„ | -| **์ด ์˜ˆ์ƒ ์‹œ๊ฐ„** | **14-16์‹œ๊ฐ„ (์•ฝ 2์ผ)** | - ---- - -## ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ -- [ ] claudedocs/microservices-migration-plan.md: Phase 5 ์™„๋ฃŒ ํ‘œ์‹œ -- [ ] CLAUDE.md: BFF ํ…Œ์ŠคํŠธ ์ „๋žต ์—…๋ฐ์ดํŠธ (Mock ํŒจํ„ด) - ---- - -**๋ฌธ์„œ ์ž‘์„ฑ์ผ**: 2025-11-02 -**์ž‘์„ฑ์ž**: Claude Code Agent -**ํ”„๋กœ์ ํŠธ**: WellMeet-Backend Phase 5 BFF Migration \ No newline at end of file diff --git a/common-client/build.gradle b/common-client/build.gradle new file mode 100644 index 0000000..b3e20a2 --- /dev/null +++ b/common-client/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java-library' +} + +java { + sourceCompatibility = '21' + targetCompatibility = '21' +} + +dependencies { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/AvailableDateDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/AvailableDateDTO.java new file mode 100644 index 0000000..f4cab65 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/AvailableDateDTO.java @@ -0,0 +1,17 @@ +package com.wellmeet.common.dto; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public record AvailableDateDTO( + Long id, + LocalDate date, + LocalTime time, + int maxCapacity, + boolean isAvailable, + String restaurantId, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/BusinessHourDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/BusinessHourDTO.java new file mode 100644 index 0000000..a7f1771 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/BusinessHourDTO.java @@ -0,0 +1,19 @@ +package com.wellmeet.common.dto; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public record BusinessHourDTO( + Long id, + DayOfWeek dayOfWeek, + boolean isOpen, + LocalTime openTime, + LocalTime closeTime, + LocalTime breakStartTime, + LocalTime breakEndTime, + String restaurantId, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/FavoriteRestaurantDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/FavoriteRestaurantDTO.java new file mode 100644 index 0000000..7fae645 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/FavoriteRestaurantDTO.java @@ -0,0 +1,12 @@ +package com.wellmeet.common.dto; + +import java.time.LocalDateTime; + +public record FavoriteRestaurantDTO( + Long id, + String memberId, + String restaurantId, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/MemberDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/MemberDTO.java new file mode 100644 index 0000000..9dc1613 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/MemberDTO.java @@ -0,0 +1,18 @@ +package com.wellmeet.common.dto; + +import java.time.LocalDateTime; + +public record MemberDTO( + String id, + String name, + String nickname, + String email, + String phone, + boolean reservationEnabled, + boolean remindEnabled, + boolean reviewEnabled, + boolean isVip, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/MenuDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/MenuDTO.java new file mode 100644 index 0000000..3427887 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/MenuDTO.java @@ -0,0 +1,14 @@ +package com.wellmeet.common.dto; + +import java.time.LocalDateTime; + +public record MenuDTO( + Long id, + String name, + String description, + int price, + String restaurantId, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/OwnerDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/OwnerDTO.java new file mode 100644 index 0000000..d4b4ce7 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/OwnerDTO.java @@ -0,0 +1,10 @@ +package com.wellmeet.common.dto; + +public record OwnerDTO( + String id, + String name, + String email, + boolean reservationEnabled, + boolean reviewEnabled +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/ReservationDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/ReservationDTO.java new file mode 100644 index 0000000..a080c89 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/ReservationDTO.java @@ -0,0 +1,16 @@ +package com.wellmeet.common.dto; + +import java.time.LocalDateTime; + +public record ReservationDTO( + Long id, + ReservationStatus status, + String restaurantId, + String memberId, + Long availableDateId, + int partySize, + String specialRequest, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/ReservationStatus.java b/common-client/src/main/java/com/wellmeet/common/dto/ReservationStatus.java new file mode 100644 index 0000000..0ff3493 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/ReservationStatus.java @@ -0,0 +1,8 @@ +package com.wellmeet.common.dto; + +public enum ReservationStatus { + PENDING, + CONFIRMED, + CANCELLED, + COMPLETED +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/RestaurantDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/RestaurantDTO.java new file mode 100644 index 0000000..a68418f --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/RestaurantDTO.java @@ -0,0 +1,16 @@ +package com.wellmeet.common.dto; + +import java.time.LocalDateTime; + +public record RestaurantDTO( + String id, + String name, + String address, + double latitude, + double longitude, + String thumbnail, + String ownerId, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/request/CreateReservationDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/request/CreateReservationDTO.java new file mode 100644 index 0000000..3398c34 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/request/CreateReservationDTO.java @@ -0,0 +1,10 @@ +package com.wellmeet.common.dto.request; + +public record CreateReservationDTO( + String restaurantId, + Long availableDateId, + String memberId, + int partySize, + String specialRequest +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/request/MemberIdsRequest.java b/common-client/src/main/java/com/wellmeet/common/dto/request/MemberIdsRequest.java new file mode 100644 index 0000000..30b896f --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/request/MemberIdsRequest.java @@ -0,0 +1,8 @@ +package com.wellmeet.common.dto.request; + +import java.util.List; + +public record MemberIdsRequest( + List memberIds +) { +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/request/UpdateOperatingHoursDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/request/UpdateOperatingHoursDTO.java new file mode 100644 index 0000000..362fde9 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/request/UpdateOperatingHoursDTO.java @@ -0,0 +1,18 @@ +package com.wellmeet.common.dto.request; + +import java.time.LocalTime; +import java.util.List; + +public record UpdateOperatingHoursDTO( + List operatingHours +) { + public record DayHoursDTO( + String dayOfWeek, + boolean isOperating, + LocalTime open, + LocalTime close, + LocalTime breakStart, + LocalTime breakEnd + ) { + } +} diff --git a/common-client/src/main/java/com/wellmeet/common/dto/request/UpdateRestaurantDTO.java b/common-client/src/main/java/com/wellmeet/common/dto/request/UpdateRestaurantDTO.java new file mode 100644 index 0000000..68ac9b2 --- /dev/null +++ b/common-client/src/main/java/com/wellmeet/common/dto/request/UpdateRestaurantDTO.java @@ -0,0 +1,10 @@ +package com.wellmeet.common.dto.request; + +public record UpdateRestaurantDTO( + String name, + String address, + double latitude, + double longitude, + String thumbnail +) { +} diff --git a/discovery-server/Dockerfile b/discovery-server/Dockerfile index 142b2ae..19f5e49 100644 --- a/discovery-server/Dockerfile +++ b/discovery-server/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /app COPY . . RUN gradle :discovery-server:bootJar --no-daemon -FROM openjdk:21-jdk-slim +FROM eclipse-temurin:21-jre-jammy WORKDIR /app COPY --from=build /app/discovery-server/build/libs/*.jar app.jar diff --git a/discovery-server/src/main/resources/application-dev.yml b/discovery-server/src/main/resources/application-dev.yml new file mode 100644 index 0000000..7bc3f05 --- /dev/null +++ b/discovery-server/src/main/resources/application-dev.yml @@ -0,0 +1,22 @@ +spring: + application: + name: discovery-server + +server: + port: 8761 + +eureka: + client: + register-with-eureka: false + fetch-registry: false + server: + enable-self-preservation: true # EC2 ๋ฐฐํฌ: ๋„คํŠธ์›Œํฌ ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ •๋ณด ์œ ์ง€ + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: always diff --git a/discovery-server/src/main/resources/application.yml b/discovery-server/src/main/resources/application-local.yml similarity index 75% rename from discovery-server/src/main/resources/application.yml rename to discovery-server/src/main/resources/application-local.yml index 36d5938..3d8c189 100644 --- a/discovery-server/src/main/resources/application.yml +++ b/discovery-server/src/main/resources/application-local.yml @@ -1,16 +1,16 @@ -server: - port: 8761 - spring: application: name: discovery-server +server: + port: 8761 + eureka: client: register-with-eureka: false fetch-registry: false server: - enable-self-preservation: false # ๊ฐœ๋ฐœ ํ™˜๊ฒฝ + enable-self-preservation: false # ๋กœ์ปฌ ๊ฐœ๋ฐœ: ๋น ๋ฅธ ์„œ๋น„์Šค ์ œ๊ฑฐ management: endpoints: diff --git a/docker-compose.yml b/docker-compose.yml index 8106e26..fb3a09a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: MYSQL_DATABASE: wellmeet_reservation MYSQL_ROOT_PASSWORD: password ports: - - "3306:3306" + - "3310:3306" volumes: - mysql-reservation-data:/var/lib/mysql networks: @@ -66,26 +66,33 @@ services: # Kafka zookeeper: image: confluentinc/cp-zookeeper:7.5.0 + hostname: zookeeper container_name: zookeeper - environment: - ZOOKEEPER_CLIENT_PORT: 2181 ports: - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 networks: - wellmeet-network kafka: image: confluentinc/cp-kafka:7.5.0 + hostname: kafka container_name: kafka depends_on: - zookeeper + ports: + - "9092:9092" environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - ports: - - "9092:9092" + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 networks: - wellmeet-network diff --git a/domain-member/Dockerfile b/domain-member/Dockerfile index 342d610..b1e854f 100644 --- a/domain-member/Dockerfile +++ b/domain-member/Dockerfile @@ -5,7 +5,7 @@ COPY . . RUN gradle :domain-member:bootJar --no-daemon # Stage 2: Runtime -FROM openjdk:21-jdk-slim +FROM eclipse-temurin:21-jre-jammy WORKDIR /app COPY --from=build /app/domain-member/build/libs/*.jar app.jar diff --git a/domain-member/build.gradle b/domain-member/build.gradle index 5a4cd12..851b3bf 100644 --- a/domain-member/build.gradle +++ b/domain-member/build.gradle @@ -1,8 +1,10 @@ plugins { + id 'org.springframework.boot' id 'java-test-fixtures' } dependencies { + implementation project(':common-client') implementation project(':domain-common') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -11,6 +13,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' + runtimeOnly 'com.mysql:mysql-connector-j' testFixturesImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/domain-member/src/main/java/com/wellmeet/domain/MemberServiceApplication.java b/domain-member/src/main/java/com/wellmeet/domain/MemberServiceApplication.java index 01f4ab8..a9882a1 100644 --- a/domain-member/src/main/java/com/wellmeet/domain/MemberServiceApplication.java +++ b/domain-member/src/main/java/com/wellmeet/domain/MemberServiceApplication.java @@ -1,12 +1,12 @@ -//package com.wellmeet.domain; -// -//import org.springframework.boot.SpringApplication; -//import org.springframework.boot.autoconfigure.SpringBootApplication; -// -//@SpringBootApplication -//public class MemberServiceApplication { -// -// public static void main(String[] args) { -// SpringApplication.run(MemberServiceApplication.class, args); -// } -//} +package com.wellmeet.domain; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MemberServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MemberServiceApplication.class, args); + } +} diff --git a/domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberController.java b/domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberDomainController.java similarity index 71% rename from domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberController.java rename to domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberDomainController.java index 28fdb6e..fb66729 100644 --- a/domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberController.java +++ b/domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberDomainController.java @@ -1,8 +1,8 @@ package com.wellmeet.domain.member.controller; +import com.wellmeet.common.dto.MemberDTO; import com.wellmeet.domain.member.dto.CreateMemberRequest; import com.wellmeet.domain.member.dto.MemberIdsRequest; -import com.wellmeet.domain.member.dto.MemberResponse; import com.wellmeet.domain.member.service.MemberApplicationService; import jakarta.validation.Valid; import java.util.List; @@ -18,17 +18,17 @@ @RestController @RequestMapping("/api/members") -public class MemberController { +public class MemberDomainController { private final MemberApplicationService memberApplicationService; - public MemberController(MemberApplicationService memberApplicationService) { + public MemberDomainController(MemberApplicationService memberApplicationService) { this.memberApplicationService = memberApplicationService; } @PostMapping - public ResponseEntity createMember(@Valid @RequestBody CreateMemberRequest request) { - MemberResponse response = memberApplicationService.createMember( + public ResponseEntity createMember(@Valid @RequestBody CreateMemberRequest request) { + MemberDTO response = memberApplicationService.createMember( request.name(), request.nickname(), request.email(), @@ -38,16 +38,16 @@ public ResponseEntity createMember(@Valid @RequestBody CreateMem } @GetMapping("/{id}") - public ResponseEntity getMember(@PathVariable String id) { - MemberResponse response = memberApplicationService.getMemberById(id); + public ResponseEntity getMember(@PathVariable String id) { + MemberDTO response = memberApplicationService.getMemberById(id); return ResponseEntity.ok(response); } @PostMapping("/batch") - public ResponseEntity> getMembersByIds( + public ResponseEntity> getMembersByIds( @Valid @RequestBody MemberIdsRequest request ) { - List responses = memberApplicationService.getMembersByIds(request.memberIds()); + List responses = memberApplicationService.getMembersByIds(request.memberIds()); return ResponseEntity.ok(responses); } diff --git a/domain-member/src/main/java/com/wellmeet/domain/member/controller/FavoriteRestaurantController.java b/domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberFavoriteRestaurantController.java similarity index 74% rename from domain-member/src/main/java/com/wellmeet/domain/member/controller/FavoriteRestaurantController.java rename to domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberFavoriteRestaurantController.java index 27109c9..48d8a80 100644 --- a/domain-member/src/main/java/com/wellmeet/domain/member/controller/FavoriteRestaurantController.java +++ b/domain-member/src/main/java/com/wellmeet/domain/member/controller/MemberFavoriteRestaurantController.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.member.controller; -import com.wellmeet.domain.member.dto.FavoriteRestaurantResponse; -import com.wellmeet.domain.member.service.FavoriteRestaurantApplicationService; +import com.wellmeet.common.dto.FavoriteRestaurantDTO; +import com.wellmeet.domain.member.service.MemberFavoriteRestaurantApplicationService; import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,11 +15,11 @@ @RestController @RequestMapping("/api/favorites") -public class FavoriteRestaurantController { +public class MemberFavoriteRestaurantController { - private final FavoriteRestaurantApplicationService favoriteRestaurantApplicationService; + private final MemberFavoriteRestaurantApplicationService favoriteRestaurantApplicationService; - public FavoriteRestaurantController(FavoriteRestaurantApplicationService favoriteRestaurantApplicationService) { + public MemberFavoriteRestaurantController(MemberFavoriteRestaurantApplicationService favoriteRestaurantApplicationService) { this.favoriteRestaurantApplicationService = favoriteRestaurantApplicationService; } @@ -33,20 +33,20 @@ public ResponseEntity isFavorite( } @GetMapping("/members/{memberId}") - public ResponseEntity> getFavoritesByMemberId( + public ResponseEntity> getFavoritesByMemberId( @PathVariable String memberId ) { - List responses = + List responses = favoriteRestaurantApplicationService.getFavoritesByMemberId(memberId); return ResponseEntity.ok(responses); } @PostMapping - public ResponseEntity addFavorite( + public ResponseEntity addFavorite( @RequestParam String memberId, @RequestParam String restaurantId ) { - FavoriteRestaurantResponse response = + FavoriteRestaurantDTO response = favoriteRestaurantApplicationService.addFavorite(memberId, restaurantId); return ResponseEntity.status(HttpStatus.CREATED).body(response); } diff --git a/domain-member/src/main/java/com/wellmeet/domain/member/dto/FavoriteRestaurantResponse.java b/domain-member/src/main/java/com/wellmeet/domain/member/dto/FavoriteRestaurantResponse.java deleted file mode 100644 index 84daf03..0000000 --- a/domain-member/src/main/java/com/wellmeet/domain/member/dto/FavoriteRestaurantResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.wellmeet.domain.member.dto; - -import com.wellmeet.domain.member.entity.FavoriteRestaurant; -import java.time.LocalDateTime; - -public record FavoriteRestaurantResponse( - Long id, - String memberId, - String restaurantId, - LocalDateTime createdAt, - LocalDateTime updatedAt -) { - public static FavoriteRestaurantResponse from(FavoriteRestaurant favoriteRestaurant) { - return new FavoriteRestaurantResponse( - favoriteRestaurant.getId(), - favoriteRestaurant.getMemberId(), - favoriteRestaurant.getRestaurantId(), - favoriteRestaurant.getCreatedAt(), - favoriteRestaurant.getUpdatedAt() - ); - } -} diff --git a/domain-member/src/main/java/com/wellmeet/domain/member/dto/MemberResponse.java b/domain-member/src/main/java/com/wellmeet/domain/member/dto/MemberResponse.java deleted file mode 100644 index 6826952..0000000 --- a/domain-member/src/main/java/com/wellmeet/domain/member/dto/MemberResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.wellmeet.domain.member.dto; - -import com.wellmeet.domain.member.entity.Member; -import java.time.LocalDateTime; - -public record MemberResponse( - String id, - String name, - String nickname, - String email, - String phone, - boolean reservationEnabled, - boolean remindEnabled, - boolean reviewEnabled, - boolean isVip, - LocalDateTime createdAt, - LocalDateTime updatedAt -) { - public static MemberResponse from(Member member) { - return new MemberResponse( - member.getId(), - member.getName(), - member.getNickname(), - member.getEmail(), - member.getPhone(), - member.isReservationEnabled(), - member.isRemindEnabled(), - member.isReviewEnabled(), - member.isVip(), - member.getCreatedAt(), - member.getUpdatedAt() - ); - } -} \ No newline at end of file diff --git a/domain-member/src/main/java/com/wellmeet/domain/member/service/MemberApplicationService.java b/domain-member/src/main/java/com/wellmeet/domain/member/service/MemberApplicationService.java index bea6466..b0f75b3 100644 --- a/domain-member/src/main/java/com/wellmeet/domain/member/service/MemberApplicationService.java +++ b/domain-member/src/main/java/com/wellmeet/domain/member/service/MemberApplicationService.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.member.service; +import com.wellmeet.common.dto.MemberDTO; import com.wellmeet.domain.member.MemberDomainService; -import com.wellmeet.domain.member.dto.MemberResponse; import com.wellmeet.domain.member.entity.Member; import com.wellmeet.domain.member.repository.MemberRepository; import java.util.List; @@ -18,21 +18,21 @@ public class MemberApplicationService { private final MemberRepository memberRepository; @Transactional - public MemberResponse createMember(String name, String nickname, String email, String phone) { + public MemberDTO createMember(String name, String nickname, String email, String phone) { Member member = new Member(name, nickname, email, phone); Member savedMember = memberRepository.save(member); - return MemberResponse.from(savedMember); + return toDTO(savedMember); } - public MemberResponse getMemberById(String memberId) { + public MemberDTO getMemberById(String memberId) { Member member = memberDomainService.getById(memberId); - return MemberResponse.from(member); + return toDTO(member); } - public List getMembersByIds(List memberIds) { + public List getMembersByIds(List memberIds) { return memberDomainService.findAllByIds(memberIds) .stream() - .map(MemberResponse::from) + .map(this::toDTO) .toList(); } @@ -41,4 +41,20 @@ public void deleteMember(String memberId) { Member member = memberDomainService.getById(memberId); memberRepository.delete(member); } + + private MemberDTO toDTO(Member member) { + return new MemberDTO( + member.getId(), + member.getName(), + member.getNickname(), + member.getEmail(), + member.getPhone(), + member.isReservationEnabled(), + member.isRemindEnabled(), + member.isReviewEnabled(), + member.isVip(), + member.getCreatedAt(), + member.getUpdatedAt() + ); + } } diff --git a/domain-member/src/main/java/com/wellmeet/domain/member/service/FavoriteRestaurantApplicationService.java b/domain-member/src/main/java/com/wellmeet/domain/member/service/MemberFavoriteRestaurantApplicationService.java similarity index 63% rename from domain-member/src/main/java/com/wellmeet/domain/member/service/FavoriteRestaurantApplicationService.java rename to domain-member/src/main/java/com/wellmeet/domain/member/service/MemberFavoriteRestaurantApplicationService.java index dec41f8..c90fad8 100644 --- a/domain-member/src/main/java/com/wellmeet/domain/member/service/FavoriteRestaurantApplicationService.java +++ b/domain-member/src/main/java/com/wellmeet/domain/member/service/MemberFavoriteRestaurantApplicationService.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.member.service; +import com.wellmeet.common.dto.FavoriteRestaurantDTO; import com.wellmeet.domain.member.FavoriteRestaurantDomainService; -import com.wellmeet.domain.member.dto.FavoriteRestaurantResponse; import com.wellmeet.domain.member.entity.FavoriteRestaurant; import java.util.List; import lombok.RequiredArgsConstructor; @@ -11,7 +11,7 @@ @Service @Transactional(readOnly = true) @RequiredArgsConstructor -public class FavoriteRestaurantApplicationService { +public class MemberFavoriteRestaurantApplicationService { private final FavoriteRestaurantDomainService favoriteRestaurantDomainService; @@ -19,18 +19,18 @@ public boolean isFavorite(String memberId, String restaurantId) { return favoriteRestaurantDomainService.isFavorite(memberId, restaurantId); } - public List getFavoritesByMemberId(String memberId) { + public List getFavoritesByMemberId(String memberId) { return favoriteRestaurantDomainService.findAllByMemberId(memberId) .stream() - .map(FavoriteRestaurantResponse::from) + .map(this::toDTO) .toList(); } @Transactional - public FavoriteRestaurantResponse addFavorite(String memberId, String restaurantId) { + public FavoriteRestaurantDTO addFavorite(String memberId, String restaurantId) { FavoriteRestaurant favoriteRestaurant = new FavoriteRestaurant(memberId, restaurantId); favoriteRestaurantDomainService.save(favoriteRestaurant); - return FavoriteRestaurantResponse.from(favoriteRestaurant); + return toDTO(favoriteRestaurant); } @Transactional @@ -39,4 +39,14 @@ public void removeFavorite(String memberId, String restaurantId) { favoriteRestaurantDomainService.getByMemberIdAndRestaurantId(memberId, restaurantId); favoriteRestaurantDomainService.delete(favoriteRestaurant); } + + private FavoriteRestaurantDTO toDTO(FavoriteRestaurant favoriteRestaurant) { + return new FavoriteRestaurantDTO( + favoriteRestaurant.getId(), + favoriteRestaurant.getMemberId(), + favoriteRestaurant.getRestaurantId(), + favoriteRestaurant.getCreatedAt(), + favoriteRestaurant.getUpdatedAt() + ); + } } diff --git a/domain-member/src/main/resources/application-dev.yml b/domain-member/src/main/resources/application-dev.yml new file mode 100644 index 0000000..80d7683 --- /dev/null +++ b/domain-member/src/main/resources/application-dev.yml @@ -0,0 +1,69 @@ +spring: + application: + name: domain-member-service + config: + import: + - classpath:dev-secret.yml + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database} + username: ${secret.datasource.username} + password: ${secret.datasource.password} + + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show_sql: false + use_sql_comments: true + open-in-view: false + + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + +server: + port: 8082 + shutdown: graceful + +eureka: + client: + enabled: true + service-url: + defaultZone: ${secret.eureka.server-url} + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} + lease-renewal-interval-in-seconds: 30 + lease-expiration-duration-in-seconds: 90 + +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + enabled: true + +logging: + level: + root: INFO + com.wellmeet: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.springframework.web: DEBUG + org.springframework.cloud: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/domain-member/src/main/resources/application-domain-test.yml b/domain-member/src/main/resources/application-domain-test.yml deleted file mode 100644 index 171bed7..0000000 --- a/domain-member/src/main/resources/application-domain-test.yml +++ /dev/null @@ -1,19 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 - username: root - password: - - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - format_sql: true - show_sql: true - - # Flyway๋Š” ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ๋น„ํ™œ์„ฑํ™” (JPA ddl-auto๋กœ ๋น ๋ฅธ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ) - flyway: - enabled: false diff --git a/domain-member/src/main/resources/application-flyway.yml b/domain-member/src/main/resources/application-flyway.yml new file mode 100644 index 0000000..b428ba9 --- /dev/null +++ b/domain-member/src/main/resources/application-flyway.yml @@ -0,0 +1,10 @@ +spring: + config: + activate: + on-profile: flyway + jpa: + hibernate: + ddl-auto: validate # Entity์™€ DB ์Šคํ‚ค๋งˆ ๋ถˆ์ผ์น˜ ์‹œ ์ฆ‰์‹œ ์‹คํŒจ + flyway: + enabled: true + baseline-on-migrate: true diff --git a/domain-member/src/main/resources/application.yml b/domain-member/src/main/resources/application-local.yml similarity index 90% rename from domain-member/src/main/resources/application.yml rename to domain-member/src/main/resources/application-local.yml index 81fb1b0..e28e713 100644 --- a/domain-member/src/main/resources/application.yml +++ b/domain-member/src/main/resources/application-local.yml @@ -1,6 +1,3 @@ -server: - port: 8082 - spring: application: name: domain-member-service @@ -20,6 +17,14 @@ spring: show-sql: false open-in-view: false + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + +server: + port: 8082 + eureka: client: service-url: diff --git a/domain-member/src/main/resources/application-test.yml b/domain-member/src/main/resources/application-test.yml new file mode 100644 index 0000000..53e7048 --- /dev/null +++ b/domain-member/src/main/resources/application-test.yml @@ -0,0 +1,44 @@ +spring: + application: + name: domain-member-service + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3307/wellmeet_member + username: root + password: password + + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + show-sql: true + open-in-view: false + + flyway: + enabled: false + +server: + port: 8082 + +eureka: + client: + enabled: false + register-with-eureka: false + fetch-registry: false + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: always + +logging: + level: + com.wellmeet: DEBUG + org.hibernate.SQL: DEBUG diff --git a/domain-member/src/main/resources/db/migration/V1__init_schema.sql b/domain-member/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..1efd6e0 --- /dev/null +++ b/domain-member/src/main/resources/db/migration/V1__init_schema.sql @@ -0,0 +1,31 @@ +-- Member ํ…Œ์ด๋ธ” +CREATE TABLE member +( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + nickname VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(255) NOT NULL, + reservation_enabled BOOLEAN NOT NULL DEFAULT TRUE, + remind_enabled BOOLEAN NOT NULL DEFAULT TRUE, + review_enabled BOOLEAN NOT NULL DEFAULT TRUE, + is_vip BOOLEAN NOT NULL DEFAULT FALSE, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + UNIQUE KEY unique_email (email), + UNIQUE KEY unique_phone (phone) +); + +-- FavoriteRestaurant ํ…Œ์ด๋ธ” +-- Microservices ์•„ํ‚คํ…์ฒ˜: ๋ฌผ๋ฆฌ์  ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์ œ๊ฑฐ, ๋…ผ๋ฆฌ์  ์ฐธ์กฐ๋งŒ ์œ ์ง€ +CREATE TABLE favorite_restaurant +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id VARCHAR(255) NOT NULL, -- ๋…ผ๋ฆฌ์  ์ฐธ์กฐ: member + restaurant_id VARCHAR(255) NOT NULL, -- ๋…ผ๋ฆฌ์  ์ฐธ์กฐ: domain-restaurant + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + UNIQUE KEY unique_member_restaurant (member_id, restaurant_id), + INDEX idx_favorite_restaurant_member (member_id), + INDEX idx_favorite_restaurant_restaurant (restaurant_id) +); \ No newline at end of file diff --git a/batch-reminder/src/main/resources/dev-secret.yml b/domain-member/src/main/resources/dev-secret.yml similarity index 100% rename from batch-reminder/src/main/resources/dev-secret.yml rename to domain-member/src/main/resources/dev-secret.yml diff --git a/domain-owner/src/test/java/com/wellmeet/domain/owner/BaseRepositoryTest.java b/domain-member/src/test/java/com/wellmeet/BaseRepositoryTest.java similarity index 89% rename from domain-owner/src/test/java/com/wellmeet/domain/owner/BaseRepositoryTest.java rename to domain-member/src/test/java/com/wellmeet/BaseRepositoryTest.java index 43f69eb..0eba216 100644 --- a/domain-owner/src/test/java/com/wellmeet/domain/owner/BaseRepositoryTest.java +++ b/domain-member/src/test/java/com/wellmeet/BaseRepositoryTest.java @@ -1,4 +1,4 @@ -package com.wellmeet.domain.owner; +package com.wellmeet; import com.wellmeet.domain.config.JpaAuditingConfig; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,7 +12,7 @@ }) @ExtendWith(DataBaseCleaner.class) @DataJpaTest -@ActiveProfiles("domain-test") +@ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public abstract class BaseRepositoryTest { } diff --git a/api-user/src/test/java/com/wellmeet/DataBaseCleaner.java b/domain-member/src/test/java/com/wellmeet/DataBaseCleaner.java similarity index 70% rename from api-user/src/test/java/com/wellmeet/DataBaseCleaner.java rename to domain-member/src/test/java/com/wellmeet/DataBaseCleaner.java index 83032b0..423accb 100644 --- a/api-user/src/test/java/com/wellmeet/DataBaseCleaner.java +++ b/domain-member/src/test/java/com/wellmeet/DataBaseCleaner.java @@ -1,7 +1,10 @@ package com.wellmeet; import jakarta.persistence.EntityManager; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; +import javax.sql.DataSource; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.context.ApplicationContext; @@ -10,12 +13,28 @@ public class DataBaseCleaner implements BeforeEachCallback { + private String databaseName; + @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { ApplicationContext context = SpringExtension.getApplicationContext(extensionContext); + + if (databaseName == null) { + extractDatabaseName(context); + } + cleanup(context); } + private void extractDatabaseName(ApplicationContext context) { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection conn = dataSource.getConnection()) { + databaseName = conn.getCatalog(); + } catch (SQLException e) { + throw new RuntimeException("Failed to extract database name", e); + } + } + private void cleanup(ApplicationContext context) { EntityManager em = context.getBean(EntityManager.class); TransactionTemplate transactionTemplate = context.getBean(TransactionTemplate.class); @@ -37,12 +56,12 @@ private void truncateTables(EntityManager em) { @SuppressWarnings("unchecked") private List findTableNames(EntityManager em) { - String tableNameSelectQuery = """ + String tableNameSelectQuery = String.format(""" SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'test' + WHERE TABLE_SCHEMA = '%s' AND TABLE_TYPE = 'BASE TABLE' - """; + """, databaseName); return em.createNativeQuery(tableNameSelectQuery) .getResultList(); diff --git a/domain-owner/src/test/java/com/wellmeet/domain/owner/TestConfiguration.java b/domain-member/src/test/java/com/wellmeet/TestConfiguration.java similarity index 78% rename from domain-owner/src/test/java/com/wellmeet/domain/owner/TestConfiguration.java rename to domain-member/src/test/java/com/wellmeet/TestConfiguration.java index 8a72e9c..2936096 100644 --- a/domain-owner/src/test/java/com/wellmeet/domain/owner/TestConfiguration.java +++ b/domain-member/src/test/java/com/wellmeet/TestConfiguration.java @@ -1,4 +1,4 @@ -package com.wellmeet.domain.owner; +package com.wellmeet; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/domain-member/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java b/domain-member/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java new file mode 100644 index 0000000..3cefd15 --- /dev/null +++ b/domain-member/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java @@ -0,0 +1,15 @@ +package com.wellmeet.domain; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"test", "flyway"}) +class FlywaySchemaValidationTest { + + @Test + void Flyway_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ_Entity๊ฐ€_์ผ์น˜ํ•ด์•ผ_ํ•œ๋‹ค() { + + } +} diff --git a/domain-member/src/test/java/com/wellmeet/domain/member/DataBaseCleaner.java b/domain-member/src/test/java/com/wellmeet/domain/member/DataBaseCleaner.java deleted file mode 100644 index fd26311..0000000 --- a/domain-member/src/test/java/com/wellmeet/domain/member/DataBaseCleaner.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.wellmeet.domain.member; - -import jakarta.persistence.EntityManager; -import java.util.List; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.springframework.context.ApplicationContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.support.TransactionTemplate; - -public class DataBaseCleaner implements BeforeEachCallback { - - @Override - public void beforeEach(ExtensionContext extensionContext) throws Exception { - ApplicationContext context = SpringExtension.getApplicationContext(extensionContext); - cleanup(context); - } - - private void cleanup(ApplicationContext context) { - EntityManager em = context.getBean(EntityManager.class); - TransactionTemplate transactionTemplate = context.getBean(TransactionTemplate.class); - - transactionTemplate.execute(action -> { - em.clear(); - truncateTables(em); - return null; - }); - } - - private void truncateTables(EntityManager em) { - em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); - for (String tableName : findTableNames(em)) { - em.createNativeQuery("TRUNCATE TABLE %s".formatted(tableName)).executeUpdate(); - } - em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); - } - - @SuppressWarnings("unchecked") - private List findTableNames(EntityManager em) { - String tableNameSelectQuery = """ - SELECT TABLE_NAME - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'test' - AND TABLE_TYPE = 'BASE TABLE' - """; - - return em.createNativeQuery(tableNameSelectQuery) - .getResultList(); - } -} diff --git a/domain-member/src/test/java/com/wellmeet/domain/member/FavoriteRestaurantDomainServiceTest.java b/domain-member/src/test/java/com/wellmeet/domain/member/FavoriteRestaurantDomainServiceTest.java index 61c3a98..9a32352 100644 --- a/domain-member/src/test/java/com/wellmeet/domain/member/FavoriteRestaurantDomainServiceTest.java +++ b/domain-member/src/test/java/com/wellmeet/domain/member/FavoriteRestaurantDomainServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.*; +import com.wellmeet.BaseRepositoryTest; import com.wellmeet.domain.member.entity.FavoriteRestaurant; import com.wellmeet.domain.member.entity.Member; import com.wellmeet.domain.member.exception.MemberException; diff --git a/domain-member/src/test/java/com/wellmeet/domain/member/MemberDomainServiceTest.java b/domain-member/src/test/java/com/wellmeet/domain/member/MemberDomainServiceTest.java index 0a2a4a6..ae1f3cf 100644 --- a/domain-member/src/test/java/com/wellmeet/domain/member/MemberDomainServiceTest.java +++ b/domain-member/src/test/java/com/wellmeet/domain/member/MemberDomainServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.*; +import com.wellmeet.BaseRepositoryTest; import com.wellmeet.domain.member.entity.Member; import com.wellmeet.domain.member.exception.MemberException; import com.wellmeet.domain.member.repository.MemberRepository; diff --git a/domain-owner/Dockerfile b/domain-owner/Dockerfile index e2b8310..1493b1f 100644 --- a/domain-owner/Dockerfile +++ b/domain-owner/Dockerfile @@ -5,7 +5,7 @@ COPY . . RUN gradle :domain-owner:bootJar --no-daemon # Stage 2: Runtime -FROM openjdk:21-jdk-slim +FROM eclipse-temurin:21-jre-jammy WORKDIR /app COPY --from=build /app/domain-owner/build/libs/*.jar app.jar diff --git a/domain-owner/build.gradle b/domain-owner/build.gradle index 5a4cd12..851b3bf 100644 --- a/domain-owner/build.gradle +++ b/domain-owner/build.gradle @@ -1,8 +1,10 @@ plugins { + id 'org.springframework.boot' id 'java-test-fixtures' } dependencies { + implementation project(':common-client') implementation project(':domain-common') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -11,6 +13,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' + runtimeOnly 'com.mysql:mysql-connector-j' testFixturesImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/domain-owner/src/main/java/com/wellmeet/domain/OwnerServiceApplication.java b/domain-owner/src/main/java/com/wellmeet/domain/OwnerServiceApplication.java index ec9cfea..399ff99 100644 --- a/domain-owner/src/main/java/com/wellmeet/domain/OwnerServiceApplication.java +++ b/domain-owner/src/main/java/com/wellmeet/domain/OwnerServiceApplication.java @@ -1,12 +1,12 @@ -//package com.wellmeet.domain; -// -//import org.springframework.boot.SpringApplication; -//import org.springframework.boot.autoconfigure.SpringBootApplication; -// -//@SpringBootApplication -//public class OwnerServiceApplication { -// -// public static void main(String[] args) { -// SpringApplication.run(OwnerServiceApplication.class, args); -// } -//} +package com.wellmeet.domain; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OwnerServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(OwnerServiceApplication.class, args); + } +} diff --git a/domain-owner/src/main/java/com/wellmeet/domain/owner/controller/OwnerController.java b/domain-owner/src/main/java/com/wellmeet/domain/owner/controller/OwnerDomainController.java similarity index 70% rename from domain-owner/src/main/java/com/wellmeet/domain/owner/controller/OwnerController.java rename to domain-owner/src/main/java/com/wellmeet/domain/owner/controller/OwnerDomainController.java index 56e1394..34f1d99 100644 --- a/domain-owner/src/main/java/com/wellmeet/domain/owner/controller/OwnerController.java +++ b/domain-owner/src/main/java/com/wellmeet/domain/owner/controller/OwnerDomainController.java @@ -1,8 +1,8 @@ package com.wellmeet.domain.owner.controller; +import com.wellmeet.common.dto.OwnerDTO; import com.wellmeet.domain.owner.dto.CreateOwnerRequest; import com.wellmeet.domain.owner.dto.OwnerIdsRequest; -import com.wellmeet.domain.owner.dto.OwnerResponse; import com.wellmeet.domain.owner.service.OwnerApplicationService; import jakarta.validation.Valid; import java.util.List; @@ -18,17 +18,17 @@ @RestController @RequestMapping("/api/owners") -public class OwnerController { +public class OwnerDomainController { private final OwnerApplicationService ownerApplicationService; - public OwnerController(OwnerApplicationService ownerApplicationService) { + public OwnerDomainController(OwnerApplicationService ownerApplicationService) { this.ownerApplicationService = ownerApplicationService; } @PostMapping - public ResponseEntity createOwner(@Valid @RequestBody CreateOwnerRequest request) { - OwnerResponse response = ownerApplicationService.createOwner( + public ResponseEntity createOwner(@Valid @RequestBody CreateOwnerRequest request) { + OwnerDTO response = ownerApplicationService.createOwner( request.name(), request.email() ); @@ -36,16 +36,16 @@ public ResponseEntity createOwner(@Valid @RequestBody CreateOwner } @GetMapping("/{id}") - public ResponseEntity getOwner(@PathVariable String id) { - OwnerResponse response = ownerApplicationService.getOwnerById(id); + public ResponseEntity getOwner(@PathVariable String id) { + OwnerDTO response = ownerApplicationService.getOwnerById(id); return ResponseEntity.ok(response); } @PostMapping("/batch") - public ResponseEntity> getOwnersByIds( + public ResponseEntity> getOwnersByIds( @Valid @RequestBody OwnerIdsRequest request ) { - List responses = ownerApplicationService.getOwnersByIds(request.ownerIds()); + List responses = ownerApplicationService.getOwnersByIds(request.ownerIds()); return ResponseEntity.ok(responses); } diff --git a/domain-owner/src/main/java/com/wellmeet/domain/owner/dto/OwnerResponse.java b/domain-owner/src/main/java/com/wellmeet/domain/owner/dto/OwnerResponse.java deleted file mode 100644 index ff01ae5..0000000 --- a/domain-owner/src/main/java/com/wellmeet/domain/owner/dto/OwnerResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.wellmeet.domain.owner.dto; - -import com.wellmeet.domain.owner.entity.Owner; - -public record OwnerResponse( - String id, - String name, - String email, - boolean reservationEnabled, - boolean reviewEnabled -) { - public static OwnerResponse from(Owner owner) { - return new OwnerResponse( - owner.getId(), - owner.getName(), - owner.getEmail(), - owner.isReservationEnabled(), - owner.isReviewEnabled() - ); - } -} diff --git a/domain-owner/src/main/java/com/wellmeet/domain/owner/service/OwnerApplicationService.java b/domain-owner/src/main/java/com/wellmeet/domain/owner/service/OwnerApplicationService.java index e983001..0a89076 100644 --- a/domain-owner/src/main/java/com/wellmeet/domain/owner/service/OwnerApplicationService.java +++ b/domain-owner/src/main/java/com/wellmeet/domain/owner/service/OwnerApplicationService.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.owner.service; +import com.wellmeet.common.dto.OwnerDTO; import com.wellmeet.domain.owner.OwnerDomainService; -import com.wellmeet.domain.owner.dto.OwnerResponse; import com.wellmeet.domain.owner.entity.Owner; import com.wellmeet.domain.owner.repository.OwnerRepository; import java.util.List; @@ -18,20 +18,20 @@ public class OwnerApplicationService { private final OwnerRepository ownerRepository; @Transactional - public OwnerResponse createOwner(String name, String email) { + public OwnerDTO createOwner(String name, String email) { Owner owner = new Owner(name, email); Owner saved = ownerRepository.save(owner); - return OwnerResponse.from(saved); + return toDTO(saved); } - public OwnerResponse getOwnerById(String ownerId) { + public OwnerDTO getOwnerById(String ownerId) { Owner owner = ownerDomainService.getById(ownerId); - return OwnerResponse.from(owner); + return toDTO(owner); } - public List getOwnersByIds(List ownerIds) { + public List getOwnersByIds(List ownerIds) { return ownerDomainService.findAllByIds(ownerIds).stream() - .map(OwnerResponse::from) + .map(this::toDTO) .toList(); } @@ -40,4 +40,14 @@ public void deleteOwner(String ownerId) { Owner owner = ownerDomainService.getById(ownerId); ownerRepository.delete(owner); } + + private OwnerDTO toDTO(Owner owner) { + return new OwnerDTO( + owner.getId(), + owner.getName(), + owner.getEmail(), + owner.isReservationEnabled(), + owner.isReviewEnabled() + ); + } } diff --git a/domain-owner/src/main/resources/application-dev.yml b/domain-owner/src/main/resources/application-dev.yml new file mode 100644 index 0000000..bfa5470 --- /dev/null +++ b/domain-owner/src/main/resources/application-dev.yml @@ -0,0 +1,69 @@ +spring: + application: + name: domain-owner-service + config: + import: + - classpath:dev-secret.yml + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database} + username: ${secret.datasource.username} + password: ${secret.datasource.password} + + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show_sql: false + use_sql_comments: true + open-in-view: false + + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + +server: + port: 8084 + shutdown: graceful + +eureka: + client: + enabled: true + service-url: + defaultZone: ${secret.eureka.server-url} + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} + lease-renewal-interval-in-seconds: 30 + lease-expiration-duration-in-seconds: 90 + +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + enabled: true + +logging: + level: + root: INFO + com.wellmeet: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.springframework.web: DEBUG + org.springframework.cloud: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/domain-owner/src/main/resources/application-flyway.yml b/domain-owner/src/main/resources/application-flyway.yml new file mode 100644 index 0000000..b428ba9 --- /dev/null +++ b/domain-owner/src/main/resources/application-flyway.yml @@ -0,0 +1,10 @@ +spring: + config: + activate: + on-profile: flyway + jpa: + hibernate: + ddl-auto: validate # Entity์™€ DB ์Šคํ‚ค๋งˆ ๋ถˆ์ผ์น˜ ์‹œ ์ฆ‰์‹œ ์‹คํŒจ + flyway: + enabled: true + baseline-on-migrate: true diff --git a/domain-owner/src/main/resources/application.yml b/domain-owner/src/main/resources/application-local.yml similarity index 90% rename from domain-owner/src/main/resources/application.yml rename to domain-owner/src/main/resources/application-local.yml index 5595b6a..bec9280 100644 --- a/domain-owner/src/main/resources/application.yml +++ b/domain-owner/src/main/resources/application-local.yml @@ -1,6 +1,3 @@ -server: - port: 8084 - spring: application: name: domain-owner-service @@ -20,6 +17,14 @@ spring: show-sql: false open-in-view: false + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + +server: + port: 8084 + eureka: client: service-url: diff --git a/domain-owner/src/main/resources/application-test.yml b/domain-owner/src/main/resources/application-test.yml new file mode 100644 index 0000000..1d081c5 --- /dev/null +++ b/domain-owner/src/main/resources/application-test.yml @@ -0,0 +1,44 @@ +spring: + application: + name: domain-owner-service + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3308/wellmeet_owner + username: root + password: password + + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + show-sql: true + open-in-view: false + + flyway: + enabled: false + +server: + port: 8084 + +eureka: + client: + enabled: false + register-with-eureka: false + fetch-registry: false + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: always + +logging: + level: + com.wellmeet: DEBUG + org.hibernate.SQL: DEBUG diff --git a/domain-owner/src/main/resources/db/migration/V1__init_schema.sql b/domain-owner/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..92edda3 --- /dev/null +++ b/domain-owner/src/main/resources/db/migration/V1__init_schema.sql @@ -0,0 +1,12 @@ +-- Owner ํ…Œ์ด๋ธ” +CREATE TABLE owner +( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + reservation_enabled BOOLEAN NOT NULL DEFAULT TRUE, + review_enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + UNIQUE KEY unique_email (email) +); \ No newline at end of file diff --git a/domain-owner/src/main/resources/dev-secret.yml b/domain-owner/src/main/resources/dev-secret.yml new file mode 100644 index 0000000..e69de29 diff --git a/domain-member/src/test/java/com/wellmeet/domain/member/BaseRepositoryTest.java b/domain-owner/src/test/java/com/wellmeet/BaseRepositoryTest.java similarity index 89% rename from domain-member/src/test/java/com/wellmeet/domain/member/BaseRepositoryTest.java rename to domain-owner/src/test/java/com/wellmeet/BaseRepositoryTest.java index 327af18..0eba216 100644 --- a/domain-member/src/test/java/com/wellmeet/domain/member/BaseRepositoryTest.java +++ b/domain-owner/src/test/java/com/wellmeet/BaseRepositoryTest.java @@ -1,4 +1,4 @@ -package com.wellmeet.domain.member; +package com.wellmeet; import com.wellmeet.domain.config.JpaAuditingConfig; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,7 +12,7 @@ }) @ExtendWith(DataBaseCleaner.class) @DataJpaTest -@ActiveProfiles("domain-test") +@ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public abstract class BaseRepositoryTest { } diff --git a/api-owner/src/test/java/com/wellmeet/DataBaseCleaner.java b/domain-owner/src/test/java/com/wellmeet/DataBaseCleaner.java similarity index 70% rename from api-owner/src/test/java/com/wellmeet/DataBaseCleaner.java rename to domain-owner/src/test/java/com/wellmeet/DataBaseCleaner.java index 83032b0..423accb 100644 --- a/api-owner/src/test/java/com/wellmeet/DataBaseCleaner.java +++ b/domain-owner/src/test/java/com/wellmeet/DataBaseCleaner.java @@ -1,7 +1,10 @@ package com.wellmeet; import jakarta.persistence.EntityManager; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; +import javax.sql.DataSource; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.context.ApplicationContext; @@ -10,12 +13,28 @@ public class DataBaseCleaner implements BeforeEachCallback { + private String databaseName; + @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { ApplicationContext context = SpringExtension.getApplicationContext(extensionContext); + + if (databaseName == null) { + extractDatabaseName(context); + } + cleanup(context); } + private void extractDatabaseName(ApplicationContext context) { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection conn = dataSource.getConnection()) { + databaseName = conn.getCatalog(); + } catch (SQLException e) { + throw new RuntimeException("Failed to extract database name", e); + } + } + private void cleanup(ApplicationContext context) { EntityManager em = context.getBean(EntityManager.class); TransactionTemplate transactionTemplate = context.getBean(TransactionTemplate.class); @@ -37,12 +56,12 @@ private void truncateTables(EntityManager em) { @SuppressWarnings("unchecked") private List findTableNames(EntityManager em) { - String tableNameSelectQuery = """ + String tableNameSelectQuery = String.format(""" SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'test' + WHERE TABLE_SCHEMA = '%s' AND TABLE_TYPE = 'BASE TABLE' - """; + """, databaseName); return em.createNativeQuery(tableNameSelectQuery) .getResultList(); diff --git a/domain-member/src/test/java/com/wellmeet/domain/member/TestConfiguration.java b/domain-owner/src/test/java/com/wellmeet/TestConfiguration.java similarity index 78% rename from domain-member/src/test/java/com/wellmeet/domain/member/TestConfiguration.java rename to domain-owner/src/test/java/com/wellmeet/TestConfiguration.java index 80b9954..2936096 100644 --- a/domain-member/src/test/java/com/wellmeet/domain/member/TestConfiguration.java +++ b/domain-owner/src/test/java/com/wellmeet/TestConfiguration.java @@ -1,4 +1,4 @@ -package com.wellmeet.domain.member; +package com.wellmeet; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/domain-owner/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java b/domain-owner/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java new file mode 100644 index 0000000..3cefd15 --- /dev/null +++ b/domain-owner/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java @@ -0,0 +1,15 @@ +package com.wellmeet.domain; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"test", "flyway"}) +class FlywaySchemaValidationTest { + + @Test + void Flyway_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ_Entity๊ฐ€_์ผ์น˜ํ•ด์•ผ_ํ•œ๋‹ค() { + + } +} diff --git a/domain-owner/src/test/java/com/wellmeet/domain/owner/DataBaseCleaner.java b/domain-owner/src/test/java/com/wellmeet/domain/owner/DataBaseCleaner.java deleted file mode 100644 index 95878ed..0000000 --- a/domain-owner/src/test/java/com/wellmeet/domain/owner/DataBaseCleaner.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.wellmeet.domain.owner; - -import jakarta.persistence.EntityManager; -import java.util.List; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.springframework.context.ApplicationContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.support.TransactionTemplate; - -public class DataBaseCleaner implements BeforeEachCallback { - - @Override - public void beforeEach(ExtensionContext extensionContext) throws Exception { - ApplicationContext context = SpringExtension.getApplicationContext(extensionContext); - cleanup(context); - } - - private void cleanup(ApplicationContext context) { - EntityManager em = context.getBean(EntityManager.class); - TransactionTemplate transactionTemplate = context.getBean(TransactionTemplate.class); - - transactionTemplate.execute(action -> { - em.clear(); - truncateTables(em); - return null; - }); - } - - private void truncateTables(EntityManager em) { - em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); - for (String tableName : findTableNames(em)) { - em.createNativeQuery("TRUNCATE TABLE %s".formatted(tableName)).executeUpdate(); - } - em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); - } - - @SuppressWarnings("unchecked") - private List findTableNames(EntityManager em) { - String tableNameSelectQuery = """ - SELECT TABLE_NAME - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'test' - AND TABLE_TYPE = 'BASE TABLE' - """; - - return em.createNativeQuery(tableNameSelectQuery) - .getResultList(); - } -} diff --git a/domain-reservation/Dockerfile b/domain-reservation/Dockerfile index 5280538..f0b37c7 100644 --- a/domain-reservation/Dockerfile +++ b/domain-reservation/Dockerfile @@ -1,35 +1,17 @@ +# Stage 1: Build FROM gradle:8.5-jdk21 AS build - WORKDIR /app +COPY . . +RUN gradle :domain-reservation:bootJar --no-daemon -COPY settings.gradle . -COPY build.gradle . -COPY gradle.properties . - -COPY domain-common/build.gradle domain-common/ -COPY domain-reservation/build.gradle domain-reservation/ - -COPY domain-common/src domain-common/src -COPY domain-reservation/src domain-reservation/src - -RUN gradle :domain-reservation:bootJar --no-daemon --parallel -x test - -FROM openjdk:21-jdk-slim - +# Stage 2: Runtime +FROM eclipse-temurin:21-jre-jammy WORKDIR /app - COPY --from=build /app/domain-reservation/build/libs/*.jar app.jar -HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ - CMD curl -f http://localhost:8085/actuator/health || exit 1 - -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - -RUN useradd -m -u 1001 appuser && chown -R appuser:appuser /app -USER appuser - EXPOSE 8085 -ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200" +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ + CMD curl -f http://localhost:8085/actuator/health || exit 1 -ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar --spring.profiles.active=docker"] +ENTRYPOINT ["java", "-Xms256m", "-Xmx512m", "-XX:+UseG1GC", "-jar", "app.jar"] diff --git a/domain-reservation/build.gradle b/domain-reservation/build.gradle index 0495b66..c0b1439 100644 --- a/domain-reservation/build.gradle +++ b/domain-reservation/build.gradle @@ -1,8 +1,10 @@ plugins { + id 'org.springframework.boot' id 'java-test-fixtures' } dependencies { + implementation project(':common-client') implementation project(':domain-common') implementation 'org.flywaydb:flyway-core' diff --git a/domain-reservation/src/main/java/com/wellmeet/domain/ReservationServiceApplication.java b/domain-reservation/src/main/java/com/wellmeet/domain/ReservationServiceApplication.java index 475888c..0f2b528 100644 --- a/domain-reservation/src/main/java/com/wellmeet/domain/ReservationServiceApplication.java +++ b/domain-reservation/src/main/java/com/wellmeet/domain/ReservationServiceApplication.java @@ -1,14 +1,14 @@ package com.wellmeet.domain; -// import org.springframework.boot.SpringApplication; -// import org.springframework.boot.autoconfigure.SpringBootApplication; -// import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -// -// @SpringBootApplication -// @EnableDiscoveryClient -// public class ReservationServiceApplication { -// -// public static void main(String[] args) { -// SpringApplication.run(ReservationServiceApplication.class, args); -// } -// } +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +public class ReservationServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ReservationServiceApplication.class, args); + } +} diff --git a/domain-reservation/src/main/java/com/wellmeet/domain/reservation/controller/DomainReservationController.java b/domain-reservation/src/main/java/com/wellmeet/domain/reservation/controller/ReservationDomainController.java similarity index 69% rename from domain-reservation/src/main/java/com/wellmeet/domain/reservation/controller/DomainReservationController.java rename to domain-reservation/src/main/java/com/wellmeet/domain/reservation/controller/ReservationDomainController.java index 2fd5236..2d81ae2 100644 --- a/domain-reservation/src/main/java/com/wellmeet/domain/reservation/controller/DomainReservationController.java +++ b/domain-reservation/src/main/java/com/wellmeet/domain/reservation/controller/ReservationDomainController.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.reservation.controller; +import com.wellmeet.common.dto.ReservationDTO; import com.wellmeet.domain.reservation.dto.CreateReservationRequest; -import com.wellmeet.domain.reservation.dto.ReservationResponse; import com.wellmeet.domain.reservation.dto.UpdateReservationRequest; import com.wellmeet.domain.reservation.service.ReservationApplicationService; import jakarta.validation.Valid; @@ -19,52 +19,52 @@ @RestController @RequestMapping("/api/reservation") -public class DomainReservationController { +public class ReservationDomainController { private final ReservationApplicationService reservationApplicationService; - public DomainReservationController(ReservationApplicationService reservationApplicationService) { + public ReservationDomainController(ReservationApplicationService reservationApplicationService) { this.reservationApplicationService = reservationApplicationService; } @PostMapping - public ResponseEntity createReservation( + public ResponseEntity createReservation( @Valid @RequestBody CreateReservationRequest request ) { - ReservationResponse response = reservationApplicationService.createReservation(request); + ReservationDTO response = reservationApplicationService.createReservation(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @GetMapping("/{id}") - public ResponseEntity getReservation(@PathVariable Long id) { - ReservationResponse response = reservationApplicationService.getReservation(id); + public ResponseEntity getReservation(@PathVariable Long id) { + ReservationDTO response = reservationApplicationService.getReservation(id); return ResponseEntity.ok(response); } @GetMapping("/restaurant/{restaurantId}") - public ResponseEntity> getReservationsByRestaurant( + public ResponseEntity> getReservationsByRestaurant( @PathVariable String restaurantId ) { - List responses = reservationApplicationService + List responses = reservationApplicationService .getReservationsByRestaurant(restaurantId); return ResponseEntity.ok(responses); } @GetMapping("/member/{memberId}") - public ResponseEntity> getReservationsByMember( + public ResponseEntity> getReservationsByMember( @PathVariable String memberId ) { - List responses = reservationApplicationService + List responses = reservationApplicationService .getReservationsByMember(memberId); return ResponseEntity.ok(responses); } @PutMapping("/{id}") - public ResponseEntity updateReservation( + public ResponseEntity updateReservation( @PathVariable Long id, @Valid @RequestBody UpdateReservationRequest request ) { - ReservationResponse response = reservationApplicationService.updateReservation(id, request); + ReservationDTO response = reservationApplicationService.updateReservation(id, request); return ResponseEntity.ok(response); } diff --git a/domain-reservation/src/main/java/com/wellmeet/domain/reservation/dto/ReservationResponse.java b/domain-reservation/src/main/java/com/wellmeet/domain/reservation/dto/ReservationResponse.java deleted file mode 100644 index a9d6623..0000000 --- a/domain-reservation/src/main/java/com/wellmeet/domain/reservation/dto/ReservationResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.wellmeet.domain.reservation.dto; - -import com.wellmeet.domain.reservation.entity.Reservation; -import com.wellmeet.domain.reservation.entity.ReservationStatus; -import java.time.LocalDateTime; - -public record ReservationResponse( - Long id, - String memberId, - String restaurantId, - Long availableDateId, - Integer partySize, - String specialRequest, - ReservationStatus status, - LocalDateTime createdAt, - LocalDateTime updatedAt -) { - - public static ReservationResponse from(Reservation reservation) { - return new ReservationResponse( - reservation.getId(), - reservation.getMemberId(), - reservation.getRestaurantId(), - reservation.getAvailableDateId(), - reservation.getPartySize(), - reservation.getSpecialRequest(), - reservation.getStatus(), - reservation.getCreatedAt(), - reservation.getUpdatedAt() - ); - } -} diff --git a/domain-reservation/src/main/java/com/wellmeet/domain/reservation/service/ReservationApplicationService.java b/domain-reservation/src/main/java/com/wellmeet/domain/reservation/service/ReservationApplicationService.java index 968618c..51db985 100644 --- a/domain-reservation/src/main/java/com/wellmeet/domain/reservation/service/ReservationApplicationService.java +++ b/domain-reservation/src/main/java/com/wellmeet/domain/reservation/service/ReservationApplicationService.java @@ -1,8 +1,8 @@ package com.wellmeet.domain.reservation.service; +import com.wellmeet.common.dto.ReservationDTO; import com.wellmeet.domain.reservation.ReservationDomainService; import com.wellmeet.domain.reservation.dto.CreateReservationRequest; -import com.wellmeet.domain.reservation.dto.ReservationResponse; import com.wellmeet.domain.reservation.dto.UpdateReservationRequest; import com.wellmeet.domain.reservation.entity.Reservation; import com.wellmeet.domain.reservation.entity.ReservationStatus; @@ -21,7 +21,7 @@ public ReservationApplicationService(ReservationDomainService reservationDomainS } @Transactional - public ReservationResponse createReservation(CreateReservationRequest request) { + public ReservationDTO createReservation(CreateReservationRequest request) { reservationDomainService.alreadyReserved( request.memberId(), request.restaurantId(), @@ -37,30 +37,30 @@ public ReservationResponse createReservation(CreateReservationRequest request) { ); Reservation saved = reservationDomainService.save(reservation); - return ReservationResponse.from(saved); + return toDTO(saved); } - public ReservationResponse getReservation(Long reservationId) { + public ReservationDTO getReservation(Long reservationId) { Reservation reservation = reservationDomainService.getById(reservationId); - return ReservationResponse.from(reservation); + return toDTO(reservation); } - public List getReservationsByRestaurant(String restaurantId) { + public List getReservationsByRestaurant(String restaurantId) { List reservations = reservationDomainService.findAllByRestaurantId(restaurantId); return reservations.stream() - .map(ReservationResponse::from) + .map(this::toDTO) .toList(); } - public List getReservationsByMember(String memberId) { + public List getReservationsByMember(String memberId) { List reservations = reservationDomainService.findAllByMemberId(memberId); return reservations.stream() - .map(ReservationResponse::from) + .map(this::toDTO) .toList(); } @Transactional - public ReservationResponse updateReservation(Long reservationId, UpdateReservationRequest request) { + public ReservationDTO updateReservation(Long reservationId, UpdateReservationRequest request) { Reservation reservation = reservationDomainService.getById(reservationId); if (request.status() == ReservationStatus.CONFIRMED) { @@ -77,7 +77,7 @@ public ReservationResponse updateReservation(Long reservationId, UpdateReservati reservation.update(availableDateId, partySize, specialRequest); Reservation saved = reservationDomainService.save(reservation); - return ReservationResponse.from(saved); + return toDTO(saved); } @Transactional @@ -86,4 +86,28 @@ public void cancelReservation(Long reservationId) { reservation.cancel(); reservationDomainService.save(reservation); } + + private ReservationDTO toDTO(Reservation reservation) { + return new ReservationDTO( + reservation.getId(), + convertReservationStatus(reservation.getStatus()), + reservation.getRestaurantId(), + reservation.getMemberId(), + reservation.getAvailableDateId(), + reservation.getPartySize(), + reservation.getSpecialRequest(), + reservation.getCreatedAt(), + reservation.getUpdatedAt() + ); + } + + private com.wellmeet.common.dto.ReservationStatus convertReservationStatus( + ReservationStatus status + ) { + return switch (status) { + case PENDING -> com.wellmeet.common.dto.ReservationStatus.PENDING; + case CONFIRMED -> com.wellmeet.common.dto.ReservationStatus.CONFIRMED; + case CANCELED -> com.wellmeet.common.dto.ReservationStatus.CANCELLED; + }; + } } diff --git a/domain-reservation/src/main/resources/application-dev.yml b/domain-reservation/src/main/resources/application-dev.yml new file mode 100644 index 0000000..54b243d --- /dev/null +++ b/domain-reservation/src/main/resources/application-dev.yml @@ -0,0 +1,69 @@ +spring: + application: + name: domain-reservation-service + config: + import: + - classpath:dev-secret.yml + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database} + username: ${secret.datasource.username} + password: ${secret.datasource.password} + + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show_sql: false + use_sql_comments: true + open-in-view: false + + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + +server: + port: 8085 + shutdown: graceful + +eureka: + client: + enabled: true + service-url: + defaultZone: ${secret.eureka.server-url} + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} + lease-renewal-interval-in-seconds: 30 + lease-expiration-duration-in-seconds: 90 + +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + enabled: true + +logging: + level: + root: INFO + com.wellmeet: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.springframework.web: DEBUG + org.springframework.cloud: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/domain-reservation/src/main/resources/application-domain-dev.yml b/domain-reservation/src/main/resources/application-domain-dev.yml deleted file mode 100644 index f8262c8..0000000 --- a/domain-reservation/src/main/resources/application-domain-dev.yml +++ /dev/null @@ -1,18 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database}?useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true&autoReconnect=true&serverTimezone=Asia/Seoul&useLegacyDatetimeCode=false - username: ${secret.datasource.username} - password: ${secret.datasource.password} - jpa: - show-sql: true - hibernate: - ddl-auto: validate - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.MySQLDialect - flyway: - enabled: true - locations: classpath:db/migration - baseline-on-migrate: true diff --git a/domain-reservation/src/main/resources/application-domain-local.yml b/domain-reservation/src/main/resources/application-domain-local.yml deleted file mode 100644 index 5e942f6..0000000 --- a/domain-reservation/src/main/resources/application-domain-local.yml +++ /dev/null @@ -1,18 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/wellmeet - username: root - password: - jpa: - show-sql: true - hibernate: - ddl-auto: validate - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.MySQLDialect - flyway: - enabled: true - locations: classpath:db/migration - baseline-on-migrate: true diff --git a/domain-reservation/src/main/resources/application-domain-test.yml b/domain-reservation/src/main/resources/application-domain-test.yml deleted file mode 100644 index 171bed7..0000000 --- a/domain-reservation/src/main/resources/application-domain-test.yml +++ /dev/null @@ -1,19 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 - username: root - password: - - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - format_sql: true - show_sql: true - - # Flyway๋Š” ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ๋น„ํ™œ์„ฑํ™” (JPA ddl-auto๋กœ ๋น ๋ฅธ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ) - flyway: - enabled: false diff --git a/domain-reservation/src/main/resources/application-flyway.yml b/domain-reservation/src/main/resources/application-flyway.yml new file mode 100644 index 0000000..b428ba9 --- /dev/null +++ b/domain-reservation/src/main/resources/application-flyway.yml @@ -0,0 +1,10 @@ +spring: + config: + activate: + on-profile: flyway + jpa: + hibernate: + ddl-auto: validate # Entity์™€ DB ์Šคํ‚ค๋งˆ ๋ถˆ์ผ์น˜ ์‹œ ์ฆ‰์‹œ ์‹คํŒจ + flyway: + enabled: true + baseline-on-migrate: true diff --git a/domain-reservation/src/main/resources/application.yml b/domain-reservation/src/main/resources/application-local.yml similarity index 53% rename from domain-reservation/src/main/resources/application.yml rename to domain-reservation/src/main/resources/application-local.yml index 64b5e55..9029ffe 100644 --- a/domain-reservation/src/main/resources/application.yml +++ b/domain-reservation/src/main/resources/application-local.yml @@ -4,7 +4,7 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/wellmeet_reservation?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + url: jdbc:mysql://localhost:3310/wellmeet_reservation?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 username: root password: ${DB_PASSWORD:} @@ -28,12 +28,9 @@ server: port: 8085 shutdown: graceful -# Eureka Client ์„ค์ • -# Phase 4: Application ํด๋ž˜์Šค๊ฐ€ ์ฃผ์„ ์ฒ˜๋ฆฌ๋˜์–ด ์žˆ์–ด ์‚ฌ์šฉ ์•ˆ ๋จ -# Phase 5: Application ํด๋ž˜์Šค ์ฃผ์„ ํ•ด์ œ ํ›„ ํ™œ์„ฑํ™” eureka: client: - enabled: false # Phase 4์—์„œ๋Š” ๋น„ํ™œ์„ฑํ™” + enabled: true service-url: defaultZone: http://localhost:8761/eureka/ register-with-eureka: true @@ -44,7 +41,6 @@ eureka: lease-renewal-interval-in-seconds: 30 lease-expiration-duration-in-seconds: 90 -# Actuator ์„ค์ • management: endpoints: web: @@ -58,7 +54,6 @@ management: prometheus: enabled: true -# ๋กœ๊น… ์„ค์ • logging: level: root: INFO @@ -69,51 +64,3 @@ logging: org.springframework.cloud: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" - ---- -# ํ…Œ์ŠคํŠธ ํ”„๋กœํŒŒ์ผ -spring: - config: - activate: - on-profile: test - - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/wellmeet_reservation_test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: root - password: - - jpa: - hibernate: - ddl-auto: create-drop - show-sql: true - - flyway: - enabled: false # ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” JPA๊ฐ€ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ - -eureka: - client: - enabled: false - -logging: - level: - root: INFO - com.wellmeet: DEBUG - ---- -# Docker ํ”„๋กœํŒŒ์ผ -spring: - config: - activate: - on-profile: docker - - datasource: - url: jdbc:mysql://mysql-reservation:3306/wellmeet_reservation?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: root - password: ${DB_PASSWORD:wellmeet123} - -eureka: - client: - enabled: true # Docker ํ™˜๊ฒฝ์—์„œ๋Š” Eureka ํ™œ์„ฑํ™” (Phase 5 ์ดํ›„) - service-url: - defaultZone: http://discovery-server:8761/eureka/ diff --git a/domain-reservation/src/main/resources/application-test.yml b/domain-reservation/src/main/resources/application-test.yml new file mode 100644 index 0000000..a704775 --- /dev/null +++ b/domain-reservation/src/main/resources/application-test.yml @@ -0,0 +1,45 @@ +spring: + application: + name: domain-reservation-service + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3310/wellmeet_reservation?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: password + + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show-sql: true + open-in-view: false + + flyway: + enabled: false + +server: + port: 8085 + +eureka: + client: + enabled: false + register-with-eureka: false + fetch-registry: false + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: always + +logging: + level: + root: INFO + com.wellmeet: DEBUG diff --git a/domain-reservation/src/main/resources/db/migration/V1__init_schema.sql b/domain-reservation/src/main/resources/db/migration/V1__init_schema.sql index 5b07d82..97a92dc 100644 --- a/domain-reservation/src/main/resources/db/migration/V1__init_schema.sql +++ b/domain-reservation/src/main/resources/db/migration/V1__init_schema.sql @@ -1,166 +1,19 @@ --- Owner ํ…Œ์ด๋ธ” -CREATE TABLE owner -( - id VARCHAR(255) PRIMARY KEY, - name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - reservation_enabled BOOLEAN NOT NULL DEFAULT TRUE, - review_enabled BOOLEAN NOT NULL DEFAULT TRUE, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - INDEX idx_owner_email (email) -); - --- Member ํ…Œ์ด๋ธ” -CREATE TABLE member -( - id VARCHAR(255) PRIMARY KEY, - name VARCHAR(255) NOT NULL, - nickname VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - phone VARCHAR(255) NOT NULL, - reservation_enabled BOOLEAN NOT NULL DEFAULT TRUE, - remind_enabled BOOLEAN NOT NULL DEFAULT TRUE, - review_enabled BOOLEAN NOT NULL DEFAULT TRUE, - is_vip BOOLEAN NOT NULL DEFAULT FALSE, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - INDEX idx_member_nickname (nickname), - INDEX idx_member_email (email) -); - --- Restaurant ํ…Œ์ด๋ธ” -CREATE TABLE restaurant -( - id VARCHAR(255) PRIMARY KEY, - name VARCHAR(255) NOT NULL, - address VARCHAR(255) NOT NULL, - latitude DOUBLE NOT NULL, - longitude DOUBLE NOT NULL, - thumbnail VARCHAR(255), - owner_id VARCHAR(255) NOT NULL, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (owner_id) REFERENCES owner (id), - INDEX idx_restaurant_location (latitude, longitude), - INDEX idx_restaurant_name (name), - INDEX idx_restaurant_owner (owner_id) -); - --- AvailableDate ํ…Œ์ด๋ธ” -CREATE TABLE available_date -( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - available_date DATE NOT NULL, - available_time TIME NOT NULL, - max_capacity INT NOT NULL, - is_available BOOLEAN NOT NULL DEFAULT TRUE, - restaurant_id VARCHAR(255) NOT NULL, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (restaurant_id) REFERENCES restaurant (id), - INDEX idx_available_date_restaurant (restaurant_id), - INDEX idx_available_date_datetime (available_date, available_time) -); - -- Reservation ํ…Œ์ด๋ธ” +-- Microservices ์•„ํ‚คํ…์ฒ˜: ๋ฌผ๋ฆฌ์  ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์ œ๊ฑฐ, ๋…ผ๋ฆฌ์  ์ฐธ์กฐ๋งŒ ์œ ์ง€ CREATE TABLE reservation ( id BIGINT AUTO_INCREMENT PRIMARY KEY, status VARCHAR(255) NOT NULL, - restaurant_id VARCHAR(255) NOT NULL, - available_date_id BIGINT NOT NULL, - member_id VARCHAR(255) NOT NULL, + restaurant_id VARCHAR(255) NOT NULL, -- ๋…ผ๋ฆฌ์  ์ฐธ์กฐ: domain-restaurant + available_date_id BIGINT NOT NULL, -- ๋…ผ๋ฆฌ์  ์ฐธ์กฐ: domain-restaurant + member_id VARCHAR(255) NOT NULL, -- ๋…ผ๋ฆฌ์  ์ฐธ์กฐ: domain-member party_size INT NOT NULL, special_request VARCHAR(255), created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (restaurant_id) REFERENCES restaurant (id), - FOREIGN KEY (available_date_id) REFERENCES available_date (id), - FOREIGN KEY (member_id) REFERENCES member (id), UNIQUE KEY unique_member_restaurant_available_date (member_id, restaurant_id, available_date_id), CHECK (status IN ('PENDING', 'CONFIRMED', 'CANCELED')), INDEX idx_reservation_restaurant (restaurant_id), INDEX idx_reservation_member (member_id), INDEX idx_reservation_available_date (available_date_id) -); - --- FavoriteRestaurant ํ…Œ์ด๋ธ” (member_restaurant๋ฅผ favorite_restaurant๋กœ ๋ณ€๊ฒฝ) -CREATE TABLE favorite_restaurant -( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - member_id VARCHAR(255) NOT NULL, - restaurant_id VARCHAR(255) NOT NULL, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (member_id) REFERENCES member (id), - FOREIGN KEY (restaurant_id) REFERENCES restaurant (id), - UNIQUE KEY unique_member_restaurant (member_id, restaurant_id), - INDEX idx_favorite_restaurant_member (member_id), - INDEX idx_favorite_restaurant_restaurant (restaurant_id) -); - --- BusinessHour ํ…Œ์ด๋ธ” -CREATE TABLE business_hour -( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - day_of_week VARCHAR(255) NOT NULL, - is_open BOOLEAN NOT NULL, - open_time TIME, - close_time TIME, - break_start_time TIME, - break_end_time TIME, - restaurant_id VARCHAR(255) NOT NULL, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (restaurant_id) REFERENCES restaurant (id), - CHECK (day_of_week IN ('MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY', 'HOLIDAY')), - INDEX idx_business_hour_restaurant (restaurant_id) -); - --- Menu ํ…Œ์ด๋ธ” -CREATE TABLE menu -( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT, - price INT NOT NULL, - restaurant_id VARCHAR(255) NOT NULL, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (restaurant_id) REFERENCES restaurant (id), - CHECK (price >= 0), - INDEX idx_menu_restaurant (restaurant_id), - INDEX idx_menu_price (price) -); - --- Review ํ…Œ์ด๋ธ” -CREATE TABLE review -( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - content VARCHAR(500) NOT NULL, - rating DOUBLE NOT NULL, - situation VARCHAR(255) NOT NULL, - restaurant_id VARCHAR(255) NOT NULL, - member_id VARCHAR(255) NOT NULL, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (restaurant_id) REFERENCES restaurant (id), - FOREIGN KEY (member_id) REFERENCES member (id), - CHECK (rating >= 0.0 AND rating <= 5.0), - CHECK (situation IN ('DATE', 'FAMILY', 'BUSINESS')), - INDEX idx_review_restaurant (restaurant_id), - INDEX idx_review_member (member_id) -); - --- ReviewTag ํ…Œ์ด๋ธ” -CREATE TABLE review_tag -( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - review_id BIGINT NOT NULL, - name VARCHAR(255) NOT NULL, - created_at DATETIME(6) NOT NULL, - updated_at DATETIME(6) NOT NULL, - FOREIGN KEY (review_id) REFERENCES review (id), - INDEX idx_review_tag_review (review_id) -); +); \ No newline at end of file diff --git a/domain-reservation/src/main/resources/dev-secret.yml b/domain-reservation/src/main/resources/dev-secret.yml new file mode 100644 index 0000000..e69de29 diff --git a/domain-reservation/src/test/java/com/wellmeet/BaseRepositoryTest.java b/domain-reservation/src/test/java/com/wellmeet/BaseRepositoryTest.java index 21bf518..0eba216 100644 --- a/domain-reservation/src/test/java/com/wellmeet/BaseRepositoryTest.java +++ b/domain-reservation/src/test/java/com/wellmeet/BaseRepositoryTest.java @@ -12,7 +12,7 @@ }) @ExtendWith(DataBaseCleaner.class) @DataJpaTest -@ActiveProfiles("domain-test") +@ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public abstract class BaseRepositoryTest { } diff --git a/domain-reservation/src/test/java/com/wellmeet/DataBaseCleaner.java b/domain-reservation/src/test/java/com/wellmeet/DataBaseCleaner.java index 83032b0..07f32b9 100644 --- a/domain-reservation/src/test/java/com/wellmeet/DataBaseCleaner.java +++ b/domain-reservation/src/test/java/com/wellmeet/DataBaseCleaner.java @@ -1,7 +1,10 @@ package com.wellmeet; import jakarta.persistence.EntityManager; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; +import javax.sql.DataSource; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.context.ApplicationContext; @@ -10,12 +13,28 @@ public class DataBaseCleaner implements BeforeEachCallback { + private String databaseName; + @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { ApplicationContext context = SpringExtension.getApplicationContext(extensionContext); + + if (databaseName == null) { + extractDatabaseName(context); + } + cleanup(context); } + private void extractDatabaseName(ApplicationContext context) { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection conn = dataSource.getConnection()) { + databaseName = conn.getCatalog(); + } catch (SQLException e) { + throw new RuntimeException("Failed to extract database name", e); + } + } + private void cleanup(ApplicationContext context) { EntityManager em = context.getBean(EntityManager.class); TransactionTemplate transactionTemplate = context.getBean(TransactionTemplate.class); @@ -37,14 +56,14 @@ private void truncateTables(EntityManager em) { @SuppressWarnings("unchecked") private List findTableNames(EntityManager em) { - String tableNameSelectQuery = """ + String tableNameSelectQuery = String.format(""" SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'test' + WHERE TABLE_SCHEMA = '%s' AND TABLE_TYPE = 'BASE TABLE' - """; + """, databaseName); return em.createNativeQuery(tableNameSelectQuery) .getResultList(); } -} +} \ No newline at end of file diff --git a/domain-reservation/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java b/domain-reservation/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java new file mode 100644 index 0000000..3cefd15 --- /dev/null +++ b/domain-reservation/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java @@ -0,0 +1,15 @@ +package com.wellmeet.domain; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"test", "flyway"}) +class FlywaySchemaValidationTest { + + @Test + void Flyway_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ_Entity๊ฐ€_์ผ์น˜ํ•ด์•ผ_ํ•œ๋‹ค() { + + } +} diff --git a/domain-restaurant/Dockerfile b/domain-restaurant/Dockerfile index d4fbaa0..ba5015e 100644 --- a/domain-restaurant/Dockerfile +++ b/domain-restaurant/Dockerfile @@ -5,7 +5,7 @@ COPY . . RUN gradle :domain-restaurant:bootJar --no-daemon # Stage 2: Runtime -FROM openjdk:21-jdk-slim +FROM eclipse-temurin:21-jre-jammy WORKDIR /app COPY --from=build /app/domain-restaurant/build/libs/*.jar app.jar diff --git a/domain-restaurant/build.gradle b/domain-restaurant/build.gradle index 5a4cd12..851b3bf 100644 --- a/domain-restaurant/build.gradle +++ b/domain-restaurant/build.gradle @@ -1,8 +1,10 @@ plugins { + id 'org.springframework.boot' id 'java-test-fixtures' } dependencies { + implementation project(':common-client') implementation project(':domain-common') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -11,6 +13,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' + runtimeOnly 'com.mysql:mysql-connector-j' testFixturesImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/RestaurantServiceApplication.java b/domain-restaurant/src/main/java/com/wellmeet/domain/RestaurantServiceApplication.java index 7b1cfa5..bf4f822 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/RestaurantServiceApplication.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/RestaurantServiceApplication.java @@ -1,12 +1,12 @@ -//package com.wellmeet.domain; -// -//import org.springframework.boot.SpringApplication; -//import org.springframework.boot.autoconfigure.SpringBootApplication; -// -//@SpringBootApplication -//public class RestaurantServiceApplication { -// -// public static void main(String[] args) { -// SpringApplication.run(RestaurantServiceApplication.class, args); -// } -//} +package com.wellmeet.domain; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RestaurantServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(RestaurantServiceApplication.class, args); + } +} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/DomainRestaurantService.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/RestaurantApplicationService.java similarity index 51% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/DomainRestaurantService.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/RestaurantApplicationService.java index 814f0fa..4634bde 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/DomainRestaurantService.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/RestaurantApplicationService.java @@ -1,6 +1,6 @@ package com.wellmeet.domain.restaurant; -import com.wellmeet.domain.restaurant.dto.RestaurantResponse; +import com.wellmeet.common.dto.RestaurantDTO; import com.wellmeet.domain.restaurant.entity.Restaurant; import com.wellmeet.domain.restaurant.exception.RestaurantErrorCode; import com.wellmeet.domain.restaurant.exception.RestaurantException; @@ -11,32 +11,46 @@ @Service @Transactional(readOnly = true) -public class DomainRestaurantService { +public class RestaurantApplicationService { private final RestaurantRepository restaurantRepository; - public DomainRestaurantService(RestaurantRepository restaurantRepository) { + public RestaurantApplicationService(RestaurantRepository restaurantRepository) { this.restaurantRepository = restaurantRepository; } - public RestaurantResponse getRestaurantById(String id) { + public RestaurantDTO getRestaurantById(String id) { Restaurant restaurant = restaurantRepository.findById(id) .orElseThrow(() -> new RestaurantException(RestaurantErrorCode.RESTAURANT_NOT_FOUND)); - return RestaurantResponse.from(restaurant); + return toDTO(restaurant); } - public List getAllRestaurants() { + public List getAllRestaurants() { return restaurantRepository.findAll() .stream() - .map(RestaurantResponse::from) + .map(this::toDTO) .toList(); } - public List getRestaurantsByIds(List restaurantIds) { + public List getRestaurantsByIds(List restaurantIds) { return restaurantRepository.findAllByIdIn(restaurantIds) .stream() - .map(RestaurantResponse::from) + .map(this::toDTO) .toList(); } + + private RestaurantDTO toDTO(Restaurant restaurant) { + return new RestaurantDTO( + restaurant.getId(), + restaurant.getName(), + restaurant.getAddress(), + restaurant.getLatitude(), + restaurant.getLongitude(), + restaurant.getThumbnail(), + restaurant.getOwnerId(), + restaurant.getCreatedAt(), + restaurant.getUpdatedAt() + ); + } } diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/DomainRestaurantController.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/RestaurantDomainController.java similarity index 58% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/DomainRestaurantController.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/RestaurantDomainController.java index fe332c8..3c4d145 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/DomainRestaurantController.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/RestaurantDomainController.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.restaurant; +import com.wellmeet.common.dto.RestaurantDTO; import com.wellmeet.domain.restaurant.dto.RestaurantIdsRequest; -import com.wellmeet.domain.restaurant.dto.RestaurantResponse; import jakarta.validation.Valid; import java.util.List; import org.springframework.http.ResponseEntity; @@ -14,31 +14,31 @@ @RestController @RequestMapping("/api/restaurants") -public class DomainRestaurantController { +public class RestaurantDomainController { - private final DomainRestaurantService domainRestaurantService; + private final RestaurantApplicationService restaurantApplicationService; - public DomainRestaurantController(DomainRestaurantService domainRestaurantService) { - this.domainRestaurantService = domainRestaurantService; + public RestaurantDomainController(RestaurantApplicationService restaurantApplicationService) { + this.restaurantApplicationService = restaurantApplicationService; } @GetMapping("/{id}") - public ResponseEntity getRestaurant(@PathVariable String id) { - RestaurantResponse response = domainRestaurantService.getRestaurantById(id); + public ResponseEntity getRestaurant(@PathVariable String id) { + RestaurantDTO response = restaurantApplicationService.getRestaurantById(id); return ResponseEntity.ok(response); } @GetMapping - public ResponseEntity> getAllRestaurants() { - List restaurants = domainRestaurantService.getAllRestaurants(); + public ResponseEntity> getAllRestaurants() { + List restaurants = restaurantApplicationService.getAllRestaurants(); return ResponseEntity.ok(restaurants); } @PostMapping("/batch") - public ResponseEntity> getRestaurantsByIds( + public ResponseEntity> getRestaurantsByIds( @Valid @RequestBody RestaurantIdsRequest request ) { - List restaurants = domainRestaurantService.getRestaurantsByIds(request.restaurantIds()); + List restaurants = restaurantApplicationService.getRestaurantsByIds(request.restaurantIds()); return ResponseEntity.ok(restaurants); } } diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/AvailableDateController.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantAvailableDateController.java similarity index 74% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/AvailableDateController.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantAvailableDateController.java index a1885b1..413b632 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/AvailableDateController.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantAvailableDateController.java @@ -1,10 +1,10 @@ package com.wellmeet.domain.restaurant.controller; +import com.wellmeet.common.dto.AvailableDateDTO; import com.wellmeet.domain.restaurant.dto.AvailableDateIdsRequest; -import com.wellmeet.domain.restaurant.dto.AvailableDateResponse; import com.wellmeet.domain.restaurant.dto.DecreaseCapacityRequest; import com.wellmeet.domain.restaurant.dto.IncreaseCapacityRequest; -import com.wellmeet.domain.restaurant.service.AvailableDateService; +import com.wellmeet.domain.restaurant.service.RestaurantAvailableDateApplicationService; import jakarta.validation.Valid; import java.util.List; import org.springframework.http.ResponseEntity; @@ -18,29 +18,29 @@ @RestController @RequestMapping("/api/available-dates") -public class AvailableDateController { +public class RestaurantAvailableDateController { - private final AvailableDateService availableDateService; + private final RestaurantAvailableDateApplicationService availableDateService; - public AvailableDateController(AvailableDateService availableDateService) { + public RestaurantAvailableDateController(RestaurantAvailableDateApplicationService availableDateService) { this.availableDateService = availableDateService; } @GetMapping("/restaurant/{restaurantId}") - public ResponseEntity> getAvailableDatesByRestaurant( + public ResponseEntity> getAvailableDatesByRestaurant( @PathVariable String restaurantId ) { - List availableDates = availableDateService + List availableDates = availableDateService .getAvailableDatesByRestaurantId(restaurantId); return ResponseEntity.ok(availableDates); } @PostMapping("/batch") - public ResponseEntity> getAvailableDatesByIds( + public ResponseEntity> getAvailableDatesByIds( @Valid @RequestBody AvailableDateIdsRequest request ) { - List availableDates = availableDateService + List availableDates = availableDateService .getAvailableDatesByIds(request.availableDateIds()); return ResponseEntity.ok(availableDates); diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/BusinessHourController.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantBusinessHourController.java similarity index 56% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/BusinessHourController.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantBusinessHourController.java index c68dd5c..26b6208 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/BusinessHourController.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantBusinessHourController.java @@ -1,7 +1,8 @@ package com.wellmeet.domain.restaurant.controller; -import com.wellmeet.domain.restaurant.dto.BusinessHoursResponse; -import com.wellmeet.domain.restaurant.service.BusinessHourService; +import com.wellmeet.common.dto.BusinessHourDTO; +import com.wellmeet.domain.restaurant.service.RestaurantBusinessHourApplicationService; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -10,19 +11,19 @@ @RestController @RequestMapping("/api/business-hours") -public class BusinessHourController { +public class RestaurantBusinessHourController { - private final BusinessHourService businessHourService; + private final RestaurantBusinessHourApplicationService businessHourService; - public BusinessHourController(BusinessHourService businessHourService) { + public RestaurantBusinessHourController(RestaurantBusinessHourApplicationService businessHourService) { this.businessHourService = businessHourService; } @GetMapping("/restaurant/{restaurantId}") - public ResponseEntity getBusinessHoursByRestaurant( + public ResponseEntity> getBusinessHoursByRestaurant( @PathVariable String restaurantId ) { - BusinessHoursResponse businessHours = businessHourService.getBusinessHoursByRestaurantId(restaurantId); + List businessHours = businessHourService.getBusinessHoursByRestaurantId(restaurantId); return ResponseEntity.ok(businessHours); } } diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/MenuController.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantMenuController.java similarity index 57% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/MenuController.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantMenuController.java index 4bef98f..83600bb 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/MenuController.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantMenuController.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.restaurant.controller; -import com.wellmeet.domain.restaurant.dto.MenuResponse; -import com.wellmeet.domain.restaurant.service.MenuService; +import com.wellmeet.common.dto.MenuDTO; +import com.wellmeet.domain.restaurant.service.RestaurantMenuApplicationService; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -11,19 +11,19 @@ @RestController @RequestMapping("/api/menus") -public class MenuController { +public class RestaurantMenuController { - private final MenuService menuService; + private final RestaurantMenuApplicationService menuService; - public MenuController(MenuService menuService) { + public RestaurantMenuController(RestaurantMenuApplicationService menuService) { this.menuService = menuService; } @GetMapping("/restaurant/{restaurantId}") - public ResponseEntity> getMenusByRestaurant( + public ResponseEntity> getMenusByRestaurant( @PathVariable String restaurantId ) { - List menus = menuService.getMenusByRestaurantId(restaurantId); + List menus = menuService.getMenusByRestaurantId(restaurantId); return ResponseEntity.ok(menus); } } diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/ReviewController.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantReviewController.java similarity index 80% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/ReviewController.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantReviewController.java index 4a015fe..9223cf7 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/ReviewController.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/controller/RestaurantReviewController.java @@ -1,7 +1,7 @@ package com.wellmeet.domain.restaurant.controller; import com.wellmeet.domain.restaurant.dto.ReviewResponse; -import com.wellmeet.domain.restaurant.service.ReviewService; +import com.wellmeet.domain.restaurant.service.RestaurantReviewApplicationService; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -11,11 +11,11 @@ @RestController @RequestMapping("/api/reviews") -public class ReviewController { +public class RestaurantReviewController { - private final ReviewService reviewService; + private final RestaurantReviewApplicationService reviewService; - public ReviewController(ReviewService reviewService) { + public RestaurantReviewController(RestaurantReviewApplicationService reviewService) { this.reviewService = reviewService; } diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/AvailableDateResponse.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/AvailableDateResponse.java deleted file mode 100644 index a0f50d7..0000000 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/AvailableDateResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.wellmeet.domain.restaurant.dto; - -import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; - -public record AvailableDateResponse( - Long id, - LocalDate date, - LocalTime time, - int maxCapacity, - boolean isAvailable, - String restaurantId, - LocalDateTime createdAt, - LocalDateTime updatedAt -) { - public static AvailableDateResponse from(AvailableDate availableDate) { - return new AvailableDateResponse( - availableDate.getId(), - availableDate.getDate(), - availableDate.getTime(), - availableDate.getMaxCapacity(), - availableDate.isAvailable(), - availableDate.getRestaurant().getId(), - availableDate.getCreatedAt(), - availableDate.getUpdatedAt() - ); - } -} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/BusinessHourResponse.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/BusinessHourResponse.java deleted file mode 100644 index b7d4a6e..0000000 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/BusinessHourResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.wellmeet.domain.restaurant.dto; - -import com.wellmeet.domain.restaurant.businesshour.entity.BusinessHour; -import com.wellmeet.domain.restaurant.businesshour.entity.DayOfWeek; -import java.time.LocalDateTime; -import java.time.LocalTime; - -public record BusinessHourResponse( - Long id, - DayOfWeek dayOfWeek, - boolean isOpen, - LocalTime openTime, - LocalTime closeTime, - LocalTime breakStartTime, - LocalTime breakEndTime, - String restaurantId, - LocalDateTime createdAt, - LocalDateTime updatedAt -) { - public static BusinessHourResponse from(BusinessHour businessHour) { - return new BusinessHourResponse( - businessHour.getId(), - businessHour.getDayOfWeek(), - businessHour.isOpen(), - businessHour.getOpenTime(), - businessHour.getCloseTime(), - businessHour.getBreakStartTime(), - businessHour.getBreakEndTime(), - businessHour.getRestaurant().getId(), - businessHour.getCreatedAt(), - businessHour.getUpdatedAt() - ); - } -} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/BusinessHoursResponse.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/BusinessHoursResponse.java deleted file mode 100644 index c334f46..0000000 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/BusinessHoursResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.wellmeet.domain.restaurant.dto; - -import com.wellmeet.domain.restaurant.businesshour.entity.BusinessHours; -import java.util.List; - -public record BusinessHoursResponse( - List businessHours -) { - public static BusinessHoursResponse from(BusinessHours businessHours) { - List businessHourResponses = businessHours.getValue().stream() - .map(BusinessHourResponse::from) - .toList(); - - return new BusinessHoursResponse(businessHourResponses); - } -} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/MenuResponse.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/MenuResponse.java deleted file mode 100644 index 1e86966..0000000 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/MenuResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wellmeet.domain.restaurant.dto; - -import com.wellmeet.domain.restaurant.menu.entity.Menu; -import java.time.LocalDateTime; - -public record MenuResponse( - Long id, - String name, - String description, - int price, - String restaurantId, - LocalDateTime createdAt, - LocalDateTime updatedAt -) { - public static MenuResponse from(Menu menu) { - return new MenuResponse( - menu.getId(), - menu.getName(), - menu.getDescription(), - menu.getPrice(), - menu.getRestaurant().getId(), - menu.getCreatedAt(), - menu.getUpdatedAt() - ); - } -} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/RestaurantResponse.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/RestaurantResponse.java deleted file mode 100644 index 49a2ba2..0000000 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/dto/RestaurantResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.wellmeet.domain.restaurant.dto; - -import com.wellmeet.domain.restaurant.entity.Restaurant; -import java.time.LocalDateTime; - -public record RestaurantResponse( - String id, - String name, - String address, - double latitude, - double longitude, - String thumbnail, - String ownerId, - LocalDateTime createdAt, - LocalDateTime updatedAt -) { - public static RestaurantResponse from(Restaurant restaurant) { - return new RestaurantResponse( - restaurant.getId(), - restaurant.getName(), - restaurant.getAddress(), - restaurant.getLatitude(), - restaurant.getLongitude(), - restaurant.getThumbnail(), - restaurant.getOwnerId(), - restaurant.getCreatedAt(), - restaurant.getUpdatedAt() - ); - } -} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/BusinessHourService.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/BusinessHourService.java deleted file mode 100644 index 9f314ed..0000000 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/BusinessHourService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wellmeet.domain.restaurant.service; - -import com.wellmeet.domain.restaurant.businesshour.entity.BusinessHours; -import com.wellmeet.domain.restaurant.businesshour.repository.BusinessHourRepository; -import com.wellmeet.domain.restaurant.dto.BusinessHoursResponse; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -public class BusinessHourService { - - private final BusinessHourRepository businessHourRepository; - - public BusinessHourService(BusinessHourRepository businessHourRepository) { - this.businessHourRepository = businessHourRepository; - } - - public BusinessHoursResponse getBusinessHoursByRestaurantId(String restaurantId) { - BusinessHours businessHours = new BusinessHours( - businessHourRepository.findAllByRestaurantId(restaurantId) - ); - - return BusinessHoursResponse.from(businessHours); - } -} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/MenuService.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/MenuService.java deleted file mode 100644 index 6b55baa..0000000 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/MenuService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.wellmeet.domain.restaurant.service; - -import com.wellmeet.domain.restaurant.dto.MenuResponse; -import com.wellmeet.domain.restaurant.menu.repository.MenuRepository; -import java.util.List; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -public class MenuService { - - private final MenuRepository menuRepository; - - public MenuService(MenuRepository menuRepository) { - this.menuRepository = menuRepository; - } - - public List getMenusByRestaurantId(String restaurantId) { - return menuRepository.findByRestaurantId(restaurantId) - .stream() - .map(MenuResponse::from) - .toList(); - } -} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/AvailableDateService.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantAvailableDateApplicationService.java similarity index 57% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/AvailableDateService.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantAvailableDateApplicationService.java index e52e5eb..8fed919 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/AvailableDateService.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantAvailableDateApplicationService.java @@ -1,7 +1,8 @@ package com.wellmeet.domain.restaurant.service; +import com.wellmeet.common.dto.AvailableDateDTO; +import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; import com.wellmeet.domain.restaurant.availabledate.repository.AvailableDateRepository; -import com.wellmeet.domain.restaurant.dto.AvailableDateResponse; import com.wellmeet.domain.restaurant.exception.RestaurantErrorCode; import com.wellmeet.domain.restaurant.exception.RestaurantException; import java.util.List; @@ -10,25 +11,25 @@ @Service @Transactional(readOnly = true) -public class AvailableDateService { +public class RestaurantAvailableDateApplicationService { private final AvailableDateRepository availableDateRepository; - public AvailableDateService(AvailableDateRepository availableDateRepository) { + public RestaurantAvailableDateApplicationService(AvailableDateRepository availableDateRepository) { this.availableDateRepository = availableDateRepository; } - public List getAvailableDatesByRestaurantId(String restaurantId) { + public List getAvailableDatesByRestaurantId(String restaurantId) { return availableDateRepository.findAllByRestaurantId(restaurantId) .stream() - .map(AvailableDateResponse::from) + .map(this::toDTO) .toList(); } - public List getAvailableDatesByIds(List availableDateIds) { + public List getAvailableDatesByIds(List availableDateIds) { return availableDateRepository.findAllByIdIn(availableDateIds) .stream() - .map(AvailableDateResponse::from) + .map(this::toDTO) .toList(); } @@ -45,4 +46,17 @@ public void decreaseCapacity(Long availableDateId, int partySize) { public void increaseCapacity(Long availableDateId, int partySize) { availableDateRepository.increaseCapacity(availableDateId, partySize); } + + private AvailableDateDTO toDTO(AvailableDate availableDate) { + return new AvailableDateDTO( + availableDate.getId(), + availableDate.getDate(), + availableDate.getTime(), + availableDate.getMaxCapacity(), + availableDate.isAvailable(), + availableDate.getRestaurant().getId(), + availableDate.getCreatedAt(), + availableDate.getUpdatedAt() + ); + } } diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantBusinessHourApplicationService.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantBusinessHourApplicationService.java new file mode 100644 index 0000000..1ba7353 --- /dev/null +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantBusinessHourApplicationService.java @@ -0,0 +1,54 @@ +package com.wellmeet.domain.restaurant.service; + +import com.wellmeet.common.dto.BusinessHourDTO; +import com.wellmeet.domain.restaurant.businesshour.entity.BusinessHour; +import com.wellmeet.domain.restaurant.businesshour.repository.BusinessHourRepository; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class RestaurantBusinessHourApplicationService { + + private final BusinessHourRepository businessHourRepository; + + public RestaurantBusinessHourApplicationService(BusinessHourRepository businessHourRepository) { + this.businessHourRepository = businessHourRepository; + } + + public List getBusinessHoursByRestaurantId(String restaurantId) { + return businessHourRepository.findAllByRestaurantId(restaurantId) + .stream() + .map(this::toDTO) + .toList(); + } + + private BusinessHourDTO toDTO(BusinessHour businessHour) { + return new BusinessHourDTO( + businessHour.getId(), + convertDayOfWeek(businessHour.getDayOfWeek()), + businessHour.isOpen(), + businessHour.getOpenTime(), + businessHour.getCloseTime(), + businessHour.getBreakStartTime(), + businessHour.getBreakEndTime(), + businessHour.getRestaurant().getId(), + businessHour.getCreatedAt(), + businessHour.getUpdatedAt() + ); + } + + private java.time.DayOfWeek convertDayOfWeek(com.wellmeet.domain.restaurant.businesshour.entity.DayOfWeek dayOfWeek) { + return switch (dayOfWeek) { + case MONDAY -> java.time.DayOfWeek.MONDAY; + case TUESDAY -> java.time.DayOfWeek.TUESDAY; + case WEDNESDAY -> java.time.DayOfWeek.WEDNESDAY; + case THURSDAY -> java.time.DayOfWeek.THURSDAY; + case FRIDAY -> java.time.DayOfWeek.FRIDAY; + case SATURDAY -> java.time.DayOfWeek.SATURDAY; + case SUNDAY -> java.time.DayOfWeek.SUNDAY; + case HOLIDAY -> java.time.DayOfWeek.SUNDAY; + }; + } +} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantMenuApplicationService.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantMenuApplicationService.java new file mode 100644 index 0000000..7f70afe --- /dev/null +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantMenuApplicationService.java @@ -0,0 +1,38 @@ +package com.wellmeet.domain.restaurant.service; + +import com.wellmeet.common.dto.MenuDTO; +import com.wellmeet.domain.restaurant.menu.entity.Menu; +import com.wellmeet.domain.restaurant.menu.repository.MenuRepository; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class RestaurantMenuApplicationService { + + private final MenuRepository menuRepository; + + public RestaurantMenuApplicationService(MenuRepository menuRepository) { + this.menuRepository = menuRepository; + } + + public List getMenusByRestaurantId(String restaurantId) { + return menuRepository.findByRestaurantId(restaurantId) + .stream() + .map(this::toDTO) + .toList(); + } + + private MenuDTO toDTO(Menu menu) { + return new MenuDTO( + menu.getId(), + menu.getName(), + menu.getDescription(), + menu.getPrice(), + menu.getRestaurant().getId(), + menu.getCreatedAt(), + menu.getUpdatedAt() + ); + } +} diff --git a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/ReviewService.java b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantReviewApplicationService.java similarity index 86% rename from domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/ReviewService.java rename to domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantReviewApplicationService.java index 423a0a7..d340298 100644 --- a/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/ReviewService.java +++ b/domain-restaurant/src/main/java/com/wellmeet/domain/restaurant/service/RestaurantReviewApplicationService.java @@ -8,11 +8,11 @@ @Service @Transactional(readOnly = true) -public class ReviewService { +public class RestaurantReviewApplicationService { private final ReviewRepository reviewRepository; - public ReviewService(ReviewRepository reviewRepository) { + public RestaurantReviewApplicationService(ReviewRepository reviewRepository) { this.reviewRepository = reviewRepository; } diff --git a/domain-restaurant/src/main/resources/application-dev.yml b/domain-restaurant/src/main/resources/application-dev.yml new file mode 100644 index 0000000..5385657 --- /dev/null +++ b/domain-restaurant/src/main/resources/application-dev.yml @@ -0,0 +1,74 @@ +spring: + application: + name: domain-restaurant-service + config: + import: + - classpath:dev-secret.yml + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${secret.datasource.url}:${secret.datasource.port}/${secret.datasource.database} + username: ${secret.datasource.username} + password: ${secret.datasource.password} + + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show_sql: false + use_sql_comments: true + open-in-view: false + + jackson: + time-zone: Asia/Seoul + serialization: + write-dates-as-timestamps: false + + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + +server: + port: 8083 + shutdown: graceful + +eureka: + client: + enabled: true + service-url: + defaultZone: ${secret.eureka.server-url} + register-with-eureka: true + fetch-registry: true + instance: + prefer-ip-address: true + instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} + lease-renewal-interval-in-seconds: 30 + lease-expiration-duration-in-seconds: 90 + +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + enabled: true + +logging: + level: + root: INFO + com.wellmeet: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.springframework.web: DEBUG + org.springframework.cloud: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/domain-restaurant/src/main/resources/application-domain-test.yml b/domain-restaurant/src/main/resources/application-domain-test.yml deleted file mode 100644 index 171bed7..0000000 --- a/domain-restaurant/src/main/resources/application-domain-test.yml +++ /dev/null @@ -1,19 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 - username: root - password: - - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - format_sql: true - show_sql: true - - # Flyway๋Š” ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ๋น„ํ™œ์„ฑํ™” (JPA ddl-auto๋กœ ๋น ๋ฅธ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ) - flyway: - enabled: false diff --git a/domain-restaurant/src/main/resources/application-flyway.yml b/domain-restaurant/src/main/resources/application-flyway.yml new file mode 100644 index 0000000..b428ba9 --- /dev/null +++ b/domain-restaurant/src/main/resources/application-flyway.yml @@ -0,0 +1,10 @@ +spring: + config: + activate: + on-profile: flyway + jpa: + hibernate: + ddl-auto: validate # Entity์™€ DB ์Šคํ‚ค๋งˆ ๋ถˆ์ผ์น˜ ์‹œ ์ฆ‰์‹œ ์‹คํŒจ + flyway: + enabled: true + baseline-on-migrate: true diff --git a/domain-restaurant/src/main/resources/application.yml b/domain-restaurant/src/main/resources/application-local.yml similarity index 86% rename from domain-restaurant/src/main/resources/application.yml rename to domain-restaurant/src/main/resources/application-local.yml index d998d16..f9c4caf 100644 --- a/domain-restaurant/src/main/resources/application.yml +++ b/domain-restaurant/src/main/resources/application-local.yml @@ -1,6 +1,6 @@ spring: application: - name: restaurant-service + name: domain-restaurant-service datasource: driver-class-name: com.mysql.cj.jdbc.Driver @@ -23,6 +23,11 @@ spring: serialization: write-dates-as-timestamps: false + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + server: port: 8083 diff --git a/domain-restaurant/src/main/resources/application-test.yml b/domain-restaurant/src/main/resources/application-test.yml new file mode 100644 index 0000000..d0577c1 --- /dev/null +++ b/domain-restaurant/src/main/resources/application-test.yml @@ -0,0 +1,45 @@ +spring: + application: + name: domain-restaurant-service + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3309/wellmeet_restaurant + username: root + password: password + + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + show-sql: true + open-in-view: false + + flyway: + enabled: false + + jackson: + time-zone: Asia/Seoul + serialization: + write-dates-as-timestamps: false + +server: + port: 8083 + +eureka: + client: + enabled: false + register-with-eureka: false + fetch-registry: false + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: always diff --git a/domain-restaurant/src/main/resources/db/migration/V1__init_schema.sql b/domain-restaurant/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..16cc01e --- /dev/null +++ b/domain-restaurant/src/main/resources/db/migration/V1__init_schema.sql @@ -0,0 +1,97 @@ +-- Restaurant ํ…Œ์ด๋ธ” +-- Microservices ์•„ํ‚คํ…์ฒ˜: ๋ฌผ๋ฆฌ์  ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์ œ๊ฑฐ, ๋…ผ๋ฆฌ์  ์ฐธ์กฐ๋งŒ ์œ ์ง€ +CREATE TABLE restaurant +( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + address VARCHAR(255) NOT NULL, + latitude DOUBLE NOT NULL, + longitude DOUBLE NOT NULL, + phone_number VARCHAR(255), + owner_id VARCHAR(255) NOT NULL, -- ๋…ผ๋ฆฌ์  ์ฐธ์กฐ: domain-owner + thumbnail VARCHAR(255), + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + INDEX idx_restaurant_owner (owner_id), + INDEX idx_restaurant_location (latitude, longitude) +); + +-- AvailableDate ํ…Œ์ด๋ธ” +CREATE TABLE available_date +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + available_date DATE NOT NULL, + available_time TIME NOT NULL, + max_capacity INT NOT NULL, + is_available BOOLEAN NOT NULL DEFAULT TRUE, + restaurant_id VARCHAR(255) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + CONSTRAINT fk_available_date_restaurant FOREIGN KEY (restaurant_id) REFERENCES restaurant (id) ON DELETE CASCADE, + INDEX idx_available_date_restaurant (restaurant_id), + INDEX idx_available_date_date_time (available_date, available_time) +); + +-- BusinessHour ํ…Œ์ด๋ธ” +CREATE TABLE business_hour +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + day_of_week VARCHAR(255) NOT NULL, + open_time TIME NOT NULL, + close_time TIME NOT NULL, + is_open BOOLEAN NOT NULL DEFAULT TRUE, + break_start_time TIME, + break_end_time TIME, + restaurant_id VARCHAR(255) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + CONSTRAINT fk_business_hour_restaurant FOREIGN KEY (restaurant_id) REFERENCES restaurant (id) ON DELETE CASCADE, + CHECK (day_of_week IN ('MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY')), + INDEX idx_business_hour_restaurant (restaurant_id) +); + +-- Menu ํ…Œ์ด๋ธ” +CREATE TABLE menu +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + price INT NOT NULL, + restaurant_id VARCHAR(255) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + CONSTRAINT fk_menu_restaurant FOREIGN KEY (restaurant_id) REFERENCES restaurant (id) ON DELETE CASCADE, + INDEX idx_menu_restaurant (restaurant_id) +); + +-- Review ํ…Œ์ด๋ธ” +-- Microservices ์•„ํ‚คํ…์ฒ˜: ๋ฌผ๋ฆฌ์  ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์ œ๊ฑฐ, ๋…ผ๋ฆฌ์  ์ฐธ์กฐ๋งŒ ์œ ์ง€ +CREATE TABLE review +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + content TEXT NOT NULL, + rating DOUBLE NOT NULL, + situation VARCHAR(255) NOT NULL, + restaurant_id VARCHAR(255) NOT NULL, + member_id VARCHAR(255) NOT NULL, -- ๋…ผ๋ฆฌ์  ์ฐธ์กฐ: domain-member + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + CONSTRAINT fk_review_restaurant FOREIGN KEY (restaurant_id) REFERENCES restaurant (id) ON DELETE CASCADE, + CHECK (situation IN ('DATE', 'FAMILY', 'BUSINESS')), + CHECK (rating >= 0.0 AND rating <= 5.0), + INDEX idx_review_restaurant (restaurant_id), + INDEX idx_review_member (member_id) +); + +-- ReviewTag ํ…Œ์ด๋ธ” +CREATE TABLE review_tag +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + review_id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + CONSTRAINT fk_review_tag_review FOREIGN KEY (review_id) REFERENCES review (id) ON DELETE CASCADE, + INDEX idx_review_tag_review (review_id) +); \ No newline at end of file diff --git a/domain-restaurant/src/main/resources/dev-secret.yml b/domain-restaurant/src/main/resources/dev-secret.yml new file mode 100644 index 0000000..e69de29 diff --git a/domain-restaurant/src/test/java/com/wellmeet/BaseRepositoryTest.java b/domain-restaurant/src/test/java/com/wellmeet/BaseRepositoryTest.java index 21bf518..0eba216 100644 --- a/domain-restaurant/src/test/java/com/wellmeet/BaseRepositoryTest.java +++ b/domain-restaurant/src/test/java/com/wellmeet/BaseRepositoryTest.java @@ -12,7 +12,7 @@ }) @ExtendWith(DataBaseCleaner.class) @DataJpaTest -@ActiveProfiles("domain-test") +@ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public abstract class BaseRepositoryTest { } diff --git a/domain-restaurant/src/test/java/com/wellmeet/DataBaseCleaner.java b/domain-restaurant/src/test/java/com/wellmeet/DataBaseCleaner.java index 83032b0..07f32b9 100644 --- a/domain-restaurant/src/test/java/com/wellmeet/DataBaseCleaner.java +++ b/domain-restaurant/src/test/java/com/wellmeet/DataBaseCleaner.java @@ -1,7 +1,10 @@ package com.wellmeet; import jakarta.persistence.EntityManager; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; +import javax.sql.DataSource; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.context.ApplicationContext; @@ -10,12 +13,28 @@ public class DataBaseCleaner implements BeforeEachCallback { + private String databaseName; + @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { ApplicationContext context = SpringExtension.getApplicationContext(extensionContext); + + if (databaseName == null) { + extractDatabaseName(context); + } + cleanup(context); } + private void extractDatabaseName(ApplicationContext context) { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection conn = dataSource.getConnection()) { + databaseName = conn.getCatalog(); + } catch (SQLException e) { + throw new RuntimeException("Failed to extract database name", e); + } + } + private void cleanup(ApplicationContext context) { EntityManager em = context.getBean(EntityManager.class); TransactionTemplate transactionTemplate = context.getBean(TransactionTemplate.class); @@ -37,14 +56,14 @@ private void truncateTables(EntityManager em) { @SuppressWarnings("unchecked") private List findTableNames(EntityManager em) { - String tableNameSelectQuery = """ + String tableNameSelectQuery = String.format(""" SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'test' + WHERE TABLE_SCHEMA = '%s' AND TABLE_TYPE = 'BASE TABLE' - """; + """, databaseName); return em.createNativeQuery(tableNameSelectQuery) .getResultList(); } -} +} \ No newline at end of file diff --git a/domain-restaurant/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java b/domain-restaurant/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java new file mode 100644 index 0000000..3cefd15 --- /dev/null +++ b/domain-restaurant/src/test/java/com/wellmeet/domain/FlywaySchemaValidationTest.java @@ -0,0 +1,15 @@ +package com.wellmeet.domain; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"test", "flyway"}) +class FlywaySchemaValidationTest { + + @Test + void Flyway_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๊ณผ_Entity๊ฐ€_์ผ์น˜ํ•ด์•ผ_ํ•œ๋‹ค() { + + } +} diff --git a/infra-kafka/docker/docker-compose.yml b/infra-kafka/docker/docker-compose.yml deleted file mode 100644 index 73e611c..0000000 --- a/infra-kafka/docker/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: '3.8' -services: - zookeeper: - image: confluentinc/cp-zookeeper:7.0.1 - hostname: zookeeper - container_name: zookeeper - ports: - - "2181:2181" - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - - kafka: - image: confluentinc/cp-kafka:7.0.1 - hostname: kafka - container_name: kafka - depends_on: - - zookeeper - ports: - - "9092:9092" - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092 - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 diff --git a/settings.gradle b/settings.gradle index bcb821e..e657779 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'WellMeet-Backend' +include 'common-client' include 'domain-common' include 'domain-reservation' include 'domain-member'