addashboard-cli 0.3.0 → 0.4.0

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.
@@ -35,6 +35,8 @@
35
35
  header.bar .brand{display:flex; align-items:baseline; gap:10px; margin-right:6px;}
36
36
  header.bar .brand .logo{font-size:15px; font-weight:800; letter-spacing:.02em;}
37
37
  header.bar .brand .pjname{font-size:12.5px; color:var(--muted);}
38
+ header.bar .acchdr{display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--muted); margin-right:6px;} /* F-3: ヘッダーのアカウント切替 */
39
+ header.bar .acchdr select{font-family:inherit; font-size:12.5px; border:1px solid var(--line); border-radius:7px; padding:4px 6px; background:#fff;}
38
40
  .seg{display:inline-flex; background:var(--soft); border:1px solid var(--line); border-radius:8px; padding:2px;}
39
41
  .seg button{border:0; background:transparent; padding:6px 14px; font-size:12.5px; font-weight:600; color:var(--muted); border-radius:6px; cursor:pointer;}
40
42
  .seg button.active{background:#fff; color:var(--fg); box-shadow:0 1px 2px rgba(16,24,40,.08);}
@@ -170,11 +172,14 @@
170
172
  .cell-in.err{border-color:var(--danger); background:var(--danger-soft);}
171
173
  .cell-in.warnempty{background:var(--amber-soft); box-shadow:inset 2px 0 0 var(--amber);}
172
174
  select.cell-in{-webkit-appearance:none; appearance:none; cursor:pointer;}
175
+ select.cell-in.fmt{min-width:56px; width:auto; padding-right:14px; text-overflow:clip;}
173
176
  .ghost{display:inline-block; font-size:11px; color:var(--faint); line-height:1.35; max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:middle;}
174
177
  .ghost.err{color:var(--danger);} .ghost b.cnt{font-variant-numeric:tabular-nums;}
175
178
  .medibadge{display:inline-flex; align-items:center; gap:6px; font-size:11px; flex-shrink:0; white-space:nowrap;}
176
179
  .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;}
177
180
  .idpill{font-family:var(--mono); font-size:10.5px; color:var(--muted);}
181
+ .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;} /* 既存行の編集不可セル表示 */
182
+ .cell-in.roinput{background:var(--soft); color:var(--muted); cursor:not-allowed;} /* 既存行の read-only 入力欄 */
178
183
  .idpill.warn{color:var(--amber);} .idpill.err{color:var(--danger);}
179
184
  .breadc{font-size:10.5px; color:var(--faint); white-space:nowrap; max-width:170px; overflow:hidden; text-overflow:ellipsis;}
180
185
  .rowact{display:inline-flex; gap:2px;}
@@ -211,6 +216,8 @@
211
216
  .cbadge{display:inline-flex; align-items:center; justify-content:center; min-width:24px; height:20px; padding:0 5px; border-radius:5px; background:var(--line2); font-size:10px; font-weight:700; color:#5b6470; font-family:var(--mono);}
212
217
  .ratio{font-family:var(--mono); font-size:11px; font-weight:700; color:var(--accent); background:var(--accent-soft); padding:2px 7px; border-radius:5px;}
213
218
  .addbtn{align-self:flex-start; font-size:11.5px; color:var(--accent); background:var(--accent-soft); border:1px dashed var(--accent-line); border-radius:7px; padding:5px 10px; cursor:pointer; font-weight:600;}
219
+ .addrow{display:flex; gap:6px; flex-wrap:wrap; align-items:center; margin-top:4px;} /* ツリー: 追加ボタンを横1行に */
220
+ .addrow .addbtn{align-self:auto; margin:0;}
214
221
  .addbtn:hover{background:#e3e7fd;}
215
222
 
216
223
  /* ── selection (spreadsheet) ── */
@@ -224,6 +231,9 @@
224
231
  td.tgtd{ padding:3px 6px;}
225
232
  .tgpick{ display:flex; align-items:center; gap:6px; min-width:130px; max-width:230px; min-height:26px; border:1px solid var(--line); border-radius:6px; padding:3px 8px; background:#fff; cursor:pointer; line-height:1.3;}
226
233
  .tgpick:hover{ border-color:var(--accent); background:var(--accent-soft);}
234
+ .cppick{ display:inline-flex; align-items:center; gap:5px; min-height:26px; border:1px solid var(--line); border-radius:6px; padding:2px 8px; background:#fff; cursor:pointer;} /* F-6: キャンペーン選択セル */
235
+ .cppick:hover{ border-color:var(--accent); background:var(--accent-soft);}
236
+ .cppick .breadc{color:var(--ink);}
227
237
  .tgpick.tgempty{ background:var(--soft);}
228
238
  .tgpick .tgval{ flex:1 1 auto; min-width:0; font-size:12px; color:var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
229
239
  .tgpick .tghint{ color:var(--faint);}
@@ -258,6 +268,26 @@
258
268
  .toast{position:fixed; bottom:22px; left:50%; transform:translateX(-50%); background:#111418; color:#fff; font-size:12.5px; padding:9px 16px; border-radius:9px; z-index:100; box-shadow:0 8px 24px rgba(0,0,0,.25); opacity:0; transition:opacity .2s, transform .2s;}
259
269
  .toast.show{opacity:1; transform:translateX(-50%) translateY(-2px);}
260
270
  .empty{padding:40px; text-align:center; color:var(--faint); font-size:13px;}
271
+ /* ── plan warning banner (accountId 不一致など) ── */
272
+ .planwarn{display:flex; align-items:center; gap:10px; background:var(--amber-soft); color:#7a4d00; border-bottom:1px solid var(--amber); font-size:12.5px; padding:9px 16px;}
273
+ .planwarn[hidden]{display:none;}
274
+ .planwarn .planwarnx{margin-left:auto; border:none; background:transparent; color:inherit; cursor:pointer; font-size:13px; line-height:1; padding:2px 6px; border-radius:6px;}
275
+ .planwarn .planwarnx:hover{background:rgba(0,0,0,.06);}
276
+ /* ── help modal ── */
277
+ .modal.help{width:min(760px,100%);}
278
+ .help .hlead{font-size:13px; color:var(--muted); line-height:1.7; margin:2px 0 14px;}
279
+ .help .hsteps{list-style:none; margin:0; padding:0; counter-reset:hstep;}
280
+ .help .hsteps>li{position:relative; padding:0 0 16px 42px; margin:0;}
281
+ .help .hsteps>li:before{counter-increment:hstep; content:counter(hstep); position:absolute; left:0; top:0; width:26px; height:26px; border-radius:50%; background:var(--accent); color:#fff; font-size:13px; font-weight:800; display:flex; align-items:center; justify-content:center;}
282
+ .help .hsteps>li:after{content:""; position:absolute; left:12.5px; top:28px; bottom:2px; width:1px; background:var(--line2);}
283
+ .help .hsteps>li:last-child:after{display:none;}
284
+ .help .hsteps .hst{font-size:13.5px; font-weight:700; margin-bottom:3px;}
285
+ .help .hsteps .hsd{font-size:12.5px; color:var(--muted); line-height:1.65;}
286
+ .help .promptcard{border:1px solid var(--line); border-radius:10px; overflow:hidden; margin:8px 0 4px;}
287
+ .help .promptcard .pchdr{display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 12px; background:var(--soft); border-bottom:1px solid var(--line2);}
288
+ .help .promptcard .pctitle{font-size:12px; font-weight:700; color:var(--fg);}
289
+ .help .promptcard pre{margin:0; padding:12px 14px; background:#0f1420; color:#d6e0ff; font-family:var(--mono); font-size:11.5px; line-height:1.6; white-space:pre-wrap; word-break:break-word; max-height:260px; overflow:auto;}
290
+ .help .promptcard .pccopy{font-size:11.5px; padding:4px 10px; white-space:nowrap;}
261
291
  @media (max-width:820px){ .grid2,.mrow{grid-template-columns:1fr;} }
262
292
  @media print{ header.bar{position:static;} .view{display:block!important;} .toolbar2,.rowact,.nodeact,.act,.seg{display:none;} details.sec[open]>.body,details.sec>.body{display:block;} }
263
293
  </style>
@@ -265,20 +295,23 @@
265
295
  <body>
266
296
  <header class="bar">
267
297
  <div class="brand"><span class="logo">YDA入稿プランナー</span><span class="pjname" id="pjLabel">—</span></div>
298
+ <!-- F-3: アカウント切替はヘッダーのプルダウンに一本化 -->
299
+ <span class="acchdr" title="アカウント切替(+新規追加/フォルダ接続/編集もここから)"><select class="chipin" id="accSelect" style="min-width:12ch"></select></span>
268
300
  <div class="seg" id="viewSeg">
269
301
  <button data-v="req">媒体要件</button>
270
302
  <button data-v="sheet" class="active">プラン</button>
271
- <button data-v="ref">参照データ</button>
303
+ <button data-v="ref">アカウントアセット</button>
272
304
  <button data-v="hist">履歴</button>
273
305
  </div>
274
306
  <span class="vbadge ok" id="vbadge" title="バリデーション状況">✓ 0件</span>
275
307
  <div class="spacer"></div>
308
+ <button class="act ghost" id="btnHelp" title="このプランナーの使い方">❓ 使い方</button>
276
309
  <button class="act ghost" id="btnUndo" title="元に戻す (Ctrl/Cmd+Z)">↶ 戻す</button>
277
- <button class="act" id="btnLoad">JSON読込</button>
278
- <button class="act" id="btnSave">バージョン保存</button>
279
- <button class="act primary" id="btnSubmitJson">入稿JSON書き出し</button>
310
+ <button class="act primary" id="btnSubmitJson">✅ 完成 → 保存して入稿</button>
280
311
  <input type="file" id="fileInput" accept=".json,application/json" style="display:none">
312
+ <input type="file" id="accFileInput" accept=".json,application/json" style="display:none">
281
313
  </header>
314
+ <div id="planWarn" class="planwarn" hidden><span id="planWarnMsg"></span><button id="planWarnClose" class="planwarnx" title="閉じる">✕</button></div>
282
315
 
283
316
  <main>
284
317
  <!-- ============ 媒体要件ビュー ============ -->
@@ -293,9 +326,19 @@
293
326
  <div id="mt-yda">
294
327
  <div class="eyebrow">YDA / Yahoo Display Ads — 媒体要件・仕様・使い方</div>
295
328
  <h1 class="title">YDA 広告入稿 媒体要件</h1>
296
- <p class="sub">2026-07-16 定例ヒアリング・2026-07-21 定例(北さん運用フィードバック)+ 現行一括入稿シート(リョウシンJVコンドロイ錠)の実データ解析にもとづく。2026-07-23 よりCSV書き出しを廃止し、保存・入稿受け渡しはJSONに統一。</p>
329
+ <p class="sub">定例ヒアリング・運用フィードバック + 実案件の一括入稿シートの実データ解析にもとづく。2026-07-23 よりCSV書き出しを廃止し、保存・入稿受け渡しはJSONに統一。</p>
330
+
331
+
332
+ <div class="seg" id="ydaTabs" style="display:inline-flex; margin-bottom:14px">
333
+ <button data-yt="overview" class="active">概要</button>
334
+ <button data-yt="resp">レスポンシブ</button>
335
+ <button data-yt="banner">バナー(手入稿)</button>
336
+ <button data-yt="param">パラメータ</button>
337
+ <button data-yt="guide">使い方</button>
338
+ </div>
297
339
 
298
- <details class="sec" open><summary><span class="num">1</span><span class="stitle">全体像 — 3層構造と量産の考え方</span><span class="chev"></span></summary>
340
+ <div id="yt-overview">
341
+ <details class="sec" open><summary><span class="num">1</span><span class="stitle">全体像 — 3層構造と量産の考え方</span><span class="chev"></span></summary>
299
342
  <div class="body">
300
343
  <p class="lead">YDA運用の核は「<b>変数=キャンペーン</b>」「<b>クリエイティブ束=広告グループ内の広告</b>」という2軸の量産構造。キャンペーンを横に増やし、広告を縦に増やす。</p>
301
344
  <table class="g"><thead><tr><th>階層</th><th>意味</th><th>このツールでの単位</th></tr></thead><tbody>
@@ -309,20 +352,7 @@
309
352
  </div>
310
353
  <div class="callout">ツリーの各ノードの複製ボタンで、この <code>1:1:N</code> を1クリックで生成できる(テンプレ複製→中身クリア→変数差し替え)。</div>
311
354
  </div></details>
312
-
313
- <details class="sec"><summary><span class="num">2</span><span class="stitle">広告形式 — 対応/非対応(入稿経路)</span><span class="chev"></span></summary>
314
- <div class="body">
315
- <p class="lead">本ツールが扱うのは <b>レスポンシブ(画像)・固定型</b>。入稿は「入稿JSON書き出し → <code>webgram submit --platform yda</code>」のAPI入稿に統一(CSVは2026-07-23廃止)。</p>
316
- <table class="g"><thead><tr><th>形式</th><th>入稿経路</th><th>利用</th><th>備考</th></tr></thead><tbody>
317
- <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>
318
- <tr><td>レスポンシブ(画像)・アセット型</td><td><span class="pill amber">非対象</span></td><td>—</td><td>複数タイトル/画像を登録しYahoo側が自動最適化。<b>1広告=単一組合せの本ツールでは非対象</b></td></tr>
319
- <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>
320
- <tr><td>バナー(静止画・固定サイズ)</td><td><span class="pill danger">手入稿</span></td><td>★★★</td><td>API入稿は未対応。Yahoo管理画面で個別入稿。本ツールでは <span class="pill">手入稿</span> フラグで区別し、入稿JSONから除外される</td></tr>
321
- <tr><td>カルーセル / テキスト</td><td><span class="pill gray">未使用</span></td><td>—</td><td>運用では使わない</td></tr>
322
- </tbody></table>
323
- </div></details>
324
-
325
- <details class="sec"><summary><span class="num">3</span><span class="stitle">入稿モード — 新規 / 既存(ASC)</span><span class="chev"></span></summary>
355
+ <details class="sec"><summary><span class="num">2</span><span class="stitle">入稿モード — 新規 / 既存(ASC)</span><span class="chev"></span></summary>
326
356
  <div class="body">
327
357
  <table class="g"><thead><tr><th>モード</th><th>構造</th><th>予算/日</th><th>頻度・工数</th></tr></thead><tbody>
328
358
  <tr><td><b>新規</b></td><td>1キャンペーン=1広告グループ=1広告を基本に、テンプレ複製で量産。変数ごとにキャンペーンを分ける</td><td><b>12,000円</b></td><td>頻度高・主戦場。既存より圧倒的に多い</td></tr>
@@ -332,8 +362,46 @@
332
362
  </tbody></table>
333
363
  <div class="callout amber">既存モードは Yahoo払出し済みの campaignId / adGroupId の取り込みが前提。paramCode がグループ参照表に無い=<b>新規/未払出</b>として黄色表示になる(実シートの <code>#N/A</code> に相当)。</div>
334
364
  </div></details>
335
-
336
- <details class="sec"><summary><span class="num">4</span><span class="stitle">固定デフォルト値 — 案件テンプレとして一括適用</span><span class="chev"></span></summary>
365
+ <details class="sec"><summary><span class="num">3</span><span class="stitle">運用フロー — 北さんの複製量産 / 戦略の後乗せ</span><span class="chev"></span></summary>
366
+ <div class="body">
367
+ <ol>
368
+ <li>完成キャンペーンを<b>テンプレとして丸ごと複製</b>する</li>
369
+ <li>複製の中身(クリエイティブ/リンク/固有パラメータ)を<b>一旦消す</b></li>
370
+ <li>変えたい<b>変数だけ差し替える</b></li>
371
+ <li>これを検証したい変数の数だけ繰り返し量産する</li>
372
+ <li>初期は 1:1:10 で撒き、数日データを見る</li>
373
+ <li>伸びた型で追加量産し、<b>戦略(配信戦略・訴求方針)は後から埋める</b></li>
374
+ </ol>
375
+ <div class="callout green">このツールはツリーの複製ダイアログで「何を残し・何をクリアするか」を選べる。キャンペーンの「量産」で M本を一括生成。<b>戦略メモ欄は空のまま量産→後乗せ</b>で埋められる。</div>
376
+ <div class="callout"><b>本ツールの立ち位置(2026-07-16 定例、2026-07-23 更新)</b>:AIで生成できる部分は<b>AIに生成させ(JSON読込)</b>、絶対に人が確定する部分(クリエイティブ選定・確認)を<b>スプシ的コピペ・一括入力・複製</b>で楽にする。ここで確認・修正して <b>OK なら 入稿JSON書き出し(=GO)</b>→ <code>webgram submit-plan --plan-file</code>(dry-run一括検証 → 本実行は1キャンペーンずつ・進捗書き戻しで再開可能)。CSVを介さない直接入稿へ移行済み。今後はキャンペーン/広告グループIDのAPI/MCP自動取得、新規画像のメディア登録の自動化へ。</div>
377
+ </div></details>
378
+ </div>
379
+ <div id="yt-resp" style="display:none">
380
+ <details class="sec" open><summary><span class="num">1</span><span class="stitle">レスポンシブ広告 — アセット型 / 固定型(本ツールの主対象)</span><span class="chev"></span></summary>
381
+ <div class="body">
382
+ <p class="lead">レスポンシブ = <b>動画 or 静止画 + テキスト</b> を組み合わせる形式。本ツールが扱うのは<b>画像・固定型</b>(⭐️⭐️ ちゃんと検証する用途)。</p>
383
+ <table class="g"><thead><tr><th>型</th><th>仕組み</th><th>利用</th><th>本ツール</th></tr></thead><tbody>
384
+ <tr><td><b>固定型</b></td><td>タイトル×説明文×画像を<b>人が1通りに固定</b>して入稿し、成果を自分で検証する。1広告=1組み合わせ</td><td>⭐️⭐️ 検証の主戦場</td><td><span class="pill green">主対象</span> シート/ツリーで量産し入稿JSONへ</td></tr>
385
+ <tr><td>アセット型</td><td>複数のタイトル/説明文/画像を登録すると<b>Yahoo側が自動で組み合わせ最適化</b>(「適当に入れたらいい感じにやってくれる」)</td><td>検証には不向き(どの組合せが効いたか分離できない)</td><td><span class="pill amber">非対象</span></td></tr>
386
+ <tr><td>動画</td><td>動画 + テキスト。サムネイル画像は <code>thumbnailFlg:'TRUE'</code> の専用登録</td><td>—</td><td><span class="pill gray">別フロー</span> <code>webgram upload-video → submit</code>(本番実績あり)</td></tr>
387
+ </tbody></table>
388
+ <div class="grid2" style="margin-top:10px">
389
+ <div class="kv"><h4>画像の2形式</h4><p><code>Card</code> = 1200×628 / <code>SQ</code> = 1080×1080。1つの画像番号に両形式の mediaId を持ち、広告行の「フォーマット」で選ぶ。</p></div>
390
+ <div class="kv"><h4>検証の型</h4><p>1キャンペーン=1変数で分離し、初期は <span class="ratio">1:1:10</span> で撒いて選ぶ → 伸びた型で追加量産(詳細は「概要」タブ)。</p></div>
391
+ </div>
392
+ </div></details>
393
+ <details class="sec" open><summary><span class="num">2</span><span class="stitle">広告形式 — 対応/非対応(入稿経路)</span><span class="chev"></span></summary>
394
+ <div class="body">
395
+ <p class="lead">本ツールが扱うのは <b>レスポンシブ(画像)・固定型</b>。入稿は「入稿JSON書き出し(入稿計画JSON)→ <code>webgram submit-plan</code>」のAPI入稿に統一(CSVは2026-07-23廃止)。</p>
396
+ <table class="g"><thead><tr><th>形式</th><th>入稿経路</th><th>利用</th><th>備考</th></tr></thead><tbody>
397
+ <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>
398
+ <tr><td>レスポンシブ(画像)・アセット型</td><td><span class="pill amber">非対象</span></td><td>—</td><td>複数タイトル/画像を登録しYahoo側が自動最適化。<b>1広告=単一組合せの本ツールでは非対象</b></td></tr>
399
+ <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>
400
+ <tr><td>バナー(静止画・固定サイズ)</td><td><span class="pill danger">手入稿</span></td><td>★★★</td><td>API入稿は未対応。Yahoo管理画面で個別入稿。本ツールでは <span class="pill">手入稿</span> フラグで区別し、入稿JSONから除外される</td></tr>
401
+ <tr><td>カルーセル / テキスト</td><td><span class="pill gray">未使用</span></td><td>—</td><td>運用では使わない</td></tr>
402
+ </tbody></table>
403
+ </div></details>
404
+ <details class="sec"><summary><span class="num">3</span><span class="stitle">固定デフォルト値 — 案件テンプレとして一括適用</span><span class="chev"></span></summary>
337
405
  <div class="body">
338
406
  <p class="lead">全広告に共通する固定値は「案件テンプレ」で1箇所定義し全広告へ一括適用する(現行の全行ハードコードを廃止)。</p>
339
407
  <table class="g"><thead><tr><th>項目</th><th>既定値</th><th>備考</th></tr></thead><tbody>
@@ -343,12 +411,11 @@
343
411
  <tr><td>予算</td><td>新規 12,000 / ASC 50,000〜</td><td>モードで切替</td></tr>
344
412
  <tr><td>配信設定</td><td><b>オフ (PAUSED)</b></td><td>誤配信防止。入稿後に手動オン</td></tr>
345
413
  <tr><td>広告タイプ / 形式</td><td>レスポンシブ / 画像</td><td>固定(API: <code>RESPONSIVE_AD + IMAGE</code>)</td></tr>
346
- <tr><td>主体者表記</td><td><b>富山常備薬</b></td><td>案件固定(薬機/景表対応)。API: <code>responsiveAd.principal</code></td></tr>
414
+ <tr><td>主体者表記</td><td><b>アカウントJSON由来</b></td><td>案件固定(薬機/景表対応)。アカウント/&lt;名前&gt;.json の <code>principal</code>。API: <code>responsiveAd.principal</code></td></tr>
347
415
  <tr><td>表示URLの形式</td><td>ドメインのみ表示する</td><td>固定(API: <code>displayUrlLevel:'DOMAIN'</code>)</td></tr>
348
416
  </tbody></table>
349
417
  </div></details>
350
-
351
- <details class="sec"><summary><span class="num">5</span><span class="stitle">命名規則・生成式(実データ突合済み)</span><span class="chev"></span></summary>
418
+ <details class="sec"><summary><span class="num">4</span><span class="stitle">命名規則・生成式(実データ突合済み)</span><span class="chev"></span></summary>
352
419
  <div class="body">
353
420
  <p class="lead">派生値はデータに保存せず、都度この式で算出する(行オフセット依存の数式を排除)。例は実データ <code>957c-45L60-jv_Cac_148</code> で検証。</p>
354
421
  <div class="fx"><span class="c">// CR名</span>
@@ -369,14 +436,40 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
369
436
  <div class="kv"><h4>入稿リンク末尾番号</h4><p>ベースURLは案件固定、末尾パラメータ番号のみ可変。既存の使用済み番号から空きを自動割当。</p></div>
370
437
  </div>
371
438
  </div></details>
372
-
373
- <details class="sec"><summary><span class="num">6</span><span class="stitle">入稿JSON — 構造と入力項目・制約(パラメータ)</span><span class="chev"></span></summary>
439
+ </div>
440
+ <div id="yt-banner" style="display:none">
441
+ <details class="sec" open><summary><span class="num">1</span><span class="stitle">バナー(静止画・固定サイズ)— 手入稿運用 ⭐️⭐️⭐️</span><span class="chev"></span></summary>
442
+ <div class="body">
443
+ <p class="lead">利用頻度は高い(⭐️⭐️⭐️)が、<b>媒体としては対応しているのに自社ツール側が未対応</b>のため、Yahoo広告管理画面での<b>手入稿</b>で運用する形式。</p>
444
+ <table class="g"><thead><tr><th>項目</th><th>内容</th></tr></thead><tbody>
445
+ <tr><td>なぜ手入稿か</td><td>旧: 自社CSVがバナー非対応(1年前に挫折) → 現: API入稿も画像レスポンシブ優先で<b>バナー(BANNER_AD)は未実装</b>。媒体側の機能としては存在する</td></tr>
446
+ <tr><td>プランナーでの扱い</td><td>広告行の <span class="pill">手入稿</span> フラグでバナーを区別。<b>入稿JSONからは自動除外</b>され、バリデーション件数にも「手入稿」として別カウントされる</td></tr>
447
+ <tr><td>手入稿時に使う情報</td><td>プラン上の キャンペーン名 / 広告グループ名(paramCode) / 画像 / 最終リンク先URL(<code>?ad=paramCode</code>)/ 広告名(命名規則はレスポンシブと共通)をそのまま管理画面へ転記</td></tr>
448
+ <tr><td>手順</td><td>① プランでバナー行に「手入稿」フラグを立てる ② レスポンシブ分は通常どおり入稿JSONでAPI入稿 ③ バナー分のみ管理画面から個別入稿 ④ 入稿後に一覧でOK確認</td></tr>
449
+ <tr><td>将来対応</td><td>API での <code>BANNER_AD</code> 作成対応を検討中(未着手)。対応後は手入稿フラグを外すだけで同じフローに乗る想定</td></tr>
450
+ </tbody></table>
451
+ </div></details>
452
+ </div>
453
+ <div id="yt-param" style="display:none">
454
+ <details class="sec" open><summary><span class="num">1</span><span class="stitle">画面の入力項目(どこで何を設定するか)</span><span class="chev"></span></summary>
455
+ <div class="body">
456
+ <table class="g"><thead><tr><th>場所</th><th>入力項目</th><th>入稿JSONへの反映</th></tr></thead><tbody>
457
+ <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>
458
+ <tr><td><b>キャンペーン行</b></td><td>キャンペーン名 / 入稿モード / 1日の予算 / 入札戦略 / 目標CV単価(tCPA) / 配信設定 / 検証変数 / 入稿リンク番号 / 戦略メモ</td><td>name・budget・biddingStrategy+targetCpa・status</td></tr>
459
+ <tr><td><b>広告グループ行</b></td><td>広告グループ名 / paramCode / 入札価格 / 配信設定 / <b>ターゲティング列</b>(デバイス・性別・年齢・地域・曜日時間帯・オーディエンス・サーチKW)</td><td>name・status・devices/ages/genders/days(+startHour/endHour)。地域・オーディエンス・サーチKWは未反映</td></tr>
460
+ <tr><td><b>広告行</b></td><td>フォーマット(Card/SQ) / 画像番号 / タイトル番号 / 説明文番号 / 記事slug / 手入稿フラグ</td><td>ads[].adName・mediaIds・headlines・descriptions・finalUrl(手入稿・未解決は除外)</td></tr>
461
+ </tbody></table>
462
+ </div></details>
463
+ <details class="sec" open><summary><span class="num">2</span><span class="stitle">入稿JSON — 構造と入力項目・制約(パラメータ)</span><span class="chev"></span></summary>
374
464
  <div class="body">
375
- <p class="lead">「入稿JSON書き出し」はワンクリックで、<b>画面のプランをそのまま</b> backend <code>ydaSubmitSchema</code> 準拠のボディに変換する(設定ダイアログなし)。<code>webgram submit --platform yda --json-file &lt;このファイル&gt;</code> がそのまま <code>/api/submission/yda</code> に渡す。手入稿(バナー)と未解決の広告は自動除外。</p>
376
- <div class="fx">{ accountId, campaigns: [ { name, goal, status, startDate, budget, biddingStrategy, targetCpa?, cpc?,
465
+ <p class="lead">「入稿JSON書き出し」はワンクリックで、<b>画面のプランをそのまま</b>入稿計画JSON(<code>planVersion:1</code>)に変換する(設定ダイアログなし)。<code>webgram submit-plan --plan-file &lt;このファイル&gt;</code> が dry-run で全キャンペーンを一括検証し、本実行では1キャンペーンずつ入稿・進捗をファイルに書き戻す(再開可能・二重入稿防止)。広告は backend <code>ydaAdSchema</code> の IMAGE 形式。手入稿(バナー)と未解決の広告は自動除外。</p>
466
+ <div class="fx">{ planVersion: 1, platform: "yda", accountId,
467
+ campaigns: [ { name, goal, status, startDate, budget, biddingStrategy, targetCpa?, cpc?,
377
468
  adGroups: [ { name, status, smartTargeting, devices, ages, genders, days, startHour?, endHour?,
378
469
  placementsInclude, placementsExclude, audiencesInclude, audiencesExclude,
379
- ads: [ { name, mediaId, title, description, finalUrl } ] } ] } ] }</div>
470
+ ads: [ { adName, status, mediaType:"IMAGE", mediaIds[], headlines[], descriptions[],
471
+ principal, finalUrl } ] } ] } ],
472
+ progress?: [...] // submit-plan が書き戻す進捗 (再開・二重入稿防止) }</div>
380
473
  <table class="g"><thead><tr><th>項目</th><th>画面上の出どころ</th><th>変換・制約</th></tr></thead><tbody>
381
474
  <tr><td class="mono">accountId</td><td><b>案件設定「アカウントID」</b></td><td>必須。未入力だと書き出し中止</td></tr>
382
475
  <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>
@@ -388,16 +481,15 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
388
481
  <tr><td class="mono">ages / genders</td><td>広告グループ行「年齢」「性別」</td><td><code>20-24歳→GT_RANGE20_24</code> 等 / <code>男性→ST_MALE</code>・<code>女性→ST_FEMALE</code>・<code>不明→ST_UNKNOWN</code>。空=全配信</td></tr>
389
482
  <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>
390
483
  <tr><td class="mono">audiences* / placements*</td><td>—</td><td>常に空配列で送信(backendスキーマ必須項目)。<b>「地域」「オーディエンスリスト」「サーチキーワード」列はJSONに反映されない</b>(APIのID解決が必要なため。入稿後に管理画面で設定 or 将来対応)</td></tr>
391
- <tr><td class="mono">ads[].name</td><td><span class="pill">自動</span></td><td>生成式 <code>{画像番号}{c|s}-{タイトル番号}L{説明文番号}-{paramCode}</code></td></tr>
392
- <tr><td class="mono">ads[].mediaId</td><td><span class="pill">自動</span></td><td>画像番号+形式 → 参照表。未解決は入稿対象外</td></tr>
393
- <tr><td class="mono">ads[].title</td><td><span class="pill purple">可変</span></td><td>タイトル番号 → 文言。<b>35字以内</b>(API上のアセット制約はタイトル≤20字・説明文≤90字)</td></tr>
394
- <tr><td class="mono">ads[].description</td><td><span class="pill purple">可変</span></td><td>説明文番号 → 文言</td></tr>
395
- <tr><td class="mono">ads[].finalUrl</td><td><span class="pill">自動</span></td><td>生成式 <code>baseURL + slug + "?ad=" + paramCode</code></td></tr>
484
+ <tr><td class="mono">ads[].adName</td><td><span class="pill">自動</span></td><td>生成式 <code>{画像番号}{c|s}-{タイトル番号}L{説明文番号}-{paramCode}</code></td></tr>
485
+ <tr><td class="mono">ads[].mediaIds</td><td><span class="pill">自動</span></td><td>画像番号+形式 → 参照表の mediaId。未解決は入稿対象外</td></tr>
486
+ <tr><td class="mono">ads[].headlines</td><td><span class="pill purple">可変</span></td><td>タイトル番号 → 文言。<b>35字以内</b>(API上のアセット制約はタイトル≤20字・説明文≤90字)</td></tr>
487
+ <tr><td class="mono">ads[].descriptions</td><td><span class="pill purple">可変</span></td><td>説明文番号 → 文言</td></tr>
488
+ <tr><td class="mono">ads[].finalUrl</td><td><span class="pill">自動</span></td><td>生成式 <code>baseURL + slug + "?ad=" + paramCode + "&creative=..."</code>(実データ準拠)</td></tr>
396
489
  </tbody></table>
397
490
  <div class="callout">プランナーの保存JSON(バージョン保存)はこれとは別物で、<code>schemaVersion:2</code> の全state(meta / project / campaigns / ui / refs / submit)を保持する。<b>meta</b> = バージョンID・作成/更新日時・作成者・対象媒体・名称・メモ。旧v1 JSONも読込時に自動マイグレーションされる。</div>
398
491
  </div></details>
399
-
400
- <details class="sec"><summary><span class="num">7</span><span class="stitle">制約・バリデーション</span><span class="chev"></span></summary>
492
+ <details class="sec"><summary><span class="num">3</span><span class="stitle">制約・バリデーション</span><span class="chev"></span></summary>
401
493
  <div class="body">
402
494
  <ul>
403
495
  <li><b>タイトル 35文字以内</b>(タイトル集ヘッダに明記)。超過はシート/ツリーで赤表示。</li>
@@ -407,22 +499,9 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
407
499
  <li>説明文は同じ文言ライブラリを共有するが、タイトルより長文可のため35字制限は<b>タイトル枠のみ</b>に適用。</li>
408
500
  </ul>
409
501
  </div></details>
410
-
411
- <details class="sec"><summary><span class="num">8</span><span class="stitle">運用フロー — 北さんの複製量産 / 戦略の後乗せ</span><span class="chev"></span></summary>
412
- <div class="body">
413
- <ol>
414
- <li>完成キャンペーンを<b>テンプレとして丸ごと複製</b>する</li>
415
- <li>複製の中身(クリエイティブ/リンク/固有パラメータ)を<b>一旦消す</b></li>
416
- <li>変えたい<b>変数だけ差し替える</b></li>
417
- <li>これを検証したい変数の数だけ繰り返し量産する</li>
418
- <li>初期は 1:1:10 で撒き、数日データを見る</li>
419
- <li>伸びた型で追加量産し、<b>戦略(配信戦略・訴求方針)は後から埋める</b></li>
420
- </ol>
421
- <div class="callout green">このツールはツリーの複製ダイアログで「何を残し・何をクリアするか」を選べる。キャンペーンの「量産」で M本を一括生成。<b>戦略メモ欄は空のまま量産→後乗せ</b>で埋められる。</div>
422
- <div class="callout"><b>本ツールの立ち位置(2026-07-16 定例、2026-07-23 更新)</b>:AIで生成できる部分は<b>AIに生成させ(JSON読込)</b>、絶対に人が確定する部分(クリエイティブ選定・確認)を<b>スプシ的コピペ・一括入力・複製</b>で楽にする。ここで確認・修正して <b>OK なら 入稿JSON書き出し(=GO)</b>→ <code>webgram submit --platform yda</code>(dry-run → 本実行)。CSVを介さない直接入稿へ移行済み。今後はキャンペーン/広告グループIDのAPI/MCP自動取得、新規画像のメディア登録の自動化へ。</div>
423
- </div></details>
424
-
425
- <details class="sec"><summary><span class="num">9</span><span class="stitle">使用手順 — 保存・履歴・入稿の流れ</span><span class="chev"></span></summary>
502
+ </div>
503
+ <div id="yt-guide" style="display:none">
504
+ <details class="sec" open><summary><span class="num">1</span><span class="stitle">使用手順 — 保存・履歴・入稿の流れ</span><span class="chev"></span></summary>
426
505
  <div class="body">
427
506
  <ol>
428
507
  <li><b>プラン作成</b>:プランビューは「戦略メモ → 案件設定 → シート/ツリー切替」の順。シート選択時はさらに広告/広告グループ/キャンペーンのタブで編集する。AI が生成した state JSON は「JSON読込」で取り込む(旧v1形式も自動マイグレーション)。</li>
@@ -430,12 +509,11 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
430
509
  <li><b>バージョン保存</b>:ヘッダーの「バージョン保存」。保存のたびに<b>常に新バージョン</b>(v1, v2…)として名称・メモ・作成者付きで保存される(上書きなし)。<b>履歴本体はブラウザ内に自動保存</b>されるため、フォルダ設定なしでも一覧・再読込できる。</li>
431
510
  <li><b>保存フォルダ接続(初回に1回だけ)</b>:履歴ビューの「📁 保存フォルダに接続」でスキル同梱の <code>入稿計画/</code> フォルダを一度選ぶと、<b>次回からは開くだけで自動接続</b>され、保存のたびにファイルにも書き出される。未接続時はダウンロード動作。</li>
432
511
  <li><b>履歴の確認・再利用</b>:履歴ビューで一覧(Ver/日時/媒体/名称)を確認し、「読込」で過去バージョンを復元、「編集」で名称・メモ変更、「削除」で破棄。</li>
433
- <li><b>入稿JSON書き出し</b>:ヘッダーの「入稿JSON書き出し」をワンクリック。アカウントID・目的・開始日は案件設定チップ、ターゲティングは広告グループ行の列から自動変換される(<code>*_submit-body_*.json</code>)。</li>
434
- <li><b>入稿(dry-run → 本実行)</b>:<code>webgram submit --platform yda --json-file &lt;submit-body&gt;</code>。必ず dry-run で内容確認後、<code>--no-dry-run</code> で本実行。実行後は管理画面の一覧でOK確認。</li>
512
+ <li><b>入稿JSON書き出し</b>:ヘッダーの「入稿JSON書き出し」をワンクリック。アカウントID・目的・開始日は案件設定チップ、ターゲティングは広告グループ行の列から自動変換される(<code>*_入稿計画_*.json</code>)。</li>
513
+ <li><b>入稿(dry-run → 本実行)</b>:<code>webgram submit-plan --plan-file &lt;入稿計画&gt;</code>。dry-run が全キャンペーンを一括検証 → OKなら <code>--no-dry-run --yes</code> で本実行(1キャンペーンずつ入稿・進捗書き戻しで中断後も再開可能)。実行後は管理画面の一覧でOK確認。</li>
435
514
  </ol>
436
515
  </div></details>
437
-
438
- <details class="sec"><summary><span class="num">10</span><span class="stitle">注意事項・未対応事項(要確認)</span><span class="chev"></span></summary>
516
+ <details class="sec" open><summary><span class="num">2</span><span class="stitle">注意事項・未対応事項(要確認)</span><span class="chev"></span></summary>
439
517
  <div class="body">
440
518
  <ul>
441
519
  <li><b>バナー(固定サイズ)はAPI未対応</b> → 引き続き管理画面で手入稿。入稿JSONからは自動除外される。</li>
@@ -448,7 +526,8 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
448
526
  <li>フォルダ連携(File System Access API)はChrome系のみ。Safariなどはダウンロードにフォールバックされる。</li>
449
527
  </ul>
450
528
  </div></details>
451
- </div><!-- /mt-yda -->
529
+ </div>
530
+ </div><!-- /mt-yda -->
452
531
 
453
532
  <!-- ======== Meta タブ(検証中) ======== -->
454
533
  <div id="mt-meta" style="display:none">
@@ -519,14 +598,18 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
519
598
  <!-- ============ シートビュー ============ -->
520
599
  <!-- ============ プランビュー (戦略メモ → 案件設定 → シート/ツリー) ============ -->
521
600
  <section class="view active" id="view-sheet">
522
- <details class="stratbox" id="stratBox">
523
- <summary>戦略メモ(開閉)</summary>
524
- <textarea id="stratInput" placeholder="この案件・検証の戦略/訴求方針をメモ(後乗せOK)"></textarea>
525
- </details>
601
+ <!-- E-2: 戦略メモパネルは撤去 -->
526
602
  <details class="cfgbox" id="cfgBox" open>
527
- <summary>案件設定</summary>
603
+ <summary>共通設定</summary>
528
604
  <div class="chipset" id="tplChips"></div>
529
605
  </details>
606
+ <!-- E-1: プランツールバー(タイプ固定プルダウンのみ。キャンペーン追加導線はシート/ツリー側に配置) -->
607
+ <div class="toolbar2" id="planTools" style="margin:0 0 10px">
608
+ <span class="chip" title="このプランナーはレスポンシブ(画像)固定です(変更不可)">タイプ
609
+ <select class="chipin" id="typeAdType" disabled title="広告タイプ(固定)"><option>レスポンシブ</option></select>
610
+ <select class="chipin" id="typeMediaFormat" disabled title="メインメディアの形式(固定)"><option>画像</option></select>
611
+ </span>
612
+ </div>
530
613
  <div class="seg" id="modeSeg" style="margin:0 0 10px">
531
614
  <button data-m="sheet" class="active">シート</button>
532
615
  <button data-m="tree">ツリー</button>
@@ -539,12 +622,12 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
539
622
  </div>
540
623
  <div class="toolbar2">
541
624
  <button class="act ghost" id="btnDupSel" title="選択した行を複製">⧉ 選択を複製</button>
625
+ <button class="act ghost" id="btnSeqParam" style="display:none" title="選択した列(paramCode / 画像番号 / タイトル番号 / 説明文番号)を先頭セル基点に連番採番">🔢 連番</button>
542
626
  <span class="hinttxt">セル選択(クリック/Shift+クリック/ドラッグ)→ Cmd/Ctrl+C コピー・Cmd/Ctrl+V 貼付(スプシから一括)</span>
543
627
  <div class="spacer" style="flex:1"></div>
544
628
  <span class="filt">表示 <select id="sheetFilter">
545
629
  <option value="all">すべて</option>
546
630
  <option value="err">エラーのみ</option>
547
- <option value="banner">手入稿(バナー)のみ</option>
548
631
  </select></span>
549
632
  </div>
550
633
  <div class="sheetwrap"><div id="sheetHost"></div></div>
@@ -560,17 +643,21 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
560
643
  </div>
561
644
  </section>
562
645
 
563
- <!-- ============ 参照データビュー ============ -->
646
+ <!-- ============ アカウントアセットビュー ============ -->
564
647
  <section class="view" id="view-ref">
565
648
  <div class="toolbar2">
566
649
  <div class="seg" id="refTabs">
567
- <button data-rt="titles" class="active">タイトル&amp;説明</button>
650
+ <button data-rt="titles" class="active">タイトル</button>
651
+ <button data-rt="descriptions">説明文</button>
568
652
  <button data-rt="media">登録画像</button>
653
+ <button data-rt="adGroups">広告グループ</button>
654
+ <button data-rt="campaigns">キャンペーン</button>
569
655
  </div>
570
656
  <span class="filt">検索 <input id="refSearch" type="text" placeholder="番号・文言・IDで絞り込み" style="width:220px"></span>
571
657
  <div class="spacer" style="flex:1"></div>
572
658
  <span class="chip" id="refCount">—</span>
573
659
  <button class="act" id="btnRefAdd">+ 新規登録</button>
660
+ <button class="act ghost" id="btnAccCopy" title="他アカウントからタイトル/説明文をコピー">⎘ 他アカウントから文言コピー</button>
574
661
  <button class="act ghost" id="btnRefReset" title="埋め込みの初期値に戻す">リセット</button>
575
662
  </div>
576
663
  <div class="sheetwrap"><div id="refHost"></div></div>
@@ -598,17 +685,21 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
598
685
  </aside>
599
686
  <div class="toast" id="toast"></div>
600
687
 
601
- <script id="embedData" type="application/json">{"headers": ["キャンペーンID", "広告グループID", "広告ID", "メディアID", "広告名", "配信設定", "広告タイプ", "メインメディアの形式", "タイトル", "説明文1", "説明文2", "ボタン", "主体者表記", "最終リンク先URL", "スマートフォン向けURL", "表示URLの形式", "トラッキングURL", "カスタムパラメータ", "表示URL", "価格のプレフィックス", "価格のサフィックス", "広告のベースカラー", "メディアID(ロゴ1)", "メディアID(ロゴ2)", "メディアID(ロゴ3)", "メディアID(動画サムネイル)", "第三者測定スクリプトタグsrc属性URL", "インプレッションビーコンURL", "ビューアブルインプレッションビーコンURL", "視聴ビーコンURL(再生開始)", "視聴ビーコンURL(3秒視聴)", "視聴ビーコンURL(10秒視聴)", "視聴ビーコンURL(25%再生)", "視聴ビーコンURL(50%再生)", "視聴ビーコンURL(75%再生)", "視聴ビーコンURL(再生完了)", "リンク先URL(キャンペーンバナー)", "メディアID(キャンペーンバナー600_500)", "メディアID(キャンペーンバナー600_600)", "メディアID(キャンペーンバナー600_80)", "メディアID(キャンペーンバナー600_1200)", "削除", "キャンペーン名(編集不可)", "広告グループ名(編集不可)", "メディアID1(画像・アセット)", "メディアID2(画像・アセット)", "メディアID3(画像・アセット)", "メディアID4(画像・アセット)", "メディアID5(画像・アセット)", "メディアID6(画像・アセット)", "メディアID7(画像・アセット)", "メディアID8(画像・アセット)", "メディアID9(画像・アセット)", "メディアID10(画像・アセット)", "メディアID11(画像・アセット)", "メディアID12(画像・アセット)", "メディアID13(画像・アセット)", "メディアID14(画像・アセット)", "メディアID15(画像・アセット)", "タイトル1(アセット)", "タイトル2(アセット)", "タイトル3(アセット)", "タイトル4(アセット)", "タイトル5(アセット)", "説明文1(アセット)", "説明文2(アセット)", "説明文3(アセット)", "説明文4(アセット)", "説明文5(アセット)", "メディアID(カルーセルカード1)", "タイトル(カルーセルカード1)", "説明文(カルーセルカード1)", "最終リンク先URL(カルーセルカード1)", "スマートフォン向けURL(カルーセルカード1)", "メディアID(カルーセルカード2)", "タイトル(カルーセルカード2)", "説明文(カルーセルカード2)", "最終リンク先URL(カルーセルカード2)", "スマートフォン向けURL(カルーセルカード2)", "メディアID(カルーセルカード3)", "タイトル(カルーセルカード3)", "説明文(カルーセルカード3)", "最終リンク先URL(カルーセルカード3)", "スマートフォン向けURL(カルーセルカード3)", "メディアID(カルーセルカード4)", "タイトル(カルーセルカード4)", "説明文(カルーセルカード4)", "最終リンク先URL(カルーセルカード4)", "スマートフォン向けURL(カルーセルカード4)", "メディアID(カルーセルカード5)", "タイトル(カルーセルカード5)", "説明文(カルーセルカード5)", "最終リンク先URL(カルーセルカード5)", "スマートフォン向けURL(カルーセルカード5)", "メディアID(カルーセルカード6)", "タイトル(カルーセルカード6)", "説明文(カルーセルカード6)", "最終リンク先URL(カルーセルカード6)", "スマートフォン向けURL(カルーセルカード6)", "メディアID(カルーセルカード7)", "タイトル(カルーセルカード7)", "説明文(カルーセルカード7)", "最終リンク先URL(カルーセルカード7)", "スマートフォン向けURL(カルーセルカード7)", "メディアID(カルーセルカード8)", "タイトル(カルーセルカード8)", "説明文(カルーセルカード8)", "最終リンク先URL(カルーセルカード8)", "スマートフォン向けURL(カルーセルカード8)", "メディアID(カルーセルカード9)", "タイトル(カルーセルカード9)", "説明文(カルーセルカード9)", "最終リンク先URL(カルーセルカード9)", "スマートフォン向けURL(カルーセルカード9)", "メディアID(カルーセルカード10)", "タイトル(カルーセルカード10)", "説明文(カルーセルカード10)", "最終リンク先URL(カルーセルカード10)", "スマートフォン向けURL(カルーセルカード10)", "インプレッション数(編集不可)", "クリック数(編集不可)", "クリック率(編集不可)", "コスト(編集不可)", "平均CPC(編集不可)", "ビューアブルインプレッション数(編集不可)", "平均掲載順位(編集不可)", "平均CPM(編集不可)", "平均vCPM(編集不可)", "コンバージョン数(編集不可)", "コンバージョン率(編集不可)", "コスト/コンバージョン数(編集不可)", "コンバージョンの価値(編集不可)", "コンバージョンの価値/コンバージョン数(編集不可)", "コンバージョンの価値/コスト(編集不可)", "クロスデバイスコンバージョン数(編集不可)", "ビュースルーコンバージョン数(編集不可)", "コンバージョン数(全て)(編集不可)", "コンバージョン率(全て)(編集不可)", "コスト/コンバージョン数(全て)(編集不可)", "コンバージョンの価値(全て)(編集不可)", "コンバージョンの価値(全て)/コンバージョン数(全て)(編集不可)", "コンバージョンの価値(全て)/コスト(編集不可)", "コンバージョン数(クリック経由)(編集不可)", "コンバージョン率(クリック経由)(編集不可)", "コスト/コンバージョン数(クリック経由)(編集不可)", "コンバージョンの価値(クリック経由)(編集不可)", "コンバージョンの価値(クリック経由)/コンバージョン数(クリック経由)(編集不可)", "コンバージョンの価値(クリック経由)/コスト(編集不可)", "ビューアブルインプレッション率(編集不可)", "ビューアブルクリック数(編集不可)", "ビューアブルクリック率(編集不可)", "メジャードインプレッション数(編集不可)", "メジャードインプレッション測定率(編集不可)", "動画の10秒再生数(編集不可)", "平均CPV(編集不可)", "動画の再生開始数(編集不可)", "動画の3秒再生数(編集不可)", "動画の25%再生数(編集不可)", "動画の50%再生数(編集不可)", "動画の75%再生数(編集不可)", "動画の95%再生数(編集不可)", "動画の100%再生数(編集不可)", "動画の平均再生率(編集不可)", "動画の平均再生時間(秒)(編集不可)", "動画の再生完了率(編集不可)", "", "", "", ""], "titles": [{"no": 1, "text": "CMで話題。腰痛薬が一度だけお得に買える"}, {"no": 2, "text": "神経痛に飲んで効く『80代もメキメキ』"}, {"no": 3, "text": "業界激震。神経痛のお薬が一度だけ買える"}, {"no": 4, "text": "業界激震。腰痛のお薬が一度だけお得に買える。"}, {"no": 5, "text": "業界激震。腰痛のお薬が一度だけ買える"}, {"no": 6, "text": "CM放送で話題。しびれに効くお薬半額以下"}, {"no": 7, "text": "CM放送で話題。腰痛に効くお薬が半額以下"}, {"no": 8, "text": "CM放送で話題。神経痛に効くお薬半額以下"}, {"no": 9, "text": "しびれにも効く腰痛のお薬。本日半額以下。"}, {"no": 10, "text": "『しびれの原因が…』放置NGの理由に驚愕"}, {"no": 11, "text": "『腰痛の原因が…』業界が震えた。飲むお薬"}, {"no": 12, "text": "『神経痛の原因が』業界が震えた。飲むお薬"}, {"no": 13, "text": "嘘みたいな腰のお薬。本日まさかの半額以下"}, {"no": 14, "text": "10年悩んだ腰痛が…富山のお薬が半額以下"}, {"no": 15, "text": "『治らなかった腰痛が』腰のお薬飲んでみて"}, {"no": 16, "text": "テレビCMで話題。腰のお薬が本日半額以下"}, {"no": 17, "text": "『神経痛に効く』嘘みたいなお薬が登場"}, {"no": 18, "text": "『関節痛に効く』嘘みたいなお薬が登場"}, {"no": 19, "text": "『しびれに効く』嘘みたいなお薬が登場"}, {"no": 20, "text": "『神経痛に効く』富山のお薬が本日半額以下"}, {"no": 21, "text": "『関節痛に効く』富山のお薬が本日半額以下"}, {"no": 22, "text": "『しびれに効く』富山のお薬が本日半額以下"}, {"no": 23, "text": "『腰痛に効く』富山のお薬が本日半額以下"}, {"no": 24, "text": "階段きつい人やって。腰痛に飲んで効くお薬"}, {"no": 25, "text": "起き上がれない人へ。腰痛に飲んで効くお薬"}, {"no": 26, "text": "肩が上がらない人へ。五十肩に効くお薬すご"}, {"no": 27, "text": "五十肩で眠れない人へ。飲んで効くお薬スゴ"}, {"no": 28, "text": "走れない70代はやって。腰痛のお薬が話題"}, {"no": 29, "text": "70歳過ぎたらやって。腰痛に飲んで効く薬"}, {"no": 30, "text": "歩けなくなる前にやって。腰痛のお薬飲むと"}, {"no": 31, "text": "正座できない70代やって。飲む腰痛改善薬"}, {"no": 32, "text": "「サプリではない」腰痛の裏ワザがすごすぎ"}, {"no": 33, "text": "「サプリではない」神経痛の裏ワザが凄すぎ"}, {"no": 34, "text": "「サプリではない」しびれの裏ワザが凄すぎ"}, {"no": 35, "text": "「サプリではない」関節痛の裏ワザが凄すぎ"}, {"no": 36, "text": "「サプリではない」しびれの裏ワザが凄すぎ"}, {"no": 37, "text": "「腰痛が嘘みたいに...」腰痛改善薬すご"}, {"no": 38, "text": "10年我慢した腰痛が…【飲んで効くお薬】"}, {"no": 39, "text": "腰痛の裏ワザが判明。70代に爆売れ"}, {"no": 40, "text": "腰痛の裏ワザが判明。80代に爆売れ"}, {"no": 41, "text": "腰痛の裏技が判明。テレビCM放送後爆売れ"}, {"no": 42, "text": "今すぐやめて。腰痛悪化の原因が判明"}, {"no": 43, "text": "『マジで80代?』腰痛改善薬飲むと…。"}, {"no": 44, "text": "『朝起きて仰天』腰に効くお薬飲んで寝ると"}, {"no": 45, "text": "「腰痛くて起きれない」CMで話題のお薬"}, {"no": 46, "text": "グルコサミンではない。CMで話題の腰の薬"}, {"no": 47, "text": "ギシギシ腰は飲んで。80代『改善薬すご』"}, {"no": 48, "text": "立ち上がる度に痛む腰に。飲む改善薬スゴ"}, {"no": 49, "text": "77歳「歩くたびに…」腰のSOS気づいて"}, {"no": 50, "text": "腰痛治らない理由やばい。60代過ぎたら"}, {"no": 51, "text": "腰痛治らない理由やばい。70代過ぎたら"}, {"no": 52, "text": "腰痛治らない理由やばい。80代過ぎても"}, {"no": 53, "text": "『腰痛に効く』嘘みたいなお薬。すごすぎ"}, {"no": 54, "text": "テレビCM放送後爆売れ。腰痛のお薬が一度だけお得に買える。"}, {"no": 55, "text": "CMで話題。腰のお薬一度だけお得に買える"}, {"no": 56, "text": "『神経痛に効く』一度だけお得に買えるお薬"}, {"no": 57, "text": "『関節痛に効く』一度だけお得に買えるお薬"}, {"no": 58, "text": "『しびれに効く』一度だけお得に買えるお薬"}, {"no": 59, "text": "『腰痛に効く』一度だけお得に買えるお薬"}, {"no": 60, "text": "『腰痛に効く』嘘みたいなお薬。一度だけお得に買えると話題。"}, {"no": 61, "text": "「朝から腰が痛い」CMで話題のお薬"}, {"no": 62, "text": "「腰が痛くて動けない」CMで話題のお薬"}, {"no": 63, "text": "「腰が痛くて眠れない」CMで話題のお薬"}, {"no": 64, "text": "「腰が痛くて歩けない」CMで話題のお薬"}, {"no": 65, "text": "「草むしりで腰痛に」CMで話題のお薬"}, {"no": 66, "text": "「孫を抱けない腰痛」CMで話題のお薬"}, {"no": 67, "text": "「腰がズキズキ痛む」CMで話題のお薬"}, {"no": 68, "text": "「腰がズーンと重い」CMで話題のお薬"}, {"no": 69, "text": "「腰がじんじん痛い」CMで話題のお薬"}, {"no": 70, "text": "「腰がガチガチに痛い」CMで話題のお薬"}, {"no": 71, "text": "「腰に鈍痛が続く」CMで話題のお薬"}, {"no": 72, "text": "「腰の奥が痛い」CMで話題のお薬"}, {"no": 73, "text": "「腰も膝も痛い」CMで話題のお薬"}, {"no": 74, "text": "「お尻から腰が痛い」CMで話題のお薬"}, {"no": 75, "text": "「しびれで眠れない」CMで話題のお薬"}, {"no": 76, "text": "「しびれで歩けない」CMで話題のお薬"}, {"no": 77, "text": "「足がしびれて立てない」CMで話題のお薬"}, {"no": 78, "text": "「腰から足がしびれる」CMで話題のお薬"}, {"no": 79, "text": "「足のしびれが続く」CMで話題のお薬"}, {"no": 80, "text": "「足がしびれて辛い」CMで話題のお薬"}, {"no": 81, "text": "「腰と足がしびれる」CMで話題のお薬"}, {"no": 82, "text": "「お尻から足がしびれる」CMで話題のお薬"}, {"no": 83, "text": ""}, {"no": 84, "text": "坐骨神経痛に伴うしびれに効く。神経のお薬"}, {"no": 85, "text": "ビリビリしびれる人やって。神経痛のお薬"}, {"no": 86, "text": "ピリピリしびれる人やって。神経痛のお薬"}, {"no": 87, "text": "チクチクしびれる人やって。神経痛のお薬"}, {"no": 88, "text": "ズキズキしびれる人やって。神経痛のお薬"}, {"no": 89, "text": "ジンジンしびれる人やって。神経痛のお薬"}, {"no": 90, "text": "しびれで夜目が覚める人やって。神経改善薬"}, {"no": 91, "text": "突然来るしびれに飲んで仰天。神経改善薬"}, {"no": 92, "text": "手足のしびれに飲んで効く。神経のお薬"}, {"no": 93, "text": "【CM放送で話題】神経に効くしびれ改善薬"}, {"no": 94, "text": "お得に買えるのは一度だけ。神経のお薬すご"}, {"no": 95, "text": ""}, {"no": 96, "text": "70歳過ぎたらやって。しびれに効くお薬"}, {"no": 97, "text": "70歳過ぎたらやって。1日約64円で…"}, {"no": 98, "text": "『サプリではない』神経の修復成分すご"}, {"no": 99, "text": "痛む腰簡単ケア。原因が判明『やばすぎ…』"}, {"no": 100, "text": "『腰痛が嘘みたいに…』富山のお薬すご"}, {"no": 101, "text": "薬都の瓶薬すご…『70代の腰はまかせて』"}, {"no": 102, "text": "10年我慢した腰痛が…【すごいお薬】でた"}, {"no": 103, "text": "腰痛の原因が判明。神経の痛みに効く方法"}, {"no": 104, "text": "走れない70代はやって。痛みのお薬スゴ"}, {"no": 105, "text": "腰痛の裏ワザが判明。70代に爆売れ"}, {"no": 106, "text": "腰痛の裏ワザが判明。80代に爆売れ"}, {"no": 107, "text": "今すぐやめて。神経痛が悪化する原因が判明"}, {"no": 108, "text": "『マジで80代?』腰痛のお薬飲むと…"}, {"no": 109, "text": "『マジで70代?』腰痛のお薬飲むと…"}, {"no": 110, "text": "薬都の会社「腰痛に効く」嘘みたいなお薬"}, {"no": 111, "text": "歩けなくなる前にやって。富山の飲み薬"}, {"no": 112, "text": "『しびれが嘘みたいに』富山のお薬すご"}, {"no": 113, "text": "まるで別人級『70代のシビレはまかせて』"}, {"no": 114, "text": "10年我慢したしびれが【すごいお薬】でた"}, {"no": 115, "text": "しびれの原因が判明。何歳でも効く方法"}, {"no": 116, "text": "走れない70代はやって。しびれのお薬スゴ"}, {"no": 117, "text": "しびれの裏ワザが判明。70代に爆売れ"}, {"no": 118, "text": "しびれの裏ワザが判明。80代に爆売れ"}, {"no": 119, "text": "今すぐやめて。しびれが治らない原因が…"}, {"no": 120, "text": "『マジで80代?』しびれのお薬飲むと…"}, {"no": 121, "text": "『マジで70代?』しびれのお薬飲むと…"}, {"no": 122, "text": "薬都の会社「しびれに効く」嘘みたいなお薬"}, {"no": 123, "text": "手足のしびれにスーッと。嘘みたいなお薬"}, {"no": 124, "text": "突然手足がしびれる人やって。富山のお薬"}, {"no": 125, "text": "立ち上がるとき痛い腰に。とんでもないお薬"}, {"no": 126, "text": "雨の日ひどくなる腰痛に。とんでもないお薬"}, {"no": 127, "text": "雨の日しびれる手足に。とんでもないお薬"}, {"no": 128, "text": "しびれ・腰痛に飲んで効く嘘みたいなお薬"}, {"no": 129, "text": "朝起きて腰痛い人へ。CMで話題のお薬"}, {"no": 130, "text": "神経に異常?しびれたら飲んで。富山のお薬"}, {"no": 131, "text": "富山の薬売りが開発!神経痛に効くお薬"}, {"no": 132, "text": "ジンジンしびれる人は使って。富山の瓶薬"}, {"no": 133, "text": "ズキズキ痛む腰にやって。神経痛に効くお薬"}, {"no": 134, "text": "年齢は関係なかった?飲んで衝撃のお薬"}, {"no": 135, "text": "しびれ放置してる人やって。富山の瓶薬"}, {"no": 136, "text": "腰の痛み放置してる人はやって。富山の瓶薬"}, {"no": 137, "text": "年々ひどくなる腰痛に。飲んで効くお薬すご"}, {"no": 138, "text": "年々ひどくなるしびれに飲んで。富山の瓶薬"}, {"no": 139, "text": "同じ体勢できない人はやって。薬都の瓶薬"}, {"no": 140, "text": "「正座がツラい」腰痛に飲んで。薬都の瓶薬"}, {"no": 141, "text": "手足のしびれは神経の悲鳴?薬都の瓶薬すご"}, {"no": 142, "text": "坐骨神経からくる痛みに!薬都の瓶薬すご"}, {"no": 143, "text": "坐骨神経からくるしびれに!薬都の瓶薬すご"}, {"no": 144, "text": "変形性関節からくる膝痛に!薬都の瓶薬すご"}, {"no": 145, "text": "頸椎からくる神経痛に!薬都の瓶薬がすごい"}, {"no": 146, "text": "頸椎からくるしびれに!薬都の瓶薬がすごい"}, {"no": 147, "text": "脊柱管狭窄からくる腰痛に!薬都の瓶薬すご"}, {"no": 148, "text": "腰にビリっとくる神経痛に!薬都の瓶薬すご"}, {"no": 149, "text": "原因は坐骨神経の圧迫?薬都の瓶薬が効く"}, {"no": 150, "text": "原因は神経の異常?しびれに効く瓶薬すご"}, {"no": 151, "text": "一度しか買えない。腰のお薬が1回約64円"}, {"no": 152, "text": "飲むだけで効く瓶薬!70代が使って感動"}, {"no": 153, "text": ""}, {"no": 154, "text": ""}, {"no": 155, "text": ""}, {"no": 156, "text": ""}, {"no": 157, "text": ""}, {"no": 158, "text": ""}, {"no": 159, "text": ""}, {"no": 160, "text": ""}, {"no": 161, "text": ""}, {"no": 162, "text": ""}, {"no": 163, "text": ""}, {"no": 164, "text": ""}, {"no": 165, "text": ""}, {"no": 166, "text": ""}, {"no": 167, "text": ""}, {"no": 168, "text": ""}, {"no": 169, "text": ""}, {"no": 170, "text": ""}, {"no": 171, "text": ""}, {"no": 172, "text": ""}, {"no": 173, "text": ""}, {"no": 174, "text": ""}, {"no": 175, "text": ""}, {"no": 176, "text": ""}, {"no": 177, "text": ""}, {"no": 178, "text": ""}, {"no": 179, "text": ""}, {"no": 180, "text": ""}, {"no": 181, "text": ""}, {"no": 182, "text": ""}, {"no": 183, "text": ""}, {"no": 184, "text": ""}, {"no": 185, "text": ""}, {"no": 186, "text": ""}, {"no": 187, "text": ""}, {"no": 188, "text": ""}, {"no": 189, "text": ""}, {"no": 190, "text": ""}, {"no": 191, "text": ""}, {"no": 192, "text": ""}, {"no": 193, "text": ""}, {"no": 194, "text": ""}, {"no": 195, "text": ""}, {"no": 196, "text": ""}, {"no": 197, "text": ""}, {"no": 198, "text": ""}, {"no": 199, "text": ""}, {"no": 200, "text": ""}, {"no": 201, "text": ""}, {"no": 202, "text": ""}, {"no": 203, "text": ""}, {"no": 204, "text": ""}, {"no": 205, "text": ""}, {"no": 206, "text": ""}, {"no": 207, "text": ""}, {"no": 208, "text": ""}, {"no": 209, "text": ""}, {"no": 210, "text": ""}, {"no": 211, "text": ""}, {"no": 212, "text": ""}, {"no": 213, "text": ""}, {"no": 214, "text": ""}, {"no": 215, "text": ""}, {"no": 216, "text": ""}, {"no": 217, "text": ""}, {"no": 218, "text": ""}, {"no": 219, "text": ""}, {"no": 220, "text": ""}, {"no": 221, "text": ""}, {"no": 222, "text": ""}, {"no": 223, "text": ""}, {"no": 224, "text": ""}, {"no": 225, "text": ""}, {"no": 226, "text": ""}, {"no": 227, "text": ""}, {"no": 228, "text": ""}, {"no": 229, "text": ""}, {"no": 230, "text": ""}, {"no": 231, "text": ""}, {"no": 232, "text": ""}, {"no": 233, "text": ""}, {"no": 234, "text": ""}, {"no": 235, "text": ""}, {"no": 236, "text": ""}, {"no": 237, "text": ""}, {"no": 238, "text": ""}, {"no": 239, "text": ""}, {"no": 240, "text": ""}, {"no": 241, "text": ""}, {"no": 242, "text": ""}, {"no": 243, "text": ""}, {"no": 244, "text": ""}, {"no": 245, "text": ""}, {"no": 246, "text": ""}, {"no": 247, "text": ""}, {"no": 248, "text": ""}, {"no": 249, "text": ""}, {"no": 250, "text": ""}, {"no": 251, "text": ""}, {"no": 252, "text": ""}, {"no": 253, "text": ""}, {"no": 254, "text": ""}, {"no": 255, "text": ""}, {"no": 256, "text": ""}, {"no": 257, "text": ""}, {"no": 258, "text": ""}, {"no": 259, "text": ""}, {"no": 260, "text": ""}, {"no": 261, "text": ""}, {"no": 262, "text": ""}, {"no": 263, "text": ""}, {"no": 264, "text": ""}, {"no": 265, "text": ""}, {"no": 266, "text": ""}, {"no": 267, "text": ""}, {"no": 268, "text": ""}, {"no": 269, "text": ""}, {"no": 270, "text": ""}, {"no": 271, "text": ""}, {"no": 272, "text": ""}, {"no": 273, "text": ""}, {"no": 274, "text": ""}, {"no": 275, "text": ""}, {"no": 276, "text": ""}, {"no": 277, "text": ""}, {"no": 278, "text": ""}, {"no": 279, "text": ""}, {"no": 280, "text": ""}, {"no": 281, "text": ""}, {"no": 282, "text": ""}, {"no": 283, "text": ""}, {"no": 284, "text": ""}, {"no": 285, "text": ""}, {"no": 286, "text": ""}, {"no": 287, "text": ""}, {"no": 288, "text": ""}, {"no": 289, "text": ""}, {"no": 290, "text": ""}, {"no": 291, "text": ""}, {"no": 292, "text": ""}, {"no": 293, "text": ""}, {"no": 294, "text": ""}, {"no": 295, "text": ""}, {"no": 296, "text": ""}, {"no": 297, "text": ""}, {"no": 298, "text": ""}, {"no": 299, "text": ""}, {"no": 300, "text": ""}, {"no": 301, "text": ""}, {"no": 302, "text": ""}, {"no": 303, "text": ""}, {"no": 304, "text": ""}, {"no": 305, "text": ""}, {"no": 306, "text": ""}, {"no": 307, "text": ""}, {"no": 308, "text": ""}, {"no": 309, "text": ""}, {"no": 310, "text": ""}, {"no": 311, "text": ""}, {"no": 312, "text": ""}, {"no": 313, "text": ""}, {"no": 314, "text": ""}, {"no": 315, "text": ""}, {"no": 316, "text": ""}, {"no": 317, "text": ""}, {"no": 318, "text": ""}, {"no": 319, "text": ""}, {"no": 320, "text": ""}, {"no": 321, "text": ""}, {"no": 322, "text": ""}, {"no": 323, "text": ""}, {"no": 324, "text": ""}, {"no": 325, "text": ""}, {"no": 326, "text": ""}, {"no": 327, "text": ""}, {"no": 328, "text": ""}, {"no": 329, "text": ""}, {"no": 330, "text": ""}, {"no": 331, "text": ""}, {"no": 332, "text": ""}, {"no": 333, "text": ""}, {"no": 334, "text": ""}, {"no": 335, "text": ""}, {"no": 336, "text": ""}, {"no": 337, "text": ""}, {"no": 338, "text": ""}, {"no": 339, "text": ""}, {"no": 340, "text": ""}, {"no": 341, "text": ""}, {"no": 342, "text": ""}, {"no": 343, "text": ""}, {"no": 344, "text": ""}, {"no": 345, "text": ""}, {"no": 346, "text": ""}, {"no": 347, "text": ""}, {"no": 348, "text": ""}, {"no": 349, "text": ""}, {"no": 350, "text": ""}, {"no": 351, "text": ""}, {"no": 352, "text": ""}, {"no": 353, "text": ""}, {"no": 354, "text": ""}, {"no": 355, "text": ""}, {"no": 356, "text": ""}, {"no": 357, "text": ""}, {"no": 358, "text": ""}, {"no": 359, "text": ""}, {"no": 360, "text": ""}, {"no": 361, "text": ""}, {"no": 362, "text": ""}, {"no": 363, "text": ""}, {"no": 364, "text": ""}, {"no": 365, "text": ""}, {"no": 366, "text": ""}, {"no": 367, "text": ""}, {"no": 368, "text": ""}, {"no": 369, "text": ""}, {"no": 370, "text": ""}, {"no": 371, "text": ""}, {"no": 372, "text": ""}, {"no": 373, "text": ""}, {"no": 374, "text": ""}, {"no": 375, "text": ""}, {"no": 376, "text": ""}, {"no": 377, "text": ""}, {"no": 378, "text": ""}, {"no": 379, "text": ""}, {"no": 380, "text": ""}, {"no": 381, "text": ""}, {"no": 382, "text": ""}, {"no": 383, "text": ""}, {"no": 384, "text": ""}, {"no": 385, "text": ""}, {"no": 386, "text": ""}, {"no": 387, "text": ""}, {"no": 388, "text": ""}, {"no": 389, "text": ""}, {"no": 390, "text": ""}, {"no": 391, "text": ""}, {"no": 392, "text": ""}, {"no": 393, "text": ""}, {"no": 394, "text": ""}, {"no": 395, "text": ""}, {"no": 396, "text": ""}, {"no": 397, "text": ""}, {"no": 398, "text": ""}, {"no": 399, "text": ""}, {"no": 400, "text": ""}, {"no": 401, "text": ""}, {"no": 402, "text": ""}, {"no": 403, "text": ""}, {"no": 404, "text": ""}, {"no": 405, "text": ""}, {"no": 406, "text": ""}, {"no": 407, "text": ""}, {"no": 408, "text": ""}, {"no": 409, "text": ""}, {"no": 410, "text": ""}, {"no": 411, "text": ""}, {"no": 412, "text": ""}, {"no": 413, "text": ""}, {"no": 414, "text": ""}, {"no": 415, "text": ""}, {"no": 416, "text": ""}, {"no": 417, "text": ""}, {"no": 418, "text": ""}, {"no": 419, "text": ""}, {"no": 420, "text": ""}, {"no": 421, "text": ""}, {"no": 422, "text": ""}, {"no": 423, "text": ""}, {"no": 424, "text": ""}, {"no": 425, "text": ""}, {"no": 426, "text": ""}, {"no": 427, "text": ""}, {"no": 428, "text": ""}, {"no": 429, "text": ""}, {"no": 430, "text": ""}, {"no": 431, "text": ""}, {"no": 432, "text": ""}, {"no": 433, "text": ""}, {"no": 434, "text": ""}, {"no": 435, "text": ""}, {"no": 436, "text": ""}, {"no": 437, "text": ""}, {"no": 438, "text": ""}, {"no": 439, "text": ""}, {"no": 440, "text": ""}, {"no": 441, "text": ""}, {"no": 442, "text": ""}, {"no": 443, "text": ""}, {"no": 444, "text": ""}, {"no": 445, "text": ""}, {"no": 446, "text": ""}, {"no": 447, "text": ""}, {"no": 448, "text": ""}, {"no": 449, "text": ""}, {"no": 450, "text": ""}, {"no": 451, "text": ""}, {"no": 452, "text": ""}, {"no": 453, "text": ""}, {"no": 454, "text": ""}, {"no": 455, "text": ""}, {"no": 456, "text": ""}, {"no": 457, "text": ""}, {"no": 458, "text": ""}, {"no": 459, "text": ""}, {"no": 460, "text": ""}, {"no": 461, "text": ""}, {"no": 462, "text": ""}, {"no": 463, "text": ""}, {"no": 464, "text": ""}, {"no": 465, "text": ""}, {"no": 466, "text": ""}, {"no": 467, "text": ""}, {"no": 468, "text": ""}, {"no": 469, "text": ""}, {"no": 470, "text": ""}, {"no": 471, "text": ""}, {"no": 472, "text": ""}, {"no": 473, "text": ""}, {"no": 474, "text": ""}, {"no": 475, "text": ""}, {"no": 476, "text": ""}, {"no": 477, "text": ""}, {"no": 478, "text": ""}, {"no": 479, "text": ""}, {"no": 480, "text": ""}, {"no": 481, "text": ""}, {"no": 482, "text": ""}, {"no": 483, "text": ""}, {"no": 484, "text": ""}, {"no": 485, "text": ""}, {"no": 486, "text": ""}, {"no": 487, "text": ""}, {"no": 488, "text": ""}, {"no": 489, "text": ""}, {"no": 490, "text": ""}, {"no": 491, "text": ""}, {"no": 492, "text": ""}, {"no": 493, "text": ""}, {"no": 494, "text": ""}, {"no": 495, "text": ""}, {"no": 496, "text": ""}, {"no": 497, "text": ""}, {"no": 498, "text": ""}, {"no": 499, "text": ""}, {"no": 500, "text": ""}, {"no": 501, "text": ""}, {"no": 502, "text": ""}, {"no": 503, "text": ""}, {"no": 504, "text": ""}, {"no": 505, "text": ""}, {"no": 506, "text": ""}, {"no": 507, "text": ""}, {"no": 508, "text": ""}, {"no": 509, "text": ""}, {"no": 510, "text": ""}, {"no": 511, "text": ""}, {"no": 512, "text": ""}, {"no": 513, "text": ""}, {"no": 514, "text": ""}, {"no": 515, "text": ""}, {"no": 516, "text": ""}, {"no": 517, "text": ""}, {"no": 518, "text": ""}, {"no": 519, "text": ""}, {"no": 520, "text": ""}, {"no": 521, "text": ""}, {"no": 522, "text": ""}, {"no": 523, "text": ""}, {"no": 524, "text": ""}, {"no": 525, "text": ""}, {"no": 526, "text": ""}, {"no": 527, "text": ""}, {"no": 528, "text": ""}, {"no": 529, "text": ""}, {"no": 530, "text": ""}, {"no": 531, "text": ""}, {"no": 532, "text": ""}, {"no": 533, "text": ""}, {"no": 534, "text": ""}, {"no": 535, "text": ""}, {"no": 536, "text": ""}, {"no": 537, "text": ""}, {"no": 538, "text": ""}, {"no": 539, "text": ""}, {"no": 540, "text": ""}, {"no": 541, "text": ""}, {"no": 542, "text": ""}, {"no": 543, "text": ""}, {"no": 544, "text": ""}, {"no": 545, "text": ""}, {"no": 546, "text": ""}, {"no": 547, "text": ""}, {"no": 548, "text": ""}, {"no": 549, "text": ""}, {"no": 550, "text": ""}, {"no": 551, "text": ""}, {"no": 552, "text": ""}, {"no": 553, "text": ""}, {"no": 554, "text": ""}, {"no": 555, "text": ""}, {"no": 556, "text": ""}, {"no": 557, "text": ""}, {"no": 558, "text": ""}, {"no": 559, "text": ""}, {"no": 560, "text": ""}, {"no": 561, "text": ""}, {"no": 562, "text": ""}, {"no": 563, "text": ""}, {"no": 564, "text": ""}, {"no": 565, "text": ""}, {"no": 566, "text": ""}, {"no": 567, "text": ""}, {"no": 568, "text": ""}, {"no": 569, "text": ""}, {"no": 570, "text": ""}, {"no": 571, "text": ""}, {"no": 572, "text": ""}, {"no": 573, "text": ""}, {"no": 574, "text": ""}, {"no": 575, "text": ""}, {"no": 576, "text": ""}, {"no": 577, "text": ""}, {"no": 578, "text": ""}, {"no": 579, "text": ""}, {"no": 580, "text": ""}, {"no": 581, "text": ""}, {"no": 582, "text": ""}, {"no": 583, "text": ""}, {"no": 584, "text": ""}, {"no": 585, "text": ""}, {"no": 586, "text": ""}, {"no": 587, "text": ""}, {"no": 588, "text": ""}, {"no": 589, "text": ""}, {"no": 590, "text": ""}, {"no": 591, "text": ""}, {"no": 592, "text": ""}, {"no": 593, "text": ""}, {"no": 594, "text": ""}, {"no": 595, "text": ""}, {"no": 596, "text": ""}, {"no": 597, "text": ""}, {"no": 598, "text": ""}, {"no": 599, "text": ""}, {"no": 600, "text": ""}, {"no": 601, "text": ""}, {"no": 602, "text": ""}, {"no": 603, "text": ""}, {"no": 604, "text": ""}, {"no": 605, "text": ""}, {"no": 606, "text": ""}, {"no": 607, "text": ""}, {"no": 608, "text": ""}, {"no": 609, "text": ""}, {"no": 610, "text": ""}, {"no": 611, "text": ""}, {"no": 612, "text": ""}, {"no": 613, "text": ""}, {"no": 614, "text": ""}, {"no": 615, "text": ""}, {"no": 616, "text": ""}, {"no": 617, "text": ""}, {"no": 618, "text": ""}, {"no": 619, "text": ""}, {"no": 620, "text": ""}, {"no": 621, "text": ""}, {"no": 622, "text": ""}, {"no": 623, "text": ""}, {"no": 624, "text": ""}, {"no": 625, "text": ""}, {"no": 626, "text": ""}, {"no": 627, "text": ""}, {"no": 628, "text": ""}, {"no": 629, "text": ""}, {"no": 630, "text": ""}, {"no": 631, "text": ""}, {"no": 632, "text": ""}, {"no": 633, "text": ""}, {"no": 634, "text": ""}, {"no": 635, "text": ""}, {"no": 636, "text": ""}, {"no": 637, "text": ""}, {"no": 638, "text": ""}, {"no": 639, "text": ""}, {"no": 640, "text": ""}, {"no": 641, "text": ""}, {"no": 642, "text": ""}, {"no": 643, "text": ""}, {"no": 644, "text": ""}, {"no": 645, "text": ""}, {"no": 646, "text": ""}, {"no": 647, "text": ""}, {"no": 648, "text": ""}, {"no": 649, "text": ""}, {"no": 650, "text": ""}, {"no": 651, "text": ""}, {"no": 652, "text": ""}, {"no": 653, "text": ""}, {"no": 654, "text": ""}, {"no": 655, "text": ""}, {"no": 656, "text": ""}, {"no": 657, "text": ""}, {"no": 658, "text": ""}, {"no": 659, "text": ""}, {"no": 660, "text": ""}, {"no": 661, "text": ""}, {"no": 662, "text": ""}, {"no": 663, "text": ""}, {"no": 664, "text": ""}, {"no": 665, "text": ""}, {"no": 666, "text": ""}, {"no": 667, "text": ""}, {"no": 668, "text": ""}, {"no": 669, "text": ""}, {"no": 670, "text": ""}, {"no": 671, "text": ""}, {"no": 672, "text": ""}, {"no": 673, "text": ""}, {"no": 674, "text": ""}, {"no": 675, "text": ""}, {"no": 676, "text": ""}, {"no": 677, "text": ""}, {"no": 678, "text": ""}, {"no": 679, "text": ""}, {"no": 680, "text": ""}, {"no": 681, "text": ""}, {"no": 682, "text": ""}, {"no": 683, "text": ""}, {"no": 684, "text": ""}, {"no": 685, "text": ""}, {"no": 686, "text": ""}, {"no": 687, "text": ""}, {"no": 688, "text": ""}, {"no": 689, "text": ""}, {"no": 690, "text": ""}, {"no": 691, "text": ""}, {"no": 692, "text": ""}, {"no": 693, "text": ""}, {"no": 694, "text": ""}, {"no": 695, "text": ""}, {"no": 696, "text": ""}, {"no": 697, "text": ""}, {"no": 698, "text": ""}, {"no": 699, "text": ""}, {"no": 700, "text": ""}, {"no": 701, "text": ""}, {"no": 702, "text": ""}, {"no": 703, "text": ""}, {"no": 704, "text": ""}, {"no": 705, "text": ""}, {"no": 706, "text": ""}, {"no": 707, "text": ""}, {"no": 708, "text": ""}, {"no": 709, "text": ""}, {"no": 710, "text": ""}, {"no": 711, "text": ""}, {"no": 712, "text": ""}, {"no": 713, "text": ""}, {"no": 714, "text": ""}, {"no": 715, "text": ""}, {"no": 716, "text": ""}, {"no": 717, "text": ""}, {"no": 718, "text": ""}, {"no": 719, "text": ""}, {"no": 720, "text": ""}, {"no": 721, "text": ""}, {"no": 722, "text": ""}, {"no": 723, "text": ""}, {"no": 724, "text": ""}, {"no": 725, "text": ""}, {"no": 726, "text": ""}, {"no": 727, "text": ""}, {"no": 728, "text": ""}, {"no": 729, "text": ""}, {"no": 730, "text": ""}, {"no": 731, "text": ""}, {"no": 732, "text": ""}, {"no": 733, "text": ""}, {"no": 734, "text": ""}, {"no": 735, "text": ""}, {"no": 736, "text": ""}, {"no": 737, "text": ""}, {"no": 738, "text": ""}, {"no": 739, "text": ""}, {"no": 740, "text": ""}, {"no": 741, "text": ""}, {"no": 742, "text": ""}, {"no": 743, "text": ""}, {"no": 744, "text": ""}, {"no": 745, "text": ""}, {"no": 746, "text": ""}, {"no": 747, "text": ""}, {"no": 748, "text": ""}, {"no": 749, "text": ""}, {"no": 750, "text": ""}, {"no": 751, "text": ""}, {"no": 752, "text": ""}, {"no": 753, "text": ""}, {"no": 754, "text": ""}, {"no": 755, "text": ""}, {"no": 756, "text": ""}, {"no": 757, "text": ""}, {"no": 758, "text": ""}, {"no": 759, "text": ""}, {"no": 760, "text": ""}, {"no": 761, "text": ""}, {"no": 762, "text": ""}, {"no": 763, "text": ""}, {"no": 764, "text": ""}, {"no": 765, "text": ""}, {"no": 766, "text": ""}, {"no": 767, "text": ""}, {"no": 768, "text": ""}, {"no": 769, "text": ""}, {"no": 770, "text": ""}, {"no": 771, "text": ""}, {"no": 772, "text": ""}, {"no": 773, "text": ""}, {"no": 774, "text": ""}, {"no": 775, "text": ""}, {"no": 776, "text": ""}, {"no": 777, "text": ""}, {"no": 778, "text": ""}, {"no": 779, "text": ""}, {"no": 780, "text": ""}, {"no": 781, "text": ""}, {"no": 782, "text": ""}, {"no": 783, "text": ""}, {"no": 784, "text": ""}, {"no": 785, "text": ""}, {"no": 786, "text": ""}, {"no": 787, "text": ""}, {"no": 788, "text": ""}, {"no": 789, "text": ""}, {"no": 790, "text": ""}, {"no": 791, "text": ""}, {"no": 792, "text": ""}, {"no": 793, "text": ""}, {"no": 794, "text": ""}, {"no": 795, "text": ""}, {"no": 796, "text": ""}, {"no": 797, "text": ""}, {"no": 798, "text": ""}, {"no": 799, "text": ""}, {"no": 800, "text": ""}, {"no": 801, "text": ""}, {"no": 802, "text": ""}, {"no": 803, "text": ""}, {"no": 804, "text": ""}, {"no": 805, "text": ""}, {"no": 806, "text": ""}, {"no": 807, "text": ""}, {"no": 808, "text": ""}, {"no": 809, "text": ""}, {"no": 810, "text": ""}, {"no": 811, "text": ""}, {"no": 812, "text": ""}, {"no": 813, "text": ""}, {"no": 814, "text": ""}, {"no": 815, "text": ""}, {"no": 816, "text": ""}, {"no": 817, "text": ""}, {"no": 818, "text": ""}, {"no": 819, "text": ""}, {"no": 820, "text": ""}, {"no": 821, "text": ""}, {"no": 822, "text": ""}, {"no": 823, "text": ""}, {"no": 824, "text": ""}, {"no": 825, "text": ""}, {"no": 826, "text": ""}, {"no": 827, "text": ""}, {"no": 828, "text": ""}, {"no": 829, "text": ""}, {"no": 830, "text": ""}, {"no": 831, "text": ""}, {"no": 832, "text": ""}, {"no": 833, "text": ""}, {"no": 834, "text": ""}, {"no": 835, "text": ""}, {"no": 836, "text": ""}, {"no": 837, "text": ""}, {"no": 838, "text": ""}, {"no": 839, "text": ""}, {"no": 840, "text": ""}, {"no": 841, "text": ""}, {"no": 842, "text": ""}, {"no": 843, "text": ""}, {"no": 844, "text": ""}, {"no": 845, "text": ""}, {"no": 846, "text": ""}, {"no": 847, "text": ""}, {"no": 848, "text": ""}, {"no": 849, "text": ""}, {"no": 850, "text": ""}, {"no": 851, "text": ""}, {"no": 852, "text": ""}, {"no": 853, "text": ""}, {"no": 854, "text": ""}, {"no": 855, "text": ""}, {"no": 856, "text": ""}, {"no": 857, "text": ""}, {"no": 858, "text": ""}, {"no": 859, "text": ""}, {"no": 860, "text": ""}, {"no": 861, "text": ""}, {"no": 862, "text": ""}, {"no": 863, "text": ""}, {"no": 864, "text": ""}, {"no": 865, "text": ""}, {"no": 866, "text": ""}, {"no": 867, "text": ""}, {"no": 868, "text": ""}, {"no": 869, "text": ""}, {"no": 870, "text": ""}, {"no": 871, "text": ""}, {"no": 872, "text": ""}, {"no": 873, "text": ""}, {"no": 874, "text": ""}, {"no": 875, "text": ""}, {"no": 876, "text": ""}, {"no": 877, "text": ""}, {"no": 878, "text": ""}, {"no": 879, "text": ""}, {"no": 880, "text": ""}, {"no": 881, "text": ""}, {"no": 882, "text": ""}, {"no": 883, "text": ""}, {"no": 884, "text": ""}, {"no": 885, "text": ""}, {"no": 886, "text": ""}, {"no": 887, "text": ""}, {"no": 888, "text": ""}, {"no": 889, "text": ""}, {"no": 890, "text": ""}, {"no": 891, "text": ""}, {"no": 892, "text": ""}, {"no": 893, "text": ""}, {"no": 894, "text": ""}, {"no": 895, "text": ""}, {"no": 896, "text": ""}, {"no": 897, "text": ""}, {"no": 898, "text": ""}, {"no": 899, "text": ""}, {"no": 900, "text": ""}, {"no": 901, "text": ""}, {"no": 902, "text": ""}, {"no": 903, "text": ""}, {"no": 904, "text": ""}, {"no": 905, "text": ""}, {"no": 906, "text": ""}, {"no": 907, "text": ""}, {"no": 908, "text": ""}, {"no": 909, "text": ""}, {"no": 910, "text": ""}, {"no": 911, "text": ""}, {"no": 912, "text": ""}, {"no": 913, "text": ""}, {"no": 914, "text": ""}, {"no": 915, "text": ""}, {"no": 916, "text": ""}, {"no": 917, "text": ""}, {"no": 918, "text": ""}, {"no": 919, "text": ""}, {"no": 920, "text": ""}, {"no": 921, "text": ""}, {"no": 922, "text": ""}, {"no": 923, "text": ""}, {"no": 924, "text": ""}, {"no": 925, "text": ""}, {"no": 926, "text": ""}, {"no": 927, "text": ""}, {"no": 928, "text": ""}, {"no": 929, "text": ""}, {"no": 930, "text": ""}, {"no": 931, "text": ""}, {"no": 932, "text": ""}, {"no": 933, "text": ""}, {"no": 934, "text": ""}, {"no": 935, "text": ""}, {"no": 936, "text": ""}, {"no": 937, "text": ""}, {"no": 938, "text": ""}, {"no": 939, "text": ""}, {"no": 940, "text": ""}, {"no": 941, "text": ""}, {"no": 942, "text": ""}, {"no": 943, "text": ""}, {"no": 944, "text": ""}, {"no": 945, "text": ""}, {"no": 946, "text": ""}, {"no": 947, "text": ""}, {"no": 948, "text": ""}, {"no": 949, "text": ""}, {"no": 950, "text": ""}, {"no": 951, "text": ""}, {"no": 952, "text": ""}, {"no": 953, "text": ""}, {"no": 954, "text": ""}, {"no": 955, "text": ""}, {"no": 956, "text": ""}, {"no": 957, "text": ""}, {"no": 958, "text": ""}, {"no": 959, "text": ""}, {"no": 960, "text": ""}, {"no": 961, "text": ""}, {"no": 962, "text": ""}, {"no": 963, "text": ""}, {"no": 964, "text": ""}, {"no": 965, "text": ""}, {"no": 966, "text": ""}, {"no": 967, "text": ""}, {"no": 968, "text": ""}, {"no": 969, "text": ""}, {"no": 970, "text": ""}, {"no": 971, "text": ""}, {"no": 972, "text": ""}, {"no": 973, "text": ""}, {"no": 974, "text": ""}, {"no": 975, "text": ""}, {"no": 976, "text": ""}, {"no": 977, "text": ""}, {"no": 978, "text": ""}, {"no": 979, "text": ""}, {"no": 980, "text": ""}, {"no": 981, "text": ""}, {"no": 982, "text": ""}, {"no": 983, "text": ""}, {"no": 984, "text": ""}, {"no": 985, "text": ""}, {"no": 986, "text": ""}, {"no": 987, "text": ""}, {"no": 988, "text": ""}, {"no": 989, "text": ""}, {"no": 990, "text": ""}, {"no": 991, "text": ""}, {"no": 992, "text": ""}, {"no": 993, "text": ""}, {"no": 994, "text": ""}, {"no": 995, "text": ""}, {"no": 996, "text": ""}, {"no": 997, "text": ""}, {"no": 998, "text": ""}, {"no": 999, "text": ""}], "media": {"199": {"card": "21300350", "sq": "21295932"}, "203": {"card": "21300349", "sq": "21295921"}, "209": {"card": "21300348", "sq": "21295909"}, "202": {"card": "21300347", "sq": "21295923"}, "201": {"card": "21300346", "sq": "21295926"}, "200": {"card": "21300345", "sq": "21295930"}, "197": {"card": "21300344", "sq": "21295936"}, "198": {"card": "21300343", "sq": "21295933"}, "196": {"card": "21300342", "sq": "21295938"}, "195": {"card": "21300287"}, "193": {"card": "21300289"}, "194": {"card": "21300288"}, "192": {"card": "21300290"}, "189": {"card": "21300293"}, "187": {"card": "21300295"}, "190": {"card": "21300292"}, "191": {"card": "21300291"}, "188": {"card": "21300294"}, "186": {"card": "21300296"}, "211": {"card": "21300331", "sq": "21295904"}, "207": {"card": "21300330", "sq": "21295913"}, "185": {"card": "21300297"}, "184": {"card": "21300298"}, "183": {"card": "21300301"}, "204": {"sq": "21295919"}, "205": {"sq": "21295917"}, "206": {"sq": "21295915"}, "208": {"sq": "21295911"}, "210": {"sq": "21295906"}, "212": {"sq": "21295902", "card": "21288098"}, "213": {"sq": "21295901", "card": "21288099"}, "214": {"sq": "21295898", "card": "21288102"}, "215": {"sq": "21295897"}, "216": {"card": "21288101"}, "250": {"card": "21295893"}, "256": {"card": "21295891"}, "255": {"card": "21295889"}, "257": {"card": "21295887"}, "259": {"card": "21295884"}, "253": {"card": "21295882"}, "269": {"sq": "21295880"}, "254": {"card": "21295878"}, "268": {"sq": "21295876"}, "252": {"card": "21295873"}, "251": {"card": "21295872"}, "267": {"sq": "21295869"}, "266": {"sq": "21295868"}, "265": {"sq": "21295866"}, "264": {"sq": "21295864"}, "262": {"sq": "21295862"}, "263": {"sq": "21295860"}, "261": {"sq": "21295858"}, "277": {"card": "21295856"}, "279": {"card": "21295854"}, "260": {"sq": "21295852"}, "278": {"card": "21295851"}, "275": {"card": "21295844"}, "272": {"card": "21295842"}, "271": {"card": "21295839"}, "270": {"card": "21295837"}, "273": {"card": "21295835"}, "40": {"card": "21288980"}, "38": {"card": "21288979"}, "36": {"card": "21288978"}, "39": {"card": "21288976"}, "37": {"card": "21288975"}, "114": {"card": "21257163", "sq": "21257157"}, "113": {"sq": "21257160", "card": "21257154"}, "112": {"card": "21257158", "sq": "21257155"}, "109": {"card": "21257153", "sq": "21257147"}, "107": {"card": "21257152", "sq": "21257149"}, "108": {"card": "21257151", "sq": "21257150"}, "106": {"card": "21257146", "sq": "21257145"}, "103": {"sq": "21257143", "card": "21257139"}, "104": {"card": "21257142", "sq": "21257141"}, "102": {"card": "21257140", "sq": "21257135"}, "101": {"sq": "21257137", "card": "21257133"}, "99": {"card": "21257136", "sq": "21257130"}, "100": {"sq": "21257134", "card": "21257132"}, "97": {"sq": "21257131", "card": "21257128"}, "98": {"sq": "21257129", "card": "21257127"}, "96": {"card": "21257126", "sq": "21257122"}, "95": {"sq": "21257125", "card": "21257124"}, "94": {"sq": "21257121", "card": "21257120"}, "93": {"card": "21257118", "sq": "21257114"}, "78": {"card": "21257117"}, "82": {"card": "21257116"}, "79": {"card": "21257115"}, "88": {"card": "21257113"}, "80": {"card": "21257112"}, "70": {"sq": "21214638"}, "65": {"sq": "21214613", "card": "21543680"}, "64": {"sq": "21214612"}, "62": {"sq": "21214611", "card": "21543684"}, "67": {"sq": "21214610"}, "68": {"sq": "21214609", "card": "21543691"}, "63": {"sq": "21214608", "card": "21543689"}, "66": {"sq": "21214607", "card": "21543682"}, "69": {"sq": "21214606", "card": "21543687"}, "87": {"card": "21118108"}, "92": {"sq": "21118107"}, "86": {"card": "21118105"}, "81": {"card": "21118104"}, "73": {"sq": "21118103"}, "76": {"sq": "21118102"}, "77": {"sq": "21118101"}, "72": {"sq": "21118099"}, "58": {"sq": "21118098"}, "59": {"sq": "21118097"}, "50": {"sq": "21118096"}, "52": {"sq": "21118095"}, "54": {"sq": "21118094"}, "53": {"sq": "21118093"}, "51": {"sq": "21118092"}, "49": {"sq": "21118091"}, "46": {"sq": "21118090"}, "48": {"sq": "21118089"}, "45": {"sq": "21118088"}, "43": {"sq": "21118087"}, "42": {"sq": "21118086"}, "35": {"card": "21118085"}, "30": {"card": "21118079"}, "28": {"card": "21118077"}, "27": {"card": "21118076"}, "24": {"card": "21118073"}, "10": {"card": "21112942"}, "9": {"card": "21112940"}, "8": {"card": "21112936"}, "5": {"card": "21112934"}, "2": {"card": "21112932"}, "4": {"card": "21112929"}, "1": {"card": "21112926"}, "3": {"card": "21112923"}, "26": {"card": "21112919"}, "32": {"card": "21112917"}, "33": {"card": "21112915"}, "31": {"card": "21112914"}, "29": {"card": "21112913"}, "25": {"card": "21112912"}, "23": {"card": "21112911"}, "21": {"card": "21112910"}, "22": {"card": "21112907"}, "20": {"card": "21112906"}, "19": {"card": "21112905"}, "18": {"card": "21112904"}, "16": {"card": "21112903"}, "12": {"card": "21112902"}, "15": {"card": "21112901"}, "17": {"card": "21112900"}, "14": {"card": "21112899"}, "13": {"card": "21112898"}, "340": {"card": "21397519"}, "343": {"card": "21397518"}, "342": {"card": "21397517"}, "338": {"card": "21397516"}, "339": {"card": "21397515"}, "344": {"card": "21397514"}, "335": {"card": "21397513"}, "336": {"card": "21397512"}, "332": {"card": "21397510"}, "333": {"card": "21397509"}, "337": {"card": "21397508"}, "334": {"card": "21397507"}, "328": {"card": "21397506"}, "327": {"card": "21397505"}, "331": {"card": "21397504"}, "330": {"card": "21397503"}, "326": {"card": "21397502"}, "329": {"card": "21397501"}, "321": {"card": "21397500"}, "323": {"card": "21397499"}, "325": {"card": "21397498"}, "322": {"card": "21397497"}, "320": {"card": "21397496"}, "324": {"card": "21397495"}, "316": {"card": "21397494"}, "317": {"card": "21397493"}, "313": {"card": "21397492"}, "318": {"card": "21397490"}, "314": {"card": "21397489"}, "315": {"card": "21397487"}, "310": {"card": "21397486"}, "309": {"card": "21397485"}, "312": {"card": "21397484"}, "311": {"card": "21397483"}, "307": {"card": "21397482"}, "308": {"card": "21397481"}, "305": {"card": "21397480"}, "302": {"card": "21397479"}, "306": {"card": "21397478"}, "301": {"card": "21397477"}, "304": {"card": "21397476"}, "303": {"card": "21397475"}, "347": {"card": "21397474"}, "345": {"card": "21397473"}, "346": {"card": "21397472"}, "348": {"card": "21397471"}, "299": {"card": "21397470"}, "300": {"card": "21397468"}, "282": {"card": "21397448"}, "284": {"card": "21397447"}, "283": {"card": "21397446"}, "449": {"card": "21397445"}, "281": {"card": "21397444"}, "280": {"card": "21397443"}, "446": {"card": "21397442"}, "448": {"card": "21397441"}, "447": {"card": "21397440"}, "441": {"card": "21397439"}, "445": {"card": "21397438"}, "439": {"card": "21397437"}, "442": {"card": "21397436"}, "443": {"card": "21397435"}, "440": {"card": "21397434"}, "437": {"card": "21397433"}, "436": {"card": "21397432"}, "434": {"card": "21397431"}, "438": {"card": "21397430"}, "435": {"card": "21397429"}, "433": {"card": "21397428"}, "431": {"card": "21397427"}, "429": {"card": "21397426"}, "427": {"card": "21397425"}, "430": {"card": "21397424"}, "428": {"card": "21397423"}, "432": {"card": "21397422"}, "426": {"card": "21397421"}, "421": {"card": "21397420"}, "420": {"card": "21397419"}, "425": {"card": "21397418"}, "423": {"card": "21397417"}, "424": {"card": "21397416"}, "418": {"card": "21397415"}, "415": {"card": "21397414"}, "414": {"card": "21397413"}, "416": {"card": "21397412"}, "419": {"card": "21397411"}, "417": {"card": "21397410"}, "410": {"card": "21397409"}, "408": {"card": "21397408"}, "413": {"card": "21397407"}, "411": {"card": "21397406"}, "412": {"card": "21397405"}, "409": {"card": "21397404"}, "402": {"card": "21397402"}, "406": {"card": "21397401"}, "405": {"card": "21397400"}, "407": {"card": "21397399"}, "404": {"card": "21397398"}, "403": {"card": "21397397"}, "397": {"card": "21397396"}, "396": {"card": "21397395"}, "398": {"card": "21397394"}, "399": {"card": "21397393"}, "401": {"card": "21397392"}, "400": {"card": "21397391"}, "391": {"card": "21397390"}, "390": {"card": "21397389"}, "395": {"card": "21397388"}, "392": {"card": "21397387"}, "394": {"card": "21397386"}, "393": {"card": "21397385"}, "386": {"card": "21397383"}, "384": {"card": "21397382"}, "387": {"card": "21397381"}, "388": {"card": "21397380"}, "389": {"card": "21397379"}, "385": {"card": "21397378"}, "381": {"card": "21397377"}, "382": {"card": "21397376"}, "379": {"card": "21397375"}, "378": {"card": "21397374"}, "383": {"card": "21397373"}, "380": {"card": "21397372"}, "374": {"card": "21397371"}, "373": {"card": "21397370"}, "377": {"card": "21397369"}, "372": {"card": "21397368"}, "375": {"card": "21397367"}, "376": {"card": "21397365"}, "370": {"card": "21397364"}, "368": {"card": "21397363"}, "369": {"card": "21397362"}, "366": {"card": "21397361"}, "371": {"card": "21397360"}, "367": {"card": "21397359"}, "363": {"card": "21397358"}, "364": {"card": "21397357"}, "362": {"card": "21397356"}, "360": {"card": "21397355"}, "361": {"card": "21397354"}, "365": {"card": "21397353"}, "358": {"card": "21397352"}, "353": {"card": "21397351"}, "357": {"card": "21397350"}, "355": {"card": "21397349"}, "359": {"card": "21397348"}, "356": {"card": "21397347"}, "352": {"card": "21397346"}, "349": {"card": "21397345"}, "350": {"card": "21397344"}, "351": {"card": "21397343"}, "297": {"card": "21397342"}, "298": {"card": "21397341"}, "294": {"card": "21397340"}, "292": {"card": "21397339"}, "295": {"card": "21397338"}, "296": {"card": "21397337"}, "291": {"card": "21397336"}, "293": {"card": "21397335"}, "290": {"card": "21397334"}, "286": {"card": "21397333"}, "285": {"card": "21397332"}, "287": {"card": "21397331"}, "289": {"card": "21397330"}, "288": {"card": "21397329"}, "460": {"card": "21409111"}, "466": {"card": "21409110"}, "459": {"card": "21409109"}, "461": {"card": "21409108"}, "457": {"card": "21409107"}, "455": {"card": "21409106"}, "454": {"card": "21409105"}, "453": {"card": "21409104"}, "458": {"card": "21409103"}, "456": {"card": "21409102"}, "450": {"card": "21409101"}, "470": {"card": "21409100"}, "469": {"card": "21409099"}, "452": {"card": "21409098"}, "471": {"card": "21409097"}, "451": {"card": "21409096"}, "468": {"card": "21409095"}, "464": {"card": "21409094"}, "463": {"card": "21409093"}, "462": {"card": "21409092"}, "465": {"card": "21409091"}, "467": {"card": "21409090"}, "578": {"card": "21541864"}, "579": {"card": "21541863"}, "580": {"card": "21541862"}, "572": {"card": "21541861"}, "581": {"card": "21541860"}, "583": {"card": "21541859"}, "573": {"card": "21541858"}, "577": {"card": "21541857"}, "582": {"card": "21541856"}, "576": {"card": "21541855"}, "574": {"card": "21541854"}, "575": {"card": "21541853"}, "761": {"card": "21600497"}, "755": {"card": "21600494"}, "759": {"card": "21600493"}, "758": {"card": "21600492"}, "756": {"card": "21600491"}, "760": {"card": "21600490"}, "757": {"card": "21600489"}, "752": {"card": "21600488"}, "754": {"card": "21600487"}, "751": {"card": "21600485"}, "750": {"card": "21600483"}, "749": {"card": "21600482"}, "753": {"card": "21600481"}, "745": {"card": "21600480"}, "744": {"card": "21600479"}, "746": {"card": "21600478"}, "747": {"card": "21600477"}, "748": {"card": "21600474"}, "743": {"card": "21600472"}, "738": {"card": "21600469"}, "739": {"card": "21600468"}, "737": {"card": "21600467"}, "740": {"card": "21600466"}, "741": {"card": "21600465"}, "742": {"card": "21600464"}, "731": {"card": "21600463"}, "732": {"card": "21600462"}, "734": {"card": "21600461"}, "733": {"card": "21600460"}, "736": {"card": "21600459"}, "735": {"card": "21600458"}, "730": {"card": "21600457"}, "727": {"card": "21600456"}, "729": {"card": "21600454"}, "725": {"card": "21600453"}, "728": {"card": "21600452"}, "726": {"card": "21600451"}, "723": {"card": "21600450"}, "721": {"card": "21600449"}, "720": {"card": "21600448"}, "724": {"card": "21600447"}, "719": {"card": "21600446"}, "722": {"card": "21600445"}, "715": {"card": "21600444"}, "717": {"card": "21600443"}, "713": {"card": "21600442"}, "718": {"card": "21600441"}, "714": {"card": "21600440"}, "716": {"card": "21600439"}, "708": {"card": "21600438"}, "709": {"card": "21600437"}, "710": {"card": "21600436"}, "712": {"card": "21600434"}, "711": {"card": "21600433"}, "707": {"card": "21600432"}, "669": {"sq": "21600431"}, "702": {"card": "21600430"}, "705": {"card": "21600429"}, "704": {"card": "21600428"}, "703": {"card": "21600427"}, "706": {"card": "21600425"}, "701": {"card": "21600756"}, "696": {"card": "21600755"}, "671": {"card": "21600754"}, "672": {"card": "21600753"}, "676": {"card": "21600751"}, "692": {"card": "21600750"}, "688": {"card": "21600748"}, "685": {"card": "21600747"}, "697": {"card": "21600746"}, "694": {"card": "21600745"}, "698": {"card": "21600744"}, "699": {"card": "21600742"}, "700": {"card": "21600741"}, "695": {"card": "21600740"}, "687": {"card": "21600739"}, "691": {"card": "21600738"}, "686": {"card": "21600736"}, "690": {"card": "21600735"}, "693": {"card": "21600734"}, "689": {"card": "21600733"}, "679": {"card": "21600731"}, "680": {"card": "21600730"}, "682": {"card": "21600729"}, "684": {"card": "21600728"}, "681": {"card": "21600727"}, "683": {"card": "21600725"}, "678": {"card": "21600724"}, "673": {"card": "21600723"}, "677": {"card": "21600722"}, "670": {"card": "21600721"}, "674": {"card": "21600720"}, "675": {"card": "21600718"}, "847": {"card": "21612301"}, "846": {"card": "21612300"}, "844": {"card": "21612299"}, "843": {"card": "21612298"}, "842": {"card": "21612297"}, "845": {"card": "21612296"}, "841": {"card": "21612295"}, "840": {"card": "21612294"}, "838": {"card": "21612293"}, "837": {"card": "21612292"}, "839": {"card": "21612291"}, "836": {"card": "21612290"}, "834": {"card": "21612289"}, "835": {"card": "21612288"}, "828": {"card": "21612287"}, "833": {"card": "21612286"}, "831": {"card": "21612285"}, "832": {"card": "21612284"}, "830": {"card": "21612283"}, "829": {"card": "21612282"}, "825": {"card": "21612281"}, "824": {"card": "21612280"}, "826": {"card": "21612279"}, "827": {"card": "21612278"}, "822": {"card": "21612277"}, "823": {"card": "21612276"}, "817": {"card": "21612275"}, "821": {"card": "21612274"}, "820": {"card": "21612273"}, "818": {"card": "21612272"}, "816": {"card": "21612271"}, "819": {"card": "21612270"}, "812": {"card": "21612269"}, "810": {"card": "21612266"}, "811": {"card": "21612265"}, "815": {"card": "21612264"}, "814": {"card": "21612263"}, "813": {"card": "21612262"}, "806": {"card": "21612261"}, "807": {"card": "21612260"}, "808": {"card": "21612259"}, "809": {"card": "21612258"}, "804": {"card": "21612257"}, "805": {"card": "21612256"}, "799": {"card": "21612255"}, "802": {"card": "21612254"}, "801": {"card": "21612253"}, "798": {"card": "21612252"}, "800": {"card": "21612251"}, "803": {"card": "21612250"}, "793": {"card": "21612249"}, "795": {"card": "21612247"}, "796": {"card": "21612246"}, "797": {"card": "21612245"}, "792": {"card": "21612244"}, "794": {"card": "21612243"}, "786": {"card": "21612242"}, "788": {"card": "21612241"}, "791": {"card": "21612240"}, "789": {"card": "21612239"}, "787": {"card": "21612238"}, "790": {"card": "21612237"}, "783": {"card": "21612236"}, "784": {"card": "21612235"}, "781": {"card": "21612234"}, "780": {"card": "21612233"}, "785": {"card": "21612232"}, "782": {"card": "21612231"}, "778": {"card": "21612230"}, "775": {"card": "21612229"}, "779": {"card": "21612228"}, "776": {"card": "21612227"}, "774": {"card": "21612226"}, "777": {"card": "21612225"}, "773": {"card": "21612224"}, "768": {"card": "21612223"}, "771": {"card": "21612222"}, "772": {"card": "21612221"}, "769": {"card": "21612220"}, "770": {"card": "21612219"}, "763": {"card": "21612218"}, "765": {"card": "21612217"}, "762": {"card": "21612216"}, "767": {"card": "21612215"}, "766": {"card": "21612214"}, "764": {"card": "21612213"}, "1040": {"card": "21667290"}, "1042": {"card": "21667287"}, "1036": {"card": "21667283"}, "1038": {"card": "21667282"}, "1035": {"card": "21667281"}, "1037": {"card": "21667280"}, "1039": {"card": "21667279"}, "1034": {"card": "21667278"}, "1033": {"card": "21667277"}, "1048": {"card": "21667276"}, "1050": {"card": "21667275"}, "1051": {"card": "21667274"}, "1032": {"card": "21667273"}, "1049": {"card": "21667272"}, "1043": {"card": "21667271"}, "1046": {"card": "21667270"}, "1044": {"card": "21667269"}, "1041": {"card": "21667268"}, "1047": {"card": "21667267"}, "1045": {"card": "21667265"}, "977": {"card": "21659293"}, "975": {"card": "21659290"}, "976": {"card": "21659286"}, "979": {"card": "21659283"}, "970": {"card": "21659281"}, "973": {"card": "21659279"}, "974": {"card": "21659277"}, "972": {"card": "21659275"}, "971": {"card": "21659274"}, "978": {"card": "21659273"}, "1002": {"card": "21659271"}, "1003": {"card": "21659270"}, "1005": {"card": "21659269"}, "1004": {"card": "21659268"}, "1001": {"card": "21659267"}, "1021": {"card": "21659266"}, "1024": {"card": "21659265"}, "1000": {"card": "21659264"}, "1023": {"card": "21659263"}, "1022": {"card": "21659262"}, "1016": {"card": "21659261"}, "1020": {"card": "21659260"}, "1017": {"card": "21659259"}, "1018": {"card": "21659258"}, "1015": {"card": "21659257"}, "1019": {"card": "21659256"}, "1013": {"card": "21659255"}, "1014": {"card": "21659254"}, "1011": {"card": "21659253"}, "1010": {"card": "21659252"}, "1012": {"card": "21659251"}, "1009": {"card": "21659250"}, "1031": {"card": "21659249"}, "1030": {"card": "21659248"}, "1008": {"card": "21659247"}, "1006": {"card": "21659246"}, "1029": {"card": "21659245"}, "1007": {"card": "21659244"}, "996": {"card": "21659243"}, "997": {"card": "21659242"}, "1028": {"card": "21659241"}, "1026": {"card": "21659240"}, "1025": {"card": "21659239"}, "1027": {"card": "21659238"}, "991": {"card": "21659237"}, "992": {"card": "21659236"}, "990": {"card": "21659235"}, "989": {"card": "21659234"}, "995": {"card": "21659232"}, "987": {"card": "21659231"}, "986": {"card": "21659230"}, "984": {"card": "21659229"}, "983": {"card": "21659228"}, "981": {"card": "21659225"}, "982": {"card": "21659222"}, "985": {"card": "21659220"}, "993": {"card": "21659218"}, "998": {"card": "21659216"}, "994": {"card": "21659215"}, "980": {"card": "21659212"}, "999": {"card": "21659209"}, "988": {"card": "21659207"}, "967": {"card": "21659037"}, "959": {"card": "21659036"}, "961": {"card": "21659035"}, "965": {"card": "21659034"}, "960": {"card": "21659033"}, "966": {"card": "21659032"}, "968": {"card": "21659031"}, "969": {"card": "21659029"}, "963": {"card": "21659028"}, "958": {"card": "21659027"}, "962": {"card": "21659026"}, "964": {"card": "21659025"}, "957": {"card": "21667629"}, "954": {"card": "21667628"}, "956": {"card": "21667627"}, "955": {"card": "21667626"}, "951": {"card": "21667625"}, "948": {"card": "21667624"}, "950": {"card": "21667621"}, "952": {"card": "21667619"}, "953": {"card": "21667616"}, "949": {"card": "21667612"}}, "groups": [{"paramCode": "ks_Cac_10", "campaignId": "32444865", "adGroupId": "451324015", "groupName": "【ks_Cac_10】電話番号全期間×Card画像固定テキスト検証_0605"}, {"paramCode": "jv_Cac_55", "campaignId": "32455449", "adGroupId": "451396524", "groupName": "【jv_Cac_55】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "jv_Cac_56", "campaignId": "32455450", "adGroupId": "451396526", "groupName": "【jv_Cac_56】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "jv_Cac_57", "campaignId": "32455451", "adGroupId": "451396528", "groupName": "【jv_Cac_57】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "jv_Cac_58", "campaignId": "32455452", "adGroupId": "451396530", "groupName": "【jv_Cac_58】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "jv_Cac_59", "campaignId": "32455453", "adGroupId": "451396532", "groupName": "【jv_Cac_59】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "jv_Cac_60", "campaignId": "32455454", "adGroupId": "451396534", "groupName": "【jv_Cac_60】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "jv_Cac_61", "campaignId": "32455455", "adGroupId": "451396536", "groupName": "【jv_Cac_61】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "jv_Cac_62", "campaignId": "32455456", "adGroupId": "451396538", "groupName": "【jv_Cac_62】TELスマタゲ_カード画像検証_20260612"}, {"paramCode": "", "campaignId": "32460012", "adGroupId": "451423856", "groupName": "jv_Cac_63】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "", "campaignId": "32460012", "adGroupId": "451423956", "groupName": "jv_Cac_64】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "", "campaignId": "32460015", "adGroupId": "451423873", "groupName": "jv_Cac_65】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "", "campaignId": "32460015", "adGroupId": "451424000", "groupName": "jv_Cac_66】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "", "campaignId": "32460015", "adGroupId": "451424002", "groupName": "jv_Cac_67】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "", "campaignId": "32460018", "adGroupId": "451423886", "groupName": "jv_Cac_68】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "", "campaignId": "32460018", "adGroupId": "451424006", "groupName": "jv_Cac_69】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "", "campaignId": "32460021", "adGroupId": "451423902", "groupName": "jv_Cac_70】TELスマタゲ_カード画像検証_20260616"}, {"paramCode": "jv_Cac_71", "campaignId": "32460342", "adGroupId": "451424838", "groupName": "【jv_Cac_71】ブロード年齢ちょい広_カード画像検証_20260616"}, {"paramCode": "jv_Cac_72", "campaignId": "32460342", "adGroupId": "451424837", "groupName": "【jv_Cac_72】ブロード年齢ちょい広_カード画像検証_20260616"}, {"paramCode": "jv_Cac_73", "campaignId": "32460345", "adGroupId": "451424847", "groupName": "【jv_Cac_73】ブロード年齢ちょい広_カード画像検証_20260616"}, {"paramCode": "jv_Cac_74", "campaignId": "32460345", "adGroupId": "451424846", "groupName": "【jv_Cac_74】ブロード年齢ちょい広_カード画像検証_20260616"}, {"paramCode": "jv_Cac_75", "campaignId": "32460345", "adGroupId": "451424845", "groupName": "【jv_Cac_75】ブロード年齢ちょい広_カード画像検証_20260616"}, {"paramCode": "jv_Cac_76", "campaignId": "32460344", "adGroupId": "451424843", "groupName": "【jv_Cac_76】ブロード年齢ちょい広_カード像検証_20260616"}, {"paramCode": "jv_Cac_77", "campaignId": "32460344", "adGroupId": "451424842", "groupName": "【jv_Cac_77】ブロード年齢ちょい広_カード画像検証_20260616"}, {"paramCode": "jv_Cac_78", "campaignId": "32460343", "adGroupId": "451424840", "groupName": "【jv_Cac_78】ブロード年齢ちょい広_カード像検証_20260616"}, {"paramCode": "jv_Cac_83", "campaignId": "32460946", "adGroupId": "451428809", "groupName": "【jv_Cac_83】女性_好調まとめ_サンクス類似10_20260616"}, {"paramCode": "jv_Cac_82", "campaignId": "32460946", "adGroupId": "451428807", "groupName": "【jv_Cac_82】女性_好調まとめ_サンクス類似10_20260616"}, {"paramCode": "jv_Cac_79", "campaignId": "32460932", "adGroupId": "451428754", "groupName": "【jv_Cac_79】男性_好調まとめ_サンクス類似10_20260616"}, {"paramCode": "jv_Cac_80", "campaignId": "32460932", "adGroupId": "451428721", "groupName": "【jv_Cac_80】男性_好調まとめ_サンクス類似10_20260616"}, {"paramCode": "jv_Cac_86", "campaignId": "32462465", "adGroupId": "451435537", "groupName": "【jv_Cac_86】男性_好調フォーマット画像検証_サンクス類似10_20260617"}, {"paramCode": "jv_Cac_87", "campaignId": "32462465", "adGroupId": "451435536", "groupName": "【jv_Cac_87】男性_好調フォーマット画像検証_サンクス類似10_20260617"}, {"paramCode": "jv_Cac_0", "campaignId": "32463816", "adGroupId": "451453510", "groupName": "【jv_Cac_0】DEMO_コピー用"}, {"paramCode": "jv_Cac_48", "campaignId": "32449955", "adGroupId": "451453944", "groupName": "【jv_Cac_48】DEMO_画像固定テキスト検証画像30_20260618"}, {"paramCode": "jv_Cac_49", "campaignId": "32449955", "adGroupId": "451453946", "groupName": "【jv_Cac_49】DEMO_画像固定テキスト検証画像30_20260618"}, {"paramCode": "jv_Cac_50", "campaignId": "32449955", "adGroupId": "451453948", "groupName": "【jv_Cac_50】DEMO_画像固定テキスト検証画像30_20260618"}, {"paramCode": "jv_Cac_51", "campaignId": "32449955", "adGroupId": "451453950", "groupName": "【jv_Cac_51】DEMO_画像固定テキスト検証画像30_20260618"}, {"paramCode": "jv_Cac_91", "campaignId": "32469296", "adGroupId": "451492129", "groupName": "【jv_Cac_91】男性_画像固定テキスト検証_サンクス類似10_20260622"}, {"paramCode": "jv_Cac_90", "campaignId": "32469296", "adGroupId": "451492095", "groupName": "【jv_Cac_90】男性_画像固定テキスト検証_サンクス類似10_20260622"}, {"paramCode": "jv_Cac_92", "campaignId": "32486920", "adGroupId": "451538014", "groupName": "【jv_Cac_92】新規検証_BGスマタゲ10_20260624"}, {"paramCode": "jv_Cac_93", "campaignId": "32486920", "adGroupId": "451538012", "groupName": "【jv_Cac_93】新規検証_BGスマタゲ10_20260624"}, {"paramCode": "jv_Cac_94", "campaignId": "32486920", "adGroupId": "451537987", "groupName": "【jv_Cac_94】新規検証_BGスマタゲ10_20260624"}, {"paramCode": "jv_Cac_95", "campaignId": "32505425", "adGroupId": "451647760", "groupName": "【jv_Cac_95】カード検証_男女CPA好調SQ→CD_サンクス類似10_20260703"}, {"paramCode": "jv_Cac_96", "campaignId": "32505660", "adGroupId": "451649708", "groupName": "【jv_Cac_96】カード検証_男女CPA好調SQ→CD_サンクス類似10_20260703"}, {"paramCode": "jv_Cac_97", "campaignId": "32505689", "adGroupId": "451649845", "groupName": "【jv_Cac_97】カード検証_男女CPA好調SQ→CD_サンクス類似10_20260703"}, {"paramCode": "jv_Cac_98", "campaignId": "32505690", "adGroupId": "451649847", "groupName": "【jv_Cac_98】カード検証_男女CPA好調SQ→CD_サンクス類似10_20260703"}, {"paramCode": "jv_Cac_99", "campaignId": "32505691", "adGroupId": "451649849", "groupName": "【jv_Cac_99】カード検証_男女CPA好調SQ→CD_BGスマ_20260703"}, {"paramCode": "jv_Cac_100", "campaignId": "32505695", "adGroupId": "451649876", "groupName": "【jv_Cac_100】カード検証_男女CPA好調SQ→CD_BGスマ_20260703"}, {"paramCode": "jv_Cac_101", "campaignId": "32505698", "adGroupId": "451649888", "groupName": "【jv_Cac_101】カード検証_男女CPA好調SQ→CD_BGスマ_20260703"}, {"paramCode": "jv_Cac_81", "campaignId": "32460932", "adGroupId": "451650333", "groupName": "【jv_Cac_81】男性_好調まとめ_サンクス類似10_20260703"}, {"paramCode": "jv_Cac_103", "campaignId": "32505842", "adGroupId": "451650396", "groupName": "【jv_Cac_103】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_102", "campaignId": "32505839", "adGroupId": "451650385", "groupName": "【jv_Cac_102】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_110", "campaignId": "32505872", "adGroupId": "451650476", "groupName": "【jv_Cac_110】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_109", "campaignId": "32505871", "adGroupId": "451650474", "groupName": "【jv_Cac_109】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_108", "campaignId": "32505869", "adGroupId": "451650470", "groupName": "【jv_Cac_108】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_107", "campaignId": "32505865", "adGroupId": "451650460", "groupName": "【jv_Cac_107】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_106", "campaignId": "32505863", "adGroupId": "451650456", "groupName": "【jv_Cac_106】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_105", "campaignId": "32505854", "adGroupId": "451650438", "groupName": "【jv_Cac_105】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_104", "campaignId": "32505844", "adGroupId": "451650404", "groupName": "【jv_Cac_104】カード検証_男女CPA好調テキスト×文字商品_BGスマ_20260703"}, {"paramCode": "jv_Cac_111", "campaignId": "32507244", "adGroupId": "451655449", "groupName": "【jv_Cac_111】カード検証_男女CPA好調SQ→CD__BGスマ_20260704"}, {"paramCode": "jv_Cac_112", "campaignId": "32512672", "adGroupId": "451686218", "groupName": "【jv_Cac_112】カード好調まとめ_男女CPA好調CD_LP90日_20260708"}, {"paramCode": "jv_Cac_113", "campaignId": "32512748", "adGroupId": "451686929", "groupName": "【jv_Cac_113】カード好調まとめ_男女CPA好調CD_LP90日_20260708"}, {"paramCode": "jv_Cac_2", "campaignId": "32430712", "adGroupId": "451453894", "groupName": "【jv_Cac_2】入稿用ぶち込み"}, {"paramCode": "jv_Cac_116", "campaignId": "32516630", "adGroupId": "451709964", "groupName": "【jv_Cac_116】テキスト検証_男女好調CD固定_BGスマ_20260710"}, {"paramCode": "jv_Cac_115", "campaignId": "32516625", "adGroupId": "451709954", "groupName": "【jv_Cac_115】テキスト検証_男女好調CD固定_BGスマ_20260710"}, {"paramCode": "jv_Cac_114", "campaignId": "32514816", "adGroupId": "451703205", "groupName": "【jv_Cac_114】テキスト検証_男女好調CD固定_BGスマ_20260710"}, {"paramCode": "jv_Cac_123", "campaignId": "32517824", "adGroupId": "451715117", "groupName": "【jv_Cac_123】テキスト検証_男女好調CD固定_サンクス類似10_20260710"}, {"paramCode": "jv_Cac_122", "campaignId": "32517823", "adGroupId": "451715115", "groupName": "【jv_Cac_122】テキスト検証_男女好調CD固定_サンクス類似10_20260710"}, {"paramCode": "jv_Cac_121", "campaignId": "32517822", "adGroupId": "451715113", "groupName": "【jv_Cac_121】テキスト検証_男女好調CD固定_サンクス類似10_20260710"}, {"paramCode": "jv_Cac_120", "campaignId": "32517821", "adGroupId": "451715111", "groupName": "【jv_Cac_120】テキスト検証_男女好調CD固定_サンクス類似10_20260710"}, {"paramCode": "jv_Cac_119", "campaignId": "32517820", "adGroupId": "451715109", "groupName": "【jv_Cac_119】テキスト検証_男女好調CD固定_サンクス類似10_20260710"}, {"paramCode": "jv_Cac_118", "campaignId": "32517819", "adGroupId": "451715107", "groupName": "【jv_Cac_118】テキスト検証_男女好調CD固定_サンクス類似10_20260710"}, {"paramCode": "jv_Cac_117", "campaignId": "32517808", "adGroupId": "451715075", "groupName": "【jv_Cac_117】テキスト検証_男女好調CD固定_サンクス類似10_20260710"}, {"paramCode": "jv_Cac_141", "campaignId": "32517933", "adGroupId": "451715457", "groupName": "【jv_Cac_141】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_140", "campaignId": "32517932", "adGroupId": "451715455", "groupName": "【jv_Cac_140】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_139", "campaignId": "32517931", "adGroupId": "451715453", "groupName": "【jv_Cac_139】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_138", "campaignId": "32517929", "adGroupId": "451715449", "groupName": "【jv_Cac_138】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_137", "campaignId": "32517928", "adGroupId": "451715447", "groupName": "【jv_Cac_137】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_136", "campaignId": "32517927", "adGroupId": "451715445", "groupName": "【jv_Cac_136】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_135", "campaignId": "32517926", "adGroupId": "451715443", "groupName": "【jv_Cac_135】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_134", "campaignId": "32517925", "adGroupId": "451715437", "groupName": "【jv_Cac_134】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_133", "campaignId": "32517924", "adGroupId": "451715435", "groupName": "【jv_Cac_133】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_132", "campaignId": "32517923", "adGroupId": "451715433", "groupName": "【jv_Cac_132】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_131", "campaignId": "32517922", "adGroupId": "451715431", "groupName": "【jv_Cac_131】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_130", "campaignId": "32517920", "adGroupId": "451715425", "groupName": "【jv_Cac_130】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_129", "campaignId": "32517918", "adGroupId": "451715421", "groupName": "【jv_Cac_129】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_128", "campaignId": "32517917", "adGroupId": "451715419", "groupName": "【jv_Cac_128】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_127", "campaignId": "32517916", "adGroupId": "451715417", "groupName": "【jv_Cac_127】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_126", "campaignId": "32517915", "adGroupId": "451715415", "groupName": "【jv_Cac_126】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_125", "campaignId": "32517913", "adGroupId": "451715411", "groupName": "【jv_Cac_125】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_124", "campaignId": "32517893", "adGroupId": "451715332", "groupName": "【jv_Cac_124】カード検証_男女古風CD_BGスマ_20260710"}, {"paramCode": "jv_Cac_143", "campaignId": "32522613", "adGroupId": "451740726", "groupName": "【jv_Cac_143】文字入りレトロ_男女CD_BGスマ_20260714"}, {"paramCode": "jv_Cac_144", "campaignId": "32522862", "adGroupId": "451742552", "groupName": "【jv_Cac_144】文字入りレトロ_男女CD_BG類似10_20260714"}, {"paramCode": "jv_Cac_145", "campaignId": "32460932", "adGroupId": "451757786", "groupName": "【jv_Cac_145】男性_カード画像検証_サンクス類似10_20260715"}, {"paramCode": "jv_Cac_146", "campaignId": "32460932", "adGroupId": "451759490", "groupName": "【jv_Cac_146】男性_カード画像検証_サンクス類似10_20260715"}, {"paramCode": "jv_Cac_147", "campaignId": "32525231", "adGroupId": "451759756", "groupName": "【jv_Cac_147】ワードショー画像検証_0715"}, {"paramCode": "jv_Cac_148", "campaignId": "32525263", "adGroupId": "451760490", "groupName": "【jv_Cac_148】ワードショー画像検証_男性_BG類似10_0715"}], "sampleAds": [{"format": "Card", "imageNo": "957", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "954", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "956", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "955", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "951", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "948", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "950", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "952", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "953", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}, {"format": "Card", "imageNo": "949", "titleNo": 45, "descNo": 60, "paramCode": "jv_Cac_148", "articleSlug": "jv_C_1"}], "advertiser": "富山常備薬"}</script>
602
688
  <script>
603
689
  "use strict";
604
690
  /* ============================================================
605
691
  YDA入稿プランナー — 単一state / シート・ツリー両ビュー投影
606
692
  ============================================================ */
607
- const RAW = JSON.parse(document.getElementById('embedData').textContent);
693
+ /* 案A: embed廃止。参照データ(refs)・初期プラン・共通設定は外部JSONから起動時に読み込む。
694
+ 同梱デフォルト: アカウント/index.json の先頭アカウント (refs+principal+projectName+baseArticleUrl) と サンプルプラン.json (project+campaigns)。
695
+ 固有名詞はハードコードせず、すべて外部JSON由来にする。 */
696
+ let bundledRefs=null; let bundledPrincipal=''; let bundledProjectName=''; let bundledBaseUrl=''; let samplePlan=null;
697
+ let planMismatchMsg=''; // サンプルプランと現在アカウントの accountId 不一致警告 (持続バナー用)
698
+ function fetchJson(rel){ try{ return fetch(rel,{cache:'no-store'}).then(r=>r.ok?r.json():null).catch(()=>null); }catch(e){ return Promise.resolve(null); } }
608
699
  const LS_KEY = 'yda_planner_state_v1';
609
700
  const HIST_KEY = 'yda_planner_history_v1';
610
701
  const AUTHOR_KEY = 'yda_planner_author';
611
- const SCHEMA_VERSION = 2;
702
+ const SCHEMA_VERSION = 3;
612
703
 
613
704
  /* ---- utils ---- */
614
705
  let _uc = 0;
@@ -618,106 +709,83 @@ const glen = s => [...String(s==null?'':s)].length;
618
709
  const today = () => { const d=new Date(); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0'); };
619
710
  function toast(m){ const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); clearTimeout(t._h); t._h=setTimeout(()=>t.classList.remove('show'),2200); }
620
711
 
621
- /* ---- refs (VLOOKUP代替) ---- */
622
- function buildRefs(){
623
- const titles={}; RAW.titles.forEach(t=>{ if(String(t.text==null?'':t.text).trim()!=='') titles[t.no]=t.text; }); // 空タイトルは除外
624
- const groups={}; RAW.groups.forEach(g=>groups[g.paramCode]={campaignId:g.campaignId,adGroupId:g.adGroupId,groupName:g.groupName});
625
- return {titles, media:RAW.media, groups};
626
- }
712
+ /* ---- refs (VLOOKUP代替) ----
713
+ B-1: refs は6テーブル (titles/descriptions/media/campaigns/adGroups/groups)。
714
+ 参照データの正は「アカウント/<名前>.json」で、フォルダ接続で差し替える (applyAccountJson)。
715
+ 同梱デフォルトアカウントは アカウント/index.json の先頭エントリを起動時に読み込んで決める(固有名詞のハードコードなし)。 */
716
+ let BUNDLED_ACCOUNT_ID = '';
717
+ let BUNDLED_ACCOUNT_NAME = '';
718
+ function emptyRefs(){ return {titles:{}, descriptions:{}, media:{}, campaigns:{}, adGroups:{}, groups:{}}; }
719
+ function validRefs(r){ return !!(r&&r.titles&&r.descriptions&&r.media&&r.groups); }
720
+ function normalizeRefs(r){ const e=emptyRefs(); if(!r) return e; return {titles:r.titles||{}, descriptions:r.descriptions||{}, media:r.media||{}, campaigns:r.campaigns||{}, adGroups:r.adGroups||{}, groups:r.groups||{}}; }
721
+ /* 案A: 同梱デフォルトアカウントの refs を起動時に外部JSONから読み込む(固有名詞ハードコードなし)。
722
+ descriptions は独立テーブル(titlesコピーの暫定を廃止)。 */
723
+ function buildRefs(){ return bundledRefs ? normalizeRefs(bundledRefs) : emptyRefs(); }
627
724
  function defaultProject(){
628
725
  return {
629
- name:'リョウシンJVコンドロイ錠', advertiser: RAW.advertiser||'富山常備薬',
726
+ // 案件名・主体者・ベースURL・accountId は同梱デフォルトアカウント(アカウント/index.json 先頭)由来。未読込時は空
727
+ name: bundledProjectName || '',
728
+ principal: bundledPrincipal,
630
729
  cta:'もっとみる', adType:'レスポンシブ', mainMediaFormat:'画像',
631
730
  deliveryStatus:'オフ', displayUrlFormat:'ドメインのみ表示する',
632
731
  attributionDays:1, biddingStrategyDefault:'tCPA', tcpaDefault:8500,
633
- budgetNew:12000, budgetAsc:50000, baseArticleUrl:'https://iiiyah.com/ab/',
634
- strategyMemo:'',
635
- // 入稿設定 (プランに内包。入稿JSON書き出し時にそのまま使う)
636
- accountId:'', goal:'CONVERSION', startDate:''
732
+ budgetNew:12000, budgetAsc:50000, baseArticleUrl: bundledBaseUrl || '',
733
+ // 入稿設定 (プランに内包。入稿JSON書き出し時にそのまま使う)。accountId/Name は同梱デフォルトアカウント
734
+ accountId:BUNDLED_ACCOUNT_ID, accountName:BUNDLED_ACCOUNT_NAME, goal:'CONVERSION', startDate:''
637
735
  };
638
736
  }
639
- const MODES = {new:'新規', asc:'既存(ASC)', 'asc-add-group':'既存-AG追加', 'asc-add-ad':'既存-広告追加'};
640
- /* 戦略メモのプレースホルダー(YDA運用戦略テンプレ / Web調査ベース・上書き用の記入例) */
641
- const STRAT_PLACEHOLDER = [
642
- '【YDA運用戦略メモ(記入例・上書きしてOK)】',
643
- '',
644
- '■ 目的 / KPI',
645
- ' ・目標CPA 8,500円 / 日予算 新規12,000円・ASC 5〜10万円',
646
- ' ・フェーズ: 立ち上げ(撒いて選ぶ) → 安定(伸びたものに寄せる)',
647
- ' ・検索(YSA)で顕在層 × ディスプレイ(YDA)で再訪・認知の両輪',
648
- '',
649
- '■ ターゲティング(人 × 面)',
650
- ' ・サイトリターゲティング(CVR高) + オーディエンスカテゴリから開始',
651
- ' ・サーチKWターゲティング: 検索で成果の出たKWを流用/検索期間30日/「無料」「とは」除外',
652
- ' ・類似ユーザー: 拡張レベル1〜3で精度重視 → 伸びたら拡張',
653
- ' ・デバイス/性別/年齢/曜日時間で最適化(PCとSPは別媒体として検証)',
654
- ' ・有効リーチ1,000件未満のリストは配信されない点に注意',
655
- '',
656
- '■ クリエイティブ検証(1キャンペーン = 1変数)',
657
- ' ・変える変数は1つだけ(画像 or 訴求 or LP)、他は固定',
658
- ' ・初期は 1:1:10 で撒く → データを見て好調に配信を寄せる',
659
- ' ・レスポンシブは多様なアセットを入稿し組合せを自動最適化',
660
- ' ・厳密比較はA/Bテスト機能(同一グループ内の並列は配信偏りで不正確)',
661
- '',
662
- '■ 入札 / 学習',
663
- ' ・初期はCPCでデータ蓄積 → 条件を満たしたら自動入札へ',
664
- ' ・tCPA導入目安: 広告グループ 直近30日 CV40件以上(CV最大化はCP 直近7日 20件以上)',
665
- ' ・tCPAは直近7日の実績CPA基準/変更は±20%以内/頻繁な変更は禁止',
666
- ' ・「学習中」表示中は設定を触らない(最低2週間・CPAは目標の1.5〜2倍に膨らむ)',
667
- '',
668
- '■ 評価 / 改善サイクル',
669
- ' ・CTR / CPC / CVR に分解し、デバイス・配信面別に確認',
670
- ' ・除外リスト整備/フリークエンシー調整/記事LP改善',
671
- ' ・学習期間(1〜2週)は触らず、本判断は最低1ヶ月',
672
- '',
673
- '■ この検証の仮説・所感(後乗せ)',
674
- ' ・'
675
- ].join('\n');
737
+ /* E-2: mode/MODES 廃止。新規/既存はID解決(isNewCampaign/isNewGroup)で自動判定。新規CPの予算は固定 12,000 円 */
738
+ const NEW_CP_BUDGET = 12000;
676
739
 
677
740
  /* ---- initial state ---- */
678
- function newAd(seed){ return Object.assign({uid:uid('a'), format:'Card', imageNo:'', titleNo:'', descNo:'', articleSlug:'', isBanner:false, adIdOverride:'', desc2:''}, seed||{}); }
741
+ function newAd(seed){ return Object.assign({uid:uid('a'), format:'Card', imageNo:'', titleNo:'', descNo:'', articleSlug:'', adIdOverride:'', desc2:''}, seed||{}); }
679
742
  /* ---- schema v2: バージョンメタ ---- */
680
743
  function defaultMeta(){ return {versionId:null, name:'', memo:'', author:'', platform:'yda', createdAt:null, updatedAt:null}; }
681
- /* v1(メタなし) → v2 へのマイグレーション。v2以降はそのまま既定値マージ。
682
- 旧v2の st.submit (2026-07-23の一時実装) は project の入稿設定に吸収する */
683
- function migrateState(j){
684
- const project=Object.assign({}, j.project);
685
- if(j.submit){
686
- if(j.submit.accountId && !project.accountId) project.accountId=j.submit.accountId;
687
- if(j.submit.goal && !project.goal) project.goal=j.submit.goal;
688
- if(j.submit.startDate && !project.startDate) project.startDate=j.submit.startDate;
689
- }
690
- return {
691
- schemaVersion:SCHEMA_VERSION,
692
- meta:Object.assign(defaultMeta(), j.meta||{}),
693
- project, campaigns:j.campaigns, ui:j.ui, refs:j.refs
694
- };
695
- }
744
+ /* 数値化 (NaN/不正入力は null 維持) — B10 */
745
+ function numOrNull(v){ if(v===''||v==null) return null; const n=Number(v); return Number.isFinite(n)?n:null; }
746
+ /* 新規キャンペーンの個別既定値 (A-1: project既定はここでのみ供給し、以後はキャンペーン個別値運用) */
747
+ function cpDefaults(project, mode){ const p=project||st.project; return {budget: mode==='new'?p.budgetNew:p.budgetAsc, biddingStrategy:p.biddingStrategyDefault||'tCPA', tcpa:p.tcpaDefault}; }
748
+ /* E-2: 新規キャンペーン生成 (予算は固定 12,000 円・空のAG1を1つ内包)。isExisting:false = 常に編集可 */
749
+ function makeNewCampaign(seed){ const p=st.project; return Object.assign({uid:uid('c'), name:'新規キャンペーン', campaignId:'', isExisting:false, variable:'', urlNumber:'', budget:NEW_CP_BUDGET, biddingStrategy:p.biddingStrategyDefault||'tCPA', tcpa:p.tcpaDefault, adGroups:[{uid:uid('g'),name:'AG1',paramCode:'',isExisting:false,ads:[newAd()]}]}, seed||{}); }
750
+ /* E-3/F-2: 既存キャンペーン呼び出し (campaignIdを持つ広告追加専用の器)。isExisting:true = 基本パラメータ編集不可 */
751
+ function makeExistingCampaign(cpId, name){ return {uid:uid('c'), name:name||'', campaignId:String(cpId), isExisting:true, variable:'', urlNumber:'', budget:null, biddingStrategy:'', tcpa:null, adGroups:[]}; }
696
752
  function initialState(){
697
- const refs=buildRefs(), project=defaultProject();
698
- const byParam={}; RAW.sampleAds.forEach(a=>{(byParam[a.paramCode]=byParam[a.paramCode]||[]).push(a);});
699
- const campaigns=[];
700
- Object.keys(byParam).forEach((param,i)=>{
701
- const ads=byParam[param].map(a=>newAd({format:a.format,imageNo:String(a.imageNo),titleNo:a.titleNo,descNo:a.descNo,articleSlug:a.articleSlug}));
702
- const g=refs.groups[param]; const nm=g?g.groupName.replace(/^【[^】]*】/,''):param;
703
- const grp={uid:uid('g'), name:'AG1', paramCode:param, ads};
704
- campaigns.push({uid:uid('c'), name:nm||param, mode:'asc', variable:'ワードショー画像検証(サンプル)', urlNumber:'', budget:null, biddingStrategy:'', tcpa:null, status:'オフ', strategyMemo:'', adGroups:[grp]});
705
- });
753
+ const refs=buildRefs();
754
+ // 共通設定(project=案件名/主体者/アカウント等)はアカウントJSON由来。サンプルプランには campaigns と accountId のみ。
755
+ const project=defaultProject();
756
+ let campaigns=[];
757
+ planMismatchMsg='';
758
+ if(samplePlan && Array.isArray(samplePlan.campaigns)){
759
+ // アカウントIDとサンプルプランのaccountIdが異なる場合はエラー(不一致プランは読み込まない)
760
+ if(samplePlan.accountId && String(samplePlan.accountId)!==String(project.accountId)){
761
+ planMismatchMsg='⚠ サンプルプランのアカウントID('+samplePlan.accountId+')が現在のアカウント('+project.accountId+(project.accountName?' '+project.accountName:'')+')と一致しません。プランは読み込みません。正しいアカウントに切り替えるか、サンプルプランの accountId を合わせてください。';
762
+ if(typeof toast==='function') toast(planMismatchMsg);
763
+ } else {
764
+ campaigns=JSON.parse(JSON.stringify(samplePlan.campaigns));
765
+ campaigns.forEach(c=>{ c.uid=uid('c'); (c.adGroups||[]).forEach(g=>{ g.uid=uid('g'); (g.ads||[]).forEach(a=>{ a.uid=uid('a'); }); }); });
766
+ }
767
+ }
706
768
  return {schemaVersion:SCHEMA_VERSION, meta:defaultMeta(), project, refs, campaigns, ui:{expanded:{}}};
707
769
  }
708
770
 
709
771
  /* ---- persistence ---- */
710
772
  function stateJson(){ const {meta,project,campaigns,ui,refs}=st; return {schemaVersion:SCHEMA_VERSION, meta, project, campaigns, ui, refs}; }
711
- function persist(){ try{ localStorage.setItem(LS_KEY, JSON.stringify(stateJson())); }catch(e){} }
712
- function load(){ try{ const j=JSON.parse(localStorage.getItem(LS_KEY)); if(!j||!j.campaigns) return null; const m=migrateState(j); const refs=(m.refs&&m.refs.titles&&m.refs.media&&m.refs.groups)?m.refs:buildRefs(); return {schemaVersion:SCHEMA_VERSION, meta:m.meta, project:Object.assign(defaultProject(),m.project), refs, campaigns:m.campaigns, ui:m.ui||{expanded:{}}}; }catch(e){ return null; } }
773
+ let _persistWarned=false;
774
+ function persist(){ /* localStorageプラン廃止: no-op。プランの永続化はサンプルプラン更新(サンプルプラン.json)で行う */ }
775
+ /* B12: SCHEMA_VERSION 不一致は後方互換なしで破棄→初期化。refs欠落時は再構築。 */
776
+ function load(){ return null; /* LSプラン廃止 */ try{ const j=JSON.parse(localStorage.getItem(LS_KEY)); if(!j||!j.campaigns||j.schemaVersion!==SCHEMA_VERSION) return null; const refs=validRefs(j.refs)?normalizeRefs(j.refs):buildRefs(); return {schemaVersion:SCHEMA_VERSION, meta:Object.assign(defaultMeta(),j.meta||{}), project:Object.assign(defaultProject(),j.project), refs, campaigns:j.campaigns, ui:j.ui||{expanded:{}}}; }catch(e){ return null; } }
713
777
 
714
- let st = load() || initialState();
778
+ /* localStorage のプラン永続化は廃止。プランの正はサンプルプラン.json(外部)。起動時に必ず読み込む。 */
779
+ let st = initialState();
715
780
 
716
781
  /* ---- undo (bounded) ---- */
717
782
  const undoStack=[];
718
783
  function snapshot(){ undoStack.push(JSON.stringify({project:st.project,campaigns:st.campaigns,refs:st.refs})); if(undoStack.length>40) undoStack.shift(); }
719
- function mutate(fn){ snapshot(); fn(); persist(); render(); }
720
- function undo(){ const s=undoStack.pop(); if(!s){toast('これ以上戻せません'); return;} const j=JSON.parse(s); st.project=j.project; st.campaigns=j.campaigns; if(j.refs) st.refs=j.refs; persist(); if(typeof buildDatalists==='function') buildDatalists(); render(); toast('元に戻しました'); }
784
+ let _dirty=false; // 未保存の編集があるか (離脱時警告用)
785
+ function markDirty(){ _dirty=true; }
786
+ function markSaved(){ _dirty=false; }
787
+ function mutate(fn){ snapshot(); fn(); _dirty=true; persist(); render(); }
788
+ function undo(){ const s=undoStack.pop(); if(!s){toast('これ以上戻せません'); return;} const j=JSON.parse(s); st.project=j.project; st.campaigns=j.campaigns; if(j.refs) st.refs=j.refs; _dirty=true; persist(); if(typeof buildDatalists==='function') buildDatalists(); render(); toast('元に戻しました'); }
721
789
 
722
790
  /* ---- lookups ---- */
723
791
  function eachAd(cb){ st.campaigns.forEach(c=>c.adGroups.forEach(g=>g.ads.forEach(a=>cb(a,g,c)))); }
@@ -731,24 +799,28 @@ function resolveAd(ad, group){
731
799
  const sizeKey = ad.format==='SQ'?'sq':'card';
732
800
  const crName = String(ad.imageNo||'') + (ad.format==='SQ'?'s':'c');
733
801
  const adName = crName + '-' + (ad.titleNo??'') + 'L' + (ad.descNo??'') + '-' + (group.paramCode||'');
734
- const g = refs.groups[group.paramCode];
735
802
  const me = refs.media[String(ad.imageNo)];
736
803
  const mediaId = me ? me[sizeKey] : undefined;
737
804
  const titleText = refs.titles[ad.titleNo];
738
- const descText = refs.titles[ad.descNo];
805
+ const descText = (refs.descriptions||{})[ad.descNo]; // A-5: 説明文テーブルのみ参照 (titlesへのフォールバックなし)
739
806
  const finalUrl = (p.baseArticleUrl||'') + (ad.articleSlug||'') + '?ad=' + (group.paramCode||'') + '&creative=' + (group.paramCode||'') + '-' + adName;
740
807
  const errs=[], warns=[];
741
- if(!ad.isBanner){
742
- if(String(ad.imageNo||'')==='' || mediaId===undefined) errs.push('画像未解決');
743
- if(String(ad.titleNo||'')==='' || titleText===undefined) errs.push('タイトル未解決');
744
- if(String(ad.descNo||'')==='' || descText===undefined) errs.push('説明文未解決');
745
- if(titleText!==undefined && glen(titleText)>35) errs.push('タイトル35字超');
746
- else if(titleText!==undefined && glen(titleText)>20) warns.push('タイトル20字超');
747
- if(!g) warns.push('新規/未払出');
748
- }
749
- return {crName, adName, campaignId:g?g.campaignId:'', adGroupId:g?g.adGroupId:'', mediaId:mediaId||'', titleText:titleText===undefined?'':titleText, descText:descText===undefined?'':descText, finalUrl, errs, warns, resolvedGroup:!!g};
808
+ // 画像を3分類: 番号空=真エラー / 番号あり&refs.mediaに無い=未登録(要アップロード=pendingImage) / 解決OK
809
+ const imgNoStr=String(ad.imageNo||'').trim();
810
+ const pendingImage = (imgNoStr!=='' && mediaId===undefined); // アカウントのメディアライブラリに未登録→先にアップロードして新規作成
811
+ if(imgNoStr==='') errs.push('画像未解決');
812
+ if(String(ad.titleNo||'')==='' || titleText===undefined) errs.push('タイトル未解決');
813
+ if(String(ad.descNo||'')==='' || descText===undefined) errs.push('説明文未解決');
814
+ if(titleText!==undefined && glen(titleText)>35) errs.push('タイトル35字超');
815
+ else if(titleText!==undefined && glen(titleText)>20) warns.push('タイトル20字超');
816
+ // B11: paramCode空はエラー(export対象外・URL生成に必須)。F-6: refs.groups 払出判定は廃止。
817
+ if(String(group.paramCode||'').trim()==='') errs.push('paramCode未入力');
818
+ if(pendingImage) warns.push('画像未登録(要アップロード)'); // errではなくwarn: 新規作成対象として保持
819
+ // F-6: ID解決は adGroupId ベース。campaignId は idsCell表示用に持たない(親CP参照は呼び出し側)
820
+ const aid=String(group.adGroupId||'').trim();
821
+ return {crName, adName, campaignId:'', adGroupId:aid, imageNo:imgNoStr, mediaId:mediaId||'', pendingImage, titleText:titleText===undefined?'':titleText, descText:descText===undefined?'':descText, finalUrl, errs, warns, resolvedGroup:!!aid};
750
822
  }
751
- function validationCounts(){ let err=0,warn=0,banner=0,ads=0; eachAd((a,g)=>{ ads++; if(a.isBanner){banner++; return;} const r=resolveAd(a,g); if(r.errs.length) err++; else if(r.warns.length) warn++; }); return {err,warn,banner,ads}; }
823
+ function validationCounts(){ let err=0,warn=0,ads=0; eachAd((a,g)=>{ ads++; const r=resolveAd(a,g); if(r.errs.length) err++; else if(r.warns.length) warn++; }); return {err,warn,ads}; }
752
824
 
753
825
  /* ============================================================
754
826
  RENDER
@@ -756,9 +828,14 @@ function validationCounts(){ let err=0,warn=0,banner=0,ads=0; eachAd((a,g)=>{ ad
756
828
  let activeView='sheet';
757
829
  let planMode='sheet'; // プランビュー内の表示モード: sheet / tree
758
830
  let sheetTab='ads'; // シート内サブタブ: ads / groups / campaigns
831
+ function renderPlanWarn(){
832
+ const bar=document.getElementById('planWarn'); if(!bar) return;
833
+ if(planMismatchMsg){ document.getElementById('planWarnMsg').textContent=planMismatchMsg; bar.hidden=false; }
834
+ else bar.hidden=true;
835
+ }
759
836
  function render(){
760
- document.getElementById('pjLabel').textContent = st.project.name + ' / 主体者:' + st.project.advertiser;
761
- const si=document.getElementById('stratInput'); if(si && document.activeElement!==si) si.value=st.project.strategyMemo||'';
837
+ document.getElementById('pjLabel').textContent = st.project.name + ' / 主体者:' + (st.project.principal||'');
838
+ renderPlanWarn();
762
839
  renderTplChips(); renderValidationBadge();
763
840
  if(activeView==='sheet'){
764
841
  document.getElementById('sheetArea').hidden = planMode!=='sheet';
@@ -776,7 +853,7 @@ function renderValidationBadge(){
776
853
  const c=validationCounts(); const b=document.getElementById('vbadge');
777
854
  b.className='vbadge '+(c.err?'err':(c.warn?'warn':'ok'));
778
855
  b.textContent = c.err? ('⚠ エラー'+c.err+'件') : (c.warn? ('△ 警告'+c.warn+'件') : ('✓ '+c.ads+'件OK'));
779
- b.title = 'エラー'+c.err+' / 警告'+c.warn+' / 手入稿'+c.banner+' / 広告'+c.ads;
856
+ b.title = 'エラー'+c.err+' / 警告'+c.warn+' / 広告'+c.ads;
780
857
  }
781
858
  function renderTplChips(){
782
859
  const p=st.project;
@@ -785,21 +862,19 @@ function renderTplChips(){
785
862
  const sel=(f,v,opts)=>'<select class="chipin" data-proj="'+f+'">'+opts.map(o=>'<option'+(o===v?' selected':'')+'>'+esc(o)+'</option>').join('')+'</select>';
786
863
  const selv=(f,v,opts)=>'<select class="chipin" data-proj="'+f+'">'+opts.map(o=>'<option value="'+esc(o.v)+'"'+(o.v===v?' selected':'')+'>'+esc(o.t)+'</option>').join('')+'</select>';
787
864
  const chip=(label,ctrl)=>'<span class="chip">'+esc(label)+' '+ctrl+'</span>';
865
+ // E-1: 案件名 / アカウントID / 主体者 / タイプ チップは撤去。
866
+ // - アカウント名/IDは上部のアカウントセレクト由来 (applyAccountJson で st.project へ同期)
867
+ // - 主体者(principal)はアカウントJSON由来 (アカウント編集モーダルで変更)
868
+ // - タイプ(レスポンシブ/画像)はプランツールバーの固定プルダウン (変更不可)
788
869
  document.getElementById('tplChips').innerHTML =
789
- chip('案件', txt('name',p.name,'13ch')) +
790
- chip('アカウントID', txt('accountId',p.accountId||'','11ch')) +
791
870
  chip('目的', selv('goal',p.goal||'CONVERSION',[{v:'CONVERSION',t:'コンバージョン'},{v:'CLICK',t:'クリック'},{v:'BRAND_AWARENESS',t:'ブランド認知'},{v:'VIDEO_VIEW',t:'動画再生'},{v:'VIEWABLE_IMPRESSION',t:'vインプ'}])) +
792
871
  chip('開始日', txt('startDate',p.startDate||'','10ch')) +
793
872
  chip('CTA', txt('cta',p.cta,'7ch')) +
794
873
  chip('配信', sel('deliveryStatus',p.deliveryStatus,['オフ','オン'])) +
795
- chip('タイプ', txt('adType',p.adType,'8ch')+'/'+txt('mainMediaFormat',p.mainMediaFormat,'4ch')) +
796
- chip('入札', sel('biddingStrategyDefault',p.biddingStrategyDefault,['tCPA','最大配信'])+' '+num('tcpaDefault',p.tcpaDefault,'6ch')+'円') +
797
874
  chip('計測', num('attributionDays',p.attributionDays,'3ch')+'日') +
798
- chip('主体者', txt('advertiser',p.advertiser,'9ch')) +
799
875
  chip('表示URL', txt('displayUrlFormat',p.displayUrlFormat,'13ch')) +
800
- chip('記事URL', txt('baseArticleUrl',p.baseArticleUrl,'16ch')) +
801
- chip('予算新規', num('budgetNew',p.budgetNew,'7ch')) +
802
- chip('予算ASC', num('budgetAsc',p.budgetAsc,'7ch'));
876
+ chip('記事URL', txt('baseArticleUrl',p.baseArticleUrl,'16ch'));
877
+ // A-1: 入札(tcpaDefault) / 予算新規 / 予算ASC チップは撤去。project既定は新規CP作成時の初期値供給のみに使い、以後はキャンペーン行で個別編集する。
803
878
  }
804
879
 
805
880
  /* ---- SHEET ---- */
@@ -808,48 +883,46 @@ function renderSheet(){
808
883
  if(sheetTab==='groups') return renderSheetGroups();
809
884
  if(sheetTab==='campaigns') return renderSheetCampaigns();
810
885
  const filter=document.getElementById('sheetFilter').value;
811
- const cols=['','パス','形式','画像番号','タイトル番号','説明文番号','paramCode(グループ)','記事slug','広告名(自動)','CP / AG ID','状態'];
886
+ const cols=['','グループ','形式','画像番号','タイトル番号','説明文番号','paramCode(グループ)','記事slug','広告名(自動)','CP / AG ID'];
812
887
  let html='<table class="sheet"><thead><tr>'+cols.map(c=>'<th>'+esc(c)+'</th>').join('')+'<th></th></tr></thead><tbody>';
813
888
  let shown=0, ri=-1;
889
+ // F-8: ネスト(キャンペーン見出しエリア)を廃止し、1広告=1行のフラットな表にする
814
890
  st.campaigns.forEach(c=>{
815
- const ratio = c.adGroups.length+':'+(c.adGroups.map(g=>g.ads.length).join('/')||'0');
816
- html+='<tr class="cphead"><td colspan="12"><div class="cptitle"><span class="ratio">'+esc(c.adGroups.length===1?('1:1:'+ (c.adGroups[0].ads.length)) : ratio)+'</span> <b>'+esc(c.name)+'</b> <span class="pill '+(c.mode==='new'?'':'green')+'">'+esc(MODES[c.mode]||c.mode)+'</span>'+(c.variable?'<span class="pill purple">変数:'+esc(c.variable)+'</span>':'')+'</div></td></tr>';
817
891
  c.adGroups.forEach(g=>{
818
892
  g.ads.forEach(a=>{
819
893
  const r=resolveAd(a,g);
820
894
  const hasErr=r.errs.length>0;
821
895
  if(filter==='err' && !hasErr) return;
822
- if(filter==='banner' && !a.isBanner) return;
823
896
  shown++; ri++;
824
- const cls='adrow'+(hasErr?' hasErr':'')+(a.isBanner?' banner':'');
897
+ const cls='adrow'+(hasErr?' hasErr':'');
825
898
  html+='<tr class="'+cls+'" data-adrow="'+a.uid+'">';
826
899
  html+='<td class="rownum" data-rownum="'+ri+'" title="行を選択('+esc(g.name)+')">'+(ri+1)+'</td>';
827
- html+='<td><span class="breadc">'+esc(g.name)+'</span></td>';
828
- html+='<td><select class="cell-in" data-sel="1" data-r="'+ri+'" data-c="0" data-u="'+a.uid+'" data-f="format"><option'+(a.format==='Card'?' selected':'')+'>Card</option><option'+(a.format==='SQ'?' selected':'')+'>SQ</option></select></td>';
900
+ html+='<td><div class="cppick" data-agpick-ad="'+a.uid+'" title="クリックで所属グループを変更(プラン内の広告グループから選択)"><span class="breadc">'+esc(g.name)+'</span><span class="tgcaret">▾</span></div></td>';
901
+ html+='<td><select class="cell-in fmt" data-sel="1" data-r="'+ri+'" data-c="0" data-u="'+a.uid+'" data-f="format"><option'+(a.format==='Card'?' selected':'')+'>Card</option><option'+(a.format==='SQ'?' selected':'')+'>SQ</option></select></td>';
829
902
  // 画像番号 : メディアID(1ボックス + ▼一覧)
830
- html+='<td><div class="cellcombo'+(!a.isBanner&&r.errs.includes('画像未解決')?' err':'')+'"><input class="cell-in num" data-sel="1" data-r="'+ri+'" data-c="1" data-u="'+a.uid+'" data-f="imageNo" value="'+esc(a.imageNo)+'"><span class="combosep">:</span><span class="ghost" data-d="'+a.uid+'-media" title="メディアID '+esc(r.mediaId)+'">'+mediaInline(a,r)+'</span><button class="combodd" data-dd="media" data-u="'+a.uid+'" data-f="imageNo" tabindex="-1" title="登録画像から選択">▾</button></div></td>';
903
+ html+='<td><div class="cellcombo'+(r.errs.includes('画像未解決')?' err':'')+'"><input class="cell-in num" data-sel="1" data-r="'+ri+'" data-c="1" data-u="'+a.uid+'" data-f="imageNo" value="'+esc(a.imageNo)+'"><span class="combosep">:</span><span class="ghost" data-d="'+a.uid+'-media" title="メディアID '+esc(r.mediaId)+'">'+mediaInline(a,r)+'</span><button class="combodd" data-dd="media" data-u="'+a.uid+'" data-f="imageNo" tabindex="-1" title="登録画像から選択">▾</button></div></td>';
831
904
  // タイトル番号 : 文言(1ボックス + 字数 + ▼一覧)
832
- html+='<td><div class="cellcombo'+(!a.isBanner&&(r.errs.includes('タイトル未解決')||r.errs.includes('タイトル35字超'))?' err':'')+'"><input class="cell-in num" data-sel="1" data-r="'+ri+'" data-c="2" data-u="'+a.uid+'" data-f="titleNo" value="'+esc(a.titleNo)+'"><span class="combosep">:</span><span class="ghost" data-d="'+a.uid+'-title" title="'+esc(r.titleText)+'">'+titleGhost(r)+'</span><button class="combodd" data-dd="titles" data-u="'+a.uid+'" data-f="titleNo" tabindex="-1" title="タイトル一覧から選択">▾</button></div></td>';
905
+ html+='<td><div class="cellcombo'+((r.errs.includes('タイトル未解決')||r.errs.includes('タイトル35字超'))?' err':'')+'"><input class="cell-in num" data-sel="1" data-r="'+ri+'" data-c="2" data-u="'+a.uid+'" data-f="titleNo" value="'+esc(a.titleNo)+'"><span class="combosep">:</span><span class="ghost" data-d="'+a.uid+'-title" title="'+esc(r.titleText)+'">'+titleGhost(r)+'</span><button class="combodd" data-dd="titles" data-u="'+a.uid+'" data-f="titleNo" tabindex="-1" title="タイトル一覧から選択">▾</button></div></td>';
833
906
  // 説明文番号 : 文言(1ボックス + ▼一覧)
834
- html+='<td><div class="cellcombo'+(!a.isBanner&&r.errs.includes('説明文未解決')?' err':'')+'"><input class="cell-in num" data-sel="1" data-r="'+ri+'" data-c="3" data-u="'+a.uid+'" data-f="descNo" value="'+esc(a.descNo)+'"><span class="combosep">:</span><span class="ghost" data-d="'+a.uid+'-desc" title="'+esc(r.descText)+'">'+descGhost(r)+'</span><button class="combodd" data-dd="titles" data-u="'+a.uid+'" data-f="descNo" tabindex="-1" title="説明文一覧から選択">▾</button></div></td>';
835
- // paramCode (group-level)
836
- html+='<td><input class="cell-in mono" data-sel="1" data-r="'+ri+'" data-c="4" data-u="'+a.uid+'" data-f="paramCode" value="'+esc(g.paramCode)+'" title="グループ共通(変更で同グループ全広告に反映)" style="width:120px"></td>';
907
+ html+='<td><div class="cellcombo'+(r.errs.includes('説明文未解決')?' err':'')+'"><input class="cell-in num" data-sel="1" data-r="'+ri+'" data-c="3" data-u="'+a.uid+'" data-f="descNo" value="'+esc(a.descNo)+'"><span class="combosep">:</span><span class="ghost" data-d="'+a.uid+'-desc" title="'+esc(r.descText)+'">'+descGhost(r)+'</span><button class="combodd" data-dd="titles" data-u="'+a.uid+'" data-f="descNo" tabindex="-1" title="説明文一覧から選択">▾</button></div></td>';
908
+ // paramCode (group-level) — 既存AGは read-only (グループの基本パラメータは編集不可・広告追加のみ)
909
+ const pcRo=isExistingGroup(g);
910
+ html+='<td><input class="cell-in mono'+(pcRo?' roinput':'')+'" data-sel="1" data-r="'+ri+'" data-c="4" data-u="'+a.uid+'" data-f="paramCode" value="'+esc(g.paramCode)+'"'+(pcRo?' readonly title="既存広告グループのため編集不可"':' title="グループ共通(変更で同グループ全広告に反映)"')+' style="width:120px"></td>';
837
911
  // slug
838
912
  html+='<td><input class="cell-in mono" data-sel="1" data-r="'+ri+'" data-c="5" data-u="'+a.uid+'" data-f="articleSlug" value="'+esc(a.articleSlug)+'" style="width:90px"></td>';
839
913
  // adName
840
914
  html+='<td><span class="idpill" data-d="'+a.uid+'-adname" title="'+esc(r.finalUrl)+'">'+esc(r.adName)+'</span></td>';
841
915
  // ids
842
- html+='<td><span class="idpill '+(a.isBanner?'':(r.resolvedGroup?'':'warn'))+'" data-d="'+a.uid+'-ids">'+idsCell(r)+'</span></td>';
843
- // banner toggle
844
- html+='<td><label class="minick"><input type="checkbox" data-u="'+a.uid+'" data-f="isBanner"'+(a.isBanner?' checked':'')+'>手入稿</label></td>';
916
+ html+='<td><span class="idpill '+(r.resolvedGroup?'':'warn')+'" data-d="'+a.uid+'-ids">'+idsCell(r)+'</span></td>';
845
917
  // actions
846
918
  html+='<td><span class="rowact"><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></td>';
847
919
  html+='</tr>';
848
920
  });
849
- // add-ad row
850
- html+='<tr><td></td><td colspan="11"><button class="addbtn" data-act="ag-addad" data-u="'+g.uid+'">+ '+esc(g.name)+' に広告を追加</button></td></tr>';
921
+ // F-4: グループごとの追加ボタンは廃止(最下部に1つ)
851
922
  });
852
923
  });
924
+ // F-4: 広告追加は最下部に1行だけ(既定=最後のグループ・グループ列で変更可)
925
+ html+='<tr><td></td><td colspan="10"><button class="addbtn" data-act="add-ad-bottom">+ 広告を追加</button></td></tr>';
853
926
  html+='</tbody></table>';
854
927
  if(shown===0 && st.campaigns.length===0) html='<div class="empty">キャンペーンがありません。シートの「キャンペーン」タブ →「+ キャンペーンを追加」から。</div>';
855
928
  const host=document.getElementById('sheetHost');
@@ -859,10 +932,10 @@ function renderSheet(){
859
932
  if(fu&&ff){ const el=host.querySelector('[data-u="'+fu+'"][data-f="'+ff+'"]'); if(el){ el.focus(); try{ if(selS!=null&&el.setSelectionRange) el.setSelectionRange(selS,selS); }catch(e){} } }
860
933
  applySelection();
861
934
  }
862
- function mediaInline(a,r){ if(a.isBanner) return '<span class="idpill">—</span>'; if(!a.imageNo) return '<span class="idpill" style="color:var(--faint)">—</span>'; const ok=r.mediaId!==''; return '<span class="idpill '+(ok?'':'err')+'">'+(ok?('ID '+esc(r.mediaId)):'#N/A')+'</span>'; }
935
+ function mediaInline(a,r){ if(!a.imageNo) return '<span class="idpill" style="color:var(--faint)">—</span>'; if(r.mediaId!=='') return '<span class="idpill">ID '+esc(r.mediaId)+'</span>'; if(r.pendingImage) return '<span class="idpill warn" title="アカウント未登録。入稿前にアップロードして新規登録">🆕未登録</span>'; return '<span class="idpill err">#N/A</span>'; }
863
936
  function titleGhost(r){ if(r.errs.includes('タイトル未解決')) return '<span class="err">未解決</span>'; return esc(r.titleText.slice(0,40)); }
864
937
  function descGhost(r){ if(r.errs.includes('説明文未解決')) return '<span class="err">— 未解決</span>'; return esc(r.descText.slice(0,44)); }
865
- function idsCell(r){ if(!r.resolvedGroup) return '新規/未払出'; return r.campaignId+' / '+r.adGroupId; }
938
+ function idsCell(r){ return r.adGroupId ? ('AG '+r.adGroupId) : '新規'; } // F-6: adGroupIdベース表示
866
939
 
867
940
  /* ---- TREE ---- */
868
941
  function isCollapsed(u){ return !!st.ui.expanded[u]===false ? false : st.ui.expanded[u]==='c'; }
@@ -873,104 +946,139 @@ function focusRestoreG(host, attrU, attrF){
873
946
  /* ---- シート: 広告グループ タブ(変更列を一覧編集) ---- */
874
947
  function renderSheetGroups(){
875
948
  const host=document.getElementById('sheetHost'); const restore=focusRestoreG(host,'gu','gf');
876
- const cols=['#','キャンペーン','広告グループ名','paramCode','広告グループID','入札','配信','デバイス','性別','年齢','地域','曜日時間','オーディエンス','サーチKW',''];
949
+ // F-6 列順: キャンペーン → 広告グループID(左から2番目) → 名前 → paramCode → 入札/配信/ターゲティング
950
+ const cols=['#','キャンペーン','広告グループID','広告グループ名','paramCode','入札','配信','デバイス','性別','年齢','地域','曜日時間','オーディエンス','サーチKW',''];
877
951
  let html='<table class="sheet"><thead><tr>'+cols.map(c=>'<th>'+esc(c)+'</th>').join('')+'</tr></thead><tbody>'; let ri=-1;
878
952
  const gin=(g,ci,w,req)=>{ const f=VARF_GROUPS[ci]; const v=g[f]; const empty=req&&(v==null||v===''); return '<td><input class="cell-in'+(empty?' warnempty':'')+'" data-sel="1" data-r="'+ri+'" data-c="'+ci+'" data-gu="'+g.uid+'" data-gf="'+f+'" value="'+esc(v==null?'':v)+'" style="width:'+w+'"></td>'; };
953
+ const roTd=v=>'<td>'+(v?'<span class="robadge">'+esc(v)+'</span>':'<span class="idpill" style="color:var(--faint)">—</span>')+'</td>';
879
954
  st.campaigns.forEach(c=>{
880
955
  c.adGroups.forEach(g=>{ ri++;
881
- const gr=st.refs.groups[g.paramCode]; const paramErr=g.paramCode&&!gr;
956
+ const gEd=!isExistingGroup(g); // 既存呼び出し行=disabled / 新規行=編集可
882
957
  html+='<tr class="adrow" data-gu="'+g.uid+'">';
883
958
  html+='<td class="rownum" data-rownum="'+ri+'">'+(ri+1)+'</td>';
884
- html+='<td><span class="breadc" title="'+esc(c.name)+'">'+esc(c.name)+'</span></td>';
885
- html+=gin(g,0,'120px',true);
886
- html+='<td><input class="cell-in mono'+(!g.paramCode?' warnempty':(paramErr?' err':''))+'" data-sel="1" data-r="'+ri+'" data-c="1" data-gu="'+g.uid+'" data-gf="paramCode" value="'+esc(g.paramCode||'')+'" style="width:110px"><span class="ghost">'+(gr?('ID '+esc(gr.adGroupId)):(g.paramCode?'<span class="err">未払出</span>':'<span style="color:var(--faint)">未記入</span>'))+'</span></td>';
887
- html+='<td><input class="cell-in mono" data-sel="1" data-r="'+ri+'" data-c="2" data-gu="'+g.uid+'" data-gf="adGroupId" value="'+esc(g.adGroupId||'')+'" placeholder="'+(gr?esc(gr.adGroupId):'')+'" style="width:90px"></td>';
888
- html+=gin(g,3,'56px',false);
889
- html+='<td><select class="cell-in" data-sel="1" data-r="'+ri+'" data-c="4" data-gu="'+g.uid+'" data-gf="deliveryStatus"><option value=""'+(!g.deliveryStatus?' selected':'')+'>(既定)</option><option'+(g.deliveryStatus==='オフ'?' selected':'')+'>オフ</option><option'+(g.deliveryStatus==='オン'?' selected':'')+'>オン</option></select></td>';
890
- [5,6,7,8,9,10,11].forEach(ci=>{ const tf=VARF_GROUPS[ci]; if(TG_OPTIONS[tf]){ const val=g[tf]||''; const cnt=val?val.split(',').filter(s=>s.trim()).length:0; const disp = val? '<span class="tgval">'+esc(val)+'</span>' : '<span class="tgval"><span class="tghint">'+esc(TG_OPTIONS[tf].label)+'</span></span>'; html+='<td class="tgtd"><div class="tgpick'+(val?'':' tgempty')+'" data-sel="1" data-r="'+ri+'" data-c="'+ci+'" data-gu="'+g.uid+'" data-gf="'+tf+'" title="'+esc(val||(TG_OPTIONS[tf].label+'(クリックで選択)'))+'">'+disp+'<span class="tgcaret">▾</span></div></td>'; } else { html+=gin(g,ci,'120px',false); } });
959
+ // キャンペーン列: 新規AGはクリックでプラン内キャンペーンをドロワー選択(親付け替え)。既存AGはキャンペーンが確定済みのため変更不可
960
+ if(gEd){
961
+ html+='<td><div class="cppick" data-cppick-g="'+g.uid+'" title="クリックでキャンペーンを選択(プラン内)"><span class="breadc" style="max-width:150px">'+esc(c.name)+'</span><span class="tgcaret">▾</span></div></td>';
962
+ } else {
963
+ html+='<td><span class="robadge" title="既存広告グループのためキャンペーン変更不可" style="max-width:150px; display:inline-block; overflow:hidden; text-overflow:ellipsis; vertical-align:middle">'+esc(c.name)+'</span></td>';
964
+ }
965
+ // 広告グループID (左から2番目): 既存=ID / 新規=「新規」バッジ (手入力なし)
966
+ html+='<td>'+newOrIdBadge(isNewGroup(g), resolveAdGroupIdOf(g))+'</td>';
967
+ if(gEd){
968
+ html+=gin(g,1,'150px',true); // 名前(c1)
969
+ html+='<td><input class="cell-in mono'+(!g.paramCode?' warnempty':'')+'" data-sel="1" data-r="'+ri+'" data-c="2" data-gu="'+g.uid+'" data-gf="paramCode" value="'+esc(g.paramCode||'')+'" title="URL生成用のパラメータ(グループ紐づけには不使用)" style="width:120px"></td>'; // paramCode(c2)
970
+ html+=gin(g,3,'56px',false); // 入札(c3)
971
+ html+='<td><select class="cell-in fmt" style="min-width:66px" data-sel="1" data-r="'+ri+'" data-c="4" data-gu="'+g.uid+'" data-gf="deliveryStatus"><option value=""'+(!g.deliveryStatus?' selected':'')+'>(既定)</option><option'+(g.deliveryStatus==='オフ'?' selected':'')+'>オフ</option><option'+(g.deliveryStatus==='オン'?' selected':'')+'>オン</option></select></td>'; // 配信(c4)
972
+ [5,6,7,8,9,10,11].forEach(ci=>{ const tf=VARF_GROUPS[ci]; if(TG_OPTIONS[tf]){ const val=g[tf]||''; const disp = val? '<span class="tgval">'+esc(val)+'</span>' : '<span class="tgval"><span class="tghint">'+esc(TG_OPTIONS[tf].label)+'</span></span>'; html+='<td class="tgtd"><div class="tgpick'+(val?'':' tgempty')+'" data-sel="1" data-r="'+ri+'" data-c="'+ci+'" data-gu="'+g.uid+'" data-gf="'+tf+'" title="'+esc(val||(TG_OPTIONS[tf].label+'(クリックで選択)'))+'">'+disp+'<span class="tgcaret">▾</span></div></td>'; } else { html+=gin(g,ci,'120px',false); } });
973
+ } else {
974
+ // 既存呼び出し行: 名前/paramCode/入札/配信/ターゲティングを read-only(disabled)。ID起点で自動で埋まった値を表示
975
+ html+='<td><span class="robadge" title="既存広告グループのため編集不可">'+esc(g.name||'')+'</span></td>';
976
+ html+='<td><span class="robadge mono">'+esc(g.paramCode||'')+'</span></td>';
977
+ html+=roTd(g.bid);
978
+ html+=roTd(g.deliveryStatus);
979
+ [5,6,7,8,9,10,11].forEach(ci=>{ const tf=VARF_GROUPS[ci]; html+=roTd(g[tf]); });
980
+ }
891
981
  html+='<td><span class="rowact"><button class="iconbtn" data-act="ag-dup" data-u="'+g.uid+'" title="複製">⧉</button><button class="iconbtn danger" data-act="ag-del" data-u="'+g.uid+'" title="削除">✕</button></span></td>';
892
982
  html+='</tr>';
893
983
  });
894
- html+='<tr><td></td><td></td><td colspan="13"><button class="addbtn" data-act="cp-addag" data-u="'+c.uid+'">+ '+esc(c.name)+' に広告グループを追加</button></td></tr>';
895
984
  });
896
- html+='</tbody></table>'; if(!st.campaigns.length) html='<div class="empty">キャンペーンがありません。</div>';
985
+ // F-5: 追加行はタブ最下部に1行だけ。追加時にキャンペーンを選択(ドロワー)
986
+ html+='<tr><td></td><td colspan="14"><button class="addbtn" data-act="add-adgroup">+ 新規広告グループ</button> <button class="addbtn" data-act="add-adgroup-existing">📂 既存広告グループを呼び出し</button></td></tr>';
987
+ html+='</tbody></table>';
897
988
  host.innerHTML=html; restore(); applySelection();
898
989
  }
899
990
  /* ---- シート: キャンペーン タブ(変更列を一覧編集) ---- */
900
991
  function renderSheetCampaigns(){
901
992
  const host=document.getElementById('sheetHost'); const restore=focusRestoreG(host,'cu','cf');
902
- const cols=['#','キャンペーン名','キャンペーンID','モード','予算/日','入札戦略','tCPA','配信','検証変数','リンク番号','戦略メモ',''];
993
+ // E-2: campaignId 最左・read-only。モード/配信/戦略メモ列は撤去
994
+ const cols=['#','キャンペーンID','キャンペーン名','予算/日','入札戦略','tCPA','検証変数','リンク番号',''];
903
995
  let html='<table class="sheet"><thead><tr>'+cols.map(c=>'<th>'+esc(c)+'</th>').join('')+'</tr></thead><tbody>';
904
996
  let RIDX=-1;
905
997
  st.campaigns.forEach((c)=>{ RIDX++;
906
- const cpId = c.campaignId || resolveGroupIds(firstParamOf(c)).campaignId;
998
+ const cEd=!isExistingCampaign(c); // F-2: 編集可否は明示フラグのみ。表示バッジはID解決ベースで分離
907
999
  const a=(f)=>'data-sel="1" data-r="'+RIDX+'" data-c="'+VARF_CAMPAIGNS.indexOf(f)+'" data-cu="'+c.uid+'" data-cf="'+f+'"';
908
1000
  html+='<tr class="adrow" data-cu="'+c.uid+'">';
909
1001
  html+='<td class="rownum" data-rownum="'+RIDX+'">'+(RIDX+1)+'</td>';
910
- html+='<td><input class="cell-in'+(!c.name?' warnempty':'')+'" '+a('name')+' value="'+esc(c.name||'')+'" style="width:170px"></td>';
911
- html+='<td><input class="cell-in mono" '+a('campaignId')+' value="'+esc(c.campaignId||'')+'" placeholder="'+esc(cpId)+'" style="width:90px"></td>';
912
- html+='<td><select class="cell-in" '+a('mode')+'>'+Object.keys(MODES).map(k=>'<option value="'+k+'"'+(c.mode===k?' selected':'')+'>'+esc(MODES[k])+'</option>').join('')+'</select></td>';
913
- html+='<td><input class="cell-in num" type="number" '+a('budget')+' value="'+esc(c.budget??'')+'" placeholder="'+(c.mode==='new'?st.project.budgetNew:st.project.budgetAsc)+'" style="width:78px"></td>';
914
- html+='<td><select class="cell-in" '+a('biddingStrategy')+'><option value="">(既定)</option><option'+(c.biddingStrategy==='tCPA'?' selected':'')+'>tCPA</option><option'+(c.biddingStrategy==='最大配信'?' selected':'')+'>最大配信</option></select></td>';
915
- html+='<td><input class="cell-in num" type="number" '+a('tcpa')+' value="'+esc(c.tcpa??'')+'" placeholder="'+st.project.tcpaDefault+'" style="width:70px"></td>';
916
- html+='<td><select class="cell-in" '+a('status')+'><option'+(c.status==='オフ'?' selected':'')+'>オフ</option><option'+(c.status==='オン'?' selected':'')+'>オン</option></select></td>';
917
- html+='<td><input class="cell-in" '+a('variable')+' value="'+esc(c.variable||'')+'" style="width:140px"></td>';
918
- html+='<td><input class="cell-in mono" '+a('urlNumber')+' value="'+esc(c.urlNumber||'')+'" style="width:54px"></td>';
919
- html+='<td><input class="cell-in" '+a('strategyMemo')+' value="'+esc(c.strategyMemo||'')+'" style="width:160px"></td>';
1002
+ // campaignId: 入力欄廃止。既存=「ID xxx」/ 新規=「新規」バッジ (表示はID解決ベース)
1003
+ html+='<td>'+newOrIdBadge(isNewCampaign(c), resolveCampaignIdOf(c))+'</td>';
1004
+ // 既存呼び出しキャンペーンは広告追加専用 → 基本パラメータ(name/予算/入札/tCPA/検証変数/リンク番号)は全て read-only (backendが無視するため編集齟齬を防ぐ)
1005
+ if(cEd){
1006
+ html+='<td><input class="cell-in'+(!c.name?' warnempty':'')+'" '+a('name')+' value="'+esc(c.name||'')+'" style="width:170px"></td>';
1007
+ html+='<td><input class="cell-in num" type="number" '+a('budget')+' value="'+esc(c.budget??'')+'" placeholder="'+NEW_CP_BUDGET+'" style="width:78px"></td>';
1008
+ html+='<td><select class="cell-in fmt" style="min-width:82px" '+a('biddingStrategy')+'><option value="">(既定)</option><option'+(c.biddingStrategy==='tCPA'?' selected':'')+'>tCPA</option><option'+(c.biddingStrategy==='最大配信'?' selected':'')+'>最大配信</option></select></td>';
1009
+ html+='<td><input class="cell-in num" type="number" '+a('tcpa')+' value="'+esc(c.tcpa??'')+'" placeholder="'+st.project.tcpaDefault+'" style="width:70px"></td>';
1010
+ html+='<td><input class="cell-in" '+a('variable')+' value="'+esc(c.variable||'')+'" style="width:140px"></td>';
1011
+ html+='<td><input class="cell-in mono" '+a('urlNumber')+' value="'+esc(c.urlNumber||'')+'" style="width:54px"></td>';
1012
+ } else {
1013
+ html+='<td><span class="robadge" title="既存キャンペーンのため編集不可">'+esc(c.name||'')+'</span></td>';
1014
+ html+='<td><span class="idpill" style="color:var(--faint)">—</span></td>';
1015
+ html+='<td><span class="idpill" style="color:var(--faint)">—</span></td>';
1016
+ html+='<td><span class="idpill" style="color:var(--faint)">—</span></td>';
1017
+ html+='<td>'+(c.variable?'<span class="robadge">'+esc(c.variable)+'</span>':'<span class="idpill" style="color:var(--faint)">—</span>')+'</td>';
1018
+ html+='<td><span class="idpill" style="color:var(--faint)">—</span></td>';
1019
+ }
920
1020
  html+='<td><span class="rowact"><button class="iconbtn" data-act="cp-dup" data-u="'+c.uid+'" title="複製/量産">⧉</button><button class="iconbtn danger" data-act="cp-del" data-u="'+c.uid+'" title="削除">✕</button></span></td>';
921
1021
  html+='</tr>';
922
1022
  });
923
- html+='<tr><td></td><td colspan="11"><button class="addbtn" data-act="add-campaign">+ キャンペーンを追加</button></td></tr>';
924
- html+='</tbody></table>'; if(!st.campaigns.length) html='<div class="empty">キャンペーンがありません。「+ キャンペーンを追加」から。</div>';
1023
+ html+='<tr><td></td><td colspan="8"><button class="addbtn" data-act="add-campaign">+ 新規キャンペーン</button> <button class="addbtn" data-act="add-campaign-existing">📂 既存キャンペーンを呼び出し</button></td></tr>';
1024
+ html+='</tbody></table>'; if(!st.campaigns.length) html='<div class="empty">キャンペーンがありません。「+ 新規キャンペーン」または「📂 既存キャンペーンを呼び出し」から。</div>';
925
1025
  host.innerHTML=html; restore(); applySelection();
926
1026
  }
927
1027
  function renderTree(){
928
- if(st.campaigns.length===0){ document.getElementById('treeHost').innerHTML='<div class="empty">キャンペーンがありません。シートの「キャンペーン」タブ →「+ キャンペーンを追加」から。</div>'; return; }
1028
+ if(st.campaigns.length===0){ document.getElementById('treeHost').innerHTML='<div class="empty">キャンペーンがありません。</div><div class="treeadd" style="margin-top:10px"><button class="addbtn" data-act="add-campaign">+ 新規キャンペーン</button><button class="addbtn" data-act="add-campaign-existing" style="margin-left:6px">📂 既存キャンペーンを呼び出し</button></div>'; return; }
929
1029
  let html='';
930
1030
  st.campaigns.forEach(c=>{
931
1031
  const col = st.ui.expanded[c.uid]==='c';
932
1032
  const totalAds=c.adGroups.reduce((n,g)=>n+g.ads.length,0);
933
1033
  const ratio = c.adGroups.length===1 ? ('1:1:'+ (c.adGroups[0].ads.length)) : (c.adGroups.length+':'+c.adGroups.map(g=>g.ads.length).join('/'));
934
- const cerr = c.adGroups.some(g=>g.ads.some(a=>!a.isBanner&&resolveAd(a,g).errs.length));
1034
+ const cerr = c.adGroups.some(g=>g.ads.some(a=>resolveAd(a,g).errs.length));
1035
+ const cpEd=!isExistingCampaign(c); // F-2: 編集可否は明示フラグ。バッジ表示はID解決ベースで分離
935
1036
  html+='<div class="cpnode'+(col?' collapsed':'')+'" data-cp="'+c.uid+'">';
936
1037
  html+='<div class="noderow"><button class="caret" data-act="toggle" data-u="'+c.uid+'"><span class="a"></span></button>';
937
- html+='<span class="nodename" contenteditable="true" data-rename="cp" data-u="'+c.uid+'">'+esc(c.name)+'</span>';
938
- html+='<span class="metae"><span class="ratio">'+esc(ratio)+'</span><span class="pill '+(c.mode==='new'?'':'green')+'">'+esc(MODES[c.mode]||c.mode)+'</span>';
939
- html+='<span class="chip">予算 <b>'+esc((c.budget??(c.mode==='new'?st.project.budgetNew:st.project.budgetAsc)).toLocaleString())+'</b>円</span>';
940
- html+='<span class="chip">'+esc(c.biddingStrategy||st.project.biddingStrategyDefault)+' <b>'+esc((c.tcpa??st.project.tcpaDefault))+'</b></span>';
1038
+ // 既存呼び出しCPは名称もrename不可 (基本パラメータ編集不可)
1039
+ html+= cpEd ? '<span class="nodename" contenteditable="true" data-rename="cp" data-u="'+c.uid+'">'+esc(c.name)+'</span>'
1040
+ : '<span class="nodename" title="既存キャンペーンのため編集不可">'+esc(c.name)+'</span>';
1041
+ html+='<span class="metae"><span class="ratio">'+esc(ratio)+'</span>'+newOrIdBadge(isNewCampaign(c), resolveCampaignIdOf(c));
1042
+ // 予算/入札は編集可(新規扱い)のCPのみ表示 (既存呼び出しCPは広告追加専用で予算等は無視)
1043
+ if(cpEd){
1044
+ html+='<span class="chip">予算 <b>'+esc((c.budget??NEW_CP_BUDGET).toLocaleString())+'</b>円</span>';
1045
+ html+='<span class="chip">'+esc(c.biddingStrategy||st.project.biddingStrategyDefault)+' <b>'+esc((c.tcpa??st.project.tcpaDefault))+'</b></span>';
1046
+ }
941
1047
  if(c.variable) html+='<span class="pill purple">変数:'+esc(c.variable)+'</span>';
942
1048
  if(cerr) html+='<span class="pill danger">要修正</span>';
943
1049
  html+='</span>';
944
- html+='<span class="nodeact"><button class="iconbtn" data-act="cp-edit" data-u="'+c.uid+'" title="設定">⚙</button><button class="iconbtn" data-act="cp-dup" data-u="'+c.uid+'" title="複製/量産">⧉</button><button class="iconbtn danger" data-act="cp-del" data-u="'+c.uid+'" title="削除">✕</button></span>';
1050
+ html+='<span class="nodeact">'+(cpEd?'<button class="iconbtn" data-act="cp-edit" data-u="'+c.uid+'" title="設定">⚙</button>':'')+'<button class="iconbtn" data-act="cp-dup" data-u="'+c.uid+'" title="複製/量産">⧉</button><button class="iconbtn danger" data-act="cp-del" data-u="'+c.uid+'" title="削除">✕</button></span>';
945
1051
  html+='</div>';
946
1052
  html+='<div class="agwrap">';
947
1053
  c.adGroups.forEach(g=>{
948
1054
  const gcol = st.ui.expanded[g.uid]==='c';
949
1055
  html+='<div class="agnode'+(gcol?' collapsed':'')+'" data-ag="'+g.uid+'">';
1056
+ const gEd=!isExistingGroup(g); // F-2: 編集可否は明示フラグ。バッジ表示はID解決ベースで分離
950
1057
  html+='<div class="noderow"><button class="caret" data-act="toggle" data-u="'+g.uid+'"><span class="a"></span></button>';
951
- html+='<span class="nodename" contenteditable="true" data-rename="ag" data-u="'+g.uid+'">'+esc(g.name)+'</span>';
1058
+ html+= gEd ? '<span class="nodename" contenteditable="true" data-rename="ag" data-u="'+g.uid+'">'+esc(g.name)+'</span>'
1059
+ : '<span class="nodename" title="既存広告グループのため編集不可">'+esc(g.name)+'</span>';
952
1060
  html+='<span class="metae"><span class="pill mono">【'+esc(g.paramCode||'—')+'】</span><span class="chip">広告 <b>'+g.ads.length+'</b></span>';
953
- const gr=st.refs.groups[g.paramCode]; html+= gr? ('<span class="idpill">'+esc(gr.campaignId)+' / '+esc(gr.adGroupId)+'</span>') : '<span class="idpill warn">新規/未払出</span>';
1061
+ html+= newOrIdBadge(isNewGroup(g), resolveAdGroupIdOf(g));
954
1062
  html+='</span>';
955
- html+='<span class="nodeact"><button class="iconbtn" data-act="ag-param" data-u="'+g.uid+'" title="paramCode編集">#</button><button class="iconbtn" data-act="ag-dup" data-u="'+g.uid+'" title="複製">⧉</button><button class="iconbtn danger" data-act="ag-del" data-u="'+g.uid+'" title="削除">✕</button></span>';
1063
+ html+='<span class="nodeact">'+(gEd?'<button class="iconbtn" data-act="ag-param" data-u="'+g.uid+'" title="paramCode編集">#</button>':'')+'<button class="iconbtn" data-act="ag-dup" data-u="'+g.uid+'" title="複製">⧉</button><button class="iconbtn danger" data-act="ag-del" data-u="'+g.uid+'" title="削除">✕</button></span>';
956
1064
  html+='</div><div class="adwrap">';
957
1065
  g.ads.forEach(a=>{
958
- const r=resolveAd(a,g); const dot=a.isBanner?'warn':(r.errs.length?'err':(r.warns.length?'warn':'ok'));
959
- html+='<div class="adnode'+(r.errs.length?' hasErr':'')+(a.isBanner?' banner':'')+'" data-ad="'+a.uid+'">';
1066
+ const r=resolveAd(a,g); const dot=r.errs.length?'err':(r.warns.length?'warn':'ok');
1067
+ html+='<div class="adnode'+(r.errs.length?' hasErr':'')+'" data-ad="'+a.uid+'">';
960
1068
  html+='<span class="dot '+dot+'"></span>';
961
- html+='<span class="cbadge">'+esc(a.imageNo||'—')+a.format[0].toLowerCase()+'</span>';
1069
+ html+='<span class="cbadge">'+esc(a.imageNo||'—')+(a.format||'Card')[0].toLowerCase()+'</span>';
962
1070
  html+='<span class="adttl">'+(r.errs.includes('タイトル未解決')?'<span style="color:var(--danger)">タイトル未解決</span>':esc(r.titleText||'(未入力)'))+' <span class="sm">'+esc(a.titleNo)+'L'+esc(a.descNo)+'</span></span>';
963
- if(a.isBanner) html+='<span class="pill">手入稿</span>';
964
1071
  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>';
965
1072
  html+='</div>';
966
1073
  });
967
- html+='<button class="addbtn" data-act="ag-addad" data-u="'+g.uid+'">+ 広告を追加</button>';
968
- html+='<button class="addbtn" data-act="ag-addn" data-u="'+g.uid+'" style="margin-left:6px">+ N本まとめて</button>';
1074
+ html+='<div class="addrow"><button class="addbtn" data-act="ag-addad" data-u="'+g.uid+'">+ 広告を追加</button><button class="addbtn" data-act="ag-addn" data-u="'+g.uid+'">+ N本まとめて</button></div>';
969
1075
  html+='</div></div>';
970
1076
  });
971
- html+='<button class="addbtn" data-act="cp-addag" data-u="'+c.uid+'">+ 広告グループを追加</button>';
1077
+ html+='<div class="addrow"><button class="addbtn" data-act="cp-addag" data-u="'+c.uid+'">+ 新規広告グループ</button><button class="addbtn" data-act="cp-addag-existing" data-u="'+c.uid+'">📂 既存広告グループを呼び出し</button></div>';
972
1078
  html+='</div></div>';
973
1079
  });
1080
+ // E-1: ツリー側のキャンペーン追加導線(新規/既存の2択)
1081
+ html+='<div class="treeadd" style="margin-top:10px"><button class="addbtn" data-act="add-campaign">+ 新規キャンペーン</button><button class="addbtn" data-act="add-campaign-existing" style="margin-left:6px">📂 既存キャンペーンを呼び出し</button></div>';
974
1082
  document.getElementById('treeHost').innerHTML=html;
975
1083
  }
976
1084
 
@@ -991,13 +1099,41 @@ function buildDatalists(){
991
1099
  function renderRef(){
992
1100
  const q=(document.getElementById('refSearch').value||'').trim().toLowerCase();
993
1101
  const host=document.getElementById('refHost'); let html='';
994
- if(refTab==='titles'){
995
- const all=Object.keys(st.refs.titles).map(no=>({no:+no,text:st.refs.titles[no]})).filter(e=>String(e.text||'').trim()!=='');
1102
+ // アカウントアセット: 広告グループ/キャンペーンは既存(アカウント)の閲覧のみ → 追加/文言コピーは非表示
1103
+ const editable = (refTab==='titles'||refTab==='descriptions'||refTab==='media');
1104
+ const setDisp=(id,show)=>{ const el=document.getElementById(id); if(el) el.style.display=show?'':'none'; };
1105
+ setDisp('btnRefAdd', editable); setDisp('btnAccCopy', refTab==='titles'||refTab==='descriptions'); setDisp('btnRefReset', editable);
1106
+ if(refTab==='adGroups'){
1107
+ const store=st.refs.adGroups||{};
1108
+ const all=Object.keys(store).map(id=>({id, ...store[id]}));
1109
+ const f=all.filter(e=>!q||String(e.id).includes(q)||String(e.name||'').toLowerCase().includes(q)||String(e.paramCode||'').toLowerCase().includes(q)||String(e.campaignId||'').includes(q)).sort((a,b)=>String(a.id).localeCompare(String(b.id)));
1110
+ document.getElementById('refCount').textContent='広告グループ '+f.length+'/'+all.length+'件';
1111
+ html='<table class="reftable"><thead><tr><th style="width:110px">広告グループID</th><th>広告グループ名</th><th style="width:130px">paramCode</th><th style="width:110px">キャンペーンID</th></tr></thead><tbody>';
1112
+ f.forEach(e=>{ html+='<tr><td class="mono">'+esc(e.id)+'</td><td class="reftext" title="'+esc(e.name||'')+'">'+esc(e.name||'')+'</td><td class="mono">'+esc(e.paramCode||'')+'</td><td class="mono">'+esc(e.campaignId||'')+'</td></tr>'; });
1113
+ html+='</tbody></table>'; if(!f.length) html='<div class="empty">既存の広告グループがありません(アカウントJSONの refs.adGroups)</div>';
1114
+ host.innerHTML=html; return;
1115
+ }
1116
+ if(refTab==='campaigns'){
1117
+ const store=st.refs.campaigns||{};
1118
+ const all=Object.keys(store).map(id=>({id, ...store[id]}));
1119
+ const f=all.filter(e=>!q||String(e.id).includes(q)||String(e.name||'').toLowerCase().includes(q)).sort((a,b)=>String(a.id).localeCompare(String(b.id)));
1120
+ document.getElementById('refCount').textContent='キャンペーン '+f.length+'/'+all.length+'件';
1121
+ html='<table class="reftable"><thead><tr><th style="width:120px">キャンペーンID</th><th>キャンペーン名</th></tr></thead><tbody>';
1122
+ f.forEach(e=>{ html+='<tr><td class="mono">'+esc(e.id)+'</td><td class="reftext" title="'+esc(e.name||'')+'">'+esc(e.name||'')+'</td></tr>'; });
1123
+ html+='</tbody></table>'; if(!f.length) html='<div class="empty">既存のキャンペーンがありません(アカウントJSONの refs.campaigns)</div>';
1124
+ host.innerHTML=html; return;
1125
+ }
1126
+ if(refTab==='titles'||refTab==='descriptions'){
1127
+ const isTitle=refTab==='titles';
1128
+ const store=isTitle?st.refs.titles:(st.refs.descriptions||{});
1129
+ const kind=isTitle?'t':'d';
1130
+ const label=isTitle?'タイトル':'説明文';
1131
+ const all=Object.keys(store).map(no=>({no:+no,text:store[no]})).filter(e=>String(e.text||'').trim()!=='');
996
1132
  const f=all.filter(e=>!q||String(e.no).includes(q)||String(e.text).toLowerCase().includes(q)).sort((a,b)=>a.no-b.no);
997
- document.getElementById('refCount').textContent='タイトル/説明 '+f.length+'/'+all.length+'件';
998
- html='<table class="reftable"><thead><tr><th style="width:70px">番号</th><th>文言(タイトル/説明で共有)</th><th style="width:70px">文字数</th><th style="width:70px"></th></tr></thead><tbody>';
999
- f.forEach(e=>{ const L=glen(e.text); const cls=L>35?'err':(L>20?'warn':''); html+='<tr><td class="mono">'+esc(e.no)+'</td><td class="reftext" title="'+esc(e.text)+'">'+esc(e.text)+'</td><td class="reflen '+cls+'">'+L+'</td><td><button class="iconbtn" data-refedit="t" data-no="'+esc(e.no)+'" title="編集">✎</button><button class="iconbtn danger" data-refdel="t" data-no="'+esc(e.no)+'" title="削除">✕</button></td></tr>'; });
1000
- html+='</tbody></table>'; if(!f.length) html='<div class="empty">該当する文言がありません</div>';
1133
+ document.getElementById('refCount').textContent=label+' '+f.length+'/'+all.length+'件';
1134
+ html='<table class="reftable"><thead><tr><th style="width:70px">番号</th><th>'+esc(label)+'</th><th style="width:70px">文字数</th><th style="width:70px"></th></tr></thead><tbody>';
1135
+ f.forEach(e=>{ const L=glen(e.text); const cls=(isTitle&&L>35)?'err':(isTitle&&L>20?'warn':''); html+='<tr><td class="mono">'+esc(e.no)+'</td><td class="reftext" title="'+esc(e.text)+'">'+esc(e.text)+'</td><td class="reflen '+cls+'">'+L+'</td><td><button class="iconbtn" data-refedit="'+kind+'" data-no="'+esc(e.no)+'" title="編集">✎</button><button class="iconbtn danger" data-refdel="'+kind+'" data-no="'+esc(e.no)+'" title="削除">✕</button></td></tr>'; });
1136
+ html+='</tbody></table>'; if(!f.length) html='<div class="empty">該当する'+esc(label)+'がありません</div>';
1001
1137
  } else {
1002
1138
  const all=Object.keys(st.refs.media).map(no=>({no:+no,m:st.refs.media[no]}));
1003
1139
  const f=all.filter(e=>!q||String(e.no).includes(q)||String(e.m.card||'').includes(q)||String(e.m.sq||'').includes(q)).sort((a,b)=>a.no-b.no);
@@ -1008,12 +1144,13 @@ function renderRef(){
1008
1144
  }
1009
1145
  host.innerHTML=html;
1010
1146
  }
1011
- function refCommit(fn,msg){ snapshot(); fn(); persist(); buildDatalists(); render(); toast(msg); }
1147
+ function refCommit(fn,msg){ snapshot(); fn(); persist(); buildDatalists(); render(); if(typeof saveActiveAccountJson==='function') saveActiveAccountJson(); toast(msg); }
1012
1148
  function refAdd(){
1013
- if(refTab==='titles'){
1014
- openModal({title:'タイトル/説明を登録', sub:'番号→文言。タイトル枠は20字目安・35字上限(説明文は長文可)', fields:[
1149
+ if(refTab==='titles'||refTab==='descriptions'){
1150
+ const isTitle=refTab==='titles'; const label=isTitle?'タイトル':'説明文';
1151
+ openModal({title:label+'を登録', sub:isTitle?'番号→文言。タイトル枠は20字目安・35字上限':'番号→説明文(長文可)', fields:[
1015
1152
  {key:'no',label:'番号',type:'number',value:''},{key:'text',label:'文言',type:'text',value:''}
1016
- ], onOk:v=>{ const no=parseInt(v.no); if(!no){toast('番号を入力してください');return;} const dup=st.refs.titles[no]!=null; refCommit(()=>{ st.refs.titles[no]=v.text; }, dup?('番号'+no+'を上書き'):'登録しました'); }});
1153
+ ], onOk:v=>{ const no=parseInt(v.no); if(!no){toast('番号を入力してください');return;} const store=isTitle?st.refs.titles:st.refs.descriptions; const dup=store[no]!=null; refCommit(()=>{ store[no]=v.text; }, dup?('番号'+no+'を上書き'):'登録しました'); }});
1017
1154
  } else {
1018
1155
  openModal({title:'画像を登録', sub:'画像番号→Card/SQ の mediaID。新規作成した画像をメディアに登録', fields:[
1019
1156
  {key:'no',label:'画像番号',type:'number',value:''},{key:'card',label:'Card mediaID (1200)',type:'text',value:''},{key:'sq',label:'SQ mediaID (1080)',type:'text',value:''}
@@ -1021,45 +1158,104 @@ function refAdd(){
1021
1158
  }
1022
1159
  }
1023
1160
  function refEdit(kind,no){
1024
- if(kind==='t'){ const cur=st.refs.titles[no]; openModal({title:'文言を編集', sub:'番号 '+no, fields:[{key:'text',label:'文言',type:'text',value:cur}], onOk:v=>{ refCommit(()=>{ st.refs.titles[no]=v.text; }, '更新しました'); }}); }
1161
+ if(kind==='t'||kind==='d'){ const store=kind==='t'?st.refs.titles:st.refs.descriptions; const cur=store[no]; openModal({title:(kind==='t'?'タイトル':'説明文')+'を編集', sub:'番号 '+no, fields:[{key:'text',label:'文言',type:'text',value:cur}], onOk:v=>{ refCommit(()=>{ store[no]=v.text; }, '更新しました'); }}); }
1025
1162
  else { const e=st.refs.media[no]||{}; openModal({title:'画像を編集', sub:'画像番号 '+no, fields:[{key:'card',label:'Card mediaID',type:'text',value:e.card||''},{key:'sq',label:'SQ mediaID',type:'text',value:e.sq||''}], onOk:v=>{ refCommit(()=>{ st.refs.media[no]={card:v.card.trim(),sq:v.sq.trim()}; }, '更新しました'); }}); }
1026
1163
  }
1027
- function refDel(kind,no){ refCommit(()=>{ if(kind==='t') delete st.refs.titles[no]; else delete st.refs.media[no]; }, '削除しました'); }
1164
+ function refDel(kind,no){ refCommit(()=>{ if(kind==='t') delete st.refs.titles[no]; else if(kind==='d') delete st.refs.descriptions[no]; else delete st.refs.media[no]; }, '削除しました'); }
1028
1165
 
1029
1166
  /* ============================================================
1030
1167
  SPREADSHEET SELECTION / COPY / PASTE
1031
1168
  ============================================================ */
1032
1169
  const VARF=['format','imageNo','titleNo','descNo','paramCode','articleSlug']; // 広告タブ列
1033
- const VARF_GROUPS=['name','paramCode','adGroupId','bid','deliveryStatus','tg_device','tg_gender','tg_age','tg_region','tg_dayHour','tg_audience','tg_searchKw'];
1034
- const VARF_CAMPAIGNS=['name','campaignId','mode','budget','biddingStrategy','tcpa','status','variable','urlNumber','strategyMemo'];
1170
+ const VARF_GROUPS=['adGroupId','name','paramCode','bid','deliveryStatus','tg_device','tg_gender','tg_age','tg_region','tg_dayHour','tg_audience','tg_searchKw']; // F-6: adGroupId(index0・非編集) → name → paramCode → …
1171
+ const VARF_CAMPAIGNS=['campaignId','name','budget','biddingStrategy','tcpa','variable','urlNumber']; // E-2: campaignId最左・read-only / mode/status/strategyMemo撤去
1035
1172
  function tabCols(){ return sheetTab==='groups'?VARF_GROUPS : sheetTab==='campaigns'?VARF_CAMPAIGNS : VARF; }
1036
1173
  function getVar(item,c){ const f=tabCols()[c]; if(item.kind==='g') return item.g[f]==null?'':String(item.g[f]); if(item.kind==='c') return item.c[f]==null?'':String(item.c[f]); if(f==='paramCode') return item.g.paramCode||''; const v=item.a[f]; return v==null?'':String(v); }
1037
- function setVar(item,c,val){ const f=tabCols()[c]; if(item.kind==='g'){ if(f==='deliveryStatus'){ const s=String(val).trim(); val=(s==='オフ'||s==='オン')?s:''; } item.g[f]=val; return; } if(item.kind==='c'){ if(f==='budget'||f==='tcpa') val=(val===''?null:Number(val)); item.c[f]=val; return; } if(f==='paramCode') item.g.paramCode=val; else if(f==='format') item.a.format=(String(val).toUpperCase()==='SQ'?'SQ':'Card'); else item.a[f]=val; }
1038
- function flatVisible(){ const out=[]; if(sheetTab==='groups'){ st.campaigns.forEach(c=>c.adGroups.forEach(g=>out.push({kind:'g',g,c}))); return out; } if(sheetTab==='campaigns'){ st.campaigns.forEach(c=>out.push({kind:'c',c})); return out; } const filter=document.getElementById('sheetFilter').value; st.campaigns.forEach(c=>c.adGroups.forEach(g=>g.ads.forEach(a=>{ const r=resolveAd(a,g); const hasErr=r.errs.length>0; if(filter==='err'&&!hasErr)return; if(filter==='banner'&&!a.isBanner)return; out.push({kind:'a',a,g,c}); }))); return out; }
1174
+ function setVar(item,c,val){ const f=tabCols()[c]; if(item.kind==='g'){ if(isExistingGroup(item.g)) return; /* F-2: 既存呼び出しAGは基本パラメータ編集不可 */ if(f==='adGroupId') return; /* E-4: read-only */ if(f==='deliveryStatus'){ const s=String(val).trim(); val=(s==='オフ'||s==='オン')?s:''; } item.g[f]=val; if(f==='paramCode') autoResolveGroupIds(item.g,item.c); return; } if(item.kind==='c'){ if(isExistingCampaign(item.c)) return; /* F-2: 既存呼び出しCPは基本パラメータ編集不可 */ if(f==='campaignId') return; /* E-2: read-only */ if(f==='budget'||f==='tcpa') val=numOrNull(val); item.c[f]=val; return; } if(f==='paramCode'){ if(isExistingGroup(item.g)) return; /* F-2: 既存呼び出しAGのparamCodeは編集不可 */ item.g.paramCode=val; autoResolveGroupIds(item.g,item.c); } else if(f==='format') item.a.format=(String(val).toUpperCase()==='SQ'?'SQ':'Card'); else item.a[f]=val; }
1175
+ function flatVisible(){ const out=[]; if(sheetTab==='groups'){ st.campaigns.forEach(c=>c.adGroups.forEach(g=>out.push({kind:'g',g,c}))); return out; } if(sheetTab==='campaigns'){ st.campaigns.forEach(c=>out.push({kind:'c',c})); return out; } const filter=document.getElementById('sheetFilter').value; st.campaigns.forEach(c=>c.adGroups.forEach(g=>g.ads.forEach(a=>{ const r=resolveAd(a,g); const hasErr=r.errs.length>0; if(filter==='err'&&!hasErr)return; out.push({kind:'a',a,g,c}); }))); return out; }
1039
1176
  let selA=null, selB=null, dragging=false;
1040
1177
  function clearSel(){ selA=selB=null; applySelection(); }
1041
1178
  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)}; }
1042
- 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) 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'); }); }
1179
+ 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(); }
1043
1180
  function copySelText(){ const rg=selRange(); if(!rg) return null; const vis=flatVisible(); 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'); }
1044
1181
  function pasteGrid(txt){
1045
1182
  const rg=selRange(); if(!rg){ toast('貼付先セルを選択してください'); return; }
1046
1183
  const rows=txt.replace(/\r/g,'').replace(/\n+$/,'').split('\n').map(l=>l.split('\t'));
1047
1184
  const vis=flatVisible(); const maxc=tabCols().length-1; if(!vis[rg.r0]){ toast('貼付先がありません'); return; }
1048
1185
  mutate(()=>{
1049
- if(sheetTab==='ads'){ const anchor=vis[rg.r0]; const group=anchor.g; const startIdx=group.ads.indexOf(anchor.a); rows.forEach((cols,i)=>{ let ad=group.ads[startIdx+i]; if(!ad){ ad=newAd(); group.ads.push(ad); } cols.forEach((val,j)=>{ const cc=rg.c0+j; if(cc>maxc) return; setVar({kind:'a',a:ad,g:group},cc,val); }); }); }
1050
- else { const anchor=vis[rg.r0]; rows.forEach((cols,i)=>{ let it=vis[rg.r0+i]; if(!it){ if(sheetTab==='groups'){ const ng={uid:uid('g'),name:'AG'+(anchor.c.adGroups.length+1),paramCode:'',ads:[newAd()]}; anchor.c.adGroups.push(ng); it={kind:'g',g:ng,c:anchor.c}; } else { const nc={uid:uid('c'),name:'新規キャンペーン',campaignId:'',mode:'new',variable:'',urlNumber:'',budget:null,biddingStrategy:'',tcpa:null,status:'オフ',strategyMemo:'',adGroups:[{uid:uid('g'),name:'AG1',paramCode:'',ads:[newAd()]}]}; st.campaigns.push(nc); it={kind:'c',c:nc}; } } cols.forEach((val,j)=>{ const cc=rg.c0+j; if(cc>maxc) return; setVar(it,cc,val); }); }); }
1186
+ if(sheetTab==='ads'){
1187
+ // B8: 可視行基準に統一 (グループ境界・絞り込み表示を尊重)。不足分は末尾の可視グループに新規広告を追加。
1188
+ const anchor=vis[rg.r0]; let lastGroup=anchor.g;
1189
+ rows.forEach((cols,i)=>{
1190
+ let it=vis[rg.r0+i];
1191
+ if(it){ lastGroup=it.g; }
1192
+ else { const ad=newAd(); lastGroup.ads.push(ad); it={kind:'a',a:ad,g:lastGroup}; }
1193
+ cols.forEach((val,j)=>{ const cc=rg.c0+j; if(cc>maxc) return; setVar(it,cc,val); });
1194
+ });
1195
+ }
1196
+ else { const anchor=vis[rg.r0]; rows.forEach((cols,i)=>{ let it=vis[rg.r0+i]; if(!it){ if(sheetTab==='groups'){ const ng={uid:uid('g'),name:'AG'+(anchor.c.adGroups.length+1),paramCode:'',ads:[newAd()]}; anchor.c.adGroups.push(ng); it={kind:'g',g:ng,c:anchor.c}; } else { const nc=makeNewCampaign(); st.campaigns.push(nc); it={kind:'c',c:nc}; } } cols.forEach((val,j)=>{ const cc=rg.c0+j; if(cc>maxc) return; setVar(it,cc,val); }); }); }
1051
1197
  });
1052
1198
  clearSel(); toast(rows.length+'行を貼り付け');
1053
1199
  }
1054
1200
  function dupSelected(){
1055
1201
  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; }
1056
1202
  mutate(()=>{ items.forEach(it=>{
1057
- if(it.kind==='g'){ const ng=JSON.parse(JSON.stringify(it.g)); ng.uid=uid('g'); ng.name=it.g.name+'_copy'; ng.adGroupId=''; ng.ads.forEach(a=>a.uid=uid('a')); const idx=it.c.adGroups.indexOf(it.g); it.c.adGroups.splice(idx+1,0,ng); }
1203
+ if(it.kind==='g'){ const ng=JSON.parse(JSON.stringify(it.g)); ng.uid=uid('g'); ng.name=it.g.name+'_copy'; ng.adGroupId=''; ng.isExisting=false; ng.ads.forEach(a=>a.uid=uid('a')); const idx=it.c.adGroups.indexOf(it.g); it.c.adGroups.splice(idx+1,0,ng); }
1058
1204
  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); }
1059
1205
  else { const idx=it.g.ads.indexOf(it.a); it.g.ads.splice(idx+1,0,cloneAd(it.a,null)); }
1060
1206
  }); }); clearSel(); toast(items.length+'件を複製');
1061
1207
  }
1062
1208
 
1209
+ /* ---- A-2: paramCode 連番採番 (複数選択→連番一括入力) ---- */
1210
+ function paramCodeColIndex(){ return sheetTab==='groups'?VARF_GROUPS.indexOf('paramCode') : sheetTab==='ads'?VARF.indexOf('paramCode') : -1; }
1211
+ /* 選択範囲が交差する「連番可能な列」を返す。{field, idx, mode:'group'|'ad'} */
1212
+ function seqColInfo(){
1213
+ const rg=selRange(); if(!rg) return null;
1214
+ if(sheetTab==='ads'){
1215
+ const map={}; ['imageNo','titleNo','descNo'].forEach(f=>{ const i=VARF.indexOf(f); if(i>=0) map[i]=f; }); const pcI=VARF.indexOf('paramCode'); if(pcI>=0) map[pcI]='paramCode';
1216
+ for(const ci of Object.keys(map).map(Number).sort((a,b)=>a-b)){ if(rg.c0<=ci && rg.c1>=ci) return {field:map[ci], idx:ci, mode: map[ci]==='paramCode'?'group':'ad'}; }
1217
+ } else if(sheetTab==='groups'){
1218
+ const pc=VARF_GROUPS.indexOf('paramCode'); if(pc>=0 && rg.c0<=pc && rg.c1>=pc) return {field:'paramCode', idx:pc, mode:'group'};
1219
+ }
1220
+ return null;
1221
+ }
1222
+ /* 連番ボタンの表示制御: 連番可能な列(paramCode/画像番号/タイトル番号/説明文番号)を含み対象2件以上で表示 */
1223
+ function updateSeqBtn(){
1224
+ const btn=document.getElementById('btnSeqParam'); if(!btn) return;
1225
+ const rg=selRange(); const sc=seqColInfo(); let show=false;
1226
+ if(rg && sc && rg.r1>rg.r0){
1227
+ const vis=flatVisible();
1228
+ if(sc.mode==='group'){ const seen=new Set(); for(let r=rg.r0;r<=rg.r1;r++){ const it=vis[r]; if(it&&it.g) seen.add(it.g.uid); } show=seen.size>=2; }
1229
+ else { let n=0; for(let r=rg.r0;r<=rg.r1;r++){ const it=vis[r]; if(it&&it.a) n++; } show=n>=2; }
1230
+ }
1231
+ btn.style.display=show?'':'none';
1232
+ }
1233
+ function applySequentialParamCodes(){
1234
+ const rg=selRange(); const sc=seqColInfo();
1235
+ if(!rg||!sc){ toast('連番できる列(paramCode / 画像番号 / タイトル番号 / 説明文番号)のセルを複数選択してください'); return; }
1236
+ const vis=flatVisible();
1237
+ if(sc.mode==='group'){
1238
+ const seen=new Set(); const groups=[];
1239
+ for(let r=rg.r0;r<=rg.r1;r++){ const it=vis[r]; if(!it||!it.g) continue; if(isExistingGroup(it.g)) continue; if(seen.has(it.g.uid)) continue; seen.add(it.g.uid); groups.push({g:it.g, c:it.c}); }
1240
+ if(groups.length<2){ toast('連番対象の広告グループが2件以上必要です'); return; }
1241
+ const m=String(groups[0].g.paramCode||'').trim().match(/^(.*?)(\d+)\s*$/);
1242
+ if(!m){ toast('基点となる paramCode を先頭セルに入力してください(例 jv_Cac_148)'); return; }
1243
+ const prefix=m[1], startNum=parseInt(m[2],10), width=m[2].length;
1244
+ mutate(()=>{ groups.forEach((pair,i)=>{ if(i===0){ autoResolveGroupIds(pair.g,pair.c); return; } pair.g.paramCode=prefix+String(startNum+i).padStart(width,'0'); autoResolveGroupIds(pair.g,pair.c); }); });
1245
+ toast((groups.length-1)+'グループに連番を採番しました('+prefix+(startNum+1)+'〜)');
1246
+ } else {
1247
+ const rows=[];
1248
+ for(let r=rg.r0;r<=rg.r1;r++){ const it=vis[r]; if(!it||!it.a) continue; if(isExistingGroup(it.g)) continue; rows.push(it); }
1249
+ if(rows.length<2){ toast('連番対象の広告が2件以上必要です'); return; }
1250
+ const lbl={imageNo:'画像番号',titleNo:'タイトル番号',descNo:'説明文番号'}[sc.field];
1251
+ const m=String(rows[0].a[sc.field]||'').trim().match(/^(.*?)(\d+)\s*$/);
1252
+ if(!m){ toast('基点の'+lbl+'を先頭セルに入力してください(例 954)'); return; }
1253
+ const prefix=m[1], startNum=parseInt(m[2],10), width=m[2].length;
1254
+ mutate(()=>{ rows.forEach((it,i)=>{ if(i===0) return; it.a[sc.field]=prefix+String(startNum+i).padStart(width,'0'); }); });
1255
+ toast((rows.length-1)+'件の'+lbl+'に連番を採番しました('+prefix+(startNum+1)+'〜)');
1256
+ }
1257
+ }
1258
+
1063
1259
  /* ---- ▼ 右スライドサイドバー(一覧から番号を選ぶ) ---- */
1064
1260
  let drawerTarget=null;
1065
1261
  function drawerTypeForField(f){ return f==='imageNo'?'media':'titles'; }
@@ -1074,6 +1270,91 @@ function openDrawerFor(u,f){
1074
1270
  document.getElementById('refDrawer').classList.add('open'); // 背景は暗くしない(バックドロップ不使用)
1075
1271
  }
1076
1272
  function openRefDrawer(btn){ openDrawerFor(btn.dataset.u, btn.dataset.f); }
1273
+ /* E-3: 既存キャンペーン呼び出しドロワー (アカウントJSON refs.campaigns の一覧) */
1274
+ function openCampaignRefDrawer(){
1275
+ const isOpen=document.getElementById('refDrawer').classList.contains('open');
1276
+ const same=drawerTarget && drawerTarget.type==='cpref';
1277
+ if(isOpen && same){ closeDrawer(); return; }
1278
+ drawerTarget={type:'cpref'};
1279
+ document.getElementById('drawerSearch').value='';
1280
+ renderDrawer();
1281
+ document.getElementById('refDrawer').classList.add('open');
1282
+ }
1283
+ /* F-5: 既存広告グループ呼び出しドロワー (アカウントアセット refs.adGroups 全体。campaignIdフィルタ撤廃) */
1284
+ function openAdGroupRefDrawer(){
1285
+ const isOpen=document.getElementById('refDrawer').classList.contains('open');
1286
+ const same=drawerTarget && drawerTarget.type==='agref';
1287
+ if(isOpen && same){ closeDrawer(); return; }
1288
+ drawerTarget={type:'agref'};
1289
+ document.getElementById('drawerSearch').value='';
1290
+ renderDrawer();
1291
+ document.getElementById('refDrawer').classList.add('open');
1292
+ }
1293
+ /* F-6: プラン内キャンペーン選択ドロワー。ctx.gUid=グループ親付け替え / ctx.mode==='addNew'=新規AG追加先選択 */
1294
+ function openCampaignPickDrawer(ctx){
1295
+ if(!st.campaigns.length){ toast('先にキャンペーンを追加してください'); return; }
1296
+ drawerTarget=Object.assign({type:'cppick'}, ctx||{});
1297
+ document.getElementById('drawerSearch').value='';
1298
+ renderDrawer();
1299
+ document.getElementById('refDrawer').classList.add('open');
1300
+ }
1301
+ /* F-4: プラン内広告グループ選択ドロワー (広告の所属変更) */
1302
+ function openAdGroupPickDrawer(adUid){
1303
+ const anyGroup=st.campaigns.some(c=>c.adGroups.length);
1304
+ if(!anyGroup){ toast('先に広告グループを追加してください'); return; }
1305
+ drawerTarget={type:'agpick', adUid};
1306
+ document.getElementById('drawerSearch').value='';
1307
+ renderDrawer();
1308
+ document.getElementById('refDrawer').classList.add('open');
1309
+ }
1310
+ /* 既に呼び出し済みの campaignId 集合 (二重追加防止) */
1311
+ function planCampaignIdSet(){ const s=new Set(); st.campaigns.forEach(c=>{ const id=resolveCampaignIdOf(c); if(id) s.add(String(id)); }); return s; }
1312
+ function planAdGroupIdSet(){ const s=new Set(); st.campaigns.forEach(c=>c.adGroups.forEach(g=>{ const id=resolveAdGroupIdOf(g); if(id) s.add(String(id)); })); return s; }
1313
+ function addExistingCampaign(cpId){
1314
+ cpId=String(cpId);
1315
+ if(planCampaignIdSet().has(cpId)){ toast('キャンペーン '+cpId+' は既にプランにあります'); return; }
1316
+ const meta=(st.refs.campaigns||{})[cpId]||{};
1317
+ mutate(()=>st.campaigns.push(makeExistingCampaign(cpId, meta.name||('CP '+cpId))));
1318
+ toast('既存キャンペーン '+cpId+' を呼び出しました');
1319
+ closeDrawer();
1320
+ }
1321
+ /* F-5/F-6: 既存AG呼び出し。所属キャンペーンは AG.meta.campaignId に一致するプラン内CP、無ければ最後のCP (キャンペーン列で付け替え可) */
1322
+ function addExistingAdGroup(agId){
1323
+ agId=String(agId);
1324
+ if(planAdGroupIdSet().has(agId)){ toast('広告グループ '+agId+' は既にプランにあります'); return; }
1325
+ if(!st.campaigns.length){ toast('先にキャンペーンを追加してください'); return; }
1326
+ const meta=(st.refs.adGroups||{})[agId]||{};
1327
+ let target=null;
1328
+ if(meta.campaignId){ target=st.campaigns.find(c=>String(resolveCampaignIdOf(c))===String(meta.campaignId)); }
1329
+ if(!target) target=st.campaigns[st.campaigns.length-1];
1330
+ // F-6: ID起点で名前・paramCode等を refs.adGroups から自動で埋める
1331
+ mutate(()=>target.adGroups.push({uid:uid('g'), name:meta.name||('AG '+agId), paramCode:meta.paramCode||'', adGroupId:agId, isExisting:true, ads:[newAd()]}));
1332
+ toast('既存広告グループ '+agId+' を呼び出しました('+esc(target.name)+')');
1333
+ closeDrawer();
1334
+ }
1335
+ /* F-6: グループを別キャンペーンへ付け替え */
1336
+ function moveGroupToCampaign(gUid, targetCUid){
1337
+ const r=findGroup(gUid); const tc=findCp(targetCUid); if(!r||!tc) return;
1338
+ if(r.c.uid===targetCUid){ closeDrawer(); return; }
1339
+ mutate(()=>{ r.c.adGroups.splice(r.i,1); tc.c.adGroups.push(r.g); });
1340
+ toast('広告グループを「'+esc(tc.c.name)+'」へ移動');
1341
+ closeDrawer();
1342
+ }
1343
+ /* F-5: 選択キャンペーンに新規空グループを追加 */
1344
+ function addNewAdGroupToCampaign(cUid){
1345
+ const r=findCp(cUid); if(!r) return;
1346
+ mutate(()=>r.c.adGroups.push({uid:uid('g'),name:'AG'+(r.c.adGroups.length+1),paramCode:'',ads:[newAd()]}));
1347
+ toast('新規広告グループを「'+esc(r.c.name)+'」に追加');
1348
+ closeDrawer();
1349
+ }
1350
+ /* F-4: 広告を別グループへ移動 */
1351
+ function moveAdToGroup(adUid, targetGUid){
1352
+ const r=findAd(adUid); const tg=findGroup(targetGUid); if(!r||!tg) return;
1353
+ if(r.g.uid===targetGUid){ closeDrawer(); return; }
1354
+ mutate(()=>{ r.g.ads.splice(r.i,1); tg.g.ads.push(r.a); });
1355
+ toast('広告を「'+esc(tg.g.name)+'」へ移動');
1356
+ closeDrawer();
1357
+ }
1077
1358
  /* YDA広告グループ ターゲティング設定値(Web調査ベース・複数選択可) */
1078
1359
  const TG_OPTIONS = {
1079
1360
  tg_device:{label:'デバイス', opts:['全てのデバイス','PC','スマートフォン','タブレット']},
@@ -1104,6 +1385,42 @@ function renderDrawer(){
1104
1385
  if(!drawerTarget) return;
1105
1386
  const q=(document.getElementById('drawerSearch').value||'').trim().toLowerCase();
1106
1387
  const titleEl=document.getElementById('drawerTitle'), listEl=document.getElementById('drawerList'), tgtEl=document.getElementById('drawerTarget');
1388
+ if(drawerTarget.type==='cpref'){
1389
+ // E-3: 既存キャンペーン一覧 (refs.campaigns)。プラン追加済みはグレーアウト
1390
+ const cps=st.refs.campaigns||{}; const inPlan=planCampaignIdSet();
1391
+ titleEl.textContent='既存キャンペーンを呼び出し';
1392
+ const ids=Object.keys(cps).sort();
1393
+ const shown=ids.filter(id=>!q||String(id).includes(q)||String((cps[id]&&cps[id].name)||'').toLowerCase().includes(q));
1394
+ tgtEl.textContent='アカウント: '+(st.project.accountName||'—')+' / '+ids.length+'件';
1395
+ let h=''; shown.forEach(id=>{ const nm=(cps[id]&&cps[id].name)||''; const dup=inPlan.has(String(id)); h+='<div class="ddrow'+(dup?' cur':'')+'"'+(dup?'':' data-cpid="'+esc(id)+'"')+' title="'+esc(nm)+'"><span class="ddno">'+esc(id)+'</span><span class="ddtx">'+esc(nm)+(dup?' <span class="pill">追加済</span>':'')+'</span></div>'; });
1396
+ listEl.innerHTML=h||'<div class="empty" style="padding:24px">既存キャンペーンがありません(アカウントJSONの refs.campaigns)</div>'; return;
1397
+ }
1398
+ if(drawerTarget.type==='agref'){
1399
+ // F-5: アカウントアセット全体の既存広告グループ (refs.adGroups)。追加済みはグレーアウト
1400
+ const ags=st.refs.adGroups||{}; const inPlan=planAdGroupIdSet();
1401
+ titleEl.textContent='既存広告グループを呼び出し';
1402
+ const ids=Object.keys(ags).sort();
1403
+ const shown=ids.filter(id=>!q||String(id).includes(q)||String((ags[id]&&ags[id].name)||'').toLowerCase().includes(q)||String((ags[id]&&ags[id].paramCode)||'').toLowerCase().includes(q)||String((ags[id]&&ags[id].campaignId)||'').includes(q));
1404
+ tgtEl.textContent='アカウントアセット全体 / '+ids.length+'件';
1405
+ let h=''; shown.forEach(id=>{ const m=ags[id]||{}; const dup=inPlan.has(String(id)); h+='<div class="ddrow'+(dup?' cur':'')+'"'+(dup?'':' data-agid="'+esc(id)+'"')+' title="'+esc(m.name||'')+'"><span class="ddno">'+esc(id)+'</span><span class="ddtx">'+(m.paramCode?'【'+esc(m.paramCode)+'】':'')+esc(m.name||'')+(dup?' <span class="pill">追加済</span>':'')+'</span></div>'; });
1406
+ listEl.innerHTML=h||'<div class="empty" style="padding:24px">既存広告グループがありません(アカウントアセットの refs.adGroups)</div>'; return;
1407
+ }
1408
+ if(drawerTarget.type==='cppick'){
1409
+ // F-6: プラン内キャンペーン選択 (親付け替え or 新規AG追加先)
1410
+ titleEl.textContent = drawerTarget.mode==='addNew' ? '新規広告グループの追加先キャンペーン' : '広告グループのキャンペーンを選択';
1411
+ tgtEl.textContent='プラン内キャンペーン / '+st.campaigns.length+'件';
1412
+ let curCUid=null; if(drawerTarget.gUid){ const r=findGroup(drawerTarget.gUid); if(r) curCUid=r.c.uid; }
1413
+ let h=''; st.campaigns.forEach((c,i)=>{ if(q&&!String(c.name||'').toLowerCase().includes(q)) return; const cur=c.uid===curCUid; h+='<div class="ddrow'+(cur?' cur':'')+'" data-cppick="'+esc(c.uid)+'" title="'+esc(c.name)+'"><span class="ddno">'+(i+1)+'</span><span class="ddtx">'+esc(c.name||'(無名)')+(cur?' <span class="pill">現在</span>':'')+'</span></div>'; });
1414
+ listEl.innerHTML=h||'<div class="empty" style="padding:24px">キャンペーンがありません</div>'; return;
1415
+ }
1416
+ if(drawerTarget.type==='agpick'){
1417
+ // F-4: プラン内広告グループ選択 (広告の所属変更)
1418
+ titleEl.textContent='広告の所属グループを選択';
1419
+ let curGUid=null; { const r=findAd(drawerTarget.adUid); if(r) curGUid=r.g.uid; }
1420
+ let n=0, h=''; st.campaigns.forEach(c=>c.adGroups.forEach(g=>{ n++; const label=c.name+' / '+g.name+(g.paramCode?('【'+g.paramCode+'】'):''); if(q&&!label.toLowerCase().includes(q)) return; const cur=g.uid===curGUid; h+='<div class="ddrow'+(cur?' cur':'')+'" data-agpick="'+esc(g.uid)+'" title="'+esc(label)+'"><span class="ddno">'+n+'</span><span class="ddtx">'+esc(label)+(cur?' <span class="pill">現在</span>':'')+'</span></div>'; }));
1421
+ tgtEl.textContent='プラン内の広告グループ / '+n+'件';
1422
+ listEl.innerHTML=h||'<div class="empty" style="padding:24px">広告グループがありません</div>'; return;
1423
+ }
1107
1424
  if(drawerTarget.type==='tg'){
1108
1425
  const cfg=TG_OPTIONS[drawerTarget.field]; const r=findGroup(drawerTarget.gUid); const g=r?r.g:null;
1109
1426
  const cur=(g&&g[drawerTarget.field]?String(g[drawerTarget.field]):'').split(',').map(s=>s.trim()).filter(Boolean);
@@ -1117,8 +1434,10 @@ function renderDrawer(){
1117
1434
  tgtEl.textContent='入力先: '+fieldName+(curNo?(' / 現在 '+curNo):' / 未設定');
1118
1435
  let html='';
1119
1436
  if(drawerTarget.type==='titles'){
1120
- titleEl.textContent=(drawerTarget.f==='descNo'?'説明文':'タイトル')+'一覧(文言ライブラリ)';
1121
- Object.keys(st.refs.titles).sort((a,b)=>a-b).forEach(no=>{ const t=st.refs.titles[no]; if(String(t||'').trim()==='') return; if(q&&!(String(no).includes(q)||String(t).toLowerCase().includes(q))) return; html+='<div class="ddrow'+(String(no)===curNo?' cur':'')+'" data-no="'+esc(no)+'"><span class="ddno">'+esc(no)+'</span><span class="ddtx">'+esc(t)+'</span></div>'; });
1437
+ const isDesc=drawerTarget.f==='descNo';
1438
+ const store=isDesc?(st.refs.descriptions||{}):st.refs.titles;
1439
+ titleEl.textContent=(isDesc?'説明文':'タイトル')+'一覧(文言ライブラリ)';
1440
+ Object.keys(store).sort((a,b)=>a-b).forEach(no=>{ const t=store[no]; if(String(t||'').trim()==='') return; if(q&&!(String(no).includes(q)||String(t).toLowerCase().includes(q))) return; html+='<div class="ddrow'+(String(no)===curNo?' cur':'')+'" data-no="'+esc(no)+'"><span class="ddno">'+esc(no)+'</span><span class="ddtx">'+esc(t)+'</span></div>'; });
1122
1441
  } else {
1123
1442
  titleEl.textContent='登録画像 一覧';
1124
1443
  Object.keys(st.refs.media).sort((a,b)=>a-b).forEach(no=>{ const m=st.refs.media[no]; if(q&&!(String(no).includes(q)||String(m.card||'').includes(q)||String(m.sq||'').includes(q))) return; html+='<div class="ddrow'+(String(no)===curNo?' cur':'')+'" data-no="'+esc(no)+'"><span class="ddno">'+esc(no)+'</span>'+newBadge('media',no)+'<span class="ddtx">Card '+esc(m.card||'—')+' / SQ '+esc(m.sq||'—')+'</span></div>'; });
@@ -1143,8 +1462,7 @@ function drawerPick(no){
1143
1462
  ============================================================ */
1144
1463
  function setField(u,f,val){
1145
1464
  const found=findAd(u); if(!found) return;
1146
- if(f==='paramCode'){ found.g.paramCode=val; } // group-level
1147
- else if(f==='isBanner'){ found.a.isBanner=val; }
1465
+ if(f==='paramCode'){ if(isExistingGroup(found.g)) return; /* F-2: 既存呼び出しAGのparamCodeは編集不可 */ found.g.paramCode=val; autoResolveGroupIds(found.g,found.c); } // group-level
1148
1466
  else { found.a[f]=val; }
1149
1467
  }
1150
1468
  document.addEventListener('change', e=>{
@@ -1155,15 +1473,15 @@ document.addEventListener('change', e=>{
1155
1473
  /* 広告グループ/キャンペーン タブの一覧編集 */
1156
1474
  document.addEventListener('change', e=>{
1157
1475
  const t=e.target; if(!t.dataset) return;
1158
- if(t.dataset.gu && t.dataset.gf){ const gf=t.dataset.gf, val=t.value; mutate(()=>{ const r=findGroup(t.dataset.gu); if(r) r.g[gf]=val; }); return; }
1159
- if(t.dataset.cu && t.dataset.cf){ const cf=t.dataset.cf; let val=t.value; if(cf==='budget'||cf==='tcpa') val=(val===''?null:Number(val)); mutate(()=>{ const r=findCp(t.dataset.cu); if(r) r.c[cf]=val; }); return; }
1476
+ if(t.dataset.gu && t.dataset.gf){ const gf=t.dataset.gf, val=t.value; mutate(()=>{ const r=findGroup(t.dataset.gu); if(r && !isExistingGroup(r.g)){ r.g[gf]=val; if(gf==='paramCode') autoResolveGroupIds(r.g,r.c); } }); return; } // F-2: 既存呼び出しAGは基本パラメータ編集不可
1477
+ if(t.dataset.cu && t.dataset.cf){ const cf=t.dataset.cf; let val=t.value; if(cf==='budget'||cf==='tcpa') val=numOrNull(val); mutate(()=>{ const r=findCp(t.dataset.cu); if(r && !isExistingCampaign(r.c)) r.c[cf]=val; }); return; } // F-2: 既存呼び出しCPは基本パラメータ編集不可
1160
1478
  });
1161
1479
 
1162
1480
  /* rename via contenteditable (tree) */
1163
1481
  document.addEventListener('blur', e=>{
1164
1482
  const t=e.target; if(!t.dataset||!t.dataset.rename) return;
1165
1483
  const val=t.textContent.trim();
1166
- mutate(()=>{ if(t.dataset.rename==='cp'){ const r=findCp(t.dataset.u); if(r) r.c.name=val; } else { const r=findGroup(t.dataset.u); if(r) r.g.name=val; } });
1484
+ mutate(()=>{ if(t.dataset.rename==='cp'){ const r=findCp(t.dataset.u); if(r && !isExistingCampaign(r.c)) r.c.name=val; } else { const r=findGroup(t.dataset.u); if(r && !isExistingGroup(r.g)) r.g.name=val; } }); // F-2: 既存呼び出しCP/AGは改名不可
1167
1485
  }, true);
1168
1486
  document.addEventListener('keydown', e=>{ if(e.target.dataset&&e.target.dataset.rename&&e.key==='Enter'){ e.preventDefault(); e.target.blur(); } });
1169
1487
 
@@ -1180,12 +1498,26 @@ document.addEventListener('click', e=>{
1180
1498
  if(act==='ag-del'){ const r=findGroup(u); if(r) mutate(()=>r.c.adGroups.splice(r.i,1)); return; }
1181
1499
  if(act==='ag-param'){ editParam(u); return; }
1182
1500
  if(act==='cp-addag'){ const r=findCp(u); if(r) mutate(()=>r.c.adGroups.push({uid:uid('g'),name:'AG'+(r.c.adGroups.length+1),paramCode:'',ads:[newAd()]})); return; }
1501
+ if(act==='cp-addag-existing'){ openAdGroupRefDrawer(); return; } // F-5: アカウントアセット全体から
1183
1502
  if(act==='cp-edit'){ editCampaign(u); return; }
1184
1503
  if(act==='cp-dup'){ dupCampaign(u); return; }
1185
1504
  if(act==='cp-del'){ const r=findCp(u); if(r) mutate(()=>st.campaigns.splice(r.i,1)); return; }
1186
1505
  if(act==='add-campaign'){ addCampaign(); return; }
1506
+ if(act==='add-campaign-existing'){ openCampaignRefDrawer(); return; }
1507
+ // F-5: 広告グループタブ最下部の統合追加行 (追加時にキャンペーン選択)
1508
+ if(act==='add-adgroup'){ openCampaignPickDrawer({mode:'addNew'}); return; }
1509
+ if(act==='add-adgroup-existing'){ openAdGroupRefDrawer(); return; }
1510
+ // F-4: 広告タブ最下部の広告追加 (既定=最後のグループ)
1511
+ if(act==='add-ad-bottom'){ addAdBottom(); return; }
1187
1512
  });
1188
- function addCampaign(){ mutate(()=>st.campaigns.push({uid:uid('c'),name:'新規キャンペーン',campaignId:'',mode:'new',variable:'',urlNumber:suggestUrlNumber(),budget:null,biddingStrategy:'',tcpa:null,status:'オフ',strategyMemo:'',adGroups:[{uid:uid('g'),name:'AG1',paramCode:'',ads:[newAd()]}]})); toast('キャンペーンを追加'); }
1513
+ /* F-4: 広告タブ最下部「広告を追加」→ プラン内の最後のグループに追加 */
1514
+ function addAdBottom(){
1515
+ const cps=st.campaigns; if(!cps.length){ toast('先にキャンペーンを追加してください'); return; }
1516
+ let g=null; for(let i=cps.length-1;i>=0 && !g;i--){ const ags=cps[i].adGroups; if(ags.length) g=ags[ags.length-1]; }
1517
+ if(!g){ toast('先に広告グループを追加してください'); return; }
1518
+ mutate(()=>g.ads.push(newAd())); toast('広告を追加('+esc(g.name)+')');
1519
+ }
1520
+ function addCampaign(){ mutate(()=>st.campaigns.push(makeNewCampaign({urlNumber:suggestUrlNumber()}))); toast('新規キャンペーンを追加'); }
1189
1521
 
1190
1522
  /* ---- clone helpers ---- */
1191
1523
  function cloneAd(a, clear){ const n=newAd(Object.assign({},a)); n.uid=uid('a'); if(clear){ clear.forEach(k=>{ if(k==='creative'){n.imageNo='';n.titleNo='';n.descNo='';} else n[k]=''; }); } return n; }
@@ -1199,10 +1531,11 @@ function dupGroup(u){
1199
1531
  openModal({title:'広告グループを複製', sub:r.g.name+' を複製します', fields:[
1200
1532
  {key:'param',label:'新しい paramCode(空ならコピー)',type:'text',value:''},
1201
1533
  {key:'clear',label:'',type:'check',value:true,text:'広告の中身(画像/タイトル/説明文)をクリアして器だけ残す'}
1202
- ], onOk:v=>{ mutate(()=>{ const ng=JSON.parse(JSON.stringify(r.g)); ng.uid=uid('g'); ng.name=r.g.name+'_copy'; ng.paramCode=(v.param||r.g.paramCode); ng.adGroupId=''; ng.ads=r.g.ads.map(a=>cloneAd(a, v.clear?['creative']:null)); r.c.adGroups.splice(r.i+1,0,ng); }); toast('複製しました'); }});
1534
+ ], onOk:v=>{ mutate(()=>{ const ng=JSON.parse(JSON.stringify(r.g)); ng.uid=uid('g'); ng.name=r.g.name+'_copy'; ng.paramCode=(v.param||r.g.paramCode); ng.adGroupId=''; ng.isExisting=false; ng.ads=r.g.ads.map(a=>cloneAd(a, v.clear?['creative']:null)); r.c.adGroups.splice(r.i+1,0,ng); }); toast('複製しました'); }});
1203
1535
  }
1204
1536
  function editParam(u){
1205
1537
  const r=findGroup(u); if(!r) return; const g=r.g;
1538
+ if(isExistingGroup(g)){ toast('既存広告グループは基本パラメータを編集できません(広告の追加のみ可)'); return; }
1206
1539
  openModal({title:'広告グループ設定', sub:'paramCode(【code】)でキャンペーン/広告グループIDを解決。既存ID・ターゲティングは任意(広告グループCSVの列)', fields:[
1207
1540
  {key:'name',label:'広告グループ名',type:'text',value:g.name},
1208
1541
  {key:'param',label:'paramCode(【code】)',type:'text',value:g.paramCode||''},
@@ -1215,38 +1548,57 @@ function editParam(u){
1215
1548
  {key:'tg_dayHour',label:'ターゲティング: 曜日・時間帯',type:'text',value:g.tg_dayHour||''},
1216
1549
  {key:'tg_audience',label:'ターゲティング: オーディエンスリスト',type:'text',value:g.tg_audience||''},
1217
1550
  {key:'tg_searchKw',label:'ターゲティング: サーチキーワード',type:'text',value:g.tg_searchKw||''}
1218
- ], onOk:v=>{ mutate(()=>{ g.name=v.name; g.paramCode=(v.param||'').trim(); g.adGroupId=v.adGroupId; g.bid=v.bid; g.tg_device=v.tg_device; g.tg_gender=v.tg_gender; g.tg_age=v.tg_age; g.tg_region=v.tg_region; g.tg_dayHour=v.tg_dayHour; g.tg_audience=v.tg_audience; g.tg_searchKw=v.tg_searchKw; }); }});
1551
+ ], onOk:v=>{ mutate(()=>{ g.name=v.name; g.paramCode=(v.param||'').trim(); g.adGroupId=v.adGroupId; g.bid=v.bid; g.tg_device=v.tg_device; g.tg_gender=v.tg_gender; g.tg_age=v.tg_age; g.tg_region=v.tg_region; g.tg_dayHour=v.tg_dayHour; g.tg_audience=v.tg_audience; g.tg_searchKw=v.tg_searchKw; autoResolveGroupIds(g,r.c); }); }});
1219
1552
  }
1220
1553
  function suggestUrlNumber(){ let mx=0; st.campaigns.forEach(c=>{ const n=parseInt(c.urlNumber); if(!isNaN(n)&&n>mx) mx=n; }); return String(mx+1).padStart(2,'0'); }
1221
1554
  function editCampaign(u){
1222
1555
  const r=findCp(u); if(!r) return; const c=r.c;
1556
+ if(isExistingCampaign(c)){ toast('既存キャンペーンは基本パラメータを編集できません(広告グループ/広告の追加のみ可)'); return; }
1557
+ // E-2: モード/campaignID/戦略メモ フィールドは撤去 (新規/既存はID解決で自動判定)
1223
1558
  openModal({title:'キャンペーン設定', sub:c.name, fields:[
1224
1559
  {key:'name',label:'キャンペーン名',type:'text',value:c.name},
1225
- {key:'campaignId',label:'キャンペーンID(既存・空=paramCodeで解決)',type:'text',value:c.campaignId||''},
1226
- {key:'mode',label:'モード',type:'select',value:c.mode,options:Object.keys(MODES).map(k=>({v:k,t:MODES[k]}))},
1227
1560
  {key:'variable',label:'検証する変数(1キャンペーン=1変数)',type:'text',value:c.variable},
1228
1561
  {key:'urlNumber',label:'入稿リンク末尾番号(空き: '+suggestUrlNumber()+')',type:'text',value:c.urlNumber},
1229
- {key:'budget',label:'予算/日(空でモード既定)',type:'number',value:c.budget??''},
1230
- {key:'biddingStrategy',label:'配信戦略',type:'select',value:c.biddingStrategy||st.project.biddingStrategyDefault,options:[{v:'tCPA',t:'tCPA'},{v:'最大配信',t:'最大配信'}]},
1231
- {key:'tcpa',label:'tCPA(円)',type:'number',value:c.tcpa??st.project.tcpaDefault},
1232
- {key:'strategyMemo',label:'戦略メモ(後乗せ可)',type:'textarea',value:c.strategyMemo}
1562
+ {key:'budget',label:'予算/日(空で既定 '+NEW_CP_BUDGET+')',type:'number',value:c.budget??''},
1563
+ {key:'biddingStrategy',label:'配信戦略',type:'select',value:c.biddingStrategy||'tCPA',options:[{v:'tCPA',t:'tCPA'},{v:'最大配信',t:'最大配信'}]},
1564
+ {key:'tcpa',label:'tCPA(円)',type:'number',value:c.tcpa??''}
1233
1565
  ], extra:'<button class="act" id="genName" style="margin-right:auto">キャンペーン名を自動生成</button>', onMount:(root,getVals,setVal)=>{
1234
1566
  root.querySelector('#genName').addEventListener('click',()=>{ const num=getVals().urlNumber||suggestUrlNumber(); const first=c.adGroups[0]&&c.adGroups[0].ads[0]; const cr=first?(String(first.imageNo||'CR')+(first.format==='SQ'?'s':'c')):'CR'; setVal('name', num+'_'+cr+'_'+today()); });
1235
- }, onOk:v=>{ mutate(()=>{ c.name=v.name; c.campaignId=v.campaignId; c.mode=v.mode; c.variable=v.variable; c.urlNumber=v.urlNumber; c.budget=v.budget===''?null:Number(v.budget); c.biddingStrategy=v.biddingStrategy; c.tcpa=v.tcpa===''?null:Number(v.tcpa); c.strategyMemo=v.strategyMemo; }); }});
1567
+ }, onOk:v=>{ mutate(()=>{ c.name=v.name; c.variable=v.variable; c.urlNumber=v.urlNumber; c.budget=numOrNull(v.budget); c.biddingStrategy=v.biddingStrategy; c.tcpa=numOrNull(v.tcpa); }); }});
1568
+ // B6解消: 配信戦略/tCPAはキャンペーン個別値のみを参照 (project既定を焼き付けない)
1236
1569
  }
1237
1570
  function cloneCampaign(c, clearCreative, clearParam){
1238
- const nc=JSON.parse(JSON.stringify(c)); nc.uid=uid('c'); nc.strategyMemo=''; nc.variable='';
1239
- nc.adGroups.forEach(g=>{ g.uid=uid('g'); if(clearParam) g.paramCode=''; g.ads.forEach(a=>{ a.uid=uid('a'); if(clearCreative){a.imageNo='';a.titleNo='';a.descNo='';} }); });
1571
+ const nc=JSON.parse(JSON.stringify(c)); nc.uid=uid('c');
1572
+ nc.campaignId=''; nc.isExisting=false; // B7/F-2: 複製はIDを引き継がず必ず新規扱い(編集可)
1573
+ 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(clearCreative){a.imageNo='';a.titleNo='';a.descNo='';} }); });
1240
1574
  return nc;
1241
1575
  }
1576
+ /* 指定prefixを持つ paramCode の現在の最大末尾番号 (プラン内 + アカウントJSON refs.groups を走査) */
1577
+ function maxParamNum(prefix){
1578
+ 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; } };
1579
+ st.campaigns.forEach(c=>c.adGroups.forEach(g=>scan(g.paramCode)));
1580
+ Object.keys(st.refs.groups||{}).forEach(scan);
1581
+ return mx;
1582
+ }
1583
+ /* E(スクショFB): 複製/量産は「中身そのまま複製 + paramCode連番自動採番 + campaignId/adGroupIdクリア(新規扱い) + キャンペーン名自動採番」で固定。モーダルは本数入力のみ */
1242
1584
  function dupCampaign(u){
1243
1585
  const r=findCp(u); if(!r) return; const c=r.c;
1244
- openModal({title:'キャンペーンを複製 / 量産', sub:'北さんの「テンプレ複製→中身クリア→変数差し替え」。'+c.name+' をテンプレに複製します', fields:[
1245
- {key:'count',label:'複製する本数(変数の数だけ)',type:'number',value:1},
1246
- {key:'clearCreative',label:'',type:'check',value:true,text:'クリエイティブ(画像/タイトル/説明文)をクリア'},
1247
- {key:'clearParam',label:'',type:'check',value:false,text:'paramCode もクリア(新規グループとして作り直す)'},
1248
- {key:'autoname',label:'',type:'check',value:true,text:'キャンペーン名を {空き番号}_{CR名}_'+today()+' で自動採番'}
1249
- ], onOk:v=>{ const raw=parseInt(v.count); if(!Number.isFinite(raw)||raw<1){ toast('本数は1以上を入力してください'); return; } const n=Math.min(50,raw); mutate(()=>{ const base=r.i; const startNum=parseInt(suggestUrlNumber())||1; for(let i=0;i<n;i++){ const nc=cloneCampaign(c, v.clearCreative, v.clearParam); if(v.autoname){ const num=String(startNum+i).padStart(2,'0'); nc.urlNumber=num; const f=nc.adGroups[0]&&nc.adGroups[0].ads[0]; const cr=f?(String(f.imageNo||'CR')+(f.format==='SQ'?'s':'c')):'CR'; nc.name=num+'_'+cr+'_'+today(); } else { nc.name=c.name+'_copy'+(n>1?(i+1):''); } st.campaigns.splice(base+1+i,0,nc); } }); toast(n+'本のキャンペーンを生成'); }});
1586
+ openModal({title:'キャンペーンを複製 / 量産', sub:'中身そのまま複製し、paramCodeは連番で自動採番します(campaignID/広告グループIDはクリアして新規扱い・キャンペーン名は自動採番)。'+c.name, fields:[
1587
+ {key:'count',label:'複製する本数',type:'number',value:1}
1588
+ ], onOk:v=>{ const raw=parseInt(v.count); if(!Number.isFinite(raw)||raw<1){ toast('本数は1以上を入力してください'); return; } const n=Math.min(50,raw);
1589
+ mutate(()=>{
1590
+ const base=r.i; const startNum=parseInt(suggestUrlNumber())||1; const nextByPrefix={};
1591
+ for(let i=0;i<n;i++){
1592
+ const nc=cloneCampaign(c, false, false); // 中身そのまま (creative/paramCode保持) + campaignId/adGroupIdクリア
1593
+ // paramCode 連番自動採番 (prefixごとに現状の最大+1から連番)
1594
+ nc.adGroups.forEach(g=>{ const m=String(g.paramCode||'').match(/^(.*?)(\d+)\s*$/); if(m){ const prefix=m[1], width=m[2].length; if(nextByPrefix[prefix]==null) nextByPrefix[prefix]=maxParamNum(prefix)+1; g.paramCode=prefix+String(nextByPrefix[prefix]++).padStart(width,'0'); autoResolveGroupIds(g,nc); } });
1595
+ // キャンペーン名 自動採番 ({空き番号}_{CR名}_{今日})
1596
+ const num=String(startNum+i).padStart(2,'0'); nc.urlNumber=num; const f=nc.adGroups[0]&&nc.adGroups[0].ads[0]; const cr=f?(String(f.imageNo||'CR')+(f.format==='SQ'?'s':'c')):'CR'; nc.name=num+'_'+cr+'_'+today();
1597
+ st.campaigns.splice(base+1+i,0,nc);
1598
+ }
1599
+ });
1600
+ toast(n+'本のキャンペーンを複製(中身そのまま・paramCode連番)');
1601
+ }});
1250
1602
  }
1251
1603
 
1252
1604
  /* ============================================================
@@ -1284,6 +1636,17 @@ function openModal(cfg){
1284
1636
  ============================================================ */
1285
1637
  function firstParamOf(c){ const g=c.adGroups.find(g=>g.paramCode); return g?g.paramCode:''; }
1286
1638
  function resolveGroupIds(paramCode){ const g=st.refs.groups[paramCode]; return {campaignId:g?g.campaignId:'', adGroupId:g?g.adGroupId:''}; }
1639
+ /* ---- F-6: 新規/既存 判定は adGroupId / campaignId の有無のみ (paramCode 紐づけ解消) ---- */
1640
+ function resolveCampaignIdOf(c){ return String((c&&c.campaignId)||'').trim(); }
1641
+ function resolveAdGroupIdOf(g){ return String((g&&g.adGroupId)||'').trim(); }
1642
+ function isNewCampaign(c){ return !resolveCampaignIdOf(c); } // 表示バッジ/export用: campaignId有無ベース
1643
+ function isNewGroup(g){ return !resolveAdGroupIdOf(g); } // 表示バッジ/export用: adGroupId有無ベース
1644
+ /* F-2: 編集可否は明示フラグ isExisting のみで判定。既存呼び出し行(adGroupId/campaignId付き)=isExisting:true。 */
1645
+ function isExistingCampaign(c){ return !!(c && c.isExisting); }
1646
+ function isExistingGroup(g){ return !!(g && g.isExisting); }
1647
+ function newOrIdBadge(isNew, id){ return isNew ? '<span class="newbadge" style="background:var(--accent)">新規</span>' : (id?'<span class="idpill">ID '+esc(id)+'</span>':''); }
1648
+ /* F-6: paramCode によるID自動解決は廃止 (no-op)。既存/新規は adGroupId の有無で判定する。 */
1649
+ function autoResolveGroupIds(g, c){ /* no-op: paramCode紐づけ解消 */ }
1287
1650
  function download(filename, text, mime){ const blob=new Blob([text],{type:mime||'text/plain;charset=utf-8'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; document.body.appendChild(a); a.click(); setTimeout(()=>{URL.revokeObjectURL(a.href); a.remove();},100); }
1288
1651
  function tsCompact(){ const d=new Date(); const p=n=>String(n).padStart(2,'0'); return ''+d.getFullYear()+p(d.getMonth()+1)+p(d.getDate())+'-'+p(d.getHours())+p(d.getMinutes())+p(d.getSeconds()); }
1289
1652
 
@@ -1291,7 +1654,11 @@ function tsCompact(){ const d=new Date(); const p=n=>String(n).padStart(2,'0');
1291
1654
  スキル同梱の「入稿計画/」フォルダを一度指定すると、以後は直接書き込み・読込・削除。
1292
1655
  非対応環境(file://等)は自動でダウンロード動作にフォールバックする。 */
1293
1656
  let dirHandle=null, pendingDirHandle=null;
1657
+ let planFileHandle=null; // 「JSON読込」で開いた入稿計画ファイル。書き出しはこのファイルを上書き更新する
1658
+ let samplePlanFileHandle=null; // サンプルプラン.json の書込ハンドル(初回ピック後 IndexedDB に保持し、以後 Finder無しで更新)
1294
1659
  const fsSupported = typeof window.showDirectoryPicker==='function';
1660
+ const fsOpenSupported = typeof window.showOpenFilePicker==='function';
1661
+ async function writeToHandle(h,text){ try{ const w=await h.createWritable(); await w.write(text); await w.close(); return true; }catch(e){ return false; } }
1295
1662
  function idb(){ return new Promise((res,rej)=>{ const r=indexedDB.open('yda_planner_fs',1); r.onupgradeneeded=()=>r.result.createObjectStore('handles'); r.onsuccess=()=>res(r.result); r.onerror=()=>rej(r.error); }); }
1296
1663
  async function idbSet(k,v){ const db=await idb(); return new Promise((res,rej)=>{ const tx=db.transaction('handles','readwrite'); tx.objectStore('handles').put(v,k); tx.oncomplete=res; tx.onerror=()=>rej(tx.error); }); }
1297
1664
  async function idbGet(k){ const db=await idb(); return new Promise((res,rej)=>{ const tx=db.transaction('handles','readonly'); const rq=tx.objectStore('handles').get(k); rq.onsuccess=()=>res(rq.result); rq.onerror=()=>rej(rq.error); }); }
@@ -1300,9 +1667,11 @@ async function connectFolder(){
1300
1667
  try{
1301
1668
  if(pendingDirHandle){ const p=await pendingDirHandle.requestPermission({mode:'readwrite'}); if(p==='granted'){ dirHandle=pendingDirHandle; pendingDirHandle=null; renderHist(); toast('保存フォルダに再接続しました'); return; } }
1302
1669
  dirHandle=await window.showDirectoryPicker({id:'yda-planner-plans', mode:'readwrite'});
1303
- pendingDirHandle=null; await idbSet('dir',dirHandle); renderHist(); toast('保存フォルダに接続しました: '+dirHandle.name);
1670
+ pendingDirHandle=null; await idbSet('dir',dirHandle); renderHist(); updateSubmitJsonBtn(); toast('保存フォルダに接続しました: '+dirHandle.name);
1304
1671
  }catch(e){ if(e && e.name!=='AbortError') toast('フォルダ接続に失敗: '+e.message); }
1305
1672
  }
1673
+ /* 入稿計画フォルダ未接続なら書き出しボタンを「接続」ボタンに切替(接続後は書き出し) */
1674
+ function updateSubmitJsonBtn(){ /* ボタンは『サンプルプラン更新』に用途変更したためラベル切替は無効化 */ }
1306
1675
  async function restoreFolder(){
1307
1676
  if(!fsSupported) return;
1308
1677
  try{ const h=await idbGet('dir'); if(!h) return;
@@ -1318,6 +1687,245 @@ async function writePlanFile(name, text){
1318
1687
  catch(e){ toast('フォルダ書き込み失敗: '+e.message+'(ダウンロードに切替)'); return false; }
1319
1688
  }
1320
1689
 
1690
+ /* ============================================================
1691
+ B-1: アカウント別参照データ (アカウント/ フォルダ + <名前>.json)
1692
+ File System Access API 前提 (Chrome系・フォールバックなし)。未接続時は同梱デフォルト(アカウント/index.json 先頭)を使用。
1693
+ IndexedDB キー 'yda_planner_fs_accounts' に第2ハンドルを保存 (入稿計画/ の 'dir' とは別枠)。
1694
+ ============================================================ */
1695
+ const ACC_JSON_VERSION=1;
1696
+ let accDirHandle=null, pendingAccDirHandle=null;
1697
+ let accountFiles=[]; // フォルダ内の <名前>.json のベース名一覧 (File System Access接続時)
1698
+ let httpAccountFiles=[]; // アカウント/index.json から取得した <名前> 一覧 (読み取り専用切替)
1699
+ /* アカウント一覧は マニフェスト アカウント/index.json (直接ファイルfetch)から取得する。
1700
+ サンプルプランと同じ「ファイル読み込み」なので、file:///autoindex無し環境でも読める。
1701
+ フォールバック: index.json が無ければディレクトリ一覧(HTML)を解析。 */
1702
+ async function fetchAccountList(){
1703
+ const set=new Set();
1704
+ // (1) マニフェスト アカウント/index.json (file://・autoindex無しでも読める)
1705
+ try{ const idx=await fetchJson('アカウント/index.json'); if(idx){ const arr=Array.isArray(idx)?idx:(idx.accounts||[]); arr.map(String).filter(Boolean).forEach(n=>set.add(n)); } }catch(e){}
1706
+ // (2) ディレクトリ一覧(HTML) — http配信時は index.json に無い新規ファイルも自動検出してマージ
1707
+ try{ const r=await fetch('アカウント/',{cache:'no-store'}); if(r&&r.ok){ const html=await r.text(); const re=/href="([^"]+\.json)"/gi; let m; while((m=re.exec(html))){ let n=decodeURIComponent(m[1]).replace(/^.*\//,'').replace(/\.json$/i,''); if(n&&n!=='index') set.add(n); } } }catch(e){}
1708
+ httpAccountFiles=[...set].sort();
1709
+ }
1710
+ function accStatusText(){
1711
+ if(!fsSupported) return 'このブラウザはフォルダ連携非対応';
1712
+ if(accDirHandle) return '📁 '+accDirHandle.name+'/ ('+accountFiles.length+'アカウント)';
1713
+ if(pendingAccDirHandle) return 'フォルダ再接続待ち(📁 フォルダ を押す)';
1714
+ return 'フォルダ接続してください(現在は同梱の'+BUNDLED_ACCOUNT_NAME+'を使用中)';
1715
+ }
1716
+ function renderAccountBar(){
1717
+ const sel=document.getElementById('accSelect'); if(!sel) return;
1718
+ const cur=st.project.accountName||BUNDLED_ACCOUNT_NAME;
1719
+ let names = accDirHandle ? accountFiles.slice() : (httpAccountFiles.length ? httpAccountFiles.slice() : [BUNDLED_ACCOUNT_NAME]);
1720
+ if(!accDirHandle && !names.includes(BUNDLED_ACCOUNT_NAME)) names.unshift(BUNDLED_ACCOUNT_NAME);
1721
+ if(!names.includes(cur)) names.unshift(cur);
1722
+ let opts=names.map(n=>'<option value="'+esc(n)+'"'+(n===cur?' selected':'')+'>'+esc(n)+((n===BUNDLED_ACCOUNT_NAME&&!accDirHandle)?'(同梱)':'')+'</option>').join('');
1723
+ // F-3: プルダウン内にアクション項目 (新規追加/このアカウントを編集)。フォルダ接続オプションは廃止
1724
+ opts+='<option disabled>──────────</option>';
1725
+ opts+='<option value="__add__">+ 新規追加…</option>';
1726
+ opts+='<option value="__edit__">✎ このアカウントを編集…</option>';
1727
+ sel.innerHTML=opts;
1728
+ const s2=document.getElementById('accStatus'); if(s2) s2.textContent=accStatusText(); // accStatusは廃止(要素なし)だが後方安全のためguard
1729
+ }
1730
+ async function connectAccountFolder(){
1731
+ if(!fsSupported){ toast('このブラウザはフォルダ連携非対応です'); return; }
1732
+ try{
1733
+ if(pendingAccDirHandle){ const p=await pendingAccDirHandle.requestPermission({mode:'readwrite'}); if(p==='granted'){ accDirHandle=pendingAccDirHandle; pendingAccDirHandle=null; await refreshAccountFiles(); toast('アカウントフォルダに再接続しました'); return; } }
1734
+ accDirHandle=await window.showDirectoryPicker({id:'yda-planner-accounts', mode:'readwrite'});
1735
+ pendingAccDirHandle=null; await idbSet('yda_planner_fs_accounts', accDirHandle);
1736
+ await refreshAccountFiles(); toast('アカウントフォルダに接続: '+accDirHandle.name);
1737
+ }catch(e){ if(e && e.name!=='AbortError') toast('フォルダ接続に失敗: '+e.message); }
1738
+ }
1739
+ async function restoreAccountFolder(){
1740
+ if(!fsSupported){ renderAccountBar(); return; }
1741
+ try{ const h=await idbGet('yda_planner_fs_accounts'); if(h){ const p=await h.queryPermission({mode:'readwrite'}); if(p==='granted'){ accDirHandle=h; await refreshAccountFiles(); } else pendingAccDirHandle=h; } }catch(e){}
1742
+ renderAccountBar();
1743
+ }
1744
+ async function refreshAccountFiles(){
1745
+ accountFiles=[];
1746
+ if(accDirHandle){ try{ for await(const [name,handle] of accDirHandle.entries()){ if(handle.kind==='file' && /\.json$/i.test(name)) accountFiles.push(name.replace(/\.json$/i,'')); } }catch(e){} accountFiles.sort(); }
1747
+ renderAccountBar();
1748
+ }
1749
+ async function readAccountJson(name){
1750
+ if(!accDirHandle) return null;
1751
+ try{ const fh=await accDirHandle.getFileHandle(name+'.json'); const f=await fh.getFile(); return JSON.parse(await f.text()); }catch(e){ return null; }
1752
+ }
1753
+ async function writeAccountJson(name, obj){
1754
+ if(!accDirHandle) return false;
1755
+ try{ const fh=await accDirHandle.getFileHandle(name+'.json',{create:true}); const w=await fh.createWritable(); await w.write(JSON.stringify(obj,null,2)); await w.close(); return true; }
1756
+ catch(e){ toast('アカウントJSON書き込み失敗: '+e.message); return false; }
1757
+ }
1758
+ /* 参照データ差し替えのシーム: フォルダ読込・新規作成・E2Eテストが共通で使う */
1759
+ window.applyAccountJson=function(obj){
1760
+ if(!obj||obj.accountJsonVersion!==ACC_JSON_VERSION||!obj.refs) throw new Error('アカウントJSON形式が不正です(accountJsonVersion:'+ACC_JSON_VERSION+' 必須)');
1761
+ snapshot();
1762
+ st.refs=normalizeRefs(obj.refs);
1763
+ st.project.accountId=String(obj.accountId||'');
1764
+ st.project.accountName=String(obj.name||'');
1765
+ // E-1: 主体者(principal)はアカウントJSON由来。無ければ既存値を維持
1766
+ if(obj.principal!=null) st.project.principal=String(obj.principal);
1767
+ persist(); buildDatalists(); render(); renderAccountBar();
1768
+ };
1769
+ async function selectAccount(name){
1770
+ if(String(name).startsWith('__')){ renderAccountBar(); return; } // アクション項目は change リスナーで処理
1771
+ if(name===st.project.accountName){ renderAccountBar(); return; }
1772
+ if(!accDirHandle){
1773
+ if(httpAccountFiles.includes(name)){ // http一覧のアカウントを fetch で読み込み(読み取り専用切替)
1774
+ const obj=await fetchJson('アカウント/'+encodeURIComponent(name)+'.json');
1775
+ if(obj){ try{ window.applyAccountJson(obj); toast('アカウント '+name+' に切替(参照データを差し替え)'); }catch(e){ toast(e.message); renderAccountBar(); } }
1776
+ else { toast('アカウントJSONを読み込めません: '+name); renderAccountBar(); }
1777
+ return;
1778
+ }
1779
+ if(name===BUNDLED_ACCOUNT_NAME){ window.applyAccountJson({accountJsonVersion:1, accountId:BUNDLED_ACCOUNT_ID, name:BUNDLED_ACCOUNT_NAME, principal:bundledPrincipal, refs:buildRefs()}); toast('同梱アカウント '+BUNDLED_ACCOUNT_NAME+' に切替'); }
1780
+ else { toast('先にアカウントフォルダに接続してください'); renderAccountBar(); }
1781
+ return;
1782
+ }
1783
+ const obj=await readAccountJson(name);
1784
+ if(!obj){ toast('アカウントJSONを読み込めません: '+name); renderAccountBar(); return; }
1785
+ try{ window.applyAccountJson(obj); toast('アカウント '+name+' に切替(参照データを差し替え)'); }
1786
+ catch(e){ toast(e.message); renderAccountBar(); }
1787
+ }
1788
+ /* refAdd/refEdit/refDel 後にアクティブアカウントJSONへ自動保存 (フォルダ接続 & フォルダ内アカウント時) */
1789
+ async function saveActiveAccountJson(){
1790
+ if(!accDirHandle) return; const name=st.project.accountName; if(!name) return;
1791
+ if(!accountFiles.includes(name)) return; // 同梱デフォルト等フォルダ外は書き戻さない
1792
+ const obj={accountJsonVersion:ACC_JSON_VERSION, accountId:String(st.project.accountId||''), name, principal:String(st.project.principal||''), updatedAt:new Date().toISOString(), refs:normalizeRefs(st.refs)};
1793
+ await writeAccountJson(name, obj);
1794
+ }
1795
+ /* F-3: 「+新規追加」チューザー — ファイルから追加 / AIで追加 の2択 */
1796
+ function openAddAccountModal(){
1797
+ const root=document.getElementById('modalRoot');
1798
+ const close=()=>root.innerHTML='';
1799
+ root.innerHTML='<div class="overlay"><div class="modal"><h3>新規アカウントを追加</h3><div class="msub">追加方法を選んでください</div>'
1800
+ +'<div class="mbody"><div style="display:flex; flex-direction:column; gap:10px">'
1801
+ +'<button class="act" id="addFromFile" style="justify-content:flex-start; text-align:left">📄 ファイルから追加<br><span class="hinttxt">アカウントJSONファイルを選んで読み込み</span></button>'
1802
+ +'<button class="act" id="addFromAi" style="justify-content:flex-start; text-align:left">🤖 AIで追加<br><span class="hinttxt">CLIから収集するプロンプト+JSONフォーマットを表示</span></button>'
1803
+ +'</div></div><div class="mfoot"><button class="act" data-mo="cancel">閉じる</button></div></div></div>';
1804
+ root.querySelector('[data-mo="cancel"]').addEventListener('click',close);
1805
+ root.querySelector('.overlay').addEventListener('mousedown',ev=>{ if(ev.target.classList.contains('overlay')) close(); });
1806
+ root.querySelector('#addFromFile').addEventListener('click',()=>{ close(); addAccountFromFile(); });
1807
+ root.querySelector('#addFromAi').addEventListener('click',()=>{ close(); openAiAddAccountModal(); });
1808
+ }
1809
+ /* F-3: ファイルから追加 — アカウントJSONを選んで読み込み (フォルダ接続時はフォルダにも保存して一覧化) */
1810
+ function addAccountFromFile(){ const inp=document.getElementById('accFileInput'); if(inp) inp.click(); }
1811
+ async function loadAccountFile(file){
1812
+ try{
1813
+ const obj=JSON.parse(await file.text());
1814
+ window.applyAccountJson(obj); // accountJsonVersion/refs を検証し project(name/id/principal)へ反映
1815
+ if(accDirHandle && obj.name){ await writeAccountJson(String(obj.name), obj); await refreshAccountFiles(); } // フォルダへコピー保存
1816
+ renderAccountBar();
1817
+ toast('アカウント '+(obj.name||'')+' を読み込みました'+(accDirHandle?'(フォルダにも保存)':''));
1818
+ }catch(e){ toast('読み込み失敗: '+(e&&e.message?e.message:'不正なアカウントJSON')); }
1819
+ }
1820
+ /* F-3: AIで追加 — フォーム入力は無し。プロンプトだけを表示し、アカウント名/ID/主体者は Claude Code 上でAIと対話して決めさせる */
1821
+ function openAiAddAccountModal(){
1822
+ const schema=[
1823
+ '{',
1824
+ ' "accountJsonVersion": 1,',
1825
+ ' "accountId": "<ID>",',
1826
+ ' "name": "<アカウント名>",',
1827
+ ' "principal": "<主体者表記(例: ○○製薬)>",',
1828
+ ' "updatedAt": "<ISO8601>",',
1829
+ ' "refs": {',
1830
+ ' "titles": {"1": "見出し文言"},',
1831
+ ' "descriptions": {"1": "説明文文言"},',
1832
+ ' "media": {"954": {"card": "<mediaId>", "sq": "<mediaId>"}},',
1833
+ ' "campaigns": {"<campaignId>": {"name": "CP名"}},',
1834
+ ' "adGroups": {"<adGroupId>": {"name": "AG名", "campaignId": "<campaignId>", "paramCode": "<code>"}},',
1835
+ ' "groups": {"<paramCode>": {"campaignId": "<campaignId>", "adGroupId": "<adGroupId>", "groupName": "AG名"}}',
1836
+ ' }',
1837
+ '}'
1838
+ ].join('\n');
1839
+ const prompt=[
1840
+ 'YDA入稿プランナー用に、新規アカウントの参照データJSONを生成してください。',
1841
+ '',
1842
+ '1. まず対象アカウントを対話で決めてください:',
1843
+ ' - webgram list --platform yda --resource accounts で候補一覧を取得し、どのアカウントを追加するか私に確認してアカウント名・accountId・主体者表記を確定する',
1844
+ '2. 確定したアカウントについて、登録画像・既存キャンペーン/広告グループを取得:',
1845
+ ' webgram list --platform yda --resource media --account-id <ID>',
1846
+ ' webgram list --platform yda --resource campaigns --account-id <ID>',
1847
+ ' (既存グループ paramCode↔ID の突合は GET /yda/campaign-import も利用可)',
1848
+ '3. 取得結果を下記スキーマに整形し、アカウント/<アカウント名>.json として書き出す:',
1849
+ ' - media は 画像番号 → {card, sq}(mediaId)',
1850
+ ' - groups は paramCode → {campaignId, adGroupId, groupName} の逆引き',
1851
+ ' - campaigns/adGroups は campaignId/adGroupId をキーに',
1852
+ '',
1853
+ 'スキーマ:',
1854
+ schema,
1855
+ '',
1856
+ '書き出したら、プランナー右上のアカウントプルダウン →「+新規追加 →「📄 ファイルから追加」で その JSON を読み込めます。'
1857
+ ].join('\n');
1858
+ showPromptModal({title:'AIで追加(CLIから収集)', sub:'このプロンプトをClaude Codeに貼ってください。アカウント名はAIと対話して決めます', prompt, hint:'生成された アカウント/<名前>.json は「+新規追加 →「📄 ファイルから追加」で読み込めます'});
1859
+ }
1860
+ /* E-1: アクティブアカウントの主体者/アカウントIDを編集 (アカウントJSONへ書き戻し) */
1861
+ function openEditAccountModal(){
1862
+ const name=st.project.accountName||'';
1863
+ openModal({title:'アカウント設定を編集', sub:name||'アカウント', fields:[
1864
+ {key:'accountId',label:'アカウントID',type:'text',value:st.project.accountId||''},
1865
+ {key:'principal',label:'主体者表記(広告のprincipal)',type:'text',value:st.project.principal||''}
1866
+ ], onOk: async v=>{
1867
+ mutate(()=>{ st.project.accountId=String(v.accountId||'').trim(); st.project.principal=String(v.principal||'').trim(); });
1868
+ await saveActiveAccountJson(); // フォルダ接続かつフォルダ内アカウント時はJSONへ書き戻し
1869
+ toast('アカウント設定を更新しました');
1870
+ }});
1871
+ }
1872
+ /* B-3: CLI経由インポートのプロンプトを表示 (静的HTMLは直接APIを叩かず Claude に生成させる) */
1873
+ function openImportModal(){
1874
+ const name=st.project.accountName||''; const accId=st.project.accountId||'';
1875
+ const schema=[
1876
+ '{',
1877
+ ' "accountJsonVersion": 1,',
1878
+ ' "accountId": "<ID>",',
1879
+ ' "name": "<アカウント名>",',
1880
+ ' "principal": "<主体者表記(例: ○○製薬)>",',
1881
+ ' "updatedAt": "<ISO8601>",',
1882
+ ' "refs": {',
1883
+ ' "titles": {"1": "見出し文言"},',
1884
+ ' "descriptions": {"1": "説明文文言"},',
1885
+ ' "media": {"954": {"card": "<mediaId>", "sq": "<mediaId>"}},',
1886
+ ' "campaigns": {"<campaignId>": {"name": "CP名"}},',
1887
+ ' "adGroups": {"<adGroupId>": {"name": "AG名", "campaignId": "<campaignId>", "paramCode": "<code>"}},',
1888
+ ' "groups": {"<paramCode>": {"campaignId": "<campaignId>", "adGroupId": "<adGroupId>", "groupName": "AG名"}}',
1889
+ ' }',
1890
+ '}'
1891
+ ].join('\n');
1892
+ const prompt=[
1893
+ 'YDA入稿プランナー用に、アカウント「'+(name||'<アカウント名>')+'」'+(accId?'(accountId: '+accId+')':'')+'の参照データJSONを生成してください。',
1894
+ '',
1895
+ '手順:',
1896
+ '1. webgram CLI で対象アカウントの情報・登録画像・既存キャンペーン/広告グループを取得:',
1897
+ ' webgram list --platform yda --resource accounts',
1898
+ ' webgram list --platform yda --resource media --account-id '+(accId||'<ID>'),
1899
+ ' webgram list --platform yda --resource campaigns --account-id '+(accId||'<ID>'),
1900
+ ' (既存グループ paramCode↔ID の突合は GET /yda/campaign-import も利用可)',
1901
+ '2. 取得結果を下記スキーマに整形して アカウント/'+(name||'<アカウント名>')+'.json として書き出す:',
1902
+ ' - media は 画像番号 → {card, sq}(mediaId)',
1903
+ ' - groups は paramCode → {campaignId, adGroupId, groupName} の逆引き',
1904
+ ' - campaigns/adGroups は campaignId/adGroupId をキーに',
1905
+ '',
1906
+ 'スキーマ:',
1907
+ schema,
1908
+ '',
1909
+ '書き出したら、プランナーで「📁 フォルダ」を再接続 or アカウントセレクトを開くと自動で一覧に出ます。'
1910
+ ].join('\n');
1911
+ showPromptModal({title:'アカウント情報をインポート', sub:'このプロンプトをClaude Codeに貼ってCLIでアカウントJSONを生成してください', prompt, hint:'アカウント/<名前>.json が生成されたら「📁 フォルダ」再接続で自動的にセレクトに現れます'});
1912
+ }
1913
+ /* 参照ビュー: 他アカウントから タイトル/説明文 をコピー (文言はアカウント間で共通のことが多い) */
1914
+ function openCopyRefsModal(){
1915
+ if(!accDirHandle){ toast('先にアカウントフォルダに接続してください'); return; }
1916
+ const others=accountFiles.filter(n=>n!==st.project.accountName);
1917
+ if(!others.length){ toast('コピー元になる他アカウントがありません'); return; }
1918
+ openModal({title:'他アカウントから文言をコピー', sub:'選んだアカウントの タイトル/説明文 を現在の参照データにマージ(既存番号は上書きしません)', fields:[
1919
+ {key:'src',label:'コピー元アカウント',type:'select',value:others[0],options:others.map(n=>({v:n,t:n}))}
1920
+ ], onOk: async v=>{
1921
+ const obj=await readAccountJson(v.src); if(!obj||!obj.refs){ toast('読み込めません'); return; }
1922
+ const src=normalizeRefs(obj.refs); let n=0; snapshot();
1923
+ ['titles','descriptions'].forEach(k=>{ Object.keys(src[k]||{}).forEach(no=>{ if(st.refs[k][no]==null){ st.refs[k][no]=src[k][no]; n++; } }); });
1924
+ persist(); buildDatalists(); render(); saveActiveAccountJson();
1925
+ toast(v.src+' から '+n+' 件の文言をコピーしました');
1926
+ }});
1927
+ }
1928
+
1321
1929
  /* ---- 履歴 (localStorage: メタ一覧 + バージョン本体) ----
1322
1930
  本体もブラウザ内に自動保存する → フォルダ選択なしで一覧・再読込・削除が完結する。
1323
1931
  フォルダ(入稿計画/)接続時はファイルにも書き出す(二重保存)。 */
@@ -1363,44 +1971,52 @@ function saveVersion(){
1363
1971
  histSave(list); persist();
1364
1972
  if(activeView==='hist') renderHist();
1365
1973
  toast('v'+versionId+' を保存しました'+(wrote?'(入稿計画フォルダにも書き出し)':''));
1974
+ // A-3: 保存完了時の次アクション(プロンプト)表示
1975
+ let info='<div class="callout green" style="margin-top:0">プラン「'+esc(st.meta.name)+'」<b>v'+versionId+'</b> を保存しました(CP '+st.campaigns.length+' / 広告 '+c.ads+' 件)</div>';
1976
+ info+='<div class="mfield"><label>保存先 / ファイル名</label><div class="mono" style="font-size:12px; word-break:break-all">'+esc((wrote&&dirHandle?dirHandle.name+'/':'')+fileName)+'</div></div>';
1977
+ if(c.err) info+='<div class="callout amber">エラー '+c.err+' 件・警告 '+c.warn+' 件があります。書き出し時に未解決広告は除外されます。</div>';
1978
+ const prompt='プラン「'+(st.meta.name||st.project.name)+'」v'+versionId+' を保存しました。\n入稿するには「入稿JSON書き出し」を押し、生成された *_入稿計画_*.json を指定して dry-run してください:\n\nwebgram submit-plan --plan-file "<入稿計画JSONのパス>"';
1979
+ showPromptModal({title:'v'+versionId+' を保存しました', sub:'次のアクション(入稿)用のプロンプトです', infoHtml:info, prompt, hint:'まず「入稿JSON書き出し」でプランを入稿計画JSONに変換してください'});
1366
1980
  }});
1367
1981
  }
1368
1982
 
1369
- /* ---- 履歴ビュー ---- */
1370
- function renderHist(){
1983
+ /* ---- 履歴ビュー: 入稿計画(planVersion:1)ファイル一覧 ----
1984
+ 「✅完成→保存して入稿」が入稿計画フォルダに書き出した *_入稿計画_*.json を一覧化する。
1985
+ (バージョン保存/localStorage履歴は廃止) */
1986
+ function tsFromFileName(name){ const m=String(name||'').match(/_(\d{8})-(\d{6})\.json$/i); if(!m) return ''; const d=m[1], t=m[2]; return d.slice(0,4)+'-'+d.slice(4,6)+'-'+d.slice(6,8)+' '+t.slice(0,2)+':'+t.slice(2,4)+':'+t.slice(4,6); }
1987
+ async function folderPlanList(){
1988
+ if(!dirHandle) return [];
1989
+ const out=[];
1990
+ try{ for await(const [name,h] of dirHandle.entries()){
1991
+ if(h.kind!=='file' || !/\.json$/i.test(name)) continue;
1992
+ try{ const f=await h.getFile(); const j=JSON.parse(await f.text());
1993
+ if(j && j.planVersion===1 && Array.isArray(j.campaigns)){
1994
+ const ads=j.campaigns.reduce((n,c)=>n+(c.adGroups||[]).reduce((m,g)=>m+(g.ads||[]).length,0),0);
1995
+ out.push({fileName:name, accountId:String(j.accountId||''), campaigns:j.campaigns.length, ads, savedAt:tsFromFileName(name)||new Date(f.lastModified).toISOString().replace('T',' ').slice(0,19)});
1996
+ }
1997
+ }catch(e){}
1998
+ } }catch(e){}
1999
+ return out;
2000
+ }
2001
+ async function renderHist(){
1371
2002
  const s=document.getElementById('fsStatus');
1372
- if(!fsSupported) s.textContent='履歴はブラウザ内に自動保存(このブラウザはフォルダ連携非対応)';
1373
- else if(dirHandle) s.textContent='📁 入稿計画フォルダ接続中: '+dirHandle.name+'/(履歴はブラウザ内+ファイルの二重保存)';
1374
- else if(pendingDirHandle) s.textContent='履歴はブラウザ内に自動保存。フォルダ再接続待ち(右のボタンで1クリック再接続)';
1375
- else s.textContent='履歴はブラウザ内に自動保存。「保存フォルダに接続」でスキル内の 入稿計画/ にも書き出せます(初回のみ選択)';
1376
- const list=histList().slice().sort((a,b)=>(b.versionId||0)-(a.versionId||0));
2003
+ if(!fsSupported) s.textContent='このブラウザはフォルダ連携非対応です(履歴表示・入稿計画の保存にはChrome系が必要)';
2004
+ else if(dirHandle) s.textContent='📁 入稿計画フォルダ接続中: '+dirHandle.name+'/(「完成→保存して入稿」で書き出した入稿計画を一覧表示)';
2005
+ else if(pendingDirHandle) s.textContent='フォルダ再接続待ち(右のボタンで1クリック再接続)';
2006
+ else s.textContent='「保存フォルダに接続」で 入稿計画/ を接続すると、保存済みの入稿計画がここに一覧表示されます(初回のみ選択)';
2007
+ const list=(await folderPlanList()).sort((a,b)=>String(b.savedAt).localeCompare(String(a.savedAt)));
1377
2008
  const host=document.getElementById('histHost');
1378
- if(!list.length){ host.innerHTML='<div class="hinttxt" style="padding:24px; font-size:13px">保存履歴はまだありません。「バージョン保存」で保存すると、バージョン・日時・媒体・名称がここに一覧表示されます。</div>'; return; }
1379
- host.innerHTML='<table class="g"><thead><tr><th>Ver</th><th>保存日時</th><th>媒体</th><th>名称</th><th>メモ</th><th>作成者</th><th>規模</th><th>ファイル</th><th style="width:200px"></th></tr></thead><tbody>'+
1380
- list.map(h=>'<tr data-hv="'+h.versionId+'">'+
1381
- '<td class="mono"><b>v'+h.versionId+'</b>'+(st.meta.versionId===h.versionId?' <span class="pill green">現在</span>':'')+'</td>'+
1382
- '<td class="mono">'+esc(String(h.createdAt||'').replace('T',' ').slice(0,19))+'</td>'+
1383
- '<td><span class="pill">'+esc((h.platform||'yda').toUpperCase())+'</span></td>'+
1384
- '<td>'+esc(h.name||'')+'</td><td style="max-width:260px">'+esc(h.memo||'')+'</td><td>'+esc(h.author||'')+'</td>'+
1385
- '<td class="mono">CP '+(h.campaigns??'—')+' / 広告 '+(h.ads??'—')+'</td>'+
1386
- '<td class="mono" style="font-size:11px">'+esc(h.fileName||'')+(h.inLocal!==false?' <span class="pill">ブラウザ</span>':'')+(h.inFolder?' <span class="pill green">フォルダ</span>':'')+((h.inLocal===false&&!h.inFolder)?' <span class="pill amber">DL</span>':'')+'</td>'+
1387
- '<td><button class="act ghost" data-hload="'+h.versionId+'">読込</button> <button class="act ghost" data-hedit="'+h.versionId+'">編集</button> <button class="act ghost" data-hdel="'+h.versionId+'">削除</button></td>'+
2009
+ if(!list.length){ host.innerHTML='<div class="hinttxt" style="padding:24px; font-size:13px">保存済みの入稿計画はまだありません。「✅ 完成 → 保存して入稿」で書き出すと、ここに日時・アカウント・規模・ファイル名が一覧表示されます。'+(dirHandle?'':'(先に「保存フォルダに接続」で 入稿計画/ を接続してください)')+'</div>'; return; }
2010
+ host.innerHTML='<table class="g"><thead><tr><th>保存日時</th><th>アカウントID</th><th>規模</th><th>ファイル名</th><th style="width:80px"></th></tr></thead><tbody>'+
2011
+ list.map(h=>'<tr data-hf="'+esc(h.fileName)+'">'+
2012
+ '<td class="mono">'+esc(h.savedAt)+'</td>'+
2013
+ '<td class="mono">'+esc(h.accountId||'—')+'</td>'+
2014
+ '<td class="mono">CP '+h.campaigns+' / 広告 '+h.ads+'</td>'+
2015
+ '<td class="mono" style="font-size:11px; word-break:break-all">'+esc(h.fileName)+'</td>'+
2016
+ '<td><button class="act ghost" data-hdelplan="'+esc(h.fileName)+'">削除</button></td>'+
1388
2017
  '</tr>').join('')+'</tbody></table>';
1389
2018
  }
1390
- async function histLoadVersion(vid){
1391
- const h=histList().find(x=>x.versionId===vid); if(!h) return;
1392
- // 1) ブラウザ内保存 → 即復元 (フォルダ不要)
1393
- const local=histGetBody(vid);
1394
- if(local){ try{ applyLoadedJson(JSON.parse(local)); toast('v'+vid+' を読み込みました'); return; }catch(e){} }
1395
- // 2) フォルダ(入稿計画/) → 3) ファイル選択
1396
- if(dirHandle && h.fileName){
1397
- try{ const fh=await dirHandle.getFileHandle(h.fileName); const f=await fh.getFile(); applyLoadedJson(JSON.parse(await f.text())); toast('v'+vid+' を読み込みました'); return; }
1398
- catch(e){ toast('フォルダから読み込めません('+(e&&e.name||'エラー')+')。ファイル選択で読み込んでください'); }
1399
- } else {
1400
- toast('保存ファイルをファイル選択で読み込んでください('+(h.fileName||'保存したJSON')+')');
1401
- }
1402
- document.getElementById('fileInput').click();
1403
- }
2019
+ async function histLoadVersion(vid){ /* 旧バージョン読込は履歴の入稿計画ベース化で未使用 (data-hload廃止) */ }
1404
2020
  function histEditVersion(vid){
1405
2021
  const list=histList(); const h=list.find(x=>x.versionId===vid); if(!h) return;
1406
2022
  openModal({title:'v'+vid+' の名称・メモを編集', fields:[
@@ -1429,10 +2045,11 @@ function histDeleteVersion(vid){
1429
2045
  }});
1430
2046
  }
1431
2047
 
1432
- /* ---- 入稿JSON書き出し (CLI submit 用ボディ) ----
2048
+ /* ---- 入稿JSON書き出し (webgram submit-plan 用の入稿計画JSON) ----
1433
2049
  画面のプラン (案件設定チップ / キャンペーン行 / 広告グループ行のターゲティング列) を
1434
- そのまま backend の ydaSubmitSchema 準拠のボディに変換する。設定モーダルは持たない。
1435
- 手入稿(バナー)と未解決(参照表で引けない)広告は除外する。 */
2050
+ そのまま SubmitPlanFile {planVersion:1, platform:'yda', accountId, campaigns} に変換する。
2051
+ 広告は backend ydaAdSchema の IMAGE 形式 (mediaIds/headlines/descriptions/principal)。
2052
+ 設定モーダルは持たない。手入稿(バナー)と未解決(参照表で引けない)広告は除外する。 */
1436
2053
 
1437
2054
  /* 画面の日本語ラベル → API enum 変換表 (正: frontend/src/pages/submission/types.ts) */
1438
2055
  const TG_ENUM = {
@@ -1460,85 +2077,287 @@ function mapDays(s){
1460
2077
  return {days, hours};
1461
2078
  }
1462
2079
  const toStatus=s=>s==='オン'?'ACTIVE':'PAUSED';
1463
-
1464
- function buildSubmitBody(){
2080
+ /* CTA(案件設定の日本語ラベル) → YDA buttonText enum。入稿ADDでは必須。
2081
+ 未知ラベルは MORE_DETAIL(=「詳しくはこちら」) にフォールバック(実データ既定)。 */
2082
+ const CTA_ENUM={'もっとみる':'MORE_DETAIL','詳しくはこちら':'MORE_DETAIL','詳細を見る':'MORE_DETAIL','今すぐ確認':'CONFIRM_NOW','今すぐ申し込む':'APPLY_NOW','購入する':'PURCHASE','今すぐ購入':'PURCHASE_NOW','応募する':'APPLY_HERE','無料体験':'EXPERIENCE','資料請求':'REQUEST_INFO','ダウンロード':'DOWNLOAD','インストール':'INSTALL','予約する':'RESERVE_HERE','今すぐ予約':'RESERVE_NOW','登録する':'REGISTER','今すぐ登録':'REGISTER_NOW','お問い合わせ':'CONTACT_US'};
2083
+ function ctaToEnum(label){ return CTA_ENUM[String(label||'').trim()]||'MORE_DETAIL'; }
2084
+
2085
+ /* A-4: plan本体 + 除外/警告集計を返す。plan本体だけ必要な既存呼び出し(E2E含む)は buildSubmitPlan() を使う。
2086
+ 除外: 未解決(errs)広告・手入稿バナー・空広告グループ(ads:[])・空キャンペーン(adGroups:[])。
2087
+ 警告: 地域/オーディエンス/サーチKW に値があるがJSON未反映の広告グループ数。 */
2088
+ function buildSubmitPlanWithStats(){
1465
2089
  const p=st.project;
2090
+ const stats={exported:0, unresolved:0, pendingImages:0, pendingImageNos:[], emptyGroups:0, emptyCampaigns:0, tgDropped:0};
1466
2091
  const campaigns=st.campaigns.map(c=>{
1467
- const bidLabel=String(c.biddingStrategy||p.biddingStrategyDefault||'').trim();
2092
+ const bidLabel=String(c.biddingStrategy||'').trim(); // A-1: キャンペーン個別値のみ (project既定へフォールバックしない)
1468
2093
  const biddingStrategy=TG_ENUM.bidding[bidLabel]||bidLabel||'MAXIMIZE_CONVERSIONS';
1469
2094
  const adGroups=c.adGroups.map(g=>{
1470
2095
  const dh=mapDays(g.tg_dayHour);
2096
+ const agStatus=toStatus(g.deliveryStatus||p.deliveryStatus);
2097
+ const rated=g.ads.map(a=>({a, r:resolveAd(a,g)}));
2098
+ rated.forEach(x=>{ if(x.r.errs.length) stats.unresolved++; });
2099
+ // Part A: 未登録画像(pendingImage)は除外せず「新規アップロード対象」として保持。真エラー(errs)のみ除外。
2100
+ const keptAds=rated.filter(x=>!x.r.errs.length).map(x=>{
2101
+ const ad={
2102
+ adName:x.r.adName,
2103
+ status:agStatus,
2104
+ mediaType:'IMAGE', // レスポンシブ画像広告 (backend ydaAdSchema IMAGE)
2105
+ headlines:[String(x.r.titleText)],
2106
+ descriptions:[String(x.r.descText)],
2107
+ principal:p.principal||'', // E-1: 主体者はアカウントJSON由来 (st.project.principal)
2108
+ buttonText:ctaToEnum(p.cta), // CTA(案件設定)→ YDA buttonText enum。入稿ADD必須
2109
+ finalUrl:x.r.finalUrl // 実データ準拠 (?ad=paramCode&creative=...)
2110
+ };
2111
+ if(x.r.pendingImage){
2112
+ // 未登録画像: mediaId未確定。管理番号を残し CLI が先にアップロードして解決する (imagePaths は agent/CLI が補完)
2113
+ ad.mediaIds=[]; ad.imageNo=x.r.imageNo; ad.pendingImageNo=x.r.imageNo; ad.imagePaths=[];
2114
+ stats.pendingImages++; if(stats.pendingImageNos.indexOf(x.r.imageNo)<0) stats.pendingImageNos.push(x.r.imageNo);
2115
+ } else {
2116
+ ad.mediaIds=[String(x.r.mediaId)];
2117
+ }
2118
+ return ad;
2119
+ });
2120
+ stats.exported+=keptAds.length;
2121
+ // 地域/オーディエンス/サーチKW列はAPIのID解決が必要なためJSON未反映 (出力ありのグループのみ警告計上)
2122
+ if(keptAds.length && ((g.tg_region&&tgList(g.tg_region).length)||(g.tg_audience&&tgList(g.tg_audience).length)||String(g.tg_searchKw||'').trim()!=='')) stats.tgDropped++;
1471
2123
  const ag={
1472
2124
  name:g.name,
1473
- status:toStatus(g.deliveryStatus||p.deliveryStatus),
2125
+ status:agStatus,
1474
2126
  smartTargeting:false, // GUI未対応 (将来ターゲティング列に追加)
1475
2127
  devices:mapDevices(g.tg_device),
1476
2128
  ages:tgList(g.tg_age).map(x=>TG_ENUM.age[x]).filter(Boolean),
1477
2129
  genders:tgList(g.tg_gender).map(x=>TG_ENUM.gender[x]).filter(Boolean),
1478
2130
  days:dh.days,
1479
2131
  placementsInclude:[], placementsExclude:[],
1480
- // 地域/オーディエンス/サーチKW列はAPIのID解決が必要なためJSON未反映 (仕様ページ参照)
1481
2132
  audiencesInclude:[], audiencesExclude:[],
1482
- ads:g.ads
1483
- .map(a=>({a, r:resolveAd(a,g)}))
1484
- .filter(x=>!x.a.isBanner && !x.r.errs.length)
1485
- .map(x=>({
1486
- name:x.r.adName,
1487
- mediaId:String(x.r.mediaId),
1488
- title:String(x.r.titleText),
1489
- description:String(x.r.descText),
1490
- finalUrl:(p.baseArticleUrl||'')+(x.a.articleSlug||'')+'?ad='+(g.paramCode||'')
1491
- }))
2133
+ ads:keptAds
1492
2134
  };
1493
2135
  if(dh.hours){ ag.startHour=dh.hours.start; ag.endHour=dh.hours.end; }
2136
+ const aid=resolveAdGroupIdOf(g); if(aid) ag.adGroupId=aid; // Phase C: 既存はIDを出力(backendで作成スキップ)。新規はID無し=作成対象
1494
2137
  return ag;
1495
- });
2138
+ }).filter(ag=>{ if(ag.ads.length===0){ stats.emptyGroups++; return false; } return true; }); // B9: 空広告グループを除外
1496
2139
  const cp={
1497
2140
  name:c.name,
1498
2141
  goal:p.goal||'CONVERSION',
1499
- status:toStatus(c.status||p.deliveryStatus),
2142
+ status:toStatus(p.deliveryStatus), // E-2: キャンペーンstatus列は撤去 → 案件設定の配信値を使用
1500
2143
  startDate:p.startDate||today(),
1501
- budget:Number(c.budget==null?(c.mode==='new'?p.budgetNew:p.budgetAsc):c.budget),
2144
+ budget:Number(c.budget)||0, // A-1: キャンペーン個別値のみ
1502
2145
  biddingStrategy,
1503
2146
  adGroups
1504
2147
  };
1505
- if(bidLabel==='tCPA'){ cp.targetCpa=Number(c.tcpa??p.tcpaDefault)||0; }
2148
+ if(bidLabel==='tCPA'){ cp.targetCpa=Number(c.tcpa)||0; }
1506
2149
  else if(biddingStrategy==='CPC'){ cp.cpc=Number(c.adGroups[0]&&c.adGroups[0].bid)||0; }
2150
+ const cid=resolveCampaignIdOf(c); if(cid) cp.campaignId=cid; // Phase C: 既存はIDを出力(backendで作成スキップ)。新規はID無し=作成対象
1507
2151
  return cp;
1508
- });
1509
- return {accountId:String(p.accountId||'').trim(), campaigns};
2152
+ }).filter(cp=>{ if(cp.adGroups.length===0){ stats.emptyCampaigns++; return false; } return true; }); // B9: 空キャンペーンを除外
2153
+ const plan={planVersion:1, platform:'yda', accountId:String(p.accountId||'').trim(), campaigns};
2154
+ return {plan, stats};
1510
2155
  }
2156
+ function buildSubmitPlan(){ return buildSubmitPlanWithStats().plan; }
1511
2157
  function submitAdCount(body){ return body.campaigns.reduce((n,c)=>n+c.adGroups.reduce((m,g)=>m+g.ads.length,0),0); }
2158
+ /* Claudeに貼るプロンプト + コピーボタン付きモーダル (A-3) */
2159
+ function showPromptModal(opts){
2160
+ const root=document.getElementById('modalRoot');
2161
+ const close=()=>root.innerHTML='';
2162
+ root.innerHTML='<div class="overlay"><div class="modal"><h3>'+esc(opts.title)+'</h3>'
2163
+ +(opts.sub?'<div class="msub">'+esc(opts.sub)+'</div>':'')
2164
+ +'<div class="mbody">'+(opts.infoHtml||'')
2165
+ +'<div class="mfield"><label>Claudeに貼るプロンプト</label>'
2166
+ +'<textarea id="promptText" readonly style="min-height:120px; font-family:var(--mono); font-size:12px; white-space:pre-wrap">'+esc(opts.prompt)+'</textarea>'
2167
+ +(opts.hint?'<div class="hint">'+esc(opts.hint)+'</div>':'')+'</div>'
2168
+ +'</div><div class="mfoot"><button class="act" data-mo="cancel">閉じる</button><button class="act primary" id="btnCopyPrompt">📋 プロンプトをコピー</button></div></div></div>';
2169
+ root.querySelector('[data-mo="cancel"]').addEventListener('click',close);
2170
+ root.querySelector('.overlay').addEventListener('mousedown',ev=>{ if(ev.target.classList.contains('overlay')) close(); });
2171
+ root.querySelector('#btnCopyPrompt').addEventListener('click',()=>{ const ta=document.getElementById('promptText'); try{ ta.select(); document.execCommand('copy'); }catch(e){} if(navigator.clipboard){ navigator.clipboard.writeText(opts.prompt).catch(()=>{}); } toast('プロンプトをコピーしました'); });
2172
+ }
2173
+ /* ============================================================
2174
+ 使い方モーダル (❓ 使い方)
2175
+ 全体の流れ(戦略壁打ち→プランナー→保存→入稿)と
2176
+ 初回セットアップ(アカウント取込)・戦略壁打ちの
2177
+ プロンプト例をコピーボタン付きで提示する。
2178
+ ============================================================ */
2179
+ /* 戦略壁打ちを始めるためのプロンプト例(アピール用の即コピー素材) */
2180
+ const HELP_STRATEGY_PROMPT=[
2181
+ 'YDA(Yahoo!ディスプレイ広告)の入稿プランを一緒に作りたいです。まず戦略を壁打ちさせてください。',
2182
+ '',
2183
+ '# 前提',
2184
+ '- アカウント: <アカウント名(日本語OK)>',
2185
+ '- 商材 / LP: <商材名・URL>',
2186
+ '- 目的: <コンバージョン獲得 / 認知 など>',
2187
+ '- 予算・期間: <月予算 / 開始日>',
2188
+ '- 訴求したいポイント: <強み・オファー・ターゲット層>',
2189
+ '',
2190
+ '# お願い',
2191
+ '1. 上記をもとにターゲティング方針・キャンペーン/広告グループ構成・訴求軸を提案してください。',
2192
+ '2. 私が「OK」と言ったら、その戦略を ad-ops Skill の「サンプルプラン.json」に書き出して(上書きして)ください。',
2193
+ ' - 保存先: .claude/skills/ad-ops/サンプルプラン.json',
2194
+ ' - フォーマット: {"schemaVersion":3, "accountId":"<対象アカウントID>", "campaigns":[ ... ]}',
2195
+ ' - campaigns には キャンペーン → 広告グループ(paramCode) → 広告 の構成を入れてください。',
2196
+ '3. 書き出したら、プランナーを開き直す(またはリロードする)と サンプルプラン.json の内容が自動で表示されます。'
2197
+ ].join('\n');
2198
+ /* 初回セットアップ: 対象アカウントの参照データ(画像・見出し・既存CP/AG)をCLIで取り込むプロンプト例 */
2199
+ const HELP_IMPORT_PROMPT=[
2200
+ 'YDA入稿プランナー用に、アカウント「<アカウント名(日本語OK)>」の参照データJSONを生成してください。',
2201
+ '',
2202
+ '手順:',
2203
+ '1. webgram CLI で対象アカウントの情報・登録画像・既存キャンペーン/広告グループを取得:',
2204
+ ' webgram list --platform yda --resource accounts',
2205
+ ' webgram list --platform yda --resource media --account-id <ID>',
2206
+ ' webgram list --platform yda --resource campaigns --account-id <ID>',
2207
+ '2. 取得結果を下記スキーマに整形して アカウント/<アカウント名>.json として書き出す:',
2208
+ ' - media は 画像番号 → {card, sq}(mediaId)',
2209
+ ' - groups は paramCode → {campaignId, adGroupId, groupName} の逆引き',
2210
+ ' - campaigns/adGroups は campaignId/adGroupId をキーに',
2211
+ '',
2212
+ 'スキーマ:',
2213
+ '{',
2214
+ ' "accountJsonVersion": 1,',
2215
+ ' "accountId": "<ID>",',
2216
+ ' "name": "<アカウント名(日本語OK)>",',
2217
+ ' "principal": "<主体者表記(例: ○○製薬)>",',
2218
+ ' "refs": {',
2219
+ ' "titles": {"1": "見出し文言"},',
2220
+ ' "descriptions": {"1": "説明文文言"},',
2221
+ ' "media": {"954": {"card": "<mediaId>", "sq": "<mediaId>"}},',
2222
+ ' "campaigns": {"<campaignId>": {"name": "CP名"}},',
2223
+ ' "adGroups": {"<adGroupId>": {"name": "AG名", "campaignId": "<campaignId>", "paramCode": "<code>"}},',
2224
+ ' "groups": {"<paramCode>": {"campaignId": "<campaignId>", "adGroupId": "<adGroupId>", "groupName": "AG名"}}',
2225
+ ' }',
2226
+ '}',
2227
+ '',
2228
+ '書き出したら、プランナーで「+新規追加 →📄 ファイルから追加」or「📁 フォルダ」再接続で一覧に出ます。'
2229
+ ].join('\n');
2230
+ function openHelpModal(){
2231
+ const root=document.getElementById('modalRoot');
2232
+ const close=()=>root.innerHTML='';
2233
+ const promptCard=(id,title,text)=>
2234
+ '<div class="promptcard"><div class="pchdr"><span class="pctitle">'+esc(title)+'</span>'
2235
+ +'<button class="act primary pccopy" data-copy="'+id+'">📋 コピー</button></div>'
2236
+ +'<pre id="'+id+'">'+esc(text)+'</pre></div>';
2237
+ const steps=[
2238
+ ['戦略を壁打ちする','これは広告を<b>YDAに入稿するための仕組み</b>です。まずAI(Claude Code)に商材・目的・予算を伝えて戦略を壁打ちします。下の「戦略壁打ちプロンプト例」をコピーして貼るところから始めてください。'],
2239
+ ['戦略OK → プランナーに反映','壁打ちで<b>OKを出す</b>と、その内容が <code>サンプルプラン.json</code> に書き出されます(AIが上書き)。<b>プランナーを開き直す/リロードする</b>と、キャンペーン・広告グループ・広告としてこの画面に自動表示されます。'],
2240
+ ['見て、AIに再指示 or 自分で編集','表示された内容を見て、<b>もう一度AIに指示を投げても</b>いいですし、<b>プランナー内でそのまま作業しても</b>OK。広告の追加・複製・文言/画像/パラメータの編集がこの画面でできます(セル選択でコピペも可)。'],
2241
+ ['右上から保存','編集が終わったら、<b>右上の「バージョン保存」</b>を押してください。常に新バージョン(v1, v2…)として保存されます。'],
2242
+ ['表示されたプロンプトをAIに貼る','保存すると<b>次のアクション用のプロンプトが表示</b>されます。そのプロンプトをコピーしてAI(Claude Code)に貼ると、入稿(dry-run→本実行)まで進められます。']
2243
+ ];
2244
+ root.innerHTML='<div class="overlay"><div class="modal help"><h3>YDA入稿プランナーの使い方</h3>'
2245
+ +'<div class="msub">戦略を壁打ち → プランナーで確認・編集 → 保存 → 表示されたプロンプトで入稿、という流れです。</div>'
2246
+ +'<div class="mbody">'
2247
+ +'<div class="callout"><b>全体像</b>:AIで作れる部分はAIに作らせ(戦略壁打ち→JSON)、人が確定すべきクリエイティブ選定・確認をこのプランナーで楽に行います。最後に表示されるプロンプトでそのまま入稿できます。</div>'
2248
+ +'<ol class="hsteps">'
2249
+ +steps.map(s=>'<li><div class="hst">'+s[0]+'</div><div class="hsd">'+s[1]+'</div></li>').join('')
2250
+ +'</ol>'
2251
+ +'<div class="mfield"><label>🚀 まず最初に:戦略壁打ちプロンプト例</label>'
2252
+ +'<div class="hint" style="margin-bottom:6px">これをコピーしてClaude Codeに貼り、&lt;&gt;の部分を埋めて壁打ちを始めてください。</div>'
2253
+ +promptCard('helpPromptStrategy','戦略壁打ち用プロンプト',HELP_STRATEGY_PROMPT)
2254
+ +'</div>'
2255
+ +'<div class="mfield"><label>🔧 初回セットアップ:アカウント取込プロンプト例</label>'
2256
+ +'<div class="hint" style="margin-bottom:6px">初めて使うアカウントは、まずこのプロンプトで登録画像・見出し・既存キャンペーン等を取り込みます(アカウント名は日本語でOK)。</div>'
2257
+ +promptCard('helpPromptImport','アカウント取込プロンプト',HELP_IMPORT_PROMPT)
2258
+ +'</div>'
2259
+ +'</div><div class="mfoot"><button class="act primary" data-mo="cancel">閉じて始める</button></div></div></div>';
2260
+ root.querySelector('[data-mo="cancel"]').addEventListener('click',close);
2261
+ root.querySelector('.overlay').addEventListener('mousedown',ev=>{ if(ev.target.classList.contains('overlay')) close(); });
2262
+ root.querySelectorAll('[data-copy]').forEach(btn=>btn.addEventListener('click',()=>{
2263
+ const txt=document.getElementById(btn.getAttribute('data-copy')).textContent;
2264
+ if(navigator.clipboard){ navigator.clipboard.writeText(txt).catch(()=>{}); }
2265
+ else { try{ const r=document.createRange(); r.selectNode(document.getElementById(btn.getAttribute('data-copy'))); const s=getSelection(); s.removeAllRanges(); s.addRange(r); document.execCommand('copy'); s.removeAllRanges(); }catch(e){} }
2266
+ toast('プロンプトをコピーしました');
2267
+ }));
2268
+ }
2269
+
2270
+ /* 「サンプルプラン更新」: 現在のプラン(共通設定+キャンペーン)を サンプルプラン.json に書き戻す。
2271
+ 初回だけ File System Access でファイルを選び、以後は IndexedDB 保持ハンドルで Finder無し更新。 */
2272
+ async function updateSamplePlan(){
2273
+ const data={schemaVersion:SCHEMA_VERSION, project:st.project, campaigns:st.campaigns};
2274
+ const json=JSON.stringify(data,null,1);
2275
+ if(!samplePlanFileHandle){ try{ const h=await idbGet('samplePlanFile'); if(h){ const perm=await h.queryPermission({mode:'readwrite'}); if(perm==='granted') samplePlanFileHandle=h; else samplePlanFileHandle=h; } }catch(e){} }
2276
+ if(samplePlanFileHandle){ try{ let perm=await samplePlanFileHandle.queryPermission({mode:'readwrite'}); if(perm!=='granted') perm=await samplePlanFileHandle.requestPermission({mode:'readwrite'}); if(perm!=='granted') samplePlanFileHandle=null; }catch(e){ samplePlanFileHandle=null; } }
2277
+ if(!samplePlanFileHandle){
2278
+ if(!fsOpenSupported){ download('サンプルプラン.json',json,'application/json'); toast('サンプルプラン.json をダウンロードしました(同名で差し替えてください)'); return; }
2279
+ try{ const [h]=await window.showOpenFilePicker({id:'yda-sample-plan', types:[{description:'JSON', accept:{'application/json':['.json']}}]}); samplePlanFileHandle=h; await idbSet('samplePlanFile',h); }
2280
+ catch(e){ if(e && e.name==='AbortError') return; toast('サンプルプラン.json を選べませんでした'); return; }
2281
+ }
2282
+ const ok=await writeToHandle(samplePlanFileHandle, json);
2283
+ if(ok){ samplePlan=data; toast('サンプルプランを更新しました('+samplePlanFileHandle.name+')'); }
2284
+ else toast('サンプルプランの更新に失敗しました');
2285
+ }
1512
2286
  async function exportSubmitJson(){
1513
- const body=buildSubmitBody();
1514
- if(!body.accountId){
1515
- toast('案件設定の「アカウントID」を入力してください');
1516
- const el=document.querySelector('#tplChips [data-proj="accountId"]'); if(el){ document.getElementById('cfgBox').open=true; el.focus(); }
2287
+ const {plan,stats}=buildSubmitPlanWithStats();
2288
+ if(!plan.accountId){
2289
+ toast('アカウントを選択してください(上部のアカウントセレクト)');
2290
+ document.getElementById('cfgBox').open=true;
2291
+ const el=document.getElementById('accSelect'); if(el) el.focus();
1517
2292
  return;
1518
2293
  }
1519
- const n=submitAdCount(body);
2294
+ const n=stats.exported;
1520
2295
  if(n===0){ toast('入稿対象の広告が0件です(空入稿を防止しました)'); return; }
1521
- const fileName=(st.project.name||'plan')+'_submit-body_'+tsCompact()+'.json';
1522
- const json=JSON.stringify(body,null,2);
2296
+ const json=JSON.stringify(plan,null,2);
2297
+ // 完成したプランは「新規ファイル」として保存する(上書きしない・毎回タイムスタンプ付き)。
2298
+ // 接続済み入稿計画フォルダがあればそこへ、無ければダウンロード。Finderは出さない。
2299
+ const fileName=(st.project.name||'plan')+'_入稿計画_'+tsCompact()+'.json';
1523
2300
  const wrote=await writePlanFile(fileName,json);
1524
2301
  if(!wrote) download(fileName,json,'application/json');
1525
- toast('入稿JSONを書き出しました(広告'+n+'件・'+(wrote?'フォルダ':'ダウンロード')+')— webgram submit --platform yda で入稿(dry-run から)');
2302
+ _dirty=false; // 保存(入稿計画書き出し)で未保存フラグをクリア
2303
+ const savedRel = '.claude/skills/ad-ops/入稿計画/'+fileName; // Skill同梱の入稿計画フォルダ内の想定パス
2304
+ const savedPath=(wrote&&dirHandle?dirHandle.name+'/':'')+fileName;
2305
+ const savedLabel = wrote?'入稿計画フォルダに新規保存':'ダウンロード(フォルダ未接続)';
2306
+ // 除外内訳
2307
+ const exc=[];
2308
+ if(stats.unresolved) exc.push('未解決 '+stats.unresolved+'件');
2309
+ if(stats.emptyGroups) exc.push('空広告グループ '+stats.emptyGroups+'件');
2310
+ if(stats.emptyCampaigns) exc.push('空キャンペーン '+stats.emptyCampaigns+'件');
2311
+ let info='<div class="callout green" style="margin-top:0">広告 <b>'+n+'</b> 件を新規保存しました('+esc(savedLabel)+')</div>';
2312
+ info+='<div class="mfield"><label>保存先 / ファイル名</label><div class="mono" style="font-size:12px; word-break:break-all">'+esc(savedPath||fileName)+'</div></div>';
2313
+ info+='<div class="mfield"><label>除外件数の内訳</label><div style="font-size:12.5px">'+(exc.length?esc(exc.join(' / ')):'なし')+'</div></div>';
2314
+ if(stats.pendingImages) info+='<div class="callout amber">🆕 未登録画像 <b>'+stats.pendingImages+'</b> 件(管理番号: '+esc(stats.pendingImageNos.join(', '))+')。これらはアカウントのメディアライブラリに未登録のため、入稿前にローカル画像をアップロードして新規登録が必要です(下記プロンプト参照)。</div>';
2315
+ if(stats.tgDropped) info+='<div class="callout amber">⚠ 地域/オーディエンス/サーチKW を設定した広告グループが '+stats.tgDropped+' 件ありますが、これらはAPIのID解決が必要なため入稿JSONには反映されていません(入稿後に管理画面で設定してください)。</div>';
2316
+ // Claudeに貼るプロンプト: 保存したJSONを Skillの入稿計画/ に入れて submit-plan で入稿を試みる
2317
+ const promptLines=[
2318
+ '入稿を試みてください。手順:',
2319
+ '1) 入稿計画JSON('+fileName+')を ad-ops Skill の入稿計画フォルダに置く(保存先: '+savedRel+')。',
2320
+ ' ・入稿計画フォルダ(.claude/skills/ad-ops/入稿計画/)に無ければ、ダウンロードフォルダ(~/Downloads/'+fileName+')を探して上記パスへ移動する。',
2321
+ (wrote? ' ・プランナーからは既に入稿計画フォルダへ保存済みのはず。' : ' ・フォルダ未接続のためダウンロード済み。~/Downloads から移動してください。')
2322
+ ];
2323
+ if(stats.pendingImages){
2324
+ promptLines.push('2) 【未登録画像の新規登録】入稿計画内に mediaIds が空で pendingImageNo が付いた広告があります(管理番号: '+stats.pendingImageNos.join(', ')+')。');
2325
+ promptLines.push(' これらは各管理番号に対応するローカル画像ファイルのパスを、その広告の "imagePaths":[] に設定してください(submit-plan 実行時に自動で /yda/upload-image → mediaId 解決されます)。');
2326
+ promptLines.push('3) 置いたパスで dry-run 入稿を実行:');
2327
+ } else {
2328
+ promptLines.push('2) 置いたパスで dry-run 入稿を実行:');
2329
+ }
2330
+ promptLines.push(' webgram submit-plan --plan-file "'+savedRel+'"');
2331
+ promptLines.push((stats.pendingImages?'4':'3')+') dry-run が全件OKなら本実行:');
2332
+ promptLines.push(' webgram submit-plan --plan-file "'+savedRel+'" --no-dry-run --yes');
2333
+ const prompt=promptLines.join('\n');
2334
+ showPromptModal({title:'入稿計画JSONを保存しました', sub:'このプロンプトをClaude Codeに貼って入稿してください', infoHtml:info, prompt, hint:'submit-plan は1キャンペーンずつ入稿・進捗書き戻しで再開可(submitted済みはスキップ)'});
1526
2335
  }
1527
2336
 
1528
2337
  /* ---- JSON読込 (v1/v2 両対応) ---- */
2338
+ /* B12: 後方互換なし。SCHEMA_VERSION 不一致のJSONは読み込まない。 */
1529
2339
  function applyLoadedJson(j){
1530
- if(!j||!j.campaigns) throw 0;
1531
- const m=migrateState(j);
2340
+ if(!j||!j.campaigns) throw new Error('読み込み失敗: 不正なJSON');
2341
+ if(j.schemaVersion!==SCHEMA_VERSION) throw new Error('スキーマ形式が異なるため読み込めません(planner v'+SCHEMA_VERSION+' のJSONのみ対応)');
1532
2342
  snapshot();
1533
- st.project=Object.assign(defaultProject(),m.project);
1534
- st.campaigns=m.campaigns;
1535
- st.ui=m.ui||{expanded:{}};
1536
- if(m.refs){ st.refs=Object.assign(st.refs,m.refs); }
1537
- st.meta=m.meta;
2343
+ st.project=Object.assign(defaultProject(),j.project);
2344
+ st.campaigns=j.campaigns;
2345
+ st.ui=j.ui||{expanded:{}};
2346
+ if(validRefs(j.refs)){ st.refs=normalizeRefs(j.refs); }
2347
+ st.meta=Object.assign(defaultMeta(),j.meta||{});
1538
2348
  if(typeof buildDatalists==='function') buildDatalists();
1539
- persist(); render();
2349
+ persist(); render(); if(typeof renderAccountBar==='function') renderAccountBar();
2350
+ }
2351
+ function loadJson(file){ planFileHandle=null; const rd=new FileReader(); rd.onload=()=>{ try{ applyLoadedJson(JSON.parse(rd.result)); toast('JSONを読み込みました'+(st.meta&&st.meta.versionId?('(v'+st.meta.versionId+')'):'')); }catch(e){ toast(e instanceof SyntaxError ? '読み込み失敗: 不正なJSONです' : (e&&e.message?e.message:'読み込み失敗: 不正なJSON')); } }; rd.readAsText(file); }
2352
+ /* JSON読込: File System Access 対応環境ではハンドル付きで開く → 書き出しで同じ元ファイルを上書き更新できる */
2353
+ async function openPlanFile(){
2354
+ if(!fsOpenSupported){ document.getElementById('fileInput').click(); return; }
2355
+ try{
2356
+ const [h]=await window.showOpenFilePicker({id:'yda-planner-plans', types:[{description:'JSON', accept:{'application/json':['.json']}}]});
2357
+ const f=await h.getFile(); applyLoadedJson(JSON.parse(await f.text())); planFileHandle=h; updateSubmitJsonBtn();
2358
+ toast('JSONを読み込みました(このファイルを「入稿JSON書き出し」で上書き更新します)');
2359
+ }catch(e){ if(e && e.name!=='AbortError') toast(e instanceof SyntaxError ? '読み込み失敗: 不正なJSONです' : ('読み込み失敗: '+(e&&e.message||''))); }
1540
2360
  }
1541
- function loadJson(file){ const rd=new FileReader(); rd.onload=()=>{ try{ applyLoadedJson(JSON.parse(rd.result)); toast('JSONを読み込みました'+(st.meta&&st.meta.versionId?('(v'+st.meta.versionId+')'):'')); }catch(e){ toast('読み込み失敗: 不正なJSON'); } }; rd.readAsText(file); }
1542
2361
 
1543
2362
  /* ============================================================
1544
2363
  WIRING
@@ -1549,20 +2368,32 @@ document.getElementById('sheetFilter').addEventListener('change',()=>{ clearSel(
1549
2368
  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(); });
1550
2369
  /* 媒体要件ビュー: YDA / Meta タブ切替 */
1551
2370
  document.getElementById('mediaTabs').addEventListener('click',e=>{ const b=e.target.closest('button[data-mt]'); if(!b) return; document.querySelectorAll('#mediaTabs button').forEach(x=>x.classList.toggle('active',x===b)); document.getElementById('mt-yda').style.display = b.dataset.mt==='yda'?'':'none'; document.getElementById('mt-meta').style.display = b.dataset.mt==='meta'?'':'none'; });
2371
+ /* YDA内: 概要 / レスポンシブ / バナー / パラメータ / 使い方 タブ切替 */
2372
+ document.getElementById('ydaTabs').addEventListener('click',e=>{ const b=e.target.closest('button[data-yt]'); if(!b) return; document.querySelectorAll('#ydaTabs button').forEach(x=>x.classList.toggle('active',x===b)); ['overview','resp','banner','param','guide'].forEach(k=>{ document.getElementById('yt-'+k).style.display = b.dataset.yt===k?'':'none'; }); });
1552
2373
  /* 案件テンプレ: チップ直接編集 */
1553
2374
  document.getElementById('tplChips').addEventListener('change',e=>{ const el=e.target.closest('[data-proj]'); if(!el) return; const f=el.dataset.proj; const v = el.type==='number'? Number(el.value) : el.value; mutate(()=>{ st.project[f]=v; }); });
1554
- /* 戦略メモ(開閉式・テーブル上) */
1555
- document.getElementById('stratInput').placeholder = STRAT_PLACEHOLDER;
1556
- document.getElementById('stratInput').addEventListener('input',e=>{ st.project.strategyMemo=e.target.value; persist(); });
1557
2375
  document.getElementById('btnSubmitJson').addEventListener('click',exportSubmitJson);
1558
- document.getElementById('btnSave').addEventListener('click',saveVersion);
2376
+ document.getElementById('btnHelp').addEventListener('click',openHelpModal);
2377
+ document.getElementById('planWarnClose').addEventListener('click',()=>{ planMismatchMsg=''; renderPlanWarn(); });
1559
2378
  document.getElementById('btnConnectFolder').addEventListener('click',connectFolder);
1560
- document.getElementById('histHost').addEventListener('click',e=>{
1561
- const ld=e.target.closest('[data-hload]'); if(ld){ histLoadVersion(+ld.dataset.hload); return; }
1562
- const ed=e.target.closest('[data-hedit]'); if(ed){ histEditVersion(+ed.dataset.hedit); return; }
1563
- const dl=e.target.closest('[data-hdel]'); if(dl){ histDeleteVersion(+dl.dataset.hdel); }
2379
+ /* ---- B-1/B-3: アカウントバー wiring ---- */
2380
+ /* F-3: 文言コピーは参照データビューへ移設。フォルダ接続/編集/インポートはヘッダープルダウンのアクション項目に集約 */
2381
+ document.getElementById('btnAccCopy').addEventListener('click',openCopyRefsModal);
2382
+ document.getElementById('accFileInput').addEventListener('change',e=>{ const f=e.target.files[0]; e.target.value=''; if(f) loadAccountFile(f); });
2383
+ document.getElementById('accSelect').addEventListener('change',e=>{ const v=e.target.value;
2384
+ if(v==='__add__'){ renderAccountBar(); openAddAccountModal(); return; }
2385
+ if(v==='__edit__'){ renderAccountBar(); openEditAccountModal(); return; }
2386
+ selectAccount(v);
2387
+ });
2388
+ /* E-1: キャンペーン追加導線はシート/ツリー内(data-act="add-campaign"/"add-campaign-existing")に配置。ツールバー独立ボタンは廃止 */
2389
+ document.getElementById('histHost').addEventListener('click',async e=>{
2390
+ // 履歴=入稿計画ファイル一覧。削除のみ (フォルダ内ファイルを削除)
2391
+ const dp=e.target.closest('[data-hdelplan]');
2392
+ if(dp){ const name=dp.dataset.hdelplan;
2393
+ openModal({title:'入稿計画を削除しますか?', sub:name+' をフォルダから削除します', fields:[], onOk: async ()=>{ if(dirHandle){ try{ await dirHandle.removeEntry(name); }catch(err){ toast('削除に失敗: '+(err&&err.message||'')); } } toast('削除しました'); renderHist(); }});
2394
+ }
1564
2395
  });
1565
- document.getElementById('btnLoad').addEventListener('click',()=>document.getElementById('fileInput').click());
2396
+ /* #btnLoad(JSON読込)/#btnSave(バージョン保存) ボタンは撤去。#fileInput は E2E/内部読込のため温存(loadJson) */
1566
2397
  document.getElementById('fileInput').addEventListener('change',e=>{ if(e.target.files[0]) loadJson(e.target.files[0]); e.target.value=''; });
1567
2398
  document.getElementById('btnUndo').addEventListener('click',undo);
1568
2399
  document.getElementById('sheetTabs').addEventListener('click',e=>{ const b=e.target.closest('button[data-st]'); if(!b) return; sheetTab=b.dataset.st; clearSel(); closeDrawer(); renderSheet(); });
@@ -1577,6 +2408,7 @@ document.getElementById('btnRefAdd').addEventListener('click',refAdd);
1577
2408
  document.getElementById('btnRefReset').addEventListener('click',()=>{ snapshot(); st.refs=buildRefs(); persist(); buildDatalists(); render(); toast('参照データを初期値に戻しました'); });
1578
2409
  document.getElementById('refHost').addEventListener('click',e=>{ const ed=e.target.closest('[data-refedit]'); if(ed){ refEdit(ed.dataset.refedit, ed.dataset.no); return; } const dl=e.target.closest('[data-refdel]'); if(dl){ refDel(dl.dataset.refdel, dl.dataset.no); } });
1579
2410
  document.getElementById('btnDupSel').addEventListener('click',dupSelected);
2411
+ document.getElementById('btnSeqParam').addEventListener('click',applySequentialParamCodes);
1580
2412
 
1581
2413
  /* ---- シート セル選択 (spreadsheet) ---- */
1582
2414
  const sheetHostEl=document.getElementById('sheetHost');
@@ -1593,15 +2425,28 @@ sheetHostEl.addEventListener('mousedown',e=>{
1593
2425
  sheetHostEl.addEventListener('mouseover',e=>{
1594
2426
  if(!dragging) return;
1595
2427
  if(dragging==='row'){ const rn=e.target.closest('td.rownum'); if(rn){ selB={r:+rn.dataset.rownum,c:tabCols().length-1}; applySelection(); } return; }
1596
- const cell=cellFromEvent(e); if(cell){ const r=+cell.dataset.r,c=+cell.dataset.c; if(!selB||selB.r!==r||selB.c!==c){ dragMoved=true; pendingDrawer=null; if(!selA||r!==selA.r||c!==selA.c) document.body.classList.add('noselect'); selB={r,c}; applySelection(); } }
2428
+ const cell=cellFromEvent(e); if(cell){ const r=+cell.dataset.r,c=+cell.dataset.c; if(!selB||selB.r!==r||selB.c!==c){ dragMoved=true; if(pendingDrawer && pendingDrawer.kind!=='ad') pendingDrawer=null; /* 画像/タイトル/説明文(ad)は複数ドラッグ選択でもサイドバーを開く。ターゲティング(tg)は単体クリックのみ */ if(!selA||r!==selA.r||c!==selA.c) document.body.classList.add('noselect'); selB={r,c}; applySelection(); } }
2429
+ });
2430
+ sheetHostEl.addEventListener('click',e=>{
2431
+ // F-6: 広告グループタブのキャンペーンセル → プラン内CP選択(親付け替え)
2432
+ const cpp=e.target.closest('[data-cppick-g]'); if(cpp){ e.preventDefault(); openCampaignPickDrawer({gUid:cpp.dataset.cppickG}); return; }
2433
+ // F-4: 広告タブのグループセル → プラン内AG選択(所属変更)
2434
+ const agp=e.target.closest('[data-agpick-ad]'); if(agp){ e.preventDefault(); openAdGroupPickDrawer(agp.dataset.agpickAd); return; }
2435
+ const dd=e.target.closest('.combodd'); if(!dd) return; e.preventDefault(); if(dd.dataset.tg){ openTargetingDrawer(dd.dataset.gu, dd.dataset.tg); } else { openRefDrawer(dd); }
1597
2436
  });
1598
- sheetHostEl.addEventListener('click',e=>{ const dd=e.target.closest('.combodd'); if(!dd) return; e.preventDefault(); if(dd.dataset.tg){ openTargetingDrawer(dd.dataset.gu, dd.dataset.tg); } else { openRefDrawer(dd); } });
1599
2437
  /* ---- 右ドロワー wiring ---- */
1600
2438
  document.getElementById('drawerClose').addEventListener('click',closeDrawer);
1601
2439
  document.getElementById('drawerBackdrop').addEventListener('click',closeDrawer);
1602
2440
  document.getElementById('drawerSearch').addEventListener('input',renderDrawer);
1603
- document.getElementById('drawerList').addEventListener('click',e=>{ const row=e.target.closest('.ddrow'); if(!row) return; if(drawerTarget&&drawerTarget.type==='tg') drawerTgToggle(row.dataset.tgval); else drawerPick(row.dataset.no); });
1604
- document.addEventListener('mouseup',()=>{ if(pendingDrawer && !dragMoved){ if(pendingDrawer.kind==='ad') openDrawerFor(pendingDrawer.u,pendingDrawer.f); else openTargetingDrawer(pendingDrawer.gu,pendingDrawer.gf); } pendingDrawer=null; if(dragging){ dragging=false; document.body.classList.remove('noselect'); } });
2441
+ document.getElementById('drawerList').addEventListener('click',e=>{ const row=e.target.closest('.ddrow'); if(!row) return; if(!drawerTarget) return;
2442
+ if(drawerTarget.type==='tg'){ drawerTgToggle(row.dataset.tgval); }
2443
+ else if(drawerTarget.type==='cpref'){ if(row.dataset.cpid) addExistingCampaign(row.dataset.cpid); }
2444
+ else if(drawerTarget.type==='agref'){ if(row.dataset.agid) addExistingAdGroup(row.dataset.agid); }
2445
+ else if(drawerTarget.type==='cppick'){ if(row.dataset.cppick){ if(drawerTarget.mode==='addNew') addNewAdGroupToCampaign(row.dataset.cppick); else moveGroupToCampaign(drawerTarget.gUid, row.dataset.cppick); } }
2446
+ else if(drawerTarget.type==='agpick'){ if(row.dataset.agpick) moveAdToGroup(drawerTarget.adUid, row.dataset.agpick); }
2447
+ else { drawerPick(row.dataset.no); }
2448
+ });
2449
+ document.addEventListener('mouseup',()=>{ if(pendingDrawer){ if(pendingDrawer.kind==='ad') openDrawerFor(pendingDrawer.u,pendingDrawer.f); /* 単体クリック/複数ドラッグ選択どちらでも開く(選択範囲全セルに一括適用) */ else if(!dragMoved) openTargetingDrawer(pendingDrawer.gu,pendingDrawer.gf); } pendingDrawer=null; if(dragging){ dragging=false; document.body.classList.remove('noselect'); } });
1605
2450
 
1606
2451
  /* ---- コピー / 貼付 ---- */
1607
2452
  document.addEventListener('copy',e=>{
@@ -1626,7 +2471,37 @@ document.addEventListener('keydown',e=>{
1626
2471
 
1627
2472
  buildDatalists();
1628
2473
  render();
1629
- restoreFolder(); // 保存フォルダのhandleをIndexedDBから復元 (権限失効時は再接続待ちに)
2474
+ renderAccountBar(); // アカウントセレクト初期化 (同梱デフォルト)
2475
+ restoreFolder(); // 入稿計画フォルダのhandleを復元 (権限失効時は再接続待ちに)
2476
+ restoreAccountFolder(); // B-1: アカウントフォルダのhandleを復元 + セレクト再列挙
2477
+ /* 離脱時: 未保存の編集があればブラウザの確認ダイアログを出す */
2478
+ window.addEventListener('beforeunload', function(e){ if(_dirty){ e.preventDefault(); e.returnValue='未保存の変更があります。「完成 → 保存して入稿」で保存してから離れてください。'; return e.returnValue; } });
2479
+ (async()=>{ try{ const h=await idbGet('samplePlanFile'); if(h){ const perm=await h.queryPermission({mode:'readwrite'}); if(perm==='granted'||perm==='prompt') samplePlanFileHandle=h; } }catch(e){} })(); // サンプルプラン更新用ハンドル復元
2480
+ /* 案A: 起動時に外部JSON(同梱デフォルトアカウント + サンプルプラン)を読み込む。localStorage無しの初回のみ作り直して再描画。 */
2481
+ (async function bootstrapBundled(){
2482
+ try{
2483
+ // 同梱デフォルトアカウント = アカウント/index.json の先頭エントリ(固有名詞をハードコードしない)
2484
+ let firstName='';
2485
+ try{ const idx=await fetchJson('アカウント/index.json'); const arr=idx?(Array.isArray(idx)?idx:(idx.accounts||[])):[]; firstName=String((arr||[])[0]||''); }catch(e){}
2486
+ if(firstName){
2487
+ const acc=await fetchJson('アカウント/'+encodeURIComponent(firstName)+'.json');
2488
+ if(acc){
2489
+ BUNDLED_ACCOUNT_NAME=firstName;
2490
+ if(acc.accountId) BUNDLED_ACCOUNT_ID=String(acc.accountId);
2491
+ if(acc.refs) bundledRefs=normalizeRefs(acc.refs);
2492
+ if(acc.principal) bundledPrincipal=acc.principal;
2493
+ if(acc.projectName) bundledProjectName=acc.projectName;
2494
+ if(acc.baseArticleUrl) bundledBaseUrl=acc.baseArticleUrl;
2495
+ }
2496
+ }
2497
+ const plan=await fetchJson('サンプルプラン.json'); if(plan) samplePlan=plan;
2498
+ await fetchAccountList();
2499
+ }catch(e){}
2500
+ // 開いたときは必ずサンプルプランを読み込む(localStorage廃止)
2501
+ if(bundledRefs || samplePlan){ st=initialState(); st.project.accountId=BUNDLED_ACCOUNT_ID; st.project.accountName=BUNDLED_ACCOUNT_NAME; if(typeof buildDatalists==='function') buildDatalists(); render(); _dirty=false; }
2502
+ else { toast('初期データ(アカウント/サンプルプラン)を読み込めません。httpサーバー経由で開くか、アカウントを接続してください'); }
2503
+ renderAccountBar();
2504
+ })();
1630
2505
  </script>
1631
2506
  </body>
1632
2507
  </html>