Skip to content

Commit

Permalink
Merge pull request #6 from woorifisa-service-dev-4th/develop
Browse files Browse the repository at this point in the history
🚀 Merge 'develop' branch into 'main' branch
  • Loading branch information
namsh1125 authored Feb 9, 2025
2 parents b2a489d + da02d3e commit c2daeba
Show file tree
Hide file tree
Showing 23 changed files with 1,227 additions and 15 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:

- name: Build with Gradle
run: ./gradlew bootJar
env:
APP_VERSION: ${{ github.event.release.tag_name }}

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
Expand Down Expand Up @@ -86,14 +88,16 @@ jobs:
port: 22
script: |
sudo chmod 666 /var/run/docker.sock
docker stop server || true
docker rm server || true
docker rmi ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPO_NAME }}:latest || true
aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
docker pull ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.AWS_HARUHANA_ECR_NAMESPACE }}/${{ secrets.AWS_HARUHANA_ECR_REPO_NAME }}:latest
sudo docker run --name server -dp 8080:8080 ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.AWS_HARUHANA_ECR_NAMESPACE }}/${{ secrets.AWS_HARUHANA_ECR_REPO_NAME }}:latest
sudo docker run --name server -dp 8080:8080 \
-e APP_VERSION=${{ github.event.release.tag_name }} \
${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.AWS_HARUHANA_ECR_NAMESPACE }}/${{ secrets.AWS_HARUHANA_ECR_REPO_NAME }}:latest
- name: Remove GitHub Actions IP from Security group
if: always()
Expand Down
185 changes: 173 additions & 12 deletions .github/workflows/code_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,178 @@ on:
jobs:
code_review:
runs-on: ubuntu-latest
name: ChatGPT Code Review
name: Gemini Code Review
steps:
- name: GenAI Code Review
uses: cirolini/genai-code-review@v2
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install dependencies
run: npm install @google/generative-ai parse-diff

- name: Review Changed Files
uses: actions/github-script@v7
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GEMINI_MODEL: ${{ secrets.GEMINI_MODEL }}
MAX_CONCURRENT_REVIEWS: 3 # 동시에 처리할 파일 수
MAX_RETRIES: 3
RETRY_DELAY: 1000

with:
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
github_pr_id: ${{ github.event.number }}
openai_model: "gpt-4o-mini" # optional
# openai_temperature: 0.5 # optional
# openai_max_tokens: 2048 # optional
# mode: files # files or patch
language: ko # optional, default is 'en'
# custom_prompt: "" # optional
script: |
const { GoogleGenerativeAI } = require("@google/generative-ai");
const parseDiff = require('parse-diff');
// core는 이미 github-script에서 제공됨
// 재시도 로직을 포함한 API 호출 함수
async function withRetry(fn, retries = process.env.MAX_RETRIES) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (error.message.includes('rate limit') && i < retries - 1) {
const delay = process.env.RETRY_DELAY * Math.pow(2, i);
console.log(`Rate limit hit, waiting ${delay}ms before retry ${i + 1}`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
// Gemini AI 클라이언트 초기화
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
const model = genAI.getGenerativeModel({ model: process.env.GEMINI_MODEL });
/**
* 코드 변경사항을 검토하고 피드백을 생성
*
* @param content 리뷰할 코드 내용
* @param filename 파일명
* @returns 리뷰 코멘트
*/
async function reviewCode(content, filename) {
try {
// 파일명 sanitize
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-_\/]/g, '_');
const prompt = `당신은 시니어 개발자입니다. 아래 ${sanitizedFilename} 파일의 코드를 검토하고 다음 사항들을 한국어로 리뷰해주세요:
1. 코드의 품질과 가독성
2. 잠재적인 버그나 문제점
3. 성능 개선 포인트
4. 보안 관련 이슈
5. 개선을 위한 구체적인 제안
코드:
${content}`;
const result = await withRetry(() => model.generateContent(prompt));
const response = await result.response;
if (!response.text()) {
throw new Error('Gemini API returned empty response');
}
console.log(`Successfully reviewed ${filename}`);
return response.text();
} catch (error) {
console.error(`Error reviewing ${filename}:`, error);
return `⚠️ 코드 리뷰 중 오류가 발생했습니다: ${error.message}
다시 시도하시거나 관리자에게 문의해주세요.`;
}
}
/**
* PR의 각 파일을 처리하고 리뷰 코멘트 작성
* @param file PR에서 변경된 파일 정보
* @param pr PR 정보
*/
async function processFile(file, pr) {
if (file.status === 'removed') {
console.log(`Skipping removed file: ${file.filename}`);
return;
}
try {
if (!file.patch) {
console.warn(`No patch found for ${file.filename}, skipping.`);
return;
}
const diff = parseDiff(file.patch)[0];
if (!diff || !diff.chunks) {
console.log(`No valid diff found for ${file.filename}`);
return;
}
// 모든 변경사항을 하나로 합치기 (추가된 부분만 필터링)
const changes = diff.chunks
.flatMap(chunk => chunk.changes)
.filter(change => change.type === 'add');
if (changes.length === 0) {
console.log(`No added changes found in ${file.filename}`);
return;
}
// 변경사항을 하나의 문자열로 합치기
const content = changes.map(change => change.content).join('\n');
const review = await reviewCode(content, file.filename);
// PR에 리뷰 코멘트 작성 (파일명을 헤더로 추가)
return github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## ${file.filename} 리뷰\n\n${review}`
});
} catch (error) {
console.error(`Error processing ${file.filename}:`, error);
throw error;
}
}
try {
// PR 정보 조회
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
// 변경된 파일 목록 조회
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
console.log(`Starting review of ${files.length} files...`);
// 모든 파일 처리 완료를 기다림
const promises = [];
const batchSize = parseInt(process.env.MAX_CONCURRENT_REVIEWS);
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchPromises = batch.map(file => processFile(file, pr));
promises.push(...batchPromises);
}
await Promise.all(promises);
console.log('All reviews completed successfully');
} catch (error) {
console.error('Workflow failed:', error);
core.setFailed(`Workflow failed: ${error.message}`);
}
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,31 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// JWT
implementation 'io.jsonwebtoken:jjwt:0.12.6'

