Skip to content

Commit b1dc5f7

Browse files
committed
Feat: 로그인에 AOP적용
1 parent d7ea389 commit b1dc5f7

22 files changed

Lines changed: 420 additions & 23 deletions

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ dependencies {
101101
implementation("org.yaml:snakeyaml:2.0")
102102
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
103103

104+
// AOP
105+
implementation("org.springframework.boot:spring-boot-starter-aop")
106+
104107
}
105108

106109
tasks.withType<Test> {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.com.stocknote.domain.notification.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.com.stocknote.domain.notification.dto.CommentNotificationResponse;
5+
import org.com.stocknote.domain.notification.service.NotificationService;
6+
import org.com.stocknote.oauth.entity.PrincipalDetails;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
import java.util.List;
12+
13+
@RestController
14+
@RequestMapping("/notifications")
15+
@RequiredArgsConstructor
16+
public class NotificationController {
17+
private final NotificationService notificationService;
18+
19+
@GetMapping("/user")
20+
public List<CommentNotificationResponse> getNotificationsByMember(
21+
@AuthenticationPrincipal PrincipalDetails principalDetails
22+
) {
23+
Long memberId = principalDetails.user().getId();
24+
return notificationService.getNotificationsByMember(memberId);
25+
}
26+
27+
@PatchMapping("/{notificationId}/read")
28+
public ResponseEntity<Void> markAsRead(@PathVariable Long notificationId) {
29+
notificationService.markAsRead(notificationId);
30+
return ResponseEntity.ok().build();
31+
}
32+
}

src/main/java/org/com/stocknote/domain/notification/service/KeywordNotificationElasticService.java

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

33
import lombok.RequiredArgsConstructor;
44

5-
import org.com.stocknote.domain.keyword.repository.KeywordRepository;
65
import org.com.stocknote.domain.notification.dto.KeywordNotificationResponse;
76
import org.com.stocknote.domain.notification.entity.KeywordNotification;
87
import org.com.stocknote.domain.notification.repository.KeywordNotificationRepository;
98
import org.com.stocknote.domain.searchDoc.document.KeywordDoc;
109
import org.com.stocknote.domain.searchDoc.document.PostDoc;
1110
import org.com.stocknote.domain.searchDoc.repository.KeywordDocRepository;
12-
import org.com.stocknote.domain.searchDoc.repository.PostDocRepository;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1312
import org.springframework.stereotype.Service;
1413
import org.springframework.transaction.annotation.Transactional;
1514

@@ -18,6 +17,7 @@
1817
@Service
1918
@Transactional
2019
@RequiredArgsConstructor
20+
@ConditionalOnProperty(name = "elasticsearch.enabled", havingValue = "true")
2121
public class KeywordNotificationElasticService {
2222
private final KeywordDocRepository keywordDocRepository;
2323
private final KeywordNotificationRepository keywordNotificationRepository;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.com.stocknote.domain.notification.service;
2+
3+
import jakarta.transaction.Transactional;
4+
import lombok.RequiredArgsConstructor;
5+
import org.com.stocknote.domain.post.entity.Post;
6+
import org.com.stocknote.domain.comment.entity.Comment;
7+
import org.com.stocknote.domain.notification.dto.CommentNotificationResponse;
8+
import org.com.stocknote.domain.notification.entity.CommentNotification;
9+
import org.com.stocknote.domain.notification.repository.CommentNotificationRepository;
10+
import org.com.stocknote.domain.post.repository.PostRepository;
11+
import org.com.stocknote.global.error.ErrorCode;
12+
import org.com.stocknote.global.exception.CustomException;
13+
import org.springframework.stereotype.Service;
14+
15+
import java.time.LocalDateTime;
16+
import java.util.List;
17+
import java.util.stream.Collectors;
18+
19+
20+
@Service
21+
@RequiredArgsConstructor
22+
@Transactional
23+
public class NotificationService {
24+
private final CommentNotificationRepository commentNotificationRepository;
25+
private final PostRepository postRepository;
26+
private final SseEmitterService sseEmitterService;
27+
28+
public void createCommentNotification(Long postId, Comment comment) {
29+
Post post = postRepository.findById(postId).orElseThrow();
30+
// 게시글 작성자가 댓글 작성자와 다를 경우에만 알림 생성
31+
if (!post.getMember().equals(comment.getMember())) {
32+
CommentNotification commentNotification = CommentNotification.builder()
33+
.memberId(post.getMember().getId())
34+
.relatedPostId(post.getId())
35+
.relatedCommentId(comment.getId())
36+
.isRead(false)
37+
.content(comment.getMember().getName() + "님이 댓글을 남겼습니다.")
38+
.build();
39+
40+
commentNotificationRepository.save(commentNotification);
41+
42+
// SSE로 실시간 알림 전송
43+
sseEmitterService.sendNotification(
44+
post.getMember().getId().toString(),
45+
CommentNotificationResponse.from(commentNotification)
46+
);
47+
}
48+
}
49+
50+
public List<CommentNotificationResponse> getNotificationsByMember(Long memberId) {
51+
LocalDateTime startDate = LocalDateTime.now().minusDays(30);
52+
return commentNotificationRepository.findByMemberIdAndIsReadFalseAndCreatedAtAfterOrderByCreatedAtDesc(
53+
memberId,
54+
startDate
55+
).stream()
56+
.map(notification -> CommentNotificationResponse.from(notification))
57+
// 또는 메서드 레퍼런스를 사용: .map(NotificationResponse::from)
58+
.collect(Collectors.toList());
59+
}
60+
61+
public void markAsRead(Long notificationId) {
62+
CommentNotification commentNotification = commentNotificationRepository.findById(notificationId)
63+
.orElseThrow(() -> new CustomException(ErrorCode.ENTITY_NOT_FOUND));
64+
commentNotification.markAsRead();
65+
}
66+
67+
}

src/main/java/org/com/stocknote/domain/notification/service/SseEmitterService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.com.stocknote.domain.notification.service;
22

3+
import org.com.stocknote.domain.notification.dto.CommentNotificationResponse;
34
import org.springframework.stereotype.Service;
45
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
56

@@ -52,4 +53,7 @@ public void sendKeywordNotification(String memberId, Object data) {
5253
}
5354
}
5455
}
56+
57+
public void sendNotification (String string, CommentNotificationResponse from) {
58+
}
5559
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.com.stocknote.domain.portfolio.portfolioStock.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.com.stocknote.domain.stockApi.dto.response.SectorResponse;
6+
import org.com.stocknote.domain.stockApi.stockToken.service.StockTokenService;
7+
import org.springframework.http.*;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.web.client.HttpStatusCodeException;
10+
import org.springframework.web.client.RestTemplate;
11+
import org.springframework.web.util.UriComponentsBuilder;
12+
13+
@Service
14+
@Slf4j
15+
@RequiredArgsConstructor
16+
public class TempStockInfoService {
17+
private final StockTokenService stockTokenService;
18+
private final RestTemplate restTemplate;
19+
20+
public SectorResponse getStockInfo(String pdno) {
21+
log.info("Getting stock info for PDNO: {}", pdno);
22+
String baseUrl = "https://openapi.koreainvestment.com:9443";
23+
String endpoint = "/uapi/domestic-stock/v1/quotations/search-stock-info";
24+
25+
// Build URL with query parameters
26+
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + endpoint)
27+
.queryParam("PDNO", pdno)
28+
.queryParam("PRDT_TYPE_CD", "300")
29+
.toUriString();
30+
31+
// Set up headers
32+
HttpHeaders headers = new HttpHeaders();
33+
headers.setContentType(MediaType.APPLICATION_JSON);
34+
headers.set("authorization", "Bearer " + stockTokenService.getAccessToken());
35+
headers.set("appkey", stockTokenService.getAppKey());
36+
headers.set("appsecret", stockTokenService.getAppSecret());
37+
headers.set("tr_id", "CTPF1002R");
38+
headers.set("custtype", "P");
39+
40+
HttpEntity<?> entity = new HttpEntity<>(headers);
41+
42+
try {
43+
log.info("Requesting stock info for PDNO: {}", pdno);
44+
ResponseEntity<SectorResponse> response = restTemplate.exchange(
45+
url,
46+
HttpMethod.GET,
47+
entity,
48+
SectorResponse.class
49+
);
50+
51+
if (response.getStatusCode() == HttpStatus.OK) {
52+
log.debug("Successfully retrieved stock info for PDNO: {}", pdno);
53+
log.debug("Stock info: {}", response.getBody().toString());
54+
return response.getBody();
55+
} else {
56+
log.error("Failed to get stock info. Status code: {}", response.getStatusCode());
57+
throw new RuntimeException("Failed to get stock info. Status code: " + response.getStatusCode());
58+
}
59+
} catch (HttpStatusCodeException e) {
60+
log.error("Stock info lookup failed: {}", e.getResponseBodyAsString(), e);
61+
throw new RuntimeException("주식 정보 조회 실패: " + e.getResponseBodyAsString(), e);
62+
} catch (Exception e) {
63+
log.error("Error occurred while getting stock info", e);
64+
throw new RuntimeException("주식 정보 조회 중 오류 발생", e);
65+
}
66+
}
67+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.com.stocknote.domain.portfolio.portfolioStock.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.com.stocknote.domain.stockApi.dto.response.StockPriceResponse;
6+
import org.com.stocknote.domain.stockApi.stockToken.service.StockTokenService;
7+
import org.springframework.http.*;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.web.client.HttpStatusCodeException;
10+
import org.springframework.web.client.RestTemplate;
11+
import org.springframework.web.util.UriComponentsBuilder;
12+
13+
@Service
14+
@Slf4j
15+
@RequiredArgsConstructor
16+
public class TempStockService {
17+
private final StockTokenService stockTokenService;
18+
private final RestTemplate restTemplate;
19+
20+
public StockPriceResponse getStockPrice(String stockCode) {
21+
String baseUrl = "https://openapivts.koreainvestment.com:29443"; // Change to production URL if needed
22+
String endpoint = "/uapi/domestic-stock/v1/quotations/inquire-price";
23+
24+
// Build URL with query parameters
25+
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + endpoint)
26+
.queryParam("FID_COND_MRKT_DIV_CODE", "J")
27+
.queryParam("FID_INPUT_ISCD", stockCode)
28+
.toUriString();
29+
30+
// Set up headers
31+
HttpHeaders headers = new HttpHeaders();
32+
headers.setContentType(MediaType.APPLICATION_JSON);
33+
headers.set("authorization", "Bearer " + stockTokenService.getAccessToken());
34+
headers.set("appkey", stockTokenService.getAppKey());
35+
headers.set("appsecret", stockTokenService.getAppSecret());
36+
headers.set("tr_id", "FHKST01010100");
37+
38+
HttpEntity<?> entity = new HttpEntity<>(headers);
39+
40+
try {
41+
ResponseEntity<StockPriceResponse> response = restTemplate.exchange(
42+
url,
43+
HttpMethod.GET,
44+
entity,
45+
StockPriceResponse.class
46+
);
47+
48+
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
49+
return response.getBody();
50+
} else {
51+
throw new RuntimeException("Failed to get stock price. Status code: " + response.getStatusCode());
52+
}
53+
} catch (HttpStatusCodeException e) {
54+
throw new RuntimeException("주식 가격 조회 실패: " + e.getResponseBodyAsString(), e);
55+
} catch (Exception e) {
56+
throw new RuntimeException("주식 가격 조회 중 오류 발생", e);
57+
}
58+
}
59+
}

