diff --git a/imweb/README.md b/imweb/README.md
index 1a9553e..b22bcc5 100644
--- a/imweb/README.md
+++ b/imweb/README.md
@@ -79,8 +79,9 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls
- 종류 [전체 · 무료 신청 · 유료 구매] 로 목록을 거르고, 위쪽 표에서 상품별·업체별 신청 수·구매 수를 함께 본다
- 구매 수는 결제완료만 센다 — 환불·취소된 구매는 목록에 '환불·취소'로 남고, 표에는 '환불 N'으로 따로 적는다
-- 유입 집계: '신청 때 유입'은 무료 신청에만 있다 (구매 시점 유입은 아임웹 주문에 남지 않는다).
- 구매는 '가입 경로'(회원이 가입할 때 기록된 경로)로만 볼 수 있고, 비회원 구매는 값이 없다
+- 유입 집계: '신청·구매 때 유입'(신청 완료·주문완료 화면이 남긴 그 순간의 경로)과
+ '가입 경로'(회원이 가입할 때 기록된 경로) 두 기준으로 본다.
+ 주문완료 화면 연동 전 구매는 구매 때 유입이 없고, 비회원 구매는 가입 경로가 없다
## 강의 신청 기록 (신청 완료 화면 → 신청·구매 내역)
@@ -120,7 +121,29 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls
- 서버가 뜬 뒤 첫 실행은 **최근 1년**, 그 뒤로는 **최근 60일**만 다시 본다 (60일보다 오래된 주문의 환불은 반영되지 않는다)
- 사은품 품목은 넣지 않는다. 0원 주문도 결제완료면 구매로 남는다
- 금액은 관리하지 않는다
-- 구매 때 유입경로는 아직 없다 — 현황·다음 작업은 [`docs/purchase-tracking.md`](../docs/purchase-tracking.md)
+- 구매 때 유입은 아래 '구매 때 유입' 절 — 현황·한계는 [`docs/purchase-tracking.md`](../docs/purchase-tracking.md)
+
+## 구매 때 유입 (주문완료 화면 → 구매 기록)
+
+아임웹 주문에는 손님이 어디서 왔는지가 없어서, Header Code(`imweb-header-top.html` 의 '구매 때 유입 전송')가
+주문완료 화면(`/shop_payment_complete?order_no=주문번호`)에서 `POST /api/order-inflows` 로 보낸다.
+
+```
+주문완료 화면 ── { orderNo, inflow: MG_UTM.peek().value, partnerRefs: MG_PARTNER.refs() }
+ │ POST /api/order-inflows (공개 · 1분 10회 · 항상 204 · 같은 주문번호는 처음 값만)
+ ▼
+order_inflows ── 주문 동기화(1시간마다)가 같은 주문번호의 구매에 붙인다
+ ├─▶ purchases.inflow_* 구매 때 유입 (비어 있는 구매에만 — 한 번 붙으면 바뀌지 않는다)
+ └─▶ partner_conversions 업체 실적 — 회원 클릭 연결 + 주문완료 클릭 ID 중 마지막 클릭 (비회원도)
+```
+
+- 주소의 `order_no` 가 아임웹 API 의 `orderNo` 와 같은 값이다 (관리자 화면에서 확인함)
+- 유입 기준은 무료 신청과 같다 — 처음 들어온 경로(2일 보관), 단 업체 링크로 오면 그 업체가 우선
+- 결제창(카카오페이·네이버페이·카드사)에서 돌아올 때의 리퍼러는 유입으로 치지 않는다 (`/shop_payment*` 경로 제외, v1.3)
+- 무통장입금은 주문완료 때 받아 두었다가 입금이 확인돼 구매로 잡힐 때 붙는다
+- 90일 동안 주문과 이어지지 않은 값(없는 주문번호·끝내 결제되지 않은 주문)은 동기화가 지운다
+- 한계: 다른 기기에서 결제하거나 스크립트가 막힌 브라우저면 남지 않는다. 공개 API 라 주문번호를 아는 사람은
+ 먼저 가짜 값을 보낼 수 있다 (실제 주문에만 붙고, 업체 실적은 실제로 있는 클릭만 인정한다)
## 업체 링크 (업체별 신청·구매 추적)
@@ -141,7 +164,7 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls
│ ② 로그인 회원이 확인되면 "이 회원이 이 클릭으로 들어왔다"를 POST /api/partner-touches 로 알린다
▼
신청: 신청 완료 위젯이 신청 기록에 클릭 ID 를 함께 보낸다 → 바로 귀속
-구매: 구매 기록과 같은 주문 동기화(1시간마다)가 회원 주문의 결제완료 상품을 귀속, 환불·취소는 집계에서 뺀다
+구매: 구매 기록과 같은 주문 동기화(1시간마다)가 결제완료 상품을 귀속 (회원은 클릭 연결, 비회원은 주문완료 화면의 클릭 ID), 환불·취소는 집계에서 뺀다
```
- **귀속 규칙**: 신청·구매 직전 30일 안에 누른 **그 상품의** 업체 링크 중 마지막 것 (마지막 클릭)
@@ -157,7 +180,7 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls
사이트 주소는 `IMWEB_SITE_URL`, 비우면 `IMWEB_ADMIN_PAGE_URL` 의 origin
- 한계
- 링크를 누른 브라우저와 신청·구매한 브라우저가 다르면(휴대폰 → PC) 추적되지 않는다
- - 비회원 주문은 회원과 이을 수 없어 세지 않는다
+ - 비회원 주문은 주문완료 화면이 클릭 ID 를 보냈을 때만 센다 (위 '구매 때 유입')
- 공개 페이지에서 오는 기록이라 위조를 완전히 막을 수는 없다 (실제 아임웹 회원·실제 클릭인지만 확인)
- 60일보다 오래된 구매의 환불은 반영되지 않는다
diff --git a/imweb/imweb-header-top.html b/imweb/imweb-header-top.html
index 7d96044..1cd62be 100644
--- a/imweb/imweb-header-top.html
+++ b/imweb/imweb-header-top.html
@@ -28,8 +28,9 @@
+
\ No newline at end of file
diff --git a/imweb/imweb-members.html b/imweb/imweb-members.html
index cfc89f0..08f3f26 100644
--- a/imweb/imweb-members.html
+++ b/imweb/imweb-members.html
@@ -476,10 +476,10 @@
-
신청 유입 상위
- 최근 30일 · 신청 때 유입 기준
+
신청·구매 유입 상위
+ 최근 30일 · 신청·구매 때 유입 기준
-
+
불러오는 중…
@@ -576,7 +576,7 @@
-
+
@@ -597,7 +597,7 @@
일시
구분
이름
연락처
상품
업체
카톡 버튼
-
신청 때 유입
가입 경로
+
신청·구매 때 유입
가입 경로
@@ -1524,7 +1524,7 @@
$('[data-role="ov-app-bars"]').addEventListener('click', function (e) {
var bar = closest(e.target, '.mgm-bar');
if (!bar || bar.disabled) return;
- openApplications({ range: '30', type: 'application', inflowFilter: { basis: 'apply', field: 'source', value: bar.getAttribute('data-key') } });
+ openApplications({ range: '30', type: 'all', inflowFilter: { basis: 'apply', field: 'source', value: bar.getAttribute('data-key') } });
});
$('[data-role="ov-join-bars"]').addEventListener('click', function (e) {
var bar = closest(e.target, '.mgm-bar');
@@ -1542,7 +1542,7 @@
/* ===================== 신청·구매 내역 ===================== */
var appForm = $('[data-role="app-filter"]');
- var APP_BASIS_LABEL = { apply: '신청 때 유입', join: '가입 경로' };
+ var APP_BASIS_LABEL = { apply: '신청·구매 때 유입', join: '가입 경로' };
var APP_GROUP_LABEL = { source: '유입원', medium: '매체', campaign: '캠페인', content: '콘텐츠' };
var APP_TYPE_LABEL = { application: '무료 신청', purchase: '유료 구매' };
@@ -1639,15 +1639,9 @@
}
function countCell(n) { return n ? '' + num(n) + '' : '0'; }
- // 유입 집계 — 구매에는 '신청 때 유입'이 없으므로 유료 구매만 볼 때는 가입 경로로 고정한다
+ // 유입 집계 기준 버튼 — 신청·구매 모두 두 기준으로 볼 수 있다
function syncBasisButtons() {
- var app = state.app, onlyPurchase = app.type === 'purchase';
- if (onlyPurchase && app.basis === 'apply') app.basis = 'join';
- each($all('[data-app-basis]'), function (b) {
- var isApply = b.getAttribute('data-app-basis') === 'apply';
- b.disabled = onlyPurchase && isApply;
- });
- setSeg('[data-app-basis]', 'data-app-basis', app.basis);
+ setSeg('[data-app-basis]', 'data-app-basis', state.app.basis);
}
function loadAppStats() {
@@ -1662,9 +1656,8 @@
$('[data-role="app-stats-bars"]').innerHTML = barsHtml(s.items.slice(0, 10), true, selected, '건');
var counted = [];
if (app.type !== 'purchase') counted.push('무료 신청 ' + num(s.applicationTotal) + '건');
- if (s.basis === 'join' && app.type !== 'application') counted.push('유료 구매 ' + num(s.purchaseTotal) + '건');
- $('[data-role="app-stats-desc"]').textContent = counted.join(' · ') + ' · ' + APP_BASIS_LABEL[s.basis] + ' 기준' +
- (s.basis === 'apply' && app.type === 'all' ? ' (구매는 가입 경로로만 볼 수 있습니다)' : '') + ' · 막대를 누르면 목록을 거릅니다';
+ if (app.type !== 'application') counted.push('유료 구매 ' + num(s.purchaseTotal) + '건');
+ $('[data-role="app-stats-desc"]').textContent = counted.join(' · ') + ' · ' + APP_BASIS_LABEL[s.basis] + ' 기준 · 막대를 누르면 목록을 거릅니다';
}).catch(function (err) { if (seq === app.statsSeq) errorNotice(err); });
}
@@ -1695,7 +1688,7 @@
'