adyou 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,11 @@ export declare class GoogleConnector implements Connector {
14
14
  private search;
15
15
  listAccessible(): Promise<string[]>;
16
16
  account(): Promise<AccountInfo>;
17
+ /** 이 커넥터의 customerId(= MCC · loginCustomerId 도 MCC) 아래 하위 광고 계정 만들기 — 대행 모드 고객당 계정. 실측(2026-09-25): SA 가 MCC 에서 ADMIN 이어야 함(STANDARD 면 ACTION_NOT_PERMITTED) ·
18
+ * 계정은 삭제 불가(화면에서 해지만) · 🔴 결제 연결은 API 불가(billingSetups → NO_SIGNUP_PERMISSION · SA 는 결제 프로필 사용자가 될 수 없음) → 운영자가 화면에서 결제 프로필을 붙인다. */
19
+ createClientAccount(name: string, currency?: string, timeZone?: string): Promise<string>;
20
+ /** 결제가 연결돼 광고가 나갈 수 있는 계정인지(billing_setup APPROVED) */
21
+ billingApproved(): Promise<boolean>;
17
22
  check(): Promise<CheckItem[]>;
18
23
  inventory(): Promise<InventoryRow[]>;
19
24
  private uploadImage;
@@ -34,7 +34,8 @@ export class GoogleConnector {
34
34
  const headers = { authorization: `Bearer ${await this.accessToken()}`, 'content-type': 'application/json' };
35
35
  if (this.c.loginCustomerId)
36
36
  headers['login-customer-id'] = this.c.loginCustomerId;
37
- const res = await fetch(`${API}/customers/${cid}/${path}`, { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(120_000) });
37
+ // `:createCustomerClient` 처럼 콜론으로 시작하는 메서드는 고객 경로에 바로 붙는다(슬래시 없음)
38
+ const res = await fetch(`${API}/customers/${cid}${path.startsWith(':') ? '' : '/'}${path}`, { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(120_000) });
38
39
  const data = (await res.json().catch(() => ({})));
39
40
  if (!res.ok || data.error)
40
41
  throw this.wrap(data.error, path);
@@ -82,6 +83,20 @@ export class GoogleConnector {
82
83
  throw new MediumError('google', '고객 정보를 읽지 못했어요');
83
84
  return { id: c.id, name: c.descriptiveName || c.id, currency: c.currencyCode, timezone: c.timeZone, status: c.status, extra: { manager: c.manager } };
84
85
  }
86
+ /** 이 커넥터의 customerId(= MCC · loginCustomerId 도 MCC) 아래 하위 광고 계정 만들기 — 대행 모드 고객당 계정. 실측(2026-09-25): SA 가 MCC 에서 ADMIN 이어야 함(STANDARD 면 ACTION_NOT_PERMITTED) ·
87
+ * 계정은 삭제 불가(화면에서 해지만) · 🔴 결제 연결은 API 불가(billingSetups → NO_SIGNUP_PERMISSION · SA 는 결제 프로필 사용자가 될 수 없음) → 운영자가 화면에서 결제 프로필을 붙인다. */
88
+ async createClientAccount(name, currency = 'KRW', timeZone = 'Asia/Seoul') {
89
+ const d = await this.call(':createCustomerClient', { customerClient: { descriptiveName: name.slice(0, 100), currencyCode: currency, timeZone } });
90
+ const id = d.resourceName?.split('/')[1];
91
+ if (!id)
92
+ throw new MediumError('google', '하위 계정 ID 를 받지 못했어요');
93
+ return id;
94
+ }
95
+ /** 결제가 연결돼 광고가 나갈 수 있는 계정인지(billing_setup APPROVED) */
96
+ async billingApproved() {
97
+ const rows = await this.search('SELECT billing_setup.status FROM billing_setup');
98
+ return rows.some((r) => r.billingSetup.status === 'APPROVED');
99
+ }
85
100
  async check() {
86
101
  const items = [];
87
102
  let accessible = [];
@@ -220,9 +220,13 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
220
220
  bodyFs = Math.round(hh * 0.056);
221
221
  ctaFs = Math.round(hh * 0.058);
222
222
  }
223
- const chipFs = Math.round(hh * 0.044);
223
+ const richPre = !bg && !banner && !short && !narrow && !googleLight && Math.min(w, hh) >= 600 && !(w / hh > 1.6);
224
+ const chipFs = richPre ? Math.round(base * 0.036) : Math.round(hh * 0.044);
224
225
  // 알약은 통째로 들어가는 짧은 증거만(자르면 「영업보증보험 1.5억원 /」 처럼 어색 · 정책 위험 표현 제외)
225
- let proofs = widePoster ? [...(brief.proofPoints || []), ...(brief.usp || [])].map((x) => String(x || '').replace(/\s+/g, ' ').trim()).filter((x) => x && wlen(x) <= 36 && !policyIssues(x).length).filter((x, i, a) => a.indexOf(x) === i).slice(0, 3) : [];
226
+ // 🔴 글 위주(배경 이미지 없음) 소재가 허전하다는 사장님 지적(2026-09-30 「이렇게 허전하면 텍스트 광고를 누가 하겠니」) → 글 위주 포스터는 장식 도형·혜택 뱃지·증거 칩으로 채운다
227
+ const rich = !bg && !banner && !short && !narrow && !googleLight && Math.min(w, hh) >= 600;
228
+ const richProofN = rich && !widePoster ? (hh / w > 1.5 ? 3 : 2) : 3;
229
+ let proofs = widePoster || rich ? [...(brief.proofPoints || []), ...(brief.usp || [])].map((x) => String(x || '').replace(/\s+/g, ' ').trim()).filter((x) => x && wlen(x) <= 36 && !policyIssues(x).length).filter((x, i, a) => a.indexOf(x) === i).slice(0, richProofN) : [];
226
230
  const cjk = /[\u3000-\u9fff\uac00-\ud7af]/.test(body);
227
231
  let bodyText = banner ? cutWords(body, 70) : widePoster ? cutWords(body, cjk ? 60 : 110) : googleLight ? cutWords(body, 48) : body;
228
232
  if (widePoster) {
@@ -253,11 +257,22 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
253
257
  if (total() > avail)
254
258
  proofs = [];
255
259
  }
260
+ const badgePool = [brief.offer, ...(brief.usp || []), ...(brief.proofPoints || [])].map((x) => String(x || '').replace(/\s+/g, ' ').trim()).filter((x) => x && wlen(x) <= 18 && !policyIssues(x).length);
261
+ const badge = rich && !widePoster ? (badgePool.find((x) => /%|할인|무료|혜택|증정|off|free/i.test(x)) || '') : '';
256
262
  const chips = proofs.length ? h('div', { display: 'flex', flexWrap: 'wrap', gap: Math.round(chipFs * 0.45) }, proofs.map((t) => h('div', { display: 'flex', alignItems: 'center', gap: Math.round(chipFs * 0.4), fontSize: chipFs, fontWeight: 700, color: fg, backgroundColor: dark ? 'rgba(255,255,255,.16)' : 'rgba(255,255,255,.78)', borderRadius: 999, padding: `${Math.round(chipFs * 0.28)}px ${Math.round(chipFs * 0.7)}px`, whiteSpace: 'nowrap' }, [h('div', { width: Math.round(chipFs * 0.42), height: Math.round(chipFs * 0.42), borderRadius: 999, backgroundColor: primary }), h('div', { whiteSpace: 'nowrap' }, t)]))) : null;
257
263
  const children = [];
258
264
  if (bg)
259
265
  children.push(h('img', { position: 'absolute', top: 0, left: 0, width: w, height: hh, objectFit: 'cover', filter: dark ? 'brightness(.66)' : 'brightness(1.02)' }, undefined, { src: bg, width: w, height: hh }));
260
266
  children.push(h('div', { position: 'absolute', top: 0, left: 0, width: w, height: hh, backgroundImage: dark ? `linear-gradient(180deg, ${hexA('#0b1526', scrimTop)} 0%, ${hexA('#0b1526', scrimBottom)} 100%)` : `linear-gradient(180deg, rgba(255,255,255,${scrimTop.toFixed(2)}) 0%, rgba(255,255,255,${scrimBottom.toFixed(2)}) 100%)` }));
267
+ if (rich) {
268
+ // 장식 — 큰 원 두 개 · 링 · 방사형 빛. 브랜드색을 옅게 깔아 「빈 배경」이 아니라 디자인된 포스터로 보이게
269
+ const tint = (a) => hexA(primary, dark ? a * 1.5 : a);
270
+ children.push(h('div', { position: 'absolute', top: 0, left: 0, width: w, height: hh, backgroundImage: `radial-gradient(circle at 82% 16%, ${tint(0.34)} 0%, ${tint(0)} 52%)` }, ''));
271
+ children.push(h('div', { position: 'absolute', right: -Math.round(base * 0.2), top: -Math.round(base * 0.16), width: Math.round(base * 0.85), height: Math.round(base * 0.85), borderRadius: 9999, backgroundColor: tint(0.16) }, ''));
272
+ children.push(h('div', { position: 'absolute', left: -Math.round(base * 0.28), bottom: -Math.round(base * 0.3), width: Math.round(base * 0.8), height: Math.round(base * 0.8), borderRadius: 9999, backgroundColor: tint(0.12) }, ''));
273
+ children.push(h('div', { position: 'absolute', right: Math.round(base * 0.06), bottom: Math.round(base * 0.16), width: Math.round(base * 0.34), height: Math.round(base * 0.34), borderRadius: 9999, border: `${Math.max(2, Math.round(base * 0.004))}px solid ${tint(0.4)}` }, ''));
274
+ children.push(h('div', { position: 'absolute', right: Math.round(base * 0.13), bottom: Math.round(base * 0.23), width: Math.round(base * 0.2), height: Math.round(base * 0.2), borderRadius: 9999, backgroundColor: tint(0.22) }, ''));
275
+ }
261
276
  const brandRow = h('div', { display: 'flex', alignItems: 'center', gap: Math.round(pad * 0.3) }, [
262
277
  logo ? h('img', { height: short ? Math.round(hh * 0.5) : Math.round(headFs * 0.9), borderRadius: 6 }, undefined, { src: logo }) : h('div', { fontSize: Math.round(headFs * 0.42), fontWeight: 800, color: fg, letterSpacing: -0.5 }, brief.company),
263
278
  ]);
@@ -274,6 +289,7 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
274
289
  children.push(h('div', { position: 'absolute', top: 0, left: 0, width: w, height: hh, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: pad }, [
275
290
  h('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, [brandRow, domain && !banner ? h('div', { fontSize: Math.round(headFs * 0.3), fontWeight: 500, color: sub }, domain) : h('div', {}, '')]),
276
291
  h('div', { display: 'flex', flexDirection: 'column', gap: Math.round(pad * (banner ? 0.35 : 0.5)), maxWidth: isWide ? Math.round(w * 0.72) : w - pad * 2 }, [
292
+ ...(rich && !isWide ? [h('div', { display: 'flex', alignItems: 'center', gap: Math.round(pad * 0.3) }, [h('div', { width: Math.round(base * 0.075), height: Math.max(6, Math.round(base * 0.008)), borderRadius: 9, backgroundColor: primary }, ''), ...(badge ? [h('div', { display: 'flex', fontSize: Math.round(bodyFs * 0.82), fontWeight: 700, color: onColor(primary), backgroundColor: primary, borderRadius: 999, padding: `${Math.round(bodyFs * 0.25)}px ${Math.round(bodyFs * 0.7)}px` }, badge)] : [])])] : []),
277
293
  linesEl(head, { fontSize: headFs, fontWeight: 800, color: fg, lineHeight: 1.12, letterSpacing: -1.5 }, isWide ? Math.round(w * 0.72) : w - pad * 2),
278
294
  // 본문도 같은 규칙으로 균형 있게(「… 분들께 / 추천해요.」 한 어절 고아 방지 · 최대 4줄 · 배너는 폭이 좁아 렌더러 줄바꿈)
279
295
  (banner && bodyFs < 14) || bodyFs === 0 ? h('div', {}, '') : banner ? wordsEl(bodyText, { fontSize: bodyFs, fontWeight: 500, color: sub, lineHeight: 1.45 }) : linesEl(bodyText, { fontSize: bodyFs, fontWeight: 500, color: sub, lineHeight: 1.45 }, isWide ? Math.round(w * 0.72) : w - pad * 2, 'left', 4),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adyou",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "ADYou — 대행사 없이, 당신이 직접. 사이트 주소 하나로 광고 소재·매체 등록·자동 운영·보고까지: AI 광고 자율주행 CLI + MCP 서버(Meta·Google · 생성은 항상 PAUSED · 승인 뒤 시작)",
5
5
  "type": "module",
6
6
  "license": "MIT",