Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# MarketShield AI MVP

Global Market Entry Decision Engine MVP (mock-first).

## Core Flow
1. Project creation
2. Orientation
3. Market analysis
4. Consumer analysis
5. Paid conversion
6. Strategy
7. Network
8. Checklist
9. Action Board
10. Report preview
11. HTML export

## Commands
- `npm run validate:stage`
- `npm run test:smoke`
- `npm run report:stage -- --stage=16 --ready=yes`

## Notes
- 현재는 mock provider/in-memory store 기반.
- PDF export는 TODO(disabled).
7 changes: 7 additions & 0 deletions app/api/ai/normalize-context/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { normalizeContext } from '../../../../lib/ai/context-builder.js';

export async function POST(req: Request) {
const body = await req.json();
const normalized = normalizeContext(body);
return Response.json({ ok: true, data: normalized });
}
9 changes: 9 additions & 0 deletions app/api/ai/run-prompt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { normalizeContext } from '../../../../lib/ai/context-builder.js';
import { runPrompt } from '../../../../lib/ai/prompt-runner.js';

export async function POST(req: Request) {
const body = await req.json();
const context = normalizeContext(body.context ?? {});
const data = await runPrompt(body.promptName ?? 'market-analysis', context, body.provider ?? 'mock');
return Response.json({ ok: true, data });
}
11 changes: 11 additions & 0 deletions app/api/analysis/checklist/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateChecklist } from '../../../../../lib/analysis/checklist.js';
import { store } from '../../../../../lib/orientation/store.js';

export async function POST(req: Request) {
const body = await req.json();
const project = body.project ?? { id: 'p-unknown' };
const data = generateChecklist(project);
store.checklists = store.checklists.filter((c) => c.project_id !== project.id).concat(data);
store.action_items = store.action_items.filter((a) => a.project_id !== project.id).concat(data.actionItems);
return Response.json({ ok: true, data });
}
10 changes: 10 additions & 0 deletions app/api/analysis/consumer/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { generateConsumerAnalysis } from '../../../../../lib/analysis/consumer.js';
import { store } from '../../../../../lib/orientation/store.js';

export async function POST(req: Request) {
const body = await req.json();
const project = body.project ?? { id: 'p-unknown', target_country: 'unknown', industry: 'general' };
const analysis = generateConsumerAnalysis(project, body.targetCustomer ?? '', store.local_insights ?? []);
store.consumer_analyses = store.consumer_analyses.filter((m) => m.project_id !== project.id).concat(analysis);
return Response.json({ ok: true, data: analysis });
}
10 changes: 10 additions & 0 deletions app/api/analysis/market/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { generateMarketAnalysis } from '../../../../../lib/analysis/market.js';
import { store } from '../../../../../lib/orientation/store.js';

export async function POST(req: Request) {
const body = await req.json();
const project = body.project ?? { id: 'p-unknown', target_country: 'unknown', industry: 'general' };
const analysis = generateMarketAnalysis({ id: project.id, target_country: project.target_country, industry: project.industry });
store.market_analyses = store.market_analyses.filter((m) => m.project_id !== project.id).concat(analysis);
return Response.json({ ok: true, data: analysis });
}
11 changes: 11 additions & 0 deletions app/api/analysis/network/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateNetworkAnalysis } from '../../../../../lib/analysis/network.js';
import { store } from '../../../../../lib/orientation/store.js';

export async function POST(req: Request) {
const body = await req.json();
const project = body.project ?? { id: 'p-unknown', target_country: 'unknown', industry: 'general' };
const data = generateNetworkAnalysis(project);
store.network_entities = data.recommendations;
store.project_network_recommendations = store.project_network_recommendations.filter((r) => r.project_id !== project.id).concat(data);
return Response.json({ ok: true, data });
}
10 changes: 10 additions & 0 deletions app/api/analysis/strategy/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { generateStrategyReport } from '../../../../../lib/analysis/strategy.js';
import { store } from '../../../../../lib/orientation/store.js';

