Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 시스템 관리자/관리자 권한별 대시보드 기능 추가 - 사용자 관리 페이지: 계정 생성, 역할 변경, 사용자 삭제 - 시스템 로그 페이지: 로그인 로그, 시스템 오류 로그 조회 - 로그 모니터링 대시보드: 실시간 통계, 최근 활동, 오류 모니터링 - 프론트엔드 ErrorBoundary 및 오류 로깅 시스템 통합 - 계정 설정 페이지: 프로필 업데이트, 비밀번호 변경 - 3단계 권한 시스템 (system/admin/user) 완전 구현 - 시스템 관리자 계정 생성 기능 (hyungi/000000) - 로그인 페이지 테스트 계정 안내 제거 - API 오류 수정: CORS, 이메일 검증, User 모델 import 등
706 lines
34 KiB
JavaScript
706 lines
34 KiB
JavaScript
import React, { useState } from 'react';
|
||
import api from '../api';
|
||
import { reportError, logUserActionError } from '../utils/errorLogger';
|
||
|
||
const AccountSettingsPage = ({ onNavigate, user, onUserUpdate }) => {
|
||
const [activeTab, setActiveTab] = useState('profile');
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [message, setMessage] = useState({ type: '', text: '' });
|
||
|
||
// 프로필 정보 상태
|
||
const [profileData, setProfileData] = useState({
|
||
name: user?.name || '',
|
||
email: user?.email || '',
|
||
department: user?.department || '',
|
||
position: user?.position || ''
|
||
});
|
||
|
||
// 비밀번호 변경 상태
|
||
const [passwordData, setPasswordData] = useState({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
});
|
||
|
||
const [validationErrors, setValidationErrors] = useState({});
|
||
|
||
const handleProfileChange = (e) => {
|
||
const { name, value } = e.target;
|
||
setProfileData(prev => ({
|
||
...prev,
|
||
[name]: value
|
||
}));
|
||
|
||
// 에러 메시지 초기화
|
||
if (validationErrors[name]) {
|
||
setValidationErrors(prev => ({
|
||
...prev,
|
||
[name]: ''
|
||
}));
|
||
}
|
||
if (message.text) setMessage({ type: '', text: '' });
|
||
};
|
||
|
||
const handlePasswordChange = (e) => {
|
||
const { name, value } = e.target;
|
||
setPasswordData(prev => ({
|
||
...prev,
|
||
[name]: value
|
||
}));
|
||
|
||
// 에러 메시지 초기화
|
||
if (validationErrors[name]) {
|
||
setValidationErrors(prev => ({
|
||
...prev,
|
||
[name]: ''
|
||
}));
|
||
}
|
||
if (message.text) setMessage({ type: '', text: '' });
|
||
};
|
||
|
||
const validateProfileForm = () => {
|
||
const errors = {};
|
||
|
||
if (!profileData.name.trim()) {
|
||
errors.name = '이름을 입력해주세요';
|
||
} else if (profileData.name.length < 2 || profileData.name.length > 50) {
|
||
errors.name = '이름은 2-50자여야 합니다';
|
||
}
|
||
|
||
if (profileData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profileData.email)) {
|
||
errors.email = '올바른 이메일 형식을 입력해주세요';
|
||
}
|
||
|
||
setValidationErrors(errors);
|
||
return Object.keys(errors).length === 0;
|
||
};
|
||
|
||
const validatePasswordForm = () => {
|
||
const errors = {};
|
||
|
||
if (!passwordData.currentPassword) {
|
||
errors.currentPassword = '현재 비밀번호를 입력해주세요';
|
||
}
|
||
|
||
if (!passwordData.newPassword) {
|
||
errors.newPassword = '새 비밀번호를 입력해주세요';
|
||
} else if (passwordData.newPassword.length < 8) {
|
||
errors.newPassword = '새 비밀번호는 8자 이상이어야 합니다';
|
||
}
|
||
|
||
if (!passwordData.confirmPassword) {
|
||
errors.confirmPassword = '비밀번호 확인을 입력해주세요';
|
||
} else if (passwordData.newPassword !== passwordData.confirmPassword) {
|
||
errors.confirmPassword = '새 비밀번호가 일치하지 않습니다';
|
||
}
|
||
|
||
setValidationErrors(errors);
|
||
return Object.keys(errors).length === 0;
|
||
};
|
||
|
||
const handleProfileSubmit = async (e) => {
|
||
e.preventDefault();
|
||
|
||
if (!validateProfileForm()) {
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
setMessage({ type: '', text: '' });
|
||
|
||
try {
|
||
const response = await api.put('/auth/profile', {
|
||
name: profileData.name.trim(),
|
||
email: profileData.email.trim() || null,
|
||
department: profileData.department.trim() || null,
|
||
position: profileData.position.trim() || null
|
||
});
|
||
|
||
if (response.data.success) {
|
||
const updatedUser = { ...user, ...response.data.user };
|
||
onUserUpdate(updatedUser);
|
||
setMessage({ type: 'success', text: '프로필이 성공적으로 업데이트되었습니다' });
|
||
} else {
|
||
setMessage({ type: 'error', text: response.data.message || '프로필 업데이트에 실패했습니다' });
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('Profile update error:', err);
|
||
|
||
const errorMessage = err.response?.data?.detail ||
|
||
err.response?.data?.message ||
|
||
'프로필 업데이트 중 오류가 발생했습니다';
|
||
|
||
setMessage({ type: 'error', text: errorMessage });
|
||
|
||
logUserActionError('profile_update', err, { userId: user?.user_id });
|
||
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handlePasswordSubmit = async (e) => {
|
||
e.preventDefault();
|
||
|
||
if (!validatePasswordForm()) {
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
setMessage({ type: '', text: '' });
|
||
|
||
try {
|
||
const response = await api.put('/auth/change-password', {
|
||
current_password: passwordData.currentPassword,
|
||
new_password: passwordData.newPassword
|
||
});
|
||
|
||
if (response.data.success) {
|
||
setPasswordData({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
});
|
||
setMessage({ type: 'success', text: '비밀번호가 성공적으로 변경되었습니다' });
|
||
} else {
|
||
setMessage({ type: 'error', text: response.data.message || '비밀번호 변경에 실패했습니다' });
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('Password change error:', err);
|
||
|
||
const errorMessage = err.response?.data?.detail ||
|
||
err.response?.data?.message ||
|
||
'비밀번호 변경 중 오류가 발생했습니다';
|
||
|
||
setMessage({ type: 'error', text: errorMessage });
|
||
|
||
logUserActionError('password_change', err, { userId: user?.user_id });
|
||
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div style={{ minHeight: '100vh', background: '#f8f9fa' }}>
|
||
{/* 헤더 */}
|
||
<div style={{
|
||
background: 'white',
|
||
borderBottom: '1px solid #e9ecef',
|
||
padding: '16px 32px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '16px'
|
||
}}>
|
||
<button
|
||
onClick={() => onNavigate('dashboard')}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
color: '#28a745',
|
||
fontSize: '20px',
|
||
cursor: 'pointer',
|
||
padding: '4px',
|
||
borderRadius: '4px',
|
||
transition: 'background-color 0.2s'
|
||
}}
|
||
onMouseEnter={(e) => e.target.style.background = '#f8f9fa'}
|
||
onMouseLeave={(e) => e.target.style.background = 'none'}
|
||
title="대시보드로 돌아가기"
|
||
>
|
||
←
|
||
</button>
|
||
<div>
|
||
<h1 style={{ fontSize: '24px', fontWeight: '700', color: '#2d3748', margin: 0 }}>
|
||
⚙️ 계정 설정
|
||
</h1>
|
||
<p style={{ color: '#6c757d', fontSize: '14px', margin: '4px 0 0 0' }}>
|
||
프로필 정보 및 비밀번호를 관리하세요
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ padding: '32px', maxWidth: '800px', margin: '0 auto' }}>
|
||
{/* 탭 메뉴 */}
|
||
<div style={{
|
||
display: 'flex',
|
||
borderBottom: '2px solid #e9ecef',
|
||
marginBottom: '32px'
|
||
}}>
|
||
<button
|
||
onClick={() => setActiveTab('profile')}
|
||
style={{
|
||
padding: '12px 24px',
|
||
background: 'none',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'profile' ? '2px solid #007bff' : '2px solid transparent',
|
||
color: activeTab === 'profile' ? '#007bff' : '#6c757d',
|
||
fontSize: '16px',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
>
|
||
👤 프로필 정보
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('password')}
|
||
style={{
|
||
padding: '12px 24px',
|
||
background: 'none',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'password' ? '2px solid #007bff' : '2px solid transparent',
|
||
color: activeTab === 'password' ? '#007bff' : '#6c757d',
|
||
fontSize: '16px',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
>
|
||
🔒 비밀번호 변경
|
||
</button>
|
||
</div>
|
||
|
||
{/* 메시지 표시 */}
|
||
{message.text && (
|
||
<div style={{
|
||
padding: '12px 16px',
|
||
borderRadius: '8px',
|
||
marginBottom: '24px',
|
||
backgroundColor: message.type === 'success' ? '#d1edff' : '#f8d7da',
|
||
border: `1px solid ${message.type === 'success' ? '#bee5eb' : '#f5c6cb'}`,
|
||
color: message.type === 'success' ? '#0c5460' : '#721c24'
|
||
}}>
|
||
{message.text}
|
||
</div>
|
||
)}
|
||
|
||
{/* 프로필 정보 탭 */}
|
||
{activeTab === 'profile' && (
|
||
<div style={{
|
||
background: 'white',
|
||
borderRadius: '12px',
|
||
padding: '32px',
|
||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'
|
||
}}>
|
||
<h2 style={{ fontSize: '20px', fontWeight: '600', color: '#2d3748', marginBottom: '24px' }}>
|
||
프로필 정보
|
||
</h2>
|
||
|
||
<form onSubmit={handleProfileSubmit}>
|
||
<div style={{ display: 'grid', gap: '20px' }}>
|
||
{/* 사용자명 (읽기 전용) */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
사용자명
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={user?.username || ''}
|
||
disabled
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
backgroundColor: '#f9fafb',
|
||
color: '#6b7280',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
<p style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
|
||
사용자명은 변경할 수 없습니다
|
||
</p>
|
||
</div>
|
||
|
||
{/* 이름 */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
이름 *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
value={profileData.name}
|
||
onChange={handleProfileChange}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: validationErrors.name ? '2px solid #ef4444' : '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
|
||
onBlur={(e) => e.target.style.borderColor = validationErrors.name ? '#ef4444' : '#d1d5db'}
|
||
/>
|
||
{validationErrors.name && (
|
||
<p style={{ color: '#ef4444', fontSize: '12px', marginTop: '4px' }}>
|
||
{validationErrors.name}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 이메일 */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
이메일
|
||
</label>
|
||
<input
|
||
type="email"
|
||
name="email"
|
||
value={profileData.email}
|
||
onChange={handleProfileChange}
|
||
placeholder="example@company.com"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: validationErrors.email ? '2px solid #ef4444' : '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
|
||
onBlur={(e) => e.target.style.borderColor = validationErrors.email ? '#ef4444' : '#d1d5db'}
|
||
/>
|
||
{validationErrors.email && (
|
||
<p style={{ color: '#ef4444', fontSize: '12px', marginTop: '4px' }}>
|
||
{validationErrors.email}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 부서 및 직책 */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
부서
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="department"
|
||
value={profileData.department}
|
||
onChange={handleProfileChange}
|
||
placeholder="IT팀"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
|
||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
직책
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="position"
|
||
value={profileData.position}
|
||
onChange={handleProfileChange}
|
||
placeholder="관리자"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
|
||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 역할 (읽기 전용) */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
역할
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={user?.role === 'system' ? '시스템 관리자' :
|
||
user?.role === 'admin' ? '관리자' : '사용자'}
|
||
disabled
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
backgroundColor: '#f9fafb',
|
||
color: '#6b7280',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
<p style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
|
||
역할은 시스템 관리자만 변경할 수 있습니다
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading}
|
||
style={{
|
||
marginTop: '24px',
|
||
padding: '12px 24px',
|
||
backgroundColor: isLoading ? '#9ca3af' : '#007bff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '8px',
|
||
fontSize: '16px',
|
||
fontWeight: '600',
|
||
cursor: isLoading ? 'not-allowed' : 'pointer',
|
||
transition: 'background-color 0.2s',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px'
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (!isLoading) e.target.style.backgroundColor = '#0056b3';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
if (!isLoading) e.target.style.backgroundColor = '#007bff';
|
||
}}
|
||
>
|
||
{isLoading ? '저장 중...' : '💾 프로필 저장'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
{/* 비밀번호 변경 탭 */}
|
||
{activeTab === 'password' && (
|
||
<div style={{
|
||
background: 'white',
|
||
borderRadius: '12px',
|
||
padding: '32px',
|
||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'
|
||
}}>
|
||
<h2 style={{ fontSize: '20px', fontWeight: '600', color: '#2d3748', marginBottom: '24px' }}>
|
||
비밀번호 변경
|
||
</h2>
|
||
|
||
<form onSubmit={handlePasswordSubmit}>
|
||
<div style={{ display: 'grid', gap: '20px', maxWidth: '400px' }}>
|
||
{/* 현재 비밀번호 */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
현재 비밀번호 *
|
||
</label>
|
||
<input
|
||
type="password"
|
||
name="currentPassword"
|
||
value={passwordData.currentPassword}
|
||
onChange={handlePasswordChange}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: validationErrors.currentPassword ? '2px solid #ef4444' : '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
|
||
onBlur={(e) => e.target.style.borderColor = validationErrors.currentPassword ? '#ef4444' : '#d1d5db'}
|
||
/>
|
||
{validationErrors.currentPassword && (
|
||
<p style={{ color: '#ef4444', fontSize: '12px', marginTop: '4px' }}>
|
||
{validationErrors.currentPassword}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 새 비밀번호 */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
새 비밀번호 *
|
||
</label>
|
||
<input
|
||
type="password"
|
||
name="newPassword"
|
||
value={passwordData.newPassword}
|
||
onChange={handlePasswordChange}
|
||
placeholder="8자 이상 입력해주세요"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: validationErrors.newPassword ? '2px solid #ef4444' : '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
|
||
onBlur={(e) => e.target.style.borderColor = validationErrors.newPassword ? '#ef4444' : '#d1d5db'}
|
||
/>
|
||
{validationErrors.newPassword && (
|
||
<p style={{ color: '#ef4444', fontSize: '12px', marginTop: '4px' }}>
|
||
{validationErrors.newPassword}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 새 비밀번호 확인 */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
color: '#374151',
|
||
marginBottom: '6px'
|
||
}}>
|
||
새 비밀번호 확인 *
|
||
</label>
|
||
<input
|
||
type="password"
|
||
name="confirmPassword"
|
||
value={passwordData.confirmPassword}
|
||
onChange={handlePasswordChange}
|
||
placeholder="새 비밀번호를 다시 입력해주세요"
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px',
|
||
border: validationErrors.confirmPassword ? '2px solid #ef4444' : '1px solid #d1d5db',
|
||
borderRadius: '8px',
|
||
fontSize: '14px',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
|
||
onBlur={(e) => e.target.style.borderColor = validationErrors.confirmPassword ? '#ef4444' : '#d1d5db'}
|
||
/>
|
||
{validationErrors.confirmPassword && (
|
||
<p style={{ color: '#ef4444', fontSize: '12px', marginTop: '4px' }}>
|
||
{validationErrors.confirmPassword}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading}
|
||
style={{
|
||
marginTop: '24px',
|
||
padding: '12px 24px',
|
||
backgroundColor: isLoading ? '#9ca3af' : '#dc3545',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '8px',
|
||
fontSize: '16px',
|
||
fontWeight: '600',
|
||
cursor: isLoading ? 'not-allowed' : 'pointer',
|
||
transition: 'background-color 0.2s',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px'
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (!isLoading) e.target.style.backgroundColor = '#c82333';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
if (!isLoading) e.target.style.backgroundColor = '#dc3545';
|
||
}}
|
||
>
|
||
{isLoading ? '변경 중...' : '🔒 비밀번호 변경'}
|
||
</button>
|
||
</form>
|
||
|
||
{/* 보안 안내 */}
|
||
<div style={{
|
||
marginTop: '32px',
|
||
padding: '16px',
|
||
backgroundColor: '#fff3cd',
|
||
border: '1px solid #ffeaa7',
|
||
borderRadius: '8px'
|
||
}}>
|
||
<h4 style={{ fontSize: '14px', fontWeight: '600', color: '#856404', margin: '0 0 8px 0' }}>
|
||
🔐 보안 안내
|
||
</h4>
|
||
<ul style={{ fontSize: '12px', color: '#856404', margin: 0, paddingLeft: '16px' }}>
|
||
<li>비밀번호는 8자 이상으로 설정해주세요</li>
|
||
<li>영문, 숫자, 특수문자를 조합하여 사용하는 것을 권장합니다</li>
|
||
<li>정기적으로 비밀번호를 변경해주세요</li>
|
||
<li>다른 사이트와 동일한 비밀번호 사용을 피해주세요</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AccountSettingsPage;
|