-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #241 from prgrms-web-devcourse-final-project/develop
main 브랜치 업데이트
- Loading branch information
Showing
31 changed files
with
1,118 additions
and
533 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ jobs: | |
|
||
|
||
# 생성된 파일들을 아티팩트로 업로드 | ||
- uses: actions/upload-artifact@v3 | ||
- uses: actions/upload-artifact@v4 | ||
with: | ||
name: application.properties | ||
path: ./src/main/resources/application.properties | ||
|
@@ -50,6 +50,39 @@ jobs: | |
artifact=$(ls ./build/libs/*.jar | head -n 1) | ||
echo "artifact=$artifact" >> $GITHUB_ENV | ||
# 9. SSH 키 설정 | ||
- name: Set up SSH | ||
uses: webfactory/[email protected] | ||
with: | ||
ssh-private-key: ${{ secrets.EC2_SSH_KEY }} | ||
# GitHub Secrets에 저장된 EC2 SSH 개인 키를 SSH 에이전트에 로드 | ||
|
||
# 10. GCP Credentials 디코딩 및 EC2로 전송 | ||
- name: Decode GCP Credentials | ||
run: echo "${{ secrets.GCP_CREDENTIALS }}" | base64 --decode > gcp-key.json | ||
# GitHub Secrets에 저장된 Base64 인코딩된 GCP JSON Key를 디코딩하여 로컬에 저장 | ||
|
||
- name: Copy GCP Credentials to EC2 | ||
run: scp -o StrictHostKeyChecking=no gcp-key.json ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP }}:/home/${{ secrets.EC2_USER }}/gcp-key.json | ||
# 디코딩된 GCP JSON Key 파일을 EC2 서버로 전송 | ||
# YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) | ||
# ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) | ||
|
||
# 11. EC2에서 GCP Credentials 설정 | ||
- name: Set up GCP Credentials on EC2 | ||
run: | | ||
ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP }} << 'EOF' | ||
mkdir -p ~/.gcp | ||
mv /home/${{ secrets.EC2_USER }}/gcp-key.json ~/.gcp/key.json | ||
chmod 600 ~/.gcp/key.json | ||
export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json | ||
# 환경 변수 설정을 영구적으로 추가 | ||
echo 'export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json' >> ~/.bash_profile | ||
EOF | ||
# EC2 서버에서 GCP Credentials 설정 | ||
# YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) | ||
# ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) | ||
|
||
# 빈스토크 배포 | ||
- name: Deploy to Elastic Beanstalk | ||
uses: einaregilsson/beanstalk-deploy@v20 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,52 +5,88 @@ on: | |
branches: | ||
- develop | ||
|
||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
# 1. 코드 체크아웃 | ||
- uses: actions/checkout@v3 | ||
|
||
# 2. Java 설정 | ||
- uses: actions/setup-java@v3 | ||
with: | ||
distribution: 'zulu' | ||
java-version: '17' | ||
|
||
# application.yml 파일 생성 | ||
# 3. application.properties 파일 생성 및 설정 | ||
- run: touch ./src/main/resources/application.properties | ||
- run: echo "${{secrets.APPLICATION}}" > ./src/main/resources/application.properties | ||
- run: echo "${{secrets.APPLICATION2}}" > ./src/main/resources/application.properties | ||
- run: echo "spring.cloud.aws.credentials.access-key=${{secrets.AWS_ACTION_ACCESS_KEY}}" >> ./src/main/resources/application.properties | ||
- run: echo "spring.cloud.aws.credentials.secret-key=${{secrets.AWS_ACTION_SECRET_ACCESS_KEY}}" >> ./src/main/resources/application.properties | ||
|
||
|
||
# 생성된 파일들을 아티팩트로 업로드 | ||
- uses: actions/upload-artifact@v3 | ||
# 4. 생성된 파일들을 아티팩트로 업로드 | ||
- uses: actions/upload-artifact@v4 | ||
with: | ||
name: application.properties | ||
path: ./src/main/resources/application.properties | ||
|
||
# 5. Gradlew 실행 권한 부여 | ||
- name: Grant execute permission for gradlew | ||
run: chmod +x gradlew | ||
|
||
# 6. Gradle 빌드 실행 | ||
- name: Build with Gradle | ||
run: ./gradlew clean build -x test | ||
|
||
# 7. 현재 시간 가져오기 | ||
- name: Get current time | ||
uses: josStorer/get-current-time@v2 | ||
id: current-time | ||
with: | ||
format: YYYY-MM-DDTHH-mm-ss | ||
utcOffset: "+09:00" | ||
|
||
# 배포용 패키지 경로 설정 | ||
# 8. 배포용 패키지 경로 설정 | ||
- name: Set artifact path | ||
run: | | ||
artifact=$(ls ./build/libs/*.jar | head -n 1) | ||
echo "artifact=$artifact" >> $GITHUB_ENV | ||
# 빈스토크 배포 | ||
# 9. SSH 키 설정 | ||
- name: Set up SSH | ||
uses: webfactory/[email protected] | ||
with: | ||
ssh-private-key: ${{ secrets.EC2_SSH_KEY_TWO }} | ||
# GitHub Secrets에 저장된 EC2 SSH 개인 키를 SSH 에이전트에 로드 | ||
|
||
# 10. GCP Credentials 디코딩 및 EC2로 전송 | ||
- name: Decode GCP Credentials | ||
run: echo "${{ secrets.GCP_CREDENTIALS }}" | base64 --decode > gcp-key.json | ||
# GitHub Secrets에 저장된 Base64 인코딩된 GCP JSON Key를 디코딩하여 로컬에 저장 | ||
|
||
- name: Copy GCP Credentials to EC2 | ||
run: scp -o StrictHostKeyChecking=no gcp-key.json ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP_TWO }}:/home/${{ secrets.EC2_USER }}/gcp-key.json | ||
# 디코딩된 GCP JSON Key 파일을 EC2 서버로 전송 | ||
# YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) | ||
# ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) | ||
|
||
# 11. EC2에서 GCP Credentials 설정 | ||
- name: Set up GCP Credentials on EC2 | ||
run: | | ||
ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_IP_TWO }} << 'EOF' | ||
mkdir -p ~/.gcp | ||
mv /home/${{ secrets.EC2_USER }}/gcp-key.json ~/.gcp/key.json | ||
chmod 600 ~/.gcp/key.json | ||
export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json | ||
# 환경 변수 설정을 영구적으로 추가 | ||
echo 'export GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/key.json' >> ~/.bash_profile | ||
EOF | ||
# EC2 서버에서 GCP Credentials 설정 | ||
# YOUR_EC2_IP -> GitHub Secrets에 저장된 EC2 IP 주소 (${ secrets.EC2_IP }) | ||
# ec2-user -> GitHub Secrets에 저장된 EC2 SSH 사용자 이름 (${ secrets.EC2_USER }) | ||
|
||
# 12. Elastic Beanstalk에 배포 | ||
- name: Deploy to Elastic Beanstalk | ||
uses: einaregilsson/beanstalk-deploy@v20 | ||
with: | ||
|
@@ -60,5 +96,4 @@ jobs: | |
environment_name: Mallangs2-two-env | ||
version_label: github-action-${{ steps.current-time.outputs.time }} | ||
region: ap-northeast-2 | ||
deployment_package: ${{ env.artifact }} | ||
|
||
deployment_package: ${{ env.artifact }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
src/main/java/com/mallangs/domain/ai/AIPromptController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package com.mallangs.domain.ai; | ||
|
||
import com.fasterxml.jackson.databind.JavaType; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | ||
import com.mallangs.domain.ai.dto.SightAIResponse; | ||
import com.mallangs.domain.article.dto.response.ArticleResponse; | ||
import com.mallangs.domain.article.dto.response.LostResponse; | ||
import com.mallangs.domain.article.service.ArticleService; | ||
import com.mallangs.domain.board.dto.response.SightingListResponse; | ||
import com.mallangs.domain.board.service.BoardService; | ||
import com.mallangs.domain.member.entity.Member; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.log4j.Log4j2; | ||
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
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; | ||
import org.springframework.web.server.ResponseStatusException; | ||
|
||
import java.util.List; | ||
|
||
@Log4j2 | ||
@RestController | ||
@RequestMapping("/api/v1/ai") | ||
@RequiredArgsConstructor | ||
public class AIPromptController { | ||
|
||
private final VertexAiGeminiChatModel vertexAiGeminiChatModel; | ||
private final ArticleService articleService; | ||
private final BoardService boardService; | ||
|
||
// AI로 비슷한 목격게시물 조회 | ||
// 관리자 전부 조회 가능 | ||
// 회원 visible + 자신의 글 조회 가능 | ||
// 비회원 mapVisible 만 조회 가능 | ||
@Operation(summary = "AI로 비슷한 목격게시물을 조회", description = "AI로 실종글타래와 비슷한 목격게시물을 조회합니다.") | ||
@GetMapping("/{articleId}") | ||
public ResponseEntity<List<SightAIResponse>> getArticleByArticleIdByAI( | ||
@Parameter(description = "조회할 글타래 ID", required = true) @PathVariable Long articleId) { | ||
|
||
String memberRole; | ||
Long memberId; | ||
|
||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); | ||
|
||
if (principal instanceof Member member) { | ||
memberRole = member.getMemberRole().name(); | ||
memberId = member.getMemberId(); | ||
} else { | ||
memberRole = "ROLE_GUEST"; | ||
memberId = -1L; | ||
} | ||
log.info("role: {} memberId: {}", memberRole, memberId); | ||
|
||
//단전조회, 실종글타래 조회 | ||
ArticleResponse articleResponse = articleService.getArticleById(articleId, memberRole, | ||
memberId); | ||
|
||
//질문 제작 | ||
StringBuilder question = new StringBuilder(); | ||
|
||
// JSON 형식에 대한 예시 설명 | ||
question.append("실종 동물과 유사한 목격 정보를 여러 개 찾습니다. " + | ||
"반드시 JSON 배열 형식으로 응답해주세요. 예시는 다음과 같습니다:\n"); | ||
|
||
question.append("[\n"); | ||
question.append(" {\n"); | ||
question.append(" \"sightArticleId\": 12345,\n"); | ||
question.append(" \"percentage\": 85.52,\n"); | ||
question.append(" \"findSpot\": \"서울특별시 강남구 역삼동\",\n"); | ||
question.append(" \"sightedAt\": \"2024-12-24\",\n"); | ||
question.append(" \"breed\": \"Labrador\",\n"); | ||
question.append(" \"color\": \"Yellow\",\n"); | ||
question.append(" \"gender\": \"Male\"\n"); | ||
question.append(" },\n"); | ||
question.append(" {\n"); | ||
question.append(" \"sightArticleId\": 67890,\n"); | ||
question.append(" \"percentage\": 93.3,\n"); | ||
question.append(" \"findSpot\": \"서울특별시 서초구 서초동\",\n"); | ||
question.append(" \"sightedAt\": \"2024-12-22\",\n"); | ||
question.append(" \"breed\": \"Labrador\",\n"); | ||
question.append(" \"color\": \"Yellow\",\n"); | ||
question.append(" \"gender\": \"Male\"\n"); | ||
question.append(" }\n"); | ||
question.append("]\n\n"); | ||
|
||
question.append("목격 정보는 반드시 `percentage` 내림차순으로 정렬해주세요. " + | ||
"그리고 50% 이상만 응답해 주세요.\n\n"); | ||
|
||
// 실종 동물 정보 | ||
LostResponse lostArticle = (LostResponse) articleResponse; | ||
question.append("실종동물에 대한 설명 : lostArticle.getDescription()"); | ||
question.append("실종 위치: 경도:" + lostArticle.getLongitude() + ", 위도:" + lostArticle.getLatitude()); | ||
question.append("실종 동물 종: " + lostArticle.getBreed()); | ||
question.append("실종 동물 색상: " + lostArticle.getPetColor()); | ||
question.append("실종 동물 성별: " + lostArticle.getPetGender()); | ||
question.append("실종 동물 chipNumber: " + lostArticle.getChipNumber()); | ||
question.append("실종동물 실종된 위치: " + lostArticle.getLostLocation()); | ||
question.append("실종동물 실종일: " + lostArticle.getLostDate()); | ||
|
||
// 여러 목격 게시글 정보 추가 | ||
List<SightingListResponse> sightingList = boardService.getAllSightingBoardToList(); | ||
|
||
for (SightingListResponse sightingListResponse : sightingList) { | ||
question.append("목격제보 ID: " + sightingListResponse.getBoardId()); | ||
question.append(", 목격동물 특징: " + sightingListResponse.getContent()); | ||
question.append(", 목격일: " + sightingListResponse.getSightedAt()); | ||
question.append(", 목격위치" + sightingListResponse.getAddress()); | ||
} | ||
|
||
question.append("\n위 정보를 바탕으로 JSON 배열을 만들어 주세요. " + | ||
"각 요소는 SightAIResponse 형식이며, 50% 미만은 제외하고 " + | ||
"percentage 내림차순으로 정렬해 주세요.\n"); | ||
|
||
//AI에게 질문하기 | ||
try { | ||
// 1. Gemini에게 질문 (질문 -> 응답) | ||
String vertexAiGeminiResponse = vertexAiGeminiChatModel.call(question.toString()); | ||
|
||
// 응답에서 백틱 및 불필요한 텍스트 제거 | ||
String cleanedResponse = cleanJsonResponse(vertexAiGeminiResponse); | ||
|
||
// json 형식인지 확인 | ||
if (!cleanedResponse.trim().startsWith("[")) { | ||
log.error("Invalid response format: {}", cleanedResponse); | ||
throw new IllegalArgumentException("AI 응답이 JSON 형식이 아닙니다."); | ||
} | ||
|
||
// 2. JSON 데이터를 DTO로 매핑 <List>버전 | ||
ObjectMapper objectMapper = new ObjectMapper(); | ||
objectMapper.registerModule(new JavaTimeModule()); //jackson 날짜추가 | ||
|
||
JavaType listType = objectMapper.getTypeFactory() | ||
.constructCollectionType(List.class, SightAIResponse.class); | ||
List<SightAIResponse> answerList = objectMapper.readValue(cleanedResponse, listType); | ||
|
||
return ResponseEntity.ok(answerList); | ||
} catch (IllegalArgumentException e) { | ||
log.error("AI answer failed {}", e.getMessage()); | ||
throw new RuntimeException("AI 응답이 유효하지 않습니다."); | ||
} catch (Exception e) { | ||
log.error("Error while processing AI response: {}", e.getMessage()); | ||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "AI 응답 처리 중 문제가 발생했습니다.", e); | ||
} | ||
} | ||
|
||
/** | ||
* AI 응답에서 백틱 및 불필요한 텍스트 제거 | ||
*/ | ||
private String cleanJsonResponse(String response) { | ||
// 백틱과 "```json" 제거 | ||
String cleaned = response.replaceAll("```json", "").replaceAll("```", "").trim(); | ||
log.debug("Cleaned response: {}", cleaned); | ||
return cleaned; | ||
} | ||
} | ||
|
34 changes: 34 additions & 0 deletions
34
src/main/java/com/mallangs/domain/ai/dto/SightAIResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.mallangs.domain.ai.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Data; | ||
|
||
import java.time.LocalDate; | ||
|
||
@Data | ||
@JsonIgnoreProperties(ignoreUnknown = true) // DTO에 정의되지 않은 필드는 무시 | ||
public class SightAIResponse { | ||
|
||
@JsonProperty("sightArticleId") | ||
private Long sightArticleId; | ||
|
||
@JsonProperty("percentage") | ||
private Double percentage; | ||
|
||
@JsonProperty("findSpot") | ||
private String findSpot; | ||
|
||
@JsonProperty("sightedAt") | ||
private LocalDate sightedAt; | ||
|
||
@JsonProperty("breed") | ||
private String breed; | ||
|
||
@JsonProperty("color") | ||
private String color; | ||
|
||
@JsonProperty("gender") | ||
private String gender; | ||
|
||
} |
Oops, something went wrong.