Files
TK-BOM-Project/frontend/src/App.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

668 lines
25 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, useEffect } from 'react';
import SimpleLogin from './SimpleLogin';
import BOMWorkspacePage from './pages/BOMWorkspacePage';
import NewMaterialsPage from './pages/NewMaterialsPage';
import SystemSettingsPage from './pages/SystemSettingsPage';
import AccountSettingsPage from './pages/AccountSettingsPage';
import UserManagementPage from './pages/UserManagementPage';
import SystemLogsPage from './pages/SystemLogsPage';
import LogMonitoringPage from './pages/LogMonitoringPage';
import ErrorBoundary from './components/ErrorBoundary';
import errorLogger from './utils/errorLogger';
import './App.css';
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState(null);
const [currentPage, setCurrentPage] = useState('dashboard');
const [pageParams, setPageParams] = useState({});
const [selectedProject, setSelectedProject] = useState(null);
const [showUserMenu, setShowUserMenu] = useState(false);
useEffect(() => {
// 저장된 토큰 확인
const token = localStorage.getItem('access_token');
const userData = localStorage.getItem('user_data');
if (token && userData) {
setIsAuthenticated(true);
setUser(JSON.parse(userData));
}
setIsLoading(false);
// 자재 목록 페이지로 이동 이벤트 리스너
const handleNavigateToMaterials = (event) => {
const { jobNo, revision, bomName, message, file_id } = event.detail;
navigateToPage('materials', {
jobNo: jobNo,
revision: revision,
bomName: bomName,
message: message,
file_id: file_id // file_id 추가
});
};
window.addEventListener('navigateToMaterials', handleNavigateToMaterials);
return () => {
window.removeEventListener('navigateToMaterials', handleNavigateToMaterials);
};
}, []);
// 사용자 메뉴 외부 클릭 시 닫기
useEffect(() => {
const handleClickOutside = (event) => {
if (showUserMenu && !event.target.closest('.user-menu-container')) {
setShowUserMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showUserMenu]);
// 로그인 성공 시 호출될 함수
const handleLoginSuccess = () => {
const userData = localStorage.getItem('user_data');
if (userData) {
setUser(JSON.parse(userData));
}
setIsAuthenticated(true);
};
// 로그아웃 함수
const handleLogout = () => {
localStorage.removeItem('access_token');
localStorage.removeItem('user_data');
setIsAuthenticated(false);
setUser(null);
setCurrentPage('dashboard');
};
// 페이지 네비게이션 함수
const navigateToPage = (page, params = {}) => {
setCurrentPage(page);
setPageParams(params);
};
// 핵심 기능만 제공
const getCoreFeatures = () => {
return [
{
id: 'bom',
title: '📋 BOM 업로드 & 분류',
description: '엑셀 파일 업로드 → 자동 분류 → 검토 → 자재 확인 → 엑셀 내보내기',
color: '#4299e1'
}
];
};
// 관리자 전용 기능
const getAdminFeatures = () => {
const features = [];
// 시스템 관리자 전용 기능
if (user?.role === 'system') {
features.push(
{
id: 'user-management',
title: '👥 사용자 관리',
description: '계정 생성, 역할 변경, 사용자 삭제',
color: '#dc2626',
badge: '시스템 관리자'
},
{
id: 'system-logs',
title: '📊 시스템 로그',
description: '로그인 기록, 시스템 오류 로그 조회',
color: '#7c3aed',
badge: '시스템 관리자'
}
);
}
// 관리자 이상 공통 기능
if (user?.role === 'admin' || user?.role === 'system') {
features.push(
{
id: 'log-monitoring',
title: '📈 로그 모니터링',
description: '사용자 활동 로그 및 오류 모니터링',
color: '#059669',
badge: user?.role === 'system' ? '시스템 관리자' : '관리자'
}
);
}
return features;
};
// 페이지 렌더링 함수
const renderCurrentPage = () => {
console.log('현재 페이지:', currentPage, '페이지 파라미터:', pageParams);
switch (currentPage) {
case 'dashboard':
const coreFeatures = getCoreFeatures();
const adminFeatures = getAdminFeatures();
return (
<div style={{ minHeight: '100vh', background: '#f7fafc' }}>
{/* 상단 헤더 */}
<div style={{
background: 'white',
borderBottom: '1px solid #e2e8f0',
padding: '16px 32px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<h1 style={{ fontSize: '24px', fontWeight: '700', color: '#2d3748', margin: 0 }}>
🏭 TK-MP BOM 관리 시스템
</h1>
<p style={{ color: '#718096', fontSize: '14px', margin: '4px 0 0 0' }}>
{user?.name || user?.username} 환영합니다
</p>
</div>
{/* 사용자 메뉴 */}
<div className="user-menu-container" style={{ position: 'relative' }}>
<button
onClick={() => setShowUserMenu(!showUserMenu)}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: '#f8f9fa',
border: '1px solid #e9ecef',
borderRadius: '8px',
padding: '8px 12px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
color: '#495057',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.target.style.background = '#e9ecef';
e.target.style.borderColor = '#dee2e6';
}}
onMouseLeave={(e) => {
e.target.style.background = '#f8f9fa';
e.target.style.borderColor = '#e9ecef';
}}
>
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '14px',
fontWeight: '600'
}}>
{(user?.name || user?.username || 'U').charAt(0).toUpperCase()}
</div>
<div style={{ textAlign: 'left' }}>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#2d3748' }}>
{user?.name || user?.username}
</div>
<div style={{ fontSize: '12px', color: '#6c757d' }}>
{user?.role === 'system' ? '시스템 관리자' :
user?.role === 'admin' ? '관리자' : '사용자'}
</div>
</div>
<div style={{
fontSize: '12px',
color: '#6c757d',
transform: showUserMenu ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease'
}}>
</div>
</button>
{/* 드롭다운 메뉴 */}
{showUserMenu && (
<div style={{
position: 'absolute',
top: '100%',
right: 0,
marginTop: '4px',
background: 'white',
border: '1px solid #e9ecef',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
minWidth: '200px',
zIndex: 1000,
overflow: 'hidden'
}}>
<div style={{ padding: '12px 16px', borderBottom: '1px solid #f1f3f4' }}>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#2d3748' }}>
{user?.name || user?.username}
</div>
<div style={{ fontSize: '12px', color: '#6c757d', marginTop: '2px' }}>
{user?.email || '이메일 없음'}
</div>
</div>
<button
onClick={() => {
setShowUserMenu(false);
navigateToPage('account-settings');
}}
style={{
width: '100%',
padding: '12px 16px',
background: 'none',
border: 'none',
textAlign: 'left',
cursor: 'pointer',
fontSize: '14px',
color: '#495057',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'background-color 0.2s ease'
}}
onMouseEnter={(e) => e.target.style.background = '#f8f9fa'}
onMouseLeave={(e) => e.target.style.background = 'none'}
>
계정 설정
</button>
<button
onClick={() => {
setShowUserMenu(false);
handleLogout();
}}
style={{
width: '100%',
padding: '12px 16px',
background: 'none',
border: 'none',
textAlign: 'left',
cursor: 'pointer',
fontSize: '14px',
color: '#dc3545',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'background-color 0.2s ease',
borderTop: '1px solid #f1f3f4'
}}
onMouseEnter={(e) => e.target.style.background = '#fff5f5'}
onMouseLeave={(e) => e.target.style.background = 'none'}
>
🚪 로그아웃
</button>
</div>
)}
</div>
</div>
{/* 메인 콘텐츠 */}
<div style={{ padding: '32px', maxWidth: '800px', margin: '0 auto' }}>
{/* 프로젝트 선택 */}
<div style={{ marginBottom: '32px' }}>
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', marginBottom: '12px' }}>
📁 프로젝트 선택
</h2>
<select
value={selectedProject?.official_project_code || ''}
onChange={(e) => {
const projectCode = e.target.value;
if (projectCode) {
setSelectedProject({
official_project_code: projectCode,
project_name: e.target.options[e.target.selectedIndex].text.split(' - ')[1]
});
} else {
setSelectedProject(null);
}
}}
style={{
width: '100%',
padding: '12px',
border: '1px solid #e2e8f0',
borderRadius: '8px',
fontSize: '14px',
background: 'white'
}}
>
<option value="">프로젝트를 선택하세요</option>
<option value="J24-001">J24-001 - 테스트 프로젝트 A</option>
<option value="J24-002">J24-002 - 테스트 프로젝트 B</option>
<option value="J24-003">J24-003 - 테스트 프로젝트 C</option>
</select>
</div>
{/* 핵심 기능 */}
{selectedProject && (
<>
<div style={{ marginBottom: '32px' }}>
<h2 style={{ fontSize: '20px', fontWeight: '600', color: '#2d3748', marginBottom: '16px' }}>
📋 BOM 관리 워크플로우
</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '16px'
}}>
{coreFeatures.map((feature) => (
<div
key={feature.id}
style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.07)',
border: '1px solid #e2e8f0',
cursor: 'pointer',
transition: 'transform 0.2s, box-shadow 0.2s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 8px 12px rgba(0, 0, 0, 0.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.07)';
}}
>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', marginBottom: '12px' }}>
{feature.title}
</h3>
<p style={{ color: '#718096', marginBottom: '16px', fontSize: '14px' }}>
{feature.description}
</p>
<button
onClick={() => navigateToPage(feature.id, { selectedProject })}
style={{
background: feature.color,
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '12px 20px',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%'
}}
>
시작하기
</button>
</div>
))}
</div>
</div>
{/* 관리자 기능 (있는 경우만) */}
{adminFeatures.length > 0 && (
<div style={{ marginBottom: '32px' }}>
<h2 style={{ fontSize: '20px', fontWeight: '600', color: '#2d3748', marginBottom: '16px' }}>
시스템 관리
</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '16px'
}}>
{adminFeatures.map((feature) => (
<div
key={feature.id}
style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.07)',
border: '1px solid #e2e8f0',
cursor: 'pointer',
transition: 'transform 0.2s, box-shadow 0.2s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 8px 12px rgba(0, 0, 0, 0.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.07)';
}}
>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', marginBottom: '12px' }}>
{feature.title}
</h3>
<p style={{ color: '#718096', marginBottom: '16px', fontSize: '14px' }}>
{feature.description}
</p>
<div style={{ marginBottom: '12px' }}>
<span style={{
background: feature.badge === '시스템 관리자' ? '#fef2f2' : '#fef7e0',
color: feature.badge === '시스템 관리자' ? '#dc2626' : '#92400e',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: '600'
}}>
{feature.badge} 전용
</span>
</div>
<button
onClick={() => navigateToPage(feature.id)}
style={{
background: feature.color,
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '12px 20px',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%'
}}
>
관리하기
</button>
</div>
))}
</div>
</div>
)}
{/* 간단한 사용법 안내 */}
<div style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
border: '1px solid #e2e8f0'
}}>
<h3 style={{ fontSize: '16px', fontWeight: '600', color: '#2d3748', marginBottom: '16px' }}>
📖 간단한 사용법
</h3>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{
background: '#4299e1',
color: 'white',
borderRadius: '50%',
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: '600'
}}>1</span>
<span style={{ fontSize: '14px', color: '#4a5568' }}>BOM 업로드</span>
</div>
<span style={{ color: '#a0aec0' }}></span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{
background: '#ed8936',
color: 'white',
borderRadius: '50%',
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: '600'
}}>2</span>
<span style={{ fontSize: '14px', color: '#4a5568' }}>자동 분류</span>
</div>
<span style={{ color: '#a0aec0' }}></span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{
background: '#48bb78',
color: 'white',
borderRadius: '50%',
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: '600'
}}>3</span>
<span style={{ fontSize: '14px', color: '#4a5568' }}>엑셀 내보내기</span>
</div>
</div>
</div>
</>
)} {/* selectedProject 조건문 닫기 */}
</div>
</div>
);
case 'bom':
return (
<BOMWorkspacePage
project={pageParams.selectedProject}
onNavigate={navigateToPage}
onBack={() => navigateToPage('dashboard')}
/>
);
case 'materials':
return (
<NewMaterialsPage
onNavigate={navigateToPage}
selectedProject={pageParams.selectedProject}
fileId={pageParams.file_id}
jobNo={pageParams.jobNo}
bomName={pageParams.bomName}
revision={pageParams.revision}
filename={pageParams.filename}
/>
);
case 'system-settings':
return (
<SystemSettingsPage
onNavigate={navigateToPage}
user={user}
/>
);
case 'account-settings':
return (
<AccountSettingsPage
onNavigate={navigateToPage}
user={user}
onUserUpdate={(updatedUser) => {
setUser(updatedUser);
localStorage.setItem('user_data', JSON.stringify(updatedUser));
}}
/>
);
case 'user-management':
return (
<UserManagementPage
onNavigate={navigateToPage}
user={user}
/>
);
case 'system-logs':
return (
<SystemLogsPage
onNavigate={navigateToPage}
user={user}
/>
);
case 'log-monitoring':
return (
<LogMonitoringPage
onNavigate={navigateToPage}
user={user}
/>
);
default:
return (
<div style={{ padding: '32px', textAlign: 'center' }}>
<h2>페이지를 찾을 없습니다</h2>
<button
onClick={() => navigateToPage('dashboard')}
style={{
background: '#4299e1',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '12px 24px',
cursor: 'pointer',
marginTop: '16px'
}}
>
대시보드로 돌아가기
</button>
</div>
);
}
};
// 로딩 중
if (isLoading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f7fafc'
}}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '16px' }}>🔄</div>
<div style={{ fontSize: '16px', color: '#718096' }}>로딩 ...</div>
</div>
</div>
);
}
// 로그인하지 않은 경우
if (!isAuthenticated) {
return <SimpleLogin onLoginSuccess={handleLoginSuccess} />;
}
// 메인 애플리케이션
return (
<ErrorBoundary errorContext={{ user, currentPage, pageParams }}>
<div style={{ minHeight: '100vh', background: '#f7fafc' }}>
{renderCurrentPage()}
</div>
</ErrorBoundary>
);
}
export default App;