Files
TK-BOM-Project/frontend/src/pages/AccountSettingsPage.jsx
Hyungi Ahn 529777aa14
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 완전한 사용자 관리 및 로그 모니터링 시스템 구현
- 시스템 관리자/관리자 권한별 대시보드 기능 추가
- 사용자 관리 페이지: 계정 생성, 역할 변경, 사용자 삭제
- 시스템 로그 페이지: 로그인 로그, 시스템 오류 로그 조회
- 로그 모니터링 대시보드: 실시간 통계, 최근 활동, 오류 모니터링
- 프론트엔드 ErrorBoundary 및 오류 로깅 시스템 통합
- 계정 설정 페이지: 프로필 업데이트, 비밀번호 변경
- 3단계 권한 시스템 (system/admin/user) 완전 구현
- 시스템 관리자 계정 생성 기능 (hyungi/000000)
- 로그인 페이지 테스트 계정 안내 제거
- API 오류 수정: CORS, 이메일 검증, User 모델 import 등
2025-09-09 12:58:14 +09:00

706 lines
34 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;