export async function POST(req: Request) {
const body = await req.json();
const project = body.project ?? { id: 'p-unknown', target_country: 'unknown', industry: 'general' };
const report = generateStrategyReport(project);
store.strategy_reports = store.strategy_reports.filter((r) => r.project_id !== project.id).concat(report);
return Response.json({ ok: true, data: report });
}
10 changes: 10 additions & 0 deletions app/api/failure-patterns/match/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { matchFailurePatterns } from '../../../../lib/failure/pattern-engine.js';
import { store } from '../../../../lib/orientation/store.js';

export async function POST(req: Request) {
const body = await req.json();
const project = body.project ?? { id: 'p-unknown', country: 'unknown', industry: 'general', risk_signals: [] };
const matched = matchFailurePatterns(project);
store.failure_pattern_matches[project.id] = matched;
return Response.json({ ok: true, data: matched });
}
11 changes: 11 additions & 0 deletions app/api/orientation/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createOrientation } from '../../../../lib/orientation/generator.js';
import { store } from '../../../../lib/orientation/store.js';

export async function POST(req: Request) {
const body = await req.json();
const project = body.project ?? { id: 'p-unknown', target_country: 'unknown', industry: 'general' };
const generated = createOrientation(project);
store.orientation_agents = store.orientation_agents.filter((a) => a.project_id !== project.id).concat(generated.agents);
store.agent_messages = store.agent_messages.filter((m) => m.project_id !== project.id).concat(generated.messages);
return Response.json({ ok: true, data: generated });
}
6 changes: 6 additions & 0 deletions app/api/projects/[id]/action-board/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { store } from '../../../../../lib/orientation/store.js';
import { calcProgress } from '../../../../../lib/analysis/checklist.js';
export async function GET(_: Request, { params }: { params: { id: string } }) {
const items = store.action_items.filter((a) => a.project_id === params.id);
return Response.json({ ok: true, data: { items, progress: calcProgress(items) } });
}
5 changes: 5 additions & 0 deletions app/api/projects/[id]/analysis/checklist/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { store } from '../../../../../../lib/orientation/store.js';
export async function GET(_: Request, { params }: { params: { id: string } }) {
const data = store.checklists.find((c) => c.project_id === params.id) ?? null;
return Response.json({ ok: true, data });
}
6 changes: 6 additions & 0 deletions app/api/projects/[id]/analysis/consumer/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { store } from '../../../../../../lib/orientation/store.js';

export async function GET(_: Request, { params }: { params: { id: string } }) {
const data = store.consumer_analyses.find((m) => m.project_id === params.id) ?? null;
return Response.json({ ok: true, data });
}
6 changes: 6 additions & 0 deletions app/api/projects/[id]/analysis/market/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { store } from '../../../../../../lib/orientation/store.js';

export async function GET(_: Request, { params }: { params: { id: string } }) {
const data = store.market_analyses.find((m) => m.project_id === params.id) ?? null;
return Response.json({ ok: true, data });
}
6 changes: 6 additions & 0 deletions app/api/projects/[id]/analysis/network/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { store } from '../../../../../../lib/orientation/store.js';

export async function GET(_: Request, { params }: { params: { id: string } }) {
const data = store.project_network_recommendations.find((r) => r.project_id === params.id) ?? null;
return Response.json({ ok: true, data });
}
6 changes: 6 additions & 0 deletions app/api/projects/[id]/analysis/strategy/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { store } from '../../../../../../lib/orientation/store.js';

export async function GET(_: Request, { params }: { params: { id: string } }) {
const data = store.strategy_reports.find((r) => r.project_id === params.id) ?? null;
return Response.json({ ok: true, data });
}
4 changes: 4 additions & 0 deletions app/api/projects/[id]/failure-patterns/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { store } from '../../../../../lib/orientation/store.js';
export async function GET(_: Request, { params }: { params: { id: string } }) {
return Response.json({ ok: true, data: store.failure_pattern_matches[params.id] ?? [] });
}
9 changes: 9 additions & 0 deletions app/api/projects/[id]/orientation/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { store } from '../../../../../lib/orientation/store.js';

