
Supabase로 백엔드를 처음 세팅하면서 진행 과정을 기록하려고 한다.
이 세팅들은커스텀 데일리 플래너 프로젝트에 적용할 것이다.각 단계별로 실제 화면 캡처와 함께 내가 한 설정을 정리해두었다.
먼저 Supabase 공식 사이트에 접속했다.
👉 https://supabase.com
오른쪽 상단 Sign up 버튼을 눌러 가입을 진행했다.
이메일, GitHub, Google 중 아무 방법으로 로그인해도 된다.
나는 GitHub 계정으로 바로 연결했다.
처음 로그인하면 Organization(조직 이름) 을 만들라고 나온다.
이건 “내 프로젝트들을 모아둘 팀 이름” 같은 개념이다.
나는 프로젝트 이름에 맞춰 CustomDailyPlanner 라고 지정했다.
가입이 끝나면 대시보드 상단에
Your organizations목록이 보이게 된다면 정상 ✅
💬 정리 메모
Organization은 팀 폴더 개념이라 프로젝트 여러 개를 모아둘 수 있다.- 개인 프로젝트라도 일관성 있게 이름을 지어두면 관리가 훨씬 편하다.
이제 첫 프로젝트를 만든다.
대시보드 우측 상단 「New Project」 버튼을 클릭한다.
방금 만든CustomDailyPlannerOrganization을 선택한다.
입력해야 할 항목은 다음과 같다 👇
항목 예시 값 설명 Project name custom-daily-planner-dev환경 구분( -dev,-prod)을 이름에 포함Database password 안알랴줌나중에 DB 연결 시 사용하므로 반드시 기록 Region ap-northeast-2 (Seoul)가장 가까운 리전 선택 Pricing Free무료 플랜으로 시작해도 충분 모두 입력한 뒤 「Create new project」 버튼 클릭.
몇 초 후 Supabase가 자동으로 데이터베이스를 초기화하고
대시보드로 이동한다.
💬 참고 팁
- 프로젝트 이름은 나중에 바꿀 수 있지만,
처음부터앱이름-환경명형태로 정하면 관리가 편하다.
(예:planner-dev,planner-prod)Database password는 잃어버리면 복구가 불가능하므로
꼭 비밀번호 관리앱(1Password, Bitwarden 등)에 저장한다.
(Project Settings → API)
이제 프론트엔드와 연결할 때 필요한 프로젝트 주소와 공개 키를 복사한다.
- 왼쪽 메뉴에서 ⚙️ Project Settings → API 클릭 (
Data API=Project URL/API Keys=anon public key & service_role key)- 아래 항목들을 찾는다 👇
항목 설명 Project URL Supabase API 요청의 기본 주소 anon public key 클라이언트에서 접근 가능한 공개 키 .
💬 주의
service_role key도 함께 보이지만,
이건 서버 전용이라 절대 프론트엔드에서 사용하면 안 된다.anon key는 프런트용이지만,
.env.local에 넣고 git에 커밋하지 말 것.
이 두 값은 이후
3️⃣ 프론트 연결(환경변수 설정) 단계에서 사용할 예정이다.
- Supabase 계정 및 Organization 생성 완료
- 새 프로젝트 생성 완료 (대시보드 확인)
- Project URL, anon key 복사 완료
Supabase의 인증(Auth) 기능을 활성화해
이메일/비밀번호로 회원가입하고 로그인할 수 있도록 설정한다.로그인 성공 후 돌아올 Redirect URL 설정까지 함께 진행했다.
Supabase 대시보드 왼쪽 메뉴에서
Authentication → Sign In / Providers 탭을 클릭 후.
맨 위의 User Signups 섹션에서
Allow new users to sign up옵션을 ON 으로 켰다.
이 스위치를 켜야 새 사용자가 회원가입을 할 수 있다.
그 아래의 Confirm email 옵션은
회원가입 시 이메일 인증 링크를 보내는 설정이다.
나는 초반 테스트를 위해 ON 상태 그대로 유지했다.
마지막으로 Save changes 버튼을 눌러 저장했다.
알림이 뜨면 정상 적용 완료 ✅
💬 용어 정리
- Provider
로그인 수단을 뜻한다.
이메일 외에도 Google, GitHub 같은 OAuth 로그인 방식이 포함된다.- Confirm Email
이 옵션을 켜면 회원가입 후 이메일로 발송된 링크를 눌러야
최종 로그인할 수 있다.
테스트 단계에서는 OFF로 두고 빠르게 개발용 로그인 테스트만 진행한다.
Email Provider를 클릭하면 세부 옵션을 설정할 수 있다.
대시보드에서 Auth Providers → Email →>클릭 후 확인.
항목 설명 내 설정 Enable Email Provider 이메일 로그인 기능 전체 ON/OFF ✅ ON Secure email change 이메일 변경 시 새 주소에서도 확인 필요 ✅ ON Secure password change 최근 로그인한 유저만 비밀번호 변경 가능 ❌ OFF (테스트 단계) Prevent leaked passwords 약하거나 유출된 비밀번호 차단 ❌ OFF (Pro 전용 기능) Minimum password length 최소 비밀번호 길이 6자 (기본값) Password Requirements 특수문자/대문자 요구 여부 기본(Default) Email OTP Expiration 인증 링크 만료 시간(초 단위) 3600초 (1시간) Email OTP Length OTP 자리 수 6자리 .
💬 정리 메모
Secure 옵션들은 보안 단계를 강화하는 기능이다.
초반에는 꺼두고, 실제 서비스 전환 시 다시 켜면 된다.OTP는 비밀번호 초기화나 인증 링크의 유효 시간에 영향을 준다.
이제 로그인 성공 후 돌아올 페이지를 지정한다.
Authentication → URL Configuration 메뉴로 이동.
아래 두 주소를 Redirect URLs에 추가했다 👇
환경 주소 개발용(Local) http://localhost:3000/auth/callback배포용(Prod) https://내-배포-도메인/auth/callback설정 후 Save 버튼 클릭 → “Settings saved” 메시지 확인.
⚠️ 주의
Redirect URL이 등록되지 않으면 로그인 직후
“Not allowed origin” 에러가 발생한다.- 개발/배포 환경 각각 반드시 등록해야 한다.
모든 설정이 완료됐으니 실제로 작동을 확인했다.
Authentication → Users 탭으로 이동해
테스트 방법은 2가지로 진행할 수 있다.
수동으로 유저를 추가하거나, 프론트엔드 로그인 폼에서 회원가입을 진행할 수 있다.
현재 프론트 UI가 안 만들어져 있다보니 수동으로 유저를 추가해야 했다.
🧩 수동으로 유저 추가하기 (Create new user)
화면 오른쪽 상단의 「Add user」 버튼을 클릭하고,
드롭다운에서 「Create new user」를 선택한다.
팝업 창이 뜨면 다음 항목을 입력한다 👇
항목 설명 Email address 테스트용 이메일 주소 입력 (예: test@example.com)User Password 간단한 테스트용 비밀번호 입력 (예: 12345678)Auto Confirm User 체크 ✅ (이메일 인증 없이 즉시 활성화) 💬 정리 메모
Auto Confirm User옵션을 켜면 인증 메일을 생략하고 즉시 로그인 가능하다.- 이 방식은 테스트용으로만 사용하고, 실제 운영 시엔 Confirm Email ON으로 전환해야 한다.
🧩 생성된 유저 정보 확인하기 (User Overview)
유저가 추가되면 목록에서 해당 이메일을 클릭해
상세 정보 패널(Overview) 을 열 수 있다.
여기서 확인할 수 있는 주요 정보 👇
항목 설명 UID 유저 고유 ID. 다른 테이블( profiles.user_id) FK로 연결할 때 사용Created at / Updated at 생성 및 수정 시각 Confirmed at 이메일 인증 완료 시각 (Auto Confirm이면 바로 채워짐) Provider Information 어떤 로그인 방식(Email/OAuth)으로 가입했는지 Reset password 수동으로 비밀번호 재설정 메일 전송 가능 Send magic link 비밀번호 없이 이메일 링크로 로그인하도록 전송 Danger zone 유저 삭제, 차단(Ban), MFA 해제 같은 관리 기능 제공 💬 정리 메모
UID는 Supabase에서 각 유저를 구분하는 식별자이자,
profiles 테이블과 연결되는 핵심 키다.Confirmed at이 비어 있으면 이메일 인증이 안 된 상태.
Auto Confirm으로 생성했다면 바로 채워진다.- “Send password recovery” / “Send magic link” 버튼은
실제 서비스 운영 시 비밀번호 분실 대응용으로 활용할 수 있다.
🧩 프론트엔드 로그인 폼으로 테스트하기 (추후)
UI가 완성되면
supabase.auth.signUp()또는signInWithPassword()메서드를 사용해
직접 로그인/회원가입 동작을 테스트할 수 있다.
그때는 Supabase Auth 클라이언트(createBrowserClient)의 상태 변화(onAuthStateChange)로
세션이 정상적으로 발급되는지 확인한다.
🧪 점검 체크리스트
테스트 항목 정상 조건 회원가입 후 Users 탭 새 유저가 목록에 표시됨 새로고침 후 상태 유지 auth.onAuthStateChange이벤트 정상 작동상세 정보 패널 UID, Provider, Confirmed at 정상 표시 로그인 후 세션 발급 (프론트에서 테스트 할 때) access_token,refresh_token생성 확인💬 테스트 팁
Confirm Email이 ON이라면 메일함에서 인증 링크를 눌러야 최종 로그인 가능.- OFF 상태에서는 바로 로그인돼서 테스트하기 편하다.
- 테스트 계정은 나중에 Delete user 버튼으로 간단히 제거할 수 있다.
- 이메일 로그인 Provider 활성화(Enabled)
Allow new users to sign upON- Redirect URLs(로컬/배포) 등록 완료
- 테스트 회원가입 및 로그인 성공
Supabase 대시보드에서 복사한 Project URL / anon key를
프론트엔드(.env)로 연결해createClient()가 정상 동작하는지 확인했다.
프로젝트 루트에
.env파일을 만들었다.
이 파일은 내 로컬에서만 쓰는 비밀값을 넣는 곳이며 git에 올리지 않는다.
.gitignore 확인
.gitignore에 아래 패턴이 포함되어 있는지 확인:.env .env.*
💬 메모- 이미
.env.local을 쓰고 있다면 그대로 사용해도 됨(Next.js 관례).- 윈도우/맥 동일하게 동작.
Supabase 대시보드 Project Settings → API에서 복사한 값을 입력했다.
# .env (또는 .env.local) NEXT_PUBLIC_SUPABASE_URL=https://<your-project-ref>.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-anon-public-key>💡 규칙
NEXT_PUBLIC_접두사는 브라우저에서 읽을 수 있는 공개 키 전용.- service_role key는 절대 넣지 말 것(서버용, 지금 단계에선 불필요).
- 값은 한 줄로 입력(개행/공백 섞이면 파싱 에러 발생).
.env에 넣은 값이 앱에서 정상 로딩되는지,
createClient()로 통신이 되는지,
auth.getSession()이 정상 응답을 주는지 확인했다.3-0 패키지 설치 (필수)
프로젝트 루트에서
@supabase/supabase-js를 설치한다.# 패키지 매니저에 맞게 하나만 실행 pnpm add @supabase/supabase-js # npm i @supabase/supabase-js # yarn add @supabase/supabase-js3-1) 클라이언트 파일 만들기
// lab/supabaseClient.ts import { createClient } from '@supabase/supabase-js'; export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! );3-2) 디버그 페이지에서 연결 체크
- App Router 예시 (
app/debug/page.tsx):// app/debug/page.tsx 'use client'; import { useEffect, useState } from 'react'; import { supabase } from '@/lab/supabaseClient'; export default function DebugPage() { const [state, setState] = useState<{ hasSession: boolean; error: any }>({ hasSession: false, error: null, }); useEffect(() => { (async () => { const { data, error } = await supabase.auth.getSession(); setState({ hasSession: !!data.session, error }); })(); // 세션 변화도 같이 로그 const { data: sub } = supabase.auth.onAuthStateChange((_evt, session) => { setState({ hasSession: !!session, error: null }); }); return () => sub.subscription.unsubscribe(); }, []); return ( <pre style={{ whiteSpace: 'pre-wrap' }}> {JSON.stringify(state, null, 2)} </pre> ); }
- Pages Router 예시 (
pages/debug.tsx):// pages/debug.tsx import { supabase } from '@/lab/supabaseClient'; export default function DebugPage() { const test = async () => { const { data, error } = await supabase.auth.getSession(); console.log({ hasSession: !!data.session, error }); }; // 간단 실행 test(); return <div>Check console & network</div>; }이제 서버를 실행해서 로컬
/debug페이지를 확인해보자!
이제 실제로 Supabase와 연결이 잘 되는지 확인했다.
로컬에서 Next.js 앱을 실행하고/debug페이지를 열어본다.
🧩 실행 명령어
pnpm dev # 또는 npm run dev / yarn dev서버가 뜨면 브라우저에서 http://localhost:3000/debug 로 이동한다.
🧠 결과 확인
현재는 로그인 기능을 아직 연결하지 않았기 때문에,
auth.getSession()의 결과는 아래처럼 나온다 👇{ "hasSession": false, "error": null }이건 정상 상태다 ✅
즉,
.env에 입력한 값이 잘 로딩되었고- Supabase 클라이언트가 서버와 정상 통신 중이며
- 아직 세션(로그인 상태)은 없다는 뜻이다.
🔐 나중에 로그인 기능을 연결하면?
다음 단계에서 로그인 기능(
supabase.auth.signInWithPassword)을 붙이고 나면,
로그인 후/debug페이지에서 아래처럼 바뀌게 된다 👇{ "hasSession": true, "error": null }
hasSession: true→ 로그인 성공 및 세션 유지 중- 새로고침을 해도 값이 그대로면 세션 유지 정상 동작
⚠️ 흔한 오류와 해결
현상 원인 해결 error: {... fetch failed ...}Project URL 오타 Supabase 대시보드에서 다시 복사 error: {... invalid key ...}anon key 줄바꿈/공백 한 줄로 입력 TypeError: Cannot read properties of undefined환경변수 미로딩 서버 재시작( Ctrl + C→pnpm dev)페이지 접속 시 404 디버그 파일 경로 오타 /app/debug/page.tsx위치 확인✅ 정상 완료 기준
/debug페이지 접속 시hasSession: false, error: null출력- 콘솔/네트워크 에러 없음
- dev 서버 정상 기동 (
ready - started server on 0.0.0.0:3000)
- .env은 절대 커밋 금지(리포지토리에 올리면 키 유출 위험).
- 협업자는 각자 로컬에 .env를 만든다(값은 노션 등으로 공유).
- 배포 환경(Vercel 등) 에서는 프로젝트 설정의 Environment Variables에
동일한 키를 등록한다
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY
💡 추가 팁- Prod와 Dev를 분리했다면, 배포 프로젝트에는 Prod용 값을 넣는다.
- Vercel에선 저장 후 Redeploy 해야 반영된다.
.env(또는.env.local) 생성 및.gitignore확인NEXT_PUBLIC_SUPABASE_URL/ANON_KEY입력/debug페이지에서getSession()호출 성공- 흔한 오류 없이 dev 서버 정상 기동
Supabase 데이터베이스에서 기본적으로 필요한 공통 자동 기능을 설정했다.
앞으로 만들 모든 테이블에 공통으로 적용될 기능으로,
새 레코드의id를 자동으로 랜덤 UUID로 생성하고
수정 시updated_at컬럼이 자동으로 갱신되도록 세팅했다.
Supabase 대시보드의 SQL Editor를 열고, 새 쿼리 탭(
+버튼)에서 아래 코드를 실행했다.
이 기능을 켜면 새 데이터가 추가될 때마다id값이 자동으로 랜덤 UUID로 채워진다.
(1, 2, 3...같은 숫자 자동 증가 대신, 고유한 문자열 ID가 생성됨)create extension if not exists pgcrypto;
SQL 실행 후 하단에 “Success. No rows returned” 메시지가 뜨면 정상 완료.
한 프로젝트(DB)당 한 번만 실행하면 된다.
💬 정리 메모
- 이제 테이블 생성 시 아래 구문을 사용할 수 있다.
id uuid default gen_random_uuid() primary keypgcrypto확장은UUID생성 기능을 제공하며, Supabase에 기본 내장돼 있다.- 한 번 실행해두면 이후 모든 테이블에서 사용할 수 있다.
이제 수정 시각(
updated_at)을 자동으로 갱신해주는 함수(Function) 를 만든다.
SQL Editor에서 새 탭(+)을 눌러 아래 코드를 실행한다.create or replace function public.set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at := now(); -- UTC 권장 (timestamptz) return new; end; 💲💲; -- 지금 여기 코드에서 이상해서 이모지로 대체했음실행 후 “Success. No rows returned” 메시지가 뜨면 정상적으로 생성된 것이다.
💬 정리 메모
now()는 UTC 기준으로 저장된다.- 한국 시간으로 표시할 때는 프론트엔드에서 변환하면 된다.
- 이 함수는 이후 각 테이블에서 트리거(trigger) 로 연결해 자동 실행된다.
profiles 자동 생성 함수 (이 부분 벡엔드 세팅 들어가기 2에 적어놓음)⚠️ 내가 모르고 글을 하나 더 써버림,,,, 아래 글보다는 자세한건 벡엔드 세팅 들어가기 2 첫부분을 보면 된다
profiles는 “로그인(회원가입) 시 자동 생성”을 전제로 설계했다.
그래서auth.users에 새로운 유저가 생성되는 순간,public.profiles에 1:1 프로필 row를 자동으로 만들어준다.
이 자동 생성이 보장돼야 이후 로직(예:profiles기반 이메일 중복 확인, 초기 설정 저장)이 안정적으로 굴러간다.
SQL Editor에서 새 쿼리 탭(+)을 열고 아래 코드를 실행한다.create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = public as $$ begin insert into public.profiles (id, email) values ( new.id, lower(new.email) ) on conflict (id) do nothing; return new; end; 💲💲; -- 이상해서 이모지로 대체
auth.users → profiles) (이 부분 벡엔드 세팅 들어가기 2에 적어놓음)⚠️ 내가 모르고 글을 하나 더 써버림,,,, 아래 글보다는 자세한건 벡엔드 세팅 들어가기 2 첫부분을 보면 된다
함수만 만들어서는 자동 생성이 실행되지 않는다.
auth.users에 유저가 추가되는 순간 위 함수를 실행하도록, after insert 트리거를 연결해야 한다.drop trigger if exists on_auth_user_created on auth.users; create trigger on_auth_user_created after insert on auth.users for each row execute function public.handle_new_user();📌 확인 포인트
- 이 트리거는
auth.users에 새 row가 생길 때마다 실행된다.- 결과적으로
public.profiles에 동일한 id(= auth.users.id) 로 1행이 자동 생성된다.- 이미 프로필이 존재하면
on conflict (id) do nothing때문에 안전하게 스킵된다.
이제 위에서 만든
set_updated_at()함수를 테이블에 연결했다.
레코드가 수정될 때마다updated_at컬럼이 자동으로 갱신되도록 설정한다.
아래는todos와profiles테이블에 적용한 예시다.
4-1) 테스트용 테이블 생성
트리거를 연결하려면 테이블이 먼저 존재해야 한다.
아직todos나profiles가 없다면, 새 탭(+)을 열고 아래 예시를 SQL Editor에 붙여넣어 실행한다.⚠️ 주의
테이블은 한번 만들어지면 나중에 SQL에서 수정 작업을 해야한다.
수정을 하려면SQL Editor에서 수정 코드를 통해 테이블을 수정하거나Table Editor에서 테이블을 수정해야만 한다 컬럼 하나만 바꾼다고 테이블 컬럼이 바뀌지 않는다!-- profiles 테이블 create table if not exists public.profiles ( id uuid primary key default gen_random_uuid(), email text unique, nickname text, avatar_url text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- todos 테이블 create table if not exists public.todos ( id uuid primary key default gen_random_uuid(), user_id uuid not null, title text not null, is_done boolean not null default false, due_date date, created_at timestamptz not null default now(), updated_at timestamptz not null default now() );
4-2) 트리거 연결
이제 위에서 만든 함수 set_updated_at()을 테이블에 연결한다.
새 탭(+)을 하나 더 열고 아래 코드를 실행한다.-- todos 테이블 트리거 연결 drop trigger if exists trg_set_timestamp_todos on public.todos; create trigger trg_set_timestamp_todos before update on public.todos for each row execute function public.set_updated_at(); -- profiles 테이블 트리거 연결 drop trigger if exists trg_set_timestamp_profiles on public.profiles; create trigger trg_set_timestamp_profiles before update on public.profiles for each row <execute function public.set_updated_at();실행 후 “Success. No rows returned” 메시지가 뜨면 완료.
각 테이블마다 한 번씩만 연결해두면 된다.
💬 정리 메모
- 테이블이 없으면
relation "public.todos" does not exist에러가 발생한다.
→ 트리거는 항상 테이블 생성 후 연결해야 한다.- 앞으로 추가될 테이블(
events_daily,events_weekly,notes,habits,dashboard_layouts)에도 동일한 방식으로 연결 예정이다.- 트리거 이름은 각 테이블별로 다르게 지정한다.
(예:trg_set_timestamp_<테이블명>)
이제 자동 갱신이 잘 되는지 테스트를 진행했다.
SQL Editor에서는 로그인 컨텍스트가 없어서auth.uid()가null이 된다.
따라서 실제 유저 UID를 가져와서user_id에 넣는 방식으로 테스트했다.
- 0️⃣ 테스트용 UID 하나 가져오기 (Users에 계정이 있어야 함)
↑ 결과로 나온 id 값을 아래 1️⃣에 붙여 넣는다.-- 1️⃣ 새 데이터 추가 (실제 UID 사용) insert into public.todos (id, user_id, title) values (gen_random_uuid(), '여기에_위_쿼리에서_가져온_UID', 'trigger-test'); -- 2️⃣ 제목 수정 update public.todos set title = 'trigger-test-updated' where title = 'trigger-test'; -- 3️⃣ 결과 확인 select title, created_at, updated_at from public.todos order by updated_at desc limit 1;✅ 기대 결과(정상 동작)
3️⃣ 결과 확인쿼리에서 맨 위 1행이 다음처럼 보여야 정상title created_at updated_at -------------------------------------------------------------- trigger-test-updated 2025-10-10 02:40:12+00 2025-10-10 02:41:05+00
판단 포인트
title이trigger-test-updated로 바뀌어 있다.updated_at이created_at보다 늦은 시각으로 찍힌다(보통 몇 초~몇 십 초 차이).- 시간대는 UTC(+00) 로 보이는 게 정상(Supabase 기본).
pgcrypto확장 활성화 완료set_updated_at()함수 생성 완료- 트리거 연결 완료 (
todos,profiles)updated_at자동 갱신 확인 (실제 UID로 테스트 통과)
이번 단계에서 실제 서비스용 최종 스키마로 테이블들을 정리한다.
이전 단계에서 만들었던 테스트용profiles,todos정의는 이 스키마로 교체한다.
기존에 연결해 둔 트리거와 트리거테스트도 먼저 삭제(drop)한 뒤 재생성해 중복을 방지한다.
- Supabase 대시보드 → Database → SQL Editor 이동
- 좌측 PRIVATE 목록에서 전에 저장해 둔 테이블 스크립트(예:
Profiles and Todos Tables)을 클릭해 열기- 같은 파일 안에서 테이블 정의를 수정하고, 우측 상단 Run으로 실행
- 하단에 Success ✅ 메시지 확인
- 변경 이력을 남기고 싶다면 상단 Save로 같은 파일 저장 또는 새 이름(예:
v02_create_tables.sql)으로 저장
- 팀 작업이라면 버전 넘버를 올려 저장하는 걸 추천
📘 참고
새로+ New query로 탭을 늘리는 대신, 기존에 쓰던 테이블 스크립트를 수정/재사용하면 이력 관리가 더 깔끔하고, 실수(중복 생성/충돌)도 줄일 수 있다.
모든 테이블은 아래 공통 컬럼을 포함한다
id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, created_at timestamptz not null default now(), updated_at timestamptz not null default now()
- id: 고유 식별자 (UUID 자동 생성)
- user_id: 로그인 사용자와 연결(FK)
- created_at: 생성 시각
- updated_at: 수정 시각(트리거 함수
set_updated_at()로 자동 갱신)예외:
profiles는user_id대신id가 곧auth.users(id)를 참조한다(1:1 매칭).
✅ 규칙을 통일하면 생기는 장점
- 모든 테이블에서 공통 속성(
created_at,updated_at) 처리 방식이 동일.- 프론트엔드에서 타입 추론, 캐싱 로직 통합 가능.
- RLS(권한 정책) 작성이 쉬워진다 (
user_id = auth.uid()통일).
사용자 정보와개인 설정을 저장하는 테이블이다.
로그인한 유저가 자동 생성되며, 닉네임·아바타·환경설정 값 등을 저장한다.create table if not exists public.profiles ( id uuid primary key references auth.users(id) on delete cascade, -- ✅ 중복 확인/표준 프로필 정보 email text not null, name text, profile_image_url text, -- ✅ 개인 설정 settings jsonb not null default '{}'::jsonb, -- ✅ 시간 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 이메일 대소문자 무시 유니크 (중복 가입 방지의 핵심) create unique index if not exists profiles_email_unique_lower on public.profiles (lower(email)); -- ✅ updated_at 자동 갱신 트리거(너가 쓰던 방식 유지) drop trigger if exists trg_set_timestamp_profiles on public.profiles; create trigger trg_set_timestamp_profiles before update on public.profiles for each row execute function public.set_updated_at();📌 확인 포인트
profiles는 공통 규칙의 예외 테이블이다. 일반 테이블처럼user_id컬럼을 두지 않고,id자체가auth.users.id를 참조하는 PK이자 FK로 동작한다.
→ 한 사용자당 프로필은 항상 1행(1:1 매칭)만 존재한다.
대소문자 차이로 인한 중복 가입을 방지하기 위해lower(email)기준의 유니크 인덱스(profiles_email_unique_lower)를 사용한다.
profiles는 로그인(회원가입) 시 자동 생성되는 테이블을 전제로 설계된다.
이 동작은auth.users에 대한 트리거로 보장되며, 프로필 생성 누락이 발생하지 않아야profiles기반 중복 확인이 정확해진다.
settings는 사용자별 환경설정 값을 저장하는jsonb컬럼이다.
테마, 언어, 모듈 순서 등 구조가 자주 바뀌는 개인 설정을 유연하게 관리하기 위해 사용한다.
created_at,updated_at은 전 테이블 공통 규칙을 그대로 따른다.
updated_at은set_updated_at()트리거에 의해 수정 시 자동 갱신되어야 한다.
일간 할 일을 저장하는 메인 데이터 테이블이다.
create table if not exists public.todos ( -- ✅ 공통 규칙: 테이블 고유 PK (자동 UUID) id uuid primary key default gen_random_uuid(), -- ✅ 공통 규칙: 소유자 FK (RLS에서 user_id = auth.uid()로 통일) user_id uuid not null references auth.users(id) on delete cascade, -- ✅ 할 일 기본 정보 title text not null, description text, -- ✅ 상태: open/done만 허용 (체크 제약으로 값 고정) status text not null default 'open' check (status in ('open','done')), -- ✅ 우선순위: 1(높음) ~ 5(낮음) 같은 규칙으로 쓰기 좋음 (기본 3) priority smallint default 3, -- ✅ 마감일 / 완료 시각 due_date date, completed_at timestamptz, -- ✅ 공통 규칙: 생성/수정 시각 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 공통 규칙: updated_at 자동 갱신 트리거 drop trigger if exists trg_set_timestamp_todos on public.todos; create trigger trg_set_timestamp_todos before update on public.todos for each row execute function public.set_updated_at();📌 확인 포인트
title은 필수,description은 선택.status는open/done만 허용.- 완료 시
completed_at기록.- 수정 시
updated_at자동 갱신 확인.
하루 단위 일정을 관리한다.
create table if not exists public.events_daily ( -- ✅ 공통 규칙: 테이블 고유 PK (자동 UUID) id uuid primary key default gen_random_uuid(), -- ✅ 공통 규칙: 소유자 FK (RLS에서 user_id = auth.uid()로 통일) user_id uuid not null references auth.users(id) on delete cascade, -- ✅ 일정 기본 정보 title text not null, notes text, -- ✅ 종일 일정 여부 all_day boolean not null default false, -- ✅ 일정 시간 범위 (시작은 필수, 종료는 선택) -- start_ts: 일정 시작 시각 -- end_ts: 일정 종료 시각(없으면 start_ts 기준 단일 시점 일정으로 취급 가능) start_ts timestamptz not null, end_ts timestamptz, -- ✅ 공통 규칙: 생성/수정 시각 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 공통 규칙: updated_at 자동 갱신 트리거 drop trigger if exists trg_set_timestamp_events_daily on public.events_daily; create trigger trg_set_timestamp_events_daily before update on public.events_daily for each row execute function public.set_updated_at();📌 확인 포인트
all_day = true이면 시작·종료 시간을 무시하고 하루 전체 일정으로 처리.start_ts/end_ts는 UTC 기준 저장.
요일 기반의 반복 일정을 관리한다.
create table if not exists public.events_weekly ( -- ✅ 공통 규칙: 테이블 고유 PK (자동 UUID) id uuid primary key default gen_random_uuid(), -- ✅ 공통 규칙: 소유자 FK (RLS에서 user_id = auth.uid()로 통일) user_id uuid not null references auth.users(id) on delete cascade, -- ✅ 주간 일정 기본 정보 title text not null, notes text, -- ✅ 기준 주(week) 정보 -- week_start: 해당 주의 시작일(예: 월요일). 이 값을 기준으로 같은 주를 그룹핑한다. week_start date not null, -- ✅ 요일 + 시간 정보 -- day_of_week: 요일(정수). 프로젝트 규칙을 반드시 고정해두는 게 중요하다. -- 예: 0=일 ~ 6=토 (또는 1=월 ~ 7=일) 중 하나로 통일 필요 day_of_week int not null, -- start_time / end_time: 그 날의 시간 범위 (주간 템플릿/반복 일정에 적합) start_time time not null, end_time time not null, -- ✅ 공통 규칙: 생성/수정 시각 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 공통 규칙: updated_at 자동 갱신 트리거 drop trigger if exists trg_set_timestamp_events_weekly on public.events_weekly; create trigger trg_set_timestamp_events_weekly before update on public.events_weekly for each row execute function public.set_updated_at();📌 확인 포인트
week_start: 해당 주 시작일(보통 월요일).day_of_week: 1~7 (월~일).- 반복 일정 관리에 활용 가능.
create table if not exists public.events_monthly ( -- ✅ 공통 규칙: 테이블 고유 PK (자동 UUID) id uuid primary key default gen_random_uuid(), -- ✅ 공통 규칙: 소유자 FK (RLS에서 user_id = auth.uid()로 통일) user_id uuid not null references auth.users(id) on delete cascade, -- ✅ 월간 일정 기본 정보 title text not null, notes text, -- ✅ 날짜(월간 캘린더용 분해 컬럼) -- year/month/day를 분리해두면 "YYYY-MM" 단위 조회/그룹핑이 쉬워진다. year int not null, month int not null check (month between 1 and 12), day int not null check (day between 1 and 31), -- ✅ 시간(선택) -- start_time/end_time이 null이면 '하루 종일/시간 미정' 일정으로 취급 가능 start_time time, end_time time, -- ✅ 공통 규칙: 생성/수정 시각 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 공통 규칙: updated_at 자동 갱신 트리거 drop trigger if exists trg_set_timestamp_events_monthly on public.events_monthly; create trigger trg_set_timestamp_events_monthly before update on public.events_monthly for each row execute function public.set_updated_at();📌 확인 포인트
year + month + day로 날짜 관리.- 월별 달력에서 표시되는 일정 데이터를 저장.
사용자의 루틴 및 습관 데이터를 저장한다.
create table if not exists public.habits ( -- ✅ 공통 규칙: 테이블 고유 PK (자동 UUID) id uuid primary key default gen_random_uuid(), -- ✅ 공통 규칙: 소유자 FK (RLS에서 user_id = auth.uid()로 통일) user_id uuid not null references auth.users(id) on delete cascade, -- ✅ 습관 기본 정보 name text not null, -- ✅ 반복 주기 -- daily: 매일 -- weekly: 매주 -- custom: 커스텀 규칙(예: 주 3회, 격일 등) → 추후 별도 필드/테이블로 확장 가능 frequency text not null check (frequency in ('daily','weekly','custom')), -- ✅ 기간당 목표 횟수(예: daily=1회, weekly=3회 등) target_per_period int default 1, -- ✅ UI 표시용 컬러(토큰/HEX 등 자유롭게 사용) color text, -- ✅ 공통 규칙: 생성/수정 시각 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 공통 규칙: updated_at 자동 갱신 트리거 drop trigger if exists trg_set_timestamp_habits on public.habits; create trigger trg_set_timestamp_habits before update on public.habits for each row execute function public.set_updated_at();📌 확인 포인트
frequency: daily/weekly/custom 중 선택.color: 습관별 구분 색상 코드 저장.- 추후 달성률 계산 및 리마인더 기능과 연결될 예정.
간단한 텍스트 메모를 저장한다.
create table if not exists public.notes ( -- ✅ 공통 규칙: 테이블 고유 PK (자동 UUID) id uuid primary key default gen_random_uuid(), -- ✅ 공통 규칙: 소유자 FK (RLS에서 user_id = auth.uid()로 통일) user_id uuid not null references auth.users(id) on delete cascade, -- ✅ 노트 기본 정보 -- title: 선택(없으면 내용 기반으로 리스트에서 요약 표시 가능) title text, content text not null, -- ✅ 태그(다중) -- text[]로 간단히 시작하고, 추후 검색/분석이 필요하면 별도 tags 테이블로 확장 가능 tags text[] not null default '{}'::text[], -- ✅ 공통 규칙: 생성/수정 시각 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 공통 규칙: updated_at 자동 갱신 트리거 drop trigger if exists trg_set_timestamp_notes on public.notes; create trigger trg_set_timestamp_notes before update on public.notes for each row execute function public.set_updated_at();📌 확인 포인트
content는 필수.tags는 배열 형태(예:['idea','work']).- 추후 검색 기능에서 활용 예정.
사용자가 설정한 모듈 위치 및 상태를 저장한다.
create table if not exists public.dashboard_layouts ( -- ✅ 공통 규칙: 테이블 고유 PK (자동 UUID) id uuid primary key default gen_random_uuid(), -- ✅ 공통 규칙: 소유자 FK (RLS에서 user_id = auth.uid()로 통일) user_id uuid not null references auth.users(id) on delete cascade, -- ✅ 대시보드 레이아웃 데이터(모듈 배치/사이즈/순서 등) -- jsonb로 저장해 유연하게 구조 변경 가능 layout jsonb not null, -- ✅ 활성 레이아웃 여부 -- 유저가 여러 레이아웃(프리셋/히스토리)을 가질 수 있어 true 하나만 활성화되도록 설계 가능 is_active boolean not null default true, -- ✅ 레이아웃 스키마/구조 버전 -- 레이아웃 구조가 변경될 때 마이그레이션/호환 처리에 사용 version int not null default 1, -- ✅ 공통 규칙: 생성/수정 시각 created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- ✅ 공통 규칙: updated_at 자동 갱신 트리거 drop trigger if exists trg_set_timestamp_dashboard_layouts on public.dashboard_layouts; create trigger trg_set_timestamp_dashboard_layouts before update on public.dashboard_layouts for each row execute function public.set_updated_at();📌 확인 포인트
layout: 모듈 위치 및 구성(JSON).is_active = true는 유저당 하나만 유지 (추후 인덱스로 제약 예정).
테이블 생성이 모두 완료됐는지, 그리고 각 테이블이 정상 작동하는지 두 가지 방법으로 점검했다.
1️⃣ 테이블 존재 여부 확인
2️⃣ 각 테이블의 행 수 확인⚠️ 실행 방법 주의
SQL Editor는 마지막SELECT만 결과창에 표시된다.
① 존재 확인과 ② 행 수 확인은 각각 따로 실행해야한다!.
방법:
1. 쿼리를 별도 탭/파일로 나눠 실행, 또는
2. 실행할 구문만 드래그 선택 → Run.
1️⃣ 테이블 존재 여부 확인
select to_regclass('public.profiles') as profiles, to_regclass('public.todos') as todos, to_regclass('public.events_daily') as events_daily, to_regclass('public.events_weekly') as events_weekly, to_regclass('public.events_monthly') as events_monthly, to_regclass('public.habits') as habits, to_regclass('public.notes') as notes, to_regclass('public.dashboard_layouts') as dashboard_layouts;📌 확인 포인트
- 결과값이
public.테이블명이면 정상 생성됨.NULL이 나온다면 아직 생성되지 않은 테이블이 있음.
2️⃣ 각 테이블의 행 수 확인
select 'profiles' as table, count(*) as rows from public.profiles union all select 'todos', count(*) from public.todos union all select 'events_daily', count(*) from public.events_daily union all select 'events_weekly', count(*) from public.events_weekly union all select 'events_monthly', count(*) from public.events_monthly union all select 'habits', count(*) from public.habits union all select 'notes', count(*) from public.notes union all select 'dashboard_layouts', count(*) from public.dashboard_layouts;📌 확인 포인트
- 각 테이블의
rows값이 0 이상이면 정상 (데이터는 없어도 괜찮음).- 이후 테스트 데이터를 넣으면
updated_at자동 갱신이 정상 작동하는지도 함께 확인.
- 기존 스크립트 파일에서 테이블 정의를 수정 → 실행 완료
- 테스트 스키마를 최종 스키마로 교체
- 기존 트리거 삭제 후 재생성 완료
- Smoke Test 통과 (
updated_at자동 변경 확인)