Skip to content

Commit

Permalink
Merge pull request #116 from TRIP-Side-Project/dev
Browse files Browse the repository at this point in the history
pr for merge
  • Loading branch information
kwondongwook authored Dec 22, 2023
2 parents 08fcbd4 + a4f2352 commit ae25b80
Show file tree
Hide file tree
Showing 14 changed files with 1,507 additions and 24 deletions.
4 changes: 3 additions & 1 deletion src/main/java/com/api/trip/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ public enum ErrorCode {
INTEREST_ARTICLE_ALREADY_EXISTS("이미 좋아한 게시글입니다.", HttpStatus.BAD_REQUEST),
INTEREST_ARTICLE_NOT_FOUND("좋아한 게시글이 아닙니다.", HttpStatus.NOT_FOUND),
UPLOAD_FAILED("파일 업로드에 실패하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
;

// 알림
NOTIFICATION_NOT_FOUND("존재하지 않는 알림입니다.", HttpStatus.NOT_FOUND);

private final String message;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,45 @@ public class OAuth2Attribute {
private String email; // 이메일
private String name; // 이름
private String picture; // 프로필 사진
private String provider; // 플랫폼

static OAuth2Attribute of(String provider, String attributeKey, Map<String, Object> attributes) {
// 각 플랫폼 별로 제공해주는 데이터가 조금씩 다르기 때문에 분기 처리함.
return switch (provider) {
case "google" -> google(attributeKey, attributes);
case "kakao" -> kakao(attributeKey, attributes);
case "naver" -> naver(attributeKey, attributes);
case "google" -> google(provider, attributeKey, attributes);
case "kakao" -> kakao(provider, attributeKey, attributes);
case "naver" -> naver(provider, attributeKey, attributes);
default -> throw new NotFoundException(ErrorCode.NOT_FOUND_PROVIDER);
};
}

private static OAuth2Attribute google(String attributeKey, Map<String, Object> attributes) {
private static OAuth2Attribute google(String provider, String attributeKey, Map<String, Object> attributes) {
log.debug("google: {}", attributes);
return OAuth2Attribute.builder()
.email((String) attributes.get("email"))
.name((String) attributes.get("name"))
.picture((String)attributes.get("picture"))
.attributes(attributes)
.attributeKey(attributeKey)
.provider(provider)
.build();
}

private static OAuth2Attribute kakao(String attributeKey, Map<String, Object> attributes) {
private static OAuth2Attribute kakao(String provider, String attributeKey, Map<String, Object> attributes) {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");

return OAuth2Attribute.builder()
.email("KAKAO_" + (String) kakaoAccount.get("email"))
.email((String) kakaoAccount.get("email"))
.name((String) kakaoProfile.get("nickname"))
.picture((String) kakaoProfile.get("profile_image_url"))
.attributes(kakaoAccount)
.attributeKey(attributeKey)
.provider(provider)
.build();
}

private static OAuth2Attribute naver(String attributeKey, Map<String, Object> attributes) {
private static OAuth2Attribute naver(String provider, String attributeKey, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");

return OAuth2Attribute.builder()
Expand All @@ -65,6 +68,7 @@ private static OAuth2Attribute naver(String attributeKey, Map<String, Object> at
.picture((String) response.get("profile_image"))
.attributes(response)
.attributeKey(attributeKey)
.provider(provider)
.build();
}

Expand All @@ -77,6 +81,7 @@ public Map<String, Object> convertToMap() {
map.put("email", email);
map.put("name", name);
map.put("picture", picture);
map.put("provider", provider);

return map;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.api.trip.common.security.oauth;

import com.api.trip.common.exception.ErrorCode;
import com.api.trip.common.exception.custom_exception.NotFoundException;
import com.api.trip.common.security.jwt.JwtToken;
import com.api.trip.common.security.jwt.JwtTokenProvider;
import com.api.trip.domain.member.controller.dto.LoginResponse;
Expand Down Expand Up @@ -39,33 +41,33 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo

OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

String email = oAuth2User.getAttribute("email");
String provider = oAuth2User.getAttribute("provider");

// [email protected]
String email = provider + "_" + oAuth2User.getAttribute("email");
Optional<Member> findMember = memberRepository.findByEmail(email);

// 회원이 아닌 경우에 회원 가입 진행

Long memberId = 0L;
String role = "";

Member member = null;
if (findMember.isEmpty()) {
String name = oAuth2User.getAttribute("name");
// KAKAO_user123
String name = provider + "_" + oAuth2User.getAttribute("name");
String picture = oAuth2User.getAttribute("picture");

Member member = Member.of(email, "", name, picture);
member = Member.of(email, "", name, picture);
memberRepository.save(member);

memberId = member.getId();
role = member.getRole().getValue();
} else {
member = findMember.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
}

// OAuth2User 객체에서 권한 가져옴
JwtToken jwtToken = jwtTokenProvider.createJwtToken(email, role);
JwtToken jwtToken = jwtTokenProvider.createJwtToken(member.getEmail(), member.getRole().getValue());

// 쿠키 세팅
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("tokenType", "Bearer"));
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("accessToken", jwtToken.getAccessToken()));
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("refreshToken", jwtToken.getRefreshToken()));
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("memberId", String.valueOf(memberId)));
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("memberId", String.valueOf(member.getId())));

// TODO: 프론트 배포 주소로 변경 예정
response.sendRedirect("http://localhost:5173/home");
}

Expand All @@ -74,8 +76,9 @@ private static String createCookie(String name, String value) {
.domain("localhost")
.path("/")
.httpOnly(true)
//.sameSite("None") // https 환경에서 활성화
//.secure(false) // https 환경에서 활성화
.maxAge(60 * 60 * 6)
// .sameSite("None") https 시 활성화
//.secure(true) https 시 활성화
.build()
.toString();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.api.trip.domain.notification.controller;

import com.api.trip.common.exception.CustomException;
import com.api.trip.common.exception.ErrorCode;
import com.api.trip.domain.member.repository.MemberRepository;
import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse;
import com.api.trip.domain.notification.emitter.SseEmitterMap;
import com.api.trip.domain.notification.service.NotificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.time.LocalDateTime;

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

private final MemberRepository memberRepository;
private final NotificationService notificationService;
private final SseEmitterMap sseEmitterMap;

@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> connect() {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
Long memberId = memberRepository.findByEmail(email)
.orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED))
.getId();

SseEmitter sseEmitter = new SseEmitter();
sseEmitterMap.put(memberId, sseEmitter);
sseEmitterMap.send(memberId, "connect", LocalDateTime.now());
return ResponseEntity.ok(sseEmitter);
}

@GetMapping("/send-to-all")
public void sendToAll(@RequestParam String message) {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
sseEmitterMap.sendToAll("send-to-all", email + ": " + message);
}

@GetMapping("/me")
public ResponseEntity<GetMyNotificationsResponse> getMyNotifications() {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
return ResponseEntity.ok(notificationService.getMyNotifications(email));
}

@PatchMapping("/{notificationId}")
public ResponseEntity<Void> readNotification(@PathVariable Long notificationId) {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
notificationService.readNotification(notificationId, email);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{notificationId}")
public ResponseEntity<Void> deleteNotification(@PathVariable Long notificationId) {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
notificationService.deleteNotification(notificationId, email);
return ResponseEntity.ok().build();
}

@DeleteMapping("/me")
public ResponseEntity<Void> deleteMyNotifications() {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
notificationService.deleteMyNotifications(email);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.api.trip.domain.notification.controller.dto;

import com.api.trip.domain.itemtag.model.ItemTag;
import com.api.trip.domain.notification.domain.Notification;
import lombok.Builder;
import lombok.Getter;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Getter
@Builder
public class GetMyNotificationsResponse {

private List<NotificationResponse> notifications;
private int unread;

public static GetMyNotificationsResponse of(List<Notification> notifications, List<ItemTag> itemTags) {
Map<Long, List<ItemTag>> map = itemTags.stream()
.collect(Collectors.groupingBy(itemTag -> itemTag.getItem().getId()));

List<NotificationResponse> notificationResponses = notifications.stream()
.map(notification -> NotificationResponse.of(notification, map.get(notification.getItem().getId())))
.toList();

int unread = (int) notifications.stream()
.filter(notification -> !notification.isRead())
.count();

return builder()
.notifications(notificationResponses)
.unread(unread)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.api.trip.domain.notification.controller.dto;

import com.api.trip.domain.itemtag.model.ItemTag;
import com.api.trip.domain.notification.domain.Notification;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@Builder
public class NotificationResponse {

private Long notificationId;
private Long itemId;
private String itemTitle;
private List<String> tags;
private boolean read;
private LocalDateTime createdAt;

public static NotificationResponse of(Notification notification, List<ItemTag> itemTags) {
List<String> tagNames = itemTags.stream().map(itemTag -> itemTag.getTag().getName()).toList();
return builder()
.notificationId(notification.getId())
.itemId(notification.getItem().getId())
.itemTitle(notification.getItem().getTitle())
.tags(tagNames)
.read(notification.isRead())
.createdAt(notification.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.api.trip.domain.notification.domain;

import com.api.trip.common.auditing.entity.BaseTimeEntity;
import com.api.trip.domain.item.model.Item;
import com.api.trip.domain.member.model.Member;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "item_id"})})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Notification extends BaseTimeEntity {

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

@ManyToOne(fetch = FetchType.LAZY)
private Member member;

@ManyToOne(fetch = FetchType.LAZY)
private Item item;

@Column(nullable = false)
private boolean read;

@Builder
private Notification(Member member, Item item, boolean read) {
this.member = member;
this.item = item;
this.read = read;
}

public void read() {
read = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.api.trip.domain.notification.emitter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;

@Component
@Slf4j
public class SseEmitterMap {

private final Map<Long, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

public void put(Long memberId, SseEmitter sseEmitter) {
sseEmitter.onCompletion(() -> remove(memberId));
sseEmitter.onTimeout(sseEmitter::complete);
sseEmitterMap.put(memberId, sseEmitter);
log.info("connected with {}, the number of connections is {}", memberId, sseEmitterMap.size());
}

public void remove(Long memberId) {
sseEmitterMap.remove(memberId);
log.info("disconnected with {}, the number of connections is {}", memberId, sseEmitterMap.size());
}

public void send(Long memberId, String eventName, Object eventData) {
SseEmitter sseEmitter = sseEmitterMap.get(memberId);
try {
sseEmitter.send(
event()
.name(eventName)
.data(eventData)
);
} catch (IOException | IllegalStateException e) {
remove(memberId);
}
}

public void sendToAll(String eventName, Object eventData) {
SseEventBuilder sseEventBuilder = event().name(eventName).data(eventData);
sseEmitterMap.forEach((memberId, sseEmitter) -> {
try {
sseEmitter.send(sseEventBuilder);
} catch (IOException | IllegalStateException e) {
remove(memberId);
}
});
}
}
Loading

0 comments on commit ae25b80

Please sign in to comment.