Skip to content

Commit

Permalink
Merge pull request #74 from Jolvre/feat/sse
Browse files Browse the repository at this point in the history
Feat/sse
  • Loading branch information
bandalgomsu authored Jun 19, 2024
2 parents f96f2c1 + 72cef38 commit 4327492
Show file tree
Hide file tree
Showing 20 changed files with 578 additions and 0 deletions.
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ dependencies {
//firebase
implementation 'com.google.firebase:firebase-admin:6.8.1'
//okhttp

implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2'

//kafka
implementation 'org.springframework.kafka:spring-kafka'

implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.2.0'

//Query Dsl
Expand All @@ -76,6 +82,7 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.1")


}

tasks.named('test') {
Expand Down
16 changes: 16 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ volumes:
jolvre_mysql:
external: true
services:
zookeeper:
image: wurstmeister/zookeeper
container_name: zookeeper
ports:
- "2181:2181"

kafka:
image: wurstmeister/kafka:2.12-2.5.0
container_name: kafka-server
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
volumes:
- /var/run/docker.sock:/var/run/docker.sock
mysql-server:
image: mysql:latest
container_name: mysql-server
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
request.requestMatchers("api/v1/comment/getComment/**").permitAll();
request.requestMatchers("/ws/chat/**").permitAll();
request.requestMatchers("/chat/**").permitAll();
request.requestMatchers("/api/v1/notification/**").permitAll();
request.anyRequest().authenticated(); // 위의 경로 이외에는 모두 인증된 사용자만 접근 가능
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.error("[AUTH] : 가입되지 않은 사용자 접근 {}", authException.getMessage(), authException);
log.error("[AUTH] : Path = {}", request.getRequestURI());
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write(
ErrorResponse.of(ErrorCode.USER_ACCESS_DENIED, request.getRequestURI()).convertToJson()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.jolvre.common.config;

import com.example.jolvre.notification.entity.NotificationMessage;
import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.Deserializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;

@Configuration
public class KafkaConsumerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;

@Value("${spring.kafka.consumer.group-id}")
private String groupId;

@Bean
public ConsumerFactory<String, NotificationMessage> consumerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
config.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
JsonDeserializer<NotificationMessage> deserializer = new JsonDeserializer<>(NotificationMessage.class);
deserializer.addTrustedPackages("com.example.jolvre");
return new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(), deserializer);
}

@Bean
public ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, NotificationMessage> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.jolvre.common.config;

import com.example.jolvre.notification.entity.NotificationMessage;
import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;

@Configuration
public class KafkaProducerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;

@Bean
public ProducerFactory<String, NotificationMessage> producerFactory() {
Map<String,Object> configs = new HashMap<>();
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(configs);
}

