에이비제트
경력근거: 인포크 위너리는 브랜드가 광고에 쓸 영상 소재를 크리에이터와 함께 만드는 서비스입니다.
“인포크 위너리는 브랜드가 광고에 쓸 영상 소재를 크리에이터와 함께 만드는 서비스입니다.” 라는 첫 줄이 이 자리의 제품을 정하고, “브랜드가 크리에이터와 함께 Winning Creative를 만드는 과정에서, 목표를 기준으로 고객의 문제를 정의하고, 가설을 검증하며 해결합니다.” 가 그 안에서 프로덕트 엔지니어가 맡을 일을 정합니다. 위너 소재를 고르는 서비스에서 고르는 기준이 조회수 순위로 남으면 큰 계정이 매번 앞서고, 작은 계정이 만든 좋은 소재는 광고비를 받지 못합니다. 이 편은 그 기준을 크리에이터 자신의 지난 릴스로 바꾸는 판정 하나에 집중합니다. 우대사항의 “여러 종류의 고객이 얽힌 복잡한 도메인에서 문제를 풀어 본 분” 과 “React Native와 웹뷰를 함께 쓰는 환경에서 웹-네이티브 연동을 다뤄 본 분” 은 판정을 보는 브랜드와 클린본·광고 코드를 보내야 하는 크리에이터가 갈리는 이 흐름에서, 3주차의 앱 웹뷰와 공유 시트로 이어집니다. 릴스 스냅숏을 어떤 주기로 모으는지, 광고 코드를 어떤 경로로 받는지는 공고에 없어 데이터 모양을 제가 정했습니다. 이 포트폴리오는 스냅숏 보간과 크리에이터별 기준선 계산을 TypeScript 로 짜고, 판정 결과를 React·Next.js 화면의 묶음과 펼침으로 옮기는 프론트엔드 구현을 보여줍니다. 인플루언서 광고 운영이나 인스타그램 광고 집행 전문성은 주장하지 않습니다.
공고는 첫 줄에서 인포크 위너리를 브랜드가 광고에 쓸 영상 소재를 크리에이터와 만드는 서비스로 소개하고, 프로덕트 엔지니어의 일을 Winning Creative 를 만드는 과정의 문제 정의와 가설 해결로 적습니다. 크리에이터가 릴스를 올린 뒤 브랜드에게 남는 일은 광고비를 붙일 릴스 고르기이고, 이 편은 그 선택을 조회수 순위가 아니라 크리에이터마다 다른 평소 성과로 판정합니다.
주요업무가 브랜드와 크리에이터가 쓰는 웹과 모바일 서비스를 React 와 React Native 로 만드는 일이고, 우대사항에 여러 종류의 고객이 얽힌 도메인과 React Native·웹뷰 연동 경험이 있습니다. 판정 화면은 브랜드 마케터와 캠페인 매니저가 보지만 클린본과 광고 코드는 크리에이터가 보내야 들어옵니다. 3주차에는 같은 판정 페이지를 React Native 앱의 웹뷰로 열고, 요청 문안을 웹뷰에서 앱의 공유 시트로 넘겨 크리에이터에게 보냅니다.
자격요건에 TypeScript·React·Next.js 웹 서비스 운영, 렌더링 원리와 성능 문제 해결, 기능이 늘어도 복잡해지지 않는 컴포넌트와 상태 구조가 있습니다. 이 편의 화면은 판정 값 네 개의 순서와 라벨을 설정 하나에서 읽어 묶음을 세우고, 펼친 줄은 URL 에 둬 링크로 공유합니다. 2주차에는 클린본 영상 미리보기를 넣으며 생기는 끊김을 React Profiler 로 원인부터 잡고, 마케터가 실제로 고른 릴스를 이벤트 로그로 남깁니다.
에이비제트의 인포크 위너리 캠페인에서 크리에이터 10~20명이 브랜드 릴스를 올리면, 이 관리자 페이지는 게시 48시간이 지난 릴스마다 48시간 시점의 조회·저장·공유를 앞뒤 스냅숏 사이로 계산하고, 같은 크리에이터의 지난 릴스 12개를 같은 48시간 시점으로 맞춘 중앙값과 견줍니다. 퍼포먼스 마케터는 릴스가 올라오고 이틀째 되는 날 이 화면을 열어, 조회수가 평소의 1.5배 이상이고 저장+공유율도 평소 이상인 위너 소재와 조회수만 크고 저장+공유율이 평소의 70% 에 못 미치는 릴스를 나눠 보고 광고비를 붙일 릴스를 정합니다. 캠페인 매니저는 게시 72시간이 지나는 때 이 화면을 다시 열어, 위너인데 클린본(자막·음악·스티커를 뺀 원본 영상)이나 파트너십 광고 코드(크리에이터가 인스타그램에서 그 릴스의 파트너십 광고를 허용하고 받은 코드)가 아직 없는 릴스에 붙은 요청 문안 초안을 크리에이터에게 보냅니다. 파트너십 광고 집행과 클린본 내려받기는 지금 쓰는 인포크 비즈니스 어드민과 인스타그램에서 하고, 이 화면은 어느 릴스에 광고비를 붙일지 판단할 근거를 보여줍니다.
인포크 위너리로 크리에이터 10~20명에게 릴스를 맡긴 뷰티·리빙 브랜드의 퍼포먼스 마케터와, 그 캠페인을 운영하는 인포크 캠페인 매니저. 릴스가 올라오고 이틀 안에 어떤 소재를 파트너십 광고로 키울지 정해야 하는데, 조회수만 보면 큰 계정 릴스부터 고르게 된다. 판정 화면을 같이 보고 키울 릴스를 정하고, 클린본·광고 코드가 안 온 크리에이터에게 요청한다
마케터가 큰 계정 릴스부터 고르게 되는 까닭은 릴스마다 누적 조회수를 나란히 놓으면 계정 크기가 그대로 순위가 되기 때문입니다. 팔로워 41만 계정의 평소 릴스는 48시간에 38만 회쯤 보이고 팔로워 8천 계정은 1만 3천 회쯤 보여서, 앞 계정이 평소보다 못 나와도 순위는 뒤집히지 않습니다. 릴스마다 올라온 시각도 달라, 같은 날 아침에 본 누적값은 서로 다른 경과 시간의 값입니다. 각 릴스를 게시 48시간 시점으로 맞추고 그 크리에이터의 지난 릴스 중앙값으로 나누면 계정 크기가 지워지고, 보는 사람이 다시 찾으려고 저장하거나 남에게 보낸 수를 같은 방식으로 견주면 조회수만 부풀려진 릴스가 드러납니다. 그러면 마케터는 이틀 안에 작은 계정의 위너 릴스에 광고비를 붙이고, 캠페인 매니저는 그 릴스의 클린본과 광고 코드가 늦어지는 크리에이터에게 먼저 연락합니다.
캠페인 릴스 표와 릴스 스냅숏 표를 올리면 Next.js 관리자 페이지가 릴스마다 48시간 값을 계산하고 크리에이터의 지난 릴스 12개 중앙값과 견줘 네 묶음으로 띄웁니다. 마케터와 캠페인 매니저는 줄을 펼쳐 지난 릴스 12개 사이에 이번 릴스가 어디 찍히는지 보고, 위너인데 클린본·광고 코드가 안 온 릴스는 Claude Haiku 4.5 가 쓴 요청 문안 초안을 복사해 크리에이터에게 보냅니다. 광고 집행과 클린본 내려받기는 지금 쓰는 도구에서 합니다.
48시간 앞뒤 스냅숏, 1.5배와 70% 문턱, 72시간, 지난 릴스 12개 미만, 협찬 릴스 빼기처럼 한 칸 차이로 판정이 바뀌는 사례 32건은 Vitest 로, 묶음 순서와 펼침 링크는 Playwright 로 써서 GitHub Actions 에서 푸시마다 돌립니다. 펼친 줄에서 클린본 영상을 미리 보고, 마케터가 누른 광고로 키움·보류가 선택 기록 화면에 판정 옆으로 쌓입니다.
React Native 앱이 같은 판정 페이지를 웹뷰로 열고, 6시간마다 판정을 다시 돌려 새로 위너가 된 릴스나 72시간을 넘긴 미수신 릴스가 생기면 Expo 푸시를 보냅니다. 캠페인 매니저가 알림을 누르면 웹뷰가 그 릴스의 펼친 줄로 열리고, 요청 문안은 앱의 공유 시트로 넘어가 카카오톡이나 인스타그램 DM 으로 나갑니다.
캠페인 매니저가 두 표를 올리면 48시간이 지난 릴스 6편이 네 묶음으로 나뉩니다. 팔로워 8,200명 계정의 RL-1041-046 은 평소의 3.19배로 위너 소재에 서고, 팔로워 41만 계정의 RL-2207-118 은 조회수 612,000회인데 저장+공유율이 평소의 31% 라 조회수만 큰 릴스로 갑니다. 마케터는 줄을 펼쳐 지난 릴스 12개 사이에 이번 릴스가 찍힌 자리와 48시간 값을 만든 스냅숏 두 개를 읽습니다.
평소의 딱 1.5배인 RL-3012-089 는 위너 소재로 남고, 정확히 48시간째에 찍힌 RL-2519-133 의 스냅숏은 보간 없이 그 값 그대로 판정에 들어갑니다. 49시간째 찍힌 지난 릴스 RL-3012-027 은 48/49 로 줄어 기준선에 들어가고, 지난 협찬 릴스 RL-1041-030 은 기준선에서 빠집니다.
깨진 스냅숏 5종(posted_at·captured_at 을 시각으로 읽지 못한 행, views·saves·shares 가 숫자가 아닌 행, captured_at 이 posted_at 보다 앞선 행, 같은 릴스에서 늦게 찍혔는데 누적 조회수가 앞 스냅숏보다 작은 행, is_campaign 이 참인데 campaign_reels 에 reel_id 가 없는 행)을 섞어 올리면, 그 행들은 계산 전에 빠져 묶음 위 확인 칸에 snap_id 와 종류로 적힙니다. 빠진 행이 캠페인 릴스의 48시간 뒤 스냅숏이었으면 그 릴스만 48시간 값 대기로 내려가고, 지난 릴스 것이었으면 그다음 오래된 지난 릴스가 기준선을 채워 다른 크리에이터의 판정은 바뀌지 않습니다.
문턱 바로 위아래에 둔 사례 32건에서 손으로 붙인 정답과 판정이 같은 건수를 셉니다. 단, 정답과 데이터를 모두 제가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 결과는 더미데이터 자가채점이며, 이 판정으로 고른 릴스가 파트너십 광고에서 실제로 매출을 더 냈는지는 에이비제트의 캠페인 스냅숏과 광고 성과 기록이 있어야 측정됩니다. 이 포트폴리오는 그 측정을 바로 돌릴 수 있는 상태로 만듭니다.
48시간 값과 기준선은 캠페인 릴스를 하루 네 번(03·09·15·21시), 지난 릴스를 게시 48시간째에 한 번 찍는다는 수집 주기 가정 위에서만 계산되고, 그 가정이 틀리면 보간에 쓸 앞뒤 스냅숏부터 달라집니다. 하루 한 번 찍으면 48시간 앞뒤 스냅숏이 24시간씩 벌어져 직선 보간이 실제 곡선과 크게 어긋나고, 지난 릴스 스냅숏이 없으면 기준선이 서지 않습니다. 스냅숏 78행과 사례 32건의 정답도 제가 만들었으니, 통과 수가 말해 주는 것은 보간·중앙값·네 문턱이 코드로 제대로 옮겨졌는가 하나입니다.
문턱에서 멀리 떨어진 릴스만 있으면 '1.5배 이상' 을 '1.5배 초과' 로 잘못 써도 판정표가 똑같이 나옵니다. RL-3012-089 는 30,600 이 20,400 의 딱 1.5배라 부등호를 거꾸로 쓰면 위너 소재에서 키울 근거 없음으로 내려가고, RL-2519-133 은 09:00 스냅숏이 정확히 48시간째라 앞뒤 스냅숏을 고르는 비교에서 등호를 빼면 48시간 값이 비며, CR-1041 의 협찬 릴스 RL-1041-030 을 기준선에 넣으면 중앙값이 12,900 에서 13,250 으로 바뀝니다. RL-4470-003 의 shares 0 도 일부러 넣었습니다.
48시간 조회수가 평소보다 높은 까닭이 소재가 아니라 게시 요일이나 시간대일 수 있습니다. 평소 값은 요일을 가리지 않은 중앙값이라 주말 저녁에 올린 캠페인 릴스가 유리하게 보일 수 있고, 같은 요일 릴스끼리만 견줄지는 실제 스냅숏 분포를 보고 정합니다.
인포크에 크리에이터의 지난 릴스 스냅숏이 쌓여 있지 않다면 기준선은 캠페인 확정 뒤부터 모아야 합니다. 그러면 첫 캠페인에서는 대부분 크리에이터가 CR-4470 처럼 지난 릴스 12개를 못 채워 키울 근거 없음에 남으니, 기준선을 몇 개부터 인정할지는 실제 수집 기록을 보고 도입 단계에서 정합니다.
저장+공유율은 보는 사람의 반응 한 가지일 뿐이고, 파트너십 광고의 구매 전환을 대신하지 못합니다. 위너 소재로 고른 릴스가 광고에서도 잘 되는지 견주는 일은 광고 성과 기록을 받는 도입 단계 과제로 둡니다.
광고비를 얼마나 붙일지, 누구에게 보일지는 이 화면이 다루지 않습니다. 이 편은 그 결정 앞에서 볼 릴스 판정과 화면까지 만들고, 퍼포먼스 마케팅 운영 전문성은 주장하지 않습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
비건 선크림 10월 캠페인(WN-2610-SUN)에 확정된 크리에이터 10명의 캠페인 릴스를 한 줄씩 담은 표로, 인포크 비즈니스 어드민의 캠페인 결과물 목록을 본떠 지원자가 만든 더미입니다. 클린본 주소와 파트너십 광고 코드는 크리에이터가 보내기 전까지 null 입니다. 판정 시각 10월 14일 09:30 에 6편은 게시 48시간이 지났고 4편은 아직 지나지 않았습니다. RL-1388-071 은 78시간이 지나도록 클린본과 광고 코드가 둘 다 없습니다.
| reel_id | creator_id | creator_handle | followers | campaign_id | posted_at | clean_file_url | pa_code |
|---|---|---|---|---|---|---|---|
| RL-1041-046 | CR-1041 | @daily_jiyu | 8,200 | WN-2610-SUN | 2026-10-11 18:00 | https://files.example.com/winnery/WN-2… | PA-7KQ2-M81X |
| RL-2207-118 | CR-2207 | @mom_living_k | 410,000 | WN-2610-SUN | 2026-10-11 12:00 | https://files.example.com/winnery/WN-2… | PA-3TZ9-A04R |
| RL-1388-071 | CR-1388 | @skin_hanul | 23,000 | WN-2610-SUN | 2026-10-11 03:30 | — | — |
| RL-3012-089 | CR-3012 | @cozy.room.s | 56,000 | WN-2610-SUN | 2026-10-12 06:00 | https://files.example.com/winnery/WN-2… | PA-9WD4-K27P |
| RL-4470-007 | CR-4470 | @new.hyun | 5,100 | WN-2610-SUN | 2026-10-12 08:00 | https://files.example.com/winnery/WN-2… | — |
아래 JSON Schema 에 맞는 캠페인 릴스 행을 더 만들어 주세요. {"type":"object","properties":{"reel_id":{"type":"string","description":"캠페인 릴스 번호입니다. 크리에이터가 릴스를 올리면 인포크가 매깁니다"},"creator_id":{"type":"string"},"creator_handle":{"type":"string","description":"인스타그램 계정 이름입니다"},"followers":{"type":"integer","minimum":0,"description":"캠페인 확정 때의 팔로워 수입니다. 화면 표시에만 쓰고 판정에는 쓰지 않습니다"},"campaign_id":{"type":"string"},"posted_at":{"type":"string","description":"릴스 게시 시각, 'YYYY-MM-DD HH:mm' 한국 시각입니다"},"clean_file_url":{"type":["string","null"],"description":"크리에이터가 올린 클린본(자막·음악·스티커를 뺀 원본 영상) 주소입니다. 아직 안 올렸으면 null 입니다"},"pa_code":{"type":["string","null"],"description":"크리에이터가 인스타그램에서 그 릴스의 파트너십 광고를 허용하고 받은 광고 코드입니다. 아직 안 보냈으면 null 입니다"}},"required":["reel_id","creator_id","creator_handle","followers","campaign_id","posted_at","clean_file_url","pa_code"]} 쉬운 데이터만 만들지 말고 다음 사례를 의도적으로 섞어 주세요: (1) 게시 72시간이 지났는데 clean_file_url 과 pa_code 가 둘 다 또는 하나만 null 인 릴스, (2) 게시 48시간이 아직 안 된 릴스, (3) 팔로워 30만 이상과 1만 미만 계정을 한 캠페인에 같이 넣기, (4) 게시 시각이 판정 시각 기준 정확히 48시간 전이나 72시간 전인 릴스. reel_snapshots 행을 같은 reel_id 와 creator_id 로 같이 만드세요. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"reel_id":{"type":"string","description":"캠페인 릴스 번호입니다. 크리에이터가 릴스를 올리면 인포크가 매깁니다"},"creator_id":{"type":"string"},"creator_handle":{"type":"string","description":"인스타그램 계정 이름입니다"},"followers":{"type":"integer","minimum":0,"description":"캠페인 확정 때의 팔로워 수입니다. 화면 표시에만 쓰고 판정에는 쓰지 않습니다"},"campaign_id":{"type":"string"},"posted_at":{"type":"string","description":"릴스 게시 시각, 'YYYY-MM-DD HH:mm' 한국 시각입니다"},"clean_file_url":{"type":["string","null"],"description":"크리에이터가 올린 클린본(자막·음악·스티커를 뺀 원본 영상) 주소입니다. 아직 안 올렸으면 null 입니다"},"pa_code":{"type":["string","null"],"description":"크리에이터가 인스타그램에서 그 릴스의 파트너십 광고를 허용하고 받은 광고 코드입니다. 아직 안 보냈으면 null 입니다"}},"required":["reel_id","creator_id","creator_handle","followers","campaign_id","posted_at","clean_file_url","pa_code"]}릴스마다 찍은 조회·저장·공유 누적값입니다. 캠페인 릴스는 하루 네 번(03·09·15·21시) 찍고, 크리에이터의 지난 릴스는 게시 48시간째에 한 번 찍는다고 가정했습니다. 수록한 행은 48시간이 지난 캠페인 릴스 6편의 48시간 앞뒤 스냅숏과, 그 크리에이터들의 지난 릴스 스냅숏입니다. 지원자가 자기 인스타그램 크리에이터 계정을 Graph API 의 릴스 인사이트(views·saved·shares)로 같은 주기에 찍으면 같은 모양의 실물이 나옵니다. RL-1041-030 은 지난 협찬 릴스라 is_campaign 이 참이고, RL-3012-027 은 수집이 한 시간 밀려 49시간째에 찍혔으며, 새 크리에이터 CR-4470 은 지난 릴스가 5개뿐입니다.
| snap_id | creator_id | reel_id | is_campaign | posted_at | captured_at | views | saves | shares |
|---|---|---|---|---|---|---|---|---|
| SN-1041-01 | CR-1041 | RL-1041-003 | false | 2026-08-20 19:00 | 2026-08-22 19:00 | 13,100 | 229 | 118 |
| SN-1041-02 | CR-1041 | RL-1041-006 | false | 2026-08-24 20:00 | 2026-08-26 20:00 | 9,400 | 143 | 73 |
| SN-1041-03 | CR-1041 | RL-1041-009 | false | 2026-08-28 21:00 | 2026-08-30 21:00 | 16,900 | 335 | 172 |
| SN-1041-04 | CR-1041 | RL-1041-012 | false | 2026-09-01 18:00 | 2026-09-03 18:00 | 12,300 | 203 | 105 |
| SN-1041-05 | CR-1041 | RL-1041-015 | false | 2026-09-05 20:00 | 2026-09-07 20:00 | 7,800 | 98 | 50 |
아래 JSON Schema 에 맞는 릴스 스냅숏 행을 더 만들어 주세요. {"type":"object","properties":{"snap_id":{"type":"string","description":"스냅숏 번호입니다"},"creator_id":{"type":"string"},"reel_id":{"type":"string"},"is_campaign":{"type":"boolean","description":"브랜드 협찬으로 올린 릴스면 참입니다. 이번 캠페인 릴스와 지난 협찬 릴스가 참입니다"},"posted_at":{"type":"string","description":"그 릴스의 게시 시각입니다. 인스타그램 미디어 응답의 timestamp 를 'YYYY-MM-DD HH:mm' 한국 시각으로 적었습니다"},"captured_at":{"type":"string","description":"스냅숏을 찍은 시각, 'YYYY-MM-DD HH:mm' 한국 시각입니다"},"views":{"type":"integer","minimum":0,"description":"찍은 시각까지의 누적 조회수입니다"},"saves":{"type":"integer","minimum":0,"description":"찍은 시각까지의 누적 저장 수입니다"},"shares":{"type":"integer","minimum":0,"description":"찍은 시각까지의 누적 공유 수입니다"}},"required":["snap_id","creator_id","reel_id","is_campaign","posted_at","captured_at","views","saves","shares"]} 쉬운 데이터만 만들지 말고 다음 사례를 의도적으로 섞어 주세요: (1) 캠페인 릴스의 48시간 시점이 두 스냅숏 사이 한가운데가 아닌 곳에 오는 경우, (2) 캠페인 릴스 스냅숏 하나가 정확히 게시 48시간째인 경우, (3) 지난 릴스 가운데 is_campaign 이 참인 협찬 릴스, (4) 48시간째 수집이 한두 시간 밀린 지난 릴스, (5) 지난 릴스가 12개보다 적은 크리에이터, (6) shares 가 0 인 작은 계정 릴스, (7) 팔로워가 많은데 저장+공유율이 평소의 절반 아래인 캠페인 릴스. 같은 reel_id 의 views·saves·shares 는 captured_at 이 늦을수록 줄지 않게 하세요. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"snap_id":{"type":"string","description":"스냅숏 번호입니다"},"creator_id":{"type":"string"},"reel_id":{"type":"string"},"is_campaign":{"type":"boolean","description":"브랜드 협찬으로 올린 릴스면 참입니다. 이번 캠페인 릴스와 지난 협찬 릴스가 참입니다"},"posted_at":{"type":"string","description":"그 릴스의 게시 시각입니다. 인스타그램 미디어 응답의 timestamp 를 'YYYY-MM-DD HH:mm' 한국 시각으로 적었습니다"},"captured_at":{"type":"string","description":"스냅숏을 찍은 시각, 'YYYY-MM-DD HH:mm' 한국 시각입니다"},"views":{"type":"integer","minimum":0,"description":"찍은 시각까지의 누적 조회수입니다"},"saves":{"type":"integer","minimum":0,"description":"찍은 시각까지의 누적 저장 수입니다"},"shares":{"type":"integer","minimum":0,"description":"찍은 시각까지의 누적 공유 수입니다"}},"required":["snap_id","creator_id","reel_id","is_campaign","posted_at","captured_at","views","saves","shares"]}| TypeScript | 판정 엔진 전체의 언어입니다. 스냅숏 두 개 사이 직선 보간으로 48시간 값을 내고, 크리에이터마다 게시 시각 순으로 최근 12개 지난 릴스를 골라 조회수와 저장+공유율의 중앙값을 구한 뒤 네 판정을 정합니다. 판정 결과 타입은 웹 화면과 3주차 앱이 같이 씁니다. |
| React | 화면을 VerdictGroup·ReelRow·BaselineStrip 세 컴포넌트로 나누고, 묶음 순서와 라벨은 설정 객체 하나에서 읽어 판정 값이 늘어도 컴포넌트를 고치지 않습니다. 판정 결과는 입력 표가 바뀔 때만 다시 계산하고, 펼친 줄은 컴포넌트 상태가 아니라 URL 의 reel 파라미터로 둡니다. |
| Next.js | App Router 페이지 하나에 표 업로드 칸과 판정 묶음을 두고, 캠페인과 펼친 릴스를 검색 파라미터로 받아 링크 하나로 같은 화면이 열립니다. Claude 호출과 2주차 선택 이벤트 저장은 route handler 에서 처리해 API 키를 브라우저에 두지 않습니다. |
| Panda CSS | 판정 배지와 묶음 머리를 recipe 로 만들고 네 판정 값을 variant 로 둡니다. 색과 간격은 토큰으로만 써서 3주차 웹뷰 화면과 선택 기록 화면도 같은 recipe 를 그대로 씁니다. |
| React Native | 3주차에 캠페인 매니저가 폰에서 여는 앱입니다. react-native-webview 로 판정 페이지를 띄우고, Expo 푸시를 받으면 그 릴스의 펼친 줄 주소로 웹뷰를 엽니다. |
| 웹-네이티브 연동 | 웹 페이지는 window.ReactNativeWebView 가 있으면 요청 문안 복사 대신 postMessage 로 문안을 앱에 넘기고, 앱은 Share API 로 공유 시트를 띄웁니다. 앱에서 웹으로는 알림이 가리킨 reel_id 를 주소로 넘깁니다. |
| 렌더링 성능 | 2주차에 펼친 줄의 클린본 영상을 preload none 과 포스터 이미지로 두고, 화면 밖으로 나가면 IntersectionObserver 로 멈춥니다. 줄 하나를 펼칠 때 다른 묶음까지 다시 그려지는지 React Profiler 커밋 기록으로 보고 memo 로 나눌 자리를 다시 잡으며, web-vitals 로 INP(누른 뒤 화면이 반응하기까지 걸린 시간)를 남깁니다. |
| 테스트·CI/CD | 판정 사례 32건은 Vitest, 묶음 순서와 reel 파라미터로 줄 열기는 Playwright 로 쓰고, GitHub Actions 가 푸시마다 둘을 돌린 뒤 Vercel 에 배포합니다. |
| 이벤트 로깅·A/B 테스트 | 2주차에 광고로 키움·보류 클릭을 reel_id·verdict·캠페인 안 조회수 순위로 묶어 SQLite 에 쌓습니다. 목록을 판정 묶음으로 보는 화면과 조회수 순으로 보는 화면을 세션마다 하나씩 배정하는 장치도 두지만, 실제 사용자가 없어 효과는 재지 않습니다. |
| 생성형 AI | Claude Haiku 4.5 가 위너인데 클린본·광고 코드가 안 들어온 릴스마다 크리에이터에게 보낼 요청 문안 초안을 씁니다. |
| AI 도구와 자동화 | 3주차에 GitHub Actions 예약 작업이 6시간마다 새 스냅숏으로 판정을 다시 내고, 직전 판정과 달라진 릴스만 골라 푸시를 보냅니다. |
✓ pass → 마케터와 캠페인 매니저가 이 화면에서 위너 소재를 고르고 요청 문안을 보내면 1주차로 완결입니다. 더 키우려면 한 칸 차이로 바뀌는 사례 32건을 테스트로 묶고, 마케터가 실제로 고른 릴스를 기록하는 단계로 갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 6시간마다 판정을 다시 돌려 새 위너와 늦어진 클린본을 캠페인 매니저 폰으로 알리고, 요청 문안을 앱 공유 시트로 보내는 단계로 갑니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · reel_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
키울 근거 없음키울 근거 없음광고로 키울 위너 소재위너 소재조회수만 크고 반응은 평소보다 낮음조회수만 큼위너인데 클린본·광고 코드가 안 들어옴클린본·광고 코드 미수신게시 48시간이 지난 캠페인 릴스마다 위너 소재 판정 하나를 남기고, 48시간 값과 크리에이터 기준선, 그 계산에 쓴 스냅숏을 근거로 싣는 형식입니다.
{"type":"object","properties":{"reel_id":{"type":"string"},"creator_id":{"type":"string"},"creator_handle":{"type":"string"},"followers":{"type":"integer","minimum":0},"campaign_id":{"type":"string"},"as_of":{"type":"string","description":"판정 시각입니다"},"hours_since_post":{"type":"number","minimum":48,"description":"정적 계산값입니다. as_of 에서 campaign_reels.posted_at 을 뺀 시간입니다"},"views_48h":{"type":["number","null"],"minimum":0,"description":"정적 계산값입니다. posted_at+48시간 이하인 마지막 스냅숏과 이상인 첫 스냅숏 사이를 직선으로 이은 48시간 시점의 views 입니다. 앞 스냅숏이 없으면 게시 시각을 0 으로 보고, 뒤 스냅숏이 없으면 null 입니다"},"saves_48h":{"type":["number","null"],"minimum":0,"description":"정적 계산값입니다. views_48h 와 같은 방법으로 낸 saves 입니다"},"shares_48h":{"type":["number","null"],"minimum":0,"description":"정적 계산값입니다. views_48h 와 같은 방법으로 낸 shares 입니다"},"react_rate_48h_pct":{"type":["number","null"],"description":"정적 계산값입니다. (saves_48h+shares_48h)/views_48h×100 을 소수 둘째 자리로 반올림한 저장+공유율입니다"},"baseline_reel_ids":{"type":"array","items":{"type":"string"},"description":"같은 creator_id 에서 is_campaign 이 거짓이고 캠페인 릴스보다 먼저 올라왔으며 48시간 값이 나오는 릴스를 posted_at 이 늦은 순으로 최대 12개 고른 목록입니다"},"baseline_count":{"type":"integer","minimum":0,"maximum":12},"baseline_views_median":{"type":["number","null"],"description":"정적 계산값입니다. baseline_reel_ids 12개의 48시간 views 중앙값(6번째와 7번째의 평균)입니다. 12개가 안 되면 null 입니다"},"baseline_rate_median_pct":{"type":["number","null"],"description":"정적 계산값입니다. baseline_reel_ids 12개마다 낸 48시간 (saves+shares)/views 의 중앙값×100 을 소수 둘째 자리로 반올림한 값입니다. 12개가 안 되면 null 입니다"},"views_ratio":{"type":["number","null"],"description":"정적 계산값입니다. views_48h ÷ baseline_views_median 을 소수 둘째 자리로 반올림한 값입니다"},"rate_ratio":{"type":["number","null"],"description":"정적 계산값입니다. 저장+공유율 ÷ 기준선 저장+공유율 중앙값을 소수 둘째 자리로 반올림한 값입니다"},"missing_assets":{"type":"array","items":{"type":"string","enum":["clean_file_url","pa_code"]},"description":"위너 조건을 넘은 릴스에서 null 인 컬럼 이름입니다"},"verdict":{"type":"string","enum":["키울 근거 없음","광고로 키울 위너 소재","조회수만 크고 반응은 평소보다 낮음","위너인데 클린본·광고 코드가 안 들어옴"]},"evidence":{"type":"array","items":{"type":"object","properties":{"table":{"type":"string","enum":["campaign_reels","reel_snapshots"]},"row":{"type":["string","null"],"description":"campaign_reels 는 reel_id, reel_snapshots 는 snap_id 입니다. agg 항목이면 null 입니다"},"field":{"type":"string"},"value":{"type":["string","integer","boolean","null"]},"agg":{"type":"string","enum":["count","sum","avg","min","max"]}},"required":["table","row","field","value"]}},"notes":{"type":"array","items":{"type":"string"},"description":"판정에 들지 않았지만 마케터나 캠페인 매니저가 알아야 할 사실입니다"},"summary":{"type":"string"}},"required":["reel_id","creator_id","creator_handle","followers","campaign_id","as_of","hours_since_post","views_48h","saves_48h","shares_48h","react_rate_48h_pct","baseline_reel_ids","baseline_count","baseline_views_median","baseline_rate_median_pct","views_ratio","rate_ratio","missing_assets","verdict","evidence","notes","summary"]}[
{
"reel_id": "RL-1041-046",
"creator_id": "CR-1041",
"creator_handle": "@daily_jiyu",
"followers": 8200,
"campaign_id": "WN-2610-SUN",
"as_of": "2026-10-14 09:30",
"hours_since_post": 63.5,
"views_48h": 41200,
"saves_48h": 1120,
"shares_48h": 550,
"react_rate_48h_pct": 4.05,
"baseline_reel_ids": [
"RL-1041-039",
"RL-1041-036",
"RL-1041-033",
"RL-1041-027",
"RL-1041-024",
"RL-1041-021",
"RL-1041-018",
"RL-1041-015",
"RL-1041-012",
"RL-1041-009",
"RL-1041-006",
"RL-1041-003"
],
"baseline_count": 12,
"baseline_views_median": 12900,
"baseline_rate_median_pct": 2.6,
"views_ratio": 3.19,
"rate_ratio": 1.56,
"missing_assets": [],
"verdict": "광고로 키울 위너 소재",
"evidence": [
{
"table": "campaign_reels",
"row": "RL-1041-046",
"field": "posted_at",
"value": "2026-10-11 18:00"
},
{
"table": "campaign_reels",
"row": "RL-1041-046",
"field": "clean_file_url",
"value": "https://files.example.com/winnery/WN-2610-SUN/RL-1041-046_clean.mp4"
},
{
"table": "campaign_reels",
"row": "RL-1041-046",
"field": "pa_code",
"value": "PA-7KQ2-M81X"
},
{
"table": "reel_snapshots",
"row": "SN-1041-14",
"field": "captured_at",
"value": "2026-10-13 15:00"
},
{
"table": "reel_snapshots",
"row": "SN-1041-14",
"field": "views",
"value": 38900
},
{
"table": "reel_snapshots",
"row": "SN-1041-14",
"field": "saves",
"value": 1050
},
{
"table": "reel_snapshots",
"row": "SN-1041-14",
"field": "shares",
"value": 520
},
{
"table": "reel_snapshots",
"row": "SN-1041-15",
"field": "captured_at",
"value": "2026-10-13 21:00"
},
{
"table": "reel_snapshots",
"row": "SN-1041-15",
"field": "views",
"value": 43500
},
{
"table": "reel_snapshots",
"row": "SN-1041-15",
"field": "saves",
"value": 1190
},
{
"table": "reel_snapshots",
"row": "SN-1041-15",
"field": "shares",
"value": 580
},
{
"table": "reel_snapshots",
"row": null,
"field": "reel_id",
"value": 12,
"agg": "count"
},
{
"table": "reel_snapshots",
"row": "SN-1041-07",
"field": "views",
"value": 12700
},
{
"table": "reel_snapshots",
"row": "SN-1041-01",
"field": "views",
"value": 13100
},
{
"table": "reel_snapshots",
"row": "SN-1041-10",
"field": "is_campaign",
"value": true
},
{
"table": "reel_snapshots",
"row": "SN-1041-10",
"field": "views",
"value": 31500
}
],
"notes": [
"48시간 시점은 10월 13일 18:00 이고, 15:00 와 21:00 스냅숏의 가운데 값을 썼습니다.",
"지난 협찬 릴스 RL-1041-030(48시간 조회 31,500)은 is_campaign 이 참이라 기준선에서 뺐습니다."
],
"summary": "팔로워 8,200명 계정의 릴스가 48시간에 41,200회 보여 평소 중앙값 12,900회의 3.19배이고, 저장+공유율 4.05% 도 평소 2.60% 보다 높으며 클린본과 광고 코드가 다 들어와 있습니다."
},
{
"reel_id": "RL-2207-118",
"creator_id": "CR-2207",
"creator_handle": "@mom_living_k",
"followers": 410000,
"campaign_id": "WN-2610-SUN",
"as_of": "2026-10-14 09:30",
"hours_since_post": 69.5,
"views_48h": 612000,
"saves_48h": 1530,
"shares_48h": 918,
"react_rate_48h_pct": 0.4,
"baseline_reel_ids": [
"RL-2207-036",
"RL-2207-033",
"RL-2207-030",
"RL-2207-027",
"RL-2207-024",
"RL-2207-021",
"RL-2207-018",
"RL-2207-015",
"RL-2207-012",
"RL-2207-009",
"RL-2207-006",
"RL-2207-003"
],
"baseline_count": 12,
"baseline_views_median": 382500,
"baseline_rate_median_pct": 1.3,
"views_ratio": 1.6,
"rate_ratio": 0.31,
"missing_assets": [],
"verdict": "조회수만 크고 반응은 평소보다 낮음",
"evidence": [
{
"table": "campaign_reels",
"row": "RL-2207-118",
"field": "posted_at",
"value": "2026-10-11 12:00"
},
{
"table": "reel_snapshots",
"row": "SN-2207-13",
"field": "captured_at",
"value": "2026-10-13 09:00"
},
{
"table": "reel_snapshots",
"row": "SN-2207-13",
"field": "views",
"value": 588000
},
{
"table": "reel_snapshots",
"row": "SN-2207-13",
"field": "saves",
"value": 1480
},
{
"table": "reel_snapshots",
"row": "SN-2207-13",
"field": "shares",
"value": 890
},
{
"table": "reel_snapshots",
"row": "SN-2207-14",
"field": "captured_at",
"value": "2026-10-13 15:00"
},
{
"table": "reel_snapshots",
"row": "SN-2207-14",
"field": "views",
"value": 636000
},
{
"table": "reel_snapshots",
"row": "SN-2207-14",
"field": "saves",
"value": 1580
},
{
"table": "reel_snapshots",
"row": "SN-2207-14",
"field": "shares",
"value": 946
},
{
"table": "reel_snapshots",
"row": null,
"field": "reel_id",
"value": 12,
"agg": "count"
},
{
"table": "reel_snapshots",
"row": "SN-2207-04",
"field": "views",
"value": 371000
},
{
"table": "reel_snapshots",
"row": "SN-2207-04",
"field": "saves",
"value": 3134
},
{
"table": "reel_snapshots",
"row": "SN-2207-04",
"field": "shares",
"value": 1615
},
{
"table": "reel_snapshots",
"row": "SN-2207-07",
"field": "views",
"value": 394000
},
{
"table": "reel_snapshots",
"row": "SN-2207-07",
"field": "saves",
"value": 3433
},
{
"table": "reel_snapshots",
"row": "SN-2207-07",
"field": "shares",
"value": 1768
}
],
"notes": [
"48시간 조회수는 평소의 1.6배인데 저장+공유율 0.40% 는 평소 1.30% 의 31% 입니다. 많이 보였지만 저장하거나 남에게 보낸 사람은 평소보다 훨씬 적습니다."
],
"summary": "팔로워 41만 계정의 릴스가 48시간에 612,000회로 캠페인에서 가장 많이 보였지만, 저장+공유율이 0.40% 로 평소 1.30% 의 31% 에 그쳤습니다."
},
{
"reel_id": "RL-1388-071",
"creator_id": "CR-1388",
"creator_handle": "@skin_hanul",
"followers": 23000,
"campaign_id": "WN-2610-SUN",
"as_of": "2026-10-14 09:30",
"hours_since_post": 78,
"views_48h": 30030,
"saves_48h": 692,
"shares_48h": 319,
"react_rate_48h_pct": 3.37,
"baseline_reel_ids": [
"RL-1388-036",
"RL-1388-033",
"RL-1388-030",
"RL-1388-027",
"RL-1388-024",
"RL-1388-021",
"RL-1388-018",
"RL-1388-015",
"RL-1388-012",
"RL-1388-009",
"RL-1388-006",
"RL-1388-003"
],
"baseline_count": 12,
"baseline_views_median": 14300,
"baseline_rate_median_pct": 2.9,
"views_ratio": 2.1,
"rate_ratio": 1.16,
"missing_assets": [
"clean_file_url",
"pa_code"
],
"verdict": "위너인데 클린본·광고 코드가 안 들어옴",
"evidence": [
{
"table": "campaign_reels",
"row": "RL-1388-071",
"field": "posted_at",
"value": "2026-10-11 03:30"
},
{
"table": "campaign_reels",
"row": "RL-1388-071",
"field": "clean_file_url",
"value": null
},
{
"table": "campaign_reels",
"row": "RL-1388-071",
"field": "pa_code",
"value": null
},
{
"table": "reel_snapshots",
"row": "SN-1388-13",
"field": "captured_at",
"value": "2026-10-13 03:00"
},
{
"table": "reel_snapshots",
"row": "SN-1388-13",
"field": "views",
"value": 29970
},
{
"table": "reel_snapshots",
"row": "SN-1388-13",
"field": "saves",
"value": 690
},
{
"table": "reel_snapshots",
"row": "SN-1388-13",
"field": "shares",
"value": 318
},
{
"table": "reel_snapshots",
"row": "SN-1388-14",
"field": "captured_at",
"value": "2026-10-13 09:00"
},
{
"table": "reel_snapshots",
"row": "SN-1388-14",
"field": "views",
"value": 30690
},
{
"table": "reel_snapshots",
"row": "SN-1388-14",
"field": "saves",
"value": 714
},
{
"table": "reel_snapshots",
"row": "SN-1388-14",
"field": "shares",
"value": 330
},
{
"table": "reel_snapshots",
"row": null,
"field": "reel_id",
"value": 12,
"agg": "count"
},
{
"table": "reel_snapshots",
"row": "SN-1388-07",
"field": "views",
"value": 14000
},
{
"table": "reel_snapshots",
"row": "SN-1388-01",
"field": "views",
"value": 14600
}
],
"notes": [
"48시간 시점(10월 13일 03:30)은 03:00 스냅숏 30분 뒤라, 03:00 값에 09:00 까지 늘어난 양의 12분의 1을 더했습니다.",
"게시 78시간이 지났고 클린본과 광고 코드가 둘 다 비어 있어 요청 문안 초안이 붙습니다."
],
"summary": "평소의 2.10배로 보이고 저장+공유율도 평소보다 높은 위너 릴스인데, 게시 78시간이 지나도록 클린본과 광고 코드가 둘 다 들어오지 않았습니다."
},
{
"reel_id": "RL-3012-089",
"creator_id": "CR-3012",
"creator_handle": "@cozy.room.s",
"followers": 56000,
"campaign_id": "WN-2610-SUN",
"as_of": "2026-10-14 09:30",
"hours_since_post": 51.5,
"views_48h": 30600,
"saves_48h": 660,
"shares_48h": 320,
"react_rate_48h_pct": 3.2,
"baseline_reel_ids": [
"RL-3012-036",
"RL-3012-033",
"RL-3012-030",
"RL-3012-027",
"RL-3012-024",
"RL-3012-021",
"RL-3012-018",
"RL-3012-015",
"RL-3012-012",
"RL-3012-009",
"RL-3012-006",
"RL-3012-003"
],
"baseline_count": 12,
"baseline_views_median": 20400,
"baseline_rate_median_pct": 3,
"views_ratio": 1.5,
"rate_ratio": 1.07,
"missing_assets": [],
"verdict": "광고로 키울 위너 소재",
"evidence": [
{
"table": "campaign_reels",
"row": "RL-3012-089",
"field": "posted_at",
"value": "2026-10-12 06:00"
},
{
"table": "campaign_reels",
"row": "RL-3012-089",
"field": "clean_file_url",
"value": "https://files.example.com/winnery/WN-2610-SUN/RL-3012-089_clean.mp4"
},
{
"table": "campaign_reels",
"row": "RL-3012-089",
"field": "pa_code",
"value": "PA-9WD4-K27P"
},
{
"table": "reel_snapshots",
"row": "SN-3012-13",
"field": "captured_at",
"value": "2026-10-14 03:00"
},
{
"table": "reel_snapshots",
"row": "SN-3012-13",
"field": "views",
"value": 29400
},
{
"table": "reel_snapshots",
"row": "SN-3012-13",
"field": "saves",
"value": 640
},
{
"table": "reel_snapshots",
"row": "SN-3012-13",
"field": "shares",
"value": 310
},
{
"table": "reel_snapshots",
"row": "SN-3012-14",
"field": "captured_at",
"value": "2026-10-14 09:00"
},
{
"table": "reel_snapshots",
"row": "SN-3012-14",
"field": "views",
"value": 31800
},
{
"table": "reel_snapshots",
"row": "SN-3012-14",
"field": "saves",
"value": 680
},
{
"table": "reel_snapshots",
"row": "SN-3012-14",
"field": "shares",
"value": 330
},
{
"table": "reel_snapshots",
"row": null,
"field": "reel_id",
"value": 12,
"agg": "count"
},
{
"table": "reel_snapshots",
"row": "SN-3012-07",
"field": "views",
"value": 19900
},
{
"table": "reel_snapshots",
"row": "SN-3012-01",
"field": "views",
"value": 20900
},
{
"table": "reel_snapshots",
"row": "SN-3012-09",
"field": "posted_at",
"value": "2026-09-26 08:30"
},
{
"table": "reel_snapshots",
"row": "SN-3012-09",
"field": "captured_at",
"value": "2026-09-28 09:30"
},
{
"table": "reel_snapshots",
"row": "SN-3012-09",
"field": "views",
"value": 24500
}
],
"notes": [
"48시간 조회수 30,600 은 평소 중앙값 20,400 의 딱 1.5배라 1.5배 이상 조건을 넘습니다.",
"지난 릴스 RL-3012-027 은 수집이 한 시간 밀려 49시간째에 찍혔습니다. 게시 시각을 0 인 점으로 보고 24,500 을 48/49 로 줄인 24,000 을 기준선에 넣었습니다."
],
"summary": "48시간 조회수가 평소의 딱 1.5배인 30,600회이고 저장+공유율 3.20% 가 평소 3.00% 보다 높아, 클린본과 광고 코드가 들어온 위너 소재입니다."
},
{
"reel_id": "RL-4470-007",
"creator_id": "CR-4470",
"creator_handle": "@new.hyun",
"followers": 5100,
"campaign_id": "WN-2610-SUN",
"as_of": "2026-10-14 09:30",
"hours_since_post": 49.5,
"views_48h": 9500,
"saves_48h": 325,
"shares_48h": 160,
"react_rate_48h_pct": 5.11,
"baseline_reel_ids": [
"RL-4470-015",
"RL-4470-012",
"RL-4470-009",
"RL-4470-006",
"RL-4470-003"
],
"baseline_count": 5,
"baseline_views_median": null,
"baseline_rate_median_pct": null,
"views_ratio": null,
"rate_ratio": null,
"missing_assets": [],
"verdict": "키울 근거 없음",
"evidence": [
{
"table": "campaign_reels",
"row": "RL-4470-007",
"field": "posted_at",
"value": "2026-10-12 08:00"
},
{
"table": "campaign_reels",
"row": "RL-4470-007",
"field": "pa_code",
"value": null
},
{
"table": "reel_snapshots",
"row": "SN-4470-06",
"field": "captured_at",
"value": "2026-10-14 03:00"
},
{
"table": "reel_snapshots",
"row": "SN-4470-06",
"field": "views",
"value": 9000
},
{
"table": "reel_snapshots",
"row": "SN-4470-06",
"field": "saves",
"value": 300
},
{
"table": "reel_snapshots",
"row": "SN-4470-06",
"field": "shares",
"value": 150
},
{
"table": "reel_snapshots",
"row": "SN-4470-07",
"field": "captured_at",
"value": "2026-10-14 09:00"
},
{
"table": "reel_snapshots",
"row": "SN-4470-07",
"field": "views",
"value": 9600
},
{
"table": "reel_snapshots",
"row": "SN-4470-07",
"field": "saves",
"value": 330
},
{
"table": "reel_snapshots",
"row": "SN-4470-07",
"field": "shares",
"value": 162
},
{
"table": "reel_snapshots",
"row": null,
"field": "reel_id",
"value": 5,
"agg": "count"
},
{
"table": "reel_snapshots",
"row": "SN-4470-01",
"field": "shares",
"value": 0
},
{
"table": "reel_snapshots",
"row": "SN-4470-04",
"field": "views",
"value": 4400
}
],
"notes": [
"지난 릴스가 5개뿐이라 기준선을 내지 않았습니다. 48시간 조회수 9,500회는 지난 릴스 5개(2,300~4,400회)보다 크게 높아 마케터가 직접 볼 만합니다.",
"pa_code 가 아직 없지만 위너 판정을 받지 않아 요청 문안은 붙지 않습니다."
],
"summary": "지난 릴스가 5개뿐인 새 크리에이터라 평소 값을 정하지 않았고, 48시간 조회수 9,500회는 판정 대신 메모로 남겼습니다."
},
{
"reel_id": "RL-2519-133",
"creator_id": "CR-2519",
"creator_handle": "@ondo_home",
"followers": 31000,
"campaign_id": "WN-2610-SUN",
"as_of": "2026-10-14 09:30",
"hours_since_post": 72.5,
"views_48h": 15840,
"saves_48h": 230,
"shares_48h": 112,
"react_rate_48h_pct": 2.16,
"baseline_reel_ids": [
"RL-2519-036",
"RL-2519-033",
"RL-2519-030",
"RL-2519-027",
"RL-2519-024",
"RL-2519-021",
"RL-2519-018",
"RL-2519-015",
"RL-2519-012",
"RL-2519-009",
"RL-2519-006",
"RL-2519-003"
],
"baseline_count": 12,
"baseline_views_median": 17600,
"baseline_rate_median_pct": 2,
"views_ratio": 0.9,
"rate_ratio": 1.08,
"missing_assets": [],
"verdict": "키울 근거 없음",
"evidence": [
{
"table": "campaign_reels",
"row": "RL-2519-133",
"field": "posted_at",
"value": "2026-10-11 09:00"
},
{
"table": "reel_snapshots",
"row": "SN-2519-13",
"field": "captured_at",
"value": "2026-10-13 03:00"
},
{
"table": "reel_snapshots",
"row": "SN-2519-14",
"field": "captured_at",
"value": "2026-10-13 09:00"
},
{
"table": "reel_snapshots",
"row": "SN-2519-14",
"field": "views",
"value": 15840
},
{
"table": "reel_snapshots",
"row": "SN-2519-14",
"field": "saves",
"value": 230
},
{
"table": "reel_snapshots",
"row": "SN-2519-14",
"field": "shares",
"value": 112
},
{
"table": "reel_snapshots",
"row": null,
"field": "reel_id",
"value": 12,
"agg": "count"
},
{
"table": "reel_snapshots",
"row": "SN-2519-01",
"field": "views",
"value": 17200
},
{
"table": "reel_snapshots",
"row": "SN-2519-07",
"field": "views",
"value": 18000
}
],
"notes": [
"09:00 스냅숏이 게시 뒤 정확히 48시간째라 보간 없이 그 값을 썼습니다.",
"저장+공유율 2.16% 는 평소 2.00% 보다 높지만 조회수가 평소의 0.9배라 위너 조건에 못 미칩니다."
],
"summary": "48시간 조회수 15,840회가 평소 중앙값 17,600회의 0.9배로, 이 크리에이터의 평소와 비슷한 성과입니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 키울 근거 없음·광고로 키울 위너 소재·조회수만 크고 반응은 평소보다 낮음·위너인데 클린본·광고 코드가 안 들어옴 네 값 중 하나인지, missing_assets 의 항목이 clean_file_url·pa_code 중 하나인지 확인합니다. 2. 필수 필드 reel_id·creator_id·creator_handle·followers·campaign_id·as_of·hours_since_post·views_48h·saves_48h·shares_48h·react_rate_48h_pct·baseline_reel_ids·baseline_count·baseline_views_median·baseline_rate_median_pct·views_ratio·rate_ratio·missing_assets·verdict·evidence·notes·summary 가 모두 존재하는지 확인합니다. 3. hours_since_post 가 48 이상인지, baseline_count 가 0 이상 12 이하이고 baseline_reel_ids 의 길이와 같은지, views_48h·saves_48h·shares_48h 가 null 이 아니면 0 이상인지 확인합니다. 4. 48시간 값 재계산: reel_snapshots 에서 그 reel_id 의 captured_at 이 posted_at+48시간 이하인 마지막 행과 이상인 첫 행을 골라 두 행 사이를 직선으로 잇고 48시간 시점의 views·saves·shares 를 냅니다. 두 행이 같은 행이면 그 값을 그대로 쓰고, 이하인 행이 없으면 게시 시각을 세 값이 0 인 점으로 씁니다. RL-1041-046 은 SN-1041-14(15:00)와 SN-1041-15(21:00)의 가운데라 views_48h 가 41200, RL-1388-071 은 SN-1388-13(03:00) 30분 뒤라 30030, RL-2519-133 은 SN-2519-14 가 정확히 48시간째라 15840 인지 다시 계산합니다. 5. 기준선 재계산: 같은 creator_id 에서 is_campaign 이 거짓이고 posted_at 이 캠페인 릴스보다 앞서며 48시간 값이 나오는 릴스를 posted_at 이 늦은 순으로 12개 골라 baseline_reel_ids 와 같은지 봅니다. 그 12개의 48시간 views 중앙값과 (saves+shares)/views 중앙값으로 baseline_views_median·baseline_rate_median_pct 를 다시 냅니다. CR-1041 은 협찬 릴스 RL-1041-030 이 빠져 12900 이 되는지, CR-3012 는 49시간째에 찍힌 RL-3012-027 의 views 24500 을 48/49 로 줄인 24000 이 들어가 20400 이 되는지 확인합니다. 6. views_ratio 는 views_48h ÷ baseline_views_median, rate_ratio 는 48시간 저장+공유율 ÷ 기준선 저장+공유율 중앙값을 소수 둘째 자리로 반올림한 값인지 확인합니다. 판정의 문턱 비교는 반올림 전 값으로 합니다. 7. 광고로 키울 위너 소재: views_48h 가 baseline_views_median 의 1.5배 이상이고 48시간 저장+공유율이 기준선 중앙값 이상이며 clean_file_url 과 pa_code 가 둘 다 있으면 이 값입니다. RL-1041-046(3.19배)과 RL-3012-089(30600 이 20400 의 딱 1.5배)가 걸리고, 0.9배인 RL-2519-133 과 저장+공유율이 평소에 못 미치는 RL-2207-118 은 걸리지 않습니다. 8. 조회수만 크고 반응은 평소보다 낮음: views_48h 가 중앙값의 1.5배 이상인데 48시간 저장+공유율이 기준선 중앙값의 70% 에 못 미치면 이 값입니다. RL-2207-118 은 1.6배에 저장+공유율 0.40% 가 평소 1.30% 의 31% 라 걸리고, 저장+공유율이 평소 이상인 RL-1041-046 은 걸리지 않습니다. 9. 위너인데 클린본·광고 코드가 안 들어옴: 7번의 조회수·저장+공유율 조건을 넘었고 hours_since_post 가 72 이상인데 clean_file_url 이나 pa_code 가 null 이면 이 값이고, missing_assets 에 null 인 컬럼 이름이 들어갑니다. 78시간째인 RL-1388-071 은 둘 다 null 이라 걸리고, 둘 다 있는 RL-1041-046 은 걸리지 않습니다. 10. 키울 근거 없음: 7~9번에 하나도 걸리지 않거나, baseline_count 가 12 미만이거나, views_48h 가 null 이면 이 값입니다. baseline_count 가 12 미만이면 baseline_views_median·baseline_rate_median_pct·views_ratio·rate_ratio 가 null 입니다. 지난 릴스가 5개인 RL-4470-007 이 여기에 듭니다. 11. hours_since_post 가 48 미만인 캠페인 릴스(RL-3365-152·RL-1720-064·RL-2894-097·RL-4102-028)는 출력에 없는지 확인합니다. 12. evidence 의 table·field 가 campaign_reels·reel_snapshots 에 실재하는 컬럼인지 대조합니다. 13. agg 가 없는 evidence 의 value 가 그 row 의 그 컬럼 값과 같은지 확인합니다. agg 가 count 인 value 는 기준선에 쓴 지난 릴스 수입니다. 14. summary 가 존댓말로 끝나는지 확인합니다. [수동 검증 · 사람이 봅니다] 1. 위너 소재로 나온 릴스를 실제로 광고로 키울지, 광고비를 얼마 붙일지는 퍼포먼스 마케터가 영상을 보고 정합니다. 2. 요청 문안은 캠페인 매니저가 고쳐 보내는 초안이고, 크리에이터에게 언제 다시 연락할지는 캠페인 매니저가 정합니다. 3. 지난 릴스가 12개 미만이라 키울 근거 없음에 남은 크리에이터는 notes 의 조회수 범위를 보고 사람이 따로 봅니다. [평가 러너 · 2주차] 1. 48시간 앞뒤 스냅숏 고르기, 1.5배·70%·72시간 문턱, 지난 릴스 12개 미만, 협찬 릴스 빼기, 정각 스냅숏처럼 한 칸 차이로 판정이 바뀌는 사례 32건에서 verdict 가 손으로 붙인 정답과 같은 건수를 셉니다. 2. 선택 기록에서 마케터가 광고로 키움을 누른 릴스 가운데 위너 소재가 아니었던 릴스 수를 셉니다. [판정식에 더한 정의 · 오너 확인 항목] 1. 지난 릴스는 campaign_reels 에 없어서, 게시 시각이 없으면 지난 릴스를 같은 48시간 시점으로 맞출 수도 최근 12개를 고를 수도 없습니다. 이 시각을 담으려고 카드 inputs 에 없던 reel_snapshots.posted_at 을 더했고, 인스타그램 미디어 응답이 스냅숏마다 게시 시각(timestamp)을 같이 주는 모양을 따랐습니다. 2. 보간은 48시간 앞뒤로 가장 가까운 스냅숏 두 개를 직선으로 잇고, 48시간 앞에 스냅숏이 없으면 게시 시각을 0 인 점으로 봅니다. 3. 최근 12개는 캠페인 릴스보다 먼저 올라온 지난 릴스만 셉니다. 12개보다 적으면 중앙값을 내지 않고 키울 근거 없음으로 둡니다. 4. 12개의 중앙값은 6번째와 7번째 값의 평균이고, 저장+공유율 중앙값은 릴스마다 (saves+shares)/views 를 낸 뒤의 중앙값입니다. 5. 72시간 전 공백: 게시 48~72시간 사이에 조회수·저장+공유율 조건은 넘었는데 클린본이나 광고 코드가 아직 없는 릴스는 카드 판정식대로 키울 근거 없음에 남고, 화면은 notes 에 72시간까지 남은 시간을 적습니다. 이 구간을 위너 소재로 먼저 보일지는 오너 확인 항목입니다. 6. 70~100% 구간: 48시간 조회수가 기준선의 1.5배 이상인데 저장+공유율이 기준선 중앙값의 70% 이상 100% 미만인 릴스는 위너 조건(중앙값 이상)에도, 조회수만 큰 릴스 조건(70% 미만)에도 들지 않아 카드 판정식 글자 그대로 키울 근거 없음에 남습니다. 화면은 이 릴스의 notes 에 views_ratio 와 rate_ratio 두 배수를 적어 마케터가 따로 보게 합니다. 판정식은 넓히지 않았고, 이 구간을 따로 묶을지는 오너 확인 항목입니다.