CAKE20

AI Web PaaS

기술별 실행 예제

Cake20이 기본 제공하는 기능을 짧은 소스와 실제 실행 결과로 확인합니다.

Cake20 View 페이지와 컴포넌트

// app/components/GreetingCard.tsx
export type Props = { name: string };

export default ({ name }: Props) => <Card>안녕하세요, {name}님</Card>;

// app/pages/index.tsx
export default () => <GreetingCard name="카페" />;

실행 결과

홈 화면에 ‘안녕하세요, 카페님’ 카드가 표시됩니다. 컴포넌트 import는 필요 없습니다.

Cake20 UI와 Tailwind CSS

export default () => (
  <Button icon="i-lucide-save" class="mt-4" onClick={save}>저장</Button>
);

실행 결과

Lucide 저장 아이콘이 있는 Cake20 UI 버튼이 표시됩니다. U 접두어와 패키지 설치가 필요 없습니다.

WYSIWYG Editor

export const data = {
  content: "<h2>오늘의 기록</h2><p>내용을 적어 보세요.</p>",
};

export default () => (
  <Editor bind={data.content} image={false} mention={false} />
);

실행 결과

Tiptap 기반 서식 편집기가 표시되며 HTML 문자열과 양방향으로 동기화됩니다.

DragList 드래그 정렬

export default () => (
  <DragList bind={data.cards} group="tasks" onEnd={saveOrder}>
    {data.cards.map((card) => <Card key={card.id}>{card.title}</Card>)}
  </DragList>
);

실행 결과

카드 전체를 잡아 순서를 바꾸거나 같은 group의 다른 목록으로 옮길 수 있습니다.

웹사이트 공개 설정

// package.json
{
  "title": "동네 카페",
  "description": "오늘의 메뉴를 소개합니다.",
  "timezone": "Asia/Seoul",
  "lang": "ko"
}

실행 결과

문서 제목과 공개 메타 정보가 적용되고 서버 시간 계산은 서울 기준을 사용합니다.

반응형 Store

// app/stores/cart.store.ts
export const config = { persist: "local" } as const;
export default {
  count: 0,
  add() { this.count += 1; }
};

// app/pages/index.tsx
export default () => (
  <Button onClick={() => store.cart.add()}>{store.cart.count}</Button>
);

실행 결과

버튼을 누르면 숫자가 증가하며 브라우저를 다시 열어도 localStorage에서 복구됩니다.

하위 컴포넌트에 Context 제공

// app/components/ThemePanel.tsx
export const data = { theme: "dark" as "light" | "dark" };
export const context = { theme: () => data.theme };
export default () => <ThemeLabel />;

// app/components/ThemeLabel.tsx
const theme = useContext<"light" | "dark">("theme");
export default () => <p>현재 테마: {theme()}</p>;

실행 결과

하위 컴포넌트가 prop 전달 없이 가장 가까운 상위 context 값을 사용합니다. 특정 UI 계층의 문맥은 context, 전역·영속 상태는 store에 둡니다.

Shared 자동 import와 Runtime 기능

// shared/utils/money.ts
export const money = (value: number) => value.toLocaleString("ko-KR") + "원";

// app/pages/index.tsx 또는 server/api/price.get.ts
const data = await fetchGet<{ price: number }>("/api/price");
const label = money(data.price);
const day = textDate(now());

실행 결과

공통 함수와 fetchGet·now·textDate를 별도 import 없이 UI와 서버에서 함께 사용합니다.

API와 입력 검증

// server/api/memos.post.ts
export const config = {
  filter: { title: z.string().trim().min(1).max(80) },
  sample: { title: "첫 메모" }
};
export default async (data: Input<typeof config.filter>) =>
  db.memo.create({ data });

실행 결과

POST /api/memos가 생성되고 잘못된 입력은 DB 실행 전에 400 응답으로 차단됩니다.

문의 폼 보관함

// server/api/contact.post.ts
export default defineApi(async (input, { request }) =>
  form.submit(request, {
    name: "contact",
    consent: input.consent === true,
    data: { email: input.email, message: input.message }
  })
);

실행 결과

문의가 웹사이트 전용 보관함에 저장되고 속도 제한·용량 제한·동의 정보가 함께 적용됩니다.

Prisma 데이터베이스 모델

