From 2bbae7eed3600459297bfebd22f78bf0ce12b6ca Mon Sep 17 00:00:00 2001 From: "focp212@naver.com" Date: Fri, 12 Sep 2025 15:21:52 +0900 Subject: [PATCH] filter-calendar --- src/app/index.tsx | 2 + .../transaction/ui/filter/billing-filter.tsx | 80 +----- src/shared/lib/appBridge.ts | 255 ++++++++++++++++++ src/shared/ui/calendar/filter-calendar.tsx | 128 +++++++++ src/shared/ui/calendar/index.tsx | 43 --- src/shared/ui/calendar/nice-calendar.tsx | 31 +++ 6 files changed, 424 insertions(+), 115 deletions(-) create mode 100644 src/shared/lib/appBridge.ts create mode 100644 src/shared/ui/calendar/filter-calendar.tsx delete mode 100644 src/shared/ui/calendar/index.tsx create mode 100644 src/shared/ui/calendar/nice-calendar.tsx diff --git a/src/app/index.tsx b/src/app/index.tsx index a50dd1b..151fd5c 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -5,10 +5,12 @@ import { App } from './app'; import { initAxios } from '@/shared/configs/axios'; import { initSentry } from '@/shared/configs/sentry'; import { AppProvider } from './providers/app-provider'; +// import appBridge from '@/shared/lib/appBridge'; const initApp = async () => { initAxios(); initSentry(); + // appBridge.sendMessage('login') }; (async () => { diff --git a/src/entities/transaction/ui/filter/billing-filter.tsx b/src/entities/transaction/ui/filter/billing-filter.tsx index 5e67460..49567f0 100644 --- a/src/entities/transaction/ui/filter/billing-filter.tsx +++ b/src/entities/transaction/ui/filter/billing-filter.tsx @@ -1,3 +1,4 @@ +import moment from 'moment'; import { ChangeEvent, useState } from 'react'; import { motion } from 'framer-motion'; import { IMAGE_ROOT } from '@/shared/constants/common'; @@ -9,8 +10,7 @@ import { BillingSearchType } from '../../model/types'; import { FilterDateOptions } from '@/entities/common/model/types'; -import moment from 'moment'; -import NiceCalendar from '@/shared/ui/calendar'; +import { FilterCalendar } from '@/shared/ui/calendar/filter-calendar'; export const BillingFilter = ({ filterOn, @@ -163,73 +163,13 @@ export const BillingFilter = ({ /> - +
-
조회기간
-
-
- setFilterDate(FilterDateOptions.Today) } - >당일 - setFilterDate(FilterDateOptions.Week) } - >일주일 - setFilterDate(FilterDateOptions.Month) } - >1개월 - setFilterDate(FilterDateOptions.Input) } - >직접입력 -
-
-
- - -
- ~ -
- - -
-
-
-
- -
요청상태
@@ -327,10 +267,6 @@ export const BillingFilter = ({ >적용
- ); diff --git a/src/shared/lib/appBridge.ts b/src/shared/lib/appBridge.ts new file mode 100644 index 0000000..d335caf --- /dev/null +++ b/src/shared/lib/appBridge.ts @@ -0,0 +1,255 @@ +import { + AppBridgeMessage, + AppBridgeResponse, + BridgeMessageType, + DeviceInfo, + ShareContent +} from '@/types'; +import { LoginCredentials, UserInfo } from '@/types/auth'; + +class AppBridge { + private static instance: AppBridge; + private messageId = 0; + private pendingCallbacks = new Map void>(); + private responseListeners: Set<(response: AppBridgeResponse) => void> = new Set(); + + private constructor() { + this.setupMessageListener(); + } + + // 외부에서 네이티브 응답을 구독할 수 있도록 리스너 등록/해제 메서드 추가 + public addResponseListener(listener: (response: AppBridgeResponse) => void) { + this.responseListeners.add(listener); + } + + public removeResponseListener(listener: (response: AppBridgeResponse) => void) { + this.responseListeners.delete(listener); + } + + static getInstance(): AppBridge { + if (!AppBridge.instance) { + AppBridge.instance = new AppBridge(); + } + return AppBridge.instance; + } + + private setupMessageListener(): void { + window.addEventListener('message', (event) => { + try { + const response: AppBridgeResponse & { callbackId?: string } = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + console.log('response', response); + if (response.callbackId && this.pendingCallbacks.has(response.callbackId)) { + const callback = this.pendingCallbacks.get(response.callbackId); + if (callback) { + callback(response); + this.pendingCallbacks.delete(response.callbackId); + } + } + // 등록된 리스너들에게 모든 응답 전달 + this.responseListeners.forEach(listener => listener(response)); + } catch (error) { + console.error('Failed to parse bridge message:', error); + } + }); + } + + private generateMessageId(): string { + return `bridge_${++this.messageId}_${Date.now()}`; + } + + private sendMessage(type: BridgeMessageType, data?: unknown): Promise { + return new Promise((resolve, reject) => { + const callbackId = this.generateMessageId(); + const message: AppBridgeMessage & { callbackId: string } = { + type, + data, + callbackId + }; + + this.pendingCallbacks.set(callbackId, (response: AppBridgeResponse) => { + if (response.success) { + resolve(response.data); + } else { + reject(new Error(response.error || 'Bridge call failed')); + } + }); + + // Android WebView 인터페이스 + if (window.AndroidBridge && window.AndroidBridge.postMessage) { + console.log('Android postMessage', message); + window.AndroidBridge.postMessage(JSON.stringify(message)); + return; + } + + // iOS WKWebView 인터페이스 + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) { + console.log('iOS postMessage', message); + window.webkit.messageHandlers.bridge.postMessage(message); + return; + } + + // 네이티브 환경이 아닌 경우 에러 발생 + setTimeout(() => { + this.pendingCallbacks.delete(callbackId); + reject(new Error('Native bridge not available')); + }, 100); + }); + } + + // 앱 정보 관련 + async getAppInfo(): Promise<{ version: string; buildNumber: string }> { + return this.sendMessage(BridgeMessageType.GET_APP_INFO); + } + + async getDeviceInfo(): Promise { + return this.sendMessage(BridgeMessageType.GET_DEVICE_INFO); + } + + // 네비게이션 관련 + async navigateBack(): Promise { + return this.sendMessage(BridgeMessageType.NAVIGATE_BACK); + } + + async navigateTo(path: string): Promise { + 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); + } + + // 알림 관련 + async showToast(message: string, duration: number = 3000): Promise { + return this.sendMessage(BridgeMessageType.SHOW_TOAST, { message, duration }); + } + + async showAlert(title: string, message: string): Promise { + return this.sendMessage(BridgeMessageType.SHOW_ALERT, { title, message }); + } + + async showConfirm(title: string, message: string): Promise { + return this.sendMessage(BridgeMessageType.SHOW_CONFIRM, { title, message }); + } + + // 저장소 관련 + async setStorage(key: string, value: unknown): Promise { + return this.sendMessage(BridgeMessageType.SET_STORAGE, { key, value: JSON.stringify(value) }); + } + + async getStorage(key: string): Promise { + try { + const result = await this.sendMessage(BridgeMessageType.GET_STORAGE, { key }); + return result ? JSON.parse(result) : null; + } catch { + return null; + } + } + + async removeStorage(key: string): Promise { + return this.sendMessage(BridgeMessageType.REMOVE_STORAGE, { key }); + } + + // 공유 관련 + 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); + } + + // 언어 설정 + async setLanguage(language: string): Promise { + return this.sendMessage(BridgeMessageType.SET_LANGUAGE, { language }); + } + + async getLanguage(): Promise { + return this.sendMessage(BridgeMessageType.GET_LANGUAGE); + } + + // 메시지 카운트 업데이트 + async updateMessageCount(count: number): Promise { + return this.sendMessage(BridgeMessageType.UPDATE_MESSAGE_COUNT, { count }); + } + + // 네이티브 환경 체크 + isNativeEnvironment(): boolean { + return !!( + (window.AndroidBridge && window.AndroidBridge.postMessage) || + (window.Android && window.Android.processMessage) || + (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) + ); + } + + isAndroid(): boolean { + return !!( + (window.AndroidBridge && window.AndroidBridge.postMessage) || + (window.Android && window.Android.processMessage) + ); + } + + isIOS(): boolean { + return !!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge); + } + + // 에러 핸들링을 위한 래퍼 메소드들 + async safeCall( + bridgeMethod: () => Promise, + fallback?: T, + onError?: (error: Error) => void + ): Promise { + try { + return await bridgeMethod(); + } catch (error) { + console.error('Bridge call failed:', error); + if (onError) { + onError(error as Error); + } + return fallback; + } + } + + // 타임아웃을 가진 브리지 호출 + async callWithTimeout( + bridgeMethod: () => Promise, + timeout: number = 5000 + ): Promise { + return Promise.race([ + bridgeMethod(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Bridge call timeout')), timeout) + ) + ]); + } +} + +// 글로벌 타입 선언 확장 +declare global { + interface Window { + AndroidBridge?: { + postMessage: (message: string) => void; + }; + Android?: { + processMessage: (message: string) => void; + }; + webkit?: { + messageHandlers?: { + bridge?: { + postMessage: (message: unknown) => void; + }; + }; + }; + } +} + +export const appBridge = AppBridge.getInstance(); +export default appBridge; \ No newline at end of file diff --git a/src/shared/ui/calendar/filter-calendar.tsx b/src/shared/ui/calendar/filter-calendar.tsx new file mode 100644 index 0000000..2a097c7 --- /dev/null +++ b/src/shared/ui/calendar/filter-calendar.tsx @@ -0,0 +1,128 @@ +import moment from 'moment'; +import { ChangeEvent, useState } from 'react'; +import { FilterDateOptions } from '@/entities/common/model/types'; +import { IMAGE_ROOT } from '@/shared/constants/common'; +import NiceCalendar from './nice-calendar'; + +interface FilterCalendarProps { + startDate: string; + endDate: string; + setStartDate: (startDate: string) => void; + setEndDate: (endDate: string) => void; +}; + +export const FilterCalendar = ({ + startDate, + endDate, + setStartDate, + setEndDate +}: FilterCalendarProps) => { + const [dateReadOnly, setDateReadyOnly] = useState(true); + const [filterDateOptionsBtn, setFilterDateOptionsBtn] = useState(FilterDateOptions.Input); + const [filterStartDate, setFilterStartDate] = useState(startDate); + const [filterEndDate, setFilterEndDate] = useState(endDate); + const [calendarOpen, setCalendarOpen] = useState(false); + + const setFilterDate = (dateOptions: FilterDateOptions) => { + if(dateOptions === FilterDateOptions.Today){ + setFilterStartDate(moment().format('YYYY-MM-DD')); + setFilterEndDate(moment().format('YYYY-MM-DD')); + setDateReadyOnly(true); + setFilterDateOptionsBtn(FilterDateOptions.Today); + } + else if(dateOptions === FilterDateOptions.Week){ + setFilterStartDate(moment().subtract(1, 'week').format('YYYY-MM-DD')); + setFilterEndDate(moment().format('YYYY-MM-DD')); + setDateReadyOnly(true); + setFilterDateOptionsBtn(FilterDateOptions.Week); + } + else if(dateOptions === FilterDateOptions.Month){ + setFilterStartDate(moment().subtract(1, 'month').format('YYYY-MM-DD')); + setFilterEndDate(moment().format('YYYY-MM-DD')); + setDateReadyOnly(true); + setFilterDateOptionsBtn(FilterDateOptions.Month); + } + else if(dateOptions === FilterDateOptions.Input){ + setDateReadyOnly(false); + setFilterDateOptionsBtn(FilterDateOptions.Input); + } + }; + + const onClickToOpenCalendar = () => { + if(!dateReadOnly){ + setCalendarOpen(true); + } + else{ + setCalendarOpen(false); + } + }; + + return ( + <> +
+
조회기간
+
+
+ setFilterDate(FilterDateOptions.Today) } + >당일 + setFilterDate(FilterDateOptions.Week) } + >일주일 + setFilterDate(FilterDateOptions.Month) } + >1개월 + setFilterDate(FilterDateOptions.Input) } + >직접입력 +
+
+
+ + +
+ ~ +
+ + +
+
+
+
+ + ); +}; \ No newline at end of file diff --git a/src/shared/ui/calendar/index.tsx b/src/shared/ui/calendar/index.tsx deleted file mode 100644 index 43c040c..0000000 --- a/src/shared/ui/calendar/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import moment from 'moment'; -import styled from "styled-components"; -import { useState } from 'react'; -import Calendar from 'react-calendar'; -import 'react-calendar/dist/Calendar.css'; - -const NiceCalendar = ({ - setNewDate -}: any) => { - const [calendarDate, setCalendarDate] = useState(moment().format('YYYY-MM-DD')); - const [isOpen, setIsOpen] = useState(false); - - const onchangeToDate = (selectedDate: any) => { - setNewDate(selectedDate) - setIsOpen(false); - }; - - return ( - <> - - - - - - - ); -}; - -const CalendarContainer = styled.div` - position: relative; -`; -const CalendarWrapper: any = styled.div` - z-index: 11; - position: absolute; - top: 100%; - left: 0; - display: ${(props: any) => ((props.isOpen)? 'block': 'none')}; -`; - -export default NiceCalendar; \ No newline at end of file diff --git a/src/shared/ui/calendar/nice-calendar.tsx b/src/shared/ui/calendar/nice-calendar.tsx new file mode 100644 index 0000000..da6f8ed --- /dev/null +++ b/src/shared/ui/calendar/nice-calendar.tsx @@ -0,0 +1,31 @@ +import moment from 'moment'; +import styled from "styled-components"; +import { useState } from 'react'; +import Calendar from 'react-calendar'; +import 'react-calendar/dist/Calendar.css'; + +const NiceCalendar = ({ + calendarOpen, + setNewDate +}: any) => { + const [calendarDate, setCalendarDate] = useState(moment().format('YYYY-MM-DD')); + const [isOpen, setIsOpen] = useState(calendarOpen); + + const onchangeToDate = (selectedDate: any) => { + setNewDate(selectedDate) + setIsOpen(false); + }; + + return ( + <> + + + + + ); +}; + +export default NiceCalendar; \ No newline at end of file