RINDA · Import UI/UX

바이어 Import — 현행 구조 & 화면별 UI/UX 분석

고아 /lead-import 제거 후, 바이어 업로드는 smart-import 단일 경로(/leads/add)입니다. 3단계 마법사의 각 화면 컴포넌트·기능·UX 결정과 백엔드 연결을 정리합니다.

작성: Claude (Opus 4.8) 기준: 2026-06-25 (post #9058) 구조감사: audit ↗ 제거로그: cleanup ↗

0개요

진입점 → 3단계 마법사 → SSE 파이프라인 → 완료 모달.

STEP 0그룹 선택기존 선택 / 새로 생성
STEP 1파일 업로드드롭존 + AI 컬럼분석
STEP 2컬럼 매핑확인·수정
STEP 2.5제외 미리보기중복 사전확인
STEP 3진행·완료SSE 단계표시
진입 경로 (모두 /leads/add) LeadsPage 그룹 화면의 "바이어 추가" 버튼(?groupId=&wsId=) · 전체 바이어 "업로드"(?newGroup=1) · 홈 대시보드(?step=group) · 앱 투어(?step=group&mode=new) · 파이프라인 연동 시트.

1파일 맵

레이어파일역할
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 APIlib/api/services/smart-import.tsanalyzeFile, startPipeline(SSE)
lib/api/hooks/smart-import.tsuseAnalyzeFile, useUnresolvedRows
BEroutes/smart-import.routes.ts4 endpoint
services/smart-import.service.tsanalyze · dedup · pipeline · map

2전체 흐름 (FE ↔ BE)

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 ─▶ 미해결(중복) 행 수동 검토

3화면 0 — 오케스트레이터 AddBuyersPage

┌─────────────────────────┐
│ ← 뒤로                   │
│ ┌─[group] 대상그룹 배너─┐ │
│ │ 👥 "독일_레더 (818)"  │ │
│ └───────────────────────┘ │
│                          │
│   [ 단계별 컴포넌트 ]    │
│   group→upload→mapping  │
│                          │
└─────────────────────────┘

AddBuyersPage

pages/leads/AddBuyersPage.tsx

상태 / 라우팅
  • step = group | upload | mapping (URL에 groupId·newGroup이면 upload부터 시작)
  • URL 파라미터: groupId · wsId · mode(new) · newGroup=1
  • resolvedGroupId · resolvedWorkspaceId · file · mapping 보관
기능
  • 대상 그룹 배너 — useCustomerGroup로 그룹명 표시("어디에 추가 중")
  • handleBack 단계 역행(mapping→upload→group→목록)
  • handleDone — 완료 시 leadKeys.lists()·customerGroupKeys 캐시 무효화

4화면 1 — 그룹 선택 StepGroupSelect

┌──────────────────────────┐
│   그룹을 선택하세요       │
│  ┌────────┬────────┐     │
│  │ 기존 ● │  새로  │     │
│  └────────┴────────┘     │
│  ┌──────────────────────┐│
│  │ 📁 독일_레더 (818) ▾ ││
│  └──────────────────────┘│
│  [뒤로]          [다음→] │
└──────────────────────────┘

StepGroupSelect

add-buyers/StepGroupSelect.tsx

컴포넌트
  • 모드 토글 — 기존/새로 (세그먼트 버튼)
  • Popover 그룹 선택 — Folder 아이콘 + 그룹명 + 리드 수(countSuffix) + 설명
  • 빈 상태 — 그룹 없을 때 안내 + "새로 만들기" CTA
  • 새 그룹 — CustomerGroupForm(hideLeadData·hideWorkspace) 재사용
데이터
  • useCustomerGroupsByWorkspace(workspaceId)
  • useCreateCustomerGroup → 생성 즉시 onGroupResolved

5화면 2 — 파일 업로드 StepUpload

┌──────────────────────────┐
│ 파일 업로드  [CSV][XLSX]↓│
│ ┌──────────────────────┐ │
│ │      ⬆               │ │
│ │  드래그 또는 클릭     │ │
│ │  .csv .xlsx .xls 50MB│ │
│ └──────────────────────┘ │
│ ✨ 파싱→중복→검증→저장   │
└──────────────────────────┘

StepUpload

add-buyers/StepUpload.tsx

컴포넌트
  • react-dropzone 드롭존 — accept csv/xlsx/xls, maxFiles 1, 분석중 disabled
  • 템플릿 다운로드 — CSV(UTF-8 BOM)·XLSX, 헤더 8칸 + 샘플 1행
  • 파이프라인 미리보기 — parse→dedup→verify→save 4단계 칩(실제 BE와 일치)
  • 분석중 스피너 + 힌트, reject(확장자/크기) 메시지
기능 / 검증
  • 50MB 초과·0byte 차단(toast)
  • useAnalyzeFile.mutate → 성공 시 onAnalyzed(file, suggestedMapping)
  • suggestedMapping 비면 "컬럼 없음" 에러

6화면 3 — 컬럼 매핑 StepMapping (핵심)

┌────────────────────────────┐
│ 컬럼 매핑                    │
│ ⚠ 확인할 칸 2개  /  ✓ 자동완료│
│ 📄 buyers.csv          [✕]  │
│ ⓘ 이메일 없음 — 보강 필요   │
│ ┌─저장항목──┬─내 파일데이터─┐│
│ │ 회사명 ▾  │ Gap Inc.      ││
│ │ 이메일 ▾  │ a@gap.com     ││
│ │ 가져안함▾ │ (메모)        ││
│ └───────────┴───────────────┘│
│            [업로드 시작]     │
└────────────────────────────┘

StepMapping

add-buyers/StepMapping.tsx

컴포넌트
  • 상태 헤더 — 확인 N개(amber) 또는 자동완료
  • 파일 헤더(파일명 + 닫기), 식별자 누락 red 차단, 이메일 누락 blue 안내(비차단)
  • 매핑 미리보기 카드 — 행별 좌: 저장항목 드롭다운 / 우: 샘플 데이터
  • 드롭다운 = 자주쓰는 8필드 + (AI가 8밖으로 잡으면) 현재값 + "가져오지 않기"(skip)
  • 저신뢰(confidence<0.75) 행 강조
기능
  • handleStartImport → (newGroup 모드면 그룹선택 모달) → proceedImport
  • proceedImportparseCsvForPreview로 제외후보 산출 → 있으면 화면4, 없으면 바로 SSE
  • startImportPipelinestartPipeline(SSE), 콜백으로 phase/progress/complete/error 처리

7화면 4 — 제외 미리보기 StepExclusion

┌────────────────────────────┐
│ 자동 제외 미리보기 (모달)   │
│ ─────────────────────────── │
│ 중복/억제로 빠질 행 N건     │
│ ☑ 회사명 중복  ☑ 이메일중복 │
│ ┌──────────────────────────┐│
│ │ 행 · 회사 · 사유          ││
│ └──────────────────────────┘│
│   [취소]        [그대로 진행]│
└────────────────────────────┘

StepExclusion → ExclusionPreviewModal

add-buyers/StepExclusion.tsx · components/exclusion-preview/

역할
  • 임포트 전, 제외될 행을 사용자에게 사전 고지(중복·억제 등)
  • 대형 파일(EXCLUSION_PREVIEW_MAX_ROWS 초과)은 미리보기 skip → 바로 진행(중복은 BE dedup이 처리)
  • 확인 시 handleExclusionConfirmstartImportPipeline
  • 미리보기 실패해도 "그대로 진행" 가능(result/filters=null)

8화면 5 — 진행 & 완료 ImportStatusDialog

── 진행중 ──         ── 완료 ──
┌──────────────┐   ┌──────────────┐
│  ◌ 회전       │   │   ✓          │
│ 업로드 중…    │   │ 추가 완료!    │
│ ✓ 파일 분석   │   │ 4명 추가됨    │
│ ✓ 중복 제거   │   │ 기존 1 편입   │
│ ◌ 이메일 검증 │   │ 중복 12 제외  │
│ · 리드 등록   │   │ [바이어 보기]│
└──────────────┘   └──────────────┘

ImportStatusDialog

add-buyers/ImportStatusDialog.tsx

진행(importing)
  • phase 리스트 — PhaseIcon: 회전(running)/체크(completed)/에러(failed)
  • 단계별 메시지(예: "823행 중 200 검증"), running 행 하이라이트
  • 진행 중 ESC/바깥클릭 닫기 차단, "백그라운드 진행" 안내
완료(done) — 수치 계산
  • 추가됨 = addedToGroup + existingAddedToGroup ("0건 추가" 패닉 버그 수정)
  • 제외 = 중복(그룹편입분 제외) + 이메일오류(리드 단위)
  • 이메일 없어 그룹 미편입(목록엔 저장) 별도 안내
  • "바이어 보기" → /leads?groupId=…

9백엔드 API & 파이프라인

Endpoint호출 화면처리
POST /smart-import/analyze화면2analyzeFileColumns — LLM(gemini-3.1-flash-lite) 컬럼 자동매핑, confidence 0.6 cap
POST /smart-import/start (SSE)화면3→5runSmartImportPipelineLocked — 락 + parse→dedup→verify→import
GET /smart-import/:jobIdjob 상태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
주의 — 회사명 dedup 2단계에서 website_url이 비면 회사명이 판정축. 정규화 회사명이 같으면 (이메일·담당자 달라도) 행이 duplicates로 빠짐 → 같은 회사 다수 담당자(ABM) 시 의도와 다르게 줄어듦. 빠진 행은 unresolvedData에서 복구 가능. 상세 ↗

10눈여겨볼 UX 결정

결정이유
저신뢰 매핑 = amber, 차단 에러(식별자 누락) = red글로벌 유저가 red를 "실패"로 오인 → 확인 신호는 톤다운
LLM confidence를 0.6으로 capAI 과신 방지, 사용자 검토 유도
진행/완료를 한 모달로 전환 + 최소 표시시간(MIN_IMPORT_MS)작은 파일에서 화면 깜빡임 방지
완료 "추가됨" = 신규 + 기존편입 합산기존 리드 매칭 시 "0건 추가" 패닉 버그 수정
이메일 누락은 비차단 안내(blue)식별자만 있으면 업로드 허용 + 보강 유도
검증 진행 200건마다 SSE push대형 파일 "멈춤" 오인 → 재업로드(이중적재) 방지
템플릿 CSV에 BOM + 샘플 행Excel 한글 깨짐 방지 + 형식 즉시 이해
red 차단 에러 amber 확인 요청 blue 비차단 안내 green 완료