Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 into feature/TSK-59/upgrade-login
  • Loading branch information
pdh90345 committed Aug 13, 2024
2 parents 7f7e1cb + 0c72b2c commit 4420569
Show file tree
Hide file tree
Showing 11 changed files with 469 additions and 272 deletions.
137 changes: 137 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Mooluck - backend

## 요구사항 및 의존성

- Java 17
- Spring Boot 3.3.1
- Spring Boot Data JPA
- Lombok
- MySQL
- JSON Web Token (JWT)
- AWS SDK (S3 포함)
- FFmpeg
- 기타 의존성은 `build.gradle` 파일을 참조하십시오.

## 설치 가이드

### 1. 저장소 클론
```sh
git clone https://github.com/Ong-gi-Jong-gi/namanmoo-backend.git
cd namanmoo-backend
```

### 2. Java 설치 및 설정
- Java JDK 17을 설치합니다.
- `JAVA_HOME` 환경 변수를 설정합니다.

### 3. IDE 설정
- IntelliJ IDEA를 사용하는 것을 권장합니다.
- IntelliJ IDEA에서 프로젝트를 열고 Gradle 의존성을 자동으로 불러오도록 합니다.

### 4. 데이터베이스 설정
- MySQL을 설치하고 데이터베이스를 생성합니다.
- `application.properties` 또는 `application.yml` 파일에 다음 정보를 추가합니다:
```properties
spring.datasource.url=jdbc:mysql://your-database-url:3306/your-database-name
spring.datasource.username=your-database-username
spring.datasource.password=your-database-password
```

### 5. AWS S3 설정
- AWS S3에 대한 접근 키와 비밀 키를 환경 변수 또는 `application.yml` 파일에 추가합니다:
```yaml
cloud:
aws:
credentials:
access-key: your-access-key-id
secret-key: your-secret-access-key
s3:
bucket: your-bucket-name
region:
static: your-region
```
### 6. OpenAI API 설정
- OpenAI API 키를 환경 변수 또는 `application.yml` 파일에 추가합니다:
```yaml
openai-service:
api-key: your-openai-api-key
gpt-model: gpt-3.5-turbo
audio-model: whisper-1
http-client:
read-timeout: 3000
connect-timeout: 3000
urls:
base-url: https://api.openai.com/v1
chat-url: /chat/completions
create-transcription-url: /audio/transcriptions
```

### 7. FFmpeg 설정
- FFmpeg와 FFprobe 경로를 환경 변수 또는 `application.yml` 파일에 추가합니다:
```yaml
ffmpeg:
path: path-to-your-ffmpeg
ffprobe:
path: path-to-your-ffprobe
```

## 사용 가이드

- 애플리케이션을 시작하려면 다음 명령어를 실행합니다:
```sh
./gradlew bootRun
```
- 브라우저에서 `http://localhost:8080`을 열어 서비스를 확인합니다.

## 테스트

- 테스트를 실행하려면 다음 명령어를 사용합니다:
```sh
./gradlew test
```

## CI/CD 설정

이 프로젝트는 GitHub Actions를 사용하여 CI/CD 파이프라인을 설정하였습니다. 변경 사항이 main 또는 dev 브랜치에 푸시되거나 풀 리퀘스트가 발생할 때 자동으로 빌드 및 배포가 실행됩니다.

### CI 파이프라인의 주요 단계

- `build`: 프로젝트를 빌드하고 Docker 이미지를 생성
- `deploy`: 빌드한 Docker 이미지를 AWS EC2 인스턴스에 배포

### 환경 변수 및 GitHub Secrets 정보 설정

GitHub Secrets에 다음 정보를 설정해야 합니다:
- DOCKERHUB_USERNAME: DockerHub 계정 이름
- DOCKERHUB_PASSWORD: DockerHub 계정 비밀번호
- PROJECT_NAME: 프로젝트 이름
- EC2_HOST: EC2 호스트 주소
- EC2_USER: EC2 사용자명
- EC2_SSH_KEY: EC2 SSH 프라이빗 키
- DB_URL: 데이터베이스 접속 URL
- DB_USERNAME: 데이터베이스 사용자명
- DB_PASSWORD: 데이터베이스 비밀번호
- JWT_SECRET_KEY: JWT 비밀 키
- S3_ACCESS_KEY_ID: AWS S3 접근 키
- S3_SECRET_ACCESS_KEY: AWS S3 비밀 키
- S3_BUCKET_NAME: AWS S3 버킷 이름
- S3_REGION: AWS S3 리전
- OPENAI_API_KEY: OpenAI API 키
- FFMPEG_PATH: FFmpeg 경로
- FFPROBE_PATH: FFprobe 경로
- AWS_REGION: AWS 리전
- LOG_GROUP_NAME: AWS Cloudwatch 로그 그룹 이름
- LOG_STREAM_NAME: AWS Cloudwatch 로그 스트림 이름

