From e91f7620a3be0155af8208882c0f9085463fe907 Mon Sep 17 00:00:00 2001 From: TtiPo Date: Tue, 6 Oct 2026 00:11:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=97=85=EC=B2=B4=20=EB=A7=81=ED=81=AC?= =?UTF-8?q?=20=EC=B6=94=EC=A0=81=EC=9D=84=20=EC=8B=A4=EC=A0=9C=20=EC=95=84?= =?UTF-8?q?=EC=9E=84=EC=9B=B9=20Header=20Code=20=EB=A1=9C=20=ED=95=A9?= =?UTF-8?q?=EC=B9=98=EA=B3=A0=20=EC=8B=A0=EC=B2=AD=20=EC=9C=A0=EC=9E=85?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - imweb-header-top.html·imweb-header.html: 아임웹에 실제로 들어가 있는 Header Code 원본을 저장소에 둔다 - 업체 링크 추적을 imweb-header-top.html 의 UTM 스크립트 아래로 옮기고 partner-ref.html 은 삭제 (UTM 스크립트처럼 localStorage + .moneyguider.co.kr 쿠키에 저장 — www ↔ apex 이동·인앱브라우저 대비) - 신청 완료 위젯: MG_UTM.peek() 는 객체라 문자열 검사에 걸려 유입정보가 빠지고 있었다 — peek().value 를 보낸다 - 단축 링크 리다이렉트에 Referrer-Policy strict-origin-when-cross-origin (helmet 기본 no-referrer 때문에 업체가 링크를 올린 곳이 지워져 유입원이 direct 로 잡혔다) Co-Authored-By: Claude Opus 5.5 --- .../applications/application.entity.ts | 2 +- .../dto/create-application.dto.ts | 6 +- backend/src/modules/imweb/imweb.config.ts | 2 +- .../partners/partner-public.controller.ts | 9 +- backend/src/shared/utils/inflow.spec.ts | 2 +- backend/src/shared/utils/inflow.ts | 2 +- backend/src/shared/utils/partner-link.ts | 4 +- imweb/README.md | 24 +- imweb/imweb-free-apply.html | 11 +- imweb/imweb-header-top.html | 622 ++++++++++++++++++ imweb/imweb-header.html | 48 ++ imweb/partner-ref.html | 114 ---- 12 files changed, 713 insertions(+), 133 deletions(-) create mode 100644 imweb/imweb-header-top.html create mode 100644 imweb/imweb-header.html delete mode 100644 imweb/partner-ref.html diff --git a/backend/src/modules/applications/application.entity.ts b/backend/src/modules/applications/application.entity.ts index ae2f04a..acba0bd 100644 --- a/backend/src/modules/applications/application.entity.ts +++ b/backend/src/modules/applications/application.entity.ts @@ -47,7 +47,7 @@ export class MemberApplication { @Column({ name: 'kakao_clicked_at', type: 'timestamptz', nullable: true }) kakaoClickedAt!: Date | null; - /** 신청 순간의 유입정보 원본 (header-top.html 이 저장해 둔 값, 파이프 형식) */ + /** 신청 순간의 유입정보 원본 (imweb-header-top.html 이 저장해 둔 값, 파이프 형식) */ @Column({ name: 'inflow_raw', type: 'text', nullable: true }) inflowRaw!: string | null; diff --git a/backend/src/modules/applications/dto/create-application.dto.ts b/backend/src/modules/applications/dto/create-application.dto.ts index b928cc3..b09713a 100644 --- a/backend/src/modules/applications/dto/create-application.dto.ts +++ b/backend/src/modules/applications/dto/create-application.dto.ts @@ -14,7 +14,7 @@ import { CLICK_REF_PATTERN } from '@/shared/utils/partner-link'; const MEMBER_ID_MAX_LENGTH = 255; const PRODUCT_NAME_MAX_LENGTH = 200; -/** header-top.html 이 만드는 값은 200자 남짓 — 넉넉히 두되 무한정 받지 않는다 */ +/** imweb-header-top.html 이 만드는 값은 200자 남짓 — 넉넉히 두되 무한정 받지 않는다 */ const INFLOW_RAW_MAX_LENGTH = 1000; /** 신청 완료 화면이 보내는 신청 기록 */ @@ -44,7 +44,7 @@ export class CreateApplicationDto { @ApiPropertyOptional({ description: - '신청 순간의 유입정보 (header-top.html 형식: src=..|med=..|cmp=..)', + '신청 순간의 유입정보 (imweb-header-top.html 형식: src=..|med=..|cmp=..)', example: 'src=facebook|med=paid|cmp=fall|jd=2026-10-05', }) @IsOptional() @@ -54,7 +54,7 @@ export class CreateApplicationDto { @ApiPropertyOptional({ description: - '브라우저가 보관한 업체 링크 클릭 ID 목록 (imweb/partner-ref.html 의 MG_PARTNER.refs())', + '브라우저가 보관한 업체 링크 클릭 ID 목록 (imweb/imweb-header-top.html 의 MG_PARTNER.refs())', example: ['AbCd1234EfGh5678'], type: [String], }) diff --git a/backend/src/modules/imweb/imweb.config.ts b/backend/src/modules/imweb/imweb.config.ts index 6bb5c9e..6e1b071 100644 --- a/backend/src/modules/imweb/imweb.config.ts +++ b/backend/src/modules/imweb/imweb.config.ts @@ -39,7 +39,7 @@ export const IMWEB_CALLBACK_PATH = '/imweb/oauth/callback'; const DEFAULT_SYNC_INTERVAL_MINUTES = 60; const DEFAULT_PAGE_SIZE = 100; -/** header-top.html 이 홈페이지 항목에 쓰는 가짜 호스트 ('https://utm.' + OWN_HOST) */ +/** imweb-header-top.html 이 홈페이지 항목에 쓰는 가짜 호스트 ('https://utm.' + OWN_HOST) */ export const DEFAULT_TRACKING_HOST = 'utm.moneyguider.co.kr'; /** diff --git a/backend/src/modules/partners/partner-public.controller.ts b/backend/src/modules/partners/partner-public.controller.ts index 642bc6f..7ba9e85 100644 --- a/backend/src/modules/partners/partner-public.controller.ts +++ b/backend/src/modules/partners/partner-public.controller.ts @@ -19,6 +19,10 @@ import { CreatePartnerTouchDto } from './dto/partner-touch.dto'; import { PartnerAttributionService } from './partner-attribution.service'; import { PartnerLinksService, SHORT_LINK_PATH } from './partner-links.service'; +const REFERRER_POLICY_HEADER = 'Referrer-Policy'; +/** 브라우저 기본값 — 다른 사이트로는 출처(도메인)만 보낸다 */ +const REDIRECT_REFERRER_POLICY = 'strict-origin-when-cross-origin'; + /** * 업체 단축 링크 — moneyguide.wandergo.net/r/{코드} (main.ts 에서 /api prefix 제외). * @@ -46,11 +50,14 @@ export class PartnerRedirectController { ); // 매 클릭을 서버가 받아야 클릭 ID 가 새로 발급된다 — 브라우저·프록시가 저장하지 않게 한다 res.setHeader('Cache-Control', 'no-store'); + // helmet 기본값(no-referrer)이 리다이렉트에 붙으면 업체가 링크를 올린 곳(블로그 등)이 지워져 + // 상품 페이지의 UTM 스크립트가 유입원을 direct 로 기록한다 — 출처(도메인)만은 넘긴다 + res.setHeader(REFERRER_POLICY_HEADER, REDIRECT_REFERRER_POLICY); res.redirect(HttpStatus.FOUND, target); } } -/** 아임웹 Header Code 스크립트(imweb/partner-ref.html)가 회원 ↔ 링크 클릭 연결을 보낸다 */ +/** 아임웹 Header Code(imweb/imweb-header-top.html 의 업체 링크 추적)가 회원 ↔ 링크 클릭 연결을 보낸다 */ @ApiTags('Partner Touches') @Controller('partner-touches') export class PartnerTouchesController { diff --git a/backend/src/shared/utils/inflow.spec.ts b/backend/src/shared/utils/inflow.spec.ts index 7e49e7e..5799dc4 100644 --- a/backend/src/shared/utils/inflow.spec.ts +++ b/backend/src/shared/utils/inflow.spec.ts @@ -2,7 +2,7 @@ import { InflowStatus, parseInflow } from './inflow'; const HOST = 'utm.moneyguider.co.kr'; -/** header-top.html 의 serialize(asUrl) 와 같은 방식으로 홈페이지 값을 만든다 */ +/** imweb-header-top.html 의 serialize(asUrl) 와 같은 방식으로 홈페이지 값을 만든다 */ function homePage(params: Record): string { const query = Object.entries(params) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) diff --git a/backend/src/shared/utils/inflow.ts b/backend/src/shared/utils/inflow.ts index 4907846..0465337 100644 --- a/backend/src/shared/utils/inflow.ts +++ b/backend/src/shared/utils/inflow.ts @@ -1,7 +1,7 @@ /** * 회원 유입경로 파싱 유틸 (순수 함수). * - * 아임웹 Header Code 의 UTM 스크립트(header-top.html)가 회원가입 폼의 '홈페이지' 항목에 + * 아임웹 Header Code 의 UTM 스크립트(imweb/imweb-header-top.html)가 회원가입 폼의 '홈페이지' 항목에 * 아래 형식으로 유입정보를 기록한다. 아임웹 Open API 회원 조회에는 사용자 정의 가입 항목이 * 나오지 않고 homePage 만 나오기 때문에 이 값을 풀어서 쓴다. * diff --git a/backend/src/shared/utils/partner-link.ts b/backend/src/shared/utils/partner-link.ts index cc52ff3..1a6d5c5 100644 --- a/backend/src/shared/utils/partner-link.ts +++ b/backend/src/shared/utils/partner-link.ts @@ -2,11 +2,11 @@ * 업체 링크 유틸 (순수 함수) — 이동 주소 검증, 클릭 ID 부착, 봇 판별. * * 업체에 주는 단축 링크(/r/{코드})를 누르면 서버가 클릭마다 고유 ID 를 발급해 - * 상품 페이지 주소에 `mg_ref` 로 붙여 보낸다. 아임웹 Header Code 스크립트(imweb/partner-ref.html)가 + * 상품 페이지 주소에 `mg_ref` 로 붙여 보낸다. 아임웹 Header Code 스크립트(imweb/imweb-header-top.html 의 업체 링크 추적)가 * 이 값을 브라우저에 보관했다가, 로그인한 회원이 확인되면 서버에 알려 회원과 클릭을 잇는다. */ -/** 상품 페이지 주소에 붙이는 클릭 ID 파라미터 — imweb/partner-ref.html 과 같아야 한다 */ +/** 상품 페이지 주소에 붙이는 클릭 ID 파라미터 — imweb/imweb-header-top.html 과 같아야 한다 */ export const PARTNER_REF_PARAM = 'mg_ref'; /** 업체 링크 코드 — /r/{코드} */ diff --git a/imweb/README.md b/imweb/README.md index 687dd92..a707553 100644 --- a/imweb/README.md +++ b/imweb/README.md @@ -12,10 +12,22 @@ 아임웹 Open API /member-info/members ← homePage 에 UTM 스크립트가 기록한 유입정보 ``` -유입정보는 아임웹 Header Code 의 UTM 스크립트(`header-top.html`)가 회원가입 폼의 +유입정보는 아임웹 Header Code 의 UTM 스크립트(`imweb-header-top.html`)가 회원가입 폼의 '홈페이지' 항목에 `https://utm.moneyguider.co.kr/?src=..&med=..&jd=..` 형식으로 남긴 값이다. 백엔드가 이 값을 풀어서 유입원·매체·캠페인별로 조회·집계한다. +## 파일 — 아임웹 어디에 붙이나 + +아임웹에 실제로 들어가 있는 코드의 원본이다. 고친 뒤에는 아임웹에 **다시 붙여 넣어야** 반영된다. + +| 파일 | 아임웹 위치 | 하는 일 | +| --- | --- | --- | +| `imweb-header-top.html` | Header Code (사이트 전체) | UTM 유입경로 기록(`MG_UTM`) + 업체 링크 클릭 보관(`MG_PARTNER`) | +| `imweb-header.html` | Header Code (사이트 전체) | 로그인 링크의 back_url 에 UTM 을 붙여 로그인 후에도 유지 | +| `imweb-free-apply.html` | `/apply` 페이지 HTML 위젯 | 신청 완료 화면 + 신청 기록·오픈카톡 클릭 전송 | +| `imweb-members.html` | 관리자 전용 페이지 코드 위젯 | 관리자 대시보드 | +| `serve-local.js` | — | 로컬 테스트용 정적 서버 | + ## 로컬에서 띄우기 1. 백엔드 env 에 아임웹 앱 키와 관리자 계정 입력 — `backend/.env.development` @@ -65,7 +77,7 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls 아임웹 `/apply` 페이지의 신청 완료 위젯(`imweb-free-apply.html`)이 표시될 때 `POST /api/applications` 로 신청을 남긴다. 관리자 화면 **신청 내역** 메뉴에서 본다. -- 보내는 값: `MEMBER_UID`(회원 아이디 또는 회원 코드), 상품번호(`idx`), 상품명, 그 순간의 유입정보(`MG_UTM.peek()`), 업체 링크 클릭 ID(`MG_PARTNER.refs()`) +- 보내는 값: `MEMBER_UID`(회원 아이디 또는 회원 코드), 상품번호(`idx`), 상품명, 그 순간의 유입정보(`MG_UTM.peek().value` — peek() 는 객체를 돌려준다), 업체 링크 클릭 ID(`MG_PARTNER.refs()`) - 백엔드는 아임웹에 실제로 있는 회원인지 확인하고(동기화 전 신규 가입자는 아임웹 API 로 직접 확인), 회원·상품당 한 건만 남긴다. 이름·연락처는 브라우저 값이 아니라 아임웹 회원 정보를 쓴다 - 오픈카톡 버튼 클릭은 `POST /api/applications/{id}/kakao-clicks` 로 처음 한 번만 남는다 @@ -83,7 +95,8 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls 업체에 링크 전달 ── https://moneyguide.wandergo.net/r/{코드} │ ① 클릭 기록 (카톡 미리보기·검색봇 제외) 후 상품 페이지로 이동하며 클릭 ID(mg_ref)를 붙인다 ▼ -상품 페이지 ── partner-ref.html(Header Code)이 클릭 ID 를 30일 보관하고 주소창에서 지운다 +상품 페이지 ── imweb-header-top.html(Header Code)의 업체 링크 추적이 클릭 ID 를 30일 보관하고 주소창에서 지운다 + │ (UTM 스크립트처럼 localStorage + .moneyguider.co.kr 쿠키 — www ↔ apex 이동에도 유지) │ ② 로그인 회원이 확인되면 "이 회원이 이 클릭으로 들어왔다"를 POST /api/partner-touches 로 알린다 ▼ 신청: 신청 완료 위젯이 신청 기록에 클릭 ID 를 함께 보낸다 → 바로 귀속 @@ -108,7 +121,8 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls 2. 백엔드 배포 (마이그레이션 자동 실행) 3. 관리자 화면 **연동 관리 → 아임웹 연결**을 다시 한다 — 새 권한이 담긴 토큰을 받는다. 다시 연결하기 전에는 상품 불러오기·구매 집계가 `IMWEB_006`(권한 부족)으로 실패한다. 회원 동기화는 그대로 돈다 -4. `partner-ref.html` 을 아임웹 **Header Code** 에 붙여 넣는다 (사이트 전체) +4. `imweb-header-top.html` 을 아임웹 **Header Code** 에 다시 붙여 넣는다 (UTM 스크립트 아래에 업체 링크 추적이 추가됨). + `imweb-header.html`(로그인 링크 UTM 보존)은 바꾸지 않았다 5. `imweb-free-apply.html` 을 신청 완료 위젯에 다시 붙여 넣는다 (신청 기록·클릭 ID 전송 추가됨) 6. 업체 링크 메뉴에서 **상품 불러오기** → 업체 추가 → 링크 발급 @@ -147,7 +161,7 @@ docker exec -u root moneyguide-backend rm /tmp/members-1.xls - `imweb-members.html` 의 `API_BASE` 운영 주소(`https://moneyguide.wandergo.net/api`)를 확인하고 아임웹 코드 위젯에 붙여 넣는다 - 아임웹에서 그 페이지의 접근 권한을 관리자로 제한한다 - 업체 링크를 쓰려면 위 '업체 링크 → 처음 켤 때' 순서를 따른다 - (`partner-ref.html`·`imweb-free-apply.html` 의 `API_BASE` 도 같은 운영 주소) + (`imweb-header-top.html`·`imweb-free-apply.html` 의 `API_BASE` 도 같은 운영 주소) ## 유입 기록 상태 diff --git a/imweb/imweb-free-apply.html b/imweb/imweb-free-apply.html index 40d99ee..1a0bbeb 100644 --- a/imweb/imweb-free-apply.html +++ b/imweb/imweb-free-apply.html @@ -434,16 +434,19 @@ })(); } - /* header-top.html(UTM 스크립트)이 보관한 유입정보 — 없으면 빈 값 */ + /* imweb-header-top.html(UTM 스크립트)이 보관한 유입정보 — 없으면 빈 값. + MG_UTM.peek() 는 { first, last, value, … } 객체를 돌려주고, + 서버가 받는 파이프 형식(src=..|med=..|…)은 그중 value 다. */ function readInflow() { try { - var value = window.MG_UTM && typeof window.MG_UTM.peek === 'function' - ? window.MG_UTM.peek() : ''; + var peeked = window.MG_UTM && typeof window.MG_UTM.peek === 'function' + ? window.MG_UTM.peek() : null; + var value = peeked && typeof peeked === 'object' ? peeked.value : peeked; return typeof value === 'string' ? value.slice(0, INFLOW_MAX) : ''; } catch (e) { return ''; } } - /* partner-ref.html(업체 링크 스크립트)이 보관한 클릭 ID — 없으면 빈 배열 */ + /* imweb-header-top.html(업체 링크 추적)이 보관한 클릭 ID — 없으면 빈 배열 */ function readPartnerRefs() { try { var refs = window.MG_PARTNER && typeof window.MG_PARTNER.refs === 'function' diff --git a/imweb/imweb-header-top.html b/imweb/imweb-header-top.html new file mode 100644 index 0000000..dd48589 --- /dev/null +++ b/imweb/imweb-header-top.html @@ -0,0 +1,622 @@ + + + + + + + + \ No newline at end of file diff --git a/imweb/imweb-header.html b/imweb/imweb-header.html new file mode 100644 index 0000000..a0b01f0 --- /dev/null +++ b/imweb/imweb-header.html @@ -0,0 +1,48 @@ + \ No newline at end of file diff --git a/imweb/partner-ref.html b/imweb/partner-ref.html deleted file mode 100644 index 7f9c9f3..0000000 --- a/imweb/partner-ref.html +++ /dev/null @@ -1,114 +0,0 @@ - - - -