Skip to content

refactor: 인증 시스템을 Supabase Auth로 통합 (#45) - #47

Merged
Siul49 merged 3 commits into
devfrom
feature/45-supabase-auth-integration
Jun 11, 2026
Merged

refactor: 인증 시스템을 Supabase Auth로 통합 (#45)#47
Siul49 merged 3 commits into
devfrom
feature/45-supabase-auth-integration

Conversation

@kokkumong

Copy link
Copy Markdown
Collaborator

🚀 작업 내용 (What)

  • 이메일/비밀번호 회원가입·로그인을 자체 JWT+Prisma에서 Supabase Auth로 이전
  • me·logout을 Supabase 세션 기반으로 교체 (getSession, signOut)
  • 닉네임 로그인은 admin 클라이언트로 이메일 조회 후 signInWithPassword
  • 중복된 자체 카카오 OAuth 라우트 제거 (로그인 화면은 이미 Supabase 카카오 OAuth 사용)
  • profiles 확장 마이그레이션 추가 — 가입 부가정보(전화·약관 동의 등)를 트리거로 자동 반영
  • 신규: lib/supabase/admin.ts (service role 클라이언트)
  • 제거: /api/auth/kakao/*, lib/auth/{jwt,kakao,password}.ts, .env.example의 JWT/카카오 키

📣 핵심 변경 이유 (Why)

  • 인증이 두 갈래(미들웨어·로그인=Supabase / API=자체 JWT+Prisma)로 분리돼 세션·사용자 식별이 일관되지 않았음
  • Supabase 단일 세션 체계로 통합해 일관성 확보 ([CHORE] 전체 사용자 작업 흐름 통합 점검 #44 점검에서 발견된 구조적 문제)

📸 스크린샷 (Visuals, 선택)

  • 해당 없음 (백엔드 인증 구조 변경, UI 변경 없음)

⚠️ 체크리스트 (Checklist)

  • 브랜치 컨벤션(feature/00-name)을 지켰나요?
  • 커밋 컨벤션(feat:, fix: 등)을 지켰나요?
  • 작업 전에 관련 이슈를 생성하고 연결했나요?
  • 내 코드가 팀의 기존 코드를 망가뜨리지 않았는지 확인했나요? (타입체크·lint·단위테스트 191개 통과)

⚠️ 검증 한계: 정적 검증(타입체크·lint·테스트)은 통과했으나, 실제 로그인 동작(E2E)은 실 Supabase 프로젝트 + OAuth 프로바이더 설정 후 검증 필요. 이메일 확인(Confirm email) 설정 여부도 결정 필요.

🔗 관련 이슈 (Issue)

Close #45

- 이메일/비번 회원가입·로그인을 자체 JWT+Prisma에서 Supabase Auth로 이전
- me·logout을 Supabase 세션 기반으로 교체
- 중복된 자체 카카오 OAuth 라우트 제거 (로그인 화면은 Supabase 카카오 OAuth 사용)
- profiles 확장 마이그레이션 추가 (가입 부가정보 트리거 반영)
- 제거: /api/auth/kakao/*, lib/auth/{jwt,kakao,password}.ts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kokkumong, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 46 minutes and 24 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d787bc6f-b005-43c9-a906-84d8ca9df815

📥 Commits

Reviewing files that changed from the base of the PR and between 09b853b and e3c3419.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json, !package-lock.json
📒 Files selected for processing (4)
  • src/app/api/auth/login/route.ts
  • src/app/api/auth/logout/route.ts
  • src/app/api/auth/me/route.ts
  • src/app/api/auth/signup/route.ts

Walkthrough

Supabase Auth 통합을 위해 프로필 테이블 스키마 확장, 관리자 클라이언트 인프라 추가, 그리고 로그인·로그아웃·프로필·회원가입 엔드포인트를 JWT+Prisma 기반에서 Supabase Auth 기반으로 마이그레이션합니다. 레거시 JWT·카카오·bcrypt 코드는 제거됩니다.

Changes

Supabase Auth 통합

Layer / File(s) Summary
프로필 테이블 확장 및 트리거 함수 업데이트
supabase/migrations/20260607000000_profiles_auth_fields.sql
public.profiles에 전화번호·연령·동의 정보 컬럼 추가 및 유니크 인덱스 생성. handle_new_user() 트리거가 Supabase Auth signup 메타데이터(raw_user_meta_data)를 읽어 프로필을 채웁니다.
Supabase Admin 클라이언트 인프라
src/lib/supabase/admin.ts
SUPABASE_SERVICE_ROLE_KEY 기반 관리자 클라이언트를 캐싱 제공. 로그인·회원가입 시 프로필 테이블 직접 조회에 사용됩니다.
로그인 엔드포인트 마이그레이션
src/app/api/auth/login/route.ts
Prisma+bcrypt에서 Supabase Auth signInWithPassword로 전환. 닉네임 로그인은 Admin 클라이언트로 이메일 먼저 조회 후 인증. 응답을 id·email만 반환하도록 단순화.
프로필 조회 엔드포인트 마이그레이션
src/app/api/auth/me/route.ts
JWT 검증에서 getSession()으로 전환하여 세션 기반 인증. 프로필 테이블 조회 후 폴백 규칙(이메일은 세션, 닉네임·전화번호 미제공은 null)으로 응답 구성.
로그아웃 엔드포인트 마이그레이션
src/app/api/auth/logout/route.ts
쿠키 수동 삭제에서 Supabase Auth signOut()으로 전환하여 서버 세션 명시적 종료.
회원가입 엔드포인트 마이그레이션
src/app/api/auth/signup/route.ts
Prisma+bcrypt 사용자 생성에서 Supabase Auth signUp으로 전환. options.data로 프로필 메타데이터 전달하여 트리거 처리. 에러 메시지로 409(중복)·400(기타) 구분.
환경 변수 설정 업데이트
.env.example
JWT_SECRET·카카오 관련 변수 제거. Supabase Auth 공급자 설정 가이드 추가.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

  • Siul49/moim#29: PR #29의 카카오 소셜 로그인 흐름(src/lib/auth/kakao.ts, Kakao 콜백/로그인 라우트)이 본 PR에서 완전 제거되는 대상입니다.

Suggested labels

refactor


핵심 검토 포인트

1. 마이그레이션 완결성

  • Admin 클라이언트의 SUPABASE_SERVICE_ROLE_KEY 누락 시 예외 처리가 단단한가?
  • 로그인 흐름에서 닉네임→이메일 조회 실패 시 401 메시지가 일관되는가? (동일한 "인증 실패" 메시지로 정보 유출 방지)
  • 회원가입 후 프로필이 비동기 트리거로 생성되는데, 즉시 응답이 정상인가? (트리거가 handle_new_user()로 동기 처리)

2. 데이터 모델 정합성

  • raw_user_meta_data 캐스팅에서 불리언/타임스탬프 기본값 처리가 누락된 경우는? (migration에서 false·NULL 명시 필요)
  • 프로필 조회(me) 시 phone_number가 NULL이어도 API 응답 스키마가 일관되는가?

3. 역호환성 & 레거시 코드

  • Kakao 콜백 라우트와 JWT 모듈이 제거되는데, 여전히 참조하는 곳이 있나? (전역 검색 필수)
  • 기존 JWT 토큰으로 인증된 세션이 남아 있는 경우는? (브라우저 쿠키 kakao_oauth_state 등 정리 필요)

4. 보안 관점

  • Admin 클라이언트 생성 시 autoRefreshToken: false, persistSession: false 설정이 맞는가? (서버 인스턴스별 격리 보장)
  • signUp 실패 시 메시지에서 "이미 사용 중"이 정규식으로만 판별되는데, 정규식이 Supabase 오류 문자열과 안정적으로 매칭되나?
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 'refactor:' 접두사로 시작하며 작업 내용(Supabase Auth 통합)을 명확히 설명하고 이슈 번호(#45)를 포함함.
Description check ✅ Passed PR 설명이 변경사항, 핵심 이유, 체크리스트, 관련 이슈를 포함하며 changeset과 명확히 연관됨.
Linked Issues check ✅ Passed 변경사항이 이슈 #45의 모든 주요 목표를 충족: 자체 JWT+Prisma 제거, Supabase Auth 통합, 세션 일관성 확보, 카카오 OAuth 루트 제거, profiles 마이그레이션 추가.
Out of Scope Changes check ✅ Passed 모든 변경사항이 인증 시스템 Supabase 통합 범위 내: 불필요한 라이브러리 업그레이드·문서 변경·무관한 기능 추가 없음.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/45-supabase-auth-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

package.json과 lock 파일 불일치로 CI의 `npm ci`가 실패하던 문제 해결.
@testing-library/dom 등 누락된 peer/전이 의존성을 lock에 반영.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/auth/login/route.ts`:
- Around line 39-46: The admin profile lookup ignores the possible error from
admin.from("profiles").select(...).maybeSingle(), causing DB/RLS failures to be
masked as auth 401s; update the code in the createAdminClient /
admin.from("profiles").select("email").eq("nickname", loginId).maybeSingle()
flow to check the returned error, log it (including error details), and either
throw or return a 500 response instead of treating email as null; ensure the
variable email is only set when no error and profile exists, and propagate or
rethrow the error so calling code doesn't incorrectly convert server errors into
authentication failures.
- Around line 55-66: The login flow currently only checks error and data.user
after calling createClient() and supabase.auth.signInWithPassword, but must also
verify data.session to fail when email confirmation prevents session creation;
update the conditional in route.ts (the signInWithPassword handling block) to
treat error || !data.user || !data.session as authentication failure and return
the existing NextResponse.json with AUTH_FAIL_MESSAGE, and add a
unit/integration test that mocks signInWithPassword returning data.session =
null to assert the 401/failed response.

In `@src/app/api/auth/logout/route.ts`:
- Around line 6-13: The POST handler calls createClient() and invokes
supabase.auth.signOut() but ignores its result; update the POST function to
capture the signOut response, check the returned error, and only return 200 when
signOut succeeded or when the error indicates "no active session"/"invalid
session" (treat as success), otherwise return a 500 JSON response with the error
message; reference the POST function, the createClient() call, and
supabase.auth.signOut() so you locate and change the response logic to
conditionally use NextResponse.json with status 200 or 500 based on the error.

In `@src/app/api/auth/me/route.ts`:
- Around line 18-23: The Supabase query result is ignoring the returned error
and only destructuring data (profile); update the block after calling
createClient() and the supabase.from("profiles").select(...).maybeSingle() call
to also destructure and check the returned error, and if error is present log
the error (include error.message and details) and return a 500 HTTP response
instead of continuing with an undefined profile; ensure the fix references
createClient, supabase, .from("profiles").select(...).maybeSingle(), and the
profile variable so the query error path cleanly logs and returns 500.

In `@src/app/api/auth/signup/route.ts`:
- Around line 85-94: The handler currently returns success with a user payload
even when data.user is null (variable user), which can cause NPEs on the client;
change the response logic in signup/route.ts to check if data.user is null and,
if so, return a clear response like success: true/false with message "이메일 인증이
필요합니다" (or similar) and omit user.id access (or return user: null) so clients
know to wait for email confirmation; update the NextResponse.json call that
currently builds { success, message, user: user ? { id: user.id, email:
user.email, nickname } : null } to branch on user === null and return the
explicit confirmation-required message and appropriate status.
- Around line 71-82: Replace the fragile regex-only status decision in the
signup error handler by first casting/inspecting the thrown error for a
structured status (e.g., check err?.status) and only fall back to the current
regex on err?.message; extract this logic into a small helper (e.g.,
determineSignupErrorStatus) referenced from the route's error branch that
returns 409 for duplicates and 400 otherwise, keep the existing localized
messages and NextResponse.json call using the computed status, and add a Vitest
unit test asserting that structured 409 errors produce 409 and that messages
matching duplicate patterns also produce 409 while all other errors produce 400.

In `@src/lib/supabase/admin.ts`:
- Around line 12-33: The admin Supabase client lacks the Database generic so
query types aren't checked; generate the Supabase types (e.g., run `supabase gen
types typescript` to produce a Database type file) and then update
createAdminClient: import the generated Database type (e.g., Database from
'`@/types/supabase`'), change the cached variable and function signature to use
SupabaseClient<Database>, and call createClient<Database>(...) so the returned
client and all queries like from("profiles").select(...) are type-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1e28a062-4021-4f6e-bd44-b408f86b5aec

📥 Commits

Reviewing files that changed from the base of the PR and between e40c252 and 09b853b.

📒 Files selected for processing (12)
  • .env.example
  • src/app/api/auth/kakao/callback/route.ts
  • src/app/api/auth/kakao/login/route.ts
  • src/app/api/auth/login/route.ts
  • src/app/api/auth/logout/route.ts
  • src/app/api/auth/me/route.ts
  • src/app/api/auth/signup/route.ts
  • src/lib/auth/jwt.ts
  • src/lib/auth/kakao.ts
  • src/lib/auth/password.ts
  • src/lib/supabase/admin.ts
  • supabase/migrations/20260607000000_profiles_auth_fields.sql
💤 Files with no reviewable changes (5)
  • src/lib/auth/kakao.ts
  • src/lib/auth/password.ts
  • src/lib/auth/jwt.ts
  • src/app/api/auth/kakao/login/route.ts
  • src/app/api/auth/kakao/callback/route.ts

Comment thread src/app/api/auth/login/route.ts
Comment thread src/app/api/auth/login/route.ts
Comment on lines 6 to 13
export async function POST() {
const res = NextResponse.json(
const supabase = await createClient();
await supabase.auth.signOut();

return NextResponse.json(
{ success: true, message: "로그아웃되었습니다." },
{ status: 200 },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

signOut() 에러 핸들링 누락

supabase.auth.signOut()이 실패해도(네트워크 오류, 세션 이미 만료 등) 항상 200 성공 응답을 반환한다.

Why: 로그아웃 실패 시에도 성공으로 처리되면 클라이언트가 상태 동기화에 혼란을 겪을 수 있다. 다만, 세션이 이미 없는 경우는 성공으로 처리해도 무방.

How: signOut() 결과의 error를 확인하고, 네트워크 오류 등 실제 장애 시 500 반환 권장. 단, 세션 없음 에러는 200 처리 가능.

🔧 수정 제안 (선택적)
 export async function POST() {
   const supabase = await createClient();
-  await supabase.auth.signOut();
+  const { error } = await supabase.auth.signOut();
+
+  // 세션 없음 에러는 이미 로그아웃된 상태이므로 성공 처리
+  if (error && !error.message.includes("session")) {
+    console.error("[auth.logout] signOut 실패:", error);
+    return NextResponse.json(
+      { success: false, message: "로그아웃 처리 중 오류가 발생했습니다." },
+      { status: 500 },
+    );
+  }

   return NextResponse.json(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/auth/logout/route.ts` around lines 6 - 13, The POST handler calls
createClient() and invokes supabase.auth.signOut() but ignores its result;
update the POST function to capture the signOut response, check the returned
error, and only return 200 when signOut succeeded or when the error indicates
"no active session"/"invalid session" (treat as success), otherwise return a 500
JSON response with the error message; reference the POST function, the
createClient() call, and supabase.auth.signOut() so you locate and change the
response logic to conditionally use NextResponse.json with status 200 or 500
based on the error.

Comment thread src/app/api/auth/me/route.ts
Comment thread src/app/api/auth/signup/route.ts
Comment on lines +85 to +94
const user = data.user;

return NextResponse.json(
{
success: true,
message: "회원가입이 완료되었습니다.",
user: user ? { id: user.id, email: user.email, nickname } : null,
},
{ status: 201 },
);

@coderabbitai coderabbitai Bot Jun 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Email confirmation 활성화 시 data.user가 null일 수 있음

Supabase Auth에서 email confirmation이 필수인 경우 signUp 성공 시에도 data.usernull로 반환될 수 있다. 현재 코드는 user: null을 그대로 응답에 포함하는데, 이는 의도된 동작인지 명확하지 않다.

Why: 클라이언트가 user.id 접근 시 NPE 발생 가능.

How: data.user가 null인 경우 "이메일 인증이 필요합니다" 등의 명시적 메시지 반환 권장.

🔧 수정 제안
   const user = data.user;

+  // Email confirmation 활성화 시 user가 null일 수 있음
+  if (!user) {
+    return NextResponse.json(
+      {
+        success: true,
+        message: "회원가입이 완료되었습니다. 이메일 인증을 진행해주세요.",
+        user: null,
+      },
+      { status: 201 },
+    );
+  }
+
   return NextResponse.json(
     {
       success: true,
       message: "회원가입이 완료되었습니다.",
-      user: user ? { id: user.id, email: user.email, nickname } : null,
+      user: { id: user.id, email: user.email, nickname },
     },
     { status: 201 },
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/auth/signup/route.ts` around lines 85 - 94, The handler currently
returns success with a user payload even when data.user is null (variable user),
which can cause NPEs on the client; change the response logic in signup/route.ts
to check if data.user is null and, if so, return a clear response like success:
true/false with message "이메일 인증이 필요합니다" (or similar) and omit user.id access (or
return user: null) so clients know to wait for email confirmation; update the
NextResponse.json call that currently builds { success, message, user: user ? {
id: user.id, email: user.email, nickname } : null } to branch on user === null
and return the explicit confirmation-required message and appropriate status.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다. 의도된 동작이라 현행 유지합니다. 이메일 확인(Confirm email) 활성 시 data.user가 null이어도 가입 자체는 접수된 것이므로, 현재 코드는 user ? { ... } : null로 그레이스풀하게 처리하고 success(201)를 반환합니다. 즉 null도 정상 분기입니다. 추후 '확인 메일 발송됨' 같은 명시적 안내 메시지는 #45 후속(이메일 확인 정책 확정 시)에서 다루겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도된 동작임을 확인했습니다. user: null 분기는 "가입 접수, 이메일 인증 대기" 상태를 나타내는 정상 응답으로 처리됩니다. 이메일 확인 정책 확정 후 명시적 메시지 추가 시 이 스레드를 참고하면 됩니다.


✏️ Learnings added
Learnt from: kokkumong
Repo: Siul49/moim PR: 47
File: src/app/api/auth/signup/route.ts:87-96
Timestamp: 2026-06-07T07:42:13.883Z
Learning: `src/app/api/auth/signup/route.ts`의 `POST` 핸들러에서 `supabase.auth.signUp` 성공 후 `data.user`가 `null`일 수 있는 것은 의도된 동작이다. Supabase email confirmation 활성화 시 가입은 접수되지만 `user`가 null로 반환되며, 현재 코드는 `user ? { id, email, nickname } : null`로 그레이스풀하게 처리하고 HTTP 201을 반환한다. "확인 메일 발송됨" 같은 명시적 안내 메시지는 이메일 확인 정책 확정 후 별도 PR(`#45` 후속)에서 다룰 예정이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread src/lib/supabase/admin.ts
Comment on lines +12 to +33
import { createClient, type SupabaseClient } from "@supabase/supabase-js";

let cached: SupabaseClient | null = null;

export function createAdminClient(): SupabaseClient {
if (cached) return cached;

const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!url || !serviceRoleKey) {
throw new Error(
"Supabase 관리자 클라이언트 설정이 누락되었습니다 (NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY).",
);
}

cached = createClient(url, serviceRoleKey, {
auth: { autoRefreshToken: false, persistSession: false },
});

return cached;
}

@coderabbitai coderabbitai Bot Jun 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Database 타입 정의 파일 존재 여부 확인
fd -t f "supabase" src/types --exec echo "Found: {}"

# 다른 Supabase 클라이언트에서 Database 타입 사용 여부 확인
rg -n "SupabaseClient<Database>" --type ts
rg -n "createClient<Database>" --type ts

Repository: Siul49/moim

Length of output: 37


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) 관련 타입 파일 존재 확인
echo "== supabase 관련 types 파일 찾기 =="
fd -t f "*supabase*" src/types --exec ls -la {}

echo "== Database 타입 정의/참조 찾기 =="
rg -n "export type Database|type Database|interface Database" src/types --type ts || true
rg -n "SupabaseClient<Database>|createClient<Database>|from\(\"profiles\"\)|profiles\"\\)" src --type ts || true

# 2) admin client 파일 실제 내용 일부 확인
echo "== src/lib/supabase/admin.ts =="
sed -n '1,120p' src/lib/supabase/admin.ts

# 3) 해당 경로의 login route 실제 코드 확인(있다면)
if [ -f src/app/api/auth/login/route.ts ]; then
  echo "== src/app/api/auth/login/route.ts =="
  sed -n '1,200p' src/app/api/auth/login/route.ts
else
  echo "route.ts not found at src/app/api/auth/login/route.ts"
fi

Repository: Siul49/moim

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) 관련 타입 파일 존재 확인
echo "== supabase 관련 types 파일 찾기 =="
fd -t f "*supabase*" src/types --exec ls -la {}

echo "== Database 타입 정의/참조 찾기 =="
rg -n "export type Database|type Database|interface Database" src/types --type ts || true
rg -n "SupabaseClient<Database>|createClient<Database>|from\(\"profiles\"\)|profiles\"\\)" src --type ts || true

# 2) admin client 파일 실제 내용 일부 확인
echo "== src/lib/supabase/admin.ts =="
sed -n '1,120p' src/lib/supabase/admin.ts

# 3) 해당 경로의 login route 실제 코드 확인(있다면)
if [ -f src/app/api/auth/login/route.ts ]; then
  echo "== src/app/api/auth/login/route.ts =="
  sed -n '1,200p' src/app/api/auth/login/route.ts
else
  echo "route.ts not found at src/app/api/auth/login/route.ts"
fi

Repository: Siul49/moim

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== supabase 관련 types 파일 찾기 (fixed-strings) =="
fd -t f --fixed-strings "supabase" src/types --exec ls -la {} || true

echo "== Database 타입 정의/참조 찾기 =="
rg -n --hidden --no-ignore-vcs "type Database|interface Database|export type Database" src/types --type ts || true
rg -n --hidden --no-ignore-vcs "SupabaseClient<Database>|createClient<Database>" src --type ts || true

echo "== admin client 파일 실제 내용 =="
sed -n '1,200p' src/lib/supabase/admin.ts

echo "== profiles 테이블 from() 사용처 찾기 =="
rg -n --hidden --no-ignore-vcs "from\\(\"profiles\"\\)|from\\('profiles'\\)" src --type ts || true

echo "== src/app/api/auth/login/route.ts 존재/내용 =="
if [ -f src/app/api/auth/login/route.ts ]; then
  sed -n '1,240p' src/app/api/auth/login/route.ts
else
  echo "route.ts not found at src/app/api/auth/login/route.ts"
fi

echo "== supabase-admin 사용처(대략) 찾기 =="
rg -n --hidden --no-ignore-vcs "createAdminClient\\(" src --type ts || true

Repository: Siul49/moim

Length of output: 3521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== supabase 관련 types 파일 찾기 (fixed-strings) =="
fd -t f --fixed-strings "supabase" src/types --exec ls -la {} || true

echo "== Database 타입 정의/참조 찾기 =="
rg -n --hidden --no-ignore-vcs "type Database|interface Database|export type Database" src/types --type ts || true
rg -n --hidden --no-ignore-vcs "SupabaseClient<Database>|createClient<Database>" src --type ts || true

echo "== admin client 파일 실제 내용 =="
sed -n '1,200p' src/lib/supabase/admin.ts

echo "== profiles 테이블 from() 사용처 찾기 =="
rg -n --hidden --no-ignore-vcs "from\\(\"profiles\"\\)|from\\('profiles'\\)" src --type ts || true

echo "== src/app/api/auth/login/route.ts 존재/내용 =="
if [ -f src/app/api/auth/login/route.ts ]; then
  sed -n '1,240p' src/app/api/auth/login/route.ts
else
  echo "route.ts not found at src/app/api/auth/login/route.ts"
fi

echo "== supabase-admin 사용처(대략) 찾기 =="
rg -n --hidden --no-ignore-vcs "createAdminClient\\(" src --type ts || true

Repository: Siul49/moim

Length of output: 3521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/lib/supabase/server.ts =="
sed -n '1,200p' src/lib/supabase/server.ts

echo "== src/lib/supabase/client.ts =="
sed -n '1,200p' src/lib/supabase/client.ts

echo "== SupabaseClient<...> 사용 여부 (src/lib/supabase) =="
rg -n "SupabaseClient<" src/lib/supabase --type ts || true

echo "== createClient<...> 사용 여부 (src/lib/supabase) =="
rg -n "createClient<" src/lib/supabase --type ts || true

Repository: Siul49/moim

Length of output: 1540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/lib/supabase/server.ts =="
sed -n '1,200p' src/lib/supabase/server.ts

echo "== src/lib/supabase/client.ts =="
sed -n '1,200p' src/lib/supabase/client.ts

echo "== SupabaseClient<...> 사용 여부 (src/lib/supabase) =="
rg -n "SupabaseClient<" src/lib/supabase --type ts || true

echo "== createClient<...> 사용 여부 (src/lib/supabase) =="
rg -n "createClient<" src/lib/supabase --type ts || true

Repository: Siul49/moim

Length of output: 1540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo 전체에서 Database 타입 정의 찾기 =="
rg -n --hidden --no-ignore-vcs "export type Database|interface Database|type Database" . --type ts || true

echo "== repo 전체에서 Supabase types 경로(예: supabase/types) 흔적 찾기 =="
rg -n --hidden --no-ignore-vcs "supabase\\.gen\\.|supabase gen types typescript|gen types" package.json .github src . --type ts --type js --type json --type md || true

echo "== repo 전체에서 '`@/types/supabase`' 참조 찾기 =="
rg -n --hidden --no-ignore-vcs "`@/types/supabase`" . --type ts --type tsx || true

echo "== repo 전체에서 'Database<' 제네릭 사용 흔적 찾기 =="
rg -n --hidden --no-ignore-vcs "SupabaseClient<Database>|createClient<Database>" . --type ts --type tsx || true

echo "== 타입 파일 유무: src/types 디렉토리 파일 목록 =="
ls -la src/types || true

find src/types -maxdepth 2 -type f -print || true

Repository: Siul49/moim

Length of output: 967


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo 전체에서 Database 타입 정의 찾기 =="
rg -n --hidden --no-ignore-vcs "export type Database|interface Database|type Database" . --type ts || true

echo "== repo 전체에서 Supabase types 경로(예: supabase/types) 흔적 찾기 =="
rg -n --hidden --no-ignore-vcs "supabase\\.gen\\.|supabase gen types typescript|gen types" package.json .github src . --type ts --type js --type json --type md || true

echo "== repo 전체에서 '`@/types/supabase`' 참조 찾기 =="
rg -n --hidden --no-ignore-vcs "`@/types/supabase`" . --type ts --type tsx || true

echo "== repo 전체에서 'Database<' 제네릭 사용 흔적 찾기 =="
rg -n --hidden --no-ignore-vcs "SupabaseClient<Database>|createClient<Database>" . --type ts --type tsx || true

echo "== 타입 파일 유무: src/types 디렉토리 파일 목록 =="
ls -la src/types || true

find src/types -maxdepth 2 -type f -print || true

Repository: Siul49/moim

Length of output: 967


Supabase 관리자 클라이언트에도 Database 제네릭을 연결해 쿼리 타입 안전성을 확보하세요.

src/lib/supabase/admin.tscreateAdminClient()SupabaseClient를 그대로 반환하고, createClient(...)에도 Database 타입을 지정하지 않아 from("profiles").select("email") 같은 쿼리가 컴파일 타임 타입 체크 없이 동작합니다. (컬럼 오타/스키마 변경이 런타임까지 늦게 노출)

현재 src/types/@/types/supabase(generated Database) 타입이 없어 그대로는 컴파일이 불가하므로, 먼저 Supabase 타입을 생성/추가한 뒤 제네릭을 적용해야 합니다.

♻️ 타입 안전성 추가
-import { createClient, type SupabaseClient } from "`@supabase/supabase-js`";
+import { createClient, type SupabaseClient } from "`@supabase/supabase-js`";
+import type { Database } from "`@/types/supabase`";

-let cached: SupabaseClient | null = null;
+let cached: SupabaseClient<Database> | null = null;

-export function createAdminClient(): SupabaseClient {
+export function createAdminClient(): SupabaseClient<Database> {
   if (cached) return cached;

   const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
   const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

   if (!url || !serviceRoleKey) {
     throw new Error(
       "Supabase 관리자 클라이언트 설정이 누락되었습니다 (NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY).",
     );
   }

-  cached = createClient(url, serviceRoleKey, {
+  cached = createClient<Database>(url, serviceRoleKey, {
     auth: { autoRefreshToken: false, persistSession: false },
   });

   return cached;
 }

@/types/supabase는 현재 저장소에 존재하지 않으니 supabase gen types typescript로 생성(또는 동일한 Database 타입 파일을 추가)한 다음 위 적용을 진행하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
let cached: SupabaseClient | null = null;
export function createAdminClient(): SupabaseClient {
if (cached) return cached;
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!url || !serviceRoleKey) {
throw new Error(
"Supabase 관리자 클라이언트 설정이 누락되었습니다 (NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY).",
);
}
cached = createClient(url, serviceRoleKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
return cached;
}
import { createClient, type SupabaseClient } from "`@supabase/supabase-js`";
import type { Database } from "`@/types/supabase`";
let cached: SupabaseClient<Database> | null = null;
export function createAdminClient(): SupabaseClient<Database> {
if (cached) return cached;
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!url || !serviceRoleKey) {
throw new Error(
"Supabase 관리자 클라이언트 설정이 누락되었습니다 (NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY).",
);
}
cached = createClient<Database>(url, serviceRoleKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
return cached;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/supabase/admin.ts` around lines 12 - 33, The admin Supabase client
lacks the Database generic so query types aren't checked; generate the Supabase
types (e.g., run `supabase gen types typescript` to produce a Database type
file) and then update createAdminClient: import the generated Database type
(e.g., Database from '`@/types/supabase`'), change the cached variable and
function signature to use SupabaseClient<Database>, and call
createClient<Database>(...) so the returned client and all queries like
from("profiles").select(...) are type-safe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 지적이라 반영하고 싶지만, 지금은 보류합니다. createClient<Database>에 넣을 생성된 Supabase 타입(@/types/supabase)이 아직 리포에 없습니다(실 Supabase 프로젝트 미연결 상태). 타입을 supabase gen types로 생성하는 작업과 함께 적용하는 게 맞아, 상위 이슈 #45의 후속(실 프로젝트 셋업)으로 넘깁니다. 그 전까지는 쿼리 타입 안전성이 없다는 점 인지하고 있습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kokkumong 납득할 수 있는 이유입니다. 실 Supabase 프로젝트 연결 및 supabase gen types typescript 실행 후 한 번에 적용하는 흐름이 맞습니다.

이 항목을 놓치지 않으려면 후속 이슈를 열어 두는 걸 권장합니다. 원하시면 바로 생성해 드릴 수 있습니다.

(ᵔ ᴥ ᵔ)っ

- login: admin profiles 조회 error를 throw해 서버오류가 401로 위장되지 않게 함
- login: signInWithPassword 결과에서 data.session까지 검사 (이메일 확인 시 미인증 차단)
- me: profiles 조회 error를 surface(500)해 폴백값으로 가리지 않게 함
- logout: signOut error 로깅 추가
- signup: 중복 판정 시 error.status를 우선 사용하고 메시지 정규식은 fallback으로

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kokkumong
kokkumong force-pushed the feature/45-supabase-auth-integration branch from 2ef26d7 to e3c3419 Compare June 7, 2026 07:33
@Siul49
Siul49 merged commit fc9633d into dev Jun 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants