← 목록으로
2026-02-26plans

title: KoreaAI Hub 구현 플랜 (L2) date: 2026-02-26T21:00:00+09:00 type: plan layer: L2-implementation status: draft task_id: "" tags: [koreaai-hub, implementation, mvp, lemon-squeezy, brevo, next.js] author: koreaai-plan-pl project: koreaai-hub

KoreaAI Hub 구현 플랜

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 한국 AI 비즈니스 허브 MVP를 2주 내 출시하여 뉴스레터 구독자 확보 + 스킬패키지 판매를 시작한다.

Architecture: Next.js 15 App Router 기반 풀스택 앱. Turso(LibSQL)에 별도 DB(koreaai-hub) 생성하여 구독자/상품/주문 관리. Lemon Squeezy로 결제 처리(Checkout URL + Webhook), Brevo로 뉴스레터 구독/발송. 랜딩 → 구독 → 결제 플로우를 MVP로 빠르게 구현.

Tech Stack:

  • Next.js 15+ (App Router), TypeScript, Tailwind CSS v4, shadcn/ui
  • Drizzle ORM + Turso (LibSQL) — DB명: koreaai-hub
  • Lemon Squeezy (결제 PG, Merchant of Record)
  • Brevo (@getbrevo/brevo, 뉴스레터/트랜잭션 이메일)
  • Vercel (배포)

참고 문서:

  • 비즈 기획(L0): docs/plans/2026/02/25/koreaai-hub-research-v4.md
  • CEO 블로킹 해소: memory/jarvis-reports/2026-02-26-1741-koreaai-ceo-unblock.md
  • 기존 프로젝트 참조: projects/apppro-kr/current-site/ (동일 기술 스택)

Phase 1 MVP 범위 (2주)

포함:

  1. 랜딩 페이지 (한국어, 반응형)
  2. 뉴스레터 구독 (이메일 수집 → Brevo 연동)
  3. AI 스킬패키지 상품 페이지 (5개 산업군)
  4. Lemon Squeezy 결제 연동 (Checkout → Webhook → 디지털 상품 전달)
  5. 관리자 대시보드 (구독자/주문 현황)
  6. SEO 기본 설정 (sitemap, robots, og:image)
  7. Vercel 배포

제외 (Phase 2 이후):

  • BizFlow API
  • 큐레이션 프리미엄 구독
  • Featured Listing 광고
  • AI 자동 콘텐츠 생성 파이프라인
  • 개인화 필터

Task 1: 프로젝트 초기화 — Next.js + 의존성

Files:

  • Create: projects/koreaai-hub/package.json
  • Create: projects/koreaai-hub/tsconfig.json
  • Create: projects/koreaai-hub/next.config.ts
  • Create: projects/koreaai-hub/tailwind.config.ts
  • Create: projects/koreaai-hub/postcss.config.mjs
  • Create: projects/koreaai-hub/.env.example
  • Create: projects/koreaai-hub/.env.local
  • Create: projects/koreaai-hub/.gitignore
  • Create: projects/koreaai-hub/CLAUDE.md

Step 1: 프로젝트 디렉토리 생성 및 Next.js 초기화

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects
mkdir -p koreaai-hub && cd koreaai-hub
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-pnpm --turbopack

Step 2: 추가 의존성 설치

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm add drizzle-orm @libsql/client @lemonsqueezy/lemonsqueezy.js @getbrevo/brevo
pnpm add class-variance-authority clsx tailwind-merge lucide-react framer-motion
pnpm add -D drizzle-kit @types/node

Step 3: shadcn/ui 초기화

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm dlx shadcn@latest init -d

Step 4: 환경변수 파일 생성

.env.example 파일:

# Turso DB
TURSO_DB_URL=libsql://koreaai-hub-migkjy.aws-ap-northeast-1.turso.io
TURSO_DB_TOKEN=

# Lemon Squeezy
LEMON_SQUEEZY_API_KEY=
LEMON_SQUEEZY_STORE_ID=
LEMON_SQUEEZY_WEBHOOK_SECRET=

# Brevo
BREVO_API_KEY=
BREVO_LIST_ID=

# App
NEXT_PUBLIC_APP_URL=http://localhost:3000

.env.local에 실제 키 복사 (루트 .env에서 가져옴).

Step 5: 프로젝트 CLAUDE.md 생성

# KoreaAI Hub

## 프로젝트 개요
한국 AI 비즈니스 허브 — 뉴스레터 + 스킬패키지 판매 플랫폼

## 기술 스택
- Next.js 15+ (App Router), TypeScript, Tailwind CSS v4, shadcn/ui
- Drizzle ORM + Turso (LibSQL) — DB: koreaai-hub
- Lemon Squeezy (결제), Brevo (이메일)
- 배포: Vercel

## GitHub Repo
- TBD (생성 후 기입)

## DB
- Turso DB명: koreaai-hub
- 환경변수: TURSO_DB_URL, TURSO_DB_TOKEN

Step 6: 빌드 확인

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm build

Expected: 빌드 성공 (exit 0)

Step 7: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/
git commit -m "feat(koreaai-hub): 프로젝트 초기화 — Next.js 15 + Turso + Lemon Squeezy + Brevo"
git pull --rebase origin main && git push origin main

Task 2: Turso DB 생성 + Drizzle 스키마 정의

Files:

  • Create: projects/koreaai-hub/src/lib/db/index.ts
  • Create: projects/koreaai-hub/src/lib/db/schema.ts
  • Create: projects/koreaai-hub/drizzle.config.ts

Step 1: Turso DB 생성 (CLI)

turso db create koreaai-hub --group default
turso db tokens create koreaai-hub

생성된 URL과 토큰을 .env.local에 기입.

Step 2: Drizzle 스키마 정의

src/lib/db/schema.ts:

import { sql } from "drizzle-orm";
import { integer, sqliteTable, text, index, uniqueIndex } from "drizzle-orm/sqlite-core";

// 뉴스레터 구독자
export const subscribers = sqliteTable("subscribers", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  email: text("email").notNull(),
  name: text("name"),
  tier: text("tier").$type<"free" | "starter" | "growth">().default("free").notNull(),
  brevoContactId: text("brevo_contact_id"),
  subscribedAt: text("subscribed_at").default(sql`(CURRENT_TIMESTAMP)`).notNull(),
  unsubscribedAt: text("unsubscribed_at"),
  status: text("status").$type<"active" | "unsubscribed" | "bounced">().default("active").notNull(),
}, (table) => [
  uniqueIndex("subscribers_email_idx").on(table.email),
  index("subscribers_status_idx").on(table.status),
]);

// 스킬패키지 상품
export const products = sqliteTable("products", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  slug: text("slug").notNull(),
  name: text("name").notNull(),
  description: text("description"),
  industry: text("industry").notNull(), // 마케팅, 금융, HR, 제조, 법률
  price: integer("price").notNull(), // 센트 단위 (e.g., 1900 = $19)
  currency: text("currency").default("USD").notNull(),
  lsVariantId: text("ls_variant_id"), // Lemon Squeezy variant ID
  lsProductId: text("ls_product_id"), // Lemon Squeezy product ID
  type: text("type").$type<"single" | "bundle" | "subscription">().default("single").notNull(),
  status: text("status").$type<"active" | "draft" | "archived">().default("draft").notNull(),
  createdAt: text("created_at").default(sql`(CURRENT_TIMESTAMP)`).notNull(),
}, (table) => [
  uniqueIndex("products_slug_idx").on(table.slug),
  index("products_industry_idx").on(table.industry),
  index("products_status_idx").on(table.status),
]);

// 주문
export const orders = sqliteTable("orders", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  subscriberEmail: text("subscriber_email").notNull(),
  productId: integer("product_id").references(() => products.id),
  lsOrderId: text("ls_order_id"), // Lemon Squeezy order ID
  status: text("status").$type<"pending" | "paid" | "refunded" | "failed">().default("pending").notNull(),
  amount: integer("amount").notNull(), // 센트 단위
  currency: text("currency").default("USD").notNull(),
  downloadUrl: text("download_url"),
  paidAt: text("paid_at"),
  createdAt: text("created_at").default(sql`(CURRENT_TIMESTAMP)`).notNull(),
}, (table) => [
  index("orders_email_idx").on(table.subscriberEmail),
  index("orders_status_idx").on(table.status),
  index("orders_ls_order_idx").on(table.lsOrderId),
]);