src/main/java/org/com/stocknote/domain/post/controller/PostController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.com.stocknote.domain.searchDoc.service.SearchDocService;
1818
import org.com.stocknote.global.globalDto.GlobalResponse;
1919
import org.com.stocknote.oauth.entity.PrincipalDetails;
20+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2021
import org.springframework.data.domain.Page;
2122
import org.springframework.data.domain.Pageable;
2223
import org.springframework.data.domain.Sort;
@@ -29,6 +30,7 @@
2930
@RequestMapping("/api/v1/posts")
3031
@RequiredArgsConstructor
3132
@Tag(name = "커뮤니티 게시글 API", description = "게시글(Post)")
33+
@ConditionalOnProperty(name = "elasticsearch.enabled", havingValue = "true")
3234
public class PostController {
3335

3436
private final PostService postService;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.com.stocknote.domain.post.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
import org.com.stocknote.domain.member.entity.Member;
7+
import org.com.stocknote.domain.post.entity.Post;
8+
import org.com.stocknote.domain.post.entity.PostCategory;
9+
10+
import java.util.List;
11+
12+
@Getter
13+
@Setter
14+
public class PostCreateDto {
15+
@NotBlank(message = "Title is required")
16+
private String title;
17+
18+
@NotBlank(message = "Content is required")
19+
private String content;
20+
21+
private List<String> hashtags;
22+
23+
private String category;
24+
25+
public Post toEntity(Member member) {
26+
PostCategory postCategory = PostCategory.valueOf(category);
27+
return Post.builder()
28+
.member(member)
29+
.title(this.title)
30+
.body(this.content)
31+
.category(postCategory)
32+
.build();
33+
}
34+
}

src/main/java/org/com/stocknote/domain/searchDoc/controller/SearchDocController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.com.stocknote.domain.searchDoc.service.SearchDocService;
1616
import org.com.stocknote.global.globalDto.GlobalResponse;
1717
import org.com.stocknote.oauth.entity.PrincipalDetails;
18+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1819
import org.springframework.data.domain.Page;
1920
import org.springframework.data.domain.Pageable;
2021
import org.springframework.data.domain.Sort;
@@ -29,6 +30,7 @@
2930
@RequiredArgsConstructor
3031
@RequestMapping("/api/v1/searchDocs")
3132
@Tag(name = "검색 API", description = "검색(Search)")
33+
@ConditionalOnProperty(name = "elasticsearch.enabled", havingValue = "true")
3234
public class SearchDocController {
3335
private final SearchDocService searchDocService;
3436

0 commit comments

Comments
 (0)