Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 상단 헤더에 🔔 알람 버튼 추가 (관리자 전용)
- 승인 대기 중인 사용자 수를 빨간 뱃지로 표시
- 클릭 시 사용자 관리 페이지로 이동
- 30초마다 자동 갱신
- 노란색 배경 + pulse 애니메이션으로 눈에 잘 띄게
- 마우스 오버 시 확대 효과
기능:
- GET /auth/signup-requests로 대기 수 조회
- 관리자(admin, system)만 표시
- 실시간 업데이트
1061 lines
41 KiB
JavaScript
1061 lines
41 KiB
JavaScript
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; |