korea_brand_email_unique_company_max3_crosschecked.csv · 2026-06-01smart-import.service.ts:635-676).파일 korea_brand_email_unique_company_max3_crosschecked.csv 는 GobizKOREA 기반 한국 수출기업 공식 홈페이지 이메일 수집분입니다. 7,160행, 이메일 전부 unique (파일 내부 중복 0), 회사 5,512, 도메인 3,756.
화면의 "고유 3,999 / 중복 3,161"은 파일 내부가 아니라 워크스페이스 DB 기준 중복 판정입니다. 그런데 "중복 3,161"이 사용자에게는 "제외됨"으로 읽히지만 실제로는 그 중 1,852건이 그룹에 합류합니다.
검증: 3,755 + 1,852 = 5,607 — 카운트가 정확히 맞아떨어짐.
업로드 UI AddBuyersPage(1 그룹 → 2 업로드 → 3 완료)는 smart-import 경로를 사용합니다. (별도 페이지인 lead-import 경로와 무관 — 그 경로엔 이 블록이 없음.)
| 단계 | 위치 |
|---|---|
| UI 제출 | StepMapping.tsx:337 → smartImportApi.startPipeline |
| API | POST /api/v1/smart-import/start |
| 서비스 | smart-import.service.ts:402 runSmartImportPipeline |
| 문제 블록 | smart-import.service.ts:635-676 |
// smart-import.service.ts:635-676 — 중복(기존) 리드를 신규 그룹에 추가 if (customerGroupId && duplicates.length > 0) { const duplicateLeadIds = duplicates.map(d => d.matchedLeadId).filter(Boolean) // 이 그룹에 "이미 있는지"만 확인 — 그게 유일한 필터 const toAdd = [...new Set(duplicateLeadIds.filter(id => !alreadyInGroupIds.has(id)))] db.insert(customerGroupMembers) .values(chunk.map(leadId => ({ groupId, leadId }))) .onConflictDoNothing() // ← lead_status·발송이력·타그룹 필터 전무 }
핵심: matchedLeadId는 이메일·URL·회사명 중 하나라도 워크스페이스 DB와 매칭되면 세팅됩니다. 그 기존 리드 전부가 — 발송했든, 수신거부했든, 다른 그룹 소속이든 — 무조건 신규 그룹에 합류합니다. 유일한 체크는 "이 그룹에 이미 있나"뿐입니다.
신규 그룹 생성 경로도 동일: StepGroupSelect가 그룹을 먼저 만들고 그 groupId를 넘기므로, 신규/기존 그룹 구분 없이 같은 블록이 실행됩니다.
실사용 경로 deduplicateRecords (smart-import.service.ts:257-397) — 우선순위 순차, 하나라도 hit이면 즉시 중복 처리:
| 순위 | 기준 | 매칭 대상 | 정규화 |
|---|---|---|---|
| 1 | 이메일 | lead_contacts (email) ∩ 같은 WS | normalizeEmail — NFC+trim+lowercase (gmail dot/plus 미정규화) |
| 2 | website URL | leads.website_url host | normalizeUrlHost — hostname만, www. 제거 |
| 3 | 회사명 | leads.company_name | normalizeCompanyName — 악센트·부호·법인접미사 제거 |
각 기준은 DB 중복 먼저, 없으면 CSV 파일 내 자가중복을 검사합니다. 워크스페이스 전체 leads/email을 메모리 Map으로 로드합니다.
| 케이스 | 새 lead 생성 | 기존 lead 업데이트 | 그룹 멤버 추가 | 이메일 컨택 추가 |
|---|---|---|---|---|
| 완전 신규 | 생성 | — | 추가 | 추가 |
| 이메일만 DB 중복 | 안 함 | 안 함 | 추가 ⚠ | 안 함 |
| URL만 DB 중복 | 안 함 | 안 함 | 추가 ⚠ | 안 함 |
| 회사명만 DB 중복 | 안 함 | 안 함 | 추가 ⚠ | 안 함 |
| CSV 내부 이메일 중복 | 안 함 | 안 함 | 안 함 | 안 함 |
| 이메일 전부 검증 실패 | 제외 | — | 제외 | — |
기존 lead는 업데이트도, 새 컨택 추가도 전혀 하지 않습니다. DB 중복(matchedLeadId 보유)은 오직 그룹에만 추가, CSV 내부 중복(matchedLeadId 없음)은 그룹에도 추가 안 됨.
"그룹에 합류한 뒤 실제 발송 시 차단되는가"가 심각도를 가릅니다. suppression 게이트가 enrollment 시점 + 발송 시점 이중으로 존재합니다 (bulk-enrollment-scheduling.service.ts:154 · resolve-lead.ts:102).
| 시나리오 | 현재 동작 | 실제 결과 | 심각도 |
|---|---|---|---|
| 완전 신규 파일 | 정상 생성·추가 | 문제 없음 | 없음 |
| 기존 리드 재업로드 | 발송이력째 그룹 합류 | 통계 오염·사용자 혼란 | 중간 |
| unsubscribed 포함 | 그룹엔 합류하나 발송 직전 skip | 실발송 차단됨 (사고 아님). 단 enroll 통계 왜곡 | 중간 |
| bounced 포함 | 그룹 합류, MV/blocklist에서 skip | 실발송 차단됨 | 중간 |
| 5만행 대용량 | WS 전체 메모리 로드 + lower() seq-scan | 메모리 급증·지연 (실측 3분+ 미완) | 높음 |
| 페이지 이탈/재연결 | 서버는 계속, SSE만 끊김 | 진행 불명확하나 데이터는 안전 | 낮음 |
| MV 크레딧 고갈 | CreditsExhaustedError → 중단 / MX fallback | 부분 처리, 재개 불명확 | 중간 |
| 검증 통과 이메일 | is_verified=false 저장 | 발송 시 재검증 (MV 비용 2회) | 중간 |
정정: 초기에 "unsubscribed 재유입 = 컴플라이언스 발송 위험"으로 봤으나, 발송 경로 검증 결과 실제 재발송은 차단됩니다. 위험도를 하향합니다. 진짜 문제는 UX 혼란 + 그룹/enroll 통계 오염 + 검증 중복비용 + 대규모 성능입니다.
5,891개 이메일 중 239개만 verified=true (그나마 과거 enrichment산), 5,652개 false. verifyImportEmails는 거절분만 반환하고 통과 이메일에 is_verified를 세팅하지 않음 (lead-import.service.ts:360-368 — isVerified 필드 누락). 발송 워커가 다시 검증 → MV 호출 2회 중복.
smart-import(중복 그룹 추가) vs lead-import(중복 skip) 의 중복 정책이 정반대. 같은 "리드 업로드"인데 동작이 다름.
dedup이 워크스페이스 전체 leads/email을 메모리 Map으로 로드 + lower(contact_value) seq-scan. 본 분석 중 교집합 쿼리가 3분+ 미완으로 종료됨 — 정규화 키 인덱스 부재가 직접 증거.
| # | 영역 | 개선 | 우선 |
|---|---|---|---|
| A | 그룹 멤버십 | 중복 리드를 3분류(신규 / 기존-발송가능 / 기존-suppressed)로. 업로드 옵션 "기존 리드 처리: 신규만 ┃ 기존도 합류 ┃ 전체", 기본=신규만 | 높음 |
| B | 검증 SSOT | approved Set 반환 → is_verified+verified_at+score 저장. 발송 워커는 90일내 검증분 재호출 skip | 높음 |
| C | dedup 성능 | 정규화 키 generated column + 인덱스, unnest 배치 JOIN으로 전환 | 높음 |
| D | UX/문구 | "신규 N 추가 / 기존 N 합류 / 제외 N" 3분류 + 완료화면 최종 그룹크기·발송이력 보유수 | 중간 |
| E | 안정성 | 업로드를 BullMQ 잡으로, 진행상태 DB SSOT, SSE는 표시 전용 (재연결·재시도 안전) | 중간 |
N0.1 정규화 키 인덱스(C) + A 그룹 합류 게이팅 묶음을 첫 PR로. 인덱스는 단독으로도 즉효, A는 혼란의 직접 원인.