// 뉴스레터 발송 이력
export const newsletterIssues = sqliteTable("newsletter_issues", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  subject: text("subject").notNull(),
  htmlContent: text("html_content"),
  brevoMessageId: text("brevo_message_id"),
  sentAt: text("sent_at"),
  recipientCount: integer("recipient_count").default(0),
  openRate: integer("open_rate"), // 퍼센트 * 100 (e.g., 3500 = 35.00%)
  status: text("status").$type<"draft" | "scheduled" | "sent" | "failed">().default("draft").notNull(),
  createdAt: text("created_at").default(sql`(CURRENT_TIMESTAMP)`).notNull(),
}, (table) => [
  index("newsletter_status_idx").on(table.status),
]);

// 타입 추출
export type InsertSubscriber = typeof subscribers.$inferInsert;
export type SelectSubscriber = typeof subscribers.$inferSelect;
export type InsertProduct = typeof products.$inferInsert;
export type SelectProduct = typeof products.$inferSelect;
export type InsertOrder = typeof orders.$inferInsert;
export type SelectOrder = typeof orders.$inferSelect;

Step 3: DB 클라이언트 설정

src/lib/db/index.ts:

import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./schema";

const client = createClient({
  url: process.env.TURSO_DB_URL!,
  authToken: process.env.TURSO_DB_TOKEN,
});

export const db = drizzle(client, { schema });

Step 4: Drizzle config

drizzle.config.ts:

import type { Config } from "drizzle-kit";

export default {
  schema: "./src/lib/db/schema.ts",
  out: "./drizzle",
  dialect: "turso",
  dbCredentials: {
    url: process.env.TURSO_DB_URL!,
    authToken: process.env.TURSO_DB_TOKEN,
  },
} satisfies Config;

Step 5: 마이그레이션 생성 및 확인

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm drizzle-kit generate

Expected: drizzle/ 폴더에 SQL 마이그레이션 파일 생성. SQL 내용 확인 — DROP/TRUNCATE가 없는지 반드시 검증.

Step 6: 마이그레이션 적용

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm drizzle-kit migrate

Expected: 테이블 4개 생성 (subscribers, products, orders, newsletter_issues)

Step 7: 빌드 확인

pnpm build

Expected: 빌드 성공

Step 8: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/lib/db/ projects/koreaai-hub/drizzle.config.ts projects/koreaai-hub/drizzle/
git commit -m "feat(koreaai-hub): DB 스키마 정의 — subscribers, products, orders, newsletter_issues"
git pull --rebase origin main && git push origin main

Task 3: 유틸리티 + 공통 컴포넌트

Files:

  • Create: projects/koreaai-hub/src/lib/utils.ts
  • Create: projects/koreaai-hub/src/components/ui/button.tsx (shadcn)
  • Create: projects/koreaai-hub/src/components/ui/input.tsx (shadcn)
  • Create: projects/koreaai-hub/src/components/ui/card.tsx (shadcn)
  • Create: projects/koreaai-hub/src/components/ui/badge.tsx (shadcn)

Step 1: shadcn 컴포넌트 추가

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm dlx shadcn@latest add button input card badge separator toast

Step 2: utils.ts 생성

src/lib/utils.ts:

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export function formatPrice(priceInCents: number, currency: string = "USD"): string {
  return new Intl.NumberFormat("ko-KR", {
    style: "currency",
    currency,
  }).format(priceInCents / 100);
}

export function formatDate(dateStr: string): string {
  return new Intl.DateTimeFormat("ko-KR", {
    year: "numeric",
    month: "long",
    day: "numeric",
  }).format(new Date(dateStr));
}

Step 3: 빌드 확인

pnpm build

Step 4: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/lib/utils.ts projects/koreaai-hub/src/components/
git commit -m "feat(koreaai-hub): 유틸리티 + shadcn UI 컴포넌트 추가"
git pull --rebase origin main && git push origin main

Task 4: Brevo 연동 — 뉴스레터 구독 API

Files:

  • Create: projects/koreaai-hub/src/lib/brevo.ts
  • Create: projects/koreaai-hub/src/app/api/subscribe/route.ts
  • Create: projects/koreaai-hub/src/__tests__/subscribe.test.ts

Step 1: Brevo 클라이언트 래퍼 작성

src/lib/brevo.ts:

import { ContactsApi, ContactsApiApiKeys, TransactionalEmailsApi, TransactionalEmailsApiApiKeys } from "@getbrevo/brevo";

function getContactsApi() {
  const api = new ContactsApi();
  api.setApiKey(ContactsApiApiKeys.apiKey, process.env.BREVO_API_KEY!);
  return api;
}

function getEmailApi() {
  const api = new TransactionalEmailsApi();
  api.setApiKey(TransactionalEmailsApiApiKeys.apiKey, process.env.BREVO_API_KEY!);
  return api;
}

export async function addSubscriber(email: string, name?: string) {
  const api = getContactsApi();
  const listId = parseInt(process.env.BREVO_LIST_ID || "0");

  const result = await api.createContact({
    email,
    attributes: name ? { FIRSTNAME: name } : undefined,
    listIds: listId ? [listId] : undefined,
    updateEnabled: true,
  });

  return result.body;
}

export async function sendWelcomeEmail(email: string, name?: string) {
  const api = getEmailApi();

  await api.sendTransacEmail({
    to: [{ email, name: name || undefined }],
    subject: "KoreaAI Hub에 오신 것을 환영합니다!",
    htmlContent: `
      <h1>환영합니다${name ? `, ${name}` : ""}!</h1>
      <p>한국 AI 비즈니스 인사이트를 매주 전달해드립니다.</p>
      <p>곧 첫 번째 뉴스레터를 받아보실 수 있습니다.</p>
      <hr/>
      <p style="color:#666;font-size:12px;">KoreaAI Hub — 한국 AI 비즈니스 허브</p>
    `,
    sender: { email: "hello@koreaai.hub", name: "KoreaAI Hub" },
  });
}

Step 2: 구독 API 라우트 작성

src/app/api/subscribe/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { subscribers } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { addSubscriber, sendWelcomeEmail } from "@/lib/brevo";

export async function POST(request: NextRequest) {
  try {
    const { email, name } = await request.json();

    if (!email || typeof email !== "string" || !email.includes("@")) {
      return NextResponse.json({ error: "유효한 이메일을 입력해주세요." }, { status: 400 });
    }

    const sanitizedEmail = email.trim().toLowerCase();

    // 중복 확인
    const existing = await db
      .select({ id: subscribers.id, status: subscribers.status })
      .from(subscribers)
      .where(eq(subscribers.email, sanitizedEmail))
      .limit(1);

    if (existing.length > 0 && existing[0].status === "active") {
      return NextResponse.json({ message: "이미 구독 중입니다." }, { status: 200 });
    }

    // DB 저장
    if (existing.length > 0) {
      await db.update(subscribers)
        .set({ status: "active", unsubscribedAt: null, name: name || undefined })
        .where(eq(subscribers.email, sanitizedEmail));
    } else {
      await db.insert(subscribers).values({
        email: sanitizedEmail,
        name: name || null,
        tier: "free",
      });
    }

    // Brevo 연동 (비동기, 실패해도 구독은 유지)
    try {
      await addSubscriber(sanitizedEmail, name);
      await sendWelcomeEmail(sanitizedEmail, name);
    } catch (brevoErr) {
      console.error("Brevo 연동 실패:", brevoErr);
    }

    return NextResponse.json({ message: "구독 완료! 환영 이메일을 확인해주세요." }, { status: 201 });
  } catch (error) {
    console.error("구독 처리 실패:", error);
    return NextResponse.json({ error: "서버 오류가 발생했습니다." }, { status: 500 });
  }
}

Step 3: 빌드 확인

