feat: 자재 관리 페이지 대규모 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산)
- UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인)
- 자재별 그룹핑 로직 개선:
  * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시
  * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등)
  * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시
  * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계)
  * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시
  * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑
- 전체 카테고리 버튼 제거 (표시 복잡도 감소)
- 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
This commit is contained in:
Hyungi Ahn
2025-09-09 09:24:45 +09:00
parent 4f8e395f87
commit 83b90ef05c
101 changed files with 10841 additions and 4813 deletions

View File

@@ -1,13 +1,8 @@
import React, { useState, useEffect } from 'react';
import SimpleLogin from './SimpleLogin';
import NavigationMenu from './components/NavigationMenu';
import DashboardPage from './pages/DashboardPage';
import ProjectsPage from './pages/ProjectsPage';
import BOMStatusPage from './pages/BOMStatusPage';
import SimpleMaterialsPage from './pages/SimpleMaterialsPage';
import MaterialComparisonPage from './pages/MaterialComparisonPage';
import RevisionPurchasePage from './pages/RevisionPurchasePage';
import JobSelectionPage from './pages/JobSelectionPage';
import BOMWorkspacePage from './pages/BOMWorkspacePage';
import NewMaterialsPage from './pages/NewMaterialsPage';
import SystemSettingsPage from './pages/SystemSettingsPage';
import './App.css';
function App() {
@@ -16,6 +11,7 @@ function App() {
const [user, setUser] = useState(null);
const [currentPage, setCurrentPage] = useState('dashboard');
const [pageParams, setPageParams] = useState({});
const [selectedProject, setSelectedProject] = useState(null);
useEffect(() => {
// 저장된 토큰 확인
@@ -28,6 +24,24 @@ function App() {
}
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);
};
}, []);
// 로그인 성공 시 호출될 함수
@@ -54,152 +68,393 @@ function App() {
setPageParams(params);
};
// 핵심 기능만 제공
const getCoreFeatures = () => {
return [
{
id: 'bom',
title: '📋 BOM 업로드 & 분류',
description: '엑셀 파일 업로드 → 자동 분류 → 검토 → 자재 확인 → 엑셀 내보내기',
color: '#4299e1'
}
];
};
// 관리자 전용 기능
const getAdminFeatures = () => {
if (user?.role !== 'admin') return [];
return [
{
id: 'system-settings',
title: '⚙️ 시스템 설정',
description: '사용자 계정 관리',
color: '#dc2626'
}
];
};
// 페이지 렌더링 함수
const renderCurrentPage = () => {
console.log('현재 페이지:', currentPage, '페이지 파라미터:', pageParams);
switch (currentPage) {
case 'dashboard':
return <DashboardPage user={user} />;
case 'projects':
return <ProjectsPage user={user} />;
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?.full_name || user?.username} 환영합니다
</p>
</div>
<button
onClick={handleLogout}
style={{
background: '#e2e8f0',
color: '#4a5568',
border: 'none',
borderRadius: '6px',
padding: '10px 16px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
}}
>
로그아웃
</button>
</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: '#fef7e0',
color: '#92400e',
padding: '2px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: '600'
}}>
관리자 전용
</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 <JobSelectionPage onJobSelect={(jobNo, jobName) =>
navigateToPage('bom-status', { job_no: jobNo, job_name: jobName })
} />;
case 'bom-status':
return <BOMStatusPage
jobNo={pageParams.job_no}
jobName={pageParams.job_name}
onNavigate={navigateToPage}
/>;
return (
<BOMWorkspacePage
project={pageParams.selectedProject}
onNavigate={navigateToPage}
onBack={() => navigateToPage('dashboard')}
/>
);
case 'materials':
return <SimpleMaterialsPage
fileId={pageParams.file_id}
jobNo={pageParams.jobNo}
bomName={pageParams.bomName}
revision={pageParams.revision}
filename={pageParams.filename}
onNavigate={navigateToPage}
/>;
case 'material-comparison':
return <MaterialComparisonPage
jobNo={pageParams.job_no}
currentRevision={pageParams.current_revision}
previousRevision={pageParams.previous_revision}
filename={pageParams.filename}
onNavigate={navigateToPage}
/>;
case 'revision-purchase':
return <RevisionPurchasePage
jobNo={pageParams.job_no}
revision={pageParams.revision}
onNavigate={navigateToPage}
/>;
case 'quotes':
return <div style={{ padding: '32px' }}>💰 견적 관리 페이지 ( 구현 예정)</div>;
case 'procurement':
return <div style={{ padding: '32px' }}>🛒 구매 관리 페이지 ( 구현 예정)</div>;
case 'production':
return <div style={{ padding: '32px' }}>🏭 생산 관리 페이지 ( 구현 예정)</div>;
case 'shipment':
return <div style={{ padding: '32px' }}>🚚 출하 관리 페이지 ( 구현 예정)</div>;
case 'users':
return <div style={{ padding: '32px' }}>👥 사용자 관리 페이지 ( 구현 예정)</div>;
case 'system':
return <div style={{ padding: '32px' }}> 시스템 설정 페이지 ( 구현 예정)</div>;
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}
/>
);
default:
return <DashboardPage user={user} />;
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={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
background: '#f7fafc'
}}>
<div>로딩 ...</div>
<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 (
<div style={{ display: 'flex', minHeight: '100vh' }}>
<NavigationMenu
user={user}
currentPage={currentPage}
onPageChange={(page) => navigateToPage(page, {})}
/>
{/* 메인 콘텐츠 영역 */}
<div style={{
flex: 1,
marginLeft: '280px', // 사이드바 너비만큼 여백
background: '#f7fafc'
}}>
{/* 상단 헤더 */}
<header style={{
background: 'white',
borderBottom: '1px solid #e2e8f0',
padding: '16px 32px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<h2 style={{
margin: '0',
fontSize: '18px',
fontWeight: '600',
color: '#2d3748'
}}>
{currentPage === 'dashboard' && '대시보드'}
{currentPage === 'projects' && '프로젝트 관리'}
{currentPage === 'bom' && 'BOM 관리'}
{currentPage === 'materials' && '자재 관리'}
{currentPage === 'quotes' && '견적 관리'}
{currentPage === 'procurement' && '구매 관리'}
{currentPage === 'production' && '생산 관리'}
{currentPage === 'shipment' && '출하 관리'}
{currentPage === 'users' && '사용자 관리'}
{currentPage === 'system' && '시스템 설정'}
</h2>
</div>
<button
onClick={handleLogout}
style={{
padding: '8px 16px',
background: '#e53e3e',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}
>
<span>🚪</span>
로그아웃
</button>
</header>
{/* 페이지 콘텐츠 */}
<main>
{renderCurrentPage()}
</main>
</div>
<div style={{ minHeight: '100vh', background: '#f7fafc' }}>
{renderCurrentPage()}
</div>
);
}
export default App;
export default App;