fix(WEB): FCM 모듈 오류 수정 및 중복 타입 제거
- fcm.ts: npm 패키지 import → window.Capacitor 전역 객체 사용 - Capacitor 앱이 런타임에 주입하는 전역 객체 활용 - 웹 빌드 시 '@capacitor/core' 모듈 오류 해결 - next.config.ts: Capacitor 패키지 webpack fallback 추가 - types.ts: VendorManagement 중복 선언 제거 (59줄 감소)
This commit is contained in:
@@ -21,6 +21,18 @@ const nextConfig: NextConfig = {
|
||||
// TODO: Fix ESLint errors after migration is complete
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
// Capacitor 패키지는 모바일 앱 전용 - 웹 빌드에서 제외
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
'@capacitor/core': false,
|
||||
'@capacitor/push-notifications': false,
|
||||
'@capacitor/app': false,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
@@ -274,63 +274,4 @@ export const VENDOR_CATEGORY_COLORS: Record<VendorCategory, string> = {
|
||||
sales: 'bg-green-100 text-green-800',
|
||||
purchase: 'bg-orange-100 text-orange-800',
|
||||
both: 'bg-blue-100 text-blue-800',
|
||||
};
|
||||
|
||||
// ===== API 데이터 타입 =====
|
||||
export interface ClientApiData {
|
||||
id: number;
|
||||
tenant_id?: number;
|
||||
client_code?: string;
|
||||
name?: string;
|
||||
client_type?: 'SALES' | 'PURCHASE' | 'BOTH';
|
||||
contact_person?: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
fax?: string;
|
||||
email?: string;
|
||||
address?: string;
|
||||
manager_name?: string;
|
||||
manager_tel?: string;
|
||||
system_manager?: string;
|
||||
purchase_payment_day?: string;
|
||||
sales_payment_day?: string;
|
||||
business_no?: string;
|
||||
business_type?: string;
|
||||
business_item?: string;
|
||||
account_id?: string;
|
||||
memo?: string;
|
||||
is_active?: boolean;
|
||||
bad_debt?: boolean;
|
||||
is_overdue?: boolean;
|
||||
has_bad_debt?: boolean;
|
||||
bad_debt_total?: number;
|
||||
outstanding_amount?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
// ===== API 타입 매핑 상수 =====
|
||||
export const CLIENT_TYPE_TO_CATEGORY: Record<string, VendorCategory> = {
|
||||
SALES: 'sales',
|
||||
PURCHASE: 'purchase',
|
||||
BOTH: 'both',
|
||||
};
|
||||
|
||||
export const CATEGORY_TO_CLIENT_TYPE: Record<VendorCategory, string> = {
|
||||
sales: 'SALES',
|
||||
purchase: 'PURCHASE',
|
||||
both: 'BOTH',
|
||||
};
|
||||
@@ -9,36 +9,30 @@
|
||||
* - url: 클릭 시 이동 URL
|
||||
* - sound_key: 사운드 파일 키 (sounds/{sound_key}.wav)
|
||||
*
|
||||
* NOTE: Capacitor 모듈은 동적 import로 로드됨 (웹 빌드 에러 방지)
|
||||
* NOTE: window.Capacitor 전역 객체 사용 (Capacitor 앱이 런타임에 주입)
|
||||
*/
|
||||
|
||||
// 동적 로드된 모듈 캐시 (any 타입 - 웹 빌드 시 모듈 없음)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let Capacitor: any = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let PushNotifications: any = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let App: any = null;
|
||||
// ===== TypeScript 타입 정의 (window.Capacitor) =====
|
||||
|
||||
/**
|
||||
* Capacitor 모듈 동적 로드
|
||||
*/
|
||||
async function loadCapacitorModules(): Promise<boolean> {
|
||||
if (Capacitor && PushNotifications && App) return true;
|
||||
interface CapacitorGlobal {
|
||||
getPlatform: () => 'ios' | 'android' | 'web';
|
||||
isPluginAvailable: (name: string) => boolean;
|
||||
Plugins: {
|
||||
PushNotifications?: {
|
||||
removeAllListeners: () => Promise<void>;
|
||||
addListener: (event: string, callback: (data: unknown) => void) => void;
|
||||
requestPermissions: () => Promise<{ receive: string }>;
|
||||
register: () => Promise<void>;
|
||||
};
|
||||
App?: {
|
||||
addListener: (event: string, callback: (data: { isActive: boolean }) => void) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [coreModule, pushModule, appModule] = await Promise.all([
|
||||
import('@capacitor/core'),
|
||||
import('@capacitor/push-notifications'),
|
||||
import('@capacitor/app'),
|
||||
]);
|
||||
Capacitor = coreModule.Capacitor;
|
||||
PushNotifications = pushModule.PushNotifications;
|
||||
App = appModule.App;
|
||||
return true;
|
||||
} catch {
|
||||
console.log('[FCM] Capacitor modules not available (web environment)');
|
||||
return false;
|
||||
declare global {
|
||||
interface Window {
|
||||
Capacitor?: CapacitorGlobal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,11 +72,10 @@ let isInitialized = false;
|
||||
|
||||
/**
|
||||
* Capacitor 네이티브 환경인지 확인
|
||||
* 동기 함수 - Capacitor가 로드되지 않은 경우 false 반환
|
||||
*/
|
||||
export function isCapacitorNative(): boolean {
|
||||
if (!Capacitor) return false;
|
||||
const platform = Capacitor.getPlatform();
|
||||
if (typeof window === 'undefined') return false;
|
||||
const platform = window.Capacitor?.getPlatform?.();
|
||||
return platform === 'ios' || platform === 'android';
|
||||
}
|
||||
|
||||
@@ -90,8 +83,8 @@ export function isCapacitorNative(): boolean {
|
||||
* 현재 플랫폼 반환
|
||||
*/
|
||||
export function getDevicePlatform(): 'ios' | 'android' | 'web' {
|
||||
if (!Capacitor) return 'web';
|
||||
const platform = Capacitor.getPlatform();
|
||||
if (typeof window === 'undefined') return 'web';
|
||||
const platform = window.Capacitor?.getPlatform?.();
|
||||
if (platform === 'ios' || platform === 'android') return platform;
|
||||
return 'web';
|
||||
}
|
||||
@@ -100,6 +93,7 @@ export function getDevicePlatform(): 'ios' | 'android' | 'web' {
|
||||
* 디바이스 이름 반환 (User-Agent 기반)
|
||||
*/
|
||||
function getDeviceName(): string | null {
|
||||
if (typeof navigator === 'undefined') return null;
|
||||
return navigator.userAgent?.substring(0, 100) || null;
|
||||
}
|
||||
|
||||
@@ -121,20 +115,28 @@ function getAppVersion(): string | null {
|
||||
export async function initializeFCM(
|
||||
onForegroundNotification?: ForegroundNotificationHandler
|
||||
): Promise<boolean> {
|
||||
// Capacitor 모듈 동적 로드
|
||||
const modulesLoaded = await loadCapacitorModules();
|
||||
if (!modulesLoaded || !Capacitor || !PushNotifications || !App) {
|
||||
console.log('[FCM] Not running in native app');
|
||||
// 브라우저 환경 체크
|
||||
if (typeof window === 'undefined') {
|
||||
console.log('[FCM] Not running in browser');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capacitor 전역 객체 체크
|
||||
if (!window.Capacitor) {
|
||||
console.log('[FCM] Capacitor not available (web environment)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 네이티브 환경 체크
|
||||
if (!isCapacitorNative()) {
|
||||
console.log('[FCM] Not running in native app');
|
||||
const platform = window.Capacitor.getPlatform();
|
||||
if (platform !== 'ios' && platform !== 'android') {
|
||||
console.log('[FCM] Not running in native app (platform:', platform, ')');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Capacitor.isPluginAvailable('PushNotifications')) {
|
||||
// PushNotifications 플러그인 체크
|
||||
const PushNotifications = window.Capacitor.Plugins?.PushNotifications;
|
||||
if (!PushNotifications) {
|
||||
console.log('[FCM] PushNotifications plugin not available');
|
||||
return false;
|
||||
}
|
||||
@@ -146,7 +148,8 @@ export async function initializeFCM(
|
||||
|
||||
try {
|
||||
// 앱 상태 리스너 (포그라운드/백그라운드)
|
||||
if (Capacitor.isPluginAvailable('App')) {
|
||||
const App = window.Capacitor.Plugins?.App;
|
||||
if (App) {
|
||||
App.addListener('appStateChange', ({ isActive }) => {
|
||||
isAppForeground = isActive;
|
||||
console.log('[FCM] App state:', isActive ? 'foreground' : 'background');
|
||||
@@ -157,24 +160,25 @@ export async function initializeFCM(
|
||||
await PushNotifications.removeAllListeners();
|
||||
|
||||
// 리스너 등록 (register() 호출 전에 등록해야 함)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
PushNotifications.addListener('registration', async (token: any) => {
|
||||
console.log('[FCM] Token received:', token.value?.substring(0, 20) + '...');
|
||||
await handleTokenRegistration(token.value);
|
||||
PushNotifications.addListener('registration', async (tokenData: unknown) => {
|
||||
const token = (tokenData as { value: string })?.value;
|
||||
console.log('[FCM] 🔥 registration event fired');
|
||||
console.log('[FCM] Token received:', token?.substring(0, 20) + '...');
|
||||
await handleTokenRegistration(token);
|
||||
});
|
||||
|
||||
PushNotifications.addListener('registrationError', (err) => {
|
||||
PushNotifications.addListener('registrationError', (err: unknown) => {
|
||||
console.error('[FCM] Registration error:', err);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
PushNotifications.addListener('pushNotificationReceived', (notification: any) => {
|
||||
PushNotifications.addListener('pushNotificationReceived', (notification: unknown) => {
|
||||
console.log('[FCM] Push received (foreground):', notification);
|
||||
|
||||
const notif = notification as { title?: string; body?: string; data?: PushNotificationData };
|
||||
const fcmNotification: FCMNotification = {
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
data: notification.data as PushNotificationData,
|
||||
title: notif.title,
|
||||
body: notif.body,
|
||||
data: notif.data,
|
||||
};
|
||||
|
||||
// 포그라운드 알림 콜백 호출
|
||||
@@ -183,13 +187,13 @@ export async function initializeFCM(
|
||||
}
|
||||
|
||||
// 사운드 재생
|
||||
handleForegroundSound(notification.data?.sound_key);
|
||||
handleForegroundSound(notif.data?.sound_key);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
PushNotifications.addListener('pushNotificationActionPerformed', (action: any) => {
|
||||
PushNotifications.addListener('pushNotificationActionPerformed', (action: unknown) => {
|
||||
console.log('[FCM] Push action performed:', action);
|
||||
const url = action.notification?.data?.url;
|
||||
const actionData = action as { notification?: { data?: { url?: string } } };
|
||||
const url = actionData.notification?.data?.url;
|
||||
if (url && typeof url === 'string') {
|
||||
// URL 이동 (router.push 대신 window.location.href 사용 - 확실한 이동)
|
||||
window.location.href = url;
|
||||
|
||||
Reference in New Issue
Block a user