# Our Recipe Atlas Instagram 게시물과 YouTube 영상의 텍스트를 레시피로 구조화해 개인 MongoDB와 로컬 SSD에 보관하는 웹 애플리케이션입니다. Fastify가 REST API와 Vanilla JavaScript 화면을 함께 제공합니다. ## 현재 구현 범위 - Google Authorization Code/OIDC 로그인, PKCE `S256`, 이메일 allowlist - HttpOnly 자체 인증 cookie와 소유자별 API 접근 제어 - Instagram caption 추출 및 YouTube 설명·자막·timestamp 추출 - MiniMax OpenAI 호환 Chat Completions API를 이용한 레시피 구조화 - 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`, `recipes_dev` - 운영: `users`, `recipes` - 각 `users*.googleSub`: unique index - 각 `recipes*`: `ownerGoogleSub + source.platform + source.sourceId` unique index - 원본 text와 YouTube transcript를 recipe source에 보존해 재분석에 사용할 수 있습니다. 이미지의 DB 값은 다음과 같은 상대 경로뿐입니다. ```text recipes//cover.webp ``` 실제 파일은 `IMAGE_ROOT` 아래에 있고 `/media/` 경로로 제공됩니다. 이미지 다운로드는 HTTPS만 허용하고 DNS가 사설/loopback 주소로 해석되면 차단합니다. ## API 모든 `/api/**` endpoint는 인증 cookie가 필요하며 로그인한 사용자의 데이터만 반환합니다. | 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)