goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,292 @@
/**
* Web Push 알림 유틸리티
* Service Worker 등록 및 Push 구독 관리
*
* ⚠️ 주의: Service Worker와 Web Push는 HTTPS 또는 localhost에서만 작동합니다!
* HTTP 환경에서는 브라우저 알림만 사용하세요.
*/
// VAPID 공개 키 (백엔드에서 생성)
// 주의: 이 키는 백엔드 application.yml의 push.vapid.public-key와 동일해야 합니다!
const VAPID_PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4xZ0NOe_lJmuwRmNvEy8sokT3IqDxQiKBOx8YRJfw3fbURO36jPl0WC-SD9ZKbgldcnfUIYJHHOdCmanShM90g';
/**
* HTTPS 환경 확인
* Service Worker는 HTTPS 또는 localhost에서만 작동
*/
export const isSecureContext = (): boolean => {
// HTTPS 또는 localhost 확인
return window.isSecureContext ||
window.location.protocol === 'https:' ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
};
/**
* PWA Web Push 지원 확인
*/
export const isPWAPushSupported = (): boolean => {
return isSecureContext() && 'serviceWorker' in navigator && 'PushManager' in window;
};
/**
* Service Worker 등록
* ⚠️ HTTPS 또는 localhost에서만 작동
*/
export const registerServiceWorker = async (): Promise<ServiceWorkerRegistration | null> => {
if (!isSecureContext()) {
console.warn('[Push] ⚠️ HTTP 환경에서는 Service Worker가 작동하지 않습니다.');
console.warn('[Push] HTTPS를 사용하거나 localhost에서 실행하세요.');
console.warn('[Push] 현재는 브라우저 알림만 사용됩니다.');
return null;
}
if (!('serviceWorker' in navigator)) {
console.warn('[Push] Service Worker not supported in this browser');
return null;
}
try {
const registration = await navigator.serviceWorker.register('/service-worker.js', {
scope: '/',
});
console.log('[Push] ✅ Service Worker registered:', registration);
// Wait for the service worker to be ready
await navigator.serviceWorker.ready;
console.log('[Push] ✅ Service Worker ready');
return registration;
} catch (error) {
console.error('[Push] ❌ Service Worker registration failed:', error);
return null;
}
};
/**
* 알림 권한 요청
*/
export const requestNotificationPermission = async (): Promise<NotificationPermission> => {
if (!('Notification' in window)) {
console.warn('Notifications not supported in this browser');
return 'denied';
}
if (Notification.permission === 'granted') {
return 'granted';
}
if (Notification.permission === 'denied') {
return 'denied';
}
const permission = await Notification.requestPermission();
console.log('[Push] Notification permission:', permission);
return permission;
};
/**
* Base64를 Uint8Array로 변환 (VAPID 키 변환용)
*/
const urlBase64ToUint8Array = (base64String: string): Uint8Array => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
/**
* Push 구독 생성
*/
export const subscribeToPush = async (): Promise<PushSubscription | null> => {
try {
// Service Worker 등록 확인
const registration = await navigator.serviceWorker.ready;
// 기존 구독 확인
let subscription = await registration.pushManager.getSubscription();
if (subscription) {
console.log('[Push] Already subscribed:', subscription);
return subscription;
}
// 알림 권한 요청
const permission = await requestNotificationPermission();
if (permission !== 'granted') {
console.warn('[Push] Notification permission not granted');
return null;
}
// 새 구독 생성
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
console.log('[Push] New subscription created:', subscription);
return subscription;
} catch (error) {
console.error('[Push] Failed to subscribe:', error);
return null;
}
};
/**
* Push 구독 해제
*/
export const unsubscribeFromPush = async (): Promise<boolean> => {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (!subscription) {
console.log('[Push] No subscription to unsubscribe');
return true;
}
const result = await subscription.unsubscribe();
console.log('[Push] Unsubscribed:', result);
return result;
} catch (error) {
console.error('[Push] Failed to unsubscribe:', error);
return false;
}
};
/**
* 현재 구독 상태 확인
*/
export const getSubscription = async (): Promise<PushSubscription | null> => {
try {
if (!('serviceWorker' in navigator)) {
return null;
}
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
return subscription;
} catch (error) {
console.error('[Push] Failed to get subscription:', error);
return null;
}
};
/**
* 구독 정보를 서버로 전송
*/
export const sendSubscriptionToServer = async (
subscription: PushSubscription,
endpoint: string = '/api/user/settings/push-subscription'
): Promise<boolean> => {
try {
const token = localStorage.getItem('accessToken');
if (!token) {
console.warn('[Push] No access token found');
return false;
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
subscription: subscription.toJSON(),
}),
});
if (!response.ok) {
throw new Error(`Failed to send subscription: ${response.statusText}`);
}
console.log('[Push] Subscription sent to server');
return true;
} catch (error) {
console.error('[Push] Failed to send subscription to server:', error);
return false;
}
};
/**
* 구독 삭제를 서버에 알림
*/
export const deleteSubscriptionFromServer = async (
endpoint: string = '/api/user/settings/push-subscription'
): Promise<boolean> => {
try {
const token = localStorage.getItem('accessToken');
if (!token) {
console.warn('[Push] No access token found');
return false;
}
const response = await fetch(endpoint, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`Failed to delete subscription: ${response.statusText}`);
}
console.log('[Push] Subscription deleted from server');
return true;
} catch (error) {
console.error('[Push] Failed to delete subscription from server:', error);
return false;
}
};
/**
* PWA 초기화 (Service Worker 등록 + Push 구독)
*/
export const initializePWA = async (): Promise<void> => {
console.log('[PWA] Initializing...');
// Service Worker 등록
const registration = await registerServiceWorker();
if (!registration) {
console.warn('[PWA] Service Worker registration failed');
return;
}
// Push 구독은 사용자가 설정에서 수동으로 활성화
console.log('[PWA] Initialized. Push subscription available in settings.');
};
/**
* 테스트 알림 표시
*/
export const showTestNotification = async (): Promise<void> => {
const permission = await requestNotificationPermission();
if (permission !== 'granted') {
alert('알림 권한이 거부되었습니다.');
return;
}
const registration = await navigator.serviceWorker.ready;
await registration.showNotification('테스트 알림', {
body: '푸시 알림이 정상적으로 작동합니다! 🎉',
icon: '/favicon.ico',
badge: '/favicon.ico',
vibrate: [200, 100, 200],
tag: 'test-notification',
requireInteraction: false,
});
};