addashboard-cli 0.5.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/ads.js +34 -5
- package/dist/commands/ads.js.map +1 -1
- package/dist/commands/submission.js +181 -18
- package/dist/commands/submission.js.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/apiClient.js +4 -0
- package/dist/lib/apiClient.js.map +1 -1
- package/dist/lib/localPath.js +58 -0
- package/dist/lib/localPath.js.map +1 -0
- package/package.json +2 -2
- package/skills/ad-ops/SKILL.md +284 -28
- package/skills/ad-ops/ad-planner.html +783 -175
- package/skills/ad-ops/scripts/adops.sh +243 -0
- package/skills/ad-ops/scripts/plan_lint.py +357 -0
- package/skills/ad-ops/scripts/refs_import.py +348 -0
- package/skills/ad-ops//343/202/242/343/202/253/343/202/246/343/203/263/343/203/210/yda/sample_account.json +14 -3
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""実アカウントから参照表(見出し・説明文・オーディエンス・配置面)を取り込む。
|
|
3
|
+
|
|
4
|
+
`adops.sh refs-import <platform> <accountId> [--write]` から呼ばれる。単体でも動く:
|
|
5
|
+
|
|
6
|
+
python3 refs_import.py yda 1002811292 [--write] [--campaign-id 32430712]
|
|
7
|
+
|
|
8
|
+
`webgram list` を campaigns / ads / resources の3系統で叩き、
|
|
9
|
+
`アカウント/<platform>/<name>.json` にマージする。--write なしは差分表示のみ。
|
|
10
|
+
|
|
11
|
+
━━ 採番アルゴリズム(最重要)━━
|
|
12
|
+
既存の番号は **絶対に振り直さない**。既存番号は過去の入稿計画・広告名・
|
|
13
|
+
`creative` パラメータに焼き込まれており、renumber は計測を壊す。
|
|
14
|
+
|
|
15
|
+
1. 比較用 norm(s): NFKC → trim → 連続空白畳み込み。**比較にのみ使い、保存は原文**
|
|
16
|
+
2. 既存 refs.titles を {norm(値): 番号} に反転
|
|
17
|
+
3. 取得した headline が既存に一致 → その番号を再利用
|
|
18
|
+
4. 未知は max(既存の整数キー)+1 から (adGroupId, adId) 昇順の初出順で採番(決定的)
|
|
19
|
+
5. 同一テキストが複数広告に出たら1番号に名寄せ
|
|
20
|
+
6. descriptions も同手順(独立した番号空間)
|
|
21
|
+
7. 消えた audiences / placements の ID は **削除せず "stale": true** を付ける
|
|
22
|
+
"""
|
|
23
|
+
import argparse
|
|
24
|
+
import glob
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import unicodedata
|
|
31
|
+
import urllib.parse
|
|
32
|
+
|
|
33
|
+
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
34
|
+
CLI = os.environ.get("ADOPS_CLI", "npx -y addashboard-cli@latest")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------- 共通
|
|
38
|
+
def norm(s: object) -> str:
|
|
39
|
+
"""比較専用の正規化。保存するのは常に原文であることに注意。"""
|
|
40
|
+
t = unicodedata.normalize("NFKC", str(s or "")).strip()
|
|
41
|
+
return re.sub(r"\s+", " ", t)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def int_keys(d: dict) -> list[int]:
|
|
45
|
+
out = []
|
|
46
|
+
for k in d or {}:
|
|
47
|
+
try:
|
|
48
|
+
out.append(int(k))
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
pass
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_list(resource: str, account_id: str, **opts) -> dict:
|
|
55
|
+
"""webgram list を1回叩いて JSON を返す。失敗したら空dict + stderr に理由。"""
|
|
56
|
+
cmd = [*CLI.split(), "list", "--platform", opts.pop("platform", "yda"),
|
|
57
|
+
"--resource", resource, "--account-id", account_id]
|
|
58
|
+
for k, v in opts.items():
|
|
59
|
+
if v:
|
|
60
|
+
cmd += [f"--{k}", str(v)]
|
|
61
|
+
print(f"▸ {resource} を取得中...", file=sys.stderr)
|
|
62
|
+
try:
|
|
63
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
64
|
+
except Exception as e: # noqa: BLE001
|
|
65
|
+
print(f" ✗ {resource} の取得に失敗: {e}", file=sys.stderr)
|
|
66
|
+
return {}
|
|
67
|
+
if proc.returncode != 0:
|
|
68
|
+
print(f" ✗ {resource} の取得に失敗 (exit {proc.returncode}): "
|
|
69
|
+
f"{proc.stderr.strip()[:400]}", file=sys.stderr)
|
|
70
|
+
return {}
|
|
71
|
+
try:
|
|
72
|
+
return json.loads(proc.stdout)
|
|
73
|
+
except Exception as e: # noqa: BLE001
|
|
74
|
+
print(f" ✗ {resource} の応答を解釈できません: {e}", file=sys.stderr)
|
|
75
|
+
return {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_account(platform: str, account_id: str) -> tuple[str | None, dict]:
|
|
79
|
+
"""accountId からアカウントJSONのパスと内容を引く(sample_account は最後に回す)。"""
|
|
80
|
+
paths = [p for p in glob.glob(os.path.join(SKILL_DIR, "アカウント", platform, "*.json"))
|
|
81
|
+
if os.path.basename(p) != "index.json"]
|
|
82
|
+
paths.sort(key=lambda p: os.path.basename(p) == "sample_account.json")
|
|
83
|
+
for path in paths:
|
|
84
|
+
try:
|
|
85
|
+
data = json.load(open(path, encoding="utf-8"))
|
|
86
|
+
except Exception: # noqa: BLE001
|
|
87
|
+
continue
|
|
88
|
+
if str(data.get("accountId", "")) == str(account_id):
|
|
89
|
+
return path, data
|
|
90
|
+
return None, {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------- 採番(既存を絶対に守る)
|
|
94
|
+
def assign_numbers(existing: dict, incoming: list[str]) -> tuple[dict, dict]:
|
|
95
|
+
"""既存番号を保ったまま未知テキストへ採番する。
|
|
96
|
+
|
|
97
|
+
existing: {"1": "見出し", ...}(原文)
|
|
98
|
+
incoming: 出現順((adGroupId, adId) 昇順)のテキスト列。重複を含んでよい。
|
|
99
|
+
|
|
100
|
+
returns (merged, stats)
|
|
101
|
+
merged: 既存 + 追加ぶん(既存の番号・原文は不変)
|
|
102
|
+
stats : {"reused": n, "added": n, "merged": n}
|
|
103
|
+
"""
|
|
104
|
+
merged = dict(existing or {})
|
|
105
|
+
by_norm: dict[str, str] = {}
|
|
106
|
+
for k, v in merged.items():
|
|
107
|
+
by_norm.setdefault(norm(v), str(k))
|
|
108
|
+
|
|
109
|
+
nxt = (max(int_keys(merged)) + 1) if int_keys(merged) else 1
|
|
110
|
+
stats = {"reused": 0, "added": 0, "merged": 0}
|
|
111
|
+
seen_this_run: set[str] = set()
|
|
112
|
+
|
|
113
|
+
for text in incoming:
|
|
114
|
+
if not str(text or "").strip():
|
|
115
|
+
continue
|
|
116
|
+
n = norm(text)
|
|
117
|
+
if n in by_norm:
|
|
118
|
+
# 既存番号を再利用(振り直さない)。同一 run で2回目なら名寄せ扱い。
|
|
119
|
+
if n in seen_this_run:
|
|
120
|
+
stats["merged"] += 1
|
|
121
|
+
else:
|
|
122
|
+
stats["reused"] += 1
|
|
123
|
+
seen_this_run.add(n)
|
|
124
|
+
continue
|
|
125
|
+
merged[str(nxt)] = text # 保存は原文
|
|
126
|
+
by_norm[n] = str(nxt)
|
|
127
|
+
seen_this_run.add(n)
|
|
128
|
+
nxt += 1
|
|
129
|
+
stats["added"] += 1
|
|
130
|
+
return merged, stats
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------- 取り込み本体
|
|
134
|
+
def import_yda(account_id: str, campaign_id: str | None) -> dict:
|
|
135
|
+
"""YDA から取り込んだ生データを {ads, placements, audiences, campaigns} で返す。"""
|
|
136
|
+
out: dict = {"ads": [], "placements": {}, "audiences": [], "campaigns": {}}
|
|
137
|
+
|
|
138
|
+
camp = run_list("campaigns", account_id, platform="yda")
|
|
139
|
+
out["campaigns"] = camp or {}
|
|
140
|
+
|
|
141
|
+
ad_kwargs = {}
|
|
142
|
+
if campaign_id:
|
|
143
|
+
ad_kwargs["campaign-id"] = campaign_id
|
|
144
|
+
else:
|
|
145
|
+
# campaignId が無いと backend が 400 を返すので、campaigns から全CPを回す
|
|
146
|
+
ids = [str(c.get("campaignId") or c.get("id") or "")
|
|
147
|
+
for c in (camp.get("campaigns") or []) if (c.get("campaignId") or c.get("id"))]
|
|
148
|
+
ads: list = []
|
|
149
|
+
for cid in ids:
|
|
150
|
+
r = run_list("ads", account_id, platform="yda", **{"campaign-id": cid})
|
|
151
|
+
ads += r.get("ads") or []
|
|
152
|
+
out["ads"] = ads
|
|
153
|
+
res = run_list("resources", account_id, platform="yda")
|
|
154
|
+
out["placements"] = res.get("placements") or {}
|
|
155
|
+
out["audiences"] = res.get("audiences") or []
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
out["ads"] = (run_list("ads", account_id, platform="yda", **ad_kwargs)).get("ads") or []
|
|
159
|
+
res = run_list("resources", account_id, platform="yda")
|
|
160
|
+
out["placements"] = res.get("placements") or {}
|
|
161
|
+
out["audiences"] = res.get("audiences") or []
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def sort_key(ad: dict) -> tuple:
|
|
166
|
+
"""(adGroupId, adId) 昇順。数値化できないものは末尾へ。"""
|
|
167
|
+
def num(v):
|
|
168
|
+
try:
|
|
169
|
+
return (0, int(v))
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
return (1, 0)
|
|
172
|
+
return (num(ad.get("adGroupId")), num(ad.get("adId")))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def merge(account: dict, fetched: dict) -> tuple[dict, list[str]]:
|
|
176
|
+
"""アカウントJSON(v1/v2 どちらでも)へマージし、(新しいdict, レポート行) を返す。"""
|
|
177
|
+
acc = json.loads(json.dumps(account)) # deep copy
|
|
178
|
+
refs = acc.setdefault("refs", {})
|
|
179
|
+
report: list[str] = []
|
|
180
|
+
|
|
181
|
+
ads = sorted(fetched.get("ads") or [], key=sort_key)
|
|
182
|
+
|
|
183
|
+
# --- 見出し / 説明文(既存番号は不変)
|
|
184
|
+
# backend の /yda/ads は複数形の配列 (headlines / descriptions) で返す。
|
|
185
|
+
# レスポンシブ広告は実測で1件だが、媒体側が複数返す形に変わっても取りこぼさない
|
|
186
|
+
# よう配列を平坦化して全件を採番対象にする(単数キーは旧レスポンスの後方互換)。
|
|
187
|
+
def texts(key_plural: str, key_singular: str) -> list:
|
|
188
|
+
out = []
|
|
189
|
+
for a in ads:
|
|
190
|
+
v = a.get(key_plural)
|
|
191
|
+
if isinstance(v, list):
|
|
192
|
+
out.extend(v)
|
|
193
|
+
elif v:
|
|
194
|
+
out.append(v)
|
|
195
|
+
else:
|
|
196
|
+
out.append(a.get(key_singular))
|
|
197
|
+
return out
|
|
198
|
+
|
|
199
|
+
titles, t_stats = assign_numbers(refs.get("titles") or {},
|
|
200
|
+
texts("headlines", "headline"))
|
|
201
|
+
descs, d_stats = assign_numbers(refs.get("descriptions") or {},
|
|
202
|
+
texts("descriptions", "description"))
|
|
203
|
+
refs["titles"], refs["descriptions"] = titles, descs
|
|
204
|
+
report.append(f"見出し 追加 {t_stats['added']} / 既存一致 {t_stats['reused']}"
|
|
205
|
+
f" / 名寄せ統合 {t_stats['merged']}(合計 {len(titles)})")
|
|
206
|
+
report.append(f"説明文 追加 {d_stats['added']} / 既存一致 {d_stats['reused']}"
|
|
207
|
+
f" / 名寄せ統合 {d_stats['merged']}(合計 {len(descs)})")
|
|
208
|
+
|
|
209
|
+
# --- キャンペーン: ID → name(プランナーの「既存キャンペーン呼び出し」が引く)
|
|
210
|
+
cps = refs.setdefault("campaigns", {})
|
|
211
|
+
cp_added = 0
|
|
212
|
+
for c in (fetched.get("campaigns") or {}).get("campaigns") or []:
|
|
213
|
+
cid = str(c.get("campaignId") or c.get("id") or "")
|
|
214
|
+
if not cid:
|
|
215
|
+
continue
|
|
216
|
+
if cid not in cps:
|
|
217
|
+
cps[cid] = {}
|
|
218
|
+
cp_added += 1
|
|
219
|
+
if c.get("name"):
|
|
220
|
+
cps[cid]["name"] = str(c["name"])
|
|
221
|
+
report.append(f"キャンペーン 新規 {cp_added}(合計 {len(cps)})")
|
|
222
|
+
|
|
223
|
+
# --- 広告グループ: campaignId / finalUrlSample / paramCode 補完
|
|
224
|
+
# ⚠ campaignId を必ず入れる。無いとプランナーの「既存広告グループ呼び出し」が親CPを
|
|
225
|
+
# 特定できず、**プラン末尾の無関係なキャンペーン配下にぶら下がる**。
|
|
226
|
+
# ⚠ AG名は /yda/ads が返さないので新規行は name 無しになる(一覧では ID 表示)。
|
|
227
|
+
# 件数をレポートに出して、名前が要るなら手で足せると分かるようにする。
|
|
228
|
+
ag = refs.setdefault("adGroups", {})
|
|
229
|
+
url_added = code_added = cid_added = 0
|
|
230
|
+
new_ags: list[str] = []
|
|
231
|
+
for a in ads:
|
|
232
|
+
gid = str(a.get("adGroupId") or "")
|
|
233
|
+
if not gid:
|
|
234
|
+
continue
|
|
235
|
+
if gid not in ag:
|
|
236
|
+
ag[gid] = {}
|
|
237
|
+
new_ags.append(gid)
|
|
238
|
+
row = ag[gid]
|
|
239
|
+
cid = str(a.get("campaignId") or "")
|
|
240
|
+
if cid and not row.get("campaignId"):
|
|
241
|
+
row["campaignId"] = cid
|
|
242
|
+
cid_added += 1
|
|
243
|
+
url = a.get("finalUrl") or ""
|
|
244
|
+
if not url:
|
|
245
|
+
continue
|
|
246
|
+
if not row.get("finalUrlSample"):
|
|
247
|
+
row["finalUrlSample"] = url
|
|
248
|
+
url_added += 1
|
|
249
|
+
if not row.get("paramCode"):
|
|
250
|
+
q = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
|
|
251
|
+
code = (q.get("ad") or [""])[0]
|
|
252
|
+
if not code:
|
|
253
|
+
m = re.search(r"【([^】]+)】", str(row.get("name") or ""))
|
|
254
|
+
code = m.group(1) if m else ""
|
|
255
|
+
if code:
|
|
256
|
+
row["paramCode"] = code
|
|
257
|
+
code_added += 1
|
|
258
|
+
report.append(f"広告グループ 新規 {len(new_ags)} / campaignId 補完 {cid_added}"
|
|
259
|
+
f" / finalUrlSample 補完 {url_added} / paramCode 補完 {code_added}"
|
|
260
|
+
+ (" ※新規行は AG名 が未設定です(/yda/ads は広告グループ名を返しません)。"
|
|
261
|
+
"プランナーの一覧では ID 表示になるので、必要なら name を手で足してください"
|
|
262
|
+
if new_ags else ""))
|
|
263
|
+
|
|
264
|
+
# --- baseArticleUrl 候補(提示のみ・自動上書きしない)
|
|
265
|
+
urls = [a.get("finalUrl") for a in ads if str(a.get("finalUrl") or "").startswith("http")]
|
|
266
|
+
if urls:
|
|
267
|
+
pre = os.path.commonprefix([u.split("?")[0] for u in urls])
|
|
268
|
+
pre = pre[: pre.rfind("/") + 1] if "/" in pre else pre
|
|
269
|
+
cur = acc.get("baseArticleUrl") or ""
|
|
270
|
+
if pre and pre != cur:
|
|
271
|
+
report.append(f"baseArticleUrl 候補: {pre}(現在: {cur or '未設定'})"
|
|
272
|
+
" ※自動では上書きしません")
|
|
273
|
+
|
|
274
|
+
# --- audiences / placements(消えたIDは削除せず stale マーク)
|
|
275
|
+
for key, incoming in (
|
|
276
|
+
("audiences", [{"id": str(x.get("id")), "name": x.get("name"), "kind": "audienceList"}
|
|
277
|
+
for x in (fetched.get("audiences") or []) if x.get("id")]),
|
|
278
|
+
("placements", [
|
|
279
|
+
{"id": str(x.get("id")), "name": x.get("name"), "kind": kind}
|
|
280
|
+
for kind, arr in (("standard", (fetched.get("placements") or {}).get("standard") or []),
|
|
281
|
+
("exclusion", (fetched.get("placements") or {}).get("exclusions") or []))
|
|
282
|
+
for x in arr if x.get("id")
|
|
283
|
+
]),
|
|
284
|
+
):
|
|
285
|
+
table = refs.setdefault(key, {})
|
|
286
|
+
live_ids = {r["id"] for r in incoming}
|
|
287
|
+
added = 0
|
|
288
|
+
for r in incoming:
|
|
289
|
+
if r["id"] not in table:
|
|
290
|
+
added += 1
|
|
291
|
+
row = table.setdefault(r["id"], {})
|
|
292
|
+
row["name"], row["kind"] = r["name"], r["kind"]
|
|
293
|
+
row.pop("stale", None)
|
|
294
|
+
stale = 0
|
|
295
|
+
for i in table:
|
|
296
|
+
if i not in live_ids:
|
|
297
|
+
table[i]["stale"] = True
|
|
298
|
+
stale += 1
|
|
299
|
+
report.append(f"{key:<10} {len(live_ids)}件(新規 {added} / stale {stale})"
|
|
300
|
+
+ (" ※stale は過去プランが参照している可能性があるため削除しません"
|
|
301
|
+
if stale else ""))
|
|
302
|
+
|
|
303
|
+
acc["accountJsonVersion"] = 2
|
|
304
|
+
return acc, report
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------- main
|
|
308
|
+
def main() -> int:
|
|
309
|
+
ap = argparse.ArgumentParser(description="実アカウントから参照表を取り込む")
|
|
310
|
+
ap.add_argument("platform")
|
|
311
|
+
ap.add_argument("account_id")
|
|
312
|
+
ap.add_argument("--write", action="store_true", help="アカウントJSONへ書き込む(既定は dry-run)")
|
|
313
|
+
ap.add_argument("--campaign-id", dest="campaign_id", default=None,
|
|
314
|
+
help="対象キャンペーンを絞る(省略時は全CP)")
|
|
315
|
+
args = ap.parse_args()
|
|
316
|
+
|
|
317
|
+
if args.platform != "yda":
|
|
318
|
+
sys.exit(f"✗ refs-import は現状 yda のみ対応です(指定: {args.platform})")
|
|
319
|
+
|
|
320
|
+
path, account = find_account(args.platform, args.account_id)
|
|
321
|
+
if not path:
|
|
322
|
+
sys.exit(f"✗ accountId {args.account_id} のアカウントJSONが "
|
|
323
|
+
f"アカウント/{args.platform}/ にありません。先に作成してください")
|
|
324
|
+
print(f"📄 {path}")
|
|
325
|
+
|
|
326
|
+
fetched = import_yda(args.account_id, args.campaign_id)
|
|
327
|
+
if not fetched["ads"] and not fetched["audiences"] and not fetched["placements"]:
|
|
328
|
+
sys.exit("✗ 取得できたデータがありません(ログイン状態・accountId を確認してください)")
|
|
329
|
+
|
|
330
|
+
merged, report = merge(account, fetched)
|
|
331
|
+
|
|
332
|
+
print("\n── 取り込み結果 ──")
|
|
333
|
+
for line in report:
|
|
334
|
+
print(f" {line}")
|
|
335
|
+
|
|
336
|
+
if not args.write:
|
|
337
|
+
print("\n(dry-run: 書き込んでいません。反映するには --write を付けてください)")
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
341
|
+
json.dump(merged, f, indent=1, ensure_ascii=False)
|
|
342
|
+
f.write("\n")
|
|
343
|
+
print(f"\n✓ 書き込みました: {path}(accountJsonVersion=2)")
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
sys.exit(main())
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"accountJsonVersion":
|
|
2
|
+
"accountJsonVersion": 2,
|
|
3
3
|
"accountId": "1002811292",
|
|
4
|
+
"mccId": "1002811292",
|
|
4
5
|
"name": "sample_account",
|
|
5
6
|
"principal": "サンプル主体者",
|
|
6
7
|
"projectName": "サンプル案件",
|
|
7
8
|
"baseArticleUrl": "https://example.com/lp/",
|
|
9
|
+
"urlParamsEnabled": true,
|
|
10
|
+
"adNamePolicy": "imageNo",
|
|
8
11
|
"refs": {
|
|
9
12
|
"titles": {
|
|
10
13
|
"1": "サンプル見出し その1(訴求ポイントA)",
|
|
@@ -27,14 +30,22 @@
|
|
|
27
30
|
"10000003": { "name": "サンプルキャンペーンC" }
|
|
28
31
|
},
|
|
29
32
|
"adGroups": {
|
|
30
|
-
"20000001": { "name": "【sample_a1】サンプル広告グループA1", "campaignId": "10000001", "paramCode": "sample_a1" },
|
|
31
|
-
"20000002": { "name": "【sample_b1】サンプル広告グループB1", "campaignId": "10000002", "paramCode": "sample_b1" },
|
|
33
|
+
"20000001": { "name": "【sample_a1】サンプル広告グループA1", "campaignId": "10000001", "paramCode": "sample_a1", "finalUrlSample": "https://example.com/lp/sample-a?ad=sample_a1&creative=sample_a1-1c-1L1-sample_a1" },
|
|
34
|
+
"20000002": { "name": "【sample_b1】サンプル広告グループB1", "campaignId": "10000002", "paramCode": "sample_b1", "finalUrlSample": "https://example.com/lp/sample-b?ad=sample_b1&creative=sample_b1-2c-2L2-sample_b1" },
|
|
32
35
|
"20000003": { "name": "【sample_c1】サンプル広告グループC1", "campaignId": "10000003", "paramCode": "sample_c1" }
|
|
33
36
|
},
|
|
34
37
|
"groups": {
|
|
35
38
|
"sample_a1": { "campaignId": "10000001", "adGroupId": "20000001", "groupName": "【sample_a1】サンプル広告グループA1" },
|
|
36
39
|
"sample_b1": { "campaignId": "10000002", "adGroupId": "20000002", "groupName": "【sample_b1】サンプル広告グループB1" },
|
|
37
40
|
"sample_c1": { "campaignId": "10000003", "adGroupId": "20000003", "groupName": "【sample_c1】サンプル広告グループC1" }
|
|
41
|
+
},
|
|
42
|
+
"audiences": {
|
|
43
|
+
"30000001": { "name": "サンプルオーディエンス(サイト訪問30日)", "kind": "audienceList" },
|
|
44
|
+
"30000002": { "name": "サンプルオーディエンス(購入者除外用)", "kind": "audienceList" }
|
|
45
|
+
},
|
|
46
|
+
"placements": {
|
|
47
|
+
"40000001": { "name": "サンプル配信面リスト(標準)", "kind": "standard" },
|
|
48
|
+
"40000002": { "name": "サンプル除外面リスト", "kind": "exclusion" }
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
51
|
}
|