export async function GET(_: Request, { params }: { params: { id: string } }) {
const id = params.id;
const agents = store.orientation_agents.filter((a) => a.project_id === id);
const messages = store.agent_messages.filter((m) => m.project_id === id);
const conflicts = messages.filter((m) => m.conflict_detected).map((m) => ({ dimension: '가격/유통/규제/신뢰', note: m.message_text }));
return Response.json({ ok: true, data: { agents, messages, conflicts } });
}
16 changes: 16 additions & 0 deletions app/api/projects/[id]/report/export/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { store } from '../../../../../../lib/orientation/store.js';
import { composeProjectReport, toHtmlReport } from '../../../../../../lib/report/compose.js';

export async function GET(req: Request, { params }: { params: { id: string } }) {
const url = new URL(req.url);
const format = url.searchParams.get('format') ?? 'html';
const plan = url.searchParams.get('plan') ?? 'free';
if (plan === 'free') return Response.json({ ok: false, error: 'preview_only_for_free' }, { status: 403 });

const data = composeProjectReport(params.id, store);
if (format === 'html') {
const html = toHtmlReport(data);
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8', 'content-disposition': `attachment; filename="marketshield-${params.id}.html"` } });
}
return Response.json({ ok: false, error: 'pdf_not_enabled_todo' }, { status: 501 });
}
24 changes: 24 additions & 0 deletions app/components/MockReport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export function MockReport({ title, stage }: { title: string; stage: string }) {
return (
<main className="grid" style={{ gap: 20 }}>
<section className="card">
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h2 style={{ marginTop: 0 }}>{title}</h2>
<div>
<span className="badge free">FREE</span>{' '}
<span className="badge pro">PRO</span>
</div>
</div>
<p className="small">Stage: {stage}</p>
</section>
<section className="grid two">
{['confidence', 'rationale', 'assumptions', 'limitations'].map((k) => (
<article className="card" key={k}>
<h3 style={{ textTransform: 'capitalize', marginTop: 0 }}>{k}</h3>
<p className="small">Mock {k} content. 실제 AI/DB 연동 전 placeholder 화면입니다.</p>
</article>
))}
</section>
</main>
);
}
20 changes: 20 additions & 0 deletions app/components/PaywallGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';
import Link from 'next/link';
import { isPaidLike } from '../../lib/billing/plan.js';

