From 8e0d17a77fa506723898f30fd092d0d2cbcc6187 Mon Sep 17 00:00:00 2001 From: Sejeong Kim <64718002+clean2001@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:43:42 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=EC=84=9C=EB=B2=84=20ncp=20->=20aw?= =?UTF-8?q?s=EB=A1=9C=20=EC=9D=B4=EC=A0=84=20(#257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: test * chore: test * chore: StoredCredential 삭제 * chore: Dockerfile 수정 * chore: aws-cicd yml 수정 * chore: docker compose 변경(Docker-dev, prod 분리) * chore: docker compose 변경(Docker-dev, prod 분리) * chore: build 부분 수정 * chore: docker build 부분 수정 * chore: docker build 부분 수정 * chore: docker build 부분 수정 * chore: cicd-prod.ym ldocker build 부분 수정 * chore: cicd-prod.yml 오타 수정 * chore: prod > deploy.sh 작성 * chore: prod > blue,green 추가 * chore: prod > blue,green 추가 * chore: prod > blue,green 추가 * chore: docker-compose depends_on 수정 * chore: deploy 수정 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: docker-compse 테스트 * chore: dev test * chore: dev test * chore: prod test * chore: prod nginx conf 변경 * chore: prod nginx conf 변경 * chore: github actionsa 파일 브랜치 명 변경 * chore: batch db 엔드포인트 변경 * chore: 포트번호 변경 * chore: depends on 변경 * chore: 포트 변경 * chore: 포트 변경 * chore: blue green sh 수정 * chore: blue green sh 수정 * chore: docker compose 수정 * chore: sh 수정 * chore: 포트 수정 * chore: 포트 수정 * chore: dns 수정 * chore: localhost로 수정 * chore: 배치 로그 추가 * chore: Dockefile 수정 * chore: 크론 임시 변경 * chore: 도커 파일 변경 * chore: 도커 파일 이름 지정 * chore: batch yml 추가 * chore: no-cache 추가 * chore: Dockerfile 이름 변경 * chore: deploy.sh 변경 * chore: 2분마다로 변경 테스트 * chore: now 로그 추가 * chore: 스케줄러 1시간 단위로 변경 --- .github/workflows/aws-cicd-dev.yml | 164 ++++++++++++++++ .github/workflows/aws-cicd-prod.yml | 179 ++++++++++++++++++ .github/workflows/k3s-dev.yaml | 2 +- .github/workflows/k3s-prod.yaml | 2 +- .gitignore | 2 + layer-api/{Dockerfile => Dockerfile-dev} | 8 +- layer-api/Dockerfile-prod | 4 + .../infra/development/docker-compose.yaml | 20 +- layer-api/infra/production/application.yml | 66 +++++++ layer-api/infra/production/deploy.sh | 66 +++++++ .../infra/production/docker-compose-blue.yaml | 33 ++++ .../production/docker-compose-green.yaml | 33 ++++ .../infra/production/docker-compose.yaml | 41 ---- layer-api/infra/production/nginx.blue.conf | 19 ++ layer-api/infra/production/nginx.green.conf | 19 ++ .../java/org/layer/config/SecurityConfig.java | 1 + .../org/layer/domain/etc/TempController.java | 13 ++ .../src/main/resources/application-dev.yml | 13 +- .../src/main/resources/application-local.yml | 5 +- .../src/main/resources/application-prod.yml | 24 ++- layer-api/src/main/resources/tokens/.gitkeep | 0 layer-batch/{Dockerfile => Dockerfile-batch} | 2 +- .../batch/scheduler/RetrospectScheduler.java | 2 + .../src/main/resources/application.yml | 6 +- .../main/resources/tokens/StoredCredential | Bin 0 -> 1130 bytes tokens/StoredCredential | Bin 0 -> 1130 bytes 26 files changed, 652 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/aws-cicd-dev.yml create mode 100644 .github/workflows/aws-cicd-prod.yml rename layer-api/{Dockerfile => Dockerfile-dev} (55%) create mode 100644 layer-api/Dockerfile-prod create mode 100644 layer-api/infra/production/application.yml create mode 100644 layer-api/infra/production/deploy.sh create mode 100644 layer-api/infra/production/docker-compose-blue.yaml create mode 100644 layer-api/infra/production/docker-compose-green.yaml delete mode 100644 layer-api/infra/production/docker-compose.yaml create mode 100644 layer-api/infra/production/nginx.blue.conf create mode 100644 layer-api/infra/production/nginx.green.conf create mode 100644 layer-api/src/main/java/org/layer/domain/etc/TempController.java create mode 100644 layer-api/src/main/resources/tokens/.gitkeep rename layer-batch/{Dockerfile => Dockerfile-batch} (56%) create mode 100644 layer-batch/src/main/resources/tokens/StoredCredential create mode 100644 tokens/StoredCredential diff --git a/.github/workflows/aws-cicd-dev.yml b/.github/workflows/aws-cicd-dev.yml new file mode 100644 index 00000000..5f6144d7 --- /dev/null +++ b/.github/workflows/aws-cicd-dev.yml @@ -0,0 +1,164 @@ +name: deploy action + +on: + push: + branches: + - develop + +env: + REGISTRY: "docker.io" + NAMESPACE: "clean01" + IMAGE_NAME: "layer-server" + +jobs: + setup: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + deploy_target: ${{ steps.set-env.outputs.DEPLOY_TARGET }} + steps: + - name: Setup Env + id: set-env + run: | + echo "DEPLOY_TARGET=development" >> $GITHUB_OUTPUT + + build: + name: build + needs: [ setup ] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + env: + DEPLOY_TARGET: ${{ needs.setup.outputs.deploy_target }} + REGISTRY: "docker.io" + NAMESPACE: "clean01" + APPLICATION_SECRET_PROPERTIES: ${{ secrets.AWS_APPLICATION_SECRET_PROPERTIES }} + GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }} + + steps: + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Gradle + uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 + + - name: Create application-secret.properties + run: | + echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-api/src/main/resources/application-secret.properties + echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-batch/src/main/resources/application-secret.properties + + - name: Build layer-api module + run: ./gradlew :layer-api:build + + - name: Test layer-api module + run: ./gradlew :layer-api:test + + - name: Build layer-batch module + run: ./gradlew :layer-batch:build + + - name: Test layer-batch module + run: ./gradlew :layer-batch:test + + - name: Docker Hub Login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_EMAIL }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-api + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch + + - name: Push layer-api Docker Image + uses: docker/build-push-action@v4 + with: + context: ./layer-api + file: ./layer-api/Dockerfile-dev # Dockerfile 이름 지정 + platforms: linux/amd64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-api:latest + + + - name: Push layer-batch Docker Image + uses: docker/build-push-action@v4 + with: + context: ./layer-batch + platforms: linux/amd64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch:latest + + deploy: + name: Deploy + needs: [ build, setup ] + runs-on: ubuntu-latest + env: + DEPLOY_TARGET: ${{ needs.setup.outputs.deploy_target }} + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Create application-secret.properties file + run: | + echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-api/infra/${{ env.DEPLOY_TARGET }}/application-secret.properties + echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-batch/src/main/resources/application-secret.properties + + - name: Archive Files + run: | + tar -cvzf layer-api.tar.gz ./layer-api/infra/${{ env.DEPLOY_TARGET }}/ + + - name: Send Docker Compose + uses: appleboy/scp-action@master + with: + host: ${{ secrets.AWS_DEV_INSTANCE_HOST }} + username: ubuntu + key: ${{ secrets.AWS_INSTANCE_PEM }} + port: 22 + source: "layer-api.tar.gz" + target: "/home/ubuntu" + + - name: Extract Files on Server and Set Permissions + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_DEV_INSTANCE_HOST }} + username: ubuntu + key: ${{ secrets.AWS_INSTANCE_PEM }} + port: 22 + script: | + cd /home/ubuntu + tar -xvzf layer-api.tar.gz + sudo chmod -R 755 /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + sudo chown -R ubuntu:ubuntu /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + + + - name: Deploy with Docker Compose + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_DEV_INSTANCE_HOST }} + username: ubuntu + key: ${{ secrets.AWS_INSTANCE_PEM }} + port: 22 + script: | + sudo apt update + sudo apt install docker-ce + sudo apt install docker-compose + sudo docker login --username ${{ secrets.DOCKER_EMAIL }} --password ${{ secrets.DOCKER_PASSWORD }} + cd /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + sudo docker-compose pull && sudo docker-compose up -d + sudo docker image prune -a -f diff --git a/.github/workflows/aws-cicd-prod.yml b/.github/workflows/aws-cicd-prod.yml new file mode 100644 index 00000000..6b3c8eb9 --- /dev/null +++ b/.github/workflows/aws-cicd-prod.yml @@ -0,0 +1,179 @@ +name: deploy action + +on: + push: + branches: + - main + - chore/migration + +env: + REGISTRY: "docker.io" + NAMESPACE: "clean01" + IMAGE_NAME: "layer-server" + +jobs: + setup: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + deploy_target: ${{ steps.set-env.outputs.DEPLOY_TARGET }} + steps: + - name: Setup Env + id: set-env + run: | + echo "DEPLOY_TARGET=production" >> $GITHUB_OUTPUT + + build: + name: build + needs: [ setup ] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + env: + DEPLOY_TARGET: ${{ needs.setup.outputs.deploy_target }} + REGISTRY: "docker.io" + NAMESPACE: "clean01" + APPLICATION_SECRET_PROPERTIES: ${{ secrets.AWS_APPLICATION_SECRET_PROPERTIES }} + GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }} + + steps: + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Gradle + uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 + + - name: Create application-secret.properties + run: | + echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-api/src/main/resources/application-secret.properties + echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-batch/src/main/resources/application-secret.properties + + - name: Build layer-api module + run: ./gradlew :layer-api:build + + - name: Test layer-api module + run: ./gradlew :layer-api:test + + - name: Build layer-batch module + run: ./gradlew :layer-batch:build + + - name: Test layer-batch module + run: ./gradlew :layer-batch:test + + - name: Docker Hub Login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_EMAIL }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-api + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch + + - name: Push layer-api Docker Image + uses: docker/build-push-action@v4 + with: + context: ./layer-api + file: ./layer-api/Dockerfile-prod # Dockerfile 이름 지정 + platforms: linux/amd64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-api:latest + no-cache: true + + - name: Push layer-batch Docker Image + uses: docker/build-push-action@v4 + with: + context: ./layer-batch + file: ./layer-batch/Dockerfile-batch # Dockerfile 이름 지정 + platforms: linux/amd64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch:latest + no-cache: true + + deploy: + name: Deploy + needs: [ build, setup ] + runs-on: ubuntu-latest + env: + DEPLOY_TARGET: ${{ needs.setup.outputs.deploy_target }} + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Create application-secret.properties file + run: | + echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-api/infra/${{ env.DEPLOY_TARGET }}/application-secret.properties + echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-batch/src/main/resources/application-secret.properties + + - name: Archive Files + run: | + tar -cvzf layer-api.tar.gz ./layer-api/infra/${{ env.DEPLOY_TARGET }}/ + + - name: Send Docker Compose + uses: appleboy/scp-action@master + with: + host: ${{ secrets.AWS_PROD_INSTANCE_HOST }} + username: ubuntu + key: ${{ secrets.AWS_INSTANCE_PEM }} + port: 22 + source: "layer-api.tar.gz" + target: "/home/ubuntu" + + - name: Extract Files on Server and Set Permissions + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_PROD_INSTANCE_HOST }} + username: ubuntu + key: ${{ secrets.AWS_INSTANCE_PEM }} + port: 22 + script: | + cd /home/ubuntu + tar -xvzf layer-api.tar.gz + sudo chmod -R 755 /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + sudo chown -R ubuntu:ubuntu /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + + + - name: Set Permissions on Transferred Files + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_PROD_INSTANCE_HOST }} + username: ubuntu + key: ${{ secrets.AWS_INSTANCE_PEM }} + port: 22 + script: | + sudo chmod -R 755 /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + sudo chown -R ubuntu:ubuntu /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + + - name: Deploy with Docker Compose + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_PROD_INSTANCE_HOST }} + username: ubuntu + key: ${{ secrets.AWS_INSTANCE_PEM }} + port: 22 + script: | + sudo apt update + sudo apt install docker-ce + sudo apt install docker-compose + sudo docker login --username ${{ secrets.DOCKER_EMAIL }} --password ${{ secrets.DOCKER_PASSWORD }} + cd /home/ubuntu/layer-api/infra/${{ env.DEPLOY_TARGET }} + chmod 777 ./deploy.sh + ./deploy.sh + sudo docker image prune -a -f diff --git a/.github/workflows/k3s-dev.yaml b/.github/workflows/k3s-dev.yaml index 35a596a5..aeb1b7b5 100644 --- a/.github/workflows/k3s-dev.yaml +++ b/.github/workflows/k3s-dev.yaml @@ -3,7 +3,7 @@ name: Spring Boot Multi-module K3s Deployment on: push: branches: - - develop + - develop-test # 실행되지 않도록 브랜치 명 변경 env: REGISTRY: "ghcr.io" diff --git a/.github/workflows/k3s-prod.yaml b/.github/workflows/k3s-prod.yaml index cfbbc34a..30cb74cb 100644 --- a/.github/workflows/k3s-prod.yaml +++ b/.github/workflows/k3s-prod.yaml @@ -3,7 +3,7 @@ name: Spring Boot Multi-module K3s Deployment on: push: branches: - - main + - main-test # 실행되지 않도록 브랜치 명 변경 env: REGISTRY: "ghcr.io" diff --git a/.gitignore b/.gitignore index 074a5fa9..e0630052 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ log/ credentials.json +layer-api/src/main/resources/tokens/StoredCredential +layer-batch/src/main/resources/application-secret.properties \ No newline at end of file diff --git a/layer-api/Dockerfile b/layer-api/Dockerfile-dev similarity index 55% rename from layer-api/Dockerfile rename to layer-api/Dockerfile-dev index 04505691..cbb49e2e 100644 --- a/layer-api/Dockerfile +++ b/layer-api/Dockerfile-dev @@ -1,10 +1,4 @@ FROM openjdk:17 - ARG JAR_FILE=./build/libs/*.jar -ARG SPRING_PROFILE - COPY ${JAR_FILE} layer-server.jar - -ENV SPRING_PROFILE=${SPRING_PROFILE} - -ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul","-Dspring.profiles.active=${SPRING_PROFILE}" ,"-jar" ,"layer-server.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul","-Dspring.profiles.active=dev" ,"-jar" ,"layer-server.jar"] \ No newline at end of file diff --git a/layer-api/Dockerfile-prod b/layer-api/Dockerfile-prod new file mode 100644 index 00000000..1cc729ea --- /dev/null +++ b/layer-api/Dockerfile-prod @@ -0,0 +1,4 @@ +FROM openjdk:17 +ARG JAR_FILE=./build/libs/*.jar +COPY ${JAR_FILE} layer-server.jar +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul","-Dspring.profiles.active=prod" ,"-jar" ,"layer-server.jar"] \ No newline at end of file diff --git a/layer-api/infra/development/docker-compose.yaml b/layer-api/infra/development/docker-compose.yaml index 5d664994..edd2b8c9 100644 --- a/layer-api/infra/development/docker-compose.yaml +++ b/layer-api/infra/development/docker-compose.yaml @@ -1,6 +1,6 @@ services: java-app: - image: ghcr.io/depromeet/kasukabe-server/layer-api:latest + image: docker.io/clean01/layer-server_layer-api:latest container_name: layer-api ports: - "8080:8080" @@ -14,6 +14,21 @@ services: networks: - app-network + batch-job: + image: docker.io/clean01/layer-server_layer-batch:latest # + container_name: layer-batch + environment: + - TZ=Asia/Seoul + volumes: + - ./application-secret.properties:/config/application-secret.properties + - ./log:/log + - ./tokens:/config/tokens + networks: + - app-network + depends_on: + - java-app + restart: always + nginx: image: nginx:latest container_name: nginx @@ -27,5 +42,4 @@ services: - app-network networks: - app-network: - driver: bridge + app-network: \ No newline at end of file diff --git a/layer-api/infra/production/application.yml b/layer-api/infra/production/application.yml new file mode 100644 index 00000000..bb74f7fd --- /dev/null +++ b/layer-api/infra/production/application.yml @@ -0,0 +1,66 @@ +server: + port: 8085 + +spring: + config: + import: application-secret.properties + datasource: + url: ${AWS_PROD_DB_URL} + username: ${AWS_PROD_DB_NAME} + password: ${AWS_PROD_DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + show_sql: true + open-in-view: false + database: mysql + +apple: + login: + issuer: ${DEV_APPLE_ISSUER} + client_id: ${DEV_APPLE_CLIENT_ID} + audience: ${DEV_APPLE_AUD} + +kakao: + login: + api_key: ${DEV_KAKAO_API_KEY} + redirect_uri: ${DEV_KAKAO_REDIRECT_URI} + uri: + base: https://kauth.kakao.com + code: /oauth/authorize + token: /oauth/token + api: + uri: + base: https://kapi.kakao.com + user: /v2/user/me + +google: + login: + client_id: ${DEV_GOOGLE_CLIENT_ID} + code_uri: ${DEV_GOOGLE_CODE_URI} + token_uri: ${DEV_GOOGLE_TOKEN_URI} + client_secret: ${DEV_GOOGLE_CLIENT_SECRET} + redirect_uri: ${DEV_GOOGLE_REDIRECT_URI} + code_redirect_uri: http://localhost:8080/api/auth/oauth2/google/code + sheet: + id: ${GOOGLE_SHEET_ID} + token_path: ${PROD_GOOGLE_TOKEN_PATH} + credential_path: ${PROD_GOOGLE_CREDENTIAL_PATH} + +ncp: + storage: + region: ${STORAGE_REGION} + bucketName: ${STORAGE_NAME} + endpoint: ${STORAGE_ENDPOINT} + accessKey: ${STORAGE_ACCESS_KEY} + secretKey: ${STORAGE_SECRET_KEY} + +openai: + systemContent: ${OPENAI_SYSTEM_CONTENT} + apiKey: ${OPENAI_API_KEY} + model: ${OPENAI_MODEL} + maxTokens: ${OPENAI_MAX_TOKENS} \ No newline at end of file diff --git a/layer-api/infra/production/deploy.sh b/layer-api/infra/production/deploy.sh new file mode 100644 index 00000000..00e8d950 --- /dev/null +++ b/layer-api/infra/production/deploy.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +IS_GREEN=$(sudo docker ps | grep layer-api-green) # 현재 실행중인 App이 blue인지 확인합니다. +DEFAULT_CONF="/etc/nginx/nginx.conf" + + +if [ -z $IS_GREEN ];then # blue라면 + + echo "### BLUE => GREEN ###" + + echo "1. get green image" + cd ./layer-api/infra/production + + echo "1.1. pull latest green image" + sudo docker-compose -f docker-compose-green.yaml pull + + echo "2. green container up" + sudo docker-compose -f docker-compose-green.yaml up -d + + while [ 1 = 1 ]; do + echo "3. green health check..." + sudo sleep 3 + + REQUEST=$(sudo curl http://127.0.0.1:8080/greeting) # green으로 request + if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지 + echo "health check success" + break ; + fi + done; + + echo "4. reload nginx" + sudo cp ./nginx.green.conf /etc/nginx/nginx.conf + sudo nginx -s reload + + echo "5. blue container down" + sudo sudo docker-compose -f docker-compose-blue.yaml down +else + echo "### GREEN => BLUE ###" + echo "1. get blue image" + cd ./layer-api/infra/production + + echo "1.1. pull latest blue image" + sudo docker-compose -f docker-compose-blue.yaml pull + + echo "2. blue container up" + sudo docker-compose -f docker-compose-blue.yaml up -d + + + while [ 1 = 1 ]; do + echo "3. blue health check..." + sleep 3 + REQUEST=$(curl http://127.0.0.1:8081/greeting) # blue로 request + + if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지 + echo "health check success" + break ; + fi + done; + + echo "4. reload nginx" + sudo cp ./nginx.blue.conf /etc/nginx/nginx.conf + sudo nginx -s reload + + echo "5. green container down" + sudo sudo docker-compose -f docker-compose-green.yaml down +fi \ No newline at end of file diff --git a/layer-api/infra/production/docker-compose-blue.yaml b/layer-api/infra/production/docker-compose-blue.yaml new file mode 100644 index 00000000..82d57859 --- /dev/null +++ b/layer-api/infra/production/docker-compose-blue.yaml @@ -0,0 +1,33 @@ +services: + layer-api-blue: + image: docker.io/clean01/layer-server_layer-api:latest + container_name: layer-api-blue + ports: + - "8081:8080" + environment: + - TZ=Asia/Seoul + - SPRING_PROFILES_ACTIVE=prod + volumes: + - ./application-secret.properties:/config/application-secret.properties + - ./log:/log + - ./tokens:/config/tokens + networks: + - app-network + + batch-job-blue: + image: docker.io/clean01/layer-server_layer-batch:latest + container_name: layer-batch-blue + environment: + - TZ=Asia/Seoul + volumes: + - ./application-secret.properties:/config/application-secret.properties + - ./log:/log + - ./tokens:/config/tokens + networks: + - app-network + depends_on: + - layer-api-blue + restart: always + +networks: + app-network: \ No newline at end of file diff --git a/layer-api/infra/production/docker-compose-green.yaml b/layer-api/infra/production/docker-compose-green.yaml new file mode 100644 index 00000000..bf36a0b6 --- /dev/null +++ b/layer-api/infra/production/docker-compose-green.yaml @@ -0,0 +1,33 @@ +services: + layer-api-green: + image: docker.io/clean01/layer-server_layer-api:latest + container_name: layer-api-green + ports: + - "8080:8080" + environment: + - TZ=Asia/Seoul + - SPRING_PROFILES_ACTIVE=prod + volumes: + - ./application-secret.properties:/config/application-secret.properties + - ./log:/log + - ./tokens:/config/tokens + networks: + - app-network + + batch-job-green: + image: docker.io/clean01/layer-server_layer-batch:latest + container_name: layer-batch-green + environment: + - TZ=Asia/Seoul + volumes: + - ./application-secret.properties:/config/application-secret.properties + - ./log:/log + - ./tokens:/config/tokens + networks: + - app-network + depends_on: + - layer-api-green + restart: always + +networks: + app-network: \ No newline at end of file diff --git a/layer-api/infra/production/docker-compose.yaml b/layer-api/infra/production/docker-compose.yaml deleted file mode 100644 index 954d959f..00000000 --- a/layer-api/infra/production/docker-compose.yaml +++ /dev/null @@ -1,41 +0,0 @@ -services: - redis: - image: redis:latest - container_name: redis - ports: - - "6379:6379" - networks: - - app-network - - java-app: - image: ghcr.io/depromeet/kasukabe-server/layer-api:latest - container_name: layer-api - depends_on: - - redis - ports: - - "8080:8080" - environment: - - TZ=Asia/Seoul - - SPRING_PROFILES_ACTIVE=prod - volumes: - - type: bind - source: ./config/application.yaml - target: ./application.yaml - networks: - - app-network - - nginx: - image: nginx:latest - container_name: nginx - depends_on: - - java-app - ports: - - "80:80" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf - networks: - - app-network - -networks: - app-network: - driver: bridge diff --git a/layer-api/infra/production/nginx.blue.conf b/layer-api/infra/production/nginx.blue.conf new file mode 100644 index 00000000..326ea474 --- /dev/null +++ b/layer-api/infra/production/nginx.blue.conf @@ -0,0 +1,19 @@ +events { } + +http { + upstream layer-api { + server localhost:8081; + } + + server { + listen 80; + + location / { + proxy_pass http://layer-api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file diff --git a/layer-api/infra/production/nginx.green.conf b/layer-api/infra/production/nginx.green.conf new file mode 100644 index 00000000..a7f4110b --- /dev/null +++ b/layer-api/infra/production/nginx.green.conf @@ -0,0 +1,19 @@ +events { } + +http { + upstream layer-api { + server localhost:8080; + } + + server { + listen 80; + + location / { + proxy_pass http://layer-api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file diff --git a/layer-api/src/main/java/org/layer/config/SecurityConfig.java b/layer-api/src/main/java/org/layer/config/SecurityConfig.java index ce833a59..266ed332 100644 --- a/layer-api/src/main/java/org/layer/config/SecurityConfig.java +++ b/layer-api/src/main/java/org/layer/config/SecurityConfig.java @@ -59,6 +59,7 @@ private void setHttp(HttpSecurity http) throws Exception { .requestMatchers(new AntPathRequestMatcher("/api/auth/oauth2/apple")).permitAll() .requestMatchers(new AntPathRequestMatcher("/api/auth/create-token")).permitAll() .requestMatchers(new AntPathRequestMatcher("/admin/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/greeting")).permitAll() .anyRequest().authenticated() ) .headers(headers -> headers diff --git a/layer-api/src/main/java/org/layer/domain/etc/TempController.java b/layer-api/src/main/java/org/layer/domain/etc/TempController.java new file mode 100644 index 00000000..aef6a6d7 --- /dev/null +++ b/layer-api/src/main/java/org/layer/domain/etc/TempController.java @@ -0,0 +1,13 @@ +package org.layer.domain.etc; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TempController { + @GetMapping("/greeting") + public String greeting(@Value("${greeting.message}") String greeting) { + return greeting; + } +} diff --git a/layer-api/src/main/resources/application-dev.yml b/layer-api/src/main/resources/application-dev.yml index 06aa6b02..9a0b49b0 100644 --- a/layer-api/src/main/resources/application-dev.yml +++ b/layer-api/src/main/resources/application-dev.yml @@ -1,10 +1,10 @@ spring: config: - import: optional:file:/config/application-secret.properties + import: application-secret.properties datasource: - url: ${DEV_DB_URL} - username: ${DEV_DB_NAME} - password: ${DEV_DB_PASSWORD} + url: ${AWS_DEV_DB_URL} + username: ${AWS_PROD_DB_NAME} + password: ${AWS_PROD_DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: @@ -80,4 +80,7 @@ openai: maxTokens: ${OPENAI_MAX_TOKENS} admin: - password: ${ADMIN_PASSWORD} \ No newline at end of file + password: ${ADMIN_PASSWORD} + +greeting: + message: this is dev \ No newline at end of file diff --git a/layer-api/src/main/resources/application-local.yml b/layer-api/src/main/resources/application-local.yml index c8fe1029..4e6ac6d3 100644 --- a/layer-api/src/main/resources/application-local.yml +++ b/layer-api/src/main/resources/application-local.yml @@ -85,4 +85,7 @@ openai: maxTokens: ${OPENAI_MAX_TOKENS} admin: - password: ${ADMIN_PASSWORD} \ No newline at end of file + password: ${ADMIN_PASSWORD} + +greeting: + message: this is local \ No newline at end of file diff --git a/layer-api/src/main/resources/application-prod.yml b/layer-api/src/main/resources/application-prod.yml index bb726150..dbfedc7a 100644 --- a/layer-api/src/main/resources/application-prod.yml +++ b/layer-api/src/main/resources/application-prod.yml @@ -1,10 +1,10 @@ spring: config: - import: optional:file:/config/application-secret.properties + import: application-secret.properties datasource: - url: ${PROD_DB_URL} - username: ${PROD_DB_NAME} - password: ${PROD_DB_PASSWORD} + url: ${AWS_PROD_DB_URL} + username: ${AWS_PROD_DB_NAME} + password: ${AWS_PROD_DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: @@ -18,12 +18,12 @@ spring: data: redis: - host: ${PROD_REDIS_HOST} - port: ${PROD_REDIS_PORT} - password: ${PROD_REDIS_PASSWORD} + host: ${DEV_REDIS_HOST} + port: ${DEV_REDIS_PORT} + password: ${DEV_REDIS_PASSWORD} jwt: - secret: ${PROD_JWT_SECRET} + secret: ${DEV_JWT_SECRET} kakao: login: @@ -41,8 +41,11 @@ kakao: google: login: client_id: ${DEV_GOOGLE_CLIENT_ID} + code_uri: ${DEV_GOOGLE_CODE_URI} + token_uri: ${DEV_GOOGLE_TOKEN_URI} client_secret: ${DEV_GOOGLE_CLIENT_SECRET} redirect_uri: ${DEV_GOOGLE_REDIRECT_URI} + code_redirect_uri: http://localhost:8080/api/auth/oauth2/google/code sheet: id: ${GOOGLE_SHEET_ID} token_path: ${PROD_GOOGLE_TOKEN_PATH} @@ -77,4 +80,7 @@ openai: maxTokens: ${OPENAI_MAX_TOKENS} admin: - password: ${ADMIN_PASSWORD} \ No newline at end of file + password: ${ADMIN_PASSWORD} + +greeting: + message: this is prod \ No newline at end of file diff --git a/layer-api/src/main/resources/tokens/.gitkeep b/layer-api/src/main/resources/tokens/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/layer-batch/Dockerfile b/layer-batch/Dockerfile-batch similarity index 56% rename from layer-batch/Dockerfile rename to layer-batch/Dockerfile-batch index ea09a3c3..1176e233 100644 --- a/layer-batch/Dockerfile +++ b/layer-batch/Dockerfile-batch @@ -7,4 +7,4 @@ COPY ${JAR_FILE} layer-batch.jar ENV SPRING_PROFILE=${SPRING_PROFILE} -ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul","-Dspring.profiles.active=prod" ,"-jar" ,"layer-batch.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul" ,"-jar" ,"layer-batch.jar"] \ No newline at end of file diff --git a/layer-batch/src/main/java/org/layer/batch/scheduler/RetrospectScheduler.java b/layer-batch/src/main/java/org/layer/batch/scheduler/RetrospectScheduler.java index 9280f083..40f70f53 100644 --- a/layer-batch/src/main/java/org/layer/batch/scheduler/RetrospectScheduler.java +++ b/layer-batch/src/main/java/org/layer/batch/scheduler/RetrospectScheduler.java @@ -40,6 +40,8 @@ public void updateRetrospectStatusToDone() { List retrospects = retrospectRepository.findAllByDeadlineBeforeAndRetrospectStatus( now, RetrospectStatus.PROCEEDING); + + Map retrospectMap = retrospects.stream() .collect(Collectors.toMap(Retrospect::getId, retrospect -> retrospect)); diff --git a/layer-batch/src/main/resources/application.yml b/layer-batch/src/main/resources/application.yml index 326c8e7c..75718ee9 100644 --- a/layer-batch/src/main/resources/application.yml +++ b/layer-batch/src/main/resources/application.yml @@ -5,9 +5,9 @@ spring: config: import: application-secret.properties datasource: - url: ${PROD_DB_URL} - username: ${PROD_DB_NAME} - password: ${PROD_DB_PASSWORD} + url: ${AWS_PROD_DB_URL} + username: ${AWS_PROD_DB_NAME} + password: ${AWS_PROD_DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: diff --git a/layer-batch/src/main/resources/tokens/StoredCredential b/layer-batch/src/main/resources/tokens/StoredCredential new file mode 100644 index 0000000000000000000000000000000000000000..63b1c6c17b9026d421421e9725d098cde63b28b2 GIT binary patch literal 1130 zcma)5L66%+6rN;vp>!*>Z~zGjq^dn|sGZo`>=N}*yh)vSv7Jqvgv}xW6MO88V|$YE zOq>M7v8vQVi&XvqCqRq1!U-WR9Jp}hKr4ZSs)`dA)KkaVN?R_~jx_J`-uJ%uzWL_( zAFz>9@Ja}0Q05FraucQ_3r;p({Qak&U;FsCHBi*S`G`PIhb|-3057o-Md^q{-h6WX zU67v(**Oun#4Rwe&QnA=1;r2cj{klAzZcGR9~1$YPXJhZe0)Nux+InZLV^g%aDrtw z!YE-fk)Cs3|u_Pv z>kAb5xdJQtDfkQ&<`V|~SU|NZ!?NaDJBpe)i!>ct4ZXv7Fx20Vc-Qu~r@D?QuTFc? z_O9L1=uy9}br!XyPI^map+vFTt#wB?+P#6*$e5<}m!ThHR38t!3ki=?hZ}<~VG6F= z^_^B6WKPEolPq+$tyz6GHJW&24LVev#P&hiqmtq_ss?4?{1wlIVx zw1#%ou`^vVs|KwdE%u%CqoAY1UNQ}(YCl}KjrQ13!!*>Z~zGjq^dn|sGZo`>=N}*yh)vSv7Jqvgv}xW6MO88V|$YE zOq>M7v8vQVi&XvqCqRq1!U-WR9Jp}hKr4ZSs)`dA)KkaVN?R_~jx_J`-uJ%uzWL_( zAFz>9@Ja}0Q05FraucQ_3r;p({Qak&U;FsCHBi*S`G`PIhb|-3057o-Md^q{-h6WX zU67v(**Oun#4Rwe&QnA=1;r2cj{klAzZcGR9~1$YPXJhZe0)Nux+InZLV^g%aDrtw z!YE-fk)Cs3|u_Pv z>kAb5xdJQtDfkQ&<`V|~SU|NZ!?NaDJBpe)i!>ct4ZXv7Fx20Vc-Qu~r@D?QuTFc? z_O9L1=uy9}br!XyPI^map+vFTt#wB?+P#6*$e5<}m!ThHR38t!3ki=?hZ}<~VG6F= z^_^B6WKPEolPq+$tyz6GHJW&24LVev#P&hiqmtq_ss?4?{1wlIVx zw1#%ou`^vVs|KwdE%u%CqoAY1UNQ}(YCl}KjrQ13! Date: Tue, 12 Nov 2024 21:04:16 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20aws-cicd-dev.yml=20batch=20Dockerfi?= =?UTF-8?q?le=20=EC=9D=B4=EB=A6=84=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/aws-cicd-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/aws-cicd-dev.yml b/.github/workflows/aws-cicd-dev.yml index 5f6144d7..83033cf9 100644 --- a/.github/workflows/aws-cicd-dev.yml +++ b/.github/workflows/aws-cicd-dev.yml @@ -98,6 +98,7 @@ jobs: uses: docker/build-push-action@v4 with: context: ./layer-batch + file: ./layer-batch/Dockerfile-batch # Dockerfile 이름 지정 platforms: linux/amd64 push: true tags: | From 332ebe030f3b9d1192a92f286f73d89f75efb756 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sun, 17 Nov 2024 13:51:10 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=ED=9B=85=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove: 파일 경로 이동 * remove: 구글 시트 기능 및 피드백 기능 삭제 * add: 디스코드 관련 설정 추가 * feat: 회고 생성 시 디스코드 알림 기능 구현 * feat: 회원가입 시 디스코드 알림 기능 구현 * feat: 스페이스 생성 시 디스코드 알림 기능 구현 * feat(DiscordAppender): 디스코드 메세지 만드는 클래스 구현 * fix: 회원가입 시 알맞은 디스코드 메서드 호출하도록 수정 --- .../auth/controller/AuthController.java | 2 - .../domain/auth/service/AuthService.java | 37 +++--- .../controller/ExternalController.java | 4 +- .../controller/dto/ExternalRequest.java | 2 +- .../controller/dto/ExternalResponse.java | 2 +- .../domain/member/controller/MemberApi.java | 5 - .../member/controller/MemberController.java | 7 -- .../domain/member/service/MemberService.java | 13 -- .../retrospect/service/RetrospectService.java | 18 ++- .../space/controller/dto/SpaceRequest.java | 1 - .../domain/space/service/SpaceService.java | 18 ++- .../src/main/resources/application-dev.yml | 8 +- .../src/main/resources/application-local.yml | 8 +- .../src/main/resources/application-prod.yml | 8 +- .../external/google/config/GoogleConfig.java | 113 ------------------ .../google/config/GoogleCredentials.java | 27 ----- .../external/google/enums/SheetType.java | 17 --- .../google/service/GoogleApiService.java | 62 ---------- .../external/ncp/enums/ImageDomain.java | 5 - .../external/discord/DiscordAppender.java | 87 ++++++++++++++ .../discord/event/CreateRetrospectEvent.java | 13 ++ .../event/CreateRetrospectEventListener.java | 25 ++++ .../discord/event/CreateSpaceEvent.java | 13 ++ .../event/CreateSpaceEventListener.java | 24 ++++ .../external/discord/event/SignUpEvent.java | 13 ++ .../discord/event/SignUpEventListener.java | 23 ++++ .../infra/DiscordWebhookMemberClient.java | 14 +++ .../infra/DiscordWebhookRetrospectClient.java | 15 +++ .../infra/DiscordWebhookSpaceClient.java | 14 +++ .../external/ncp/config/NcpConfig.java | 2 +- .../external/ncp/dto/NcpResponse.java | 2 +- .../layer/external/ncp/enums/ImageDomain.java | 5 + .../ncp/exception/ExternalException.java | 2 +- .../external/ncp/service/NcpService.java | 8 +- 34 files changed, 323 insertions(+), 294 deletions(-) delete mode 100644 layer-external/src/main/java/org/layer/domain/external/google/config/GoogleConfig.java delete mode 100644 layer-external/src/main/java/org/layer/domain/external/google/config/GoogleCredentials.java delete mode 100644 layer-external/src/main/java/org/layer/domain/external/google/enums/SheetType.java delete mode 100644 layer-external/src/main/java/org/layer/domain/external/google/service/GoogleApiService.java delete mode 100644 layer-external/src/main/java/org/layer/domain/external/ncp/enums/ImageDomain.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/DiscordAppender.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEvent.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEventListener.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEvent.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEventListener.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/event/SignUpEvent.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/event/SignUpEventListener.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookMemberClient.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookRetrospectClient.java create mode 100644 layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookSpaceClient.java rename layer-external/src/main/java/org/layer/{domain => }/external/ncp/config/NcpConfig.java (96%) rename layer-external/src/main/java/org/layer/{domain => }/external/ncp/dto/NcpResponse.java (90%) create mode 100644 layer-external/src/main/java/org/layer/external/ncp/enums/ImageDomain.java rename layer-external/src/main/java/org/layer/{domain => }/external/ncp/exception/ExternalException.java (84%) rename layer-external/src/main/java/org/layer/{domain => }/external/ncp/service/NcpService.java (92%) diff --git a/layer-api/src/main/java/org/layer/domain/auth/controller/AuthController.java b/layer-api/src/main/java/org/layer/domain/auth/controller/AuthController.java index 3ed33634..2f699d8b 100644 --- a/layer-api/src/main/java/org/layer/domain/auth/controller/AuthController.java +++ b/layer-api/src/main/java/org/layer/domain/auth/controller/AuthController.java @@ -6,7 +6,6 @@ import org.layer.common.annotation.MemberId; import org.layer.domain.auth.controller.dto.*; import org.layer.domain.auth.service.AuthService; -import org.layer.domain.jwt.service.JwtService; import org.layer.domain.member.entity.SocialType; import org.layer.oauth.service.GoogleService; import org.layer.oauth.service.KakaoService; @@ -24,7 +23,6 @@ public class AuthController implements AuthApi { private final AuthService authService; private final GoogleService googleService; private final KakaoService kakaoService; - private final JwtService jwtService; private static final String SOCIAL_TOKEN_NAME = "X-AUTH-TOKEN"; diff --git a/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java b/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java index d88325d9..b12eeb55 100644 --- a/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java +++ b/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java @@ -5,23 +5,22 @@ import org.layer.common.exception.BaseCustomException; import org.layer.domain.auth.controller.dto.*; import org.layer.domain.auth.service.dto.ReissueTokenServiceResponse; -import org.layer.domain.external.google.enums.SheetType; -import org.layer.domain.external.google.service.GoogleApiService; +import org.layer.domain.common.time.Time; import org.layer.domain.jwt.JwtToken; import org.layer.domain.jwt.exception.AuthExceptionType; import org.layer.domain.jwt.service.JwtService; import org.layer.domain.member.entity.Member; import org.layer.domain.member.entity.SocialType; import org.layer.domain.member.service.MemberService; +import org.layer.external.discord.event.SignUpEvent; import org.layer.oauth.dto.service.MemberInfoServiceResponse; import org.layer.oauth.service.GoogleService; import org.layer.oauth.service.KakaoService; import org.layer.oauth.service.apple.AppleService; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.stream.IntStream; - @Slf4j @RequiredArgsConstructor @@ -33,8 +32,8 @@ public class AuthService { private final JwtService jwtService; private final MemberService memberService; - - private final GoogleApiService googleApiService; + private final ApplicationEventPublisher eventPublisher; + private final Time time; //== 로그인 ==// @Transactional @@ -57,12 +56,21 @@ public SignUpResponse signUp(final String socialAccessToken, final SignUpRequest // DB에 회원 저장 Member member = memberService.saveMember(signUpRequest, memberInfo); + publishCreateRetrospectEvent(member); // 토큰 발급 JwtToken jwtToken = jwtService.issueToken(member.getId(), member.getMemberRole()); return SignUpResponse.of(member, jwtToken); } + public void publishCreateRetrospectEvent(final Member member) { + eventPublisher.publishEvent(SignUpEvent.of( + member.getName(), + member.getId(), + time.now() + )); + } + //== 로그아웃 ==// @Transactional public void signOut(final Long memberId) { @@ -76,23 +84,6 @@ public void signOut(final Long memberId) { @Transactional public void withdraw(final Long memberId, WithdrawMemberRequest withdrawMemberRequest) { - // 구글시트 적재 - var foundMemberFeedback = memberService.findFeedback(memberId); - if (foundMemberFeedback.isPresent()) { - /* - * 체크박스 배열을 boolean[]로써 길이 3을 고정으로 한다. - * 0번 인덱스가 true -> +1 - * 1번 인덱스가 true -> +2 - * 3번 인덱스가 true -> +4 - */ - var score = IntStream.range(0, 3) - .filter(i -> Boolean.TRUE.equals(withdrawMemberRequest.booleans()[i])) - .map(i -> (int) Math.pow(2, i)) - .sum(); - googleApiService.writeFeedback(SheetType.WITHDRAW, foundMemberFeedback.get(), score, withdrawMemberRequest.description()); - } - - // soft delete memberService.withdrawMember(memberId); } diff --git a/layer-api/src/main/java/org/layer/domain/external/controller/ExternalController.java b/layer-api/src/main/java/org/layer/domain/external/controller/ExternalController.java index 6e5b8c15..ceae0288 100644 --- a/layer-api/src/main/java/org/layer/domain/external/controller/ExternalController.java +++ b/layer-api/src/main/java/org/layer/domain/external/controller/ExternalController.java @@ -5,8 +5,8 @@ import org.layer.common.annotation.MemberId; import org.layer.domain.external.controller.dto.ExternalRequest; import org.layer.domain.external.controller.dto.ExternalResponse; -import org.layer.domain.external.ncp.dto.NcpResponse; -import org.layer.domain.external.ncp.service.NcpService; +import org.layer.external.ncp.dto.NcpResponse; +import org.layer.external.ncp.service.NcpService; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; diff --git a/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalRequest.java b/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalRequest.java index e73e83ad..25cf6fa6 100644 --- a/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalRequest.java +++ b/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalRequest.java @@ -1,7 +1,7 @@ package org.layer.domain.external.controller.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.layer.domain.external.ncp.enums.ImageDomain; +import org.layer.external.ncp.enums.ImageDomain; public class ExternalRequest { diff --git a/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalResponse.java b/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalResponse.java index e3fbfec5..b6b6e807 100644 --- a/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalResponse.java +++ b/layer-api/src/main/java/org/layer/domain/external/controller/dto/ExternalResponse.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Builder; -import org.layer.domain.external.ncp.exception.ExternalException; +import org.layer.external.ncp.exception.ExternalException; import java.util.Optional; diff --git a/layer-api/src/main/java/org/layer/domain/member/controller/MemberApi.java b/layer-api/src/main/java/org/layer/domain/member/controller/MemberApi.java index 4e60455d..5aebb878 100644 --- a/layer-api/src/main/java/org/layer/domain/member/controller/MemberApi.java +++ b/layer-api/src/main/java/org/layer/domain/member/controller/MemberApi.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.layer.common.annotation.MemberId; -import org.layer.domain.member.controller.dto.CreateFeedbackRequest; import org.layer.domain.member.controller.dto.GetMemberAnalyzesResponse; import org.layer.domain.member.controller.dto.UpdateMemberInfoRequest; @@ -16,10 +15,6 @@ public interface MemberApi { @Operation(summary = "회원 정보(이름, 프로필 사진) 수정", method = "POST", description = "회원의 이름과 프로필 사진(url)을 수정합니다.") ResponseEntity updateMemberInfo(@MemberId Long memberId, @Valid UpdateMemberInfoRequest updateMemberInfoRequest); - @Operation(summary = "서비스 사용에 대한 피드백 남기기", method = "POST") - ResponseEntity createFeedback(@MemberId Long memberId, @Valid CreateFeedbackRequest createFeedbackRequest); - - @Operation(summary = "내 회고 분석 조회", method = "GET") ResponseEntity getMyAnalyzes(@MemberId Long memberId); diff --git a/layer-api/src/main/java/org/layer/domain/member/controller/MemberController.java b/layer-api/src/main/java/org/layer/domain/member/controller/MemberController.java index c4cfbdc7..534d5eeb 100644 --- a/layer-api/src/main/java/org/layer/domain/member/controller/MemberController.java +++ b/layer-api/src/main/java/org/layer/domain/member/controller/MemberController.java @@ -25,13 +25,6 @@ public ResponseEntity updateMemberInfo(@MemberId Long return new ResponseEntity<>(response, HttpStatus.OK); } - - @Override - @PostMapping("/feedback") - public ResponseEntity createFeedback(@MemberId Long memberId, @Valid @RequestBody CreateFeedbackRequest createFeedbackRequest) { - memberService.createFeedback(memberId, createFeedbackRequest); - return null; - } @Override @GetMapping("/analyze") public ResponseEntity getMyAnalyzes(@MemberId Long memberId) { diff --git a/layer-api/src/main/java/org/layer/domain/member/service/MemberService.java b/layer-api/src/main/java/org/layer/domain/member/service/MemberService.java index 64582d95..56712d68 100644 --- a/layer-api/src/main/java/org/layer/domain/member/service/MemberService.java +++ b/layer-api/src/main/java/org/layer/domain/member/service/MemberService.java @@ -9,8 +9,6 @@ import org.layer.domain.analyze.repository.AnalyzeRepository; import org.layer.domain.auth.controller.dto.SignUpRequest; import org.layer.domain.common.time.Time; -import org.layer.domain.external.google.enums.SheetType; -import org.layer.domain.external.google.service.GoogleApiService; import org.layer.domain.jwt.SecurityUtil; import org.layer.domain.member.controller.dto.*; import org.layer.domain.member.entity.Member; @@ -46,8 +44,6 @@ public class MemberService { private final RetrospectRepository retrospectRepository; private final AnalyzeRepository analyzeRepository; - private final GoogleApiService googleApiService; - private final SecurityUtil securityUtil; private final Time time; @@ -74,15 +70,6 @@ public void checkIsNewMember(String socialId, SocialType socialType) { } } - public void createFeedback(Long memberId, CreateFeedbackRequest createFeedbackRequest) { - var foundMemberFeedback = findFeedback(memberId); - if (foundMemberFeedback.isEmpty()) { - return; - } - googleApiService.writeFeedback(SheetType.FEEDBACK, foundMemberFeedback.get(), - createFeedbackRequest.satisfaction(), createFeedbackRequest.description()); - } - @Transactional public Member saveMember(SignUpRequest signUpRequest, MemberInfoServiceResponse memberInfo) { Member member = Member.builder() diff --git a/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java b/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java index 825943f4..117627e7 100644 --- a/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java +++ b/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java @@ -1,10 +1,7 @@ package org.layer.domain.retrospect.service; -import static org.layer.common.exception.RetrospectExceptionType.*; - import lombok.RequiredArgsConstructor; -import org.layer.domain.analyze.service.AnalyzeService; import org.layer.domain.answer.entity.Answers; import org.layer.domain.answer.repository.AnswerRepository; import org.layer.domain.common.time.Time; @@ -30,6 +27,8 @@ import org.layer.domain.space.repository.MemberSpaceRelationRepository; import org.layer.domain.space.repository.SpaceRepository; import org.layer.external.ai.service.AIAnalyzeService; +import org.layer.external.discord.event.CreateRetrospectEvent; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -48,6 +47,8 @@ public class RetrospectService { private final FormRepository formRepository; private final SpaceRepository spaceRepository; + private final ApplicationEventPublisher eventPublisher; + private final AIAnalyzeService aiAnalyzeService; private final Time time; @@ -64,6 +65,8 @@ public Long createRetrospect(RetrospectCreateRequest request, Long spaceId, Long List questions = getQuestions(request.questions(), savedRetrospect.getId(), null); questionRepository.saveAll(questions); + publishCreateRetrospectEvent(retrospect, memberId); + Space space = spaceRepository.findByIdOrThrow(spaceId); // 새로운 폼 생성(수정)인지 확인 @@ -87,7 +90,6 @@ public Long createRetrospect(RetrospectCreateRequest request, Long spaceId, Long // 스페이스 최근 폼 수정 space.updateRecentFormId(savedForm.getId(), memberId); - return savedRetrospect.getId(); } @@ -102,6 +104,14 @@ private Retrospect getRetrospect(RetrospectCreateRequest request, Long spaceId) .build(); } + public void publishCreateRetrospectEvent(final Retrospect retrospect, final Long memberId) { + eventPublisher.publishEvent(CreateRetrospectEvent.of( + retrospect.getTitle(), + memberId, + time.now() + )); + } + public RetrospectListGetResponse getRetrospects(Long spaceId, Long memberId) { // 해당 스페이스 팀원인지 검증 Team team = new Team(memberSpaceRelationRepository.findAllBySpaceId(spaceId)); diff --git a/layer-api/src/main/java/org/layer/domain/space/controller/dto/SpaceRequest.java b/layer-api/src/main/java/org/layer/domain/space/controller/dto/SpaceRequest.java index 93d8626b..408b3479 100644 --- a/layer-api/src/main/java/org/layer/domain/space/controller/dto/SpaceRequest.java +++ b/layer-api/src/main/java/org/layer/domain/space/controller/dto/SpaceRequest.java @@ -24,7 +24,6 @@ public record CreateSpaceRequest( @NotNull SpaceCategory category, @Schema(description = "진행중인 프로젝트 유형") - @NotNull List fieldList, diff --git a/layer-api/src/main/java/org/layer/domain/space/service/SpaceService.java b/layer-api/src/main/java/org/layer/domain/space/service/SpaceService.java index 48ea4912..1efcbea8 100644 --- a/layer-api/src/main/java/org/layer/domain/space/service/SpaceService.java +++ b/layer-api/src/main/java/org/layer/domain/space/service/SpaceService.java @@ -5,7 +5,9 @@ import lombok.extern.slf4j.Slf4j; import org.layer.common.dto.Meta; import org.layer.domain.actionItem.repository.ActionItemRepository; -import org.layer.domain.external.ncp.service.NcpService; +import org.layer.domain.common.time.Time; +import org.layer.external.discord.event.CreateSpaceEvent; +import org.layer.external.ncp.service.NcpService; import org.layer.domain.retrospect.repository.RetrospectRepository; import org.layer.domain.space.controller.dto.SpaceRequest; import org.layer.domain.space.controller.dto.SpaceResponse; @@ -16,6 +18,7 @@ import org.layer.domain.space.exception.SpaceException; import org.layer.domain.space.repository.MemberSpaceRelationRepository; import org.layer.domain.space.repository.SpaceRepository; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +41,10 @@ public class SpaceService { private final ActionItemRepository actionItemRepository; private final RetrospectRepository retrospectRepository; + private final ApplicationEventPublisher eventPublisher; + + private final Time time; + public SpaceResponse.SpacePage getSpaceListFromMemberId(Long memberId, SpaceRequest.GetSpaceRequest getSpaceRequest) { var spacePages = spaceRepository.findAllSpacesByMemberIdAndCategoryAndCursor(memberId, getSpaceRequest.cursorId(), getSpaceRequest.category(), getSpaceRequest.pageSize()); @@ -65,9 +72,18 @@ public Long createSpace(Long memberId, SpaceRequest.CreateSpaceRequest createSpa memberSpaceRelationRepository.save(memberSpaceRelation); + publishCreateSpaceEvent(newSpace, memberId); return newSpace.getId(); } + public void publishCreateSpaceEvent(final Space space, final Long memberId) { + eventPublisher.publishEvent(CreateSpaceEvent.of( + space.getName(), + memberId, + time.now() + )); + } + @Transactional public void updateSpace(Long memberId, SpaceRequest.UpdateSpaceRequest updateSpaceRequest) { spaceRepository.findByIdAndJoinedMemberId(updateSpaceRequest.id(), memberId).orElseThrow(() -> new SpaceException(NOT_FOUND_SPACE)); diff --git a/layer-api/src/main/resources/application-dev.yml b/layer-api/src/main/resources/application-dev.yml index 9a0b49b0..6f4ff31e 100644 --- a/layer-api/src/main/resources/application-dev.yml +++ b/layer-api/src/main/resources/application-dev.yml @@ -83,4 +83,10 @@ admin: password: ${ADMIN_PASSWORD} greeting: - message: this is dev \ No newline at end of file + message: this is dev + +discord: + webhook: + retrospect-url: ${DISCORD_RETROSPECT_URL} + space-url: ${DISCORD_SPACE_URL} + member-url: ${DISCORD_MEMBER_URL} \ No newline at end of file diff --git a/layer-api/src/main/resources/application-local.yml b/layer-api/src/main/resources/application-local.yml index 4e6ac6d3..25fb998c 100644 --- a/layer-api/src/main/resources/application-local.yml +++ b/layer-api/src/main/resources/application-local.yml @@ -88,4 +88,10 @@ admin: password: ${ADMIN_PASSWORD} greeting: - message: this is local \ No newline at end of file + message: this is local + +discord: + webhook: + retrospect-url: ${DISCORD_RETROSPECT_URL} + space-url: ${DISCORD_SPACE_URL} + member-url: ${DISCORD_MEMBER_URL} \ No newline at end of file diff --git a/layer-api/src/main/resources/application-prod.yml b/layer-api/src/main/resources/application-prod.yml index dbfedc7a..b4d50e5a 100644 --- a/layer-api/src/main/resources/application-prod.yml +++ b/layer-api/src/main/resources/application-prod.yml @@ -83,4 +83,10 @@ admin: password: ${ADMIN_PASSWORD} greeting: - message: this is prod \ No newline at end of file + message: this is prod + +discord: + webhook: + retrospect-url: ${DISCORD_RETROSPECT_URL} + space-url: ${DISCORD_SPACE_URL} + member-url: ${DISCORD_MEMBER_URL} \ No newline at end of file diff --git a/layer-external/src/main/java/org/layer/domain/external/google/config/GoogleConfig.java b/layer-external/src/main/java/org/layer/domain/external/google/config/GoogleConfig.java deleted file mode 100644 index 473fcfe1..00000000 --- a/layer-external/src/main/java/org/layer/domain/external/google/config/GoogleConfig.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.layer.domain.external.google.config; - - -import com.google.api.client.auth.oauth2.Credential; -import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; -import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; -import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; -import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.gson.GsonFactory; -import com.google.api.client.util.store.FileDataStoreFactory; -import com.google.api.services.sheets.v4.Sheets; -import com.google.api.services.sheets.v4.SheetsScopes; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; - -import java.io.*; -import java.security.GeneralSecurityException; -import java.util.Collections; -import java.util.List; - -@Configuration -@Slf4j -@RequiredArgsConstructor -public class GoogleConfig { - private final GoogleCredentials googleCredentials; - - private final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); - private final List SCOPES = Collections.singletonList(SheetsScopes.SPREADSHEETS); - @Value("${google.sheet.token_path}") - private String tokenPath; - - @Value("${google.sheet.credential_path}") - private String credentialPath; - - - @Bean - public Sheets getGoogleSheetService() throws GeneralSecurityException, IOException { - final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); - - String APPLICATION_NAME = "Google Sheets API Java Quickstart"; - return new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT)) - .setApplicationName(APPLICATION_NAME) - .build(); - } - - - private Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException { - GoogleClientSecrets clientSecrets = createGoogleClientSecrets(); - - log.info("{}< 0) { - os.write(buffer, 0, length); - } - } - } - } -} diff --git a/layer-external/src/main/java/org/layer/domain/external/google/config/GoogleCredentials.java b/layer-external/src/main/java/org/layer/domain/external/google/config/GoogleCredentials.java deleted file mode 100644 index 02b40a16..00000000 --- a/layer-external/src/main/java/org/layer/domain/external/google/config/GoogleCredentials.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.layer.domain.external.google.config; - - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Data -@Configuration -@ConfigurationProperties(prefix = "google.credentials") -public class GoogleCredentials { - - private Installed installed; - - @Data - public static class Installed { - private String clientId; - private String projectId; - private String authUri; - private String tokenUri; - private String authProviderX509CertUrl; - private String clientSecret; - private List redirectUris; - } -} \ No newline at end of file diff --git a/layer-external/src/main/java/org/layer/domain/external/google/enums/SheetType.java b/layer-external/src/main/java/org/layer/domain/external/google/enums/SheetType.java deleted file mode 100644 index afaa4ca9..00000000 --- a/layer-external/src/main/java/org/layer/domain/external/google/enums/SheetType.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.layer.domain.external.google.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public enum SheetType { - - FEEDBACK("feedback", "A:H"), - WITHDRAW("withdraw", "A:H"); - - private String sheetName; - private String Columns; - - -} diff --git a/layer-external/src/main/java/org/layer/domain/external/google/service/GoogleApiService.java b/layer-external/src/main/java/org/layer/domain/external/google/service/GoogleApiService.java deleted file mode 100644 index 98dc885e..00000000 --- a/layer-external/src/main/java/org/layer/domain/external/google/service/GoogleApiService.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.layer.domain.external.google.service; - -import com.google.api.services.sheets.v4.Sheets; -import com.google.api.services.sheets.v4.model.ValueRange; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.layer.domain.external.google.enums.SheetType; -import org.layer.domain.member.entity.MemberFeedback; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Service -@Slf4j -@RequiredArgsConstructor -public class GoogleApiService { - private final Sheets googleSheetService; - - @Value("${google.sheet.id}") - private String sheetId; - - - public void writeFeedback(SheetType sheetType, MemberFeedback memberFeedback, int score, String description) { - new MemberFeedback(); - try { - String feedbackSheet = sheetType.getSheetName() + "!" + sheetType.getColumns(); - ValueRange response = googleSheetService.spreadsheets().values().get(sheetId, feedbackSheet).execute(); - - List> existingValues = response.getValues(); - int currentRowCount = existingValues != null ? existingValues.size() : 0; - - // 다음 행에서 데이터를 시작하도록 설정 - String range = sheetType.getSheetName() + "!A" + (currentRowCount + 1) + ":H" + (currentRowCount + 1); - List> values = new ArrayList<>(); - List row = new ArrayList<>(); - - row.add(memberFeedback.getMemberId() != null ? memberFeedback.getMemberId() : ""); - row.add(Optional.ofNullable(memberFeedback.getEmail()).orElse("")); - row.add(Optional.ofNullable(memberFeedback.getMemberName()).orElse("")); - row.add(memberFeedback.getMemberCreatedAt() != null ? memberFeedback.getMemberCreatedAt().toString() : ""); - row.add(memberFeedback.getRetrospectCount() != null ? memberFeedback.getRetrospectCount() : ""); - row.add(memberFeedback.getSpaceCount() != null ? memberFeedback.getSpaceCount() : ""); - row.add(score != 0 ? score : ""); - row.add(Optional.ofNullable(description).orElse("")); - - - values.add(row); - ValueRange body = new ValueRange().setValues(values); - googleSheetService.spreadsheets().values().update(sheetId, range, body) - .setValueInputOption("RAW") - .execute(); - - } catch (IOException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/layer-external/src/main/java/org/layer/domain/external/ncp/enums/ImageDomain.java b/layer-external/src/main/java/org/layer/domain/external/ncp/enums/ImageDomain.java deleted file mode 100644 index a8d737ef..00000000 --- a/layer-external/src/main/java/org/layer/domain/external/ncp/enums/ImageDomain.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.layer.domain.external.ncp.enums; - -public enum ImageDomain { - SPACE, -} diff --git a/layer-external/src/main/java/org/layer/external/discord/DiscordAppender.java b/layer-external/src/main/java/org/layer/external/discord/DiscordAppender.java new file mode 100644 index 00000000..444d0d25 --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/DiscordAppender.java @@ -0,0 +1,87 @@ +package org.layer.external.discord; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.layer.external.discord.infra.DiscordWebhookMemberClient; +import org.layer.external.discord.infra.DiscordWebhookRetrospectClient; +import org.layer.external.discord.infra.DiscordWebhookSpaceClient; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Setter +@Component +@RequiredArgsConstructor +public class DiscordAppender { + + private final DiscordWebhookRetrospectClient retrospectClient; + private final DiscordWebhookMemberClient memberClient; + private final DiscordWebhookSpaceClient spaceClient; + + public void createRetrospectAppend(String title, Long memberId, LocalDateTime now) { + String content = "회고"; + int green = 3066993; + Map body = getMessage(title, memberId, now, content, green); + + retrospectClient.sendNotification(body); + } + + public void createSpaceAppend(String title, Long memberId, LocalDateTime now) { + String content = "스페이스"; + int blue = 65280; + Map body = getMessage(title, memberId, now, content, blue); + + spaceClient.sendNotification(body); + } + + public void createMember(String name, Long memberId, LocalDateTime now){ + Map embed = new HashMap<>(); + embed.put("title", "\uD83D\uDE80[회원 가입] 새로운 유저가 가입하였습니다.\uD83D\uDE80"); + embed.put("description", memberId + " 번째 유저가 회원가입하였습니다."); + embed.put("color", 15105570); + + // 필드 추가 + List> fields = List.of( + Map.of("name", "유저 ID", "value", memberId.toString(), "inline", "true"), + Map.of("name", "이름", "value", name, "inline", "true"), + Map.of("name", "생성 시간", "value", now.toString(), "inline", "false") + ); + embed.put("fields", fields); + + // 요청 Body 구성 + Map body = new HashMap<>(); + body.put("content", "회원 가입 알림"); + body.put("embeds", List.of(embed)); + + memberClient.sendNotification(body); + } + + private Map getMessage(String title, Long memberId, LocalDateTime now, String content, int color) { + // Embeds 객체 구성 + Map embed = new HashMap<>(); + embed.put("title", title); + embed.put("description", memberId + " 번 유저가 '" + title + "' " + content + "를 생성했습니다."); + embed.put("color", color); // 컬러 코드 (디스코드 RGB 정수값) + + // 필드 추가 + List> fields = List.of( + Map.of("name", "유저 ID", "value", memberId.toString(), "inline", "true"), + Map.of("name", "제목", "value", title, "inline", "true"), + Map.of("name", "생성 시간", "value", now.toString(), "inline", "false") + ); + embed.put("fields", fields); + + // 요청 Body 구성 + Map body = new HashMap<>(); + body.put("content", "새 " + content + " 생성 알림"); + body.put("embeds", List.of(embed)); + return body; + } + +} diff --git a/layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEvent.java b/layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEvent.java new file mode 100644 index 00000000..9ca03cae --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEvent.java @@ -0,0 +1,13 @@ +package org.layer.external.discord.event; + +import java.time.LocalDateTime; + +public record CreateRetrospectEvent( + String title, + Long memberId, + LocalDateTime createdDate +) { + public static CreateRetrospectEvent of(String title, Long memberId, LocalDateTime createdDate) { + return new CreateRetrospectEvent(title, memberId, createdDate); + } +} diff --git a/layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEventListener.java b/layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEventListener.java new file mode 100644 index 00000000..2f253b54 --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/event/CreateRetrospectEventListener.java @@ -0,0 +1,25 @@ +package org.layer.external.discord.event; + +import org.layer.external.discord.DiscordAppender; +import org.layer.external.discord.event.CreateRetrospectEvent; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@Profile("prod") +@RequiredArgsConstructor +public class CreateRetrospectEventListener { + + private final DiscordAppender discordAppender; + + @EventListener + public void handleSignUpEvent(CreateRetrospectEvent event) { + discordAppender.createRetrospectAppend( + event.title(), + event.memberId(), + event.createdDate()); + } +} diff --git a/layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEvent.java b/layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEvent.java new file mode 100644 index 00000000..ffb6231d --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEvent.java @@ -0,0 +1,13 @@ +package org.layer.external.discord.event; + +import java.time.LocalDateTime; + +public record CreateSpaceEvent( + String title, + Long memberId, + LocalDateTime createdDate +) { + public static CreateSpaceEvent of(String title, Long memberId, LocalDateTime now){ + return new CreateSpaceEvent(title, memberId, now); + } +} diff --git a/layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEventListener.java b/layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEventListener.java new file mode 100644 index 00000000..f6b8c52c --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/event/CreateSpaceEventListener.java @@ -0,0 +1,24 @@ +package org.layer.external.discord.event; + +import org.layer.external.discord.DiscordAppender; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@Profile("prod") +@RequiredArgsConstructor +public class CreateSpaceEventListener { + + private final DiscordAppender discordAppender; + + @EventListener + public void handleSignUpEvent(CreateSpaceEvent event) { + discordAppender.createSpaceAppend( + event.title(), + event.memberId(), + event.createdDate()); + } +} diff --git a/layer-external/src/main/java/org/layer/external/discord/event/SignUpEvent.java b/layer-external/src/main/java/org/layer/external/discord/event/SignUpEvent.java new file mode 100644 index 00000000..9adbfa0c --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/event/SignUpEvent.java @@ -0,0 +1,13 @@ +package org.layer.external.discord.event; + +import java.time.LocalDateTime; + +public record SignUpEvent( + String name, + Long memberId, + LocalDateTime createdDate +) { + public static SignUpEvent of(String name, Long memberId, LocalDateTime now){ + return new SignUpEvent(name, memberId, now); + } +} diff --git a/layer-external/src/main/java/org/layer/external/discord/event/SignUpEventListener.java b/layer-external/src/main/java/org/layer/external/discord/event/SignUpEventListener.java new file mode 100644 index 00000000..628b56df --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/event/SignUpEventListener.java @@ -0,0 +1,23 @@ +package org.layer.external.discord.event; + +import org.layer.external.discord.DiscordAppender; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@Profile("prod") +@RequiredArgsConstructor +public class SignUpEventListener { + private final DiscordAppender discordAppender; + + @EventListener + public void handleSignUpEvent(SignUpEvent event) { + discordAppender.createMember( + event.name(), + event.memberId(), + event.createdDate()); + } +} diff --git a/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookMemberClient.java b/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookMemberClient.java new file mode 100644 index 00000000..ea15f6ce --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookMemberClient.java @@ -0,0 +1,14 @@ +package org.layer.external.discord.infra; + +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "DiscordWebhookMemberClient", url = "${discord.webhook.member-url}") +public interface DiscordWebhookMemberClient { + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + void sendNotification(@RequestBody Map body); +} diff --git a/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookRetrospectClient.java b/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookRetrospectClient.java new file mode 100644 index 00000000..fa803f33 --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookRetrospectClient.java @@ -0,0 +1,15 @@ +package org.layer.external.discord.infra; + +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "DiscordWebhookRetrospectClient", url = "${discord.webhook.retrospect-url}") +public interface DiscordWebhookRetrospectClient { + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + void sendNotification(@RequestBody Map body); + +} diff --git a/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookSpaceClient.java b/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookSpaceClient.java new file mode 100644 index 00000000..e0a6ac35 --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/discord/infra/DiscordWebhookSpaceClient.java @@ -0,0 +1,14 @@ +package org.layer.external.discord.infra; + +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "DiscordWebhookSpaceClient", url = "${discord.webhook.space-url}") +public interface DiscordWebhookSpaceClient { + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + void sendNotification(@RequestBody Map body); +} diff --git a/layer-external/src/main/java/org/layer/domain/external/ncp/config/NcpConfig.java b/layer-external/src/main/java/org/layer/external/ncp/config/NcpConfig.java similarity index 96% rename from layer-external/src/main/java/org/layer/domain/external/ncp/config/NcpConfig.java rename to layer-external/src/main/java/org/layer/external/ncp/config/NcpConfig.java index 90f55800..f06bab44 100644 --- a/layer-external/src/main/java/org/layer/domain/external/ncp/config/NcpConfig.java +++ b/layer-external/src/main/java/org/layer/external/ncp/config/NcpConfig.java @@ -1,4 +1,4 @@ -package org.layer.domain.external.ncp.config; +package org.layer.external.ncp.config; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; diff --git a/layer-external/src/main/java/org/layer/domain/external/ncp/dto/NcpResponse.java b/layer-external/src/main/java/org/layer/external/ncp/dto/NcpResponse.java similarity index 90% rename from layer-external/src/main/java/org/layer/domain/external/ncp/dto/NcpResponse.java rename to layer-external/src/main/java/org/layer/external/ncp/dto/NcpResponse.java index ddcf906f..a92e824d 100644 --- a/layer-external/src/main/java/org/layer/domain/external/ncp/dto/NcpResponse.java +++ b/layer-external/src/main/java/org/layer/external/ncp/dto/NcpResponse.java @@ -1,4 +1,4 @@ -package org.layer.domain.external.ncp.dto; +package org.layer.external.ncp.dto; import lombok.Builder; diff --git a/layer-external/src/main/java/org/layer/external/ncp/enums/ImageDomain.java b/layer-external/src/main/java/org/layer/external/ncp/enums/ImageDomain.java new file mode 100644 index 00000000..534a6a49 --- /dev/null +++ b/layer-external/src/main/java/org/layer/external/ncp/enums/ImageDomain.java @@ -0,0 +1,5 @@ +package org.layer.external.ncp.enums; + +public enum ImageDomain { + SPACE, +} diff --git a/layer-external/src/main/java/org/layer/domain/external/ncp/exception/ExternalException.java b/layer-external/src/main/java/org/layer/external/ncp/exception/ExternalException.java similarity index 84% rename from layer-external/src/main/java/org/layer/domain/external/ncp/exception/ExternalException.java rename to layer-external/src/main/java/org/layer/external/ncp/exception/ExternalException.java index 580d23ca..28505c46 100644 --- a/layer-external/src/main/java/org/layer/domain/external/ncp/exception/ExternalException.java +++ b/layer-external/src/main/java/org/layer/external/ncp/exception/ExternalException.java @@ -1,4 +1,4 @@ -package org.layer.domain.external.ncp.exception; +package org.layer.external.ncp.exception; import org.layer.common.exception.BaseCustomException; import org.layer.common.exception.ExceptionType; diff --git a/layer-external/src/main/java/org/layer/domain/external/ncp/service/NcpService.java b/layer-external/src/main/java/org/layer/external/ncp/service/NcpService.java similarity index 92% rename from layer-external/src/main/java/org/layer/domain/external/ncp/service/NcpService.java rename to layer-external/src/main/java/org/layer/external/ncp/service/NcpService.java index f81cdf55..f998c945 100644 --- a/layer-external/src/main/java/org/layer/domain/external/ncp/service/NcpService.java +++ b/layer-external/src/main/java/org/layer/external/ncp/service/NcpService.java @@ -1,4 +1,4 @@ -package org.layer.domain.external.ncp.service; +package org.layer.external.ncp.service; import com.amazonaws.HttpMethod; import com.amazonaws.services.s3.AmazonS3Client; @@ -7,9 +7,9 @@ import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.layer.domain.external.ncp.dto.NcpResponse; -import org.layer.domain.external.ncp.enums.ImageDomain; -import org.layer.domain.external.ncp.exception.ExternalException; +import org.layer.external.ncp.dto.NcpResponse; +import org.layer.external.ncp.enums.ImageDomain; +import org.layer.external.ncp.exception.ExternalException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service;