
📜 프로젝트 내용
캠핑장 예약 및 검색, 캠핑장 운영, 커뮤니티 플랫폼 '캠퍼스팟'
📅 개발일정
👥 팀원 구성
⚙️ 기술 스택
⚙️ 기타 라이브러리
⚙️ BaaS
1. 캠핑장 검색 및 예약을 할 수 있습니다.
2. 캠핑장 운영 및 관리, 예약관리를 할 수 있습니다.
3. 커뮤니티를 통해 캠핑러들과 소통할 수 있습니다.
NextAuth Credentials 활용하여 업체 회원 회원가입 & 로그인 구현
NextAuth Providers 활용하여 네이버 & 카카오 소셜 로그인 구현
Session 으로 유저 로그인 상태 확인
bcrypt 활용하여 회원가입시 비밀번호 암호화 후 저장
Image 컴포넌트를 활용하여 이미지 최적화 진행
Suspense 컴포넌트 활용하여 로딩 페이지 구현
Route Handlers 를 활용하여 서버리스 API 구현
// api/auth/signup/route.ts
export const POST = async (req: NextRequest) => {
const userData = await req.json();
const { data } = await supabase
.from('company_user')
.select('email')
.eq('email', userData.email)
.single();
if (data) {
return NextResponse.json({
status: 409,
message: '이미 가입되어있는 이메일 입니다!',
});
}
const hashedPassword = await bcrypt.hashSync(userData.password, 5);
const { error: saveError } = await supabase
.from('company_user')
.insert<Omit<CompanyUserSignUpType, 'confirmPassword'>>({
...userData,
password: hashedPassword,
});
if (saveError) {
return NextResponse.json({
status: saveError.code,
message: '회원가입에 실패하였습니다!',
});
}
return NextResponse.json({ status: 200, message: '회원가입 완료!' });
};
Tanstack Query + CSS 로 별점 리뷰 기능 구현
업체 회원과 일반 유저가 url로 접근할 수 있으므로 특정 url에 접근하지 못하도록 라우트 가드 적용
이미 로그인 되어있는 회원 -> 로그인 url 접근 막기
로그인 안되어있는 회원 -> 로그인 url로 리다이렉트
React Portal 활용하여 커스텀 모달 구현
Tanstack Query 활용하여 유저 프로필 변경 기능 구현
유저 프로필 변경시 사용자 경험을 위해 이미지 업로드시 미리보기 구현
UI / UX 측면에서 기존의 캘린더 디자인이 좋지 못하여 react-datepicker 디자인 커스텀 적용
react-hook-form 을 활용하여 UX 높이기 위해 실시간 유효성 검사 진행
<div className={styles['content-container']}>
<label>아이디</label>
<input
type='email'
placeholder='예) email@gmail.com'
required={true}
{...register('email', {
required: true,
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
})}
/>
기존의 비즈니스 로직과 UI 로직이 한 군데에 존재하던것을 Custom Hook을 이용하여 분리
// useProfileQuery.ts
import { modifyUserData } from '@/app/profile/[id]/_lib/profile';
import { getUserData } from '@/app/profile/[id]/_lib/profile';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
export const useProfileQuery = () => {
const queryClient = useQueryClient();
const params = useParams();
const userId = params.id;
const { data: profile, isLoading: isProfileLoading } = useQuery({
queryKey: ['mypage', 'profile', userId],
queryFn: getUserData,
refetchOnMount: true,
});
const {mutate: profileMutate, isPending: isProfilePending} = useMutation({
mutationFn: modifyUserData,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['mypage', 'profile', userId],
});
},
});
return {profile, profileMutate ,isProfileLoading, isProfilePending}
}
'use client';
import Loading from '@/app/loading';
import Modal from '@/components/Modal';
import ModalPortal from '@/components/ModalPortal';
import { useProfileQuery } from '@/hooks/useProfileQuery';
...
const Profile = () => {
const { show, toggleModal } = useModalStore();
const { profile, isProfileLoading } = useProfileQuery();
if (isProfileLoading) {
return <Loading />;
}
return (
<div className={styles.container}>
<div className={styles['profile-container']}>
<div className={styles['profile-header']}>프로필 관리</div>
<div className={styles['profile-inner']}>
<div className={styles['profile-image-wrapper']}>
{profile?.profile_url && (
<Image
src={`${profile?.profile_url}` as string}
width={120}
height={120}
alt='profile'
priority
className={styles['profile-image']}
onClick={toggleModal}
/>
)}
</div>
<div className={styles['nickname-container']}>
<div className={styles['nickname-header']}>닉네임</div>
...
export default Profile;
Failed to execute 'json' on 'Response': body stream already read
최종 프로젝트를 통해서 Next.js의 편의성에 대해서 크게 느꼈다.
특히 기존 Page Router가 아닌 14버전의 App Router를 사용했는데, getStaticProps 같은 함수들을 layout으로 활용해버리니 DX가 정말 많이 증가했다.
또한 Route Handlers를 활용하여 직접 API를 만드는것이 신기한 경험이었다. Next.js는 정말 기능이 많다.
최종 프로젝트이지만 너무 많은 기능을 넣어서 시간이 부족했다.
업체회원 기능을 빼면 아마 시간이 널널하게 리팩토링과 반응형 까지 완성 했을 것 같은 생각이 든다.
앞으로 못한 반응형과 지속적으로 유지보수를 하면서, 리팩토링, 기능 추가를 해야겠다.
5주동안 부족한 리더였지만 잘 따라와준 팀원들에게 감사를 보낸다.