Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merge] Develop <- Main #1

Merged
merged 28 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4ee5120
cicd: main branch test deploy
Jeongh00 Nov 21, 2024
c90bb8d
fix: home api url
Jeongh00 Nov 21, 2024
e288f8e
Merge branch 'develop'
Jeongh00 Nov 21, 2024
94b11cb
fix: cors link 추가
Jeongh00 Nov 21, 2024
9902d0f
Merge remote-tracking branch 'origin/develop'
Jeongh00 Nov 21, 2024
0772a66
feat: chat gpt 챗봇 기능
Jeongh00 Nov 22, 2024
8cf8d9f
Merge remote-tracking branch 'origin/develop'
Jeongh00 Nov 22, 2024
cd49c0b
fix: find by email 로 변경
Jeongh00 Nov 23, 2024
04e095b
[Feat] Home API가 값을 반환할 때 Picture 값도 반환하도록 변경, Image 관련 코드를 추가
NARUBROWN Nov 23, 2024
0f1e049
[Fix] IsQuizCleared, IsChapterCleared 가 이미 등록되어있는 경우에도 또 등록되는 문제를 해결했…
NARUBROWN Nov 23, 2024
5e27c16
[Feat] 선인장 랜덤 뽑기 기능 추가
NARUBROWN Nov 23, 2024
4e13be8
[Feat] 선인장 도감 기능 추가
NARUBROWN Nov 23, 2024
4dbac4c
[Fix] 선인장 생성 시 선인장 type을 저장하지 못하는 문제를 해결했습니다
NARUBROWN Nov 23, 2024
186ae14
[Feat] 유저 삭제 기능 추가, 챕터 보상 버튼 상태 제공
NARUBROWN Nov 23, 2024
3cb691c
fix: cors domain
Jeongh00 Nov 25, 2024
e828a95
[Fix] N+1 문제 완화, 연관관계를 기본적으로 LAZY로 가져오게끔 수정, Transactional 추가
NARUBROWN Nov 28, 2024
4aa776a
[Fix] 챕터 단 건 조회 해결 상태 조건 불일치 문제 해결
NARUBROWN Nov 29, 2024
599b8b2
[Fix] 챕터 모두 조회 메서드 N+1 문제 해결
NARUBROWN Nov 29, 2024
6d1e260
[Fix] 모든 Quiz 정보를 가져오는 메서드가 일부 값을 null로 return 하던 문제를 해결했습니다.
NARUBROWN Nov 29, 2024
35746c6
[Fix] 모든 Quiz 정보를 가져오는 메서드의 N+1 문제를 최적화했습니다.
NARUBROWN Nov 29, 2024
48478ca
[Fix] (관리자) 유저 삭제에서 유저를 삭제할 수 없었던 문제를 해결했습니다.
NARUBROWN Nov 29, 2024
d6c4039
[Fix] 챕터 보상 수령 처리 API 요청을 두 번 이상 보낼 수 있는 문제를 해결했습니다.
NARUBROWN Nov 29, 2024
887b9a1
[Fix] 챕터 단 건 조회에서 보상 수령 버튼이 True로 바뀌지 않던 문제를 해결했습니다.
NARUBROWN Nov 29, 2024
b868c17
[Fix] Chapter에서 Quiz를 조회할때 오름차순으로 가져오도록 처리
NARUBROWN Nov 29, 2024
606de21
[Fix] Chapter 모두 가져오기 API에서 Chapter의 순서를 보장 & 오름차순으로 가져오도록 수정
NARUBROWN Dec 1, 2024
144c7e8
[Feat] CactusType이 더 풍부한 값을 가지고, 사용자에게 반환할 수 있도록 수정했습니다.
NARUBROWN Dec 7, 2024
0086c89
[Fix] 선인장 이름 변경
NARUBROWN Dec 7, 2024
4217807
[Fix] 선인장 타입을 가져오지 못하는 문제를 해결 했습니다.
NARUBROWN Dec 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Backend CI / CD # actions 이름

on:
push:
branches: [ main ]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: 21
distribution: 'adopt'

- name: 저장소 Checkout
uses: actions/checkout@v3

- name: make application-prod yml file
run: |
cd ./api-service/src/main/resources
touch ./application-prod.yml
echo "${{ secrets.APPLICATION_PROD }}" > ./application-prod.yml
shell: bash

- name: Make application-ai.yml
run: |
cd ./api-service/src/main/resources
touch ./application-ai.yml
echo "${{ secrets.APPLICATION_AI }}" > ./application-ai.yml
shell: bash

- name: make application-oauth yml file
run: |
cd ./security/src/main/resources
touch ./application-oauth.yml
echo "${{ secrets.APPLICATION_OAUTH }}" > ./application-oauth.yml
shell: bash

