반응형
이번 단계에서는 유저가 직접 장소를 등록할 수 있는 기능을 개발해보았다.
추후에는 본인이 등록한 장소들 또는 기존에 다른 유저가 등록한 장소들을 가지고 코스를 등록할 수 있도록 기획을 해볼 예정이다!
새로운 데이터베이스 스키마를 추가했고 유저의 무분별한 장소 등록을 방지하고자 하루에 최대 3개정도까지만 등록할 수 있도록 기획했다.
그리고 유저 본인이 등록한 장소를 카드 형태로 확인할 수 있고, 장소 수정은 불가능하고 삭제 후 재등록을 통해서만 가능하도록 기획해보았다.
그리고 현재는 모든 장소가 관리되어지고 있는데 나중에는 관리자의 승인에 의해서만 장소 등록이 될 수 있도록도 고려를 해야하지 않을까 생각한다.
일단은 유저 참여형 서비스를 개발해보고 싶었다. 어드민에서 등록되어져 있는 장소들만 가지고 유저에게 보여주기에는 너무 서비스가 이상해보이기도 하고.... 일단 내 생각은 그럼
장소 테이블에서는 source를 추가해서 유저에 의해서 등록된 장소인지 관리자에 의해서 등록된 장소인지 구분이 가능하도록 했다.
DAY 4: 유저 장소 등록 기능 구현
📋 기능 개요
유저가 직접 데이트 장소를 추천하고 공유할 수 있는 기능을 구현했습니다.
이는 관리자가 등록한 장소와 함께 추천 시스템에서 활용될 예정입니다.
🔄 구현된 기능
1. 데이터베이스 스키마 변경
- 위치:
supabase/migrations/20240729000000_add_user_places_support.sql
- 변경사항:
places
테이블에user_id
와source
필드 추가- RLS 정책 추가로 유저별 접근 제어
- 인덱스 추가로 성능 최적화
2. 타입 정의 업데이트
- 위치:
app/types/database.types.ts
,app/types/forms/index.ts
- 내용:
- 데이터베이스 타입에
user_id
,source
필드 추가 - 유저 장소 등록을 위한 폼 타입 정의
- 데이터베이스 타입에
3. 서버 함수 구현
- 위치:
app/lib/user-places.server.ts
- 기능:
- 일일 등록 제한 체크 (하루 3개)
- 유저 장소 등록
- 유저 장소 목록 조회
- 유저 장소 삭제
- 이미지 업로드 처리
4. 라우트 구현
장소 등록 페이지 (/register-place
)
- 위치:
app/routes/register-place.tsx
- 기능:
- 일일 등록 제한 확인 (3개/일)
- 장소 정보 입력 폼
- 이미지 업로드 (1-3장)
- 태그 등록 (최대 5개)
내 장소 목록 페이지 (/my-places
)
- 위치:
app/routes/my-places.tsx
- 기능:
- 등록한 장소 목록 조회
- 장소 삭제 기능
- 카드 형태의 깔끔한 UI
5. UI 개선
메인 페이지 개선
- 위치:
app/routes/_index.tsx
- 변경사항:
- 메인 페이지 헤더에 "내 장소" 버튼 추가
- 로그인한 유저에게 쉬운 접근성 제공
- 인증 에러 처리 및 사용자 알림 추가
내 장소 페이지 UX 개선
- 위치:
app/routes/my-places.tsx
- 주요 기능:
- 조건부 UI: 장소가 없을 때와 있을 때 다른 인터페이스 제공
- 플로팅 액션 버튼: 장소가 1개 이상일 때 화면 우하단에 + 버튼 표시
- 모바일 최적화: 반응형 디자인으로 모바일/데스크톱 모두 지원
- 접근성: aria-label, 툴팁, 키보드 네비게이션 지원
- 애니메이션: 호버/클릭 효과로 자연스러운 인터랙션
🛡️ 보안 및 제한사항
Row Level Security (RLS) 정책
-- 유저는 자신이 등록한 장소만 삭제 가능
CREATE POLICY "Users can delete their own places" ON places FOR DELETE USING (
user_id = auth.uid() AND source = 'user'
);
-- 유저는 자신의 장소를 조회 가능
CREATE POLICY "Users can view their own places" ON places FOR SELECT USING (
user_id = auth.uid() OR is_active = true
);
-- 인증된 유저는 장소 등록 가능
CREATE POLICY "Authenticated users can insert places" ON places FOR INSERT WITH CHECK (
auth.uid() IS NOT NULL AND source = 'user'
);
등록 제한
- 일일 제한: 하루 최대 3개 장소만 등록 가능
- 이미지 제한: 최소 1장, 최대 3장까지 업로드
- 태그 제한: 최대 5개까지 등록 가능
📊 데이터 구조
확장된 places 테이블
-- 기존 필드들 +
user_id UUID REFERENCES auth.users(id), -- 등록한 유저 ID
source VARCHAR(10) -- 'admin' 또는 'user'
이미지 저장
- 저장소: Supabase Storage (
place-images
bucket) - 경로:
{user_id}/{timestamp}-{random}.{ext}
- 연결:
place_images
테이블로 관계 관리
🔧 기술적 구현 세부사항
1. 권한 분리 아키텍처
// 관리자 전용 함수 (admin.server.ts)
export async function getRegions(request: Request) {
const { supabase } = await requireAdmin(request) // 관리자 권한 필요
// ...
}
// 일반 사용자용 함수 (recommendation.server.ts)
export async function getRegions(request: Request) {
const supabase = createSupabaseServerClient(request) // 인증만 필요
// ...
}
2. 플로팅 액션 버튼 구현
// 조건부 렌더링으로 UX 최적화
{places.length > 0 && (
<div className="fixed bottom-6 right-6 z-50 group">
{/* 반응형 크기 조정 */}
<button className="w-14 h-14 sm:w-16 sm:h-16 ...">
{/* 애니메이션 효과 */}
<svg className="w-6 h-6 sm:w-7 sm:h-7 transition-transform">
</button>
{/* 접근성을 위한 툴팁 */}
<div className="absolute bottom-full right-0 mb-2 opacity-0 group-hover:opacity-100 ...">
</div>
)}
3. 에러 처리 개선
// 인증 에러 처리
export async function requireAuth(request: Request) {
try {
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
throw redirect(`${ROUTES.LOGIN}?redirectTo=${encodeURIComponent(redirectTo)}`)
}
return { user, supabase }
} catch (error) {
if (error instanceof Response) throw error // redirect 유지
throw redirect(`${ROUTES.HOME}?error=auth_failed`) // 일반 에러
}
}
🔄 향후 개발 계획
1. 추천 시스템 통합
- 유저 등록 장소를 기존 추천 알고리즘에 포함
- 가중치 조정 (관리자 vs 유저 등록 장소)
2. 승인 시스템
- 관리자가 유저 등록 장소를 검토/승인하는 기능
- 부적절한 장소 필터링
3. 커뮤니티 기능
- 다른 유저들의 리뷰/평점 시스템
- 등록된 장소 좋아요/유저 코스 북마크 기능
4. 통계 및 분석
- 인기 장소 분석
- 유저 등록 패턴 분석
🚨 해결된 문제들
1. 인증 관련 문제 해결
- 문제: 일반 유저가 장소 등록 시
/?error=unauthorized
에러 발생 - 원인:
register-place
라우트에서 관리자 전용 함수(admin.server.ts
) 사용 - 해결: 일반 유저용 함수를
recommendation.server.ts
로 분리 - 결과: 로그인한 모든 유저가 장소 등록 가능
2. Supabase Storage 설정 완료
- 문제: "Bucket not found" 에러로 이미지 업로드 실패
- 해결: Supabase Dashboard에서
place-images
버킷 생성 및 정책 설정 - 결과: 이미지 업로드 기능 정상 작동
3. 사용자 경험 개선
- 추가: 플로팅 액션 버튼으로 장소 추가 접근성 향상
- 개선: 조건부 UI로 상황에 맞는 인터페이스 제공
- 최적화: 모바일 친화적 반응형 디자인 적용
🚨 현재 제한사항
- 지도 API: 위치 검색 및 자동 좌표 설정 기능 미구현
- 이미지 최적화: 이미지 리사이징 및 압축 기능 없음
- 오프라인 지원: PWA 기능 미구현
🔧 설치 및 실행
1. 마이그레이션 적용
npx supabase db reset --linked
# 또는
npx supabase db push
2. 환경 변수 확인
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
3. 개발 서버 실행
npm run dev
📝 테스트 시나리오
1. 장소 등록 테스트
- 로그인 후 "장소 등록" 버튼 클릭
- 필수 정보 입력 (장소명, 카테고리, 지역, 주소, 설명)
- 이미지 1-3장 업로드
- 태그 입력 (쉼표로 구분)
- 등록 완료 확인
2. 일일 제한 테스트
- 하루에 3개 장소 등록
- 4번째 등록 시도 시 제한 메시지 확인
3. 내 장소 관리 테스트
- "내 장소" 페이지에서 등록한 장소 목록 확인
- 장소 삭제 기능 테스트
- 삭제 확인 모달 동작 확인
- 플로팅 + 버튼 동작 확인 (장소가 1개 이상일 때)
- 모바일 환경에서 터치 인터랙션 테스트
4. 사용자 경험 테스트
- 장소가 없을 때 "첫 장소 등록하기" 버튼 표시 확인
- 장소가 있을 때 플로팅 + 버튼 표시 확인
- 버튼 호버/클릭 애니메이션 동작 확인
- 툴팁 표시 확인
- 반응형 디자인 동작 확인 (모바일/태블릿/데스크톱)
🎯 성공 지표
- ✅ 유저 장소 등록 기능 구현 완료
- ✅ 일일 등록 제한 구현 (하루 3개)
- ✅ 이미지 업로드 기능 구현 (1-3장)
- ✅ 보안 정책 (RLS) 적용
- ✅ 사용자 친화적 UI/UX 제공
- ✅ 인증 문제 해결 및 접근성 개선
- ✅ 플로팅 액션 버튼으로 UX 향상
- ✅ 모바일 최적화 완료
- ✅ 에러 처리 및 사용자 피드백 개선
- ✅ 코드 정리 및 디버깅 로그 제거
반응형
'🧑💻 바이브 코딩' 카테고리의 다른 글
[바이브 코딩 #6] 데이트 코스 추천 서비스 개발기 (0) | 2025.06.29 |
---|---|
[바이브 코딩 #5] 데이트 코스 추천 서비스 개발기 (1) | 2025.06.28 |
[바이브 코딩 #3] 데이트 코스 추천 서비스 개발기 (2) | 2025.06.25 |
[바이브 코딩 #2] 데이트 코스 추천 서비스 개발기 (0) | 2025.06.23 |
[바이브 코딩 #1] 데이트 코스 추천 서비스 개발기 (6) | 2025.06.15 |