Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ out/
.env
postgres_data/
src/main/resources/application-local.yml
.claude
.claude

### macOS ###
.DS_Store
**/.DS_Store
72 changes: 72 additions & 0 deletions docs/db/20260901_create_ad_tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
-- 제휴 광고(쿠팡 파트너스 / 애드픽) 소재·클릭·노출 테이블
-- 관련 설계: docs/superpowers/specs/2026-09-01-ad-picke-store-design.md
--
-- 이 파일은 참고용이다. 운영은 spring.jpa.hibernate.ddl-auto=update 라 배포 시 자동 생성된다.
-- 스키마를 손으로 관리하는 환경이나 사후 검증이 필요할 때 쓴다.
--
-- CHECK 제약은 엔티티의 @Check 로도 선언되어 있어 테이블 최초 생성 시 함께 만들어진다.
-- 다만 ddl-auto=update 는 이미 있는 제약을 갱신하지 않는다.
-- enum 에 값을 추가하면 아래 ALTER 를 별도로 실행해야 한다.
-- ALTER TABLE ad_creatives DROP CONSTRAINT ck_ad_creatives_slot;
-- ALTER TABLE ad_creatives ADD CONSTRAINT ck_ad_creatives_slot CHECK (slot IN (...));
-- 빠뜨리면 새 지면의 INSERT 가 조용히 막힌다. AdCheckConstraintTest 가 제약과 enum 의 불일치를 먼저 잡는다.

CREATE TABLE IF NOT EXISTS ad_creatives (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(16) NOT NULL UNIQUE,
network VARCHAR(20) NOT NULL,
slot VARCHAR(40) NOT NULL,
title VARCHAR(100) NOT NULL,
subtitle VARCHAR(200),
image_url VARCHAR(500) NOT NULL,
cta_text VARCHAR(30) NOT NULL,
landing_url VARCHAR(1000) NOT NULL,
status VARCHAR(20) NOT NULL,
source VARCHAR(20) NOT NULL DEFAULT 'MANUAL',
external_id VARCHAR(64),
target_os VARCHAR(20) NOT NULL DEFAULT 'ALL',
weight INTEGER NOT NULL DEFAULT 1,
starts_at TIMESTAMP,
ends_at TIMESTAMP,
created_at TIMESTAMP,
updated_at TIMESTAMP,
CONSTRAINT ck_ad_creatives_network CHECK (network IN ('COUPANG', 'ADPICK')),
CONSTRAINT ck_ad_creatives_slot CHECK (slot IN ('HOME_FEED', 'BATTLE_RESULT_BOTTOM', 'CHAT_ROOM_INLINE', 'ATTENDANCE_COMPLETE', 'PROFILE_BOTTOM')),
CONSTRAINT ck_ad_creatives_status CHECK (status IN ('DRAFT', 'ACTIVE', 'PAUSED')),
CONSTRAINT ck_ad_creatives_source CHECK (source IN ('MANUAL', 'ADPICK_API')),
CONSTRAINT ck_ad_creatives_target_os CHECK (target_os IN ('ALL', 'ANDROID', 'IOS')),
-- 애드픽 캠페인을 다시 찾는 키. 수동 등록 소재는 external_id 가 NULL 이라 중복되지 않는다.
CONSTRAINT uk_ad_creatives_external UNIQUE (source, external_id)
);

-- 지면 조회는 (slot, status)로만 들어온다.
CREATE INDEX IF NOT EXISTS idx_ad_creatives_slot_status ON ad_creatives (slot, status);

CREATE TABLE IF NOT EXISTS ad_click_logs (
id BIGSERIAL PRIMARY KEY,
creative_id BIGINT NOT NULL,
slot VARCHAR(40) NOT NULL,
ip_hash VARCHAR(64),
user_agent VARCHAR(500),
created_at TIMESTAMP,
updated_at TIMESTAMP,
CONSTRAINT ck_ad_click_logs_slot CHECK (slot IN ('HOME_FEED', 'BATTLE_RESULT_BOTTOM', 'CHAT_ROOM_INLINE', 'ATTENDANCE_COMPLETE', 'PROFILE_BOTTOM'))
);

