고아 /lead-import 제거 후, 바이어 업로드는 smart-import 단일 경로(/leads/add)입니다. 3단계 마법사의 각 화면 컴포넌트·기능·UX 결정과 백엔드 연결을 정리합니다.
진입점 → 3단계 마법사 → SSE 파이프라인 → 완료 모달.
/leads/add)
LeadsPage 그룹 화면의 "바이어 추가" 버튼(?groupId=&wsId=) · 전체 바이어 "업로드"(?newGroup=1) · 홈 대시보드(?step=group) · 앱 투어(?step=group&mode=new) · 파이프라인 연동 시트.
| 레이어 | 파일 | 역할 |
|---|---|---|
| FE 화면 | pages/leads/AddBuyersPage.tsx | 오케스트레이터(state machine) |
add-buyers/StepGroupSelect.tsx | 화면1 — 그룹 선택/생성 | |
add-buyers/StepUpload.tsx | 화면2 — 드롭존 + 분석 | |
add-buyers/StepMapping.tsx (27KB) | 화면3 — 매핑 + SSE 구동 | |
add-buyers/StepExclusion.tsx | 화면4 — 제외 미리보기 래퍼 | |
add-buyers/ImportStatusDialog.tsx | 화면5 — 진행/완료 모달 | |
| FE API | lib/api/services/smart-import.ts | analyzeFile, startPipeline(SSE) |
lib/api/hooks/smart-import.ts | useAnalyzeFile, useUnresolvedRows … | |
| BE | routes/smart-import.routes.ts | 4 endpoint |
services/smart-import.service.ts | analyze · dedup · pipeline · map |
FE state machine (AddBuyersPage): group → upload → mapping → (모달들) [화면1 그룹] StepGroupSelect ──onGroupResolved(groupId,wsId)──┐ ▼ [화면2 업로드] StepUpload ──POST /smart-import/analyze──▶ analyzeFileColumns() │ ◀── {columns, suggestedMapping(LLM), totalRows} └──onAnalyzed(file,mapping)──▶ [화면3 매핑] StepMapping │ parseCsvForPreview() (클라 미리보기/제외후보) ├─ records>0 ─▶ [화면4] ExclusionPreviewModal ─확인─┐ └─ records=0 ────────────────────────────────────────┤ ▼ POST /smart-import/start (SSE) ──▶ runSmartImportPipelineLocked() [화면5 진행] ImportStatusDialog ◀─ SSE events: pipeline_step(parsing/dedup/verifying/importing) verify_progress · import_progress pipeline_complete {imported, addedToGroup, ...} │ [화면5 완료] 완료 요약 ──"바이어 보기"──▶ /leads?groupId=… (이후) GET /:jobId/unresolved ─▶ 미해결(중복) 행 수동 검토
AddBuyersPage┌─────────────────────────┐ │ ← 뒤로 │ │ ┌─[group] 대상그룹 배너─┐ │ │ │ 👥 "독일_레더 (818)" │ │ │ └───────────────────────┘ │ │ │ │ [ 단계별 컴포넌트 ] │ │ group→upload→mapping │ │ │ └─────────────────────────┘
pages/leads/AddBuyersPage.tsx
step = group | upload | mapping (URL에 groupId·newGroup이면 upload부터 시작)groupId · wsId · mode(new) · newGroup=1resolvedGroupId · resolvedWorkspaceId · file · mapping 보관useCustomerGroup로 그룹명 표시("어디에 추가 중")handleBack 단계 역행(mapping→upload→group→목록)handleDone — 완료 시 leadKeys.lists()·customerGroupKeys 캐시 무효화StepGroupSelect┌──────────────────────────┐ │ 그룹을 선택하세요 │ │ ┌────────┬────────┐ │ │ │ 기존 ● │ 새로 │ │ │ └────────┴────────┘ │ │ ┌──────────────────────┐│ │ │ 📁 독일_레더 (818) ▾ ││ │ └──────────────────────┘│ │ [뒤로] [다음→] │ └──────────────────────────┘
add-buyers/StepGroupSelect.tsx
Popover 그룹 선택 — Folder 아이콘 + 그룹명 + 리드 수(countSuffix) + 설명CustomerGroupForm(hideLeadData·hideWorkspace) 재사용useCustomerGroupsByWorkspace(workspaceId)useCreateCustomerGroup → 생성 즉시 onGroupResolvedStepUpload┌──────────────────────────┐ │ 파일 업로드 [CSV][XLSX]↓│ │ ┌──────────────────────┐ │ │ │ ⬆ │ │ │ │ 드래그 또는 클릭 │ │ │ │ .csv .xlsx .xls 50MB│ │ │ └──────────────────────┘ │ │ ✨ 파싱→중복→검증→저장 │ └──────────────────────────┘
add-buyers/StepUpload.tsx
react-dropzone 드롭존 — accept csv/xlsx/xls, maxFiles 1, 분석중 disableduseAnalyzeFile.mutate → 성공 시 onAnalyzed(file, suggestedMapping)StepMapping (핵심)┌────────────────────────────┐ │ 컬럼 매핑 │ │ ⚠ 확인할 칸 2개 / ✓ 자동완료│ │ 📄 buyers.csv [✕] │ │ ⓘ 이메일 없음 — 보강 필요 │ │ ┌─저장항목──┬─내 파일데이터─┐│ │ │ 회사명 ▾ │ Gap Inc. ││ │ │ 이메일 ▾ │ a@gap.com ││ │ │ 가져안함▾ │ (메모) ││ │ └───────────┴───────────────┘│ │ [업로드 시작] │ └────────────────────────────┘
add-buyers/StepMapping.tsx
handleStartImport → (newGroup 모드면 그룹선택 모달) → proceedImportproceedImport → parseCsvForPreview로 제외후보 산출 → 있으면 화면4, 없으면 바로 SSEstartImportPipeline → startPipeline(SSE), 콜백으로 phase/progress/complete/error 처리StepExclusion┌────────────────────────────┐ │ 자동 제외 미리보기 (모달) │ │ ─────────────────────────── │ │ 중복/억제로 빠질 행 N건 │ │ ☑ 회사명 중복 ☑ 이메일중복 │ │ ┌──────────────────────────┐│ │ │ 행 · 회사 · 사유 ││ │ └──────────────────────────┘│ │ [취소] [그대로 진행]│ └────────────────────────────┘
add-buyers/StepExclusion.tsx · components/exclusion-preview/
EXCLUSION_PREVIEW_MAX_ROWS 초과)은 미리보기 skip → 바로 진행(중복은 BE dedup이 처리)handleExclusionConfirm → startImportPipelineresult/filters=null)ImportStatusDialog── 진행중 ── ── 완료 ── ┌──────────────┐ ┌──────────────┐ │ ◌ 회전 │ │ ✓ │ │ 업로드 중… │ │ 추가 완료! │ │ ✓ 파일 분석 │ │ 4명 추가됨 │ │ ✓ 중복 제거 │ │ 기존 1 편입 │ │ ◌ 이메일 검증 │ │ 중복 12 제외 │ │ · 리드 등록 │ │ [바이어 보기]│ └──────────────┘ └──────────────┘
add-buyers/ImportStatusDialog.tsx
PhaseIcon: 회전(running)/체크(completed)/에러(failed)addedToGroup + existingAddedToGroup ("0건 추가" 패닉 버그 수정)/leads?groupId=…| Endpoint | 호출 화면 | 처리 |
|---|---|---|
POST /smart-import/analyze | 화면2 | analyzeFileColumns — LLM(gemini-3.1-flash-lite) 컬럼 자동매핑, confidence 0.6 cap |
POST /smart-import/start (SSE) | 화면3→5 | runSmartImportPipelineLocked — 락 + parse→dedup→verify→import |
GET /smart-import/:jobId | job 상태 | import_jobs 조회 (5s 폴링) |
GET /:jobId/unresolved | 사후 | 중복(미해결) 행 수동 검토 |
runSmartImportPipelineLocked [워크스페이스당 1실행 락] 0 preflight 식별자(회사명/URL/이메일) ≥1 필수, 아니면 400 1 parse 빈 행 제거 → emptyRowsSkipped 2 dedup email → url → 회사명 → {unique, duplicates} duplicates → import_jobs.unresolvedData (복구 가능) 2.5 verify MillionVerifier — 배달불가·위험(≤20) 제거 3 import bulkImportLeads(unique) + 그룹 멤버 추가 3.5 group DB매칭 중복(matchedLeadId) → 그룹 편입 ✓ complete imported / duplicates / addedToGroup / existingAddedToGroup
website_url이 비면 회사명이 판정축. 정규화 회사명이 같으면 (이메일·담당자 달라도) 행이 duplicates로 빠짐 → 같은 회사 다수 담당자(ABM) 시 의도와 다르게 줄어듦. 빠진 행은 unresolvedData에서 복구 가능. 상세 ↗
| 결정 | 이유 |
|---|---|
| 저신뢰 매핑 = amber, 차단 에러(식별자 누락) = red | 글로벌 유저가 red를 "실패"로 오인 → 확인 신호는 톤다운 |
| LLM confidence를 0.6으로 cap | AI 과신 방지, 사용자 검토 유도 |
진행/완료를 한 모달로 전환 + 최소 표시시간(MIN_IMPORT_MS) | 작은 파일에서 화면 깜빡임 방지 |
| 완료 "추가됨" = 신규 + 기존편입 합산 | 기존 리드 매칭 시 "0건 추가" 패닉 버그 수정 |
| 이메일 누락은 비차단 안내(blue) | 식별자만 있으면 업로드 허용 + 보강 유도 |
| 검증 진행 200건마다 SSE push | 대형 파일 "멈춤" 오인 → 재업로드(이중적재) 방지 |
| 템플릿 CSV에 BOM + 샘플 행 | Excel 한글 깨짐 방지 + 형식 즉시 이해 |