addashboard-cli 0.7.0 → 0.8.1
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/package.json +1 -1
- package/skills/ad-ops/SKILL.md +9 -5
- package/skills/ad-ops/ad-planner.html +254 -94
- package/skills/ad-ops/scripts/refs_import.py +1 -1
- package/skills/ad-ops//343/202/242/343/202/253/343/202/246/343/203/263/343/203/210/yda/sample_account.json +2 -2
- package/skills/ad-ops//343/202/265/343/203/263/343/203/227/343/203/253/343/203/227/343/203/251/343/203/263.json +1 -1
package/package.json
CHANGED
package/skills/ad-ops/SKILL.md
CHANGED
|
@@ -51,7 +51,7 @@ skills/ad-ops/scripts/adops.sh preflight yda <accountId>
|
|
|
51
51
|
* **アカウントJSON(`アカウント/<媒体>/<名前>.json`)が存在することは、入稿できることを意味しない。**
|
|
52
52
|
参照データは過去に取り込んだものが残るだけで、現在の API 権限とは無関係
|
|
53
53
|
|
|
54
|
-
> 実例: 2026-08-06
|
|
54
|
+
> 実例: 2026-08-06 とある案件のアカウント。アカウントJSONは揃っていて dry-run も
|
|
55
55
|
> 全件パスしたが、`list accounts` が返す 17 件に含まれておらず本実行で 401。
|
|
56
56
|
> フェーズ1〜3 の作業がすべて無駄になった。
|
|
57
57
|
|
|
@@ -355,7 +355,8 @@ skills/ad-ops/scripts/adops.sh open yda <accountId> campaigns
|
|
|
355
355
|
| 入札 | 最大配信 / tCPA(`MAXIMIZE_CONVERSIONS`+targetCpa) | `LOWEST_COST_WITHOUT_CAP` / `LOWEST_COST_WITH_BID_CAP` / `COST_CAP` / `LOWEST_COST_WITH_MIN_ROAS`(後2者は `bidAmount` 必須) |
|
|
356
356
|
| ターゲティング列 | 年齢 / 性別 / デバイス / 曜日時間帯 / **オーディエンス(含む・除外)** / **配置(含む・除外)** / 地域・サーチKW。**オーディエンス・配置は参照表の実IDで入稿JSONに載り、本番の配信ターゲティングに反映される**(数値IDでない値は落として警告)。**入稿JSONに載らないのは 地域 と サーチKW だけ** | 年齢(ageMin/ageMax) / 性別 / デバイス / 配置(Facebook / Instagram / Audience Network / Messenger、既定は自動配置) |
|
|
357
357
|
| 命名規則 | `{番号}{c\|s}-{見出し}L{本文}-{paramCode}` / `?ad=&creative=` | **同形式を踏襲**(形式記号は `i`(画像) / `v`(動画))。**最終URLは `{記事URL}?ad={paramCode}` のみ**(`creative=` もパス連結もしない)。paramCode は**広告行 > 広告セット**の実効値 |
|
|
358
|
-
|
|
|
358
|
+
| 広告タイプ | 広告行の**「広告タイプ」列**で レスポンシブ(`RESPONSIVE_AD`) / バナー(`BANNER_AD`) を選ぶ。**同一キャンペーンで混在可**。バナーはタイトル/説明文/主体者/CTA が API に存在しないのでセルが「—」で無効化される | 区分なし(レスポンシブ相当のみ) |
|
|
359
|
+
| 書き出しの除外 | **未解決の広告のみ**除外(バナーは除外しない=API入稿対象) | 同じ(未解決の広告のみ除外) |
|
|
359
360
|
| 管理画面 | `https://ads.yahoo.co.jp/manager/#/{mccId}/{accountId}/list/campaigns`(**CP詳細への直リンクは不可**) | `adsmanager.facebook.com/adsmanager/manage/campaigns?act=<数値>` |
|
|
360
361
|
|
|
361
362
|
**Meta で未対応(今回スコープ外)**: カルーセル/コレクション等の複数素材フォーマット、カスタムオーディエンス/類似オーディエンスの ID 解決、地域ターゲティングの細分化(国単位のみ)、既存キャンペーンの複製テンプレート運用の GUI 化。**本番 Meta API への本入稿 PoC も未実施**。
|
|
@@ -481,7 +482,7 @@ npx -y addashboard-cli@latest list --platform yda --resource ads \
|
|
|
481
482
|
```
|
|
482
483
|
* **`campaigns --campaign-id`(campaign-import)はキャンペーン + 広告グループ設定までしか返さない**。広告の文言が要るときはこちらを使う
|
|
483
484
|
|
|
484
|
-
**【必須】アカウントJSONには `baseArticleUrl` を必ず入れる(YDA / Meta 共通)。** 記事ベースURL(例: `https://
|
|
485
|
+
**【必須】アカウントJSONには `baseArticleUrl` を必ず入れる(YDA / Meta 共通)。** 記事ベースURL(例: `https://example.com/lp/`)。
|
|
485
486
|
|
|
486
487
|
* 未設定だと `finalUrl` が `jv_C_1?ad=...` のような**相対パス**になり、**遷移先の壊れた広告が入稿される**
|
|
487
488
|
* **`submit-plan` の dry-run はこれを検出しない**(形式チェックのみ)。`adops.sh plan` の lint が ERROR で拾う
|
|
@@ -582,7 +583,7 @@ npx -y addashboard-cli@latest list --platform yda --resource ads \
|
|
|
582
583
|
- **履歴ビュー**: バージョン・日時・媒体・名称・メモ・作成者の一覧。`読込`(過去バージョン復元)/ `編集`(名称・メモ)/ `削除`。
|
|
583
584
|
- **入稿JSON書き出し**: 設定ダイアログなしの**ワンクリック**。参照未解決の広告は自動除外、accountId 未入力・0件なら書き出し中止(空入稿防止)。媒体別の中身:
|
|
584
585
|
- **YDA(見出し・本文の空欄許容)**: **タイトル番号 / 説明文番号は空欄でよい**(見出し・本文を使わない広告のケース)。空欄の広告はエラーにならず、`headlines` / `descriptions` は**空配列**で書き出され、backend も空を受理する。**空を許容するのはこの2フィールドのみ**(画像番号 `imageNo`・`paramCode`・principal・finalUrl は従来どおり必須)。**番号を入れたのに参照表(refs.titles / refs.descriptions)に無い場合は従来どおり「未解決」エラー**で、その広告は書き出しから除外される(別の障害なので緩めない)。Meta は従来どおり見出し・本文とも必須。
|
|
585
|
-
- **YDA**: アカウントID・目的(goal)・配信開始日は案件設定チップ、入札はキャンペーン行(tCPA→`MAXIMIZE_CONVERSIONS`+targetCpa)、ターゲティングは広告グループ行の列(日本語ラベル→API enum 自動変換)から `*_入稿計画_*.json`(planVersion:1、広告は IMAGE 形式: mediaIds/headlines/descriptions/principal
|
|
586
|
+
- **YDA**: アカウントID・目的(goal)・配信開始日は案件設定チップ、入札はキャンペーン行(tCPA→`MAXIMIZE_CONVERSIONS`+targetCpa)、ターゲティングは広告グループ行の列(日本語ラベル→API enum 自動変換)から `*_入稿計画_*.json`(planVersion:1、広告は IMAGE 形式: mediaIds/headlines/descriptions/principal)を生成。広告タイプ=バナーの行は `adType:"BANNER_AD"` + `mediaIds` のみ(テキスト系は出さない)。除外されるのは参照未解決の広告だけ。
|
|
586
587
|
- **Meta**: 予算は広告セット行(CBO は使わない)、目的は `objective`、最適化/課金/入札戦略/pixel/CVイベント/FBページID は案件設定チップ、ターゲティング(年齢・性別・デバイス・配置)は広告セット行の列から `*_Meta入稿計画_*.json`(planVersion:1、`platform:"meta"`、`campaigns[].adSets[]`、広告は IMAGE / VIDEO)を生成。`act_` 形式違反・FBページID 未入力・pixelId 必須ケースの未入力でブロック。 **配信日時(`startTime` / `endTime`)は広告セットごとに解決される(広告セット行 > 案件設定チップ > 既定=翌日05:00)。入力は `YYYY-MM-DD [HH:mm[:ss]]`・JST 前提で、書き出し時に `+0900` 付き ISO8601 に正規化される(時刻を明示した場合は 23:59:59 に丸めずその時刻を尊重)。`startTime` は常に送り、配信終了日が空欄なら `endTime` キー自体を送らない(無期限配信)。`lifetimeBudget` 指定時に限り backend が配信開始日+30日を補完する。**
|
|
587
588
|
|
|
588
589
|
### 量産構造(媒体要件の核)
|
|
@@ -595,7 +596,10 @@ npx -y addashboard-cli@latest list --platform yda --resource ads \
|
|
|
595
596
|
### 入稿モードと手入稿
|
|
596
597
|
|
|
597
598
|
- モード: **新規**(12,000円/日・1CP=1AG=1広告を量産)/ **既存ASC**(5万〜10万円/日・まとめ入稿)/ 既存-広告グループ追加 / 既存-広告追加。
|
|
598
|
-
- **YDA のバナー(
|
|
599
|
+
- **YDA のバナー(静止画・固定サイズ)は API 入稿に対応済**(2026-08-19 本番実機で `AdGroupAdService/add` 成功)。プランナーの広告行「広告タイプ」列で **レスポンシブ / バナー** を選ぶだけで、レスポンシブと同じ入稿計画JSON → `submit-plan` に乗る(**手入稿フラグという仕組みは存在しない**。過去のドキュメント記述だけが残っていた)。
|
|
600
|
+
- **画像プールは共用**: YDA のメディアはアカウントに1ライブラリだけで「バナー用/レスポンシブ用」の区分は無い。バナー行も参照表 `refs.media[画像番号].card` / `.sq` をそのまま引く(形式列 = 画像サイズ Card/SQ)。**広告タイプ(レスポンシブ/バナー)と 形式(Card/SQ)は独立した2軸**。
|
|
601
|
+
- **命名**: バナーはタイトル/説明文が存在しないので L 区間を出さない → `{画像番号}{c|s}-{paramCode}`(例 `957c-jv_Cac_148`)。レスポンシブは従来どおり `957c-45L60-jv_Cac_148`。
|
|
602
|
+
- 入稿計画JSON では `adType:"BANNER_AD"` が載り、`headlines` / `descriptions` / `principal` / `buttonText` は出さない(`adType` 省略時は `RESPONSIVE_AD` 扱い=旧プラン後方互換)。
|
|
599
603
|
|
|
600
604
|
### 発火時の案内
|
|
601
605
|
|
|
@@ -141,6 +141,19 @@
|
|
|
141
141
|
|
|
142
142
|
/* ── sheet ── */
|
|
143
143
|
.sheetwrap{background:#fff; border:1px solid var(--line); border-radius:var(--radius); overflow:auto; max-height:calc(100vh - 220px);}
|
|
144
|
+
/* ▼ シートタブ + 表示フィルタを1行に並べ、表の上辺に接合する (フォルダタブ風)。
|
|
145
|
+
.seg の共通スタイルは #modeSeg 等でも使うので壊さず、専用クラスで上書きする。
|
|
146
|
+
アクティブタブは表ヘッダー(table.sheet th)と同じ var(--soft) にして、境界を消して地続きに見せる。 */
|
|
147
|
+
.tabrow{display:flex; align-items:flex-end; justify-content:space-between; gap:12px; margin:0;}
|
|
148
|
+
.seg.sheettabs{display:flex; margin:0; padding:0; gap:2px; background:transparent; border:0; border-radius:0; position:relative; z-index:1;}
|
|
149
|
+
.seg.sheettabs button{border:1px solid transparent; border-bottom:0; background:transparent; color:var(--muted); border-radius:8px 8px 0 0; padding:7px 16px; margin-bottom:-1px;}
|
|
150
|
+
.seg.sheettabs button:hover:not(.active){background:var(--softer); color:var(--fg);}
|
|
151
|
+
/* 選択中タブは表ヘッダーと同色。下辺の境界を消して表と繋げる */
|
|
152
|
+
.seg.sheettabs button.active{background:var(--soft); color:var(--fg); border-color:var(--line); box-shadow:none; padding-bottom:8px;}
|
|
153
|
+
/* 表示フィルタは同じ行の右端。アクティブタブと同色で帯として揃える */
|
|
154
|
+
.filt.tabfilt{margin-bottom:6px;}
|
|
155
|
+
.filt.tabfilt select{background:var(--soft); border-color:var(--line);}
|
|
156
|
+
.sheetwrap.tabbed{border-top-left-radius:0; border-top-right-radius:0;}
|
|
144
157
|
table.sheet{border-collapse:separate; border-spacing:0; width:100%; font-size:12.5px; min-width:1180px; user-select:none; -webkit-user-select:none;}
|
|
145
158
|
table.sheet input.cell-in{user-select:text; -webkit-user-select:text;}
|
|
146
159
|
table.sheet th{position:sticky; top:0; z-index:5; background:var(--soft); color:#3a4150; font-weight:700; font-size:11px; letter-spacing:.02em; text-align:left; padding:8px 9px; border-bottom:1px solid var(--line); white-space:nowrap;}
|
|
@@ -181,7 +194,6 @@
|
|
|
181
194
|
table.sheet tr.cphead .cptitle{font-size:12.5px; color:var(--fg); display:flex; align-items:center; gap:8px;}
|
|
182
195
|
table.sheet tr.adrow:hover td{background:#fcfdff;}
|
|
183
196
|
table.sheet tr.adrow.hasErr td{background:var(--danger-soft);}
|
|
184
|
-
table.sheet tr.adrow.banner td{background:#faf9fc;}
|
|
185
197
|
.cell-in{width:100%; border:1px solid transparent; background:transparent; border-radius:5px; padding:4px 6px; font-family:inherit; font-size:12.5px; color:var(--fg);}
|
|
186
198
|
.cell-in:hover{border-color:var(--line);}
|
|
187
199
|
.cell-in:focus{outline:none; border-color:var(--accent); background:#fff; box-shadow:0 0 0 2px var(--accent-soft);}
|
|
@@ -195,6 +207,7 @@
|
|
|
195
207
|
.medibadge{display:inline-flex; align-items:center; gap:6px; font-size:11px; flex-shrink:0; white-space:nowrap;}
|
|
196
208
|
.medibadge .sw{width:26px; height:26px; border-radius:5px; background:linear-gradient(135deg,#e9ecf5,#dfe3ee); display:inline-flex; align-items:center; justify-content:center; font-size:9px; color:#6b7280; font-weight:700; flex-shrink:0;}
|
|
197
209
|
.idpill{font-family:var(--mono); font-size:10.5px; color:var(--muted);}
|
|
210
|
+
.celldis{color:var(--faint); cursor:not-allowed;} /* その行には存在しない列 (バナー行のタイトル/説明文) */
|
|
198
211
|
.robadge{display:inline-block; font-size:12px; color:var(--muted); padding:2px 2px; max-width:170px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:middle;} /* 既存行の編集不可セル表示 */
|
|
199
212
|
.cell-in.roinput{background:var(--soft); color:var(--muted); cursor:not-allowed;} /* 既存行の read-only 入力欄 */
|
|
200
213
|
.idpill.warn{color:var(--amber);} .idpill.err{color:var(--danger);}
|
|
@@ -225,7 +238,6 @@
|
|
|
225
238
|
.agnode.collapsed .adwrap{display:none;}
|
|
226
239
|
.adnode{display:flex; align-items:center; gap:9px; padding:7px 9px; border:1px solid var(--line2); border-radius:8px; background:#fff;}
|
|
227
240
|
.adnode.hasErr{border-color:var(--danger); background:var(--danger-soft);}
|
|
228
|
-
.adnode.banner{background:#faf9fc;}
|
|
229
241
|
.dot{width:8px;height:8px;border-radius:50%; flex-shrink:0;}
|
|
230
242
|
.dot.ok{background:var(--green);} .dot.err{background:var(--danger);} .dot.warn{background:var(--amber);}
|
|
231
243
|
.adttl{font-size:12.5px; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
|
|
@@ -384,7 +396,7 @@
|
|
|
384
396
|
<div class="seg" id="ydaTabs" style="display:inline-flex; margin-bottom:14px">
|
|
385
397
|
<button data-yt="overview" class="active">概要</button>
|
|
386
398
|
<button data-yt="resp">レスポンシブ</button>
|
|
387
|
-
<button data-yt="banner"
|
|
399
|
+
<button data-yt="banner">バナー</button>
|
|
388
400
|
<button data-yt="param">パラメータ</button>
|
|
389
401
|
<button data-yt="guide">使い方</button>
|
|
390
402
|
</div>
|
|
@@ -449,7 +461,7 @@
|
|
|
449
461
|
<tr><td><b>レスポンシブ(画像)・固定型</b></td><td><span class="pill green">API入稿 ★主用途</span></td><td>★★★</td><td>タイトル/説明文/画像を人が1通りに固定し、成果を自分で検証。<code>Card</code>(1200x628)/<code>SQ</code>(1080x1080) の2形式。API上は <code>RESPONSIVE_AD + mainMediaFormat:IMAGE</code>、画像は <code>asset.mediaAssets[].mediaId</code>(PR #82・実API疎通は検証中)</td></tr>
|
|
450
462
|
<tr><td>レスポンシブ(画像)・アセット型</td><td><span class="pill amber">非対象</span></td><td>—</td><td>複数タイトル/画像を登録しYahoo側が自動最適化。<b>1広告=単一組合せの本ツールでは非対象</b></td></tr>
|
|
451
463
|
<tr><td>レスポンシブ(動画)</td><td><span class="pill gray">別フロー(CLI)</span></td><td>—</td><td><code>webgram upload-video → submit</code> のAPI入稿(本番実績あり)。<code>thumbnailFlg:'TRUE'</code> のサムネ画像は動画専用で、画像広告のメイン画像とは排他</td></tr>
|
|
452
|
-
<tr><td>バナー(静止画・固定サイズ)</td><td><span class="pill
|
|
464
|
+
<tr><td><b>バナー(静止画・固定サイズ)</b></td><td><span class="pill green">API入稿 対応済</span></td><td>★★★</td><td>広告行の<b>広告タイプ列で「バナー」</b>を選ぶ(画像は形式列=Card/SQ のまま、<b>レスポンシブと同じ画像プールを共用</b>)。API上は <code>BANNER_AD + mainMediaFormat:IMAGE</code>、画像は <b>operand 直下の <code>mediaId</code></b>。<b>タイトル/説明文/主体者/CTAはAPIフィールド自体が無い</b>(画像に焼き込み)。2026-08-19 に本番実機で add 成功・読み戻し検証済み</td></tr>
|
|
453
465
|
<tr><td>カルーセル / テキスト</td><td><span class="pill gray">未使用</span></td><td>—</td><td>運用では使わない</td></tr>
|
|
454
466
|
</tbody></table>
|
|
455
467
|
</div></details>
|
|
@@ -463,7 +475,8 @@
|
|
|
463
475
|
<tr><td>入札戦略</td><td><b>tCPA 8,500円</b></td><td>API値は <code>MAXIMIZE_CONVERSIONS + targetCpa:8500</code>。もう一択は「最大配信」</td></tr>
|
|
464
476
|
<tr><td>予算</td><td>新規 12,000 / ASC 50,000〜</td><td>モードで切替</td></tr>
|
|
465
477
|
<tr><td>配信設定</td><td><b>オフ (PAUSED)</b></td><td>誤配信防止。入稿後に手動オン</td></tr>
|
|
466
|
-
<tr><td
|
|
478
|
+
<tr><td>広告タイプ</td><td>レスポンシブ</td><td><b>既定値</b>。広告行の<b>広告タイプ列</b>で「バナー」(API: <code>BANNER_AD</code>)に切替できる。同一キャンペーン内で混在可</td></tr>
|
|
479
|
+
<tr><td>メインメディア形式</td><td>画像</td><td>固定(API: <code>mainMediaFormat:IMAGE</code>)</td></tr>
|
|
467
480
|
<tr><td>主体者表記</td><td><b>アカウントJSON由来</b></td><td>案件固定(薬機/景表対応)。アカウント/<名前>.json の <code>principal</code>。API: <code>responsiveAd.principal</code></td></tr>
|
|
468
481
|
<tr><td>表示URLの形式</td><td>ドメインのみ表示する</td><td>固定(API: <code>displayUrlLevel:'DOMAIN'</code>)</td></tr>
|
|
469
482
|
</tbody></table>
|
|
@@ -476,6 +489,8 @@ crName = 画像番号 + (形式=="SQ" ? "<span class="k">s</span>" : "<span cl
|
|
|
476
489
|
<span class="c">// 広告名 (col5)</span>
|
|
477
490
|
adName = crName + "-" + タイトル番号 + "<span class="k">L</span>" + 説明文番号 + "-" + paramCode
|
|
478
491
|
<span class="c">→ 957c-45L60-jv_Cac_148</span>
|
|
492
|
+
<span class="c">// 広告タイプ=バナーはタイトル/説明文が存在しないので L 区間を出さない</span>
|
|
493
|
+
adName(バナー) = crName + "-" + paramCode <span class="c">→ 957c-jv_Cac_148</span>
|
|
479
494
|
<span class="c">// キャンペーン/広告グループID … paramCode で参照表を引く</span>
|
|
480
495
|
campaignId, adGroupId = groups[ paramCode ] <span class="c">→ 32525263 / 451760490</span>
|
|
481
496
|
<span class="c">// メディアID … 画像番号+形式で参照表を引く</span>
|
|
@@ -492,16 +507,26 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
492
507
|
</div></details>
|
|
493
508
|
</div>
|
|
494
509
|
<div id="yt-banner" style="display:none">
|
|
495
|
-
<details class="sec" open><summary><span class="num">1</span><span class="stitle">バナー(静止画・固定サイズ)—
|
|
510
|
+
<details class="sec" open><summary><span class="num">1</span><span class="stitle">バナー(静止画・固定サイズ)— API入稿 対応済 ⭐️⭐️⭐️</span><span class="chev"></span></summary>
|
|
496
511
|
<div class="body">
|
|
497
|
-
<p class="lead"
|
|
512
|
+
<p class="lead">利用頻度が高い(⭐️⭐️⭐️)形式。<b>2026-08-19 に本番実機で <code>AdGroupAdService/add</code> 成功・読み戻し検証済み</b>。以後は<b>レスポンシブと同じ入稿計画JSON→<code>webgram submit-plan</code> のAPI入稿</b>に乗る(管理画面での手入稿は不要)。</p>
|
|
513
|
+
<div class="callout"><b>広告行には2つの独立した軸がある</b>。<b>「広告タイプ」列</b>=レスポンシブ(<code>RESPONSIVE_AD</code>)/バナー(<code>BANNER_AD</code>)、<b>「形式」列</b>=画像のサイズ(<code>Card</code> 1200x628 / <code>SQ</code> 1080x1080)。<b>同一キャンペーン内にレスポンシブ行とバナー行を混在</b>できる。かつてドキュメントに書いていた「手入稿フラグ」は<b>実装には存在しなかった</b>(記述だけが残っていた)。</div>
|
|
514
|
+
<div class="callout"><b>YDA のメディアはアカウントに1ライブラリだけ</b>(2026-08-19 <code>MediaService</code> 実機調査)。「バナー用/レスポンシブ用」の区分は<b>存在しない</b>ので、<b>バナーとレスポンシブは同じ画像プールを共用する</b>。画像が持つのは <code>mediaAdFormat</code>(<code>YJ_1200_628</code> / <code>SQUARE_600</code> 等・<b>画像から自動判定</b>)/<code>width</code>/<code>height</code>/<code>approvalStatus</code>/<code>logoFlg</code>/<code>thumbnailFlg</code> で、<b>サイズを指定する場所は API のどこにも無い</b>(アップロードは base64 のみ、広告作成は <code>mediaId</code> のみ)。実測でも稼働中バナー広告の <code>mediaId 22103986</code> は <code>SQUARE_600</code> / 1080x1080 = <b>レスポンシブの SQ と同じ枠の画像</b>だった。なお <code>webgram list --platform yda --resource media</code> が返すのは <code>mediaId</code> / <code>mediaName</code> / <code>mediaType</code> / <code>thumbnailFlg</code> の4つだけなので、参照表の <code>card</code>/<code>sq</code> 振り分けは <code>mediaName</code>(<code>954-1200.jpg</code> 等)から判断する。</div>
|
|
498
515
|
<table class="g"><thead><tr><th>項目</th><th>内容</th></tr></thead><tbody>
|
|
499
|
-
<tr><td
|
|
500
|
-
<tr><td
|
|
501
|
-
<tr><td
|
|
502
|
-
<tr><td
|
|
503
|
-
<tr><td>将来対応</td><td>API での <code>BANNER_AD</code> 作成対応を検討中(未着手)。対応後は手入稿フラグを外すだけで同じフローに乗る想定</td></tr>
|
|
516
|
+
<tr><td>プランナーでの扱い</td><td>広告行の<b>広告タイプ列 = 「バナー」</b>。タイトル番号/説明文番号/タイトル本文/説明文本文のセルは <b>「—」で無効化</b>され、未入力でもバリデーションエラーにならない(列自体はレスポンシブ行と混在するため隠さない)</td></tr>
|
|
517
|
+
<tr><td>画像の参照</td><td><b>レスポンシブと同じ</b> 参照表 <code>refs.media[画像番号].card</code>/<code>.sq</code>(形式列で選ぶ)。<b>バナー専用の画像プールは無い</b>。未登録なら 🆕未登録 として画像パスからアップロード入稿する経路に乗る</td></tr>
|
|
518
|
+
<tr><td>命名規則</td><td>サイズ記号(<code>c</code>/<code>s</code>)は残し、<b>タイトル/説明文番号の区間を出さない</b> → <code>{画像番号}{c|s}-{paramCode}</code>(例 <code>957c-jv_Cac_148</code>)。レスポンシブは従来どおり <code>957c-45L60-jv_Cac_148</code></td></tr>
|
|
519
|
+
<tr><td>入稿計画JSON</td><td><code>ads[]</code> に <code>adType:"BANNER_AD"</code> が載り、<b><code>headlines</code>/<code>descriptions</code>/<code>principal</code>/<code>buttonText</code> は出力しない</b>。<code>adType</code> <b>省略時は <code>RESPONSIVE_AD</code> 扱い</b>(旧プランJSONの後方互換)</td></tr>
|
|
504
520
|
</tbody></table>
|
|
521
|
+
<div class="fx"><span class="c">// 本番実測で確定した AdGroupAdService/add の operand</span>
|
|
522
|
+
{ accountId, campaignId, adGroupId, adName, userStatus,
|
|
523
|
+
<span class="k">mediaId</span>: <画像mediaId>, <span class="c">// operand 直下 (レスポンシブ createImageAd と同じ位置)</span>
|
|
524
|
+
ad: { adType: '<span class="k">BANNER_AD</span>', mainMediaFormat: '<span class="k">IMAGE</span>',
|
|
525
|
+
bannerAd: {}, <span class="c">// IMAGE のときは空オブジェクト (中身は動画専用フィールドのみ)</span>
|
|
526
|
+
finalUrl: <URL>,
|
|
527
|
+
displayUrlLevel: 'DOMAIN' } <span class="c">// displayUrl は送らない (finalUrl から自動生成される)</span> }
|
|
528
|
+
<span class="c">// headline / description / principal / buttonText / logoMediaId は BANNER_AD に**フィールド自体が無い**</span>
|
|
529
|
+
<span class="c">// 読み戻すと ad.responsiveAd === null になる</span></div>
|
|
505
530
|
</div></details>
|
|
506
531
|
</div>
|
|
507
532
|
<div id="yt-param" style="display:none">
|
|
@@ -511,18 +536,20 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
511
536
|
<tr><td><b>案件設定チップ</b></td><td>案件名 / <b>アカウントID</b> / <b>目的(goal)</b> / <b>配信開始日時</b> / CTA / 配信 / タイプ / 入札既定(tCPA・目標CPA) / フリークエンシー / 主体者 / 表示URL / 記事URL / 予算(新規・ASC)</td><td>accountId・goal・startDate・入札既定・予算既定・finalUrlベース</td></tr>
|
|
512
537
|
<tr><td><b>キャンペーン行</b></td><td>キャンペーン名 / 入稿モード / 1日の予算 / 入札戦略 / 目標CV単価(tCPA) / 配信設定 / 入稿リンク番号</td><td>name・budget・biddingStrategy+targetCpa・status</td></tr>
|
|
513
538
|
<tr><td><b>広告グループ行</b></td><td>広告グループ名 / paramCode / 入札価格 / 配信設定 / スマートTG / <b>ターゲティング列</b>(デバイス・性別・年齢・地域・曜日時間帯・<b>オーディエンス(含む/除外)</b>・<b>配置(含む/除外)</b>・サーチKW)</td><td>name・status・smartTargeting・devices/ages/genders/days(+startHour/endHour)・<b>audiencesInclude/Exclude・placementsInclude/Exclude(実IDで送信)</b>。<b>未反映は 地域・サーチKW のみ</b></td></tr>
|
|
514
|
-
<tr><td><b>広告行</b></td><td
|
|
539
|
+
<tr><td><b>広告行</b></td><td>形式(Card/SQ) / <b>広告タイプ(レスポンシブ/バナー)</b> / 画像番号 / タイトル番号 / 説明文番号 / 最終リンク先URL(自動) / タイトル本文 / 説明文本文 / 画像パス / 広告名</td><td>ads[].adName・<b>adType</b>・mediaIds・imagePaths・headlines・descriptions・finalUrl(未解決は除外。<b>バナー行はテキスト系を出力しない</b>)</td></tr>
|
|
515
540
|
</tbody></table>
|
|
516
541
|
</div></details>
|
|
517
542
|
<details class="sec" open><summary><span class="num">2</span><span class="stitle">入稿JSON — 構造と入力項目・制約(パラメータ)</span><span class="chev"></span></summary>
|
|
518
543
|
<div class="body">
|
|
519
|
-
<p class="lead">「入稿JSON書き出し」はワンクリックで、<b>画面のプランをそのまま</b>入稿計画JSON(<code>planVersion:1</code>)に変換する(設定ダイアログなし)。<code>webgram submit-plan --plan-file <このファイル></code> が dry-run で全キャンペーンを一括検証し、本実行では1キャンペーンずつ入稿・進捗をファイルに書き戻す(再開可能・二重入稿防止)。広告は backend <code>ydaAdSchema</code> の IMAGE
|
|
544
|
+
<p class="lead">「入稿JSON書き出し」はワンクリックで、<b>画面のプランをそのまま</b>入稿計画JSON(<code>planVersion:1</code>)に変換する(設定ダイアログなし)。<code>webgram submit-plan --plan-file <このファイル></code> が dry-run で全キャンペーンを一括検証し、本実行では1キャンペーンずつ入稿・進捗をファイルに書き戻す(再開可能・二重入稿防止)。広告は backend <code>ydaAdSchema</code> の IMAGE 形式(<code>adType</code> で レスポンシブ/バナーを切り替える。画像プールは共通)。未解決の広告は自動除外。</p>
|
|
520
545
|
<div class="fx">{ planVersion: 1, platform: "yda", accountId,
|
|
521
546
|
campaigns: [ { name, goal, status, startDate, budget, biddingStrategy, targetCpa?, cpc?,
|
|
522
547
|
adGroups: [ { name, status, smartTargeting, devices, ages, genders, days, startHour?, endHour?,
|
|
523
548
|
placementsInclude, placementsExclude, audiencesInclude, audiencesExclude,
|
|
524
|
-
ads: [ { adName, status, mediaType:"IMAGE",
|
|
525
|
-
|
|
549
|
+
ads: [ { adName, status, mediaType:"IMAGE", adType:"RESPONSIVE_AD"|"BANNER_AD",
|
|
550
|
+
mediaIds[], headlines[], descriptions[], principal, buttonText,
|
|
551
|
+
finalUrl } ] } ] } ], // adType 省略時は RESPONSIVE_AD 扱い
|
|
552
|
+
// BANNER_AD は headlines/descriptions/principal/buttonText を出さない
|
|
526
553
|
progress?: [...] // submit-plan が書き戻す進捗 (再開・二重入稿防止) }</div>
|
|
527
554
|
<table class="g"><thead><tr><th>項目</th><th>画面上の出どころ</th><th>変換・制約</th></tr></thead><tbody>
|
|
528
555
|
<tr><td class="mono">accountId</td><td><b>案件設定「アカウントID」</b></td><td>必須。未入力だと書き出し中止</td></tr>
|
|
@@ -536,7 +563,8 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
536
563
|
<tr><td class="mono">days / startHour / endHour</td><td>広告グループ行「曜日・時間帯」</td><td><code>月→MONDAY</code> 等。<code>平日</code>=月〜金、<code>土日祝</code>=土日 に展開。時間帯(午前(6-12時)等)は <code>startHour/endHour</code> に変換(複数選択時は最初の1つ)</td></tr>
|
|
537
564
|
<tr><td class="mono">audiences* / placements*</td><td>広告グループ行「オーディエンス(除外)」「配置(除外)」</td><td><b>参照表(refs.audiences / refs.placements)の実IDで送信される</b>(表示は name)。数値IDでない値(旧・日本語ラベルや手入力の名前)は落として書き出し時に警告。<b>JSONに反映されない列は「地域」「サーチキーワード」だけ</b>(入稿後に管理画面で設定)</td></tr>
|
|
538
565
|
<tr><td class="mono">ads[].adName</td><td>広告行「広告名」(空欄なら<span class="pill">自動</span>)</td><td>空欄時の生成式 <code>{画像番号}{c|s}-{タイトル番号}L{説明文番号}-{paramCode}</code>。手入力するとその名前が使われ、<code>finalUrl</code> の <code>creative=</code> も変わる</td></tr>
|
|
539
|
-
<tr><td class="mono">ads[].
|
|
566
|
+
<tr><td class="mono">ads[].adType</td><td>広告行「広告タイプ」</td><td><code>レスポンシブ → RESPONSIVE_AD</code>・<code>バナー → BANNER_AD</code>。<b>省略時は RESPONSIVE_AD 扱い</b>(旧プランJSONの後方互換)。BANNER_AD では <code>headlines / descriptions / principal / buttonText</code> を<b>出力しない</b>(APIフィールド自体が無い)</td></tr>
|
|
567
|
+
<tr><td class="mono">ads[].mediaIds</td><td><span class="pill">自動</span></td><td>画像番号+形式 → 参照表の mediaId(<code>media[番号].card / .sq</code>。バナーも同じプールを使う)。未解決は入稿対象外</td></tr>
|
|
540
568
|
<tr><td class="mono">ads[].imagePaths</td><td>広告行「画像パス」</td><td><b>参照表に無い画像番号(未登録)のときだけ</b>送る。CLI が入稿前にアップロードして mediaId を解決する。画像番号が解決済みの行に入れても<b>送らない</b>(CLI は追加アップロードするが広告には既存 mediaId が使われるため。差し替えたいときは画像番号を空にする)</td></tr>
|
|
541
569
|
<tr><td class="mono">ads[].headlines</td><td><span class="pill purple">可変</span></td><td>タイトル番号 → 文言。<b>35字以内</b>(API上のアセット制約はタイトル≤20字・説明文≤90字)</td></tr>
|
|
542
570
|
<tr><td class="mono">ads[].descriptions</td><td><span class="pill purple">可変</span></td><td>説明文番号 → 文言</td></tr>
|
|
@@ -549,7 +577,7 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
549
577
|
<ul>
|
|
550
578
|
<li><b>タイトル 35文字以内</b>(タイトル集ヘッダに明記)。超過はシート/ツリーで赤表示。</li>
|
|
551
579
|
<li><b>メディアID未解決</b>:画像番号が参照表に無い→入稿不可(実シートの <code>#N/A</code>)。赤エラー。</li>
|
|
552
|
-
<li><b>タイトル/説明文番号未解決</b
|
|
580
|
+
<li><b>タイトル/説明文番号未解決</b>:文言ライブラリに無い→赤エラー。<b>広告タイプ=バナー の行はタイトル/説明文セルが無効化され、この検証の対象外</b>(BANNER_AD にAPI項目が無いため)。</li>
|
|
553
581
|
<li><b>paramCode未払出</b>:参照表に無い→黄色警告(新規は想定内、既存は要確認)。</li>
|
|
554
582
|
<li>説明文は同じ文言ライブラリを共有するが、タイトルより長文可のため35字制限は<b>タイトル枠のみ</b>に適用。</li>
|
|
555
583
|
</ul>
|
|
@@ -560,7 +588,7 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
560
588
|
<div class="body">
|
|
561
589
|
<ol>
|
|
562
590
|
<li><b>プラン作成</b>:プランビューは「戦略メモ → 案件設定 → シート/ツリー切替」の順。シート選択時はさらに広告/広告グループ/キャンペーンのタブで編集する。AI が生成した state JSON は「JSON読込」で取り込む(旧v1形式も自動マイグレーション)。</li>
|
|
563
|
-
<li><b>確認</b>:バリデーションバッジ(タイトル35字超・参照未解決・paramCode
|
|
591
|
+
<li><b>確認</b>:バリデーションバッジ(タイトル35字超・参照未解決・paramCode未払出)が緑になるまで修正。バナーは広告行の「広告タイプ」列で「バナー」を選ぶ(タイトル/説明文は無効化される。画像はレスポンシブと同じ Card/SQ を使う)。</li>
|
|
564
592
|
<li><b>バージョン保存</b>:ヘッダーの「バージョン保存」。保存のたびに<b>常に新バージョン</b>(v1, v2…)として名称・メモ・作成者付きで保存される(上書きなし)。<b>履歴本体はブラウザ内に自動保存</b>されるため、フォルダ設定なしでも一覧・再読込できる。</li>
|
|
565
593
|
<li><b>保存フォルダ接続(初回に1回だけ)</b>:履歴ビューの「📁 保存フォルダに接続」でスキル同梱の <code>入稿計画/</code> フォルダを一度選ぶと、<b>次回からは開くだけで自動接続</b>され、保存のたびにファイルにも書き出される。未接続時はダウンロード動作。</li>
|
|
566
594
|
<li><b>履歴の確認・再利用</b>:履歴ビューで一覧(Ver/日時/媒体/名称)を確認し、「読込」で過去バージョンを復元、「編集」で名称・メモ変更、「削除」で破棄。</li>
|
|
@@ -571,7 +599,7 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
571
599
|
<details class="sec" open><summary><span class="num">2</span><span class="stitle">注意事項・未対応事項(要確認)</span><span class="chev"></span></summary>
|
|
572
600
|
<div class="body">
|
|
573
601
|
<ul>
|
|
574
|
-
<li><b>バナー(固定サイズ)
|
|
602
|
+
<li><b>バナー(固定サイズ)もAPI入稿に対応済</b>(2026-08-19 本番実機で確認) → 広告行の<b>広告タイプ列</b>で「バナー」を選ぶとレスポンシブと同じフローで入稿できる(画像プールも共通)。</li>
|
|
575
603
|
<li><b>画像レスポンシブのAPI入稿(PR #82)は実API疎通が未検証</b>。本入稿前に必ず dry-run → 少数テスト入稿で確認。</li>
|
|
576
604
|
<li>CTA既定が「もっとみる」(画像) か「詳細を見る」(動画) か。画像/動画でCTA既定が異なる可能性。<code>buttonText</code> のenum値も要実測。</li>
|
|
577
605
|
<li>年齢区分enumは実装内に系統差あり(<code>GT_RANGE*</code> が本番採用)。公式ドキュメント裏取りは未記録のため本入稿前に確認推奨。</li>
|
|
@@ -660,34 +688,30 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
660
688
|
<summary>共通設定</summary>
|
|
661
689
|
<div class="chipset" id="tplChips"></div>
|
|
662
690
|
</details>
|
|
663
|
-
<!-- E-1: プランツールバー(タイプ固定プルダウンのみ。キャンペーン追加導線はシート/ツリー側に配置) -->
|
|
664
|
-
<div class="toolbar2" id="planTools" style="margin:0 0 10px">
|
|
665
|
-
<span class="chip" title="このプランナーはレスポンシブ(画像)固定です(変更不可)">タイプ
|
|
666
|
-
<select class="chipin" id="typeAdType" disabled title="広告タイプ(固定)"><option>レスポンシブ</option></select>
|
|
667
|
-
<select class="chipin" id="typeMediaFormat" disabled title="メインメディアの形式(固定)"><option>画像</option></select>
|
|
668
|
-
</span>
|
|
669
|
-
</div>
|
|
670
691
|
<div class="seg" id="modeSeg" style="margin:0 0 10px">
|
|
671
692
|
<button data-m="sheet" class="active">シート</button>
|
|
672
693
|
<button data-m="tree">ツリー</button>
|
|
673
694
|
</div>
|
|
674
695
|
<div id="sheetArea">
|
|
675
|
-
<div class="seg" id="sheetTabs" style="margin:0 0 10px">
|
|
676
|
-
<button data-st="ads" class="active">広告</button>
|
|
677
|
-
<button data-st="groups">広告グループ</button>
|
|
678
|
-
<button data-st="campaigns">キャンペーン</button>
|
|
679
|
-
</div>
|
|
680
696
|
<div class="toolbar2">
|
|
681
697
|
<button class="act ghost" id="btnDupSel" title="選択した行を複製">⧉ 選択を複製</button>
|
|
682
698
|
<button class="act ghost" id="btnSeqParam" style="display:none" title="選択した列(paramCode / 画像番号 / タイトル番号 / 説明文番号)を先頭セル基点に連番採番">🔢 連番</button>
|
|
683
699
|
<span class="hinttxt">セル選択(クリック/Shift+クリック/ドラッグ)→ Cmd/Ctrl+C コピー・Cmd/Ctrl+V 貼付(1セルのコピーは選択範囲全体へ充填)/範囲選択のままプルダウン・入力欄を確定すると同じ列へ一括、ターゲティング列はサイドバーで一括ON/OFF</span>
|
|
684
|
-
|
|
685
|
-
|
|
700
|
+
</div>
|
|
701
|
+
<!-- シートタブと表示フィルタは同じ行に並べ、表の上辺に接合する (フォルダタブ風)。
|
|
702
|
+
id と data-st の構造は E2E が依存するので変えない -->
|
|
703
|
+
<div class="tabrow">
|
|
704
|
+
<div class="seg sheettabs" id="sheetTabs">
|
|
705
|
+
<button data-st="ads" class="active">広告</button>
|
|
706
|
+
<button data-st="groups">広告グループ</button>
|
|
707
|
+
<button data-st="campaigns">キャンペーン</button>
|
|
708
|
+
</div>
|
|
709
|
+
<span class="filt tabfilt">表示 <select id="sheetFilter">
|
|
686
710
|
<option value="all">すべて</option>
|
|
687
711
|
<option value="err">エラーのみ</option>
|
|
688
712
|
</select></span>
|
|
689
713
|
</div>
|
|
690
|
-
<div class="sheetwrap"><div id="sheetHost"></div></div>
|
|
714
|
+
<div class="sheetwrap tabbed"><div id="sheetHost"></div></div>
|
|
691
715
|
</div>
|
|
692
716
|
<div id="treeArea" hidden>
|
|
693
717
|
<div class="toolbar2">
|
|
@@ -920,18 +944,6 @@ function renderChrome(){
|
|
|
920
944
|
return '<button data-rt="'+esc(k)+'"'+(k===refTab?' class="active"':'')+'>'+esc(T.countLabel||T.label||k)+'</button>';
|
|
921
945
|
}).join('');
|
|
922
946
|
}
|
|
923
|
-
renderFixedType();
|
|
924
|
-
}
|
|
925
|
-
/* 固定タイプチップ (変更不可) は媒体定義 pf().fixedType から描画する。
|
|
926
|
-
YDA=レスポンシブ/画像、Meta=単一画像 / 単一動画。コアに媒体語彙を持たない。 */
|
|
927
|
-
function renderFixedType(){
|
|
928
|
-
const host=document.getElementById('planTools'); if(!host) return;
|
|
929
|
-
const list=(pf()&&pf().fixedType)||[];
|
|
930
|
-
if(!list.length){ host.innerHTML=''; host.hidden=true; return; }
|
|
931
|
-
host.hidden=false;
|
|
932
|
-
host.innerHTML='<span class="chip" title="このプランナーは固定タイプです(変更不可)">タイプ'
|
|
933
|
-
+ list.map(f=>'<select class="chipin" disabled title="'+esc(f.label)+'(固定)"><option>'+esc(f.value)+'</option></select>').join('')
|
|
934
|
-
+ '</span>';
|
|
935
947
|
}
|
|
936
948
|
window.switchPlatform=function(id){
|
|
937
949
|
if(!PLATFORMS[id]){ toast('この媒体はまだ選択できません(媒体スキーマ未登録)'); return false; }
|
|
@@ -1009,8 +1021,10 @@ const REF_TABLES = {
|
|
|
1009
1021
|
keyKind:'number', keyLabel:'画像番号', keyWidth:'80px',
|
|
1010
1022
|
addSub:'画像番号→Card/SQ の mediaID。新規作成した画像をメディアに登録',
|
|
1011
1023
|
emptyMsg:'該当する画像がありません',
|
|
1012
|
-
|
|
1013
|
-
|
|
1024
|
+
/* YDA のメディアライブラリはアカウントに1つだけ (バナー専用プールは存在しない)。
|
|
1025
|
+
バナー広告もここの card / sq をそのまま使う。 */
|
|
1026
|
+
valueFields:[{k:'card', label:'Card mediaID (1200)', editLabel:'Card mediaID'},
|
|
1027
|
+
{k:'sq', label:'SQ mediaID (1080)', editLabel:'SQ mediaID'}],
|
|
1014
1028
|
keyCell:no=>'<td><span class="sw" style="display:inline-flex;width:26px;height:26px;vertical-align:middle">'+esc(no)+'</span> '+newBadge('media',no)+'</td>',
|
|
1015
1029
|
columns:[
|
|
1016
1030
|
{label:'Card mediaID (1200)', cls:'mono', cell:(no,v)=>esc(v&&v.card||'—')},
|
|
@@ -1018,7 +1032,7 @@ const REF_TABLES = {
|
|
|
1018
1032
|
{label:'ファイル名', cls:'mono', style:'color:var(--faint);font-size:11px',
|
|
1019
1033
|
cell:no=>esc(no)+'-1200.jpg / '+esc(no)+'-1080.jpg'}
|
|
1020
1034
|
],
|
|
1021
|
-
optionLabel:v=>
|
|
1035
|
+
optionLabel:v=>[v&&v.card?'Card':'', v&&v.sq?'SQ':''].filter(Boolean).join('/'),
|
|
1022
1036
|
/* ▼ドロワー(一覧)の1行テキスト。媒体ごとの値の見せ方はここだけが持つ */
|
|
1023
1037
|
drawerLabel:v=>'Card '+esc(v&&v.card||'—')+' / SQ '+esc(v&&v.sq||'—') },
|
|
1024
1038
|
adGroups:{ key:'adGroups', label:'広告グループ', countLabel:'広告グループ', editable:false, required:false,
|
|
@@ -1120,11 +1134,24 @@ PLATFORMS.yda = {
|
|
|
1120
1134
|
consoleUrl:'https://ads.yahoo.co.jp/manager/#/{mcc}/{accountId}/list/{resource}',
|
|
1121
1135
|
consoleResourceMap:{campaigns:'campaigns', adGroups:'adgroups', ads:'ads'},
|
|
1122
1136
|
|
|
1123
|
-
/* ---- 形式(Card/SQ)
|
|
1137
|
+
/* ---- 形式(Card/SQ) = **画像のサイズ軸**。クリエイティブ名サフィックス・media 値のキーの対応 ---- */
|
|
1138
|
+
/* YDA のメディアライブラリは **アカウントに1つだけ** で、バナー用/レスポンシブ用の区分は存在しない
|
|
1139
|
+
(2026-08-19 MediaService 実機調査)。画像が持つのは mediaAdFormat(YJ_1200_628 / SQUARE_600 等・画像から自動判定)
|
|
1140
|
+
/ width / height / approvalStatus で、サイズを指定する場所は API のどこにも無い
|
|
1141
|
+
(アップロードは base64 のみ、広告作成は mediaId のみ)。
|
|
1142
|
+
→ **バナーもレスポンシブも同じ画像プール (refs.media[番号].card / .sq) を共用する**。
|
|
1143
|
+
広告タイプ (RESPONSIVE_AD / BANNER_AD) は別の軸で、広告行の「広告タイプ」列 (ad.adKind) が持つ。 */
|
|
1124
1144
|
mediaFormats:[
|
|
1125
|
-
{id:'Card',
|
|
1126
|
-
{id:'SQ',
|
|
1145
|
+
{id:'Card', crSuffix:'c', mediaKey:'card', apiMediaKind:'IMAGE', label:'Card mediaID (1200)'},
|
|
1146
|
+
{id:'SQ', crSuffix:'s', mediaKey:'sq', apiMediaKind:'IMAGE', label:'SQ mediaID (1080)'}
|
|
1127
1147
|
],
|
|
1148
|
+
/* 広告タイプがバナー (BANNER_AD) か。**サイズ(形式)ではなく ad.adKind で決まる**。
|
|
1149
|
+
(タイトル/説明文セルの無効化・広告名・入稿ボディのテキスト系省略の判定はすべてここを見る。
|
|
1150
|
+
未設定 = 旧プランJSON は 'レスポンシブ' 扱い) */
|
|
1151
|
+
/* 判定は mappers.adType と同じ正規化 (trim) を使う。ここがズレると
|
|
1152
|
+
adKind:' バナー ' のような値で adType だけ BANNER_AD になり、
|
|
1153
|
+
テキスト系フィールドが入稿計画JSONに混入する。 */
|
|
1154
|
+
isBanner(ad){ return PLATFORMS.yda.mappers.adType((ad&&ad.adKind), PLATFORMS.yda)==='BANNER_AD'; },
|
|
1128
1155
|
fmtOf(ad){ const F=PLATFORMS.yda.mediaFormats; return F.find(x=>x.id===(ad&&ad.format)) || F[0]; },
|
|
1129
1156
|
|
|
1130
1157
|
/* ---- 参照テーブル (S6 の REF_TABLES) ---- */
|
|
@@ -1157,7 +1184,7 @@ PLATFORMS.yda = {
|
|
|
1157
1184
|
title:'OFF にすると最終リンク先URLに ?ad= / &creative= を付けません(案件単位)'}
|
|
1158
1185
|
],
|
|
1159
1186
|
/* プランツールバーの固定プルダウン (変更不可) */
|
|
1160
|
-
|
|
1187
|
+
/* 広告タイプ (レスポンシブ/バナー) は **広告行の列**で切り替えるので固定チップには出さない */
|
|
1161
1188
|
|
|
1162
1189
|
/* ---- ターゲティング列の選択肢 (旧 TG_OPTIONS)。キーは COLS の tgKey ---- */
|
|
1163
1190
|
targeting:{
|
|
@@ -1189,6 +1216,10 @@ PLATFORMS.yda = {
|
|
|
1189
1216
|
/* S14: viewableFrequencyCap の enum (正: frontend/src/pages/submission/types.ts の FREQ_*_OPTIONS) */
|
|
1190
1217
|
freqLevel:{'広告グループ':'AD_GROUP','キャンペーン':'CAMPAIGN','広告':'AD'},
|
|
1191
1218
|
freqUnit:{'週':'WEEK','日':'DAY','月':'MONTH'},
|
|
1219
|
+
/* 広告タイプ列 (ad.adKind) → AdGroupAdService/add の ad.adType。
|
|
1220
|
+
**省略時は RESPONSIVE_AD 扱い**が backend/CLI の後方互換規約なので、未設定もここに落ちる。 */
|
|
1221
|
+
adKind:{'レスポンシブ':'RESPONSIVE_AD','バナー':'BANNER_AD'},
|
|
1222
|
+
adKindFallback:'RESPONSIVE_AD',
|
|
1192
1223
|
statusOn:'ACTIVE', statusOff:'PAUSED'
|
|
1193
1224
|
},
|
|
1194
1225
|
|
|
@@ -1212,6 +1243,8 @@ PLATFORMS.yda = {
|
|
|
1212
1243
|
},
|
|
1213
1244
|
status(s, P){ return s==='オン'?P.enums.statusOn:P.enums.statusOff; },
|
|
1214
1245
|
cta(label, P){ return P.enums.cta[String(label||'').trim()] || P.enums.ctaFallback; },
|
|
1246
|
+
/* 広告タイプ(日本語ラベル) → adType enum。空欄・未知は RESPONSIVE_AD (旧プランJSONの後方互換) */
|
|
1247
|
+
adType(label, P){ return P.enums.adKind[String(label||'').trim()] || P.enums.adKindFallback; },
|
|
1215
1248
|
/* C7: refpick セル (実IDのCSV) → 数値ID配列。落ちた分は ctx.stats.droppedRefIds に計上 */
|
|
1216
1249
|
idList(s, ctx){ return refIdList(s, ctx); }
|
|
1217
1250
|
},
|
|
@@ -1230,7 +1263,8 @@ PLATFORMS.yda = {
|
|
|
1230
1263
|
budgetNew:12000, budgetAsc:50000, goal:'CONVERSION', startDate:''
|
|
1231
1264
|
},
|
|
1232
1265
|
/* C6: adName は空 = 自動生成 (naming.adName)。C1: imagePath は空 = 従来挙動 */
|
|
1233
|
-
|
|
1266
|
+
/* adKind = 広告タイプ軸 (レスポンシブ / バナー)。format はサイズ軸 (Card / SQ) で直交する */
|
|
1267
|
+
ad:{format:'Card', adKind:'レスポンシブ', imageNo:'', titleNo:'', descNo:'', imagePath:'', adName:'', desc2:''},
|
|
1234
1268
|
/* S14: freq* は「案件設定チップの既定を新規CP作成時にコピーする」ので factories.newCampaign 側で入れる */
|
|
1235
1269
|
campaign:{name:'新規キャンペーン', campaignId:'', isExisting:false, urlNumber:'',
|
|
1236
1270
|
freqMode:'default', freqOn:false, freqLevel:'AD_GROUP', freqUnit:'WEEK', freqCount:3}
|
|
@@ -1264,9 +1298,12 @@ PLATFORMS.yda = {
|
|
|
1264
1298
|
naming:{
|
|
1265
1299
|
crName(ad, P){ return String(ad.imageNo||'') + P.fmtOf(ad).crSuffix; },
|
|
1266
1300
|
/* C8: URLパラメータ OFF の案件は paramCode を入力しない (列も隠す) ので、
|
|
1267
|
-
広告名の末尾 "-{paramCode}" も付けない (末尾が "-" だけ残るのを避ける)。
|
|
1301
|
+
広告名の末尾 "-{paramCode}" も付けない (末尾が "-" だけ残るのを避ける)。
|
|
1302
|
+
バナー (BANNER_AD) にはタイトル/説明文が存在しないので、"-{タイトル番号}L{説明文番号}" の区間も
|
|
1303
|
+
出さない (957c-jv_Cac_148)。サイズ記号 c/s は残す。 */
|
|
1268
1304
|
adName(ad, group, crName, project){
|
|
1269
|
-
const head =
|
|
1305
|
+
const head = PLATFORMS.yda.isBanner(ad) ? crName
|
|
1306
|
+
: (crName + '-' + (ad.titleNo??'') + 'L' + (ad.descNo??''));
|
|
1270
1307
|
if(project && project.urlParams===false) return head;
|
|
1271
1308
|
return head + '-' + (group.paramCode||'');
|
|
1272
1309
|
},
|
|
@@ -1306,7 +1343,10 @@ PLATFORMS.yda = {
|
|
|
1306
1343
|
},
|
|
1307
1344
|
groupBadges(g){ return '<span class="pill mono">【'+esc(g.paramCode||'—')+'】</span>'; },
|
|
1308
1345
|
adBadge(a, r){ return '<span class="cbadge">'+esc(a.imageNo||'—')+(a.format||'Card')[0].toLowerCase()+'</span>'; },
|
|
1309
|
-
adSub(a, r){ return esc(a.titleNo)+'L'+esc(a.descNo); }
|
|
1346
|
+
adSub(a, r){ return r.isBanner ? 'バナー' : esc(a.titleNo)+'L'+esc(a.descNo); },
|
|
1347
|
+
/* バナーにはタイトルが存在しない。シート側は「—」で無効化しているのに
|
|
1348
|
+
ツリーが「(未入力)」と出すと「埋めるべき欄」に見えるので別表記にする。 */
|
|
1349
|
+
adTitle(a, r){ return r.isBanner ? '<span class="sm">タイトルなし(画像に焼き込み)</span>' : null; }
|
|
1310
1350
|
},
|
|
1311
1351
|
|
|
1312
1352
|
/* ---- S8: 解決 (派生値 / 純関数) ---- */
|
|
@@ -1320,8 +1360,12 @@ PLATFORMS.yda = {
|
|
|
1320
1360
|
const adName=String(ad.adName||'').trim() || autoAdName;
|
|
1321
1361
|
const me = refs.media[String(ad.imageNo)];
|
|
1322
1362
|
const mediaId = me ? me[fmt.mediaKey] : undefined;
|
|
1323
|
-
|
|
1324
|
-
|
|
1363
|
+
/* バナー広告 (BANNER_AD) はタイトル/説明文のAPI項目が存在しない (画像に焼き込む運用) ので、
|
|
1364
|
+
参照も検証もしない。セル自体も列記述子の col.disabled で無効化している。
|
|
1365
|
+
判定は「広告タイプ」列 (ad.adKind) — 画像サイズ (Card/SQ) とは独立した軸。 */
|
|
1366
|
+
const isBanner = P.isBanner(ad);
|
|
1367
|
+
const titleText = isBanner ? undefined : refs.titles[ad.titleNo];
|
|
1368
|
+
const descText = isBanner ? undefined : (refs.descriptions||{})[ad.descNo]; // A-5: 説明文テーブルのみ参照 (titlesへのフォールバックなし)
|
|
1325
1369
|
const finalUrl = P.naming.finalUrl(ad, group, adName, p);
|
|
1326
1370
|
const errs=[], warns=[], errCodes=[], warnCodes=[];
|
|
1327
1371
|
// S2: 日本語ラベル(errs/warns)と安定コード(errCodes/warnCodes)を同順で1:1に積む
|
|
@@ -1335,10 +1379,12 @@ PLATFORMS.yda = {
|
|
|
1335
1379
|
// 番号が入っているのに参照表で引けない場合のみ従来どおり未解決エラー。
|
|
1336
1380
|
const titleNoStr=String(ad.titleNo||'').trim();
|
|
1337
1381
|
const descNoStr=String(ad.descNo||'').trim();
|
|
1338
|
-
if(
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1382
|
+
if(!isBanner){
|
|
1383
|
+
if(titleNoStr!=='' && titleText===undefined) pushErr('タイトル未解決', EC.TITLE_UNRESOLVED);
|
|
1384
|
+
if(descNoStr!=='' && descText===undefined) pushErr('説明文未解決', EC.DESC_UNRESOLVED);
|
|
1385
|
+
if(titleText!==undefined && glen(titleText)>35) pushErr('タイトル35字超', EC.TITLE_TOO_LONG);
|
|
1386
|
+
else if(titleText!==undefined && glen(titleText)>20) pushWarn('タイトル20字超', EC.TITLE_LONG_WARN);
|
|
1387
|
+
}
|
|
1342
1388
|
// B11: paramCode空はエラー(export対象外・URL生成に必須)。F-6: refs.groups 払出判定は廃止。
|
|
1343
1389
|
/* C8: URLパラメータ OFF の案件では paramCode 自体が不要 (列も隠す) のでエラーにしない */
|
|
1344
1390
|
if(p.urlParams!==false && String(group.paramCode||'').trim()==='') pushErr('paramCode未入力', EC.PARAM_CODE_EMPTY);
|
|
@@ -1354,7 +1400,8 @@ PLATFORMS.yda = {
|
|
|
1354
1400
|
if(!pendingImage && imgNoStr!=='' && imagePaths.length>0) pushWarn('画像パス無視(画像番号が解決済み)', EC.IMAGE_PATH_IGNORED);
|
|
1355
1401
|
// F-6: ID解決は adGroupId ベース。campaignId は idsCell表示用に持たない(親CP参照は呼び出し側)
|
|
1356
1402
|
const aid=String(group.adGroupId||'').trim();
|
|
1357
|
-
return {crName, adName, autoAdName, imagePaths, campaignId:'', adGroupId:aid, mediaKind:fmt.apiMediaKind,
|
|
1403
|
+
return {crName, adName, autoAdName, imagePaths, campaignId:'', adGroupId:aid, mediaKind:fmt.apiMediaKind,
|
|
1404
|
+
adType:P.mappers.adType(ad.adKind, P), isBanner, imageNo:imgNoStr,
|
|
1358
1405
|
mediaId:mediaId||'', pendingImage, titleText:titleText===undefined?'':titleText,
|
|
1359
1406
|
descText:descText===undefined?'':descText, finalUrl, errs, warns, errCodes, warnCodes, resolvedGroup:!!aid};
|
|
1360
1407
|
},
|
|
@@ -1430,17 +1477,24 @@ PLATFORMS.yda = {
|
|
|
1430
1477
|
},
|
|
1431
1478
|
ad(a, r, g, ctx){
|
|
1432
1479
|
const P=PLATFORMS.yda, p=ctx.project;
|
|
1480
|
+
/* adType は「広告タイプ」列から決まる (レスポンシブ→RESPONSIVE_AD / バナー→BANNER_AD)。
|
|
1481
|
+
backend/CLI は **省略時 RESPONSIVE_AD 扱い** なので、明示しても旧プランと矛盾しない。 */
|
|
1433
1482
|
const ad={
|
|
1434
1483
|
adName:r.adName,
|
|
1435
1484
|
status:P.mappers.status(g.deliveryStatus||p.deliveryStatus, P),
|
|
1436
|
-
mediaType:'IMAGE', //
|
|
1437
|
-
|
|
1438
|
-
headlines:String(r.titleText||'')===''?[]:[String(r.titleText)],
|
|
1439
|
-
descriptions:String(r.descText||'')===''?[]:[String(r.descText)],
|
|
1440
|
-
principal:p.principal||'', // E-1: 主体者はアカウントJSON由来 (st.project.principal)
|
|
1441
|
-
buttonText:P.mappers.cta(p.cta, P), // CTA(案件設定)→ YDA buttonText enum。入稿ADD必須
|
|
1485
|
+
mediaType:'IMAGE', // 画像広告 (backend ydaAdSchema IMAGE)。バナーもメイン形式は IMAGE
|
|
1486
|
+
adType:r.adType,
|
|
1442
1487
|
finalUrl:r.finalUrl // 実データ準拠 (?ad=paramCode&creative=...)
|
|
1443
1488
|
};
|
|
1489
|
+
/* バナー (BANNER_AD) は headline / description / principal / buttonText / logoMediaId の
|
|
1490
|
+
**API フィールドが存在しない**。送るとエラーになるので出力しない (画像に焼き込む運用)。 */
|
|
1491
|
+
if(!r.isBanner){
|
|
1492
|
+
// 空欄許容: 空文字を要素に持つ配列を送らない (空なら [])
|
|
1493
|
+
ad.headlines=String(r.titleText||'')===''?[]:[String(r.titleText)];
|
|
1494
|
+
ad.descriptions=String(r.descText||'')===''?[]:[String(r.descText)];
|
|
1495
|
+
ad.principal=p.principal||''; // E-1: 主体者はアカウントJSON由来 (st.project.principal)
|
|
1496
|
+
ad.buttonText=P.mappers.cta(p.cta, P); // CTA(案件設定)→ YDA buttonText enum。入稿ADD必須
|
|
1497
|
+
}
|
|
1444
1498
|
if(r.pendingImage){
|
|
1445
1499
|
// 未登録画像: mediaId未確定。管理番号を残し CLI が先にアップロードして解決する
|
|
1446
1500
|
// C6-1: 画像パス列 (カンマ区切り) を imagePaths に展開する。空なら [] のまま (agent/CLI が補完)
|
|
@@ -1962,6 +2016,12 @@ function renderCell(col, item, ri, ci, ctx){
|
|
|
1962
2016
|
if(col.kind==='badge'){ const id=String(row[col.field]||'').trim(); return '<td>'+newOrIdBadge(!id, id)+'</td>'; }
|
|
1963
2017
|
if(col.kind==='derived') return '<td>'+col.derive(row, ctx)+'</td>';
|
|
1964
2018
|
|
|
2019
|
+
/* col.disabled(item, ctx) = **その行ではこの列自体が存在しない** (YDA バナー行のタイトル/説明文)。
|
|
2020
|
+
data-sel を付けないので範囲選択・コピー&ペースト・一括入力の対象からも外れる。
|
|
2021
|
+
列そのものは隠さない (同一シートにレスポンシブ行と混在するため) = セル単位の無効化。 */
|
|
2022
|
+
if(col.disabled && col.disabled(item, ctx))
|
|
2023
|
+
return '<td><span class="robadge celldis"'+(col.disabledTitle?(' title="'+esc(col.disabledTitle)+'"'):'')+'>—</span></td>';
|
|
2024
|
+
|
|
1965
2025
|
if(ro){
|
|
1966
2026
|
/* 既存呼び出し行の read-only 表現は列記述子で明示する:
|
|
1967
2027
|
hideOnExisting = 常に「—」/ roEmptyDash = 値ありは robadge・空は「—」/ roStyle:'input' = readonly入力欄 /
|
|
@@ -2016,6 +2076,21 @@ function renderCell(col, item, ri, ci, ctx){
|
|
|
2016
2076
|
return '<td class="tgtd"><div class="tgpick'+(shown?'':' tgempty')+'" '+sel+' title="'+esc(shown||(label+'(クリックで選択)'))+'">'+disp+'<span class="tgcaret">▾</span></div></td>';
|
|
2017
2077
|
}
|
|
2018
2078
|
|
|
2079
|
+
/* S16: optpick = **単一選択**のドロワーセル。DOM は tgpick と同じ (.tgpick) に揃え、
|
|
2080
|
+
見た目・範囲選択・コピペ・Undo を完全に共通化する (判別用に optpick クラスを併記)。
|
|
2081
|
+
複数選択の tgpick と違い、ドロワーで押した値は「選択範囲 ∩ 同じ列」へ **上書き代入** する。
|
|
2082
|
+
選択肢は列記述子の col.options が正 (媒体固有の語彙をコアに書かない)。 */
|
|
2083
|
+
if(col.kind==='optpick'){
|
|
2084
|
+
const label=col.label||'';
|
|
2085
|
+
const shown=String(val==null?'':val);
|
|
2086
|
+
const disp = shown? '<span class="tgval">'+esc(shown)+'</span>' : '<span class="tgval"><span class="tghint">'+esc(label)+'</span></span>';
|
|
2087
|
+
/* 列幅は **列記述子 (col.width / col.minWidth) が正**。.tgpick の min-width:130px を素通しすると
|
|
2088
|
+
シートが横に膨らみ、右端列が右サイドバーの下に潜り込む (矩形選択が届かなくなる)。 */
|
|
2089
|
+
const w=col.width||col.minWidth||'';
|
|
2090
|
+
const sty=w? ' style="min-width:'+w+'"' : '';
|
|
2091
|
+
return '<td class="tgtd"><div class="tgpick optpick'+(shown?'':' tgempty')+'" '+sel+sty+' title="'+esc((shown||label)+'(クリックで選択)')+'">'+disp+'<span class="tgcaret">▾</span></div></td>';
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2019
2094
|
/* S15: kind:'datetime' = セル内を <input type=date> + <input type=time> の2分割にする。
|
|
2020
2095
|
state は従来どおり1本の文字列 ("YYYY-MM-DD HH:mm" / 時刻が空なら "YYYY-MM-DD") なので、
|
|
2021
2096
|
col.get/col.set は不要でコピペ・一括入力・連番は既存経路のまま動く。
|
|
@@ -2092,7 +2167,8 @@ function renderTree(){
|
|
|
2092
2167
|
html+='<div class="adnode'+(r.errs.length?' hasErr':'')+'" data-ad="'+a.uid+'">';
|
|
2093
2168
|
html+='<span class="dot '+dot+'"></span>';
|
|
2094
2169
|
html+=pf().tree.adBadge(a,r); // S7: クリエイティブ名バッジ (媒体スキーマ)
|
|
2095
|
-
|
|
2170
|
+
const adTitleHtml = (pf().tree.adTitle && pf().tree.adTitle(a,r)) || null; // 媒体側が出し分ける場合のみ差し替える
|
|
2171
|
+
html+='<span class="adttl">'+(adTitleHtml!==null?adTitleHtml:(r.errCodes.includes(EC.TITLE_UNRESOLVED)?'<span style="color:var(--danger)">タイトル未解決</span>':esc(r.titleText||'(未入力)')))+' <span class="sm">'+pf().tree.adSub(a,r)+'</span></span>';
|
|
2096
2172
|
html+='<span class="nodeact"><button class="iconbtn" data-act="ad-dup" data-u="'+a.uid+'" title="複製">⧉</button><button class="iconbtn danger" data-act="ad-del" data-u="'+a.uid+'" title="削除">✕</button></span>';
|
|
2097
2173
|
html+='</div>';
|
|
2098
2174
|
});
|
|
@@ -2320,8 +2396,20 @@ function freqInheritText(){
|
|
|
2320
2396
|
|
|
2321
2397
|
const COLS = {
|
|
2322
2398
|
ads:[
|
|
2323
|
-
|
|
2324
|
-
|
|
2399
|
+
/* 形式 = **画像のサイズ軸** (Card=1200x628 / SQ=1080x1080)。参照表 refs.media[番号].card / .sq を引く。
|
|
2400
|
+
広告タイプ (レスポンシブ/バナー) とは直交する軸で、バナーも同じ画像プールを使う。 */
|
|
2401
|
+
{field:'format', label:'形式', kind:'optpick', options:['Card','SQ'], width:'72px',
|
|
2402
|
+
title:'画像のサイズ。参照表の Card / SQ mediaID を引きます(バナーも同じ画像プールを使います)',
|
|
2403
|
+
coerce:v=>{ const s=String(v).trim().toUpperCase();
|
|
2404
|
+
if(s==='SQ') return 'SQ';
|
|
2405
|
+
return 'Card'; }},
|
|
2406
|
+
/* 広告タイプ = **RESPONSIVE_AD / BANNER_AD の軸**。行単位で切り替えるので同一キャンペーン内に混在できる。
|
|
2407
|
+
バナー行はタイトル/説明文のセルが col.disabled で無効化される (PLATFORMS.yda.isBanner)。 */
|
|
2408
|
+
{field:'adKind', label:'広告タイプ', kind:'optpick', options:['レスポンシブ','バナー'], width:'96px',
|
|
2409
|
+
title:'レスポンシブ = RESPONSIVE_AD / バナー = BANNER_AD(タイトル・説明文・主体者・CTAのAPI項目が無く、画像に焼き込む)',
|
|
2410
|
+
get:row=>row.adKind||'レスポンシブ',
|
|
2411
|
+
coerce:v=>{ const s=String(v).trim();
|
|
2412
|
+
return (s==='バナー'||s.toUpperCase()==='BANNER'||s.toUpperCase()==='BANNER_AD')?'バナー':'レスポンシブ'; }},
|
|
2325
2413
|
{field:'imageNo', label:'画像番号', kind:'refcombo', refTable:'media', drawerType:'media', mediaAxis:'format',
|
|
2326
2414
|
width:'6ch', sequenceable:true, errCodes:[EC.IMAGE_UNRESOLVED],
|
|
2327
2415
|
ghostKey:'media', ddTitle:'登録画像から選択', ghostTitle:r=>'メディアID '+r.mediaId,
|
|
@@ -2331,10 +2419,16 @@ const COLS = {
|
|
|
2331
2419
|
return '<span class="idpill err">#N/A</span>'; }},
|
|
2332
2420
|
{field:'titleNo', label:'タイトル番号', kind:'refcombo', refTable:'titles', drawerType:'titles',
|
|
2333
2421
|
width:'6ch', sequenceable:true, errCodes:[EC.TITLE_UNRESOLVED, EC.TITLE_TOO_LONG],
|
|
2422
|
+
disabled:it=>it.kind==='a' && PLATFORMS.yda.isBanner(it.a),
|
|
2423
|
+
disabledTitle:'バナー広告(BANNER_AD)にはタイトル/説明文のAPI項目がありません(画像に焼き込み)',
|
|
2424
|
+
|
|
2334
2425
|
ghostKey:'title', ddTitle:'タイトル一覧から選択', ghostTitle:r=>r.titleText,
|
|
2335
2426
|
ghost:r=>r.errCodes.includes(EC.TITLE_UNRESOLVED)?'<span class="err">未解決</span>':esc(r.titleText.slice(0,40))},
|
|
2336
2427
|
{field:'descNo', label:'説明文番号', kind:'refcombo', refTable:'descriptions', drawerType:'titles',
|
|
2337
2428
|
width:'6ch', sequenceable:true, errCodes:[EC.DESC_UNRESOLVED],
|
|
2429
|
+
disabled:it=>it.kind==='a' && PLATFORMS.yda.isBanner(it.a),
|
|
2430
|
+
disabledTitle:'バナー広告(BANNER_AD)にはタイトル/説明文のAPI項目がありません(画像に焼き込み)',
|
|
2431
|
+
|
|
2338
2432
|
ghostKey:'desc', ddTitle:'説明文一覧から選択', ghostTitle:r=>r.descText,
|
|
2339
2433
|
ghost:r=>r.errCodes.includes(EC.DESC_UNRESOLVED)?'<span class="err">— 未解決</span>':esc(r.descText.slice(0,44))},
|
|
2340
2434
|
{field:'paramCode', label:'paramCode(グループ)', kind:'mono', width:'120px',
|
|
@@ -2351,14 +2445,18 @@ const COLS = {
|
|
|
2351
2445
|
get:(row,ctx)=>ctx.r?ctx.r.finalUrl:''},
|
|
2352
2446
|
/* C5: 参照表(refs.titles / refs.descriptions)の本文を直接編集する列。
|
|
2353
2447
|
番号未入力のまま本文を書くと空き番号を自動採番して参照表に登録する。 */
|
|
2354
|
-
makeRefTextCol({field:'titleText', label:'タイトル本文', table:'titles', noField:'titleNo', width:'180px'}),
|
|
2355
|
-
|
|
2448
|
+
Object.assign(makeRefTextCol({field:'titleText', label:'タイトル本文', table:'titles', noField:'titleNo', width:'180px'}),
|
|
2449
|
+
{disabled:it=>it.kind==='a' && PLATFORMS.yda.isBanner(it.a),
|
|
2450
|
+
disabledTitle:'バナー広告(BANNER_AD)にはタイトルのAPI項目がありません(画像に焼き込み)'}),
|
|
2451
|
+
Object.assign(makeRefTextCol({field:'descText', label:'説明文本文', table:'descriptions', noField:'descNo', width:'180px'}),
|
|
2452
|
+
{disabled:it=>it.kind==='a' && PLATFORMS.yda.isBanner(it.a),
|
|
2453
|
+
disabledTitle:'バナー広告(BANNER_AD)には説明文のAPI項目がありません(画像に焼き込み)'}),
|
|
2356
2454
|
/* C6-1: 量産時のローカル画像パス担体 (カンマ区切りで複数可)。少数入稿は Skill の画像入稿フローが主導線 */
|
|
2357
2455
|
{field:'imagePath', label:'画像パス', kind:'mono', width:'150px',
|
|
2358
2456
|
title:'ローカル画像の絶対 / ~/… / 計画JSON基準の相対パス。カンマ区切りで複数可。※画像番号が参照表で解決できる行では入稿JSONに載りません(差し替えたいときは画像番号を空に)'},
|
|
2359
2457
|
/* C6: 広告名は自由入力 (空 = 自動生成)。placeholder に自動生成値を出す */
|
|
2360
2458
|
{field:'adName', label:'広告名', kind:'mono', width:'170px',
|
|
2361
|
-
title:'
|
|
2459
|
+
title:'空欄なら自動生成(レスポンシブ: {画像番号}{c|s}-{タイトル番号}L{説明文番号}-{paramCode} / バナー: {画像番号}{c|s}-{paramCode})。手入力すると最終リンク先URLの creative= も変わります',
|
|
2362
2460
|
placeholder:ctx=>ctx.r?ctx.r.autoAdName:''},
|
|
2363
2461
|
/* derived: resolveAd() 由来の読取専用セル。tabCols()/貼付/セル選択の対象外 (末尾固定) */
|
|
2364
2462
|
{field:'adGroupId', label:'CP / AG ID', kind:'derived',
|
|
@@ -2499,7 +2597,7 @@ const META_REF_TABLES = {
|
|
|
2499
2597
|
/* 列記述子 (Meta)。役割・意味は COLS のコメント参照 */
|
|
2500
2598
|
const META_COLS = {
|
|
2501
2599
|
ads:[
|
|
2502
|
-
{field:'format', label:'形式', kind:'
|
|
2600
|
+
{field:'format', label:'形式', kind:'optpick', options:['IMAGE','VIDEO'], minWidth:'70px',
|
|
2503
2601
|
coerce:v=>(String(v).toUpperCase()==='VIDEO'?'VIDEO':'IMAGE')},
|
|
2504
2602
|
{field:'imageNo', label:'クリエイティブ番号', kind:'refcombo', refTable:'media', drawerType:'media', mediaAxis:'format',
|
|
2505
2603
|
width:'6ch', sequenceable:true, errCodes:[EC.IMAGE_UNRESOLVED],
|
|
@@ -2684,7 +2782,6 @@ PLATFORMS.meta = {
|
|
|
2684
2782
|
{key:'urlParams', label:'URLパラメータ', kind:'toggle', fallback:true,
|
|
2685
2783
|
title:'OFF にすると最終リンク先URLに ?ad= / &creative= を付けません(案件単位)'}
|
|
2686
2784
|
],
|
|
2687
|
-
fixedType:[{label:'広告タイプ',value:'単一画像 / 単一動画'}],
|
|
2688
2785
|
|
|
2689
2786
|
targeting:{
|
|
2690
2787
|
tg_gender:{label:'性別', multi:true, allLabel:'すべて', opts:['すべて','男性','女性']},
|
|
@@ -3220,6 +3317,7 @@ function cellCtx(item, tab, ctx){
|
|
|
3220
3317
|
}
|
|
3221
3318
|
function getVar(item,c,tab,ctx){
|
|
3222
3319
|
const col=colDefs(tab)[c]; if(!col) return '';
|
|
3320
|
+
if(col.disabled && col.disabled(item, cellCtx(item,tab,ctx))) return ''; /* 無効セルはコピーにも載せない */
|
|
3223
3321
|
const row=rowOf(item,col); if(!row) return '';
|
|
3224
3322
|
if(col.get){ const gv=col.get(row, cellCtx(item,tab,ctx)); return gv==null?'':String(gv); }
|
|
3225
3323
|
const v=row[col.field];
|
|
@@ -3229,6 +3327,7 @@ function setVar(item,c,val,tab,ctx){
|
|
|
3229
3327
|
const col=colDefs(tab)[c]; if(!col) return;
|
|
3230
3328
|
if(col.kind==='derived') return;
|
|
3231
3329
|
if(col.alwaysReadonly) return; /* adGroupId / campaignId */
|
|
3330
|
+
if(col.disabled && col.disabled(item, cellCtx(item,tab,ctx))) return; /* バナー行のタイトル/説明文 */
|
|
3232
3331
|
if((col.readonlyOnExisting||col.hideOnExisting) && rowIsExisting(item,col)) return; /* F-2: 既存呼び出し行は基本パラメータ編集不可 */
|
|
3233
3332
|
const row=rowOf(item,col); if(!row) return;
|
|
3234
3333
|
const v = col.coerce?col.coerce(val):val;
|
|
@@ -3271,7 +3370,11 @@ function flatVisible(){ const out=[]; if(sheetTab==='groups'){ st.campaigns.forE
|
|
|
3271
3370
|
let selA=null, selB=null, dragging=false;
|
|
3272
3371
|
function clearSel(){ selA=selB=null; applySelection(); }
|
|
3273
3372
|
function selRange(){ if(!selA||!selB) return null; return {r0:Math.min(selA.r,selB.r),r1:Math.max(selA.r,selB.r),c0:Math.min(selA.c,selB.c),c1:Math.max(selA.c,selB.c)}; }
|
|
3274
|
-
function applySelection(){ const host=document.getElementById('sheetHost'); if(!host) return; host.querySelectorAll('.selcell,.selanchor').forEach(el=>el.classList.remove('selcell','selanchor')); host.querySelectorAll('.cellcombo.comboSel').forEach(el=>el.classList.remove('comboSel')); host.querySelectorAll('td.rownum.rowsel').forEach(el=>el.classList.remove('rowsel')); const rg=selRange(); if(!rg){ if(typeof updateSeqBtn==='function') updateSeqBtn(); return; } host.querySelectorAll('[data-sel]').forEach(el=>{ const r=+el.dataset.r, c=+el.dataset.c; if(r>=rg.r0&&r<=rg.r1&&c>=rg.c0&&c<=rg.c1){ el.classList.add('selcell'); const cb=el.closest('.cellcombo'); if(cb) cb.classList.add('comboSel'); if(selA&&r===selA.r&&c===selA.c) el.classList.add('selanchor'); } }); host.querySelectorAll('td.rownum').forEach(el=>{ const r=+el.dataset.rownum; if(r>=rg.r0&&r<=rg.r1) el.classList.add('rowsel'); }); if(typeof updateSeqBtn==='function') updateSeqBtn();
|
|
3373
|
+
function applySelection(){ const host=document.getElementById('sheetHost'); if(!host) return; host.querySelectorAll('.selcell,.selanchor').forEach(el=>el.classList.remove('selcell','selanchor')); host.querySelectorAll('.cellcombo.comboSel').forEach(el=>el.classList.remove('comboSel')); host.querySelectorAll('td.rownum.rowsel').forEach(el=>el.classList.remove('rowsel')); const rg=selRange(); if(!rg){ if(typeof updateSeqBtn==='function') updateSeqBtn(); return; } host.querySelectorAll('[data-sel]').forEach(el=>{ const r=+el.dataset.r, c=+el.dataset.c; if(r>=rg.r0&&r<=rg.r1&&c>=rg.c0&&c<=rg.c1){ el.classList.add('selcell'); const cb=el.closest('.cellcombo'); if(cb) cb.classList.add('comboSel'); if(selA&&r===selA.r&&c===selA.c) el.classList.add('selanchor'); } }); host.querySelectorAll('td.rownum').forEach(el=>{ const r=+el.dataset.rownum; if(r>=rg.r0&&r<=rg.r1) el.classList.add('rowsel'); }); if(typeof updateSeqBtn==='function') updateSeqBtn();
|
|
3374
|
+
/* S16: optpick ドロワーは「選択範囲 ∩ 同一列」を入力先にする。選択が動いたら
|
|
3375
|
+
✓/− と「入力先: N セル」を描き直さないと、実際に書き換わる範囲と表示が食い違う。 */
|
|
3376
|
+
if(drawerTarget && drawerTarget.type==='opt' && document.getElementById('refDrawer').classList.contains('open')) renderDrawer();
|
|
3377
|
+
}
|
|
3275
3378
|
function copySelText(){ const rg=selRange(); if(!rg) return null; const vis=flatVisible();
|
|
3276
3379
|
/* C1: 範囲全セルで getVar → get フック列が resolveAd を呼ぶ。読み取り専用パスなのでメモ化する */
|
|
3277
3380
|
return withResolvePass(()=>{ const lines=[]; for(let r=rg.r0;r<=rg.r1;r++){ const it=vis[r]; if(!it) continue; const cells=[]; for(let c=rg.c0;c<=rg.c1;c++) cells.push(getVar(it,c)); lines.push(cells.join('\t')); } return lines.join('\n'); }); }
|
|
@@ -3473,32 +3576,57 @@ function openTargetingDrawer(gUid, field){
|
|
|
3473
3576
|
renderDrawer();
|
|
3474
3577
|
document.getElementById('refDrawer').classList.add('open');
|
|
3475
3578
|
}
|
|
3579
|
+
/* S16: optpick 列 (広告シートの単一選択セル)。同じセルの再クリックで閉じるトグルは tg と同じ。 */
|
|
3580
|
+
function openOptDrawer(uid, field){
|
|
3581
|
+
const fa=findAd(uid); if(!fa) return;
|
|
3582
|
+
const col=colDefs('ads').find(c=>c.field===field); if(!col) return;
|
|
3583
|
+
const item={kind:'a', a:fa.a, g:fa.g, c:fa.c};
|
|
3584
|
+
if(col.disabled && col.disabled(item, cellCtx(item,'ads'))) return; /* その行に存在しない列は開かない */
|
|
3585
|
+
const isOpen=document.getElementById('refDrawer').classList.contains('open');
|
|
3586
|
+
const same=drawerTarget && drawerTarget.type==='opt' && drawerTarget.uid===uid && drawerTarget.field===field;
|
|
3587
|
+
if(isOpen && same){ closeDrawer(); return; }
|
|
3588
|
+
drawerTarget={type:'opt', uid, field};
|
|
3589
|
+
if(!same) document.getElementById('drawerSearch').value='';
|
|
3590
|
+
renderDrawer();
|
|
3591
|
+
document.getElementById('refDrawer').classList.add('open');
|
|
3592
|
+
}
|
|
3476
3593
|
/* ---- ターゲティング列の一括トグル ----
|
|
3477
3594
|
ドロワーで押した項目を「選択範囲 ∩ 同種の列」の全セルへ ON/OFF 一括適用する。
|
|
3478
3595
|
同種の判定は列記述子から引く (refpick は同じ refTable / tgpick は同じ tgKey)。
|
|
3479
3596
|
選択が無い・対象列に掛かっていない場合はアンカーの1セルだけを返し、従来の単体トグルと同じ挙動になる。 */
|
|
3480
|
-
function
|
|
3481
|
-
const
|
|
3482
|
-
const
|
|
3483
|
-
|
|
3484
|
-
const
|
|
3485
|
-
if(
|
|
3486
|
-
const compat=[]; cols.forEach((c,i)=>{
|
|
3487
|
-
if(base.kind==='refpick'){ if(c.kind==='refpick' && c.refTable===base.refTable) compat.push(i); }
|
|
3488
|
-
else if(c.kind==='tgpick' && c.tgKey===base.tgKey) compat.push(i);
|
|
3489
|
-
});
|
|
3597
|
+
function bulkPickTargets(tab, field, anchorItem, isCompat){
|
|
3598
|
+
const cols=colDefs(tab), ci=colIndex(field, tab), base=cols[ci];
|
|
3599
|
+
const anchor=(anchorItem&&ci>=0)?[{item:anchorItem, ci}]:[];
|
|
3600
|
+
if(!base||!anchorItem) return anchor;
|
|
3601
|
+
const compat=[]; cols.forEach((c,i)=>{ if(isCompat(c, base)) compat.push(i); });
|
|
3602
|
+
if(normTab(sheetTab)!==tab) return anchor; /* 別タブに切り替わっていたら選択範囲は無関係 */
|
|
3490
3603
|
const rg=selRange(); if(!rg) return anchor;
|
|
3491
3604
|
const vis=flatVisible(), out=[];
|
|
3492
3605
|
for(let r=rg.r0;r<=rg.r1;r++){
|
|
3493
|
-
|
|
3606
|
+
/* 行種別はアンカーに合わせる (同一シートに複数 kind の行が並ぶビューがあるため) */
|
|
3607
|
+
const it=vis[r]; if(!it||it.kind!==anchorItem.kind) continue;
|
|
3494
3608
|
for(let c=rg.c0;c<=rg.c1;c++){
|
|
3495
3609
|
if(compat.indexOf(c)<0) continue;
|
|
3496
3610
|
if(rowIsExisting(it, cols[c])) continue; /* F-2: 既存呼び出しAGは編集不可 */
|
|
3611
|
+
if(cols[c].disabled && cols[c].disabled(it, cellCtx(it, tab))) continue; /* その行に存在しない列 (バナー行のタイトル等) */
|
|
3497
3612
|
out.push({item:it, ci:c});
|
|
3498
3613
|
}
|
|
3499
3614
|
}
|
|
3500
3615
|
return out.length?out:anchor;
|
|
3501
3616
|
}
|
|
3617
|
+
function tgBulkTargets(gUid, field){
|
|
3618
|
+
const tab=normTab(sheetTab), fg=findGroup(gUid);
|
|
3619
|
+
return bulkPickTargets(tab, field, fg?{kind:'g', g:fg.g, c:fg.c}:null, (c,base)=>
|
|
3620
|
+
base.kind==='refpick' ? (c.kind==='refpick' && c.refTable===base.refTable)
|
|
3621
|
+
: (c.kind==='tgpick' && c.tgKey===base.tgKey));
|
|
3622
|
+
}
|
|
3623
|
+
/* optpick の同種列は **同一 field のみ**。形式(Card/SQ) と 広告タイプ(レスポンシブ/バナー) は
|
|
3624
|
+
選択肢が排他なので、まとめて一括対象にしてはいけない。 */
|
|
3625
|
+
function optBulkTargets(uid, field){
|
|
3626
|
+
const fa=findAd(uid);
|
|
3627
|
+
return bulkPickTargets('ads', field, fa?{kind:'a', a:fa.a, g:fa.g, c:fa.c}:null,
|
|
3628
|
+
(c,base)=> c.kind==='optpick' && c.field===base.field);
|
|
3629
|
+
}
|
|
3502
3630
|
function tgListOf(t){ return String(getVar(t.item, t.ci)||'').split(',').map(s=>s.trim()).filter(Boolean); }
|
|
3503
3631
|
function drawerTgToggle(val){
|
|
3504
3632
|
if(!drawerTarget||drawerTarget.type!=='tg') return;
|
|
@@ -3514,6 +3642,16 @@ function drawerTgToggle(val){
|
|
|
3514
3642
|
if(targets.length>1) toast((allOn?'解除':'追加')+':'+targets.length+'セルに一括適用');
|
|
3515
3643
|
renderDrawer();
|
|
3516
3644
|
}
|
|
3645
|
+
/* optpick は単一選択なので **トグルではなく上書き代入**。値は必ず setVar 経由 (col.coerce/set を通す)。
|
|
3646
|
+
選択後もドロワーは閉じない (tgpick と同じ操作感。範囲選択したまま値を試せる)。 */
|
|
3647
|
+
function drawerOptPick(val){
|
|
3648
|
+
if(!drawerTarget||drawerTarget.type!=='opt') return;
|
|
3649
|
+
const targets=optBulkTargets(drawerTarget.uid, drawerTarget.field);
|
|
3650
|
+
if(!targets.length) return;
|
|
3651
|
+
mutate(()=>{ targets.forEach(t=> setVar(t.item, t.ci, val, 'ads')); });
|
|
3652
|
+
if(targets.length>1) toast('「'+(val||'(既定)')+'」を'+targets.length+'セルに一括適用');
|
|
3653
|
+
renderDrawer();
|
|
3654
|
+
}
|
|
3517
3655
|
function closeDrawer(){ document.getElementById('refDrawer').classList.remove('open'); drawerTarget=null; }
|
|
3518
3656
|
function renderDrawer(){
|
|
3519
3657
|
if(!drawerTarget) return;
|
|
@@ -3586,6 +3724,25 @@ function renderDrawer(){
|
|
|
3586
3724
|
cfg.opts.forEach(o=>{ if(q&&!o.toLowerCase().includes(q)) return; const mk=mark(o); h+='<div class="ddrow'+mk.cls+'" data-tgval="'+esc(o)+'"><span class="ddno">'+mk.ic+'</span><span class="ddtx">'+esc(o)+'</span></div>'; });
|
|
3587
3725
|
listEl.innerHTML=h||'<div class="empty" style="padding:24px">該当なし</div>'; return;
|
|
3588
3726
|
}
|
|
3727
|
+
if(drawerTarget.type==='opt'){
|
|
3728
|
+
const col=colDefs('ads').find(c=>c.field===drawerTarget.field);
|
|
3729
|
+
if(!col){ listEl.innerHTML=''; return; }
|
|
3730
|
+
const targets=optBulkTargets(drawerTarget.uid, drawerTarget.field), N=targets.length;
|
|
3731
|
+
const vals=targets.map(t=>{ const v=getVar(t.item, t.ci, 'ads'); return String(v==null?'':v); });
|
|
3732
|
+
/* 一括対象セル全体で ✓ を判定する: 全セル一致なら ✓ / 一部だけなら −(押すと全セルへ揃える) */
|
|
3733
|
+
const mark=v=>{ const n=vals.reduce((a,x)=>a+(x===v?1:0),0);
|
|
3734
|
+
return n===0?{cls:'', ic:''}:(n===N?{cls:' cur', ic:'✓'}:{cls:' cur', ic:'−'}); };
|
|
3735
|
+
const label=col.label||'';
|
|
3736
|
+
titleEl.textContent=label+' を選択'+(N>1?(' / '+N+'セル一括'):'');
|
|
3737
|
+
tgtEl.textContent = N>1
|
|
3738
|
+
? ('入力先: 選択中の '+N+' セル / '+label+'(押した項目を全セルへ適用)')
|
|
3739
|
+
: ('入力先: '+label+' / 現在 '+(vals[0]||'(既定)'));
|
|
3740
|
+
let h='';
|
|
3741
|
+
(col.options||[]).forEach(o=>{ const t=(o===''?'(既定)':o); if(q && !t.toLowerCase().includes(q)) return;
|
|
3742
|
+
const mk=mark(o);
|
|
3743
|
+
h+='<div class="ddrow'+mk.cls+'" data-optv="'+esc(o)+'"><span class="ddno">'+mk.ic+'</span><span class="ddtx">'+esc(t)+'</span></div>'; });
|
|
3744
|
+
listEl.innerHTML=h||'<div class="empty" style="padding:24px">該当なし</div>'; return;
|
|
3745
|
+
}
|
|
3589
3746
|
/* S7: 入力先の見出しは列記述子のラベル (媒体ごとの列名をここに書かない) */
|
|
3590
3747
|
const dcol=colDefs('ads').find(c=>c.field===drawerTarget.f); const fieldName=dcol?dcol.label:'';
|
|
3591
3748
|
let curNo=''; const fa=findAd(drawerTarget.u); if(fa) curNo=String(fa.a[drawerTarget.f]||'');
|
|
@@ -4621,7 +4778,9 @@ async function openPlanFile(){
|
|
|
4621
4778
|
WIRING
|
|
4622
4779
|
============================================================ */
|
|
4623
4780
|
document.getElementById('viewSeg').addEventListener('click',e=>{ const b=e.target.closest('button[data-v]'); if(!b) return; clearSel(); closeDrawer(); activeView=b.dataset.v; document.querySelectorAll('#viewSeg button').forEach(x=>x.classList.toggle('active',x===b)); document.querySelectorAll('.view').forEach(v=>v.classList.toggle('active', v.id==='view-'+activeView)); render(); });
|
|
4624
|
-
|
|
4781
|
+
/* 選択を解除したらドロワーも閉じる。閉じないと「N セル一括」の表示が残ったまま
|
|
4782
|
+
次の操作はアンカー1セルだけに当たる (viewSeg / modeSeg と同じ扱いに揃える)。 */
|
|
4783
|
+
document.getElementById('sheetFilter').addEventListener('change',()=>{ clearSel(); closeDrawer(); renderSheet(); });
|
|
4625
4784
|
/* プランビュー: シート / ツリー モード切替 (1階層下) */
|
|
4626
4785
|
document.getElementById('modeSeg').addEventListener('click',e=>{ const b=e.target.closest('button[data-m]'); if(!b) return; clearSel(); closeDrawer(); planMode=b.dataset.m; document.querySelectorAll('#modeSeg button').forEach(x=>x.classList.toggle('active',x===b)); render(); });
|
|
4627
4786
|
/* S11: 媒体セレクタ (#pfSeg) / 媒体要件タブ (#mediaTabs) は廃止。媒体切替はヘッダーの統合プルダウンに一本化 */
|
|
@@ -4727,7 +4886,7 @@ sheetHostEl.addEventListener('mousedown',e=>{
|
|
|
4727
4886
|
pendingDrawer=null; dragMoved=false;
|
|
4728
4887
|
// S3: 旧 [1,2,3] リテラルを撤去。列記述子の kind で「▼参照ドロワー列」「ターゲティング列」を判定する
|
|
4729
4888
|
const col=colDefs()[c];
|
|
4730
|
-
if(e.shiftKey && selA){ selB={r,c}; e.preventDefault(); } else { selA={r,c}; selB={r,c}; dragging='cell'; if(col && col.kind==='refcombo') pendingDrawer={kind:'ad', u:cell.dataset.u, f:cell.dataset.f}; else if(col && (col.kind==='tgpick'||col.kind==='refpick') && cell.dataset.gf) pendingDrawer={kind:'tg', gu:cell.dataset.gu, gf:cell.dataset.gf}; }
|
|
4889
|
+
if(e.shiftKey && selA){ selB={r,c}; e.preventDefault(); } else { selA={r,c}; selB={r,c}; dragging='cell'; if(col && col.kind==='refcombo') pendingDrawer={kind:'ad', u:cell.dataset.u, f:cell.dataset.f}; else if(col && (col.kind==='tgpick'||col.kind==='refpick') && cell.dataset.gf) pendingDrawer={kind:'tg', gu:cell.dataset.gu, gf:cell.dataset.gf}; else if(col && col.kind==='optpick' && cell.dataset.f) pendingDrawer={kind:'opt', u:cell.dataset.u, f:cell.dataset.f}; }
|
|
4731
4890
|
applySelection();
|
|
4732
4891
|
});
|
|
4733
4892
|
sheetHostEl.addEventListener('mouseover',e=>{
|
|
@@ -4751,6 +4910,7 @@ document.getElementById('drawerBackdrop').addEventListener('click',closeDrawer);
|
|
|
4751
4910
|
document.getElementById('drawerSearch').addEventListener('input',renderDrawer);
|
|
4752
4911
|
document.getElementById('drawerList').addEventListener('click',e=>{ const row=e.target.closest('.ddrow'); if(!row) return; if(!drawerTarget) return;
|
|
4753
4912
|
if(drawerTarget.type==='tg'){ drawerTgToggle(row.dataset.tgval); }
|
|
4913
|
+
else if(drawerTarget.type==='opt'){ if('optv' in row.dataset) drawerOptPick(row.dataset.optv); }
|
|
4754
4914
|
else if(drawerTarget.type==='cpref'){ if(row.dataset.cpid) addExistingCampaign(row.dataset.cpid); }
|
|
4755
4915
|
else if(drawerTarget.type==='agref'){ if(row.dataset.agid) addExistingAdGroup(row.dataset.agid); }
|
|
4756
4916
|
else if(drawerTarget.type==='cppick'){ if(row.dataset.cppick){ if(drawerTarget.mode==='addNew') addNewAdGroupToCampaign(row.dataset.cppick); else moveGroupToCampaign(drawerTarget.gUid, row.dataset.cppick); } }
|
|
@@ -4758,7 +4918,7 @@ document.getElementById('drawerList').addEventListener('click',e=>{ const row=e.
|
|
|
4758
4918
|
else { drawerPick(row.dataset.no); }
|
|
4759
4919
|
});
|
|
4760
4920
|
/* 単体クリック/複数ドラッグ選択どちらでも開く(選択範囲全セルに一括適用) */
|
|
4761
|
-
document.addEventListener('mouseup',()=>{ if(pendingDrawer){ if(pendingDrawer.kind==='ad') openDrawerFor(pendingDrawer.u,pendingDrawer.f); else openTargetingDrawer(pendingDrawer.gu,pendingDrawer.gf); } pendingDrawer=null; deferredCollapse=null; if(dragging){ dragging=false; document.body.classList.remove('noselect'); } });
|
|
4921
|
+
document.addEventListener('mouseup',()=>{ if(pendingDrawer){ if(pendingDrawer.kind==='ad') openDrawerFor(pendingDrawer.u,pendingDrawer.f); else if(pendingDrawer.kind==='opt') openOptDrawer(pendingDrawer.u,pendingDrawer.f); else openTargetingDrawer(pendingDrawer.gu,pendingDrawer.gf); } pendingDrawer=null; deferredCollapse=null; if(dragging){ dragging=false; document.body.classList.remove('noselect'); } });
|
|
4762
4922
|
|
|
4763
4923
|
/* ---- コピー / 貼付 ---- */
|
|
4764
4924
|
document.addEventListener('copy',e=>{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
`adops.sh refs-import <platform> <accountId> [--write]` から呼ばれる。単体でも動く:
|
|
5
5
|
|
|
6
|
-
python3 refs_import.py yda
|
|
6
|
+
python3 refs_import.py yda <accountId> [--write] [--campaign-id <campaignId>]
|
|
7
7
|
|
|
8
8
|
`webgram list` を campaigns / ads / resources の3系統で叩き、
|
|
9
9
|
`アカウント/<platform>/<name>.json` にマージする。--write なしは差分表示のみ。
|