- name: make application-redis yml file
run: |
cd ./security/src/main/resources
touch ./application-redis.yml
echo "${{ secrets.APPLICATION_REDIS }}" > ./application-redis.yml
shell: bash

- name: Gradlew 권한 부여
run: chmod +x ./gradlew

- name: 스프링부트 애플리케이션 빌드
run: |
cd api-service
../gradlew bootJar

- name: 도커 이미지 빌드
run: sudo docker build -t ${{ secrets.DOCKER_IMG }} --platform linux/amd64 .

- name: 도커 이미지 push
run: |
sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
sudo docker push ${{ secrets.DOCKER_IMG }}

- name: scp file
uses: appleboy/scp-action@master
with:
host: ${{ secrets.EC2_SERVER_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
source: "docker-compose.yml"
target: "/home/ec2-user"

- name: 배포
uses: appleboy/[email protected]
with:
host: ${{ secrets.EC2_SERVER_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
touch ./docker-compose.yml
echo "${{ secrets.DOCKER_COMPOSE }}" > ./docker-compose.yml
sudo docker stop $(sudo docker ps -a -q)
sudo docker rm -f $(sudo docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_FRONT_IMG }}
sudo docker pull ${{ secrets.DOCKER_IMG }}
sudo docker-compose -f docker-compose.yml up -d
sudo docker image prune -f
3 changes: 3 additions & 0 deletions api-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' // swagger

implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'org.json:json:20210307'

implementation 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,4 @@ public ApplicationResponse<Void> deleteChapter(@PathVariable Long chapter_id) {
chapterService.deleteChapter(chapter_id);
return ApplicationResponse.ok(null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.codingland.apiservice.chapter.presentation;

import com.codingland.common.common.ApplicationResponse;
import com.codingland.domain.chapter.service.HasReceivedRewardService;
import com.codingland.domain.user.entity.User;
import com.codingland.security.annotation.UserResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/v1/api/chapter-reward")
@RequiredArgsConstructor
@Tag(name = "[HasReceivedReward] 챕터 보상 수령 여부 API", description = "챕터 보상 완료 처리")
public class HasReceivedRewardController {
private final HasReceivedRewardService hasReceivedRewardService;

@PostMapping
@Operation(summary = "챕터 보상 완료 처리", description = """
(사용자) 챕터 보상을 받았다고 처리합니다.
""")
public ApplicationResponse<Void> solvedChapter(
@RequestParam Long chapterId,
@UserResolver User user) {
hasReceivedRewardService.processChapterReward(chapterId, user.getUserId());
return ApplicationResponse.ok(null);
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.codingland.apiservice.character.presentation;

import com.codingland.common.common.ApplicationResponse;
import com.codingland.domain.character.dto.RequestCharacterDto;
import com.codingland.domain.character.dto.ResponseCreateCharacterDto;
import com.codingland.domain.character.dto.*;
import com.codingland.domain.character.service.CharacterService;
import com.codingland.domain.user.entity.User;
import com.codingland.security.annotation.UserResolver;
Expand Down Expand Up @@ -54,4 +53,22 @@ public ApplicationResponse<Void> decreasedCharacter(@RequestParam Long character
characterService.decreasedPoint(character_id, activityPoint);
return ApplicationResponse.ok(null);
}

@GetMapping("/pickup/random")
@Operation(summary = "캐릭터 랜덤 뽑기 후 홈화면 선인장 교체", description = """
(사용자) 캐릭터를 랜덤 뽑기 한 후 홈화면에 있는 선인장을 새롭게 뽑은 선인장으로 교체합니다.
""")
public ApplicationResponse<ResponseCharacterDetailDto> getRandomCharacter(@UserResolver User user) {
ResponseCharacterDetailDto result = characterService.pickRandomCharacter(user.getUserId());
return ApplicationResponse.ok(result);
}

@GetMapping("/guidebook")
@Operation(summary = "유저의 선인장 도감", description = """
(사용자) 유저가 가지고 있는 선인장의 목록을 보여줍니다.
""")
public ApplicationResponse<ResponseListCharacterDto> getGuidebookCharacter(@UserResolver User user) {
ResponseListCharacterDto result = characterService.getAllCharacters(user.getUserId());
return ApplicationResponse.ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
package com.codingland.apiservice.chat.presentation;

import com.codingland.apiservice.chat.service.ChatService;
import com.codingland.common.common.ApplicationResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/api/chat")
@Tag(name = "Chat API", description = "ChatGPT OpenAI API를 호출하는 엔드포인트")
public class ChatController {

private final ChatService chatService;

@GetMapping()
@Operation(
summary = "ChatGPT OpenAI 응답 가져오기",
description = "주어진 키워드와 타입에 따라 ChatGPT OpenAI API로부터 응답을 가져옵니다.",
tags = {"Chat API"}
)
public ApplicationResponse<String> getChatGptOpenApi(
@RequestParam String keyword
) throws IOException {

final String chatGPTResponse = chatService.getChatGPTResponse(keyword);
return ApplicationResponse.ok(chatGPTResponse);
}
}


Original file line number Diff line number Diff line change
@@ -1,4 +1,107 @@
package com.codingland.apiservice.chat.service;

import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

@Service
public class ChatService {

@Value("${openai.model}")
private String model;

@Value("${openai.api.url}")
private String apiUrl;

@Value("${openai.api.key}")
private String apiKey;

public String getChatGPTResponse(String prompt) throws IOException {
OkHttpClient client = createHttpClient();

// JSON 요청 생성
JSONObject jsonObject = new JSONObject();
jsonObject.put("model", model);
jsonObject.put("messages", new JSONArray()
.put(new JSONObject()
.put("role", "user")
.put("content", prompt)
)
);
jsonObject.put("max_tokens", 100);
jsonObject.put("temperature", 0.7);

// RequestBody 생성
RequestBody body = RequestBody.create(
jsonObject.toString(),
MediaType.parse("application/json")
);

// Request 생성
Request request = new Request.Builder()
.url(apiUrl)
.post(body)
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.build();

// 요청 실행 및 응답 처리
return sendRequestWithRetry(request, client);
}

private OkHttpClient createHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
.protocols(Collections.singletonList(Protocol.HTTP_1_1)) // HTTP/2 비활성화
.build();
}

private String sendRequestWithRetry(Request request, OkHttpClient client) throws IOException {
int attempts = 0;
int backoff = 1000; // 초기 대기 시간 1초

while (attempts < 5) {
try (Response response = client.newCall(request).execute()) {
// 응답 상태 코드 출력
System.out.println("Response Code: " + response.code());

if (response.isSuccessful() && response.body() != null) {
// 응답 로그 출력
String responseBodyString = response.body().string();
System.out.println("Response Body: " + responseBodyString);

// JSON 응답 파싱
JSONObject responseBody = new JSONObject(responseBodyString);
return responseBody
.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content");
} else if (response.code() == 429) {
// 429 에러: 속도 제한 초과
System.out.println("Rate limit exceeded. Retrying in " + backoff + "ms...");
Thread.sleep(backoff);
backoff *= 2; // 지수 백오프
} else {
// 다른 HTTP 오류 처리
throw new IOException("Unexpected response code: " + response.code() + " - " + response.message());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", e);
}
attempts++;
}
throw new IOException("Exceeded maximum retry attempts.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/home")
@RequestMapping("/v1/api/home")
@RequiredArgsConstructor
@Tag(name = "[Home] 홈 API", description = "홈 생성, 홈 조회, 홈 수정, 홈 삭제")
public class HomeController {
Expand Down Expand Up @@ -69,6 +69,7 @@ public ResponseEntity<Void> deleteHome(@PathVariable Long home_id) {
homeService.deleteHome(home_id);
return ResponseEntity.status(HttpStatus.OK).body(null);
}

}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.codingland.apiservice.image.presentation;

import com.codingland.domain.image.service.ImageService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/api/image")
@RequiredArgsConstructor
@Tag(name = "[Image] 이미지 API", description = "사진 조회")
public class ImageController {
private final ImageService imageService;

@GetMapping("/{fileName}")
public ResponseEntity<byte[]> download(@PathVariable String fileName) {
byte[] downloadImage = imageService.downloadImage(fileName);
return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.valueOf("image/png"))
.body(downloadImage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,14 @@ public ApplicationResponse<Boolean> checkIsCompleteTraining(@UserResolver User u
return ApplicationResponse.ok(result);
}

@DeleteMapping
@Operation(summary = "유저 탈퇴", description = """
(관리자) 유저를 완전히 삭제하는 API입니다.
""")
public ApplicationResponse<Void> deleteUser(@RequestParam Long user_id) {
userService.deleteAccount(user_id);
return ApplicationResponse.ok(null);
}


}
Empty file.
4 changes: 2 additions & 2 deletions api-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ spring:
config:
activate:
on-profile: prod
import: application-prod.yml, application-oauth.yml, application-redis.yml
import: application-prod.yml, application-oauth.yml, application-redis.yml, application-ai.yml

---
spring:
config:
activate:
on-profile: prod
import: application-prod.yml, application-oauth.yml, application-redis.yml
import: application-prod.yml, application-oauth.yml, application-redis.yml, application-ai.yml


server:
Expand Down
Loading
Loading