pnpm build

Step 4: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/lib/brevo.ts projects/koreaai-hub/src/app/api/subscribe/
git commit -m "feat(koreaai-hub): Brevo 연동 + 뉴스레터 구독 API"
git pull --rebase origin main && git push origin main

Task 5: Lemon Squeezy 연동 — 결제 + Webhook

Files:

  • Create: projects/koreaai-hub/src/lib/lemonsqueezy.ts
  • Create: projects/koreaai-hub/src/app/api/checkout/route.ts
  • Create: projects/koreaai-hub/src/app/api/webhook/lemonsqueezy/route.ts

Step 1: Lemon Squeezy 클라이언트 설정

src/lib/lemonsqueezy.ts:

import {
  lemonSqueezySetup,
  createCheckout,
  getProduct,
  type Checkout,
} from "@lemonsqueezy/lemonsqueezy.js";

export function initLemonSqueezy() {
  lemonSqueezySetup({
    apiKey: process.env.LEMON_SQUEEZY_API_KEY!,
    onError: (error) => console.error("Lemon Squeezy error:", error),
  });
}

export async function createCheckoutUrl(
  variantId: string,
  email: string,
  productName: string,
) {
  initLemonSqueezy();

  const storeId = process.env.LEMON_SQUEEZY_STORE_ID!;

  const { data, error } = await createCheckout(storeId, variantId, {
    checkoutData: {
      email,
      custom: {
        product_name: productName,
      },
    },
    productOptions: {
      redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success`,
    },
  });

  if (error) {
    throw new Error(`Checkout 생성 실패: ${error.message}`);
  }

  return data?.data.attributes.url;
}

Step 2: 체크아웃 API 라우트

src/app/api/checkout/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { products } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { createCheckoutUrl } from "@/lib/lemonsqueezy";

export async function POST(request: NextRequest) {
  try {
    const { productSlug, email } = await request.json();

    if (!productSlug || !email) {
      return NextResponse.json({ error: "상품과 이메일이 필요합니다." }, { status: 400 });
    }

    const product = await db
      .select()
      .from(products)
      .where(eq(products.slug, productSlug))
      .limit(1);

    if (!product.length || !product[0].lsVariantId) {
      return NextResponse.json({ error: "상품을 찾을 수 없습니다." }, { status: 404 });
    }

    const checkoutUrl = await createCheckoutUrl(
      product[0].lsVariantId,
      email,
      product[0].name,
    );

    return NextResponse.json({ checkoutUrl });
  } catch (error) {
    console.error("체크아웃 생성 실패:", error);
    return NextResponse.json({ error: "서버 오류가 발생했습니다." }, { status: 500 });
  }
}

Step 3: Webhook 핸들러

src/app/api/webhook/lemonsqueezy/route.ts:

import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { db } from "@/lib/db";
import { orders } from "@/lib/db/schema";
import { eq } from "drizzle-orm";

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string,
): boolean {
  const hmac = crypto.createHmac("sha256", secret);
  const digest = hmac.update(payload).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}

export async function POST(request: NextRequest) {
  try {
    const rawBody = await request.text();
    const signature = request.headers.get("x-signature") || "";
    const secret = process.env.LEMON_SQUEEZY_WEBHOOK_SECRET!;

    if (!verifyWebhookSignature(rawBody, signature, secret)) {
      return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
    }

    const event = JSON.parse(rawBody);
    const eventName = event.meta.event_name;
    const data = event.data;

    switch (eventName) {
      case "order_created": {
        const attrs = data.attributes;
        await db.insert(orders).values({
          subscriberEmail: attrs.user_email,
          lsOrderId: String(data.id),
          status: attrs.status === "paid" ? "paid" : "pending",
          amount: attrs.total,
          currency: attrs.currency.toUpperCase(),
          paidAt: attrs.status === "paid" ? new Date().toISOString() : null,
        });
        break;
      }

      case "order_refunded": {
        await db.update(orders)
          .set({ status: "refunded" })
          .where(eq(orders.lsOrderId, String(data.id)));
        break;
      }

      default:
        console.log(`Unhandled LS event: ${eventName}`);
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error("Webhook 처리 실패:", error);
    return NextResponse.json({ error: "Webhook 처리 실패" }, { status: 500 });
  }
}

Step 4: 빌드 확인

pnpm build

Step 5: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/lib/lemonsqueezy.ts projects/koreaai-hub/src/app/api/checkout/ projects/koreaai-hub/src/app/api/webhook/
git commit -m "feat(koreaai-hub): Lemon Squeezy 결제 연동 — checkout + webhook"
git pull --rebase origin main && git push origin main

Task 6: 레이아웃 + 헤더 + 푸터

Files:

  • Modify: projects/koreaai-hub/src/app/layout.tsx
  • Create: projects/koreaai-hub/src/components/layout/header.tsx
  • Create: projects/koreaai-hub/src/components/layout/footer.tsx
  • Create: projects/koreaai-hub/src/app/globals.css

Step 1: globals.css 설정 (Tailwind v4)

src/app/globals.css:

@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #0a0a0a;
  --primary: #2563eb;
  --primary-foreground: #ffffff;
  --secondary: #06b6d4;
  --muted: #f5f5f5;
  --muted-foreground: #737373;
  --border: #e5e5e5;
  --accent: #f0f9ff;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
    --primary: #3b82f6;
    --muted: #1a1a1a;
    --muted-foreground: #a3a3a3;
    --border: #262626;
    --accent: #0c1929;
  }
}

body {
  font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont,
    system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo",
    "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji",
    "Segoe UI Symbol", sans-serif;
}

Step 2: Header 컴포넌트

src/components/layout/header.tsx:

import Link from "next/link";

export function Header() {
  return (
    <header className="sticky top-0 z-50 w-full border-b border-[var(--border)] bg-[var(--background)]/80 backdrop-blur-sm">
      <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4">
        <Link href="/" className="text-xl font-bold text-[var(--primary)]">
          KoreaAI Hub
        </Link>
        <nav className="flex items-center gap-6">
          <Link href="/skills" className="text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors">
            스킬패키지
          </Link>
          <Link href="/newsletter" className="text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors">
            뉴스레터
          </Link>
          <Link
            href="/newsletter"
            className="rounded-lg bg-[var(--primary)] px-4 py-2 text-sm font-medium text-[var(--primary-foreground)] hover:opacity-90 transition-opacity"
          >
            무료 구독
          </Link>
        </nav>
      </div>
    </header>
  );
}

Step 3: Footer 컴포넌트

src/components/layout/footer.tsx:

export function Footer() {
  return (
    <footer className="border-t border-[var(--border)] bg-[var(--muted)]">
      <div className="mx-auto max-w-6xl px-4 py-12">
        <div className="grid grid-cols-1 gap-8 md:grid-cols-3">
          <div>
            <h3 className="text-lg font-bold">KoreaAI Hub</h3>
            <p className="mt-2 text-sm text-[var(--muted-foreground)]">
              한국 AI 비즈니스의 모든 것
            </p>
          </div>
          <div>
            <h4 className="font-semibold">상품</h4>
            <ul className="mt-2 space-y-1 text-sm text-[var(--muted-foreground)]">
              <li>AI 스킬패키지</li>
              <li>비즈 아이디어 뉴스레터</li>
              <li>큐레이션 프리미엄 (곧 출시)</li>
            </ul>
          </div>
          <div>
            <h4 className="font-semibold">문의</h4>
            <p className="mt-2 text-sm text-[var(--muted-foreground)]">
              hello@koreaai.hub
            </p>
          </div>
        </div>
        <div className="mt-8 border-t border-[var(--border)] pt-4 text-center text-xs text-[var(--muted-foreground)]">
          &copy; {new Date().getFullYear()} KoreaAI Hub. All rights reserved.
        </div>
      </div>
    </footer>
  );
}

Step 4: layout.tsx 수정

src/app/layout.tsx:

import type { Metadata } from "next";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import "./globals.css";

export const metadata: Metadata = {
  title: "KoreaAI Hub — 한국 AI 비즈니스 허브",
  description: "한국 산업에 특화된 AI 스킬패키지, 비즈 아이디어 뉴스레터, 큐레이션 서비스를 제공합니다.",
  openGraph: {
    title: "KoreaAI Hub — 한국 AI 비즈니스 허브",
    description: "한국 산업에 특화된 AI 스킬패키지와 비즈 아이디어 뉴스레터",
    type: "website",
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <head>
        <link
          rel="stylesheet"
          as="style"
          crossOrigin="anonymous"
          href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
        />
      </head>
      <body className="bg-[var(--background)] text-[var(--foreground)]">
        <Header />
        <main className="min-h-screen">{children}</main>
        <Footer />
      </body>
    </html>
  );
}

Step 5: 빌드 확인

pnpm build

Step 6: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/layout.tsx projects/koreaai-hub/src/app/globals.css projects/koreaai-hub/src/components/layout/
git commit -m "feat(koreaai-hub): 레이아웃 + 헤더 + 푸터 — Pretendard 폰트, 다크모드"
git pull --rebase origin main && git push origin main

Task 7: 랜딩 페이지 — 메인

Files:

  • Modify: projects/koreaai-hub/src/app/page.tsx
  • Create: projects/koreaai-hub/src/components/sections/hero-section.tsx
  • Create: projects/koreaai-hub/src/components/sections/features-section.tsx
  • Create: projects/koreaai-hub/src/components/sections/newsletter-cta.tsx
  • Create: projects/koreaai-hub/src/components/sections/skills-preview-section.tsx

Step 1: Hero 섹션

src/components/sections/hero-section.tsx:

import Link from "next/link";

export function HeroSection() {
  return (
    <section className="relative overflow-hidden py-24 md:py-32">
      <div className="mx-auto max-w-6xl px-4 text-center">
        <div className="inline-block rounded-full bg-[var(--accent)] px-4 py-1.5 text-sm font-medium text-[var(--primary)] mb-6">
          한국 AI 비즈니스 특화
        </div>
        <h1 className="text-4xl font-bold tracking-tight md:text-6xl">
          AI로 비즈니스를 시작하는
          <br />
          <span className="text-[var(--primary)]">가장 빠른 방법</span>
        </h1>
        <p className="mx-auto mt-6 max-w-2xl text-lg text-[var(--muted-foreground)]">
          한국 산업에 딱 맞는 AI 스킬패키지와 매일 실행 가능한 비즈 아이디어를
          뉴스레터로 받아보세요.
        </p>
        <div className="mt-10 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
          <Link
            href="/newsletter"
            className="rounded-lg bg-[var(--primary)] px-8 py-3 text-base font-semibold text-[var(--primary-foreground)] hover:opacity-90 transition-opacity"
          >
            무료 뉴스레터 구독
          </Link>
          <Link
            href="/skills"
            className="rounded-lg border border-[var(--border)] px-8 py-3 text-base font-semibold hover:bg-[var(--muted)] transition-colors"
          >
            스킬패키지 보기
          </Link>
        </div>
      </div>
    </section>
  );
}

Step 2: Features 섹션

src/components/sections/features-section.tsx:

const features = [
  {
    title: "산업군별 AI 스킬",
    description: "마케팅, 금융, HR, 제조, 법률 — 내 업종에 딱 맞는 AI 활용법 패키지",
    icon: "🎯",
  },
  {
    title: "실행 가능한 비즈 아이디어",
    description: "매일 '오늘 당장 시작 가능한' AI 비즈니스 아이디어를 뉴스레터로 배달",
    icon: "💡",
  },
  {
    title: "한국 시장 특화",
    description: "한국 법률, 세금, 시장 환경에 최적화된 AI 활용 전략과 프롬프트",
    icon: "🇰🇷",
  },
];

export function FeaturesSection() {
  return (
    <section className="border-t border-[var(--border)] bg-[var(--muted)] py-20">
      <div className="mx-auto max-w-6xl px-4">
        <h2 className="text-center text-3xl font-bold">왜 KoreaAI Hub인가</h2>
        <div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
          {features.map((f) => (
            <div
              key={f.title}
              className="rounded-xl border border-[var(--border)] bg-[var(--background)] p-6"
            >
              <div className="text-4xl">{f.icon}</div>
              <h3 className="mt-4 text-xl font-semibold">{f.title}</h3>
              <p className="mt-2 text-[var(--muted-foreground)]">{f.description}</p>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

Step 3: Newsletter CTA 섹션

src/components/sections/newsletter-cta.tsx:

"use client";

import { useState } from "react";

export function NewsletterCta() {
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
  const [message, setMessage] = useState("");

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setStatus("loading");

    try {
      const res = await fetch("/api/subscribe", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email }),
      });
      const data = await res.json();

      if (res.ok) {
        setStatus("success");
        setMessage(data.message);
        setEmail("");
      } else {
        setStatus("error");
        setMessage(data.error || "오류가 발생했습니다.");
      }
    } catch {
      setStatus("error");
      setMessage("네트워크 오류가 발생했습니다.");
    }
  }

  return (
    <section className="py-20">
      <div className="mx-auto max-w-2xl px-4 text-center">
        <h2 className="text-3xl font-bold">
          매주 AI 비즈 아이디어를 받아보세요
        </h2>
        <p className="mt-4 text-[var(--muted-foreground)]">
          무료 구독으로 시작하세요. 주 3회 AI 비즈니스 인사이트를 보내드립니다.
        </p>
        <form onSubmit={handleSubmit} className="mt-8 flex gap-2 sm:flex-row flex-col">
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="your@email.com"
            required
            className="flex-1 rounded-lg border border-[var(--border)] bg-[var(--background)] px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
          />
          <button
            type="submit"
            disabled={status === "loading"}
            className="rounded-lg bg-[var(--primary)] px-6 py-3 font-semibold text-[var(--primary-foreground)] hover:opacity-90 transition-opacity disabled:opacity-50"
          >
            {status === "loading" ? "처리 중..." : "무료 구독"}
          </button>
        </form>
        {message && (
          <p className={`mt-4 text-sm ${status === "success" ? "text-green-600" : "text-red-600"}`}>
            {message}
          </p>
        )}
      </div>
    </section>
  );
}

Step 4: Skills Preview 섹션

src/components/sections/skills-preview-section.tsx:

import Link from "next/link";

const industries = [
  { name: "마케팅/광고", slug: "marketing", emoji: "📢", desc: "AI 기반 광고 카피, 타겟팅, 캠페인 자동화" },
  { name: "금융/핀테크", slug: "finance", emoji: "💰", desc: "AI 리스크 분석, 고객 신용평가, 자동 보고서" },
  { name: "HR/채용", slug: "hr", emoji: "👥", desc: "AI 이력서 스크리닝, 면접 질문 생성, 노무 상담" },
  { name: "제조업", slug: "manufacturing", emoji: "🏭", desc: "AI 품질검사, 공정 최적화, 예측 정비" },
  { name: "법률/컨설팅", slug: "legal", emoji: "⚖️", desc: "AI 계약서 검토, 법률 리서치, 사례 분석" },
];

export function SkillsPreviewSection() {
  return (
    <section className="border-t border-[var(--border)] py-20">
      <div className="mx-auto max-w-6xl px-4">
        <div className="text-center">
          <h2 className="text-3xl font-bold">산업군별 AI 스킬패키지</h2>
          <p className="mt-4 text-[var(--muted-foreground)]">
            내 업종에 딱 맞는 AI 활용법. 프롬프트 10종 + 실전 가이드 포함.
          </p>
        </div>
        <div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
          {industries.map((ind) => (
            <Link
              key={ind.slug}
              href={`/skills/${ind.slug}`}
              className="flex items-start gap-4 rounded-xl border border-[var(--border)] p-5 hover:border-[var(--primary)] hover:bg-[var(--accent)] transition-colors"
            >
              <span className="text-3xl">{ind.emoji}</span>
              <div>
                <h3 className="font-semibold">{ind.name}</h3>
                <p className="mt-1 text-sm text-[var(--muted-foreground)]">{ind.desc}</p>
                <span className="mt-2 inline-block text-sm font-medium text-[var(--primary)]">
                  $19 →
                </span>
              </div>
            </Link>
          ))}
        </div>
        <div className="mt-8 text-center">
          <Link
            href="/skills"
            className="inline-block rounded-lg border border-[var(--border)] px-6 py-3 font-medium hover:bg-[var(--muted)] transition-colors"
          >
            전체 패키지 보기
          </Link>
        </div>
      </div>
    </section>
  );
}

Step 5: 메인 페이지 조합

src/app/page.tsx:

import { HeroSection } from "@/components/sections/hero-section";
import { FeaturesSection } from "@/components/sections/features-section";
import { SkillsPreviewSection } from "@/components/sections/skills-preview-section";
import { NewsletterCta } from "@/components/sections/newsletter-cta";

export default function HomePage() {
  return (
    <>
      <HeroSection />
      <FeaturesSection />
      <SkillsPreviewSection />
      <NewsletterCta />
    </>
  );
}

Step 6: 빌드 확인

pnpm build

Step 7: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/page.tsx projects/koreaai-hub/src/components/sections/
git commit -m "feat(koreaai-hub): 랜딩 페이지 — hero, features, skills preview, newsletter CTA"
git pull --rebase origin main && git push origin main

Task 8: 뉴스레터 구독 전용 페이지

Files:

  • Create: projects/koreaai-hub/src/app/newsletter/page.tsx

Step 1: 뉴스레터 페이지

src/app/newsletter/page.tsx:

import type { Metadata } from "next";
import { NewsletterCta } from "@/components/sections/newsletter-cta";

export const metadata: Metadata = {
  title: "AI 비즈 아이디어 뉴스레터 — KoreaAI Hub",
  description: "매주 3회, 한국에서 당장 실행 가능한 AI 비즈니스 아이디어를 배달합니다.",
};

const benefits = [
  { title: "주 3회 무료 배달", desc: "월/수/금 아침, 실행 가능한 AI 비즈 아이디어 1건" },
  { title: "한국 시장 특화", desc: "해외 트렌드를 한국 법/세금/시장에 맞게 분석" },
  { title: "실행 가이드 포함", desc: "아이디어 + 초기비용 + 수익예상 + 3일 실행플랜" },
  { title: "Pro 업그레이드", desc: "$4.99/월로 일간 즉시 수신 + 아카이브 검색" },
];

export default function NewsletterPage() {
  return (
    <div className="py-16">
      <div className="mx-auto max-w-4xl px-4">
        <div className="text-center">
          <h1 className="text-4xl font-bold">
            매일 아침, AI 비즈 아이디어가 도착합니다
          </h1>
          <p className="mt-4 text-lg text-[var(--muted-foreground)]">
            &quot;오늘 당장 실행 가능한 AI 비즈니스 아이디어 1개&quot; — 리서치, 전략, 실행 방법까지
          </p>
        </div>

        <div className="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2">
          {benefits.map((b) => (
            <div key={b.title} className="rounded-xl border border-[var(--border)] p-6">
              <h3 className="font-semibold text-lg">{b.title}</h3>
              <p className="mt-2 text-sm text-[var(--muted-foreground)]">{b.desc}</p>
            </div>
          ))}
        </div>

        <div className="mt-12 rounded-xl border border-[var(--border)] bg-[var(--muted)] p-8">
          <h2 className="text-center text-2xl font-bold mb-2">뉴스레터 미리보기</h2>
          <div className="mx-auto max-w-lg rounded-lg bg-[var(--background)] p-6 border border-[var(--border)]">
            <div className="text-xs text-[var(--muted-foreground)] mb-2">2026년 2월 26일 (수)</div>
            <h3 className="text-lg font-bold">AI 기반 세무 자동화 대행 서비스</h3>
            <p className="mt-2 text-sm text-[var(--muted-foreground)]">
              한국 세무사 수요 급증 + AI 세무 자동화 기술 성숙 = 소상공인 대상 월 $29 구독 서비스.
              초기 비용 $50, 예상 월 수익 $2,000+
            </p>
            <div className="mt-3 text-xs text-[var(--primary)] font-medium">
              전체 실행 가이드는 구독 후 확인 →
            </div>
          </div>
        </div>

        <NewsletterCta />
      </div>
    </div>
  );
}

Step 2: 빌드 확인

pnpm build

Step 3: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/newsletter/
git commit -m "feat(koreaai-hub): 뉴스레터 구독 전용 페이지"
git pull --rebase origin main && git push origin main

Task 9: 스킬패키지 목록 페이지

Files:

  • Create: projects/koreaai-hub/src/app/skills/page.tsx
  • Create: projects/koreaai-hub/src/lib/data/industries.ts

Step 1: 산업군 데이터

src/lib/data/industries.ts:

export interface Industry {
  slug: string;
  name: string;
  emoji: string;
  description: string;
  skills: string[];
  price: number; // 센트
}

export const industries: Industry[] = [
  {
    slug: "marketing",
    name: "마케팅/광고",
    emoji: "📢",
    description: "AI 기반 광고 카피라이팅, 타겟 고객 분석, 캠페인 자동화 등 마케팅 실무에 즉시 적용 가능한 AI 스킬",
    skills: [
      "AI 광고 카피 생성 (네이버/카카오 맞춤)",
      "타겟 페르소나 자동 분석",
      "SNS 콘텐츠 캘린더 자동 생성",
      "A/B 테스트 카피 변형 생성",
      "경쟁사 광고 분석 프롬프트",
      "이메일 마케팅 시퀀스 설계",
      "블로그 SEO 키워드 리서치",
      "랜딩페이지 카피 최적화",
      "고객 리뷰 분석 및 인사이트 추출",
      "마케팅 보고서 자동 생성",
    ],
    price: 1900,
  },
  {
    slug: "finance",
    name: "금융/핀테크",
    emoji: "💰",
    description: "AI 리스크 분석, 신용평가 자동화, 금융 보고서 생성 등 한국 금융 규제에 맞춘 AI 활용법",
    skills: [
      "재무제표 AI 분석 프롬프트",
      "신용 리스크 평가 자동화",
      "투자 포트폴리오 분석",
      "한국 세법 기반 절세 전략 AI",
      "금융 규제 컴플라이언스 체크",
      "고객 이탈 예측 모델 설계",
      "자동 투자 보고서 생성",
      "사기 탐지 패턴 분석",
      "대출 심사 자동화 워크플로우",
      "금융 뉴스 감성 분석",
    ],
    price: 1900,
  },
  {
    slug: "hr",
    name: "HR/채용",
    emoji: "👥",
    description: "AI 이력서 스크리닝, 면접 질문 자동 생성, 한국 노동법 기반 노무 상담 등 HR 전문 스킬",
    skills: [
      "이력서/자기소개서 AI 스크리닝",
      "직무별 면접 질문 자동 생성",
      "한국 노동법 기반 노무 상담 AI",
      "연봉 벤치마킹 분석",
      "직원 만족도 설문 분석",
      "온보딩 체크리스트 자동 생성",
      "퇴사 예측 분석 프롬프트",
      "채용 공고 최적화",
      "교육 프로그램 커리큘럼 설계",
      "인사 평가 피드백 작성 지원",
    ],
    price: 1900,
  },
  {
    slug: "manufacturing",
    name: "제조업",
    emoji: "🏭",
    description: "AI 품질검사, 공정 최적화, 예측 정비 등 한국 제조업 현장에 맞춘 AI 도입 가이드",
    skills: [
      "품질검사 이미지 분석 워크플로우",
      "공정 최적화 데이터 분석",
      "예측 정비 스케줄 자동화",
      "재고 수요 예측 모델",
      "공급망 리스크 분석",
      "제조 원가 분석 자동화",
      "안전사고 예방 체크리스트 AI",
      "생산 보고서 자동 생성",
      "장비 이상 탐지 패턴 분석",
      "스마트팩토리 도입 ROI 분석",
    ],
    price: 1900,
  },
  {
    slug: "legal",
    name: "법률/컨설팅",
    emoji: "⚖️",
    description: "AI 계약서 검토, 법률 리서치, 사례 분석 등 한국 법체계에 특화된 AI 활용 패키지",
    skills: [
      "계약서 AI 검토 프롬프트",
      "판례 검색 및 요약 자동화",
      "법률 의견서 초안 생성",
      "컴플라이언스 체크리스트 자동화",
      "특허 검색 및 선행기술 분석",
      "법률 용어 쉬운 한글 변환",
      "소송 리스크 평가 분석",
      "정관/약관 AI 검토",
      "법률 상담 초기 응대 자동화",
      "규제 변경 모니터링 설정",
    ],
    price: 1900,
  },
];

export const bundlePrice = 4900; // $49 = All-in-One

Step 2: 스킬패키지 목록 페이지

src/app/skills/page.tsx:

import type { Metadata } from "next";
import Link from "next/link";
import { industries, bundlePrice } from "@/lib/data/industries";
import { formatPrice } from "@/lib/utils";

export const metadata: Metadata = {
  title: "AI 스킬패키지 — KoreaAI Hub",
  description: "한국 산업군별 AI 활용 스킬패키지. 프롬프트 10종 + 실전 가이드.",
};

export default function SkillsPage() {
  return (
    <div className="py-16">
      <div className="mx-auto max-w-6xl px-4">
        <div className="text-center">
          <h1 className="text-4xl font-bold">산업군별 AI 스킬패키지</h1>
          <p className="mt-4 text-lg text-[var(--muted-foreground)]">
            내 업종에 딱 맞는 AI 활용법 10가지 + 실전 가이드. 한국 시장 특화.
          </p>
        </div>

        {/* 번들 배너 */}
        <div className="mt-12 rounded-xl border-2 border-[var(--primary)] bg-[var(--accent)] p-6 text-center">
          <div className="text-sm font-medium text-[var(--primary)]">BEST VALUE</div>
          <h2 className="mt-2 text-2xl font-bold">All-in-One 패키지</h2>
          <p className="mt-2 text-[var(--muted-foreground)]">
            전체 {industries.length}개 산업군 × 10개 스킬 = 총 {industries.length * 10}개 AI 스킬
          </p>
          <div className="mt-4 flex items-center justify-center gap-3">
            <span className="text-2xl font-bold text-[var(--primary)]">
              {formatPrice(bundlePrice)}
            </span>
            <span className="text-sm text-[var(--muted-foreground)] line-through">
              {formatPrice(industries.length * 1900)}
            </span>
            <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
              {Math.round((1 - bundlePrice / (industries.length * 1900)) * 100)}% 할인
            </span>
          </div>
          <Link
            href="/skills/bundle"
            className="mt-4 inline-block rounded-lg bg-[var(--primary)] px-8 py-3 font-semibold text-[var(--primary-foreground)] hover:opacity-90 transition-opacity"
          >
            번들 구매하기
          </Link>
        </div>

        {/* 개별 산업군 카드 */}
        <div className="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
          {industries.map((ind) => (
            <Link
              key={ind.slug}
              href={`/skills/${ind.slug}`}
              className="group rounded-xl border border-[var(--border)] p-6 hover:border-[var(--primary)] hover:shadow-md transition-all"
            >
              <div className="text-4xl">{ind.emoji}</div>
              <h3 className="mt-3 text-xl font-semibold group-hover:text-[var(--primary)]">
                {ind.name}
              </h3>
              <p className="mt-2 text-sm text-[var(--muted-foreground)] line-clamp-2">
                {ind.description}
              </p>
              <div className="mt-4 flex items-center justify-between">
                <span className="text-lg font-bold">{formatPrice(ind.price)}</span>
                <span className="text-sm text-[var(--muted-foreground)]">
                  프롬프트 {ind.skills.length}종
                </span>
              </div>
            </Link>
          ))}
        </div>
      </div>
    </div>
  );
}

Step 3: 빌드 확인

pnpm build

Step 4: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/skills/ projects/koreaai-hub/src/lib/data/
git commit -m "feat(koreaai-hub): 스킬패키지 목록 페이지 + 산업군 데이터"
git pull --rebase origin main && git push origin main

Task 10: 스킬패키지 상세 페이지 + 결제 버튼

Files:

  • Create: projects/koreaai-hub/src/app/skills/[slug]/page.tsx
  • Create: projects/koreaai-hub/src/components/checkout-button.tsx

Step 1: 체크아웃 버튼 컴포넌트

src/components/checkout-button.tsx:

"use client";

import { useState } from "react";

interface CheckoutButtonProps {
  productSlug: string;
  label?: string;
  className?: string;
}

export function CheckoutButton({ productSlug, label = "구매하기", className = "" }: CheckoutButtonProps) {
  const [email, setEmail] = useState("");
  const [loading, setLoading] = useState(false);
  const [showEmailInput, setShowEmailInput] = useState(false);

  async function handleCheckout() {
    if (!showEmailInput) {
      setShowEmailInput(true);
      return;
    }

    if (!email || !email.includes("@")) return;

    setLoading(true);
    try {
      const res = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ productSlug, email }),
      });
      const data = await res.json();

      if (data.checkoutUrl) {
        window.location.href = data.checkoutUrl;
      }
    } catch {
      alert("결제 페이지 연결에 실패했습니다. 다시 시도해주세요.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className={className}>
      {showEmailInput && (
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="결제용 이메일 주소"
          className="mb-3 w-full rounded-lg border border-[var(--border)] px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
        />
      )}
      <button
        onClick={handleCheckout}
        disabled={loading}
        className="w-full rounded-lg bg-[var(--primary)] px-8 py-3 font-semibold text-[var(--primary-foreground)] hover:opacity-90 transition-opacity disabled:opacity-50"
      >
        {loading ? "처리 중..." : label}
      </button>
    </div>
  );
}

Step 2: 스킬패키지 상세 페이지

src/app/skills/[slug]/page.tsx:

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { industries } from "@/lib/data/industries";
import { formatPrice } from "@/lib/utils";
import { CheckoutButton } from "@/components/checkout-button";
import { NewsletterCta } from "@/components/sections/newsletter-cta";

interface PageProps {
  params: Promise<{ slug: string }>;
}

export async function generateStaticParams() {
  return industries.map((ind) => ({ slug: ind.slug }));
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const industry = industries.find((i) => i.slug === slug);
  if (!industry) return {};

  return {
    title: `${industry.name} AI 스킬패키지 — KoreaAI Hub`,
    description: industry.description,
  };
}

export default async function SkillDetailPage({ params }: PageProps) {
  const { slug } = await params;
  const industry = industries.find((i) => i.slug === slug);

  if (!industry) notFound();

  return (
    <div className="py-16">
      <div className="mx-auto max-w-4xl px-4">
        <div className="flex items-start gap-4">
          <span className="text-5xl">{industry.emoji}</span>
          <div>
            <h1 className="text-3xl font-bold">{industry.name} AI 스킬패키지</h1>
            <p className="mt-2 text-[var(--muted-foreground)]">{industry.description}</p>
          </div>
        </div>

        <div className="mt-12 grid grid-cols-1 gap-8 lg:grid-cols-3">
          {/* 스킬 목록 */}
          <div className="lg:col-span-2">
            <h2 className="text-xl font-bold">포함된 AI 스킬 {industry.skills.length}종</h2>
            <ul className="mt-4 space-y-3">
              {industry.skills.map((skill, idx) => (
                <li key={idx} className="flex items-start gap-3 rounded-lg border border-[var(--border)] p-4">
                  <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--primary)] text-xs font-bold text-white">
                    {idx + 1}
                  </span>
                  <span>{skill}</span>
                </li>
              ))}
            </ul>
          </div>

          {/* 구매 카드 */}
          <div className="lg:col-span-1">
            <div className="sticky top-24 rounded-xl border border-[var(--border)] p-6">
              <div className="text-3xl font-bold text-[var(--primary)]">
                {formatPrice(industry.price)}
              </div>
              <p className="mt-1 text-sm text-[var(--muted-foreground)]">
                일회성 구매 (평생 이용)
              </p>
              <ul className="mt-4 space-y-2 text-sm">
                <li>AI 프롬프트 {industry.skills.length}종</li>
                <li>실전 활용 가이드 (PDF)</li>
                <li>한국 시장 특화 예시</li>
                <li>무료 업데이트 (1년)</li>
              </ul>
              <CheckoutButton
                productSlug={industry.slug}
                label={`${formatPrice(industry.price)} 구매하기`}
                className="mt-6"
              />
            </div>
          </div>
        </div>

        <NewsletterCta />
      </div>
    </div>
  );
}

Step 3: 빌드 확인

pnpm build

Step 4: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/skills/ projects/koreaai-hub/src/components/checkout-button.tsx
git commit -m "feat(koreaai-hub): 스킬패키지 상세 페이지 + 체크아웃 버튼"
git pull --rebase origin main && git push origin main

Task 11: 결제 성공 페이지

Files:

  • Create: projects/koreaai-hub/src/app/checkout/success/page.tsx

Step 1: 결제 성공 페이지

src/app/checkout/success/page.tsx:

import type { Metadata } from "next";
import Link from "next/link";

export const metadata: Metadata = {
  title: "결제 완료 — KoreaAI Hub",
};

export default function CheckoutSuccessPage() {
  return (
    <div className="flex min-h-[60vh] items-center justify-center px-4">
      <div className="max-w-md text-center">
        <div className="text-6xl">🎉</div>
        <h1 className="mt-4 text-3xl font-bold">결제가 완료되었습니다!</h1>
        <p className="mt-4 text-[var(--muted-foreground)]">
          구매하신 스킬패키지의 다운로드 링크가 이메일로 전송됩니다.
          이메일이 도착하지 않으면 스팸함을 확인해주세요.
        </p>
        <div className="mt-8 flex flex-col gap-3">
          <Link
            href="/skills"
            className="rounded-lg bg-[var(--primary)] px-6 py-3 font-semibold text-[var(--primary-foreground)] hover:opacity-90"
          >
            다른 패키지 둘러보기
          </Link>
          <Link
            href="/"
            className="rounded-lg border border-[var(--border)] px-6 py-3 font-semibold hover:bg-[var(--muted)]"
          >
            홈으로 돌아가기
          </Link>
        </div>
      </div>
    </div>
  );
}

Step 2: 빌드 확인

pnpm build

Step 3: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/checkout/
git commit -m "feat(koreaai-hub): 결제 성공 페이지"
git pull --rebase origin main && git push origin main

Task 12: 관리자 대시보드 — 구독자/주문 현황

Files:

  • Create: projects/koreaai-hub/src/app/admin/page.tsx
  • Create: projects/koreaai-hub/src/app/admin/layout.tsx
  • Create: projects/koreaai-hub/src/app/api/admin/stats/route.ts

Step 1: 관리자 통계 API

src/app/api/admin/stats/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { subscribers, orders } from "@/lib/db/schema";
import { eq, sql } from "drizzle-orm";

const ADMIN_KEY = process.env.ADMIN_API_KEY || "admin-secret-key";

export async function GET(request: NextRequest) {
  const authHeader = request.headers.get("x-admin-key");
  if (authHeader !== ADMIN_KEY) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const [
    totalSubscribers,
    activeSubscribers,
    totalOrders,
    paidOrders,
    revenue,
  ] = await Promise.all([
    db.select({ count: sql<number>`count(*)` }).from(subscribers),
    db.select({ count: sql<number>`count(*)` }).from(subscribers).where(eq(subscribers.status, "active")),
    db.select({ count: sql<number>`count(*)` }).from(orders),
    db.select({ count: sql<number>`count(*)` }).from(orders).where(eq(orders.status, "paid")),
    db.select({ total: sql<number>`COALESCE(sum(amount), 0)` }).from(orders).where(eq(orders.status, "paid")),
  ]);

  return NextResponse.json({
    subscribers: {
      total: totalSubscribers[0].count,
      active: activeSubscribers[0].count,
    },
    orders: {
      total: totalOrders[0].count,
      paid: paidOrders[0].count,
      revenue: revenue[0].total,
    },
  });
}

Step 2: 관리자 레이아웃

src/app/admin/layout.tsx:

export default function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="min-h-screen bg-[var(--muted)]">
      <div className="border-b border-[var(--border)] bg-[var(--background)] px-4 py-3">
        <div className="mx-auto flex max-w-6xl items-center justify-between">
          <h1 className="text-lg font-bold">KoreaAI Hub Admin</h1>
        </div>
      </div>
      <div className="mx-auto max-w-6xl px-4 py-8">{children}</div>
    </div>
  );
}

Step 3: 관리자 대시보드 페이지

src/app/admin/page.tsx:

"use client";

import { useEffect, useState } from "react";

interface Stats {
  subscribers: { total: number; active: number };
  orders: { total: number; paid: number; revenue: number };
}

export default function AdminPage() {
  const [stats, setStats] = useState<Stats | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchStats() {
      try {
        const res = await fetch("/api/admin/stats", {
          headers: { "x-admin-key": "admin-secret-key" },
        });
        if (res.ok) {
          setStats(await res.json());
        }
      } finally {
        setLoading(false);
      }
    }
    fetchStats();
  }, []);

  if (loading) return <div className="text-center py-20">로딩 중...</div>;
  if (!stats) return <div className="text-center py-20">데이터 로드 실패</div>;

  const cards = [
    { label: "총 구독자", value: stats.subscribers.total, sub: `활성: ${stats.subscribers.active}` },
    { label: "총 주문", value: stats.orders.total, sub: `결제완료: ${stats.orders.paid}` },
    { label: "총 매출", value: `$${(stats.orders.revenue / 100).toFixed(2)}`, sub: "USD" },
  ];

  return (
    <div>
      <h2 className="text-2xl font-bold mb-6">대시보드</h2>
      <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
        {cards.map((card) => (
          <div key={card.label} className="rounded-xl border border-[var(--border)] bg-[var(--background)] p-6">
            <div className="text-sm text-[var(--muted-foreground)]">{card.label}</div>
            <div className="mt-2 text-3xl font-bold">{card.value}</div>
            <div className="mt-1 text-xs text-[var(--muted-foreground)]">{card.sub}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

Step 4: 빌드 확인

pnpm build

Step 5: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/admin/ projects/koreaai-hub/src/app/api/admin/
git commit -m "feat(koreaai-hub): 관리자 대시보드 — 구독자/주문/매출 통계"
git pull --rebase origin main && git push origin main

Task 13: SEO 설정 — sitemap, robots, og:image

Files:

  • Create: projects/koreaai-hub/src/app/sitemap.ts
  • Create: projects/koreaai-hub/src/app/robots.ts
  • Create: projects/koreaai-hub/src/app/opengraph-image.tsx (또는 정적 이미지)

Step 1: sitemap.ts

src/app/sitemap.ts:

import type { MetadataRoute } from "next";
import { industries } from "@/lib/data/industries";

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://koreaai.hub";

  const industryPages = industries.map((ind) => ({
    url: `${baseUrl}/skills/${ind.slug}`,
    lastModified: new Date(),
    changeFrequency: "weekly" as const,
    priority: 0.8,
  }));

  return [
    { url: baseUrl, lastModified: new Date(), changeFrequency: "daily", priority: 1.0 },
    { url: `${baseUrl}/skills`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.9 },
    { url: `${baseUrl}/newsletter`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.9 },
    ...industryPages,
  ];
}

Step 2: robots.ts

src/app/robots.ts:

import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://koreaai.hub";

  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/admin/", "/api/"],
    },
    sitemap: `${baseUrl}/sitemap.xml`,
  };
}

Step 3: 빌드 확인

pnpm build

Step 4: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/src/app/sitemap.ts projects/koreaai-hub/src/app/robots.ts
git commit -m "feat(koreaai-hub): SEO 설정 — sitemap.xml + robots.txt"
git pull --rebase origin main && git push origin main

Task 14: GitHub 레포 생성 + Vercel 배포

Files:

  • Create: projects/koreaai-hub/vercel.json

Step 1: GitHub 레포 생성

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
git init
git add .
git commit -m "feat: KoreaAI Hub MVP 초기 구현"
gh repo create migkjy/koreaai-hub --private --source . --push

Step 2: vercel.json 생성

vercel.json:

{
  "framework": "nextjs",
  "buildCommand": "pnpm build",
  "installCommand": "pnpm install"
}

Step 3: Vercel 프로젝트 연결

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
vercel link

Step 4: 환경변수 설정 (Vercel)

# Turso
vercel env add TURSO_DB_URL
vercel env add TURSO_DB_TOKEN

# Lemon Squeezy
vercel env add LEMON_SQUEEZY_API_KEY
vercel env add LEMON_SQUEEZY_STORE_ID
vercel env add LEMON_SQUEEZY_WEBHOOK_SECRET

# Brevo
vercel env add BREVO_API_KEY
vercel env add BREVO_LIST_ID

# App
vercel env add NEXT_PUBLIC_APP_URL
vercel env add ADMIN_API_KEY

Step 5: Preview 배포

vercel

Expected: Preview URL 생성. 프로덕션은 CEO 승인 후에만.

Step 6: 커밋 (business-builder 레포)

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/vercel.json projects/koreaai-hub/CLAUDE.md
git commit -m "feat(koreaai-hub): Vercel 배포 설정 + GitHub 레포 연결"
git pull --rebase origin main && git push origin main

Task 15: Lemon Squeezy 상품 등록 + DB 시드

Files:

  • Create: projects/koreaai-hub/scripts/seed-products.ts

Step 1: 상품 시드 스크립트

scripts/seed-products.ts:

import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import { products } from "../src/lib/db/schema";

const client = createClient({
  url: process.env.TURSO_DB_URL!,
  authToken: process.env.TURSO_DB_TOKEN,
});

const db = drizzle(client);

const seedProducts = [
  {
    slug: "marketing",
    name: "마케팅/광고 AI 스킬패키지",
    description: "AI 기반 광고 카피라이팅, 타겟 고객 분석, 캠페인 자동화 등 마케팅 실무 AI 스킬 10종",
    industry: "마케팅",
    price: 1900,
    currency: "USD",
    type: "single" as const,
    status: "active" as const,
  },
  {
    slug: "finance",
    name: "금융/핀테크 AI 스킬패키지",
    description: "AI 리스크 분석, 신용평가 자동화, 금융 보고서 생성 등 금융 특화 AI 스킬 10종",
    industry: "금융",
    price: 1900,
    currency: "USD",
    type: "single" as const,
    status: "active" as const,
  },
  {
    slug: "hr",
    name: "HR/채용 AI 스킬패키지",
    description: "AI 이력서 스크리닝, 면접 질문 생성, 노무 상담 등 HR 특화 AI 스킬 10종",
    industry: "HR",
    price: 1900,
    currency: "USD",
    type: "single" as const,
    status: "active" as const,
  },
  {
    slug: "manufacturing",
    name: "제조업 AI 스킬패키지",
    description: "AI 품질검사, 공정 최적화, 예측 정비 등 제조업 특화 AI 스킬 10종",
    industry: "제조",
    price: 1900,
    currency: "USD",
    type: "single" as const,
    status: "active" as const,
  },
  {
    slug: "legal",
    name: "법률/컨설팅 AI 스킬패키지",
    description: "AI 계약서 검토, 판례 검색, 법률 의견서 생성 등 법률 특화 AI 스킬 10종",
    industry: "법률",
    price: 1900,
    currency: "USD",
    type: "single" as const,
    status: "active" as const,
  },
  {
    slug: "bundle",
    name: "All-in-One AI 스킬패키지",
    description: "전체 5개 산업군 × 10개 스킬 = 총 50개 AI 스킬 번들",
    industry: "전체",
    price: 4900,
    currency: "USD",
    type: "bundle" as const,
    status: "active" as const,
  },
];

async function seed() {
  console.log("시드 데이터 삽입 시작...");

  for (const product of seedProducts) {
    await db.insert(products).values(product);
    console.log(`  ✅ ${product.name}`);
  }

  console.log("완료! 총 " + seedProducts.length + "개 상품 등록");
}

seed().catch(console.error);

Step 2: package.json에 시드 스크립트 추가

package.jsonscripts에 추가:

"db:seed": "tsx scripts/seed-products.ts"

tsx 설치:

pnpm add -D tsx

Step 3: 시드 실행

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm db:seed

Expected: 6개 상품 등록 완료

Step 4: Lemon Squeezy 대시보드에서 상품 생성 후 variant ID 업데이트

CEO 액션 필요: Lemon Squeezy 대시보드에서 6개 상품 생성 후 variant_id를 DB에 업데이트해야 결제 연동이 완성됩니다.

Step 5: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/koreaai-hub/scripts/ projects/koreaai-hub/package.json
git commit -m "feat(koreaai-hub): 상품 시드 스크립트 — 5개 산업군 + 번들"
git pull --rebase origin main && git push origin main

Task 16: E2E 스모크 테스트

Step 1: 수동 스모크 테스트 체크리스트

다음 항목을 로컬(pnpm dev)에서 확인:

#테스트 항목기대 결과
1http://localhost:3000 메인 페이지Hero, Features, Skills, Newsletter CTA 모두 렌더링
2http://localhost:3000/newsletter뉴스레터 구독 페이지 렌더링, 이메일 입력 폼 동작
3http://localhost:3000/skills5개 산업군 카드 + 번들 배너 렌더링
4http://localhost:3000/skills/marketing마케팅 패키지 상세 — 10개 스킬 목록 + 구매 버튼
5구독 폼 이메일 제출POST /api/subscribe → 201 응답 + DB 저장
6빌드 성공pnpm build exit 0
7반응형 (모바일)320px 뷰포트에서 레이아웃 깨지지 않음

Step 2: 빌드 최종 확인

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/koreaai-hub
pnpm build

Expected: 모든 페이지 빌드 성공


CEO 블로킹 항목 (Phase 1 완료 후)

#항목필요한 행동
1Lemon Squeezy 계정/스토어 생성CEO가 LS 가입 → Store 생성 → API Key 발급
2LS 상품 6개 등록LS 대시보드에서 수동 생성 → variant_id를 DB에 업데이트
3Brevo API KeyCEO Brevo 계정에서 발급 → env에 추가
4Brevo 리스트 생성뉴스레터 리스트 생성 → BREVO_LIST_ID 설정
5도메인 결정koreaai.hub 또는 다른 도메인 → Vercel 커스텀 도메인
6Turso DB 토큰turso db tokens create koreaai-hub 실행
7프로덕션 배포 승인CEO 확인 후 vercel --prod

예상 구현 시간

Task예상
Task 1-3: 프로젝트 초기화 + DB + UI15분
Task 4-5: Brevo + Lemon Squeezy 연동15분
Task 6-8: 레이아웃 + 랜딩 + 뉴스레터20분
Task 9-11: 스킬패키지 페이지 + 결제20분
Task 12-13: 관리자 + SEO10분
Task 14-16: 배포 + 시드 + 테스트15분
합계~95분

리뷰 로그

[koreaai-plan-pl 초안 작성] 2026-02-26T21:00:00+09:00

  • L0 리서치 v4 + CEO 블로킹 해소 보고서 기반으로 L2 구현 플랜 작성
  • 기술 스택: apppro-kr 기준 (Next.js 15 + Drizzle + Turso) + Lemon Squeezy + Brevo
  • MVP 범위: 뉴스레터 구독 + 스킬패키지 5종 + 번들 + 결제 + 관리자 대시보드
  • Phase 2 이후 항목 명확히 제외 (BizFlow API, 큐레이션, Featured Listing)
  • 총 16개 태스크, bite-sized steps 포함
  • CEO 블로킹 7건 정리 (LS 계정, Brevo API, 도메인 등)
  • Lemon Squeezy SDK + Brevo Node SDK + Drizzle ORM 최신 API 참조
plans/2026/02/26/koreaai-hub-implementation.md