AI-driven equity research SaaS — enter a ticker, get a professional analyst-style report in Korean or English. 글로벌 주식 AI 분석 SaaS — 종목만 입력하면 한국어/영어로 전문 애널리스트 리포트를 받아보세요.
- 🔍 Stock symbol search with normalized input (AAPL, NVDA, 005930, …)
- 🤖 MiniMax LLM-powered equity analysis (investment score + 5-section report)
- 🌍 Multi-language UI (🇰🇷 Korean / 🇺🇸 English) with
next-intl - 💾 Supabase PostgreSQL cache — same
(symbol, language)returns instantly without re-hitting the LLM - 🛡️ API-key safe — keys are server-only env vars, never exposed to the client
- ⚡ In-process dedup — concurrent identical requests share a single LLM call
- 📱 Responsive — mobile-first, works from 320px to 4K
- 🚀 Vercel-ready — deploy in under 2 minutes
| Layer | Choice |
|---|---|
| Frontend | Next.js 14 (App Router) · React 18 · TypeScript · Tailwind CSS · shadcn/ui |
| i18n | next-intl (URL-based: /ko/... and /en/...) |
| AI | MiniMax LLM API (MiniMax-M3 model) via direct fetch + JSON mode |
| Database | Supabase (PostgreSQL) + service-role key for server-side writes |
| Hosting | Vercel Free Plan |
app/
├─ app/ # Next.js App Router
│ ├─ [locale]/
│ │ ├─ layout.tsx # Locale-aware root layout
│ │ ├─ page.tsx # Home: search + recent analyses
│ │ ├─ loading.tsx # Suspense fallback
│ │ └─ analysis/[symbol]/
│ │ └─ page.tsx # Report view (DB hit) or trigger
│ ├─ api/analyze/route.ts # POST: DB check → LLM → save
│ ├─ globals.css # Tailwind + design tokens
│ └─ layout.tsx # Root (passes through to [locale])
│
├─ components/
│ ├─ ui/ # shadcn primitives (inline, no CLI needed)
│ │ ├─ button.tsx
│ │ ├─ input.tsx
│ │ ├─ card.tsx
│ │ ├─ badge.tsx
│ │ └─ skeleton.tsx
│ ├─ StockSearch.tsx # Search form (client)
│ ├─ ReportCard.tsx # 5-section report renderer
│ ├─ ScoreBadge.tsx # 0-100 color-coded badge
│ ├─ LanguageSwitcher.tsx # URL-based locale switcher
│ ├─ RecentAnalyses.tsx # Server component listing recent reports
│ ├─ AnalysisTrigger.tsx # Client component that calls /api/analyze
│ └─ SiteHeader.tsx # Server component with nav + language
│
├─ lib/
│ ├─ MiniMax.ts # LLM client + prompt engineering
│ ├─ supabase.ts # Service-role client + CRUD
│ ├─ stock-lookup.ts # Symbol → company name (top 50 tickers)
│ └─ utils.ts # cn(), normalize, format, clamp
│
├─ i18n/
│ ├─ config.ts # locales, labels, prompt-language map
│ └─ request.ts # next-intl request config
│
├─ messages/
│ ├─ ko.json # Korean UI strings
│ └─ en.json # English UI strings
│
├─ types/
│ └─ stock.ts # Domain types (AIReport, StockAnalysisRow, …)
│
├─ supabase/
│ └─ schema.sql # Table + indexes + RLS policy
│
├─ middleware.ts # next-intl URL-based locale routing
├─ next.config.mjs # Next config with next-intl plugin
├─ tailwind.config.ts
├─ tsconfig.json
├─ package.json
├─ .env.local.example # Copy to .env.local and fill in
└─ README.md
cd app
npm installNode 18.17+ required (we test on Node 20 / 22).
cp .env.local.example .env.localEdit .env.local and fill in:
# MiniMax (https://api.MiniMax.chat → API Keys)
MINIMAX_API_KEY=sk-...
MINIMAX_API_BASE=https://api.MiniMax.chat/v1
MINIMAX_MODEL=MiniMax-M3
# Supabase (Project Settings → API)
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
NEXT_PUBLIC_APP_URL=http://localhost:3000- Go to your Supabase project → SQL Editor → New query
- Paste the contents of
supabase/schema.sql - Click Run
You should see Success. No rows returned.
npm run dev
# → http://localhost:3000 (redirects to /ko or /en based on browser language)npm run build
npm start| # | Action | Expected |
|---|---|---|
| 1 | Open http://localhost:3000 |
Lands on /ko (or /en if browser language is English) |
| 2 | Type NVDA, click AI 분석 시작 |
Navigates to /ko/analysis/NVDA, shows loading skeleton, then full Korean report |
| 3 | Reload the same page | Loads immediately from cache (no LLM call) |
| 4 | Click English in header | Re-renders at /en/analysis/NVDA with a separate English report (different cache key) |
| 5 | Reload the English page | Also instant from cache |
curl -X POST http://localhost:3000/api/analyze \
-H "Content-Type: application/json" \
-d '{"symbol":"TSLA","language":"en"}'Successful response:
{
"ok": true,
"fromCache": false,
"data": {
"id": 1,
"symbol": "TSLA",
"company_name": "Tesla, Inc.",
"language": "en",
"investment_score": 72,
"ai_report": { "summary": "...", "...": "..." },
"created_at": "2026-07-14T02:14:39.123Z"
}
}Run it a second time → "fromCache": true.
- Push this folder to a GitHub/GitLab/Bitbucket repo.
- Go to vercel.com/new → import the repo.
- Framework Preset auto-detects as Next.js. Leave defaults.
- Environment Variables — add every key from
.env.local.example:MINIMAX_API_KEYMINIMAX_API_BASEMINIMAX_MODELNEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYNEXT_PUBLIC_APP_URL(use the Vercel preview URL, e.g.https://ai-stock-analyst.vercel.app)
- Click Deploy. First build takes ~60–90s.
- Done — every git push triggers a preview deploy.
npm i -g vercel
vercel login
vercel link
vercel env add MINIMAX_API_KEY
# … repeat for each var
vercel --prod- ✅ 100 GB egress / month, 100 deployments / day — more than enough for an MVP.
- ✅ Serverless functions on the free plan have a 10s execution limit on Hobby. The
/api/analyzeroute makes one MiniMax call + two Supabase queries → typically 3–7s. If you hit a timeout, setexport const maxDuration = 60;at the top ofapp/api/analyze/route.ts(Pro plan only — for free plan the 10s ceiling is enforced). ⚠️ Edge runtime is not supported by this route (we usenodejsruntime to keep env vars + fetch stable).
User submits "NVDA" + "ko"
│
▼
POST /api/analyze ──► Supabase: SELECT … WHERE symbol='NVDA' AND language='ko'
│
├─ Hit → return cached row (fromCache: true, no LLM cost)
│
└─ Miss → in-process dedup check (Set<string> of "NVDA::ko")
│
└─ free → call MiniMax → parse JSON → INSERT → return (fromCache: false)
symbolis normalized to UPPERCASE before lookup.(symbol, language)is the cache key —NVDA/koandNVDA/enare separate cache rows.- The most recent row is returned (
ORDER BY created_at DESC LIMIT 1), so re-analyses just create a new row. - Concurrent identical requests share a single in-process inflight set to avoid duplicate LLM calls.
| Error | Fix |
|---|---|
Missing Supabase env vars |
Make sure .env.local has both NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY. |
MINIMAX_API_KEY is not set |
Add it to .env.local (or Vercel env vars). Restart next dev after editing. |
401 Unauthorized from MiniMax |
Wrong or expired key. Regenerate on the MiniMax dashboard. |
JSON object not in DB / API returns 502 |
The LLM returned malformed output. The route logs the raw content; the prompt asks for json_object. |
permission denied for table stock_analysis |
You ran the API with the anon key. The service role key must do the writes (this is the default). |
Hydration mismatch on the language switcher |
Refresh the page. We render the locale from useLocale(), which is hydrated after middleware. |
Vercel build fails with Module not found |
Make sure the local npm install finished; commit package-lock.json. |
- API key safety —
MINIMAX_API_KEYandSUPABASE_SERVICE_ROLE_KEYare only read in server components, route handlers, andlib/*(no"use client"files import them). The Vercel deployment only shipsNEXT_PUBLIC_*vars to the browser. - RLS — the
stock_analysistable has Row Level Security enabled with a public read policy. Writes are blocked for anon/authenticated roles; only the service role bypasses RLS. - Input validation — the route validates
symbol(1–20 alphanumeric, uppercased) andlanguage(must bekooren). - Disclaimer — every report and the site footer includes a "not investment advice" disclaimer.
MIT — go build something cool.