가상매매 시간봉 제거

This commit is contained in:
Macbook
2026-05-28 20:48:28 +09:00
parent 7e3644cb62
commit 15160f7d2c
45 changed files with 996 additions and 405 deletions
+18 -7
View File
@@ -12,11 +12,18 @@
## 2. 프로젝트에 배치
```bash
# 권장: repo 루트 app/google-services.json (com.goldenchart.app 포함)
cp ~/Downloads/google-services.json app/google-services.json
./scripts/ensure-android-google-services.sh
# 또는 한 번에
./scripts/setup-android-fcm.sh ~/Downloads/google-services.json
# 또는
cp ~/Downloads/google-services.json app/android/app/google-services.json
```
빌드 시 `ensure-android-google-services.sh``app/google-services.json`
`app/android/app/google-services.json` 으로 복사합니다. **이 파일이 없으면**
알림 권한 허용 직후 `PushNotifications.register()` 에서 **앱이 종료**될 수 있습니다.
## 3. 빌드
```bash
@@ -35,17 +42,21 @@ cd app/android && ./gradlew assembleRelease
## 5. 앱에서 확인
1. 로그인 후 **설정 → FCM 푸시** ON
2. 알림 권한 허용
3. **테스트 발송** 버튼
4. 전략 시그널 발생 시 푸시 수신
1. **2.4 이상 APK** 설치 (`google-services.json` 포함 빌드 필수)
2. 크래시가 반복되면 **앱 삭제 후 재설치** (이전에 허용한 알림 권한 초기화)
3. 로그인 → **설정 → FCM 푸시** ON
4. **「알림 허용 · FCM 등록」** 버튼 → 시스템 팝업에서 허용 (2초 후 등록)
5. **테스트 발송** 으로 확인
로그인·메인 진입 시에는 **알림 권한을 절대 요청하지 않습니다** (2.3+).
FCM 푸시 토글 ON → **「알림 허용 · FCM 등록」** 버튼을 눌렀을 때만 권한 팝업이 뜹니다.
## goldenApp 과의 차이
| 항목 | goldenApp | GoldenChart (Capacitor) |
|------|-----------|-------------------------|
| 패키지 | `com.golden.app` | `com.goldenchart.app` |
| FCM SDK | 직접 Firebase Messaging | `@capacitor/push-notifications` |
| FCM SDK | 직접 Firebase Messaging | 앱 내장 `GoldenFcm` 플러그인 (Capacitor Push 미사용) |
| 토큰 등록 | `POST /api/fcm/token` | 동일 (`registerFcmToken`) |
| 알림 채널 | `golden_alerts` | `goldenchart_trade_signals` |
+11 -9
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.goldenchart.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 7
versionName "1.6"
versionCode 16
versionName "2.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -40,15 +40,17 @@ dependencies {
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
implementation platform('com.google.firebase:firebase-bom:33.7.0')
implementation 'com.google.firebase:firebase-messaging'
implementation 'com.google.android.gms:play-services-base:18.5.0'
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
def servicesJSON = file('google-services.json')
if (!servicesJSON.exists()) {
throw new GradleException(
'google-services.json 없음. 실행: ./scripts/ensure-android-google-services.sh'
)
}
apply plugin: 'com.google.gms.google-services'
-1
View File
@@ -12,7 +12,6 @@ dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-haptics')
implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications')
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
+48
View File
@@ -0,0 +1,48 @@
{
"project_info": {
"project_number": "353782909389",
"project_id": "goldenanalysisapp",
"storage_bucket": "goldenanalysisapp.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:353782909389:android:45e0218e69611efcc75c84",
"android_client_info": {
"package_name": "com.golden.app"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBZWkVrPvG88_bYnIG94yiml3VXzht157Y"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:353782909389:android:62e71bbb669c64bbc75c84",
"android_client_info": {
"package_name": "com.goldenchart.app"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBZWkVrPvG88_bYnIG94yiml3VXzht157Y"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
+20 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -18,6 +19,16 @@
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<!-- 권한 허용·앱 시작 시 FCM 자동 초기화 방지 — JS register() 시에만 활성화 -->
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="false"
tools:replace="android:value" />
<!-- Capacitor push-notifications 잔여 병합 제거 (이중 MessagingService 크래시 방지) -->
<service
android:name="com.capacitorjs.plugins.pushnotifications.MessagingService"
tools:node="remove" />
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
@@ -34,6 +45,14 @@
</activity>
<service
android:name=".GoldenChartMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -0,0 +1,38 @@
package com.goldenchart.app;
import androidx.annotation.NonNull;
import com.getcapacitor.JSObject;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import java.util.Map;
/**
* FCM 수신 — JS GoldenFcm 리스너로 전달
*/
public class GoldenChartMessagingService extends FirebaseMessagingService {
@Override
public void onNewToken(@NonNull String token) {
super.onNewToken(token);
GoldenFcmPlugin.emitRegistration(token);
}
@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
super.onMessageReceived(message);
JSObject notification = new JSObject();
RemoteMessage.Notification n = message.getNotification();
if (n != null) {
if (n.getTitle() != null) notification.put("title", n.getTitle());
if (n.getBody() != null) notification.put("body", n.getBody());
}
JSObject data = new JSObject();
for (Map.Entry<String, String> e : message.getData().entrySet()) {
data.put(e.getKey(), e.getValue());
}
notification.put("data", data);
GoldenFcmPlugin.emitPushReceived(notification);
}
}
@@ -0,0 +1,196 @@
package com.goldenchart.app;
import android.Manifest;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;
import com.getcapacitor.annotation.PermissionCallback;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.firebase.FirebaseApp;
import com.google.firebase.messaging.FirebaseMessaging;
import java.util.concurrent.Executors;
/**
* FCM — Capacitor PushNotifications 미사용 (크래시 방지)
*/
@CapacitorPlugin(
name = "GoldenFcm",
permissions = {
@Permission(strings = { Manifest.permission.POST_NOTIFICATIONS }, alias = GoldenFcmPlugin.PERM)
}
)
public class GoldenFcmPlugin extends Plugin {
static final String PERM = "notifications";
private static final String TAG = "GoldenFcm";
private static GoldenFcmPlugin instance;
@Override
public void load() {
instance = this;
}
static void emitRegistration(String token) {
if (instance == null || token == null) return;
JSObject data = new JSObject();
data.put("value", token);
instance.notifyListeners("registration", data);
}
static void emitRegistrationError(String message) {
if (instance == null) return;
JSObject data = new JSObject();
data.put("error", message != null ? message : "unknown");
instance.notifyListeners("registrationError", data);
}
static void emitPushReceived(JSObject notification) {
if (instance == null) return;
instance.notifyListeners("pushNotificationReceived", notification);
}
static void emitPushAction(JSObject action) {
if (instance == null) return;
instance.notifyListeners("pushNotificationActionPerformed", action);
}
private static String permissionStateToString(com.getcapacitor.PermissionState state) {
if (state == com.getcapacitor.PermissionState.GRANTED) return "granted";
if (state == com.getcapacitor.PermissionState.DENIED) return "denied";
return "prompt";
}
private boolean ensurePlayServices(PluginCall call) {
try {
int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getContext());
if (code != ConnectionResult.SUCCESS) {
call.reject("Google Play 서비스 필요 (코드 " + code + ")");
return false;
}
} catch (Throwable t) {
call.reject("Play 서비스 확인 실패: " + t.getMessage());
return false;
}
return true;
}
@PluginMethod
public void checkPermissions(PluginCall call) {
JSObject r = new JSObject();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
r.put("receive", "granted");
call.resolve(r);
return;
}
r.put("receive", permissionStateToString(getPermissionState(PERM)));
call.resolve(r);
}
/** 알림 권한만 요청 — FCM register 호출 없음 */
@PluginMethod
public void requestPermissions(PluginCall call) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
JSObject r = new JSObject();
r.put("receive", "granted");
call.resolve(r);
return;
}
if (getPermissionState(PERM) == com.getcapacitor.PermissionState.GRANTED) {
JSObject r = new JSObject();
r.put("receive", "granted");
call.resolve(r);
return;
}
call.setKeepAlive(true);
requestPermissionForAlias(PERM, call, "onPermResult");
}
@PermissionCallback
private void onPermResult(PluginCall call) {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
try {
JSObject r = new JSObject();
r.put("receive", permissionStateToString(getPermissionState(PERM)));
call.resolve(r);
} catch (Throwable t) {
Log.e(TAG, "onPermResult", t);
call.reject(t.getMessage());
}
}, 600);
}
/** 권한 허용 후 별도 호출 — Firebase getToken */
@PluginMethod
public void register(PluginCall call) {
call.setKeepAlive(true);
if (getActivity() == null || getActivity().isFinishing()) {
call.reject("Activity unavailable");
return;
}
if (!ensurePlayServices(call)) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& getPermissionState(PERM) != com.getcapacitor.PermissionState.GRANTED) {
call.reject("알림 권한이 필요합니다");
return;
}
Executors.newSingleThreadExecutor().execute(() -> {
try {
if (FirebaseApp.getApps(getContext()).isEmpty()) {
FirebaseApp.initializeApp(getContext());
}
FirebaseMessaging.getInstance().setAutoInitEnabled(true);
FirebaseMessaging.getInstance()
.getToken()
.addOnCompleteListener(task -> {
new Handler(Looper.getMainLooper()).post(() -> {
if (!task.isSuccessful()) {
Exception ex = task.getException();
String msg = ex != null ? ex.getMessage() : "getToken failed";
Log.w(TAG, "getToken failed", ex);
emitRegistrationError(msg);
call.reject(msg);
return;
}
String token = task.getResult();
Log.i(TAG, "FCM token ok");
emitRegistration(token);
JSObject ret = new JSObject();
ret.put("value", token);
call.resolve(ret);
});
});
} catch (Throwable t) {
Log.e(TAG, "register", t);
new Handler(Looper.getMainLooper()).post(() ->
call.reject(t.getMessage() != null ? t.getMessage() : "register failed")
);
}
});
}
@PluginMethod
public void unregister(PluginCall call) {
try {
if (!FirebaseApp.getApps(getContext()).isEmpty()) {
FirebaseMessaging.getInstance().setAutoInitEnabled(false);
FirebaseMessaging.getInstance().deleteToken();
}
} catch (Throwable t) {
Log.w(TAG, "unregister", t);
}
call.resolve();
}
}
@@ -4,20 +4,37 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import com.getcapacitor.BridgeActivity;
import com.google.firebase.FirebaseApp;
/**
* Capacitor 메인 액티비티 — FCM 알림 채널 생성 (goldenApp / Android 8+)
* Capacitor 메인 액티비티 — GoldenFcm 플러그인 + 알림 채널
*/
public class MainActivity extends BridgeActivity {
private static final String TAG = "GoldenChart";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerPlugin(GoldenFcmPlugin.class);
initFirebaseSafe();
createNotificationChannel();
}
private void initFirebaseSafe() {
try {
if (FirebaseApp.getApps(this).isEmpty()) {
FirebaseApp.initializeApp(this);
Log.i(TAG, "FirebaseApp initialized");
}
} catch (Throwable t) {
Log.w(TAG, "FirebaseApp init failed", t);
}
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
String channelId = getString(R.string.default_notification_channel_id);
-3
View File
@@ -11,9 +11,6 @@ project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacit
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../../node_modules/@capacitor/preferences/android')
include ':capacitor-push-notifications'
project(':capacitor-push-notifications').projectDir = new File('../../node_modules/@capacitor/push-notifications/android')
include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../../node_modules/@capacitor/splash-screen/android')