- 각 워크플로우 단계 클릭 시 상세 업무 모달 표시 - 서브플로우 4단계 (단계별 아코디언 펼침) - Input/Output, 담당자, 소요시간, TIP 정보 포함 - 미니 서브플로우 다이어그램으로 단계 간 이동 가능
988 lines
51 KiB
PHP
988 lines
51 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: '건설사/시행사로부터 프로젝트 정보 수집 및 수주 활동',
|
|
documents: ['입찰공고문', '현장조사서', '고객 요구사항서'],
|
|
samLink: null, samMenu: null,
|
|
subSteps: [
|
|
{
|
|
id: 'S1-1', name: '정보 수집', icon: '🔎',
|
|
description: '나라장터, 건설사 공고, 인맥 등을 통해 프로젝트 정보를 수집한다.',
|
|
input: ['나라장터 공고', '건설사 입찰 안내', '업계 네트워크 정보'],
|
|
output: ['프로젝트 리스트 (엑셀)'],
|
|
responsible: '영업 담당자',
|
|
tips: ['주 2회 이상 나라장터/건설사 사이트 모니터링', '기존 거래처 정기 연락 유지'],
|
|
duration: '수시',
|
|
},
|
|
{
|
|
id: 'S1-2', name: '현장 실측', icon: '📐',
|
|
description: '프로젝트 대상 현장을 방문하여 실측하고 요구사항을 파악한다.',
|
|
input: ['도면 (건설사 제공)', '현장 위치 정보'],
|
|
output: ['현장조사서', '실측 데이터', '현장 사진'],
|
|
responsible: '영업 담당자 + 기술팀',
|
|
tips: ['도면과 현장 차이 반드시 확인', '사진은 전경/상세 모두 촬영'],
|
|
duration: '1~2일',
|
|
},
|
|
{
|
|
id: 'S1-3', name: '고객 미팅', icon: '🤝',
|
|
description: '건설사 담당자와 미팅하여 요구사항, 예산, 일정 등을 협의한다.',
|
|
input: ['현장조사서', '회사 소개서', '시공 포트폴리오'],
|
|
output: ['미팅 기록', '고객 요구사항서'],
|
|
responsible: '영업 담당자',
|
|
tips: ['고객 예산 범위를 사전에 파악', '경쟁사 동향 정보 수집'],
|
|
duration: '1~3시간',
|
|
},
|
|
{
|
|
id: 'S1-4', name: '수주 가능성 평가', icon: '📊',
|
|
description: '수집된 정보를 바탕으로 수주 가능성과 수익성을 평가하여 입찰 참여 여부를 결정한다.',
|
|
input: ['고객 요구사항서', '예상 공사비', '경쟁 현황'],
|
|
output: ['수주 검토서 (Go/No-Go 결정)'],
|
|
responsible: '영업팀장',
|
|
tips: ['수익률 10% 미만 프로젝트는 팀장 승인 필요', '리스크 요소 반드시 기재'],
|
|
duration: '1일',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2, phase: 'estimate', name: '견적서 작성', icon: '🧮', dept: '견적팀',
|
|
color: '#8B5CF6', bgColor: '#F5F3FF',
|
|
description: '자재/인건비/경비를 산출하여 견적서 작성',
|
|
documents: ['견적서', '단가산출서', '자재목록'],
|
|
samLink: '/juil/estimate', samMenu: '견적/입찰/공사관리',
|
|
subSteps: [
|
|
{
|
|
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: '견적/입찰/공사관리',
|
|
branch: true,
|
|
subSteps: [
|
|
{
|
|
id: 'S3-1', name: '입찰서류 준비', icon: '📂',
|
|
description: '입찰 참여에 필요한 서류(사업자등록증, 실적증명, 재무제표 등)를 준비한다.',
|
|
input: ['입찰 공고문 (필요서류 목록)', '회사 기본서류'],
|
|
output: ['입찰서류 패키지'],
|
|
responsible: '영업 담당자 + 경영지원팀',
|
|
tips: ['서류 유효기간 확인 (인감증명 등 3개월)', '전자입찰 시 공인인증서 사전 확인'],
|
|
duration: '2~3일',
|
|
},
|
|
{
|
|
id: 'S3-2', name: '기술제안서 작성', icon: '📝',
|
|
description: '시공 방법, 품질관리 계획, 안전관리 계획 등을 포함한 기술제안서를 작성한다.',
|
|
input: ['현장조사서', '시공 실적', '품질/안전 매뉴얼'],
|
|
output: ['기술제안서'],
|
|
responsible: '기술팀 + 영업팀',
|
|
tips: ['유사 프로젝트 실적 강조', '차별화 포인트 명확히 기술'],
|
|
duration: '3~5일',
|
|
},
|
|
{
|
|
id: 'S3-3', name: '입찰가 결정', icon: '🎯',
|
|
description: '견적가를 기반으로 경쟁 상황을 고려한 최종 입찰가를 결정한다.',
|
|
input: ['내부 견적서', '경쟁사 동향', '예정가 분석'],
|
|
output: ['최종 입찰가 결정서'],
|
|
responsible: '대표이사 승인',
|
|
tips: ['예정가 대비 적정 투찰률 분석', '최저가 낙찰 vs 종합심사 방식 확인'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S3-4', name: '입찰서 제출', icon: '📮',
|
|
description: '준비된 서류와 입찰가를 포함하여 입찰서를 제출한다.',
|
|
input: ['입찰서류 패키지', '최종 입찰가', '입찰보증금'],
|
|
output: ['입찰 접수증', '입찰 결과 대기'],
|
|
responsible: '영업 담당자',
|
|
tips: ['마감시간 최소 2시간 전 제출', '전자입찰 시스템 사전 테스트'],
|
|
duration: '당일',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 4, phase: 'contract', name: '낙찰/계약', icon: '📝', dept: '영업팀',
|
|
color: '#10B981', bgColor: '#ECFDF5',
|
|
description: '낙찰 후 공사 계약 체결',
|
|
documents: ['공사계약서', '착공계', '공정표'],
|
|
samLink: '/juil/project', samMenu: '프로젝트관리/기성청구',
|
|
subSteps: [
|
|
{
|
|
id: 'S4-1', name: '낙찰 통보 확인', icon: '🏆',
|
|
description: '입찰 결과를 확인하고, 낙찰 통보서를 수령한다.',
|
|
input: ['입찰 결과 공고', '낙찰 통보서'],
|
|
output: ['낙찰 확인 (사내 공유)'],
|
|
responsible: '영업 담당자',
|
|
tips: ['낙찰 후 계약 기한 확인 (보통 7~10일)', '유찰 시 원인 분석하여 다음 입찰에 반영'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S4-2', name: '계약서 검토/체결', icon: '🖊️',
|
|
description: '계약 조건(금액, 공기, 하자보증 등)을 검토하고 계약서에 서명한다.',
|
|
input: ['계약서 초안 (발주처 제공)', '낙찰 내역서'],
|
|
output: ['공사계약서 (날인본)', '계약보증금 납부 영수증'],
|
|
responsible: '대표이사 + 경영지원팀',
|
|
tips: ['지체상금 조항 반드시 확인', '하자보증기간/비율 확인', '대금지급 조건 확인'],
|
|
duration: '3~5일',
|
|
},
|
|
{
|
|
id: 'S4-3', name: '프로젝트 등록', icon: '💻',
|
|
description: 'SAM 시스템에 프로젝트를 등록하고 담당자를 배정한다.',
|
|
input: ['공사계약서', '공정표'],
|
|
output: ['프로젝트 등록 완료 (SAM)', '담당자 배정표'],
|
|
responsible: '영업팀장 → PM',
|
|
tips: ['계약 금액/공기 정확히 입력', '하도급 계획이 있으면 동시 등록'],
|
|
duration: '1일',
|
|
},
|
|
{
|
|
id: 'S4-4', name: '착공 준비', icon: '🚧',
|
|
description: '착공계를 제출하고, 현장 착공에 필요한 사전 준비를 완료한다.',
|
|
input: ['공사계약서', '현장 배치도'],
|
|
output: ['착공계', '현장 투입 계획서', '안전관리 계획서'],
|
|
responsible: 'PM + 안전관리자',
|
|
tips: ['착공 전 현장 안전교육 필수', '인근 주민 민원 사전 확인'],
|
|
duration: '2~3일',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 5, phase: 'order', name: '자재 발주', icon: '📦', dept: '구매팀',
|
|
color: '#F59E0B', bgColor: '#FFFBEB',
|
|
description: '시공에 필요한 자재를 발주',
|
|
documents: ['발주서', 'BOM', '납품요청서'],
|
|
samLink: null, samMenu: null,
|
|
subSteps: [
|
|
{
|
|
id: 'S5-1', name: 'BOM 작성', icon: '📋',
|
|
description: '프로젝트에 필요한 자재 명세서(BOM)를 작성한다.',
|
|
input: ['도면', '물량산출서', '시방서'],
|
|
output: ['BOM (자재명세서)'],
|
|
responsible: '기술팀 + 구매 담당자',
|
|
tips: ['규격/색상/사양 정확히 기재', '예비 자재(로스분) 포함'],
|
|
duration: '2~3일',
|
|
},
|
|
{
|
|
id: 'S5-2', name: '공급사 선정/견적 비교', icon: '🔄',
|
|
description: '2개 이상 공급사로부터 견적을 받아 비교하고 공급사를 선정한다.',
|
|
input: ['BOM', '기존 거래처 목록', '시세 정보'],
|
|
output: ['공급사 비교표', '공급사 선정 품의서'],
|
|
responsible: '구매 담당자 → 구매팀장 승인',
|
|
tips: ['기존 거래처 우선 검토', '납기 준수율 이력 확인'],
|
|
duration: '2~3일',
|
|
},
|
|
{
|
|
id: 'S5-3', name: '발주서 발행', icon: '📤',
|
|
description: '선정된 공급사에 발주서를 발행하고, 납기일을 확정한다.',
|
|
input: ['공급사 선정 결과', 'BOM'],
|
|
output: ['발주서', '발주 확인서 (공급사)'],
|
|
responsible: '구매 담당자',
|
|
tips: ['시공일 기준 최소 1주 전 입고되도록 납기 설정', '분할 납품 시 일정 명확히'],
|
|
duration: '1일',
|
|
},
|
|
{
|
|
id: 'S5-4', name: '납기 관리', icon: '📅',
|
|
description: '발주 후 공급사의 생산/배송 진행 상황을 추적한다.',
|
|
input: ['발주서', '납기 일정'],
|
|
output: ['납기 진행현황표'],
|
|
responsible: '구매 담당자',
|
|
tips: ['납기 3일 전 공급사에 확인 연락', '지연 시 즉시 PM에게 보고'],
|
|
duration: '발주~입고 기간 중 수시',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 6, phase: 'receive', name: '자재 입고/검수', icon: '🔍', dept: '자재팀',
|
|
color: '#06B6D4', bgColor: '#ECFEFF',
|
|
description: '입고된 자재의 수량/품질 검수',
|
|
documents: ['입고검수서', '거래명세서', '반품요청서'],
|
|
samLink: null, samMenu: null,
|
|
subSteps: [
|
|
{
|
|
id: 'S6-1', name: '입고 접수', icon: '🚛',
|
|
description: '공급사로부터 자재가 도착하면 거래명세서와 대조하여 접수한다.',
|
|
input: ['발주서', '거래명세서 (공급사)', '납품 자재'],
|
|
output: ['입고 접수 기록'],
|
|
responsible: '자재 담당자',
|
|
tips: ['거래명세서와 발주서 수량 대조 필수', '포장 상태 외관 확인'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S6-2', name: '수량 검수', icon: '🔢',
|
|
description: '입고된 자재의 수량이 발주 수량과 일치하는지 전수 검사한다.',
|
|
input: ['발주서', '입고 자재'],
|
|
output: ['수량 검수 체크리스트'],
|
|
responsible: '자재 담당자',
|
|
tips: ['부족분 즉시 공급사에 연락', '과다 입고 시에도 기록 후 보고'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S6-3', name: '품질 검사', icon: '🔬',
|
|
description: '자재의 규격, 색상, 기능 등 품질 기준에 부합하는지 검사한다.',
|
|
input: ['자재 시방서 (규격 기준)', '샘플'],
|
|
output: ['품질검사 성적서'],
|
|
responsible: '품질 담당자',
|
|
tips: ['블라인드: 색상/슬랫 간격 확인', '셔터: 개폐 동작 테스트 필수'],
|
|
duration: '1~2일',
|
|
},
|
|
{
|
|
id: 'S6-4', name: '입고 확정/불량 처리', icon: '✅',
|
|
description: '검수 합격 자재는 입고 확정, 불량 자재는 반품 처리한다.',
|
|
input: ['수량 검수 결과', '품질검사 결과'],
|
|
output: ['입고검수서 (확정)', '반품요청서 (불량 시)'],
|
|
responsible: '자재팀장',
|
|
tips: ['불량률 3% 이상 시 공급사 품질 회의 요청', '입고 확정 후 재고에 즉시 반영'],
|
|
duration: '당일',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 7, phase: 'construct', name: '현장 시공', icon: '🔧', dept: '시공팀',
|
|
color: '#EF4444', bgColor: '#FEF2F2',
|
|
description: '현장에서 블라인드/스크린/셔터 설치 시공',
|
|
documents: ['시공계획서', '작업일보', '시공사진'],
|
|
samLink: '/juil/construction-photos', samMenu: '공사현장 사진대지',
|
|
subSteps: [
|
|
{
|
|
id: 'S7-1', name: '시공 계획 수립', icon: '📅',
|
|
description: '건설사 공정표에 맞춰 시공 일정, 인력, 장비 계획을 수립한다.',
|
|
input: ['건설사 공정표', '자재 입고 일정', '인력 가용 현황'],
|
|
output: ['시공계획서', '인력/장비 투입 계획'],
|
|
responsible: 'PM + 현장소장',
|
|
tips: ['건설사 타 공종과 간섭 구간 사전 확인', '우천 시 대체 일정 수립'],
|
|
duration: '2~3일',
|
|
},
|
|
{
|
|
id: 'S7-2', name: '자재 반출/현장 배송', icon: '🚚',
|
|
description: '창고에서 필요 자재를 반출하여 현장에 배송한다.',
|
|
input: ['시공계획서', '자재 재고 현황'],
|
|
output: ['자재 반출 전표', '현장 입고 확인서'],
|
|
responsible: '자재 담당자 + 운송팀',
|
|
tips: ['현장 반입 가능 시간 확인 (아파트: 주로 오전)', '양중 장비 필요 여부 사전 확인'],
|
|
duration: '시공일 1~2일 전',
|
|
},
|
|
{
|
|
id: 'S7-3', name: '설치 시공', icon: '🔧',
|
|
description: '현장에서 블라인드/스크린/셔터를 설치한다.',
|
|
input: ['도면', '시공계획서', '자재'],
|
|
output: ['작업일보', '시공 진행률'],
|
|
responsible: '현장소장 + 시공기사',
|
|
tips: ['안전장구 착용 필수 (안전모, 안전화, 안전벨트)', '일일 작업일보 당일 작성'],
|
|
duration: '프로젝트 규모에 따라 상이',
|
|
},
|
|
{
|
|
id: 'S7-4', name: '시공 사진 촬영/기록', icon: '📸',
|
|
description: '시공 전/중/후 사진을 촬영하고, SAM 시스템에 등록한다.',
|
|
input: ['시공 현장'],
|
|
output: ['시공사진 (SAM 등록)', '시공 진행 보고서'],
|
|
responsible: '현장소장',
|
|
tips: ['공정별 Before/After 반드시 촬영', 'SAM 공사현장 사진대지에 당일 업로드'],
|
|
duration: '시공 기간 중 매일',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 8, phase: 'inspect', name: '시공 검수', icon: '✅', dept: '현장팀',
|
|
color: '#14B8A6', bgColor: '#F0FDFA',
|
|
description: '시공 완료 후 품질 검수 및 하자 보수',
|
|
documents: ['검수확인서', '하자보수보고서', '준공사진'],
|
|
samLink: '/juil/construction-photos', samMenu: '공사현장 사진대지',
|
|
subSteps: [
|
|
{
|
|
id: 'S8-1', name: '자체 검수', icon: '🔎',
|
|
description: '건설사 합동검수 전, 시공팀 자체적으로 시공 품질을 점검한다.',
|
|
input: ['시공 도면', '시방서', '시공 체크리스트'],
|
|
output: ['자체 검수 보고서', '보수 필요 항목 리스트'],
|
|
responsible: '현장소장',
|
|
tips: ['체크리스트 기반 전수 검사', '사소한 하자도 미리 보수'],
|
|
duration: '1~2일',
|
|
},
|
|
{
|
|
id: 'S8-2', name: '하자 보수', icon: '🔨',
|
|
description: '자체 검수에서 발견된 하자 사항을 보수한다.',
|
|
input: ['보수 필요 항목 리스트', '보수 자재'],
|
|
output: ['하자보수 완료 보고서'],
|
|
responsible: '시공기사',
|
|
tips: ['보수 전/후 사진 촬영 필수', '동일 하자 재발 방지 대책 수립'],
|
|
duration: '1~3일',
|
|
},
|
|
{
|
|
id: 'S8-3', name: '합동 검수', icon: '👥',
|
|
description: '건설사(감리) 담당자와 합동으로 시공 품질을 검수한다.',
|
|
input: ['시공 완료 현장', '검수 체크리스트', '시공사진'],
|
|
output: ['합동검수 확인서', '추가 보수 요청 (있을 경우)'],
|
|
responsible: '현장소장 + 건설사 감리',
|
|
tips: ['검수 일정 최소 3일 전 건설사와 협의', '추가 보수 요청 시 기한/범위 명확히 합의'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S8-4', name: '준공 확인', icon: '🏁',
|
|
description: '모든 검수를 완료하고 준공 확인서를 수령한다.',
|
|
input: ['합동검수 확인서', '준공사진'],
|
|
output: ['준공확인서', '준공도서'],
|
|
responsible: 'PM',
|
|
tips: ['준공확인서 수령 후 기성청구 가능', '준공도서(도면, 매뉴얼) 제출 확인'],
|
|
duration: '1~2일',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 9, phase: 'billing', name: '기성 청구', icon: '💰', dept: '경리팀',
|
|
color: '#7C3AED', bgColor: '#F5F3FF',
|
|
description: '시공 진행률에 따른 기성금 청구',
|
|
documents: ['기성내역서', '세금계산서', '기성청구서'],
|
|
samLink: '/juil/project', samMenu: '프로젝트관리/기성청구',
|
|
subSteps: [
|
|
{
|
|
id: 'S9-1', name: '기성 내역서 작성', icon: '📊',
|
|
description: '시공 진행률에 따라 청구할 기성금 내역을 작성한다.',
|
|
input: ['계약서 (계약금액)', '시공 진행률', '기 청구 내역'],
|
|
output: ['기성내역서'],
|
|
responsible: '경리 담당자 + PM',
|
|
tips: ['계약 조건의 기성 산정 기준 확인 (월별/단계별)', '기 청구분 공제 정확히'],
|
|
duration: '1~2일',
|
|
},
|
|
{
|
|
id: 'S9-2', name: '세금계산서 발행', icon: '🧾',
|
|
description: '기성금에 대한 세금계산서를 발행한다.',
|
|
input: ['기성내역서', '사업자등록 정보'],
|
|
output: ['전자세금계산서'],
|
|
responsible: '경리 담당자',
|
|
tips: ['발행일과 기성 승인일 맞추기', '수정 세금계산서 발행 시 사유 기재'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S9-3', name: '청구서 제출', icon: '📮',
|
|
description: '기성내역서, 세금계산서 등 청구 서류를 건설사에 제출한다.',
|
|
input: ['기성내역서', '세금계산서', '시공사진', '검수확인서'],
|
|
output: ['기성청구서 (접수 확인)'],
|
|
responsible: '경리 담당자',
|
|
tips: ['건설사별 청구 마감일 확인', '첨부서류 누락 없이 제출'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S9-4', name: '입금 추적', icon: '🔄',
|
|
description: '청구 후 입금 일정을 추적하고, 지연 시 독촉한다.',
|
|
input: ['기성청구서 제출 내역', '계약상 지급 기한'],
|
|
output: ['입금 예정일 관리표'],
|
|
responsible: '경리 담당자',
|
|
tips: ['지급 기한 초과 시 3일 내 독촉', '장기 미수금은 월간 보고에 포함'],
|
|
duration: '청구 후 지속',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 10, phase: 'payment', name: '입금 확인', icon: '🏦', dept: '경리팀',
|
|
color: '#059669', bgColor: '#ECFDF5',
|
|
description: '기성금 입금 확인 및 매출 처리',
|
|
documents: ['입금확인서', '매출대장'],
|
|
samLink: null, samMenu: null,
|
|
subSteps: [
|
|
{
|
|
id: 'S10-1', name: '입금 확인', icon: '💳',
|
|
description: '은행 계좌에 기성금이 입금되었는지 확인한다.',
|
|
input: ['은행 거래내역', '기성청구 내역'],
|
|
output: ['입금 확인 기록'],
|
|
responsible: '경리 담당자',
|
|
tips: ['청구 금액과 입금액 일치 여부 확인', '부분 입금 시 잔액 별도 관리'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S10-2', name: '매출 전표 처리', icon: '📑',
|
|
description: '입금 확인 후 회계 시스템에 매출 전표를 기록한다.',
|
|
input: ['입금 확인 기록', '세금계산서'],
|
|
output: ['매출 전표', '매출대장 업데이트'],
|
|
responsible: '경리 담당자',
|
|
tips: ['매출 인식 시점 확인 (발생주의/현금주의)', '부가세 신고 일정과 연계'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S10-3', name: '미수금 관리', icon: '📊',
|
|
description: '미입금 건을 관리하고, 장기 미수금은 별도 조치한다.',
|
|
input: ['기성청구 내역', '입금 내역'],
|
|
output: ['미수금 현황표', '독촉장 (필요 시)'],
|
|
responsible: '경리팀장',
|
|
tips: ['30일 이상 미수금은 주간 보고', '60일 이상 시 법적 조치 검토'],
|
|
duration: '월간',
|
|
},
|
|
{
|
|
id: 'S10-4', name: '프로젝트 수익 정산', icon: '📈',
|
|
description: '프로젝트 완료 시 총 매출/비용을 정산하여 최종 수익을 산출한다.',
|
|
input: ['총 매출 내역', '총 비용 내역 (자재+인건비+경비)'],
|
|
output: ['프로젝트 수익 정산서'],
|
|
responsible: '경리팀장 → 대표이사 보고',
|
|
tips: ['예정 이익률 대비 실제 이익률 비교 분석', '손실 프로젝트 원인 분석 필수'],
|
|
duration: '프로젝트 완료 후 1주일',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 11, phase: 'as', name: 'A/S 관리', icon: '🛠️', dept: '시공팀',
|
|
color: '#6B7280', bgColor: '#F9FAFB',
|
|
description: '하자보수기간 내 A/S 대응',
|
|
documents: ['A/S 접수대장', '하자보수 보고서'],
|
|
samLink: null, samMenu: null,
|
|
subSteps: [
|
|
{
|
|
id: 'S11-1', name: 'A/S 접수', icon: '📞',
|
|
description: '건설사/입주자로부터 A/S 요청을 접수한다.',
|
|
input: ['A/S 요청 (전화/메일/문서)'],
|
|
output: ['A/S 접수 기록'],
|
|
responsible: '영업 담당자 → 시공팀',
|
|
tips: ['접수 즉시 SAM에 등록', '긴급 A/S(누수 등)는 24시간 내 대응'],
|
|
duration: '당일',
|
|
},
|
|
{
|
|
id: 'S11-2', name: '현장 확인/진단', icon: '🔍',
|
|
description: 'A/S 현장을 방문하여 하자 원인을 진단한다.',
|
|
input: ['A/S 접수 기록', '시공 이력'],
|
|
output: ['하자 진단 보고서'],
|
|
responsible: '시공기사',
|
|
tips: ['시공 하자 vs 사용자 과실 판단 명확히', '사진/영상 촬영 필수'],
|
|
duration: '접수 후 2~3일 이내',
|
|
},
|
|
{
|
|
id: 'S11-3', name: '보수 작업', icon: '🔧',
|
|
description: '진단 결과에 따라 하자 보수를 실시한다.',
|
|
input: ['하자 진단 보고서', '보수 자재'],
|
|
output: ['보수 완료 보고서', '보수 전/후 사진'],
|
|
responsible: '시공기사',
|
|
tips: ['보수 완료 후 고객 확인 서명 수령', '동일 부위 재발 방지 조치'],
|
|
duration: '1~3일',
|
|
},
|
|
{
|
|
id: 'S11-4', name: '이력 관리/보증기간', icon: '📂',
|
|
description: 'A/S 이력을 관리하고, 하자보증기간 만료를 추적한다.',
|
|
input: ['A/S 완료 기록', '계약서 (보증기간)'],
|
|
output: ['A/S 이력대장', '보증기간 만료 알림'],
|
|
responsible: '영업팀 + 경영지원팀',
|
|
tips: ['하자보증기간: 보통 1~3년 (항목별 상이)', '만료 1개월 전 최종 점검 실시'],
|
|
duration: '보증기간 종료까지',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
// --- 화살표 컴포넌트 ---
|
|
function Arrow() {
|
|
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 MiniArrow() {
|
|
return (
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
width: '32px', flexShrink: 0,
|
|
}}>
|
|
<div style={{ width: '100%', height: '2px', backgroundColor: '#D1D5DB' }}></div>
|
|
<div style={{
|
|
position: 'relative', right: 0, width: 0, height: 0,
|
|
borderTop: '4px solid transparent', borderBottom: '4px solid transparent',
|
|
borderLeft: '6px solid #D1D5DB',
|
|
}}></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DownArrow() {
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '2px 0' }}>
|
|
<div style={{ width: '2px', height: '12px', backgroundColor: '#D1D5DB' }}></div>
|
|
<div style={{
|
|
width: 0, height: 0,
|
|
borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
|
|
borderTop: '6px solid #D1D5DB',
|
|
}}></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: '#FFF', 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 SubStepDetail({ sub, index, color, bgColor, isExpanded, onToggle }) {
|
|
return (
|
|
<div style={{
|
|
backgroundColor: '#FFFFFF', borderRadius: '12px',
|
|
border: isExpanded ? `2px solid ${color}` : '1px solid #E5E7EB',
|
|
overflow: 'hidden', transition: 'all 0.2s ease',
|
|
boxShadow: isExpanded ? `0 4px 12px ${color}20` : 'none',
|
|
}}>
|
|
{/* 헤더 (클릭 영역) */}
|
|
<div
|
|
onClick={onToggle}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: '10px',
|
|
padding: '12px 16px', cursor: 'pointer',
|
|
backgroundColor: isExpanded ? bgColor : '#FAFBFC',
|
|
}}
|
|
>
|
|
<div style={{
|
|
width: '28px', height: '28px', borderRadius: '50%',
|
|
backgroundColor: isExpanded ? color : '#E5E7EB',
|
|
color: isExpanded ? '#FFF' : '#6B7280',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: '12px', fontWeight: 700, flexShrink: 0,
|
|
}}>{index + 1}</div>
|
|
<span style={{ fontSize: '18px' }}>{sub.icon}</span>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontSize: '14px', fontWeight: 700, color: '#1F2937' }}>{sub.name}</div>
|
|
<div style={{ fontSize: '12px', color: '#6B7280', marginTop: '1px' }}>{sub.responsible}</div>
|
|
</div>
|
|
<div style={{
|
|
fontSize: '11px', padding: '2px 8px', borderRadius: '8px',
|
|
backgroundColor: '#F3F4F6', color: '#6B7280', whiteSpace: 'nowrap',
|
|
}}>{sub.duration}</div>
|
|
<span style={{
|
|
fontSize: '16px', color: '#9CA3AF', transition: 'transform 0.2s',
|
|
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
}}>▾</span>
|
|
</div>
|
|
|
|
{/* 상세 내용 (펼침) */}
|
|
{isExpanded && (
|
|
<div style={{ padding: '0 16px 16px' }}>
|
|
{/* 설명 */}
|
|
<div style={{
|
|
fontSize: '13px', color: '#374151', padding: '12px',
|
|
backgroundColor: bgColor, borderRadius: '8px',
|
|
borderLeft: `3px solid ${color}`, margin: '4px 0 12px',
|
|
}}>{sub.description}</div>
|
|
|
|
{/* Input → Output */}
|
|
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}>
|
|
<div style={{ flex: '1 1 180px' }}>
|
|
<div style={{ fontSize: '11px', fontWeight: 700, color: '#EF4444', marginBottom: '6px' }}>
|
|
INPUT (필요 자료)
|
|
</div>
|
|
{sub.input.map((item, i) => (
|
|
<div key={i} style={{
|
|
fontSize: '12px', color: '#4B5563', padding: '3px 0',
|
|
display: 'flex', alignItems: 'flex-start', gap: '6px',
|
|
}}>
|
|
<span style={{ color: '#EF4444', fontSize: '8px', marginTop: '4px', flexShrink: 0 }}>●</span>
|
|
{item}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: '20px', color: '#D1D5DB', flexShrink: 0, padding: '0 4px',
|
|
}}>→</div>
|
|
<div style={{ flex: '1 1 180px' }}>
|
|
<div style={{ fontSize: '11px', fontWeight: 700, color: '#10B981', marginBottom: '6px' }}>
|
|
OUTPUT (산출물)
|
|
</div>
|
|
{sub.output.map((item, i) => (
|
|
<div key={i} style={{
|
|
fontSize: '12px', color: '#4B5563', padding: '3px 0',
|
|
display: 'flex', alignItems: 'flex-start', gap: '6px',
|
|
}}>
|
|
<span style={{ color: '#10B981', fontSize: '8px', marginTop: '4px', flexShrink: 0 }}>●</span>
|
|
{item}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 팁/주의사항 */}
|
|
<div style={{
|
|
backgroundColor: '#FFFBEB', borderRadius: '8px', padding: '10px 12px',
|
|
border: '1px solid #FDE68A',
|
|
}}>
|
|
<div style={{ fontSize: '11px', fontWeight: 700, color: '#92400E', marginBottom: '4px' }}>
|
|
TIP / 주의사항
|
|
</div>
|
|
{sub.tips.map((tip, i) => (
|
|
<div key={i} style={{
|
|
fontSize: '12px', color: '#78350F', padding: '2px 0',
|
|
display: 'flex', alignItems: 'flex-start', gap: '6px',
|
|
}}>
|
|
<span style={{ flexShrink: 0 }}>💡</span>
|
|
{tip}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- 상세 모달 컴포넌트 ---
|
|
function DetailModal({ process, onClose }) {
|
|
const [expandedSub, setExpandedSub] = useState(0); // 첫번째 펼침
|
|
const modalRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const handleEsc = (e) => { if (e.key === 'Escape') onClose(); };
|
|
document.addEventListener('keydown', handleEsc);
|
|
return () => document.removeEventListener('keydown', handleEsc);
|
|
}, [onClose]);
|
|
|
|
if (!process) return null;
|
|
|
|
return (
|
|
<div
|
|
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
|
style={{
|
|
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
|
backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 9999,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
padding: '20px',
|
|
}}
|
|
>
|
|
<div
|
|
ref={modalRef}
|
|
style={{
|
|
backgroundColor: '#FFFFFF', borderRadius: '20px',
|
|
width: '100%', maxWidth: '800px', maxHeight: '90vh',
|
|
overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
|
boxShadow: '0 25px 60px rgba(0,0,0,0.3)',
|
|
}}
|
|
>
|
|
{/* 모달 헤더 */}
|
|
<div style={{
|
|
padding: '20px 24px', borderBottom: '1px solid #E5E7EB',
|
|
backgroundColor: process.bgColor, flexShrink: 0,
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<div style={{
|
|
width: '48px', height: '48px', borderRadius: '14px',
|
|
backgroundColor: '#FFFFFF', display: 'flex',
|
|
alignItems: 'center', justifyContent: 'center', fontSize: '28px',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
}}>{process.icon}</div>
|
|
<div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<span style={{
|
|
width: '24px', height: '24px', borderRadius: '50%',
|
|
backgroundColor: process.color, color: '#FFF',
|
|
fontSize: '12px', fontWeight: 700, display: 'inline-flex',
|
|
alignItems: 'center', justifyContent: 'center',
|
|
}}>{process.id}</span>
|
|
<span style={{ fontSize: '20px', fontWeight: 800, color: '#111827' }}>
|
|
{process.name}
|
|
</span>
|
|
</div>
|
|
<div style={{ fontSize: '13px', color: '#6B7280', marginTop: '2px' }}>
|
|
담당: <span style={{ color: process.color, fontWeight: 600 }}>{process.dept}</span>
|
|
{' · '}상세 업무 {process.subSteps.length}단계
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} style={{
|
|
width: '36px', height: '36px', borderRadius: '50%',
|
|
border: '1px solid #E5E7EB', backgroundColor: '#FFFFFF',
|
|
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: '18px', color: '#6B7280',
|
|
}}>×</button>
|
|
</div>
|
|
|
|
{/* 프로세스 설명 */}
|
|
<div style={{
|
|
fontSize: '13px', color: '#374151', marginTop: '12px',
|
|
padding: '10px 12px', backgroundColor: '#FFFFFF', borderRadius: '8px',
|
|
borderLeft: `4px solid ${process.color}`,
|
|
}}>{process.description}</div>
|
|
|
|
{/* 미니 서브플로우 다이어그램 */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
gap: '2px', marginTop: '16px', overflowX: 'auto', padding: '4px 0',
|
|
}}>
|
|
{process.subSteps.map((sub, i) => (
|
|
<React.Fragment key={sub.id}>
|
|
<div
|
|
onClick={() => setExpandedSub(i)}
|
|
style={{
|
|
padding: '4px 10px', borderRadius: '8px', cursor: 'pointer',
|
|
backgroundColor: expandedSub === i ? process.color : '#FFFFFF',
|
|
color: expandedSub === i ? '#FFFFFF' : '#374151',
|
|
fontSize: '11px', fontWeight: 600, whiteSpace: 'nowrap',
|
|
border: `1px solid ${expandedSub === i ? process.color : '#D1D5DB'}`,
|
|
transition: 'all 0.15s ease',
|
|
}}
|
|
>{sub.name}</div>
|
|
{i < process.subSteps.length - 1 && <MiniArrow />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 모달 본문 (스크롤) */}
|
|
<div style={{ overflowY: 'auto', padding: '16px 24px 24px', flex: 1 }}>
|
|
{/* 서브스텝 목록 */}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
{process.subSteps.map((sub, i) => (
|
|
<React.Fragment key={sub.id}>
|
|
<SubStepDetail
|
|
sub={sub}
|
|
index={i}
|
|
color={process.color}
|
|
bgColor={process.bgColor}
|
|
isExpanded={expandedSub === i}
|
|
onToggle={() => setExpandedSub(expandedSub === i ? -1 : i)}
|
|
/>
|
|
{i < process.subSteps.length - 1 && <DownArrow />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
|
|
{/* 하단 - 필요서류 + SAM 연동 */}
|
|
<div style={{
|
|
display: 'flex', gap: '16px', flexWrap: 'wrap',
|
|
marginTop: '20px', padding: '16px',
|
|
backgroundColor: '#F9FAFB', borderRadius: '12px', border: '1px solid #E5E7EB',
|
|
}}>
|
|
<div style={{ flex: '1 1 250px' }}>
|
|
<div style={{ fontSize: '12px', fontWeight: 700, color: '#374151', marginBottom: '8px' }}>
|
|
주요 서류
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
|
{process.documents.map((doc, i) => (
|
|
<span key={i} style={{
|
|
fontSize: '11px', padding: '3px 10px',
|
|
backgroundColor: '#FFFFFF', borderRadius: '12px',
|
|
color: '#374151', border: '1px solid #E5E7EB',
|
|
}}>{doc}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div style={{ flex: '0 0 auto' }}>
|
|
<div style={{ fontSize: '12px', fontWeight: 700, color: '#374151', marginBottom: '8px' }}>
|
|
SAM 연동
|
|
</div>
|
|
{process.samLink ? (
|
|
<a href={process.samLink} style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
|
fontSize: '12px', color: '#FFFFFF', fontWeight: 600,
|
|
padding: '8px 16px', borderRadius: '8px',
|
|
backgroundColor: process.color, textDecoration: 'none',
|
|
}}>↗ {process.samMenu}</a>
|
|
) : (
|
|
<span style={{ fontSize: '12px', color: '#9CA3AF', fontStyle: 'italic' }}>
|
|
추후 연동 예정
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</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 [modalProcess, setModalProcess] = useState(null);
|
|
|
|
const topRow = processes.slice(0, 6);
|
|
const bottomRow = processes.slice(6);
|
|
|
|
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={false} onClick={setModalProcess} />
|
|
{i < topRow.length - 1 && <Arrow />}
|
|
</React.Fragment>
|
|
))}
|
|
</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>
|
|
</div>
|
|
|
|
{/* 하단 행 */}
|
|
<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 />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 범례 */}
|
|
<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>
|
|
<span>ESC 키로 모달 닫기</span>
|
|
</div>
|
|
|
|
{/* 상세 모달 */}
|
|
{modalProcess && (
|
|
<DetailModal process={modalProcess} onClose={() => setModalProcess(null)} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
@endverbatim
|
|
</script>
|
|
@endpush
|