검증게시판 단계 추가
This commit is contained in:
@@ -3,7 +3,13 @@ package com.goldenchart.service;
|
||||
import com.goldenchart.entity.GcFcmToken;
|
||||
import com.goldenchart.repository.GcFcmTokenRepository;
|
||||
import com.google.firebase.FirebaseApp;
|
||||
import com.google.firebase.messaging.*;
|
||||
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;
|
||||
@@ -19,6 +25,9 @@ import java.util.Optional;
|
||||
@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;
|
||||
|
||||
@@ -59,51 +68,90 @@ public class FcmPushService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 매매 시그널 FCM — deviceId 일치 토큰 + fcm_push_enabled 설정 시.
|
||||
* 전략 조건 일치(매매 시그널) 시 FCM 푸시 — fcm_push_enabled + 등록된 토큰 대상.
|
||||
*/
|
||||
public void sendTradeSignalIfEnabled(String deviceId, Long userId, String market,
|
||||
String signalType, double price,
|
||||
String strategyName, Long signalId) {
|
||||
if (!isAvailable() || deviceId == null) return;
|
||||
var app = appSettingsService.getEntity(userId, deviceId);
|
||||
if (!Boolean.TRUE.equals(app.getFcmPushEnabled())) return;
|
||||
|
||||
List<GcFcmToken> tokens = tokenRepo.findByDeviceIdAndActiveTrue(deviceId);
|
||||
if (tokens.isEmpty()) {
|
||||
tokens = tokenRepo.findByActiveTrue();
|
||||
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;
|
||||
}
|
||||
if (tokens.isEmpty()) return;
|
||||
|
||||
String title = "BUY".equals(signalType) ? "🟢 매수 시그널" : "🔴 매도 시그널";
|
||||
String body = String.format("%s · ₩%,.0f%s",
|
||||
market, price,
|
||||
strategyName != null ? " · " + strategyName : "");
|
||||
strategyName != null && !strategyName.isBlank() ? " · " + strategyName : "");
|
||||
|
||||
int sent = 0;
|
||||
for (GcFcmToken t : tokens) {
|
||||
try {
|
||||
Message msg = Message.builder()
|
||||
.setToken(t.getToken())
|
||||
.setNotification(Notification.builder().setTitle(title).setBody(body).build())
|
||||
.putData("type", "trade_signal")
|
||||
.putData("signalId", signalId != null ? String.valueOf(signalId) : "")
|
||||
.putData("market", market)
|
||||
.putData("signalType", signalType)
|
||||
.putData("price", String.valueOf(price))
|
||||
.build();
|
||||
FirebaseMessaging.getInstance().send(msg);
|
||||
t.setLastUsedAt(LocalDateTime.now());
|
||||
tokenRepo.save(t);
|
||||
} catch (FirebaseMessagingException e) {
|
||||
log.warn("[FCM] send failed: {}", e.getMessagingErrorCode());
|
||||
if (e.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED
|
||||
|| e.getMessagingErrorCode() == MessagingErrorCode.INVALID_ARGUMENT) {
|
||||
t.setActive(false);
|
||||
tokenRepo.save(t);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[FCM] send error: {}", e.getMessage());
|
||||
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) {
|
||||
@@ -111,9 +159,11 @@ public class FcmPushService {
|
||||
log.warn("[FCM] test skipped — Firebase not initialized");
|
||||
return;
|
||||
}
|
||||
List<GcFcmToken> tokens = deviceId != null
|
||||
? tokenRepo.findByDeviceIdAndActiveTrue(deviceId)
|
||||
: tokenRepo.findByActiveTrue();
|
||||
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()
|
||||
|
||||
@@ -44,8 +44,10 @@ public class TradeSignalService {
|
||||
.executionType(executionType)
|
||||
.build();
|
||||
GcTradeSignal saved = repo.save(entity);
|
||||
log.info("[TradeSignal] saved {} {} @ {} market={}", signalType, candleType, price, market);
|
||||
if (deviceId != null) {
|
||||
log.info("[TradeSignal] saved {} {} @ {} market={} userId={} deviceId={}",
|
||||
signalType, candleType, price, market, userId, deviceId);
|
||||
// 로그인 사용자는 live 설정에 userId만 있고 deviceId가 null — FCM은 userId 기준으로 발송
|
||||
if (userId != null || (deviceId != null && !deviceId.isBlank())) {
|
||||
fcmPushService.sendTradeSignalIfEnabled(
|
||||
deviceId, userId, market, signalType, price, strategyName, saved.getId());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user