From 17a68d3cdb889ab48bd2559a287ee73603e3d3c4 Mon Sep 17 00:00:00 2001 From: youbeen Date: Fri, 13 Jun 2025 16:40:46 +0900 Subject: [PATCH] store register add --- .github/workflows/member-ci.yml | 362 ++++---- .../biz/service/AnalyticsService.java | 804 +++++++++--------- .../infra/config/SecurityConfig.java | 102 +-- .../analytics/infra/config/SwaggerConfig.java | 90 +- analytics/src/main/resources/application.yml | 190 ++--- .../com/ktds/hi/common/config/CorsConfig.java | 202 ++--- .../src/main/resources/application-common.yml | 186 ++-- .../ktds/hi/member/config/SecurityConfig.java | 242 +++--- .../ktds/hi/member/config/SwaggerConfig.java | 78 +- member/src/main/resources/application.yml | 112 +-- .../infra/config/SecurityConfig.java | 104 +-- .../recommend/infra/config/SwaggerConfig.java | 94 +- recommend/src/main/resources/application.yml | 452 +++++----- .../review/biz/service/ReviewInteractor.java | 286 +++---- .../review/infra/config/SecurityConfig.java | 100 +-- .../hi/review/infra/config/SwaggerConfig.java | 66 +- review/src/main/resources/application.yml | 84 +- .../hi/store/biz/service/StoreService.java | 346 +++++++- .../hi/store/biz/usecase/out/CachePort.java | 50 +- .../hi/store/biz/usecase/out/Coordinates.java | 20 +- .../store/biz/usecase/out/GeocodingPort.java | 21 +- .../usecase/out/StoreTagRepositoryPort.java | 28 +- .../ktds/hi/store/config/SecurityConfig.java | 100 +-- .../java/com/ktds/hi/store/domain/Menu.java | 175 +--- .../java/com/ktds/hi/store/domain/Store.java | 177 ++-- .../com/ktds/hi/store/domain/StoreStatus.java | 53 +- .../infra/controller/StoreController.java | 124 ++- .../hi/store/infra/dto/MenuCreateRequest.java | 58 +- .../ktds/hi/store/infra/dto/MenuResponse.java | 59 +- .../store/infra/dto/MyStoreListResponse.java | 46 +- .../store/infra/dto/StoreCreateRequest.java | 52 +- .../store/infra/dto/StoreCreateResponse.java | 31 +- .../store/infra/dto/StoreDeleteResponse.java | 43 +- .../store/infra/dto/StoreDetailResponse.java | 69 +- .../store/infra/dto/StoreSearchResponse.java | 43 +- .../store/infra/dto/StoreUpdateRequest.java | 45 +- .../store/infra/dto/StoreUpdateResponse.java | 43 +- store/src/main/resources/application.yml | 96 +-- 38 files changed, 2906 insertions(+), 2327 deletions(-) diff --git a/.github/workflows/member-ci.yml b/.github/workflows/member-ci.yml index e313066..ec8a2e0 100644 --- a/.github/workflows/member-ci.yml +++ b/.github/workflows/member-ci.yml @@ -1,182 +1,182 @@ -name: Member CI - -on: - push: - branches: [ main, develop ] - paths: - - 'member/**' - - 'common/**' - - 'build.gradle' - - 'settings.gradle' - pull_request: - branches: [ main ] - paths: - - 'member/**' - - 'common/**' - - 'build.gradle' - - 'settings.gradle' - workflow_dispatch: - -env: - ACR_NAME: acrdigitalgarage03 - IMAGE_NAME: hiorder/member - MANIFEST_REPO: dg04-hi/hi-manifest - MANIFEST_FILE_PATH: member/deployment.yml - - -jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - with: - gradle-version: '8.13' - - - name: Cache Gradle packages - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Generate Gradle Wrapper - run: | - echo "Generating gradle wrapper..." - gradle wrapper --gradle-version 8.13 - chmod +x gradlew - echo "Testing gradle wrapper..." - ./gradlew --version - - - name: Build analytics module with dependencies - run: ./gradlew member:build -x test - - - name: Run analytics tests - run: ./gradlew member:test - - - name: Generate build timestamp - id: timestamp - run: echo "BUILD_TIME=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Azure Container Registry - uses: azure/docker-login@v1 - with: - login-server: ${{ env.ACR_NAME }}.azurecr.io - username: ${{ secrets.ACR_USERNAME }} - password: ${{ secrets.ACR_PASSWORD }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./member/Dockerfile - platforms: linux/amd64 - push: true - tags: | - ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.timestamp.outputs.BUILD_TIME }} - ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:latest - - - name: Output image tags - run: | - echo "๐ŸŽ‰ Image pushed successfully!" - echo "๐Ÿ“ฆ Image: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}" - echo "๐Ÿท๏ธ Tags: ${{ steps.timestamp.outputs.BUILD_TIME }}, latest" - - # ๐Ÿš€ Manifest ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์—…๋ฐ์ดํŠธ ๋‹จ๊ณ„ ์ถ”๊ฐ€ - - name: Checkout manifest repository - uses: actions/checkout@v4 - with: - repository: ${{ env.MANIFEST_REPO }} - token: ${{ secrets.MANIFEST_REPO_TOKEN }} - path: manifest-repo - - - name: Install yq - run: | - sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 - sudo chmod +x /usr/local/bin/yq - - - name: Update deployment image tag - run: | - cd manifest-repo - NEW_IMAGE="${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.timestamp.outputs.BUILD_TIME }}" - echo "Updating image tag to: $NEW_IMAGE" - - # deployment.yml์—์„œ ์ด๋ฏธ์ง€ ํƒœ๊ทธ ์—…๋ฐ์ดํŠธ - yq eval '.spec.template.spec.containers[0].image = "'$NEW_IMAGE'"' -i ${{ env.MANIFEST_FILE_PATH }} - - # ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ - echo "Updated deployment.yml:" - cat ${{ env.MANIFEST_FILE_PATH }} - - - name: Commit and push changes - run: | - cd manifest-repo - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - git add ${{ env.MANIFEST_FILE_PATH }} - - if git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -m "๐Ÿš€ Update analytics image tag to ${{ steps.timestamp.outputs.BUILD_TIME }} - - - Updated by: ${{ github.actor }} - - Triggered by: ${{ github.event_name }} - - Source commit: ${{ github.sha }} - - Build time: ${{ steps.timestamp.outputs.BUILD_TIME }}" - - git push - echo "โœ… Successfully updated manifest repository" - fi - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: always() - with: - name: member-test-results - path: member/build/reports/tests/test/ - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - if: success() - with: - name: member-jar - path: member/build/libs/*.jar - - # ๐ŸŽฏ ๋ฐฐํฌ ์™„๋ฃŒ ์•Œ๋ฆผ - - name: Deployment summary - if: success() - run: | - echo "## ๐Ÿš€ Analytics Service Deployment Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿ“ฆ Container Image" >> $GITHUB_STEP_SUMMARY - echo "- **Registry**: ${{ env.ACR_NAME }}.azurecr.io" >> $GITHUB_STEP_SUMMARY - echo "- **Image**: ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY - echo "- **Tag**: ${{ steps.timestamp.outputs.BUILD_TIME }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿ”„ ArgoCD Sync" >> $GITHUB_STEP_SUMMARY - echo "- **Manifest Repo**: https://github.com/${{ env.MANIFEST_REPO }}" >> $GITHUB_STEP_SUMMARY - echo "- **Updated File**: ${{ env.MANIFEST_FILE_PATH }}" >> $GITHUB_STEP_SUMMARY - echo "- **ArgoCD will automatically sync the new image**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โฑ๏ธ Build Info" >> $GITHUB_STEP_SUMMARY - echo "- **Build Time**: $(date)" >> $GITHUB_STEP_SUMMARY - echo "- **Triggered By**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY +name: Member CI + +on: + push: + branches: [ main, develop ] + paths: + - 'member/**' + - 'common/**' + - 'build.gradle' + - 'settings.gradle' + pull_request: + branches: [ main ] + paths: + - 'member/**' + - 'common/**' + - 'build.gradle' + - 'settings.gradle' + workflow_dispatch: + +env: + ACR_NAME: acrdigitalgarage03 + IMAGE_NAME: hiorder/member + MANIFEST_REPO: dg04-hi/hi-manifest + MANIFEST_FILE_PATH: member/deployment.yml + + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: '8.13' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Generate Gradle Wrapper + run: | + echo "Generating gradle wrapper..." + gradle wrapper --gradle-version 8.13 + chmod +x gradlew + echo "Testing gradle wrapper..." + ./gradlew --version + + - name: Build analytics module with dependencies + run: ./gradlew member:build -x test + + - name: Run analytics tests + run: ./gradlew member:test + + - name: Generate build timestamp + id: timestamp + run: echo "BUILD_TIME=$(date +'%y%m%d%H%M')" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Azure Container Registry + uses: azure/docker-login@v1 + with: + login-server: ${{ env.ACR_NAME }}.azurecr.io + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./member/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.timestamp.outputs.BUILD_TIME }} + ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:latest + + - name: Output image tags + run: | + echo "๐ŸŽ‰ Image pushed successfully!" + echo "๐Ÿ“ฆ Image: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}" + echo "๐Ÿท๏ธ Tags: ${{ steps.timestamp.outputs.BUILD_TIME }}, latest" + + # ๐Ÿš€ Manifest ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์—…๋ฐ์ดํŠธ ๋‹จ๊ณ„ ์ถ”๊ฐ€ + - name: Checkout manifest repository + uses: actions/checkout@v4 + with: + repository: ${{ env.MANIFEST_REPO }} + token: ${{ secrets.MANIFEST_REPO_TOKEN }} + path: manifest-repo + + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Update deployment image tag + run: | + cd manifest-repo + NEW_IMAGE="${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.timestamp.outputs.BUILD_TIME }}" + echo "Updating image tag to: $NEW_IMAGE" + + # deployment.yml์—์„œ ์ด๋ฏธ์ง€ ํƒœ๊ทธ ์—…๋ฐ์ดํŠธ + yq eval '.spec.template.spec.containers[0].image = "'$NEW_IMAGE'"' -i ${{ env.MANIFEST_FILE_PATH }} + + # ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ + echo "Updated deployment.yml:" + cat ${{ env.MANIFEST_FILE_PATH }} + + - name: Commit and push changes + run: | + cd manifest-repo + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + git add ${{ env.MANIFEST_FILE_PATH }} + + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "๐Ÿš€ Update analytics image tag to ${{ steps.timestamp.outputs.BUILD_TIME }} + + - Updated by: ${{ github.actor }} + - Triggered by: ${{ github.event_name }} + - Source commit: ${{ github.sha }} + - Build time: ${{ steps.timestamp.outputs.BUILD_TIME }}" + + git push + echo "โœ… Successfully updated manifest repository" + fi + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: member-test-results + path: member/build/reports/tests/test/ + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + if: success() + with: + name: member-jar + path: member/build/libs/*.jar + + # ๐ŸŽฏ ๋ฐฐํฌ ์™„๋ฃŒ ์•Œ๋ฆผ + - name: Deployment summary + if: success() + run: | + echo "## ๐Ÿš€ Analytics Service Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ“ฆ Container Image" >> $GITHUB_STEP_SUMMARY + echo "- **Registry**: ${{ env.ACR_NAME }}.azurecr.io" >> $GITHUB_STEP_SUMMARY + echo "- **Image**: ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY + echo "- **Tag**: ${{ steps.timestamp.outputs.BUILD_TIME }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”„ ArgoCD Sync" >> $GITHUB_STEP_SUMMARY + echo "- **Manifest Repo**: https://github.com/${{ env.MANIFEST_REPO }}" >> $GITHUB_STEP_SUMMARY + echo "- **Updated File**: ${{ env.MANIFEST_FILE_PATH }}" >> $GITHUB_STEP_SUMMARY + echo "- **ArgoCD will automatically sync the new image**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โฑ๏ธ Build Info" >> $GITHUB_STEP_SUMMARY + echo "- **Build Time**: $(date)" >> $GITHUB_STEP_SUMMARY + echo "- **Triggered By**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY echo "- **Event**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java index 2a36644..55043df 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/biz/service/AnalyticsService.java @@ -1,402 +1,402 @@ -package com.ktds.hi.analytics.biz.service; - -import com.ktds.hi.analytics.biz.domain.Analytics; -import com.ktds.hi.analytics.biz.domain.AiFeedback; -import com.ktds.hi.analytics.biz.usecase.in.AnalyticsUseCase; -import com.ktds.hi.analytics.biz.usecase.out.*; -import com.ktds.hi.analytics.infra.dto.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -/** - * ๋ถ„์„ ์„œ๋น„์Šค ๊ตฌํ˜„ ํด๋ž˜์Šค (์ˆ˜์ •๋ฒ„์ „) - * Clean Architecture์˜ UseCase๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌ - */ -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AnalyticsService implements AnalyticsUseCase { - - private final AnalyticsPort analyticsPort; - private final AIServicePort aiServicePort; - private final ExternalReviewPort externalReviewPort; - private final OrderDataPort orderDataPort; - private final CachePort cachePort; - private final EventPort eventPort; - - @Override - @Cacheable(value = "storeAnalytics", key = "#storeId") - public StoreAnalyticsResponse getStoreAnalytics(Long storeId) { - log.info("๋งค์žฅ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); - - try { - // 1. ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ - String cacheKey = "analytics:store:" + storeId; - var cachedResult = cachePort.getAnalyticsCache(cacheKey); - if (cachedResult.isPresent()) { - log.info("์บ์‹œ์—์„œ ๋ถ„์„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜: storeId={}", storeId); - return (StoreAnalyticsResponse) cachedResult.get(); - } - - // 2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ธฐ์กด ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ - var analytics = analyticsPort.findAnalyticsByStoreId(storeId); - - if (analytics.isEmpty()) { - // 3. ๋ถ„์„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ - analytics = Optional.of(generateNewAnalytics(storeId)); - } - - // 4. ์‘๋‹ต ์ƒ์„ฑ - StoreAnalyticsResponse response = StoreAnalyticsResponse.builder() - .storeId(storeId) - .totalReviews(analytics.get().getTotalReviews()) - .averageRating(analytics.get().getAverageRating()) - .sentimentScore(analytics.get().getSentimentScore()) - .positiveReviewRate(analytics.get().getPositiveReviewRate()) - .negativeReviewRate(analytics.get().getNegativeReviewRate()) - .lastAnalysisDate(analytics.get().getLastAnalysisDate()) - .build(); - - // 5. ์บ์‹œ์— ์ €์žฅ - cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(1)); - - log.info("๋งค์žฅ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); - return response; - - } catch (Exception e) { - log.error("๋งค์žฅ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); - throw new RuntimeException("๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); - } - } - - // ... ๋‚˜๋จธ์ง€ ๋ฉ”์„œ๋“œ๋“ค์€ ์ด์ „๊ณผ ๋™์ผ ... - - @Override - @Cacheable(value = "aiFeedback", key = "#storeId") - public AiFeedbackDetailResponse getAIFeedbackDetail(Long storeId) { - log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ธ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); - - try { - // 1. ๊ธฐ์กด AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ - var aiFeedback = analyticsPort.findAIFeedbackByStoreId(storeId); - - if (aiFeedback.isEmpty()) { - // 2. AI ํ”ผ๋“œ๋ฐฑ์ด ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ - aiFeedback = Optional.of(generateAIFeedback(storeId)); - } - - // 3. ์‘๋‹ต ์ƒ์„ฑ - AiFeedbackDetailResponse response = AiFeedbackDetailResponse.builder() - .storeId(storeId) - .summary(aiFeedback.get().getSummary()) - .positivePoints(aiFeedback.get().getPositivePoints()) - .improvementPoints(aiFeedback.get().getImprovementPoints()) - .recommendations(aiFeedback.get().getRecommendations()) - .sentimentAnalysis(aiFeedback.get().getSentimentAnalysis()) - .confidenceScore(aiFeedback.get().getConfidenceScore()) - .generatedAt(aiFeedback.get().getGeneratedAt()) - .build(); - - log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ธ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); - return response; - - } catch (Exception e) { - log.error("AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); - throw new RuntimeException("AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); - } - } - - // ๋‚˜๋จธ์ง€ ๋ฉ”์„œ๋“œ๋“ค๊ณผ private ๋ฉ”์„œ๋“œ๋“ค์€ ์ด์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ๊ตฌํ˜„ - // ... (getStoreStatistics, getAIFeedbackSummary, getReviewAnalysis ๋“ฑ) - - @Override - public StoreStatisticsResponse getStoreStatistics(Long storeId, LocalDate startDate, LocalDate endDate) { - log.info("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ ์‹œ์ž‘: storeId={}, startDate={}, endDate={}", storeId, startDate, endDate); - - try { - // 1. ์บ์‹œ ํ‚ค ์ƒ์„ฑ - String cacheKey = String.format("statistics:store:%d:%s:%s", storeId, startDate, endDate); - var cachedResult = cachePort.getAnalyticsCache(cacheKey); - if (cachedResult.isPresent()) { - log.info("์บ์‹œ์—์„œ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜: storeId={}", storeId); - return (StoreStatisticsResponse) cachedResult.get(); - } - - // 2. ์ฃผ๋ฌธ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ (์‹ค์ œ OrderStatistics ๋„๋ฉ”์ธ ํ•„๋“œ ์‚ฌ์šฉ) - var orderStatistics = orderDataPort.getOrderStatistics(storeId, startDate, endDate); - - // 3. ์‘๋‹ต ์ƒ์„ฑ - StoreStatisticsResponse response = StoreStatisticsResponse.builder() - .storeId(storeId) - .startDate(startDate) - .endDate(endDate) - .totalOrders(orderStatistics.getTotalOrders()) - .totalRevenue(orderStatistics.getTotalRevenue()) - .averageOrderValue(orderStatistics.getAverageOrderValue()) - .peakHour(orderStatistics.getPeakHour()) - .popularMenus(orderStatistics.getPopularMenus()) - .customerAgeDistribution(orderStatistics.getCustomerAgeDistribution()) - .build(); - - // 4. ์บ์‹œ์— ์ €์žฅ - cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofMinutes(30)); - - log.info("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); - return response; - - } catch (Exception e) { - log.error("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); - throw new RuntimeException("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); - } - } - - @Override - public AiFeedbackSummaryResponse getAIFeedbackSummary(Long storeId) { - log.info("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); - - try { - // 1. ์บ์‹œ์—์„œ ํ™•์ธ - String cacheKey = "ai_feedback_summary:store:" + storeId; - var cachedResult = cachePort.getAnalyticsCache(cacheKey); - if (cachedResult.isPresent()) { - return (AiFeedbackSummaryResponse) cachedResult.get(); - } - - // 2. AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ - var aiFeedback = analyticsPort.findAIFeedbackByStoreId(storeId); - - if (aiFeedback.isEmpty()) { - // 3. ํ”ผ๋“œ๋ฐฑ์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ - AiFeedbackSummaryResponse emptyResponse = AiFeedbackSummaryResponse.builder() - .storeId(storeId) - .hasData(false) - .message("๋ถ„์„ํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.") - .lastUpdated(LocalDateTime.now()) - .build(); - - cachePort.putAnalyticsCache(cacheKey, emptyResponse, java.time.Duration.ofHours(1)); - return emptyResponse; - } - - // 4. ์‘๋‹ต ์ƒ์„ฑ - AiFeedbackSummaryResponse response = AiFeedbackSummaryResponse.builder() - .storeId(storeId) - .hasData(true) - .message("AI ๋ถ„์„์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") - .overallScore(aiFeedback.get().getConfidenceScore()) - .keyInsight(aiFeedback.get().getSummary()) - .priorityRecommendation(getFirstRecommendation(aiFeedback.get())) - .lastUpdated(aiFeedback.get().getUpdatedAt()) - .build(); - - // 5. ์บ์‹œ์— ์ €์žฅ - cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(2)); - - log.info("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); - return response; - - } catch (Exception e) { - log.error("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); - throw new RuntimeException("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); - } - } - - @Override - public ReviewAnalysisResponse getReviewAnalysis(Long storeId) { - log.info("๋ฆฌ๋ทฐ ๋ถ„์„ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); - - try { - // 1. ์บ์‹œ์—์„œ ํ™•์ธ - String cacheKey = "review_analysis:store:" + storeId; - var cachedResult = cachePort.getAnalyticsCache(cacheKey); - if (cachedResult.isPresent()) { - return (ReviewAnalysisResponse) cachedResult.get(); - } - - // 2. ์ตœ๊ทผ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์กฐํšŒ (30์ผ) - List recentReviews = externalReviewPort.getRecentReviews(storeId, 30); - - if (recentReviews.isEmpty()) { - ReviewAnalysisResponse emptyResponse = ReviewAnalysisResponse.builder() - .storeId(storeId) - .totalReviews(0) - .positiveReviewCount(0) - .negativeReviewCount(0) - .positiveRate(0.0) - .negativeRate(0.0) - .analysisDate(LocalDate.now()) - .build(); - - cachePort.putAnalyticsCache(cacheKey, emptyResponse, java.time.Duration.ofHours(1)); - return emptyResponse; - } - - // 3. ์‘๋‹ต ์ƒ์„ฑ - int positiveCount = countPositiveReviews(recentReviews); - int negativeCount = countNegativeReviews(recentReviews); - int totalCount = recentReviews.size(); - - ReviewAnalysisResponse response = ReviewAnalysisResponse.builder() - .storeId(storeId) - .totalReviews(totalCount) - .positiveReviewCount(positiveCount) - .negativeReviewCount(negativeCount) - .positiveRate((double) positiveCount / totalCount * 100) - .negativeRate((double) negativeCount / totalCount * 100) - .analysisDate(LocalDate.now()) - .build(); - - // 4. ์บ์‹œ์— ์ €์žฅ - cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(4)); - - log.info("๋ฆฌ๋ทฐ ๋ถ„์„ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); - return response; - - } catch (Exception e) { - log.error("๋ฆฌ๋ทฐ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); - throw new RuntimeException("๋ฆฌ๋ทฐ ๋ถ„์„์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); - } - } - - // private ๋ฉ”์„œ๋“œ๋“ค - @Transactional - public Analytics generateNewAnalytics(Long storeId) { - log.info("์ƒˆ๋กœ์šด ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹œ์ž‘: storeId={}", storeId); - - try { - // 1. ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - List reviewData = externalReviewPort.getReviewData(storeId); - int totalReviews = reviewData.size(); - - if (totalReviews == 0) { - log.warn("๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ: storeId={}", storeId); - return createDefaultAnalytics(storeId); - } - - // 2. ๊ธฐ๋ณธ ํ†ต๊ณ„ ๊ณ„์‚ฐ - double averageRating = 4.0; // ๊ธฐ๋ณธ๊ฐ’ - double sentimentScore = 0.5; // ์ค‘๋ฆฝ - double positiveRate = 60.0; - double negativeRate = 20.0; - - // 3. Analytics ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ - Analytics analytics = Analytics.builder() - .storeId(storeId) - .totalReviews(totalReviews) - .averageRating(averageRating) - .sentimentScore(sentimentScore) - .positiveReviewRate(positiveRate) - .negativeReviewRate(negativeRate) - .lastAnalysisDate(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - // 4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ - Analytics saved = analyticsPort.saveAnalytics(analytics); - - log.info("์ƒˆ๋กœ์šด ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ: storeId={}", storeId); - return saved; - - } catch (Exception e) { - log.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); - return createDefaultAnalytics(storeId); - } - } - - @Transactional - public AiFeedback generateAIFeedback(Long storeId) { - log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์‹œ์ž‘: storeId={}", storeId); - - try { - // 1. ์ตœ๊ทผ 30์ผ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - List reviewData = externalReviewPort.getRecentReviews(storeId, 30); - - if (reviewData.isEmpty()) { - log.warn("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: storeId={}", storeId); - return createDefaultAIFeedback(storeId); - } - - // 2. AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ (์‹ค์ œ๋กœ๋Š” AI ์„œ๋น„์Šค ํ˜ธ์ถœ) - AiFeedback aiFeedback = AiFeedback.builder() - .storeId(storeId) - .summary("๊ณ ๊ฐ๋“ค์˜ ์ „๋ฐ˜์ ์ธ ๋งŒ์กฑ๋„๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค.") - .positivePoints(List.of("๋ง›์ด ์ข‹๋‹ค", "์„œ๋น„์Šค๊ฐ€ ์นœ์ ˆํ•˜๋‹ค", "๋ถ„์œ„๊ธฐ๊ฐ€ ์ข‹๋‹ค")) - .improvementPoints(List.of("๋Œ€๊ธฐ์‹œ๊ฐ„ ๋‹จ์ถ•", "๊ฐ€๊ฒฉ ๊ฒฝ์Ÿ๋ ฅ", "๋ฉ”๋‰ด ๋‹ค์–‘์„ฑ")) - .recommendations(List.of("ํŠน๋ณ„ ๋ฉ”๋‰ด ๊ฐœ๋ฐœ", "์˜ˆ์•ฝ ์‹œ์Šคํ…œ ๋„์ž…", "๊ณ ๊ฐ ์„œ๋น„์Šค ๊ต์œก")) - .sentimentAnalysis("POSITIVE") - .confidenceScore(0.85) - .generatedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - // 3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ - AiFeedback saved = analyticsPort.saveAIFeedback(aiFeedback); - - log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์™„๋ฃŒ: storeId={}", storeId); - return saved; - - } catch (Exception e) { - log.error("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); - return createDefaultAIFeedback(storeId); - } - - - } - - private Analytics createDefaultAnalytics(Long storeId) { - return Analytics.builder() - .storeId(storeId) - .totalReviews(0) - .averageRating(0.0) - .sentimentScore(0.0) - .positiveReviewRate(0.0) - .negativeReviewRate(0.0) - .lastAnalysisDate(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } - - private AiFeedback createDefaultAIFeedback(Long storeId) { - return AiFeedback.builder() - .storeId(storeId) - .summary("๋ถ„์„ํ•  ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.") - .positivePoints(List.of("๋ฐ์ดํ„ฐ ๋ถ€์กฑ์œผ๋กœ ๋ถ„์„ ๋ถˆ๊ฐ€")) - .improvementPoints(List.of("๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ•„์š”")) - .recommendations(List.of("๊ณ ๊ฐ๋“ค์˜ ๋ฆฌ๋ทฐ ์ž‘์„ฑ์„ ์œ ๋„ํ•ด๋ณด์„ธ์š”")) - .sentimentAnalysis("NEUTRAL") - .confidenceScore(0.0) - .generatedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } - - private String getFirstRecommendation(AiFeedback feedback) { - if (feedback.getRecommendations() != null && !feedback.getRecommendations().isEmpty()) { - return feedback.getRecommendations().get(0); - } - return "์ถ”์ฒœ์‚ฌํ•ญ์ด ์—†์Šต๋‹ˆ๋‹ค."; - } - - private int countPositiveReviews(List reviews) { - // ์‹ค์ œ๋กœ๋Š” AI ์„œ๋น„์Šค๋ฅผ ํ†ตํ•œ ๊ฐ์ • ๋ถ„์„ ํ•„์š” - return (int) (reviews.size() * 0.6); // 60% ๊ฐ€์ • - } - - private int countNegativeReviews(List reviews) { - // ์‹ค์ œ๋กœ๋Š” AI ์„œ๋น„์Šค๋ฅผ ํ†ตํ•œ ๊ฐ์ • ๋ถ„์„ ํ•„์š” - return (int) (reviews.size() * 0.2); // 20% ๊ฐ€์ • - } -} +package com.ktds.hi.analytics.biz.service; + +import com.ktds.hi.analytics.biz.domain.Analytics; +import com.ktds.hi.analytics.biz.domain.AiFeedback; +import com.ktds.hi.analytics.biz.usecase.in.AnalyticsUseCase; +import com.ktds.hi.analytics.biz.usecase.out.*; +import com.ktds.hi.analytics.infra.dto.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * ๋ถ„์„ ์„œ๋น„์Šค ๊ตฌํ˜„ ํด๋ž˜์Šค (์ˆ˜์ •๋ฒ„์ „) + * Clean Architecture์˜ UseCase๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌ + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AnalyticsService implements AnalyticsUseCase { + + private final AnalyticsPort analyticsPort; + private final AIServicePort aiServicePort; + private final ExternalReviewPort externalReviewPort; + private final OrderDataPort orderDataPort; + private final CachePort cachePort; + private final EventPort eventPort; + + @Override + @Cacheable(value = "storeAnalytics", key = "#storeId") + public StoreAnalyticsResponse getStoreAnalytics(Long storeId) { + log.info("๋งค์žฅ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); + + try { + // 1. ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ + String cacheKey = "analytics:store:" + storeId; + var cachedResult = cachePort.getAnalyticsCache(cacheKey); + if (cachedResult.isPresent()) { + log.info("์บ์‹œ์—์„œ ๋ถ„์„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜: storeId={}", storeId); + return (StoreAnalyticsResponse) cachedResult.get(); + } + + // 2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ธฐ์กด ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ + var analytics = analyticsPort.findAnalyticsByStoreId(storeId); + + if (analytics.isEmpty()) { + // 3. ๋ถ„์„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ + analytics = Optional.of(generateNewAnalytics(storeId)); + } + + // 4. ์‘๋‹ต ์ƒ์„ฑ + StoreAnalyticsResponse response = StoreAnalyticsResponse.builder() + .storeId(storeId) + .totalReviews(analytics.get().getTotalReviews()) + .averageRating(analytics.get().getAverageRating()) + .sentimentScore(analytics.get().getSentimentScore()) + .positiveReviewRate(analytics.get().getPositiveReviewRate()) + .negativeReviewRate(analytics.get().getNegativeReviewRate()) + .lastAnalysisDate(analytics.get().getLastAnalysisDate()) + .build(); + + // 5. ์บ์‹œ์— ์ €์žฅ + cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(1)); + + log.info("๋งค์žฅ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); + return response; + + } catch (Exception e) { + log.error("๋งค์žฅ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); + throw new RuntimeException("๋ถ„์„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + } + } + + // ... ๋‚˜๋จธ์ง€ ๋ฉ”์„œ๋“œ๋“ค์€ ์ด์ „๊ณผ ๋™์ผ ... + + @Override + @Cacheable(value = "aiFeedback", key = "#storeId") + public AiFeedbackDetailResponse getAIFeedbackDetail(Long storeId) { + log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ธ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); + + try { + // 1. ๊ธฐ์กด AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ + var aiFeedback = analyticsPort.findAIFeedbackByStoreId(storeId); + + if (aiFeedback.isEmpty()) { + // 2. AI ํ”ผ๋“œ๋ฐฑ์ด ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ + aiFeedback = Optional.of(generateAIFeedback(storeId)); + } + + // 3. ์‘๋‹ต ์ƒ์„ฑ + AiFeedbackDetailResponse response = AiFeedbackDetailResponse.builder() + .storeId(storeId) + .summary(aiFeedback.get().getSummary()) + .positivePoints(aiFeedback.get().getPositivePoints()) + .improvementPoints(aiFeedback.get().getImprovementPoints()) + .recommendations(aiFeedback.get().getRecommendations()) + .sentimentAnalysis(aiFeedback.get().getSentimentAnalysis()) + .confidenceScore(aiFeedback.get().getConfidenceScore()) + .generatedAt(aiFeedback.get().getGeneratedAt()) + .build(); + + log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ธ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); + return response; + + } catch (Exception e) { + log.error("AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); + throw new RuntimeException("AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + } + } + + // ๋‚˜๋จธ์ง€ ๋ฉ”์„œ๋“œ๋“ค๊ณผ private ๋ฉ”์„œ๋“œ๋“ค์€ ์ด์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ๊ตฌํ˜„ + // ... (getStoreStatistics, getAIFeedbackSummary, getReviewAnalysis ๋“ฑ) + + @Override + public StoreStatisticsResponse getStoreStatistics(Long storeId, LocalDate startDate, LocalDate endDate) { + log.info("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ ์‹œ์ž‘: storeId={}, startDate={}, endDate={}", storeId, startDate, endDate); + + try { + // 1. ์บ์‹œ ํ‚ค ์ƒ์„ฑ + String cacheKey = String.format("statistics:store:%d:%s:%s", storeId, startDate, endDate); + var cachedResult = cachePort.getAnalyticsCache(cacheKey); + if (cachedResult.isPresent()) { + log.info("์บ์‹œ์—์„œ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜: storeId={}", storeId); + return (StoreStatisticsResponse) cachedResult.get(); + } + + // 2. ์ฃผ๋ฌธ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ (์‹ค์ œ OrderStatistics ๋„๋ฉ”์ธ ํ•„๋“œ ์‚ฌ์šฉ) + var orderStatistics = orderDataPort.getOrderStatistics(storeId, startDate, endDate); + + // 3. ์‘๋‹ต ์ƒ์„ฑ + StoreStatisticsResponse response = StoreStatisticsResponse.builder() + .storeId(storeId) + .startDate(startDate) + .endDate(endDate) + .totalOrders(orderStatistics.getTotalOrders()) + .totalRevenue(orderStatistics.getTotalRevenue()) + .averageOrderValue(orderStatistics.getAverageOrderValue()) + .peakHour(orderStatistics.getPeakHour()) + .popularMenus(orderStatistics.getPopularMenus()) + .customerAgeDistribution(orderStatistics.getCustomerAgeDistribution()) + .build(); + + // 4. ์บ์‹œ์— ์ €์žฅ + cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofMinutes(30)); + + log.info("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); + return response; + + } catch (Exception e) { + log.error("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); + throw new RuntimeException("๋งค์žฅ ํ†ต๊ณ„ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + } + } + + @Override + public AiFeedbackSummaryResponse getAIFeedbackSummary(Long storeId) { + log.info("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); + + try { + // 1. ์บ์‹œ์—์„œ ํ™•์ธ + String cacheKey = "ai_feedback_summary:store:" + storeId; + var cachedResult = cachePort.getAnalyticsCache(cacheKey); + if (cachedResult.isPresent()) { + return (AiFeedbackSummaryResponse) cachedResult.get(); + } + + // 2. AI ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ + var aiFeedback = analyticsPort.findAIFeedbackByStoreId(storeId); + + if (aiFeedback.isEmpty()) { + // 3. ํ”ผ๋“œ๋ฐฑ์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ + AiFeedbackSummaryResponse emptyResponse = AiFeedbackSummaryResponse.builder() + .storeId(storeId) + .hasData(false) + .message("๋ถ„์„ํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.") + .lastUpdated(LocalDateTime.now()) + .build(); + + cachePort.putAnalyticsCache(cacheKey, emptyResponse, java.time.Duration.ofHours(1)); + return emptyResponse; + } + + // 4. ์‘๋‹ต ์ƒ์„ฑ + AiFeedbackSummaryResponse response = AiFeedbackSummaryResponse.builder() + .storeId(storeId) + .hasData(true) + .message("AI ๋ถ„์„์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + .overallScore(aiFeedback.get().getConfidenceScore()) + .keyInsight(aiFeedback.get().getSummary()) + .priorityRecommendation(getFirstRecommendation(aiFeedback.get())) + .lastUpdated(aiFeedback.get().getUpdatedAt()) + .build(); + + // 5. ์บ์‹œ์— ์ €์žฅ + cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(2)); + + log.info("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); + return response; + + } catch (Exception e) { + log.error("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); + throw new RuntimeException("AI ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + } + } + + @Override + public ReviewAnalysisResponse getReviewAnalysis(Long storeId) { + log.info("๋ฆฌ๋ทฐ ๋ถ„์„ ์กฐํšŒ ์‹œ์ž‘: storeId={}", storeId); + + try { + // 1. ์บ์‹œ์—์„œ ํ™•์ธ + String cacheKey = "review_analysis:store:" + storeId; + var cachedResult = cachePort.getAnalyticsCache(cacheKey); + if (cachedResult.isPresent()) { + return (ReviewAnalysisResponse) cachedResult.get(); + } + + // 2. ์ตœ๊ทผ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์กฐํšŒ (30์ผ) + List recentReviews = externalReviewPort.getRecentReviews(storeId, 30); + + if (recentReviews.isEmpty()) { + ReviewAnalysisResponse emptyResponse = ReviewAnalysisResponse.builder() + .storeId(storeId) + .totalReviews(0) + .positiveReviewCount(0) + .negativeReviewCount(0) + .positiveRate(0.0) + .negativeRate(0.0) + .analysisDate(LocalDate.now()) + .build(); + + cachePort.putAnalyticsCache(cacheKey, emptyResponse, java.time.Duration.ofHours(1)); + return emptyResponse; + } + + // 3. ์‘๋‹ต ์ƒ์„ฑ + int positiveCount = countPositiveReviews(recentReviews); + int negativeCount = countNegativeReviews(recentReviews); + int totalCount = recentReviews.size(); + + ReviewAnalysisResponse response = ReviewAnalysisResponse.builder() + .storeId(storeId) + .totalReviews(totalCount) + .positiveReviewCount(positiveCount) + .negativeReviewCount(negativeCount) + .positiveRate((double) positiveCount / totalCount * 100) + .negativeRate((double) negativeCount / totalCount * 100) + .analysisDate(LocalDate.now()) + .build(); + + // 4. ์บ์‹œ์— ์ €์žฅ + cachePort.putAnalyticsCache(cacheKey, response, java.time.Duration.ofHours(4)); + + log.info("๋ฆฌ๋ทฐ ๋ถ„์„ ์กฐํšŒ ์™„๋ฃŒ: storeId={}", storeId); + return response; + + } catch (Exception e) { + log.error("๋ฆฌ๋ทฐ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); + throw new RuntimeException("๋ฆฌ๋ทฐ ๋ถ„์„์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + } + } + + // private ๋ฉ”์„œ๋“œ๋“ค + @Transactional + public Analytics generateNewAnalytics(Long storeId) { + log.info("์ƒˆ๋กœ์šด ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹œ์ž‘: storeId={}", storeId); + + try { + // 1. ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + List reviewData = externalReviewPort.getReviewData(storeId); + int totalReviews = reviewData.size(); + + if (totalReviews == 0) { + log.warn("๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ: storeId={}", storeId); + return createDefaultAnalytics(storeId); + } + + // 2. ๊ธฐ๋ณธ ํ†ต๊ณ„ ๊ณ„์‚ฐ + double averageRating = 4.0; // ๊ธฐ๋ณธ๊ฐ’ + double sentimentScore = 0.5; // ์ค‘๋ฆฝ + double positiveRate = 60.0; + double negativeRate = 20.0; + + // 3. Analytics ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ + Analytics analytics = Analytics.builder() + .storeId(storeId) + .totalReviews(totalReviews) + .averageRating(averageRating) + .sentimentScore(sentimentScore) + .positiveReviewRate(positiveRate) + .negativeReviewRate(negativeRate) + .lastAnalysisDate(LocalDateTime.now()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + // 4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ + Analytics saved = analyticsPort.saveAnalytics(analytics); + + log.info("์ƒˆ๋กœ์šด ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ: storeId={}", storeId); + return saved; + + } catch (Exception e) { + log.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); + return createDefaultAnalytics(storeId); + } + } + + @Transactional + public AiFeedback generateAIFeedback(Long storeId) { + log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์‹œ์ž‘: storeId={}", storeId); + + try { + // 1. ์ตœ๊ทผ 30์ผ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + List reviewData = externalReviewPort.getRecentReviews(storeId, 30); + + if (reviewData.isEmpty()) { + log.warn("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: storeId={}", storeId); + return createDefaultAIFeedback(storeId); + } + + // 2. AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ (์‹ค์ œ๋กœ๋Š” AI ์„œ๋น„์Šค ํ˜ธ์ถœ) + AiFeedback aiFeedback = AiFeedback.builder() + .storeId(storeId) + .summary("๊ณ ๊ฐ๋“ค์˜ ์ „๋ฐ˜์ ์ธ ๋งŒ์กฑ๋„๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค.") + .positivePoints(List.of("๋ง›์ด ์ข‹๋‹ค", "์„œ๋น„์Šค๊ฐ€ ์นœ์ ˆํ•˜๋‹ค", "๋ถ„์œ„๊ธฐ๊ฐ€ ์ข‹๋‹ค")) + .improvementPoints(List.of("๋Œ€๊ธฐ์‹œ๊ฐ„ ๋‹จ์ถ•", "๊ฐ€๊ฒฉ ๊ฒฝ์Ÿ๋ ฅ", "๋ฉ”๋‰ด ๋‹ค์–‘์„ฑ")) + .recommendations(List.of("ํŠน๋ณ„ ๋ฉ”๋‰ด ๊ฐœ๋ฐœ", "์˜ˆ์•ฝ ์‹œ์Šคํ…œ ๋„์ž…", "๊ณ ๊ฐ ์„œ๋น„์Šค ๊ต์œก")) + .sentimentAnalysis("POSITIVE") + .confidenceScore(0.85) + .generatedAt(LocalDateTime.now()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + // 3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ + AiFeedback saved = analyticsPort.saveAIFeedback(aiFeedback); + + log.info("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์™„๋ฃŒ: storeId={}", storeId); + return saved; + + } catch (Exception e) { + log.error("AI ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: storeId={}", storeId, e); + return createDefaultAIFeedback(storeId); + } + + + } + + private Analytics createDefaultAnalytics(Long storeId) { + return Analytics.builder() + .storeId(storeId) + .totalReviews(0) + .averageRating(0.0) + .sentimentScore(0.0) + .positiveReviewRate(0.0) + .negativeReviewRate(0.0) + .lastAnalysisDate(LocalDateTime.now()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + private AiFeedback createDefaultAIFeedback(Long storeId) { + return AiFeedback.builder() + .storeId(storeId) + .summary("๋ถ„์„ํ•  ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.") + .positivePoints(List.of("๋ฐ์ดํ„ฐ ๋ถ€์กฑ์œผ๋กœ ๋ถ„์„ ๋ถˆ๊ฐ€")) + .improvementPoints(List.of("๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ•„์š”")) + .recommendations(List.of("๊ณ ๊ฐ๋“ค์˜ ๋ฆฌ๋ทฐ ์ž‘์„ฑ์„ ์œ ๋„ํ•ด๋ณด์„ธ์š”")) + .sentimentAnalysis("NEUTRAL") + .confidenceScore(0.0) + .generatedAt(LocalDateTime.now()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + private String getFirstRecommendation(AiFeedback feedback) { + if (feedback.getRecommendations() != null && !feedback.getRecommendations().isEmpty()) { + return feedback.getRecommendations().get(0); + } + return "์ถ”์ฒœ์‚ฌํ•ญ์ด ์—†์Šต๋‹ˆ๋‹ค."; + } + + private int countPositiveReviews(List reviews) { + // ์‹ค์ œ๋กœ๋Š” AI ์„œ๋น„์Šค๋ฅผ ํ†ตํ•œ ๊ฐ์ • ๋ถ„์„ ํ•„์š” + return (int) (reviews.size() * 0.6); // 60% ๊ฐ€์ • + } + + private int countNegativeReviews(List reviews) { + // ์‹ค์ œ๋กœ๋Š” AI ์„œ๋น„์Šค๋ฅผ ํ†ตํ•œ ๊ฐ์ • ๋ถ„์„ ํ•„์š” + return (int) (reviews.size() * 0.2); // 20% ๊ฐ€์ • + } +} diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java index 722388e..632111c 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SecurityConfig.java @@ -1,51 +1,51 @@ -package com.ktds.hi.analytics.infra.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfigurationSource; - -import lombok.RequiredArgsConstructor; - -/** - * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค - * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • - */ -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class SecurityConfig { - - - private final CorsConfigurationSource corsConfigurationSource; - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ - .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() - - // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .requestMatchers("/api/analytics/**").permitAll() - .requestMatchers("/api/action-plans/**").permitAll() - - // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ - .requestMatchers("/actuator/**").permitAll() - - // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .anyRequest().permitAll() - ); - - return http.build(); - } -} +package com.ktds.hi.analytics.infra.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfigurationSource; + +import lombok.RequiredArgsConstructor; + +/** + * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค + * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + + private final CorsConfigurationSource corsConfigurationSource; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ + .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() + + // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .requestMatchers("/api/analytics/**").permitAll() + .requestMatchers("/api/action-plans/**").permitAll() + + // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ + .requestMatchers("/actuator/**").permitAll() + + // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .anyRequest().permitAll() + ); + + return http.build(); + } +} diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java index e894a51..c3d6fa0 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/config/SwaggerConfig.java @@ -1,45 +1,45 @@ -package com.ktds.hi.analytics.infra.config; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Swagger ์„ค์ • ํด๋ž˜์Šค - * API ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ OpenAPI ์„ค์ • - */ -@Configuration -public class SwaggerConfig { - - @Bean - public OpenAPI openAPI() { - return new OpenAPI() - .info(new Info() - .title("Analytics Service API") - .description("ํ•˜์ด์˜ค๋” ๋ถ„์„ ์„œ๋น„์Šค API ๋ฌธ์„œ") - .version("1.0.0")); - } - /** - * JWT Bearer ํ† ํฐ์„ ์œ„ํ•œ Security Scheme ์ƒ์„ฑ - */ - private SecurityScheme createAPIKeyScheme() { - return new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .in(SecurityScheme.In.HEADER) - .name("Authorization") - .description(""" - JWT ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š” - - ์‚ฌ์šฉ๋ฒ•: - 1. ๋กœ๊ทธ์ธ API๋กœ ํ† ํฐ ๋ฐœ๊ธ‰ - 2. Bearer ์ ‘๋‘์‚ฌ ์—†์ด ํ† ํฐ๋งŒ ์ž…๋ ฅ - 3. ์˜ˆ: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOi... - """); - } -} +package com.ktds.hi.analytics.infra.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger ์„ค์ • ํด๋ž˜์Šค + * API ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ OpenAPI ์„ค์ • + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Analytics Service API") + .description("ํ•˜์ด์˜ค๋” ๋ถ„์„ ์„œ๋น„์Šค API ๋ฌธ์„œ") + .version("1.0.0")); + } + /** + * JWT Bearer ํ† ํฐ์„ ์œ„ํ•œ Security Scheme ์ƒ์„ฑ + */ + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name("Authorization") + .description(""" + JWT ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š” + + ์‚ฌ์šฉ๋ฒ•: + 1. ๋กœ๊ทธ์ธ API๋กœ ํ† ํฐ ๋ฐœ๊ธ‰ + 2. Bearer ์ ‘๋‘์‚ฌ ์—†์ด ํ† ํฐ๋งŒ ์ž…๋ ฅ + 3. ์˜ˆ: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOi... + """); + } +} diff --git a/analytics/src/main/resources/application.yml b/analytics/src/main/resources/application.yml index f9b1049..4414724 100644 --- a/analytics/src/main/resources/application.yml +++ b/analytics/src/main/resources/application.yml @@ -1,95 +1,95 @@ -server: - port: ${ANALYTICS_SERVICE_PORT:8084} - -logging: - level: - org.springframework.web.servlet.resource.ResourceHttpRequestHandler: ERROR - org.springframework.web.servlet.DispatcherServlet: WARN - -spring: - application: - name: analytics-service - - datasource: - url: ${ANALYTICS_DB_URL:jdbc:postgresql://20.249.162.125:5432/hiorder_analytics} - username: ${ANALYTICS_DB_USERNAME:hiorder_user} - password: ${ANALYTICS_DB_PASSWORD:hiorder_pass} - driver-class-name: org.postgresql.Driver - - jpa: - hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} - show-sql: ${JPA_SHOW_SQL:false} - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.PostgreSQLDialect - - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - -ai-api: - openai: - api-key: ${OPENAI_API_KEY:} - base-url: https://api.openai.com/v1 - model: gpt-4o-mini - claude: - api-key: ${CLAUDE_API_KEY:} - base-url: https://api.anthropic.com - model: claude-3-sonnet-20240229 - -#external-api: -# openai: -# api-key: ${OPENAI_API_KEY:} -# base-url: https://api.openai.com -# claude: -# api-key: ${CLAUDE_API_KEY:} -# base-url: https://api.anthropic.com - -# ์™ธ๋ถ€ ์„œ๋น„์Šค ์„ค์ • -external: - services: - review: ${EXTERNAL_SERVICES_REVIEW:http://localhost:8082} - store: ${EXTERNAL_SERVICES_STORE:http://localhost:8081} - member: ${EXTERNAL_SERVICES_MEMBER:http://localhost:8080} - -#springdoc: -# api-docs: -# path: /api-docs -# swagger-ui: -# path: /swagger-ui.html -springdoc: - swagger-ui: - enabled: true - path: /swagger-ui.html - try-it-out-enabled: true - -management: - endpoints: - web: - exposure: - include: health,info,metrics - -# AI ์„œ๋น„์Šค ์„ค์ • -ai: - azure: - cognitive: - endpoint: ${AI_AZURE_COGNITIVE_ENDPOINT:https://your-cognitive-service.cognitiveservices.azure.com} - key: ${AI_AZURE_COGNITIVE_KEY:your-cognitive-service-key} - openai: - api-key: ${AI_OPENAI_API_KEY:your-openai-api-key} - -# Azure Event Hub ์„ค์ • -azure: - eventhub: - connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING:Endpoint=sb://your-eventhub.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=your-key} - consumer-group: ${AZURE_EVENTHUB_CONSUMER_GROUP:analytics-consumer} - event-hubs: - review-events: ${AZURE_EVENTHUB_REVIEW_EVENTS:review-events} - ai-analysis-events: ${AZURE_EVENTHUB_AI_ANALYSIS_EVENTS:ai-analysis-events} - storage: - connection-string: ${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=your-storage-key;EndpointSuffix=core.windows.net} - container-name: ${AZURE_STORAGE_CONTAINER_NAME:eventhub-checkpoints} +server: + port: ${ANALYTICS_SERVICE_PORT:8084} + +logging: + level: + org.springframework.web.servlet.resource.ResourceHttpRequestHandler: ERROR + org.springframework.web.servlet.DispatcherServlet: WARN + +spring: + application: + name: analytics-service + + datasource: + url: ${ANALYTICS_DB_URL:jdbc:postgresql://20.249.162.125:5432/hiorder_analytics} + username: ${ANALYTICS_DB_USERNAME:hiorder_user} + password: ${ANALYTICS_DB_PASSWORD:hiorder_pass} + driver-class-name: org.postgresql.Driver + + jpa: + hibernate: + ddl-auto: ${JPA_DDL_AUTO:update} + show-sql: ${JPA_SHOW_SQL:false} + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + +ai-api: + openai: + api-key: ${OPENAI_API_KEY:} + base-url: https://api.openai.com/v1 + model: gpt-4o-mini + claude: + api-key: ${CLAUDE_API_KEY:} + base-url: https://api.anthropic.com + model: claude-3-sonnet-20240229 + +#external-api: +# openai: +# api-key: ${OPENAI_API_KEY:} +# base-url: https://api.openai.com +# claude: +# api-key: ${CLAUDE_API_KEY:} +# base-url: https://api.anthropic.com + +# ์™ธ๋ถ€ ์„œ๋น„์Šค ์„ค์ • +external: + services: + review: ${EXTERNAL_SERVICES_REVIEW:http://localhost:8082} + store: ${EXTERNAL_SERVICES_STORE:http://localhost:8081} + member: ${EXTERNAL_SERVICES_MEMBER:http://localhost:8080} + +#springdoc: +# api-docs: +# path: /api-docs +# swagger-ui: +# path: /swagger-ui.html +springdoc: + swagger-ui: + enabled: true + path: /swagger-ui.html + try-it-out-enabled: true + +management: + endpoints: + web: + exposure: + include: health,info,metrics + +# AI ์„œ๋น„์Šค ์„ค์ • +ai: + azure: + cognitive: + endpoint: ${AI_AZURE_COGNITIVE_ENDPOINT:https://your-cognitive-service.cognitiveservices.azure.com} + key: ${AI_AZURE_COGNITIVE_KEY:your-cognitive-service-key} + openai: + api-key: ${AI_OPENAI_API_KEY:your-openai-api-key} + +# Azure Event Hub ์„ค์ • +azure: + eventhub: + connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING:Endpoint=sb://your-eventhub.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=your-key} + consumer-group: ${AZURE_EVENTHUB_CONSUMER_GROUP:analytics-consumer} + event-hubs: + review-events: ${AZURE_EVENTHUB_REVIEW_EVENTS:review-events} + ai-analysis-events: ${AZURE_EVENTHUB_AI_ANALYSIS_EVENTS:ai-analysis-events} + storage: + connection-string: ${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=your-storage-key;EndpointSuffix=core.windows.net} + container-name: ${AZURE_STORAGE_CONTAINER_NAME:eventhub-checkpoints} diff --git a/common/src/main/java/com/ktds/hi/common/config/CorsConfig.java b/common/src/main/java/com/ktds/hi/common/config/CorsConfig.java index 013a726..6eb0dcb 100644 --- a/common/src/main/java/com/ktds/hi/common/config/CorsConfig.java +++ b/common/src/main/java/com/ktds/hi/common/config/CorsConfig.java @@ -1,102 +1,102 @@ -package com.ktds.hi.common.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import java.util.Arrays; -import java.util.List; - -/** - * ์ „์ฒด ์„œ๋น„์Šค ํ†ตํ•ฉ CORS ์„ค์ • ํด๋ž˜์Šค - * ๋ชจ๋“  ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” CORS ์ •์ฑ…์„ ์ •์˜ - */ -@Configuration -public class CorsConfig implements WebMvcConfigurer { - - @Value("${app.cors.allowed-origins:http://20.214.126.84,http://localhost:3000}") - private String allowedOrigins; - - @Value("${app.cors.allowed-methods:GET,POST,PUT,DELETE,OPTIONS}") - private String allowedMethods; - - @Value("${app.cors.allowed-headers:*}") - private String allowedHeaders; - - @Value("${app.cors.exposed-headers:Authorization,X-Total-Count}") - private String exposedHeaders; - - @Value("${app.cors.allow-credentials:true}") - private boolean allowCredentials; - - @Value("${app.cors.max-age:3600}") - private long maxAge; - - /** - * WebMvcConfigurer๋ฅผ ํ†ตํ•œ CORS ์„ค์ • - */ - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOriginPatterns(allowedOrigins.split(",")) - .allowedMethods(allowedMethods.split(",")) - .allowedHeaders(allowedHeaders.split(",")) - .exposedHeaders(exposedHeaders.split(",")) - .allowCredentials(allowCredentials) - .maxAge(maxAge); - } - - /** - * CorsConfigurationSource Bean ์ƒ์„ฑ - * Spring Security์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜๋Š” CORS ์„ค์ • - */ - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - // Origin ์„ค์ • - List origins = Arrays.asList(allowedOrigins.split(",")); - configuration.setAllowedOriginPatterns(origins); - - // Method ์„ค์ • - List methods = Arrays.asList(allowedMethods.split(",")); - configuration.setAllowedMethods(methods); - - // Header ์„ค์ • - if ("*".equals(allowedHeaders)) { - configuration.addAllowedHeader("*"); - } else { - List headers = Arrays.asList(allowedHeaders.split(",")); - configuration.setAllowedHeaders(headers); - } - - // Exposed Headers ์„ค์ • - List exposed = Arrays.asList(exposedHeaders.split(",")); - configuration.setExposedHeaders(exposed); - - // Credentials ์„ค์ • - configuration.setAllowCredentials(allowCredentials); - - // Max Age ์„ค์ • - configuration.setMaxAge(maxAge); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } - - /** - * CorsFilter Bean ์ƒ์„ฑ - * ๊ธ€๋กœ๋ฒŒ CORS ํ•„ํ„ฐ๋กœ ์‚ฌ์šฉ - */ - @Bean - public CorsFilter corsFilter() { - return new CorsFilter(corsConfigurationSource()); - } +package com.ktds.hi.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; +import java.util.List; + +/** + * ์ „์ฒด ์„œ๋น„์Šค ํ†ตํ•ฉ CORS ์„ค์ • ํด๋ž˜์Šค + * ๋ชจ๋“  ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” CORS ์ •์ฑ…์„ ์ •์˜ + */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Value("${app.cors.allowed-origins:http://20.214.126.84,http://localhost:3000}") + private String allowedOrigins; + + @Value("${app.cors.allowed-methods:GET,POST,PUT,DELETE,OPTIONS}") + private String allowedMethods; + + @Value("${app.cors.allowed-headers:*}") + private String allowedHeaders; + + @Value("${app.cors.exposed-headers:Authorization,X-Total-Count}") + private String exposedHeaders; + + @Value("${app.cors.allow-credentials:true}") + private boolean allowCredentials; + + @Value("${app.cors.max-age:3600}") + private long maxAge; + + /** + * WebMvcConfigurer๋ฅผ ํ†ตํ•œ CORS ์„ค์ • + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns(allowedOrigins.split(",")) + .allowedMethods(allowedMethods.split(",")) + .allowedHeaders(allowedHeaders.split(",")) + .exposedHeaders(exposedHeaders.split(",")) + .allowCredentials(allowCredentials) + .maxAge(maxAge); + } + + /** + * CorsConfigurationSource Bean ์ƒ์„ฑ + * Spring Security์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜๋Š” CORS ์„ค์ • + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // Origin ์„ค์ • + List origins = Arrays.asList(allowedOrigins.split(",")); + configuration.setAllowedOriginPatterns(origins); + + // Method ์„ค์ • + List methods = Arrays.asList(allowedMethods.split(",")); + configuration.setAllowedMethods(methods); + + // Header ์„ค์ • + if ("*".equals(allowedHeaders)) { + configuration.addAllowedHeader("*"); + } else { + List headers = Arrays.asList(allowedHeaders.split(",")); + configuration.setAllowedHeaders(headers); + } + + // Exposed Headers ์„ค์ • + List exposed = Arrays.asList(exposedHeaders.split(",")); + configuration.setExposedHeaders(exposed); + + // Credentials ์„ค์ • + configuration.setAllowCredentials(allowCredentials); + + // Max Age ์„ค์ • + configuration.setMaxAge(maxAge); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + /** + * CorsFilter Bean ์ƒ์„ฑ + * ๊ธ€๋กœ๋ฒŒ CORS ํ•„ํ„ฐ๋กœ ์‚ฌ์šฉ + */ + @Bean + public CorsFilter corsFilter() { + return new CorsFilter(corsConfigurationSource()); + } } \ No newline at end of file diff --git a/common/src/main/resources/application-common.yml b/common/src/main/resources/application-common.yml index 0d276bd..f7ecdf3 100644 --- a/common/src/main/resources/application-common.yml +++ b/common/src/main/resources/application-common.yml @@ -1,93 +1,93 @@ -spring: - # JPA ์„ค์ • - jpa: - hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} - naming: - physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl - implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl - show-sql: ${JPA_SHOW_SQL:false} - properties: - hibernate: - format_sql: true - use_sql_comments: true - jdbc: - batch_size: 20 - order_inserts: true - order_updates: true - batch_versioned_data: true - - # Redis ์„ค์ • - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - timeout: 2000ms - lettuce: - pool: - max-active: 8 - max-wait: -1ms - max-idle: 8 - min-idle: 0 - - # Jackson ์„ค์ • - jackson: - time-zone: Asia/Seoul - date-format: yyyy-MM-dd HH:mm:ss - serialization: - write-dates-as-timestamps: false - deserialization: - fail-on-unknown-properties: false - - # ํŠธ๋žœ์žญ์…˜ ์„ค์ • - transaction: - default-timeout: 30 - -# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ • -app: - # JWT ์„ค์ • - jwt: - secret-key: ${JWT_SECRET_KEY:hiorder-secret-key-for-jwt-token-generation-2024-very-long-secret-key} - access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000} # 1์‹œ๊ฐ„ - refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7์ผ - # CORS ์„ค์ • - cors: - allowed-origins: ${CORS_ALLOWED_ORIGINS:http://20.214.126.84,http://localhost:8080} - allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS} - allowed-headers: ${CORS_ALLOWED_HEADERS:*} - exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization, X-Total-Count} - allow-credentials: ${CORS_ALLOW_CREDENTIALS:true} - max-age: ${CORS_MAX_AGE:3600} - - # ์บ์‹œ ์„ค์ • - cache: - default-ttl: ${CACHE_DEFAULT_TTL:3600} # 1์‹œ๊ฐ„ - - # Swagger ์„ค์ • - swagger: - title: ${SWAGGER_TITLE:ํ•˜์ด์˜ค๋” API} - description: ${SWAGGER_DESCRIPTION:ํ•˜์ด์˜ค๋” ๋ฐฑ์—”๋“œ API ๋ฌธ์„œ} - version: ${SWAGGER_VERSION:1.0.0} - server-url: ${SWAGGER_SERVER_URL:http://localhost:8080} - -# ๋กœ๊น… ์„ค์ • -logging: - level: - com.ktds.hi: ${LOG_LEVEL:INFO} - org.springframework.security: ${SECURITY_LOG_LEVEL:INFO} - org.hibernate.SQL: ${SQL_LOG_LEVEL:INFO} - org.hibernate.type.descriptor.sql.BasicBinder: ${SQL_PARAM_LOG_LEVEL:INFO} - pattern: - console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" - -# ๊ด€๋ฆฌ ์—”๋“œํฌ์ธํŠธ ์„ค์ • -management: - endpoints: - web: - exposure: - include: health,info,metrics,prometheus - endpoint: - health: - show-details: when-authorized +spring: + # JPA ์„ค์ • + jpa: + hibernate: + ddl-auto: ${JPA_DDL_AUTO:update} + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl + show-sql: ${JPA_SHOW_SQL:false} + properties: + hibernate: + format_sql: true + use_sql_comments: true + jdbc: + batch_size: 20 + order_inserts: true + order_updates: true + batch_versioned_data: true + + # Redis ์„ค์ • + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + timeout: 2000ms + lettuce: + pool: + max-active: 8 + max-wait: -1ms + max-idle: 8 + min-idle: 0 + + # Jackson ์„ค์ • + jackson: + time-zone: Asia/Seoul + date-format: yyyy-MM-dd HH:mm:ss + serialization: + write-dates-as-timestamps: false + deserialization: + fail-on-unknown-properties: false + + # ํŠธ๋žœ์žญ์…˜ ์„ค์ • + transaction: + default-timeout: 30 + +# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ • +app: + # JWT ์„ค์ • + jwt: + secret-key: ${JWT_SECRET_KEY:hiorder-secret-key-for-jwt-token-generation-2024-very-long-secret-key} + access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000} # 1์‹œ๊ฐ„ + refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7์ผ + # CORS ์„ค์ • + cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://20.214.126.84,http://localhost:8080} + allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS} + allowed-headers: ${CORS_ALLOWED_HEADERS:*} + exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization, X-Total-Count} + allow-credentials: ${CORS_ALLOW_CREDENTIALS:true} + max-age: ${CORS_MAX_AGE:3600} + + # ์บ์‹œ ์„ค์ • + cache: + default-ttl: ${CACHE_DEFAULT_TTL:3600} # 1์‹œ๊ฐ„ + + # Swagger ์„ค์ • + swagger: + title: ${SWAGGER_TITLE:ํ•˜์ด์˜ค๋” API} + description: ${SWAGGER_DESCRIPTION:ํ•˜์ด์˜ค๋” ๋ฐฑ์—”๋“œ API ๋ฌธ์„œ} + version: ${SWAGGER_VERSION:1.0.0} + server-url: ${SWAGGER_SERVER_URL:http://localhost:8080} + +# ๋กœ๊น… ์„ค์ • +logging: + level: + com.ktds.hi: ${LOG_LEVEL:INFO} + org.springframework.security: ${SECURITY_LOG_LEVEL:INFO} + org.hibernate.SQL: ${SQL_LOG_LEVEL:INFO} + org.hibernate.type.descriptor.sql.BasicBinder: ${SQL_PARAM_LOG_LEVEL:INFO} + pattern: + console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + +# ๊ด€๋ฆฌ ์—”๋“œํฌ์ธํŠธ ์„ค์ • +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: when-authorized diff --git a/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java b/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java index 20dbd85..bc7cae7 100644 --- a/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java +++ b/member/src/main/java/com/ktds/hi/member/config/SecurityConfig.java @@ -1,121 +1,121 @@ -package com.ktds.hi.member.config; - - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.ktds.hi.common.security.JwtTokenProvider; -import com.ktds.hi.common.security.JwtAuthenticationFilter; -import lombok.RequiredArgsConstructor; - -import org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfigurationSource; - -/** - * Spring Security ์„ค์ • ํด๋ž˜์Šค - * JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ ์„ค์ • - */ -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class SecurityConfig { - - private final JwtTokenProvider jwtTokenProvider; - private final CorsConfigurationSource corsConfigurationSource; - - /** - * ๋ณด์•ˆ ํ•„ํ„ฐ ์ฒด์ธ ์„ค์ • - * JWT ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ณ  ์„ธ์…˜์€ ๋ฌด์ƒํƒœ๋กœ ๊ด€๋ฆฌ - */ - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authz -> authz - .requestMatchers("/api/auth/**", "/api/members/register", "/api/auth/login").permitAll() - .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() - .requestMatchers("/actuator/**").permitAll() - .anyRequest().authenticated() - ) - .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } - - /** - * JWT ์ธ์ฆ ํ•„ํ„ฐ ๋นˆ - */ - @Bean - public JwtAuthenticationFilter jwtAuthenticationFilter() { - return new JwtAuthenticationFilter(jwtTokenProvider,new ObjectMapper()); - } - - /** - * ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋นˆ - */ - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - /** - * ์ธ์ฆ ๋งค๋‹ˆ์ € ๋นˆ - */ - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { - return config.getAuthenticationManager(); - } - - // @Qualifier("memberJwtTokenProvider") - // private final JwtTokenProvider jwtTokenProvider; - // private final AuthService authService; - // - // /** - // * ๋ณด์•ˆ ํ•„ํ„ฐ ์ฒด์ธ ์„ค์ • - // * JWT ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ณ  ์„ธ์…˜์€ ๋ฌด์ƒํƒœ๋กœ ๊ด€๋ฆฌ - // */ - // @Bean - // public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // http - // .csrf(csrf -> csrf.disable()) - // .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // .authorizeHttpRequests(authz -> authz - // .requestMatchers("/api/auth/**", "/api/members/register").permitAll() - // .requestMatchers("/swagger-ui/**", "/api-docs/**").permitAll() - // .requestMatchers("/actuator/**").permitAll() - // .anyRequest().authenticated() - // ) - // .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, authService), - // UsernamePasswordAuthenticationFilter.class); - // - // return http.build(); - // } - // - // /** - // * ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋นˆ - // */ - // @Bean - // public PasswordEncoder passwordEncoder() { - // return new BCryptPasswordEncoder(); - // } - // - // /** - // * ์ธ์ฆ ๋งค๋‹ˆ์ € ๋นˆ - // */ - // @Bean - // public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { - // return config.getAuthenticationManager(); - // } -} +package com.ktds.hi.member.config; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ktds.hi.common.security.JwtTokenProvider; +import com.ktds.hi.common.security.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; + +import org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfigurationSource; + +/** + * Spring Security ์„ค์ • ํด๋ž˜์Šค + * JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ ์„ค์ • + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + private final CorsConfigurationSource corsConfigurationSource; + + /** + * ๋ณด์•ˆ ํ•„ํ„ฐ ์ฒด์ธ ์„ค์ • + * JWT ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ณ  ์„ธ์…˜์€ ๋ฌด์ƒํƒœ๋กœ ๊ด€๋ฆฌ + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authz -> authz + .requestMatchers("/api/auth/**", "/api/members/register", "/api/auth/login").permitAll() + .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() + .requestMatchers("/actuator/**").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + /** + * JWT ์ธ์ฆ ํ•„ํ„ฐ ๋นˆ + */ + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter() { + return new JwtAuthenticationFilter(jwtTokenProvider,new ObjectMapper()); + } + + /** + * ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋นˆ + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * ์ธ์ฆ ๋งค๋‹ˆ์ € ๋นˆ + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + // @Qualifier("memberJwtTokenProvider") + // private final JwtTokenProvider jwtTokenProvider; + // private final AuthService authService; + // + // /** + // * ๋ณด์•ˆ ํ•„ํ„ฐ ์ฒด์ธ ์„ค์ • + // * JWT ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ณ  ์„ธ์…˜์€ ๋ฌด์ƒํƒœ๋กœ ๊ด€๋ฆฌ + // */ + // @Bean + // public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // http + // .csrf(csrf -> csrf.disable()) + // .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // .authorizeHttpRequests(authz -> authz + // .requestMatchers("/api/auth/**", "/api/members/register").permitAll() + // .requestMatchers("/swagger-ui/**", "/api-docs/**").permitAll() + // .requestMatchers("/actuator/**").permitAll() + // .anyRequest().authenticated() + // ) + // .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, authService), + // UsernamePasswordAuthenticationFilter.class); + // + // return http.build(); + // } + // + // /** + // * ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋นˆ + // */ + // @Bean + // public PasswordEncoder passwordEncoder() { + // return new BCryptPasswordEncoder(); + // } + // + // /** + // * ์ธ์ฆ ๋งค๋‹ˆ์ € ๋นˆ + // */ + // @Bean + // public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + // return config.getAuthenticationManager(); + // } +} diff --git a/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java b/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java index 993c465..810c003 100644 --- a/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java +++ b/member/src/main/java/com/ktds/hi/member/config/SwaggerConfig.java @@ -1,39 +1,39 @@ -package com.ktds.hi.member.config; - -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.servers.Server; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Swagger ์„ค์ • ํด๋ž˜์Šค - * API ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ OpenAPI ์„ค์ • - */ -@Configuration -public class SwaggerConfig { - - @Bean - public OpenAPI openAPI() { - return new OpenAPI() - // .addServersItem(new Server().url("/")) - .info(new Info() - .title("ํ•˜์ด์˜ค๋” ํšŒ์› ๊ด€๋ฆฌ ์„œ๋น„์Šค API") - .description("ํšŒ์› ๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ์ทจํ–ฅ ๊ด€๋ฆฌ ๋“ฑ ํšŒ์› ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” API") - .version("1.0.0")); - } - - /** - * JWT Bearer ํ† ํฐ์„ ์œ„ํ•œ Security Scheme ์ƒ์„ฑ - */ - private SecurityScheme createAPIKeyScheme() { - return new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .in(SecurityScheme.In.HEADER) - .name("Authorization") - .description("JWT ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š” (Bearer ์ ‘๋‘์‚ฌ ์ œ์™ธ)"); - } -} +package com.ktds.hi.member.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger ์„ค์ • ํด๋ž˜์Šค + * API ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ OpenAPI ์„ค์ • + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + // .addServersItem(new Server().url("/")) + .info(new Info() + .title("ํ•˜์ด์˜ค๋” ํšŒ์› ๊ด€๋ฆฌ ์„œ๋น„์Šค API") + .description("ํšŒ์› ๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ์ทจํ–ฅ ๊ด€๋ฆฌ ๋“ฑ ํšŒ์› ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” API") + .version("1.0.0")); + } + + /** + * JWT Bearer ํ† ํฐ์„ ์œ„ํ•œ Security Scheme ์ƒ์„ฑ + */ + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name("Authorization") + .description("JWT ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š” (Bearer ์ ‘๋‘์‚ฌ ์ œ์™ธ)"); + } +} diff --git a/member/src/main/resources/application.yml b/member/src/main/resources/application.yml index 202da20..aea9b1f 100644 --- a/member/src/main/resources/application.yml +++ b/member/src/main/resources/application.yml @@ -1,56 +1,56 @@ -server: - port: ${MEMBER_SERVICE_PORT:8081} - -spring: - application: - name: member-service - - datasource: - url: ${MEMBER_DB_URL:jdbc:postgresql://20.249.152.184:5432/hiorder_member} - username: ${MEMBER_DB_USERNAME:hiorder_user} - password: ${MEMBER_DB_PASSWORD:hiorder_pass} - driver-class-name: org.postgresql.Driver - - jpa: - hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} - show-sql: ${JPA_SHOW_SQL:false} - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.PostgreSQLDialect - - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - timeout: 2000ms - lettuce: - pool: - max-active: 8 - max-wait: -1ms - max-idle: 8 - min-idle: 0 - -jwt: - secret: ${JWT_SECRET:hiorder-secret-key-for-jwt-token-generation-must-be-long-enough} - access-token-expiration: ${JWT_ACCESS_EXPIRATION:3600000} # 1์‹œ๊ฐ„ - refresh-token-expiration: ${JWT_REFRESH_EXPIRATION:604800000} # 7์ผ - -sms: - api-key: ${SMS_API_KEY:} - api-secret: ${SMS_API_SECRET:} - from-number: ${SMS_FROM_NUMBER:} - -springdoc: - swagger-ui: - enabled: true - path: /swagger-ui.html - try-it-out-enabled: true - -management: - endpoints: - web: - exposure: - include: health,info,metrics +server: + port: ${MEMBER_SERVICE_PORT:8081} + +spring: + application: + name: member-service + + datasource: + url: ${MEMBER_DB_URL:jdbc:postgresql://20.249.152.184:5432/hiorder_member} + username: ${MEMBER_DB_USERNAME:hiorder_user} + password: ${MEMBER_DB_PASSWORD:hiorder_pass} + driver-class-name: org.postgresql.Driver + + jpa: + hibernate: + ddl-auto: ${JPA_DDL_AUTO:update} + show-sql: ${JPA_SHOW_SQL:false} + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + timeout: 2000ms + lettuce: + pool: + max-active: 8 + max-wait: -1ms + max-idle: 8 + min-idle: 0 + +jwt: + secret: ${JWT_SECRET:hiorder-secret-key-for-jwt-token-generation-must-be-long-enough} + access-token-expiration: ${JWT_ACCESS_EXPIRATION:3600000} # 1์‹œ๊ฐ„ + refresh-token-expiration: ${JWT_REFRESH_EXPIRATION:604800000} # 7์ผ + +sms: + api-key: ${SMS_API_KEY:} + api-secret: ${SMS_API_SECRET:} + from-number: ${SMS_FROM_NUMBER:} + +springdoc: + swagger-ui: + enabled: true + path: /swagger-ui.html + try-it-out-enabled: true + +management: + endpoints: + web: + exposure: + include: health,info,metrics diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SecurityConfig.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SecurityConfig.java index f1b3a0c..2fa86a9 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SecurityConfig.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SecurityConfig.java @@ -1,52 +1,52 @@ -package com.ktds.hi.recommend.infra.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfigurationSource; - -import lombok.RequiredArgsConstructor; - -/** - * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค - * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • - */ -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class SecurityConfig { - - private final CorsConfigurationSource corsConfigurationSource; - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - - - http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ - .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() - - // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .requestMatchers("/api/analytics/**").permitAll() - .requestMatchers("/api/action-plans/**").permitAll() - - // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ - .requestMatchers("/actuator/**").permitAll() - - // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .anyRequest().permitAll() - ); - - return http.build(); - } -} +package com.ktds.hi.recommend.infra.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfigurationSource; + +import lombok.RequiredArgsConstructor; + +/** + * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค + * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final CorsConfigurationSource corsConfigurationSource; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + + + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ + .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() + + // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .requestMatchers("/api/analytics/**").permitAll() + .requestMatchers("/api/action-plans/**").permitAll() + + // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ + .requestMatchers("/actuator/**").permitAll() + + // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .anyRequest().permitAll() + ); + + return http.build(); + } +} diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java index f0e4f1d..cb7d7f9 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/SwaggerConfig.java @@ -1,47 +1,47 @@ -package com.ktds.hi.recommend.infra.config; - -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.servers.Server; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Swagger ์„ค์ • ํด๋ž˜์Šค - * API ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ OpenAPI ์„ค์ • - */ -@Configuration -public class SwaggerConfig { - - - @Bean - public OpenAPI openAPI() { - return new OpenAPI() - // .addServersItem(new Server().url("/")) - .info(new Info() - .title("ํ•˜์ด์˜ค๋” ์ถ”์ฒœ ์„œ๋น„์Šค API") - .description("์‚ฌ์šฉ์ž ์ทจํ–ฅ ๊ธฐ๋ฐ˜ ๋งค์žฅ ์ถ”์ฒœ ๋ฐ ์ทจํ–ฅ ๋ถ„์„ ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” API") - .version("1.0.0")); - } - - /** - * JWT Bearer ํ† ํฐ์„ ์œ„ํ•œ Security Scheme ์ƒ์„ฑ - */ - private SecurityScheme createAPIKeyScheme() { - return new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .in(SecurityScheme.In.HEADER) - .name("Authorization") - .description(""" - JWT ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š” - - ์‚ฌ์šฉ๋ฒ•: - 1. ๋กœ๊ทธ์ธ API๋กœ ํ† ํฐ ๋ฐœ๊ธ‰ - 2. Bearer ์ ‘๋‘์‚ฌ ์—†์ด ํ† ํฐ๋งŒ ์ž…๋ ฅ - 3. ์˜ˆ: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOi... - """); - } -} +package com.ktds.hi.recommend.infra.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger ์„ค์ • ํด๋ž˜์Šค + * API ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ OpenAPI ์„ค์ • + */ +@Configuration +public class SwaggerConfig { + + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + // .addServersItem(new Server().url("/")) + .info(new Info() + .title("ํ•˜์ด์˜ค๋” ์ถ”์ฒœ ์„œ๋น„์Šค API") + .description("์‚ฌ์šฉ์ž ์ทจํ–ฅ ๊ธฐ๋ฐ˜ ๋งค์žฅ ์ถ”์ฒœ ๋ฐ ์ทจํ–ฅ ๋ถ„์„ ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” API") + .version("1.0.0")); + } + + /** + * JWT Bearer ํ† ํฐ์„ ์œ„ํ•œ Security Scheme ์ƒ์„ฑ + */ + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name("Authorization") + .description(""" + JWT ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š” + + ์‚ฌ์šฉ๋ฒ•: + 1. ๋กœ๊ทธ์ธ API๋กœ ํ† ํฐ ๋ฐœ๊ธ‰ + 2. Bearer ์ ‘๋‘์‚ฌ ์—†์ด ํ† ํฐ๋งŒ ์ž…๋ ฅ + 3. ์˜ˆ: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOi... + """); + } +} diff --git a/recommend/src/main/resources/application.yml b/recommend/src/main/resources/application.yml index 6f8573a..191d819 100644 --- a/recommend/src/main/resources/application.yml +++ b/recommend/src/main/resources/application.yml @@ -1,226 +1,228 @@ -# recommend/src/main/resources/application.yml -server: - port: ${RECOMMEND_SERVICE_PORT:8085} - -spring: - cloud: - compatibility-verifier: - enabled: false - application: - name: recommend-service - - # ํ”„๋กœํ•„ ์„ค์ • - profiles: - active: ${SPRING_PROFILES_ACTIVE:local} - - # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • - datasource: - url: ${RECOMMEND_DB_URL:jdbc:postgresql://20.249.162.245:5432/hiorder_recommend} - username: ${RECOMMEND_DB_USERNAME:hiorder_user} - password: ${RECOMMEND_DB_PASSWORD:hiorder_pass} - driver-class-name: org.postgresql.Driver - hikari: - maximum-pool-size: ${DB_POOL_SIZE:20} - minimum-idle: ${DB_POOL_MIN_IDLE:5} - connection-timeout: ${DB_CONNECTION_TIMEOUT:30000} - idle-timeout: ${DB_IDLE_TIMEOUT:600000} - max-lifetime: ${DB_MAX_LIFETIME:1800000} - pool-name: RecommendHikariCP - - # JPA ์„ค์ • - jpa: - hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} - show-sql: ${JPA_SHOW_SQL:false} - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect - format_sql: ${JPA_FORMAT_SQL:true} - show_sql: ${JPA_SHOW_SQL:false} - use_sql_comments: ${JPA_USE_SQL_COMMENTS:true} - jdbc: - batch_size: 20 - order_inserts: true - order_updates: true - open-in-view: false - - # Redis ์„ค์ • (์˜ฌ๋ฐ”๋ฅธ ๊ตฌ์กฐ) - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - timeout: 2000ms - database: ${REDIS_DATABASE:0} - lettuce: - pool: - max-active: ${REDIS_POOL_MAX_ACTIVE:8} - max-idle: ${REDIS_POOL_MAX_IDLE:8} - min-idle: ${REDIS_POOL_MIN_IDLE:2} - max-wait: -1ms - shutdown-timeout: 100ms - -# ์™ธ๋ถ€ ์„œ๋น„์Šค URL ์„ค์ • -services: - store: - url: ${STORE_SERVICE_URL:http://store-service:8082} - review: - url: ${REVIEW_SERVICE_URL:http://review-service:8083} - member: - url: ${MEMBER_SERVICE_URL:http://member-service:8081} - -# Feign ์„ค์ • -feign: - client: - config: - default: - connectTimeout: 5000 - readTimeout: 10000 - loggerLevel: basic - store-service: - connectTimeout: 3000 - readTimeout: 8000 - review-service: - connectTimeout: 3000 - readTimeout: 8000 - circuitbreaker: - enabled: true - compression: - request: - enabled: true - response: - enabled: true - -# Circuit Breaker ์„ค์ • -resilience4j: - circuitbreaker: - instances: - store-service: - failure-rate-threshold: 50 - wait-duration-in-open-state: 30000 - sliding-window-size: 10 - minimum-number-of-calls: 5 - review-service: - failure-rate-threshold: 50 - wait-duration-in-open-state: 30000 - sliding-window-size: 10 - minimum-number-of-calls: 5 - retry: - instances: - store-service: - max-attempts: 3 - wait-duration: 1000 - review-service: - max-attempts: 3 - wait-duration: 1000 - - -# Actuator ์„ค์ • -management: - endpoints: - web: - exposure: - include: health,info,metrics,prometheus - endpoint: - health: - show-details: always - metrics: - export: - prometheus: - -# Swagger/OpenAPI ์„ค์ • -springdoc: - api-docs: - path: /api-docs - swagger-ui: - path: /swagger-ui.html - tags-sorter: alpha - operations-sorter: alpha - display-request-duration: true - display-operation-id: true - show-actuator: false - -# ๋กœ๊น… ์„ค์ • -logging: - level: - root: ${LOG_LEVEL_ROOT:INFO} - com.ktds.hi.recommend: ${LOG_LEVEL:INFO} - org.springframework.cloud.openfeign: ${LOG_LEVEL_FEIGN:DEBUG} - org.springframework.web: ${LOG_LEVEL_WEB:INFO} - org.springframework.data.redis: ${LOG_LEVEL_REDIS:INFO} - org.hibernate.SQL: ${LOG_LEVEL_SQL:INFO} - org.hibernate.type.descriptor.sql.BasicBinder: ${LOG_LEVEL_SQL_PARAM:INFO} - pattern: - console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n" - file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n" - file: - name: ${LOG_FILE_PATH:./logs/recommend-service.log} - max-size: 100MB - max-history: 30 - -# Security ์„ค์ • -security: - jwt: - secret: ${JWT_SECRET:hiorder-recommend-secret-key-2024} - expiration: ${JWT_EXPIRATION:86400000} # 24์‹œ๊ฐ„ - cors: - allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:8080} - allowed-methods: GET,POST,PUT,DELETE,OPTIONS - allowed-headers: "*" - allow-credentials: true - -recommend: - cache: - recommendation-ttl: ${RECOMMENDATION_CACHE_TTL:1800} - user-preference-ttl: ${USER_PREFERENCE_CACHE_TTL:3600} - algorithm: - max-recommendations: ${MAX_RECOMMENDATIONS:20} - default-radius: ${DEFAULT_SEARCH_RADIUS:5000} - max-radius: ${MAX_SEARCH_RADIUS:10000} ---- -# Local ํ™˜๊ฒฝ ์„ค์ • -spring: - config: - activate: - on-profile: local - jpa: - show-sql: true - hibernate: - ddl-auto: create-drop - -logging: - level: - com.ktds.hi.recommend: DEBUG - org.springframework.web: DEBUG - ---- -# Development ํ™˜๊ฒฝ ์„ค์ • -spring: - config: - activate: - on-profile: dev - jpa: - show-sql: true - hibernate: - ddl-auto: update - -logging: - level: - com.ktds.hi.recommend: DEBUG - ---- -# Production ํ™˜๊ฒฝ ์„ค์ • -spring: - config: - activate: - on-profile: prod - jpa: - show-sql: false - hibernate: - ddl-auto: validate - -logging: - level: - root: WARN - com.ktds.hi.recommend: INFO +# recommend/src/main/resources/application.yml +server: + port: ${RECOMMEND_SERVICE_PORT:8085} + +spring: + cloud: + compatibility-verifier: + enabled: false + application: + name: recommend-service + + # ํ”„๋กœํ•„ ์„ค์ • + profiles: + active: ${SPRING_PROFILES_ACTIVE:local} + + # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • + datasource: + url: ${RECOMMEND_DB_URL:jdbc:postgresql://20.249.162.245:5432/hiorder_recommend} + username: ${RECOMMEND_DB_USERNAME:hiorder_user} + password: ${RECOMMEND_DB_PASSWORD:hiorder_pass} + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: ${DB_POOL_SIZE:20} + minimum-idle: ${DB_POOL_MIN_IDLE:5} + connection-timeout: ${DB_CONNECTION_TIMEOUT:30000} + idle-timeout: ${DB_IDLE_TIMEOUT:600000} + max-lifetime: ${DB_MAX_LIFETIME:1800000} + pool-name: RecommendHikariCP + + # JPA ์„ค์ • + jpa: + hibernate: + ddl-auto: ${JPA_DDL_AUTO:update} + show-sql: ${JPA_SHOW_SQL:false} + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: ${JPA_FORMAT_SQL:true} + show_sql: ${JPA_SHOW_SQL:false} + use_sql_comments: ${JPA_USE_SQL_COMMENTS:true} + jdbc: + batch_size: 20 + order_inserts: true + order_updates: true + open-in-view: false + + + + # Redis ์„ค์ • (์˜ฌ๋ฐ”๋ฅธ ๊ตฌ์กฐ) + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + timeout: 2000ms + database: ${REDIS_DATABASE:0} + lettuce: + pool: + max-active: ${REDIS_POOL_MAX_ACTIVE:8} + max-idle: ${REDIS_POOL_MAX_IDLE:8} + min-idle: ${REDIS_POOL_MIN_IDLE:2} + max-wait: -1ms + shutdown-timeout: 100ms + +# ์™ธ๋ถ€ ์„œ๋น„์Šค URL ์„ค์ • +services: + store: + url: ${STORE_SERVICE_URL:http://store-service:8082} + review: + url: ${REVIEW_SERVICE_URL:http://review-service:8083} + member: + url: ${MEMBER_SERVICE_URL:http://member-service:8081} + +# Feign ์„ค์ • +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 10000 + loggerLevel: basic + store-service: + connectTimeout: 3000 + readTimeout: 8000 + review-service: + connectTimeout: 3000 + readTimeout: 8000 + circuitbreaker: + enabled: true + compression: + request: + enabled: true + response: + enabled: true + +# Circuit Breaker ์„ค์ • +resilience4j: + circuitbreaker: + instances: + store-service: + failure-rate-threshold: 50 + wait-duration-in-open-state: 30000 + sliding-window-size: 10 + minimum-number-of-calls: 5 + review-service: + failure-rate-threshold: 50 + wait-duration-in-open-state: 30000 + sliding-window-size: 10 + minimum-number-of-calls: 5 + retry: + instances: + store-service: + max-attempts: 3 + wait-duration: 1000 + review-service: + max-attempts: 3 + wait-duration: 1000 + + +# Actuator ์„ค์ • +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + +# Swagger/OpenAPI ์„ค์ • +springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui.html + tags-sorter: alpha + operations-sorter: alpha + display-request-duration: true + display-operation-id: true + show-actuator: false + +# ๋กœ๊น… ์„ค์ • +logging: + level: + root: ${LOG_LEVEL_ROOT:INFO} + com.ktds.hi.recommend: ${LOG_LEVEL:INFO} + org.springframework.cloud.openfeign: ${LOG_LEVEL_FEIGN:DEBUG} + org.springframework.web: ${LOG_LEVEL_WEB:INFO} + org.springframework.data.redis: ${LOG_LEVEL_REDIS:INFO} + org.hibernate.SQL: ${LOG_LEVEL_SQL:INFO} + org.hibernate.type.descriptor.sql.BasicBinder: ${LOG_LEVEL_SQL_PARAM:INFO} + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n" + file: + name: ${LOG_FILE_PATH:./logs/recommend-service.log} + max-size: 100MB + max-history: 30 + +# Security ์„ค์ • +security: + jwt: + secret: ${JWT_SECRET:hiorder-recommend-secret-key-2024} + expiration: ${JWT_EXPIRATION:86400000} # 24์‹œ๊ฐ„ + cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:8080} + allowed-methods: GET,POST,PUT,DELETE,OPTIONS + allowed-headers: "*" + allow-credentials: true + +recommend: + cache: + recommendation-ttl: ${RECOMMENDATION_CACHE_TTL:1800} + user-preference-ttl: ${USER_PREFERENCE_CACHE_TTL:3600} + algorithm: + max-recommendations: ${MAX_RECOMMENDATIONS:20} + default-radius: ${DEFAULT_SEARCH_RADIUS:5000} + max-radius: ${MAX_SEARCH_RADIUS:10000} +--- +# Local ํ™˜๊ฒฝ ์„ค์ • +spring: + config: + activate: + on-profile: local + jpa: + show-sql: true + hibernate: + ddl-auto: create-drop + +logging: + level: + com.ktds.hi.recommend: DEBUG + org.springframework.web: DEBUG + +--- +# Development ํ™˜๊ฒฝ ์„ค์ • +spring: + config: + activate: + on-profile: dev + jpa: + show-sql: true + hibernate: + ddl-auto: update + +logging: + level: + com.ktds.hi.recommend: DEBUG + +--- +# Production ํ™˜๊ฒฝ ์„ค์ • +spring: + config: + activate: + on-profile: prod + jpa: + show-sql: false + hibernate: + ddl-auto: validate + +logging: + level: + root: WARN + com.ktds.hi.recommend: INFO org.springframework.cloud.openfeign: INFO \ No newline at end of file diff --git a/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java b/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java index b69a463..c046bd7 100644 --- a/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java +++ b/review/src/main/java/com/ktds/hi/review/biz/service/ReviewInteractor.java @@ -1,143 +1,143 @@ -package com.ktds.hi.review.biz.service; - -import com.ktds.hi.review.biz.usecase.in.CreateReviewUseCase; -import com.ktds.hi.review.biz.usecase.in.DeleteReviewUseCase; -import com.ktds.hi.review.biz.usecase.in.GetReviewUseCase; -import com.ktds.hi.review.biz.usecase.out.ReviewRepository; -import com.ktds.hi.review.biz.domain.Review; -import com.ktds.hi.review.biz.domain.ReviewStatus; -import com.ktds.hi.review.infra.dto.request.ReviewCreateRequest; -import com.ktds.hi.review.infra.dto.response.*; -import com.ktds.hi.common.exception.BusinessException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - -/** - * ๋ฆฌ๋ทฐ ์ธํ„ฐ๋ž™ํ„ฐ ํด๋ž˜์Šค - * ๋ฆฌ๋ทฐ ์ƒ์„ฑ, ์กฐํšŒ, ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ - */ -@Service -@RequiredArgsConstructor -@Slf4j -@Transactional -public class ReviewInteractor implements CreateReviewUseCase, DeleteReviewUseCase, GetReviewUseCase { - - private final ReviewRepository reviewRepository; - - @Override - public ReviewCreateResponse createReview(Long memberId, ReviewCreateRequest request) { - // ๋ฆฌ๋ทฐ ์ƒ์„ฑ - Review review = Review.builder() - .storeId(request.getStoreId()) - .memberId(memberId) - .memberNickname("ํšŒ์›" + memberId) // TODO: ํšŒ์› ์„œ๋น„์Šค์—์„œ ๋‹‰๋„ค์ž„ ์กฐํšŒ - .rating(request.getRating()) - .content(request.getContent()) - .imageUrls(request.getImageUrls()) - .status(ReviewStatus.ACTIVE) - .likeCount(0) - .dislikeCount(0) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - Review savedReview = reviewRepository.saveReview(review); - - log.info("๋ฆฌ๋ทฐ ์ƒ์„ฑ ์™„๋ฃŒ: reviewId={}, storeId={}, memberId={}", - savedReview.getId(), savedReview.getStoreId(), savedReview.getMemberId()); - - return ReviewCreateResponse.builder() - .reviewId(savedReview.getId()) - .message("๋ฆฌ๋ทฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค") - .build(); - } - - @Override - public ReviewDeleteResponse deleteReview(Long reviewId, Long memberId) { - Review review = reviewRepository.findReviewByIdAndMemberId(reviewId, memberId) - .orElseThrow(() -> new BusinessException("๋ฆฌ๋ทฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค")); - - reviewRepository.deleteReview(reviewId); - - log.info("๋ฆฌ๋ทฐ ์‚ญ์ œ ์™„๋ฃŒ: reviewId={}, memberId={}", reviewId, memberId); - - return ReviewDeleteResponse.builder() - .success(true) - .message("๋ฆฌ๋ทฐ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค") - .build(); - } - - @Override - @Transactional(readOnly = true) - public List getStoreReviews(Long storeId, Integer page, Integer size) { - Pageable pageable = PageRequest.of(page != null ? page : 0, size != null ? size : 20); - Page reviews = reviewRepository.findReviewsByStoreId(storeId, pageable); - - return reviews.stream() - .filter(review -> review.getStatus() == ReviewStatus.ACTIVE) - .map(review -> ReviewListResponse.builder() - .reviewId(review.getId()) - .memberNickname(review.getMemberNickname()) - .rating(review.getRating()) - .content(review.getContent()) - .imageUrls(review.getImageUrls()) - .likeCount(review.getLikeCount()) - .dislikeCount(review.getDislikeCount()) - .createdAt(review.getCreatedAt()) - .build()) - .collect(Collectors.toList()); - } - - @Override - @Transactional(readOnly = true) - public ReviewDetailResponse getReviewDetail(Long reviewId) { - Review review = reviewRepository.findReviewById(reviewId) - .orElseThrow(() -> new BusinessException("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค")); - - if (review.getStatus() != ReviewStatus.ACTIVE) { - throw new BusinessException("์‚ญ์ œ๋˜์—ˆ๊ฑฐ๋‚˜ ์ˆจ๊ฒจ์ง„ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค"); - } - - return ReviewDetailResponse.builder() - .reviewId(review.getId()) - .storeId(review.getStoreId()) - .memberNickname(review.getMemberNickname()) - .rating(review.getRating()) - .content(review.getContent()) - .imageUrls(review.getImageUrls()) - .likeCount(review.getLikeCount()) - .dislikeCount(review.getDislikeCount()) - .createdAt(review.getCreatedAt()) - .build(); - } - - @Override - @Transactional(readOnly = true) - public List getMyReviews(Long memberId, Integer page, Integer size) { - Pageable pageable = PageRequest.of(page != null ? page : 0, size != null ? size : 20); - Page reviews = reviewRepository.findReviewsByMemberId(memberId, pageable); - - return reviews.stream() - .filter(review -> review.getStatus() == ReviewStatus.ACTIVE) - .map(review -> ReviewListResponse.builder() - .reviewId(review.getId()) - .memberNickname(review.getMemberNickname()) - .rating(review.getRating()) - .content(review.getContent()) - .imageUrls(review.getImageUrls()) - .likeCount(review.getLikeCount()) - .dislikeCount(review.getDislikeCount()) - .createdAt(review.getCreatedAt()) - .build()) - .collect(Collectors.toList()); - } -} +package com.ktds.hi.review.biz.service; + +import com.ktds.hi.review.biz.usecase.in.CreateReviewUseCase; +import com.ktds.hi.review.biz.usecase.in.DeleteReviewUseCase; +import com.ktds.hi.review.biz.usecase.in.GetReviewUseCase; +import com.ktds.hi.review.biz.usecase.out.ReviewRepository; +import com.ktds.hi.review.biz.domain.Review; +import com.ktds.hi.review.biz.domain.ReviewStatus; +import com.ktds.hi.review.infra.dto.request.ReviewCreateRequest; +import com.ktds.hi.review.infra.dto.response.*; +import com.ktds.hi.common.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * ๋ฆฌ๋ทฐ ์ธํ„ฐ๋ž™ํ„ฐ ํด๋ž˜์Šค + * ๋ฆฌ๋ทฐ ์ƒ์„ฑ, ์กฐํšŒ, ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ + */ +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional +public class ReviewInteractor implements CreateReviewUseCase, DeleteReviewUseCase, GetReviewUseCase { + + private final ReviewRepository reviewRepository; + + @Override + public ReviewCreateResponse createReview(Long memberId, ReviewCreateRequest request) { + // ๋ฆฌ๋ทฐ ์ƒ์„ฑ + Review review = Review.builder() + .storeId(request.getStoreId()) + .memberId(memberId) + .memberNickname("ํšŒ์›" + memberId) // TODO: ํšŒ์› ์„œ๋น„์Šค์—์„œ ๋‹‰๋„ค์ž„ ์กฐํšŒ + .rating(request.getRating()) + .content(request.getContent()) + .imageUrls(request.getImageUrls()) + .status(ReviewStatus.ACTIVE) + .likeCount(0) + .dislikeCount(0) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + Review savedReview = reviewRepository.saveReview(review); + + log.info("๋ฆฌ๋ทฐ ์ƒ์„ฑ ์™„๋ฃŒ: reviewId={}, storeId={}, memberId={}", + savedReview.getId(), savedReview.getStoreId(), savedReview.getMemberId()); + + return ReviewCreateResponse.builder() + .reviewId(savedReview.getId()) + .message("๋ฆฌ๋ทฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค") + .build(); + } + + @Override + public ReviewDeleteResponse deleteReview(Long reviewId, Long memberId) { + Review review = reviewRepository.findReviewByIdAndMemberId(reviewId, memberId) + .orElseThrow(() -> new BusinessException("๋ฆฌ๋ทฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค")); + + reviewRepository.deleteReview(reviewId); + + log.info("๋ฆฌ๋ทฐ ์‚ญ์ œ ์™„๋ฃŒ: reviewId={}, memberId={}", reviewId, memberId); + + return ReviewDeleteResponse.builder() + .success(true) + .message("๋ฆฌ๋ทฐ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค") + .build(); + } + + @Override + @Transactional(readOnly = true) + public List getStoreReviews(Long storeId, Integer page, Integer size) { + Pageable pageable = PageRequest.of(page != null ? page : 0, size != null ? size : 20); + Page reviews = reviewRepository.findReviewsByStoreId(storeId, pageable); + + return reviews.stream() + .filter(review -> review.getStatus() == ReviewStatus.ACTIVE) + .map(review -> ReviewListResponse.builder() + .reviewId(review.getId()) + .memberNickname(review.getMemberNickname()) + .rating(review.getRating()) + .content(review.getContent()) + .imageUrls(review.getImageUrls()) + .likeCount(review.getLikeCount()) + .dislikeCount(review.getDislikeCount()) + .createdAt(review.getCreatedAt()) + .build()) + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public ReviewDetailResponse getReviewDetail(Long reviewId) { + Review review = reviewRepository.findReviewById(reviewId) + .orElseThrow(() -> new BusinessException("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค")); + + if (review.getStatus() != ReviewStatus.ACTIVE) { + throw new BusinessException("์‚ญ์ œ๋˜์—ˆ๊ฑฐ๋‚˜ ์ˆจ๊ฒจ์ง„ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค"); + } + + return ReviewDetailResponse.builder() + .reviewId(review.getId()) + .storeId(review.getStoreId()) + .memberNickname(review.getMemberNickname()) + .rating(review.getRating()) + .content(review.getContent()) + .imageUrls(review.getImageUrls()) + .likeCount(review.getLikeCount()) + .dislikeCount(review.getDislikeCount()) + .createdAt(review.getCreatedAt()) + .build(); + } + + @Override + @Transactional(readOnly = true) + public List getMyReviews(Long memberId, Integer page, Integer size) { + Pageable pageable = PageRequest.of(page != null ? page : 0, size != null ? size : 20); + Page reviews = reviewRepository.findReviewsByMemberId(memberId, pageable); + + return reviews.stream() + .filter(review -> review.getStatus() == ReviewStatus.ACTIVE) + .map(review -> ReviewListResponse.builder() + .reviewId(review.getId()) + .memberNickname(review.getMemberNickname()) + .rating(review.getRating()) + .content(review.getContent()) + .imageUrls(review.getImageUrls()) + .likeCount(review.getLikeCount()) + .dislikeCount(review.getDislikeCount()) + .createdAt(review.getCreatedAt()) + .build()) + .collect(Collectors.toList()); + } +} diff --git a/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java b/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java index 6c66b1a..8c975ca 100644 --- a/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java +++ b/review/src/main/java/com/ktds/hi/review/infra/config/SecurityConfig.java @@ -1,50 +1,50 @@ -package com.ktds.hi.review.infra.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfigurationSource; - -import lombok.RequiredArgsConstructor; - -/** - * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค - * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • - */ -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class SecurityConfig { - - private final CorsConfigurationSource corsConfigurationSource; - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ - .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() - - // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .requestMatchers("/api/analytics/**").permitAll() - .requestMatchers("/api/action-plans/**").permitAll() - - // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ - .requestMatchers("/actuator/**").permitAll() - - // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .anyRequest().permitAll() - ); - - return http.build(); - } -} +package com.ktds.hi.review.infra.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfigurationSource; + +import lombok.RequiredArgsConstructor; + +/** + * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค + * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final CorsConfigurationSource corsConfigurationSource; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ + .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() + + // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .requestMatchers("/api/analytics/**").permitAll() + .requestMatchers("/api/action-plans/**").permitAll() + + // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ + .requestMatchers("/actuator/**").permitAll() + + // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .anyRequest().permitAll() + ); + + return http.build(); + } +} diff --git a/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java b/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java index ff1f042..da566ba 100644 --- a/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java +++ b/review/src/main/java/com/ktds/hi/review/infra/config/SwaggerConfig.java @@ -1,34 +1,34 @@ -package com.ktds.hi.review.infra.config; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.servers.Server; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class SwaggerConfig { - - @Bean - public OpenAPI openAPI() { - final String securitySchemeName = "Bearer Authentication"; - - return new OpenAPI() - .addServersItem(new Server().url("/")) - .info(new Info() - .title("ํ•˜์ด์˜ค๋” ๋ฆฌ๋ทฐ ๊ด€๋ฆฌ ์„œ๋น„์Šค API") - .description("๋ฆฌ๋ทฐ ์ž‘์„ฑ, ์กฐํšŒ, ์‚ญ์ œ, ๋ฐ˜์‘, ๋Œ“๊ธ€ ๋“ฑ ๋ฆฌ๋ทฐ ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” API") - .version("1.0.0")) - .addSecurityItem(new SecurityRequirement() - .addList(securitySchemeName)) - .components(new Components() - .addSecuritySchemes(securitySchemeName, new SecurityScheme() - .name(securitySchemeName) - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT"))); - } +package com.ktds.hi.review.infra.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + final String securitySchemeName = "Bearer Authentication"; + + return new OpenAPI() + .addServersItem(new Server().url("/")) + .info(new Info() + .title("ํ•˜์ด์˜ค๋” ๋ฆฌ๋ทฐ ๊ด€๋ฆฌ ์„œ๋น„์Šค API") + .description("๋ฆฌ๋ทฐ ์ž‘์„ฑ, ์กฐํšŒ, ์‚ญ์ œ, ๋ฐ˜์‘, ๋Œ“๊ธ€ ๋“ฑ ๋ฆฌ๋ทฐ ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” API") + .version("1.0.0")) + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } } \ No newline at end of file diff --git a/review/src/main/resources/application.yml b/review/src/main/resources/application.yml index f20d8a6..b07473c 100644 --- a/review/src/main/resources/application.yml +++ b/review/src/main/resources/application.yml @@ -1,42 +1,42 @@ -server: - port: ${REVIEW_SERVICE_PORT:8083} - -spring: - application: - name: review-service - - datasource: - url: ${REVIEW_DB_URL:jdbc:postgresql://20.214.91.15:5432/hiorder_review} - username: ${REVIEW_DB_USERNAME:hiorder_user} - password: ${REVIEW_DB_PASSWORD:hiorder_pass} - driver-class-name: org.postgresql.Driver - - jpa: - hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} - show-sql: ${JPA_SHOW_SQL:false} - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.PostgreSQLDialect - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - - servlet: - multipart: - max-file-size: ${MAX_FILE_SIZE:10MB} - max-request-size: ${MAX_REQUEST_SIZE:50MB} - -file-storage: - base-path: ${FILE_STORAGE_PATH:/var/hiorder/uploads} - allowed-extensions: jpg,jpeg,png,gif,webp - max-file-size: 10485760 # 10MB - -springdoc: - api-docs: - path: /api-docs - swagger-ui: - path: /swagger-ui.html +server: + port: ${REVIEW_SERVICE_PORT:8083} + +spring: + application: + name: review-service + + datasource: + url: ${REVIEW_DB_URL:jdbc:postgresql://20.214.91.15:5432/hiorder_review} + username: ${REVIEW_DB_USERNAME:hiorder_user} + password: ${REVIEW_DB_PASSWORD:hiorder_pass} + driver-class-name: org.postgresql.Driver + + jpa: + hibernate: + ddl-auto: ${JPA_DDL_AUTO:update} + show-sql: ${JPA_SHOW_SQL:false} + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + + servlet: + multipart: + max-file-size: ${MAX_FILE_SIZE:10MB} + max-request-size: ${MAX_REQUEST_SIZE:50MB} + +file-storage: + base-path: ${FILE_STORAGE_PATH:/var/hiorder/uploads} + allowed-extensions: jpg,jpeg,png,gif,webp + max-file-size: 10485760 # 10MB + +springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui.html diff --git a/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java b/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java index 5dc6ecc..010f058 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java +++ b/store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java @@ -1,4 +1,342 @@ -package com.ktds.hi.store.biz.service; - -public class StoreService { -} +// store/src/main/java/com/ktds/hi/store/biz/service/StoreService.java +package com.ktds.hi.store.biz.service; + +import com.ktds.hi.store.biz.usecase.in.StoreUseCase; +import com.ktds.hi.store.biz.usecase.out.*; +import com.ktds.hi.store.biz.domain.Store; +import com.ktds.hi.store.biz.domain.StoreStatus; +import com.ktds.hi.store.infra.dto.*; +import com.ktds.hi.common.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * ๋งค์žฅ ์„œ๋น„์Šค ๊ตฌํ˜„์ฒด + * Clean Architecture์˜ Application Service Layer + * + * @author ํ•˜์ด์˜ค๋” ๊ฐœ๋ฐœํŒ€ + * @version 1.0.0 + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StoreService implements StoreUseCase { + + private final StoreRepositoryPort storeRepositoryPort; + private final MenuRepositoryPort menuRepositoryPort; + private final StoreTagRepositoryPort storeTagRepositoryPort; + private final GeocodingPort geocodingPort; + private final CachePort cachePort; + private final EventPort eventPort; + + @Override + @Transactional + public StoreCreateResponse createStore(Long ownerId, StoreCreateRequest request) { + log.info("๋งค์žฅ ๋“ฑ๋ก ์‹œ์ž‘: ownerId={}, storeName={}", ownerId, request.getStoreName()); + + try { + // 1. ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ + validateStoreCreateRequest(request); + + // 2. ์ ์ฃผ ๋งค์žฅ ๊ฐœ์ˆ˜ ์ œํ•œ ํ™•์ธ (์˜ˆ: ์ตœ๋Œ€ 10๊ฐœ) + validateOwnerStoreLimit(ownerId); + + // 3. ์ฃผ์†Œ ์ง€์˜ค์ฝ”๋”ฉ (์ขŒํ‘œ ๋ณ€ํ™˜) + Coordinates coordinates = geocodingPort.getCoordinates(request.getAddress()); + + // 4. Store ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ + Store store = Store.builder() + .ownerId(ownerId) + .storeName(request.getStoreName()) + .address(request.getAddress()) + .latitude(coordinates.getLatitude()) + .longitude(coordinates.getLongitude()) + .description(request.getDescription()) + .phone(request.getPhone()) + .operatingHours(request.getOperatingHours()) + .category(request.getCategory()) + .status(StoreStatus.ACTIVE) + .rating(0.0) + .reviewCount(0) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + // 5. ๋งค์žฅ ์ €์žฅ + Store savedStore = storeRepositoryPort.saveStore(store); + + // 6. ๋งค์žฅ ํƒœ๊ทธ ์ €์žฅ + if (request.getTags() != null && !request.getTags().isEmpty()) { + storeTagRepositoryPort.saveStoreTags(savedStore.getId(), request.getTags()); + } + + // 7. ๋ฉ”๋‰ด ์ •๋ณด ์ €์žฅ + if (request.getMenus() != null && !request.getMenus().isEmpty()) { + menuRepositoryPort.saveMenus(savedStore.getId(), + request.getMenus().stream() + .map(menuReq -> menuReq.toDomain(savedStore.getId())) + .collect(Collectors.toList())); + } + + // 8. ๋งค์žฅ ์ƒ์„ฑ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + eventPort.publishStoreCreatedEvent(savedStore); + + // 9. ์บ์‹œ ๋ฌดํšจํ™” + cachePort.invalidateStoreCache(ownerId); + + log.info("๋งค์žฅ ๋“ฑ๋ก ์™„๋ฃŒ: storeId={}", savedStore.getId()); + + return StoreCreateResponse.builder() + .storeId(savedStore.getId()) + .storeName(savedStore.getStoreName()) + .message("๋งค์žฅ์ด ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + .build(); + + } catch (Exception e) { + log.error("๋งค์žฅ ๋“ฑ๋ก ์‹คํŒจ: ownerId={}, error={}", ownerId, e.getMessage(), e); + throw new BusinessException("STORE_CREATE_FAILED", "๋งค์žฅ ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: " + e.getMessage()); + } + } + + @Override + public List getMyStores(Long ownerId) { + log.info("๋‚ด ๋งค์žฅ ๋ชฉ๋ก ์กฐํšŒ: ownerId={}", ownerId); + + // 1. ์บ์‹œ ํ™•์ธ + String cacheKey = "stores:owner:" + ownerId; + List cachedStores = cachePort.getStoreCache(cacheKey); + if (cachedStores != null) { + log.info("์บ์‹œ์—์„œ ๋งค์žฅ ๋ชฉ๋ก ๋ฐ˜ํ™˜: ownerId={}, count={}", ownerId, cachedStores.size()); + return cachedStores; + } + + // 2. DB์—์„œ ๋งค์žฅ ๋ชฉ๋ก ์กฐํšŒ + List stores = storeRepositoryPort.findStoresByOwnerId(ownerId); + + // 3. ์‘๋‹ต DTO ๋ณ€ํ™˜ + List responses = stores.stream() + .map(store -> { + String status = calculateStoreStatus(store); + return MyStoreListResponse.builder() + .storeId(store.getId()) + .storeName(store.getStoreName()) + .address(store.getAddress()) + .category(store.getCategory()) + .rating(store.getRating()) + .reviewCount(store.getReviewCount()) + .status(status) + .operatingHours(store.getOperatingHours()) + .build(); + }) + .collect(Collectors.toList()); + + // 4. ์บ์‹œ ์ €์žฅ (1์‹œ๊ฐ„) + cachePort.putStoreCache(cacheKey, responses, 3600); + + log.info("๋‚ด ๋งค์žฅ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ: ownerId={}, count={}", ownerId, responses.size()); + return responses; + } + + @Override + public StoreDetailResponse getStoreDetail(Long storeId) { + log.info("๋งค์žฅ ์ƒ์„ธ ์กฐํšŒ: storeId={}", storeId); + + // 1. ๋งค์žฅ ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ + Store store = storeRepositoryPort.findStoreById(storeId) + .orElseThrow(() -> new BusinessException("STORE_NOT_FOUND", "๋งค์žฅ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + // 2. ๋งค์žฅ ํƒœ๊ทธ ์กฐํšŒ + List tags = storeTagRepositoryPort.findTagsByStoreId(storeId); + + // 3. ๋ฉ”๋‰ด ์ •๋ณด ์กฐํšŒ + List menus = menuRepositoryPort.findMenusByStoreId(storeId) + .stream() + .map(MenuResponse::from) + .collect(Collectors.toList()); + + // 4. AI ์š”์•ฝ ์ •๋ณด ์กฐํšŒ (์™ธ๋ถ€ ์„œ๋น„์Šค) + String aiSummary = getAISummary(storeId); + + return StoreDetailResponse.builder() + .storeId(store.getId()) + .storeName(store.getStoreName()) + .address(store.getAddress()) + .latitude(store.getLatitude()) + .longitude(store.getLongitude()) + .description(store.getDescription()) + .phone(store.getPhone()) + .operatingHours(store.getOperatingHours()) + .category(store.getCategory()) + .rating(store.getRating()) + .reviewCount(store.getReviewCount()) + .status(store.getStatus().name()) + .tags(tags) + .menus(menus) + .aiSummary(aiSummary) + .build(); + } + + @Override + @Transactional + public StoreUpdateResponse updateStore(Long storeId, Long ownerId, StoreUpdateRequest request) { + log.info("๋งค์žฅ ์ •๋ณด ์ˆ˜์ •: storeId={}, ownerId={}", storeId, ownerId); + + // 1. ๋งค์žฅ ์†Œ์œ ๊ถŒ ํ™•์ธ + Store store = validateStoreOwnership(storeId, ownerId); + + // 2. ์ฃผ์†Œ ๋ณ€๊ฒฝ ์‹œ ์ง€์˜ค์ฝ”๋”ฉ + Coordinates coordinates = null; + if (!store.getAddress().equals(request.getAddress())) { + coordinates = geocodingPort.getCoordinates(request.getAddress()); + } + + // 3. ๋งค์žฅ ์ •๋ณด ์—…๋ฐ์ดํŠธ + store.updateBasicInfo( + request.getStoreName(), + request.getAddress(), + request.getDescription(), + request.getPhone(), + request.getOperatingHours() + ); + + if (coordinates != null) { + store.updateLocation(coordinates); + } + + Store updatedStore = storeRepositoryPort.saveStore(store); + + // 4. ํƒœ๊ทธ ์—…๋ฐ์ดํŠธ + if (request.getTags() != null) { + storeTagRepositoryPort.deleteTagsByStoreId(storeId); + storeTagRepositoryPort.saveStoreTags(storeId, request.getTags()); + } + + // 5. ๋งค์žฅ ์ˆ˜์ • ์ด๋ฒคํŠธ ๋ฐœํ–‰ + eventPort.publishStoreUpdatedEvent(updatedStore); + + // 6. ์บ์‹œ ๋ฌดํšจํ™” + cachePort.invalidateStoreCache(storeId); + cachePort.invalidateStoreCache(ownerId); + + return StoreUpdateResponse.builder() + .storeId(storeId) + .message("๋งค์žฅ ์ •๋ณด๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + .build(); + } + + @Override + @Transactional + public StoreDeleteResponse deleteStore(Long storeId, Long ownerId) { + log.info("๋งค์žฅ ์‚ญ์ œ: storeId={}, ownerId={}", storeId, ownerId); + + // 1. ๋งค์žฅ ์†Œ์œ ๊ถŒ ํ™•์ธ + Store store = validateStoreOwnership(storeId, ownerId); + + // 2. ์†Œํ”„ํŠธ ์‚ญ์ œ (์ƒํƒœ ๋ณ€๊ฒฝ) + store.delete(); + storeRepositoryPort.saveStore(store); + + // 3. ๋งค์žฅ ์‚ญ์ œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + eventPort.publishStoreDeletedEvent(storeId); + + // 4. ์บ์‹œ ๋ฌดํšจํ™” + cachePort.invalidateStoreCache(storeId); + cachePort.invalidateStoreCache(ownerId); + + return StoreDeleteResponse.builder() + .storeId(storeId) + .message("๋งค์žฅ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + .build(); + } + + @Override + public List searchStores(String keyword, String category, String tags, + Double latitude, Double longitude, Integer radius, + Integer page, Integer size) { + log.info("๋งค์žฅ ๊ฒ€์ƒ‰: keyword={}, category={}, location=({}, {})", keyword, category, latitude, longitude); + + StoreSearchCriteria criteria = StoreSearchCriteria.builder() + .keyword(keyword) + .category(category) + .tags(tags) + .latitude(latitude) + .longitude(longitude) + .radius(radius) + .page(page) + .size(size) + .build(); + + List stores = storeRepositoryPort.searchStores(criteria); + + return stores.stream() + .map(store -> StoreSearchResponse.builder() + .storeId(store.getId()) + .storeName(store.getStoreName()) + .address(store.getAddress()) + .category(store.getCategory()) + .rating(store.getRating()) + .reviewCount(store.getReviewCount()) + .distance(calculateDistance(latitude, longitude, store.getLatitude(), store.getLongitude())) + .build()) + .collect(Collectors.toList()); + } + + // === Private Helper Methods === + + private void validateStoreCreateRequest(StoreCreateRequest request) { + if (request.getStoreName() == null || request.getStoreName().trim().isEmpty()) { + throw new BusinessException("INVALID_STORE_NAME", "๋งค์žฅ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + if (request.getStoreName().length() > 100) { + throw new BusinessException("INVALID_STORE_NAME", "๋งค์žฅ๋ช…์€ 100์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + if (request.getAddress() == null || request.getAddress().trim().isEmpty()) { + throw new BusinessException("INVALID_ADDRESS", "์ฃผ์†Œ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."); + } + if (request.getPhone() != null && !request.getPhone().matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) { + throw new BusinessException("INVALID_PHONE", "์ „ํ™”๋ฒˆํ˜ธ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + } + + private void validateOwnerStoreLimit(Long ownerId) { + Long storeCount = storeRepositoryPort.countStoresByOwnerId(ownerId); + if (storeCount >= 10) { + throw new BusinessException("STORE_LIMIT_EXCEEDED", "๋งค์žฅ์€ ์ตœ๋Œ€ 10๊ฐœ๊นŒ์ง€ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."); + } + } + + private Store validateStoreOwnership(Long storeId, Long ownerId) { + return storeRepositoryPort.findStoreByIdAndOwnerId(storeId, ownerId) + .orElseThrow(() -> new BusinessException("STORE_ACCESS_DENIED", "๋งค์žฅ์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.")); + } + + private String calculateStoreStatus(Store store) { + if (!store.isActive()) { + return "๋น„ํ™œ์„ฑ"; + } + // ์šด์˜์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ํ˜„์žฌ ์ƒํƒœ ๊ณ„์‚ฐ ๋กœ์ง + return "์šด์˜์ค‘"; + } + + private String getAISummary(Long storeId) { + // TODO: AI ๋ถ„์„ ์„œ๋น„์Šค ์—ฐ๋™ ๊ตฌํ˜„ + return "AI ์š”์•ฝ ์ •๋ณด๊ฐ€ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค."; + } + + private Double calculateDistance(Double lat1, Double lon1, Double lat2, Double lon2) { + if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) { + return null; + } + return geocodingPort.calculateDistance( + new Coordinates(lat1, lon1), + new Coordinates(lat2, lon2) + ); + } +} diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/CachePort.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/CachePort.java index dd78c30..34fd4f9 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/CachePort.java +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/CachePort.java @@ -1,26 +1,24 @@ -package com.ktds.hi.store.biz.usecase.out; - -import java.time.Duration; -import java.util.Optional; - -/** - * ์บ์‹œ ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค - * ์บ์‹œ ๊ธฐ๋Šฅ์„ ์ •์˜ - */ -public interface CachePort { - - /** - * ์บ์‹œ์—์„œ ๋งค์žฅ ๋ฐ์ดํ„ฐ ์กฐํšŒ - */ - Optional getStoreCache(String key); - - /** - * ์บ์‹œ์— ๋งค์žฅ ๋ฐ์ดํ„ฐ ์ €์žฅ - */ - void putStoreCache(String key, Object value, Duration ttl); - - /** - * ์บ์‹œ ๋ฌดํšจํ™” - */ - void invalidateStoreCache(Long storeId); -} +package com.ktds.hi.store.biz.usecase.out; + +import java.util.List; + +/** + * ์บ์‹œ ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค + */ +public interface CachePort { + + /** + * ์บ์‹œ์—์„œ ๋งค์žฅ ์ •๋ณด ์กฐํšŒ + */ + T getStoreCache(String key); + + /** + * ์บ์‹œ์— ๋งค์žฅ ์ •๋ณด ์ €์žฅ + */ + void putStoreCache(String key, Object value, long ttlSeconds); + + /** + * ์บ์‹œ ๋ฌดํšจํ™” + */ + void invalidateStoreCache(Object key); +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/Coordinates.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/Coordinates.java index 14b01f4..d16a672 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/Coordinates.java +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/Coordinates.java @@ -1,4 +1,16 @@ -package com.ktds.hi.store.biz.usecase.out; - -public class Coordinates { -} +package com.ktds.hi.store.biz.usecase.out; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ์ขŒํ‘œ ์ •๋ณด ๊ฐ’ ๊ฐ์ฒด + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Coordinates { + private Double latitude; + private Double longitude; +} diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/GeocodingPort.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/GeocodingPort.java index b087862..daebf40 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/GeocodingPort.java +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/GeocodingPort.java @@ -1,4 +1,17 @@ -package com.ktds.hi.store.biz.usecase.out; - -public interface GeocodingPort { -} +package com.ktds.hi.store.biz.usecase.out; + +/** + * ์ง€์˜ค์ฝ”๋”ฉ ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค + */ +public interface GeocodingPort { + + /** + * ์ฃผ์†Œ๋ฅผ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ + */ + Coordinates getCoordinates(String address); + + /** + * ๋‘ ์ขŒํ‘œ ๊ฐ„ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (km) + */ + Double calculateDistance(Coordinates coord1, Coordinates coord2); +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/StoreTagRepositoryPort.java b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/StoreTagRepositoryPort.java index 1b4cc3b..2125a58 100644 --- a/store/src/main/java/com/ktds/hi/store/biz/usecase/out/StoreTagRepositoryPort.java +++ b/store/src/main/java/com/ktds/hi/store/biz/usecase/out/StoreTagRepositoryPort.java @@ -1,4 +1,24 @@ -package com.ktds.hi.store.biz.usecase.out; - -public interface StoreTagRepositoryPort { -} +package com.ktds.hi.store.biz.usecase.out; + +import java.util.List; + +/** + * ๋งค์žฅ ํƒœ๊ทธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค + */ +public interface StoreTagRepositoryPort { + + /** + * ๋งค์žฅ ID๋กœ ํƒœ๊ทธ ๋ชฉ๋ก ์กฐํšŒ + */ + List findTagsByStoreId(Long storeId); + + /** + * ๋งค์žฅ ํƒœ๊ทธ ์ €์žฅ + */ + void saveStoreTags(Long storeId, List tags); + + /** + * ๋งค์žฅ ํƒœ๊ทธ ์‚ญ์ œ + */ + void deleteTagsByStoreId(Long storeId); +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java b/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java index 1c1d436..74dba58 100644 --- a/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java +++ b/store/src/main/java/com/ktds/hi/store/config/SecurityConfig.java @@ -1,50 +1,50 @@ -package com.ktds.hi.store.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfigurationSource; - -import lombok.RequiredArgsConstructor; - -/** - * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค - * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • - */ -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class SecurityConfig { - - private final CorsConfigurationSource corsConfigurationSource; - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ - .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() - - // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .requestMatchers("/api/analytics/**").permitAll() - .requestMatchers("/api/action-plans/**").permitAll() - - // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ - .requestMatchers("/actuator/**").permitAll() - - // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) - .anyRequest().permitAll() - ); - - return http.build(); - } -} +package com.ktds.hi.store.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfigurationSource; + +import lombok.RequiredArgsConstructor; + +/** + * Analytics ์„œ๋น„์Šค ๋ณด์•ˆ ์„ค์ • ํด๋ž˜์Šค + * ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final CorsConfigurationSource corsConfigurationSource; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Swagger ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชจ๋‘ ํ—ˆ์šฉ + .requestMatchers("/swagger-ui.html","/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api-docs/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/swagger-resources/**", "/webjars/**").permitAll() + + // Analytics API ๋ชจ๋‘ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .requestMatchers("/api/analytics/**").permitAll() + .requestMatchers("/api/action-plans/**").permitAll() + + // Actuator ์—”๋“œํฌ์ธํŠธ ํ—ˆ์šฉ + .requestMatchers("/actuator/**").permitAll() + + // ๊ธฐํƒ€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ (ํ…Œ์ŠคํŠธ์šฉ) + .anyRequest().permitAll() + ); + + return http.build(); + } +} diff --git a/store/src/main/java/com/ktds/hi/store/domain/Menu.java b/store/src/main/java/com/ktds/hi/store/domain/Menu.java index 691f6b4..131247a 100644 --- a/store/src/main/java/com/ktds/hi/store/domain/Menu.java +++ b/store/src/main/java/com/ktds/hi/store/domain/Menu.java @@ -1,23 +1,13 @@ package com.ktds.hi.store.biz.domain; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; /** - * ๋ฉ”๋‰ด ๋„๋ฉ”์ธ ํด๋ž˜์Šค - * ๋ฉ”๋‰ด ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด - * - * @author ํ•˜์ด์˜ค๋” ๊ฐœ๋ฐœํŒ€ - * @version 1.0.0 + * ๋ฉ”๋‰ด ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ */ @Getter @Builder -@NoArgsConstructor -@AllArgsConstructor public class Menu { private Long id; @@ -27,164 +17,31 @@ public class Menu { private Integer price; private String category; private String imageUrl; - private Boolean isAvailable; - private Integer orderCount; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + private Boolean available; /** - * ๋ฉ”๋‰ด ๊ธฐ๋ณธ ์ •๋ณด ์—…๋ฐ์ดํŠธ + * ๋ฉ”๋‰ด ์ •๋ณด ์—…๋ฐ์ดํŠธ */ - public Menu updateInfo(String menuName, String description, Integer price) { - return Menu.builder() - .id(this.id) - .storeId(this.storeId) - .menuName(menuName) - .description(description) - .price(price) - .category(this.category) - .imageUrl(this.imageUrl) - .isAvailable(this.isAvailable) - .orderCount(this.orderCount) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); + public void updateMenuInfo(String menuName, String description, Integer price, + String category, String imageUrl) { + this.menuName = menuName; + this.description = description; + this.price = price; + this.category = category; + this.imageUrl = imageUrl; } /** - * ๋ฉ”๋‰ด ์ด๋ฏธ์ง€ ์—…๋ฐ์ดํŠธ + * ๋ฉ”๋‰ด ํŒ๋งค ์ƒํƒœ ๋ณ€๊ฒฝ */ - public Menu updateImage(String imageUrl) { - return Menu.builder() - .id(this.id) - .storeId(this.storeId) - .menuName(this.menuName) - .description(this.description) - .price(this.price) - .category(this.category) - .imageUrl(imageUrl) - .isAvailable(this.isAvailable) - .orderCount(this.orderCount) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); + public void setAvailable(Boolean available) { + this.available = available; } /** - * ๋ฉ”๋‰ด ํŒ๋งค ๊ฐ€๋Šฅ ์ƒํƒœ ์„ค์ • + * ๋ฉ”๋‰ด ์ด์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ */ - public Menu setAvailable(Boolean available) { - return Menu.builder() - .id(this.id) - .storeId(this.storeId) - .menuName(this.menuName) - .description(this.description) - .price(this.price) - .category(this.category) - .imageUrl(this.imageUrl) - .isAvailable(available) - .orderCount(this.orderCount) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); - } - - /** - * ์ฃผ๋ฌธ ์ˆ˜ ์ฆ๊ฐ€ - */ - public Menu incrementOrderCount() { - return Menu.builder() - .id(this.id) - .storeId(this.storeId) - .menuName(this.menuName) - .description(this.description) - .price(this.price) - .category(this.category) - .imageUrl(this.imageUrl) - .isAvailable(this.isAvailable) - .orderCount(this.orderCount != null ? this.orderCount + 1 : 1) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); - } - - /** - * ๋ฉ”๋‰ด ํŒ๋งค ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ - */ - public Boolean isAvailable() { - return this.isAvailable != null && this.isAvailable; - } - - /** - * ํŠน์ • ๋งค์žฅ์— ์†ํ•˜๋Š”์ง€ ํ™•์ธ - */ - public boolean belongsToStore(Long storeId) { - return this.storeId != null && this.storeId.equals(storeId); - } - - /** - * ๋ฉ”๋‰ด๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธ - */ - public boolean isValid() { - return this.menuName != null && !this.menuName.trim().isEmpty() && - this.price != null && this.price > 0 && - this.storeId != null; - } - - /** - * ๋ฉ”๋‰ด ์นดํ…Œ๊ณ ๋ฆฌ ์—…๋ฐ์ดํŠธ - */ - public Menu updateCategory(String category) { - return Menu.builder() - .id(this.id) - .storeId(this.storeId) - .menuName(this.menuName) - .description(this.description) - .price(this.price) - .category(category) - .imageUrl(this.imageUrl) - .isAvailable(this.isAvailable) - .orderCount(this.orderCount) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); - } - - /** - * ๋ฉ”๋‰ด ๊ฐ€๊ฒฉ ํ• ์ธ ์ ์šฉ - */ - public Menu applyDiscount(double discountRate) { - if (discountRate < 0 || discountRate > 1) { - throw new IllegalArgumentException("ํ• ์ธ์œจ์€ 0~1 ์‚ฌ์ด์˜ ๊ฐ’์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - - int discountedPrice = (int) (this.price * (1 - discountRate)); - - return Menu.builder() - .id(this.id) - .storeId(this.storeId) - .menuName(this.menuName) - .description(this.description) - .price(discountedPrice) - .category(this.category) - .imageUrl(this.imageUrl) - .isAvailable(this.isAvailable) - .orderCount(this.orderCount) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); - } - - /** - * ๋ฉ”๋‰ด ์ •๋ณด๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - */ - public boolean hasChanges(Menu other) { - if (other == null) return true; - - return !this.menuName.equals(other.menuName) || - !this.description.equals(other.description) || - !this.price.equals(other.price) || - !this.category.equals(other.category) || - !this.isAvailable.equals(other.isAvailable); + public boolean isAvailable() { + return this.available != null && this.available; } } \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/domain/Store.java b/store/src/main/java/com/ktds/hi/store/domain/Store.java index cee1a87..85457a4 100644 --- a/store/src/main/java/com/ktds/hi/store/domain/Store.java +++ b/store/src/main/java/com/ktds/hi/store/domain/Store.java @@ -1,24 +1,19 @@ +// store/src/main/java/com/ktds/hi/store/biz/domain/Store.java package com.ktds.hi.store.domain; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; - import java.time.LocalDateTime; -import java.util.List; /** - * ๋งค์žฅ ๋„๋ฉ”์ธ ํด๋ž˜์Šค - * ๋งค์žฅ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด + * ๋งค์žฅ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ + * Clean Architecture์˜ Domain Layer * * @author ํ•˜์ด์˜ค๋” ๊ฐœ๋ฐœํŒ€ * @version 1.0.0 */ @Getter @Builder -@NoArgsConstructor -@AllArgsConstructor public class Store { private Long id; @@ -27,160 +22,87 @@ public class Store { private String address; private Double latitude; private Double longitude; - private String category; private String description; private String phone; private String operatingHours; - private List tags; - private StoreStatus status; + private String category; private Double rating; private Integer reviewCount; - private String imageUrl; + private StoreStatus status; private LocalDateTime createdAt; private LocalDateTime updatedAt; /** * ๋งค์žฅ ๊ธฐ๋ณธ ์ •๋ณด ์—…๋ฐ์ดํŠธ */ - public Store updateBasicInfo(String storeName, String address, String description, - String phone, String operatingHours) { - return Store.builder() - .id(this.id) - .ownerId(this.ownerId) - .storeName(storeName) - .address(address) - .latitude(this.latitude) - .longitude(this.longitude) - .category(this.category) - .description(description) - .phone(phone) - .operatingHours(operatingHours) - .tags(this.tags) - .status(this.status) - .rating(this.rating) - .reviewCount(this.reviewCount) - .imageUrl(this.imageUrl) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); + public void updateBasicInfo(String storeName, String address, String description, + String phone, String operatingHours) { + this.storeName = storeName; + this.address = address; + this.description = description; + this.phone = phone; + this.operatingHours = operatingHours; + this.updatedAt = LocalDateTime.now(); } /** - * ๋งค์žฅ ์œ„์น˜ ์ •๋ณด ์—…๋ฐ์ดํŠธ + * ๋งค์žฅ ์œ„์น˜ ์—…๋ฐ์ดํŠธ */ - public Store updateLocation(Double latitude, Double longitude) { - return Store.builder() - .id(this.id) - .ownerId(this.ownerId) - .storeName(this.storeName) - .address(this.address) - .latitude(latitude) - .longitude(longitude) - .category(this.category) - .description(this.description) - .phone(this.phone) - .operatingHours(this.operatingHours) - .tags(this.tags) - .status(this.status) - .rating(this.rating) - .reviewCount(this.reviewCount) - .imageUrl(this.imageUrl) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); + public void updateLocation(Coordinates coordinates) { + this.latitude = coordinates.getLatitude(); + this.longitude = coordinates.getLongitude(); + this.updatedAt = LocalDateTime.now(); } /** - * ๋งค์žฅ ํ‰์  ๋ฐ ๋ฆฌ๋ทฐ ์ˆ˜ ์—…๋ฐ์ดํŠธ + * ๋งค์žฅ ํ‰์  ์—…๋ฐ์ดํŠธ */ - public Store updateRating(Double rating, Integer reviewCount) { - return Store.builder() - .id(this.id) - .ownerId(this.ownerId) - .storeName(this.storeName) - .address(this.address) - .latitude(this.latitude) - .longitude(this.longitude) - .category(this.category) - .description(this.description) - .phone(this.phone) - .operatingHours(this.operatingHours) - .tags(this.tags) - .status(this.status) - .rating(rating) - .reviewCount(reviewCount) - .imageUrl(this.imageUrl) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); + public void updateRating(Double rating, Integer reviewCount) { + this.rating = rating; + this.reviewCount = reviewCount; + this.updatedAt = LocalDateTime.now(); } /** * ๋งค์žฅ ํ™œ์„ฑํ™” */ - public Store activate() { - return Store.builder() - .id(this.id) - .ownerId(this.ownerId) - .storeName(this.storeName) - .address(this.address) - .latitude(this.latitude) - .longitude(this.longitude) - .category(this.category) - .description(this.description) - .phone(this.phone) - .operatingHours(this.operatingHours) - .tags(this.tags) - .status(StoreStatus.ACTIVE) - .rating(this.rating) - .reviewCount(this.reviewCount) - .imageUrl(this.imageUrl) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); + public void activate() { + this.status = StoreStatus.ACTIVE; + this.updatedAt = LocalDateTime.now(); } /** * ๋งค์žฅ ๋น„ํ™œ์„ฑํ™” */ - public Store deactivate() { - return Store.builder() - .id(this.id) - .ownerId(this.ownerId) - .storeName(this.storeName) - .address(this.address) - .latitude(this.latitude) - .longitude(this.longitude) - .category(this.category) - .description(this.description) - .phone(this.phone) - .operatingHours(this.operatingHours) - .tags(this.tags) - .status(StoreStatus.INACTIVE) - .rating(this.rating) - .reviewCount(this.reviewCount) - .imageUrl(this.imageUrl) - .createdAt(this.createdAt) - .updatedAt(LocalDateTime.now()) - .build(); + public void deactivate() { + this.status = StoreStatus.INACTIVE; + this.updatedAt = LocalDateTime.now(); } /** - * ๋งค์žฅ ํ™œ์„ฑ ์ƒํƒœ ํ™•์ธ + * ๋งค์žฅ ์‚ญ์ œ (์†Œํ”„ํŠธ ์‚ญ์ œ) + */ + public void delete() { + this.status = StoreStatus.DELETED; + this.updatedAt = LocalDateTime.now(); + } + + /** + * ํ™œ์„ฑ ์ƒํƒœ ํ™•์ธ */ public boolean isActive() { - return StoreStatus.ACTIVE.equals(this.status); + return this.status == StoreStatus.ACTIVE; } /** - * ๋งค์žฅ ์†Œ์œ ๊ถŒ ํ™•์ธ + * ์ ์ฃผ ์†Œ์œ  ํ™•์ธ */ public boolean isOwnedBy(Long ownerId) { - return this.ownerId != null && this.ownerId.equals(ownerId); + return this.ownerId.equals(ownerId); } /** - * ๋‘ ์ขŒํ‘œ ๊ฐ„์˜ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (ํ‚ฌ๋กœ๋ฏธํ„ฐ) + * ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ */ public Double calculateDistance(Double targetLatitude, Double targetLongitude) { if (this.latitude == null || this.longitude == null || @@ -188,17 +110,18 @@ public class Store { return null; } - final int EARTH_RADIUS = 6371; // ์ง€๊ตฌ ๋ฐ˜์ง€๋ฆ„ (ํ‚ฌ๋กœ๋ฏธํ„ฐ) + // Haversine ๊ณต์‹์„ ์‚ฌ์šฉํ•œ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ + double earthRadius = 6371; // ์ง€๊ตฌ ๋ฐ˜์ง€๋ฆ„ (km) - double latDistance = Math.toRadians(targetLatitude - this.latitude); - double lonDistance = Math.toRadians(targetLongitude - this.longitude); + double dLat = Math.toRadians(targetLatitude - this.latitude); + double dLon = Math.toRadians(targetLongitude - this.longitude); - double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) - + Math.cos(Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(targetLatitude)) - * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2); + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(targetLatitude)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return EARTH_RADIUS * c; + return earthRadius * c; } } \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/domain/StoreStatus.java b/store/src/main/java/com/ktds/hi/store/domain/StoreStatus.java index d81a6d6..bb1dc91 100644 --- a/store/src/main/java/com/ktds/hi/store/domain/StoreStatus.java +++ b/store/src/main/java/com/ktds/hi/store/domain/StoreStatus.java @@ -2,32 +2,12 @@ package com.ktds.hi.store.domain; /** * ๋งค์žฅ ์ƒํƒœ ์—ด๊ฑฐํ˜• - * ๋งค์žฅ์˜ ์šด์˜ ์ƒํƒœ๋ฅผ ์ •์˜ - * - * @author ํ•˜์ด์˜ค๋” ๊ฐœ๋ฐœํŒ€ - * @version 1.0.0 */ public enum StoreStatus { - - /** - * ํ™œ์„ฑ ์ƒํƒœ - ์ •์ƒ ์šด์˜ ์ค‘ - */ ACTIVE("ํ™œ์„ฑ"), - - /** - * ๋น„ํ™œ์„ฑ ์ƒํƒœ - ์ž„์‹œ ํœด์—… - */ INACTIVE("๋น„ํ™œ์„ฑ"), - - /** - * ์ผ์‹œ ์ •์ง€ ์ƒํƒœ - ๊ด€๋ฆฌ์ž์— ์˜ํ•œ ์ผ์‹œ ์ •์ง€ - */ - SUSPENDED("์ผ์‹œ์ •์ง€"), - - /** - * ์‚ญ์ œ ์ƒํƒœ - ์˜๊ตฌ ์‚ญ์ œ (์†Œํ”„ํŠธ ์‚ญ์ œ) - */ - DELETED("์‚ญ์ œ"); + DELETED("์‚ญ์ œ๋จ"), + PENDING("์Šน์ธ๋Œ€๊ธฐ"); private final String description; @@ -38,33 +18,4 @@ public enum StoreStatus { public String getDescription() { return description; } - - /** - * ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ StoreStatus ๋ณ€ํ™˜ - */ - public static StoreStatus fromString(String status) { - if (status == null) { - return INACTIVE; - } - - try { - return StoreStatus.valueOf(status.toUpperCase()); - } catch (IllegalArgumentException e) { - return INACTIVE; - } - } - - /** - * ๋งค์žฅ์ด ์„œ๋น„์Šค ๊ฐ€๋Šฅํ•œ ์ƒํƒœ์ธ์ง€ ํ™•์ธ - */ - public boolean isServiceable() { - return this == ACTIVE; - } - - /** - * ๋งค์žฅ์ด ์‚ญ์ œ๋œ ์ƒํƒœ์ธ์ง€ ํ™•์ธ - */ - public boolean isDeleted() { - return this == DELETED; - } } \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java b/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java index b8e6cf1..00cbbf3 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java +++ b/store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java @@ -1,4 +1,120 @@ -package com.ktds.hi.store.infra.controller; - -public class StoreController { -} +// store/src/main/java/com/ktds/hi/store/infra/controller/StoreController.java +package com.ktds.hi.store.infra.controller; + +import com.ktds.hi.store.biz.usecase.in.StoreUseCase; +import com.ktds.hi.store.infra.dto.*; +import com.ktds.hi.common.dto.ApiResponse; +import com.ktds.hi.common.security.JwtTokenProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import java.util.List; + +/** + * ๋งค์žฅ ๊ด€๋ฆฌ ์ปจํŠธ๋กค๋Ÿฌ + * ๋งค์žฅ ๋“ฑ๋ก, ์ˆ˜์ •, ์‚ญ์ œ, ์กฐํšŒ ๊ธฐ๋Šฅ ์ œ๊ณต + * + * @author ํ•˜์ด์˜ค๋” ๊ฐœ๋ฐœํŒ€ + * @version 1.0.0 + */ +@Tag(name = "๋งค์žฅ ๊ด€๋ฆฌ", description = "๋งค์žฅ ๋“ฑ๋ก, ์ˆ˜์ •, ์‚ญ์ œ, ์กฐํšŒ API") +@Slf4j +@RestController +@RequestMapping("/api/stores") +@RequiredArgsConstructor +@Validated +public class StoreController { + + private final StoreUseCase storeUseCase; + private final JwtTokenProvider jwtTokenProvider; + + @Operation(summary = "๋งค์žฅ ๋“ฑ๋ก", description = "์ƒˆ๋กœ์šด ๋งค์žฅ์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.") + @PostMapping + @PreAuthorize("hasRole('OWNER')") + public ResponseEntity> createStore( + @Valid @RequestBody StoreCreateRequest request, + HttpServletRequest httpRequest) { + + Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest); + StoreCreateResponse response = storeUseCase.createStore(ownerId, request); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(response, "๋งค์žฅ์ด ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")); + } + + @Operation(summary = "๋‚ด ๋งค์žฅ ๋ชฉ๋ก ์กฐํšŒ", description = "์ ์ฃผ๊ฐ€ ๋“ฑ๋กํ•œ ๋งค์žฅ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/my") + @PreAuthorize("hasRole('OWNER')") + public ResponseEntity>> getMyStores( + HttpServletRequest httpRequest) { + + Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest); + List responses = storeUseCase.getMyStores(ownerId); + + return ResponseEntity.ok(ApiResponse.success(responses, "๋‚ด ๋งค์žฅ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ")); + } + + @Operation(summary = "๋งค์žฅ ์ƒ์„ธ ์กฐํšŒ", description = "๋งค์žฅ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/{storeId}") + public ResponseEntity> getStoreDetail( + @Parameter(description = "๋งค์žฅ ID") @PathVariable Long storeId) { + + StoreDetailResponse response = storeUseCase.getStoreDetail(storeId); + return ResponseEntity.ok(ApiResponse.success(response, "๋งค์žฅ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์™„๋ฃŒ")); + } + + @Operation(summary = "๋งค์žฅ ์ •๋ณด ์ˆ˜์ •", description = "๋งค์žฅ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.") + @PutMapping("/{storeId}") + @PreAuthorize("hasRole('OWNER')") + public ResponseEntity> updateStore( + @Parameter(description = "๋งค์žฅ ID") @PathVariable Long storeId, + @Valid @RequestBody StoreUpdateRequest request, + HttpServletRequest httpRequest) { + + Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest); + StoreUpdateResponse response = storeUseCase.updateStore(storeId, ownerId, request); + + return ResponseEntity.ok(ApiResponse.success(response, "๋งค์žฅ ์ •๋ณด ์ˆ˜์ • ์™„๋ฃŒ")); + } + + @Operation(summary = "๋งค์žฅ ์‚ญ์ œ", description = "๋งค์žฅ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.") + @DeleteMapping("/{storeId}") + @PreAuthorize("hasRole('OWNER')") + public ResponseEntity> deleteStore( + @Parameter(description = "๋งค์žฅ ID") @PathVariable Long storeId, + HttpServletRequest httpRequest) { + + Long ownerId = jwtTokenProvider.extractOwnerInfo(httpRequest); + StoreDeleteResponse response = storeUseCase.deleteStore(storeId, ownerId); + + return ResponseEntity.ok(ApiResponse.success(response, "๋งค์žฅ ์‚ญ์ œ ์™„๋ฃŒ")); + } + + @Operation(summary = "๋งค์žฅ ๊ฒ€์ƒ‰", description = "์กฐ๊ฑด์— ๋”ฐ๋ผ ๋งค์žฅ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/search") + public ResponseEntity>> searchStores( + @Parameter(description = "๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ") @RequestParam(required = false) String keyword, + @Parameter(description = "์นดํ…Œ๊ณ ๋ฆฌ") @RequestParam(required = false) String category, + @Parameter(description = "ํƒœ๊ทธ") @RequestParam(required = false) String tags, + @Parameter(description = "์œ„๋„") @RequestParam(required = false) Double latitude, + @Parameter(description = "๊ฒฝ๋„") @RequestParam(required = false) Double longitude, + @Parameter(description = "๊ฒ€์ƒ‰ ๋ฐ˜๊ฒฝ(km)") @RequestParam(defaultValue = "5") Integer radius, + @Parameter(description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ") @RequestParam(defaultValue = "0") Integer page, + @Parameter(description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ") @RequestParam(defaultValue = "20") Integer size) { + + List responses = storeUseCase.searchStores( + keyword, category, tags, latitude, longitude, radius, page, size); + + return ResponseEntity.ok(ApiResponse.success(responses, "๋งค์žฅ ๊ฒ€์ƒ‰ ์™„๋ฃŒ")); + } +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/MenuCreateRequest.java b/store/src/main/java/com/ktds/hi/store/infra/dto/MenuCreateRequest.java index 245cc00..54b9eff 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/MenuCreateRequest.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/MenuCreateRequest.java @@ -1,4 +1,54 @@ -package com.ktds.hi.store.infra.dto; - -public class MenuCreateRequest { -} +package com.ktds.hi.store.infra.dto; + +import com.ktds.hi.store.biz.domain.Menu; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ๋ฉ”๋‰ด ๋“ฑ๋ก ์š”์ฒญ DTO + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋ฉ”๋‰ด ๋“ฑ๋ก ์š”์ฒญ") +public class MenuCreateRequest { + + @NotBlank(message = "๋ฉ”๋‰ด๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @Schema(description = "๋ฉ”๋‰ด๋ช…", example = "๊น€์น˜์ฐŒ๊ฐœ") + private String menuName; + + @Schema(description = "๋ฉ”๋‰ด ์„ค๋ช…", example = "์–ผํฐํ•œ ๊น€์น˜์ฐŒ๊ฐœ") + private String description; + + @Min(value = 0, message = "๊ฐ€๊ฒฉ์€ 0์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Schema(description = "๊ฐ€๊ฒฉ", example = "8000") + private Integer price; + + @Schema(description = "๋ฉ”๋‰ด ์นดํ…Œ๊ณ ๋ฆฌ", example = "๋ฉ”์ธ") + private String category; + + @Schema(description = "์ด๋ฏธ์ง€ URL", example = "https://example.com/kimchi.jpg") + private String imageUrl; + + @Schema(description = "์ด์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€", example = "true") + private Boolean available = true; + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ + */ + public Menu toDomain(Long storeId) { + return Menu.builder() + .storeId(storeId) + .menuName(this.menuName) + .description(this.description) + .price(this.price) + .category(this.category) + .imageUrl(this.imageUrl) + .available(this.available != null ? this.available : true) + .build(); + } +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/MenuResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/MenuResponse.java index 79052b2..06ae458 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/MenuResponse.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/MenuResponse.java @@ -1,4 +1,55 @@ -package com.ktds.hi.store.infra.dto; - -public class MenuResponse { -} +package com.ktds.hi.store.infra.dto; + +import com.ktds.hi.store.biz.domain.Menu; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ๋ฉ”๋‰ด ์‘๋‹ต DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋ฉ”๋‰ด ์‘๋‹ต") +public class MenuResponse { + + @Schema(description = "๋ฉ”๋‰ด ID", example = "1") + private Long menuId; + + @Schema(description = "๋ฉ”๋‰ด๋ช…", example = "๊น€์น˜์ฐŒ๊ฐœ") + private String menuName; + + @Schema(description = "๋ฉ”๋‰ด ์„ค๋ช…", example = "์–ผํฐํ•œ ๊น€์น˜์ฐŒ๊ฐœ") + private String description; + + @Schema(description = "๊ฐ€๊ฒฉ", example = "8000") + private Integer price; + + @Schema(description = "๋ฉ”๋‰ด ์นดํ…Œ๊ณ ๋ฆฌ", example = "๋ฉ”์ธ") + private String category; + + @Schema(description = "์ด๋ฏธ์ง€ URL") + private String imageUrl; + + @Schema(description = "์ด์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€", example = "true") + private Boolean available; + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑ + */ + public static MenuResponse from(Menu menu) { + return MenuResponse.builder() + .menuId(menu.getId()) + .menuName(menu.getMenuName()) + .description(menu.getDescription()) + .price(menu.getPrice()) + .category(menu.getCategory()) + .imageUrl(menu.getImageUrl()) + .available(menu.getAvailable()) + .build(); + } +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/MyStoreListResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/MyStoreListResponse.java index 545f265..41efdc2 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/MyStoreListResponse.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/MyStoreListResponse.java @@ -1,4 +1,42 @@ -package com.ktds.hi.store.infra.dto; - -public class MyStoreListResponse { -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ๋‚ด ๋งค์žฅ ๋ชฉ๋ก ์‘๋‹ต DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋‚ด ๋งค์žฅ ๋ชฉ๋ก ์‘๋‹ต") +public class MyStoreListResponse { + + @Schema(description = "๋งค์žฅ ID", example = "1") + private Long storeId; + + @Schema(description = "๋งค์žฅ๋ช…", example = "๋ง›์ง‘ ํ•œ๋ฒˆ ๊ฐ€๋ณผ๋ž˜?") + private String storeName; + + @Schema(description = "์ฃผ์†Œ", example = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123") + private String address; + + @Schema(description = "์นดํ…Œ๊ณ ๋ฆฌ", example = "ํ•œ์‹") + private String category; + + @Schema(description = "ํ‰์ ", example = "4.5") + private Double rating; + + @Schema(description = "๋ฆฌ๋ทฐ ์ˆ˜", example = "127") + private Integer reviewCount; + + @Schema(description = "์šด์˜ ์ƒํƒœ", example = "์šด์˜์ค‘") + private String status; + + @Schema(description = "์šด์˜์‹œ๊ฐ„", example = "์›”-๊ธˆ 09:00-21:00") + private String operatingHours; +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateRequest.java b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateRequest.java index 27d979f..a58a3f3 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateRequest.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateRequest.java @@ -1,4 +1,48 @@ -package com.ktds.hi.store.infra.dto; - -public class StoreCreateRequest { -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * ๋งค์žฅ ๋“ฑ๋ก ์š”์ฒญ DTO + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋งค์žฅ ๋“ฑ๋ก ์š”์ฒญ") +public class StoreCreateRequest { + + @NotBlank(message = "๋งค์žฅ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @Size(max = 100, message = "๋งค์žฅ๋ช…์€ 100์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") + @Schema(description = "๋งค์žฅ๋ช…", example = "๋ง›์ง‘ ํ•œ๋ฒˆ ๊ฐ€๋ณผ๋ž˜?") + private String storeName; + + @NotBlank(message = "์ฃผ์†Œ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @Schema(description = "๋งค์žฅ ์ฃผ์†Œ", example = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123") + private String address; + + @Schema(description = "๋งค์žฅ ์„ค๋ช…", example = "๋ง›์žˆ๋Š” ํ•œ์‹๋‹น์ž…๋‹ˆ๋‹ค.") + private String description; + + @Schema(description = "์ „ํ™”๋ฒˆํ˜ธ", example = "02-1234-5678") + private String phone; + + @Schema(description = "์šด์˜์‹œ๊ฐ„", example = "์›”-๊ธˆ 09:00-21:00, ํ† -์ผ 10:00-20:00") + private String operatingHours; + + @NotBlank(message = "์นดํ…Œ๊ณ ๋ฆฌ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @Schema(description = "์นดํ…Œ๊ณ ๋ฆฌ", example = "ํ•œ์‹") + private String category; + + @Schema(description = "๋งค์žฅ ํƒœ๊ทธ ๋ชฉ๋ก", example = "[\"๋ง›์ง‘\", \"ํ˜ผ๋ฐฅ\", \"๊ฐ€์„ฑ๋น„\"]") + private List tags; + + @Schema(description = "๋ฉ”๋‰ด ๋ชฉ๋ก") + private List menus; +} diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateResponse.java index 923ecac..7f4338b 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateResponse.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreCreateResponse.java @@ -1,4 +1,27 @@ -package com.ktds.hi.store.infra.dto; - -public class StoreCreateResponse { -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ๋งค์žฅ ๋“ฑ๋ก ์‘๋‹ต DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋งค์žฅ ๋“ฑ๋ก ์‘๋‹ต") +public class StoreCreateResponse { + + @Schema(description = "์ƒ์„ฑ๋œ ๋งค์žฅ ID", example = "1") + private Long storeId; + + @Schema(description = "๋งค์žฅ๋ช…", example = "๋ง›์ง‘ ํ•œ๋ฒˆ ๊ฐ€๋ณผ๋ž˜?") + private String storeName; + + @Schema(description = "์‘๋‹ต ๋ฉ”์‹œ์ง€", example = "๋งค์žฅ์ด ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + private String message; +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDeleteResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDeleteResponse.java index 77204a4..b7aac7f 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDeleteResponse.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDeleteResponse.java @@ -1,19 +1,24 @@ -package com.ktds.hi.store.infra.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * ๋งค์žฅ ์‚ญ์ œ ์‘๋‹ต DTO - */ -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class StoreDeleteResponse { - - private Boolean success; - private String message; -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ๋งค์žฅ ์‚ญ์ œ ์‘๋‹ต DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋งค์žฅ ์‚ญ์ œ ์‘๋‹ต") +public class StoreDeleteResponse { + + @Schema(description = "์‚ญ์ œ๋œ ๋งค์žฅ ID", example = "1") + private Long storeId; + + @Schema(description = "์‘๋‹ต ๋ฉ”์‹œ์ง€", example = "๋งค์žฅ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + private String message; +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDetailResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDetailResponse.java index bafee42..84ba2e0 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDetailResponse.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreDetailResponse.java @@ -1,4 +1,65 @@ -package com.ktds.hi.store.infra.dto; - -public class StoreDetailResponse { -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * ๋งค์žฅ ์ƒ์„ธ ์กฐํšŒ ์‘๋‹ต DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋งค์žฅ ์ƒ์„ธ ์กฐํšŒ ์‘๋‹ต") +public class StoreDetailResponse { + + @Schema(description = "๋งค์žฅ ID", example = "1") + private Long storeId; + + @Schema(description = "๋งค์žฅ๋ช…", example = "๋ง›์ง‘ ํ•œ๋ฒˆ ๊ฐ€๋ณผ๋ž˜?") + private String storeName; + + @Schema(description = "์ฃผ์†Œ", example = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123") + private String address; + + @Schema(description = "์œ„๋„", example = "37.5665") + private Double latitude; + + @Schema(description = "๊ฒฝ๋„", example = "126.9780") + private Double longitude; + + @Schema(description = "๋งค์žฅ ์„ค๋ช…", example = "๋ง›์žˆ๋Š” ํ•œ์‹๋‹น์ž…๋‹ˆ๋‹ค.") + private String description; + + @Schema(description = "์ „ํ™”๋ฒˆํ˜ธ", example = "02-1234-5678") + private String phone; + + @Schema(description = "์šด์˜์‹œ๊ฐ„", example = "์›”-๊ธˆ 09:00-21:00") + private String operatingHours; + + @Schema(description = "์นดํ…Œ๊ณ ๋ฆฌ", example = "ํ•œ์‹") + private String category; + + @Schema(description = "ํ‰์ ", example = "4.5") + private Double rating; + + @Schema(description = "๋ฆฌ๋ทฐ ์ˆ˜", example = "127") + private Integer reviewCount; + + @Schema(description = "๋งค์žฅ ์ƒํƒœ", example = "ACTIVE") + private String status; + + @Schema(description = "๋งค์žฅ ํƒœ๊ทธ ๋ชฉ๋ก") + private List tags; + + @Schema(description = "๋ฉ”๋‰ด ๋ชฉ๋ก") + private List menus; + + @Schema(description = "AI ์š”์•ฝ ์ •๋ณด") + private String aiSummary; +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreSearchResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreSearchResponse.java index 1475bad..4de68d6 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreSearchResponse.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreSearchResponse.java @@ -1,4 +1,39 @@ -package com.ktds.hi.store.infra.dto; - -public class StoreSearchResponse { -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ๋งค์žฅ ๊ฒ€์ƒ‰ ์‘๋‹ต DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋งค์žฅ ๊ฒ€์ƒ‰ ์‘๋‹ต") +public class StoreSearchResponse { + + @Schema(description = "๋งค์žฅ ID", example = "1") + private Long storeId; + + @Schema(description = "๋งค์žฅ๋ช…", example = "๋ง›์ง‘ ํ•œ๋ฒˆ ๊ฐ€๋ณผ๋ž˜?") + private String storeName; + + @Schema(description = "์ฃผ์†Œ", example = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123") + private String address; + + @Schema(description = "์นดํ…Œ๊ณ ๋ฆฌ", example = "ํ•œ์‹") + private String category; + + @Schema(description = "ํ‰์ ", example = "4.5") + private Double rating; + + @Schema(description = "๋ฆฌ๋ทฐ ์ˆ˜", example = "127") + private Integer reviewCount; + + @Schema(description = "๊ฑฐ๋ฆฌ(km)", example = "1.2") + private Double distance; +} \ No newline at end of file diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateRequest.java b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateRequest.java index 01b68b6..4157e13 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateRequest.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateRequest.java @@ -1,4 +1,41 @@ -package com.ktds.hi.store.infra.dto; - -public class StoreUpdateRequest { -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * ๋งค์žฅ ์ˆ˜์ • ์š”์ฒญ DTO + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋งค์žฅ ์ˆ˜์ • ์š”์ฒญ") +public class StoreUpdateRequest { + + @NotBlank(message = "๋งค์žฅ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @Size(max = 100, message = "๋งค์žฅ๋ช…์€ 100์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") + @Schema(description = "๋งค์žฅ๋ช…", example = "๋ง›์ง‘ ํ•œ๋ฒˆ ๊ฐ€๋ณผ๋ž˜?") + private String storeName; + + @NotBlank(message = "์ฃผ์†Œ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @Schema(description = "๋งค์žฅ ์ฃผ์†Œ", example = "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123") + private String address; + + @Schema(description = "๋งค์žฅ ์„ค๋ช…", example = "๋ง›์žˆ๋Š” ํ•œ์‹๋‹น์ž…๋‹ˆ๋‹ค.") + private String description; + + @Schema(description = "์ „ํ™”๋ฒˆํ˜ธ", example = "02-1234-5678") + private String phone; + + @Schema(description = "์šด์˜์‹œ๊ฐ„", example = "์›”-๊ธˆ 09:00-21:00, ํ† -์ผ 10:00-20:00") + private String operatingHours; + + @Schema(description = "๋งค์žฅ ํƒœ๊ทธ ๋ชฉ๋ก", example = "[\"๋ง›์ง‘\", \"ํ˜ผ๋ฐฅ\", \"๊ฐ€์„ฑ๋น„\"]") + private List tags; +} diff --git a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateResponse.java b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateResponse.java index d42a481..e5ecb9f 100644 --- a/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateResponse.java +++ b/store/src/main/java/com/ktds/hi/store/infra/dto/StoreUpdateResponse.java @@ -1,19 +1,24 @@ -package com.ktds.hi.store.infra.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * ๋งค์žฅ ์ˆ˜์ • ์‘๋‹ต DTO - */ -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class StoreUpdateResponse { - - private Boolean success; - private String message; -} +package com.ktds.hi.store.infra.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ๋งค์žฅ ์ˆ˜์ • ์‘๋‹ต DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "๋งค์žฅ ์ˆ˜์ • ์‘๋‹ต") +public class StoreUpdateResponse { + + @Schema(description = "๋งค์žฅ ID", example = "1") + private Long storeId; + + @Schema(description = "์‘๋‹ต ๋ฉ”์‹œ์ง€", example = "๋งค์žฅ ์ •๋ณด๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + private String message; +} \ No newline at end of file diff --git a/store/src/main/resources/application.yml b/store/src/main/resources/application.yml index cb7b868..1d20a51 100644 --- a/store/src/main/resources/application.yml +++ b/store/src/main/resources/application.yml @@ -1,48 +1,48 @@ -server: - port: ${STORE_SERVICE_PORT:8082} - -spring: - application: - name: store-service - - datasource: - url: ${STORE_DB_URL:jdbc:postgresql://20.249.154.116:5432/hiorder_store} - username: ${STORE_DB_USERNAME:hiorder_user} - password: ${STORE_DB_PASSWORD:hiorder_pass} - driver-class-name: org.postgresql.Driver - - jpa: - hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} - show-sql: ${JPA_SHOW_SQL:false} - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.PostgreSQLDialect - - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - -external-api: - naver: - client-id: ${NAVER_CLIENT_ID:} - client-secret: ${NAVER_CLIENT_SECRET:} - base-url: https://openapi.naver.com - kakao: - api-key: ${KAKAO_API_KEY:} - base-url: https://dapi.kakao.com - google: - api-key: ${GOOGLE_API_KEY:} - base-url: https://maps.googleapis.com - hiorder: - api-key: ${HIORDER_API_KEY:} - base-url: ${HIORDER_BASE_URL:https://api.hiorder.com} - -springdoc: - api-docs: - path: /api-docs - swagger-ui: - path: /swagger-ui.html +server: + port: ${STORE_SERVICE_PORT:8082} + +spring: + application: + name: store-service + + datasource: + url: ${STORE_DB_URL:jdbc:postgresql://20.249.154.116:5432/hiorder_store} + username: ${STORE_DB_USERNAME:hiorder_user} + password: ${STORE_DB_PASSWORD:hiorder_pass} + driver-class-name: org.postgresql.Driver + + jpa: + hibernate: + ddl-auto: ${JPA_DDL_AUTO:update} + show-sql: ${JPA_SHOW_SQL:false} + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + +external-api: + naver: + client-id: ${NAVER_CLIENT_ID:} + client-secret: ${NAVER_CLIENT_SECRET:} + base-url: https://openapi.naver.com + kakao: + api-key: ${KAKAO_API_KEY:} + base-url: https://dapi.kakao.com + google: + api-key: ${GOOGLE_API_KEY:} + base-url: https://maps.googleapis.com + hiorder: + api-key: ${HIORDER_API_KEY:} + base-url: ${HIORDER_BASE_URL:https://api.hiorder.com} + +springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui.html