# 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/`에 격리되어 있습니다. ## 개발 환경 실행 ```bash npm install cp .env.example .env npm start ``` Windows PowerShell에서는 다음처럼 복사할 수 있습니다. ```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에서 자동 적용되는 개발 프로필: ```text 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에서 자동 적용되는 운영 프로필: ```text 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//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로 생성할 수 있습니다. ```bash 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: ```text http://localhost:3000/auth/google/callback ``` 운영용 Authorized redirect URI: ```text 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`입니다. 허용할 계정은 다음처럼 설정합니다. ```env ALLOWED_GOOGLE_EMAILS=user1@gmail.com,user2@gmail.com ``` 이 값은 `allowed_google_emails` 컬렉션이 비어 있을 때 한 번만 이관됩니다. 이후 로그인 허용 계정은 앱 하단의 **로그인 허용 계정**에서 이메일만 입력해 관리합니다. 최초 등록된 계정은 관리자이며, 관리자가 추가한 계정은 로그인만 허용되고 다른 계정을 초대할 수 없습니다. ## 외부 서비스 설정 ### MiniMax MiniMax의 OpenAI 호환 endpoint를 사용합니다. key와 model은 서버 환경변수로만 전달합니다. ```env 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를 넣습니다. ```env 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는 함께 조회하되 수정과 삭제는 작성자만 할 수 있습니다. ```json { "name": "우리 가족", "memberGoogleSubs": ["google-sub-1", "google-sub-2"] } ``` 이미지의 DB 값은 다음과 같은 상대 경로뿐입니다. ```text recipes//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를 모킹합니다. ```bash 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 이미지 표시 ## 운영 배포 예시 권장 구성: ```text 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`에 만들 수 있습니다. 서비스 시작 전에 이미지 디렉터리를 서비스 계정 소유로 준비합니다. ```bash 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를 실제 값으로 교체한 뒤 서비스를 재시작합니다. 운영 환경 예시: ```env 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 서버에서만 접근 가능하게 제한합니다. ## 참고 문서 - [Fastify OAuth2](https://github.com/fastify/fastify-oauth2) - [Google ID Token 검증](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token) - [YouTube.js](https://github.com/LuanRT/YouTube.js) - [insta-fetcher](https://github.com/Gimenz/insta-fetcher) - [MiniMax OpenAI 호환 Text Chat](https://platform.minimax.io/docs/api-reference/text-chat-openai)