Skip to content

Commit ae25b80

Browse files
authored
Merge pull request #116 from TRIP-Side-Project/dev
pr for merge
2 parents 08fcbd4 + a4f2352 commit ae25b80

File tree

14 files changed

+1507
-24
lines changed

14 files changed

+1507
-24
lines changed

src/main/java/com/api/trip/common/exception/ErrorCode.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ public enum ErrorCode {
4545
INTEREST_ARTICLE_ALREADY_EXISTS("이미 좋아한 게시글입니다.", HttpStatus.BAD_REQUEST),
4646
INTEREST_ARTICLE_NOT_FOUND("좋아한 게시글이 아닙니다.", HttpStatus.NOT_FOUND),
4747
UPLOAD_FAILED("파일 업로드에 실패하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
48-
;
48+
49+
// 알림
50+
NOTIFICATION_NOT_FOUND("존재하지 않는 알림입니다.", HttpStatus.NOT_FOUND);
4951

5052
private final String message;
5153
private final HttpStatus status;

src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,42 +21,45 @@ public class OAuth2Attribute {
2121
private String email; // 이메일
2222
private String name; // 이름
2323
private String picture; // 프로필 사진
24+
private String provider; // 플랫폼
2425

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

35-
private static OAuth2Attribute google(String attributeKey, Map<String, Object> attributes) {
36+
private static OAuth2Attribute google(String provider, String attributeKey, Map<String, Object> attributes) {
3637
log.debug("google: {}", attributes);
3738
return OAuth2Attribute.builder()
3839
.email((String) attributes.get("email"))
3940
.name((String) attributes.get("name"))
4041
.picture((String)attributes.get("picture"))
4142
.attributes(attributes)
4243
.attributeKey(attributeKey)
44+
.provider(provider)
4345
.build();
4446
}
4547

46-
private static OAuth2Attribute kakao(String attributeKey, Map<String, Object> attributes) {
48+
private static OAuth2Attribute kakao(String provider, String attributeKey, Map<String, Object> attributes) {
4749
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
4850
Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");
4951

5052
return OAuth2Attribute.builder()
51-
.email("KAKAO_" + (String) kakaoAccount.get("email"))
53+
.email((String) kakaoAccount.get("email"))
5254
.name((String) kakaoProfile.get("nickname"))
5355
.picture((String) kakaoProfile.get("profile_image_url"))
5456
.attributes(kakaoAccount)
5557
.attributeKey(attributeKey)
58+
.provider(provider)
5659
.build();
5760
}
5861

59-
private static OAuth2Attribute naver(String attributeKey, Map<String, Object> attributes) {
62+
private static OAuth2Attribute naver(String provider, String attributeKey, Map<String, Object> attributes) {
6063
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
6164

6265
return OAuth2Attribute.builder()
@@ -65,6 +68,7 @@ private static OAuth2Attribute naver(String attributeKey, Map<String, Object> at
6568
.picture((String) response.get("profile_image"))
6669
.attributes(response)
6770
.attributeKey(attributeKey)
71+
.provider(provider)
6872
.build();
6973
}
7074

@@ -77,6 +81,7 @@ public Map<String, Object> convertToMap() {
7781
map.put("email", email);
7882
map.put("name", name);
7983
map.put("picture", picture);
84+
map.put("provider", provider);
8085

8186
return map;
8287
}

src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.api.trip.common.security.oauth;
22

3+
import com.api.trip.common.exception.ErrorCode;
4+
import com.api.trip.common.exception.custom_exception.NotFoundException;
35
import com.api.trip.common.security.jwt.JwtToken;
46
import com.api.trip.common.security.jwt.JwtTokenProvider;
57
import com.api.trip.domain.member.controller.dto.LoginResponse;
@@ -39,33 +41,33 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
3941

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

42-
String email = oAuth2User.getAttribute("email");
44+
String provider = oAuth2User.getAttribute("provider");
45+
46+
47+
String email = provider + "_" + oAuth2User.getAttribute("email");
4348
Optional<Member> findMember = memberRepository.findByEmail(email);
4449

4550
// 회원이 아닌 경우에 회원 가입 진행
46-
47-
Long memberId = 0L;
48-
String role = "";
49-
51+
Member member = null;
5052
if (findMember.isEmpty()) {
51-
String name = oAuth2User.getAttribute("name");
53+
// KAKAO_user123
54+
String name = provider + "_" + oAuth2User.getAttribute("name");
5255
String picture = oAuth2User.getAttribute("picture");
5356

54-
Member member = Member.of(email, "", name, picture);
57+
member = Member.of(email, "", name, picture);
5558
memberRepository.save(member);
56-
57-
memberId = member.getId();
58-
role = member.getRole().getValue();
59+
} else {
60+
member = findMember.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
5961
}
6062

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

64-
// 쿠키 세팅
65-
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("tokenType", "Bearer"));
6666
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("accessToken", jwtToken.getAccessToken()));
6767
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("refreshToken", jwtToken.getRefreshToken()));
68-
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("memberId", String.valueOf(memberId)));
68+
response.addHeader(HttpHeaders.SET_COOKIE, createCookie("memberId", String.valueOf(member.getId())));
69+
70+
// TODO: 프론트 배포 주소로 변경 예정
6971
response.sendRedirect("http://localhost:5173/home");
7072
}
7173

@@ -74,8 +76,9 @@ private static String createCookie(String name, String value) {
7476
.domain("localhost")
7577
.path("/")
7678
.httpOnly(true)
77-
//.sameSite("None") // https 환경에서 활성화
78-
//.secure(false) // https 환경에서 활성화
79+
.maxAge(60 * 60 * 6)
80+
// .sameSite("None") https 시 활성화
81+
//.secure(true) https 시 활성화
7982
.build()
8083
.toString();
8184
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.api.trip.domain.notification.controller;
2+
3+
import com.api.trip.common.exception.CustomException;
4+
import com.api.trip.common.exception.ErrorCode;
5+
import com.api.trip.domain.member.repository.MemberRepository;
6+
import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse;
7+
import com.api.trip.domain.notification.emitter.SseEmitterMap;
8+
import com.api.trip.domain.notification.service.NotificationService;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.MediaType;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.core.context.SecurityContextHolder;
13+
import org.springframework.web.bind.annotation.*;
14+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
15+
16+
import java.time.LocalDateTime;
17+
18+
@RestController
19+
@RequestMapping("/api/notifications")
20+
@RequiredArgsConstructor
21+
public class NotificationController {
22+
23+
private final MemberRepository memberRepository;
24+
private final NotificationService notificationService;
25+
private final SseEmitterMap sseEmitterMap;
26+
27+
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
28+
public ResponseEntity<SseEmitter> connect() {
29+
String email = SecurityContextHolder.getContext().getAuthentication().getName();
30+
Long memberId = memberRepository.findByEmail(email)
31+
.orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED))
32+
.getId();
33+
34+
SseEmitter sseEmitter = new SseEmitter();
35+
sseEmitterMap.put(memberId, sseEmitter);
36+
sseEmitterMap.send(memberId, "connect", LocalDateTime.now());
37+
return ResponseEntity.ok(sseEmitter);
38+
}
39+
40+
@GetMapping("/send-to-all")
41+
public void sendToAll(@RequestParam String message) {
42+
String email = SecurityContextHolder.getContext().getAuthentication().getName();
43+
sseEmitterMap.sendToAll("send-to-all", email + ": " + message);
44+
}
45+
46+
@GetMapping("/me")
47+
public ResponseEntity<GetMyNotificationsResponse> getMyNotifications() {
48+
String email = SecurityContextHolder.getContext().getAuthentication().getName();
49+
return ResponseEntity.ok(notificationService.getMyNotifications(email));
50+
}
51+
52+
@PatchMapping("/{notificationId}")
53+
public ResponseEntity<Void> readNotification(@PathVariable Long notificationId) {
54+
String email = SecurityContextHolder.getContext().getAuthentication().getName();
55+
notificationService.readNotification(notificationId, email);
56+
return ResponseEntity.ok().build();
57+
}
58+
59+
@DeleteMapping("/{notificationId}")
60+
public ResponseEntity<Void> deleteNotification(@PathVariable Long notificationId) {
61+
String email = SecurityContextHolder.getContext().getAuthentication().getName();
62+
notificationService.deleteNotification(notificationId, email);
63+
return ResponseEntity.ok().build();
64+
}
65+
66+
@DeleteMapping("/me")
67+
public ResponseEntity<Void> deleteMyNotifications() {
68+
String email = SecurityContextHolder.getContext().getAuthentication().getName();
69+
notificationService.deleteMyNotifications(email);
70+
return ResponseEntity.ok().build();
71+
}
72+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.api.trip.domain.notification.controller.dto;
2+
3+
import com.api.trip.domain.itemtag.model.ItemTag;
4+
import com.api.trip.domain.notification.domain.Notification;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.stream.Collectors;
11+
12+
@Getter
13+
@Builder
14+
public class GetMyNotificationsResponse {
15+
16+
private List<NotificationResponse> notifications;
17+
private int unread;
18+
19+
public static GetMyNotificationsResponse of(List<Notification> notifications, List<ItemTag> itemTags) {
20+
Map<Long, List<ItemTag>> map = itemTags.stream()
21+
.collect(Collectors.groupingBy(itemTag -> itemTag.getItem().getId()));
22+
23+
List<NotificationResponse> notificationResponses = notifications.stream()
24+
.map(notification -> NotificationResponse.of(notification, map.get(notification.getItem().getId())))
25+
.toList();
26+
27+
int unread = (int) notifications.stream()
28+
.filter(notification -> !notification.isRead())
29+
.count();
30+
31+
return builder()
32+
.notifications(notificationResponses)
33+
.unread(unread)
34+
.build();
35+
}
36+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.api.trip.domain.notification.controller.dto;
2+
3+
import com.api.trip.domain.itemtag.model.ItemTag;
4+
import com.api.trip.domain.notification.domain.Notification;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
import java.time.LocalDateTime;
9+
import java.util.List;
10+
11+
@Getter
12+
@Builder
13+
public class NotificationResponse {
14+
15+
private Long notificationId;
16+
private Long itemId;
17+
private String itemTitle;
18+
private List<String> tags;
19+
private boolean read;
20+
private LocalDateTime createdAt;
21+
22+
public static NotificationResponse of(Notification notification, List<ItemTag> itemTags) {
23+
List<String> tagNames = itemTags.stream().map(itemTag -> itemTag.getTag().getName()).toList();
24+
return builder()
25+
.notificationId(notification.getId())
26+
.itemId(notification.getItem().getId())
27+
.itemTitle(notification.getItem().getTitle())
28+
.tags(tagNames)
29+
.read(notification.isRead())
30+
.createdAt(notification.getCreatedAt())
31+
.build();
32+
}
33+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.api.trip.domain.notification.domain;
2+
3+
import com.api.trip.common.auditing.entity.BaseTimeEntity;
4+
import com.api.trip.domain.item.model.Item;
5+
import com.api.trip.domain.member.model.Member;
6+
import jakarta.persistence.*;
7+
import lombok.AccessLevel;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
@Entity
13+
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "item_id"})})
14+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
15+
@Getter
16+
public class Notification extends BaseTimeEntity {
17+
18+
@Id
19+
@GeneratedValue(strategy = GenerationType.IDENTITY)
20+
private Long id;
21+
22+
@ManyToOne(fetch = FetchType.LAZY)
23+
private Member member;
24+
25+
@ManyToOne(fetch = FetchType.LAZY)
26+
private Item item;
27+
28+
@Column(nullable = false)
29+
private boolean read;
30+
31+
@Builder
32+
private Notification(Member member, Item item, boolean read) {
33+
this.member = member;
34+
this.item = item;
35+
this.read = read;
36+
}
37+
38+
public void read() {
39+
read = true;
40+
}
41+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.api.trip.domain.notification.emitter;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.stereotype.Component;
5+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
6+
7+
import java.io.IOException;
8+
import java.util.Map;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
11+
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;
12+
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;
13+
14+
@Component
15+
@Slf4j
16+
public class SseEmitterMap {
17+
18+
private final Map<Long, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
19+
20+
public void put(Long memberId, SseEmitter sseEmitter) {
21+
sseEmitter.onCompletion(() -> remove(memberId));
22+
sseEmitter.onTimeout(sseEmitter::complete);
23+
sseEmitterMap.put(memberId, sseEmitter);
24+
log.info("connected with {}, the number of connections is {}", memberId, sseEmitterMap.size());
25+
}
26+
27+
public void remove(Long memberId) {
28+
sseEmitterMap.remove(memberId);
29+
log.info("disconnected with {}, the number of connections is {}", memberId, sseEmitterMap.size());
30+
}
31+
32+
public void send(Long memberId, String eventName, Object eventData) {
33+
SseEmitter sseEmitter = sseEmitterMap.get(memberId);
34+
try {
35+
sseEmitter.send(
36+
event()
37+
.name(eventName)
38+
.data(eventData)
39+
);
40+
} catch (IOException | IllegalStateException e) {
41+
remove(memberId);
42+
}
43+
}
44+
45+
public void sendToAll(String eventName, Object eventData) {
46+
SseEventBuilder sseEventBuilder = event().name(eventName).data(eventData);
47+
sseEmitterMap.forEach((memberId, sseEmitter) -> {
48+
try {
49+
sseEmitter.send(sseEventBuilder);
50+
} catch (IOException | IllegalStateException e) {
51+
remove(memberId);
52+
}
53+
});
54+
}
55+
}

0 commit comments

Comments
 (0)