Skip to content

Commit 57c390f

Browse files
authored
Merge pull request #77 from CommitField/feat/#66
Feat: ๋ ˆ๋“œํŒ๋‹ค ๋ฐ ๋ ˆ๋””์Šค ์˜์กด์„ฑ ์ถ”๊ฐ€, ๊ธฐ์ดˆ ์„ธํŒ…
2 parents fb45f46 + cc883a9 commit 57c390f

File tree

17 files changed

+342
-17
lines changed

17 files changed

+342
-17
lines changed

โ€Ž.gitignoreโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ docker-compose.yaml
7575

7676
### dbํŒŒ์ผ
7777
db/
78+
.docker
79+
data/
7880

7981
### secret ํ”„๋กœํ•„
8082
application-secret.yml

โ€Žbuild.gradle.ktsโ€Ž

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,13 @@ dependencies {
4040
compileOnly("org.projectlombok:lombok")
4141
annotationProcessor("org.projectlombok:lombok")
4242

43-
// DB
43+
//DB
4444
runtimeOnly("com.h2database:h2")
4545
runtimeOnly("com.mysql:mysql-connector-j")
4646
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
4747

48-
// redis
48+
//redis
4949
implementation("org.springframework.boot:spring-boot-starter-data-redis")
50-
implementation("org.springframework.session:spring-session-data-redis")
51-
52-
// actuator
53-
implementation("org.springframework.boot:spring-boot-starter-actuator")
5450

5551
// Security
5652
implementation("org.springframework.boot:spring-boot-starter-security")
@@ -59,10 +55,7 @@ dependencies {
5955

6056
//Swagger
6157
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
62-
63-
//Web Socket
6458
implementation("org.java-websocket:Java-WebSocket:1.5.2")
65-
implementation ("org.springframework:spring-messaging")
6659

6760
// JWT
6861
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
@@ -81,6 +74,17 @@ dependencies {
8174
implementation ("org.springframework.security:spring-security-oauth2-client:6.4.2") // Or the version you're using
8275
implementation ("org.springframework.security:spring-security-oauth2-core:6.4.2") // Or the version you're using
8376

77+
// Spring Kafka
78+
implementation("org.springframework.kafka:spring-kafka")
79+
80+
// Kafka ํด๋ผ์ด์–ธํŠธ (Redpanda์™€ ํ˜ธํ™˜)
81+
implementation("org.apache.kafka:kafka-clients:3.6.0")
82+
83+
// JSON ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” (Kafka ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ)
84+
implementation("com.fasterxml.jackson.core:jackson-databind")
85+
86+
// ํ…Œ์ŠคํŠธ์šฉ Kafka ์ž„๋ฒ ๋””๋“œ ์„œ๋ฒ„ (์„ ํƒ ์‚ฌํ•ญ)
87+
testImplementation("org.springframework.kafka:spring-kafka-test")
8488
}
8589

8690
tasks.withType<Test> {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cmf.commitField.domain.commit.scheduler;
2+
3+
import cmf.commitField.domain.commit.sinceCommit.service.CommitCacheService;
4+
import cmf.commitField.domain.commit.sinceCommit.service.GithubService;
5+
import cmf.commitField.domain.redpanda.RedpandaProducer;
6+
import cmf.commitField.domain.user.entity.User;
7+
import cmf.commitField.domain.user.repository.UserRepository;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.scheduling.annotation.Scheduled;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.util.List;
14+
15+
@Slf4j
16+
@Service
17+
@RequiredArgsConstructor
18+
public class CommitScheduler {
19+
private final GithubService githubService;
20+
private final CommitCacheService commitCacheService;
21+
private final RedpandaProducer redpandaProducer;
22+
private final UserRepository userRepository;
23+
24+
@Scheduled(fixedRate = 60000) // 1๋ถ„๋งˆ๋‹ค ์‹คํ–‰
25+
public void updateUserCommits() {
26+
List<User> activeUsers = userRepository.findAll(); // ๐Ÿ’ซ ๋ณ€๊ฒฝ ํ•„์š”, ์ฐจํ›„ active ์ƒํƒœ์ธ user๋งŒ ์ฐพ๊ฒŒ๋” ๋ณ€๊ฒฝํ•ด์•ผ ํ•จ.
27+
28+
for (User user : activeUsers) {
29+
Integer cachedCount = commitCacheService.getCachedCommitCount(user.getUsername());
30+
int newCommitCount = githubService.getUserCommitCount(user.getUsername());
31+
32+
if (cachedCount == null || cachedCount != newCommitCount) { // ๋ณ€ํ™”๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์ฒ˜๋ฆฌ
33+
commitCacheService.updateCachedCommitCount(user.getUsername(), newCommitCount);
34+
redpandaProducer.sendCommitUpdate(user.getUsername(), newCommitCount);
35+
}
36+
}
37+
}
38+
}

โ€Žsrc/main/java/cmf/commitField/domain/commit/sinceCommit/controller/SinceCommitController.javaโ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import cmf.commitField.domain.commit.sinceCommit.dto.CommitAnalysisResponseDto;
44
import cmf.commitField.domain.commit.sinceCommit.dto.SinceCommitResponseDto;
5+
import cmf.commitField.domain.commit.sinceCommit.service.GithubService;
56
import cmf.commitField.domain.commit.sinceCommit.service.SinceCommitService;
67
import lombok.RequiredArgsConstructor;
78
import org.springframework.format.annotation.DateTimeFormat;
89
import org.springframework.http.ResponseEntity;
910
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
1012
import org.springframework.web.bind.annotation.RequestParam;
1113
import org.springframework.web.bind.annotation.RestController;
1214

@@ -18,6 +20,7 @@
1820
@RequiredArgsConstructor
1921
public class SinceCommitController {
2022
private final SinceCommitService sinceCommitService;
23+
private final GithubService githubService;
2124

2225
@GetMapping("/api/github/commits-since")
2326
public ResponseEntity<List<SinceCommitResponseDto>> getCommits(
@@ -100,4 +103,12 @@ public ResponseEntity<CommitAnalysisResponseDto> getWinterSeasonCommits(
100103
CommitAnalysisResponseDto analysis = sinceCommitService.getCommitAnalysis(owner, repo, since, until);
101104
return ResponseEntity.ok(analysis);
102105
}
106+
107+
// api ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ ์ถ”๊ฐ€
108+
@GetMapping("/api/commit-count/{username}")
109+
public ResponseEntity<Integer> getCommitCount(@PathVariable String username) {
110+
System.out.println("โšก API ์—”๋“œํฌ์ธํŠธ ํ˜ธ์ถœ: " + username);
111+
int commitCount = githubService.getUserCommitCount(username);
112+
return ResponseEntity.ok(commitCount);
113+
}
103114
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package cmf.commitField.domain.commit.sinceCommit.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.Setter;
7+
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
@Getter
11+
@Setter
12+
public class CommitData {
13+
String user;
14+
int commits;
15+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cmf.commitField.domain.commit.sinceCommit.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.data.redis.core.StringRedisTemplate;
6+
import org.springframework.stereotype.Service;
7+
8+
import java.time.Duration;
9+
10+
@Slf4j
11+
@Service
12+
@RequiredArgsConstructor
13+
public class CommitCacheService {
14+
private final StringRedisTemplate redisTemplate;
15+
16+
public Integer getCachedCommitCount(String username) {
17+
String key = "commit:" + username;
18+
String value = redisTemplate.opsForValue().get(key);
19+
return value != null ? Integer.parseInt(value) : null;
20+
}
21+
22+
public void updateCachedCommitCount(String username, int count) {
23+
String key = "commit:" + username;
24+
redisTemplate.opsForValue().set(key, String.valueOf(count), Duration.ofHours(1)); // 1์‹œ๊ฐ„ ์บ์‹ฑ
25+
}
26+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package cmf.commitField.domain.commit.sinceCommit.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.core.ParameterizedTypeReference;
6+
import org.springframework.http.*;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.web.client.HttpClientErrorException;
9+
import org.springframework.web.client.RestTemplate;
10+
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class GithubService {
17+
private final RestTemplate restTemplate;
18+
private final String GITHUB_API_URL = "https://api.github.com";
19+
20+
@Value("${github.token}")
21+
private String GITHUB_TOKEN;
22+
23+
public int getUserCommitCount(String username) {
24+
HttpHeaders headers = new HttpHeaders();
25+
headers.set("Authorization", "Bearer " + GITHUB_TOKEN);
26+
headers.set("Accept", "application/vnd.github.v3+json"); // ์ตœ์‹  GitHub API ๋ฒ„์ „ ์ง€์ •
27+
28+
HttpEntity<String> entity = new HttpEntity<>(headers);
29+
String url = String.format("%s/users/%s/events", GITHUB_API_URL, username);
30+
31+
// ๐Ÿ“Œ API ํ˜ธ์ถœ ํšŸ์ˆ˜ ํ™•์ธ์šฉ ๋กœ๊ทธ ์ถ”๊ฐ€
32+
System.out.println("GitHub API ํ˜ธ์ถœ: " + url);
33+
34+
try {
35+
ResponseEntity<List<Map<String, Object>>> response =
36+
restTemplate.exchange(url, HttpMethod.GET, entity, new ParameterizedTypeReference<>() {});
37+
38+
// GitHub API Rate Limit ํ™•์ธ (๋‚จ์€ ์š”์ฒญ ํšŸ์ˆ˜ ๋กœ๊น…)
39+
HttpHeaders responseHeaders = response.getHeaders();
40+
String remainingRequests = responseHeaders.getFirst("X-RateLimit-Remaining");
41+
System.out.println("GitHub API ๋‚จ์€ ์š”์ฒญ ํšŸ์ˆ˜: " + remainingRequests);
42+
43+
int commitCount = 0;
44+
if (response.getBody() != null) {
45+
for (Map<String, Object> event : response.getBody()) {
46+
if ("PushEvent".equals(event.get("type"))) {
47+
Map<String, Object> payload = (Map<String, Object>) event.get("payload");
48+
if (payload != null && payload.containsKey("commits")) {
49+
List<?> commits = (List<?>) payload.get("commits");
50+
commitCount += (commits != null) ? commits.size() : 0;
51+
}
52+
}
53+
}
54+
}
55+
return commitCount;
56+
57+
} catch (HttpClientErrorException e) {
58+
System.err.println("GitHub API ์š”์ฒญ ์‹คํŒจ: " + e.getStatusCode() + " - " + e.getResponseBodyAsString());
59+
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {
60+
throw new RuntimeException("GitHub API ์ธ์ฆ ์‹คํŒจ: ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์„ธ์š”.");
61+
} else if (e.getStatusCode() == HttpStatus.FORBIDDEN) {
62+
throw new RuntimeException("GitHub API ์š”์ฒญ ์ œํ•œ ์ดˆ๊ณผ (Rate Limit ์ดˆ๊ณผ). ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.");
63+
}
64+
return 0; // ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜
65+
} catch (Exception e) {
66+
System.err.println("์˜ˆ๊ธฐ์น˜ ์•Š์€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: " + e.getMessage());
67+
return 0; // ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜
68+
}
69+
}
70+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package cmf.commitField.domain.redpanda;
2+
3+
import org.springframework.kafka.annotation.KafkaListener;
4+
import org.springframework.stereotype.Service;
5+
6+
@Service
7+
public class KafkaConsumer {
8+
9+
@KafkaListener(topics = "commit-topic", groupId = "commit-group")
10+
public void listen(String message) {
11+
System.out.println("Received message: " + message);
12+
}
13+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cmf.commitField.domain.redpanda;
2+
3+
import org.springframework.kafka.core.KafkaTemplate;
4+
import org.springframework.stereotype.Service;
5+
6+
@Service
7+
public class RedpandaProducer {
8+
9+
private final KafkaTemplate<String, String> kafkaTemplate;
10+
private static final String TOPIC = "commit-topic"; // Redpanda์—์„œ ์‚ฌ์šฉํ•  ํ† ํ”ฝ๋ช…
11+
12+
public RedpandaProducer(KafkaTemplate<String, String> kafkaTemplate) {
13+
this.kafkaTemplate = kafkaTemplate;
14+
}
15+
16+
// ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฉ”์„œ๋“œ
17+
public void sendMessage(String message) {
18+
kafkaTemplate.send(TOPIC, message);
19+
System.out.println("๐Ÿ“จ Sent message to Redpanda: " + message);
20+
}
21+
22+
// ์ปค๋ฐ‹ ์—…๋ฐ์ดํŠธ ์ „์†ก ๋ฉ”์„œ๋“œ
23+
public void sendCommitUpdate(String username, int commitCount) {
24+
String message = String.format("{\"user\": \"%s\", \"commits\": %d}", username, commitCount);
25+
kafkaTemplate.send(TOPIC, message);
26+
System.out.println("๐Ÿ“จ Sent commit update to Redpanda: " + message);
27+
}
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package cmf.commitField.domain.redpanda.commit;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.kafka.annotation.KafkaListener;
5+
import org.springframework.messaging.simp.SimpMessagingTemplate;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class CommitConsumer {
10+
11+
@Autowired
12+
private SimpMessagingTemplate messagingTemplate;
13+
14+
@KafkaListener(topics = "commit-events", groupId = "commit-group")
15+
public void consume(String message) {
16+
System.out.println("Received commit event: " + message);
17+
// WebSocket์„ ํ†ตํ•ด Frontend๋กœ ์ „์†ก
18+
messagingTemplate.convertAndSend("/topic/commits", message);
19+
}
20+
21+
}

0 commit comments

Comments
ย (0)