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에 제한을 둡니다.

Instagram

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.sourceId unique 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로 다음을 확인합니다.

  1. Instagram Reel/Post의 긴 caption 추출
  2. 실제 YouTube 레시피 영상의 description, transcript, timestamp 추출
  3. MiniMax 응답 품질과 미리보기 수정/저장
  4. 서버 재시작 후 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 서버에서만 접근 가능하게 제한합니다.

참고 문서

S
Description
No description provided
Readme
1,022 KiB
Languages
JavaScript 83.4%
CSS 12.5%
HTML 4.1%