AIP로 생성된 디자인을 CSV로 올려, 전체 페이지를 맥락과 함께 봅니다.
폴더를 열면 aip-design-eval-gz 안에 월별 파일이 하나씩
있습니다. 보고 싶은 달을 체크해 Download.
한 달치가 약 116MB입니다(압축 안 한 원본은 592MB).
사내망 또는 VPN 연결 상태여야 열립니다.
압축을 풀지 마세요. .csv.gz 그대로 놓으면 앱이 알아서 풉니다.
여러 달을 함께 보려면 파일 여러 개를 한꺼번에 놓으면 됩니다.
데이터브릭스 CLI가 설치돼 있으면 한 줄로 받습니다.
코딩은 필요 없습니다. 쿼리를 복사해 붙여넣고, 날짜만 바꾸고, 실행하면 끝입니다.
SQL Editor 에 붙여넣습니다
오른쪽 위에서 COMMON_USE_SQL_CLUSTER 를 고르세요. 그게 실행할 컴퓨터입니다.
그 아래는 손대지 않아도 됩니다.
| 이 줄 | 이렇게 바꾸면 |
|---|---|
start_d · end_d |
보고 싶은 기간. 그대로 두면 7월 14일부터 어제까지 |
min_pages |
3 으로 하면 3장 미만인 디자인은 빠집니다 |
only_with_feedback |
true 로 하면 별점·의견을 남긴 것만 — 결과가 훨씬 작아집니다 |
only_with_comment |
true 로 하면 글로 의견을 쓴 것만 (위보다 더 적음) |
only_country |
'KR' 로 하면 한국 사용자만. 'JP' 는 일본 |
prompt_maxlen |
1500 처럼 적으면 긴 요청문을 잘라 파일을 줄입니다 |
실행은 ⌘↵. 결과 표 위쪽의 내려받기 버튼으로 CSV 를 받으세요.
내려받기가 안 되거나 파일이 잘려요
결과가 너무 큰 경우입니다. 먼저 3번에서 only_with_feedback 을
true 로 바꿔 보세요. 대부분 이걸로 해결됩니다.
그래도 전량이 필요하면 아래 두 줄을 쿼리 맨 앞에 붙여 실행하세요.
결과가 화면이 아니라 파일로 저장됩니다. 저장된 파일은 ① 방법으로 받습니다.
<본인ID> 는 본인 사번/아이디로 바꾸세요.
INSERT OVERWRITE DIRECTORY '/Volumes/temp/<본인ID>_temp/exports/my-aip-eval'
USING CSV OPTIONS ('header'='true', 'escape'='"')
파일이 여러 개로 쪼개지면, 쿼리의 첫 SELECT 바로 뒤에
/*+ COALESCE(1) */ 를 넣으면 하나로 합쳐집니다.
컬럼을 더 넣고 싶어요
쿼리 아래쪽 ⑧ ⑨ ⑩ 표시가 있는 자리에
예시가 주석으로 달려 있습니다. 붙일 수 있는 데이터 목록은 쿼리 맨 위에 있습니다.
맨 앞 두 칸(design_id · page_url)의 순서만 지키면
나머지는 자유입니다 — 앱이 알아서 필터나 검색으로 처리합니다.
알아둘 것
별점·의견은 디자인을 완성한 시점에 남긴 것만 붙습니다.
요청문(프롬프트)은 파일을 줄이려고 디자인의 첫 페이지 줄에만 들어 있습니다.
-- ─────────────────────────────────────────────────────────────────────────────
-- AIP 디자인 평가 뷰어용 CSV 추출
--
-- 사용법: 아래 ▼ 파라미터만 고쳐서 Databricks SQL 에디터에서 실행 → CSV 다운로드 → 앱에 업로드
--
-- 출력: 한 행 = 한 페이지. 1·2번째 컬럼이 design_id · page_url (앱 필수 계약)
-- 나머지 컬럼은 앱에서 자동으로 검색 필터가 된다
--
-- ⚠️ 긴 텍스트(프롬프트)는 디자인의 첫 페이지 행에만 채우고 나머지는 빈칸이다.
-- 모든 행에 넣으면 페이지 수만큼(평균 12.8배) 중복돼 CSV가 7.6GB로 폭발한다.
--
-- ⚠️ NPS·코멘트는 완성후(completed) 피드백만 쓴다.
-- 생성중(generating/T1)은 대기 경험에 대한 말이라 결과물 평가와 대응하지 않는다.
--
-- ── 컬럼 추가하기 ───────────────────────────────────────────────────────────
-- 아래 세 자리만 채우면 된다. 값 종류가 40개 이하면 앱이 필터 드롭다운을 자동 생성한다.
-- ⑧ CTE 추가 ⑨ SELECT 컬럼 추가 ⑩ JOIN 추가
--
-- 붙일 수 있는 것 (이번 조사에서 확인한 것만)
-- design_stat_info 다운로드수·좋아요·조회수·댓글수 bronze...design.design_stat_info
-- 디자인 제목 title silver...design_version_union.title
-- 프롬프트 슬라이드 수 $.slideCount OUTLINE_DOCUMENT.request
-- AIP가 해석한 브리프 $.outline.presentationBrief CONTENT.request
-- .subject / .purpose / .audience / .contentLanguage
-- 레이아웃 템플릿 idx layout_template_idx ai_presentation_design_statistics
-- 생성 소요시간 CONTENT.created - OUTLINE.created
--
-- ⚠️ 붙이기 전에 알아야 할 함정
-- · 페이지 순서는 design_atlas.design.page_ids 배열 인덱스가 원본이다.
-- page_sequence.sequence / template_page_no 는 AIP 디자인에서 0·NULL 이다.
-- · design_idx 는 2026-07-14 에 적재가 끊겼다. 신규 디자인은 UUID(design_id)만 있다.
-- · 피드백·프롬프트는 디자인과 직접 키가 없어 account_id + 시각 근접으로 붙인다.
-- · 긴 텍스트를 추가하면 반드시 페이지순서=1 행에만 넣어라(아래 프롬프트 방식 참고).
-- 전 행에 넣으면 평균 12.8배 중복된다.
-- ─────────────────────────────────────────────────────────────────────────────
WITH params AS (
SELECT
-- ══════════════════════════════════════════════════════════════════════
-- ▼▼▼ 설정 — 이 블록만 고치면 됩니다 ▼▼▼
-- ══════════════════════════════════════════════════════════════════════
-- 1) 기간 ─────────────────────────────────────────────────────────────
-- 디자인이 "생성된" 날짜 기준입니다.
DATE'2026-07-14' AS start_d, -- 시작일 (이 날 포함)
-- 종료일: 기본은 '데이터가 있는 마지막 날'을 자동으로 씁니다.
-- ⚠️ 오늘 날짜를 쓰면 최근 1~2일 디자인이 통째로 빠집니다
-- (페이지 썸네일 적재가 그만큼 늦습니다).
-- 특정 날짜로 자르려면 아래 줄을 지우고 DATE'2026-07-20' AS end_d 로 바꾸세요.
(SELECT max(p_created_date) FROM silver.miricanvas_design.design_version_union) AS end_d,
-- 2) 최소 페이지 수 ───────────────────────────────────────────────────
-- 1 = 전부. 표지 한 장짜리를 걸러내려면 3~5 정도로 올리세요.
1 AS min_pages,
-- 3) 피드백이 있는 디자인만? ──────────────────────────────────────────
-- false = 전체 디자인 (기본)
-- true = NPS·코멘트가 달린 디자인만 → 파일이 훨씬 작아집니다
false AS only_with_feedback,
-- 4) 코멘트가 있는 디자인만? ──────────────────────────────────────────
-- true 로 하면 유저가 글로 남긴 것만 봅니다 (3번보다 더 좁음)
false AS only_with_comment,
-- 5) 국가 ─────────────────────────────────────────────────────────────
-- NULL = 전체. 특정 국가만 보려면 'KR' 또는 'JP' 처럼 넣으세요.
CAST(NULL AS STRING) AS only_country,
-- 6) 프롬프트 길이 상한 ───────────────────────────────────────────────
-- 프롬프트는 최대 2만자입니다. 파일이 너무 크면 1500 정도로 줄이세요.
-- 0 = 자르지 않음
0 AS prompt_maxlen
-- ══════════════════════════════════════════════════════════════════════
-- ▲▲▲ 여기까지 ▲▲▲
--
-- 파일이 너무 크면? → 기간을 좁히거나(1), only_with_feedback=true(3),
-- 프롬프트를 자르세요(6). 실측: 14일 전량 = 480MB
-- 컬럼을 더 넣고 싶으면? → 파일 아래쪽 ⑧ ⑨ ⑩ 주석을 보세요.
-- ══════════════════════════════════════════════════════════════════════
),
b AS (
SELECT CAST(start_d AS STRING) AS s,
CAST(end_d AS STRING) AS e,
CAST(date_add(end_d, 1) AS STRING) AS nxt,
CAST(date_sub(start_d, 1) AS STRING) AS s_m1,
min_pages, only_with_feedback, only_with_comment, only_country, prompt_maxlen
FROM params
),
-- ① AIP 생성 디자인 (전체. 피드백 유무와 무관)
aip AS (
SELECT s.design_id,
acg.account_id,
s.request_type,
s.device_type,
acg.usage AS aip_mode, -- COMPONENT_BASED / TEMPLATE_BASED / LAYOUT_BASED
s.layout_template_key,
-- CONTENT.request 의 브리프는 OUTLINE_DOCUMENT.response 의 산출물이 그대로 들어온 것.
-- subject 문자열이 두 단계를 잇는 키가 된다(시각과 무관).
get_json_object(acg.request,'$.outline.presentationBrief.subject') AS brief_subject,
from_utc_timestamp(CAST(s.created_date_tz AS timestamp),'Asia/Seoul') AS gen_ts,
acg.idx AS cg_idx
FROM bronze.miridih_miricanvas.ai_presentation_design_statistics s
JOIN bronze.miridih_miricanvas.ai_async_content_generation acg
ON acg.idx = s.ai_async_content_generation_idx
CROSS JOIN b
WHERE s.created_date_tz >= b.s AND s.created_date_tz < b.nxt
AND s.request_type IS NOT NULL
AND s.design_id IS NOT NULL AND s.design_id <> ''
),
-- ② 페이지 순서 = 디자인 2.0 도큐먼트의 page_ids 배열 인덱스
-- download_page.page_number(다운로드분의 확정 순서)와 653/653 일치 검증됨
atlas AS (
SELECT design_id, pos, pid
FROM (SELECT design_id, page_ids,
row_number() OVER (PARTITION BY design_id ORDER BY updated_date DESC) rn
FROM bronze.miridih_miricanvas_design_atlas.design
WHERE NOT coalesce(__deleted, false)) t
LATERAL VIEW posexplode(page_ids) e AS pos, pid
WHERE rn = 1
),
pages AS (
SELECT v.design_id, v.page_id,
replace(v.thumbnail_abs_path,'http://','https://') AS page_url,
v.width, v.height,
a.pos
FROM silver.miricanvas_design.design_version_union v
CROSS JOIN b
LEFT JOIN atlas a ON a.design_id = v.design_id AND a.pid = v.page_id
WHERE v.p_created_date >= b.s_m1
AND v.thumbnail_abs_path IS NOT NULL AND v.thumbnail_abs_path <> ''
),
-- ③ 유저 정보
user_info AS (
SELECT CAST(u.account_id AS BIGINT) AS account_id,
COALESCE(u.main_user_type,'설문미제출') AS user_type,
COALESCE(c.country_code, u.country, 'KR') AS country
FROM (SELECT account_id, main_user_type, country
FROM gold.miridih_analytics.mican_user_info_hst
WHERE p_date = (SELECT MAX(p_date) FROM gold.miridih_analytics.mican_user_info_hst)) u
LEFT JOIN gold.miridih_analytics.mican_sign_up_session_ga_hst g ON u.account_id = g.account_id
LEFT JOIN gold.miridih_analytics.country_code_mapping_ref c ON g.country = c.country_name
),
-- ④ 다운로드된 확장자
dl AS (
SELECT d.design_id, concat_ws(',', array_sort(collect_set(d.type))) AS dl_types
FROM bronze.miridih_miricanvas_design.download_event d CROSS JOIN b
WHERE d.p_created_date >= b.s_m1
GROUP BY d.design_id
),
-- ⑤ NPS·코멘트 — 완성후(completed)만
-- 직접 키가 없어 account_id + 시각 근접으로 붙인다.
-- 방향 강제: 완성된 덱을 보고 남기므로 디자인이 먼저다(자명한 매칭 실측 96.5%).
-- gap ∈ [-2분, +60분] 중 gap 최소 = 피드백 직전에 만든 가장 최근 디자인.
-- -2분은 dynamo/DB 시계 오차 유예 (자명한 매칭의 99.5% 보존).
fb_raw AS (
SELECT CAST(f.accountId AS BIGINT) AS account_id,
CAST(f.score AS INT) AS score,
f.comment,
from_utc_timestamp(COALESCE(
try_to_timestamp(f.feedbackTime,"yyyy.MM.dd HH:mm:ss"),
try_to_timestamp(f.feedbackTime,"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
try_to_timestamp(f.feedbackTime)),'Asia/Seoul') AS fb_ts
FROM bronze.miridih_miricanvas_user_feedback.mc_user_feedback f CROSS JOIN b
WHERE f.option.preset_key.S = 'AI_PRESENTATION'
AND f.accountId RLIKE '^[0-9]+$'
AND f.option.phase.S = 'completed' -- ★ completed only
AND f.score IS NOT NULL
AND f.feedbackTime >= b.s_m1
),
fb AS (
-- ⚠️ 디자인당 1행으로 줄여야 한다. (account_id, fb_ts) 기준으로만 줄이면
-- 한 계정이 남긴 여러 피드백이 같은 디자인에 배정될 수 있어 최종 조인에서 행이 배로 늘어난다.
-- (실제로 14일 전량에서 88만 → 335만 행으로 폭발) → 디자인별 최신 피드백 1건만 남긴다.
SELECT design_id, score, comment, fb_ts, gap_min, rivals
FROM (
SELECT *, row_number() OVER (PARTITION BY design_id ORDER BY fb_ts DESC) AS rk2
FROM (
SELECT a.design_id, f.score, f.comment, f.fb_ts,
round((unix_timestamp(f.fb_ts) - unix_timestamp(a.gen_ts))/60.0, 1) AS gap_min,
count(*) OVER (PARTITION BY f.account_id, f.fb_ts) - 1 AS rivals,
row_number() OVER (PARTITION BY f.account_id, f.fb_ts
ORDER BY CASE WHEN unix_timestamp(f.fb_ts) >= unix_timestamp(a.gen_ts)
THEN 0 ELSE 1 END,
abs(unix_timestamp(f.fb_ts) - unix_timestamp(a.gen_ts))) AS rk
FROM fb_raw f
JOIN aip a ON a.account_id = f.account_id
AND unix_timestamp(f.fb_ts) - unix_timestamp(a.gen_ts) BETWEEN -120 AND 3600
) WHERE rk = 1
) WHERE rk2 = 1
),
-- ⑥ 최초 프롬프트 — PRESENTATION_OUTLINE_DOCUMENT
-- ⚠️ CONTENT 행의 $.userInput 은 항상 NULL이다($.outline은 개요 단계의 산출물).
--
-- 조인은 2단 우선순위다. 실측 커버리지(7/15~27, 디자인 51,216개):
-- subject 일치 단독 86.7%
-- 시각 근접 30분 단독 90.7%
-- 시각 근접 6시간 단독 93.3%
-- subject OR 6시간 93.6% ← 채택
-- 시각 창을 30분으로 뒀을 때 5%가 빠졌다. 실제 gap 중앙값이 97분이다
-- (유저가 개요를 받고 한참 뒤에 생성한다). 창만 넓히면 오귀속이 늘어나므로
-- subject 일치를 1순위로 두고, 없을 때만 시각으로 고른다.
--
-- ⚠️ userInput 이 없는 건도 가져온다(NULL 조건 없음).
-- 파일만 올리고 프롬프트를 안 쓴 경우가 있다 — 7/15~27 기준 57,455건 중 2,992건.
-- 이건 매칭 실패가 아니라 프롬프트가 원래 없는 정상 케이스라서,
-- `입력방식` 컬럼으로 구분해 화면에서 데이터 누락과 헷갈리지 않게 한다.
od AS (
SELECT o.account_id,
CAST(o.created_date_tz AS timestamp) AS od_ts,
get_json_object(o.request,'$.userInput') AS user_input,
get_json_object(o.request,'$.fileKey') AS file_key,
get_json_object(o.response,'$.presentationBrief.subject') AS od_subject
FROM bronze.miridih_miricanvas.ai_async_content_generation o CROSS JOIN b
WHERE o.type = 'PRESENTATION_OUTLINE_DOCUMENT'
AND o.created_date_tz >= b.s_m1
),
prompt AS (
SELECT design_id, user_input, file_key
FROM (
SELECT a.design_id, o.user_input, o.file_key,
row_number() OVER (PARTITION BY a.design_id ORDER BY
-- 1순위: 브리프 subject 일치 (시각 무관, 확정에 가깝다)
CASE WHEN a.brief_subject IS NOT NULL AND o.od_subject = a.brief_subject THEN 0 ELSE 1 END,
-- 2순위: 개요가 생성보다 먼저인 것 중 가장 가까운 것
abs(unix_timestamp(a.gen_ts) - unix_timestamp(from_utc_timestamp(o.od_ts,'Asia/Seoul')))) rk
FROM aip a
JOIN od o ON o.account_id = a.account_id
AND ( (a.brief_subject IS NOT NULL AND o.od_subject = a.brief_subject)
OR unix_timestamp(a.gen_ts) - unix_timestamp(from_utc_timestamp(o.od_ts,'Asia/Seoul'))
BETWEEN -300 AND 21600 ) -- 6시간 창 (실측 gap 중앙 97분)
) WHERE rk = 1
),
-- ⑧ ▼ 여기에 CTE 추가 (예시)
-- my_stat AS (
-- SELECT design_id, download_cnt, like_cnt, view_cnt
-- FROM bronze.miridih_miricanvas_design.design_stat_info
-- ),
-- ⑦ 디자인 단위 집계
dsn AS (
SELECT a.design_id, a.account_id, a.request_type, a.device_type, a.gen_ts,
a.aip_mode, a.layout_template_key, a.brief_subject,
count(p.page_id) AS page_cnt,
max(p.width) AS w, max(p.height) AS h,
sum(CASE WHEN p.pos IS NULL THEN 1 ELSE 0 END) AS no_order_cnt
FROM aip a JOIN pages p ON p.design_id = a.design_id
GROUP BY a.design_id, a.account_id, a.request_type, a.device_type, a.gen_ts,
a.aip_mode, a.layout_template_key, a.brief_subject
)
SELECT
-- ★ 필수 2개 (순서 고정)
d.design_id AS design_id,
p.page_url AS page_url,
-- 페이지
row_number() OVER (PARTITION BY d.design_id
ORDER BY coalesce(p.pos, 999999), p.page_id) AS `페이지순서`,
d.page_cnt AS `페이지수`,
CASE WHEN d.no_order_cnt = 0 THEN '확정' ELSE '일부미상' END AS `순서정확도`,
-- 디자인
date_format(d.gen_ts,'yyyy-MM-dd') AS `생성일`,
date_format(d.gen_ts,'yyyy-MM-dd HH:mm') AS `생성시각`,
-- AIP 생성 모드 (usage). 컴포넌트 94% / 템플릿 6% / 레이아웃 0.2%
CASE d.aip_mode
WHEN 'COMPONENT_BASED' THEN '컴포넌트'
WHEN 'TEMPLATE_BASED' THEN '템플릿'
WHEN 'LAYOUT_BASED' THEN '레이아웃'
ELSE COALESCE(d.aip_mode,'-') END AS `AIP모드`,
COALESCE(d.aip_mode,'') AS `AIP모드원본`,
COALESCE(d.layout_template_key,'') AS `레이아웃키`,
d.request_type AS `요청타입`,
d.device_type AS `디바이스`,
concat(CAST(d.w AS STRING),'x',CAST(d.h AS STRING)) AS `캔버스`,
-- 유저
CAST(d.account_id AS STRING) AS account_id,
COALESCE(u.country,'-') AS `국가`,
COALESCE(u.user_type,'-') AS `유저유형`,
-- 행동
COALESCE(dl.dl_types,'') AS `다운로드`,
CASE WHEN dl.dl_types IS NULL THEN '안함' ELSE '함' END AS `다운로드여부`,
-- 피드백 (완성후만)
CASE WHEN f.score IS NULL THEN '' ELSE CAST(f.score AS STRING) END AS NPS,
CASE WHEN f.score IS NULL THEN '피드백없음'
WHEN f.score >= 9 THEN '프로모터'
WHEN f.score >= 7 THEN '패시브'
ELSE '디트랙터' END AS `NPS구간`,
CASE WHEN f.comment IS NULL OR trim(f.comment) = '' THEN '없음' ELSE '있음' END AS `코멘트여부`,
-- 코멘트는 짧아(평균 24자) 전 행에 둬도 부담 없다 → 필터·검색에 유용
COALESCE(f.comment,'') AS `코멘트`,
-- 입력방식 — 프롬프트 빈칸이 '데이터 누락'인지 '원래 없음'인지 구분한다
CASE WHEN pr.design_id IS NULL THEN '개요없음'
WHEN trim(coalesce(pr.user_input,'')) <> '' AND coalesce(pr.file_key,'') <> '' THEN '파일+프롬프트'
WHEN trim(coalesce(pr.user_input,'')) <> '' THEN '프롬프트'
WHEN coalesce(pr.file_key,'') <> '' THEN '파일만'
ELSE '입력없음' END AS `입력방식`,
COALESCE(d.brief_subject,'') AS `주제`,
-- ⑨ ▼ 여기에 컬럼 추가 (예시)
-- COALESCE(CAST(m.like_cnt AS STRING),'0') AS `좋아요`,
-- 프롬프트 — 첫 페이지 행에만 (중복 방지)
CASE WHEN row_number() OVER (PARTITION BY d.design_id
ORDER BY coalesce(p.pos, 999999), p.page_id) = 1
THEN CASE WHEN (SELECT prompt_maxlen FROM b) > 0
THEN substr(COALESCE(pr.user_input,''), 1, (SELECT prompt_maxlen FROM b))
ELSE COALESCE(pr.user_input,'') END
ELSE '' END AS `프롬프트`
FROM dsn d
JOIN pages p ON p.design_id = d.design_id
LEFT JOIN user_info u ON u.account_id = d.account_id
LEFT JOIN dl dl ON dl.design_id = d.design_id
LEFT JOIN fb f ON f.design_id = d.design_id
LEFT JOIN prompt pr ON pr.design_id = d.design_id
-- ⑩ ▼ 여기에 조인 추가 (예시)
-- LEFT JOIN my_stat m ON m.design_id = d.design_id
CROSS JOIN b bb
WHERE d.page_cnt >= bb.min_pages
AND (NOT bb.only_with_feedback OR f.score IS NOT NULL)
AND (NOT bb.only_with_comment OR (f.comment IS NOT NULL AND trim(f.comment) <> ''))
AND (bb.only_country IS NULL OR u.country = bb.only_country)
ORDER BY d.gen_ts DESC, d.design_id, `페이지순서`
.csv.gz도 그대로