리드 캡처 유입구 전수 조사 — themodernblinds.com¶
조사일 2026-08-14 · 대상 라이브 Flask 사이트 (landing-page-modernblinds/)
이 문서는 **지금 프로덕션에 떠 있는 것**만 다룹니다. 신규 Next.js 사이트
(mb_homepage_dev)는 범위 밖입니다.
이 문서를 만든 이유
유입구가 오랫동안 늘어나기만 하고 정리된 적이 없었습니다. 조사 결과
약 108개의 form_source 코드가 있지만 프로덕션에서 한 건도 저장되지
않고, 마그넷 파이프라인은 3개월째 동작하지 않고 있습니다. 상세는 §5.
1. 한 장 요약¶
| 구분 | 수량 |
|---|---|
| 캡처 표면 종류 | 5종 (+ LP v2 별도 체계) |
| 폼이 달린 페이지 | 인라인 30 · L2 팝업 25 · L4 마그넷 팝업 4 · 인라인 마그넷 5 · L3 전화 폴백 전 페이지 |
작성된 form_source 코드 |
83개 (템플릿 하드코딩 59 + LP v2 파생 24) |
| 런타임에 실제로 생기는 문자열 | 약 208개 (팝업 25개가 트리거별로 ×5 분화) |
| 서버 엔드포인트 | 3개 (+ Meta CAPI 브리지 1) |
| 프로덕션에 저장되는 유입구 정보 | 🔴 없음 (태그 9종 + 콜백 태그 1종이 전부) |
2. 캡처 표면 5종¶
2-1. 인라인 견적 폼 (상시 노출)¶
- 파일
app/templates/components/lp_form.html - 게시 30개 페이지 — 제품 6, 지역 17, gallery, faq, LP v1 4, Meta LP 1
- 전송
POST /api/estimate form_source호출부가 Jinja 로 넘김. 미지정 시lp_form_legacy(lp_form.html:18)
product-{제품}_inline 예: product-shutters_inline
local-{전체슬러그}_inline 예: local-window-treatments-kyle_inline
gallery_inline · faq_inline
2-2. L2 팝업 CTA — 전체 폼 (참여 트리거)¶
- 파일
components/popup_cta.html+static/js/popup-cta.js - 게시 25개 페이지 — index, 제품 6, faq, 지역 17
- 전송
POST /api/estimate(인라인 폼과 같은 파이프라인)
트리거 — 먼저 걸리는 것 하나 (popup-cta.js:23-28)
| 조건 | 임계값 |
|---|---|
| 스크롤 깊이 | 40% |
| 체류 시간 | 10초 |
| Exit intent | 데스크톱만 (min-width:768px + hover:hover) |
| 아티클 푸터 진입 | .bp-back, .be-back, .article-footer |
코드와 주석이 다릅니다
popup-cta.js 상단 주석은 "스크롤 60% / 60초" 라고 적혀 있지만
실제 TRIGGERS 값은 40% / 10초 입니다 (4-6행 vs 23-28행).
임계값을 조정하려는 사람이 주석을 믿으면 틀립니다.
🔴 제출 시점에 트리거 접미사가 붙습니다 (popup-cta.js:58-71).
서버에 도착하는 값은 템플릿에 적힌 것과 다릅니다.
이 때문에 팝업 25개 코드가 런타임에 **최대 125개**로 분화합니다.
2-3. L3 전화 폴백 — 전화 1필드¶
- 파일
components/popup_phone_fallback.html+static/js/popup-phone-fallback.js - 게시 전 페이지 (
base.html,base_lp.html에 포함) - 전송
POST /api/estimate
트리거 — 기기별로 다릅니다 (popup-phone-fallback.js:35-36)
| 기기 | 조건 |
|---|---|
| 모바일 (≤768px) | 스크롤 50% 또는 체류 30초 |
| 데스크톱 (≥768px hover) | 스크롤 70% 또는 exit-intent |
데스크톱이 더 보수적인 이유는 **이미 L2 전체 폼을 한 번 거절한 사용자**이기 때문입니다.
form_source 는 런타임에 L2 오버레이 값에서 파생합니다.
영업은 _phone1 접미사로 콜백 대기열을 식별합니다. 프로덕션에서 유일하게
살아 있는 form_source 용도입니다 (§5-② 참조).
2-4. L4 마그넷 팝업 — 이메일 1필드¶
- 파일
components/popup_magnet.html+static/js/popup-magnet.js - 게시 4개 —
blog,blog_post,gallery,about - 전송
POST /api/magnet-signup🔴 프로덕션 장애 중 (§5-①) - 트리거 스크롤 40% / 체류 20초 / exit-intent
L2 와 달리 트리거 접미사를 붙이지 않습니다. 메인 Lead 전환도 발화하지
않고 magnet_signup GA4 이벤트만 냅니다 (콜드 티어).
| 페이지 | magnet_type |
form_source |
|---|---|---|
| about | price_guide |
about_magnet_pricing_popup |
| blog | price_guide |
blog-index_magnet_pricing_popup |
| blog_post | price_guide |
blog-post_magnet_pricing_popup |
| gallery | lookbook |
gallery_magnet_lookbook_popup |
2-5. 인라인 마그넷 위젯 (상시 노출)¶
- 파일
components/magnet_optin_widget.html - 게시 5개 —
index,about,gallery,blog,blog_post - 전송
POST /api/magnet-signup🔴 프로덕션 장애 중
| 페이지 | magnet_type |
form_source |
|---|---|---|
| index | price_guide |
home_magnet_pricing |
| about | price_guide |
about_magnet_pricing |
| blog | price_guide |
blog-index_magnet_pricing |
| blog_post | price_guide |
blog-post_magnet_pricing |
| gallery | lookbook |
gallery_magnet_lookbook |
2-6. 폼이 아닌 유입구¶
| 컴포넌트 | 성격 |
|---|---|
mobile_phone_bar.html |
tel: 링크. 전 페이지. 폼 아님 |
cta_banner.html |
연락처 페이지 링크. base.html. 폼 아님 |
3. 🔴 3계층 팝업 상호 억제 규칙¶
가장 문서화가 안 돼 있던 부분입니다. 팝업이 안 뜬다는 문의가 반복되는 이유가 여기 있습니다. 네 개의 스토리지 키가 세 팝업에 걸쳐 얽혀 있습니다.
| 키 | 저장소 | 수명 | 설정 주체 | 효과 |
|---|---|---|---|---|
lp_popup_submitted |
session | 탭 종료까지 | L2·L4 공유 | 어느 팝업이든 제출하면 전부 정지 |
lp_popup_dismissed |
local | 24h | L2 | L2 정지 + L3 자격의 기준점 |
lp_phone_fallback_dismissed |
local | 24h | L3 | L3 정지 |
lp_magnet_popup_dismissed |
local | 24h | L4 | L4 정지 |
L3 는 조건이 하나 더 있습니다 (popup-phone-fallback.js:32).
즉 L2 를 방금 닫은 사용자에게는 L3 가 절대 뜨지 않습니다. 같은 세션에서 연달아 테스트하면 "L3 가 고장났다"고 오인하게 됩니다.
공통 억제: GPC(Global Privacy Control) 가 켜져 있으면 세 팝업 모두 뜨지 않습니다.
QA 강제 노출¶
브라우저 저장소를 지우지 않고 즉시 띄우는 방법입니다.
강제 노출로 제출하면 form_source 에 _force 가 붙습니다(L2 한정).
4. LP v2 — 별도 체계¶
pages/lp/google/v2/ 8개 페이지는 위 컴포넌트를 쓰지 않고 자체
lp2/ 컴포넌트를 씁니다. **페이지당 유입구가 4개**입니다.
form_source 는 하드코딩이 아니라 슬러그에서 파생됩니다
(app/lp/schema.py:365-375).
form_source_hero → f"lp-{self.slug}_hero"
form_source_second_cta → f"lp-{self.slug}_second-cta"
form_source_bottom_bar → f"lp-{self.slug}_bottom-bar"
| 컴포넌트 | form_source |
|---|---|
lp2/hero.html |
lp-{slug}_hero |
lp2/second_cta.html |
lp-{slug}_second-cta |
lp2/bottom_bar.html |
lp-{slug}_bottom-bar |
lp2/popup_form.html |
hero 값을 재사용 (popup_form.html:124) |
8개 슬러그: blinds-near-me-austin, custom-blinds-austin,
energy-efficient-blinds-austin, faux-wood-blinds-austin,
motorized-blinds-austin, plantation-shutters-austin,
roller-shades-austin, zebra-shades-austin
→ 8 × 3 = 24개 파생 코드. LP v2 는 자동 트리거를 의도적으로 쓰지 않습니다
(popup-cta.js:337).
5. 현황 진단 — 실측 4건¶
① 🔴 마그넷 신청이 프로덕션에서 전부 실패한다¶
lead_magnet_subscribers 테이블이 프로덕션에 존재하지 않습니다.
프로덕션 alembic head : 20260326_0033
필요한 마이그레이션 : 20260510_0035_lead_magnet_subscribers.py ← 미적용
(20260427_0034_add_ai_visualize_audit_action.py 도 함께 미적용)
Loki 30일 로그 — 매일 07:00 UTC 육성 잡이 같은 오류로 죽습니다.
UndefinedTableError: relation "lead_magnet_subscribers" does not exist
2026-07-17 ~ 2026-08-14 · 34건 (하루 1건)
영향 범위: §2-4 의 팝업 4개 페이지 + §2-5 의 인라인 5개 페이지.
완화 요인 — 그리고 그것이 더 나쁜 신호일 수 있습니다
같은 30일 동안 **실제 신청 시도 로그가 0건**입니다. 34건 전부 육성 잡의 자체 오류입니다. 지금 유실되는 리드는 없어 보입니다.
뒤집으면 **마그넷 유입구 9개가 3개월간 단 한 건도 만들지 못했다**는 뜻이기도 합니다. 장애 복구보다 존치 여부가 먼저인 이유입니다.
② 🔴 form_source 108개가 프로덕션에서 측정되지 않는다¶
프로덕션 FastAPI(main 브랜치)는 form_source 를 받기는 하지만 저장하지
않습니다. 유일한 용도가 _phone1 접미사로 콜백 태그를 붙이는 것뿐입니다
(public_form_service.py:238-240).
-- 프로덕션 crm_lead 에서 form_source 를 담은 행
SELECT count(*) FROM crm_lead WHERE description ILIKE '%form%source%';
-- → 0
x_form_source 등 8개 구조화 필드는 스테이징에만 있습니다
(WO-001, 2026-08-12 배포). 프로덕션 반영은 홈페이지 컷오버 시점입니다.
결과 — 웹 리드 110건에 대해 프로덕션이 구분할 수 있는 것은 태그뿐입니다.
SITE: MODERNBLINDS 62 (2026-03-18 ~ 08-12)
LP: BLINDS-AUSTIN 16
LP: MOTORIZED-BLINDS-AUSTIN 8
LP: CUSTOM-BLINDS-AUSTIN 7
SITE: AUSTINJDX 7
LP: PLANTATION-SHUTTERS-AUSTIN 6
LP: BLOG/... 외 3종 4
PHONE CALLBACK 2
즉 "어느 폼이 계약으로 이어지는가"를 지금은 답할 수 없습니다. 표면·트리거·페이지 축이 전부 버려집니다.
③ LP v1·Meta 5개 페이지가 같은 값을 보낸다¶
form_source 를 지정하지 않아 전부 기본값 lp_form_legacy 로 떨어집니다
(lp_form.html:18).
lp/google/blinds-austin.html
lp/google/motorized-blinds-austin.html
lp/google/shutters-austin.html
lp/google/outdoor-screens-austin.html
lp/meta/free-consultation.html
5개 LP 가 서로 구분되지 않습니다. §2-2 의 태그(LP: BLINDS-AUSTIN 등)로는
페이지가 구분되지만, 그 페이지 안의 **어느 폼**인지는 알 수 없습니다.
④ 마그넷 이름이 두 체계로 갈려 있다¶
| 위치 | 값 |
|---|---|
서버 enum (app/routes/magnet.py:47) |
swatch · price_guide · lookbook |
form_source 문자열 |
pricing · lookbook |
home_magnet_pricing 의 magnet_type 은 price_guide 입니다. 두 축을
조인하려면 pricing ↔ price_guide 매핑이 필요한데 어디에도 적혀 있지
않습니다.
또 swatch 는 서버 enum 과 컴포넌트 주석에만 있고 실제로 쓰는 페이지가
없습니다. 스왓치 팩은 정책에는 있으나 배포된 적이 없습니다.
(magnet.py:48 의 주소 필수 검증도 그래서 한 번도 실행되지 않았습니다)
6. 데이터 흐름¶
[브라우저]
utm-capture.js → hidden 필드 채움 (gclid/gbraid/wbraid/utm_*/lp_slug)
│
├─ 인라인 폼 · L2 팝업 · L3 전화 폴백 · LP v2 폼
│ └→ POST /api/estimate ──→ FastAPI ──→ Odoo crm.lead ✅
│
├─ L4 마그넷 팝업 · 인라인 마그넷
│ └→ POST /api/magnet-signup ─→ FastAPI ─→ lead_magnet_subscribers
│ 🔴 테이블 없음
└─ 헬프데스크 폼 (helpdesk 페이지 1개)
└→ POST /api/helpdesk ────→ FastAPI ──→ Odoo 티켓 ✅
| 경로 | 파일 | 레이트 리밋 |
|---|---|---|
POST /api/estimate |
app/routes/estimate.py:56 |
— |
POST /api/magnet-signup |
app/routes/magnet.py:53 |
5회 / 60초 |
POST /api/helpdesk |
app/routes/helpdesk.py:187 |
— |
POST /api/lp/meta-capi |
app/routes/estimate.py:177 |
폼 아님 (CAPI 브리지) |
모든 폼은 reCAPTCHA v3 토큰을 JSON 본문에 담아 보냅니다. FormData 가
아닙니다 — 새 폼을 만들 때 main.js 의 submitForm 을 따라야 합니다.
7. 미결 판단 (오너용)¶
조치를 제안하지 않고 선택지와 근거만 적습니다.
| # | 판단 대상 | 배경 | 시점 |
|---|---|---|---|
| 1 | ✅ 2026-08-14 결정: 철거. 신규 사이트에 만들지 않음. 프로덕션 alembic 복구 불필요 | 종결 | |
| 2 | ✅ 종결 — 마그넷 철거에 포함. 신규 사이트로 이식하지 않음 | 종결 | |
| 3 | LP v1 5개 lp_form_legacy 개별화 |
컷오버로 사라질 페이지면 불필요 | 컷오버 전 |
| 4 | ✅ 종결 — 마그넷 철거로 소멸. 다만 **서버 파서의 마그넷 분해 로직은 유지**한다(레거시가 컷오버까지 계속 보냄) | 종결 | |
| 5 | ✅ 2026-08-14 결정: L2 만 승계. L3 전화 폴백 미승계(성과 2건), L4 는 마그넷 철거로 소멸. 억제 규칙 4키→2키, 12시간 자격 규칙 소멸 | 종결 | |
| 6 | popup-cta.js 주석/코드 불일치 |
주석 60%/60초, 실제 40%/10초. 둘 중 어느 쪽이 의도인지 확인 필요 | 아무 때나 |
8. 재확인 방법¶
이 문서의 수치를 다시 검증할 때 씁니다.
cd landing-page-modernblinds
# 게시 페이지 수
for c in popup_cta popup_magnet magnet_optin_widget lp_form; do
echo -n "$c: "; grep -rl "components/$c.html" app/templates | wc -l
done
# form_source 하드코딩 값 개수 (59 이어야 함)
grep -rhoE "form_source[\"' ]*[:=][\"' ]*[a-z0-9_-]+" app/templates \
| sed 's/.*[:=]//' | tr -d "\"' " | grep -v '^form_source$' | sort -u | wc -l
# 마그넷 장애가 여전한가 — 0033 이면 미해결
ssh infra-production "PGPASSWORD='<fastapi>' psql -h <rds> -U fastapi \
-d fastapi_db -c 'SELECT version_num FROM alembic_version'"
# 프로덕션이 form_source 를 저장하는가 — 0 이면 미측정
ssh infra-production "PGPASSWORD='<odoo>' psql -h <rds> -U austinjdx -d production \
-c \"SELECT count(*) FROM crm_lead WHERE description ILIKE '%form%source%'\""
자격 증명은 프로젝트 CLAUDE.md 의 Credentials 절을 참조하십시오.
9. 이 조사가 낳은 것¶
이 문서의 진단은 MB-WO-05 로 실행에 옮겨졌습니다.
docs/handoff/MB-WO-05-lead-capture-architecture.md
신규 Next.js 사이트의 캡처 영역을 한 파일에서 관리하는 살아 있는 지시서이며, frontend → FastAPI → Odoo 체인을 계층별 소유와 함께 명시합니다. 위 §7 의 결정 4건이 거기에 반영돼 있습니다.
🔴 부수 효과 하나 — x_form_magnet_popup(2026-08-12 추가)은 마그넷 철거로
값을 가질 수 있는 리드가 없어졌습니다. 신규 사이트는 마그넷이 없고, 레거시
리드는 프로덕션이 form_source 를 저장하지 않아 못 채웁니다. 아직 스테이징에만
있어 제거 비용은 0이며, 판단은 열려 있습니다.
10. 관련 문서¶
docs/MB_Landingpage_status.md— 컷오버 의존 관계 중앙 상태판docs/work-orders/WO-001-attribution-persistence.md— 스테이징에만 있는 구조화 필드 8개docs/handoff/MB-REVIEW-2026-08-09-wo-realignment.md— 신규 사이트와의 계약 (§10 마그넷 팝업 필드)