Skip to content

Commit 629782b

Browse files
authored
Merge pull request #185 from GoToBILL/feature/enhancement
refactor: 전체적인 로직 개선
2 parents 19b24af + 0611225 commit 629782b

24 files changed

+627
-121
lines changed

src/main/java/com/example/cherrydan/activity/service/ActivityAlertService.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.example.cherrydan.fcm.dto.NotificationRequest;
1212
import com.example.cherrydan.fcm.dto.NotificationResultDto;
1313
import com.example.cherrydan.fcm.service.NotificationService;
14+
import com.example.cherrydan.user.domain.User;
1415
import com.example.cherrydan.user.dto.AlertIdsRequestDTO;
1516
import com.example.cherrydan.user.repository.UserRepository;
1617
import lombok.RequiredArgsConstructor;
@@ -23,6 +24,7 @@
2324

2425
import java.time.LocalDate;
2526
import java.time.ZoneId;
27+
import java.util.ArrayList;
2628
import java.util.List;
2729
import java.util.Map;
2830
import java.util.Set;
@@ -100,7 +102,7 @@ public void sendActivityNotifications() {
100102
* @return 성공적으로 발송된 알림 개수
101103
*/
102104
private int processBatchNotifications(List<ActivityAlert> batch) {
103-
int successCount = 0;
105+
ArrayList<ActivityAlert> successfulAlerts = new ArrayList<>();
104106

105107
for (ActivityAlert alert : batch) {
106108
try {
@@ -125,8 +127,7 @@ private int processBatchNotifications(List<ActivityAlert> batch) {
125127
if (result.getSuccessCount() > 0) {
126128
// 성공 시 즉시 상태 업데이트
127129
alert.markAsNotified();
128-
activityAlertRepository.save(alert);
129-
successCount++;
130+
successfulAlerts.add(alert);
130131

131132
log.debug("알림 발송 성공: userId={}, alertType={}, campaignId={}",
132133
alert.getUser().getId(), alert.getAlertType(), alert.getCampaign().getId());
@@ -141,7 +142,11 @@ private int processBatchNotifications(List<ActivityAlert> batch) {
141142
}
142143
}
143144

144-
return successCount;
145+
if (!successfulAlerts.isEmpty()){
146+
activityAlertRepository.saveAll(successfulAlerts);
147+
}
148+
149+
return successfulAlerts.size();
145150
}
146151

147152
/**
@@ -171,7 +176,8 @@ public void deleteActivityAlert(Long userId, List<Long> alertIds) {
171176

172177
// 모든 알림이 해당 사용자의 것인지 확인
173178
for (ActivityAlert alert : alerts) {
174-
if (!alert.getUser().getId().equals(userId)) {
179+
User user = alert.getUser();
180+
if (user == null || !user.getId().equals(userId)) {
175181
throw new UserException(ErrorMessage.ACTIVITY_ALERT_ACCESS_DENIED);
176182
}
177183
alert.hide();

src/main/java/com/example/cherrydan/campaign/domain/Bookmark.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,8 @@ public class Bookmark extends BaseTimeEntity {
3737
public void setIsActive(Boolean isActive) {
3838
this.isActive = isActive;
3939
}
40-
}
40+
41+
public void activate() {
42+
this.isActive = true;
43+
}
44+
}

src/main/java/com/example/cherrydan/campaign/domain/CampaignStatus.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,12 @@ public class CampaignStatus extends BaseTimeEntity {
6363
@Builder.Default
6464
@Column(name = "is_visible_to_user", nullable = false)
6565
private Boolean isVisibleToUser = true;
66+
67+
public void activate(){
68+
this.isActive = true;
69+
}
70+
71+
public void updateStatus(CampaignStatusType newStatus){
72+
this.status = newStatus;
73+
}
6674
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.example.cherrydan.campaign.scheduler;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.jdbc.core.JdbcTemplate;
6+
import org.springframework.scheduling.annotation.Scheduled;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.time.LocalDate;
10+
import java.time.LocalDateTime;
11+
import java.time.ZoneId;
12+
13+
@Component
14+
@RequiredArgsConstructor
15+
@Slf4j
16+
public class CampaignSearchSyncScheduler {
17+
18+
private final JdbcTemplate jdbcTemplate;
19+
20+
@Scheduled(cron = "0 30 6 * * ?", zone = "Asia/Seoul")
21+
public void syncDailySearchTable() {
22+
try {
23+
LocalDate yesterday = LocalDate.now(ZoneId.of("Asia/Seoul")).minusDays(1);
24+
25+
log.info("campaigns_daily_search 동기화 시작 - 날짜: {}", yesterday);
26+
27+
// 1. TRUNCATE
28+
jdbcTemplate.execute("TRUNCATE TABLE campaigns_daily_search");
29+
30+
// 2. INSERT ... SELECT (한 방 쿼리, 인덱스 활용)
31+
String sql = """
32+
INSERT INTO campaigns_daily_search (id, title, created_at)
33+
SELECT id, title, created_at
34+
FROM campaigns
35+
WHERE is_active = 1
36+
AND created_at >= ?
37+
AND created_at < ?
38+
""";
39+
40+
LocalDateTime startOfDay = yesterday.atStartOfDay();
41+
LocalDateTime startOfNextDay = yesterday.plusDays(1).atStartOfDay();
42+
43+
int count = jdbcTemplate.update(sql, startOfDay, startOfNextDay);
44+
45+
log.info("campaigns_daily_search 동기화 완료 - 날짜: {}, 건수: {}", yesterday, count);
46+
47+
} catch (Exception e) {
48+
log.error("campaigns_daily_search 동기화 실패: {}", e.getMessage(), e);
49+
}
50+
}
51+
}

src/main/java/com/example/cherrydan/campaign/service/BookmarkServiceImpl.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,16 @@ public void addBookmark(Long userId, Long campaignId) {
4444
Campaign campaign = campaignRepository.findById(campaignId)
4545
.orElseThrow(() -> new BaseException(ErrorMessage.RESOURCE_NOT_FOUND));
4646
Optional<Bookmark> optionalBookmark = bookmarkRepository.findByUserAndCampaign(user, campaign);
47-
if (optionalBookmark.isPresent()) {
48-
Bookmark bookmark = optionalBookmark.get();
49-
bookmark.setIsActive(true);
50-
bookmarkRepository.save(bookmark);
51-
} else {
52-
Bookmark bookmark = Bookmark.builder()
53-
.user(user)
54-
.campaign(campaign)
55-
.isActive(true)
56-
.build();
57-
bookmarkRepository.save(bookmark);
58-
}
47+
48+
Bookmark bookmark = optionalBookmark
49+
.orElseGet(() -> Bookmark.builder()
50+
.user(user)
51+
.campaign(campaign)
52+
.isActive(false)
53+
.build());
54+
55+
bookmark.activate();
56+
bookmarkRepository.save(bookmark);
5957
}
6058

6159
@Override

src/main/java/com/example/cherrydan/campaign/service/CampaignService.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,4 @@ PageListResponseDTO<CampaignResponseDTO> getCampaignsByProduct(
5454
Pageable pageable,
5555
Long userId
5656
);
57-
58-
// campaigns_daily_search 테이블 사용 (향후 확장용)
59-
Page<CampaignResponseDTO> searchDailyCampaignsByFulltext(String keyword, Pageable pageable, Long userId);
6057
}

src/main/java/com/example/cherrydan/campaign/service/CampaignServiceImpl.java

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springframework.data.domain.*;
1313
import org.springframework.stereotype.Service;
1414
import java.time.LocalDate;
15+
import java.time.ZoneId;
1516
import java.util.List;
1617
import java.util.Set;
1718
import java.util.stream.Collectors;
@@ -152,16 +153,32 @@ public PageListResponseDTO<CampaignResponseDTO> searchByKeyword(String keyword,
152153
@Override
153154
public Page<CampaignResponseDTO> getPersonalizedCampaignsByKeyword(String keyword, LocalDate date, Long userId, Pageable pageable) {
154155

155-
// Simple LIKE 검색으로 변경
156156
String searchKeyword = keyword.trim();
157-
List<Campaign> campaigns = campaignRepository.findByKeywordSimpleLike(
158-
searchKeyword,
159-
date,
160-
(int) pageable.getOffset(),
161-
pageable.getPageSize()
162-
);
157+
LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul"));
163158

164-
long totalElements = campaignRepository.countByKeywordSimpleLike(searchKeyword, date);
159+
List<Campaign> campaigns;
160+
long totalElements;
161+
162+
if (date.equals(today)) {
163+
// alertDate가 오늘이면 = 전날 캠페인 조회
164+
// FULLTEXT 검색 (campaigns_daily_search 테이블, 빠름)
165+
campaigns = campaignRepository.searchDailyCampaignsByFulltext(
166+
"+" + searchKeyword + "*",
167+
(int) pageable.getOffset(),
168+
pageable.getPageSize()
169+
);
170+
totalElements = campaignRepository.countDailyCampaignsByFulltext(searchKeyword);
171+
} else {
172+
// alertDate가 과거이면 = 그 날짜 기준 전날 캠페인 조회
173+
// Simple LIKE 검색 (campaigns 테이블, 폴백)
174+
campaigns = campaignRepository.findByKeywordSimpleLike(
175+
searchKeyword,
176+
date,
177+
(int) pageable.getOffset(),
178+
pageable.getPageSize()
179+
);
180+
totalElements = campaignRepository.countByKeywordSimpleLike(searchKeyword, date);
181+
}
165182

166183
// N+1 문제 해결: 벌크 조회로 북마크 여부 확인
167184
List<Long> campaignIds = campaigns.stream()
@@ -185,34 +202,8 @@ public Page<CampaignResponseDTO> getPersonalizedCampaignsByKeyword(String keywor
185202

186203
@Override
187204
public long getDailyCampaignCountByKeyword(String keyword, LocalDate date) {
188-
return campaignRepository.countByKeywordSimpleLike(keyword.trim(), date);
189-
}
190-
191-
@Override
192-
@Transactional
193-
public Page<CampaignResponseDTO> searchDailyCampaignsByFulltext(String keyword, Pageable pageable, Long userId) {
194-
List<Campaign> campaigns = campaignRepository.searchDailyCampaignsByFulltext(
195-
"+" + keyword.trim() + "*",
196-
(int) pageable.getOffset(),
197-
pageable.getPageSize()
198-
);
199-
200-
long totalElements = campaignRepository.countDailyCampaignsByFulltext(keyword.trim());
201-
202-
List<Long> campaignIds = campaigns.stream()
203-
.map(Campaign::getId)
204-
.collect(Collectors.toList());
205-
206-
Set<Long> bookmarkedCampaignIds = bookmarkRepository.findBookmarkedCampaignIds(userId, campaignIds);
207-
208-
List<CampaignResponseDTO> content = campaigns.stream()
209-
.map(campaign -> {
210-
boolean isBookmarked = bookmarkedCampaignIds.contains(campaign.getId());
211-
return CampaignResponseDTO.fromEntityWithBookmark(campaign, isBookmarked);
212-
})
213-
.collect(Collectors.toList());
214-
215-
return new PageImpl<>(content, pageable, totalElements);
205+
// 이 메서드는 processKeywordAsync에서만 호출되며, 항상 전날 데이터를 조회
206+
return campaignRepository.countDailyCampaignsByFulltext("+" + keyword.trim() + "*");
216207
}
217208

218209
@Override

src/main/java/com/example/cherrydan/campaign/service/CampaignStatusServiceImpl.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,15 @@ public CampaignStatusResponseDTO createOrRecoverStatus(CampaignStatusRequestDTO
4747

4848
Optional<CampaignStatus> optional = campaignStatusRepository.findByUserAndCampaign(user, campaign);
4949

50-
CampaignStatus status;
51-
if (optional.isPresent()) {
52-
status = optional.get();
53-
status.setIsActive(true);
54-
status.setStatus(requestDTO.getStatus());
55-
} else {
56-
status = CampaignStatus.builder()
57-
.user(user)
58-
.campaign(campaign)
59-
.status(requestDTO.getStatus())
60-
.isActive(true)
61-
.build();
62-
}
50+
CampaignStatus status = optional.orElseGet(() -> CampaignStatus.builder()
51+
.user(user)
52+
.campaign(campaign)
53+
.isActive(false)
54+
.build());
55+
56+
status.activate();
57+
status.updateStatus(requestDTO.getStatus());
58+
6359
CampaignStatus saved = campaignStatusRepository.save(status);
6460
return CampaignStatusResponseDTO.fromEntity(saved);
6561
}

src/main/java/com/example/cherrydan/common/exception/ErrorMessage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ public enum ErrorMessage {
104104
SNS_USER_INFO_ACQUISITION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "SNS 사용자 정보 획득에 실패했습니다."),
105105
SNS_CONNECTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "SNS 연동에 실패했습니다."),
106106
SNS_TOKEN_REFRESH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "SNS 토큰 갱신에 실패했습니다."),
107+
SNS_CODE_CHALLENGE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "Code challenge 생성에 실패했습니다."),
108+
107109

108110
// 문의 관련 에러
109111
INQUIRY_NOT_FOUND(NOT_FOUND, "문의 정보를 찾을 수 없습니다."),

src/main/java/com/example/cherrydan/fcm/service/FCMTokenService.java

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010
import com.example.cherrydan.fcm.repository.UserFCMTokenRepository;
1111
import lombok.RequiredArgsConstructor;
1212
import lombok.extern.slf4j.Slf4j;
13-
import org.springframework.scheduling.annotation.Scheduled;
1413
import org.springframework.stereotype.Service;
1514
import org.springframework.transaction.annotation.Transactional;
1615

17-
import java.time.LocalDateTime;
1816
import java.util.List;
1917
import java.util.Optional;
2018
import java.util.stream.Collectors;
@@ -111,27 +109,22 @@ public void registerOrUpdateToken(FCMTokenRequest request) {
111109
Optional<UserFCMToken> existingToken = tokenRepository
112110
.findByUserIdAndDeviceModelAndIsActiveTrue(request.getUserId(), request.getDeviceModel());
113111

114-
UserFCMToken token;
115-
if (existingToken.isPresent()) {
116-
token = existingToken.get();
117-
token.updateToken(request);
118-
token.activate();
119-
log.info("FCM 토큰 업데이트 - 사용자: {}, 디바이스: {}", request.getUserId(), request.getDeviceModel());
120-
} else {
121-
token = UserFCMToken.builder()
122-
.userId(request.getUserId())
123-
.fcmToken(request.getFcmToken())
124-
.isActive(true)
125-
.isAllowed(request.getIsAllowed() != null ? request.getIsAllowed() : true)
126-
.deviceType(DeviceType.from(request.getDeviceType()))
127-
.deviceModel(request.getDeviceModel())
128-
.appVersion(request.getAppVersion())
129-
.osVersion(request.getOsVersion())
130-
.build();
131-
tokenRepository.save(token);
132-
log.info("새 FCM 토큰 등록 - 사용자: {}, 디바이스: {}", request.getUserId(), request.getDeviceModel());
112+
UserFCMToken userFCMToken = existingToken
113+
.orElseGet(() -> createInactiveFCMToken(request));
114+
115+
boolean isNewToken = existingToken.isEmpty();
116+
if (!isNewToken) {
117+
userFCMToken.updateToken(request);
133118
}
134119

120+
userFCMToken.activate();
121+
tokenRepository.save(userFCMToken);
122+
123+
log.info("{} FCM 토큰 - 사용자: {}, 디바이스: {}",
124+
isNewToken ? "새" : "업데이트",
125+
request.getUserId(),
126+
request.getDeviceModel());
127+
135128
} catch (IllegalArgumentException e) {
136129
log.error("잘못된 디바이스 타입: {}", request.getDeviceType());
137130
} catch (FCMException e) {
@@ -141,6 +134,22 @@ public void registerOrUpdateToken(FCMTokenRequest request) {
141134
}
142135
}
143136

137+
/**
138+
* 비활성 상태의 FCM 토큰 생성
139+
*/
140+
private UserFCMToken createInactiveFCMToken(FCMTokenRequest request) {
141+
return UserFCMToken.builder()
142+
.userId(request.getUserId())
143+
.fcmToken(request.getFcmToken())
144+
.isActive(false)
145+
.isAllowed(request.getIsAllowed() != null ? request.getIsAllowed() : true)
146+
.deviceType(DeviceType.from(request.getDeviceType()))
147+
.deviceModel(request.getDeviceModel())
148+
.appVersion(request.getAppVersion())
149+
.osVersion(request.getOsVersion())
150+
.build();
151+
}
152+
144153
/**
145154
* 사용자의 모든 FCM 토큰 비활성화 (소프트 삭제)
146155
* 사용자 탈퇴 시 호출

0 commit comments

Comments
 (0)