- 주일기업 기획 하위 '업무 Workflow' 메뉴 추가 - 11단계 업무처리과정 인터랙티브 플로우차트 구현 - 각 단계 클릭 시 상세정보(담당부서, 필요서류, SAM 연동) 표시
571 lines
21 KiB
PHP
571 lines
21 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '업무 Workflow')
|
|
|
|
@section('content')
|
|
<div id="root"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
@include('partials.react-cdn')
|
|
<script type="text/babel">
|
|
@verbatim
|
|
const { useState, useRef, useEffect } = React;
|
|
|
|
// --- 업무 프로세스 데이터 ---
|
|
const processes = [
|
|
{
|
|
id: 1,
|
|
phase: 'sales',
|
|
name: '영업/수주',
|
|
icon: '📋',
|
|
dept: '영업팀',
|
|
color: '#3B82F6',
|
|
bgColor: '#EFF6FF',
|
|
description: '건설사/시행사로부터 프로젝트 정보 수집 및 수주 활동',
|
|
details: [
|
|
'건설사 입찰공고 모니터링',
|
|
'현장 실측 및 요구사항 파악',
|
|
'고객사 미팅 및 관계 관리',
|
|
],
|
|
documents: ['입찰공고문', '현장조사서', '고객 요구사항서'],
|
|
samLink: null,
|
|
samMenu: null,
|
|
},
|
|
{
|
|
id: 2,
|
|
phase: 'estimate',
|
|
name: '견적서 작성',
|
|
icon: '🧮',
|
|
dept: '견적팀',
|
|
color: '#8B5CF6',
|
|
bgColor: '#F5F3FF',
|
|
description: '자재/인건비/경비를 산출하여 견적서 작성',
|
|
details: [
|
|
'자재 단가 산출 (블라인드, 스크린, 셔터)',
|
|
'인건비/시공비 산정',
|
|
'이윤율 적용 및 최종 견적가 확정',
|
|
],
|
|
documents: ['견적서', '단가산출서', '자재목록'],
|
|
samLink: '/juil/estimate',
|
|
samMenu: '견적/입찰/공사관리',
|
|
},
|
|
{
|
|
id: 3,
|
|
phase: 'bid',
|
|
name: '입찰 참여',
|
|
icon: '🏷️',
|
|
dept: '영업팀',
|
|
color: '#EC4899',
|
|
bgColor: '#FDF2F8',
|
|
description: '견적서 기반으로 입찰에 참여',
|
|
details: [
|
|
'입찰서류 준비 및 제출',
|
|
'기술제안서 작성',
|
|
'가격 협상',
|
|
],
|
|
documents: ['입찰서', '기술제안서', '사업자등록증 사본'],
|
|
samLink: '/juil/estimate',
|
|
samMenu: '견적/입찰/공사관리',
|
|
branch: true,
|
|
},
|
|
{
|
|
id: 4,
|
|
phase: 'contract',
|
|
name: '낙찰/계약',
|
|
icon: '📝',
|
|
dept: '영업팀',
|
|
color: '#10B981',
|
|
bgColor: '#ECFDF5',
|
|
description: '낙찰 후 공사 계약 체결',
|
|
details: [
|
|
'계약서 작성 및 검토',
|
|
'계약금 수령',
|
|
'프로젝트 등록 및 담당자 배정',
|
|
],
|
|
documents: ['공사계약서', '착공계', '공정표'],
|
|
samLink: '/juil/project',
|
|
samMenu: '프로젝트관리/기성청구',
|
|
},
|
|
{
|
|
id: 5,
|
|
phase: 'order',
|
|
name: '자재 발주',
|
|
icon: '📦',
|
|
dept: '구매팀',
|
|
color: '#F59E0B',
|
|
bgColor: '#FFFBEB',
|
|
description: '시공에 필요한 자재를 발주',
|
|
details: [
|
|
'BOM(자재명세서) 기반 발주량 산정',
|
|
'협력업체 발주서 발행',
|
|
'납기일 관리',
|
|
],
|
|
documents: ['발주서', 'BOM', '납품요청서'],
|
|
samLink: null,
|
|
samMenu: null,
|
|
},
|
|
{
|
|
id: 6,
|
|
phase: 'receive',
|
|
name: '자재 입고/검수',
|
|
icon: '🔍',
|
|
dept: '자재팀',
|
|
color: '#06B6D4',
|
|
bgColor: '#ECFEFF',
|
|
description: '입고된 자재의 수량/품질 검수',
|
|
details: [
|
|
'입고 수량 확인',
|
|
'품질 검사 (규격, 색상, 하자)',
|
|
'불량 자재 반품 처리',
|
|
],
|
|
documents: ['입고검수서', '거래명세서', '반품요청서'],
|
|
samLink: null,
|
|
samMenu: null,
|
|
},
|
|
{
|
|
id: 7,
|
|
phase: 'construct',
|
|
name: '현장 시공',
|
|
icon: '🔧',
|
|
dept: '시공팀',
|
|
color: '#EF4444',
|
|
bgColor: '#FEF2F2',
|
|
description: '현장에서 블라인드/스크린/셔터 설치 시공',
|
|
details: [
|
|
'시공 일정 조율 (건설사와 협의)',
|
|
'현장 설치 작업',
|
|
'시공 사진 촬영 및 기록',
|
|
],
|
|
documents: ['시공계획서', '작업일보', '시공사진'],
|
|
samLink: '/juil/construction-photos',
|
|
samMenu: '공사현장 사진대지',
|
|
},
|
|
{
|
|
id: 8,
|
|
phase: 'inspect',
|
|
name: '시공 검수',
|
|
icon: '✅',
|
|
dept: '현장팀',
|
|
color: '#14B8A6',
|
|
bgColor: '#F0FDFA',
|
|
description: '시공 완료 후 품질 검수 및 하자 보수',
|
|
details: [
|
|
'건설사 합동 검수',
|
|
'하자 사항 보수',
|
|
'검수 완료 확인서 수령',
|
|
],
|
|
documents: ['검수확인서', '하자보수보고서', '준공사진'],
|
|
samLink: '/juil/construction-photos',
|
|
samMenu: '공사현장 사진대지',
|
|
},
|
|
{
|
|
id: 9,
|
|
phase: 'billing',
|
|
name: '기성 청구',
|
|
icon: '💰',
|
|
dept: '경리팀',
|
|
color: '#7C3AED',
|
|
bgColor: '#F5F3FF',
|
|
description: '시공 진행률에 따른 기성금 청구',
|
|
details: [
|
|
'기성 내역서 작성',
|
|
'세금계산서 발행',
|
|
'기성금 청구서 제출',
|
|
],
|
|
documents: ['기성내역서', '세금계산서', '기성청구서'],
|
|
samLink: '/juil/project',
|
|
samMenu: '프로젝트관리/기성청구',
|
|
},
|
|
{
|
|
id: 10,
|
|
phase: 'payment',
|
|
name: '입금 확인',
|
|
icon: '🏦',
|
|
dept: '경리팀',
|
|
color: '#059669',
|
|
bgColor: '#ECFDF5',
|
|
description: '기성금 입금 확인 및 매출 처리',
|
|
details: [
|
|
'입금 내역 대사',
|
|
'미수금 관리',
|
|
'매출 장부 기록',
|
|
],
|
|
documents: ['입금확인서', '매출대장'],
|
|
samLink: null,
|
|
samMenu: null,
|
|
},
|
|
{
|
|
id: 11,
|
|
phase: 'as',
|
|
name: 'A/S 관리',
|
|
icon: '🛠️',
|
|
dept: '시공팀',
|
|
color: '#6B7280',
|
|
bgColor: '#F9FAFB',
|
|
description: '하자보수기간 내 A/S 대응',
|
|
details: [
|
|
'A/S 접수 및 처리',
|
|
'하자보수 비용 관리',
|
|
'보증기간 만료 관리',
|
|
],
|
|
documents: ['A/S 접수대장', '하자보수 보고서'],
|
|
samLink: null,
|
|
samMenu: null,
|
|
},
|
|
];
|
|
|
|
// --- 화살표 컴포넌트 ---
|
|
function Arrow({ direction = 'right', branch = false }) {
|
|
if (direction === 'down') {
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: '4px 0' }}>
|
|
<div style={{
|
|
width: 0, height: 0,
|
|
borderLeft: '8px solid transparent',
|
|
borderRight: '8px solid transparent',
|
|
borderTop: '10px solid #9CA3AF',
|
|
}}></div>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
minWidth: '40px', position: 'relative',
|
|
}}>
|
|
<div style={{
|
|
width: '100%', height: '2px', backgroundColor: '#9CA3AF',
|
|
}}></div>
|
|
<div style={{
|
|
position: 'absolute', right: 0,
|
|
width: 0, height: 0,
|
|
borderTop: '6px solid transparent',
|
|
borderBottom: '6px solid transparent',
|
|
borderLeft: '8px solid #9CA3AF',
|
|
}}></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- 프로세스 노드 컴포넌트 ---
|
|
function ProcessNode({ process, isActive, onClick }) {
|
|
return (
|
|
<div
|
|
onClick={() => onClick(process)}
|
|
style={{
|
|
minWidth: '140px',
|
|
maxWidth: '160px',
|
|
padding: '12px',
|
|
borderRadius: '12px',
|
|
border: `2px solid ${isActive ? process.color : '#E5E7EB'}`,
|
|
backgroundColor: isActive ? process.bgColor : '#FFFFFF',
|
|
cursor: 'pointer',
|
|
transition: 'all 0.2s ease',
|
|
boxShadow: isActive ? `0 4px 12px ${process.color}33` : '0 1px 3px rgba(0,0,0,0.1)',
|
|
textAlign: 'center',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<div style={{ fontSize: '24px', marginBottom: '6px' }}>{process.icon}</div>
|
|
<div style={{
|
|
fontSize: '13px', fontWeight: 700, color: '#1F2937',
|
|
marginBottom: '4px', whiteSpace: 'nowrap',
|
|
}}>{process.name}</div>
|
|
<div style={{
|
|
fontSize: '11px',
|
|
color: '#FFFFFF',
|
|
backgroundColor: process.color,
|
|
borderRadius: '10px',
|
|
padding: '1px 8px',
|
|
display: 'inline-block',
|
|
}}>{process.dept}</div>
|
|
{process.branch && (
|
|
<div style={{
|
|
position: 'absolute', top: '-8px', right: '-8px',
|
|
width: '20px', height: '20px', borderRadius: '50%',
|
|
backgroundColor: '#FEF3C7', border: '2px solid #F59E0B',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: '10px',
|
|
}}>⑂</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- 상세 패널 컴포넌트 ---
|
|
function DetailPanel({ process, onClose }) {
|
|
if (!process) return null;
|
|
return (
|
|
<div style={{
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: '16px',
|
|
border: `2px solid ${process.color}`,
|
|
padding: '24px',
|
|
boxShadow: '0 10px 25px rgba(0,0,0,0.1)',
|
|
position: 'relative',
|
|
}}>
|
|
<button
|
|
onClick={onClose}
|
|
style={{
|
|
position: 'absolute', top: '12px', right: '12px',
|
|
width: '28px', height: '28px', borderRadius: '50%',
|
|
border: '1px solid #E5E7EB', backgroundColor: '#F9FAFB',
|
|
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: '14px', color: '#6B7280',
|
|
}}
|
|
>×</button>
|
|
|
|
{/* 헤더 */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
|
|
<div style={{
|
|
width: '48px', height: '48px', borderRadius: '12px',
|
|
backgroundColor: process.bgColor, display: 'flex',
|
|
alignItems: 'center', justifyContent: 'center', fontSize: '24px',
|
|
}}>{process.icon}</div>
|
|
<div>
|
|
<div style={{ fontSize: '18px', fontWeight: 700, color: '#1F2937' }}>
|
|
<span style={{
|
|
display: 'inline-block', width: '24px', height: '24px',
|
|
borderRadius: '50%', backgroundColor: process.color, color: '#FFF',
|
|
fontSize: '12px', textAlign: 'center', lineHeight: '24px',
|
|
marginRight: '8px',
|
|
}}>{process.id}</span>
|
|
{process.name}
|
|
</div>
|
|
<div style={{ fontSize: '13px', color: '#6B7280', marginTop: '2px' }}>
|
|
담당: <span style={{ color: process.color, fontWeight: 600 }}>{process.dept}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 설명 */}
|
|
<div style={{
|
|
fontSize: '14px', color: '#374151', marginBottom: '16px',
|
|
padding: '12px', backgroundColor: process.bgColor, borderRadius: '8px',
|
|
borderLeft: `4px solid ${process.color}`,
|
|
}}>
|
|
{process.description}
|
|
</div>
|
|
|
|
{/* 3컬럼 그리드 */}
|
|
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
|
|
{/* 상세 업무 */}
|
|
<div style={{ flex: '1 1 200px' }}>
|
|
<div style={{
|
|
fontSize: '13px', fontWeight: 700, color: '#374151',
|
|
marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '4px',
|
|
}}>
|
|
<span style={{ color: process.color }}>●</span> 상세 업무
|
|
</div>
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
|
{process.details.map((d, i) => (
|
|
<li key={i} style={{
|
|
fontSize: '12px', color: '#4B5563', padding: '4px 0',
|
|
borderBottom: '1px solid #F3F4F6',
|
|
paddingLeft: '12px', position: 'relative',
|
|
}}>
|
|
<span style={{ position: 'absolute', left: 0, color: '#9CA3AF' }}>-</span>
|
|
{d}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* 필요 서류 */}
|
|
<div style={{ flex: '1 1 200px' }}>
|
|
<div style={{
|
|
fontSize: '13px', fontWeight: 700, color: '#374151',
|
|
marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '4px',
|
|
}}>
|
|
<span style={{ color: process.color }}>●</span> 필요 서류
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
|
{process.documents.map((doc, i) => (
|
|
<span key={i} style={{
|
|
fontSize: '11px', padding: '3px 10px',
|
|
backgroundColor: '#F3F4F6', borderRadius: '12px',
|
|
color: '#374151', whiteSpace: 'nowrap',
|
|
}}>{doc}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* SAM 연동 */}
|
|
<div style={{ flex: '1 1 150px' }}>
|
|
<div style={{
|
|
fontSize: '13px', fontWeight: 700, color: '#374151',
|
|
marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '4px',
|
|
}}>
|
|
<span style={{ color: process.color }}>●</span> SAM 연동
|
|
</div>
|
|
{process.samLink ? (
|
|
<a
|
|
href={process.samLink}
|
|
style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
|
fontSize: '12px', color: process.color, fontWeight: 600,
|
|
padding: '6px 12px', borderRadius: '8px',
|
|
backgroundColor: process.bgColor,
|
|
border: `1px solid ${process.color}40`,
|
|
textDecoration: 'none',
|
|
}}
|
|
>
|
|
↗ {process.samMenu}
|
|
</a>
|
|
) : (
|
|
<span style={{
|
|
fontSize: '12px', color: '#9CA3AF', fontStyle: 'italic',
|
|
}}>추후 연동 예정</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- 입찰 분기 표시 ---
|
|
function BranchInfo() {
|
|
return (
|
|
<div style={{
|
|
display: 'flex', gap: '8px', justifyContent: 'center',
|
|
margin: '8px 0', fontSize: '11px',
|
|
}}>
|
|
<span style={{
|
|
padding: '2px 10px', borderRadius: '10px',
|
|
backgroundColor: '#DCFCE7', color: '#166534', fontWeight: 600,
|
|
}}>낙찰 → 계약 진행</span>
|
|
<span style={{
|
|
padding: '2px 10px', borderRadius: '10px',
|
|
backgroundColor: '#FEE2E2', color: '#991B1B', fontWeight: 600,
|
|
}}>유찰 → 재입찰/종료</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- 메인 앱 ---
|
|
function App() {
|
|
const [selected, setSelected] = useState(null);
|
|
|
|
const topRow = processes.slice(0, 6); // 영업~입고검수
|
|
const bottomRow = processes.slice(6); // 시공~A/S
|
|
|
|
return (
|
|
<div style={{ padding: '20px' }}>
|
|
{/* 헤더 */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
marginBottom: '24px',
|
|
}}>
|
|
<div>
|
|
<h1 style={{
|
|
fontSize: '22px', fontWeight: 800, color: '#111827', margin: 0,
|
|
}}>업무 Workflow</h1>
|
|
<p style={{ fontSize: '13px', color: '#6B7280', margin: '4px 0 0' }}>
|
|
주일기업 업무처리과정 플로우차트 — 각 단계를 클릭하면 상세 정보를 확인할 수 있습니다
|
|
</p>
|
|
</div>
|
|
<div style={{
|
|
display: 'flex', gap: '6px', flexWrap: 'wrap', justifyContent: 'flex-end',
|
|
}}>
|
|
{['영업', '견적', '시공', '정산'].map((label, i) => (
|
|
<span key={i} style={{
|
|
fontSize: '11px', padding: '2px 10px',
|
|
borderRadius: '10px', backgroundColor: '#F3F4F6',
|
|
color: '#6B7280', fontWeight: 500,
|
|
}}>{label}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 플로우차트 영역 */}
|
|
<div style={{
|
|
backgroundColor: '#FAFBFC',
|
|
borderRadius: '16px',
|
|
border: '1px solid #E5E7EB',
|
|
padding: '24px 16px',
|
|
marginBottom: '24px',
|
|
overflowX: 'auto',
|
|
}}>
|
|
{/* 상단 행: 영업 → 입고검수 */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
gap: '4px', minWidth: 'fit-content', margin: '0 auto',
|
|
}}>
|
|
{topRow.map((p, i) => (
|
|
<React.Fragment key={p.id}>
|
|
<ProcessNode
|
|
process={p}
|
|
isActive={selected?.id === p.id}
|
|
onClick={setSelected}
|
|
/>
|
|
{i < topRow.length - 1 && <Arrow />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
|
|
{/* 입찰 분기 표시 (3번째 노드 아래) */}
|
|
<BranchInfo />
|
|
|
|
{/* 연결 화살표 (상단 → 하단) */}
|
|
<div style={{
|
|
display: 'flex', justifyContent: 'center', padding: '4px 0',
|
|
}}>
|
|
<div style={{
|
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
}}>
|
|
<div style={{ width: '2px', height: '16px', backgroundColor: '#9CA3AF' }}></div>
|
|
<div style={{
|
|
width: 0, height: 0,
|
|
borderLeft: '6px solid transparent',
|
|
borderRight: '6px solid transparent',
|
|
borderTop: '8px solid #9CA3AF',
|
|
}}></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 하단 행: 시공 → A/S */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
gap: '4px', minWidth: 'fit-content', margin: '0 auto',
|
|
}}>
|
|
{bottomRow.map((p, i) => (
|
|
<React.Fragment key={p.id}>
|
|
<ProcessNode
|
|
process={p}
|
|
isActive={selected?.id === p.id}
|
|
onClick={setSelected}
|
|
/>
|
|
{i < bottomRow.length - 1 && <Arrow />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 상세 패널 */}
|
|
{selected && (
|
|
<DetailPanel process={selected} onClose={() => setSelected(null)} />
|
|
)}
|
|
|
|
{/* 범례 */}
|
|
<div style={{
|
|
display: 'flex', gap: '16px', flexWrap: 'wrap',
|
|
padding: '12px 16px', backgroundColor: '#F9FAFB',
|
|
borderRadius: '8px', border: '1px solid #E5E7EB',
|
|
fontSize: '12px', color: '#6B7280',
|
|
}}>
|
|
<span><strong>범례:</strong></span>
|
|
<span>클릭 → 상세 보기</span>
|
|
<span>⑂ 분기점 (입찰 결과)</span>
|
|
<span>↗ SAM 메뉴 바로가기</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
@endverbatim
|
|
</script>
|
|
@endpush
|