Open-Source Cold Email & Warmup Platform
Warmbly is the open-source platform for cold email: warm your mailboxes, send campaigns, and triage every reply. Run it on our cloud or self-host the whole thing.
AI SNS Newsletter Generator created from Stitch
Translation pending
MarketingTypeScript · CSSGitHub @choiyh040-bitOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
키워드나 기사 URL을 넣으면 인스타그램 카드뉴스(1080×1350, 4:5) 4~5장을 만들고 PNG로 내려받는 웹앱입니다. 생성 결과는 브라우저의 localStorage에 최대 20건까지 보관합니다 (src/lib/cardNewsStore.ts). 결과마다 id가 있어 /preview?id=... 주소로 다시 열 수 있고, 새로고침하거나 브라우저를 껐다 켜도 남습니다. 서버에 저장하지는 않습니다. 그래서 다른 기기나 다른 사람과는 공유되지 않습니다. 링크 공유가 필요해지면 데이터베이스가 필요합니다. 규칙의 원본은 .claude/skills/mice-cardnews/SKILL.md입니다. 코드를 고칠 때는 그 문서를 먼저 읽으세요. 아래는 각 규칙이 실제로 어디에 구현돼 있는지입니다. SKILL.md는 태그로 줄을 끊으라고 하지만, 여기서는 줄 배열을 씁니다. 효과는 같으면서 HTML을 문자열로 주입하지 않아도 되기 때문입니다.Read the full README ↗
키워드나 기사 URL을 넣으면 인스타그램 카드뉴스(1080×1350, 4:5) 5~7장을 만들고 PNG로 내려받는 웹앱입니다.
홈(/) → 로딩(/loading) → 미리보기(/preview)
↓
① POST /api/research 기사 본문 읽기 (못 읽거나 키워드뿐이면 Gemini 검색으로 자료 수집)
↓ 자료(subject·material·evidence)를 화면이 받아서 그대로 넘김
② POST /api/generate 자료로 카드뉴스 생성 (검색 없음) → 카드 데이터
요청을 둘로 나눈 것은 시간 제한 때문입니다. 한 요청에서 검색과 생성을 다 하면 합이 서버 예산(55초)을 넘었습니다. 요청마다 예산이 따로라, 나누면 단계마다 온전히 씁니다.
생성 결과는 브라우저의 localStorage에 최대 20건까지 보관합니다
(src/lib/cardNewsStore.ts). 결과마다 id가 있어 /preview?id=... 주소로
다시 열 수 있고, 새로고침하거나 브라우저를 껐다 켜도 남습니다.
서버에 저장하지는 않습니다. 그래서 다른 기기나 다른 사람과는 공유되지 않습니다. 링크 공유가 필요해지면 데이터베이스가 필요합니다.
규칙의 원본은 .claude/skills/mice-cardnews/SKILL.md입니다. 코드를 고칠 때는
그 문서를 먼저 읽으세요. 아래는 각 규칙이 실제로 어디에 구현돼 있는지입니다.
| 규칙 (SKILL.md) | 구현 위치 |
|---|---|
| 원문 베끼지 않고 패러프레이즈 | api/generate/route.ts 프롬프트 [문장 규칙] |
| 직접인용 15단어 미만, 세트당 1회 | 〃 |
| 관계자 코멘트는 요약체로 | 〃 |
| 카드 5~7장 (표지 + 상세 + 마무리) | 프롬프트 [구성], cardnews.ts의 MIN_SLIDES/GENERATED_MAX_SLIDES, 벗어나면 generate/route.ts가 재시도 |
| 보도용·유료 스톡 이미지 사용 금지 | 사진을 아예 쓰지 않음. CardNewsCard.tsx가 그라데이션으로 대체 |
| 캔버스 1080 × 1350 (4:5) | cardnews.ts의 CARD_WIDTH/CARD_HEIGHT |
| 원티드산스 | public/fonts/, globals.css의 @font-face. 명조 템플릿은 마루 부리 (일부러 다르게: SKILL.md 는 원티드산스만 말한다) |
| 좌상단 카테고리 배지 | 각 템플릿 (src/components/templates/). DarkNeon은 알약 대신 네온색 글자 |
| 중앙 반투명 다크 카드 위 흰 텍스트 | GradientCenter.tsx (rgba(10,14,20,0.58)) |
| 세트 전체를 포인트 컬러 하나로 통일 | lib/templates/color.ts — 색상은 고정, 명도만 장별로 변화 |
| 표지는 작은 소제목 → 큰 헤드라인 | 각 템플릿의 isCover 분기 |
| 한글 줄바꿈을 직접 끊기 | 헤드라인·본문을 줄 배열로 받음 (CardSlide.heading, .body) |
| (규칙 밖) 낱말 강조 | *색만*과 ==형광펜== 두 가지. parseSpans가 조각으로 쪼갠다 |
| 캡션 3~5줄 + 출처 표기 | 프롬프트 JSON 구조의 caption, source |
| (규칙 밖) 상세 장 결론 상자 | CardSlide.note — 데이터와 결론을 눈으로 분리 |
저장 위치 YYYY-MM-DD-제목 | preview/page.tsx의 baseName (PNG/ZIP 파일명) |
SKILL.md는 <br> 태그로 줄을 끊으라고 하지만, 여기서는 줄 배열을 씁니다.
효과는 같으면서 HTML을 문자열로 주입하지 않아도 되기 때문입니다.
한 줄 안에서 일부만 다르게 칠할 수 있습니다. 세기가 다른 두 가지입니다.
| 표시 | 효과 | 얼마나 |
|---|---|---|
*이렇게* | 글자 색만 바뀝니다 | 슬라이드마다 1~2번 |
==이렇게== | 형광펜처럼 뒤에 색이 깔립니다 | 세트 전체에서 2~3번 |
cardnews.ts의 parseSpans가 조각(TextSpan)으로 쪼개고, 템플릿이
style에 따라 다르게 그립니다.
형광펜 위에 올릴 글자색은 readableOn이 바탕 밝기를 보고 고릅니다. 색을
열어 둔 이상 이것도 계산해야 합니다 — 노란 형광펜에는 검은 글자가, 분홍
형광펜에는 흰 글자가 읽힙니다.
모델에게 중첩 객체 배열을 받는 대신 문자열 안에 표시를 넣게 했습니다. 응답 형식이 문자열 배열 그대로라 이미 저장해 둔 결과도 그대로 읽힙니다. 모델 입장에서도 배열을 겹쳐 만드는 것보다 쉽습니다.
아직 구현하지 않은 규칙:
/my)만든 결과는 이 브라우저(localStorage)에만 최근 20건 저장됩니다(cardNewsStore.ts의 MAX_ENTRIES).
/my 에서 표지 썸네일로 모아 보고, 누르면 미리보기로 다시 열립니다. 입구는 메뉴(☰·상단), 홈 입력칸 아래
(결과가 있을 때만), 미리보기 "최근 생성"의 "전체 보기"입니다. 서버에 저장하지 않으므로 다른 기기에서는 안 보입니다.
미리보기에서 내용 고치기를 누르면 지금 보고 있는 장을 손볼 수 있습니다
(src/components/SlideEditor.tsx). 고치는 즉시 왼쪽 카드에 반영되고
브라우저에 저장됩니다.
| 할 수 있는 것 | 방법 |
|---|---|
| 문장 수정 | 칸에 그냥 고쳐 씁니다 |
| 줄바꿈 | 줄바꿈 하나가 카드의 한 줄입니다 |
| 강조 | 글자를 고르고 색 / 형광펜 버튼. 직접 *...*, ==...== 를 쳐도 됩니다 |
| 장 추가·삭제 | 칸 아래 버튼 |
사람과 모델이 같은 표기(*...*, ==...==)를 씁니다. 그래서 편집기가
따로 배울 것을 만들지 않습니다.
줄 수 상한을 넘기면 입력을 받지 않고 이유를 알려 줍니다. 상한은
normalizeCardNews 가 자르는 값과 같습니다.
생성은 5~7장이지만 사람이 최대 8장까지 늘릴 수 있습니다(MAX_SLIDES).
src/lib/templates/에 있습니다. 두 축으로 나눕니다.
color(단색·그라데이션)와 photo(사진 위 글씨)sans(원티드산스), serif(마루 부리), hand(개구 손글씨). 사진 · 자막은 장마다
도현·검은고딕도 씁니다(OFL).사진형은 글자에 테두리나 그늘을 넣는데, 그건 사진이 복잡해서 생긴 장치입니다. 같은 처리를 단색 배경에 쓰면 근거 없이 요란해지므로 섞지 않습니다.
템플릿은 10종입니다: 그라데이션 · 고딕, 사진 · 반투명 카드, 크림 바탕 · 굵은 고딕, 흰 바탕 · 사진 상자, 사진 · 자막, 검정 · 번호 정리, 손글씨 메모, 문서 페이지, 어두운 바탕 · 네온, 흰 바탕 · 강조색(이 둘은 서체 고딕·명조를 고름). 사진 템플릿도 늘 고를 수 있습니다(사진을 못 찾으면 그라데이션으로 그립니다).
어두운 바탕 · 네온은 헤드라인과 본문 크기를 숫자로 박지 않고
fitFontSize(src/lib/templates/fit.ts)로 계산합니다.
고정값으로는 성립하지 않기 때문입니다. 한 줄이 세 글자인 장과 열 글자인 장에 같은 크기를 주면 짧은 쪽은 혼자 작게 남고 긴 쪽은 넘칩니다. 가로로는 가장 긴 줄이, 세로로는 전체 줄이 들어가야 하므로 둘 중 빡빡한 쪽을 따릅니다.
그래서 프롬프트가 줄을 짧게 끊게 합니다(표지 한 줄 36자, 45줄).
줄이 짧아야 글자가 커지고 화면이 찹니다. 참고 자료를 재 보니 "텅 비어
보인다"의 원인은 글자 크기가 아니라 줄 수였습니다 — 자세한 것은
DEVLOG 2026-10-05 항목에 있습니다.
새 디자인을 넣으려면 src/components/templates/에 컴포넌트를 만들고
src/lib/templates/index.ts의 TEMPLATES 배열에 한 줄 추가하면 됩니다.
홈 화면의 디자인 선택기(src/components/TemplatePicker.tsx)는 이 배열을 읽어
실제 템플릿을 견본 표지로 작게 그립니다. 미리보기 그림 파일을 따로 만들
필요가 없고, 추가한 템플릿은 선택기에도 저절로 나옵니다. 사진 템플릿의
견본에는 public/sample-photo.svg 를 깝니다.
사용자가 보낸 참고 게시물(서울 신상 미술관, 서울썸머비치)을 옮긴 레이아웃입니다
(PhotoStory.tsx). 장마다 다른 사진을 꽉 채워 깔고,
강조색은 사람이 고릅니다(PHOTO_VARIANTS + 직접 고르기).
사진은 생성할 때 서버가 자동으로 찾습니다(src/lib/photo.ts). 모델이 장마다
다른 장면의 검색어(slides[].imageQuery)를 내고, findSlidePhotos가 이를
한꺼번에 찾아 장마다 겹치지 않게 나눠 줍니다. 장 검색어로 못 찾은 장은 세트
검색어의 후보에서 아직 안 쓴 사진을 받습니다. 그릴 때는 이 장에 올린 사진 →
이 장 사진 → 세트 사진(옛 결과) 순으로 씁니다(slidePhoto).
미리보기의 다른 사진 / 내 사진 올리기 / 올린 사진 지우기는 지금 보는 장에만 적용됩니다. 캡션 끝 사진 표기는 쓴 사진을 모두 적되, 픽사베이·언스플래쉬는 곳마다 작가를 한 줄로 묶고 위키미디어는 사진마다 원본 주소까지 한 줄입니다.
docs/template-references.md 의 "H. 추가 묶음" 1~4번에서 하나씩 만들었습니다.
| 템플릿 | 묶음 | 컴포넌트 | 핵심 |
|---|---|---|---|
| 흰 바탕 · 사진 상자 | 1번 제작 가이드 | PhotoFrame | 흰 바탕에 둥근 사진을 얹는다. 본문 장은 글보다 사진이 크다 |
| 사진 · 자막 | 3번 사진 자막 | PhotoCaption | 상자 없이 자막처럼. 장마다 글씨 모양 7가지 중 사진 밝기에 맞는 것을 자동으로(아래) |
| 손글씨 메모 | 2번 손글씨·필기 | HandNote | 종이색 바탕, 개구 손글씨, 형광펜·빨간 펜·손으로 그은 밑줄 |
| 문서 페이지 | 4번 노션 스타일 | DocPage | 흰 문서 한 쪽. 굵은 제목, 구분선, 체크한 주제 |
| 검정 · 번호 정리 | 2026-10-30 "8가지 프롬프트" 게시물 | DarkNumbered | 표지는 사진 아래 큰 제목. 상세는 큰 번호 "01." + 색 점, 제목·부제, 둥근 글 상자(형광펜은 회색 칩). 사진은 표지에만(coverPhotoOnly) |
background: "photo" 라 장마다 다른 사진과 사진 패널(다른 사진·올리기)이 그대로 붙습니다.Hand Fallback 이 채웁니다.
나눔은 OFL 예약 이름이 있어 떼어 낸 판의 이름을 바꿨습니다.src/lib/templates/captionLooks.ts): 굵은 고딕·기울임, 좁고 높은 고딕(도현),
외곽선 + 흰 띠, 명조 색, 개큰글씨 스티커(검은고딕), 색 고딕, 검정 띠 자막.src/lib/photoTone.ts) 그 자리에서 읽히는 모양만 고르고, 세트 전체에서
이웃한 장이 겹치지 않게 정합니다(assignLooks, 미리보기에서). 미리보기에서 장마다 바꾸거나
모든 장을 한 모양으로 맞출 수 있습니다.copy: "caption").홈에서 뉴스형·정보형·홍보형·감성형(반말 구어체)을 고릅니다(src/lib/tone.ts). 생성 프롬프트의 문장 규칙과
마무리 장 규칙만 바뀌고, 사실 규칙(자료에 없는 사실·과장 금지)은 같습니다.
사용자가 보낸 참고 게시물("인스타 카드뉴스 성공하는 방법 vs 실패하는 방법")에서
가져왔습니다(CreamBold.tsx). 크림 바탕에 아주 굵은 검정 제목, 핵심어만
강조색(기본 빨강, CREAM_VARIANTS + 직접 고르기). 장의 주제는 가는 가로줄 위에
얹힌 회색 알약, 표지 아래에는 "옆으로 넘겨보세요 →". 참고 게시물의 그림(휴대폰
화면, ✅❌ 이모지)은 그 작가가 그린 것이라 옮기지 않았습니다.
찾는 곳과 순서: 픽사베이 → 언스플래쉬 → 위키미디어 공용. 앞의 두 곳은 API 키가 있을 때만 씁니다(아래). 키가 없으면 위키미디어만 씁니다.
| 곳 | 라이선스 | 카드·캡션 표기 |
|---|---|---|
| 픽사베이 | Pixabay Content License (상업적 사용 가능) | 사진 · 작가 · Pixabay |
| 언스플래쉬 | Unsplash License (상업적 사용 가능) | 사진 · 작가 · Unsplash (API 규정상 의무) |
| 위키미디어 | CC0·PD·CC BY·CC BY-SA 만 | 사진 · 작가 · 라이선스 · Wikimedia Commons |
| 직접 올린 사진 | 올린 사람이 확인 | 붙이지 않음 |
imageQuery). 행사장 그 자체가 아니어도 되고
장면("conference audience")·소재("semiconductor wafer")도 됩니다. 실존 인물·
브랜드 이름은 금지./api/photo 를 거쳐 받습니다(PNG 로 구울 때 같은 출처여야 함).
픽사베이는 사진 주소가 한동안만 살아 있어 사진 번호만 저장하고 그릴 때마다
새 주소를 받습니다(/api/photo?pixabay=번호).COMMONS_API_URL, PIXABAY_API_URL, UNSPLASH_API_URL, PHOTO_HOSTS
로 가짜 서버를 붙일 수 있습니다.| 변수 이름 | 받는 곳 |
|---|---|
PIXABAY_API_KEY | pixabay.com 가입 → https://pixabay.com/api/docs/ 에 로그인한 채로 들어가면 "key" 에 바로 나옴 |
UNSPLASH_ACCESS_KEY | https://unsplash.com/developers → Your apps → New Application → 약관 동의 → 앱 이름 입력 → 만들어진 앱의 "Access Key" (Secret Key 아님) |
Vercel: 프로젝트 → Settings → Environment Variables 에 위 이름으로 넣고(Production),
Deployments 에서 Redeploy 해야 반영됩니다. 들어갔는지는 /api/photo-check 로 확인합니다
(키 값은 보여 주지 않고, 있는지·시험 검색 상태만 알려 줍니다). 언스플래쉬는 처음에 시간당 50회
제한(Demo)이고, 앱 페이지에서 Production 신청을 하면 늘어납니다.
홈에서 고르거나 미리보기에서 바꾼 디자인(템플릿·색 조합·정렬)은
localStorage의 cardNewsDesign에 남고, 다음 생성의 기본값이 됩니다.
결과마다 붙는 선택(CardNewsEntry.templateId 등)과는 따로입니다. 결과의
선택은 그 결과를 다시 열 때 쓰고, 이것은 새로 만들 때 씁니다.
다른 곳은 건드릴 필요가 없습니다. 배열의 첫 항목이 기본값입니다.
고른 템플릿은 결과와 함께 보관되므로(CardNewsEntry.templateId), 다른 결과를
보다가 돌아와도 유지됩니다.
레이아웃은 같고 색만 다른 것은 새 템플릿이 아니라 색 조합입니다. 그렇게 나누지 않으면 템플릿 목록이 금세 색 견본으로 뒤덮입니다.
어두운 바탕 · 네온 템플릿이 이 구조를 씁니다. 거의 검정인 바탕 위에 형광
한 색만 올리는 디자인이라, 색 한 쌍을 바꾸면 인상이 완전히 달라집니다.
새 조합을 넣는 일은 src/lib/templates/variants.ts에 한 줄 더하는 것이
전부입니다.
{ id: "black-lime", name: "검정 · 형광 그린", base: "#0A0C0A", neon: "#B8FF2E" },
컴포넌트는 base와 neon 두 값만 보고 나머지를 계산합니다. 글자색은
inkFor(base)가 바탕 밝기를 보고 뒤집으므로, 밝은 바탕도 그대로 됩니다.
그래서 흰 바탕 · 강조색은 같은 컴포넌트(DarkNeon)에 흰 바탕 색 조합
(LIGHT_VARIANTS)만 붙인 템플릿입니다.
두 템플릿 모두 색 조합 견본 외에 강조색을 직접 고를 수 있습니다(홈과
미리보기의 AccentPicker). 바탕은 고른 견본의 바탕 그대로이고 강조색만
바뀝니다. 직접 고른 색은 템플릿별로 따로 기억합니다(customAccents).
검정 위에서 고른 형광 연두가 흰 바탕으로 넘어오면 안 읽히기 때문입니다.
사용자가 바탕에서 안 읽히는 색(흰 바탕에 노랑 등)을 고르면, 글자로 쓰는
강조색만 legibleOn이 대비 3:1이 될 때까지 바탕 반대쪽으로 옮깁니다.
형광펜 바탕·바·네모는 고른 색 그대로 둡니다. 형광펜 위 글자색은
readableOn이 고릅니다.
네온·흰 바탕 계열은 정렬과 서체도 고를 수 있습니다(Template.alignable).
홈의 디자인 선택기와 미리보기 양쪽에서 고릅니다. 기본은
좌측이고, 짧고 단정적인 세트에서 중앙으로 바꿀 수 있습니다.
정렬은 장식이 아니라 기능입니다. 중앙 정렬은 줄마다 왼쪽 끝이 움직여서 서너 줄만 넘어가도 눈이 돌아올 지점을 잃습니다. 그래서 본문이 길면 좌측입니다.
우측 정렬은 넣지 않았습니다. 한글에서 우측 정렬은 너무 센 제스처라 본문에 쓰면 읽기가 확 나빠집니다. 필요해지면 한 줄짜리 전용으로 열면 됩니다.
모든 템플릿에 열지 않았습니다. GradientCenter는 중앙이 그 디자인의 정체성이라
바꾸면 다른 템플릿이 되어 버립니다.
네온과 흰 바탕 · 강조색은 서체를 고를 수 있습니다(Template.serifable). 정렬처럼 홈과 미리보기에서
고르고, 결과마다 저장합니다(CardNewsEntry.font). 같은 컴포넌트(DarkNeon)에 font 만 넘깁니다.
예전에는 명조가 별도 템플릿(… · 명조)이었습니다(2026-10-30 옵션으로 바꿈). 그 id 와 단색 · 명조
(SolidSerif, 2026-10-15 뺌)로 저장된 결과는 같은 배치 + 명조로 열립니다(templates/index.ts 의 RETIRED, fontFor).
이 템플릿만 기사에서 뽑은 accent를 쓰지 않습니다. 바탕이 고정이고 네온 한
색이 혼자 튀는 것이 전부라, 기사마다 색이 달라지면 인상이 무너지기 때문입니다.
색은 사람이 고르고, 고른 값은 결과에 함께 보관됩니다(CardNewsEntry.variantId).
앞으로 만들 디자인의 참고 자료와, 그것을 넣기 전에 풀어야 할 구조적 문제는
docs/template-references.md에 정리해 두었습니다.
카드가 아니라 사이트 화면(홈·미리보기·안내 페이지)의 규칙입니다(2026-10-16 사용자 결정).
tailwind.config.js 의 primary(#5b9bff)와 globals.css 의
.cta-gradient(#3b82f6 → #1d4ed8). 청록·인스타그램식 주황분홍은 쓰지 않습니다.
빨강(오류)만 예외입니다.components/SampleCards.tsx).app/opengraph-image.png(1200×630 고정 파일)입니다./about, /guide, /privacy, /terms (src/components/InfoPage.tsx 틀).
개인정보처리방침은 코드가 실제로 다루는 정보만 적었다. 다루는 정보가 바뀌면 함께 고친다.| 환경변수 | 내용 | 없으면 |
|---|---|---|
NEXT_PUBLIC_ADSENSE_CLIENT | 애드센스 게시자 번호 ca-pub-… | 광고 스크립트·광고 칸·/ads.txt·소유 확인 태그·방침의 광고 항목 모두 없음 |
NEXT_PUBLIC_ADSENSE_SLOT_LOADING | 생성 대기 화면 광고 단위 번호 | 그 칸 없음 |
NEXT_PUBLIC_ADSENSE_SLOT_RESULT | 결과 화면 맨 아래 광고 단위 번호 | 그 칸 없음 |
NEXT_PUBLIC_CONTACT_EMAIL | 문의 이메일 | "문의처 준비 중" |
/feedback)익명 공개 게시판. 글은 이 저장소의 GitHub 이슈(feedback 표시)로 저장됩니다
(src/lib/feedback.ts, src/app/api/feedback/route.ts).
FEEDBACK_GITHUB_TOKEN: 이 저장소만 Issues 읽기·쓰기 권한이 있는 fine-grained 토큰.
없으면 게시판은 "준비 중". FEEDBACK_REPO(기본 choiyh040-bit/newsletter)로 다른 저장소를 쓸 수 있습니다.npm install
cp .env.example .env.local # GEMINI_API_KEY 채우기
npm run dev # http://localhost:3000
GEMINI_API_KEY는 Google AI Studio에서
발급받습니다. 없으면 화면은 뜨지만 생성 요청이 500으로 실패합니다.
기사 URL을 줘서 본문을 읽어오는 데 성공하면 검색 도구를 붙이지 않습니다. 근거가 이미 손에 있고, 검색 도구는 일반 생성과 할당량이 따로라 붙이는 것만으로 429가 날 수 있기 때문입니다.
키워드만 준 경우에는 검색을 씁니다. 근거가 없는데 검색까지 빼면 모델이
없는 숫자를 지어냅니다(실제로 확인했습니다). 다만 검색은 /api/research
에서만 합니다. 사실만 평문으로 모으고, 그 자료를 기사 본문처럼 넘겨
/api/generate가 검색 없이 카드뉴스를 짓습니다.
검색 결과에 검색 근거 표시(groundingMetadata)가 없어도 버리지 않습니다.
lite 모델은 검색을 하고도 표시를 비워 보낼 때가 많습니다. 서버 로그에
검색 근거 표시 없음으로만 남깁니다.
즉 기사 본문을 읽어오는 성공률이 곧 기능의 가용성입니다. 그래서
readArticle은 평범한 브라우저 User-Agent로 요청합니다. 언론사 상당수가
봇처럼 보이는 요청을 막기 때문입니다.
출처 주소는 모델이 적은 것을 쓰지 않고 서버가 아는 주소를 붙입니다
(generate/route.ts의 withSources). 모델은 주소를 지어냅니다.
groundingChunks)의 주소. 구글이 주는 것은
vertexaisearch.cloud.google.com 경유 주소라 한동안만 살아 있으므로, 서버가
따라가서 실제 기사 주소로 바꿉니다(research/route.ts의 resolveLinks).
못 바꾸면 뺍니다.미리보기의 출처 칸에서 출처: 매체명 주소 꼴로 복사합니다.
src/lib/gemini.ts가 실행할 때 models.list()로 목록을 받아
쓸 만한 것을 골라 차례로 시도합니다. 이름을 박아 두면 낡습니다 — 실제로
단종(404)과 용량(503)으로 두 번 낡았습니다.
고르는 규칙:
generateContent를 지원한다고 밝힌 것만image·tts·transcribe·audio·lyria 같은 게 들어가면 제외
(목록에는 음악·이미지·음성 모델이 섞여 있습니다)gemini.ts의 Tier)flash 를 빼고 lite + LOW 로 고정한 것은 시간 때문입니다. flash(3.8)는 생성 한 번에 24~30초였고 대부분이 생각하는 시간이었습니다. lite + LOW 는 검색 2초, 생성 3초 안팎입니다. lite 가 전부 막히는 일이 로그에 실제로 보이면 그때 flash 를 맨 뒤에 붙입니다.
넘어가는 실패: 503·429·404, 시도 상한(32초)에 걸린 무응답, 생각 수준 설정을 거부한 400, 빈 검색 결과.
어느 모델이 응답했는지는 서버 로그([gemini] 검색|생성 성공 · 모델명 (걸린 시간))에
남습니다.
실패 화면이 원인을 세 갈래로 나눠 알려줍니다.
| 화면 문구 | 원인 | 할 일 |
|---|---|---|
| AI 모델 설정을 고쳐야 합니다 | 모델 단종(NOT_FOUND) | MODEL 값을 바꿉니다. 다시 시도해도 소용없습니다 |
| 사용량 한도 안내 | 쿼터 초과 | 할당량을 확인합니다 |
| 서버 혼잡 안내 | 일시적 UNAVAILABLE | 잠시 후 다시 시도합니다 |
주의할 점 두 가지입니다.
ListModels에는 보이는데
generateContent는 404를 주는 경우가 있습니다. 호출해 봐야 압니다.googleSearch)는 쿼터가 따로입니다. 맨 요청은 되는데 카드뉴스
생성만 429가 날 수 있습니다.GEMINI_API_KEY를 추가합니다.
Production / Preview / Development 세 곳 모두 체크하세요.환경변수를 나중에 추가했다면 재배포해야 반영됩니다. 기존 배포에는 적용되지 않습니다.
저장소의 vercel.json이 "framework": "nextjs"를 지정합니다. 이 값은 Vercel
대시보드의 Framework Preset 설정보다 우선합니다.
이 파일이 없고 대시보드 설정이 Other이면, Vercel은 next build를 돌려
빌드는 성공시키지만 그 결과물 대신 public/ 폴더를 정적 사이트로 서빙합니다.
public/에는 index.html이 없으므로 모든 경로가 NOT_FOUND로 404가 됩니다.
빌드가 성공했는데 사이트가 404인 증상이면 이 설정부터 확인하세요.
/api/research와 /api/generate는 각각 maxDuration을 60초로 잡아두었고,
그 안에서 55초 예산으로 먼저 끝냅니다(src/lib/gemini.ts의 BUDGET_MS).
Vercel 요금제별 함수 실행 시간 상한을 넘으면 이 값과 무관하게 잘리므로,
타임아웃이 나면 요금제 상한부터 확인하세요.
카드에 쓰는 원티드산스(고딕)와
마루 부리(명조, 네이버)를
public/fonts/에 함께 넣어 두었습니다. 둘 다 OFL-1.1이고 라이선스 원문은
public/fonts/OFL.txt(원티드산스), public/fonts/OFL-MaruBuri.txt에 있습니다.
마루 부리는 원본 TTF를 글자를 빼지 않고 WOFF2로만 바꿨고, 굵기 셋
(400·600·700, 각 약 440KB)만 넣었습니다.
서체를 바꿀 때는 라이선스부터 봅니다. 웹사이트에 올리는 것은 서체 파일을 남에게 내려보내는 일(재배포·임베딩)이고, WOFF2로 바꾸는 것은 형식 변환입니다. 둘 다 막아 둔 서체가 많습니다. 열린명조가 그래서 후보에서 빠졌습니다.
한글 한 자의 폭이 서체마다 다릅니다(원티드산스 0.86em, 마루 부리 0.97em).
글자 크기 계산(fitFontSize)은 이 폭으로 가장 긴 줄을 어림하므로, 서체를
바꾸면 charWidth도 맞춰야 합니다. 안 맞추면 긴 줄이 칸을 넘어 쪼개집니다.
CDN에서 불러오지 않고 저장소에 두는 이유는 PNG로 내보낼 때입니다. 카드를 이미지로 구우려면 폰트를 파일 안에 심어야 하는데, 다른 출처에서 받은 폰트는 CORS 때문에 글자가 깨질 수 있습니다.
| 경로 | 하는 일 |
|---|---|
src/app/api/research/route.ts | 기사 읽기, 못 읽으면 검색으로 자료 수집 |
src/app/api/generate/route.ts | 자료로 카드뉴스 생성 + 형식 검증/재시도 |
src/lib/gemini.ts | 모델 고르기(lite·LOW), 폴백, 시간 예산 |
src/lib/cardnews.ts | 카드 규격, 데이터 타입, 모델 응답 정리 |
src/components/templates/ | 카드 템플릿별 렌더링 (1080×1350) |
src/lib/templates/ | 템플릿 목록과 배경·글꼴 축 정의 |
src/lib/templates/variants.ts | 색 조합 목록. 새 색은 여기 한 줄 |
docs/template-references.md | 디자인 참고 자료와 미해결 과제 |
src/lib/exportCards.ts | 카드 DOM을 PNG/ZIP으로 내보내기 |
src/app/preview/page.tsx | 미리보기 화면과 내보내기 버튼 |