diff --git a/.github/workflows/dev-CD.yml b/.github/workflows/dev-CD.yml
index abe6fbf0..80d19424 100644
--- a/.github/workflows/dev-CD.yml
+++ b/.github/workflows/dev-CD.yml
@@ -1,9 +1,13 @@
-name: DEV Dockerhub Push
+name: Push Image to Amazon ECR
on:
push:
branches:
- dev
+env:
+ AWS_REGION: ap-northeast-2
+ ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
+ GITHUB_SHA_FIX: ${{ github.sha }}
jobs:
build:
@@ -40,117 +44,41 @@ jobs:
run: |
cd ./src/main/resources
touch ./application-secret.yml
- echo "${{ secrets.APPLICATION_SECRET}}" > ./application-secret.yml
+ echo "${{ secrets.APPLICATION_SECRET }}" > ./application-secret.yml
shell: bash
# dev.yml 반영
- name: Make application-dev.yml
run: |
- cd ./src/main/resources
- echo "${{ secrets.APPLICATION_DEV}}" > ./application-dev.yml
+ cd ./src/main/resources
+ echo "${{ secrets.APPLICATION_DEV }}" > ./application-release.yml
shell: bash
# Gradle BootJar
- name: BootJar with Gradle
run: ./gradlew clean bootJar -Dspring.profiles.active=dev
- # Docker Image Push
- - name: Docker Image push
- run: |
- docker login -u ${{ secrets.DEV_DOCKERHUB_USERNAME }} -p ${{ secrets.DEV_DOCKERHUB_PASSWORD }}
- docker build -t ${{ secrets.DEV_DOCKERHUB_USERNAME}}/${{ secrets.DEV_DOCKERHUB_REPOSITORY}} ./
- docker push ${{ secrets.DEV_DOCKERHUB_USERNAME }}/${{ secrets.DEV_DOCKERHUB_REPOSITORY}}
-
-#name: Beanstalk Deploy
-#
-#on:
-# push:
-# branches:
-# - dev
-#
-#jobs:
-# build:
-# # ubuntu 버전 지정
-# runs-on: ubuntu-22.04
-# steps:
-# # Checkout 진행
-# - uses: actions/checkout@v3
-#
-# # JDK 11 설치
-# - name: Set up JDK 11
-# uses: actions/setup-java@v3
+ # Configure AWS Credentials by using IAM inform
+# - name: Configure AWS credentials
+# uses: aws-actions/configure-aws-credentials@v1
# with:
-# java-version: '11'
-# distribution: 'temurin'
-#
-# # Gradle 캐싱
-# - name: Gradle Caching
-# uses: actions/cache@v3
-# with:
-# path: |
-# ~/.gradle/caches
-# ~/.gradle/wrapper
-# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
-# restore-keys: |
-# ${{ runner.os }}-gradle-
-#
-# # Gradle 권한 부여
-# - name: Grant execute permission for gradlew
-# run: chmod +x gradlew
-#
-# # yml 반영
-# - name: Make application-secret.yml
-# run: |
-# cd ./src/main/resources
-# touch ./application-secret.yml
-# echo "${{ secrets.APPLICATION_SECRET}}" > ./application-secret.yml
-# shell: bash
-#
-## - name: Make firebase json
-## run: |
-## mkdir -p ./src/main/resources/firebase
-## echo "${{ secrets.FIREBASE_DEV_ADMIN}}" > ./src/main/resources/firebase/firebase-dev-admin.json
-## shell: bash
-#
-## # makeFiles.config 반영
-## - name: Make 00-makeFiles.config
-## run: |
-## cd ./.ebextensions
-## touch ./00-makeFiles.config
-## echo "${{ secrets.DEV_MAKEFILES}}" > ./00-makeFiles.config
-## shell: bash
-#
-# # Gradle BootJar
-# - name: BootJar with Gradle
-# run: ./gradlew clean bootJar
-#
-# # 현재 시간 반영
-# - name: Get current time
-# uses: 1466587594/get-current-time@v2
-# id: current-time
-# with:
-# format: YYYY-MM-DDTHH-mm-ss
-# utcOffset: "+09:00"
-#
-# # grandle build를 통해 만들어진 jar를 beanstalk에 배포하기 위한 zip 파일로 만드는 것
-# - name: Generate deployment package
+# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} # 나의 ECR 정보
+# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+# aws-region: ${{ env.AWS_REGION }}
+
+ # Login to ECR
+# - name: Login to Amazon ECR
+# id: login-ecr
+# uses: aws-actions/amazon-ecr-login@v1
+
+ # Docker Image Push to ECR and Run container with Image pull from ECR
+# - name: Build, tag, and push image to Amazon ECR
+# id: build-image
+# env:
+# ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
+# IMAGE_TAG: ${{ env.GITHUB_SHA_FIX }}
# run: |
-# mkdir -p deploy
-# cp build/libs/*.jar deploy/application.jar
-# cp Procfile deploy/Procfile
-# cp -r .ebextensions deploy/.ebextensions
-# cp -r .platform deploy/.platform
-# cd deploy && zip -r deploy.zip .
-#
-# # Beanstalk Deploy 플러그인 사용
-# - name: Beanstalk Deploy
-# uses: einaregilsson/beanstalk-deploy@v14
-# with:
-# aws_access_key: ${{ secrets.AWS_ACCESS_KEY }} # github secrets로 등록한 값 사용
-# aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # github secrets로 등록한 값 사용
-# application_name: Gwalit-dev # EB application 이름
-# environment_name: Gwalit-dev-env # EB environment 이름
-# version_label: Github Action-${{steps.current-time.outputs.formattedTime}} # 배포 버전은 타임스탬프를 이용하여 구분
-# region: ap-northeast-2
-# deployment_package: deploy/deploy.zip
-# wait_for_environment_recovery: 100 # default wait time은 30초이며, 필자의 EB가 느려서 180초로 지정했습니다(지정 안하면 간혹 timeout 발생).
\ No newline at end of file
+# # Build a docker container and push it to ECR so that it can be deployed to ECS.
+# docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
+# docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
+# echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
\ No newline at end of file
diff --git a/.github/workflows/release-CD.yml b/.github/workflows/release-CD.yml
index 9f61ffbb..40653c0c 100644
--- a/.github/workflows/release-CD.yml
+++ b/.github/workflows/release-CD.yml
@@ -1,10 +1,15 @@
-name: Beanstalk Deploy
+name: Deploy release version
on:
push:
branches:
- release
+env:
+ AWS_REGION: ap-northeast-2
+ ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
+ GITHUB_SHA: ${{ github.sha }}
+
jobs:
build:
# ubuntu 버전 지정
@@ -40,137 +45,56 @@ jobs:
run: |
cd ./src/main/resources
touch ./application-secret.yml
- echo "${{ secrets.APPLICATION_SECRET}}" > ./application-secret.yml
+ echo "${{ secrets.APPLICATION_SECRET }}" > ./application-secret.yml
shell: bash
# release.yml 반영
- name: Make application-release.yml
run: |
cd ./src/main/resources
- echo "${{ secrets.GCP_APPLICATION_RELEASE}}" > ./application-release.yml
+ echo "${{ secrets.APPLICATION_RELEASE }}" > ./application-release.yml
shell: bash
# Gradle BootJar
- name: BootJar with Gradle
run: ./gradlew clean bootJar -Dspring.profiles.active=release
- # Docker Image Push
- - name: Docker Image push
+ # Configure AWS Credentials by using IAM inform
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} # 나의 ECR 정보
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+
+ # Login to ECR
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+
+ # Docker Image Push to ECR and Run container with Image pull from ECR
+ - name: Build, tag, and push image to Amazon ECR
+ id: build-image
+ env:
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
+ IMAGE_TAG: ${{ env.GITHUB_SHA }}
run: |
- docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
- docker build -t ${{ secrets.DOCKERHUB_USERNAME}}/${{ secrets.DOCKERHUB_REPOSITORY}} ./
- docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY}}
+ # Build a docker container and push it to ECR so that it can be deployed to ECS.
+ docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
+ docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
+ echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
# Docker Compose
- name: Docker Compose
- uses: appleboy/ssh-action@master
+ uses: appleboy/ssh-action@v1.0.3
with:
- host: ${{ secrets.GCP_SERVER_IP }}
+ host: ${{ secrets.AWS_SERVER_IP }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
- passphrase: ${{ secrets.SSH_PASSPHRASE }}
- envs: GITHUB_SHA
script: |
- sudo docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
- sudo docker-compose stop ${{ secrets.DOCKER_SERVICE_NAME }}
- sudo docker-compose rm -f ${{ secrets.DOCKER_SERVICE_NAME }}
- sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY}}
- sudo docker tag ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY}} ${{ secrets.DOCKER_IMAGE_NAME }}
- sudo docker-compose up -d
-
-
-#name: Beanstalk Deploy
-#
-#on:
-# push:
-# branches:
-# - release
-#
-#jobs:
-# build:
-# # ubuntu 버전 지정
-# runs-on: ubuntu-22.04
-# steps:
-# # Checkout 진행
-# - uses: actions/checkout@v3
-#
-# # JDK 11 설치
-# - name: Set up JDK 11
-# uses: actions/setup-java@v3
-# with:
-# java-version: '11'
-# distribution: 'temurin'
-#
-# # Gradle 캐싱
-# - name: Gradle Caching
-# uses: actions/cache@v3
-# with:
-# path: |
-# ~/.gradle/caches
-# ~/.gradle/wrapper
-# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
-# restore-keys: |
-# ${{ runner.os }}-gradle-
-#
-# # Gradle 권한 부여
-# - name: Grant execute permission for gradlew
-# run: chmod +x gradlew
-#
-# # secret.yml 반영
-# - name: Make application-secret.yml
-# run: |
-# cd ./src/main/resources
-# touch ./application-secret.yml
-# echo "${{ secrets.APPLICATION_SECRET}}" > ./application-secret.yml
-# shell: bash
-#
-# # release.yml 반영
-# - name: Make application-release.yml
-# run: |
-# cd ./src/main/resources
-# echo "${{ secrets.APPLICATION_RELEASE}}" > ./application-release.yml
-# shell: bash
-#
-#
-## # makeFiles.config 반영
-## - name: Make 00-makeFiles.config
-## run: |
-## cd ./.ebextensions
-## touch ./00-makeFiles.config
-## echo "${{ secrets.RELEASE_MAKEFILES}}" > ./00-makeFiles.config
-## shell: bash
-#
-# # Gradle BootJar
-# - name: BootJar with Gradle
-# run: ./gradlew clean bootJar -Dspring.profiles.active=release
-#
-# # 현재 시간 반영
-# - name: Get current time
-# uses: 1466587594/get-current-time@v2
-# id: current-time
-# with:
-# format: YYYY-MM-DDTHH-mm-ss
-# utcOffset: "+09:00"
-#
-# # grandle build를 통해 만들어진 jar를 beanstalk에 배포하기 위한 zip 파일로 만드는 것
-# - name: Generate deployment package
-# run: |
-# mkdir -p deploy
-# cp build/libs/*.jar deploy/application.jar
-# cp Procfile deploy/Procfile
-# cp -r .ebextensions deploy/.ebextensions
-# cp -r .platform deploy/.platform
-# cd deploy && zip -r deploy.zip .
-#
-# # Beanstalk Deploy 플러그인 사용
-# - name: Beanstalk Deploy
-# uses: einaregilsson/beanstalk-deploy@v14
-# with:
-# aws_access_key: ${{ secrets.AWS_ACCESS_KEY }} # github secrets로 등록한 값 사용
-# aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # github secrets로 등록한 값 사용
-# application_name: Gwalit-release # EB application 이름
-# environment_name: Gwalit-release-env # EB environment 이름
-# version_label: Github Action-${{steps.current-time.outputs.formattedTime}} # 배포 버전은 타임스탬프를 이용하여 구분
-# region: ap-northeast-2
-# deployment_package: deploy/deploy.zip
-# wait_for_environment_recovery: 100 # default wait time은 30초이며, 필자의 EB가 느려서 180초로 지정했습니다(지정 안하면 간혹 timeout 발생).
\ No newline at end of file
+ aws ecr get-login-password --region ${{ env.AWS_REGION }} | docker login --username AWS --password-stdin ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}
+ docker-compose stop ${{ secrets.DOCKER_SERVICE_NAME }}
+ docker-compose rm -f ${{ secrets.DOCKER_SERVICE_NAME }}
+ docker pull ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.GITHUB_SHA_FIX }}
+ docker tag ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.GITHUB_SHA_FIX }} ${{ secrets.DOCKER_IMAGE_NAME }}
+ docker-compose up -d
diff --git a/README.md b/README.md
index d016a7a7..b8c32f43 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,79 @@
-# gwalit-server
-과릿 서버
+# 과릿 - 선생님과 학생이 함께 쓰는 쉬운 수업 관리 서비스
+![image](https://github.com/self-runner/gwalit-server/assets/76556999/a25468fb-dc2c-4ea9-8724-8908141aa06b)
+
+## 과릿 사용하기
+- [안드로이드 설치하기](https://bit.ly/gwarit-android)
+- [애플 설치하기](https://bit.ly/gwarit-apple)
+- [과릿 공지사항](https://wjdwls.notion.site/Gwarit-c2f1540c688a4e85ab132be6992c23cb?pvs=4)
+- [과릿 콘텐츠자료실](https://wjdwls.notion.site/5c7069be76044bf2b6ba5464a4841134?pvs=4)
+- [과릿 인스타그램](https://www.instagram.com/gwa_rit)
+
+## 개발
+### 기술 스택
+**Language&Framework**
+- Java11/Spring Framework (Spring Boot 2.7.13)
+- Spring Data JPA, QueryDsl
+- Spring Batch
+- Swagger
+
+**Database**
+- MySQL
+- Redis
+
+**Infra**
+- AWS Elastic Beanstalk, EC2, AWS RDS, AWS LB, Nginx, AWS S3
+- GCP GCE, ALB, GCS, Cloud SQL
+- Git, Github Actions
+
+**ETC**
+- Slack
+- Sentry
+- JWT
+- Nave SENS API, CoolSMS
+- FCM, Jasypt
+
+### 주요 개발 내용
+- 스프링 배치를 활용한 매일 오전 9시 지정 알림 발송:
금일의 수업 여부를 알려주기 위해, 스프링 배치를 활용하여 매일 오전 9시 대상 사용자에 한해 FCM 알림 발송
+- Bulk Insert를 활용한 API 성능 개선:
JPA의 saveAll의 기존 코드를 JdbcTemplate의 Bulk Insert로 실행 시간 60% 감소 (최대 3초 -> 최대 1.2초)
+- 비동기 설정을 통해 FCM 전송 성능 개선:
FCM 알림 발송 로직 비동기 처리를 통해 실행 시간 25% 감소 (최대 2초 -> 최대 1.5초)
+- [2시간 주기의 Active Health Check
Github Actions, Slack을 활용하여 2시간 주기마다 서버 헬스체크 진행](https://velog.io/@dl-00-e8/%EC%84%9C%EB%B2%84-%ED%97%AC%EC%8A%A4%EC%B2%B4%ED%81%AC-Github-Actions%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B2%84-%ED%97%AC%EC%8A%A4%EC%B2%B4%ED%81%AC)
+- Sentry를 활용한 슬로우 쿼리, 에러 모니터링
AOP를 활용한 슬로우 쿼리 측정 및 서버 에러 발생 시, Sentry 이벤트와 Slack 알림 발송 설정
+- 사용자별 테마 적용을 위한 데이터베이스 설계
사용자가 클래스별 테마를 설정할 수 있도록 참조 테이블을 활용하여 이름/색상 정보 관리
+- 운영 환경과 개발 환경 분리 구성(AWS 활용)
+- [서버 비용 최소화를 위한 AWS에서 GCP로의 인프라 전환 (다운타임 최소화)](https://velog.io/@dl-00-e8/GCP-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EB%A5%BC-%EB%8F%84%EC%BB%A4%EC%97%90-%EB%84%A3%EC%96%B4%EC%84%9C-GCE%EC%97%90-%EC%98%AC%EB%A0%A4%EB%B3%B4%EA%B2%A0%EB%82%98)
+- [Naver SENS API의 개인 대상 서비스 종료로 인한, CoolSMS 전환](https://velog.io/@dl-00-e8/%EA%B3%BC%EB%A6%BF-Cool-SMS-%EC%A0%84%ED%99%98%EA%B8%B0)
+
+### ERD
+**V3 (2023. 12)**
+![image](https://github.com/self-runner/gwalit-server/assets/76556999/df4ff755-7f50-4b72-ac9d-289dec16064a)
+
+V2 (2023. 11)
+
+
+![gwalitdb](https://github.com/self-runner/gwalit-server/assets/76556999/4f5639b5-443e-4439-b52d-7a597485b391)
+
+
+
+
+V1 (2023. 06)
+
+
+![image](https://github.com/self-runner/gwalit-server/assets/76556999/787d2997-8022-41b8-8409-765390e5262b)
+
+
+
+
+
+
+
+### Infra Architecture
+**V2 (2023. 01 ~ NOW)**
+![gwalit-architecture-gcp 1](https://github.com/self-runner/gwalit-server/assets/76556999/59904463-e604-4a5b-b524-c7e337032f68)
+
+V1 (2023. 06 ~ 2023. 12)
+
+
+![image](https://github.com/self-runner/gwalit-server/assets/76556999/4c9cccf0-6388-4a83-a3bb-db2470e5bf8b)
+
+
+
diff --git a/build.gradle b/build.gradle
index 76f866fc..6e69d3d3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -88,6 +88,9 @@ dependencies {
// FCM
implementation 'com.google.firebase:firebase-admin:9.2.0'
+
+ // CoolSMS
+ implementation 'net.nurigo:javaSDK:2.2'
}
tasks.named('test') {
diff --git a/src/main/java/com/selfrunner/gwalit/GwalitApplication.java b/src/main/java/com/selfrunner/gwalit/GwalitApplication.java
index fc7f5928..adb28e61 100644
--- a/src/main/java/com/selfrunner/gwalit/GwalitApplication.java
+++ b/src/main/java/com/selfrunner/gwalit/GwalitApplication.java
@@ -6,7 +6,6 @@
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.EnableAsync;
-import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@EnableAsync
@EnableJpaAuditing
diff --git a/src/main/java/com/selfrunner/gwalit/domain/board/repository/BoardRepositoryImpl.java b/src/main/java/com/selfrunner/gwalit/domain/board/repository/BoardRepositoryImpl.java
index cb12b089..d4e0273f 100644
--- a/src/main/java/com/selfrunner/gwalit/domain/board/repository/BoardRepositoryImpl.java
+++ b/src/main/java/com/selfrunner/gwalit/domain/board/repository/BoardRepositoryImpl.java
@@ -41,7 +41,7 @@ public Slice findBoardPaginationByCategory(Member m, Long lectureI
.leftJoin(lecture).on(board.lecture.lectureId.eq(lecture.lectureId))
.leftJoin(member).on(board.member.memberId.eq(member.memberId))
.leftJoin(reply).on(board.boardId.eq(reply.board.boardId))
- .where(board.lecture.lectureId.eq(lectureId), eqCursorAndCursorCreatedAt(cursor, cursorCreatedAt), checkCategory(category), board.deletedAt.isNull())
+ .where(board.lecture.lectureId.eq(lectureId), eqCursorAndCursorCreatedAt(cursor, cursorCreatedAt), checkCategory(category), board.deletedAt.isNull(), reply.deletedAt.isNull())
.orderBy(board.createdAt.desc(), board.boardId.asc())
.groupBy(board.boardId)
.limit(pageable.getPageSize() + 1)
diff --git a/src/main/java/com/selfrunner/gwalit/domain/lecture/service/LectureService.java b/src/main/java/com/selfrunner/gwalit/domain/lecture/service/LectureService.java
index 36a13804..b421f82b 100644
--- a/src/main/java/com/selfrunner/gwalit/domain/lecture/service/LectureService.java
+++ b/src/main/java/com/selfrunner/gwalit/domain/lecture/service/LectureService.java
@@ -25,7 +25,7 @@
import com.selfrunner.gwalit.global.common.Schedule;
import com.selfrunner.gwalit.global.exception.ErrorCode;
import com.selfrunner.gwalit.global.util.fcm.FCMClient;
-import com.selfrunner.gwalit.global.util.sms.SmsClient;
+import com.selfrunner.gwalit.global.util.sms.CoolSMSClient;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -52,7 +52,7 @@ public class LectureService {
private final LessonRepository lessonRepository;
private final LessonJdbcRepository lessonJdbcRepository;
private final HomeworkRepository homeworkRepository;
- private final SmsClient smsClient;
+ private final CoolSMSClient smsClient;
private final FCMClient fcmClient;
@Transactional
@@ -285,7 +285,7 @@ public void inviteStudent(Member member, Long lectureId, PostInviteReq postInvit
Member check = memberRepository.findNotFakeByPhoneAndType(postInviteReq.getPhone(), MemberType.STUDENT).orElse(null);
if(check != null) {
if(check.getState().equals(MemberState.INVITE)) {
- smsClient.sendInvitation(member.getName(), lectureName, postInviteReq, Boolean.TRUE);
+ smsClient.sendInvitation(postInviteReq, Boolean.TRUE);
}
if(check.getState().equals(MemberState.ACTIVE) && check.getToken() != null) {
// smsClient.sendInvitation(member.getName(), lectureName,postInviteReq, Boolean.FALSE);
@@ -302,7 +302,7 @@ public void inviteStudent(Member member, Long lectureId, PostInviteReq postInvit
memberAndLectureRepository.save(studentAndLecture);
}
if(check == null) {
- smsClient.sendInvitation(member.getName(), memberAndLecture.getLecture().getName(),postInviteReq, Boolean.TRUE);
+ smsClient.sendInvitation(postInviteReq, Boolean.TRUE);
Member student = postInviteReq.toEntity();
memberRepository.save(student);
MemberAndLecture studentAndLecture = MemberAndLecture.builder()
diff --git a/src/main/java/com/selfrunner/gwalit/domain/member/service/AuthService.java b/src/main/java/com/selfrunner/gwalit/domain/member/service/AuthService.java
index 60319ee5..82a4f816 100644
--- a/src/main/java/com/selfrunner/gwalit/domain/member/service/AuthService.java
+++ b/src/main/java/com/selfrunner/gwalit/domain/member/service/AuthService.java
@@ -22,7 +22,7 @@
import com.selfrunner.gwalit.global.util.jwt.TokenDto;
import com.selfrunner.gwalit.global.util.jwt.TokenProvider;
import com.selfrunner.gwalit.global.util.redis.RedisClient;
-import com.selfrunner.gwalit.global.util.sms.SmsClient;
+import com.selfrunner.gwalit.global.util.sms.CoolSMSClient;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -40,7 +40,7 @@
@RequiredArgsConstructor
public class AuthService {
- private final SmsClient smsClient;
+ private final CoolSMSClient smsClient;
private final RedisClient redisClient;
private final TokenProvider tokenProvider;
private final MemberRepository memberRepository;
@@ -53,13 +53,11 @@ public class AuthService {
public void sendAuthorizationCode(PostAuthPhoneReq postAuthPhoneReq) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {
// Business Logic - 테스트계정은 문자 발송이 되지 않도록 수정
if(!postAuthPhoneReq.getPhone().equals("01011111111")) {
- System.out.println("test");
String authorizationCode = smsClient.sendAuthorizationCode(postAuthPhoneReq);
redisClient.setValue(postAuthPhoneReq.getPhone(), authorizationCode, 300L);
}
-
// Response
}
public void checkAuthorizationCode(PostAuthCodeReq postAuthCodeReq) {
diff --git a/src/main/java/com/selfrunner/gwalit/global/util/sms/CoolSMSClient.java b/src/main/java/com/selfrunner/gwalit/global/util/sms/CoolSMSClient.java
new file mode 100644
index 00000000..aeea68f4
--- /dev/null
+++ b/src/main/java/com/selfrunner/gwalit/global/util/sms/CoolSMSClient.java
@@ -0,0 +1,130 @@
+package com.selfrunner.gwalit.global.util.sms;
+
+import com.selfrunner.gwalit.domain.lecture.dto.request.PostInviteReq;
+import com.selfrunner.gwalit.domain.member.dto.request.PostAuthCodeReq;
+import com.selfrunner.gwalit.domain.member.dto.request.PostAuthPhoneReq;
+import net.nurigo.java_sdk.api.Message;
+import net.nurigo.java_sdk.exceptions.CoolsmsException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+@Component
+public class CoolSMSClient {
+
+ @Value("${coolsms.api-key}")
+ private String apiKey;
+ @Value("${coolsms.api-secret}")
+ private String apiSecretKey;
+ @Value("${coolsms.sender-phone}")
+ private String senderPhone;
+
+ // 단일 메시지 발송 예제
+ public String sendAuthorizationCode(PostAuthPhoneReq postAuthPhoneReq) {
+ // 인증 번호 생성
+ String authorizationCode = Integer.toString((int)(Math.random() * (999999 - 100000 + 1)) + 100000);
+
+ Message coolsms = new Message(apiKey, apiSecretKey);
+
+ HashMap params = new HashMap<>();
+ params.put("to", postAuthPhoneReq.getPhone());
+ params.put("from", senderPhone);
+ params.put("type", "SMS");
+ params.put("text", "[과릿]" + "\n" + "인증번호: " + authorizationCode + "\n" + "인증 번호를 입력해주세요.");
+ params.put("app_version", "Gwarit 1.3.4");
+
+ try {
+ coolsms.send(params);
+ } catch (CoolsmsException e) {
+ throw new RuntimeException(e);
+ }
+
+ return authorizationCode;
+ }
+
+ public String sendTemporaryPassword(PostAuthCodeReq postAuthCodeReq) {
+ // 임시 비밀번호 발급
+ char[] list = new char[] {
+ '1','2','3','4','5','6','7','8','9','0',
+ 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
+ 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
+ '!','@','#','$','%','^','&','*','(',')'};
+
+ StringBuilder temporaryPassword = new StringBuilder();
+
+ // 숫자 하나 추가
+ int idx = (int)(Math.random() * 10); // 0에서 9 사이의 인덱스 선택
+ temporaryPassword.append(list[idx]);
+
+ // 문자 하나 추가
+ idx = (int)(Math.random() * 26) + 10; // 10에서 35 사이의 인덱스 선택 (알파벳 대문자)
+ temporaryPassword.append(list[idx]);
+
+ // 특수 문자 하나 추가
+ idx = (int)(Math.random() * 9) + 62; // 36에서 44 사이의 인덱스 선택 (특수 문자)
+ temporaryPassword.append(list[idx]);
+
+ for(int i = 3; i < 10; i++) {
+ idx = (int) (Math.random() * (list.length));
+ temporaryPassword.append(list[idx]);
+ }
+ // 문자열을 문자 리스트로 변환
+ List charList = new ArrayList<>();
+ for (char c : temporaryPassword.toString().toCharArray()) {
+ charList.add(c);
+ }
+
+ // 문자 리스트를 섞기
+ Collections.shuffle(charList);
+
+ // 섞인 문자 리스트를 다시 문자열로 변환
+ StringBuilder shuffledPassword = new StringBuilder();
+ for (char c : charList) {
+ shuffledPassword.append(c);
+ }
+
+ Message coolsms = new Message(apiKey, apiSecretKey);
+
+ HashMap params = new HashMap<>();
+ params.put("to", postAuthCodeReq.getPhone());
+ params.put("from", senderPhone);
+ params.put("type", "SMS");
+ params.put("text", "[과릿]" + "\n" + "임시비밀번호: " + "\n" + temporaryPassword + "\n" + "\n" + "로그인 이후 비밀번호를 변경해주세요.");
+ params.put("app_version", "Gwarit 1.3.4");
+
+ try {
+ coolsms.send(params);
+ } catch (CoolsmsException e) {
+ throw new RuntimeException(e);
+ }
+
+ return temporaryPassword.toString();
+ }
+
+ public void sendInvitation(PostInviteReq postInviteReq, Boolean type) {
+ Message coolsms = new Message(apiKey, apiSecretKey);
+
+ HashMap params = new HashMap<>();
+ params.put("to", postInviteReq.getPhone());
+ params.put("from", senderPhone);
+ params.put("type", "LMS");
+ params.put("app_version", "Gwarit 1.3.4");
+
+ if(type.equals(Boolean.TRUE)) {
+ params.put("text", "[과릿]" + "\n" + "선생님으로부터 클래스 초대가 도착했습니다." + "\n" + "아래 링크를 통해 앱 설치 및 회원가입을 통해 수업에 참여해보세요!" + "\n" + "\n" + "안드로이드: " + "https://bit.ly/gwarit-android" + "\n" + "\n" + "애플: " + "https://bit.ly/gwarit-apple");
+ }
+ if(type.equals(Boolean.FALSE)) {
+ params.put("text", "[과릿]" + "\n" + "선생님으로부터 클래스 초대가 도착했습니다." + "\n" + "아래 링크를 클릭 후 앱 열기를 통해 수업에 참여해보세요!" + "\n" + "\n" + "안드로이드: " + "https://bit.ly/gwarit-android" + "\n" + "\n" + "애플: " + "https://bit.ly/gwarit-apple");
+ }
+
+ try {
+ coolsms.send(params);
+ } catch (CoolsmsException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/com/selfrunner/gwalit/global/util/sms/SmsClient.java b/src/main/java/com/selfrunner/gwalit/global/util/sms/SmsClient.java
deleted file mode 100644
index e19ccbe8..00000000
--- a/src/main/java/com/selfrunner/gwalit/global/util/sms/SmsClient.java
+++ /dev/null
@@ -1,221 +0,0 @@
-package com.selfrunner.gwalit.global.util.sms;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.selfrunner.gwalit.domain.lecture.dto.request.PostInviteReq;
-import com.selfrunner.gwalit.domain.member.dto.request.PostAuthCodeReq;
-import com.selfrunner.gwalit.domain.member.dto.request.PostAuthPhoneReq;
-import com.selfrunner.gwalit.global.util.sms.dto.SmsMessageDto;
-import com.selfrunner.gwalit.global.util.sms.dto.SmsNaverReq;
-import com.selfrunner.gwalit.global.util.sms.dto.SmsNaverRes;
-import lombok.RequiredArgsConstructor;
-import org.apache.tomcat.util.codec.binary.Base64;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
-import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-@Service
-@RequiredArgsConstructor
-public class SmsClient {
- @Value("${naver-sens-api-service-id}")
- private String serviceId;
-
- @Value("${naver-sens-api-access-key}")
- private String accessKey;
-
- @Value("${naver-sens-api-secret-key}")
- private String secretKey;
-
- @Value("${naver-sens-api-sender-phone}")
- private String senderPhone;
-
- public String sendAuthorizationCode(PostAuthPhoneReq postAuthPhoneReq) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {
- // 인증 번호 생성
- String authorizationCode = Integer.toString((int)(Math.random() * (999999 - 100000 + 1)) + 100000);
- Long time = System.currentTimeMillis();
-
- // API 요청 Header, Body 구성
- List smsMessageDtoList = new ArrayList<>();
-
- smsMessageDtoList.add(new SmsMessageDto(postAuthPhoneReq.getPhone(), "[과릿] 인증번호: " + authorizationCode + "\n 인증 번호를 입력해주세요."));
- ObjectMapper objectMapper = new ObjectMapper();
- String jsonBody = objectMapper.writeValueAsString(new SmsNaverReq("SMS", this.senderPhone, authorizationCode, smsMessageDtoList));
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
- headers.set("x-ncp-apigw-timestamp", time.toString());
- headers.set("x-ncp-iam-access-key", this.accessKey);
- String sig = makeSignature(time); // 암호화
- headers.set("x-ncp-apigw-signature-v2", sig);
-
- // Header 포함 전송
- HttpEntity body = new HttpEntity<>(jsonBody, headers);
-
- RestTemplate restTemplate = new RestTemplate();
- restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
- SmsNaverRes smsNaverRes = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/"+this.serviceId+"/messages"), body, SmsNaverRes.class);
-
- // 실페 시, Error 던지기
- if(!smsNaverRes.getStatusCode().equals("202")) {
- throw new RuntimeException("문자 전송에 실패했습니다.");
- }
-
- // 성공 시, authorizationCode 반환
- return authorizationCode;
- }
-
- public String sendTemporaryPassword(PostAuthCodeReq postAuthCodeReq) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {
- // 임시 비밀번호 발급
- char list[] = new char[] {
- '1','2','3','4','5','6','7','8','9','0',
- 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
- 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
- '!','@','#','$','%','^','&','*','(',')'};
-
- String temporaryPassword = "";
-
- // 숫자 하나 추가
- int idx = (int)(Math.random() * 10); // 0에서 9 사이의 인덱스 선택
- temporaryPassword += list[idx];
-
- // 문자 하나 추가
- idx = (int)(Math.random() * 26) + 10; // 10에서 35 사이의 인덱스 선택 (알파벳 대문자)
- temporaryPassword += list[idx];
-
- // 특수 문자 하나 추가
- idx = (int)(Math.random() * 9) + 62; // 36에서 44 사이의 인덱스 선택 (특수 문자)
- temporaryPassword += list[idx];
-
- for(Integer i = 3; i < 10; i++) {
- idx = (int) (Math.random() * (list.length));
- temporaryPassword += list[idx];
- }
- // 문자열을 문자 리스트로 변환
- List charList = new ArrayList<>();
- for (char c : temporaryPassword.toCharArray()) {
- charList.add(c);
- }
-
- // 문자 리스트를 섞기
- Collections.shuffle(charList);
-
- // 섞인 문자 리스트를 다시 문자열로 변환
- StringBuilder shuffledPassword = new StringBuilder();
- for (char c : charList) {
- shuffledPassword.append(c);
- }
- Long time = System.currentTimeMillis();
-
- // API 요청 Header, Body 구성
- List smsMessageDtoList = new ArrayList<>();
-
- smsMessageDtoList.add(new SmsMessageDto(postAuthCodeReq.getPhone(), "[과릿] 임시비밀번호: " + "\n" + temporaryPassword + "\n" + "\n" + "로그인 이후 비밀번호를 변경해주세요."));
- ObjectMapper objectMapper = new ObjectMapper();
- String jsonBody = objectMapper.writeValueAsString(new SmsNaverReq("SMS", this.senderPhone, temporaryPassword, smsMessageDtoList));
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
- headers.set("x-ncp-apigw-timestamp", time.toString());
- headers.set("x-ncp-iam-access-key", this.accessKey);
- String sig = makeSignature(time); // 암호화
- headers.set("x-ncp-apigw-signature-v2", sig);
-
- // Header 포함 전송
- HttpEntity body = new HttpEntity<>(jsonBody, headers);
-
- RestTemplate restTemplate = new RestTemplate();
- restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
- SmsNaverRes smsNaverRes = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/"+this.serviceId+"/messages"), body, SmsNaverRes.class);
-
- // 실페 시, Error 던지기
- if(!smsNaverRes.getStatusCode().equals("202")) {
- throw new RuntimeException("문자 전송에 실패했습니다.");
- }
-
- // 성공 시, 임시 비밀번호 반환
- return temporaryPassword;
- }
-
- public Void sendInvitation(String name, String lectureName, PostInviteReq postInviteReq, Boolean type) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {
- // API 요청 Header, Body 구성
- Long time = System.currentTimeMillis();
- List smsMessageDtoList = new ArrayList<>();
-
- if(type.equals(Boolean.TRUE)) {
- String regexOfEmojis = "[\uD83C-\uDBFF\uDC00-\uDFFF]+";
- //String content = "[과릿] " + name + " 선생님으로부터 " + lectureName + " 클래스 초대가 도착했습니다." + "\n" + "아래 링크를 통해 앱 설치 및 회원가입을 통해 수업에 참여해보세요!" + "\n" + "\n" + "안드로이드: " + "https://bit.ly/gwarit-android" + "\n" + "\n" + "애플: " + "https://bit.ly/gwarit-apple";
- String content = "[과릿] 선생님으로부터 클래스 초대가 도착했습니다." + "\n" + "아래 링크를 통해 앱 설치 및 회원가입을 통해 수업에 참여해보세요!" + "\n" + "\n" + "안드로이드: " + "https://bit.ly/gwarit-android" + "\n" + "\n" + "애플: " + "https://bit.ly/gwarit-apple";
- smsMessageDtoList.add(new SmsMessageDto(postInviteReq.getPhone(), content));
- }
- if(type.equals(Boolean.FALSE)) {
- String regexOfEmojis = "[\uD83C-\uDBFF\uDC00-\uDFFF]+";
- String content = "[과릿] 선생님으로부터 클래스 초대가 도착했습니다." + "\n" + "아래 링크를 클릭 후 앱 열기를 통해 수업에 참여해보세요!" + "\n" + "\n" + "안드로이드: " + "https://bit.ly/gwarit-android" + "\n" + "\n" + "애플: " + "https://bit.ly/gwarit-apple";
- smsMessageDtoList.add(new SmsMessageDto(postInviteReq.getPhone(), content));
- }
-
- ObjectMapper objectMapper = new ObjectMapper();
- String jsonBody = objectMapper.writeValueAsString(new SmsNaverReq("MMS", this.senderPhone, name, smsMessageDtoList));
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
- headers.set("x-ncp-apigw-timestamp", time.toString());
- headers.set("x-ncp-iam-access-key", this.accessKey);
- String sig = makeSignature(time); // 암호화
- headers.set("x-ncp-apigw-signature-v2", sig);
-
- // Header 포함 전송
- HttpEntity body = new HttpEntity<>(jsonBody, headers);
-
- RestTemplate restTemplate = new RestTemplate();
- restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
- SmsNaverRes smsNaverRes = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/"+this.serviceId+"/messages"), body, SmsNaverRes.class);
-
- // 실페 시, Error 던지기
- if(!smsNaverRes.getStatusCode().equals("202")) {
- throw new RuntimeException("문자 전송에 실패했습니다.");
- }
-
- return null;
- }
-
- public String makeSignature(Long time) throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
- String space = " "; // one space
- String newLine = "\n"; // new line
- String method = "POST"; // method
- String url = "/sms/v2/services/"+ this.serviceId +"/messages"; // url (include query string)
- String timestamp = time.toString(); // current timestamp (epoch)
- String accessKey = this.accessKey; // access key id (from portal or Sub Account)
- String secretKey = this.secretKey;
-
- String message = new StringBuilder()
- .append(method)
- .append(space)
- .append(url)
- .append(newLine)
- .append(timestamp)
- .append(newLine)
- .append(accessKey)
- .toString();
-
- SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
- Mac mac = Mac.getInstance("HmacSHA256");
- mac.init(signingKey);
-
- byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8"));
- String encodeBase64String = Base64.encodeBase64String(rawHmac);
-
- return encodeBase64String;
- }
-}
diff --git a/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsMessageDto.java b/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsMessageDto.java
deleted file mode 100644
index dcf978f1..00000000
--- a/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsMessageDto.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.selfrunner.gwalit.global.util.sms.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-public class SmsMessageDto {
- private String to;
- private String content;
-}
diff --git a/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsNaverReq.java b/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsNaverReq.java
deleted file mode 100644
index 251511a0..00000000
--- a/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsNaverReq.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.selfrunner.gwalit.global.util.sms.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.util.List;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-public class SmsNaverReq {
- private String type;
- private String from;
- private String Content;
- private List messages;
-}
diff --git a/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsNaverRes.java b/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsNaverRes.java
deleted file mode 100644
index 6ef57c45..00000000
--- a/src/main/java/com/selfrunner/gwalit/global/util/sms/dto/SmsNaverRes.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.selfrunner.gwalit.global.util.sms.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-public class SmsNaverRes {
-
- private String statusCode;
-
- private String statusName;
-}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index fb77b364..39da5404 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -10,6 +10,10 @@ spring:
username: ENC(23VtkEODP7NFAlIXh6iu3w==)
url: ENC(xcpdNBbgA3cWXKlrjVOVHiQCCIWuZWy7JKD5qIxMnkBLZsDV2UOzYFr57bZZhEXj)
password: ENC(Z3bshepqUUYRmo/xDJI/tsLRQ54VFjbS)
+# url: ENC(cjE9nt26Kyi+TPxF8pPD0RXLenxNaoiIVGzsUxPa3xSwYIxY387damK4Zf7xN8J/p9XO2QWE2lVdkb3LjQSuapA0PEXLPtGf7uHKND0pOultghs/78YfaKv37bR3I8uA)
+# username: ENC(WEGH/622uWGO/MqSOC6Uw7Vcbb+Ivy+7)
+# password: ENC(11L+R8sQN5HBKnXMxFpfAm55jSn+gekH)
+
redis:
host: ENC(w9qDnaEabKQ4NbweCUEcxoMp+an+TBDU)
port: 6378