addashboard-cli 0.6.3 → 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/dist/commands/submission.js +12 -9
- package/dist/commands/submission.js.map +1 -1
- package/dist/schemas/submit-plan.js +133 -0
- package/dist/schemas/submit-plan.js.map +1 -0
- package/package.json +4 -2
- package/skills/ad-ops/SKILL.md +64 -61
- package/skills/ad-ops/ad-planner.html +344 -127
- 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
|
@@ -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,23 +536,25 @@ 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>
|
|
529
556
|
<tr><td class="mono">goal</td><td><b>案件設定「目的」</b></td><td><code>CONVERSION</code> / <code>CLICK</code> / <code>BRAND_AWARENESS</code> / <code>VIDEO_VIEW</code> / <code>VIEWABLE_IMPRESSION</code></td></tr>
|
|
530
|
-
<tr><td class="mono">startDate</td><td><b>案件設定「配信開始日時」</b>(カレンダー+時計から選択)</td><td>YYYY-MM-DD
|
|
557
|
+
<tr><td class="mono">startDate</td><td><b>案件設定「配信開始日時」</b>(カレンダー+時計から選択)</td><td>YYYY-MM-DD。空欄=<b>翌日 05:00</b>。<b>時刻は別フィールド <code>startTime</code>(HHmm)として送られる</b>(公式: ディスプレイ広告API v18 CampaignService。startTime を指定するときは startDate が必須)。日付だけを入れた場合は startTime を送らない</td></tr>
|
|
531
558
|
<tr><td class="mono">status</td><td>キャンペーン行/広告グループ行の「配信設定」</td><td><code>オン→ACTIVE</code> / <code>オフ→PAUSED</code>(既定オフ=誤配信防止)</td></tr>
|
|
532
559
|
<tr><td class="mono">budget</td><td>キャンペーン行「1日の予算」</td><td>未入力時はモード別既定(新規12,000/ASC50,000)</td></tr>
|
|
533
560
|
<tr><td class="mono">biddingStrategy</td><td>キャンペーン行「入札戦略」+ 目標CV単価</td><td><code>tCPA → MAXIMIZE_CONVERSIONS + targetCpa</code>(tCPA値はキャンペーン行→案件既定8,500の順)/ <code>最大配信 → MAXIMIZE_CONVERSIONS</code>(目標なし <span class="pill amber">要実機確認</span>)</td></tr>
|
|
@@ -536,11 +563,12 @@ 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>
|
|
543
|
-
<tr><td class="mono">ads[].finalUrl</td><td>自動生成(広告行の入力項目ではない)</td><td>YDA は <code>baseURL + paramCode + "?ad=" + paramCode + "&creative=" + paramCode + "-" + 広告名</code> / Meta は <code>baseURL + "?ad=" + paramCode</code
|
|
571
|
+
<tr><td class="mono">ads[].finalUrl</td><td>自動生成(広告行の入力項目ではない)</td><td>YDA は <code>baseURL + paramCode + "?ad=" + paramCode + "&creative=" + paramCode + "-" + 広告名</code> / Meta は <code>baseURL + "?ad=" + paramCode</code>(<b>creative= もパス連結もしない</b>。paramCode は<b>広告行 > 広告セット</b>の実効値)。案件設定の <b>URLパラメータ を OFF</b> にすると <code>baseURL</code> のみ(paramCode 列は非表示になり未入力でもエラーにならない)。記事URLに既にクエリがある場合は <code>&</code> で連結する</td></tr>
|
|
544
572
|
</tbody></table>
|
|
545
573
|
<div class="callout">プランナーの保存JSON(バージョン保存)はこれとは別物で、<code>schemaVersion:2</code> の全state(meta / project / campaigns / ui / refs / submit)を保持する。<b>meta</b> = バージョンID・作成/更新日時・作成者・対象媒体・名称・メモ。旧v1 JSONも読込時に自動マイグレーションされる。</div>
|
|
546
574
|
</div></details>
|
|
@@ -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>
|
|
@@ -638,7 +666,7 @@ finalUrl = baseURL + paramCode + "?ad=" + paramCode + "&creative=" + …
|
|
|
638
666
|
|
|
639
667
|
<details class="sec" open><summary><span class="num">5</span><span class="stitle">未確定事項・未対応</span><span class="chev"></span></summary>
|
|
640
668
|
<div class="body">
|
|
641
|
-
<p class="lead">命名規則は <b>YDA 踏襲で確定</b>(<code>{番号}{i|v}-{見出し}L{本文}-{paramCode}</code>
|
|
669
|
+
<p class="lead">命名規則は <b>YDA 踏襲で確定</b>(<code>{番号}{i|v}-{見出し}L{本文}-{paramCode}</code>)。<b>最終URLは <code>{記事URL}?ad={paramCode}</code> のみ</b>(YDA と違い <code>creative=</code> もパス連結もしない)。<b>paramCode は広告セット行が既定値・広告行が上書き</b>(広告行が空欄なら広告セットの値を継承する)。以下が残っている。</p>
|
|
642
670
|
<ul>
|
|
643
671
|
<li>Meta広告フィードバックの所在と最終確認者(タスク確認事項より・未回答)。</li>
|
|
644
672
|
<li>クリエイティブ検証の単位(YDAの「1キャンペーン=1変数」に相当する実験設計)。</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">
|
|
@@ -783,6 +807,13 @@ const uid = p => p + '_' + (Date.now().toString(36)) + (_uc++).toString(36);
|
|
|
783
807
|
const esc = s => String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
784
808
|
const glen = s => [...String(s==null?'':s)].length;
|
|
785
809
|
const today = () => { const d=new Date(); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0'); };
|
|
810
|
+
/* 配信開始日時の既定 = 翌日 05:00 (JST)。当日配信は審査が間に合わず意図しない時間に出るため。
|
|
811
|
+
YDA も Meta も 05:00 が効く: YDA は startDate(yyyyMMdd) + **startTime(HHmm)** の2フィールドに分けて送る
|
|
812
|
+
(公式: ディスプレイ広告API v18 CampaignService。startTime を送るときは startDate が必須)。 */
|
|
813
|
+
const DEFAULT_START_TIME = '05:00';
|
|
814
|
+
const defaultStart = () => { const d=new Date(); d.setDate(d.getDate()+1); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0'); };
|
|
815
|
+
const defaultStartDateTime = () => defaultStart()+' '+DEFAULT_START_TIME;
|
|
816
|
+
const defaultStartIso = () => defaultStart()+'T'+DEFAULT_START_TIME+':00+0900';
|
|
786
817
|
/* S12: 日時文字列 "YYYY-MM-DD[ HH:mm[:ss]]" ⇄ {date, time} の相互変換。
|
|
787
818
|
kind:'datetime' チップ (日付ピッカー + 時刻ピッカー) と state の1本文字列を橋渡しする。
|
|
788
819
|
・秒は input[type=time] の既定表示に合わせて分までに丸める (秒付きの旧データも壊さず読める)
|
|
@@ -796,8 +827,11 @@ const joinDateTime = (date, time) => {
|
|
|
796
827
|
const d=String(date||'').trim(); if(!d) return '';
|
|
797
828
|
const t=String(time||'').trim(); return t ? d+' '+t : d;
|
|
798
829
|
};
|
|
799
|
-
/* 入稿ボディ用: 日時文字列から日付部分だけを取り出す (YDA の startDate
|
|
830
|
+
/* 入稿ボディ用: 日時文字列から日付部分だけを取り出す (YDA の startDate は yyyyMMdd 相当の日付のみ) */
|
|
800
831
|
const dateOnly = v => splitDateTime(v).date;
|
|
832
|
+
/* 入稿ボディ用: 時刻部分を YDA の startTime フォーマット **HHmm** にする。
|
|
833
|
+
時刻が無ければ '' を返し、呼び出し側で startTime キー自体を出さない (媒体既定のまま)。 */
|
|
834
|
+
const timeHHmm = v => { const t=splitDateTime(v).time; return t ? t.replace(':','') : ''; };
|
|
801
835
|
/* 引数を左から順に見て「trim して空文字でない最初の値」を返す。優先順位を1行で書くための道具。 */
|
|
802
836
|
const firstNonEmpty = (...vs) => { for(const v of vs){ const s=String(v==null?'':v).trim(); if(s!=='') return s; } return ''; };
|
|
803
837
|
/* URLのクエリ値に埋める文字列のエスケープ (媒体非依存のURLユーティリティ)。
|
|
@@ -910,18 +944,6 @@ function renderChrome(){
|
|
|
910
944
|
return '<button data-rt="'+esc(k)+'"'+(k===refTab?' class="active"':'')+'>'+esc(T.countLabel||T.label||k)+'</button>';
|
|
911
945
|
}).join('');
|
|
912
946
|
}
|
|
913
|
-
renderFixedType();
|
|
914
|
-
}
|
|
915
|
-
/* 固定タイプチップ (変更不可) は媒体定義 pf().fixedType から描画する。
|
|
916
|
-
YDA=レスポンシブ/画像、Meta=単一画像 / 単一動画。コアに媒体語彙を持たない。 */
|
|
917
|
-
function renderFixedType(){
|
|
918
|
-
const host=document.getElementById('planTools'); if(!host) return;
|
|
919
|
-
const list=(pf()&&pf().fixedType)||[];
|
|
920
|
-
if(!list.length){ host.innerHTML=''; host.hidden=true; return; }
|
|
921
|
-
host.hidden=false;
|
|
922
|
-
host.innerHTML='<span class="chip" title="このプランナーは固定タイプです(変更不可)">タイプ'
|
|
923
|
-
+ list.map(f=>'<select class="chipin" disabled title="'+esc(f.label)+'(固定)"><option>'+esc(f.value)+'</option></select>').join('')
|
|
924
|
-
+ '</span>';
|
|
925
947
|
}
|
|
926
948
|
window.switchPlatform=function(id){
|
|
927
949
|
if(!PLATFORMS[id]){ toast('この媒体はまだ選択できません(媒体スキーマ未登録)'); return false; }
|
|
@@ -999,8 +1021,10 @@ const REF_TABLES = {
|
|
|
999
1021
|
keyKind:'number', keyLabel:'画像番号', keyWidth:'80px',
|
|
1000
1022
|
addSub:'画像番号→Card/SQ の mediaID。新規作成した画像をメディアに登録',
|
|
1001
1023
|
emptyMsg:'該当する画像がありません',
|
|
1002
|
-
|
|
1003
|
-
|
|
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'}],
|
|
1004
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>',
|
|
1005
1029
|
columns:[
|
|
1006
1030
|
{label:'Card mediaID (1200)', cls:'mono', cell:(no,v)=>esc(v&&v.card||'—')},
|
|
@@ -1008,7 +1032,7 @@ const REF_TABLES = {
|
|
|
1008
1032
|
{label:'ファイル名', cls:'mono', style:'color:var(--faint);font-size:11px',
|
|
1009
1033
|
cell:no=>esc(no)+'-1200.jpg / '+esc(no)+'-1080.jpg'}
|
|
1010
1034
|
],
|
|
1011
|
-
optionLabel:v=>
|
|
1035
|
+
optionLabel:v=>[v&&v.card?'Card':'', v&&v.sq?'SQ':''].filter(Boolean).join('/'),
|
|
1012
1036
|
/* ▼ドロワー(一覧)の1行テキスト。媒体ごとの値の見せ方はここだけが持つ */
|
|
1013
1037
|
drawerLabel:v=>'Card '+esc(v&&v.card||'—')+' / SQ '+esc(v&&v.sq||'—') },
|
|
1014
1038
|
adGroups:{ key:'adGroups', label:'広告グループ', countLabel:'広告グループ', editable:false, required:false,
|
|
@@ -1110,11 +1134,24 @@ PLATFORMS.yda = {
|
|
|
1110
1134
|
consoleUrl:'https://ads.yahoo.co.jp/manager/#/{mcc}/{accountId}/list/{resource}',
|
|
1111
1135
|
consoleResourceMap:{campaigns:'campaigns', adGroups:'adgroups', ads:'ads'},
|
|
1112
1136
|
|
|
1113
|
-
/* ---- 形式(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) が持つ。 */
|
|
1114
1144
|
mediaFormats:[
|
|
1115
|
-
{id:'Card',
|
|
1116
|
-
{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)'}
|
|
1117
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'; },
|
|
1118
1155
|
fmtOf(ad){ const F=PLATFORMS.yda.mediaFormats; return F.find(x=>x.id===(ad&&ad.format)) || F[0]; },
|
|
1119
1156
|
|
|
1120
1157
|
/* ---- 参照テーブル (S6 の REF_TABLES) ---- */
|
|
@@ -1129,7 +1166,8 @@ PLATFORMS.yda = {
|
|
|
1129
1166
|
時刻は運用メモとして state / 保存JSON にだけ残り、入稿ボディへは日付部分だけを送る (buildPlan.campaign)。 */
|
|
1130
1167
|
{key:'startDate', label:'配信開始日時', kind:'datetime', fallback:'',
|
|
1131
1168
|
title:'配信開始日時(JST)。日付はカレンダー、時刻は時計から選べます。\n'
|
|
1132
|
-
+'
|
|
1169
|
+
+'空欄なら翌日 05:00 が既定です。日付は startDate、時刻は startTime(HHmm)として入稿されます。\n'
|
|
1170
|
+
+'日付だけを入れた場合は時刻を送らず、媒体側の既定(0時)から配信されます。'},
|
|
1133
1171
|
{key:'cta', label:'CTA', kind:'text', width:'7ch'},
|
|
1134
1172
|
{key:'deliveryStatus', label:'配信', kind:'select', options:['オフ','オン']},
|
|
1135
1173
|
/* S14: フリークエンシー(viewableFrequencyCap)の案件既定。
|
|
@@ -1146,7 +1184,7 @@ PLATFORMS.yda = {
|
|
|
1146
1184
|
title:'OFF にすると最終リンク先URLに ?ad= / &creative= を付けません(案件単位)'}
|
|
1147
1185
|
],
|
|
1148
1186
|
/* プランツールバーの固定プルダウン (変更不可) */
|
|
1149
|
-
|
|
1187
|
+
/* 広告タイプ (レスポンシブ/バナー) は **広告行の列**で切り替えるので固定チップには出さない */
|
|
1150
1188
|
|
|
1151
1189
|
/* ---- ターゲティング列の選択肢 (旧 TG_OPTIONS)。キーは COLS の tgKey ---- */
|
|
1152
1190
|
targeting:{
|
|
@@ -1178,6 +1216,10 @@ PLATFORMS.yda = {
|
|
|
1178
1216
|
/* S14: viewableFrequencyCap の enum (正: frontend/src/pages/submission/types.ts の FREQ_*_OPTIONS) */
|
|
1179
1217
|
freqLevel:{'広告グループ':'AD_GROUP','キャンペーン':'CAMPAIGN','広告':'AD'},
|
|
1180
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',
|
|
1181
1223
|
statusOn:'ACTIVE', statusOff:'PAUSED'
|
|
1182
1224
|
},
|
|
1183
1225
|
|
|
@@ -1201,6 +1243,8 @@ PLATFORMS.yda = {
|
|
|
1201
1243
|
},
|
|
1202
1244
|
status(s, P){ return s==='オン'?P.enums.statusOn:P.enums.statusOff; },
|
|
1203
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; },
|
|
1204
1248
|
/* C7: refpick セル (実IDのCSV) → 数値ID配列。落ちた分は ctx.stats.droppedRefIds に計上 */
|
|
1205
1249
|
idList(s, ctx){ return refIdList(s, ctx); }
|
|
1206
1250
|
},
|
|
@@ -1219,7 +1263,8 @@ PLATFORMS.yda = {
|
|
|
1219
1263
|
budgetNew:12000, budgetAsc:50000, goal:'CONVERSION', startDate:''
|
|
1220
1264
|
},
|
|
1221
1265
|
/* C6: adName は空 = 自動生成 (naming.adName)。C1: imagePath は空 = 従来挙動 */
|
|
1222
|
-
|
|
1266
|
+
/* adKind = 広告タイプ軸 (レスポンシブ / バナー)。format はサイズ軸 (Card / SQ) で直交する */
|
|
1267
|
+
ad:{format:'Card', adKind:'レスポンシブ', imageNo:'', titleNo:'', descNo:'', imagePath:'', adName:'', desc2:''},
|
|
1223
1268
|
/* S14: freq* は「案件設定チップの既定を新規CP作成時にコピーする」ので factories.newCampaign 側で入れる */
|
|
1224
1269
|
campaign:{name:'新規キャンペーン', campaignId:'', isExisting:false, urlNumber:'',
|
|
1225
1270
|
freqMode:'default', freqOn:false, freqLevel:'AD_GROUP', freqUnit:'WEEK', freqCount:3}
|
|
@@ -1253,9 +1298,12 @@ PLATFORMS.yda = {
|
|
|
1253
1298
|
naming:{
|
|
1254
1299
|
crName(ad, P){ return String(ad.imageNo||'') + P.fmtOf(ad).crSuffix; },
|
|
1255
1300
|
/* C8: URLパラメータ OFF の案件は paramCode を入力しない (列も隠す) ので、
|
|
1256
|
-
広告名の末尾 "-{paramCode}" も付けない (末尾が "-" だけ残るのを避ける)。
|
|
1301
|
+
広告名の末尾 "-{paramCode}" も付けない (末尾が "-" だけ残るのを避ける)。
|
|
1302
|
+
バナー (BANNER_AD) にはタイトル/説明文が存在しないので、"-{タイトル番号}L{説明文番号}" の区間も
|
|
1303
|
+
出さない (957c-jv_Cac_148)。サイズ記号 c/s は残す。 */
|
|
1257
1304
|
adName(ad, group, crName, project){
|
|
1258
|
-
const head =
|
|
1305
|
+
const head = PLATFORMS.yda.isBanner(ad) ? crName
|
|
1306
|
+
: (crName + '-' + (ad.titleNo??'') + 'L' + (ad.descNo??''));
|
|
1259
1307
|
if(project && project.urlParams===false) return head;
|
|
1260
1308
|
return head + '-' + (group.paramCode||'');
|
|
1261
1309
|
},
|
|
@@ -1295,7 +1343,10 @@ PLATFORMS.yda = {
|
|
|
1295
1343
|
},
|
|
1296
1344
|
groupBadges(g){ return '<span class="pill mono">【'+esc(g.paramCode||'—')+'】</span>'; },
|
|
1297
1345
|
adBadge(a, r){ return '<span class="cbadge">'+esc(a.imageNo||'—')+(a.format||'Card')[0].toLowerCase()+'</span>'; },
|
|
1298
|
-
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; }
|
|
1299
1350
|
},
|
|
1300
1351
|
|
|
1301
1352
|
/* ---- S8: 解決 (派生値 / 純関数) ---- */
|
|
@@ -1309,8 +1360,12 @@ PLATFORMS.yda = {
|
|
|
1309
1360
|
const adName=String(ad.adName||'').trim() || autoAdName;
|
|
1310
1361
|
const me = refs.media[String(ad.imageNo)];
|
|
1311
1362
|
const mediaId = me ? me[fmt.mediaKey] : undefined;
|
|
1312
|
-
|
|
1313
|
-
|
|
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へのフォールバックなし)
|
|
1314
1369
|
const finalUrl = P.naming.finalUrl(ad, group, adName, p);
|
|
1315
1370
|
const errs=[], warns=[], errCodes=[], warnCodes=[];
|
|
1316
1371
|
// S2: 日本語ラベル(errs/warns)と安定コード(errCodes/warnCodes)を同順で1:1に積む
|
|
@@ -1324,10 +1379,12 @@ PLATFORMS.yda = {
|
|
|
1324
1379
|
// 番号が入っているのに参照表で引けない場合のみ従来どおり未解決エラー。
|
|
1325
1380
|
const titleNoStr=String(ad.titleNo||'').trim();
|
|
1326
1381
|
const descNoStr=String(ad.descNo||'').trim();
|
|
1327
|
-
if(
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
+
}
|
|
1331
1388
|
// B11: paramCode空はエラー(export対象外・URL生成に必須)。F-6: refs.groups 払出判定は廃止。
|
|
1332
1389
|
/* C8: URLパラメータ OFF の案件では paramCode 自体が不要 (列も隠す) のでエラーにしない */
|
|
1333
1390
|
if(p.urlParams!==false && String(group.paramCode||'').trim()==='') pushErr('paramCode未入力', EC.PARAM_CODE_EMPTY);
|
|
@@ -1343,15 +1400,16 @@ PLATFORMS.yda = {
|
|
|
1343
1400
|
if(!pendingImage && imgNoStr!=='' && imagePaths.length>0) pushWarn('画像パス無視(画像番号が解決済み)', EC.IMAGE_PATH_IGNORED);
|
|
1344
1401
|
// F-6: ID解決は adGroupId ベース。campaignId は idsCell表示用に持たない(親CP参照は呼び出し側)
|
|
1345
1402
|
const aid=String(group.adGroupId||'').trim();
|
|
1346
|
-
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,
|
|
1347
1405
|
mediaId:mediaId||'', pendingImage, titleText:titleText===undefined?'':titleText,
|
|
1348
1406
|
descText:descText===undefined?'':descText, finalUrl, errs, warns, errCodes, warnCodes, resolvedGroup:!!aid};
|
|
1349
1407
|
},
|
|
1350
1408
|
|
|
1351
1409
|
/* ---- S8: 入稿計画JSON (SubmitPlanFile) の3階層ボディ生成 ---- */
|
|
1352
1410
|
/* S13: 案件設定の書き出し前ガード。
|
|
1353
|
-
startDate
|
|
1354
|
-
|
|
1411
|
+
startDate は空欄なら翌日にフォールバックする仕様だが、「読めない書式」まで空と同じ扱いにすると
|
|
1412
|
+
入力した日付が黙って翌日にすり替わる。GUI (日付/時刻ピッカー) からは不正値は入らないので、
|
|
1355
1413
|
ここに来るのは旧保存JSONの読み込みか手編集だけ。その場合は書き出しを止めて気づけるようにする。 */
|
|
1356
1414
|
validateProject(p){
|
|
1357
1415
|
const s=String((p&&p.startDate)||'').trim();
|
|
@@ -1369,12 +1427,17 @@ PLATFORMS.yda = {
|
|
|
1369
1427
|
name:c.name,
|
|
1370
1428
|
goal:p.goal||'CONVERSION',
|
|
1371
1429
|
status:P.mappers.status(p.deliveryStatus, P), // E-2: キャンペーンstatus列は撤去 → 案件設定の配信値を使用
|
|
1372
|
-
/* S12:
|
|
1373
|
-
|
|
1430
|
+
/* S12/I-YDA-TIME: Yahoo Ads API は startDate(yyyyMMdd 相当) と startTime(HHmm) の2フィールド。
|
|
1431
|
+
日付と時刻を分けて送る (公式: startTime を指定するときは startDate が必須)。 */
|
|
1432
|
+
startDate:dateOnly(p.startDate)||defaultStart(), /* 既定は翌日 */
|
|
1374
1433
|
budget:Number(c.budget)||0, // A-1: キャンペーン個別値のみ
|
|
1375
1434
|
biddingStrategy,
|
|
1376
1435
|
adGroups:[]
|
|
1377
1436
|
};
|
|
1437
|
+
/* 開始時刻 (HHmm)。案件設定が時刻を持たないときは既定 05:00 を使う。
|
|
1438
|
+
空欄になるのは「日付だけを明示的に入れた」ケースなので、そのときは startTime を送らない。 */
|
|
1439
|
+
const stTime = p.startDate ? timeHHmm(p.startDate) : timeHHmm(defaultStartDateTime());
|
|
1440
|
+
if(stTime) cp.startTime=stTime;
|
|
1378
1441
|
/* S14: フリークエンシー (viewableFrequencyCap)。OFF / 回数未入力なら **キー自体を出さない**
|
|
1379
1442
|
(backend の campaignSchema では全て optional。付けない = 媒体既定のまま) */
|
|
1380
1443
|
/* S14: 'default' は案件共通設定を引き、'off' は付けない。小数はここまでに落ちている
|
|
@@ -1414,17 +1477,24 @@ PLATFORMS.yda = {
|
|
|
1414
1477
|
},
|
|
1415
1478
|
ad(a, r, g, ctx){
|
|
1416
1479
|
const P=PLATFORMS.yda, p=ctx.project;
|
|
1480
|
+
/* adType は「広告タイプ」列から決まる (レスポンシブ→RESPONSIVE_AD / バナー→BANNER_AD)。
|
|
1481
|
+
backend/CLI は **省略時 RESPONSIVE_AD 扱い** なので、明示しても旧プランと矛盾しない。 */
|
|
1417
1482
|
const ad={
|
|
1418
1483
|
adName:r.adName,
|
|
1419
1484
|
status:P.mappers.status(g.deliveryStatus||p.deliveryStatus, P),
|
|
1420
|
-
mediaType:'IMAGE', //
|
|
1421
|
-
|
|
1422
|
-
headlines:String(r.titleText||'')===''?[]:[String(r.titleText)],
|
|
1423
|
-
descriptions:String(r.descText||'')===''?[]:[String(r.descText)],
|
|
1424
|
-
principal:p.principal||'', // E-1: 主体者はアカウントJSON由来 (st.project.principal)
|
|
1425
|
-
buttonText:P.mappers.cta(p.cta, P), // CTA(案件設定)→ YDA buttonText enum。入稿ADD必須
|
|
1485
|
+
mediaType:'IMAGE', // 画像広告 (backend ydaAdSchema IMAGE)。バナーもメイン形式は IMAGE
|
|
1486
|
+
adType:r.adType,
|
|
1426
1487
|
finalUrl:r.finalUrl // 実データ準拠 (?ad=paramCode&creative=...)
|
|
1427
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
|
+
}
|
|
1428
1498
|
if(r.pendingImage){
|
|
1429
1499
|
// 未登録画像: mediaId未確定。管理番号を残し CLI が先にアップロードして解決する
|
|
1430
1500
|
// C6-1: 画像パス列 (カンマ区切り) を imagePaths に展開する。空なら [] のまま (agent/CLI が補完)
|
|
@@ -1946,6 +2016,12 @@ function renderCell(col, item, ri, ci, ctx){
|
|
|
1946
2016
|
if(col.kind==='badge'){ const id=String(row[col.field]||'').trim(); return '<td>'+newOrIdBadge(!id, id)+'</td>'; }
|
|
1947
2017
|
if(col.kind==='derived') return '<td>'+col.derive(row, ctx)+'</td>';
|
|
1948
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
|
+
|
|
1949
2025
|
if(ro){
|
|
1950
2026
|
/* 既存呼び出し行の read-only 表現は列記述子で明示する:
|
|
1951
2027
|
hideOnExisting = 常に「—」/ roEmptyDash = 値ありは robadge・空は「—」/ roStyle:'input' = readonly入力欄 /
|
|
@@ -2000,6 +2076,21 @@ function renderCell(col, item, ri, ci, ctx){
|
|
|
2000
2076
|
return '<td class="tgtd"><div class="tgpick'+(shown?'':' tgempty')+'" '+sel+' title="'+esc(shown||(label+'(クリックで選択)'))+'">'+disp+'<span class="tgcaret">▾</span></div></td>';
|
|
2001
2077
|
}
|
|
2002
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
|
+
|
|
2003
2094
|
/* S15: kind:'datetime' = セル内を <input type=date> + <input type=time> の2分割にする。
|
|
2004
2095
|
state は従来どおり1本の文字列 ("YYYY-MM-DD HH:mm" / 時刻が空なら "YYYY-MM-DD") なので、
|
|
2005
2096
|
col.get/col.set は不要でコピペ・一括入力・連番は既存経路のまま動く。
|
|
@@ -2076,7 +2167,8 @@ function renderTree(){
|
|
|
2076
2167
|
html+='<div class="adnode'+(r.errs.length?' hasErr':'')+'" data-ad="'+a.uid+'">';
|
|
2077
2168
|
html+='<span class="dot '+dot+'"></span>';
|
|
2078
2169
|
html+=pf().tree.adBadge(a,r); // S7: クリエイティブ名バッジ (媒体スキーマ)
|
|
2079
|
-
|
|
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>';
|
|
2080
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>';
|
|
2081
2173
|
html+='</div>';
|
|
2082
2174
|
});
|
|
@@ -2304,8 +2396,20 @@ function freqInheritText(){
|
|
|
2304
2396
|
|
|
2305
2397
|
const COLS = {
|
|
2306
2398
|
ads:[
|
|
2307
|
-
|
|
2308
|
-
|
|
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')?'バナー':'レスポンシブ'; }},
|
|
2309
2413
|
{field:'imageNo', label:'画像番号', kind:'refcombo', refTable:'media', drawerType:'media', mediaAxis:'format',
|
|
2310
2414
|
width:'6ch', sequenceable:true, errCodes:[EC.IMAGE_UNRESOLVED],
|
|
2311
2415
|
ghostKey:'media', ddTitle:'登録画像から選択', ghostTitle:r=>'メディアID '+r.mediaId,
|
|
@@ -2315,10 +2419,16 @@ const COLS = {
|
|
|
2315
2419
|
return '<span class="idpill err">#N/A</span>'; }},
|
|
2316
2420
|
{field:'titleNo', label:'タイトル番号', kind:'refcombo', refTable:'titles', drawerType:'titles',
|
|
2317
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
|
+
|
|
2318
2425
|
ghostKey:'title', ddTitle:'タイトル一覧から選択', ghostTitle:r=>r.titleText,
|
|
2319
2426
|
ghost:r=>r.errCodes.includes(EC.TITLE_UNRESOLVED)?'<span class="err">未解決</span>':esc(r.titleText.slice(0,40))},
|
|
2320
2427
|
{field:'descNo', label:'説明文番号', kind:'refcombo', refTable:'descriptions', drawerType:'titles',
|
|
2321
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
|
+
|
|
2322
2432
|
ghostKey:'desc', ddTitle:'説明文一覧から選択', ghostTitle:r=>r.descText,
|
|
2323
2433
|
ghost:r=>r.errCodes.includes(EC.DESC_UNRESOLVED)?'<span class="err">— 未解決</span>':esc(r.descText.slice(0,44))},
|
|
2324
2434
|
{field:'paramCode', label:'paramCode(グループ)', kind:'mono', width:'120px',
|
|
@@ -2335,14 +2445,18 @@ const COLS = {
|
|
|
2335
2445
|
get:(row,ctx)=>ctx.r?ctx.r.finalUrl:''},
|
|
2336
2446
|
/* C5: 参照表(refs.titles / refs.descriptions)の本文を直接編集する列。
|
|
2337
2447
|
番号未入力のまま本文を書くと空き番号を自動採番して参照表に登録する。 */
|
|
2338
|
-
makeRefTextCol({field:'titleText', label:'タイトル本文', table:'titles', noField:'titleNo', width:'180px'}),
|
|
2339
|
-
|
|
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項目がありません(画像に焼き込み)'}),
|
|
2340
2454
|
/* C6-1: 量産時のローカル画像パス担体 (カンマ区切りで複数可)。少数入稿は Skill の画像入稿フローが主導線 */
|
|
2341
2455
|
{field:'imagePath', label:'画像パス', kind:'mono', width:'150px',
|
|
2342
2456
|
title:'ローカル画像の絶対 / ~/… / 計画JSON基準の相対パス。カンマ区切りで複数可。※画像番号が参照表で解決できる行では入稿JSONに載りません(差し替えたいときは画像番号を空に)'},
|
|
2343
2457
|
/* C6: 広告名は自由入力 (空 = 自動生成)。placeholder に自動生成値を出す */
|
|
2344
2458
|
{field:'adName', label:'広告名', kind:'mono', width:'170px',
|
|
2345
|
-
title:'
|
|
2459
|
+
title:'空欄なら自動生成(レスポンシブ: {画像番号}{c|s}-{タイトル番号}L{説明文番号}-{paramCode} / バナー: {画像番号}{c|s}-{paramCode})。手入力すると最終リンク先URLの creative= も変わります',
|
|
2346
2460
|
placeholder:ctx=>ctx.r?ctx.r.autoAdName:''},
|
|
2347
2461
|
/* derived: resolveAd() 由来の読取専用セル。tabCols()/貼付/セル選択の対象外 (末尾固定) */
|
|
2348
2462
|
{field:'adGroupId', label:'CP / AG ID', kind:'derived',
|
|
@@ -2483,7 +2597,7 @@ const META_REF_TABLES = {
|
|
|
2483
2597
|
/* 列記述子 (Meta)。役割・意味は COLS のコメント参照 */
|
|
2484
2598
|
const META_COLS = {
|
|
2485
2599
|
ads:[
|
|
2486
|
-
{field:'format', label:'形式', kind:'
|
|
2600
|
+
{field:'format', label:'形式', kind:'optpick', options:['IMAGE','VIDEO'], minWidth:'70px',
|
|
2487
2601
|
coerce:v=>(String(v).toUpperCase()==='VIDEO'?'VIDEO':'IMAGE')},
|
|
2488
2602
|
{field:'imageNo', label:'クリエイティブ番号', kind:'refcombo', refTable:'media', drawerType:'media', mediaAxis:'format',
|
|
2489
2603
|
width:'6ch', sequenceable:true, errCodes:[EC.IMAGE_UNRESOLVED],
|
|
@@ -2501,11 +2615,13 @@ const META_COLS = {
|
|
|
2501
2615
|
width:'6ch', sequenceable:true, errCodes:[EC.DESC_UNRESOLVED, EC.DESC_TOO_LONG],
|
|
2502
2616
|
ghostKey:'desc', ddTitle:'メインテキスト一覧から選択', ghostTitle:r=>r.descText,
|
|
2503
2617
|
ghost:r=>r.errCodes.includes(EC.DESC_UNRESOLVED)?'<span class="err">— 未解決</span>':esc(r.descText.slice(0,44))},
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2618
|
+
/* I-150: 広告ごとに個別設定できる。空欄なら広告セット行の値を継承 (薄字で継承元を表示)。
|
|
2619
|
+
⚠ scope:'parentGroup' を付けない。付けると広告セット行の値を書き換えてしまう。 */
|
|
2620
|
+
{field:'paramCode', label:'paramCode', kind:'mono', width:'120px',
|
|
2621
|
+
sequenceable:true, errCodes:[EC.PARAM_CODE_EMPTY],
|
|
2622
|
+
title:'空欄なら広告セットの paramCode を継承。入力するとこの広告だけ上書きする。最終URLは 記事URL?ad={paramCode}',
|
|
2507
2623
|
hidden:p=>urlParamsOff(p),
|
|
2508
|
-
|
|
2624
|
+
placeholder:ctx=>(ctx.r?ctx.r.groupParamCode:'')},
|
|
2509
2625
|
/* C8: 記事slug / 記事URL(上書き) 列は廃止。最終URL = 記事URL + ?ad={paramCode} の導出セル */
|
|
2510
2626
|
{field:'finalUrl', label:'最終リンク先URL', kind:'mono', width:'240px',
|
|
2511
2627
|
alwaysReadonly:true, roStyle:'input',
|
|
@@ -2513,7 +2629,7 @@ const META_COLS = {
|
|
|
2513
2629
|
get:(row,ctx)=>ctx.r?ctx.r.finalUrl:''},
|
|
2514
2630
|
/* C6: 広告名は自由入力 (空 = 自動生成)。YDA と同じ規約 */
|
|
2515
2631
|
{field:'adName', label:'広告名', kind:'mono', width:'170px',
|
|
2516
|
-
title:'空欄なら自動生成({番号}{i|v}-{見出し番号}L{本文番号}-{paramCode}
|
|
2632
|
+
title:'空欄なら自動生成({番号}{i|v}-{見出し番号}L{本文番号}-{paramCode})。Meta の最終リンク先URLは 記事URL?ad={paramCode} だけで決まるので、広告名を手入力してもURLは変わりません',
|
|
2517
2633
|
placeholder:ctx=>ctx.r?ctx.r.autoAdName:''},
|
|
2518
2634
|
{field:'adGroupId', label:'CP / AdSet ID', kind:'derived',
|
|
2519
2635
|
derive:(a,ctx)=>'<span class="idpill '+(ctx.r.resolvedGroup?'':'warn')+'" data-d="'+a.uid+'-ids">'+esc(ctx.r.adGroupId?('AS '+ctx.r.adGroupId):'新規')+'</span>'}
|
|
@@ -2524,7 +2640,7 @@ const META_COLS = {
|
|
|
2524
2640
|
readonlyOnExisting:true, roTitle:'既存広告セットのため編集不可'},
|
|
2525
2641
|
{field:'paramCode', label:'paramCode', kind:'mono', width:'120px', required:true,
|
|
2526
2642
|
sequenceable:true, readonlyOnExisting:true, hidden:p=>urlParamsOff(p),
|
|
2527
|
-
title:'URL
|
|
2643
|
+
title:'URL生成用のパラメータ(広告セット紐づけには不使用)。この広告セット配下の広告の既定値で、広告行に入力があればそちらが優先される。最終URLは 記事URL?ad={paramCode}'},
|
|
2528
2644
|
/* budgetLevel='adGroup' → 予算は広告セット行が持つ (CBO は使わない) */
|
|
2529
2645
|
{field:'dailyBudget', label:'日予算(円)', kind:'number', width:'84px', required:true,
|
|
2530
2646
|
coerce:numOrNull, readonlyOnExisting:true, roEmptyDash:true, errCodes:[EC.BUDGET_MISSING],
|
|
@@ -2551,9 +2667,9 @@ const META_COLS = {
|
|
|
2551
2667
|
readonlyOnExisting:true, roEmptyDash:true, coerce:v=>String(v==null?'':v).trim(),
|
|
2552
2668
|
errCodes:[EC.START_TIME_INVALID, EC.END_BEFORE_START],
|
|
2553
2669
|
placeholder:()=>{ const v=String((st.project&&st.project.startDate)||'').trim();
|
|
2554
|
-
return v!=='' ? v : '案件設定 /
|
|
2670
|
+
return v!=='' ? v : '案件設定 / 翌日05:00'; },
|
|
2555
2671
|
title:'この広告セットだけの配信開始日時(JST)。空欄なら案件設定「配信開始日時」を継承し、'
|
|
2556
|
-
+'
|
|
2672
|
+
+'それも空欄なら翌日 05:00。日付のみなら 00:00 開始(例 2026-08-01 10:30)。'
|
|
2557
2673
|
+'末尾に +09:00 / Z などのオフセットを書いた場合はそのまま採用される(JST 補完なし)。'},
|
|
2558
2674
|
{field:'endDate', label:'配信終了日時', kind:'datetime', width:'17ch',
|
|
2559
2675
|
readonlyOnExisting:true, roEmptyDash:true, coerce:v=>String(v==null?'':v).trim(),
|
|
@@ -2655,7 +2771,7 @@ PLATFORMS.meta = {
|
|
|
2655
2771
|
{key:'startDate', label:'配信開始日時', kind:'datetime', fallback:'',
|
|
2656
2772
|
title:'案件の既定値(広告セット行が空のときに使う)。すべて JST。'
|
|
2657
2773
|
+'日付のみなら 00:00 開始。時刻を入れるとその時刻ちょうどから配信(例 2026-08-01 10:30)。'
|
|
2658
|
-
+'
|
|
2774
|
+
+'空欄なら翌日 05:00 から。末尾に +09:00 / Z などのオフセットを書いた場合はそのまま採用される。'},
|
|
2659
2775
|
{key:'endDate', label:'配信終了日時', kind:'datetime', fallback:'',
|
|
2660
2776
|
title:'案件の既定値(広告セット行が空のときに使う)。すべて JST。'
|
|
2661
2777
|
+'日付のみならその日の 23:59:59 まで。時刻を入れるとその時刻で終了(例 2026-08-31 18:00)。'
|
|
@@ -2666,7 +2782,6 @@ PLATFORMS.meta = {
|
|
|
2666
2782
|
{key:'urlParams', label:'URLパラメータ', kind:'toggle', fallback:true,
|
|
2667
2783
|
title:'OFF にすると最終リンク先URLに ?ad= / &creative= を付けません(案件単位)'}
|
|
2668
2784
|
],
|
|
2669
|
-
fixedType:[{label:'広告タイプ',value:'単一画像 / 単一動画'}],
|
|
2670
2785
|
|
|
2671
2786
|
targeting:{
|
|
2672
2787
|
tg_gender:{label:'性別', multi:true, allLabel:'すべて', opts:['すべて','男性','女性']},
|
|
@@ -2789,7 +2904,7 @@ PLATFORMS.meta = {
|
|
|
2789
2904
|
startTime(dateStr){
|
|
2790
2905
|
const r=PLATFORMS.meta.mappers.normDateTime(dateStr);
|
|
2791
2906
|
if(r && typeof r==='object') return String(r.input); /* 不正はそのまま返し検証側で弾く */
|
|
2792
|
-
return r || (
|
|
2907
|
+
return r || defaultStartIso();
|
|
2793
2908
|
},
|
|
2794
2909
|
/* 終了日は「その日の終わりまで配信」の解釈。日付のみの入力を JST 23:59:59 に寄せる。
|
|
2795
2910
|
空欄なら null を返し、呼び出し側で endTime キー自体を出さない (無期限配信)。 */
|
|
@@ -2809,7 +2924,8 @@ PLATFORMS.meta = {
|
|
|
2809
2924
|
deliveryStatus:'オフ', startDate:'', endDate:''
|
|
2810
2925
|
},
|
|
2811
2926
|
/* C6: adName は空 = 自動生成 (naming.adName) */
|
|
2812
|
-
|
|
2927
|
+
/* I-150: paramCode は空 = 広告セットの値を継承 (広告ごとに入力すると上書き) */
|
|
2928
|
+
ad:{format:'IMAGE', imageNo:'', titleNo:'', descNo:'', adName:'', paramCode:''},
|
|
2813
2929
|
adGroup:{name:'', paramCode:'', isExisting:false, dailyBudget:1000, bidAmount:null,
|
|
2814
2930
|
ageMin:18, ageMax:65, tg_gender:'すべて', tg_device:'すべてのデバイス',
|
|
2815
2931
|
tg_placement:'自動配置', startDate:'', endDate:'', deliveryStatus:''},
|
|
@@ -2832,6 +2948,14 @@ PLATFORMS.meta = {
|
|
|
2832
2948
|
isExisting:true, ads:[P.factories.newAd()]}); }
|
|
2833
2949
|
},
|
|
2834
2950
|
|
|
2951
|
+
/* I-150: Meta の paramCode は「広告セットが既定値・広告行が上書き」の2層。
|
|
2952
|
+
広告行の paramCode が空 (=未入力) のときだけ広告セットの値を継承する。
|
|
2953
|
+
⚠ 広告名・最終URL・検証はすべてこの実効値を見る。group.paramCode を直接読まない。 */
|
|
2954
|
+
paramCodeOf(ad, group){
|
|
2955
|
+
const own=String((ad&&ad.paramCode)||'').trim();
|
|
2956
|
+
return own || String((group&&group.paramCode)||'');
|
|
2957
|
+
},
|
|
2958
|
+
|
|
2835
2959
|
/* 命名規則は YDA 踏襲 (形式記号は mediaFormats.crSuffix = i/v) */
|
|
2836
2960
|
naming:{
|
|
2837
2961
|
crName(ad, P){ return String(ad.imageNo||'') + P.fmtOf(ad).crSuffix; },
|
|
@@ -2839,15 +2963,16 @@ PLATFORMS.meta = {
|
|
|
2839
2963
|
adName(ad, group, crName, project){
|
|
2840
2964
|
const head = crName + '-' + (ad.titleNo??'') + 'L' + (ad.descNo??'');
|
|
2841
2965
|
if(project && project.urlParams===false) return head;
|
|
2842
|
-
return head + '-' + (group
|
|
2966
|
+
return head + '-' + PLATFORMS.meta.paramCodeOf(ad, group);
|
|
2843
2967
|
},
|
|
2844
2968
|
/* C8: Meta の最終URL = {記事URL}?ad={paramCode} のみ (YDA と違い creative= もパス連結もしない)。
|
|
2845
|
-
urlParams===false なら記事URLのみ。base が既にクエリを持つときは '&' で繋ぐ。
|
|
2969
|
+
urlParams===false なら記事URLのみ。base が既にクエリを持つときは '&' で繋ぐ。
|
|
2970
|
+
I-150: paramCode は広告行 > 広告セット の実効値。 */
|
|
2846
2971
|
finalUrl(ad, group, adName, project){
|
|
2847
2972
|
const base = (project.baseArticleUrl||'');
|
|
2848
2973
|
if(project.urlParams===false) return base;
|
|
2849
2974
|
const sep = base.indexOf('?')>=0 ? '&' : '?';
|
|
2850
|
-
return base + sep + 'ad=' + urlq(group
|
|
2975
|
+
return base + sep + 'ad=' + urlq(PLATFORMS.meta.paramCodeOf(ad, group));
|
|
2851
2976
|
},
|
|
2852
2977
|
campaignName(urlNumber, firstAd, P){
|
|
2853
2978
|
const cr = firstAd ? (String(firstAd.imageNo||'CR') + P.fmtOf(firstAd).crSuffix) : 'CR';
|
|
@@ -2875,6 +3000,8 @@ PLATFORMS.meta = {
|
|
|
2875
3000
|
const P=PLATFORMS.meta, p=ctx.project, refs=ctx.refs;
|
|
2876
3001
|
const fmt=P.fmtOf(ad);
|
|
2877
3002
|
const crName=P.naming.crName(ad, P);
|
|
3003
|
+
/* I-150: 実効 paramCode (広告行 > 広告セット)。表示・検証はこれを見る */
|
|
3004
|
+
const paramCode=P.paramCodeOf(ad, group).trim();
|
|
2878
3005
|
/* C6: 広告名は自由入力。空 = 自動生成にフォールバック */
|
|
2879
3006
|
const autoAdName=P.naming.adName(ad, group, crName, p);
|
|
2880
3007
|
const adName=String(ad.adName||'').trim() || autoAdName;
|
|
@@ -2899,11 +3026,12 @@ PLATFORMS.meta = {
|
|
|
2899
3026
|
else if(titleText!==undefined && glen(titleText)>T.warn) pushWarn('見出し'+T.warn+'字超', EC.TITLE_LONG_WARN);
|
|
2900
3027
|
if(descText!==undefined && glen(descText)>D.err) pushErr('本文'+D.err+'字超', EC.DESC_TOO_LONG);
|
|
2901
3028
|
else if(descText!==undefined && glen(descText)>D.warn) pushWarn('本文'+D.warn+'字超', EC.DESC_LONG_WARN);
|
|
2902
|
-
/* C8: URLパラメータ OFF の案件では paramCode 自体が不要 (列も隠す) のでエラーにしない
|
|
2903
|
-
|
|
3029
|
+
/* C8: URLパラメータ OFF の案件では paramCode 自体が不要 (列も隠す) のでエラーにしない
|
|
3030
|
+
I-150: 広告行・広告セットの **どちらにも無い** ときだけエラー */
|
|
3031
|
+
if(p.urlParams!==false && paramCode==='') pushErr('paramCode未入力', EC.PARAM_CODE_EMPTY);
|
|
2904
3032
|
if(pendingImage) pushWarn(ad.format==='VIDEO'?'動画未登録(要アップロード)':'画像未登録(要アップロード)', EC.MEDIA_MISSING);
|
|
2905
3033
|
const aid=String(group.adGroupId||'').trim();
|
|
2906
|
-
return {crName, adName, autoAdName, campaignId:'', adGroupId:aid, mediaKind:fmt.apiMediaKind, imageNo:noStr,
|
|
3034
|
+
return {crName, adName, autoAdName, paramCode, groupParamCode:String(group.paramCode||''), campaignId:'', adGroupId:aid, mediaKind:fmt.apiMediaKind, imageNo:noStr,
|
|
2907
3035
|
mediaId:mediaId||'', thumbHash:thumbHash||'', pendingImage,
|
|
2908
3036
|
titleText:titleText===undefined?'':titleText, descText:descText===undefined?'':descText,
|
|
2909
3037
|
finalUrl, errs, warns, errCodes, warnCodes, resolvedGroup:!!aid};
|
|
@@ -2924,7 +3052,7 @@ PLATFORMS.meta = {
|
|
|
2924
3052
|
if(sBad) errCodes.push(EC.START_TIME_INVALID);
|
|
2925
3053
|
if(eBad) errCodes.push(EC.END_TIME_INVALID);
|
|
2926
3054
|
if(!sBad && !eBad){
|
|
2927
|
-
const s=M.epochOf(firstNonEmpty(sRaw, p.startDate) ||
|
|
3055
|
+
const s=M.epochOf(firstNonEmpty(sRaw, p.startDate) || defaultStart());
|
|
2928
3056
|
const e=M.epochOf(firstNonEmpty(eRaw, p.endDate), {endOfDay:true}); /* 日付のみの終了は 23:59:59 で比較 */
|
|
2929
3057
|
if(!isNaN(s) && !isNaN(e) && e<s) errCodes.push(EC.END_BEFORE_START);
|
|
2930
3058
|
}
|
|
@@ -2945,7 +3073,7 @@ PLATFORMS.meta = {
|
|
|
2945
3073
|
if(psBad) errs.push({code:EC.START_TIME_INVALID, msg:'案件設定「配信開始日時」の書式が不正です(例 2026-08-01 10:30): '+ps});
|
|
2946
3074
|
if(peBad) errs.push({code:EC.END_TIME_INVALID, msg:'案件設定「配信終了日時」の書式が不正です(例 2026-08-31 23:59): '+pe});
|
|
2947
3075
|
if(!psBad && !peBad){
|
|
2948
|
-
const s=M.epochOf(ps||
|
|
3076
|
+
const s=M.epochOf(ps||defaultStart()), e=M.epochOf(pe, {endOfDay:true}); /* 日付のみの終了は 23:59:59 で比較 */
|
|
2949
3077
|
if(!isNaN(s) && !isNaN(e) && e<s) errs.push({code:EC.END_BEFORE_START, msg:'案件設定の配信終了日時が配信開始日時より前です'});
|
|
2950
3078
|
}
|
|
2951
3079
|
/* 広告セット行の配信日時 (日予算・入札の未入力は従来どおり行除外で扱い、ここでは見ない) */
|
|
@@ -2996,7 +3124,7 @@ PLATFORMS.meta = {
|
|
|
2996
3124
|
billingEvent:p.billingEvent||'IMPRESSIONS',
|
|
2997
3125
|
bidStrategy:p.bidStrategy||'LOWEST_COST_WITHOUT_CAP',
|
|
2998
3126
|
dailyBudget:Number(g.dailyBudget)||0, /* JPY はそのまま (currency_offset=1) */
|
|
2999
|
-
startTime:P.mappers.startTime(startSrc), /* 常に送る (
|
|
3127
|
+
startTime:P.mappers.startTime(startSrc), /* 常に送る (空なら翌日05:00)。JST 明示 ISO8601 */
|
|
3000
3128
|
/* 特別カテゴリ時は 18-65 固定・性別指定不可 (公式制約) */
|
|
3001
3129
|
ageMin: constrained?K.forcedAgeMin:(Number(g.ageMin)||18),
|
|
3002
3130
|
/* 年齢上限は空欄 = 上限なし → ageMax キー自体を出さない (age_max 省略 = Meta 側で上限なし)。
|
|
@@ -3104,6 +3232,7 @@ PLATFORMS.meta = {
|
|
|
3104
3232
|
' - 保存先: .claude/skills/ad-ops/サンプルプラン_meta.json',
|
|
3105
3233
|
' - フォーマット: {"schemaVersion":'+SCHEMA_VERSION+', "platform":"meta", "accountId":"act_<対象アカウントID>", "campaigns":[ ... ]}',
|
|
3106
3234
|
' - campaigns には キャンペーン → 広告セット(paramCode / dailyBudget / 年齢 / 性別 / 配置) → 広告(形式 IMAGE|VIDEO / クリエイティブ番号 / 見出し番号 / 本文番号) の構成を入れてください。',
|
|
3235
|
+
' 広告セットの paramCode は配下の広告の既定値です。広告ごとに変えたいときだけ広告側に paramCode を書いてください(空欄なら継承)。',
|
|
3107
3236
|
'3. 書き出したら、プランナーを開き直す(またはリロードして媒体を Meta に切り替える)と内容が自動で表示されます。'].join('\n');
|
|
3108
3237
|
},
|
|
3109
3238
|
submitHelp(ctx){
|
|
@@ -3188,6 +3317,7 @@ function cellCtx(item, tab, ctx){
|
|
|
3188
3317
|
}
|
|
3189
3318
|
function getVar(item,c,tab,ctx){
|
|
3190
3319
|
const col=colDefs(tab)[c]; if(!col) return '';
|
|
3320
|
+
if(col.disabled && col.disabled(item, cellCtx(item,tab,ctx))) return ''; /* 無効セルはコピーにも載せない */
|
|
3191
3321
|
const row=rowOf(item,col); if(!row) return '';
|
|
3192
3322
|
if(col.get){ const gv=col.get(row, cellCtx(item,tab,ctx)); return gv==null?'':String(gv); }
|
|
3193
3323
|
const v=row[col.field];
|
|
@@ -3197,6 +3327,7 @@ function setVar(item,c,val,tab,ctx){
|
|
|
3197
3327
|
const col=colDefs(tab)[c]; if(!col) return;
|
|
3198
3328
|
if(col.kind==='derived') return;
|
|
3199
3329
|
if(col.alwaysReadonly) return; /* adGroupId / campaignId */
|
|
3330
|
+
if(col.disabled && col.disabled(item, cellCtx(item,tab,ctx))) return; /* バナー行のタイトル/説明文 */
|
|
3200
3331
|
if((col.readonlyOnExisting||col.hideOnExisting) && rowIsExisting(item,col)) return; /* F-2: 既存呼び出し行は基本パラメータ編集不可 */
|
|
3201
3332
|
const row=rowOf(item,col); if(!row) return;
|
|
3202
3333
|
const v = col.coerce?col.coerce(val):val;
|
|
@@ -3239,7 +3370,11 @@ function flatVisible(){ const out=[]; if(sheetTab==='groups'){ st.campaigns.forE
|
|
|
3239
3370
|
let selA=null, selB=null, dragging=false;
|
|
3240
3371
|
function clearSel(){ selA=selB=null; applySelection(); }
|
|
3241
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)}; }
|
|
3242
|
-
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
|
+
}
|
|
3243
3378
|
function copySelText(){ const rg=selRange(); if(!rg) return null; const vis=flatVisible();
|
|
3244
3379
|
/* C1: 範囲全セルで getVar → get フック列が resolveAd を呼ぶ。読み取り専用パスなのでメモ化する */
|
|
3245
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'); }); }
|
|
@@ -3274,7 +3409,7 @@ function pasteGrid(txt){
|
|
|
3274
3409
|
function dupSelected(){
|
|
3275
3410
|
const rg=selRange(); if(!rg){ toast('複製する行を選択してください'); return; } const vis=flatVisible(); const items=[]; for(let r=rg.r0;r<=rg.r1;r++){ if(vis[r]) items.push(vis[r]); } if(!items.length){ toast('対象なし'); return; }
|
|
3276
3411
|
mutate(()=>{ items.forEach(it=>{
|
|
3277
|
-
if(it.kind==='g'){ const ng=JSON.parse(JSON.stringify(it.g)); ng.uid=uid('g'); ng.adGroupId=''; ng.isExisting=false; ng.name=dupGroupName(it.g.name); ng.paramCode=nextParamCode(it.g.paramCode); ng.ads.forEach(a=>a.uid=uid('a')); const idx=it.c.adGroups.indexOf(it.g); it.c.adGroups.splice(idx+1,0,ng); autoResolveGroupIds(ng, it.c); }
|
|
3412
|
+
if(it.kind==='g'){ const ng=JSON.parse(JSON.stringify(it.g)); ng.uid=uid('g'); ng.adGroupId=''; ng.isExisting=false; ng.name=dupGroupName(it.g.name); ng.paramCode=nextParamCode(it.g.paramCode); ng.ads.forEach(a=>{ a.uid=uid('a'); }); const idx=it.c.adGroups.indexOf(it.g); it.c.adGroups.splice(idx+1,0,ng); ng.ads.forEach(renumberAdParamCode); autoResolveGroupIds(ng, it.c); }
|
|
3278
3413
|
else if(it.kind==='c'){ const nc=cloneCampaign(it.c,false,false); const idx=st.campaigns.indexOf(it.c); st.campaigns.splice(idx+1,0,nc); }
|
|
3279
3414
|
else { const idx=it.g.ads.indexOf(it.a); it.g.ads.splice(idx+1,0,cloneAd(it.a,null)); }
|
|
3280
3415
|
}); }); clearSel(); toast(items.length+'件を複製');
|
|
@@ -3441,32 +3576,57 @@ function openTargetingDrawer(gUid, field){
|
|
|
3441
3576
|
renderDrawer();
|
|
3442
3577
|
document.getElementById('refDrawer').classList.add('open');
|
|
3443
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
|
+
}
|
|
3444
3593
|
/* ---- ターゲティング列の一括トグル ----
|
|
3445
3594
|
ドロワーで押した項目を「選択範囲 ∩ 同種の列」の全セルへ ON/OFF 一括適用する。
|
|
3446
3595
|
同種の判定は列記述子から引く (refpick は同じ refTable / tgpick は同じ tgKey)。
|
|
3447
3596
|
選択が無い・対象列に掛かっていない場合はアンカーの1セルだけを返し、従来の単体トグルと同じ挙動になる。 */
|
|
3448
|
-
function
|
|
3449
|
-
const
|
|
3450
|
-
const
|
|
3451
|
-
|
|
3452
|
-
const
|
|
3453
|
-
if(
|
|
3454
|
-
const compat=[]; cols.forEach((c,i)=>{
|
|
3455
|
-
if(base.kind==='refpick'){ if(c.kind==='refpick' && c.refTable===base.refTable) compat.push(i); }
|
|
3456
|
-
else if(c.kind==='tgpick' && c.tgKey===base.tgKey) compat.push(i);
|
|
3457
|
-
});
|
|
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; /* 別タブに切り替わっていたら選択範囲は無関係 */
|
|
3458
3603
|
const rg=selRange(); if(!rg) return anchor;
|
|
3459
3604
|
const vis=flatVisible(), out=[];
|
|
3460
3605
|
for(let r=rg.r0;r<=rg.r1;r++){
|
|
3461
|
-
|
|
3606
|
+
/* 行種別はアンカーに合わせる (同一シートに複数 kind の行が並ぶビューがあるため) */
|
|
3607
|
+
const it=vis[r]; if(!it||it.kind!==anchorItem.kind) continue;
|
|
3462
3608
|
for(let c=rg.c0;c<=rg.c1;c++){
|
|
3463
3609
|
if(compat.indexOf(c)<0) continue;
|
|
3464
3610
|
if(rowIsExisting(it, cols[c])) continue; /* F-2: 既存呼び出しAGは編集不可 */
|
|
3611
|
+
if(cols[c].disabled && cols[c].disabled(it, cellCtx(it, tab))) continue; /* その行に存在しない列 (バナー行のタイトル等) */
|
|
3465
3612
|
out.push({item:it, ci:c});
|
|
3466
3613
|
}
|
|
3467
3614
|
}
|
|
3468
3615
|
return out.length?out:anchor;
|
|
3469
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
|
+
}
|
|
3470
3630
|
function tgListOf(t){ return String(getVar(t.item, t.ci)||'').split(',').map(s=>s.trim()).filter(Boolean); }
|
|
3471
3631
|
function drawerTgToggle(val){
|
|
3472
3632
|
if(!drawerTarget||drawerTarget.type!=='tg') return;
|
|
@@ -3482,6 +3642,16 @@ function drawerTgToggle(val){
|
|
|
3482
3642
|
if(targets.length>1) toast((allOn?'解除':'追加')+':'+targets.length+'セルに一括適用');
|
|
3483
3643
|
renderDrawer();
|
|
3484
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
|
+
}
|
|
3485
3655
|
function closeDrawer(){ document.getElementById('refDrawer').classList.remove('open'); drawerTarget=null; }
|
|
3486
3656
|
function renderDrawer(){
|
|
3487
3657
|
if(!drawerTarget) return;
|
|
@@ -3554,6 +3724,25 @@ function renderDrawer(){
|
|
|
3554
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>'; });
|
|
3555
3725
|
listEl.innerHTML=h||'<div class="empty" style="padding:24px">該当なし</div>'; return;
|
|
3556
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
|
+
}
|
|
3557
3746
|
/* S7: 入力先の見出しは列記述子のラベル (媒体ごとの列名をここに書かない) */
|
|
3558
3747
|
const dcol=colDefs('ads').find(c=>c.field===drawerTarget.f); const fieldName=dcol?dcol.label:'';
|
|
3559
3748
|
let curNo=''; const fa=findAd(drawerTarget.u); if(fa) curNo=String(fa.a[drawerTarget.f]||'');
|
|
@@ -3749,12 +3938,32 @@ function dupGroup(u){
|
|
|
3749
3938
|
ng.uid=uid('g'); ng.adGroupId=''; ng.isExisting=false;
|
|
3750
3939
|
ng.name=dupGroupName(r.g.name);
|
|
3751
3940
|
ng.paramCode=nextParamCode(r.g.paramCode);
|
|
3941
|
+
/* I-150: 広告行に明示指定された paramCode をそのままコピーすると、同じ最終URLの広告が
|
|
3942
|
+
量産される。広告セットと同じ規則で振り直す (空欄=継承 の行は空欄のまま)。 */
|
|
3752
3943
|
ng.ads=r.g.ads.map(a=>cloneAd(a, null));
|
|
3753
3944
|
r.c.adGroups.splice(r.i+1,0,ng);
|
|
3945
|
+
/* 採番はプランへ挿入した後に行う。maxParamNum はプラン全体を走査するので、
|
|
3946
|
+
先に振ると同じ広告セット内の広告が全部同じ番号になる。 */
|
|
3947
|
+
ng.ads.forEach(renumberAdParamCode);
|
|
3754
3948
|
autoResolveGroupIds(ng, r.c);
|
|
3755
3949
|
});
|
|
3756
3950
|
toast('複製しました');
|
|
3757
3951
|
}
|
|
3952
|
+
/* I-150: prefix ごとの連番プール (nextByPrefix) から次の paramCode を採る。
|
|
3953
|
+
末尾数字が無い / 空欄 (=継承) のときは null を返し、呼び出し側で「触らない」を選ばせる。 */
|
|
3954
|
+
function seqParamCode(code, nextByPrefix){
|
|
3955
|
+
const m=String(code||'').match(/^(.*?)(\d+)\s*$/); if(!m) return null;
|
|
3956
|
+
const prefix=m[1], width=m[2].length;
|
|
3957
|
+
if(nextByPrefix[prefix]==null) nextByPrefix[prefix]=maxParamNum(prefix)+1;
|
|
3958
|
+
return prefix+String(nextByPrefix[prefix]++).padStart(width,'0');
|
|
3959
|
+
}
|
|
3960
|
+
/* I-150: 広告行が個別に持つ paramCode を複製時に振り直す。空欄 (=広告セット継承) は触らない。 */
|
|
3961
|
+
function renumberAdParamCode(ad){
|
|
3962
|
+
if(!ad) return;
|
|
3963
|
+
const own=String(ad.paramCode||'').trim();
|
|
3964
|
+
if(own==='') return;
|
|
3965
|
+
ad.paramCode=nextParamCode(own);
|
|
3966
|
+
}
|
|
3758
3967
|
/* paramCode の末尾数字を「プラン内 + 参照表の最大 +1」に振り直す。数字が無い / 空なら元の値のまま。 */
|
|
3759
3968
|
function nextParamCode(code){
|
|
3760
3969
|
const m=String(code||'').match(/^(.*?)(\d+)\s*$/); if(!m) return code||'';
|
|
@@ -3797,13 +4006,16 @@ function editCampaign(u){
|
|
|
3797
4006
|
function cloneCampaign(c, clearCreative, clearParam){
|
|
3798
4007
|
const nc=JSON.parse(JSON.stringify(c)); nc.uid=uid('c');
|
|
3799
4008
|
nc.campaignId=''; nc.isExisting=false; // B7/F-2: 複製はIDを引き継がず必ず新規扱い(編集可)
|
|
3800
|
-
|
|
4009
|
+
/* I-150: 広告行も個別 paramCode を持つので clearParam の対象に含める */
|
|
4010
|
+
nc.adGroups.forEach(g=>{ g.uid=uid('g'); g.adGroupId=''; g.isExisting=false; if(clearParam) g.paramCode=''; g.ads.forEach(a=>{ a.uid=uid('a'); if(clearParam) a.paramCode=''; if(clearCreative) pf().creativeFields.forEach(f=>{ a[f]=''; }); }); });
|
|
3801
4011
|
return nc;
|
|
3802
4012
|
}
|
|
3803
4013
|
/* 指定prefixを持つ paramCode の現在の最大末尾番号 (プラン内 + アカウントJSON refs.groups を走査) */
|
|
3804
4014
|
function maxParamNum(prefix){
|
|
3805
4015
|
let mx=0; const scan=code=>{ const m=String(code||'').match(/^(.*?)(\d+)\s*$/); if(m && m[1]===prefix){ const n=parseInt(m[2],10); if(n>mx) mx=n; } };
|
|
3806
|
-
|
|
4016
|
+
/* I-150: Meta は広告行が個別の paramCode を持てるので、広告行も走査対象に含める
|
|
4017
|
+
(含めないと複製時の採番が広告行の番号とぶつかる) */
|
|
4018
|
+
st.campaigns.forEach(c=>c.adGroups.forEach(g=>{ scan(g.paramCode); (g.ads||[]).forEach(a=>scan(a.paramCode)); }));
|
|
3807
4019
|
Object.keys(st.refs.groups||{}).forEach(scan);
|
|
3808
4020
|
return mx;
|
|
3809
4021
|
}
|
|
@@ -3827,7 +4039,9 @@ function dupCampaign(u){
|
|
|
3827
4039
|
for(let i=0;i<n;i++){
|
|
3828
4040
|
const nc=cloneCampaign(c, false, false); // 中身そのまま (creative/paramCode保持) + campaignId/adGroupIdクリア
|
|
3829
4041
|
// paramCode 連番自動採番 (prefixごとに現状の最大+1から連番)
|
|
3830
|
-
|
|
4042
|
+
/* I-150: 広告行が個別に持つ paramCode も同じ連番プールで採番する (空欄=継承 は触らない) */
|
|
4043
|
+
nc.adGroups.forEach(g=>{ const ng=seqParamCode(g.paramCode, nextByPrefix); if(ng!==null){ g.paramCode=ng; autoResolveGroupIds(g,nc); }
|
|
4044
|
+
g.ads.forEach(a=>{ const na=seqParamCode(a.paramCode, nextByPrefix); if(na!==null) a.paramCode=na; }); });
|
|
3831
4045
|
// キャンペーン名は複製元を引き継ぐ (先頭の番号のみ新しい urlNumber に差し替え / 空なら自動生成)
|
|
3832
4046
|
const num=String(startNum+i).padStart(2,'0'); nc.urlNumber=num; const f=nc.adGroups[0]&&nc.adGroups[0].ads[0]; nc.name=dupCampaignName(c.name, num, f);
|
|
3833
4047
|
st.campaigns.splice(base+1+i,0,nc);
|
|
@@ -4564,7 +4778,9 @@ async function openPlanFile(){
|
|
|
4564
4778
|
WIRING
|
|
4565
4779
|
============================================================ */
|
|
4566
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(); });
|
|
4567
|
-
|
|
4781
|
+
/* 選択を解除したらドロワーも閉じる。閉じないと「N セル一括」の表示が残ったまま
|
|
4782
|
+
次の操作はアンカー1セルだけに当たる (viewSeg / modeSeg と同じ扱いに揃える)。 */
|
|
4783
|
+
document.getElementById('sheetFilter').addEventListener('change',()=>{ clearSel(); closeDrawer(); renderSheet(); });
|
|
4568
4784
|
/* プランビュー: シート / ツリー モード切替 (1階層下) */
|
|
4569
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(); });
|
|
4570
4786
|
/* S11: 媒体セレクタ (#pfSeg) / 媒体要件タブ (#mediaTabs) は廃止。媒体切替はヘッダーの統合プルダウンに一本化 */
|
|
@@ -4670,7 +4886,7 @@ sheetHostEl.addEventListener('mousedown',e=>{
|
|
|
4670
4886
|
pendingDrawer=null; dragMoved=false;
|
|
4671
4887
|
// S3: 旧 [1,2,3] リテラルを撤去。列記述子の kind で「▼参照ドロワー列」「ターゲティング列」を判定する
|
|
4672
4888
|
const col=colDefs()[c];
|
|
4673
|
-
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}; }
|
|
4674
4890
|
applySelection();
|
|
4675
4891
|
});
|
|
4676
4892
|
sheetHostEl.addEventListener('mouseover',e=>{
|
|
@@ -4694,6 +4910,7 @@ document.getElementById('drawerBackdrop').addEventListener('click',closeDrawer);
|
|
|
4694
4910
|
document.getElementById('drawerSearch').addEventListener('input',renderDrawer);
|
|
4695
4911
|
document.getElementById('drawerList').addEventListener('click',e=>{ const row=e.target.closest('.ddrow'); if(!row) return; if(!drawerTarget) return;
|
|
4696
4912
|
if(drawerTarget.type==='tg'){ drawerTgToggle(row.dataset.tgval); }
|
|
4913
|
+
else if(drawerTarget.type==='opt'){ if('optv' in row.dataset) drawerOptPick(row.dataset.optv); }
|
|
4697
4914
|
else if(drawerTarget.type==='cpref'){ if(row.dataset.cpid) addExistingCampaign(row.dataset.cpid); }
|
|
4698
4915
|
else if(drawerTarget.type==='agref'){ if(row.dataset.agid) addExistingAdGroup(row.dataset.agid); }
|
|
4699
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); } }
|
|
@@ -4701,7 +4918,7 @@ document.getElementById('drawerList').addEventListener('click',e=>{ const row=e.
|
|
|
4701
4918
|
else { drawerPick(row.dataset.no); }
|
|
4702
4919
|
});
|
|
4703
4920
|
/* 単体クリック/複数ドラッグ選択どちらでも開く(選択範囲全セルに一括適用) */
|
|
4704
|
-
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'); } });
|
|
4705
4922
|
|
|
4706
4923
|
/* ---- コピー / 貼付 ---- */
|
|
4707
4924
|
document.addEventListener('copy',e=>{
|