diff --git a/next.config.ts b/next.config.ts index a83f31b9..0774cffd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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); diff --git a/src/components/accounting/VendorManagement/types.ts b/src/components/accounting/VendorManagement/types.ts index 6b7eda13..de532db1 100644 --- a/src/components/accounting/VendorManagement/types.ts +++ b/src/components/accounting/VendorManagement/types.ts @@ -274,63 +274,4 @@ export const VENDOR_CATEGORY_COLORS: Record = { 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 { - success: boolean; - message?: string; - data?: T; -} - -export interface PaginatedResponse { - data: T[]; - total: number; - page?: number; - size?: number; -} - -// ===== API 타입 매핑 상수 ===== -export const CLIENT_TYPE_TO_CATEGORY: Record = { - SALES: 'sales', - PURCHASE: 'purchase', - BOTH: 'both', -}; - -export const CATEGORY_TO_CLIENT_TYPE: Record = { - sales: 'SALES', - purchase: 'PURCHASE', - both: 'BOTH', }; \ No newline at end of file diff --git a/src/lib/capacitor/fcm.ts b/src/lib/capacitor/fcm.ts index 65ad6ef2..4d12ba06 100644 --- a/src/lib/capacitor/fcm.ts +++ b/src/lib/capacitor/fcm.ts @@ -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 { - if (Capacitor && PushNotifications && App) return true; +interface CapacitorGlobal { + getPlatform: () => 'ios' | 'android' | 'web'; + isPluginAvailable: (name: string) => boolean; + Plugins: { + PushNotifications?: { + removeAllListeners: () => Promise; + addListener: (event: string, callback: (data: unknown) => void) => void; + requestPermissions: () => Promise<{ receive: string }>; + register: () => Promise; + }; + 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 { - // 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;