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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
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