Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion .expo/devices.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
{
"devices": []
"devices": [
{
"installationId": "F0B2F989-D042-4D9B-BE7C-8D09B00BD4C5",
"lastUsed": 1758012043849
},
{
"installationId": "8E6F5961-3A90-478E-B947-09F0A98060DB",
"lastUsed": 1757575505218
},
{
"installationId": "38AB03E5-3FFC-4068-95D6-6819BAC877BA",
"lastUsed": 1757568886349
},
{
"installationId": "FE1F6210-9324-47BF-971B-25ED7730754F",
"lastUsed": 1757554107669
},
{
"installationId": "2A00458E-CB06-4F84-954E-01AB454A72C9",
"lastUsed": 1757484949022
},
{
"installationId": "453B7860-472B-43DC-987E-3F54A4AA6955",
"lastUsed": 1757381271381
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
183 changes: 163 additions & 20 deletions App.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,181 @@
import 'react-native-gesture-handler';
import React, { useEffect, useRef } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import StackNavigator from './src/navigation/StackNavigator';
import { setupNotificationListeners } from './src/services/PushNotificationService';
// App.js
import "react-native-gesture-handler";
import React, { useEffect, useRef } from "react";
import { Platform } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import * as Device from "expo-device";
import * as Notifications from "expo-notifications";
import StackNavigator from "./src/navigation/StackNavigator";

// Push 서비스
import {
setupNotificationListeners,
registerExpoPushToken,
} from "./src/services/PushNotificationService";

const SHOW_WELCOME_ON_LAUNCH = true;

// ============================================
// 전역 Notification Handler
// ============================================
console.log("[Push] setNotificationHandler: init");
Notifications.setNotificationHandler({
handleNotification: async () => {
console.log("[Push] handleNotification called (foreground display enabled)");
return {
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});

export default function App() {
const navigationRef = useRef();
const cleanupRef = useRef();
const navigationRef = useRef(null);
const cleanupRef = useRef(null);
const localListeners = useRef({ received: null, response: null });
const shownWelcomeRef = useRef(false);

useEffect(() => {
let timer;

const initializeNotifications = async () => {
console.log("[Push] initializeNotifications: start");
console.log("[Push] Platform:", Platform.OS);

const initializeNotifications = () => {
if (navigationRef.current) {
console.log('expo 푸시알림 리스너 초기화');
const cleanup = setupNotificationListeners(navigationRef.current);
cleanupRef.current = cleanup;

return cleanup;
if (Platform.OS !== "ios") {
console.log("[Push] (skipped) iOS 전용 로직. 현재:", Platform.OS);
return;
}

if (!Device.isDevice) {
console.log("[Push][WARN] 시뮬레이터는 원격 푸시 수신 불가. 실기기 필요.");
return;
}

// ===== 권한 확인 =====
try {
const existing = await Notifications.getPermissionsAsync();
console.log("[Push] permissions(existing):", existing?.status, existing);
if (existing.status !== "granted") {
const { status } = await Notifications.requestPermissionsAsync({
ios: { allowAlert: true, allowSound: true, allowBadge: true },
});
console.log("[Push] permission after request:", status);
if (status !== "granted") {
console.warn("[Push] permission denied; skip token");
return;
}
}
} catch (e) {
console.log("[Push][ERR] getPermissionsAsync failed:", e?.message || e);
return;
}

// ===== 리스너 등록 =====
console.log("[Push] setupNotificationListeners() 호출");
const serviceCleanup = setupNotificationListeners?.();
cleanupRef.current = serviceCleanup;

try {
if (!localListeners.current.received) {
localListeners.current.received =
Notifications.addNotificationReceivedListener((n) => {
try {
console.log(
"[Push][recv] (fg) notification:",
JSON.stringify(n, null, 2)
);
} catch {
console.log("[Push][recv] (fg) notification received");
}
});
}
if (!localListeners.current.response) {
localListeners.current.response =
Notifications.addNotificationResponseReceivedListener((r) => {
try {
console.log("[Push][tap] response:", JSON.stringify(r, null, 2));
} catch {
console.log("[Push][tap] notification tapped");
}
});
}
} catch (e) {
console.log("[Push][ERR] add listeners failed:", e?.message || e);
}

// ===== 토큰 발급 + 서버 업서트 =====
try {
console.log("[Push] registerExpoPushToken() 호출");
const res = await registerExpoPushToken();
console.log("[Push] registerExpoPushToken() result:", res);

if (res?.success && res?.expoPushToken) {
console.log("✅ [Push] ExpoPushToken:", res.expoPushToken);
} else {
Comment on lines +111 to +117
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

민감정보(토큰) 포함 결과 로그 — 마스킹/축약 필요

결과 전체 로그에 토큰이 포함됩니다. DEV에서만 마스킹해 출력하거나 핵심 필드만 남기세요.

-        const res = await registerExpoPushToken();
-        console.log("[Push] registerExpoPushToken() result:", res);
+        const res = await registerExpoPushToken();
+        const masked =
+          res?.expoPushToken ? { ...res, expoPushToken: `***${res.expoPushToken.slice(-6)}` } : res;
+        console.log(
+          "[Push] registerExpoPushToken() result:",
+          __DEV__ ? masked : { success: res?.success, uploaded: res?.uploaded, skipped: res?.skipped }
+        );
-        if (res?.success && res?.expoPushToken) {
-          console.log("✅ [Push] ExpoPushToken:", res.expoPushToken);
+        if (res?.success && res?.expoPushToken) {
+          if (__DEV__) console.log("✅ [Push] ExpoPushToken:", `***${res.expoPushToken.slice(-6)}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log("[Push] registerExpoPushToken() 호출");
const res = await registerExpoPushToken();
console.log("[Push] registerExpoPushToken() result:", res);
if (res?.success && res?.expoPushToken) {
console.log("✅ [Push] ExpoPushToken:", res.expoPushToken);
} else {
console.log("[Push] registerExpoPushToken() 호출");
const res = await registerExpoPushToken();
const masked =
res?.expoPushToken ? { ...res, expoPushToken: `***${res.expoPushToken.slice(-6)}` } : res;
console.log(
"[Push] registerExpoPushToken() result:",
__DEV__ ? masked : { success: res?.success, uploaded: res?.uploaded, skipped: res?.skipped }
);
if (res?.success && res?.expoPushToken) {
if (__DEV__) console.log("✅ [Push] ExpoPushToken:", `***${res.expoPushToken.slice(-6)}`);
} else {
🤖 Prompt for AI Agents
In App.js around lines 111 to 117, the code logs the full expoPushToken which
leaks sensitive token data; change the logging to avoid printing the entire
token by either (a) only logging the presence/status (e.g., "expoPushToken
received") or (b) logging a masked/abbreviated token (e.g., first 4 and last 4
chars with ellipsis) and wrap any unmasked logging behind a development-only
check (NODE_ENV === 'development') so production never emits the raw token.

console.warn("❌ [Push] Expo 토큰 발급 실패:", res?.error);
}
} catch (e) {
console.warn("[Push][ERR] registerExpoPushToken error:", e?.message || e);
}

// ===== 환영 배너 =====
if (SHOW_WELCOME_ON_LAUNCH && !shownWelcomeRef.current) {
shownWelcomeRef.current = true;
try {
const now = new Date();
const time = new Intl.DateTimeFormat("ko-KR", {
hour: "2-digit",
minute: "2-digit",
}).format(now);
await Notifications.scheduleNotificationAsync({
content: {
title: "👋 환영합니다!",
body: `${time} 접속했습니다.`,
data: { _meta: "welcome" },
sound: "default",
},
trigger: null,
});
} catch (e) {
console.log("[Push][ERR] schedule welcome failed:", e?.message || e);
}
}

console.log("[Push] initializeNotifications: done");
};

const timer = setTimeout(initializeNotifications, 1000);
timer = setTimeout(initializeNotifications, 500);

return () => {
clearTimeout(timer);
if (cleanupRef.current) {
cleanupRef.current();
}
try {
cleanupRef.current?.();
} catch {}
try {
if (localListeners.current.received) {
Notifications.removeNotificationSubscription(localListeners.current.received);
localListeners.current.received = null;
}
if (localListeners.current.response) {
Notifications.removeNotificationSubscription(localListeners.current.response);
localListeners.current.response = null;
}
} catch {}
console.log("[Push] cleanup completed");
};
}, []);

return (
<NavigationContainer ref={navigationRef}>
<NavigationContainer
ref={(r) => {
navigationRef.current = r;
if (r) console.log("[Nav] navigationRef ready");
}}
>
<StackNavigator />
</NavigationContainer>
);
}
}
15 changes: 0 additions & 15 deletions app.config.js

This file was deleted.

13 changes: 9 additions & 4 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"expo": {
"name": "Doodook",
"name": "두둑",
"slug": "doodook",
"owner": "yehyeon",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./src/assets/images/logo-d-app-icon-1500.png",
Expand All @@ -18,7 +19,7 @@
],
"ios": {
"bundleIdentifier": "com.lipeuoo.doodook",
"buildNumber": "7",
"buildNumber": "22",
"supportsTablet": true,
"infoPlist": {
"UIBackgroundModes": [
Expand Down Expand Up @@ -46,7 +47,11 @@
"color": "#F074BA"
}
]
]

],
"extra": {
"eas": {
"projectId": "57b0a621-af5d-4605-b6e0-cc46a6c474ec"
}
}
}
}
Loading