264 lines
12 KiB
Markdown
264 lines
12 KiB
Markdown
# 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/<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로 생성할 수 있습니다.
|
|
|
|
```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/<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를 모킹합니다.
|
|
|
|
```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)
|