자세한 정보는 `.github/workflows/Spring Boot CI-CD.yml` 파일을 참고해주시길 바랍니다.

<!--
## 기여

- 기여는 언제나 환영합니다! 저장소를 포크하고, 개선 사항이나 버그 수정을 위한 풀 리퀘스트를 제출해 주세요.

## 라이선스

- 이 프로젝트는 MIT 라이선스를 따릅니다. 자세한 내용은 `LICENSE` 파일을 참조하십시오.
-->
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies {

// thumbnailator for merging image
implementation 'net.coobird:thumbnailator:0.4.20'
implementation 'com.drewnoakes:metadata-extractor:2.18.0'

//ffmpeg
implementation ('net.bramp.ffmpeg:ffmpeg:0.8.0')
Expand Down
62 changes: 44 additions & 18 deletions src/main/java/ongjong/namanmoo/controller/ChallengeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
Expand All @@ -45,8 +47,9 @@ public class ChallengeController {

@PostMapping // 챌린지 생성 -> 캐릭터 생성 및 답변 생성
@Transactional
public ApiResponse<Void> saveChallenge(@RequestBody SaveChallengeRequest request) {
Long challengeDate = request.getChallengeDate();
public ApiResponse<Void> saveChallenge() {
// Long challengeDate = request.getChallengeDate();
Long challengeDate = System.currentTimeMillis();
Long familyId = familyService.findFamilyId();

try {
Expand Down Expand Up @@ -86,7 +89,8 @@ public ApiResponse<CurrentLuckyDto> getChallengeStartDate(){

// 오늘의 챌린지 조회
@GetMapping("/today")
public ApiResponse<CurrentChallengeDto> getChallenge(@RequestParam("challengeDate") Long challengeDate) throws Exception {
public ApiResponse<CurrentChallengeDto> getChallenge() throws Exception {
Long challengeDate = System.currentTimeMillis();
if(String.valueOf(challengeDate).length() != 13){
return new ApiResponse<>("404", "Challenge date must be a 13-number", null);
}
Expand All @@ -101,10 +105,8 @@ public ApiResponse<CurrentChallengeDto> getChallenge(@RequestParam("challengeDat

// 챌린지 리스트 조회
@GetMapping("/list") // 챌린지 리스트는 lucky가 여러개 일때를 고려하여 죽은 럭키 개수 * 30 +1 부터 챌린지가 보여져야한다.
public ApiResponse<List<ChallengeListDto>> getChallengeList(@RequestParam("challengeDate") Long challengeDate) throws Exception {
if(String.valueOf(challengeDate).length() != 13){
return new ApiResponse<>("404", "Challenge date must be a 13-number", null);
}
public ApiResponse<List<ChallengeListDto>> getChallengeList() throws Exception {
Long challengeDate = System.currentTimeMillis();

List<Challenge> challenges = challengeService.findChallenges(challengeDate);

Expand Down Expand Up @@ -302,8 +304,20 @@ public ApiResponse<Map<String, String>> saveFaceTimeAnswer(
// 이미지 업로드 동기 처리
try {
Map<String, String> response = sharedFileService.uploadImageFile(challenge, answerFile, FileType.IMAGE);
// 병합 작업을 비동기적으로 예약
sharedFileService.scheduleMergeImages(challenge.getChallengeNum(), lucky);

// 업로드된 파일의 URL에서 그룹 정보를 추출
String uploadedUrl = response.get("url");
Matcher matcher = Pattern.compile("screenshot_(\\d+)").matcher(uploadedUrl);
if (matcher.find()) {
String group = matcher.group(1);

// 해당 그룹이 4장 다 모였는지 확인하고, 모였으면 병합 수행
boolean mergeNeeded = sharedFileService.checkIfMergeNeededForGroup(challenge.getChallengeNum(), lucky, group);
if (mergeNeeded) {
sharedFileService.scheduleMergeImagesForGroup(challenge.getChallengeNum(), lucky, group);
}
}

return new ApiResponse<>("200", response.get("message"), response);
} catch (Exception e) {
log.error("Image upload failed", e);
Expand All @@ -313,10 +327,9 @@ public ApiResponse<Map<String, String>> saveFaceTimeAnswer(
} else if (answerFile.getContentType().startsWith("video/")) {
// 비디오 업로드 동기 처리
try {
String uploadedUrl = awsS3Service.uploadOriginalFile(answerFile);
String uploadedUrl = awsS3Service.uploadOriginalFile(answerFile, member.getMemberId());
answerService.modifyAnswer(challengeId, uploadedUrl);
// 병합 작업을 비동기적으로 예약
sharedFileService.scheduleMergeImages(challenge.getChallengeNum(), lucky);

return new ApiResponse<>("200", "Video uploaded successfully", Map.of("url", uploadedUrl));
} catch (Exception e) {
log.error("Video upload failed", e);
Expand Down Expand Up @@ -352,12 +365,25 @@ public ApiResponse<Map<Integer, List<String>>> getFaceTimeAnswer(

Map<Integer, List<String>> results = sharedFileService.getFaceChallengeResults(challenge.getChallengeNum(), matchedLucky.getLuckyId());

// 병합 이미지가 하나도 없는지 확인
boolean areMergedImagesPresent = results.values().stream().anyMatch(list -> !list.isEmpty());

if (!areMergedImagesPresent) {
// 병합된 이미지가 하나도 없다면, 다시 병합 작업을 예약
sharedFileService.scheduleMergeImages(challenge.getChallengeNum(), matchedLucky);
// 병합된 이미지가 4개 미만인지 확인
long mergedImagesCount = results.values().stream()
.flatMap(List::stream)
.count();

if (mergedImagesCount < 4) {
// 필요한 모든 그룹 (cut_1, cut_2, cut_3, cut_4)에 대해 병합된 이미지가 있는지 확인
List<String> requiredCuts = Arrays.asList("1", "2", "3", "4");
Map<Integer, List<String>> finalResults = results;
List<CompletableFuture<Void>> mergeFutures = requiredCuts.stream()
.filter(cut -> !finalResults.containsKey(Integer.parseInt(cut)) || finalResults.get(Integer.parseInt(cut)).isEmpty())
.map(missingCut -> sharedFileService.scheduleMergeImagesForGroup(challenge.getChallengeNum(), matchedLucky, missingCut))
.toList();

// 모든 병합 작업이 완료될 때까지 대기
CompletableFuture.allOf(mergeFutures.toArray(new CompletableFuture[0])).join();

// 병합 작업이 완료된 후 결과를 다시 조회
results = sharedFileService.getFaceChallengeResults(challenge.getChallengeNum(), matchedLucky.getLuckyId());
}

return new ApiResponse<>("200", "FaceTime Challenge results found successfully", results);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class LuckyController {
private final AnswerService answerService;

@GetMapping
public ApiResponse<LuckyStatusDto> getLuckyStatus(@RequestParam("challengeDate") String challengeDate) throws Exception {
public ApiResponse<LuckyStatusDto> getLuckyStatus() throws Exception {
Long challengeDate = System.currentTimeMillis();
luckyService.luckyDeadOrAlive(challengeDate);
LuckyStatusDto luckyStatusDto = luckyService.getLuckyStatus(challengeDate);
return new ApiResponse<>("200", "Success", luckyStatusDto);
Expand Down
43 changes: 41 additions & 2 deletions src/main/java/ongjong/namanmoo/controller/RecapController.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -55,6 +58,9 @@ public ResponseEntity<?> getLuckyList() {
@GetMapping("/ranking")
public ApiResponse<MemberRankingListDto> getRanking(@RequestParam("luckyId") Long luckyId){
Lucky lucky = luckyService.getLucky(luckyId);
if (!canAccessRecap(lucky)) {
return new ApiResponse<>("403", "Access not allowed. The challenge period has not yet ended.", null);
}
Integer totalCount = lucky.getStatus(); // todo answer content값이 null인 걸 찾는걸로 수정해야할듯
Integer luckyStatus = luckyService.calculateLuckyStatus(lucky);
List<MemberAndCountDto> memberAndCountList = memberService.getMemberAndCount(lucky);
Expand All @@ -65,6 +71,10 @@ public ApiResponse<MemberRankingListDto> getRanking(@RequestParam("luckyId") Lon
// recap 화상통화
@GetMapping("/face")
public ApiResponse<MemberFacetimeDto> getFacetime(@RequestParam("luckyId") Long luckyId) throws Exception {
Lucky lucky = luckyService.getLucky(luckyId);
if (!canAccessRecap(lucky)) {
return new ApiResponse<>("403", "Access not allowed. The challenge period has not yet ended.", null);
}
MemberFacetimeDto answerList = answerService.getFacetimeAnswerList(luckyId);
return new ApiResponse<>("200", "facetime retrieved successfully", answerList);
}
Expand All @@ -73,6 +83,9 @@ public ApiResponse<MemberFacetimeDto> getFacetime(@RequestParam("luckyId") Long
@GetMapping("/statistics")
public ApiResponse<List<Map<String, Object>>> getStatistics(@RequestParam("luckyId") Long luckyId) throws Exception {
Lucky lucky = luckyService.getLucky(luckyId);
if (!canAccessRecap(lucky)) {
return new ApiResponse<>("403", "Access not allowed. The challenge period has not yet ended.", null);
}

// 가장 조회수가 많은 챌린지
Challenge mostViewedChallenge = challengeService.findMostViewedChallenge(lucky);
Expand Down Expand Up @@ -103,13 +116,21 @@ public ApiResponse<List<Map<String, Object>>> getStatistics(@RequestParam("lucky

@GetMapping("/youth")
public ApiResponse<List<MemberYouthAnswerDto>> getYouth(@RequestParam("luckyId") Long luckyId) throws Exception{
Lucky lucky = luckyService.getLucky(luckyId);
if (!canAccessRecap(lucky)) {
return new ApiResponse<>("403", "Access not allowed. The challenge period has not yet ended.", null);
}
List<MemberYouthAnswerDto> memberAnswerDtoList = answerService.getYouthByMember(luckyId, 13, 1);
return new ApiResponse<>("200", "Youth photos retrieved successfully", memberAnswerDtoList);
}

// recap 미안한점 고마운점
@GetMapping("/appreciations")
public ApiResponse<List<MemberAppreciationDto>> getAppreciations(@RequestParam("luckyId") Long luckyId) throws Exception {
Lucky lucky = luckyService.getLucky(luckyId);
if (!canAccessRecap(lucky)) {
return new ApiResponse<>("403", "Access not allowed. The challenge period has not yet ended.", null);
}
List<MemberAppreciationDto> appreciationList = answerService.getAppreciationByMember(luckyId, 27, 29);
return new ApiResponse<>("200", "Success", appreciationList);
}
Expand All @@ -118,19 +139,26 @@ public ApiResponse<List<MemberAppreciationDto>> getAppreciations(@RequestParam("
// recap 가족사진
@GetMapping("/photos")
public ApiResponse<MemberPhotosAnswerDto> getPhotos(@RequestParam("luckyId") Long luckyId) throws Exception {
Lucky lucky = luckyService.getLucky(luckyId);
if (!canAccessRecap(lucky)) {
return new ApiResponse<>("403", "Access not allowed. The challenge period has not yet ended.", null);
}
MemberPhotosAnswerDto photosAnswerDto = answerService.getPhotos(luckyId);
return new ApiResponse<>("200", "Success", photosAnswerDto);
}

// recap 음성
@GetMapping("/voice")
public ApiResponse<Map<String, String>> mergeVoiceClips(@RequestParam("luckyId") Long luckyId) {
Lucky lucky = luckyService.getLucky(luckyId);
if (!canAccessRecap(lucky)) {
return new ApiResponse<>("403", "Access not allowed. The challenge period has not yet ended.", null);
}

List<File> localFiles = null;
File outputFile = null;

try {
Lucky lucky = luckyService.getLucky(luckyId);

// 이미 병합된 음성 파일이 있는지 확인
SharedFile mergeVoice = sharedFileService.getMergeVoice(lucky, FileType.AUDIO);
if (mergeVoice != null) {
Expand Down Expand Up @@ -215,4 +243,15 @@ public ApiResponse<Map<String, String>> mergeVoiceClips(@RequestParam("luckyId")
// data.put("challengeTitle", challenge != null ? challenge.getChallengeTitle() : "");
// return data;
// }

private boolean canAccessRecap(Lucky lucky) {
String startDateString = lucky.getChallengeStartDate();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd");
LocalDate startDate = LocalDate.parse(startDateString, formatter);
LocalDate currentDate = LocalDate.now();

Duration duration = Duration.between(startDate.atStartOfDay(), currentDate.atStartOfDay());

return duration.toDays() > lucky.getLifetime().getDays();
}
}
Loading

0 comments on commit 4420569

Please sign in to comment.