Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ out/
.vscode/

.env
*secret.yaml
*secret.yaml

### Secret Config ###
src/main/resources/application-secret.yml
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
Expand Down
7 changes: 6 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ services:
- 'MYSQL_ROOT_PASSWORD=verysecret'
- 'MYSQL_USER=myuser'
ports:
- '3306'
- '33006:3306'

redis:
image: 'redis:latest'
ports:
- '6379:6379'
4 changes: 4 additions & 0 deletions k8s/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ spec:
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: SPRING_REDIS_HOST
value: "redis.redis.svc.cluster.local"
- name: SPRING_REDIS_PORT
value: "6379"
envFrom:
- secretRef:
name: db-secret
Expand Down
84 changes: 84 additions & 0 deletions k8s/redis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
apiVersion: v1
kind: Namespace
metadata:
name: redis
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
namespace: redis
data:
redis.conf: |
bind 0.0.0.0
port 6379
protected-mode yes
appendonly yes
appendfsync everysec
maxmemory 512mb
maxmemory-policy volatile-lru
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data
namespace: redis
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: local-path
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.2
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
ports:
- name: redis
containerPort: 6379
volumeMounts:
- name: redis-config
mountPath: /usr/local/etc/redis
- name: redis-data
mountPath: /data
volumes:
- name: redis-config
configMap:
name: redis-config
items:
- key: redis.conf
path: redis.conf
- name: redis-data
persistentVolumeClaim:
claimName: redis-data
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: redis
spec:
type: ClusterIP
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
protocol: TCP
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class SkillBoostApplication {

public static void main(String[] args) {
SpringApplication.run(SkillBoostApplication.class, args);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// src/main/java/com/example/skillboost/codereview/controller/CodeReviewController.java
package com.example.skillboost.codereview.controller;

import com.example.skillboost.codereview.dto.CodeReviewRequest;
import com.example.skillboost.codereview.dto.CodeReviewResponse;
import com.example.skillboost.codereview.service.CodeReviewService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api/review")
@RequiredArgsConstructor
public class CodeReviewController {

private final CodeReviewService codeReviewService;

@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public CodeReviewResponse review(@RequestBody CodeReviewRequest request) {
return codeReviewService.review(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// src/main/java/com/example/skillboost/codereview/dto/CodeReviewRequest.java
package com.example.skillboost.codereview.dto;

public class CodeReviewRequest {

private String code;
private String comment;

// 🔹 레포지터리 기반 리뷰용 필드
private String repoUrl; // 예: https://github.com/Junseung-Ock/java-calculator-7
private String branch; // 기본값: main

public CodeReviewRequest() {
}

public CodeReviewRequest(String code, String comment) {
this.code = code;
this.comment = comment;
}

public CodeReviewRequest(String code, String comment, String repoUrl, String branch) {
this.code = code;
this.comment = comment;
this.repoUrl = repoUrl;
this.branch = branch;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getComment() {
return comment;
}

public void setComment(String comment) {
this.comment = comment;
}

public String getRepoUrl() {
return repoUrl;
}

public void setRepoUrl(String repoUrl) {
this.repoUrl = repoUrl;
}

public String getBranch() {
return branch;
}

public void setBranch(String branch) {
this.branch = branch;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.skillboost.codereview.dto;

import java.util.ArrayList;
import java.util.List;

public class CodeReviewResponse {

private String review;
private List<String> questions = new ArrayList<>();

public CodeReviewResponse() {}

public CodeReviewResponse(String review, List<String> questions) {
this.review = review;
this.questions = questions;
}

public String getReview() { return review; }
public void setReview(String review) { this.review = review; }

public List<String> getQuestions() { return questions; }
public void setQuestions(List<String> questions) { this.questions = questions; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example.skillboost.codereview.github;

public class GithubController {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// src/main/java/com/example/skillboost/codereview/github/GithubFile.java
package com.example.skillboost.codereview.github;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class GithubFile {

private String path;
private String content;

public GithubFile(String path, String content) {
this.path = path;
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// src/main/java/com/example/skillboost/codereview/github/GithubService.java
package com.example.skillboost.codereview.github;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.*;

@Slf4j
@Service
public class GithubService {

private final RestTemplate restTemplate = new RestTemplate();

@Value("${github.token:}")
private String githubToken;

private static final List<String> TEXT_EXTENSIONS = List.of(
".java", ".kt", ".xml", ".json", ".yml", ".yaml",
".md", ".gradle", ".gitignore", ".txt", ".properties", ".csv"
);

public List<GithubFile> fetchRepoCode(String repoUrl, String branch) {
if (repoUrl == null || !repoUrl.contains("github.com/")) {
throw new IllegalArgumentException("잘못된 GitHub URL 형식입니다.");
}

try {
String[] parts = repoUrl.replace("https://github.com/", "")
.replace("http://github.com/", "")
.split("/");
if (parts.length < 2) throw new IllegalArgumentException("잘못된 GitHub URL 형식입니다.");

String owner = parts[0];
String repo = parts[1];

String treeUrl = String.format(
"https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1",
owner, repo, branch
);

log.info("[GithubService] tree 호출: {}", treeUrl);

HttpHeaders headers = new HttpHeaders();
if (githubToken != null && !githubToken.isEmpty()) {
headers.setBearerAuth(githubToken);
}
HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<Map> resp = restTemplate.exchange(
treeUrl, HttpMethod.GET, entity, Map.class
);

Map<String, Object> body = resp.getBody();
if (body == null || !body.containsKey("tree")) {
return Collections.emptyList();
}

List<Map<String, Object>> tree = (List<Map<String, Object>>) body.get("tree");
List<GithubFile> files = new ArrayList<>();

for (Map<String, Object> file : tree) {
if (!"blob".equals(file.get("type"))) continue;

String path = (String) file.get("path");
if (!isTextFile(path)) continue;

String rawUrl = String.format(
"https://raw.githubusercontent.com/%s/%s/%s/%s",
owner, repo, branch, path
);

String content = fetchFileContent(rawUrl);
files.add(new GithubFile(path, content));
}

log.info("[GithubService] {} 개 파일 로드 완료", files.size());
return files;

} catch (Exception e) {
log.error("[GithubService] 레포지터리 로드 실패: {}", e.getMessage());
return Collections.emptyList();
}
}

private String fetchFileContent(String rawUrl) {
try {
HttpHeaders headers = new HttpHeaders();
if (githubToken != null && !githubToken.isEmpty()) {
headers.setBearerAuth(githubToken);
}
HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<String> resp = restTemplate.exchange(
rawUrl, HttpMethod.GET, entity, String.class
);
return resp.getBody() != null ? resp.getBody() : "";
} catch (Exception e) {
log.warn("[GithubService] 파일 읽기 실패: {} ({})", rawUrl, e.getMessage());
return "";
}
}

private boolean isTextFile(String path) {
String lower = path.toLowerCase();
for (String ext : TEXT_EXTENSIONS) {
if (lower.endsWith(ext)) return true;
}
return false;
}
}
Loading