juni
WelKo_Supabase 연동 및 리뷰CRUD 구현 본문
Supabase 세팅법
폴더구조
📦src
┣ 📂app
┃ ┣ 📂(providers)
┃ ┃ ┣ 📂(root)
┃ ┃ ┃ ┣ 📂(mypage)
┃ ┃ ┃ ┃ ┣ 📂my-page
┃ ┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┃ ┃ ┃ ┗ 📂review-page
┃ ┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┃ ┃ ┣ 📜layout.tsx
┃ ┃ ┃ ┗ 📜page.tsx
┃ ┃ ┣ 📂_providers
┃ ┃ ┃ ┗ 📜QueryProvider.tsx
┃ ┃ ┗ 📜layout.tsx
┃ ┣ 📂api
┃ ┃ ┗ 📂reviews
┃ ┃ ┃ ┗ 📜route.ts
┃ ┣ 📂login
┃ ┃ ┗ 📜page.tsx
┃ ┣ 📜globals.css
┃ ┗ 📜layout.tsx
┣ 📂assets
┃ ┗ 📜a.ts
┣ 📂components
┃ ┗ 📜a.ts
┣ 📂hooks
┃ ┗ 📜a.ts
┣ 📂lib
┃ ┗ 📜a.ts
┣ 📂services
┃ ┗ 📜a.ts
┣ 📂types
┃ ┗ 📜supabase.ts
┣ 📂utils
┃ ┗ 📂supabase
┃ ┃ ┣ 📜client.ts
┃ ┃ ┣ 📜middleware.ts
┃ ┃ ┣ 📜server.ts
┃ ┃ ┗ 📜service.ts
┗ 📜middleware.ts
코드
1. client.ts
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
- 역할: 브라우저 클라이언트를 생성하는 함수입니다.
- 구성 요소:
- createBrowserClient: Supabase의 ssr 패키지에서 가져온 함수로, 브라우저 환경에서 Supabase 클라이언트를 생성하는 데 사용됩니다.
- process.env.NEXT_PUBLIC_SUPABASE_URL 및 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY: Supabase 프로젝트에 접근하기 위한 환경 변수입니다.
- 설명: 이 함수는 브라우저 환경에서 Supabase 클라이언트를 초기화하여 사용자가 Supabase 서비스를 사용할 수 있도록 설정합니다. 주로 브라우저 사이드에서 Supabase API를 호출할 때 사용됩니다.
2. middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value));
supabaseResponse = NextResponse.next({
request
});
cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options));
}
}
}
);
const {
data: { user }
} = await supabase.auth.getUser();
if (user && (request.nextUrl.pathname.startsWith('/login') || request.nextUrl.pathname.startsWith('/signup'))) {
return NextResponse.redirect(request.nextUrl.origin);
}
return supabaseResponse;
}
- 역할: 서버 클라이언트를 생성하고, 세션을 업데이트하는 미들웨어 함수입니다.
- 구성 요소:
- createServerClient: Supabase의 ssr 패키지에서 가져온 함수로, 서버 환경에서 Supabase 클라이언트를 생성하는 데 사용됩니다.
- NextRequest 및 NextResponse: Next.js에서 서버 사이드 미들웨어를 처리하는 객체들입니다.
- supabase.auth.getUser(): 현재 인증된 사용자를 가져오는 함수입니다.
- 리다이렉션 로직: 사용자가 로그인된 상태에서 로그인 페이지나 회원가입 페이지에 접근할 때 홈으로 리다이렉션합니다.
- 설명: 이 미들웨어 함수는 서버 사이드에서 Supabase 클라이언트를 초기화하고, 사용자의 세션을 업데이트합니다. 특정 경로에 따라 리다이렉션을 처리하여 올바른 페이지로 사용자를 안내합니다.
3. server.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
export function createClient() {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
);
}
- 역할: 서버 클라이언트를 생성하는 함수입니다.
- 구성 요소:
- createServerClient: Supabase의 ssr 패키지에서 가져온 함수로, 서버 환경에서 Supabase 클라이언트를 생성하는 데 사용됩니다.
- cookies: Next.js의 헤더에서 쿠키를 관리하는 객체입니다.
- cookieStore: 쿠키를 저장하고 관리하는 로직을 포함합니다.
- 설명: 이 함수는 서버 환경에서 Supabase 클라이언트를 초기화하고, 요청에 따라 쿠키를 설정하거나 가져오는 기능을 제공합니다. 서버 컴포넌트에서 Supabase 클라이언트를 사용할 때 유용합니다.
4. service.ts
import { createClient } from '@/utils/supabase/client';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const validateEmail = (email: string): boolean => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
const validatePassword = (password: string): boolean => {
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return regex.test(password);
};
export const handleLogin = async (
email: string,
password: string,
router: any,
setError: (message: string) => void
) => {
const supabase = createClient();
if (!email) {
return toast.error('이메일을 입력하세요!');
}
if (!password) {
return toast.error('비밀번호를 입력하세요!');
}
const { data, error: signInError } = await supabase.auth.signInWithPassword({
email,
password
});
if (signInError) {
if (signInError.message.toLowerCase().includes('invalid login credentials')) {
toast.error('잘못된 로그인 자격 증명입니다.');
} else {
setError(signInError.message);
}
} else {
toast.success('로그인 되었습니다.');
router.push('/');
}
};
export const handleSignUp = async (
email: string,
password: string,
nickname: string,
router: any,
setError: (message: string) => void
) => {
const supabase = createClient();
if (!email || !password || !nickname) {
return toast.error('빈칸을 모두 채워주세요!');
}
if (!validateEmail(email)) {
return toast.error('유효한 이메일 주소를 입력하세요!(꼭 .com 으로 끝나는 이메일이어야 합니다!)');
}
if (!validatePassword(password)) {
return toast.error('비밀번호는 최소 8자 이상, 영문자, 숫자, 특수 문자를 포함해야 합니다!');
}
const { data: emailExist, error: emailError } = await supabase.from('users').select('id').eq('email', email).single();
if (emailExist) {
return toast.error('이미 사용 중인 이메일입니다!');
}
const { data: nicknameExist, error: nicknameError } = await supabase
.from('users')
.select('id')
.eq('nickname', nickname)
.single();
if (nicknameExist) {
return toast.error('이미 사용 중인 닉네임입니다!');
}
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
options: {
data: {
nickname: nickname
}
}
});
if (error) {
setError(error.message);
} else {
toast.success('회원가입 성공!');
router.push('/');
}
};
- 역할: 로그인 및 회원가입 기능을 처리하는 서비스 함수들입니다.
- 구성 요소:
- createClient: Supabase 클라이언트를 생성하는 함수입니다.
- validateEmail 및 validatePassword: 이메일과 비밀번호의 유효성을 검사하는 함수들입니다.
- handleLogin: 사용자의 로그인 요청을 처리하는 함수입니다.
- handleSignUp: 사용자의 회원가입 요청을 처리하는 함수입니다.
- 설명: 이 파일은 사용자 인증을 위한 로직을 포함하고 있으며, 클라이언트 사이드 유효성 검사와 Supabase를 통한 인증 및 데이터베이스 조작을 수행합니다. 성공 또는 실패 시 적절한 메시지를 사용자에게 보여줍니다.
5. middleware.ts (Next.js 미들웨어 설정)
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
- 역할: Next.js 미들웨어를 설정하고, 요청 경로에 따라 세션을 업데이트합니다.
- 구성 요소:
- updateSession: 사용자 세션을 업데이트하는 함수입니다.
- matcher: 특정 경로를 제외한 모든 요청에 대해 미들웨어를 적용하도록 설정합니다.
- 설명: 이 파일은 Next.js의 미들웨어로 사용되어 모든 요청에 대해 세션을 업데이트하고, 특정 조건에 따라 리다이렉션을 처리합니다. 특정 파일 경로(정적 파일, 이미지 등)를 제외한 모든 요청에 대해 적용됩니다.
요약
- client.ts: 브라우저 클라이언트를 생성하여 브라우저 사이드에서 Supabase 서비스를 사용할 수 있도록 합니다.
- middleware.ts: 서버 클라이언트를 생성하고, 사용자 세션을 업데이트하는 미들웨어 함수로 사용됩니다.
- server.ts: 서버 클라이언트를 생성하여 서버 사이드에서 Supabase 서비스를 사용할 수 있도록 합니다.
- service.ts: 로그인 및 회원가입 기능을 처리하는 서비스 함수들을 포함하여 클라이언트 사이드 유효성 검사와 Supabase 인증을 처리합니다.
- src/middleware.ts: Next.js 미들웨어 설정 파일로, 모든 요청에 대해 세션을 업데이트하고 리다이렉션을 처리합니다.
api/reviews
GET , POST , PUT , DELETE 구현
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server';
export async function GET(req: NextRequest) {
const supabase = createClient();
const { data, error } = await supabase.from('reviews').select('*');
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data, { status: 200 });
}
export async function POST(req: NextRequest) {
const supabase = createClient();
const { content, rating, post_id, user_id } = await req.json();
const { data, error } = await supabase.from('reviews').insert({ content, rating, post_id, user_id });
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data, { status: 201 });
}
export async function PUT(req: NextRequest) {
const supabase = createClient();
const { id, content, rating } = await req.json();
const { data, error } = await supabase.from('reviews').update({ content, rating }).eq('id', id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data, { status: 200 });
}
export async function DELETE(req: NextRequest) {
const supabase = createClient();
const { id } = await req.json();
const { data, error } = await supabase.from('reviews').delete().eq('id', id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data, { status: 200 });
}
(mypage)/my-page/page.tsx
마이 페이지 리뷰 관리 구현 ( 독자적인 테이블로 CRUD 동작 , 유저 및 게시글 구현 시 연결 예정 )
'use client';
import axios from 'axios';
import { useEffect, useState } from 'react';
import Link from 'next/link';
type Review = {
id: string;
created_at: string;
post_id: string;
user_id: string;
content: string;
rating: number;
};
const MyPage = () => {
const [reviews, setReviews] = useState<Review[]>([]);
useEffect(() => {
const fetchReviews = async () => {
const response = await axios.get('/api/mypage');
setReviews(response.data);
};
fetchReviews();
}, []);
const handleDelete = async (id: string) => {
await axios.delete('/api/mypage', { data: { id } });
setReviews(reviews.filter((review) => review.id !== id));
};
return (
<div>
<h1>My Page</h1>
<Link href={`/review-page`}>
<button>Create New Review</button>
</Link>
{reviews.length === 0 ? (
<div>Loading...</div>
) : (
reviews.map((item) => (
<div key={item.id}>
<h2>{item.content}</h2>
<p>Rating: {item.rating}</p>
<button onClick={() => handleDelete(item.id)}>Delete</button>
<Link href={`/review-page?id=${item.id}`}>
<button>Edit</button>
</Link>
</div>
))
)}
</div>
);
};
export default MyPage;
(mypage)/my-page/page.tsx
리뷰 작성 및 수정 페이지 ( 완료 및 취소 시 꼭 mypage가 아닌 이전페이지로 돌아갈 예정 )
'use client';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { v4 as uuidv4 } from 'uuid';
type Review = {
id: string;
created_at: string;
post_id: string;
user_id: string;
content: string;
rating: number;
};
const ReviewPage = () => {
const [review, setReview] = useState<Review | null>(null);
const [content, setContent] = useState('');
const [rating, setRating] = useState(0);
const router = useRouter();
const searchParams = useSearchParams();
const id = searchParams.get('id');
useEffect(() => {
const fetchReview = async () => {
if (id) {
const response = await axios.get(`/api/mypage?id=${id}`);
const reviewData = response.data[0];
setReview(reviewData);
setContent(reviewData.content);
setRating(reviewData.rating);
}
};
fetchReview();
}, [id]);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (id) {
await axios.put('/api/mypage', { id, content, rating });
} else {
const postId = uuidv4();
const userId = uuidv4();
await axios.post('/api/mypage', { content, rating, post_id: postId, user_id: userId });
}
router.back();
};
const handleDelete = async () => {
if (id) {
await axios.delete('/api/mypage', { data: { id } });
router.back();
}
};
const handleBack = () => {
router.back();
};
return (
<div>
<h1>{id ? 'Edit Review' : 'New Review'}</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Content"
className="text-black"
/>
<input
type="number"
value={rating}
onChange={(e) => setRating(parseInt(e.target.value))}
placeholder="Rating"
className="text-black"
/>
<button type="submit">{id ? 'Update Review' : 'Add Review'}</button>
</form>
{id && <button onClick={handleDelete}>Delete Review</button>}
<button onClick={handleBack}>Back</button>
</div>
);
};
export default ReviewPage;
회의 내용
튜터님 조언
- 반응형 기획 시 웹보다는 앱을 먼저 기획하고 구현하는게 좋음
- 작은집에서 큰집으로 이사가는 것을 생각하면됨
- 반대로 할 시 기능적으로 문제가 생길 수 있고 시간도 오래 걸림
→ 앱 기준으로 가로 360px 작업 예정
회의
- 결제 관련해서 외국인을 주 타겟층으로 고려하여 paypal API를 사용 예정
- 연동 및 결제 대금에관하여 백오피스가 중앙에서 관리하게끔 기획 중
- 컴포넌트 분리 방식 - app 폴더 아래 “components” 또는 app 폴더 아래 개인 page폴더 아래 “_components”
- 폴더 분리 방식 - page.tsx 파일 안에 로직을 다 작성하기 또는 세부적으로 파일 나누기
노션 정리 , 피그마에 앱 시안 방향성 회의 , 각자 분담한 기능 구현 조사 및 시작
'프로젝트 > WelKo' 카테고리의 다른 글
WelKo_프로필 수정 및 내 게시글 리스트 확인 , 상세페이지 이동 (1) | 2024.07.23 |
---|---|
WelKo_와이어프레임 및 기능 구체화 (3) | 2024.07.22 |
WelKo_기획 및 기능 구체화 , 테이블 설계 (0) | 2024.07.18 |
WelKo_레퍼런스 조사 및 지도 코드 임시 구현 (1) | 2024.07.17 |
WelKo_주제 선정 , 와이어프레임 기획 , 개발 기획 (0) | 2024.07.17 |