@mandujs/mcp 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +367 -367
- package/package.json +2 -2
- package/src/activity-monitor.ts +847 -847
- package/src/adapters/index.ts +20 -20
- package/src/adapters/monitor-adapter.ts +100 -100
- package/src/adapters/tool-adapter.ts +88 -88
- package/src/executor/error-handler.ts +250 -250
- package/src/executor/index.ts +22 -22
- package/src/executor/tool-executor.ts +148 -148
- package/src/hooks/config-watcher.ts +174 -174
- package/src/hooks/index.ts +23 -23
- package/src/hooks/mcp-hooks.ts +227 -227
- package/src/index.ts +106 -106
- package/src/logging/index.ts +15 -15
- package/src/logging/mcp-transport.ts +134 -134
- package/src/registry/index.ts +13 -13
- package/src/registry/mcp-tool-registry.ts +298 -298
- package/src/resources/skills/guides.ts +1136 -1136
- package/src/resources/skills/index.ts +12 -12
- package/src/resources/skills/loader.ts +218 -218
- package/src/resources/skills/mandu-composition/SKILL.md +91 -91
- package/src/resources/skills/mandu-composition/metadata.json +13 -13
- package/src/resources/skills/mandu-composition/rules/_sections.md +26 -26
- package/src/resources/skills/mandu-composition/rules/_template.md +77 -77
- package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -146
- package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -164
- package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -161
- package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -167
- package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -149
- package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -148
- package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -150
- package/src/resources/skills/mandu-deployment/SKILL.md +92 -92
- package/src/resources/skills/mandu-deployment/_sections.md +41 -41
- package/src/resources/skills/mandu-deployment/_template.md +38 -38
- package/src/resources/skills/mandu-deployment/metadata.json +13 -13
- package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -109
- package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -115
- package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -219
- package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -150
- package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -223
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -152
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -179
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -323
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -140
- package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -82
- package/src/resources/skills/mandu-fs-routes/metadata.json +12 -12
- package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -36
- package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -69
- package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -65
- package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -93
- package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -55
- package/src/resources/skills/mandu-guard/SKILL.md +129 -129
- package/src/resources/skills/mandu-guard/metadata.json +12 -12
- package/src/resources/skills/mandu-guard/rules/_sections.md +36 -36
- package/src/resources/skills/mandu-guard/rules/_template.md +82 -82
- package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -100
- package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -76
- package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -81
- package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -80
- package/src/resources/skills/mandu-hydration/SKILL.md +91 -91
- package/src/resources/skills/mandu-hydration/metadata.json +12 -12
- package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -31
- package/src/resources/skills/mandu-hydration/rules/_template.md +72 -72
- package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -109
- package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -55
- package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -113
- package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -68
- package/src/resources/skills/mandu-performance/SKILL.md +85 -85
- package/src/resources/skills/mandu-performance/metadata.json +14 -14
- package/src/resources/skills/mandu-performance/rules/_sections.md +31 -31
- package/src/resources/skills/mandu-performance/rules/_template.md +64 -64
- package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -103
- package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -95
- package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -124
- package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -125
- package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -80
- package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -145
- package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -98
- package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -154
- package/src/resources/skills/mandu-security/SKILL.md +87 -87
- package/src/resources/skills/mandu-security/metadata.json +13 -13
- package/src/resources/skills/mandu-security/rules/_sections.md +31 -31
- package/src/resources/skills/mandu-security/rules/_template.md +74 -74
- package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -127
- package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -133
- package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -148
- package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -146
- package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -138
- package/src/resources/skills/mandu-slot/SKILL.md +85 -85
- package/src/resources/skills/mandu-slot/metadata.json +12 -12
- package/src/resources/skills/mandu-slot/rules/_sections.md +36 -36
- package/src/resources/skills/mandu-slot/rules/_template.md +63 -63
- package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -38
- package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -56
- package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -59
- package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -64
- package/src/resources/skills/mandu-styling/SKILL.md +154 -154
- package/src/resources/skills/mandu-styling/_sections.md +43 -43
- package/src/resources/skills/mandu-styling/_template.md +32 -32
- package/src/resources/skills/mandu-styling/metadata.json +15 -15
- package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -235
- package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -255
- package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -205
- package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -272
- package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -167
- package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -221
- package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -209
- package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -192
- package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -162
- package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -164
- package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +170 -170
- package/src/resources/skills/mandu-styling/rules/style-tailwind-v4-gotchas.md +179 -179
- package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -229
- package/src/resources/skills/mandu-testing/SKILL.md +99 -99
- package/src/resources/skills/mandu-testing/metadata.json +13 -13
- package/src/resources/skills/mandu-testing/rules/_sections.md +26 -26
- package/src/resources/skills/mandu-testing/rules/_template.md +65 -65
- package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -195
- package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -196
- package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -219
- package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -192
- package/src/resources/skills/mandu-ui/SKILL.md +117 -117
- package/src/resources/skills/mandu-ui/_sections.md +23 -23
- package/src/resources/skills/mandu-ui/_template.md +32 -32
- package/src/resources/skills/mandu-ui/metadata.json +13 -13
- package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -232
- package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -238
- package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -259
- package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -258
- package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -213
- package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -209
- package/src/resources/skills/recipes.ts +932 -932
- package/src/tools/generate.ts +7 -4
- package/src/tools/guard.ts +17 -4
- package/src/tools/hydration.ts +10 -10
- package/src/tools/project.ts +334 -334
- package/src/tools/runtime.ts +497 -497
- package/src/tools/seo.ts +417 -417
- package/src/tools/spec.ts +80 -159
- package/src/utils/project.ts +22 -12
- package/src/utils/withWarnings.ts +83 -83
|
@@ -1,323 +1,323 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Deploy with Supabase
|
|
3
|
-
impact: HIGH
|
|
4
|
-
impactDescription: Backend-as-a-Service with PostgreSQL and Edge Functions
|
|
5
|
-
tags: deployment, supabase, database, edge, baas
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Deploy with Supabase
|
|
9
|
-
|
|
10
|
-
**Impact: HIGH (Backend-as-a-Service with PostgreSQL and Edge Functions)**
|
|
11
|
-
|
|
12
|
-
Supabase를 사용하여 PostgreSQL 데이터베이스, 인증, Edge Functions를 통합하세요.
|
|
13
|
-
|
|
14
|
-
**프로젝트 설정:**
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
# Supabase CLI 설치
|
|
18
|
-
npm install -g supabase
|
|
19
|
-
|
|
20
|
-
# 로그인
|
|
21
|
-
supabase login
|
|
22
|
-
|
|
23
|
-
# 프로젝트 초기화
|
|
24
|
-
supabase init
|
|
25
|
-
|
|
26
|
-
# 로컬 개발 환경 시작
|
|
27
|
-
supabase start
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Supabase 클라이언트 설정
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// lib/supabase.ts
|
|
34
|
-
import { createClient } from "@supabase/supabase-js";
|
|
35
|
-
import type { Database } from "./database.types";
|
|
36
|
-
|
|
37
|
-
const supabaseUrl = process.env.SUPABASE_URL!;
|
|
38
|
-
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!;
|
|
39
|
-
|
|
40
|
-
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
|
|
41
|
-
|
|
42
|
-
// 서버 사이드 (Service Role)
|
|
43
|
-
export function createServerClient() {
|
|
44
|
-
return createClient<Database>(
|
|
45
|
-
supabaseUrl,
|
|
46
|
-
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
47
|
-
{
|
|
48
|
-
auth: {
|
|
49
|
-
autoRefreshToken: false,
|
|
50
|
-
persistSession: false,
|
|
51
|
-
},
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## 데이터베이스 마이그레이션
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
# 마이그레이션 생성
|
|
61
|
-
supabase migration new create_users_table
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```sql
|
|
65
|
-
-- supabase/migrations/20240101000000_create_users_table.sql
|
|
66
|
-
CREATE TABLE users (
|
|
67
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
68
|
-
email TEXT UNIQUE NOT NULL,
|
|
69
|
-
name TEXT,
|
|
70
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
-- RLS 활성화
|
|
74
|
-
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
|
75
|
-
|
|
76
|
-
-- 정책 설정
|
|
77
|
-
CREATE POLICY "Users can read own data"
|
|
78
|
-
ON users FOR SELECT
|
|
79
|
-
USING (auth.uid() = id);
|
|
80
|
-
|
|
81
|
-
CREATE POLICY "Users can update own data"
|
|
82
|
-
ON users FOR UPDATE
|
|
83
|
-
USING (auth.uid() = id);
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
# 마이그레이션 적용
|
|
88
|
-
supabase db push
|
|
89
|
-
|
|
90
|
-
# 타입 생성
|
|
91
|
-
supabase gen types typescript --local > lib/database.types.ts
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Mandu Slot에서 Supabase 사용
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// app/users/slot.ts
|
|
98
|
-
import { Mandu } from "@mandujs/core";
|
|
99
|
-
import { createServerClient } from "@/lib/supabase";
|
|
100
|
-
|
|
101
|
-
export default Mandu.filling({
|
|
102
|
-
get: async (ctx) => {
|
|
103
|
-
const supabase = createServerClient();
|
|
104
|
-
const user = ctx.get<User>("user");
|
|
105
|
-
|
|
106
|
-
const { data, error } = await supabase
|
|
107
|
-
.from("users")
|
|
108
|
-
.select("*")
|
|
109
|
-
.eq("id", user.id)
|
|
110
|
-
.single();
|
|
111
|
-
|
|
112
|
-
if (error) {
|
|
113
|
-
return ctx.error({ message: error.message });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return ctx.ok({ user: data });
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
post: async (ctx) => {
|
|
120
|
-
const supabase = createServerClient();
|
|
121
|
-
const body = await ctx.body<{ email: string; name: string }>();
|
|
122
|
-
|
|
123
|
-
const { data, error } = await supabase
|
|
124
|
-
.from("users")
|
|
125
|
-
.insert(body)
|
|
126
|
-
.select()
|
|
127
|
-
.single();
|
|
128
|
-
|
|
129
|
-
if (error) {
|
|
130
|
-
return ctx.error({ message: error.message });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return ctx.created({ user: data });
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Supabase Auth 통합
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
// app/auth/slot.ts
|
|
142
|
-
import { Mandu } from "@mandujs/core";
|
|
143
|
-
import { supabase } from "@/lib/supabase";
|
|
144
|
-
|
|
145
|
-
export default Mandu.filling({
|
|
146
|
-
// 로그인
|
|
147
|
-
post: async (ctx) => {
|
|
148
|
-
const { email, password } = await ctx.body<{
|
|
149
|
-
email: string;
|
|
150
|
-
password: string;
|
|
151
|
-
}>();
|
|
152
|
-
|
|
153
|
-
const { data, error } = await supabase.auth.signInWithPassword({
|
|
154
|
-
email,
|
|
155
|
-
password,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
if (error) {
|
|
159
|
-
return ctx.unauthorized(error.message);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return ctx.ok({
|
|
163
|
-
user: data.user,
|
|
164
|
-
session: data.session,
|
|
165
|
-
});
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
// 로그아웃
|
|
169
|
-
delete: async (ctx) => {
|
|
170
|
-
await supabase.auth.signOut();
|
|
171
|
-
return ctx.noContent();
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**Auth Middleware:**
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
// middleware/auth.ts
|
|
180
|
-
import { createServerClient } from "@/lib/supabase";
|
|
181
|
-
|
|
182
|
-
export async function authMiddleware(ctx: Context) {
|
|
183
|
-
const authHeader = ctx.headers.get("authorization");
|
|
184
|
-
|
|
185
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
186
|
-
return ctx.unauthorized("Missing token");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const token = authHeader.slice(7);
|
|
190
|
-
const supabase = createServerClient();
|
|
191
|
-
|
|
192
|
-
const { data: { user }, error } = await supabase.auth.getUser(token);
|
|
193
|
-
|
|
194
|
-
if (error || !user) {
|
|
195
|
-
return ctx.unauthorized("Invalid token");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
ctx.set("user", user);
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Edge Functions
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
# Edge Function 생성
|
|
206
|
-
supabase functions new hello
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
// supabase/functions/hello/index.ts
|
|
211
|
-
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
|
212
|
-
|
|
213
|
-
serve(async (req) => {
|
|
214
|
-
const { name } = await req.json();
|
|
215
|
-
|
|
216
|
-
return new Response(
|
|
217
|
-
JSON.stringify({ message: `Hello ${name}!` }),
|
|
218
|
-
{ headers: { "Content-Type": "application/json" } }
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
# 로컬 테스트
|
|
225
|
-
supabase functions serve hello
|
|
226
|
-
|
|
227
|
-
# 배포
|
|
228
|
-
supabase functions deploy hello
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## 환경 변수 설정
|
|
232
|
-
|
|
233
|
-
```bash
|
|
234
|
-
# .env.local
|
|
235
|
-
SUPABASE_URL=http://localhost:54321
|
|
236
|
-
SUPABASE_ANON_KEY=eyJ...
|
|
237
|
-
SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
|
238
|
-
|
|
239
|
-
# 프로덕션 (Supabase Dashboard에서 확인)
|
|
240
|
-
SUPABASE_URL=https://xxx.supabase.co
|
|
241
|
-
SUPABASE_ANON_KEY=eyJ...
|
|
242
|
-
SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
## Supabase + Render 배포
|
|
246
|
-
|
|
247
|
-
```yaml
|
|
248
|
-
# render.yaml
|
|
249
|
-
services:
|
|
250
|
-
- type: web
|
|
251
|
-
name: mandu-app
|
|
252
|
-
runtime: node
|
|
253
|
-
buildCommand: bun install && bun run build
|
|
254
|
-
startCommand: bun run start
|
|
255
|
-
envVars:
|
|
256
|
-
- key: SUPABASE_URL
|
|
257
|
-
sync: false # Dashboard에서 설정
|
|
258
|
-
- key: SUPABASE_ANON_KEY
|
|
259
|
-
sync: false
|
|
260
|
-
- key: SUPABASE_SERVICE_ROLE_KEY
|
|
261
|
-
sync: false
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
## Realtime 구독 (Island)
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
// app/messages/client.tsx
|
|
268
|
-
"use client";
|
|
269
|
-
|
|
270
|
-
import { useEffect, useState } from "react";
|
|
271
|
-
import { supabase } from "@/lib/supabase";
|
|
272
|
-
|
|
273
|
-
export function MessagesIsland() {
|
|
274
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
275
|
-
|
|
276
|
-
useEffect(() => {
|
|
277
|
-
// 초기 데이터 로드
|
|
278
|
-
supabase.from("messages").select("*").then(({ data }) => {
|
|
279
|
-
setMessages(data || []);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// Realtime 구독
|
|
283
|
-
const channel = supabase
|
|
284
|
-
.channel("messages")
|
|
285
|
-
.on(
|
|
286
|
-
"postgres_changes",
|
|
287
|
-
{ event: "INSERT", schema: "public", table: "messages" },
|
|
288
|
-
(payload) => {
|
|
289
|
-
setMessages((prev) => [...prev, payload.new as Message]);
|
|
290
|
-
}
|
|
291
|
-
)
|
|
292
|
-
.subscribe();
|
|
293
|
-
|
|
294
|
-
return () => {
|
|
295
|
-
supabase.removeChannel(channel);
|
|
296
|
-
};
|
|
297
|
-
}, []);
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
<ul>
|
|
301
|
-
{messages.map((msg) => (
|
|
302
|
-
<li key={msg.id}>{msg.content}</li>
|
|
303
|
-
))}
|
|
304
|
-
</ul>
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
## Storage 사용
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
// 파일 업로드
|
|
313
|
-
const { data, error } = await supabase.storage
|
|
314
|
-
.from("avatars")
|
|
315
|
-
.upload(`${userId}/avatar.png`, file);
|
|
316
|
-
|
|
317
|
-
// 공개 URL 가져오기
|
|
318
|
-
const { data: { publicUrl } } = supabase.storage
|
|
319
|
-
.from("avatars")
|
|
320
|
-
.getPublicUrl(`${userId}/avatar.png`);
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Reference: [Supabase Documentation](https://supabase.com/docs)
|
|
1
|
+
---
|
|
2
|
+
title: Deploy with Supabase
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Backend-as-a-Service with PostgreSQL and Edge Functions
|
|
5
|
+
tags: deployment, supabase, database, edge, baas
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Deploy with Supabase
|
|
9
|
+
|
|
10
|
+
**Impact: HIGH (Backend-as-a-Service with PostgreSQL and Edge Functions)**
|
|
11
|
+
|
|
12
|
+
Supabase를 사용하여 PostgreSQL 데이터베이스, 인증, Edge Functions를 통합하세요.
|
|
13
|
+
|
|
14
|
+
**프로젝트 설정:**
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Supabase CLI 설치
|
|
18
|
+
npm install -g supabase
|
|
19
|
+
|
|
20
|
+
# 로그인
|
|
21
|
+
supabase login
|
|
22
|
+
|
|
23
|
+
# 프로젝트 초기화
|
|
24
|
+
supabase init
|
|
25
|
+
|
|
26
|
+
# 로컬 개발 환경 시작
|
|
27
|
+
supabase start
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Supabase 클라이언트 설정
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// lib/supabase.ts
|
|
34
|
+
import { createClient } from "@supabase/supabase-js";
|
|
35
|
+
import type { Database } from "./database.types";
|
|
36
|
+
|
|
37
|
+
const supabaseUrl = process.env.SUPABASE_URL!;
|
|
38
|
+
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!;
|
|
39
|
+
|
|
40
|
+
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
|
|
41
|
+
|
|
42
|
+
// 서버 사이드 (Service Role)
|
|
43
|
+
export function createServerClient() {
|
|
44
|
+
return createClient<Database>(
|
|
45
|
+
supabaseUrl,
|
|
46
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
47
|
+
{
|
|
48
|
+
auth: {
|
|
49
|
+
autoRefreshToken: false,
|
|
50
|
+
persistSession: false,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 데이터베이스 마이그레이션
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 마이그레이션 생성
|
|
61
|
+
supabase migration new create_users_table
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
-- supabase/migrations/20240101000000_create_users_table.sql
|
|
66
|
+
CREATE TABLE users (
|
|
67
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
68
|
+
email TEXT UNIQUE NOT NULL,
|
|
69
|
+
name TEXT,
|
|
70
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
-- RLS 활성화
|
|
74
|
+
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
|
75
|
+
|
|
76
|
+
-- 정책 설정
|
|
77
|
+
CREATE POLICY "Users can read own data"
|
|
78
|
+
ON users FOR SELECT
|
|
79
|
+
USING (auth.uid() = id);
|
|
80
|
+
|
|
81
|
+
CREATE POLICY "Users can update own data"
|
|
82
|
+
ON users FOR UPDATE
|
|
83
|
+
USING (auth.uid() = id);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 마이그레이션 적용
|
|
88
|
+
supabase db push
|
|
89
|
+
|
|
90
|
+
# 타입 생성
|
|
91
|
+
supabase gen types typescript --local > lib/database.types.ts
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Mandu Slot에서 Supabase 사용
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// app/users/slot.ts
|
|
98
|
+
import { Mandu } from "@mandujs/core";
|
|
99
|
+
import { createServerClient } from "@/lib/supabase";
|
|
100
|
+
|
|
101
|
+
export default Mandu.filling({
|
|
102
|
+
get: async (ctx) => {
|
|
103
|
+
const supabase = createServerClient();
|
|
104
|
+
const user = ctx.get<User>("user");
|
|
105
|
+
|
|
106
|
+
const { data, error } = await supabase
|
|
107
|
+
.from("users")
|
|
108
|
+
.select("*")
|
|
109
|
+
.eq("id", user.id)
|
|
110
|
+
.single();
|
|
111
|
+
|
|
112
|
+
if (error) {
|
|
113
|
+
return ctx.error({ message: error.message });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return ctx.ok({ user: data });
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
post: async (ctx) => {
|
|
120
|
+
const supabase = createServerClient();
|
|
121
|
+
const body = await ctx.body<{ email: string; name: string }>();
|
|
122
|
+
|
|
123
|
+
const { data, error } = await supabase
|
|
124
|
+
.from("users")
|
|
125
|
+
.insert(body)
|
|
126
|
+
.select()
|
|
127
|
+
.single();
|
|
128
|
+
|
|
129
|
+
if (error) {
|
|
130
|
+
return ctx.error({ message: error.message });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return ctx.created({ user: data });
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Supabase Auth 통합
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// app/auth/slot.ts
|
|
142
|
+
import { Mandu } from "@mandujs/core";
|
|
143
|
+
import { supabase } from "@/lib/supabase";
|
|
144
|
+
|
|
145
|
+
export default Mandu.filling({
|
|
146
|
+
// 로그인
|
|
147
|
+
post: async (ctx) => {
|
|
148
|
+
const { email, password } = await ctx.body<{
|
|
149
|
+
email: string;
|
|
150
|
+
password: string;
|
|
151
|
+
}>();
|
|
152
|
+
|
|
153
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
154
|
+
email,
|
|
155
|
+
password,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (error) {
|
|
159
|
+
return ctx.unauthorized(error.message);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return ctx.ok({
|
|
163
|
+
user: data.user,
|
|
164
|
+
session: data.session,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// 로그아웃
|
|
169
|
+
delete: async (ctx) => {
|
|
170
|
+
await supabase.auth.signOut();
|
|
171
|
+
return ctx.noContent();
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Auth Middleware:**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// middleware/auth.ts
|
|
180
|
+
import { createServerClient } from "@/lib/supabase";
|
|
181
|
+
|
|
182
|
+
export async function authMiddleware(ctx: Context) {
|
|
183
|
+
const authHeader = ctx.headers.get("authorization");
|
|
184
|
+
|
|
185
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
186
|
+
return ctx.unauthorized("Missing token");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const token = authHeader.slice(7);
|
|
190
|
+
const supabase = createServerClient();
|
|
191
|
+
|
|
192
|
+
const { data: { user }, error } = await supabase.auth.getUser(token);
|
|
193
|
+
|
|
194
|
+
if (error || !user) {
|
|
195
|
+
return ctx.unauthorized("Invalid token");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
ctx.set("user", user);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Edge Functions
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Edge Function 생성
|
|
206
|
+
supabase functions new hello
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// supabase/functions/hello/index.ts
|
|
211
|
+
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
|
212
|
+
|
|
213
|
+
serve(async (req) => {
|
|
214
|
+
const { name } = await req.json();
|
|
215
|
+
|
|
216
|
+
return new Response(
|
|
217
|
+
JSON.stringify({ message: `Hello ${name}!` }),
|
|
218
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# 로컬 테스트
|
|
225
|
+
supabase functions serve hello
|
|
226
|
+
|
|
227
|
+
# 배포
|
|
228
|
+
supabase functions deploy hello
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## 환경 변수 설정
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# .env.local
|
|
235
|
+
SUPABASE_URL=http://localhost:54321
|
|
236
|
+
SUPABASE_ANON_KEY=eyJ...
|
|
237
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
|
238
|
+
|
|
239
|
+
# 프로덕션 (Supabase Dashboard에서 확인)
|
|
240
|
+
SUPABASE_URL=https://xxx.supabase.co
|
|
241
|
+
SUPABASE_ANON_KEY=eyJ...
|
|
242
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Supabase + Render 배포
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
# render.yaml
|
|
249
|
+
services:
|
|
250
|
+
- type: web
|
|
251
|
+
name: mandu-app
|
|
252
|
+
runtime: node
|
|
253
|
+
buildCommand: bun install && bun run build
|
|
254
|
+
startCommand: bun run start
|
|
255
|
+
envVars:
|
|
256
|
+
- key: SUPABASE_URL
|
|
257
|
+
sync: false # Dashboard에서 설정
|
|
258
|
+
- key: SUPABASE_ANON_KEY
|
|
259
|
+
sync: false
|
|
260
|
+
- key: SUPABASE_SERVICE_ROLE_KEY
|
|
261
|
+
sync: false
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Realtime 구독 (Island)
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// app/messages/client.tsx
|
|
268
|
+
"use client";
|
|
269
|
+
|
|
270
|
+
import { useEffect, useState } from "react";
|
|
271
|
+
import { supabase } from "@/lib/supabase";
|
|
272
|
+
|
|
273
|
+
export function MessagesIsland() {
|
|
274
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
// 초기 데이터 로드
|
|
278
|
+
supabase.from("messages").select("*").then(({ data }) => {
|
|
279
|
+
setMessages(data || []);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Realtime 구독
|
|
283
|
+
const channel = supabase
|
|
284
|
+
.channel("messages")
|
|
285
|
+
.on(
|
|
286
|
+
"postgres_changes",
|
|
287
|
+
{ event: "INSERT", schema: "public", table: "messages" },
|
|
288
|
+
(payload) => {
|
|
289
|
+
setMessages((prev) => [...prev, payload.new as Message]);
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
.subscribe();
|
|
293
|
+
|
|
294
|
+
return () => {
|
|
295
|
+
supabase.removeChannel(channel);
|
|
296
|
+
};
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<ul>
|
|
301
|
+
{messages.map((msg) => (
|
|
302
|
+
<li key={msg.id}>{msg.content}</li>
|
|
303
|
+
))}
|
|
304
|
+
</ul>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Storage 사용
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// 파일 업로드
|
|
313
|
+
const { data, error } = await supabase.storage
|
|
314
|
+
.from("avatars")
|
|
315
|
+
.upload(`${userId}/avatar.png`, file);
|
|
316
|
+
|
|
317
|
+
// 공개 URL 가져오기
|
|
318
|
+
const { data: { publicUrl } } = supabase.storage
|
|
319
|
+
.from("avatars")
|
|
320
|
+
.getPublicUrl(`${userId}/avatar.png`);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Reference: [Supabase Documentation](https://supabase.com/docs)
|