generated from ttipo/project-base
feat(imweb): 주문완료 화면에서 구매 때 유입 전송·관리자 화면에 표시
- Header Code '구매 때 유입 전송': /shop_payment_complete 에서 order_no·유입·업체 클릭 ID 를 POST /api/order-inflows 로 한 번 보낸다 (같은 탭 새로고침 때는 다시 보내지 않는다) - 결제 페이지(/shop_payment*)에서는 리퍼러를 유입으로 기록하지 않는다 (v1.3) — 결제창에서 돌아올 때 pay.kakao.com 이 '카카오 소셜', pay.naver.com 이 '네이버 검색' 유입으로 잡혀 구매 때 유입을 가릴 수 있었다 - 관리자 화면: 유입 기준을 '신청·구매 때 유입'으로, 구매 행·상세 패널에 구매 때 유입, 업체 실적 상세의 비회원 구매는 주문자 이름으로 보여준다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-23
@@ -476,10 +476,10 @@
|
||||
<div class="mgm-grid-2">
|
||||
<section class="mgm-card">
|
||||
<div class="mgm-card-head">
|
||||
<h3>신청 유입 상위</h3>
|
||||
<span class="mgm-muted">최근 30일 · 신청 때 유입 기준</span>
|
||||
<h3>신청·구매 유입 상위</h3>
|
||||
<span class="mgm-muted">최근 30일 · 신청·구매 때 유입 기준</span>
|
||||
<span class="mgm-spacer"></span>
|
||||
<button type="button" class="mgm-link" data-goto="applications" data-app-preset="30" data-app-kind="application">신청·구매 →</button>
|
||||
<button type="button" class="mgm-link" data-goto="applications" data-app-preset="30">신청·구매 →</button>
|
||||
</div>
|
||||
<div class="mgm-bars" data-role="ov-app-bars"><div class="mgm-muted">불러오는 중…</div></div>
|
||||
</section>
|
||||
@@ -576,7 +576,7 @@
|
||||
</div>
|
||||
<div class="mgm-row" style="margin-bottom:12px">
|
||||
<div class="mgm-seg" role="group" aria-label="유입 기준">
|
||||
<button type="button" data-app-basis="apply" class="is-active" title="신청 시점에 브라우저에 남아 있던 유입경로 (UTM 스크립트 2일 보관) — 무료 신청에만 있습니다">신청 때 유입</button>
|
||||
<button type="button" data-app-basis="apply" class="is-active" title="신청 완료·주문완료 화면이 남긴 그 순간의 유입경로 (UTM 스크립트 2일 보관, 업체 링크 우선) — 주문완료 화면 연동 전 구매는 값 없음">신청·구매 때 유입</button>
|
||||
<button type="button" data-app-basis="join" title="신청·구매한 회원이 가입할 때 기록된 경로 (가입 후 채워진 값은 제외, 비회원 구매는 값 없음)">가입 경로</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -597,7 +597,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>일시</th><th>구분</th><th>이름</th><th>연락처</th><th>상품</th><th>업체</th><th title="신청 완료 화면의 오픈카톡 버튼을 누른 수 — 실제 입장 여부는 카카오가 알려주지 않아 알 수 없습니다">카톡 버튼</th>
|
||||
<th title="신청 시점에 브라우저에 남아 있던 유입경로 (UTM 스크립트 2일 보관) — 구매에는 이 기록이 없습니다">신청 때 유입</th><th>가입 경로</th>
|
||||
<th title="신청 완료·주문완료 화면이 남긴 그 순간의 유입경로 (UTM 스크립트 2일 보관, 업체 링크 우선) — 주문완료 화면 연동 전 구매는 값 없음">신청·구매 때 유입</th><th>가입 경로</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-role="app-rows"></tbody>
|
||||
@@ -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 ? '<b>' + num(n) + '</b>' : '<span class="mgm-faint">0</span>'; }
|
||||
|
||||
// 유입 집계 — 구매에는 '신청 때 유입'이 없으므로 유료 구매만 볼 때는 가입 경로로 고정한다
|
||||
// 유입 집계 기준 버튼 — 신청·구매 모두 두 기준으로 볼 수 있다
|
||||
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 @@
|
||||
'<td>' + (isApp && a.kakaoClickedAt
|
||||
? '<span class="mgm-badge is-ok" title="' + esc(kstDateTime(a.kakaoClickedAt)) + ' 에 버튼을 눌렀습니다 (실제 입장 여부는 알 수 없음)">클릭</span>'
|
||||
: '<span class="mgm-faint">-</span>') + '</td>' +
|
||||
'<td class="mgm-wrap">' + (isApp ? inflowText(a.inflow) : '<span class="mgm-faint">-</span>') + '</td>' +
|
||||
'<td class="mgm-wrap">' + inflowText(a.inflow) + '</td>' +
|
||||
'<td class="mgm-wrap">' + (!a.member && !a.memberUid ? '<span class="mgm-faint">비회원</span>' : joinInflowCell(a.member)) + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
@@ -1773,6 +1766,12 @@
|
||||
['연락처', dash(phone(o.phone))],
|
||||
['회원 여부', isGuest ? '비회원 주문' : '회원 주문']
|
||||
]) + '</section>' +
|
||||
'<section class="mgm-dsec"><h4>구매 때 유입</h4>' + (a.inflow && a.inflow.source ? dlHtml([
|
||||
['유입원', esc(a.inflow.source)],
|
||||
['매체', esc(a.inflow.medium)],
|
||||
['캠페인', esc(a.inflow.campaign)],
|
||||
['콘텐츠', esc(a.inflow.content)]
|
||||
]) : '<p class="mgm-muted">기록 없음 — 주문완료 화면 연동 전 주문이거나, 다른 기기·브라우저에서 결제한 경우입니다.</p>') + '</section>' +
|
||||
(isGuest ? '' : memberSectionsHtml(a.member));
|
||||
}
|
||||
openDrawer((activityName(a) || '알 수 없음') + ' 님의 ' + APP_TYPE_LABEL[a.type], html);
|
||||
@@ -1796,9 +1795,6 @@
|
||||
var type = b.getAttribute('data-app-type');
|
||||
if (type === state.app.type) return;
|
||||
state.app.type = type;
|
||||
// 구매에는 신청 때 유입이 없으므로 그 기준의 유입 필터는 풀어 준다
|
||||
var inf = state.app.inflowFilter;
|
||||
if (type === 'purchase' && inf && inf.basis === 'apply') state.app.inflowFilter = null;
|
||||
setSeg('[data-app-type]', 'data-app-type', type);
|
||||
loadApplications(true);
|
||||
});
|
||||
@@ -2028,7 +2024,9 @@
|
||||
'<td>' + (PTN_CONV_TYPE[c.type] || esc(c.type)) +
|
||||
(c.isValid ? '' : ' <span class="mgm-badge is-danger" title="환불·취소되어 집계에서 빠졌습니다">환불·취소</span>') +
|
||||
(c.orderNo ? '<span class="mgm-cell-sub">주문 ' + esc(c.orderNo) + '</span>' : '') + '</td>' +
|
||||
'<td>' + (m ? personCell(m.name, m.uid) : personCell(c.memberCode, '회원 정보 없음')) + '</td>' +
|
||||
'<td>' + (m ? personCell(m.name, m.uid)
|
||||
: c.memberCode ? personCell(c.memberCode, '회원 정보 없음')
|
||||
: personCell(c.ordererName, '비회원 구매')) + '</td>' +
|
||||
'<td>' + dash(phone(m && m.callnum)) + '</td>' +
|
||||
'<td><span class="mgm-cell-sub" style="margin:0">' + esc(kstShort(c.touchedAt)) + '</span></td>' +
|
||||
'</tr>';
|
||||
|
||||
Reference in New Issue
Block a user