adyou 0.7.1 → 0.7.3

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.
@@ -76,17 +76,16 @@ const isHangul = (s) => /[가-힣]/.test(s);
76
76
  const KO_ADVERB_FOR_MODIFIER = /[가-힣](에|에서|로|으로|부터|께|에게|한테|와|과|서|처럼|같이|보다)$/;
77
77
  /** 자주 쓰는 2음절 관형형 용언(광고 문구) — 명사와 겹치지 않는 것만 */
78
78
  const KO_MODIFIER_2 = new Set(['구운', '만든', '고른', '담은', '키운', '빚은', '캐낸', '뽑은', '볶은', '우린', '지은', '튀긴', '삶은', '말린', '재운', '절인', '쪄낸', '끓인', '내린', '갈아낸', '짜낸', '따낸', '건진', '골라낸', '추린', '모은', '익힌', '갓난', '길러낸', '빻은', '깎은', '썰은', '다진', '무친', '데친', '졸인', '지진', '부친', '섞은', '띄운', '숙성한', '엄선한', '수확한', '재배한', '직접', '갓']);
79
+ /** 1음절 관형형 용언(광고 문구에서 부사어 뒤에 오는 것) — 명사와 겹치는 글자(문·손·길·말·물·눈)는 넣지 않는다 */
80
+ const KO_MODIFIER_1 = new Set(['딴', '산', '쓴', '든', '본', '준', '큰', '낼', '쓸']);
79
81
  /** 짧은 관형형 용언인가 — 1음절은 받침 ㄴ·ㄹ(딴·산·쓴·든·본·준·큰·낼·쓸 …)이고 관형사·수사·부사 결속 표에 없는 것 · 2음절은 표에 있는 것 */
80
82
  function isShortModifier(w) {
81
83
  const chars = [...w.replace(/[^가-힣]/g, '')];
82
84
  if (!chars.length || chars.length !== [...w].length)
83
85
  return false;
84
- if (chars.length === 1) {
85
- if (KO_GLUE_NEXT_STRONG.has(w) || KO_GLUE_NEXT_WEAK.has(w))
86
- return false;
87
- const fin = (w.codePointAt(0) - 0xac00) % 28;
88
- return fin === 4 || fin === 8;
89
- }
86
+ // 🔴 받침 ㄴ·ㄹ 이면 다 관형형이라고 보면 「문 앞까지」의 「문」·「손」·「길」 같은 명사가 앞 어절에 붙는다(2026-10-03 「하우스에서 문 / 앞까지 하루」) → 실제로 쓰는 1음절 관형형만 표로
87
+ if (chars.length === 1)
88
+ return KO_MODIFIER_1.has(w);
90
89
  if (chars.length === 2)
91
90
  return KO_MODIFIER_2.has(w);
92
91
  return false;
@@ -220,9 +220,24 @@ 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
+ // 🔴 같은 사실을 뱃지·알약·본문에 되풀이하지 않는다(2026-10-03 「무료배송」이 뱃지·알약·본문에 세 번) — 헤드라인 > 뱃지 > 알약 > 본문 순으로 한 번만 보여 준다
230
+ const flat = (t) => String(t || '').replace(/[\s·,.]/g, '').toLowerCase();
231
+ const headFlat = flat(head), bodyFlat = flat(body);
232
+ 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);
233
+ const badge = rich && !widePoster ? (badgePool.find((x) => /%|할인|무료|혜택|증정|off|free/i.test(x) && !headFlat.includes(flat(x))) || '') : '';
234
+ const badgeFlat = flat(badge);
235
+ const grams = (t) => { const a = []; for (let i = 0; i + 1 < t.length; i++)
236
+ a.push(t.slice(i, i + 2)); return a; };
237
+ const seenText = headFlat + '|' + bodyFlat + '|' + badgeFlat;
238
+ const novel = (x) => { const f = flat(x); if (!f || headFlat.includes(f) || bodyFlat.includes(f) || (badgeFlat && (badgeFlat.includes(f) || f.includes(badgeFlat))))
239
+ return false; const g = grams(f).filter((q) => !/\d/.test(q)); return !(g.length >= 4 && g.filter((q) => seenText.includes(q)).length / g.length >= 0.5); };
240
+ 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).filter(novel).slice(0, richProofN) : [];
226
241
  const cjk = /[\u3000-\u9fff\uac00-\ud7af]/.test(body);
