Skip to content

Commit

Permalink
Merge pull request #25 from 2024-Iris/notification
Browse files Browse the repository at this point in the history
SSE 및 알림 기능 구현중
  • Loading branch information
dokkisan authored Jul 17, 2024
2 parents d0c8d40 + ebd089e commit a549bb0
Show file tree
Hide file tree
Showing 41 changed files with 1,128 additions and 83 deletions.
25 changes: 12 additions & 13 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
name: sonarCloud

on:
push:
branches:
Expand All @@ -8,17 +10,20 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [ 17 ]
services:
redis:
image: redis:latest
ports:
- 6379:6379

steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.java-version }}
java-version: 17
distribution: 'adopt'

- name: Grant execute permission for gradlew
Expand All @@ -31,14 +36,8 @@ jobs:
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar

- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: check
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}

- name: SonarCloud scan
- name: Run tests and SonarCloud scan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew sonar --info --stacktrace
run: ./gradlew test sonar --info
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ out/
.vscode/
/config.yml
/src/main/resources/logback.xml
docker-compose.yml
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dependencies {
testImplementation "org.testcontainers:mysql:1.19.8"
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

}

def jacocoDir = layout.buildDirectory.dir("reports/")
Expand Down Expand Up @@ -112,7 +113,8 @@ jacocoTestReport {
'**/vo/**',
'**/dto/**',
'**/entity/**',
'**/response/**'
'**/response/**',
'**/component/LambdaKey'
])
})
)
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/site/iris/issuefy/component/LambdaKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package site.iris.issuefy.component;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Component
@Getter
@NoArgsConstructor
public class LambdaKey {
@Value("${jwt.lambdaKey}")
private String key;

public LambdaKey(String key) {
this.key = key;
}
}
9 changes: 4 additions & 5 deletions src/main/java/site/iris/issuefy/config/CorsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // CORS를 적용할 URL 패턴 설정
.allowedOrigins("*")
.allowedOrigins("http://localhost:3000") // 허용할 Origin 설정
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드 설정
.allowedHeaders("*"); // 허용할 HTTP 헤더 설정
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://issuefy.site")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("*");
}
}
7 changes: 4 additions & 3 deletions src/main/java/site/iris/issuefy/config/FilterConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.context.annotation.Configuration;

import lombok.RequiredArgsConstructor;
import site.iris.issuefy.component.LambdaKey;
import site.iris.issuefy.filter.JwtAuthenticationFilter;
import site.iris.issuefy.service.TokenProvider;

Expand All @@ -13,13 +14,13 @@
public class FilterConfig {

private final TokenProvider tokenProvider;
private final LambdaKey lambdaKey;

@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> jwtAuthenticationFilter() {
FilterRegistrationBean<JwtAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtAuthenticationFilter(tokenProvider));
registrationBean.addUrlPatterns("/api/*");

registrationBean.setFilter(new JwtAuthenticationFilter(tokenProvider, lambdaKey));
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
36 changes: 29 additions & 7 deletions src/main/java/site/iris/issuefy/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
package site.iris.issuefy.config;

import java.io.Serializable;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

import site.iris.issuefy.service.NotificationService;

@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
public RedisMessageListenerContainer container(LettuceConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, sseMessagesTopic());
container.addMessageListener(listenerAdapter, repositoryUpdatesTopic());
return container;
}

@Bean
public MessageListenerAdapter listenerAdapter(NotificationService notificationService) {
return new MessageListenerAdapter(notificationService, "handleRedisMessage");
}

@Bean
public ChannelTopic sseMessagesTopic() {
return new ChannelTopic("sse:messages");
}

@Bean
public PatternTopic repositoryUpdatesTopic() {
return new PatternTopic("repository_updates");
}

}

15 changes: 15 additions & 0 deletions src/main/java/site/iris/issuefy/config/SseConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package site.iris.issuefy.config;

import java.util.concurrent.ConcurrentHashMap;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Configuration
public class SseConfig {
@Bean
public ConcurrentHashMap<String, SseEmitter> SseConnectionMap() {
return new ConcurrentHashMap<>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package site.iris.issuefy.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import site.iris.issuefy.model.dto.NotificationDto;
import site.iris.issuefy.model.dto.NotificationReadDto;
import site.iris.issuefy.service.NotificationService;

@RestController
@RequiredArgsConstructor
@RequestMapping("api/notifications")
@Slf4j
public class NotificationController {

private final NotificationService notificationService;

@GetMapping
public ResponseEntity<List<NotificationDto>> getNotifications(@RequestAttribute String githubId) {
List<NotificationDto> notificationDtos = notificationService.findNotifications(githubId);
log.info(notificationDtos.toString());
return ResponseEntity.ok(notificationDtos);
}

@PatchMapping
public ResponseEntity<Void> updateNotification(@RequestBody NotificationReadDto notificationReadDto) {
notificationService.updateUserNotificationsAsRead(notificationReadDto);
return ResponseEntity.ok().build();
}

}
52 changes: 52 additions & 0 deletions src/main/java/site/iris/issuefy/controller/SseController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package site.iris.issuefy.controller;

import java.io.IOException;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import site.iris.issuefy.model.dto.UpdateRepositoryDto;
import site.iris.issuefy.service.NotificationService;

@RestController
@RequiredArgsConstructor
@Slf4j
public class SseController {
private final NotificationService notificationService;

@GetMapping(value = "/api/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> connect(@RequestAttribute String githubId) {
log.info("New connection for user: {}", githubId);
SseEmitter emitter = new SseEmitter(60000L);
notificationService.addEmitter(githubId, emitter);
notificationService.sendInitialNotification(githubId, emitter);

emitter.onTimeout(() -> {
log.info("SSE connection timed out for user: {}", githubId);
notificationService.removeEmitter(githubId);
try {
emitter.send(SseEmitter.event().name("error").data("Connection timed out"));
} catch (IOException e) {
log.error("Error sending timeout message", e);
} finally {
emitter.complete();
}
});

return ResponseEntity.ok(emitter);
}

@PostMapping("/api/receive")
public void receive(@RequestBody UpdateRepositoryDto updateRepositoryDto) {
log.info("lambda request receive");
notificationService.handleRedisMessage(updateRepositoryDto);
}
}
53 changes: 53 additions & 0 deletions src/main/java/site/iris/issuefy/entity/Notification.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package site.iris.issuefy.entity;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "notification")
public class Notification {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@JoinColumn(name = "repository_id", nullable = false)
private Repository repository;

@Column(name = "repository_name", nullable = false)
private String repositoryName;

@Column(name = "push_time", nullable = false)
private LocalDateTime pushTime;

public Notification(Repository repository, String repositoryName, LocalDateTime pushTime) {
this.repository = repository;
this.repositoryName = repositoryName;
this.pushTime = pushTime;
}

@Override
public String toString() {
return "Notification{" +
"id=" + id +
", repository=" + repository +
", message='" + repositoryName + '\'' +
", pushTime=" + pushTime +
'}';
}
}
2 changes: 2 additions & 0 deletions src/main/java/site/iris/issuefy/entity/Org.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "organization")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Org {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/site/iris/issuefy/entity/Repository.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "repository")
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Repository {
@Id
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/site/iris/issuefy/entity/Subscription.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "subscription")
public class Subscription {
Expand Down
Loading

0 comments on commit a549bb0

Please sign in to comment.