설정 페이지 및 관련 컴포넌트 다국어(localization) 적용

- 설정 페이지 전체 텍스트 다국어 지원
- 로그인 방식 선택 bottom sheet 다국어 적용
- 서비스 언어 선택 bottom sheet 다국어 적용
- 앱브리지에서 언어 설정 조회 기능 추가
- 페이지 로드 시 앱브리지 언어 설정으로 초기화
- AppLanguage enum 값 변경 (KO/EN → ko/en)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jay Sheen
2025-10-28 13:15:09 +09:00
parent e125a73228
commit 95cc870dbc
8 changed files with 203 additions and 77 deletions

View File

@@ -13,6 +13,8 @@ import {
} from '@/widgets/sub-layout/use-sub-layout';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { useAppBridge } from '@/hooks/useAppBridge';
import { useTranslation } from 'react-i18next';
import appBridge from '@/utils/appBridge';
export const SettingPage = () => {
let userInfo = useStore.getState().UserStore.userInfo;
@@ -23,17 +25,21 @@ export const SettingPage = () => {
getLoginType,
getNotificationSetting,
setNotificationSetting: updateNotificationSetting,
isAndroid
isAndroid,
getLanguage
} = useAppBridge();
const { t, i18n } = useTranslation();
useSetHeaderTitle('설정');
useSetHeaderTitle(t('settings.title'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
const [loginTypeBottomSheetOn, setLoginTypeBottomSheetOn] = useState<boolean>(false);
const [serviceLanguageBottomSheetOn, setServiceLanguageBottomSheetOn] = useState<boolean>(false);
const [loginType, setLoginType] = useState<LoginType>(LoginType.ID);
const [appLanguage, setAppLanguage] = useState<AppLanguage>(AppLanguage.KO);
const [appLanguage, setAppLanguage] = useState<AppLanguage>(
(i18n.language === 'en' ? AppLanguage.EN : AppLanguage.KO)
);
const {mutateAsync: appAlarmFind} = useAppAlarmFindMutation();
const {mutateAsync: appAlarmConsent} = useAppAlarmConsentMutation();
@@ -71,6 +77,17 @@ export const SettingPage = () => {
});
}, [getLoginType]);
const loadLanguage = useCallback(() => {
getLanguage().then((language) => {
const languageType = language === 'en' ? AppLanguage.EN : AppLanguage.KO;
setAppLanguage(languageType);
// i18n 언어도 동기화
if (i18n.language !== language) {
i18n.changeLanguage(language);
}
});
}, [getLanguage, i18n]);
const onClickPushNotificationToggle = () => {
openAppSettings();
};
@@ -142,19 +159,7 @@ export const SettingPage = () => {
}
}, [isAndroid, notificationSetting, updateNotificationSetting, getNotificationSetting]);
const callAppAlarmFind = () => {
if(userInfo.usrid){
let params: AppAlarmFindParams = {
usrid: userInfo.usrid,
appCl: MERCHANT_ADMIN_APP.MERCHANT_ADMIN_APP
};
appAlarmFind(params).then((rs: AppAlarmFindResponse) => {
responseAlarmSetting(rs);
});
}
};
const responseAlarmSetting = (data: AppAlarmFindResponse) => {
const responseAlarmSetting = useCallback((data: AppAlarmFindResponse) => {
let responseAlarms: Record<string, boolean> = {};
for(let i=0;i<data.alarmAgree.length;i++){
let item = data.alarmAgree[i];
@@ -164,11 +169,23 @@ export const SettingPage = () => {
responseAlarms['' + appNotificationSubCategory] = appNotificationAllowed;
}
}
setAlarmSetting({
...alarmSetting,
setAlarmSetting(prevState => ({
...prevState,
...responseAlarms
});
};
}));
}, []);
const callAppAlarmFind = useCallback(() => {
if(userInfo.usrid){
let params: AppAlarmFindParams = {
usrid: userInfo.usrid,
appCl: MERCHANT_ADMIN_APP.MERCHANT_ADMIN_APP
};
appAlarmFind(params).then((rs: AppAlarmFindResponse) => {
responseAlarmSetting(rs);
});
}
}, [userInfo.usrid, appAlarmFind, responseAlarmSetting]);
const callAppAlarmConsent = (value: boolean, alarmCode: string) => {
if(userInfo.usrid){
@@ -178,7 +195,7 @@ export const SettingPage = () => {
appNotificationSubCategory: alarmCode,
appNotificationAllowed: value
};
appAlarmConsent(params).then((rs: AppAlarmConsentResponse) => {
appAlarmConsent(params).then((_rs: AppAlarmConsentResponse) => {
setAlarmSetting({
...alarmSetting,
...{ ['' + alarmCode]: value }
@@ -186,24 +203,66 @@ export const SettingPage = () => {
});
}
};
const changeLanguage = async (language: string) => {
// 웹 언어 변경
i18n.changeLanguage(language);
localStorage.setItem('i18nextLng', language);
// 네이티브 환경에서 네이티브 언어도 변경
if (appBridge.isNativeEnvironment()) {
try {
await appBridge.setLanguage(language);
} catch (error) {
console.error('Failed to set native language:', error);
}
}
};
const getLoginTypeLabel = (type: LoginType): string => {
switch (type) {
case LoginType.ID:
return t('settings.loginType.idPassword');
case LoginType.BIOMETRIC:
return t('settings.loginType.biometric');
default:
return '';
}
};
const getLanguageLabel = (language: AppLanguage): string => {
switch (language) {
case AppLanguage.KO:
return '한국어';
case AppLanguage.EN:
return 'English';
default:
return '';
}
};
const handleSetLoginType = (type: string) => {
setLoginType(type as LoginType);
};
useEffect(() => {
callAppAlarmFind();
checkPushNotificationStatus();
loadLoginType();
loadNotificationSetting(); // ✅ 추가
loadNotificationSetting();
loadLanguage();
// 앱이 포어그라운드로 돌아올 때 푸시 알림 권한 상태 재확인
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
checkPushNotificationStatus();
loadNotificationSetting(); // ✅ 추가
loadNotificationSetting();
}
};
const handleFocus = () => {
checkPushNotificationStatus();
loadNotificationSetting(); // ✅ 추가
loadNotificationSetting();
};
document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -213,7 +272,7 @@ export const SettingPage = () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('focus', handleFocus);
};
}, [checkPushNotificationStatus, loadLoginType, loadNotificationSetting]);
}, [callAppAlarmFind, checkPushNotificationStatus, loadLoginType, loadNotificationSetting, loadLanguage]);
return (
<>
@@ -221,7 +280,7 @@ export const SettingPage = () => {
<div className="sub-wrap">
{/* ✅ Android일 때는 앱 내 설정, 아니면 시스템 권한 표시 */}
<div className="settings-header">
<div className="settings-title"> </div>
<div className="settings-title">{t('settings.notificationSettings')}</div>
<label className="settings-switch">
<input
type="checkbox"
@@ -243,9 +302,9 @@ export const SettingPage = () => {
<div className="settings-divider"></div>
<div className="settings-section">
<div className="settings-section-title"> </div>
<div className="settings-section-title">{t('settings.noticeAlarms')}</div>
<div className="settings-row">
<div className="settings-row-title"> </div>
<div className="settings-row-title">{t('settings.serviceOperation')}</div>
<label className="settings-switch">
<input
type="checkbox"
@@ -257,7 +316,7 @@ export const SettingPage = () => {
</label>
</div>
<div className="settings-row">
<div className="settings-row-title"> </div>
<div className="settings-row-title">{t('settings.interestFreeEvent')}</div>
<label className="settings-switch">
<input
type="checkbox"
@@ -269,7 +328,7 @@ export const SettingPage = () => {
</label>
</div>
<div className="settings-row">
<div className="settings-row-title"> </div>
<div className="settings-row-title">{t('settings.servicePolicyChange')}</div>
<label className="settings-switch">
<input
type="checkbox"
@@ -281,7 +340,7 @@ export const SettingPage = () => {
</label>
</div>
<div className="settings-row">
<div className="settings-row-title">NICE소식 </div>
<div className="settings-row-title">{t('settings.niceNews')}</div>
<label className="settings-switch">
<input
type="checkbox"
@@ -295,9 +354,9 @@ export const SettingPage = () => {
</div>
<div className="settings-section nopadding">
<div className="settings-section-title"> </div>
<div className="settings-section-title">{t('settings.settlementAlarms')}</div>
<div className="settings-row">
<div className="settings-row-title"> </div>
<div className="settings-row-title">{t('settings.settlementStatus')}</div>
<label className="settings-switch">
<input
type="checkbox"
@@ -309,7 +368,7 @@ export const SettingPage = () => {
</label>
</div>
<div className="settings-row nopadding">
<div className="settings-row-title"> </div>
<div className="settings-row-title">{t('settings.settlementLimit')}</div>
<label className="settings-switch">
<input
type="checkbox"
@@ -326,10 +385,10 @@ export const SettingPage = () => {
<div className="settings-section nopadding">
<div className="settings-row link">
<div className="settings-row-title bd-style"> </div>
<div className="settings-row-title bd-style">{t('settings.marketingConsent')}</div>
<label className="settings-switch">
<input
type="checkbox"
<input
type="checkbox"
checked={ alarmSetting['15'] }
onChange={ (e: ChangeEvent<HTMLInputElement>) => callAppAlarmConsent(e.target.checked, '15') }
/>
@@ -342,22 +401,22 @@ export const SettingPage = () => {
className="settings-row link"
onClick={ () => setLoginTypeBottomSheetOn(true) }
>
<div className="settings-row-title bd-style"> </div>
<div className="click">{ APP_LOGIN_TYPE[loginType] }</div>
<div className="settings-row-title bd-style">{t('settings.loginTypeSetting')}</div>
<div className="click">{ getLoginTypeLabel(loginType) }</div>
</div>
<div
className="settings-row link nopadding"
onClick={ () => setServiceLanguageBottomSheetOn(true) }
>
<div className="settings-row-title bd-style"> </div>
<div className="click">{ APP_LANGUAGE[appLanguage] }</div>
<div className="settings-row-title bd-style">{t('settings.serviceLanguageSetting')}</div>
<div className="click">{ getLanguageLabel(appLanguage) }</div>
</div>
<div className="settings-divider"></div>
<div className="settings-row danger" onClick={onClickLogout}>
<div className="settings-row-title bd-style"></div>
<div className="settings-row-title bd-style">{t('settings.logout')}</div>
</div>
<div style={{ marginTop: '10px', paddingBottom: '10px' }}>
@@ -370,7 +429,7 @@ export const SettingPage = () => {
textAlign: 'center',
padding: '12px'
}}
></div>
>{t('settings.privacyPolicy')}</div>
</div>
</div>
</main>
@@ -379,7 +438,7 @@ export const SettingPage = () => {
loginTypeBottomSheetOn={ loginTypeBottomSheetOn }
setLoginTypeBottomSheetOn={ setLoginTypeBottomSheetOn }
loginType={ loginType }
setLoginType={ setLoginType }
setLoginType={ handleSetLoginType }
></LoginTypeBottomSheet>
}
{ !!serviceLanguageBottomSheetOn &&
@@ -388,6 +447,7 @@ export const SettingPage = () => {
setServiceLanguageBottomSheetOn={ setServiceLanguageBottomSheetOn }
appLanguage={ appLanguage }
setAppLanguage={ setAppLanguage }
changeLanguage={ changeLanguage }
></ServiceLanguageBottomSheet>
}
</>