227
242
  let bodyText = banner ? cutWords(body, 70) : widePoster ? cutWords(body, cjk ? 60 : 110) : googleLight ? cutWords(body, 48) : body;
228
243
  if (widePoster) {
@@ -253,11 +268,37 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
253
268
  if (total() > avail)
254
269
  proofs = [];
255
270
  }
271
+ if (rich && !widePoster && proofs.length) {
272
+ const colW = w - pad * 2;
273
+ const lns = (t, fs, cap = 9) => Math.min(cap, Math.max(1, breakLines(t, fs, colW).length));
274
+ const chipRows = () => { let rows = 0, x = 0; for (const t of proofs) {
275
+ const cw = textW(t, chipFs) + chipFs * 2.4;
276
+ if (!rows || x + cw > colW) {
277
+ rows++;
278
+ x = cw;
279
+ }
280
+ else
281
+ x += cw + chipFs * 0.45;
282
+ } return rows; };
283
+ const total = () => headFs * 0.9 + (badge ? bodyFs * 1.7 : 0) + lns(head, headFs) * headFs * 1.12 + lns(bodyText, bodyFs, 4) * bodyFs * 1.45 + chipRows() * chipFs * 1.9 + ctaFs * 2.2 + headFs * 0.3 + pad * 0.5 * 4;
284
+ const avail = hh - pad * 2;
285
+ while (proofs.length && total() > avail)
286
+ proofs = proofs.slice(0, -1);
287
+ }
256
288
  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
289
  const children = [];
258
290
  if (bg)
259
291
  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
292
  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%)` }));
293
+ if (rich) {
294
+ // 장식 — 큰 원 두 개 · 링 · 방사형 빛. 브랜드색을 옅게 깔아 「빈 배경」이 아니라 디자인된 포스터로 보이게
295
+ const tint = (a) => hexA(primary, dark ? a * 1.5 : a);
296
+ 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%)` }, ''));
297
+ 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) }, ''));
298
+ 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) }, ''));
299
+ children.push(h('div', { position: 'absolute', right: Math.round(base * 0.07), top: Math.round(base * 0.2), width: Math.round(base * 0.15), height: Math.round(base * 0.15), borderRadius: 9999, border: `${Math.max(2, Math.round(base * 0.004))}px solid ${tint(0.4)}` }, ''));
300
+ children.push(h('div', { position: 'absolute', right: Math.round(base * 0.095), top: Math.round(base * 0.225), width: Math.round(base * 0.1), height: Math.round(base * 0.1), borderRadius: 9999, backgroundColor: tint(0.22) }, ''));
301
+ }
261
302
  const brandRow = h('div', { display: 'flex', alignItems: 'center', gap: Math.round(pad * 0.3) }, [
262
303
  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
304
  ]);
@@ -274,6 +315,7 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
274
315
  children.push(h('div', { position: 'absolute', top: 0, left: 0, width: w, height: hh, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: pad }, [
275
316
  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
317
  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 }, [
318
+ ...(rich && !isWide ? [h('div', { display: 'flex', alignItems: 'center', gap: Math.round(pad * 0.3) }, [...(badge ? [] : [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
319
  linesEl(head, { fontSize: headFs, fontWeight: 800, color: fg, lineHeight: 1.12, letterSpacing: -1.5 }, isWide ? Math.round(w * 0.72) : w - pad * 2),
278
320
  // 본문도 같은 규칙으로 균형 있게(「… 분들께 / 추천해요.」 한 어절 고아 방지 · 최대 4줄 · 배너는 폭이 좁아 렌더러 줄바꿈)
279
321
  (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.1",
3
+ "version": "0.7.3",
4
4
  "description": "ADYou — 대행사 없이, 당신이 직접. 사이트 주소 하나로 광고 소재·매체 등록·자동 운영·보고까지: AI 광고 자율주행 CLI + MCP 서버(Meta·Google · 생성은 항상 PAUSED · 승인 뒤 시작)",
5
5
  "type": "module",
6
6
  "license": "MIT",