Files
TK-BOM-Project/frontend/src/App.jsx
Hyungi Ahn 521446d56b
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
🔔 회원가입 승인 알람 기능 추가
- 상단 헤더에 🔔 알람 버튼 추가 (관리자 전용)
- 승인 대기 중인 사용자 수를 빨간 뱃지로 표시
- 클릭 시 사용자 관리 페이지로 이동
- 30초마다 자동 갱신
- 노란색 배경 + pulse 애니메이션으로 눈에 잘 띄게
- 마우스 오버 시 확대 효과

기능:
- GET /auth/signup-requests로 대기 수 조회
- 관리자(admin, system)만 표시
- 실시간 업데이트
2025-10-14 07:51:16 +09:00

1061 lines
41 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.
This file contains Unicode characters that might be confused with other characters. 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 api from './api';
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);
const [projects, setProjects] = useState([]);
const [editingProject, setEditingProject] = useState(null);
const [editedProjectName, setEditedProjectName] = useState('');
const [showCreateProject, setShowCreateProject] = useState(false);
const [newProjectCode, setNewProjectCode] = useState('');
const [newProjectName, setNewProjectName] = useState('');
const [newClientName, setNewClientName] = useState('');
const [pendingSignupCount, setPendingSignupCount] = useState(0);
// 승인 대기 중인 회원가입 수 조회
const loadPendingSignups = async () => {
try {
const response = await api.get('/auth/signup-requests');
if (response.data && response.data.requests) {
setPendingSignupCount(response.data.count || 0);
}
} catch (error) {
// 에러 무시 (관리자가 아니면 403)
}
};
// 프로젝트 목록 로드
const loadProjects = async () => {
try {
const response = await api.get('/dashboard/projects');
if (response.data && response.data.projects) {
setProjects(response.data.projects);
}
} catch (error) {
console.error('프로젝트 목록 로드 실패:', error);
// API 실패 시 에러를 무시하고 더미 데이터 사용
}
};
// 프로젝트 생성
const createProject = async () => {
if (!newProjectCode || !newProjectName) {
alert('프로젝트 코드와 이름을 입력해주세요.');
return;
}
try {
const response = await api.post(`/dashboard/projects?official_project_code=${encodeURIComponent(newProjectCode)}&project_name=${encodeURIComponent(newProjectName)}&client_name=${encodeURIComponent(newClientName)}`);
if (response.data.success) {
// 프로젝트 목록 갱신
await loadProjects();
// 폼 초기화
setShowCreateProject(false);
setNewProjectCode('');
setNewProjectName('');
setNewClientName('');
alert('프로젝트가 생성되었습니다.');
}
} catch (error) {
console.error('프로젝트 생성 실패:', error);
const errorMsg = error.response?.data?.detail || '프로젝트 생성에 실패했습니다.';
alert(errorMsg);
}
};
// 프로젝트 이름 수정
const updateProjectName = async (projectId) => {
try {
const response = await api.patch(`/dashboard/projects/${projectId}?job_name=${encodeURIComponent(editedProjectName)}`);
if (response.data.success) {
// 프로젝트 목록 갱신
await loadProjects();
// 선택된 프로젝트 업데이트
if (selectedProject && selectedProject.id === projectId) {
setSelectedProject({
...selectedProject,
project_name: editedProjectName
});
}
setEditingProject(null);
setEditedProjectName('');
alert('프로젝트 이름이 수정되었습니다.');
}
} catch (error) {
console.error('프로젝트 이름 수정 실패:', error);
alert('프로젝트 이름 수정에 실패했습니다.');
}
};
useEffect(() => {
// 저장된 토큰 확인
const token = localStorage.getItem('access_token');
const userData = localStorage.getItem('user_data');
if (token && userData) {
setIsAuthenticated(true);
const userObj = JSON.parse(userData);
setUser(userObj);
loadProjects(); // 프로젝트 목록 로드
// 관리자인 경우 승인 대기 수 조회
if (userObj.role === 'admin' || userObj.role === 'system') {
loadPendingSignups();
// 30초마다 갱신
const interval = setInterval(loadPendingSignups, 30000);
return () => clearInterval(interval);
}
}
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 style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
{/* 회원가입 승인 알람 (관리자 전용) */}
{(user?.role === 'admin' || user?.role === 'system') && pendingSignupCount > 0 && (
<button
onClick={() => navigateToPage('user-management')}
style={{
position: 'relative',
width: '44px',
height: '44px',
background: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
transition: 'all 0.2s ease',
animation: 'pulse 2s ease-in-out infinite'
}}
title={`회원가입 승인 대기: ${pendingSignupCount}`}
onMouseEnter={(e) => {
e.target.style.background = '#fde68a';
e.target.style.transform = 'scale(1.1)';
}}
onMouseLeave={(e) => {
e.target.style.background = '#fef3c7';
e.target.style.transform = 'scale(1)';
}}
>
🔔
<div style={{
position: 'absolute',
top: '-4px',
right: '-4px',
background: '#ef4444',
color: 'white',
borderRadius: '50%',
width: '22px',
height: '22px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '11px',
fontWeight: '700',
border: '2px solid white',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
}}>
{pendingSignupCount}
</div>
</button>
)}
{/* 사용자 메뉴 */}
<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' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', margin: 0 }}>
📁 프로젝트 관리
</h2>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => setShowCreateProject(!showCreateProject)}
style={{
padding: '6px 12px',
background: showCreateProject ? '#ef4444' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
>
{showCreateProject ? '✕ 닫기' : ' 새 프로젝트'}
</button>
{selectedProject && (
<button
onClick={() => {
setEditingProject(selectedProject);
setEditedProjectName(selectedProject.project_name || '');
}}
style={{
padding: '6px 12px',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
title="프로젝트 이름 수정"
>
이름 수정
</button>
)}
</div>
</div>
{/* 프로젝트 생성 폼 */}
{showCreateProject && (
<div style={{
background: '#f0fdf4',
border: '2px solid #10b981',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px'
}}>
<div style={{ marginBottom: '12px', fontWeight: '600', color: '#065f46', fontSize: '14px' }}>
프로젝트 생성
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', fontWeight: '500', color: '#374151' }}>
프로젝트 코드 <span style={{ color: '#ef4444' }}>*</span>
</label>
<input
type="text"
value={newProjectCode}
onChange={(e) => setNewProjectCode(e.target.value)}
placeholder="예: J24-004"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', fontWeight: '500', color: '#374151' }}>
프로젝트 이름 <span style={{ color: '#ef4444' }}>*</span>
</label>
<input
type="text"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
placeholder="예: 새로운 프로젝트"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px', fontWeight: '500', color: '#374151' }}>
고객사명 (선택)
</label>
<input
type="text"
value={newClientName}
onChange={(e) => setNewClientName(e.target.value)}
placeholder="예: ABC 주식회사"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px'
}}
/>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button
onClick={createProject}
style={{
flex: 1,
padding: '12px',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
프로젝트 생성
</button>
<button
onClick={() => {
setShowCreateProject(false);
setNewProjectCode('');
setNewProjectName('');
setNewClientName('');
}}
style={{
padding: '12px 24px',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
취소
</button>
</div>
</div>
</div>
)}
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: '500', color: '#374151' }}>
프로젝트 선택
</label>
<select
value={selectedProject?.official_project_code || ''}
onChange={(e) => {
const projectCode = e.target.value;
if (projectCode) {
const project = projects.find(p => p.official_project_code === projectCode);
setSelectedProject(project || {
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>
{projects.length > 0 ? (
projects.map(project => (
<option key={project.id || project.official_project_code} value={project.official_project_code}>
{project.official_project_code} - {project.project_name || project.job_name}
</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>
{/* 프로젝트 이름 편집 폼 */}
{editingProject && (
<div style={{
marginTop: '16px',
background: '#eff6ff',
border: '2px solid #3b82f6',
borderRadius: '8px',
padding: '16px'
}}>
<div style={{ marginBottom: '12px', fontWeight: '600', color: '#1e40af', fontSize: '14px' }}>
프로젝트 이름 수정: {editingProject.official_project_code}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={editedProjectName}
onChange={(e) => setEditedProjectName(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') updateProjectName(editingProject.id);
}}
placeholder="새 프로젝트 이름"
autoFocus
style={{
flex: 1,
padding: '10px 12px',
border: '2px solid #3b82f6',
borderRadius: '6px',
fontSize: '14px'
}}
/>
<button
onClick={() => updateProjectName(editingProject.id)}
style={{
padding: '10px 20px',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
💾 저장
</button>
<button
onClick={() => {
setEditingProject(null);
setEditedProjectName('');
}}
style={{
padding: '10px 20px',
background: '#ef4444',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
취소
</button>
</div>
</div>
)}
</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;