- 안드로이드 알림 설정 AppBridge 추가

- KeyIn Request 필드 수정
This commit is contained in:
HyeonJongKim
2025-10-28 11:33:24 +09:00
parent feaaac73f7
commit e125a73228
16 changed files with 298 additions and 113 deletions

View File

@@ -105,6 +105,7 @@
"@tanstack/router-plugin": "^1.131.41", "@tanstack/router-plugin": "^1.131.41",
"@types/react": "^19.1.12", "@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0", "@typescript-eslint/parser": "^8.43.0",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",

View File

@@ -23,7 +23,9 @@ export interface ExtensionArsResendParams {
mid: string; mid: string;
tid: string; tid: string;
}; };
export interface ExtensionArsResendResponse {}; export interface ExtensionArsResendResponse {
status: boolean;
};
export interface ExtensionArsListParams { export interface ExtensionArsListParams {
mid?: string; mid?: string;
moid?: string; moid?: string;

View File

@@ -1,11 +1,11 @@
import { KeyInPaymentPaymentStatus } from "./types"; import { KeyInPaymentTansactionType } from "./types";
// contant로 옮기기 // contant로 옮기기
export const keyInPaymentPaymentStatusBtnGroup = [ export const keyInPaymentPaymentStatusBtnGroup = [
{ name: '전체', value: KeyInPaymentPaymentStatus.ALL }, { name: '전체', value: KeyInPaymentTansactionType.ALL },
{ name: '승인', value: KeyInPaymentPaymentStatus.APPROVAL }, { name: '승인', value: KeyInPaymentTansactionType.APPROVAL },
{ name: '전취소', value: KeyInPaymentPaymentStatus.PRE_CANCEL }, { name: '전취소', value: KeyInPaymentTansactionType.FULL_CANCEL },
{ name: '후취소', value: KeyInPaymentPaymentStatus.POST_CANCEL } { name: '후취소', value: KeyInPaymentTansactionType.PARTIAL_CANCEL }
]; ];
export const getKeyInPaymentPaymentStatusName = (status?: string): string => { export const getKeyInPaymentPaymentStatusName = (status?: string): string => {

View File

@@ -4,18 +4,30 @@ import { AdditionalServiceCategory, ExtensionRequestParams, FilterProps } from "
// ======================================== // ========================================
// 키인결제 관련 타입들 // 키인결제 관련 타입들
// ======================================== // ========================================
export enum KeyInPaymentPaymentStatus { export enum KeyInPaymentTansactionType {
ALL = 'ALL', ALL = 'ALL',
APPROVAL = 'APPROVAL', APPROVAL = 'APPROVAL',
PRE_CANCEL = 'PRE_CANCEL', FULL_CANCEL = 'FULL_CANCEL',
POST_CANCEL = 'POST_CANCEL' PARTIAL_CANCEL = 'PARTIAL_CANCEL'
}
export enum KeyInPaymentStatus {
ALL = 'ALL',
REGISTRED = 'REGISTRED',
WAITING = 'WAITING',
PROCESSING = 'PROCESSING',
COMPLETED = 'COMPLETED'
} }
export interface KeyInPaymentListItem { export interface KeyInPaymentListItem {
transactionDate?: string;
transactionTime?: string;
customerName?: string;
transactionCode?: string;
tid?: string; tid?: string;
paymentDate?: string;
paymentStatus?: string;
amount?: number; amount?: number;
statusLabel?: string;
transactionType?: string;
} }
export interface KeyInPaymentListProps { export interface KeyInPaymentListProps {
@@ -28,13 +40,13 @@ export interface KeyInPaymentFilterProps extends FilterProps {
mid: string, mid: string,
startDate: string; startDate: string;
endDate: string; endDate: string;
transactionStatus: KeyInPaymentPaymentStatus; transactionStatus: KeyInPaymentTansactionType;
minAmount?: number; minAmount?: number;
maxAmount?: number; maxAmount?: number;
setMid: (mid: string) => void; setMid: (mid: string) => void;
setStartDate: (startDate: string) => void; setStartDate: (startDate: string) => void;
setEndDate: (endDate: string) => void; setEndDate: (endDate: string) => void;
setTransactionStatus: (transactionStatus: KeyInPaymentPaymentStatus) => void; setTransactionStatus: (transactionStatus: KeyInPaymentTansactionType) => void;
setMinAmount: (minAmount?: number) => void; setMinAmount: (minAmount?: number) => void;
setMaxAmount: (maxAmount?: number) => void; setMaxAmount: (maxAmount?: number) => void;
} }
@@ -42,19 +54,25 @@ export interface KeyInPaymentFilterProps extends FilterProps {
// KEY-IN 결제 확장 서비스 // KEY-IN 결제 확장 서비스
// ======================================== // ========================================
export interface ExtensionKeyinListParams extends ExtensionRequestParams { export interface ExtensionKeyinListParams extends ExtensionRequestParams {
fromDate: string; startDate: string;
toDate: string; endDate: string;
paymentStatus: string; transactionType: string;
status: string;
minAmount?: number; minAmount?: number;
maxAmount?: number; maxAmount?: number;
page?: DefaultRequestPagination; page?: DefaultRequestPagination;
} }
export interface ExtensionKeyinListItemProps { export interface ExtensionKeyinListItemProps {
transactionDate: string;
transactionTime: string;
customerName: string;
transactionCode: string;
tid: string; tid: string;
paymentDate: string;
paymentStatus: string;
amount: number; amount: number;
statusLabel: string;
transactionType: string;
status: string;
} }
export interface ExtensionKeyinListResponse extends DefaulResponsePagination { export interface ExtensionKeyinListResponse extends DefaulResponsePagination {
@@ -74,15 +92,16 @@ export interface ExtensionKeyinDownloadExcelResponse {
} }
export interface ExtensionKeyinApplyParams extends ExtensionRequestParams { export interface ExtensionKeyinApplyParams extends ExtensionRequestParams {
goodsName: string;
amount: number;
buyerName: string;
email: string;
phoneNumber: string;
cardNo: string; cardNo: string;
cardExpirationDate: string; expYear: string;
instmntMonth: string; expMon: string;
moid: string; instmnt: string;
amount: number;
productName: string;
orderNumber: string;
customerName: string;
phoneNumber: string;
email: string;
} }
export interface ExtensionKeyinApplyResponse { export interface ExtensionKeyinApplyResponse {

View File

@@ -8,7 +8,7 @@ import { FilterButtonGroups } from '@/shared/ui/filter/button-groups';
import { FilterRangeAmount } from '@/shared/ui/filter/range-amount'; import { FilterRangeAmount } from '@/shared/ui/filter/range-amount';
import { FilterMotionDuration, FilterMotionStyle, FilterMotionVariants } from '@/entities/common/model/constant'; import { FilterMotionDuration, FilterMotionStyle, FilterMotionVariants } from '@/entities/common/model/constant';
import { useStore } from '@/shared/model/store'; import { useStore } from '@/shared/model/store';
import { KeyInPaymentFilterProps, KeyInPaymentPaymentStatus } from '@/entities/additional-service/model/key-in/types'; import { KeyInPaymentFilterProps, KeyInPaymentTansactionType } from '@/entities/additional-service/model/key-in/types';
import { keyInPaymentPaymentStatusBtnGroup } from '@/entities/additional-service/model/key-in/constant'; import { keyInPaymentPaymentStatusBtnGroup } from '@/entities/additional-service/model/key-in/constant';
export const KeyInPaymentFilter = ({ export const KeyInPaymentFilter = ({
@@ -31,7 +31,7 @@ export const KeyInPaymentFilter = ({
const [filterMid, setFilterMid] = useState<string>(mid); const [filterMid, setFilterMid] = useState<string>(mid);
const [filterStartDate, setFilterStartDate] = useState<string>(startDate); const [filterStartDate, setFilterStartDate] = useState<string>(startDate);
const [filterEndDate, setFilterEndDate] = useState<string>(endDate); const [filterEndDate, setFilterEndDate] = useState<string>(endDate);
const [filterTransactionStatus, setFilterTransactionStatus] = useState<KeyInPaymentPaymentStatus>(transactionStatus); const [filterTransactionStatus, setFilterTransactionStatus] = useState<KeyInPaymentTansactionType>(transactionStatus);
const [filterMinAmount, setFilterMinAmount] = useState<number | undefined>(minAmount); const [filterMinAmount, setFilterMinAmount] = useState<number | undefined>(minAmount);
const [filterMaxAmount, setFilterMaxAmount] = useState<number | undefined>(maxAmount); const [filterMaxAmount, setFilterMaxAmount] = useState<number | undefined>(maxAmount);

View File

@@ -18,13 +18,13 @@ export const KeyInPaymentList = ({
for (let i = 0; i < listItems.length; i++) { for (let i = 0; i < listItems.length; i++) {
let items = listItems[i]; let items = listItems[i];
if (!!items) { if (!!items) {
let paymentDate = items?.paymentDate; let transactionDate = items?.transactionDate;
paymentDate = paymentDate?.substring(0, 8) transactionDate = transactionDate?.substring(0, 8)
if (!!paymentDate) { if (!!transactionDate) {
if (i === 0) { if (i === 0) {
date = paymentDate; date = transactionDate;
} }
if (date !== paymentDate) { if (date !== transactionDate) {
if (list.length > 0) { if (list.length > 0) {
rs.push( rs.push(
<ListDateGroup <ListDateGroup
@@ -35,7 +35,7 @@ export const KeyInPaymentList = ({
></ListDateGroup> ></ListDateGroup>
); );
} }
date = paymentDate; date = transactionDate;
list = []; list = [];
} }
list.push(items); list.push(items);

View File

@@ -38,6 +38,12 @@ export const ListDateGroup = ({
resultStatus={ items[i]?.resultStatus } resultStatus={ items[i]?.resultStatus }
resultMessage={ items[i]?.resultMessage } resultMessage={ items[i]?.resultMessage }
applicationDate={ items[i]?.applicationDate } applicationDate={ items[i]?.applicationDate }
transactionTime={ items[i]?.transactionTime }
transactionDate={ items[i]?.transactionDate }
transactionCode={ items[i]?.transactionCode }
transactionType={ items[i]?.transactionType }
customerName={ items[i]?.customerName }
statusLabel={ items[i]?.statusLabel }
amount={ items[i]?.amount } amount={ items[i]?.amount }
sendDate={ items[i]?.sendDate } sendDate={ items[i]?.sendDate }

View File

@@ -24,7 +24,6 @@ export const ListItem = ({
accountName, accountName,
submallId, settlementDate, companyName, submallId, settlementDate, companyName,
status: disbursementStatus, amount: disbursementAmount,
orderStatus, arsPaymentMethod, orderStatus, arsPaymentMethod,
@@ -452,7 +451,7 @@ export const ListItem = ({
else if (additionalServiceCategory === AdditionalServiceCategory.Payout) { else if (additionalServiceCategory === AdditionalServiceCategory.Payout) {
rs.push( rs.push(
<div className="transaction-details"> <div className="transaction-details">
<span>{getPayoutStatusText(disbursementStatus)}</span> <span>{getPayoutStatusText(status)}</span>
<span className="separator">|</span> <span className="separator">|</span>
<span>{submallId}</span> <span>{submallId}</span>
</div> </div>
@@ -580,7 +579,7 @@ export const ListItem = ({
className="transaction-amount" className="transaction-amount"
> >
<NumericFormat <NumericFormat
value={disbursementAmount} value={amount}
thousandSeparator thousandSeparator
displayType="text" displayType="text"
suffix='원' suffix='원'

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { appBridge } from '@/utils/appBridge'; import { appBridge } from '@/utils/appBridge';
import { DeviceInfo, ShareContent } from '@/types'; import { DeviceInfo, NotificationSettingResponse, ShareContent } from '@/types';
import { LoginResponse } from '@/entities/user/model/types'; import { LoginResponse } from '@/entities/user/model/types';
interface UseAppBridgeReturn { interface UseAppBridgeReturn {
@@ -8,23 +8,23 @@ interface UseAppBridgeReturn {
isAndroid: boolean; isAndroid: boolean;
isIOS: boolean; isIOS: boolean;
deviceInfo: DeviceInfo | null; deviceInfo: DeviceInfo | null;
// 네비게이션 // 네비게이션
navigateBack: () => Promise<void>; navigateBack: () => Promise<void>;
navigateTo: (path: string) => Promise<void>; navigateTo: (path: string) => Promise<void>;
navigateToLogin: () => Promise<void>; navigateToLogin: () => Promise<void>;
closeWebView: () => Promise<void>; closeWebView: () => Promise<void>;
// 알림 // 알림
showToast: (message: string, duration?: number) => Promise<void>; showToast: (message: string, duration?: number) => Promise<void>;
showAlert: (title: string, message: string) => Promise<void>; showAlert: (title: string, message: string) => Promise<void>;
showConfirm: (title: string, message: string) => Promise<boolean>; showConfirm: (title: string, message: string) => Promise<boolean>;
// 저장소 // 저장소
setStorage: (key: string, value: unknown) => Promise<void>; setStorage: (key: string, value: unknown) => Promise<void>;
// getStorage: <T = unknown>(key: string) => Promise<T | null>; // getStorage: <T = unknown>(key: string) => Promise<T | null>;
removeStorage: (key: string) => Promise<void>; removeStorage: (key: string) => Promise<void>;
/* /*
// 미디어 // 미디어
openCamera: (options?: { quality?: number; allowEdit?: boolean }) => Promise<string>; openCamera: (options?: { quality?: number; allowEdit?: boolean }) => Promise<string>;
@@ -62,11 +62,15 @@ interface UseAppBridgeReturn {
// 로그인 방식 설정 조회 // 로그인 방식 설정 조회
getLoginType: () => Promise<string>; getLoginType: () => Promise<string>;
getNotificationSetting: () => Promise<boolean>;
setNotificationSetting: (enabled: boolean) => Promise<NotificationSettingResponse>;
} }
export const useAppBridge = (): UseAppBridgeReturn => { export const useAppBridge = (): UseAppBridgeReturn => {
const [deviceInfo, setDeviceInfo] = useState<DeviceInfo | null>(null); const [deviceInfo, setDeviceInfo] = useState<DeviceInfo | null>(null);
const isNativeEnvironment = appBridge.isNativeEnvironment(); const isNativeEnvironment = appBridge.isNativeEnvironment();
const isAndroid = appBridge.isAndroid(); const isAndroid = appBridge.isAndroid();
const isIOS = appBridge.isIOS(); const isIOS = appBridge.isIOS();
@@ -124,7 +128,7 @@ export const useAppBridge = (): UseAppBridgeReturn => {
toast.className = 'fixed top-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-md z-50 animate-fade-in'; toast.className = 'fixed top-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-md z-50 animate-fade-in';
toast.textContent = message; toast.textContent = message;
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(() => { setTimeout(() => {
document.body.removeChild(toast); document.body.removeChild(toast);
}, duration); }, duration);
@@ -207,7 +211,7 @@ export const useAppBridge = (): UseAppBridgeReturn => {
} }
return appBridge.safeCall(() => appBridge.closeBiometricRegistrationPopup()); return appBridge.safeCall(() => appBridge.closeBiometricRegistrationPopup());
}, [isNativeEnvironment]); }, [isNativeEnvironment]);
const shareContent = useCallback(async (content: ShareContent): Promise<void> => { const shareContent = useCallback(async (content: ShareContent): Promise<void> => {
if (!isNativeEnvironment) { if (!isNativeEnvironment) {
// 웹 환경에서는 Web Share API 사용 (지원되는 경우) // 웹 환경에서는 Web Share API 사용 (지원되는 경우)
@@ -261,6 +265,35 @@ export const useAppBridge = (): UseAppBridgeReturn => {
return result || 'ID'; return result || 'ID';
}, [isNativeEnvironment]); }, [isNativeEnvironment]);
const getNotificationSetting = useCallback(async (): Promise<boolean> => {
if (!isAndroid) {
console.log('getNotificationSetting: Not Android, skipping');
return true;
}
try {
const result = await appBridge.safeCall(() => appBridge.getNotificationSetting(), { enabled: true });
return result?.enabled ?? true;
} catch (error) {
console.error('Failed to get notification setting:', error);
return true;
}
}, [isAndroid]);
const setNotificationSetting = useCallback(async (enabled: boolean): Promise<any> => {
if (!isAndroid) {
console.log('setNotificationSetting: Not Android, skipping');
return null;
}
try {
const result = await appBridge.safeCall(() => appBridge.setNotificationSetting(enabled));
return result;
} catch (error) {
console.error('Failed to set notification setting:', error);
return null;
}
}, [isAndroid]);
return { return {
isNativeEnvironment, isNativeEnvironment,
isAndroid, isAndroid,
@@ -284,6 +317,8 @@ export const useAppBridge = (): UseAppBridgeReturn => {
closeBiometricRegistrationPopup, closeBiometricRegistrationPopup,
isPushNotificationEnabled, isPushNotificationEnabled,
openAppSettings, openAppSettings,
getLoginType getLoginType,
getNotificationSetting,
setNotificationSetting
}; };
}; };

View File

@@ -19,7 +19,7 @@ import { useExtensionKeyinListMutation } from '@/entities/additional-service/api
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant'; import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
import { KeyInPaymentList } from '@/entities/additional-service/ui/key-in-payment/key-in-payment-list'; import { KeyInPaymentList } from '@/entities/additional-service/ui/key-in-payment/key-in-payment-list';
import { useStore } from '@/shared/model/store'; import { useStore } from '@/shared/model/store';
import { KeyInPaymentListItem, KeyInPaymentPaymentStatus } from '@/entities/additional-service/model/key-in/types'; import { KeyInPaymentListItem, KeyInPaymentStatus, KeyInPaymentTansactionType } from '@/entities/additional-service/model/key-in/types';
import { keyInPaymentPaymentStatusBtnGroup } from '@/entities/additional-service/model/key-in/constant'; import { keyInPaymentPaymentStatusBtnGroup } from '@/entities/additional-service/model/key-in/constant';
import { EmailBottomSheet } from '@/entities/common/ui/email-bottom-sheet'; import { EmailBottomSheet } from '@/entities/common/ui/email-bottom-sheet';
import { useExtensionAccessCheck } from '@/shared/lib/hooks/use-extension-access-check'; import { useExtensionAccessCheck } from '@/shared/lib/hooks/use-extension-access-check';
@@ -40,9 +40,10 @@ export const KeyInPaymentPage = () => {
const [filterOn, setFilterOn] = useState<boolean>(false); const [filterOn, setFilterOn] = useState<boolean>(false);
const [pageParam, setPageParam] = useState<DefaultRequestPagination>(DEFAULT_PAGE_PARAM); const [pageParam, setPageParam] = useState<DefaultRequestPagination>(DEFAULT_PAGE_PARAM);
const [mid, setMid] = useState<string>(userMid); const [mid, setMid] = useState<string>(userMid);
const [startDate, setStartDate] = useState(moment().format('YYYY-MM-DD')); const [startDate, setStartDate] = useState(moment().format('YYYYMMDD'));
const [endDate, setEndDate] = useState(moment().format('YYYY-MM-DD')); const [endDate, setEndDate] = useState(moment().format('YYYYMMDD'));
const [paymentStatus, setPaymentStatus] = useState<KeyInPaymentPaymentStatus>(KeyInPaymentPaymentStatus.ALL) const [transactionType, setTransactionType] = useState<KeyInPaymentTansactionType>(KeyInPaymentTansactionType.ALL)
const [status, setStatus] = useState<KeyInPaymentStatus>(KeyInPaymentStatus.ALL);
const [minAmount, setMinAmount] = useState<number>(); const [minAmount, setMinAmount] = useState<number>();
const [maxAmount, setMaxAmount] = useState<number>(); const [maxAmount, setMaxAmount] = useState<number>();
const [emailBottomSheetOn, setEmailBottomSheetOn] = useState<boolean>(false); const [emailBottomSheetOn, setEmailBottomSheetOn] = useState<boolean>(false);
@@ -72,13 +73,15 @@ export const KeyInPaymentPage = () => {
onIntersect onIntersect
}); });
// 목록 조회
const callList = (type?: string) => { const callList = (type?: string) => {
setOnActionIntersect(false); setOnActionIntersect(false);
let listParams = { let listParams = {
mid: mid, mid: mid,
fromDate: startDate, startDate: startDate,
toDate: endDate, endDate: endDate,
paymentStatus: paymentStatus, transactionType: transactionType,
status: status,
minAmount: minAmount, minAmount: minAmount,
maxAmount: maxAmount, maxAmount: maxAmount,
page: { page: {
@@ -131,13 +134,14 @@ export const KeyInPaymentPage = () => {
setEmailBottomSheetOn(true); setEmailBottomSheetOn(true);
}; };
// 엑셀 다운로드
const onSendRequest = (selectedEmail?: string) => { const onSendRequest = (selectedEmail?: string) => {
if (selectedEmail) { if (selectedEmail) {
downloadExcel({ downloadExcel({
mid: mid, mid: mid,
fromDate: startDate, fromDate: startDate,
toDate: endDate, toDate: endDate,
paymentStatus: paymentStatus, paymentStatus: transactionType,
minAmount: minAmount, minAmount: minAmount,
maxAmount: maxAmount, maxAmount: maxAmount,
//email: selectedEmail //email: selectedEmail
@@ -152,8 +156,8 @@ export const KeyInPaymentPage = () => {
setSortType(sort); setSortType(sort);
}; };
const onClickToPaymentStatus = (val: KeyInPaymentPaymentStatus) => { const onClickToPaymentStatus = (val: KeyInPaymentTansactionType) => {
setPaymentStatus(val); setTransactionType(val);
}; };
useEffect(() => { useEffect(() => {
@@ -162,7 +166,7 @@ export const KeyInPaymentPage = () => {
mid, mid,
startDate, startDate,
endDate, endDate,
paymentStatus, transactionType,
minAmount, minAmount,
maxAmount, maxAmount,
sortType sortType
@@ -222,7 +226,7 @@ export const KeyInPaymentPage = () => {
keyInPaymentPaymentStatusBtnGroup.map((value, index) => ( keyInPaymentPaymentStatusBtnGroup.map((value, index) => (
<span <span
key={`key-service-code=${index}`} key={`key-service-code=${index}`}
className={`keyword-tag ${(paymentStatus === value.value) ? 'active' : ''}`} className={`keyword-tag ${(transactionType === value.value) ? 'active' : ''}`}
onClick={() => onClickToPaymentStatus(value.value)} onClick={() => onClickToPaymentStatus(value.value)}
>{value.name}</span> >{value.name}</span>
)) ))
@@ -245,13 +249,13 @@ export const KeyInPaymentPage = () => {
mid={mid} mid={mid}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
transactionStatus={paymentStatus} transactionStatus={transactionType}
minAmount={minAmount} minAmount={minAmount}
maxAmount={maxAmount} maxAmount={maxAmount}
setMid={setMid} setMid={setMid}
setStartDate={setStartDate} setStartDate={setStartDate}
setEndDate={setEndDate} setEndDate={setEndDate}
setTransactionStatus={setPaymentStatus} setTransactionStatus={setTransactionType}
setMinAmount={setMinAmount} setMinAmount={setMinAmount}
setMaxAmount={setMaxAmount} setMaxAmount={setMaxAmount}
></KeyInPaymentFilter> ></KeyInPaymentFilter>

View File

@@ -14,6 +14,7 @@ import {
import { overlay } from 'overlay-kit'; import { overlay } from 'overlay-kit';
import { Dialog } from '@/shared/ui/dialogs/dialog'; import { Dialog } from '@/shared/ui/dialogs/dialog';
import { useStore } from '@/shared/model/store'; import { useStore } from '@/shared/model/store';
import { snackBar } from '@/shared/lib';
export const KeyInPaymentRequestPage = () => { export const KeyInPaymentRequestPage = () => {
const { navigate } = useNavigate(); const { navigate } = useNavigate();
@@ -22,19 +23,19 @@ export const KeyInPaymentRequestPage = () => {
const userMid = useStore.getState().UserStore.mid; const userMid = useStore.getState().UserStore.mid;
const [mid, setMid] = useState<string>(userMid || ''); const [mid, setMid] = useState<string>(userMid || '');
const [goodsName, setGoodsName] = useState<string>(''); const [productName, setProductName] = useState<string>('');
const [amount, setAmount] = useState<number>(0); const [amount, setAmount] = useState<number>(0);
const [buyerName, setBuyerName] = useState<string>(''); const [customerName, setCustomerName] = useState<string>('');
const [email, setEmail] = useState<string>(''); const [email, setEmail] = useState<string>('');
const [phoneNumber, setPhoneNumber] = useState<string>(''); const [phoneNumber, setPhoneNumber] = useState<string>('');
const [cardNo1, setCardNo1] = useState<string>(''); const [cardNo1, setCardNo1] = useState<string>('');
const [cardNo2, setCardNo2] = useState<string>(''); const [cardNo2, setCardNo2] = useState<string>('');
const [cardNo3, setCardNo3] = useState<string>(''); const [cardNo3, setCardNo3] = useState<string>('');
const [cardNo4, setCardNo4] = useState<string>(''); const [cardNo4, setCardNo4] = useState<string>('');
const [cardExpirationMonth, setCardExpirationMonth] = useState<string>(''); const [expMon, setExpMon] = useState<string>('');
const [cardExpirationYear, setCardExpirationYear] = useState<string>(''); const [expYear, setExpYear] = useState<string>('');
const [instmntMonth, setInstmntMonth] = useState<string>('00'); const [instmnt, setInstmnt] = useState<string>('00');
const [moid, setMoid] = useState<string>(''); const [orderNumber, setOrderNumber] = useState<string>('');
const { mutateAsync: keyInApply } = useExtensionKeyinApplyMutation(); const { mutateAsync: keyInApply } = useExtensionKeyinApplyMutation();
@@ -46,43 +47,43 @@ export const KeyInPaymentRequestPage = () => {
}); });
const resetForm = () => { const resetForm = () => {
setGoodsName(''); setProductName('');
setAmount(0); setAmount(0);
setBuyerName(''); setCustomerName('');
setEmail(''); setEmail('');
setPhoneNumber(''); setPhoneNumber('');
setCardNo1(''); setCardNo1('');
setCardNo2(''); setCardNo2('');
setCardNo3(''); setCardNo3('');
setCardNo4(''); setCardNo4('');
setCardExpirationMonth(''); setExpMon('');
setCardExpirationYear(''); setExpYear('');
setInstmntMonth('00'); setInstmnt('00');
setMoid(''); setOrderNumber('');
}; };
const callKeyInPaymentRequest = () => { const callKeyInPaymentRequest = () => {
const cardNo = `${cardNo1}${cardNo2}${cardNo3}${cardNo4}`; const cardNo = `${cardNo1}${cardNo2}${cardNo3}${cardNo4}`;
const cardExpirationDate = `${cardExpirationMonth}${cardExpirationYear}`;
let keyInApplyParams = { let keyInApplyParams = {
mid: mid, mid: mid,
goodsName: goodsName,
amount: amount,
buyerName: buyerName,
email: email,
phoneNumber: phoneNumber,
cardNo: cardNo, cardNo: cardNo,
cardExpirationDate: cardExpirationDate, expYear: expYear,
instmntMonth: instmntMonth, expMon: expMon,
moid: moid, instmnt: instmnt,
amount: amount,
productName: productName,
orderNumber: orderNumber,
customerName: customerName,
phoneNumber: phoneNumber,
email: email,
}; };
keyInApply(keyInApplyParams).then((rs) => { keyInApply(keyInApplyParams).then((rs) => {
console.log('결제 응답:', rs); console.log('결제 응답:', rs);
if (rs.status) { if (rs.status) {
// 성공: 화면 유지 & 입력 내용 초기화 // 성공: 화면 유지 & 입력 내용 초기화
showSuccessDialog(); snackBar("KEY-IN 결제 신청을 성공하였습니다.")
resetForm(); resetForm();
} else { } else {
// 실패: 화면 유지 & 입력 내용 유지 // 실패: 화면 유지 & 입력 내용 유지
@@ -140,19 +141,19 @@ export const KeyInPaymentRequestPage = () => {
}; };
const isValidCardExpiration = () => { const isValidCardExpiration = () => {
if (cardExpirationMonth.length !== 2 || cardExpirationYear.length !== 2) { if (expMon.length !== 2 || expYear.length !== 2) {
return false; return false;
} }
const month = parseInt(cardExpirationMonth); const month = parseInt(expMon);
return month >= 1 && month <= 12; return month >= 1 && month <= 12;
}; };
const isFormValid = () => { const isFormValid = () => {
return ( return (
mid.trim() !== '' && mid.trim() !== '' &&
goodsName.trim() !== '' && productName.trim() !== '' &&
amount > 0 && amount > 0 &&
buyerName.trim() !== '' && customerName.trim() !== '' &&
isValidEmail(email) && isValidEmail(email) &&
isValidPhoneNumber(phoneNumber) && isValidPhoneNumber(phoneNumber) &&
isValidCardNumber() && isValidCardNumber() &&
@@ -187,8 +188,8 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-field"> <div className="billing-field">
<input <input
type="text" type="text"
value={goodsName} value={productName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setGoodsName(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setProductName(e.target.value)}
/> />
</div> </div>
</div> </div>
@@ -215,8 +216,8 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-field"> <div className="billing-field">
<input <input
type="text" type="text"
value={buyerName} value={customerName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setBuyerName(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setCustomerName(e.target.value)}
/> />
</div> </div>
</div> </div>
@@ -318,10 +319,10 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-field" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> <div className="billing-field" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input <input
type="text" type="text"
value={cardExpirationMonth} value={expMon}
onChange={(e: ChangeEvent<HTMLInputElement>) => { onChange={(e: ChangeEvent<HTMLInputElement>) => {
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); const onlyNumbers = e.target.value.replace(/[^0-9]/g, '');
if (onlyNumbers.length <= 2) setCardExpirationMonth(onlyNumbers); if (onlyNumbers.length <= 2) setExpMon(onlyNumbers);
}} }}
inputMode="numeric" inputMode="numeric"
pattern="[0-9]*" pattern="[0-9]*"
@@ -332,10 +333,10 @@ export const KeyInPaymentRequestPage = () => {
<span>/</span> <span>/</span>
<input <input
type="text" type="text"
value={cardExpirationYear} value={expYear}
onChange={(e: ChangeEvent<HTMLInputElement>) => { onChange={(e: ChangeEvent<HTMLInputElement>) => {
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); const onlyNumbers = e.target.value.replace(/[^0-9]/g, '');
if (onlyNumbers.length <= 2) setCardExpirationYear(onlyNumbers); if (onlyNumbers.length <= 2) setExpYear(onlyNumbers);
}} }}
inputMode="numeric" inputMode="numeric"
pattern="[0-9]*" pattern="[0-9]*"
@@ -351,8 +352,8 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-field"> <div className="billing-field">
<select <select
disabled={amount < 50000} disabled={amount < 50000}
value={instmntMonth} value={instmnt}
onChange={(e: ChangeEvent<HTMLSelectElement>) => setInstmntMonth(e.target.value)} onChange={(e: ChangeEvent<HTMLSelectElement>) => setInstmnt(e.target.value)}
> >
<option value="00"> ()</option> <option value="00"> ()</option>
{amount >= 50000 && ( {amount >= 50000 && (
@@ -379,8 +380,8 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-field"> <div className="billing-field">
<input <input
type="text" type="text"
value={moid} value={orderNumber}
onChange={(e: ChangeEvent<HTMLInputElement>) => setMoid(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setOrderNumber(e.target.value)}
/> />
</div> </div>
</div> </div>

View File

@@ -154,21 +154,21 @@ export const LinkPaymentDetailPage = () => {
additionalServiceCategory={AdditionalServiceCategory.LinkPaymentHistory} additionalServiceCategory={AdditionalServiceCategory.LinkPaymentHistory}
detailInfo={detailInfo} detailInfo={detailInfo}
></DetailInfoWrap> ></DetailInfoWrap>
<div className="link-payment-detail-button">
<button
className="btn-50 btn-blue flex-1"
onClick={() => onClickToSeparateApproval()}
disabled={false}
> </button>
</div>
</div> </div>
<div className="apply-row"> <div className="apply-row">
<button
className="btn-50 btn-blue flex-1"
onClick={() => onClickToSeparateApproval()}
disabled={false}
> </button>
</div>
{/* <div className="apply-row">
<button <button
className="btn-50 btn-blue flex-1" className="btn-50 btn-blue flex-1"
onClick={() => onClickToResend()} onClick={() => onClickToResend()}
disabled={!isResendEnabled()} disabled={!isResendEnabled()}
></button> ></button>
</div> */} </div>
</div> </div>
</div> </div>
</main > </main >

View File

@@ -16,7 +16,15 @@ import { useAppBridge } from '@/hooks/useAppBridge';
export const SettingPage = () => { export const SettingPage = () => {
let userInfo = useStore.getState().UserStore.userInfo; let userInfo = useStore.getState().UserStore.userInfo;
const { isPushNotificationEnabled, openAppSettings, logout, getLoginType } = useAppBridge(); const {
isPushNotificationEnabled,
openAppSettings,
logout,
getLoginType,
getNotificationSetting,
setNotificationSetting: updateNotificationSetting,
isAndroid
} = useAppBridge();
useSetHeaderTitle('설정'); useSetHeaderTitle('설정');
useSetHeaderType(HeaderType.LeftArrow); useSetHeaderType(HeaderType.LeftArrow);
@@ -31,6 +39,7 @@ export const SettingPage = () => {
const {mutateAsync: appAlarmConsent} = useAppAlarmConsentMutation(); const {mutateAsync: appAlarmConsent} = useAppAlarmConsentMutation();
const [pushNotificationEnabled, setPushNotificationEnabled] = useState<boolean>(false); const [pushNotificationEnabled, setPushNotificationEnabled] = useState<boolean>(false);
const [notificationSetting, setNotificationSetting] = useState<boolean>(true);
const [alarmSetting, setAlarmSetting] = useState<Record<string, boolean>>({ const [alarmSetting, setAlarmSetting] = useState<Record<string, boolean>>({
'21': false, '21': false,
'11': false, '11': false,
@@ -65,7 +74,74 @@ export const SettingPage = () => {
const onClickPushNotificationToggle = () => { const onClickPushNotificationToggle = () => {
openAppSettings(); openAppSettings();
}; };
// 알림 설정 로드 (Android만)
const loadNotificationSetting = useCallback(async () => {
if (!isAndroid) {
console.log('[loadNotificationSetting] Not Android, skipping');
return;
}
try {
console.log('[loadNotificationSetting] Calling getNotificationSetting()...');
const enabled = await getNotificationSetting();
console.log('[loadNotificationSetting] Received value from bridge:', enabled);
console.log('[loadNotificationSetting] Type of enabled:', typeof enabled);
setNotificationSetting(enabled);
console.log('[loadNotificationSetting] State updated to:', enabled);
} catch (error) {
console.error('[loadNotificationSetting] Failed to load notification setting:', error);
}
}, [isAndroid, getNotificationSetting]);
// 알림 설정 토글 핸들러 (Android만)
const onClickNotificationSettingToggle = useCallback(async () => {
if (!isAndroid) {
console.log('[onClickNotificationSettingToggle] Not Android, skipping');
return;
}
try {
const newValue = !notificationSetting;
console.log('[onClickNotificationSettingToggle] to save:', newValue);
const result = await updateNotificationSetting(newValue);
console.log('[onClickNotificationSettingToggle] result:', result);
// ✅ needsPermission이 true이면 설정 화면으로 이동한 것
if (result && typeof result === 'object' && 'needsPermission' in result && result.needsPermission) {
console.log('[onClickNotificationSettingToggle] Permission needed - opened settings');
// 설정이 변경되지 않았으므로 상태 유지
return;
}
// ✅ 성공한 경우에만 상태 업데이트
if (result && typeof result === 'object' && 'enabled' in result) {
setNotificationSetting(result.enabled);
console.log('[onClickNotificationSettingToggle] State updated to:', result.enabled);
} else {
// Fallback
setNotificationSetting(newValue);
console.log('[onClickNotificationSettingToggle] State updated to (fallback):', newValue);
}
// ✅ 저장 후 바로 다시 읽어서 확인
console.log('[onClickNotificationSettingToggle] Verifying saved value...');
const verifyValue = await getNotificationSetting();
console.log('[onClickNotificationSettingToggle] Verified value:', verifyValue);
if (verifyValue !== result?.enabled && !result?.needsPermission) {
console.error('[onClickNotificationSettingToggle] WARNING: Saved value != Verified value', {
saved: result?.enabled,
verified: verifyValue
});
}
} catch (error) {
console.error('[onClickNotificationSettingToggle] Failed to update notification setting:', error);
}
}, [isAndroid, notificationSetting, updateNotificationSetting, getNotificationSetting]);
const callAppAlarmFind = () => { const callAppAlarmFind = () => {
if(userInfo.usrid){ if(userInfo.usrid){
let params: AppAlarmFindParams = { let params: AppAlarmFindParams = {
@@ -115,16 +191,19 @@ export const SettingPage = () => {
callAppAlarmFind(); callAppAlarmFind();
checkPushNotificationStatus(); checkPushNotificationStatus();
loadLoginType(); loadLoginType();
loadNotificationSetting(); // ✅ 추가
// 앱이 포어그라운드로 돌아올 때 푸시 알림 권한 상태 재확인 // 앱이 포어그라운드로 돌아올 때 푸시 알림 권한 상태 재확인
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
checkPushNotificationStatus(); checkPushNotificationStatus();
loadNotificationSetting(); // ✅ 추가
} }
}; };
const handleFocus = () => { const handleFocus = () => {
checkPushNotificationStatus(); checkPushNotificationStatus();
loadNotificationSetting(); // ✅ 추가
}; };
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -134,20 +213,28 @@ export const SettingPage = () => {
document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('focus', handleFocus); window.removeEventListener('focus', handleFocus);
}; };
}, [checkPushNotificationStatus, loadLoginType]); }, [checkPushNotificationStatus, loadLoginType, loadNotificationSetting]);
return ( return (
<> <>
<main className="pop"> <main className="pop">
<div className="sub-wrap"> <div className="sub-wrap">
{/* ✅ Android일 때는 앱 내 설정, 아니면 시스템 권한 표시 */}
<div className="settings-header"> <div className="settings-header">
<div className="settings-title"> </div> <div className="settings-title"> </div>
<label className="settings-switch" onClick={onClickPushNotificationToggle}> <label className="settings-switch">
<input <input
type="checkbox" type="checkbox"
checked={pushNotificationEnabled} checked={isAndroid ? notificationSetting : pushNotificationEnabled}
readOnly readOnly
onClick={(e) => e.preventDefault()} onClick={(e) => {
e.preventDefault();
if (isAndroid) {
onClickNotificationSettingToggle();
} else {
onClickPushNotificationToggle();
}
}}
/> />
<span className="slider"></span> <span className="slider"></span>
</label> </label>

View File

@@ -403,4 +403,14 @@ main.home-main{
} }
.auth-list{ .auth-list{
padding-bottom: 0px; padding-bottom: 0px;
}
/* 링크 결제 상세 페이지 */
.link-payment-detail-button {
padding: 16px 0px 16px 0px;
display: flex;
}
.link-payment-detail-button button {
width: 100%;
} }

View File

@@ -74,7 +74,11 @@ export enum BridgeMessageType {
OPEN_APP_SETTINGS = 'openAppSettings', OPEN_APP_SETTINGS = 'openAppSettings',
// 로그인 방식 설정 // 로그인 방식 설정
GET_LOGIN_TYPE = 'getLoginType' GET_LOGIN_TYPE = 'getLoginType',
// 알림 수신 설정 (Android only)
GET_NOTIFICATION_SETTING = 'getNotificationSetting',
SET_NOTIFICATION_SETTING = 'setNotificationSetting'
} }
export interface DeviceInfo { export interface DeviceInfo {
@@ -103,4 +107,11 @@ export interface ShareContent {
text: string; text: string;
url?: string; url?: string;
image?: string; image?: string;
}
export interface NotificationSettingResponse {
success: boolean;
enabled: boolean;
needsPermission?: boolean;
message?: string;
} }

View File

@@ -210,6 +210,16 @@ class AppBridge {
return this.sendMessage(BridgeMessageType.GET_LOGIN_TYPE); return this.sendMessage(BridgeMessageType.GET_LOGIN_TYPE);
} }
// 알림 수신 설정 가져오기 (Android only)
async getNotificationSetting(): Promise<{ enabled: boolean }> {
return this.sendMessage(BridgeMessageType.GET_NOTIFICATION_SETTING);
}
// 알림 수신 설정 저장하기 (Android only)
async setNotificationSetting(enabled: boolean): Promise<any> {
return this.sendMessage(BridgeMessageType.SET_NOTIFICATION_SETTING, { enabled });
}
// 네이티브 환경 체크 // 네이티브 환경 체크
isNativeEnvironment(): boolean { isNativeEnvironment(): boolean {
return !!( return !!(