CREATE INDEX IF NOT EXISTS idx_ad_click_logs_creative ON ad_click_logs (creative_id);
CREATE INDEX IF NOT EXISTS idx_ad_click_logs_created_at ON ad_click_logs (created_at);

-- 노출은 raw 로그로 쌓지 않는다. 배너가 스크롤에 걸릴 때마다 행이 생기면 금방 수천만 건이 된다.
CREATE TABLE IF NOT EXISTS ad_impression_daily (
id BIGSERIAL PRIMARY KEY,
creative_id BIGINT NOT NULL,
slot VARCHAR(40) NOT NULL,
stat_date DATE NOT NULL,
impressions BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP,
updated_at TIMESTAMP,
CONSTRAINT uk_ad_impression_daily UNIQUE (creative_id, slot, stat_date),
CONSTRAINT ck_ad_impression_daily_slot CHECK (slot IN ('HOME_FEED', 'BATTLE_RESULT_BOTTOM', 'CHAT_ROOM_INLINE', 'ATTENDANCE_COMPLETE', 'PROFILE_BOTTOM'))
);

CREATE INDEX IF NOT EXISTS idx_ad_impression_daily_date ON ad_impression_daily (stat_date);
82 changes: 82 additions & 0 deletions docs/erd/ad.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
@startuml ad
hide circle
hide methods
skinparam linetype ortho

entity "ad_creatives" as ad_creatives {
* id : BIGINT <<PK>>
--
code : VARCHAR(16) <<UK>>
network : VARCHAR(20)
slot : VARCHAR(40)
title : VARCHAR(100)
subtitle : VARCHAR(200) <<nullable>>
image_url : VARCHAR(500)
cta_text : VARCHAR(30)
landing_url : VARCHAR(1000)
status : VARCHAR(20)
source : VARCHAR(20)
external_id : VARCHAR(64) <<nullable>>
target_os : VARCHAR(20)
weight : INTEGER
starts_at : TIMESTAMP <<nullable>>
ends_at : TIMESTAMP <<nullable>>
created_at : TIMESTAMP
updated_at : TIMESTAMP
}

entity "ad_click_logs" as ad_click_logs {
* id : BIGINT <<PK>>
--
creative_id : BIGINT
slot : VARCHAR(40)
ip_hash : VARCHAR(64) <<nullable>>
user_agent : VARCHAR(500) <<nullable>>
created_at : TIMESTAMP
updated_at : TIMESTAMP
}

entity "ad_impression_daily" as ad_impression_daily {
* id : BIGINT <<PK>>
--
creative_id : BIGINT
slot : VARCHAR(40)
stat_date : DATE
impressions : BIGINT
created_at : TIMESTAMP
updated_at : TIMESTAMP
}

ad_creatives ||--o{ ad_click_logs : clicked
ad_creatives ||--o{ ad_impression_daily : impressed

note bottom of ad_creatives
제휴 광고 소재. 각 매체 콘솔에서 발급한
완성형 제휴 링크를 어드민이 등록한다.
network : COUPANG | ADPICK
status : DRAFT | ACTIVE | PAUSED
source : MANUAL | ADPICK_API
target_os : ALL | ANDROID | IOS

ADPICK_API 소재는 캠페인 API 가 주기적으로
덮어쓴다. external_id 는 애드픽 apOffer 다.
end note

note bottom of ad_click_logs
클릭 원장. 제휴사 리포트와 대조하는 용도다.
/c/{code}는 외부 브라우저에서 열려
Authorization 헤더가 없으므로 사용자를 특정하지 않는다.
end note

note bottom of ad_impression_daily
일별 노출 집계. (creative_id, slot, stat_date) 유니크.
노출을 raw 로그로 쌓으면 배너가 스크롤에 걸릴 때마다
행이 생겨 금방 수천만 건이 된다.
end note

note top of ad_creatives
creative_id는 논리적 참조다.
소재를 지워도 집계 이력은 남기려고 FK를 걸지 않았다.
end note

@enduml
Loading