์ค๋์ ์จํ ์๋น์ค์์
์ฌ์ฉ์๊ฐ ์ง์ โ๋ชจ์์ ๋ฑ๋กโํ ์ ์๋ ๊ธฐ๋ฅ์ ์์ฑํ๋ค.
ํ๋ก ํธ โ ๋ฐฑ์๋ โ DB๊น์ง ํ๋ฆ์ ์ง์ ์ฐ๊ฒฐํ๋ฉด์,
Next.js์
Route Handler์Supabase, ๊ทธ๋ฆฌ๊ณaxios์ ์ญํ ์ ํ์คํ ์ดํดํ ์ ์์๋ค.
axios๋ก ์๋ฒ(/api/post) ์์ฒญ์ค๋ ๋ง๋ ๊ตฌ์กฐ๋ Next.js App Router ๊ธฐ๋ฐ์ ์์ ํ ํ์คํ ํ๋ฆ์ด๋ค.
๐ฆ on-fit
โฃ ๐ app
โ โฃ ๐ post
โ โ โฃ ๐ create
โ โ โ โ ๐ page.tsx โ ํด๋ผ์ด์ธํธ ํผ (axios ์์ฒญ)
โ โ ๐ api
โ โ ๐ post
โ โ ๐ route.ts โ ์๋ฒ Route Handler (Supabase DB ์ ์ฅ)
โฃ ๐ lib
โ โฃ ๐ axios.ts โ axios ๊ณตํต ์ธ์คํด์ค
โ โ ๐ supabase-admin.ts โ ์๋ฒ์ฉ Supabase ํด๋ผ์ด์ธํธ
โ ๐ components/common โ Input, DropBox, Button ๋ฑ UI ์ปดํฌ๋ํธ
์ฌ์ฉ์๊ฐ ์์ฑํ๋ ํ์ด์ง /post/create
์ด ํผ์ ํต์ฌ์ FormData โ axios.post() ์กฐํฉ์ด๋ค.
'use client'
import { useState } from 'react'
import { Card, CardHeader, CardContent } from '@/components/common/Card'
import { Input } from '@/components/common/Input'
import { TextArea } from '@/components/common/TextArea'
import DropBox from '@/components/common/DropBox'
import { Button } from '@/components/common/Button'
import { api } from '@/lib/axios'
const sportOption = ['๋ฐฐ๋๋ฏผํด', '์ถ๊ตฌ', '์ผ๊ตฌ']
const levelOption = ['๋ธ๋ก ์ฆ', '์ค๋ฒ', '๊ณจ๋']
export default function Page() {
const [sport, setSport] = useState(sportOption[0])
const [level, setLevel] = useState(levelOption[0])
const [loading, setLoading] = useState(false)
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const fd = new FormData(e.currentTarget)
const payload = {
sport: fd.get('sport'),
title: fd.get('title'),
description: fd.get('description') || '',
location: fd.get('location'),
date: fd.get('date'),
time: fd.get('time'),
level: fd.get('level'),
maxParticipants: Number(fd.get('maxParticipants') || 0),
currentParticipants: 1,
status: '๋ชจ์ง์ค',
author: 'ํ
์คํธ์ ์ ',
equipment: fd.get('equipment') || '',
fee: fd.get('fee') || '',
}
try {
setLoading(true)
const res = await api.post('/api/post', payload)
console.log('์ฑ๊ณต:', res.data.item)
alert('๋ชจ์์ด ์์ฑ๋์์ต๋๋ค!')
e.currentTarget.reset() // ํผ ์ด๊ธฐํ
} catch (err: any) {
console.log('SERVER ERROR:', err.response?.data)
alert(`์์ฑ ์คํจ: ${err.response?.data?.error ?? err.message}`)
} finally {
setLoading(false)
}
}
return (
<main className="container mx-auto px-4 py-8 max-w-2xl">
<Card>
<CardHeader>
<h3 className="text-2xl font-semibold">๋ฒ๊ฐ ๋ชจ์ ๋ง๋ค๊ธฐ</h3>
<p className="text-sm text-muted-foreground">ํจ๊ป ์ด๋ํ ์ฌ๋๋ค์ ๋ชจ์งํด๋ณด์ธ์!</p>
</CardHeader>
<CardContent>
<form onSubmit={onSubmit} className="space-y-6">
<DropBox name="sport" value={sport} onChange={setSport} options={sportOption} />
<Input name="title" placeholder="์: ๊ฐ๋จ ๋ฐฐ๋๋ฏผํด ์ด๊ธ์ ๋ชจ์ง!" required />
<TextArea name="description" placeholder="๋ชจ์์ ๋ํด ๊ฐ๋จํ ์๊ฐํด์ฃผ์ธ์" />
<Input name="location" placeholder="์: ๊ฐ๋จ๊ตฌ ์ญ์ผ๋ ์ฒด์ก๊ด" required />
<Input name="date" type="date" required />
<Input name="time" type="time" required />
<Input name="maxParticipants" type="number" placeholder="์: 6" required />
<DropBox name="level" value={level} onChange={setLevel} options={levelOption} />
<Input name="equipment" placeholder="์: ๋ผ์ผ(๋์ฌ ๊ฐ๋ฅ), ์ด๋ํ" />
<Input name="fee" placeholder="์: 15,000์ (์์ค๋น ํฌํจ)" />
<div className="flex gap-3 pt-4">
<Button type="button" variant="outline" fullWidth>์ทจ์</Button>
<Button type="submit" variant="hero" fullWidth disabled={loading}>
{loading ? '๋ฑ๋ก ์คโฆ' : '๋ชจ์ ๋ง๋ค๊ธฐ'}
</Button>
</div>
</form>
</CardContent>
</Card>
</main>
)
}
์ด ์ฝ๋์์๋ ์ฌ์ฉ์ ์ ๋ ฅ์ ์์งํด์ axios๋ก ์๋ฒ์ POST ์์ฒญ์ ๋ณด๋ธ๋ค.
FormData๋ก key/value๋ฅผ ์ถ์ถํด์ payload๋ก ์ ์กํ๋ ๊ตฌ์กฐ๋ค.
์ด์ /app/api/post/route.ts ์์ ์๋ฒ๊ฐ Supabase DB์ ๋ฐ์ดํฐ ์ฝ์
์ ๋ด๋นํ๋ค.
import { sbAdmin } from '@/lib/supabase-admin'
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
try {
const body = await req.json()
const { data, error } = await sbAdmin
.from('fits')
.insert({
...body,
created_at: new Date().toISOString(),
})
.select()
.single()
if (error) throw error
return NextResponse.json({ ok: true, item: data })
} catch (err: any) {
console.error(err)
return NextResponse.json(
{ ok: false, error: err.message ?? '์๋ฒ ์ค๋ฅ' },
{ status: 500 }
)
}
}
์ฌ๊ธฐ์ sbAdmin์ ์๋ฒ ์ ์ฉ Supabase ํด๋ผ์ด์ธํธ๋ก,
Service Role Key๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ํด๋ผ์ด์ธํธ์์๋ ์ ๋ ์ธ ์ ์๋ค.
axios๋ฅผ ํ๋ก์ ํธ ์ ์ญ์์ ํต์ผ๋ ํํ๋ก ์ฐ๊ธฐ ์ํด ์๋์ฒ๋ผ ์ค์ ํ๋ค.
import axios from 'axios'
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '',
headers: { 'Content-Type': 'application/json' },
})
api.interceptors.response.use(
(res) => res,
(err) => {
console.error('API Error:', err.response?.data || err.message)
return Promise.reject(err)
}
)
์ด๋ ๊ฒ ํ๋ฉด ์๋ฌ ๋ก๊ทธ๋ ํ ํฐ ์ฃผ์ , ๊ณตํต ํค๋ ์ค์ ๋ฑ์ ์ฝ๊ฒ ๊ด๋ฆฌํ ์ ์๋ค.
์ค๋ ๊ตฌํํ ํ๋ฆ์ ์ด๋ ๊ฒ ๋ ๋ฒ์ ํต์ ์ผ๋ก ์ด๋ค์ง๋ค.
(1) ํด๋ผ์ด์ธํธ โ axios โ /api/post
(2) ์๋ฒ(Route Handler) โ sbAdmin โ Supabase(PostgreSQL)
axios.post('/api/post') ์คํroute.ts)๊ฐ ์์ฒญ ๋ณธ๋ฌธ์ ๋ฐ๊ณ sbAdmin.from('fits').insert() ๋ก Supabase DB์ ์ ์ฅ{ ok: true, item: {...} } ์๋ต| ์ด์ | ์ค๋ช |
|---|---|
| ๋ณด์ | Service Role Key๋ฅผ ๋ธ๋ผ์ฐ์ ์ ๋ ธ์ถํ๋ฉด ์ ๋จ |
| ๊ฒ์ฆ | ์๋ฒ์์ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ํํฐ๋ง ๊ฐ๋ฅ |
| ํ์ฅ์ฑ | ๋์ค์ ์๋ฆผ, ์บ์, ํ์ผ ์ ๋ก๋ ๋ฑ ์ฝ๊ฒ ์ถ๊ฐ ๊ฐ๋ฅ |
์ฆ, ์ด ๊ตฌ์กฐ๋ ์์ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ํ์ค์ ์ธ Next.js ๋ฐฑ์๋ ํจํด์ด๋ค.
| ๋ฐฉ์ | ์ฅ์ | ๋จ์ |
|---|---|---|
| Supabase SDK๋ง ์ฌ์ฉ | ๋น ๋ฅด๊ณ ๊ฐ๋จํจ | Service Role ๋ชป ์, ๋ก์ง ์ถ๊ฐ ์ด๋ ค์ |
| axios โ route.ts โ sbAdmin | ๋ณด์/๊ฒ์ฆ/ํ์ฅ์ฑ ํ์ | ๊ตฌ์กฐ ํ ๋จ๊ณ ๋ ๋ณต์ก |
โ๏ธ /post/create ํ์ด์ง์์ ํผ ์์ฑ ํ โ๋ชจ์ ๋ง๋ค๊ธฐโ ํด๋ฆญ ์
Supabase DB์ ์๋ก์ด ๊ฒ์๊ธ ์์ฑ
โ๏ธ axios โ Route โ Supabase ํ๋ฆ ์์ ์ฐ๊ฒฐ
โ๏ธ ๋ก๋ฉ ์ํ, ํผ ์ด๊ธฐํ, ์๋ฌ ์ฒ๋ฆฌ ๊ตฌํ ์๋ฃ
| ๊ฐ๋ | ์ค๋ช |
|---|---|
| axios | ํด๋ผ์ด์ธํธ โ Next.js Route ํต์ ์ฉ HTTP ํด๋ผ์ด์ธํธ |
| Supabase sbAdmin | ์๋ฒ(Route) โ PostgreSQL ์ฐ๊ฒฐ์ฉ SDK |
| ๋ ๋จ๊ณ ํต์ | (1) axios ์์ฒญ โ (2) sbAdmin DB ์์ฒญ |
| ๋ณด์ ๊ตฌ์กฐ | ๋ธ๋ผ์ฐ์ ๋ anon key, ์๋ฒ๋ service role key ์ฌ์ฉ |
| FormData | <form>์์ key/value ์ถ์ถํ๋ ๊ธฐ๋ณธ Web API |
์ค๋์ ์ฒ์์ผ๋ก Next.js ๋ด๋ถ API(Route Handler) ์
Supabase(PostgreSQL) ๋ฅผ ์ง์ ์ฐ๊ฒฐํด๋ดค๋ค.
์ฒ์์ axios์ Supabase์ ์ญํ ์ด ๊ฒน์ณ๋ณด์์ง๋ง,
์ง๊ธ์ ์์ ํ ์ดํด๋๋ค ๐
axios๋ โ๋ด ์ฑ ์๋ฒ๊น์งโ,
sbAdmin(Supabase) ์ โ์๋ฒ์์ DB๊น์งโ.
์ด์ ์ด ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก
๋ค์ ๋จ๊ณ โ ๊ฒ์๊ธ ๋ชฉ๋ก/์์ธ ์กฐํ ๊ธฐ๋ฅ์ ๋ง๋ค ์์ ์ด๋ค.