계정 관리 페이지 및 컴포넌트 다국어화 완료

- 계정 관리 페이지 전체 다국어화 (8개 페이지)
  * 사용자 관리: 계정 추가, 메뉴 권한, 계정 정보, 로그인 인증정보
  * 비밀번호 관리: 로그인/취소 비밀번호 변경
- 계정 엔티티 컴포넌트 다국어화
  * account-tab: 사용자 관리/비밀번호 관리 탭
  * account-user-tab: 로그인 인증정보/계정권한 서브탭
  * password-manage-wrap: 비밀번호 변경 버튼
  * user-manage-wrap: 등록 현황, 사용자 추가 버튼
  * user-login-auth-info-wrap: 이메일/휴대폰 관리 인터페이스
- 계정 추가 페이지 상세 다국어화
  * 폼 라벨: 사용자ID, 비밀번호, 로그인 범위
  * 본인인증 정보 입력 섹션
  * 유효성 검사 메시지
  * 성공/실패 알림 메시지
- 메뉴 권한 페이지 다국어화
  * 권한 설정 안내 메시지
  * 저장/실행/다운로드 권한 라벨
  * 탭 변경 시 초기화 안내
- 번역 키 추가: account 네임스페이스 50개 키
- 모든 폼, 버튼, 알림 메시지 일관된 다국어 지원

🤖 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-29 18:14:40 +09:00
parent 9b193ee6f9
commit 3f0ab49a3d
15 changed files with 226 additions and 100 deletions

View File

