From 8446ad91e387f7847710f6da85b3c0e5d94fc019 Mon Sep 17 00:00:00 2001 From: "focp212@naver.com" Date: Wed, 15 Oct 2025 10:35:01 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/index.ts | 1 - src/hooks/useAuth.tsx | 173 ------ src/pages/Test.tsx | 527 ------------------ src/pages/index.ts | 3 +- src/routes/test.tsx | 6 - src/shared/lib/appBridge.ts | 10 - .../lib/convert-currency-string-to-number.ts | 4 - src/shared/lib/hooks/index.tsx | 1 - src/shared/lib/hooks/use-app-version.tsx | 17 - src/shared/ui/full-app-version/index.tsx | 8 - src/types/api.ts | 47 -- src/types/auth.ts | 62 --- src/types/filter.ts | 117 ---- src/types/index.ts | 5 +- src/utils/api.ts | 260 --------- src/utils/appBridge.ts | 12 +- src/utils/auth.ts | 188 ------- src/utils/index.ts | 3 - src/utils/jwtDecoder.ts | 121 ---- src/utils/safeArea.ts | 17 - src/utils/tokenManager.ts | 189 ------- 21 files changed, 3 insertions(+), 1768 deletions(-) delete mode 100644 src/hooks/useAuth.tsx delete mode 100644 src/pages/Test.tsx delete mode 100644 src/routes/test.tsx delete mode 100644 src/shared/lib/convert-currency-string-to-number.ts delete mode 100644 src/shared/lib/hooks/use-app-version.tsx delete mode 100644 src/shared/ui/full-app-version/index.tsx delete mode 100644 src/types/api.ts delete mode 100644 src/types/auth.ts delete mode 100644 src/types/filter.ts delete mode 100644 src/utils/api.ts delete mode 100644 src/utils/auth.ts delete mode 100644 src/utils/jwtDecoder.ts delete mode 100644 src/utils/safeArea.ts delete mode 100644 src/utils/tokenManager.ts diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 85f634a..1bf9f2d 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,2 @@ -export { useAuth } from './useAuth'; export { useAppBridge } from './useAppBridge'; export { useScrollDirection } from './useScrollDirection'; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx deleted file mode 100644 index 0f65c26..0000000 --- a/src/hooks/useAuth.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; -import { AuthContextType, AuthState, LoginCredentials, RegisterData, UserRole } from '@/types'; -import { authService } from '@/utils/auth'; - -const AuthContext = createContext(null); - -interface AuthProviderProps { - children: ReactNode; -} - -export const AuthProvider: React.FC = ({ children }) => { - const [authState, setAuthState] = useState({ - isAuthenticated: false, - user: null, - token: null, - isLoading: true, - }); - - const login = async (credentials: LoginCredentials): Promise => { - try { - setAuthState(prev => ({ ...prev, isLoading: true })); - - const result = await authService.login(credentials); - - setAuthState({ - isAuthenticated: true, - user: result.user, - token: result.accessToken, - isLoading: false, - }); - } catch (error) { - setAuthState(prev => ({ ...prev, isLoading: false })); - throw error; - } - }; - - const register = async (data: RegisterData): Promise => { - try { - setAuthState(prev => ({ ...prev, isLoading: true })); - - const result = await authService.register(data); - - setAuthState({ - isAuthenticated: true, - user: result.user, - token: result.accessToken, - isLoading: false, - }); - } catch (error) { - setAuthState(prev => ({ ...prev, isLoading: false })); - throw error; - } - }; - - const logout = useCallback((): void => { - authService.logout(); - setAuthState({ - isAuthenticated: false, - user: null, - token: null, - isLoading: false, - }); - }, []); - - const refreshToken = useCallback(async (): Promise => { - try { - const newToken = await authService.refreshToken(); - const user = authService.getCurrentUserFromToken(); - - setAuthState(prev => ({ - ...prev, - token: newToken, - user, - isAuthenticated: true, - })); - } catch (error) { - logout(); - throw error; - } - }, [logout]); - - const hasRole = (role: UserRole): boolean => { - return authService.hasRole(role); - }; - - const hasPermission = (permission: string): boolean => { - return authService.hasPermission(permission); - }; - - const loadUserFromToken = useCallback(async (): Promise => { - try { - if (authService.isAuthenticated()) { - const user = authService.getCurrentUserFromToken(); - const token = authService.getAccessToken(); - - if (user && token) { - setAuthState({ - isAuthenticated: true, - user, - token, - isLoading: false, - }); - - // 토큰 갱신 체크 - if (authService.shouldRefreshToken()) { - await refreshToken(); - } - } else { - // 토큰이 유효하지 않으면 최신 사용자 정보 가져오기 - const currentUser = await authService.getCurrentUser(); - setAuthState({ - isAuthenticated: true, - user: currentUser, - token, - isLoading: false, - }); - } - } else { - setAuthState({ - isAuthenticated: false, - user: null, - token: null, - isLoading: false, - }); - } - } catch (error) { - console.error('Failed to load user from token:', error); - logout(); - } - }, [refreshToken, logout]); - - useEffect(() => { - loadUserFromToken(); - }, [loadUserFromToken]); - - // 토큰 자동 갱신 체크 - useEffect(() => { - if(!authState.isAuthenticated){ - return () => {}; - } - - const checkTokenExpiration = setInterval(() => { - if (authService.shouldRefreshToken()) { - refreshToken().catch(() => { - logout(); - }); - } - }, 60000); // 1분마다 체크 - - return () => clearInterval(checkTokenExpiration); - }, [authState.isAuthenticated, refreshToken, logout]); - - const contextValue: AuthContextType = { - ...authState, - login, - register, - logout, - refreshToken, - hasRole, - hasPermission, - }; - - return {children}; -}; - -// eslint-disable-next-line react-refresh/only-export-components -export const useAuth = (): AuthContextType => { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; -}; \ No newline at end of file diff --git a/src/pages/Test.tsx b/src/pages/Test.tsx deleted file mode 100644 index 0f1fb96..0000000 --- a/src/pages/Test.tsx +++ /dev/null @@ -1,527 +0,0 @@ -import React, { useState } from 'react'; -import { appBridge } from '../utils/appBridge'; -import { LanguageSwitcher } from '../components'; -import { SearchFilter } from '../types/filter'; - -interface TestResult { - method: string; - success: boolean; - result?: any; - error?: string; - timestamp: string; -} - -const Test: React.FC = () => { - const [results, setResults] = useState([]); - const [isLoading, setIsLoading] = useState(null); - const [messageCount, setMessageCount] = useState(0); - const [searchData, setSearchData] = useState>([]); - - const addResult = (method: string, success: boolean, result?: any, error?: string) => { - const newResult: TestResult = { - method, - success, - result, - error, - timestamp: new Date().toLocaleTimeString() - }; - setResults(prev => [newResult, ...prev]); - }; - - const executeTest = async (method: string, testFn: () => Promise) => { - setIsLoading(method); - try { - const result = await testFn(); - addResult(method, true, result); - } catch (error) { - addResult(method, false, undefined, error instanceof Error ? error.message : String(error)); - } finally { - setIsLoading(null); - } - }; - - const clearResults = () => { - setResults([]); - }; - - // 앱 정보 테스트 - const testGetAppInfo = () => executeTest('getAppInfo', () => appBridge.getAppInfo()); - const testGetDeviceInfo = () => executeTest('getDeviceInfo', () => appBridge.getDeviceInfo()); - - // 네비게이션 테스트 - const testNavigateBack = () => executeTest('navigateBack', () => appBridge.navigateBack()); - const testNavigateTo = () => executeTest('navigateTo', () => appBridge.navigateTo('/home')); - const testCloseWebView = () => executeTest('closeWebView', () => appBridge.closeWebView()); - const testNavigateToLogin = () => executeTest('navigateToLogin', () => appBridge.navigateToLogin()); - - // 알림 테스트 - const testShowToast = () => executeTest('showToast', () => appBridge.showToast('테스트 토스트 메시지', 3000)); - const testShowAlert = () => executeTest('showAlert', () => appBridge.showAlert('테스트 알림', '이것은 테스트 알림입니다.')); - const testShowConfirm = () => executeTest('showConfirm', () => appBridge.showConfirm('확인', '계속하시겠습니까?')); - - // 저장소 테스트 - const testSetStorage = () => executeTest('setStorage', () => appBridge.setStorage('test_key', { message: '테스트 데이터', timestamp: new Date().toISOString() })); - const testGetStorage = () => executeTest('getStorage', () => appBridge.getStorage('test_key')); - const testRemoveStorage = () => executeTest('removeStorage', () => appBridge.removeStorage('test_key')); - - // 공유 테스트 - const testShareContent = () => executeTest('shareContent', () => appBridge.shareContent({ - title: '테스트 공유', - text: '이것은 테스트 공유 내용입니다.', - url: 'https://nicepayments.com' - })); - - // 인증 테스트 - const testLogin = () => executeTest('login', () => appBridge.login({ email: 'test@example.com', password: 'test123' })); - const testLogout = () => executeTest('logout', () => appBridge.logout()); - - // 안전한 호출 테스트 - const testSafeCall = () => executeTest('safeCall', () => appBridge.safeCall( - () => appBridge.getAppInfo(), - { version: 'fallback', buildNumber: 'fallback' }, - (error) => console.error('Safe call error:', error) - )); - - // 타임아웃 테스트 - const testCallWithTimeout = () => executeTest('callWithTimeout', () => appBridge.callWithTimeout( - () => appBridge.getDeviceInfo(), - 2000 - )); - - // 메시지 카운트 업데이트 테스트 - const testUpdateMessageCount = () => executeTest('updateMessageCount', () => appBridge.updateMessageCount(messageCount)); - - // 조회조건 변경 핸들러 - const handleFilterChange = (filter: SearchFilter) => { - addResult('filterChange', true, filter); - - // 가상 데이터로 검색 결과 시뮬레이션 - const mockData: any = generateMockData(filter); - setSearchData(mockData); - }; - - // 가상 거래 데이터 생성 - const generateMockData = (filter: SearchFilter) => { - const transactionTypes = ['deposit', 'withdrawal']; - const data = []; - - for (let i = 0; i < 10; i++) { - const type = filter.transactionType === 'all' - ? transactionTypes[Math.floor(Math.random() * transactionTypes.length)] - : filter.transactionType; - - const amount = Math.floor(Math.random() * 1000000) + 10000; - const date = new Date(); - date.setDate(date.getDate() - Math.floor(Math.random() * 180)); - - data.push({ - id: `TXN${String(i + 1).padStart(3, '0')}`, - type: type, - amount: amount, - date: date, - status: Math.random() > 0.1 ? 'completed' : 'failed', - description: type === 'deposit' ? '입금' : '출금' - }); - } - - // 정렬 적용 - data.sort((a, b) => { - if (filter.sortOrder === 'latest') { - return b.date.getTime() - a.date.getTime(); - } else { - return a.date.getTime() - b.date.getTime(); - } - }); - - return data; - }; - - return ( -
-
-
-
-

AppBridge 테스트

-
- - - {appBridge.isNativeEnvironment() ? '네이티브 환경' : '웹 환경'} - - {appBridge.isNativeEnvironment() && ( - - {appBridge.isAndroid() ? 'Android' : appBridge.isIOS() ? 'iOS' : 'Unknown'} - - )} -
-
- - {!appBridge.isNativeEnvironment() && ( -
-
-
- - - -
-
-

- 웹 환경에서 실행 중 -

-
-

대부분의 AppBridge 기능은 네이티브 환경에서만 작동합니다. 실제 테스트를 위해서는 모바일 앱에서 실행해주세요.

-
-
-
-
- )} -
- - - {/* 검색 결과 표시 */} - {searchData.length > 0 && ( -
-

- 검색 결과 ({searchData.length}건) -

-
- - - - - - - - - - - - {searchData.map((item, index) => ( - - - - - - - - ))} - -
- 거래ID - - 구분 - - 금액 - - 날짜 - - 상태 -
- {item.id} - - - {item.description} - - - {item.amount.toLocaleString()}원 - - {item.date.toLocaleDateString('ko-KR')} - - - {item.status === 'completed' ? '완료' : '실패'} - -
-
-
- )} - -
- {/* 테스트 버튼들 */} -
-

테스트 기능

- - {/* 앱 정보 */} -
-

앱 정보

-
- - -
-
- - {/* 네비게이션 */} -
-

네비게이션

-
- - - - -
-
- - {/* 알림 */} -
-

알림

-
- - - -
-
- - {/* 저장소 */} -
-

저장소

-
- - - -
-
- - - {/* 결제 및 인증 */} -
-

결제 & 인증

-
-
- - -
-
-
- - {/* 공유 */} -
-

공유

-
- -
-
- - {/* 메시지 카운트 */} -
-

메시지 카운트

-
-
- setMessageCount(parseInt(e.target.value) || 0)} - min="0" - className="border border-gray-300 rounded px-3 py-1 text-sm w-20" - placeholder="0" - /> - -
-

- iOS: 앱 아이콘 배지 / Android: 로그 출력 -

-
-
- - {/* 고급 테스트 */} -
-

고급 테스트

-
- - -
-
-
- - {/* 테스트 결과 */} -
-
-

테스트 결과

- -
- -
- {results.length === 0 ? ( -

- 테스트 버튼을 클릭하여 결과를 확인하세요. -

- ) : ( - results.map((result, index) => ( -
-
- - {result.method} - - - {result.timestamp} - -
- {result.success ? ( -
- ✅ 성공 - {result.result && ( -
-                            {JSON.stringify(result.result, null, 2)}
-                          
- )} -
- ) : ( -
- ❌ 실패 -

- {result.error} -

-
- )} -
- )) - )} -
-
-
-
-
- ); -}; - -export default Test; \ No newline at end of file diff --git a/src/pages/index.ts b/src/pages/index.ts index 213c275..741a7ea 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,3 +1,2 @@ export { default as Home } from './Home'; -export { default as Contact } from './Contact'; -export { default as Test } from './Test'; \ No newline at end of file +export { default as Contact } from './Contact'; \ No newline at end of file diff --git a/src/routes/test.tsx b/src/routes/test.tsx deleted file mode 100644 index e6b40f5..0000000 --- a/src/routes/test.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { Test } from '@/pages' - -export const Route = createFileRoute('/test')({ - component: Test, -}) \ No newline at end of file diff --git a/src/shared/lib/appBridge.ts b/src/shared/lib/appBridge.ts index d335caf..fda242b 100644 --- a/src/shared/lib/appBridge.ts +++ b/src/shared/lib/appBridge.ts @@ -5,7 +5,6 @@ import { DeviceInfo, ShareContent } from '@/types'; -import { LoginCredentials, UserInfo } from '@/types/auth'; class AppBridge { private static instance: AppBridge; @@ -114,10 +113,6 @@ class AppBridge { return this.sendMessage(BridgeMessageType.NAVIGATE_TO, { path }); } - async navigateToLogin(): Promise { - return this.sendMessage(BridgeMessageType.NAVIGATE_TO_LOGIN); - } - async closeWebView(): Promise { return this.sendMessage(BridgeMessageType.CLOSE_WEBVIEW); } @@ -158,11 +153,6 @@ class AppBridge { return this.sendMessage(BridgeMessageType.SHARE_CONTENT, content); } - // 로그인 요청 - async login(credentials?: LoginCredentials): Promise { - return this.sendMessage(BridgeMessageType.LOGIN, credentials); - } - async logout(): Promise { return this.sendMessage(BridgeMessageType.LOGOUT); } diff --git a/src/shared/lib/convert-currency-string-to-number.ts b/src/shared/lib/convert-currency-string-to-number.ts deleted file mode 100644 index e695945..0000000 --- a/src/shared/lib/convert-currency-string-to-number.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const convertCurrencyStringToNumber = (currencyString: string): number => { - const cleanedString = currencyString.replace(/[^\d]/g, ''); - return parseInt(cleanedString, 10); -}; \ No newline at end of file diff --git a/src/shared/lib/hooks/index.tsx b/src/shared/lib/hooks/index.tsx index 5f5657a..826b165 100644 --- a/src/shared/lib/hooks/index.tsx +++ b/src/shared/lib/hooks/index.tsx @@ -1,5 +1,4 @@ export * from './use-navigate'; -export * from './use-app-version'; export * from './use-change-bg-color'; export * from './use-device-uid'; export * from './use-fix-scroll-view-safari'; diff --git a/src/shared/lib/hooks/use-app-version.tsx b/src/shared/lib/hooks/use-app-version.tsx deleted file mode 100644 index f7b7c5e..0000000 --- a/src/shared/lib/hooks/use-app-version.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useMemo } from 'react'; -import useLocalStorageState from 'use-local-storage-state'; -import { config } from '@/shared/configs'; -import { StorageKeys } from '@/shared/constants/local-storage'; - -export const DEFAULT_APP_VERSION = '1.0'; -export const useAppVersion = () => { - const [appVersion] = useLocalStorageState(StorageKeys.AppVersion, { defaultValue: DEFAULT_APP_VERSION }); - return appVersion; -}; - -export const webVersion = config.WEB_VERSION.replace(/\b(\d+)(?:\.0\.0)?\b/g, '$1'); -export const useFullVersion = () => { - const appVersion = useAppVersion(); - const fullVersion = useMemo(() => `${appVersion}.${webVersion}`, [appVersion]); - return { fullVersion }; -}; diff --git a/src/shared/ui/full-app-version/index.tsx b/src/shared/ui/full-app-version/index.tsx deleted file mode 100644 index 71e80df..0000000 --- a/src/shared/ui/full-app-version/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { Fragment } from 'react/jsx-runtime'; -import { useFullVersion } from '@/shared/lib/hooks'; - -export const FullAppVersion = React.memo(function FullAppVersion() { - const { fullVersion } = useFullVersion(); - return {fullVersion}; -}); diff --git a/src/types/api.ts b/src/types/api.ts deleted file mode 100644 index 26e8896..0000000 --- a/src/types/api.ts +++ /dev/null @@ -1,47 +0,0 @@ -export interface ApiResponse { - success: boolean; - data: T; - message: string; - timestamp: string; -} - -export interface ApiErrorResponse { - success: false; - error: { - code: string; - message: string; - details?: any; - }; - timestamp: string; -} - -export interface PaginatedResponse { - data: T[]; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - }; -} - -export interface ApiRequestConfig { - timeout?: number; - retries?: number; - retryDelay?: number; - headers?: Record; -} - -export interface FileUploadProgress { - loaded: number; - total: number; - percentage: number; -} - -export interface FileUploadResponse { - fileId: string; - filename: string; - url: string; - size: number; - mimeType: string; -} \ No newline at end of file diff --git a/src/types/auth.ts b/src/types/auth.ts deleted file mode 100644 index 96f9bc0..0000000 --- a/src/types/auth.ts +++ /dev/null @@ -1,62 +0,0 @@ -export interface LoginCredentials { - email: string; - password: string; -} - -export interface RegisterData { - email: string; - password: string; - name: string; - phone?: string; -} - -export interface UserInfo { - id: string; - email: string; - name: string; - phone?: string; - role: UserRole; - createdAt: string; - updatedAt: string; -} - -export enum UserRole { - ADMIN = 'admin', - USER = 'user', - MERCHANT = 'merchant' -} - -export interface JWTPayload { - userId: string; - email: string; - role: UserRole; - iat: number; - exp: number; -} - -export interface AuthState { - isAuthenticated: boolean; - user: UserInfo | null; - token: string | null; - isLoading: boolean; -} - -export interface AuthContextType extends AuthState { - login: (credentials: LoginCredentials) => Promise; - register: (data: RegisterData) => Promise; - logout: () => void; - refreshToken: () => Promise; - hasRole: (role: UserRole) => boolean; - hasPermission: (permission: string) => boolean; -} - -// Type aliases for React Query hooks -export type User = UserInfo; - -export interface UserPermission { - id: string; - name: string; - description?: string; - resource: string; - action: string; -} \ No newline at end of file diff --git a/src/types/filter.ts b/src/types/filter.ts deleted file mode 100644 index bd8c0ce..0000000 --- a/src/types/filter.ts +++ /dev/null @@ -1,117 +0,0 @@ -export type DatePeriod = '1month' | '3months' | '6months' | 'custom'; - -export type TransactionType = 'all' | 'deposit' | 'withdrawal'; - -export type SortOrder = 'latest' | 'oldest'; - -export interface DateRange { - startDate: Date; - endDate: Date; -} - -export interface SearchFilter { - period: DatePeriod; - dateRange?: DateRange; - transactionType: TransactionType; - sortOrder: SortOrder; -} - -export interface SearchFilterOptions { - periods: Array<{ - value: DatePeriod; - label: string; - }>; - transactionTypes: Array<{ - value: TransactionType; - label: string; - }>; - sortOrders: Array<{ - value: SortOrder; - label: string; - }>; -} - -export interface SearchFilterModalProps { - isOpen: boolean; - onClose: () => void; - onConfirm: (filter: SearchFilter) => void; - initialFilter?: SearchFilter; - options?: Partial; -} - -// 기본 옵션 상수 -// locale 적용: label을 i18n key로 지정 -export const DEFAULT_FILTER_OPTIONS: SearchFilterOptions = { - periods: [ - { value: '1month', label: 'filter.periods.1month' }, - { value: '3months', label: 'filter.periods.3months' }, - { value: '6months', label: 'filter.periods.6months' }, - { value: 'custom', label: 'filter.periods.custom' } - ], - transactionTypes: [ - { value: 'all', label: 'filter.transactionTypes.all' }, - { value: 'deposit', label: 'filter.transactionTypes.deposit' }, - { value: 'withdrawal', label: 'filter.transactionTypes.withdrawal' } - ], - sortOrders: [ - { value: 'latest', label: 'filter.sortOrders.latest' }, - { value: 'oldest', label: 'filter.sortOrders.oldest' } - ] -}; - -// 기본 필터 값 -export const DEFAULT_SEARCH_FILTER: SearchFilter = { - period: '1month', - transactionType: 'all', - sortOrder: 'latest' -}; - -// 날짜 유틸리티 함수들 -export const getDateRangeFromPeriod = (period: DatePeriod): DateRange | null => { - const endDate = new Date(); - const startDate = new Date(); - - switch (period) { - case '1month': - startDate.setMonth(endDate.getMonth() - 1); - break; - case '3months': - startDate.setMonth(endDate.getMonth() - 3); - break; - case '6months': - startDate.setMonth(endDate.getMonth() - 6); - break; - case 'custom': - return null; // 사용자가 직접 설정 - default: - return null; - } - - return { startDate, endDate }; -}; - -export const formatDateRange = (dateRange: DateRange): string => { - const formatDate = (date: Date) => { - return date.toLocaleDateString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }); - }; - - return `${formatDate(dateRange.startDate)} ~ ${formatDate(dateRange.endDate)}`; -}; - -export const getFilterDisplayText = (filter: SearchFilter, t: (key: string) => string): string => { - const periodOption = DEFAULT_FILTER_OPTIONS.periods.find(p => p.value === filter.period); - const transactionOption = DEFAULT_FILTER_OPTIONS.transactionTypes.find(tt => tt.value === filter.transactionType); - const sortOption = DEFAULT_FILTER_OPTIONS.sortOrders.find(s => s.value === filter.sortOrder); - - const parts = [ - periodOption?.label ? t(periodOption.label) : undefined, - transactionOption?.label ? t(transactionOption.label) : undefined, - sortOption?.label ? t(sortOption.label) : undefined - ].filter(Boolean); - - return parts.join(' · '); -}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index fcc650e..0a58918 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1 @@ -export * from './auth'; -export * from './api'; -export * from './bridge'; -export * from './filter'; \ No newline at end of file +export * from './bridge'; \ No newline at end of file diff --git a/src/utils/api.ts b/src/utils/api.ts deleted file mode 100644 index a145888..0000000 --- a/src/utils/api.ts +++ /dev/null @@ -1,260 +0,0 @@ -import axios, { AxiosInstance, AxiosResponse, AxiosError, AxiosRequestConfig } from 'axios'; -import { ApiResponse, ApiErrorResponse, FileUploadProgress, FileUploadResponse } from '@/types'; -import { tokenManager } from './tokenManager'; -import config from '@/config'; -import { useAppBridge } from '@/hooks'; - - -class ApiClient { - private instance: AxiosInstance; - private isRefreshing = false; - private failedQueue: Array<{ - resolve: (value?: unknown) => void; - reject: (error?: unknown) => void; - }> = []; - private isNativeEnvironment: any; - private requestRefreshToken: any; - - constructor() { - this.instance = axios.create({ - baseURL: config.api.baseURL, - timeout: config.api.timeout, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - }); - - this.setupInterceptors(); - } - - private setupInterceptors(): void { - // 요청 인터셉터 - this.instance.interceptors.request.use( - (config) => { - const token = tokenManager.getAccessToken(); - console.log('setupInterceptors request ==> ', token) - - if(token){ - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => Promise.reject(error) - ); - - // 응답 인터셉터 - this.instance.interceptors.response.use( - (response: AxiosResponse) => response, - async (error: AxiosError) => { - console.log(' this.instance.interceptors.response' ); - const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }; - console.log('originalRequest ==> ', JSON.stringify(originalRequest)); - if (error.response?.status === 401 && !originalRequest._retry) { - if (this.isRefreshing) { - return new Promise((resolve, reject) => { - this.failedQueue.push({ resolve, reject }); - }).then(token => { - originalRequest.headers!.Authorization = `Bearer ${token}`; - return this.instance(originalRequest); - }).catch(err => { - return Promise.reject(err); - }); - } - - originalRequest._retry = true; - this.isRefreshing = true; - - console.log('refreshToken!!'); - /* - this.requestRefreshToken1().then((token) => { - console.log('requestRefreshToken +[' + JSON.stringify(token) + ']' ); - }); - - - try { - const newToken = await this.refreshAccessToken(); - this.processQueue(null, newToken); - originalRequest.headers!.Authorization = `Bearer ${newToken}`; - return this.instance(originalRequest); - } catch (refreshError) { - this.processQueue(refreshError, null); - tokenManager.clearTokens(); - // 로그인 페이지로 리디렉션 또는 로그아웃 처리 - window.location.href = '/login'; - return Promise.reject(refreshError); - } finally { - this.isRefreshing = false; - } - */ - } - - return Promise.reject(this.handleError(error)); - } - ); - } - - private processQueue(error: unknown, token: string | null): void { - this.failedQueue.forEach(({ resolve, reject }) => { - if (error) { - reject(error); - } else { - resolve(token); - } - }); - - this.failedQueue = []; - } - /* - private async requestRefreshAccessToken(): Promise { - const { - isNativeEnvironment, - requestRefreshToken - } = useAppBridge(); - - - } - */ - - private async refreshAccessToken(): Promise { - const refreshToken = tokenManager.getRefreshToken(); - if (!refreshToken) { - throw new Error('No refresh token available'); - } - - try { - const response = await axios.post(`${config.api.baseURL}/auth/refresh`, { - refresh_token: refreshToken - }); - - const { access_token, refresh_token: newRefreshToken } = response.data.data; - tokenManager.setTokens(access_token, newRefreshToken); - return access_token; - } catch { - throw new Error('Token refresh failed'); - } - } - - private handleError(error: AxiosError): ApiErrorResponse { - const errorResponse: ApiErrorResponse = { - success: false, - error: { - code: 'UNKNOWN_ERROR', - message: '알 수 없는 오류가 발생했습니다.', - }, - timestamp: new Date().toISOString(), - }; - - if (error.response) { - // 서버 응답이 있는 경우 - const { status, data } = error.response; - errorResponse.error.code = `HTTP_${status}`; - - if (data && typeof data === 'object' && 'message' in data) { - errorResponse.error.message = (data as { message: string }).message; - } else { - errorResponse.error.message = this.getErrorMessage(status); - } - - errorResponse.error.details = data; - } else if (error.request) { - // 네트워크 오류 - errorResponse.error.code = 'NETWORK_ERROR'; - errorResponse.error.message = '네트워크 연결을 확인해주세요.'; - } else { - // 요청 설정 오류 - errorResponse.error.code = 'REQUEST_ERROR'; - errorResponse.error.message = '요청 처리 중 오류가 발생했습니다.'; - } - - return errorResponse; - } - - private getErrorMessage(status: number): string { - const messages: Record = { - 400: '잘못된 요청입니다.', - 401: '인증이 필요합니다.', - 403: '접근 권한이 없습니다.', - 404: '요청한 리소스를 찾을 수 없습니다.', - 409: '데이터 충돌이 발생했습니다.', - 422: '입력 데이터를 확인해주세요.', - 500: '서버 오류가 발생했습니다.', - 502: '서버 연결에 문제가 있습니다.', - 503: '서비스를 일시적으로 사용할 수 없습니다.', - }; - - return messages[status] || '알 수 없는 오류가 발생했습니다.'; - } - - async get(url: string, config?: AxiosRequestConfig): Promise> { - const response = await this.instance.get(url, config); - return response.data; - } - - async post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - const response = await this.instance.post(url, data, config); - return response.data; - } - - async put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - const response = await this.instance.put(url, data, config); - return response.data; - } - - async patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - const response = await this.instance.patch(url, data, config); - return response.data; - } - - async delete(url: string, config?: AxiosRequestConfig): Promise> { - const response = await this.instance.delete(url, config); - return response.data; - } - - async uploadFile( - url: string, - file: File, - onProgress?: (progress: FileUploadProgress) => void - ): Promise> { - const formData = new FormData(); - formData.append('file', file); - - const config: AxiosRequestConfig = { - headers: { - 'Content-Type': 'multipart/form-data', - }, - onUploadProgress: (progressEvent) => { - if (onProgress && progressEvent.total) { - const progress: FileUploadProgress = { - loaded: progressEvent.loaded, - total: progressEvent.total, - percentage: Math.round((progressEvent.loaded * 100) / progressEvent.total), - }; - onProgress(progress); - } - }, - }; - - const response = await this.instance.post(url, formData, config); - return response.data; - } - - async downloadFile(url: string, filename: string): Promise { - const response = await this.instance.get(url, { - responseType: 'blob', - }); - - const blob = new Blob([response.data]); - const downloadUrl = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = downloadUrl; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(downloadUrl); - } -} - -export const apiClient = new ApiClient(); -export default apiClient; \ No newline at end of file diff --git a/src/utils/appBridge.ts b/src/utils/appBridge.ts index ee19d73..bf3176f 100644 --- a/src/utils/appBridge.ts +++ b/src/utils/appBridge.ts @@ -5,7 +5,6 @@ import { DeviceInfo, ShareContent } from '@/types'; -import { LoginCredentials, UserInfo } from '@/types/auth'; class AppBridge { private static instance: AppBridge; @@ -114,10 +113,6 @@ class AppBridge { return this.sendMessage(BridgeMessageType.NAVIGATE_TO, { path }); } - async navigateToLogin(): Promise { - return this.sendMessage(BridgeMessageType.NAVIGATE_TO_LOGIN); - } - async closeWebView(): Promise { return this.sendMessage(BridgeMessageType.CLOSE_WEBVIEW); } @@ -165,12 +160,7 @@ class AppBridge { async shareContent(content: ShareContent): Promise { return this.sendMessage(BridgeMessageType.SHARE_CONTENT, content); } - - // 로그인 요청 - async login(credentials?: LoginCredentials): Promise { - return this.sendMessage(BridgeMessageType.LOGIN, credentials); - } - + // 로그아웃 요청 async logout(): Promise { return this.sendMessage(BridgeMessageType.LOGOUT); diff --git a/src/utils/auth.ts b/src/utils/auth.ts deleted file mode 100644 index 6b0984c..0000000 --- a/src/utils/auth.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { LoginCredentials, RegisterData, UserInfo, UserRole } from '@/types'; -import { apiClient } from './api'; -import { tokenManager } from './tokenManager'; - -export class AuthService { - private static instance: AuthService; - - private constructor() {} - - static getInstance(): AuthService { - if (!AuthService.instance) { - AuthService.instance = new AuthService(); - } - return AuthService.instance; - } - - async login(credentials: LoginCredentials): Promise<{ - user: UserInfo; - accessToken: string; - refreshToken: string; - }> { - const response = await apiClient.post<{ - user: UserInfo; - access_token: string; - refresh_token: string; - }>('/auth/login', credentials); - - const { user, access_token, refresh_token } = response.data; - tokenManager.setTokens(access_token, refresh_token); - - return { - user, - accessToken: access_token, - refreshToken: refresh_token, - }; - } - - async register(data: RegisterData): Promise<{ - user: UserInfo; - accessToken: string; - refreshToken: string; - }> { - const response = await apiClient.post<{ - user: UserInfo; - access_token: string; - refresh_token: string; - }>('/auth/register', data); - - const { user, access_token, refresh_token } = response.data; - tokenManager.setTokens(access_token, refresh_token); - - return { - user, - accessToken: access_token, - refreshToken: refresh_token, - }; - } - - async logout(): Promise { - try { - await apiClient.post('/auth/logout'); - } catch (error) { - // 로그아웃 API 호출 실패해도 로컬 토큰은 삭제 - console.error('Logout API call failed:', error); - } finally { - tokenManager.clearTokens(); - } - } - - async refreshToken(): Promise { - const refreshToken = tokenManager.getRefreshToken(); - if (!refreshToken) { - throw new Error('No refresh token available'); - } - - const response = await apiClient.post<{ - access_token: string; - refresh_token: string; - }>('/auth/refresh', { - refresh_token: refreshToken, - }); - - const { access_token, refresh_token: newRefreshToken } = response.data; - tokenManager.setTokens(access_token, newRefreshToken); - - return access_token; - } - - async getCurrentUser(): Promise { - const response = await apiClient.get('/auth/me'); - return response.data; - } - - async updateProfile(data: Partial): Promise { - const response = await apiClient.put('/auth/profile', data); - return response.data; - } - - async changePassword(currentPassword: string, newPassword: string): Promise { - await apiClient.post('/auth/change-password', { - current_password: currentPassword, - new_password: newPassword, - }); - } - - async forgotPassword(email: string): Promise { - await apiClient.post('/auth/forgot-password', { email }); - } - - async resetPassword(token: string, password: string): Promise { - await apiClient.post('/auth/reset-password', { - token, - password, - }); - } - - isAuthenticated(): boolean { - return tokenManager.hasValidTokens(); - } - - getCurrentUserFromToken(): UserInfo | null { - return tokenManager.getUserInfoFromToken(); - } - - hasRole(role: UserRole): boolean { - const user = this.getCurrentUserFromToken(); - return user?.role === role; - } - - hasPermission(permission: string): boolean { - const user = this.getCurrentUserFromToken(); - if (!user) return false; - - // 권한 체크 로직 구현 - const permissions = this.getRolePermissions(user.role); - return permissions.includes(permission); - } - - private getRolePermissions(role: UserRole): string[] { - const permissionMap: Record = { - [UserRole.ADMIN]: [ - 'user:read', - 'user:write', - 'user:delete', - 'payment:read', - 'payment:write', - 'payment:refund', - 'system:config', - ], - [UserRole.MERCHANT]: [ - 'payment:read', - 'payment:write', - 'payment:refund', - 'profile:read', - 'profile:write', - ], - [UserRole.USER]: [ - 'payment:read', - 'profile:read', - 'profile:write', - ], - }; - - return permissionMap[role] || []; - } - - async verifyEmail(token: string): Promise { - await apiClient.post('/auth/verify-email', { token }); - } - - async resendVerificationEmail(): Promise { - await apiClient.post('/auth/resend-verification'); - } - - shouldRefreshToken(): boolean { - return tokenManager.shouldRefreshToken(); - } - - getAccessToken(): string | null { - return tokenManager.getAccessToken(); - } - - getRefreshToken(): string | null { - return tokenManager.getRefreshToken(); - } -} - -export const authService = AuthService.getInstance(); \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 83acdfe..6764c45 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1 @@ -export { apiClient } from './api'; -export { authService } from './auth'; -export { tokenManager } from './tokenManager'; export { appBridge } from './appBridge'; \ No newline at end of file diff --git a/src/utils/jwtDecoder.ts b/src/utils/jwtDecoder.ts deleted file mode 100644 index 6fd9498..0000000 --- a/src/utils/jwtDecoder.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { JWTPayload } from '@/types'; - -/** - * Browser-compatible JWT decoder - * Only supports decoding (reading) JWT tokens, not signing/verifying - */ -export class JWTDecoder { - /** - * Decode a JWT token to extract the payload - * @param token - The JWT token string - * @returns The decoded payload or null if invalid - */ - static decode(token: string): JWTPayload | null { - try { - if (!token || typeof token !== 'string') { - return null; - } - - // JWT tokens have 3 parts separated by dots: header.payload.signature - const parts = token.split('.'); - if (parts.length !== 3) { - return null; - } - - // Get the payload (second part) - const payload = parts[1]; - if (!payload) { - return null; - } - - // Decode base64url - const decoded = this.base64UrlDecode(payload); - - // Parse JSON - return JSON.parse(decoded) as JWTPayload; - } catch (error) { - console.error('JWT decode error:', error); - return null; - } - } - - /** - * Check if a JWT token is expired - * @param token - The JWT token string - * @returns true if expired, false if valid - */ - static isExpired(token: string): boolean { - try { - const payload = this.decode(token); - if (!payload || !payload.exp) { - return true; - } - - const currentTime = Date.now() / 1000; - return payload.exp < currentTime; - } catch { - return true; - } - } - - /** - * Get the expiration timestamp from a JWT token - * @param token - The JWT token string - * @returns The expiration timestamp or null if not found - */ - static getExpiration(token: string): number | null { - try { - const payload = this.decode(token); - return payload?.exp || null; - } catch { - return null; - } - } - - /** - * Get time until token expiration in seconds - * @param token - The JWT token string - * @returns Seconds until expiration, 0 if expired or invalid - */ - static getTimeUntilExpiration(token: string): number { - try { - const expirationTime = this.getExpiration(token); - if (!expirationTime) return 0; - - const currentTime = Date.now() / 1000; - const timeUntilExpiration = expirationTime - currentTime; - return Math.max(0, timeUntilExpiration); - } catch { - return 0; - } - } - - /** - * Decode base64url string to regular string - * @param str - base64url encoded string - * @returns decoded string - */ - private static base64UrlDecode(str: string): string { - // Convert base64url to base64 - let base64 = str.replace(/-/g, '+').replace(/_/g, '/'); - - // Add padding if needed - while (base64.length % 4) { - base64 += '='; - } - - // Decode base64 - const decoded = atob(base64); - - // Convert to UTF-8 - return decodeURIComponent( - decoded - .split('') - .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) - .join('') - ); - } -} - -// Export a default instance for convenience -export const jwtDecoder = JWTDecoder; \ No newline at end of file diff --git a/src/utils/safeArea.ts b/src/utils/safeArea.ts deleted file mode 100644 index 7047b9a..0000000 --- a/src/utils/safeArea.ts +++ /dev/null @@ -1,17 +0,0 @@ -// safeArea.ts -export function setSafeAreaInsets(top: number, right: number, bottom: number, left: number) { - const root = document.documentElement; - root.style.setProperty("--safe-area-inset-top", `${top}px`); - root.style.setProperty("--safe-area-inset-right", `${right}px`); - root.style.setProperty("--safe-area-inset-bottom", `${bottom}px`); - root.style.setProperty("--safe-area-inset-left", `${left}px`); - } - - // 전역에서 호출할 수 있게 window에 바인딩 (Android WebView 네이티브에서 실행할 수 있도록) - declare global { - interface Window { - setSafeAreaInsets: typeof setSafeAreaInsets; - } - } - - window.setSafeAreaInsets = setSafeAreaInsets; \ No newline at end of file diff --git a/src/utils/tokenManager.ts b/src/utils/tokenManager.ts deleted file mode 100644 index 2fd2755..0000000 --- a/src/utils/tokenManager.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { JWTPayload, UserInfo } from '@/types'; -import { appBridge } from './appBridge'; -import { jwtDecoder } from './jwtDecoder'; - -const ACCESS_TOKEN_KEY = 'nice_access_token'; -const REFRESH_TOKEN_KEY = 'nice_refresh_token'; - -export class TokenManager { - private static instance: TokenManager; - - private constructor() {} - - static getInstance(): TokenManager { - if (!TokenManager.instance) { - TokenManager.instance = new TokenManager(); - } - return TokenManager.instance; - } - - setTokens(accessToken: string, refreshToken: string): void { - localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); - localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); - this.saveTokensToAppBridge(accessToken, refreshToken); - } - - getAccessToken(): string | null { - console.log('getAccessToken') - return localStorage.getItem(ACCESS_TOKEN_KEY); - } - - getRefreshToken(): string | null { - return localStorage.getItem(REFRESH_TOKEN_KEY); - } - - clearTokens(): void { - localStorage.removeItem(ACCESS_TOKEN_KEY); - localStorage.removeItem(REFRESH_TOKEN_KEY); - } - - isTokenExpired(token: string): boolean { - return jwtDecoder.isExpired(token); - } - - isAccessTokenExpired(): boolean { - const token = this.getAccessToken(); - if (!token) return true; - return this.isTokenExpired(token); - } - - isRefreshTokenExpired(): boolean { - const token = this.getRefreshToken(); - if (!token) return true; - return this.isTokenExpired(token); - } - - decodeToken(token: string): JWTPayload | null { - return jwtDecoder.decode(token); - } - - getUserInfoFromToken(token?: string): UserInfo | null { - const accessToken = token || this.getAccessToken(); - if (!accessToken) return null; - - const decoded = this.decodeToken(accessToken); - if (!decoded) return null; - - return { - id: decoded.userId, - email: decoded.email, - name: '', // 토큰에서 이름을 가져오지 않으므로 빈 문자열 - role: decoded.role, - createdAt: '', - updatedAt: '' - }; - } - - getTokenExpirationTime(token: string): number | null { - return jwtDecoder.getExpiration(token); - } - - getTimeUntilExpiration(token: string): number { - return jwtDecoder.getTimeUntilExpiration(token); - } - - shouldRefreshToken(): boolean { - const accessToken = this.getAccessToken(); - if (!accessToken) return false; - - const timeUntilExpiration = this.getTimeUntilExpiration(accessToken); - // 토큰이 5분 이내에 만료되면 갱신 - const REFRESH_BUFFER_SECONDS = 5 * 60; // 5 minutes - return timeUntilExpiration < REFRESH_BUFFER_SECONDS; - } - - hasValidTokens(): boolean { - const accessToken = this.getAccessToken(); - const refreshToken = this.getRefreshToken(); - - if (!accessToken || !refreshToken) return false; - - // 리프레시 토큰이 유효하면 액세스 토큰은 갱신 가능 - return !this.isRefreshTokenExpired(); - } - - async refreshTokens(): Promise<{accessToken: string, refreshToken: string}> { - const refreshToken = this.getRefreshToken(); - if (!refreshToken) { - throw new Error('No refresh token available'); - } - - const response = await fetch('http://3.35.79.250:8090/auth/v1/refresh', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ refreshToken }), - }); - - if (!response.ok) { - throw new Error(`Token refresh failed: ${response.status}`); - } - - const data = await response.json(); - const newAccessToken = data.accessToken; - const newRefreshToken = data.refreshToken; - - this.setTokens(newAccessToken, newRefreshToken); - - return { accessToken: newAccessToken, refreshToken: newRefreshToken }; - } - - async makeAuthenticatedRequest(url: string, options: RequestInit = {}): Promise { - let accessToken = this.getAccessToken(); - - if (!accessToken || this.isAccessTokenExpired()) { - try { - const refreshed = await this.refreshTokens(); - accessToken = refreshed.accessToken; - } catch (error) { - this.clearTokens(); - throw new Error('Authentication failed - please log in again'); - } - } - - const requestOptions = { - ...options, - headers: { - ...options.headers, - 'Authorization': `Bearer ${accessToken}`, - }, - }; - - const response = await fetch(url, requestOptions); - - if (response.status === 401) { - try { - const refreshed = await this.refreshTokens(); - const retryOptions = { - ...requestOptions, - headers: { - ...requestOptions.headers, - 'Authorization': `Bearer ${refreshed.accessToken}`, - }, - }; - return await fetch(url, retryOptions); - } catch (error) { - this.clearTokens(); - throw new Error('Authentication failed - please log in again'); - } - } - - return response; - } - - private async saveTokensToAppBridge(accessToken: string, refreshToken: string): Promise { - try { - if (appBridge.isNativeEnvironment()) { - await Promise.all([ - appBridge.setStorage(ACCESS_TOKEN_KEY, accessToken), - appBridge.setStorage(REFRESH_TOKEN_KEY, refreshToken) - ]); - } - } catch (error) { - console.error('Failed to save tokens to AppBridge:', error); - } - } -} - -export const tokenManager = TokenManager.getInstance(); \ No newline at end of file