Skip to content

Commit

Permalink
Merge pull request #384 from twenty-three-23/feature/TT-486
Browse files Browse the repository at this point in the history
TT-486
  • Loading branch information
ch8930 authored Nov 10, 2024
2 parents 0a30644 + 1b23397 commit a86b09b
Show file tree
Hide file tree
Showing 31 changed files with 561 additions and 9 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/cd_prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
logging.level.com.twentythree.peech: off
logging.config: classpath:spring-logback.xml
gpt.api.key: ${{secrets.GPT_API_KEY_PROD}}
gpt.prompt.system: ${{secrets.GPT_PROMPT_SYSTEM}}
jwt.secret.key: ${{secrets.JWT_SECRET_KEY_PROD}}
jwt.access.key: ${{secrets.JWT_ACCESS_KEY}}
jwt.refresh.key: ${{secrets.JWT_REFRESH_KEY}}
Expand All @@ -55,6 +56,9 @@ jobs:
ffmpeg.path: ${{ secrets.FFMPEG_PATH_PROD }}
ffprobe.path: ${{ secrets.FFPROBE_PATH_PROD }}
spring.cors.allowed-origins: ${{ secrets.CORS_ALLOWED_ORIGINS_PROD }}
fcm.secret-key: ${{ secrets.FCM_SECRET_KEY }}
fcm.key-path: ${{ secrets.FCM_KEY_PATH }}
fcm.project-id: ${{ secrets.FCM_PROJECT_ID }}
meta.api-version: ${{ secrets.META_API_VERSION}}
meta.web.pixel-id: ${{ secrets.META_WEB_PIXEL_ID_PROD}}
meta.web.access-token: ${{ secrets.META_WEB_ACCESS_TOKEN_PROD}}
Expand All @@ -71,6 +75,12 @@ jobs:
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# fcm 서버 키 설정
- name: Set up FCM server key
run: |
cd ./peech/src/main/resources/
echo "${{ secrets.FCM_SECRET_KEY }}" > ./peech_fcm.json
# 2. Spring Boot 애플리케이션 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci_prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
logging.level.com.twentythree.peech: off
logging.config: classpath:spring-logback.xml
gpt.api.key: ${{secrets.GPT_API_KEY_PROD}}
gpt.prompt.system: ${{secrets.GPT_PROMPT_SYSTEM}}
jwt.secret.key: ${{secrets.JWT_SECRET_KEY_PROD}}
jwt.access.key: ${{secrets.JWT_ACCESS_KEY}}
jwt.refresh.key: ${{secrets.JWT_REFRESH_KEY}}
Expand All @@ -55,6 +56,9 @@ jobs:
ffmpeg.path: ${{ secrets.FFMPEG_PATH_PROD }}
ffprobe.path: ${{ secrets.FFPROBE_PATH_PROD }}
spring.cors.allowed-origins: ${{ secrets.CORS_ALLOWED_ORIGINS_PROD }}
fcm.secret-key: ${{ secrets.FCM_SECRET_KEY }}
fcm.key-path: ${{ secrets.FCM_KEY_PATH }}
fcm.project-id: ${{ secrets.FCM_PROJECT_ID }}
meta.api-version: ${{ secrets.META_API_VERSION}}
meta.web.pixel-id: ${{ secrets.META_WEB_PIXEL_ID_PROD}}
meta.web.access-token: ${{ secrets.META_WEB_ACCESS_TOKEN_PROD}}
Expand All @@ -70,6 +74,12 @@ jobs:
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# fcm 서버 키 설정
- name: Set up FCM server key
run: |
cd ./peech/src/main/resources/
echo "${{ secrets.FCM_SECRET_KEY }}" > ./peech_fcm.json
# 2. Spring Boot 애플리케이션 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/cicd_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
logging.level.com.twentythree.peech: off
logging.config: classpath:spring-logback.xml
gpt.api.key: ${{secrets.GPT_API_KEY}}
gpt.prompt.system: ${{secrets.GPT_PROMPT_SYSTEM}}
jwt.secret.key: ${{secrets.JWT_SECRET_KEY}}
jwt.access.key: ${{secrets.JWT_ACCESS_KEY}}
jwt.refresh.key: ${{secrets.JWT_REFRESH_KEY}}
Expand All @@ -56,6 +57,9 @@ jobs:
ffmpeg.path: ${{ secrets.FFMPEG_PATH_DEV }}
ffprobe.path: ${{ secrets.FFPROBE_PATH_DEV }}
spring.cors.allowed-origins: ${{ secrets.CORS_ALLOWED_ORIGINS }}
fcm.secret-key: ${{ secrets.FCM_SECRET_KEY }}
fcm.key-path: ${{ secrets.FCM_KEY_PATH }}
fcm.project-id: ${{ secrets.FCM_PROJECT_ID }}
meta.api-version: ${{ secrets.META_API_VERSION}}
meta.web.pixel-id: ${{ secrets.META_WEB_PIXEL_ID}}
meta.web.access-token: ${{ secrets.META_WEB_ACCESS_TOKEN}}
Expand All @@ -70,6 +74,12 @@ jobs:
# excute 허용
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# fcm 서버 키 설정
- name: Set up FCM server key
run: |
cd ./peech/src/main/resources/
echo "${{ secrets.FCM_SECRET_KEY }}" > ./peech_fcm.json
# 2. Spring Boot 애플리케이션 빌드
- name: Build with Gradle
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ data.sql

