From 95cc870dbcd64440268b381067e5094c0bd0220d Mon Sep 17 00:00:00 2001 From: Jay Sheen Date: Tue, 28 Oct 2025 13:15:09 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=8B=A4=EA=B5=AD=EC=96=B4(localization)?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설정 페이지 전체 텍스트 다국어 지원 - 로그인 방식 선택 bottom sheet 다국어 적용 - 서비스 언어 선택 bottom sheet 다국어 적용 - 앱브리지에서 언어 설정 조회 기능 추가 - 페이지 로드 시 앱브리지 언어 설정으로 초기화 - AppLanguage enum 값 변경 (KO/EN → ko/en) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../alarm/ui/login-type-bottom-sheet.tsx | 27 ++-- .../ui/service-language-bottom-sheet.tsx | 25 +-- src/entities/common/model/constant.ts | 6 +- src/entities/common/model/types.ts | 6 +- src/hooks/useAppBridge.tsx | 14 +- src/locales/en.json | 26 +++ src/locales/ko.json | 26 +++ src/pages/setting/setting-page.tsx | 150 ++++++++++++------ 8 files changed, 203 insertions(+), 77 deletions(-) diff --git a/src/entities/alarm/ui/login-type-bottom-sheet.tsx b/src/entities/alarm/ui/login-type-bottom-sheet.tsx index 8393814..81815f9 100644 --- a/src/entities/alarm/ui/login-type-bottom-sheet.tsx +++ b/src/entities/alarm/ui/login-type-bottom-sheet.tsx @@ -1,12 +1,12 @@ -import { APP_LOGIN_TYPE } from "@/entities/common/model/constant"; import { LoginType } from "@/entities/common/model/types"; import { IMAGE_ROOT } from "@/shared/constants/common"; +import { useTranslation } from "react-i18next"; export interface LoginTypeBottomSheetProps { loginTypeBottomSheetOn: boolean; setLoginTypeBottomSheetOn: (loginTypeBottomSheetOn: boolean) => void; loginType: LoginType; - setLoginType: (loginType: LoginType) => void; + setLoginType: (loginType: string) => void; }; export const LoginTypeBottomSheet = ({ @@ -15,11 +15,12 @@ export const LoginTypeBottomSheet = ({ loginType, setLoginType }: LoginTypeBottomSheetProps) => { + const { t } = useTranslation(); const onClickToClose = () => { setLoginTypeBottomSheetOn(false); }; - const onChangeLoginType = (type: LoginType) => { + const onChangeLoginType = (type: string) => { setLoginType(type); onClickToClose(); }; @@ -30,14 +31,14 @@ export const LoginTypeBottomSheet = ({
-

로그인 방식을 선택하세요.

- @@ -47,14 +48,14 @@ export const LoginTypeBottomSheet = ({
    -
  • onChangeLoginType(LoginType.ID) } - >{ APP_LOGIN_TYPE[LoginType.ID] }
  • + onClick={ () => onChangeLoginType('ID') } + >{t('settings.loginType.idPassword')}
  • onChangeLoginType(LoginType.BIOMETRIC) } - >{ APP_LOGIN_TYPE[LoginType.BIOMETRIC] }
  • + onClick={ () => onChangeLoginType('BIOMETRIC') } + >{t('settings.loginType.biometric')}
diff --git a/src/entities/alarm/ui/service-language-bottom-sheet.tsx b/src/entities/alarm/ui/service-language-bottom-sheet.tsx index 692ac9f..3ff73d8 100644 --- a/src/entities/alarm/ui/service-language-bottom-sheet.tsx +++ b/src/entities/alarm/ui/service-language-bottom-sheet.tsx @@ -1,26 +1,31 @@ import { APP_LANGUAGE } from "@/entities/common/model/constant"; import { AppLanguage } from "@/entities/common/model/types"; import { IMAGE_ROOT } from "@/shared/constants/common"; +import { useTranslation } from "react-i18next"; export interface ServiceLanguageBottomSheetProps { serviceLanguageBottomSheetOn: boolean; setServiceLanguageBottomSheetOn: (serviceLanguageBottomSheetOn: boolean) => void; appLanguage: AppLanguage; setAppLanguage: (appLanguage: AppLanguage) => void; + changeLanguage: (language: string) => void | Promise; }; export const ServiceLanguageBottomSheet = ({ - serviceLanguageBottomSheetOn, + serviceLanguageBottomSheetOn: _serviceLanguageBottomSheetOn, setServiceLanguageBottomSheetOn, appLanguage, - setAppLanguage + setAppLanguage, + changeLanguage }: ServiceLanguageBottomSheetProps) => { + const { t } = useTranslation(); const onClickToClose = () => { setServiceLanguageBottomSheetOn(false); }; const onChangeServiceLanguage = (language: AppLanguage) => { setAppLanguage(language); + changeLanguage(language); onClickToClose(); }; @@ -30,14 +35,14 @@ export const ServiceLanguageBottomSheet = ({
-

서비스 언어를 선택하세요.

- @@ -46,12 +51,8 @@ export const ServiceLanguageBottomSheet = ({
-

※ 미지원 언어일 경우 ENGLISH 자동 설정

+

{t('settings.serviceLanguage.notice')}

    -
  • onChangeServiceLanguage(AppLanguage.DEVICE) } - >{ APP_LANGUAGE[AppLanguage.DEVICE] }
  • onChangeServiceLanguage(AppLanguage.KO) } diff --git a/src/entities/common/model/constant.ts b/src/entities/common/model/constant.ts index 04b15ac..74d9b5e 100644 --- a/src/entities/common/model/constant.ts +++ b/src/entities/common/model/constant.ts @@ -2,9 +2,9 @@ import { PATHS } from "@/shared/constants/paths"; import { SortTypeKeys } from "./types"; export const APP_LANGUAGE = { - DEVICE: '기기 설정 언어', - KO: '한국어', - EN: 'ENGLISH', + // DEVICE: '기기 설정 언어', + ko: '한국어', + en: 'English', }; export const APP_LOGIN_TYPE = { ID: 'ID/PW 입력', diff --git a/src/entities/common/model/types.ts b/src/entities/common/model/types.ts index 2e836a3..f5a9b02 100644 --- a/src/entities/common/model/types.ts +++ b/src/entities/common/model/types.ts @@ -1,7 +1,7 @@ export enum AppLanguage { - DEVICE = 'DEVICE', - KO = 'KO', - EN = 'EN' + // DEVICE = 'DEVICE', + KO = 'ko', + EN = 'en' }; export enum LoginType { ID = 'ID', diff --git a/src/hooks/useAppBridge.tsx b/src/hooks/useAppBridge.tsx index e7db5dd..1d5c01d 100644 --- a/src/hooks/useAppBridge.tsx +++ b/src/hooks/useAppBridge.tsx @@ -66,6 +66,9 @@ interface UseAppBridgeReturn { getNotificationSetting: () => Promise; setNotificationSetting: (enabled: boolean) => Promise; + + // 언어 설정 조회 + getLanguage: () => Promise; } export const useAppBridge = (): UseAppBridgeReturn => { @@ -294,6 +297,14 @@ export const useAppBridge = (): UseAppBridgeReturn => { } }, [isAndroid]); + const getLanguage = useCallback(async (): Promise => { + if (!isNativeEnvironment) { + return localStorage.getItem('i18nextLng') || 'ko'; + } + const result = await appBridge.safeCall(() => appBridge.getLanguage(), 'ko'); + return result || 'ko'; + }, [isNativeEnvironment]); + return { isNativeEnvironment, isAndroid, @@ -319,6 +330,7 @@ export const useAppBridge = (): UseAppBridgeReturn => { openAppSettings, getLoginType, getNotificationSetting, - setNotificationSetting + setNotificationSetting, + getLanguage }; }; \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 8b9c630..8d784b0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -75,6 +75,32 @@ "completed": "Completed", "failed": "Failed" }, + "settings": { + "title": "Settings", + "notificationSettings": "Notification Settings", + "noticeAlarms": "Notice Alarms", + "serviceOperation": "Service Operation & Failures", + "interestFreeEvent": "Interest-Free Benefits & Events", + "servicePolicyChange": "Service & Policy Changes", + "niceNews": "NICE News & Other Information", + "settlementAlarms": "Settlement Alarms", + "settlementStatus": "Settlement Status", + "settlementLimit": "Settlement Limit Alarm", + "marketingConsent": "Marketing Information Consent", + "loginTypeSetting": "Login Type", + "serviceLanguageSetting": "Language", + "logout": "Logout", + "privacyPolicy": "Privacy Policy", + "loginType": { + "title": "Please select a login type.", + "idPassword": "ID/PW Login", + "biometric": "Biometric Authentication" + }, + "serviceLanguage": { + "title": "Please select language.", + "notice": "※ If the language is not supported, English will be set automatically" + } + }, "support": { "notice": { "title": "Notice", diff --git a/src/locales/ko.json b/src/locales/ko.json index c0a9447..5092fa6 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -75,6 +75,32 @@ "completed": "완료", "failed": "실패" }, + "settings": { + "title": "설정", + "notificationSettings": "알림 수신 설정", + "noticeAlarms": "공지사항 알림", + "serviceOperation": "서비스 운영 및 장애", + "interestFreeEvent": "무이자 혜택 및 이벤트", + "servicePolicyChange": "서비스 이용 및 정책 변경", + "niceNews": "NICE소식 및 기타 정보", + "settlementAlarms": "정산 알림", + "settlementStatus": "정산처리 현황", + "settlementLimit": "정산한도 알림", + "marketingConsent": "마케팅 정보 수신 동의", + "loginTypeSetting": "로그인 방식 설정", + "serviceLanguageSetting": "서비스 언어 설정", + "logout": "로그아웃", + "privacyPolicy": "개인정보처리방침", + "loginType": { + "title": "로그인 방식을 선택하세요.", + "idPassword": "ID/PW 입력", + "biometric": "생체 인증" + }, + "serviceLanguage": { + "title": "서비스 언어를 선택하세요.", + "notice": "※ 미지원 언어일 경우 ENGLISH 자동 설정" + } + }, "support": { "notice": { "title": "공지사항", diff --git a/src/pages/setting/setting-page.tsx b/src/pages/setting/setting-page.tsx index 595cb27..9e04611 100644 --- a/src/pages/setting/setting-page.tsx +++ b/src/pages/setting/setting-page.tsx @@ -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(false); const [serviceLanguageBottomSheetOn, setServiceLanguageBottomSheetOn] = useState(false); const [loginType, setLoginType] = useState(LoginType.ID); - const [appLanguage, setAppLanguage] = useState(AppLanguage.KO); + const [appLanguage, setAppLanguage] = useState( + (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 = {}; for(let i=0;i { 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 = () => {
    {/* ✅ Android일 때는 앱 내 설정, 아니면 시스템 권한 표시 */}
    -
    알림 수신 설정
    +
    {t('settings.notificationSettings')}