// AWS
// Reference: https://github.com/aws/aws-sdk-java-v2
implementation platform('software.amazon.awssdk:bom:2.30.16') // BOM 적용. AWS SDK 모듈들이 자동으로 2.30.16 버전에 맞춰짐
implementation 'software.amazon.awssdk:s3'

// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'

// JUnit 5
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Swagger (API Document)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4'
}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/site/haruhana/www/HaruhanaApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class HaruhanaApplication {

public static void main(String[] args) {
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/site/haruhana/www/base/BaseResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package site.haruhana.www.base;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "statusCode", "message", "data"})
@ToString
public class BaseResponse<T> {

private Boolean isSuccess;

private int statusCode;

private String message;

@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;

public static <T> BaseResponse<T> onSuccess(String message, T data) {
return new BaseResponse<>(true, 200, message, data);
}

public static <T> BaseResponse<T> onCreate(String message, T data) {
return new BaseResponse<>(true, 201, message, data);
}

public static <T> BaseResponse<T> onBadRequest(String message) {
return new BaseResponse<>(false, 400, message, null);
}

public static <T> BaseResponse<T> onUnauthorized(String message) {
return new BaseResponse<>(false, 401, message, null);
}

public static <T> BaseResponse<T> onForbidden(String message) {
return new BaseResponse<>(false, 403, message, null);
}

public static <T> BaseResponse<T> onNotFound(String message) {
return new BaseResponse<>(false, 404, message, null);
}

public static <T> BaseResponse<T> onConflict(String message) {
return new BaseResponse<>(false, 409, message, null);
}

public static <T> BaseResponse<T> onInternalServerError(String message) {
return new BaseResponse<>(false, 500, message, null);
}

}
29 changes: 29 additions & 0 deletions src/main/java/site/haruhana/www/base/TokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package site.haruhana.www.base;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import site.haruhana.www.entity.Role;

import java.util.Date;

@Getter
@Builder
@AllArgsConstructor
public class TokenDto {

private String tokenType;

private Role role;

private String accessToken;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date accessTokenExpiresAt;

private String refreshToken;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date refreshTokenExpiresAt;
}
30 changes: 30 additions & 0 deletions src/main/java/site/haruhana/www/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package site.haruhana.www.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class S3Config {

@Value("${aws.s3.access-key}")
private String accessKey;

@Value("${aws.s3.secret-key}")
private String secretKey;

@Value("${aws.s3.region}")
private String region;

@Bean
public S3Client s3Client() {
return S3Client.builder()
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)))
.region(Region.of(region))
.build();
}
}
Loading

0 comments on commit c2daeba

Please sign in to comment.