adyou 0.7.2 → 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;
@@ -226,7 +226,18 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
226
226
  // 🔴 글 위주(배경 이미지 없음) 소재가 허전하다는 사장님 지적(2026-09-30 「이렇게 허전하면 텍스트 광고를 누가 하겠니」) → 글 위주 포스터는 장식 도형·혜택 뱃지·증거 칩으로 채운다
227
227
  const rich = !bg && !banner && !short && !narrow && !googleLight && Math.min(w, hh) >= 600;
228
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) : [];
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) : [];
230
241
  const cjk = /[\u3000-\u9fff\uac00-\ud7af]/.test(body);
231
242
  let bodyText = banner ? cutWords(body, 70) : widePoster ? cutWords(body, cjk ? 60 : 110) : googleLight ? cutWords(body, 48) : body;
232
243
  if (widePoster) {
@@ -257,8 +268,23 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
257
268
  if (total() > avail)
258
269
  proofs = [];
259
270
  }
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)) || '') : '';
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
+ }
262
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;
263
289
  const children = [];
264
290
  if (bg)
@@ -270,8 +296,8 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
270
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%)` }, ''));
271
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) }, ''));
272
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) }, ''));
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) }, ''));
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) }, ''));
275
301
  }
276
302
  const brandRow = h('div', { display: 'flex', alignItems: 'center', gap: Math.round(pad * 0.3) }, [
277
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),
@@ -289,7 +315,7 @@ export function tree(size, concept, brief, bg, logo, ctaText) {
289
315
  children.push(h('div', { position: 'absolute', top: 0, left: 0, width: w, height: hh, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: pad }, [
290
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', {}, '')]),
291
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 }, [
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)] : [])])] : []),
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)] : [])])] : []),
293
319
  linesEl(head, { fontSize: headFs, fontWeight: 800, color: fg, lineHeight: 1.12, letterSpacing: -1.5 }, isWide ? Math.round(w * 0.72) : w - pad * 2),
294
320
  // 본문도 같은 규칙으로 균형 있게(「… 분들께 / 추천해요.」 한 어절 고아 방지 · 최대 4줄 · 배너는 폭이 좁아 렌더러 줄바꿈)
295
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.2",
3
+ "version": "0.7.3",
4
4
  "description": "ADYou — 대행사 없이, 당신이 직접. 사이트 주소 하나로 광고 소재·매체 등록·자동 운영·보고까지: AI 광고 자율주행 CLI + MCP 서버(Meta·Google · 생성은 항상 PAUSED · 승인 뒤 시작)",
5
5
  "type": "module",
6
6
  "license": "MIT",