184 lines
7.4 KiB
Java
184 lines
7.4 KiB
Java
package com.goldenchart.service;
|
|
|
|
import com.goldenchart.entity.GcFcmToken;
|
|
import com.goldenchart.repository.GcFcmTokenRepository;
|
|
import com.google.firebase.FirebaseApp;
|
|
import com.google.firebase.messaging.AndroidConfig;
|
|
import com.google.firebase.messaging.AndroidNotification;
|
|
import com.google.firebase.messaging.FirebaseMessaging;
|
|
import com.google.firebase.messaging.FirebaseMessagingException;
|
|
import com.google.firebase.messaging.Message;
|
|
import com.google.firebase.messaging.MessagingErrorCode;
|
|
import com.google.firebase.messaging.Notification;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class FcmPushService {
|
|
|
|
/** Capacitor Android 앱 알림 채널 (app/android strings.xml 과 동일) */
|
|
private static final String ANDROID_CHANNEL_ID = "goldenchart_trade_signals";
|
|
|
|
private final GcFcmTokenRepository tokenRepo;
|
|
private final AppSettingsService appSettingsService;
|
|
|
|
@Value("${firebase.enabled:false}")
|
|
private boolean firebaseEnabled;
|
|
|
|
@Transactional
|
|
public void registerToken(Long userId, String deviceId, String token) {
|
|
if (token == null || token.isBlank()) return;
|
|
String dev = deviceId != null ? deviceId : "anonymous";
|
|
Optional<GcFcmToken> existing = tokenRepo.findByToken(token);
|
|
if (existing.isPresent()) {
|
|
GcFcmToken t = existing.get();
|
|
t.setUserId(userId);
|
|
t.setDeviceId(dev);
|
|
t.setActive(true);
|
|
t.setLastUsedAt(LocalDateTime.now());
|
|
tokenRepo.save(t);
|
|
} else {
|
|
tokenRepo.save(GcFcmToken.builder()
|
|
.token(token)
|
|
.userId(userId)
|
|
.deviceId(dev)
|
|
.active(true)
|
|
.lastUsedAt(LocalDateTime.now())
|
|
.build());
|
|
}
|
|
log.info("[FCM] token registered device={}", dev);
|
|
}
|
|
|
|
@Transactional
|
|
public void deleteByDevice(String deviceId) {
|
|
if (deviceId != null) tokenRepo.deleteByDeviceId(deviceId);
|
|
}
|
|
|
|
public boolean isAvailable() {
|
|
return firebaseEnabled && !FirebaseApp.getApps().isEmpty();
|
|
}
|
|
|
|
/**
|
|
* 전략 조건 일치(매매 시그널) 시 FCM 푸시 — fcm_push_enabled + 등록된 토큰 대상.
|
|
*/
|
|
public void sendTradeSignalIfEnabled(String deviceId, Long userId, String market,
|
|
String signalType, double price,
|
|
String strategyName, Long signalId) {
|
|
if (!isAvailable()) {
|
|
log.debug("[FCM] skip trade_signal {} {} — Firebase unavailable", market, signalType);
|
|
return;
|
|
}
|
|
|
|
var app = appSettingsService.getEntity(userId, deviceId);
|
|
if (!Boolean.TRUE.equals(app.getFcmPushEnabled())) {
|
|
log.debug("[FCM] skip trade_signal {} — fcm_push_enabled=false userId={} deviceId={}",
|
|
market, userId, deviceId);
|
|
return;
|
|
}
|
|
|
|
List<GcFcmToken> tokens = resolveTokens(userId, deviceId);
|
|
if (tokens.isEmpty()) {
|
|
log.info("[FCM] no active tokens for trade_signal market={} userId={} deviceId={}",
|
|
market, userId, deviceId);
|
|
return;
|
|
}
|
|
|
|
String title = "BUY".equals(signalType) ? "🟢 매수 시그널" : "🔴 매도 시그널";
|
|
String body = String.format("%s · ₩%,.0f%s",
|
|
market, price,
|
|
strategyName != null && !strategyName.isBlank() ? " · " + strategyName : "");
|
|
|
|
int sent = 0;
|
|
for (GcFcmToken t : tokens) {
|
|
if (sendTradeSignalMessage(t, title, body, market, signalType, price, signalId)) {
|
|
sent++;
|
|
}
|
|
}
|
|
log.info("[FCM] trade_signal sent {}/{} tokens market={} {}", sent, tokens.size(), market, signalType);
|
|
}
|
|
|
|
private List<GcFcmToken> resolveTokens(Long userId, String deviceId) {
|
|
if (userId != null) {
|
|
List<GcFcmToken> byUser = tokenRepo.findByUserIdAndActiveTrue(userId);
|
|
if (!byUser.isEmpty()) return byUser;
|
|
}
|
|
if (deviceId != null && !deviceId.isBlank()) {
|
|
return tokenRepo.findByDeviceIdAndActiveTrue(deviceId);
|
|
}
|
|
return List.of();
|
|
}
|
|
|
|
private boolean sendTradeSignalMessage(GcFcmToken t, String title, String body,
|
|
String market, String signalType,
|
|
double price, Long signalId) {
|
|
try {
|
|
Message msg = Message.builder()
|
|
.setToken(t.getToken())
|
|
.setNotification(Notification.builder().setTitle(title).setBody(body).build())
|
|
.setAndroidConfig(AndroidConfig.builder()
|
|
.setPriority(AndroidConfig.Priority.HIGH)
|
|
.setNotification(AndroidNotification.builder()
|
|
.setChannelId(ANDROID_CHANNEL_ID)
|
|
.build())
|
|
.build())
|
|
.putData("type", "trade_signal")
|
|
.putData("signalId", signalId != null ? String.valueOf(signalId) : "")
|
|
.putData("market", market != null ? market : "")
|
|
.putData("signalType", signalType != null ? signalType : "")
|
|
.putData("price", String.valueOf(price))
|
|
.build();
|
|
FirebaseMessaging.getInstance().send(msg);
|
|
t.setLastUsedAt(LocalDateTime.now());
|
|
tokenRepo.save(t);
|
|
return true;
|
|
} catch (FirebaseMessagingException e) {
|
|
log.warn("[FCM] send failed tokenId={} code={}", t.getId(), e.getMessagingErrorCode());
|
|
if (e.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED
|
|
|| e.getMessagingErrorCode() == MessagingErrorCode.INVALID_ARGUMENT) {
|
|
t.setActive(false);
|
|
tokenRepo.save(t);
|
|
}
|
|
return false;
|
|
} catch (Exception e) {
|
|
log.warn("[FCM] send error: {}", e.getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void sendTest(Long userId, String deviceId) {
|
|
if (!isAvailable()) {
|
|
log.warn("[FCM] test skipped — Firebase not initialized");
|
|
return;
|
|
}
|
|
List<GcFcmToken> tokens = resolveTokens(userId, deviceId);
|
|
if (tokens.isEmpty()) {
|
|
log.warn("[FCM] test skipped — no tokens userId={} deviceId={}", userId, deviceId);
|
|
return;
|
|
}
|
|
for (GcFcmToken t : tokens) {
|
|
try {
|
|
Message msg = Message.builder()
|
|
.setToken(t.getToken())
|
|
.setNotification(Notification.builder()
|
|
.setTitle("GoldenChart 테스트")
|
|
.setBody("FCM 푸시가 정상 동작합니다.")
|
|
.build())
|
|
.putData("type", "test")
|
|
.build();
|
|
FirebaseMessaging.getInstance().send(msg);
|
|
} catch (Exception e) {
|
|
log.warn("[FCM] test send failed: {}", e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
}
|