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.
λΆμμ΄ λλλ©΄ μλμΌλ‘ 1080x1080 PNG + μΊ‘μ
μ΄ μμ±λμ΄ Supabase Storage μ μ μ₯λκ³ /admin/instagram νμ μμ¬. μ¬μ©μκ° μΈμ€ν μ±μ λΆμ¬λ£λ λ°©μ. API μΉμΈ 0, λΉμ© 0.
-
Supabase λμ보λ β Storage β New bucket:
- Name:
instagram-content - Public bucket: ON
- Create
- Name:
-
Supabase SQL Editor β
supabase/migrations/20260101_instagram_posts.sqlν΅μ§Έλ‘ μ€ν -
.env.localμSOCIAL_AUTO_POST=1(λλ κ·Έλ₯ λΉμλκΈ°; κΈ°λ³Έ νμ±) -
dev μλ² μ¬μμ
- μ¬μ©μκ° AAPL λΆμ μμ²
- λ°±κ·ΈλΌμ΄λ: 1080x1080 PNG μμ± + μΊ‘μ
μμ± β Supabase Storage μ
λ‘λ β
instagram_postsν INSERT - μ¬μ©μκ°
http://localhost:3000/admin/instagramμμ:- μ΄λ―Έμ§ λ€μ΄λ‘λ ν΄λ¦ β νΈλν°μΌλ‘ μ μ‘ (AirDrop / ν λ κ·Έλ¨ / μΉ΄ν‘ / λ©μΌ)
- μΊ‘μ λ³΅μ¬ ν΄λ¦ β μΈμ€ν μ±μμ "μ κ²μλ¬Ό" β μΊ‘μ λμ λΆμ¬λ£κΈ° β μ΄λ―Έμ§ μ ν β κ²μ
- λ. ν κ²μλ¬ΌλΉ 30μ΄.
SOCIAL_AUTO_POST=0
Facebook Business κ³μ + Instagram Business κ³μ + Facebook App + instagram_content_publish κΆν μΉμΈ νμ (μμΌ~μμ£Ό). μ
μ
μ΄ λ¬΄κ±°μμ MVP μμ manual. νμν΄μ§λ©΄ lib/social/instagram-api.ts μΆκ°νκ³ orchestrator μμ νΈμΆνλ©΄ λ¨.
MIT β go build something cool.