@@ -1,11 +1,12 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { AccountTab } from '@/entities/account/ui/account-tab';
import { PasswordManageWrap } from '@/entities/account/ui/password-manage-wrap';
import { AccountTabKeys } from '@/entities/account/model/types';
import { HeaderType } from '@/entities/common/model/types';
import {
import {
useSetHeaderTitle,
useSetHeaderType,
useSetFooterMode,
@@ -13,10 +14,11 @@ import {
} from '@/widgets/sub-layout/use-sub-layout';
export const PasswordManagePage = () => {
const { t } = useTranslation();
const { navigate } = useNavigate();
const [activeTab, setActiveTab] = useState<AccountTabKeys>(AccountTabKeys.PasswordManage);
useSetHeaderTitle(t('account.manage'));
useSetHeaderTitle(t('account.title'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
@@ -13,6 +14,7 @@ import { useStore } from '@/shared/model/store';
import { snackBar } from '@/shared/lib/toast';
export const PasswordModifyCancelPasswordPage = () => {
const { t } = useTranslation();
const { navigate } = useNavigate();
const midOptions = useStore.getState().UserStore.selectOptionsMids;
const userMid = useStore.getState().UserStore.mid;
@@ -23,7 +25,7 @@ export const PasswordModifyCancelPasswordPage = () => {
const changeCancelPasswordMutation = useUserChangeCancelPasswordMutation({
onSuccess: () => {
snackBar('비밀번호가 성공적으로 변경되었습니다.');
snackBar(t('account.passwordChangedSuccessfully'));
// Clear form
setPassword('');
setConfirmPassword('');
@@ -31,11 +33,11 @@ export const PasswordModifyCancelPasswordPage = () => {
navigate(PATHS.account.password.manage);
},
onError: (error) => {
snackBar(error?.response?.data?.message || '비밀번호 변경에 실패했습니다.');
snackBar(error?.response?.data?.message || t('account.passwordChangeFailed'));
}
});
useSetHeaderTitle('거래취소 비밀번호 변경');
useSetHeaderTitle(t('account.changeCancelPassword'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {
@@ -69,7 +71,7 @@ export const PasswordModifyCancelPasswordPage = () => {
<div className="ing-list add">
<div className="user-add">
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<div className="ua-label">{t('account.merchant')} <span className="red">*</span></div>
<select
className="wid-100"
value={mid}
@@ -86,27 +88,27 @@ export const PasswordModifyCancelPasswordPage = () => {
</select>
</div>
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<div className="ua-label">{t('account.newPassword')} <span className="red">*</span></div>
<input
className={`wid-100 ${confirmPassword && password !== confirmPassword ? 'error' : ''}`}
type="password"
placeholder="비밀번호를 입력하세요"
placeholder={t('account.enterPassword')}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<div className="ua-label">{t('account.reEnterNewPassword')} <span className="red">*</span></div>
<input
className={`wid-100 ${confirmPassword && password !== confirmPassword ? 'error' : ''}`}
type="password"
placeholder="비밀번호를 다시 입력하세요"
placeholder={t('account.reEnterPassword')}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
{confirmPassword && password !== confirmPassword && (
<div className="ua-help error"> </div>
<div className="ua-help error">{t('account.inputMismatch')}</div>
)}
</div>
@@ -116,7 +118,7 @@ export const PasswordModifyCancelPasswordPage = () => {
type="button"
disabled={!isFormValid() || changeCancelPasswordMutation.isPending}
onClick={handleSave}
>{changeCancelPasswordMutation.isPending ? '처리중...' : '저장'}</button>
>{changeCancelPasswordMutation.isPending ? t('account.processing') : t('common.save')}</button>
</div>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
@@ -12,6 +13,7 @@ import { useUserChangePasswordMutation } from '@/entities/user/api/use-user-chan
import { snackBar } from '@/shared/lib/toast';
export const PasswordModifyLoginPasswordPage = () => {
const { t } = useTranslation();
const { navigate } = useNavigate();
const [currentPassword, setCurrentPassword] = useState<string>('');
const [newPassword, setNewPassword] = useState<string>('');
@@ -20,7 +22,7 @@ export const PasswordModifyLoginPasswordPage = () => {
const changePasswordMutation = useUserChangePasswordMutation({
onSuccess: () => {
snackBar('비밀번호가 성공적으로 변경되었습니다.');
snackBar(t('account.passwordChangedSuccessfully'));
// Clear form
setCurrentPassword('');
setNewPassword('');
@@ -29,11 +31,11 @@ export const PasswordModifyLoginPasswordPage = () => {
navigate(PATHS.account.password.manage);
},
onError: (error) => {
snackBar(error?.response?.data?.message || '비밀번호 변경에 실패했습니다.');
snackBar(error?.response?.data?.message || t('account.passwordChangeFailed'));
}
});
useSetHeaderTitle('로그인 비밀번호 변경');
useSetHeaderTitle(t('account.changeLoginPassword'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {
@@ -70,7 +72,7 @@ export const PasswordModifyLoginPasswordPage = () => {
<div className="ing-list add">
<div className="user-add">
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<div className="ua-label">{t('account.currentPassword')} <span className="red">*</span></div>
<input
className="wid-100"
type="password"
@@ -80,7 +82,7 @@ export const PasswordModifyLoginPasswordPage = () => {
/>
</div>
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<div className="ua-label">{t('account.newPassword')} <span className="red">*</span></div>
<input
className={`wid-100 ${confirmPassword && newPassword !== confirmPassword ? 'error' : ''}`}
type="password"
@@ -90,7 +92,7 @@ export const PasswordModifyLoginPasswordPage = () => {
/>
</div>
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<div className="ua-label">{t('account.reEnterNewPassword')} <span className="red">*</span></div>
<input
className={`wid-100 ${confirmPassword && newPassword !== confirmPassword ? 'error' : ''}`}
type="password"
@@ -100,7 +102,7 @@ export const PasswordModifyLoginPasswordPage = () => {
/>
</div>
{confirmPassword && newPassword !== confirmPassword && (
<div className="ua-help error"> </div>
<div className="ua-help error">{t('account.inputMismatch')}</div>
)}
</div>
@@ -110,7 +112,7 @@ export const PasswordModifyLoginPasswordPage = () => {
type="button"
disabled={!isFormValid() || changePasswordMutation.isPending}
onClick={handleSave}
>{changePasswordMutation.isPending ? '처리중...' : '저장'}</button>
>{changePasswordMutation.isPending ? t('account.processing') : t('common.save')}</button>
</div>
</div>
</div>

View File

@@ -1,12 +1,13 @@
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { AccountUserTab } from '@/entities/account/ui/account-user-tab';
import { UserAccountAuthWrap } from '@/entities/account/ui/user-account-auth-wrap';
import { AccountUserTabKeys } from '@/entities/account/model/types';
import { HeaderType } from '@/entities/common/model/types';
import {
import {
useSetHeaderTitle,
useSetHeaderType,
useSetFooterMode,
@@ -14,12 +15,13 @@ import {
} from '@/widgets/sub-layout/use-sub-layout';
export const UserAccountAuthPage = () => {
const { t } = useTranslation();
const { navigate } = useNavigate();
const location = useLocation();
const { mid, usrid, idCL, status } = location.state || {};
const [activeTab, ] = useState<AccountUserTabKeys>(AccountUserTabKeys.AccountAuth);
useSetHeaderTitle('사용자 설정');
useSetHeaderTitle(t('account.userSettings'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {

View File

@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
@@ -15,16 +16,17 @@ import { useLocation } from 'react-router';
import { snackBar } from '@/shared/lib/toast';
export const UserAddAccountPage = () => {
const { t } = useTranslation();
const { navigate } = useNavigate();
const location = useLocation();
const { mid } = location.state || {};
const { mutateAsync: userCreate, isPending } = useUserCreateMutation({
onSuccess: () => {
snackBar('사용자가 성공적으로 추가되었습니다.');
snackBar(t('account.userAddedSuccessfully'));
},
onError: (error) => {
snackBar(error?.response?.data?.message || '사용자 추가에 실패했습니다.');
snackBar(error?.response?.data?.message || t('account.userAddFailed'));
}
});
@@ -67,13 +69,13 @@ export const UserAddAccountPage = () => {
if (userExistsData && shouldCheckUsrid) {
setIsCheckingUsrid(false);
if (userExistsData.exists) {
setErrors(prev => ({ ...prev, usrid: '동일한 ID가 이미 존재합니다.' }));
setErrors(prev => ({ ...prev, usrid: t('account.duplicateIdExists') }));
} else {
setErrors(prev => ({ ...prev, usrid: '' }));
}
setShouldCheckUsrid(false);
}
}, [userExistsData, shouldCheckUsrid]);
}, [userExistsData, shouldCheckUsrid, t]);
// 이메일/전화번호 관리 함수들 (user-login-auth-info-wrap 방식)
const handleAddEmail = () => {
@@ -159,9 +161,9 @@ export const UserAddAccountPage = () => {
// 비밀번호 검증 함수
const validatePassword = (password: string) => {
if (!password.trim()) {
return '비밀번호를 입력해 주세요';
return t('account.pleaseEnterPassword');
} else if (password.length < 8) {
return '8자리 이상 입력해 주세요';
return t('account.pleaseEnter8OrMoreCharacters');
}
return '';
};
@@ -317,16 +319,16 @@ export const UserAddAccountPage = () => {
// 사용자 ID 검증
if (!formData.usrid.trim()) {
newErrors.usrid = 'ID를 입력해 주세요';
newErrors.usrid = t('account.pleaseEnterId');
isValid = false;
}
// 비밀번호 검증
if (!formData.password.trim()) {
newErrors.password = '비밀번호를 입력해 주세요';
newErrors.password = t('account.pleaseEnterPassword');
isValid = false;
} else if (formData.password.length < 8) {
newErrors.password = '8자리 이상 입력해 주세요';
newErrors.password = t('account.pleaseEnter8OrMoreCharacters');
isValid = false;
}
@@ -382,12 +384,12 @@ export const UserAddAccountPage = () => {
if (response.status) {
// 성공 시 사용자 관리 페이지로 이동
snackBar('사용자가 성공적으로 추가되었습니다.');
snackBar(t('account.userAddedSuccessfully'));
navigate(PATHS.account.user.manage);
} else if (response.error) {
// 에러 처리
if (response.error.errKey === 'USER_DUPLICATE') {
setErrors(prev => ({ ...prev, usrid: '동일한 ID가 이미 존재합니다.' }));
setErrors(prev => ({ ...prev, usrid: t('account.duplicateIdExists') }));
} else {
// 기타 에러 처리
console.error('User creation failed:', response.error.message);
@@ -398,7 +400,7 @@ export const UserAddAccountPage = () => {
}
};
useSetHeaderTitle('사용자 추가');
useSetHeaderTitle(t('account.addUser'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {
@@ -413,12 +415,12 @@ export const UserAddAccountPage = () => {
<div className="ing-list pb-86">
<div className="user-add">
<div className="ua-row">
<div className="ua-label">ID <span className="red">*</span></div>
<div className="ua-label">{t('account.userId')} <span className="red">*</span></div>
<div style={{ position: 'relative' }}>
<input
className={`wid-100 ${errors.usrid ? 'error' : ''}`}
type="text"
placeholder="ID를 입력해 주세요"
placeholder={t('account.pleaseEnterId')}
value={formData.usrid}
onChange={(e) => handleInputChange('usrid', e.target.value)}
/>
@@ -431,22 +433,22 @@ export const UserAddAccountPage = () => {
fontSize: '12px',
color: '#666'
}}>
...
{t('account.checking')}
</div>
)}
</div>
</div>
{errors.usrid && <div className="ua-help error pt-10">{errors.usrid}</div>}
{!errors.usrid && formData.usrid && userExistsData && !userExistsData.exists && (
<div className="ua-help" style={{ color: '#78D197' }}> ID입니다.</div>
<div className="ua-help" style={{ color: '#78D197' }}>{t('account.availableId')}</div>
)}
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<div className="ua-label">{t('account.password')} <span className="red">*</span></div>
<input
className={`wid-100 ${errors.password ? 'error' : ''}`}
type="password"
placeholder="8자리 이상 입력해 주세요"
placeholder={t('account.pleaseEnter8OrMoreCharacters')}
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
onBlur={handlePasswordBlur}
@@ -455,7 +457,7 @@ export const UserAddAccountPage = () => {
{errors.password && <div className="ua-help error pt-10">{errors.password}</div>}
<div className="ua-row">
<div className="ua-label"> </div>
<div className="ua-label">{t('account.loginRange')}</div>
<select
className="wid-100"
value={formData.loginRange}
@@ -469,17 +471,17 @@ export const UserAddAccountPage = () => {
<div className="info-divider"></div>
<div className="user-add info">
<div className="ua-desc">
<div className="ua-title"> </div>
<p className="ua-note"> .</p>
<div className="ua-title">{t('account.identityVerificationInfo')}</div>
<p className="ua-note">{t('account.identityVerificationNotice')}</p>
</div>
<div className="ua-group">
<div className="ua-group-header">
<div className="ua-group-title"> </div>
<div className="ua-group-title">{t('account.emailAddress')}</div>
<button
className="ic20 plus"
type="button"
aria-label="이메일 추가"
aria-label={t('account.addEmail')}
onClick={handleAddEmail}
disabled={!isEmailAddButtonEnabled()}
></button>
@@ -497,7 +499,7 @@ export const UserAddAccountPage = () => {
<button
className="icon-btn minus"
type="button"
aria-label="삭제"
aria-label={t('common.delete')}
onClick={() => handleRemoveNewEmail(index)}
disabled={!isDeleteButtonEnabled()}
></button>
@@ -507,11 +509,11 @@ export const UserAddAccountPage = () => {
<div className="ua-group">
<div className="ua-group-header">
<div className="ua-group-title"> </div>
<div className="ua-group-title">{t('account.phoneNumber')}</div>
<button
className="ic20 plus"
type="button"
aria-label="휴대폰 추가"
aria-label={t('account.addPhone')}
onClick={handleAddPhone}
disabled={!isPhoneAddButtonEnabled()}
></button>
@@ -529,7 +531,7 @@ export const UserAddAccountPage = () => {
<button
className="icon-btn minus"
type="button"
aria-label="삭제"
aria-label={t('common.delete')}
onClick={() => handleRemoveNewPhone(index)}
disabled={!isDeleteButtonEnabled()}
></button>
@@ -545,7 +547,7 @@ export const UserAddAccountPage = () => {
onClick={handleSave}
disabled={!isSaveButtonEnabled() || isPending}
>
{isPending ? '저장 중...' : '저장'}
{isPending ? t('common.saving') : t('common.save')}
</button>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { AccountUserTab } from '@/entities/account/ui/account-user-tab';
@@ -14,12 +15,13 @@ import {
} from '@/widgets/sub-layout/use-sub-layout';
export const UserLoginAuthInfoPage = () => {
const { t } = useTranslation();
const location = useLocation();
const { mid, usrid, idCL, status } = location.state || {};
const { navigate } = useNavigate();
const [activeTab, ] = useState<AccountUserTabKeys>(AccountUserTabKeys.LoginAuthInfo);
useSetHeaderTitle('사용자 설정');
useSetHeaderTitle(t('account.userSettings'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { AccountTab } from '@/entities/account/ui/account-tab';
@@ -6,7 +7,7 @@ import { UserManageWrap } from '@/entities/account/ui/user-manage-wrap';
import { AccountTabKeys } from '@/entities/account/model/types';
import { FooterItemActiveKey } from '@/entities/common/model/types';
import { HeaderType } from '@/entities/common/model/types';
import {
import {
useSetHeaderTitle,
useSetHeaderType,
useSetFooterMode,
@@ -14,10 +15,11 @@ import {
} from '@/widgets/sub-layout/use-sub-layout';
export const UserManagePage = () => {
const { t } = useTranslation();
const { navigate } = useNavigate();
const [activeTab, ] = useState<AccountTabKeys>(AccountTabKeys.UserManage);
useSetHeaderTitle('계정 관리');
useSetHeaderTitle(t('account.title'));
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
useSetOnBack(() => {

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
@@ -23,6 +24,7 @@ const PERMISSION = {
};
export const UserMenuAuthPage = () => {
const { t } = useTranslation();
const { navigate } = useNavigate();
const location = useLocation();
const { mid, usrid, menuName, subMenu, menuGrants } = location.state || {};
@@ -35,7 +37,7 @@ export const UserMenuAuthPage = () => {
const [isInitialLoad, setIsInitialLoad] = useState(true);
const savePermissionsMutation = useUserMenuPermissionsSaveMutation({
onSuccess: () => {
snackBar('권한이 성공적으로 저장되었습니다.');
snackBar(t('account.permissionSavedSuccessfully'));
navigate(PATHS.account.user.accountAuth, {
state: {
mid,
@@ -46,7 +48,7 @@ export const UserMenuAuthPage = () => {
});
},
onError: (error) => {
snackBar(error?.response?.data?.message || '권한 저장에 실패했습니다.');
snackBar(error?.response?.data?.message || t('account.permissionSaveFailed'));
}
});
@@ -181,13 +183,13 @@ export const UserMenuAuthPage = () => {
{ mid, namsUserMenuAccess },
{
onSuccess: () => {
snackBar('권한이 저장되었습니다.');
snackBar(t('account.permissionSaved'));
// 저장 성공 후 초기값 업데이트
setInitialPermissions({...permissions});
setHasChanges(false);
},
onError: (error) => {
alert('권한 저장에 실패했습니다.');
alert(t('account.permissionSaveFailed'));
console.error(error);
}
}
@@ -200,8 +202,8 @@ export const UserMenuAuthPage = () => {
<div className="tab-content">
<div className="tab-pane pt-46 active">
<div className="ing-list pb-86">
<div className="desc service-tip"> .</div>
<div className="desc service-tip"> .</div>
<div className="desc service-tip">{t('account.setMenuPermissions')}</div>
<div className="desc service-tip">{t('account.permissionRestrictionsNotice')}</div>
{subMenu && subMenu.map((menu: { menuId: number; menuName: string }) => {
const menuGrant = permissions[menu.menuId] || 0;
@@ -236,7 +238,7 @@ export const UserMenuAuthPage = () => {
{hasDefaultPermission(menu.menuId, PERMISSION.SAVE) && (
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<span className="settings-row-title bd-sub dot">{t('account.save')}</span>
<label className="settings-switch">
<input
type="checkbox"
@@ -250,7 +252,7 @@ export const UserMenuAuthPage = () => {
{hasDefaultPermission(menu.menuId, PERMISSION.EXECUTE) && (
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<span className="settings-row-title bd-sub dot">{t('account.execute')}</span>
<label className="settings-switch">
<input
type="checkbox"
@@ -264,7 +266,7 @@ export const UserMenuAuthPage = () => {
{hasDefaultPermission(menu.menuId, PERMISSION.DOWNLOAD) && (
<div className="settings-row">
<span className="settings-row-title bd-sub dot"></span>
<span className="settings-row-title bd-sub dot">{t('account.download')}</span>
<label className="settings-switch">
<input
type="checkbox"
@@ -290,7 +292,7 @@ export const UserMenuAuthPage = () => {
onClick={handleSave}
disabled={!hasChanges || savePermissionsMutation.isPending}
>
{savePermissionsMutation.isPending ? '저장 중...' : '저장'}
{savePermissionsMutation.isPending ? t('common.saving') : t('common.save')}
</button>
</div>
</div>