AIP 디자인 평가

AIP로 생성된 디자인을 CSV로 올려, 전체 페이지를 맥락과 함께 봅니다.

코딩은 필요 없습니다. 쿼리를 복사해 붙여넣고, 날짜만 바꾸고, 실행하면 끝입니다.

1
쿼리를 복사합니다
2
데이터브릭스 SQL Editor 에 붙여넣습니다

오른쪽 위에서 COMMON_USE_SQL_CLUSTER 를 고르세요. 그게 실행할 컴퓨터입니다.

3
맨 위 7줄에서 원하는 것만 바꿉니다

그 아래는 손대지 않아도 됩니다.

이 줄이렇게 바꾸면
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 처럼 적으면 긴 요청문을 잘라 파일을 줄입니다
4
실행하고, 결과를 CSV 로 받아 아래에 끌어다 놓습니다

실행은 ⌘↵. 결과 표 위쪽의 내려받기 버튼으로 CSV 를 받으세요.

잘 안 될 때 · 더 하고 싶을 때

내려받기가 안 되거나 파일이 잘려요
결과가 너무 큰 경우입니다. 먼저 3번에서 only_with_feedbacktrue 로 바꿔 보세요. 대부분 이걸로 해결됩니다.
그래도 전량이 필요하면 아래 두 줄을 쿼리 맨 앞에 붙여 실행하세요. 결과가 화면이 아니라 파일로 저장됩니다. 저장된 파일은 ① 방법으로 받습니다. <본인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 파일 올리기 여기에 끌어다 놓거나 클릭해서 선택 — 여러 개 한꺼번에 가능, .csv.gz도 그대로

AIP 디자인 평가