// server/db/memo.db.ts
export const Memo = {
  id: z.id(),
  title: z.string().max(80),
  createdAt: z.date().defaultNow().timestamp()
};

실행 결과

Memo 테이블과 Prisma 접근 객체 db.memo가 생성됩니다. 테이블마다 db.ts 파일 하나를 사용합니다.

초기 데이터와 Preview 샘플

// server/db/memo.db.ts
export const Memo = {
  id: z.string().id().max(30),
  title: z.string().max(100)
};

export const seed = async () => [
  { id: "first", title: "첫 메모" },
  { id: "shopping", title: "장보기" }
];

실행 결과

현재 반환 row로 Preview JSON을 만들고, 신규·변경된 seed만 운영 DB에 upsert합니다.

재사용 SQL 함수

// server/db/memo.sql.ts
export const recentMemos = db.sql(
  z.object({ id: z.string(), title: z.string() }),
  () => sql`SELECT id, title FROM "Memo" ORDER BY "createdAt" DESC LIMIT 5`
);

const rows = await db.sql.recentMemos();

실행 결과

검증된 최근 메모 배열이 반환되며 API·Hook·Task에서 같은 함수를 재사용할 수 있습니다.

로그인과 권한

// server/api/me.get.ts
export default async (_data: unknown, context: ApiContext) => {
  const user = await auth.require(context.request, { roles: ["admin"] });
  return { email: user.email };
};

실행 결과

관리자는 이메일을 받고, 미로그인은 401, 권한이 없는 사용자는 403 응답을 받습니다.

Route, WebSocket과 SSE

// server/routes/notice.sse.ts
export default () => ({
  event: "notice",
  data: { text: "주문이 준비됐습니다." }
});

// server/routes/chat.ws.ts
export default { onMessage(peer, message) { peer.publish("room", message); } };

실행 결과

브라우저는 /notice에서 서버 알림을 받고 /chat에서 같은 방의 메시지를 실시간 수신합니다.

Hook, Task, Job과 스케줄

// server/hooks/order.hook.ts
export default (order: Order) => console.log(order.id);

// server/tasks/cleanup.task.ts
export default async () => ({ removed: await db.memo.deleteMany() });

// server/tasks/daily.job.ts
export const config = { cron: "0 0 * * *" };
export default async () => db.report.create({ data: { day: textDate(now()) } });

실행 결과

이벤트 Hook, 수동 Task와 매일 자정 Job이 서로 독립된 파일과 실행 이력으로 관리됩니다.

Redis 기반 비동기 Job

// server/api/report.post.ts
const work = await job.report({ month: "2026-08" });
return { id: work.id };

// 상태 조회
const state = await job.$get(work.id);

실행 결과

API는 즉시 작업 ID를 반환하고 서버 Job은 웹사이트 전용 Redis Queue에서 재시도·상태를 관리합니다.

알람, 이메일과 결제

await mail.send({ to: "[email protected]", subject: "가입 완료", text: "환영합니다." });

if (telegram.active) await telegram.send("새 주문이 등록되었습니다.");

if (gmail.active) {
  await gmail.send({ subject: "새 주문", text: "새 주문이 등록되었습니다." });
}

const ready = await payment.request({
  orderId: "ORDER-1",
  orderName: "딸기 케이크",
  amount: 12000,
  returnUrl: "https://example.com/pay/success",
  cancelUrl: "https://example.com/pay/fail"
});

실행 결과

사이트 관리자 Telegram·Gmail과 SMTP로 알림을 보내고 결제 요청 정보를 반환합니다.

파일과 Object Storage

const file = await storage.put("images/menu.webp", body);
return { url: storage.url(file.path) };

실행 결과

로컬과 Object Storage 설정에 관계없이 공개 가능한 이미지 URL이 반환됩니다.

Excel XLSX 생성

const rows = [["주문"], ["딸기 케이크"]];
return excel.download("orders.xlsx", rows);

실행 결과

단순 행 데이터를 storage에 남기지 않고 XLSX 다운로드 Response로 바로 반환합니다.

실행 가능한 테스트

test("메모 목록", async () => {
  const data = await fetchGet<{ items: unknown[] }>("/api/memos");
  expect(data.items.length).toBeGreaterThan(0);
});

실행 결과

검수 빌드에서 API를 실제로 호출하고 통과·실패 결과가 에디터 테스트 콘솔에 표시됩니다.