@Bean
public KafkaTemplate<String, NotificationMessage> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.example.jolvre.exhibition.entity.Exhibit;
import com.example.jolvre.exhibition.repository.DiaryRepository;
import com.example.jolvre.exhibition.repository.ExhibitRepository;
import com.example.jolvre.notification.service.NotificationService;
import com.example.jolvre.user.entity.User;
import com.example.jolvre.user.service.UserService;
import jakarta.transaction.Transactional;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import com.example.jolvre.exhibition.repository.ExhibitImageRepository;
import com.example.jolvre.exhibition.repository.ExhibitQueryDslRepository;
import com.example.jolvre.exhibition.repository.ExhibitRepository;
import com.example.jolvre.notification.entity.NotificationType;
import com.example.jolvre.notification.service.NotificationService;
import com.example.jolvre.user.entity.User;
import com.example.jolvre.user.service.UserService;
import jakarta.transaction.Transactional;
Expand All @@ -38,8 +40,14 @@ public class ExhibitService {
private final UserService userService;
private final DiaryRepository diaryRepository;
private final ExhibitCommentRepository exhibitCommentRepository;

private final WebClient webClient;
private final NotificationService notificationService;


private final ExhibitQueryDslRepository exhibitQueryDslRepository;


@Transactional
public ExhibitUploadResponse uploadExhibit(ExhibitUploadRequest request, Long userId) {

Expand Down Expand Up @@ -232,7 +240,52 @@ public ExhibitInvitationResponse createInvitation(Long exhibitId) {

}


@Transactional
public void uploadComment(Long exhibitId, Long loginUserId, ExhibitCommentUploadRequest request) {
Exhibit exhibit = exhibitRepository.findById(exhibitId).orElseThrow(
ExhibitNotFoundException::new);
User user = userService.getUserById(loginUserId);

ExhibitComment comment = ExhibitComment.builder()
.exhibit(exhibit)
.user(user)
.content(request.getContent()).build();

exhibitCommentRepository.save(comment);

notificationService.commentNotificationCreate(loginUserId, exhibit.getUser().getId(),
user.getNickname() + "님이 감상평을 남겼습니다", NotificationType.EXHIBIT_COMMENT);
}

@Transactional
public ExhibitCommentInfoResponses getAllCommentInfo(Long exhibitId) {
List<ExhibitComment> comments = exhibitCommentRepository.findAllByExhibitId(exhibitId);

return ExhibitCommentInfoResponses.toDTO(comments);
}

@Transactional
public void updateComment(Long commentId, Long loginUserId, ExhibitCommentUpdateRequest request) {
ExhibitComment comment = exhibitCommentRepository.findByIdAndUserId(commentId, loginUserId)
.orElseThrow(CommentNotFoundException::new);

comment.updateContent(request.getContent());

exhibitCommentRepository.save(comment);
}

@Transactional
public void deleteComment(Long commentId, Long loginUserId) {
ExhibitComment comment = exhibitCommentRepository.findByIdAndUserId(commentId, loginUserId)
.orElseThrow(CommentNotFoundException::new);

exhibitCommentRepository.delete(comment);
}


// 키워드 기반 전시 조회

public Page<ExhibitInfoResponse> getExhibitInfoByKeyword(String keyword, Pageable pageable) {
return exhibitQueryDslRepository.findAllByFilter(true, keyword, pageable);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.example.jolvre.group.repository.GroupExhibitRepository;
import com.example.jolvre.group.repository.GroupInviteStateRepository;
import com.example.jolvre.group.repository.MemberRepository;
import com.example.jolvre.notification.entity.NotificationType;
import com.example.jolvre.notification.service.NotificationService;
import com.example.jolvre.user.entity.User;
import com.example.jolvre.user.service.UserService;
import jakarta.transaction.Transactional;
Expand All @@ -29,6 +31,7 @@ public class GroupInviteService {
private final GroupInviteStateRepository groupInviteStateRepository;
private final MemberRepository memberRepository;
private final GroupRoleChecker checker;
private final NotificationService notificationService;

@Transactional // 유저 초대
public void inviteUser(Long fromUser, String toUser, Long groupId) {
Expand All @@ -51,6 +54,10 @@ public void inviteUser(Long fromUser, String toUser, Long groupId) {
.user(to).build();

groupInviteStateRepository.save(inviteState);

notificationService.commentNotificationCreate(fromUser, to.getId(),
group.getName() + "에서 초대요청이 도착했습니다",
NotificationType.GROUP_EXHIBIT_INVITE);
}

@Transactional //초대 리스트 조회
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.example.jolvre.notification.api;

import com.example.jolvre.auth.PrincipalDetails;
import com.example.jolvre.notification.dto.NotificationDTO.NotificationInfoResponses;
import com.example.jolvre.notification.entity.NotificationType;
import com.example.jolvre.notification.service.EmitterService;
import com.example.jolvre.notification.service.NotificationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Tag(name = "알림", description = "알림 설정 및 정보를 관리합니다")
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/api/v1/notification")
public class NotificationController {
private final NotificationService notificationService;
private final EmitterService emitterService;

@GetMapping("/test")
public ResponseEntity<Void> test() {
notificationService.commentNotificationCreate(1L, 2L, "야 !", NotificationType.EXHIBIT_COMMENT);

return ResponseEntity.ok().build();
}

@Operation(summary = "sse 연결", description = "서버와 sse 커넥션을 설정합니다")
@GetMapping(produces = "text/event-stream")
public ResponseEntity<SseEmitter> stream(@AuthenticationPrincipal PrincipalDetails principalDetails
, @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId) {
log.info("emitter connect");

SseEmitter response = emitterService.addEmitter(String.valueOf(principalDetails.getId()), lastEventId);

return ResponseEntity.ok(response);
}

@Operation(summary = "알림 조회", description = "자신의 알림을 조회합니닥")
@GetMapping("/me")
public ResponseEntity<NotificationInfoResponses> getNotificationInfo(
@AuthenticationPrincipal PrincipalDetails principalDetails) {
NotificationInfoResponses response = notificationService.getNotificationInfoById(
principalDetails.getId());

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.jolvre.notification.dto;


import com.example.jolvre.notification.entity.NotificationType;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

public class NotificationDTO {

@Getter
@AllArgsConstructor
@Builder
public static class NotificationInfoResponse {
private String message;
private NotificationType notificationType;
}

@Getter
@AllArgsConstructor
@Builder
public static class NotificationInfoResponses {
List<NotificationInfoResponse> notificationInfoResponses;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.jolvre.notification.entity;

import com.example.jolvre.common.entity.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@Entity
@NoArgsConstructor
public class Notification extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "notification_id")
private Long id;

private String message;

@Enumerated(EnumType.STRING)
private NotificationType notificationType;

private Long receiverId;

@Builder
public Notification(String message, NotificationType notificationType, Long receiverId) {
this.message = message;
this.notificationType = notificationType;
this.receiverId = receiverId;
}
}
Loading

0 comments on commit 4327492

Please sign in to comment.