export function PaywallGate({ plan, title, children }: { plan: string; title: string; children: React.ReactNode }) {
if (isPaidLike(plan)) return <>{children}</>;
return (
<section className="card">
<h3>{title} (유료 기능)</h3>
<p className="small">미리보기: 핵심 아웃라인만 표시됩니다. 전체 결과는 업그레이드 후 확인 가능합니다.</p>
<ul>
<li>진출 전략 전체 보기</li>
<li>네트워크 추천 열기</li>
<li>실행 체크리스트 생성</li>
<li>리포트 다운로드</li>
</ul>
<Link href="/pricing"><button>업그레이드하기</button></Link>
</section>
);
}
14 changes: 14 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
:root { color-scheme: dark; }
body { margin:0; font-family: Inter, system-ui, sans-serif; background:#0b1530; color:#e6eefc; }
.container { max-width: 1120px; margin: 0 auto; padding: 24px; }
.card { background: linear-gradient(180deg,#122347,#0f1e3d); border:1px solid #284679; border-radius:14px; padding:16px; }
.badge { display:inline-block; padding:4px 10px; border-radius:999px; font-size:12px; font-weight:600; }
.badge.free { background:#17396b; color:#a7ceff; }
.badge.pro { background:#3d245f; color:#d7b7ff; }
.grid { display:grid; gap:16px; }
.grid.two { grid-template-columns:repeat(2,minmax(0,1fr)); }
.small { color:#9db5de; font-size:13px; }
input, textarea, select { width:100%; margin-top:6px; background:#0b1530; color:#e6eefc; border:1px solid #34558b; border-radius:8px; padding:10px; }
label { font-size:14px; color:#c6d9fa; }
button { background:#24539a; color:white; border:none; border-radius:8px; padding:10px 14px; cursor:pointer; }
button:disabled { opacity:.5; cursor:not-allowed; }
18 changes: 18 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import './globals.css';
import type { ReactNode } from 'react';

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ko">
<body>
<div className="container">
<header style={{ marginBottom: 20 }}>
<h1 style={{ margin: 0 }}>MarketShield AI</h1>
<p className="small">Global Market Entry Decision Engine · Mock UI</p>
</header>
{children}
</div>
</body>
</html>
);
}
34 changes: 34 additions & 0 deletions app/lib/mockContextBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type InterviewInput = {
country: string;
region: string;
itemType: string;
itemDescription: string;
hasTarget: string;
targetDescription: string;
priceRange: string;
distribution: string;
reasonCountry: string;
hasLocalNetwork: string;
painPoints: string;
outputPriority: string;
};

export function buildMockContext(input: InterviewInput) {
return {
project: {
title: `${input.country} ${input.itemType} Entry Project`,
target_country: input.country,
industry: input.itemType
},
context: {
region: input.region,
target_consumer: input.targetDescription,
item_name: input.itemType,
price_positioning: input.priceRange,
distribution_model: input.distribution,
network_condition: `${input.hasLocalNetwork}; pain=${input.painPoints}`,
reason_country: input.reasonCountry,
output_priority: input.outputPriority
}
};
}
22 changes: 22 additions & 0 deletions app/lib/mockDb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type MockProject = { id: string; title: string; target_country: string; industry: string };
export type MockContext = { project_id: string; [k: string]: string };
const KEY = 'marketshield_mock_db';

function read() {
if (typeof window === 'undefined') return { projects: [], project_contexts: [] };
const raw = window.localStorage.getItem(KEY);
return raw ? JSON.parse(raw) : { projects: [], project_contexts: [] };
}
function write(data: { projects: MockProject[]; project_contexts: MockContext[] }) {
if (typeof window === 'undefined') return;
window.localStorage.setItem(KEY, JSON.stringify(data));
}

export function createProjectWithContext(project: Omit<MockProject, 'id'>, context: Omit<MockContext, 'project_id'>) {
const db = read();
const id = `p-${Date.now()}`;
db.projects.push({ id, ...project });
db.project_contexts.push({ project_id: id, ...context });
write(db);
return id;
}
2 changes: 2 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { MockReport } from './components/MockReport';
export default function Page() { return <MockReport title="Orientation Briefing Room" stage="Home" />; }
16 changes: 16 additions & 0 deletions app/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';
import { useState } from 'react';

export default function PricingPage() {
const [plan, setPlan] = useState('free');
return <main className="grid" style={{ gap: 16 }}>
<section className="card"><h2>Pricing</h2><p>Free / Trial / Paid</p>
<div style={{display:'flex', gap:8}}>
<button onClick={()=>setPlan('free')}>Free</button>
<button onClick={()=>setPlan('trial')}>Trial (Dev)</button>
<button onClick={()=>setPlan('paid')}>Paid (Dev)</button>
</div>
<p className="small">개발용 paid toggle: 현재 <b>{plan}</b></p>
</section>
</main>;
}
22 changes: 22 additions & 0 deletions app/project/[id]/action-board/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';
import { useEffect, useState } from 'react';
import { PaywallGate } from '../../../components/PaywallGate';
import { isPaidLike } from '../../../../lib/billing/plan.js';

export default function ActionBoardPage({ params }: { params: { id: string } }) {
const [plan, setPlan] = useState('free');
const [d, setD] = useState<any>(null);
const [fp, setFp] = useState<any[]>([]);
useEffect(() => {
if (!isPaidLike(plan)) return;
fetch(`/api/projects/${params.id}/action-board`).then(r=>r.json()).then(j=>setD(j.data));
fetch(`/api/projects/${params.id}/failure-patterns`).then(r=>r.json()).then(j=>setFp(j.data));
}, [params.id, plan]);

return <main className='grid' style={{gap:16}}>
<section className='card'><h2>Action Board</h2><button onClick={()=>setPlan(plan==='free'?'paid':'free')}>Dev Plan Toggle: {plan}</button></section>
<PaywallGate plan={plan} title='Action Board'>
{!d ? <section className='card'>Loading...</section> : <section className='card'><h3>진행률 {d.progress}%</h3><p className='small'>Failure warning count: {fp.length}</p>{d.items.map((i:any)=><article key={i.id}><p>{i.title} | 우선순위:{i.priority} | 상태:{i.status} | 시간:{i.eta_band}</p><p>리스크:{i.risk} / 네트워크:{i.network}</p><p>완료:{i.done?'✅':'⬜'}</p></article>)}</section>}
</PaywallGate>
</main>;
}
Loading