Our Recipe Atlas
Instagram 게시물과 YouTube 영상의 텍스트를 레시피로 구조화해 개인 MongoDB와 로컬 SSD에 보관하는 웹 애플리케이션입니다. Fastify가 REST API와 Vanilla JavaScript 화면을 함께 제공합니다.
현재 구현 범위
- Google Authorization Code/OIDC 로그인, PKCE
S256, 이메일 allowlist - HttpOnly 자체 인증 cookie와 그룹 멤버 간 recipe 공유
- Instagram caption 추출 및 YouTube 설명·자막·timestamp 추출
- MiniMax OpenAI 호환 Chat Completions API를 이용한 레시피 구조화
- 영어 등 외국어 원문의 recipe 필드를 자연스러운 한국어로 번역
- AI 결과 미리보기/편집 후 저장
- Recipe CRUD, 그룹 멤버 조회와 소유자·원본별 중복 방지
- 외부 이미지를 최대 1280px WebP로 변환해
IMAGE_ROOT에 저장 - 반응형 Recipe 목록/상세 화면과 YouTube timestamp 링크
- nginx, systemd 운영 예제
영상 다운로드, STT, 브라우저 자동화 scraping, 회원가입, 결제, 소셜 기능은 포함하지 않습니다.
요구 사항
- Node.js 22 이상
- MongoDB
- Google OAuth 2.0 Web client
- MiniMax API/Token Plan key
- Instagram caption import를 사용할 경우 유효한 Instagram session cookie
YouTube.js와 insta-fetcher는 각 서비스의 비공식 API를 사용하므로 원본 서비스 변경에 따라 조정이 필요할 수 있습니다. 구현은 src/services/extractors/에 격리되어 있습니다.
개발 환경 실행
npm install
cp .env.example .env
npm start
Windows PowerShell에서는 다음처럼 복사할 수 있습니다.
Copy-Item .env.example .env
npm start
기본 주소는 http://localhost:3000입니다. MongoDB가 실행 중이어야 서버가 시작됩니다. Google 설정이 비어 있으면 정적 로그인 화면은 열리지만 /auth/google은 설정 오류를 반환합니다.
개발 중 파일 변경을 감시하려면 npm run dev를 사용합니다.
Windows 개발과 Linux 운영 환경 분리
실행 시 process.platform을 기준으로 프로필을 자동 선택합니다. 환경파일에 NODE_ENV, MongoDB URI, 이미지 경로를 넣어도 Windows/Linux 프로필 값이 우선합니다.
Windows에서 자동 적용되는 개발 프로필:
NODE_ENV development
MONGO_URI mongodb://192.168.0.240:27017/our_recipe_atlas
MONGO_FALLBACK_URI mongodb://172.16.0.7:27017/our_recipe_atlas
IMAGE_ROOT ./data/images
개발과 운영 모두 our_recipe_atlas DB를 사용합니다. 개발 데이터는 users_dev, recipes_dev 컬렉션으로 분리되며, 로그인 허용 계정은 공통 allowed_google_emails 컬렉션을 사용합니다. 프로젝트를 D:\project\our_recipe_atlas에서 실행하면 이미지는 D:\project\our_recipe_atlas\data\images\recipes\...에 저장됩니다.
Linux에서 자동 적용되는 운영 프로필:
NODE_ENV production
MONGO_URI mongodb://192.168.0.240:27017/our_recipe_atlas
MONGO_FALLBACK_URI mongodb://172.16.0.7:27017/our_recipe_atlas
IMAGE_ROOT /mnt/recipe-ssd/our_recipe_atlas/images
운영 데이터는 같은 DB의 users, recipes 컬렉션에 저장되고 이미지는 /mnt/recipe-ssd/our_recipe_atlas/images/recipes/...에 저장됩니다. 이미지의 DB 값은 두 OS 모두 recipes/<recipe-id>/cover.webp 형식입니다.
Windows에서 만든 node_modules는 운영 서버로 복사하지 말고, sharp 등 OS별 바이너리가 Linux용으로 설치되도록 운영 서버에서 npm ci --omit=dev를 실행해야 합니다.
환경변수
| 변수 | 설명 |
|---|---|
HOST, PORT |
Fastify listen 주소와 포트 |
PUBLIC_BASE_URL |
브라우저가 접근하는 외부 기준 URL |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
Google OAuth Web client 자격 증명 |
GOOGLE_CALLBACK_URL |
Google Console에 등록한 정확한 callback URL |
ALLOWED_GOOGLE_EMAILS |
DB가 비어 있을 때 최초 관리자로 등록할 이메일 목록 |
AUTH_JWT_SECRET |
자체 session token 서명 비밀값. 운영에서 필수 |
AUTH_SESSION_TTL_HOURS |
로그인 세션의 절대 만료시간(시간). 기본값 24, 범위 1~720 |
MINIMAX_API_KEY |
MiniMax API 또는 Token Plan key |
MINIMAX_BASE_URL |
기본값 https://api.minimax.io/v1 |
MINIMAX_MODEL |
기본값 MiniMax-M2.7 |
INSTAGRAM_SESSION_COOKIE |
insta-fetcher에 전달할 session cookie |
MongoDB 주소와 이미지 루트는 위의 OS 프로필에 고정됩니다. 서버 시작 시 primary MongoDB를 먼저 시도하고, 3초 내 연결되지 않으면 VPN fallback으로 연결합니다. 실행 중인 연결이 끊겼을 때 주소를 자동 전환하는 기능은 아닙니다.
.env는 Git에 포함되지 않습니다. Google secret, MiniMax key, Instagram cookie를 브라우저 코드나 로그에 넣지 마세요.
안전한 AUTH_JWT_SECRET 예시는 Node로 생성할 수 있습니다.
node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))"
Google OAuth 설정
Google Cloud Console에서 OAuth 동의 화면과 OAuth 2.0 Web application client를 준비합니다.
개발용 Authorized redirect URI:
http://localhost:3000/auth/google/callback
운영용 Authorized redirect URI:
https://recipe.example.com/auth/google/callback
운영에서는 http://192.168.0.250:3000/... 같은 raw IP callback을 사용하지 않습니다. 실제 도메인과 HTTPS를 nginx 또는 Caddy에서 종료하고 내부 192.168.0.250:3000으로 proxy합니다. scope는 openid email profile이며 ID Token의 서명, audience, issuer, expiry를 google-auth-library로 검증합니다. 내부 사용자 키는 이메일이 아닌 Google sub입니다.
허용할 계정은 다음처럼 설정합니다.
ALLOWED_GOOGLE_EMAILS=user1@gmail.com,user2@gmail.com
이 값은 allowed_google_emails 컬렉션이 비어 있을 때 한 번만 이관됩니다. 이후 로그인 허용 계정은 앱 하단의 로그인 허용 계정에서 이메일만 입력해 관리합니다. 최초 등록된 계정은 관리자이며, 관리자가 추가한 계정은 로그인만 허용되고 다른 계정을 초대할 수 없습니다.
외부 서비스 설정
MiniMax
MiniMax의 OpenAI 호환 endpoint를 사용합니다. key와 model은 서버 환경변수로만 전달합니다.
MINIMAX_API_KEY=...
MINIMAX_BASE_URL=https://api.minimax.io/v1
MINIMAX_MODEL=MiniMax-M2.7
파서는 최대 한 번만 repair 요청을 수행하며, 결과를 Zod schema로 검증한 뒤 미리보기로 반환합니다. 원문에 없는 재료와 수량을 추측하지 않도록 system prompt에 제한을 둡니다.
insta-fetcher가 사용할 유효한 session cookie를 넣습니다.
INSTAGRAM_SESSION_COOKIE=...
ID와 비밀번호로 서버에서 자동 로그인하지 않습니다. cookie는 만료될 수 있으며, caption 추출이 갑자기 실패하면 먼저 cookie 상태를 확인합니다.
YouTube
youtubei.js로 title, author, description, thumbnail, transcript를 가져옵니다. 자막이 없더라도 description이 있으면 분석을 계속합니다. 영상 파일은 다운로드하지 않습니다.
데이터와 이미지
MongoDB는 our_recipe_atlas 하나만 사용하고 collection으로 환경을 분리합니다.
- 공통:
allowed_google_emails - 개발:
users_dev,groups_dev,recipes_dev - 운영:
users,groups,recipes - 각
users*.googleSub: unique index - 각
groups*.memberGoogleSubs: 조회 index - 각
recipes*:ownerGoogleSub + source.platform + source.sourceIdunique index - 원본 text와 YouTube transcript를 recipe source에 보존해 재분석에 사용할 수 있습니다.
그룹 문서는 recipe를 함께 볼 Google 계정의 sub를 보관합니다. 그룹이 없는 사용자는 자기 recipe만 볼 수 있고, 같은 그룹의 recipe는 함께 조회하되 수정과 삭제는 작성자만 할 수 있습니다.
{
"name": "우리 가족",
"memberGoogleSubs": ["google-sub-1", "google-sub-2"]
}
이미지의 DB 값은 다음과 같은 상대 경로뿐입니다.
recipes/<recipe-id>/cover.webp
실제 파일은 IMAGE_ROOT 아래에 있고 /media/ 경로로 제공됩니다. 이미지 다운로드는 HTTPS만 허용하고 DNS가 사설/loopback 주소로 해석되면 차단합니다.
API
모든 /api/** endpoint는 인증 cookie가 필요합니다. Recipe 목록과 상세는 같은 그룹 멤버에게 공유되고, 생성·수정·삭제 권한은 작성자에게 유지됩니다.
| Method | Path | 역할 |
|---|---|---|
POST |
/api/import/preview |
URL 추출 및 AI recipe 미리보기 |
GET |
/api/recipes |
내 그룹의 recipe 목록 |
GET |
/api/recipes/:id |
내 그룹의 recipe 상세 |
POST |
/api/recipes |
미리보기 확인 후 저장 |
PATCH |
/api/recipes/:id |
recipe 필드 수정 |
DELETE |
/api/recipes/:id |
recipe와 로컬 이미지 삭제 |
GET |
/auth/me |
현재 session 조회 |
POST |
/auth/logout |
session cookie 삭제 |
GET |
/api/allowed-emails |
관리자용 허용 이메일 목록 |
POST |
/api/allowed-emails |
관리자용 허용 이메일 추가 |
DELETE |
/api/allowed-emails/:email |
관리자용 허용 이메일 삭제 |
테스트
외부 서비스와 MongoDB를 계속 호출하지 않도록 repository, extractor, parser, image storage를 모킹합니다.
npm test
npm run lint
테스트 범위에는 URL/ID 판별, AI schema, timestamp 정규화, allowlist, 인증 middleware, 그룹 공유와 소유자 쓰기 권한, CRUD, 중복 source 처리, 이미지 상대경로와 SSRF 차단이 포함됩니다.
실제 계정과 네트워크가 준비된 뒤에는 별도 smoke test로 다음을 확인합니다.
- Instagram Reel/Post의 긴 caption 추출
- 실제 YouTube 레시피 영상의 description, transcript, timestamp 추출
- MiniMax 응답 품질과 미리보기 수정/저장
- 서버 재시작 후 MongoDB recipe와 SSD 이미지 표시
운영 배포 예시
권장 구성:
Browser
→ HTTPS reverse proxy
→ 192.168.0.250:3000 Fastify + SSD image storage
→ 192.168.0.240:27017 MongoDB
deploy/nginx.example.conf의 도메인과 인증서 경로를 바꾸고, deploy/our-recipe-atlas.service의 사용자·설치 경로·ReadWritePaths를 실제 환경에 맞춥니다.
Linux 운영 환경파일은 deploy/our-recipe-atlas.env.example을 기준으로 /etc/our-recipe-atlas.env에 만들 수 있습니다. 서비스 시작 전에 이미지 디렉터리를 서비스 계정 소유로 준비합니다.
sudo install -d -o recipe-atlas -g recipe-atlas /mnt/recipe-ssd/our_recipe_atlas/images
sudo cp deploy/our-recipe-atlas.env.example /etc/our-recipe-atlas.env
sudo chmod 600 /etc/our-recipe-atlas.env
npm ci --omit=dev
환경파일의 도메인, Google 설정, 이메일 allowlist, JWT secret, MiniMax key를 실제 값으로 교체한 뒤 서비스를 재시작합니다.
운영 환경 예시:
HOST=0.0.0.0
PORT=3000
PUBLIC_BASE_URL=https://recipe.example.com
GOOGLE_CALLBACK_URL=https://recipe.example.com/auth/google/callback
MongoDB 포트는 인터넷에 공개하지 말고 API 서버에서만 접근 가능하게 제한합니다.