first commit
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
# 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/<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`, `recipes_dev`
|
||||
- 운영: `users`, `recipes`
|
||||
- 각 `users*.googleSub`: unique index
|
||||
- 각 `recipes*`: `ownerGoogleSub + source.platform + source.sourceId` unique index
|
||||
- 원본 text와 YouTube transcript를 recipe source에 보존해 재분석에 사용할 수 있습니다.
|
||||
|
||||
이미지의 DB 값은 다음과 같은 상대 경로뿐입니다.
|
||||
|
||||
```text
|
||||
recipes/<recipe-id>/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)
|
||||
Reference in New Issue
Block a user