feat: [juil] 업무 Workflow 분기형 UI 구현
- 입찰 참여 기업 / 수의계약 기업 두 경로로 분기 - A경로: 영업 → 견적서 작성 → 입찰 참여 → 수주/계약 - B경로: 영업 → 견적서 작성 → 수주/계약 (입찰 생략) - 분기/합류 시각적 연결선으로 표현 - 수주/계약 이후 공통 프로세스로 합류
This commit is contained in:
@@ -61,15 +61,59 @@
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2, phase: 'bid', name: '입찰 참여', icon: '🏷️', dept: '영업팀',
|
||||
color: '#EC4899', bgColor: '#FDF2F8',
|
||||
description: '공공/대형 프로젝트의 경우 입찰에 참여한다. 소규모 업체나 수의계약의 경우 이 단계를 생략하고 바로 견적서 작성으로 진행한다.',
|
||||
documents: ['입찰서', '기술제안서', '사업자등록증 사본'],
|
||||
id: 2, phase: 'estimate', name: '견적서 작성', icon: '🧮', dept: '견적팀',
|
||||
color: '#8B5CF6', bgColor: '#F5F3FF',
|
||||
description: '자재/인건비/경비를 산출하여 견적서를 작성한다. 입찰 프로젝트는 입찰가 산정에, 수의계약은 고객 제출용으로 활용한다.',
|
||||
documents: ['견적서', '단가산출서', '자재목록'],
|
||||
samLink: '/juil/estimate', samMenu: '견적/입찰/공사관리',
|
||||
branch: true, optional: true,
|
||||
subSteps: [
|
||||
{
|
||||
id: 'S2-1', name: '입찰서류 준비', icon: '📂',
|
||||
id: 'S2-1', name: '물량 산출', icon: '📏',
|
||||
description: '도면과 실측 데이터를 기반으로 자재별 물량(수량)을 산출한다.',
|
||||
input: ['도면', '실측 데이터', '현장조사서'],
|
||||
output: ['물량산출서 (자재별 수량)'],
|
||||
responsible: '견적 담당자',
|
||||
tips: ['로스율(5~10%) 반드시 반영', '현장 여건에 따른 추가 물량 고려'],
|
||||
duration: '2~3일',
|
||||
},
|
||||
{
|
||||
id: 'S2-2', name: '단가 산정', icon: '💵',
|
||||
description: '자재 단가, 인건비, 장비비, 경비 등 각 항목의 단가를 산정한다.',
|
||||
input: ['물량산출서', '최근 자재 시세', '노무단가표'],
|
||||
output: ['단가산출서'],
|
||||
responsible: '견적 담당자',
|
||||
tips: ['자재 시세는 최근 3개월 평균 적용', '노무단가는 대한건설협회 기준 참조'],
|
||||
duration: '1~2일',
|
||||
},
|
||||
{
|
||||
id: 'S2-3', name: '견적가 산출', icon: '🧮',
|
||||
description: '물량 x 단가를 계산하고, 이윤율/관리비를 적용하여 최종 견적가를 산출한다.',
|
||||
input: ['물량산출서', '단가산출서', '이윤율 기준'],
|
||||
output: ['원가계산서', '내부 견적서'],
|
||||
responsible: '견적팀장',
|
||||
tips: ['이윤율은 프로젝트 규모별 차등 적용', '원가 대비 견적가 검증 필수'],
|
||||
duration: '1일',
|
||||
},
|
||||
{
|
||||
id: 'S2-4', name: '견적서 작성/검토', icon: '📋',
|
||||
description: '고객 제출용 견적서를 작성하고, 팀장 검토 후 최종 확정한다.',
|
||||
input: ['원가계산서', '고객 요구 양식'],
|
||||
output: ['공식 견적서 (PDF)'],
|
||||
responsible: '견적팀장 → 대표이사 승인',
|
||||
tips: ['고객사 양식이 별도로 있는지 확인', '견적 유효기간 명시 (보통 30일)'],
|
||||
duration: '1일',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3, phase: 'bid', name: '입찰 참여', icon: '🏷️', dept: '영업팀',
|
||||
color: '#EC4899', bgColor: '#FDF2F8',
|
||||
description: '공공/대형 프로젝트의 경우 견적서를 기반으로 입찰에 참여한다. 소규모 업체나 수의계약은 이 단계를 생략한다.',
|
||||
documents: ['입찰서', '기술제안서', '사업자등록증 사본'],
|
||||
samLink: '/juil/estimate', samMenu: '견적/입찰/공사관리',
|
||||
subSteps: [
|
||||
{
|
||||
id: 'S3-1', name: '입찰서류 준비', icon: '📂',
|
||||
description: '입찰 참여에 필요한 서류(사업자등록증, 실적증명, 재무제표 등)를 준비한다.',
|
||||
input: ['입찰 공고문 (필요서류 목록)', '회사 기본서류'],
|
||||
output: ['입찰서류 패키지'],
|
||||
@@ -106,51 +150,6 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3, phase: 'estimate', name: '견적서 작성', icon: '🧮', dept: '견적팀',
|
||||
color: '#8B5CF6', bgColor: '#F5F3FF',
|
||||
description: '자재/인건비/경비를 산출하여 견적서를 작성한다. 입찰 프로젝트는 입찰가 산정에, 수의계약은 고객 제출용으로 활용한다.',
|
||||
documents: ['견적서', '단가산출서', '자재목록'],
|
||||
samLink: '/juil/estimate', samMenu: '견적/입찰/공사관리',
|
||||
subSteps: [
|
||||
{
|
||||
id: 'S3-1', name: '물량 산출', icon: '📏',
|
||||
description: '도면과 실측 데이터를 기반으로 자재별 물량(수량)을 산출한다.',
|
||||
input: ['도면', '실측 데이터', '현장조사서'],
|
||||
output: ['물량산출서 (자재별 수량)'],
|
||||
responsible: '견적 담당자',
|
||||
tips: ['로스율(5~10%) 반드시 반영', '현장 여건에 따른 추가 물량 고려'],
|
||||
duration: '2~3일',
|
||||
},
|
||||
{
|
||||
id: 'S3-2', name: '단가 산정', icon: '💵',
|
||||
description: '자재 단가, 인건비, 장비비, 경비 등 각 항목의 단가를 산정한다.',
|
||||
input: ['물량산출서', '최근 자재 시세', '노무단가표'],
|
||||
output: ['단가산출서'],
|
||||
responsible: '견적 담당자',
|
||||
tips: ['자재 시세는 최근 3개월 평균 적용', '노무단가는 대한건설협회 기준 참조'],
|
||||
duration: '1~2일',
|
||||
},
|
||||
{
|
||||
id: 'S3-3', name: '견적가 산출', icon: '🧮',
|
||||
description: '물량 x 단가를 계산하고, 이윤율/관리비를 적용하여 최종 견적가를 산출한다.',
|
||||
input: ['물량산출서', '단가산출서', '이윤율 기준'],
|
||||
output: ['원가계산서', '내부 견적서'],
|
||||
responsible: '견적팀장',
|
||||
tips: ['이윤율은 프로젝트 규모별 차등 적용', '원가 대비 견적가 검증 필수'],
|
||||
duration: '1일',
|
||||
},
|
||||
{
|
||||
id: 'S3-4', name: '견적서 작성/검토', icon: '📋',
|
||||
description: '고객 제출용 견적서를 작성하고, 팀장 검토 후 최종 확정한다.',
|
||||
input: ['원가계산서', '고객 요구 양식'],
|
||||
output: ['공식 견적서 (PDF)'],
|
||||
responsible: '견적팀장 → 대표이사 승인',
|
||||
tips: ['고객사 양식이 별도로 있는지 확인', '견적 유효기간 명시 (보통 30일)'],
|
||||
duration: '1일',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4, phase: 'contract', name: '수주/계약', icon: '📝', dept: '영업팀',
|
||||
color: '#10B981', bgColor: '#ECFDF5',
|
||||
@@ -583,18 +582,13 @@ function ProcessNode({ process, isActive, onClick }) {
|
||||
fontSize: '11px', color: '#FFF', backgroundColor: process.color,
|
||||
borderRadius: '10px', padding: '1px 8px', display: 'inline-block',
|
||||
}}>{process.dept}</div>
|
||||
{process.optional && (
|
||||
{process.phase === 'bid' && (
|
||||
<div style={{
|
||||
position: 'absolute', top: '-8px', right: '-8px',
|
||||
width: '20px', height: '20px', borderRadius: '50%',
|
||||
backgroundColor: '#FEF3C7', border: '2px solid #F59E0B',
|
||||
backgroundColor: '#FCE7F3', border: '2px solid #EC4899',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px',
|
||||
}}>⑂</div>
|
||||
)}
|
||||
{process.optional && (
|
||||
<div style={{
|
||||
fontSize: '9px', color: '#F59E0B', fontWeight: 600, marginTop: '4px',
|
||||
}}>생략 가능</div>
|
||||
}}>A</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -875,26 +869,16 @@ function DetailModal({ process, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
// --- 분기 표시 ---
|
||||
function BranchInfo() {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center', margin: '8px 0', fontSize: '11px' }}>
|
||||
<span style={{ padding: '2px 10px', borderRadius: '10px', backgroundColor: '#FEF3C7', color: '#92400E', fontWeight: 600 }}>
|
||||
대형/공공 → 입찰 참여 → 견적서
|
||||
</span>
|
||||
<span style={{ padding: '2px 10px', borderRadius: '10px', backgroundColor: '#DBEAFE', color: '#1E40AF', fontWeight: 600 }}>
|
||||
소규모/수의계약 → 입찰 생략 → 견적서
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- 메인 앱 ---
|
||||
function App() {
|
||||
const [modalProcess, setModalProcess] = useState(null);
|
||||
|
||||
const topRow = processes.slice(0, 6);
|
||||
const bottomRow = processes.slice(6);
|
||||
// 프로세스 참조
|
||||
const p = {};
|
||||
processes.forEach(proc => { p[proc.phase] = proc; });
|
||||
|
||||
// 공통 후반 프로세스 (수주/계약 이후)
|
||||
const postContract = [p.order, p.receive, p.construct, p.inspect, p.billing, p.payment, p.as].filter(Boolean);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
@@ -908,57 +892,128 @@ function App() {
|
||||
주일기업 업무처리과정 플로우차트 — 각 단계를 클릭하면 상세 업무 흐름을 확인할 수 있습니다
|
||||
</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',
|
||||
padding: '32px 24px', 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={false} onClick={setModalProcess} />
|
||||
{i < topRow.length - 1 && <Arrow />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{/* === 1단계: 영업 (공통 시작점) === */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '8px' }}>
|
||||
<ProcessNode process={p.sales} isActive={false} onClick={setModalProcess} />
|
||||
</div>
|
||||
|
||||
<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 style={{ display: 'flex', justifyContent: 'center', position: 'relative', height: '60px' }}>
|
||||
{/* 중앙 세로선 */}
|
||||
<div style={{ position: 'absolute', top: 0, width: '2px', height: '20px', backgroundColor: '#9CA3AF' }}></div>
|
||||
{/* 가로 분기선 */}
|
||||
<div style={{ position: 'absolute', top: '20px', width: '400px', height: '2px', backgroundColor: '#9CA3AF', left: '50%', transform: 'translateX(-50%)' }}></div>
|
||||
{/* 왼쪽 세로선 + 화살표 */}
|
||||
<div style={{ position: 'absolute', top: '20px', left: '50%', transform: 'translateX(calc(-200px))', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ width: '2px', height: '20px', backgroundColor: '#9CA3AF' }}></div>
|
||||
<div style={{ width: 0, height: 0, borderLeft: '6px solid transparent', borderRight: '6px solid transparent', borderTop: '8px solid #9CA3AF' }}></div>
|
||||
</div>
|
||||
{/* 오른쪽 세로선 + 화살표 */}
|
||||
<div style={{ position: 'absolute', top: '20px', left: '50%', transform: 'translateX(calc(200px - 2px))', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ width: '2px', height: '20px', backgroundColor: '#9CA3AF' }}></div>
|
||||
<div style={{ width: 0, height: 0, borderLeft: '6px solid transparent', borderRight: '6px solid transparent', borderTop: '8px solid #9CA3AF' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 행 */}
|
||||
{/* === 2단계: 두 경로 분기 === */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '24px', marginBottom: '8px' }}>
|
||||
{/* A 경로: 입찰 기업 */}
|
||||
<div style={{
|
||||
flex: '0 0 auto', padding: '16px', borderRadius: '16px',
|
||||
border: '2px dashed #EC4899', backgroundColor: '#FDF2F820',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px',
|
||||
minWidth: '360px',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '12px', fontWeight: 700, color: '#BE185D',
|
||||
padding: '3px 14px', borderRadius: '12px', backgroundColor: '#FCE7F3',
|
||||
marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px',
|
||||
}}>
|
||||
<span style={{ fontSize: '14px' }}>🏢</span>
|
||||
A. 입찰 참여 기업 (대형/공공 프로젝트)
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<ProcessNode process={p.estimate} isActive={false} onClick={setModalProcess} />
|
||||
<Arrow />
|
||||
<ProcessNode process={p.bid} isActive={false} onClick={setModalProcess} />
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: '#9CA3AF', marginTop: '4px' }}>
|
||||
견적서 작성 → 입찰 참여 → 낙찰 시 수주
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* B 경로: 수의계약 */}
|
||||
<div style={{
|
||||
flex: '0 0 auto', padding: '16px', borderRadius: '16px',
|
||||
border: '2px dashed #3B82F6', backgroundColor: '#EFF6FF20',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px',
|
||||
minWidth: '200px',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '12px', fontWeight: 700, color: '#1D4ED8',
|
||||
padding: '3px 14px', borderRadius: '12px', backgroundColor: '#DBEAFE',
|
||||
marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px',
|
||||
}}>
|
||||
<span style={{ fontSize: '14px' }}>🏠</span>
|
||||
B. 수의계약 (소규모/직접 의뢰)
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<ProcessNode process={p.estimate} isActive={false} onClick={setModalProcess} />
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: '#9CA3AF', marginTop: '4px' }}>
|
||||
견적서 작성 → 고객 승인 시 수주
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 합류 화살표 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', position: 'relative', height: '60px' }}>
|
||||
{/* 왼쪽 세로선 */}
|
||||
<div style={{ position: 'absolute', top: 0, left: '50%', transform: 'translateX(calc(-200px))', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ width: '2px', height: '20px', backgroundColor: '#9CA3AF' }}></div>
|
||||
</div>
|
||||
{/* 오른쪽 세로선 */}
|
||||
<div style={{ position: 'absolute', top: 0, left: '50%', transform: 'translateX(calc(200px - 2px))', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ width: '2px', height: '20px', backgroundColor: '#9CA3AF' }}></div>
|
||||
</div>
|
||||
{/* 가로 합류선 */}
|
||||
<div style={{ position: 'absolute', top: '20px', width: '400px', height: '2px', backgroundColor: '#9CA3AF', left: '50%', transform: 'translateX(-50%)' }}></div>
|
||||
{/* 중앙 세로선 + 화살표 */}
|
||||
<div style={{ position: 'absolute', top: '20px', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ width: '2px', height: '20px', backgroundColor: '#9CA3AF' }}></div>
|
||||
<div style={{ width: 0, height: 0, borderLeft: '6px solid transparent', borderRight: '6px solid transparent', borderTop: '8px solid #9CA3AF' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === 3단계: 수주/계약 (합류 지점) === */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '8px' }}>
|
||||
<ProcessNode process={p.contract} isActive={false} onClick={setModalProcess} />
|
||||
</div>
|
||||
|
||||
{/* 아래 화살표 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '4px 0' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ width: '2px', height: '12px', backgroundColor: '#9CA3AF' }}></div>
|
||||
<div style={{ width: 0, height: 0, borderLeft: '6px solid transparent', borderRight: '6px solid transparent', borderTop: '8px solid #9CA3AF' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === 4단계: 공통 후반 프로세스 === */}
|
||||
<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={false} onClick={setModalProcess} />
|
||||
{i < bottomRow.length - 1 && <Arrow />}
|
||||
{postContract.map((proc, i) => (
|
||||
<React.Fragment key={proc.id}>
|
||||
<ProcessNode process={proc} isActive={false} onClick={setModalProcess} />
|
||||
{i < postContract.length - 1 && <Arrow />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
@@ -969,11 +1024,18 @@ function App() {
|
||||
display: 'flex', gap: '16px', flexWrap: 'wrap',
|
||||
padding: '12px 16px', backgroundColor: '#F9FAFB',
|
||||
borderRadius: '8px', border: '1px solid #E5E7EB',
|
||||
fontSize: '12px', color: '#6B7280',
|
||||
fontSize: '12px', color: '#6B7280', alignItems: 'center',
|
||||
}}>
|
||||
<span><strong>범례:</strong></span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<span style={{ display: 'inline-block', width: '16px', height: '3px', borderTop: '2px dashed #EC4899' }}></span>
|
||||
입찰 참여 경로
|
||||
</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<span style={{ display: 'inline-block', width: '16px', height: '3px', borderTop: '2px dashed #3B82F6' }}></span>
|
||||
수의계약 경로
|
||||
</span>
|
||||
<span>클릭 → 상세 업무 모달</span>
|
||||
<span>⑂ 생략 가능 단계 (입찰)</span>
|
||||
<span>↗ SAM 메뉴 바로가기</span>
|
||||
<span>ESC 키로 모달 닫기</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user