filter-calendar
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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 = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FilterCalendar
|
||||
startDate={ filterStartDate }
|
||||
endDate={ filterEndDate }
|
||||
setStartDate={ setFilterStartDate }
|
||||
setEndDate={ setFilterEndDate }
|
||||
></FilterCalendar>
|
||||
<div className="opt-field">
|
||||
<div className="opt-label">조회기간</div>
|
||||
<div className="opt-controls col below h36">
|
||||
<div className="chip-row">
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Today)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Today) }
|
||||
>당일</span>
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Week)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Week) }
|
||||
>일주일</span>
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Month)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Month) }
|
||||
>1개월</span>
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Input)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Input) }
|
||||
>직접입력</span>
|
||||
</div>
|
||||
<div className="range-row">
|
||||
<div className="input-wrapper date">
|
||||
<input
|
||||
className="date-input"
|
||||
type="text"
|
||||
placeholder="날짜 선택"
|
||||
value={ filterStartDate }
|
||||
readOnly={ true }
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="date-btn"
|
||||
onClick={ () => onClickToOpenCalendar() }
|
||||
>
|
||||
<img
|
||||
src={ IMAGE_ROOT + '/ico_date.svg' }
|
||||
alt="날짜 선택"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span className="beetween">~</span>
|
||||
<div className="input-wrapper date">
|
||||
<input
|
||||
className="date-input"
|
||||
type="text"
|
||||
placeholder="날짜 선택"
|
||||
value={ filterEndDate }
|
||||
readOnly={ true }
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="date-btn"
|
||||
onClick={ () => onClickToOpenCalendar() }
|
||||
>
|
||||
<img
|
||||
src={ IMAGE_ROOT + '/ico_date.svg' }
|
||||
alt="날짜 선택"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="opt-field">
|
||||
<div className="opt-label">요청상태</div>
|
||||
<div className="opt-controls col below h36">
|
||||
<div className="chip-row">
|
||||
@@ -327,10 +267,6 @@ export const BillingFilter = ({
|
||||
>적용</button>
|
||||
</div>
|
||||
</div>
|
||||
<NiceCalendar
|
||||
calendarOpen={ calendarOpen }
|
||||
setNewDate={ setNewDate }
|
||||
></NiceCalendar>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
|
||||
255
src/shared/lib/appBridge.ts
Normal file
255
src/shared/lib/appBridge.ts
Normal file
@@ -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<string, (response: AppBridgeResponse) => 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<T>(type: BridgeMessageType, data?: unknown): Promise<T> {
|
||||
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<DeviceInfo> {
|
||||
return this.sendMessage(BridgeMessageType.GET_DEVICE_INFO);
|
||||
}
|
||||
|
||||
// 네비게이션 관련
|
||||
async navigateBack(): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.NAVIGATE_BACK);
|
||||
}
|
||||
|
||||
async navigateTo(path: string): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.NAVIGATE_TO, { path });
|
||||
}
|
||||
|
||||
async navigateToLogin(): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.NAVIGATE_TO_LOGIN);
|
||||
}
|
||||
|
||||
async closeWebView(): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.CLOSE_WEBVIEW);
|
||||
}
|
||||
|
||||
// 알림 관련
|
||||
async showToast(message: string, duration: number = 3000): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.SHOW_TOAST, { message, duration });
|
||||
}
|
||||
|
||||
async showAlert(title: string, message: string): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.SHOW_ALERT, { title, message });
|
||||
}
|
||||
|
||||
async showConfirm(title: string, message: string): Promise<boolean> {
|
||||
return this.sendMessage(BridgeMessageType.SHOW_CONFIRM, { title, message });
|
||||
}
|
||||
|
||||
// 저장소 관련
|
||||
async setStorage(key: string, value: unknown): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.SET_STORAGE, { key, value: JSON.stringify(value) });
|
||||
}
|
||||
|
||||
async getStorage<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const result = await this.sendMessage<string>(BridgeMessageType.GET_STORAGE, { key });
|
||||
return result ? JSON.parse(result) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async removeStorage(key: string): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.REMOVE_STORAGE, { key });
|
||||
}
|
||||
|
||||
// 공유 관련
|
||||
async shareContent(content: ShareContent): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.SHARE_CONTENT, content);
|
||||
}
|
||||
|
||||
// 로그인 요청
|
||||
async login(credentials?: LoginCredentials): Promise<UserInfo> {
|
||||
return this.sendMessage(BridgeMessageType.LOGIN, credentials);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.LOGOUT);
|
||||
}
|
||||
|
||||
// 언어 설정
|
||||
async setLanguage(language: string): Promise<void> {
|
||||
return this.sendMessage(BridgeMessageType.SET_LANGUAGE, { language });
|
||||
}
|
||||
|
||||
async getLanguage(): Promise<string> {
|
||||
return this.sendMessage(BridgeMessageType.GET_LANGUAGE);
|
||||
}
|
||||
|
||||
// 메시지 카운트 업데이트
|
||||
async updateMessageCount(count: number): Promise<void> {
|
||||
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<T>(
|
||||
bridgeMethod: () => Promise<T>,
|
||||
fallback?: T,
|
||||
onError?: (error: Error) => void
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
return await bridgeMethod();
|
||||
} catch (error) {
|
||||
console.error('Bridge call failed:', error);
|
||||
if (onError) {
|
||||
onError(error as Error);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 타임아웃을 가진 브리지 호출
|
||||
async callWithTimeout<T>(
|
||||
bridgeMethod: () => Promise<T>,
|
||||
timeout: number = 5000
|
||||
): Promise<T> {
|
||||
return Promise.race([
|
||||
bridgeMethod(),
|
||||
new Promise<never>((_, 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;
|
||||
128
src/shared/ui/calendar/filter-calendar.tsx
Normal file
128
src/shared/ui/calendar/filter-calendar.tsx
Normal file
@@ -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<boolean>(true);
|
||||
const [filterDateOptionsBtn, setFilterDateOptionsBtn] = useState<FilterDateOptions>(FilterDateOptions.Input);
|
||||
const [filterStartDate, setFilterStartDate] = useState<string>(startDate);
|
||||
const [filterEndDate, setFilterEndDate] = useState<string>(endDate);
|
||||
const [calendarOpen, setCalendarOpen] = useState<boolean>(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 (
|
||||
<>
|
||||
<div className="opt-field">
|
||||
<div className="opt-label">조회기간</div>
|
||||
<div className="opt-controls col below h36">
|
||||
<div className="chip-row">
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Today)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Today) }
|
||||
>당일</span>
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Week)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Week) }
|
||||
>일주일</span>
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Month)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Month) }
|
||||
>1개월</span>
|
||||
<span
|
||||
className={ `keyword-tag ${(filterDateOptionsBtn === FilterDateOptions.Input)? 'active': ''}` }
|
||||
onClick={ () => setFilterDate(FilterDateOptions.Input) }
|
||||
>직접입력</span>
|
||||
</div>
|
||||
<div className="range-row">
|
||||
<div className="input-wrapper date">
|
||||
<input
|
||||
className="date-input"
|
||||
type="text"
|
||||
placeholder="날짜 선택"
|
||||
value={ filterStartDate }
|
||||
readOnly={ true }
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="date-btn"
|
||||
onClick={ () => onClickToOpenCalendar() }
|
||||
>
|
||||
<img
|
||||
src={ IMAGE_ROOT + '/ico_date.svg' }
|
||||
alt="날짜 선택"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span className="beetween">~</span>
|
||||
<div className="input-wrapper date">
|
||||
<input
|
||||
className="date-input"
|
||||
type="text"
|
||||
placeholder="날짜 선택"
|
||||
value={ filterEndDate }
|
||||
readOnly={ true }
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="date-btn"
|
||||
onClick={ () => onClickToOpenCalendar() }
|
||||
>
|
||||
<img
|
||||
src={ IMAGE_ROOT + '/ico_date.svg' }
|
||||
alt="날짜 선택"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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<string>(moment().format('YYYY-MM-DD'));
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const onchangeToDate = (selectedDate: any) => {
|
||||
setNewDate(selectedDate)
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CalendarContainer>
|
||||
<CalendarWrapper isOpen={isOpen}>
|
||||
<Calendar
|
||||
onChange={ onchangeToDate }
|
||||
value={ calendarDate }
|
||||
></Calendar>
|
||||
</CalendarWrapper>
|
||||
</CalendarContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
31
src/shared/ui/calendar/nice-calendar.tsx
Normal file
31
src/shared/ui/calendar/nice-calendar.tsx
Normal file
@@ -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<string>(moment().format('YYYY-MM-DD'));
|
||||
const [isOpen, setIsOpen] = useState<boolean>(calendarOpen);
|
||||
|
||||
const onchangeToDate = (selectedDate: any) => {
|
||||
setNewDate(selectedDate)
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Calendar
|
||||
onChange={ onchangeToDate }
|
||||
value={ calendarDate }
|
||||
></Calendar>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NiceCalendar;
|
||||
Reference in New Issue
Block a user