### Logging File ###
/logs/**

### FCM Admin SDK ###
**/src/main/resources/peech_fcm.json
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ dependencies {
// ffmpeg
implementation 'net.bramp.ffmpeg:ffmpeg:0.8.0'

//firebase sdk
implementation 'com.google.firebase:firebase-admin:9.3.0'

//thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/twentythree/peech/PeechApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@EnableJpaAuditing
@SpringBootApplication
public class PeechApplication {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.twentythree.peech.analyzescript.application;

import com.twentythree.peech.analyzescript.domain.AnalyzeScriptPredictor;
import com.twentythree.peech.fcm.application.NotificationService;
import com.twentythree.peech.script.service.ScriptService;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class AnalyzeScriptFacade {

private final AnalyzeScriptPredictor analyzeScriptPredictor;
private final ScriptService scriptService;
private final NotificationService notificationService;

@Async
public void analyzeScriptAndSave(Long userId, Long scriptId, String scriptContent) {
analyzeScriptPredictor.requestAnalyzeScript(scriptContent)
.thenAccept(result -> {
scriptService.reflectAnalyzeResult(scriptId, result);
notificationService.pushNotification(userId);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.twentythree.peech.analyzescript.client;

import com.twentythree.peech.analyzescript.dto.request.GPTRequest;
import com.twentythree.peech.common.dto.response.GPTResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;

@FeignClient(
name = "analyzeScriptGPT",
url = "https://api.openai.com/")
public interface AnalyzeScriptGPT {

@PostMapping("v1/chat/completions")
GPTResponse analyzeScript(GPTRequest gptRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.twentythree.peech.analyzescript.domain;

import com.twentythree.peech.analyzescript.client.AnalyzeScriptGPT;
import com.twentythree.peech.analyzescript.dto.request.GPTRequest;
import com.twentythree.peech.aop.annotation.Trace;
import com.twentythree.peech.common.dto.Message;
import com.twentythree.peech.common.dto.response.GPTResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

@Async
@RequiredArgsConstructor
@Component
public class AnalyzeScriptPredictor {

@Value("${gpt.prompt.system}")
private String gptSystemPrompt;

private final AnalyzeScriptGPT analyzeScriptGPT;

public CompletableFuture<String> requestAnalyzeScript(String scriptContent) {
List<Message> messages = new ArrayList<>();

String systemPrompt = gptSystemPrompt;
messages.add(new Message("system", systemPrompt));

String userPrompt = "아래의 글은 회사 면접을 위한 자기소개서입니다. 내용을 보고 자기소개서를 분석해주세요.\n"+
"자기소개서: " +
scriptContent;
messages.add(new Message("user", userPrompt));

GPTRequest gptRequest = new GPTRequest("gpt-4o-mini", messages);
GPTResponse gptResponse = analyzeScriptGPT.analyzeScript(gptRequest);
String result = gptResponse.getChoices().get(0).getMessage().getContent();

return CompletableFuture.completedFuture(result);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.twentythree.peech.analyzescript.dto.request;

import com.twentythree.peech.common.dto.Message;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public class GPTRequest {
private String model;
private List<Message> messages;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.twentythree.peech.fcm.application;

import com.twentythree.peech.fcm.dto.request.RequestFCMTokenDTO;

public interface NotificationService {
void pushNotification(Long userId);
void saveOrUpdateToken(RequestFCMTokenDTO fcmTokenDTO, Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.twentythree.peech.fcm.application;


import com.twentythree.peech.fcm.dto.request.RequestFCMTokenDTO;
import com.twentythree.peech.fcm.entity.NotificationEntity;
import com.twentythree.peech.fcm.event.FCMPushedEvent;
import com.twentythree.peech.fcm.infra.NotificationRepository;
import com.twentythree.peech.user.entity.UserEntity;
import com.twentythree.peech.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Service
public class NotificationServiceImpl implements NotificationService {

private final NotificationRepository notificationRepository;
private final UserRepository userRepository;
private final ApplicationEventPublisher applicationEventPublisher;

public void pushNotification(Long userId) {
List<String> fcmTokenList = notificationRepository.findAllByUserId(userId);

if(fcmTokenList.isEmpty()){
throw new IllegalStateException("FCM 토큰이 존재하지 않습니다.");
}

applicationEventPublisher.publishEvent(new FCMPushedEvent(fcmTokenList));
}

@Transactional
@Override
public void saveOrUpdateToken(RequestFCMTokenDTO request, Long userId) {
notificationRepository.findByDeviceId(request.getDeviceId())
.ifPresentOrElse(
fcmToken -> updateFCMToken(fcmToken, request.getFcmToken()),

() -> {
UserEntity userEntity = userRepository.findById(userId)
.orElseThrow(() -> new IllegalStateException("유저가 존재하지 않습니다."));
NotificationEntity newEntity = NotificationEntity
.ofCreateFCMToken(
userEntity,
request.getDeviceId(),
request.getFcmToken()
);
notificationRepository.save(newEntity);
});
}

private void updateFCMToken(NotificationEntity originEntity, String newToken){
originEntity.updateToken(newToken);
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/twentythree/peech/fcm/client/FCMClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.twentythree.peech.fcm.client;


import com.google.firebase.messaging.FirebaseMessaging;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;

@FeignClient(
name = "FCMClient",
url = "https://fcm.googleapis.com/v1/projects/${fcm.project-id}/messages:send"
)
public interface FCMClient {

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
void sendNotification(
FirebaseMessaging message
);


}
37 changes: 37 additions & 0 deletions src/main/java/com/twentythree/peech/fcm/config/FcmConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.twentythree.peech.fcm.config;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.io.InputStream;


@Configuration
public class FcmConfig {

@Value("${fcm.key-path}")
private String fcmKeyPath;
private final Logger log = LoggerFactory.getLogger(this.getClass());

@PostConstruct
private void init() throws IOException {
if (FirebaseApp.getApps().isEmpty()) {
log.info("FirebaseApp을 초기화합니다");
InputStream firebaseAccount = new ClassPathResource(fcmKeyPath).getInputStream();

FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(firebaseAccount))
.build();

FirebaseApp.initializeApp(options);
}
}

}
35 changes: 35 additions & 0 deletions src/main/java/com/twentythree/peech/fcm/dto/FCMMessageDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.twentythree.peech.fcm.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class FCMMessageDTO {

private boolean validateOnly;
private Message message;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class Message {
private Notification notification;
private String token;
}

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class Notification {
private String title;
private String body;
private String image;
}
}
Loading

0 comments on commit a86b09b

Please sign in to comment.