Conversation
* feat(auth): 네이버 로그인 Supabase Admin 브리지 구현 네이버는 Supabase가 기본 지원하지 않는 OAuth 제공자라, #45 마이그레이션 이후 /api/auth/naver/* 가 naver_not_implemented 스텁으로 비활성 상태였다. - 네이버 OAuth 헬퍼(getNaverAuthUrl/getNaverToken/getNaverUser) 복원 (자체 JWT 의존 제거, fetch-with-timeout 기반) - naver/login: authorize 리다이렉트 + state 쿠키 복원 - naver/callback: 네이버 프로필 수신 → Supabase Admin(service_role)으로 유저 생성/조회 → magiclink 토큰을 verifyOtp로 교환해 일반 Supabase 세션 쿠키 발급. 카카오/구글/애플과 동일하게 getUser() 세션으로 일원화. - 이메일 미제공 시 결정적 placeholder로 동일 사용자 식별, 닉네임 unique 충돌 시 naverId 기반으로 대체. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auth): 코드래빗 리뷰 반영 — 네이버 식별자/닉네임 보강 - naver_id를 profiles 1차 식별자로 사용 (이메일 단독 조회 시 같은 네이버 계정이 갈라지는 문제 해결). profiles.naver_id 컬럼+unique 인덱스 추가하고 handle_new_user 트리거가 user_metadata.naver_id를 채우도록 갱신. - 콜백: naver_id로 먼저 조회, 미존재 시 이메일로 매칭 후 naver_id 백필. - extractNaverUserInfo: 빈 문자열/공백 닉네임·이메일을 "값 없음"으로 취급해 name → naver_<id> fallback이 정상 동작하도록 수정. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auth): 코드래빗 2차 리뷰 반영 — naver_id 백필 가드/정규화/보안 - 콜백: 이메일 매칭 백필 시 byEmail.naver_id가 비어있을 때만 갱신해 기존 네이버 연결을 덮어쓰지 않도록 가드. - 마이그레이션: naver_id 빈 문자열/공백을 NULL로 정규화(트리거 nullif(btrim(...))), unique 인덱스 predicate도 동일 정규화로 일치. - handle_new_user에 SET search_path = public 추가 (SECURITY DEFINER 함수의 search_path 고정). Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(auth): 네이버 동시요청 race 시 기존 사용자 추가정보 오라우팅 수정 createUser가 "already exists"로 실패하는 동시요청 race에서는 기존 사용자인데도 profileComplete를 false로 고정해, 추가정보 입력 페이지로 잘못 보내던 문제를 수정. race 감지 시 naver_id(없으면 이메일)로 실제 프로필 상태를 재조회해 isNewUser=false + 정확한 profileComplete로 분기. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 28 minutes and 14 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
WalkthroughNaver OAuth 완전 구현(토큰 교환, 프로필 동기화, 세션 발급), Google OAuth origin-aware 리다이렉트 URI, 캘린더 어댑터 인터페이스→추상클래스 전환, 에러 계층구조 중앙화, Supabase E2E 모킹 인프라, 포트 4000 기반 환경설정. Changes에러 계층구조 통합
Naver OAuth 완전 구현
Google OAuth Origin-Aware 리다이렉트
캘린더 어댑터 아키텍처 리팩토링
Supabase E2E 테스트 인프라
설정 및 인프라
E2E 테스트 강화
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
핵심 리뷰 포인트1. Naver OAuth 콜백의 Race Condition 처리 (HIGH RISK)문제: // src/app/api/auth/naver/callback/route.ts:126-140
} catch (error) {
if (error?.message?.includes('already registered') ||
error?.message?.includes('User already exists')) {
// Race 간주 → 프로필 재조회
const existingProfile = await adminClient
.from('profiles')
.select()
.eq('naver_id', naverId)
.maybeSingle();Why: 동시 요청으로 User 생성이 먼저 성공한 후 Profile insert 실패하는 경우, How & Code: // 개선안: 에러 코드/상태 기반 검증
const isUserExistsError = error?.status === 409 ||
error?.code === 'PGRST116';
if (isUserExistsError) {
// Profile 재조회 시 실제 상태 검증
const recheck = await adminClient
.from('profiles')
.select()
.eq('naver_id', naverId)
.single();
if (!recheck.data) {
// naver_id가 없으면 다른 User에 바인딩된 상태 → 실패
return redirectWithStateCleanup(origin, 'login?error=naver_login_failed');
}
}위험: profileComplete 재산정이 틀리면 사용자가 입력한 추가정보를 무시하고 2. GoogleCalendarAdapter의 All-Day 이벤트 로직 불일치 (MEDIUM RISK)문제: // src/lib/calendar/adapters/google.ts
protected getIsAllDay(event: GoogleEvent): boolean {
return !!(event.start.date && event.end.date); // 부분 조건
}
protected getStartAt(event: GoogleEvent): Date {
if (event.start.date) {
return new Date(event.start.date); // All-day는 YYYY-MM-DD
}
return new Date(event.start.dateTime); // 시간 이벤트
}Why: How & Code: // 방어적 로직
protected getIsAllDay(event: GoogleEvent): boolean {
// All-day: date만 존재, 시간 이벤트: dateTime 존재
return !!(event.start.date && !event.start.dateTime);
}
protected getStartAt(event: GoogleEvent): Date {
const startStr = event.start.date || event.start.dateTime;
if (!startStr) throw new ExternalServiceError('Google event missing start', 'Google');
return new Date(startStr);
}검증: Google Calendar API 테스트 데이터로 all-day + 시간 이벤트 조합 확인. 3. Supabase 모킹의 Prisma 동적 Import 신뢰성 (MEDIUM RISK)문제: // src/lib/supabase/server.ts:260-280
const { PrismaClient } = await import('`@prisma/client`');
const prisma = new PrismaClient();E2E 환경에서 Prisma를 매번 동적 import → 인스턴스 생성. 콘테이너 환경에서 데이터베이스 초기화 시점과 타이밍 불일치 가능. How & Code: // 싱글톤 보장
let prismaInstance: PrismaClient | null = null;
async function getPrismaInstance() {
if (!prismaInstance) {
const { PrismaClient } = await import('`@prisma/client`');
prismaInstance = new PrismaClient();
}
return prismaInstance;
}
// 테스트 후 cleanup
afterAll(async () => {
if (prismaInstance) {
await prismaInstance.$disconnect();
}
});4. getNaverToken의 예외 처리 불완전 (LOW-MEDIUM RISK)문제: // src/lib/auth/naver.ts:90-105
const text = await response.text();
const data = JSON.parse(text);
if (!response.ok || data.error) {
throw new Error(`Naver token failed: ${data.error || response.statusText}`);
}
if (!data.access_token) {
throw new Error('No access_token in Naver response');
}Why: JSON 파싱 실패 시 즉시 예외 발생. 네이버가 HTML 에러 페이지 반환 가능. How & Code: let data;
try {
data = JSON.parse(text);
} catch (e) {
throw new ExternalServiceError(
`Naver token response parse failed: ${response.status}`,
'Naver',
response.status
);
}
// 안전한 필드 접근
const accessToken = data?.access_token;
if (!accessToken || typeof accessToken !== 'string') {
throw new ExternalServiceError(
'Naver response missing or invalid access_token',
'Naver',
data?.error ? 400 : 500
);
}5. E2E 테스트의 WebKit 자동완성 우회 근본 원인 불명확 (LOW RISK)문제: // e2e/host-flow.spec.ts:35-55
// 입력 전에 현재값 검증 후 다르면 재입력
if (await page.inputValue('[name="phone"]') !== testPhone) {
await page.fill('[name="phone"]', testPhone);
}Why: WebKit이 폼을 왜 리셋하는지 근본 원인이 불명 → 다른 상황(예: 새 탭)에서 재발 가능성. How & Code: // 근본 원인 분석: form reset trigger 찾기
// 1. Javascript: form.reset() 호출 확인
// 2. HTML: type="reset" 버튼 확인
// 3. Playwright: 입력 후 다른 필드 focus 순서 변경
// 임시 우회 + 주석
// FIXME: WebKit bug - form fields reset unexpectedly
// Workaround: re-fill before submission
// Related: https://bugs.webkit.org/show_bug.cgi?id=...검증 전략: 다른 브라우저(Chrome, Firefox)에서 동일 테스트 실행 → WebKit 특정 버그 확인. 6. Prisma Schema SQLite 전환의 제약 미충분 (LOW RISK)문제: // prisma/schema.prisma
provider = "sqlite"
// directUrl, schemas, multiSchema 주석 처리SQLite는 다중 스키마 미지원. 그러나 향후 PostgreSQL 복귀 시 코드에 하드코딩된 SQLite 쿼리가 있으면 호환성 깨짐. How & Code: // ensure-sqlite-schema.mjs 확인
// ALTER TABLE은 SQLite 전용 구문이 아니므로 PostgreSQL도 지원
// 단, NOT NULL 제약 추가는 DB별로 다름
// 안전한 형태:
`ALTER TABLE "Schedule" ADD COLUMN "creatorId" TEXT`; // 모두 지원
// 위험한 형태:
`ALTER TABLE "Schedule" ADD COLUMN "creatorId" TEXT NOT NULL DEFAULT uuid()`; // SQLite uuid() 함수 없음검증: scripts/ensure-sqlite-schema.mjs가 순수 SQL만 사용 확인. 7. 에러 클래스의 프로토타입 체인 보정 필요성 (LOW RISK)문제: // src/lib/errors.ts:5-10
export class MoimError extends Error {
constructor(message: string, ...) {
super(message);
this.name = 'MoimError';
Object.setPrototypeOf(this, MoimError.prototype); // ← 왜?
}
}Why: TypeScript transpile 후 How & Code: // 주석으로 명확히
Object.setPrototypeOf(this, MoimError.prototype);
// ↑ Ensure instanceof works after transpilation
// See: https://github.com/Microsoft/TypeScript/issues/13965종합 평가강점:
약점:
필수 수정:
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/google/__tests__/auth.test.ts (1)
80-117:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
exchangeCodeForTokens(origin)경로도 테스트로 묶어 두세요.Why: 지금 추가된 케이스는
buildAuthUrl만 검증합니다. 그런데 실제 콜백 라우트는 같은origin을exchangeCodeForTokens(code, origin)에도 넘기고, Google은 인가 단계와 토큰 교환 단계의redirect_uri가 정확히 같아야 합니다. POST body 검증이 없으면 한쪽만 깨져도 테스트가 통과합니다.How:
🧪 최소 추가 예시
describe("exchangeCodeForTokens", () => { test("authorization code로 토큰을 교환한다", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ access_token: "access-123", refresh_token: "refresh-456", expires_in: 3600, }), }); const tokens = await exchangeCodeForTokens("auth-code-789"); @@ expect(options.body.toString()).toContain("code=auth-code-789"); expect(options.body.toString()).toContain("grant_type=authorization_code"); }); + + test("origin 매개변수를 토큰 교환 redirect_uri에도 반영한다", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + access_token: "access-123", + refresh_token: "refresh-456", + expires_in: 3600, + }), + }); + + await exchangeCodeForTokens("auth-code-789", "https://my-custom-domain.com"); + + const body = mockFetch.mock.calls[0][1].body.toString(); + expect(body).toContain( + "redirect_uri=https%3A%2F%2Fmy-custom-domain.com%2Fapi%2Fgoogle%2Fcallback", + ); + }); });🤖 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/google/__tests__/auth.test.ts` around lines 80 - 117, Add a test that verifies exchangeCodeForTokens receives and uses the same origin as buildAuthUrl: when calling buildAuthUrl(origin) and then exchangeCodeForTokens(code, origin) in the test, assert the POST body sent by exchangeCodeForTokens (mockFetch.mock.calls) contains the exact redirect_uri matching the origin + callback path used by buildAuthUrl; update the existing test suite for exchangeCodeForTokens to call exchangeCodeForTokens with the origin parameter and include assertions that options.body includes "redirect_uri=" with the same origin value so the redirect_uri parity between buildAuthUrl and exchangeCodeForTokens is enforced.
🤖 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 `@e2e/host-flow.spec.ts`:
- Around line 39-54: When you re-fill potentially-cleared fields (phoneInput,
nicknameInput, emailInput, pwInput, pwConfirmInput) add an immediate
verification after each fill using the Playwright assertion (await
expect(<input>).toHaveValue(<expected>)) so the test fails fast on client-side
autocomplete/clearing issues; e.g., after calling phoneInput.fill(testPhone)
await expect(phoneInput).toHaveValue(testPhone), and do the same for
nicknameInput/testNickname, emailInput/testEmail, pwInput/"Test1234!", and
pwConfirmInput/"Test1234!".
In `@prisma/schema.prisma`:
- Around line 6-10: The Prisma schema is set to provider="sqlite" (datasource db
in prisma/schema.prisma) while CI injects a PostgreSQL DATABASE_URL, causing
mismatched client generation; fix by making CI use a SQLite DATABASE_URL (e.g.,
set env DATABASE_URL to file:./dev.db in .github/workflows/ci.yml and
remove/align DIRECT_URL) or else create a separate Postgres Prisma schema and
generation flow, and add a defensive guard in scripts/with-database-url.mjs
(and/or scripts/ensure-sqlite-schema.mjs) that throws if
process.env.DATABASE_URL startsWith("postgres") to fail fast when modes
disagree.
In `@src/lib/__tests__/errors.test.ts`:
- Around line 72-86: Add a regression test that constructs an EverytimeError
(and/or specific subclass like EverytimeAuthError) by passing a top-level type
argument plus a details object containing a conflicting type, then assert the
final error.details.type equals the constructor's type (not the provided
details.type) and that statusCode/instance checks still hold; specifically
exercise EverytimeError's constructor behavior (and EverytimeAuthError
instantiation) to ensure the constructor-enforced type wins over details.type.
In `@src/lib/auth/naver.ts`:
- Around line 42-48: The error check uses redirectUri =
process.env.NAVER_REDIRECT_URI || `${base}/api/auth/naver/callback`, making
!redirectUri unreachable; update getNaverAuthUrl and getNaverToken to only
validate the actual required environment variables: in getNaverAuthUrl remove
the redundant !redirectUri check and only throw if NAVER_CLIENT_ID is missing
(reference: getNaverAuthUrl, redirectUri); in getNaverToken ensure you validate
NAVER_CLIENT_ID and NAVER_CLIENT_SECRET (reference: getNaverToken, clientId,
clientSecret) and either validate process.env.NAVER_REDIRECT_URI explicitly if
you require it or adjust the thrown message to reflect that a fallback redirect
is used.
In `@src/lib/calendar/adapters/__tests__/manual.test.ts`:
- Around line 47-59: 테스트의 slots 변수가 암시적 타입으로 인해 'day: "MON"' 리터럴이 유지되지 않을 수 있으니,
slots를 명시적으로 TimeSlot[] 타입으로 선언해 테스트 의도를 분명히 하세요; 예를 들어 테스트 내에서 slots 변수에 대해
TimeSlot[] 타입 주석을 추가하여 'slots'와 관련된 타입 불일치(수정 대상: slots 변수 선언) 문제를 제거하고
manualAdapter.toCalendarEvents 호출이 기대한 리터럴 타입을 받도록 하세요.
In `@src/lib/calendar/adapters/google.ts`:
- Around line 19-29: getStartAt and getEndAt currently assert
event.start.dateTime / event.end.dateTime with "as string" which can produce
Invalid Date at runtime if dateTime is undefined; change these functions to
defensively check whether event.start.date is present else whether
event.start.dateTime is a defined non-empty string before calling new Date(),
and do the same for event.end; if dateTime is missing or invalid, either throw a
clear error (including event identifiers/context) or return a well-defined
fallback/nullable value instead of creating an Invalid Date; update references
to parseAllDay, getStartAt, and getEndAt accordingly and remove the "as string"
assertions.
In `@src/lib/calendar/adapters/manual.ts`:
- Around line 26-50: The protected methods' parameter types must match the class
generic TItem ({ slot: TimeSlot; index: number; weekStart: Date }) so update the
signatures for getExternalId, getTitle, and getIsAllDay to accept that full
TItem shape (or TItem directly) instead of the current narrower or unknown
types; specifically, include weekStart in getExternalId's parameter, and change
getTitle(_item: unknown) and getIsAllDay(_item: unknown) to accept the same
TItem type used by getStartAt/getEndAt, ensuring consistency with
mapToCalendarEvent and the class generic.
In `@src/lib/errors.ts`:
- Around line 68-74: In the Everytime error subclass where you call super(..., {
type, ...details }), callers can override the error's canonical type because
details.type is merged after the constructor's type; fix this by reversing the
merge so the constructor-provided type always wins (merge as { ...details, type
}) in the constructor/helper that builds the details object (the super call in
the Everytime error class) to preserve the invariant that the class's type
cannot be overwritten by caller-supplied details.
- Around line 15-24: The constructors of UnauthorizedError and ForbiddenError
currently pass the diagnostic `message` into the `clientMessage` slot of
MoimError, leaking internal details to clients; change the constructors
(UnauthorizedError, ForbiddenError) to call super with a fixed, non-sensitive
default clientMessage (e.g. "인증이 필요합니다." / "접근 권한이 없습니다.") and only allow an
explicit optional clientMessage parameter to override when truly needed, so
internal diagnostic `message` remains separate from the safe `clientMessage`.
In `@src/lib/google/auth.ts`:
- Around line 37-43: The getRedirectUri function builds redirect_uri via string
concatenation which can produce double slashes when NEXT_PUBLIC_BASE_URL ends
with a slash; change it to normalize using the URL API: if
process.env.GOOGLE_CALENDAR_REDIRECT_URI is set return it, otherwise construct
the base from origin || process.env.NEXT_PUBLIC_BASE_URL ||
"http://localhost:4000" and create the final callback with the URL constructor
(e.g., new URL('/api/google/callback', base)) so the path is resolved correctly
without duplicate slashes and works for both trailing-slash and
no-trailing-slash bases; update getRedirectUri accordingly.
In `@src/lib/supabase/env.ts`:
- Around line 12-19: 현재 브라우저 분기에서 url/anonKey에 무조건 mock 기본값을 주입해 배포 오설정을 숨기고
있으므로, 이 동작을 E2E 전용 플래그로 제한하고 그렇지 않으면 즉시 실패하도록 바꾸세요: 즉, 변경할 조건문에서 추가로 E2E 플래그(예:
process.env.NEXT_PUBLIC_SUPABASE_E2E === '1' 또는 'true')가 설정된 경우에만 url ||=
"https://example-project..."와 anonKey ||= "test-anon-key"를 적용하고, 그 외
브라우저(non-server, typeof window !== "undefined", NODE_ENV !== "test") 상황에서는 url
또는 anonKey가 비어있으면 즉시 예외를 throw(또는 processLogger/error)하도록 수정해 options.isServer,
url, anonKey 변수를 사용해 검증 흐름을 명확히 유지하세요.
In `@src/lib/supabase/server.ts`:
- Around line 198-235: The mock upsert path is not persisting the naver_id
field, so eqFilters.naver_id lookups fail; update the write mapping in
prisma.user.upsert to include naver_id (map this.updateData.naver_id into the
same key used by reads), and ensure the local data object (the one merged into
create/update) contains naverId/naver_id consistently with eqFilters.naver_id
lookup logic (updateData.naver_id -> data.naverId or data.naver_id depending on
convention), and repeat the same fix in the other upsert branch around the code
referenced at lines 269-277 so reads and writes use the same field name.
In `@supabase/migrations/20260611000000_profiles_naver_id.sql`:
- Around line 16-18: The current create unique index statement for
profiles_naver_id_key on public.profiles can block writes during deployment;
change the migration to create the index using CONCURRENTLY (i.e., create unique
index concurrently if not exists profiles_naver_id_key on public.profiles
(naver_id) where nullif(btrim(naver_id), '') is not null) or move this DDL into
a separate non-transactional maintenance migration/run it during a low‑traffic
window so the Naver callback flow (reads, backfill, inserts against
public.profiles) isn’t blocked.
---
Outside diff comments:
In `@src/lib/google/__tests__/auth.test.ts`:
- Around line 80-117: Add a test that verifies exchangeCodeForTokens receives
and uses the same origin as buildAuthUrl: when calling buildAuthUrl(origin) and
then exchangeCodeForTokens(code, origin) in the test, assert the POST body sent
by exchangeCodeForTokens (mockFetch.mock.calls) contains the exact redirect_uri
matching the origin + callback path used by buildAuthUrl; update the existing
test suite for exchangeCodeForTokens to call exchangeCodeForTokens with the
origin parameter and include assertions that options.body includes
"redirect_uri=" with the same origin value so the redirect_uri parity between
buildAuthUrl and exchangeCodeForTokens is enforced.
🪄 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: b63f9b8a-8e63-4c70-84aa-44f07423ac06
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.json,!**/*.json,!package-lock.jsonpackage.jsonis excluded by!**/*.json
📒 Files selected for processing (28)
.env.examplee2e/host-flow.spec.tsprisma/schema.prismascripts/ensure-sqlite-schema.mjssrc/app/api/auth/naver/callback/route.tssrc/app/api/auth/naver/login/route.tssrc/app/api/google/auth/route.tssrc/app/api/google/callback/route.tssrc/lib/__tests__/errors.test.tssrc/lib/auth/naver.tssrc/lib/auth/session.tssrc/lib/caldav/client.tssrc/lib/calendar/adapter.tssrc/lib/calendar/adapters/__tests__/manual.test.tssrc/lib/calendar/adapters/google.tssrc/lib/calendar/adapters/icloud.tssrc/lib/calendar/adapters/manual.tssrc/lib/calendar/adapters/photo.tssrc/lib/errors.tssrc/lib/everytime/auth.tssrc/lib/everytime/timetable.tssrc/lib/everytime/url-scraper.tssrc/lib/google/__tests__/auth.test.tssrc/lib/google/auth.tssrc/lib/supabase/client.tssrc/lib/supabase/env.tssrc/lib/supabase/server.tssupabase/migrations/20260611000000_profiles_naver_id.sql
| // WebKit 자동완성 버그 우회: 폼 제출 직전에 비워진 필드들을 검사하고 재기입함 | ||
| if ((await phoneInput.inputValue()) !== testPhone) { | ||
| await phoneInput.fill(testPhone); | ||
| } | ||
| if ((await nicknameInput.inputValue()) !== testNickname) { | ||
| await nicknameInput.fill(testNickname); | ||
| } | ||
| if ((await emailInput.inputValue()) !== testEmail) { | ||
| await emailInput.fill(testEmail); | ||
| } | ||
| if ((await pwInput.inputValue()) !== "Test1234!") { | ||
| await pwInput.fill("Test1234!"); | ||
| } | ||
| if ((await pwConfirmInput.inputValue()) !== "Test1234!") { | ||
| await pwConfirmInput.fill("Test1234!"); | ||
| } |
There was a problem hiding this comment.
재기입 분기 뒤에 즉시 재검증이 없어 flake 원인이 늦게 드러납니다.
왜: 지금은 값이 틀리면 다시 fill()만 하고 바로 제출합니다. WebKit이 같은 이유로 다시 필드를 비우면 실패가 회원가입 이후 단계에서 터져서, 원인이 입력 유지 문제인지 서버/라우팅 문제인지 구분이 어려워집니다.
How: 재기입한 필드만 바로 toHaveValue로 다시 확인하세요. 수정 범위는 이 분기 내부 몇 줄이면 충분합니다.
최소 수정 예시
if ((await phoneInput.inputValue()) !== testPhone) {
await phoneInput.fill(testPhone);
+ await expect(phoneInput).toHaveValue(testPhone);
}
if ((await nicknameInput.inputValue()) !== testNickname) {
await nicknameInput.fill(testNickname);
+ await expect(nicknameInput).toHaveValue(testNickname);
}
if ((await emailInput.inputValue()) !== testEmail) {
await emailInput.fill(testEmail);
+ await expect(emailInput).toHaveValue(testEmail);
}🤖 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 `@e2e/host-flow.spec.ts` around lines 39 - 54, When you re-fill
potentially-cleared fields (phoneInput, nicknameInput, emailInput, pwInput,
pwConfirmInput) add an immediate verification after each fill using the
Playwright assertion (await expect(<input>).toHaveValue(<expected>)) so the test
fails fast on client-side autocomplete/clearing issues; e.g., after calling
phoneInput.fill(testPhone) await expect(phoneInput).toHaveValue(testPhone), and
do the same for nicknameInput/testNickname, emailInput/testEmail,
pwInput/"Test1234!", and pwConfirmInput/"Test1234!".
| datasource db { | ||
| provider = "postgresql" | ||
| provider = "sqlite" | ||
| url = env("DATABASE_URL") | ||
| directUrl = env("DIRECT_URL") | ||
| schemas = ["public", "auth"] | ||
| // directUrl = env("DIRECT_URL") | ||
| // schemas = ["public", "auth"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
echo '--- prisma/schema.prisma ---'
sed -n '1,120p' prisma/schema.prisma
echo
echo '--- scripts/ensure-sqlite-schema.mjs ---'
sed -n '1,40p' scripts/ensure-sqlite-schema.mjs
echo
echo '--- PostgreSQL / Prisma 사용 지점 검색 ---'
rg -n --glob '!node_modules' 'postgres(ql)?://|DIRECT_URL|DATABASE_URL|new PrismaClient|from "`@prisma/client`"' .Repository: Siul49/moim
Length of output: 4725
🏁 Script executed:
#!/bin/bash
set -eu
echo '--- supabase 관련 검색 ---'
rg -n --glob '!node_modules' 'supabase|SUPABASE|postgres(ql)?://|DIRECT_URL|DATABASE_URL\s*:' . || true
echo
echo '--- CI에서 DATABASE_URL 설정 여부 ---'
rg -n --glob '!node_modules' 'DATABASE_URL' .github workflows package.json playwright.config.ts prisma/schema.prisma src/lib/prisma.ts README.md scripts/with-database-url.mjs || true
echo
echo '--- schema.prisma(provider)로 PrismaClient 생성 사용 여부 ---'
sed -n '1,120p' src/lib/prisma.ts
echo
echo '--- playwright/e2e에 주입되는 DATABASE_URL ---'
sed -n '1,120p' playwright.config.ts
echo
echo '--- scripts/seed 및 기타 PrismaClient 생성 지점 ---'
rg -n --glob '!node_modules' 'new PrismaClient' scripts src . || trueRepository: Siul49/moim
Length of output: 20282
🏁 Script executed:
#!/bin/bash
set -eu
echo '--- .github/workflows/ci.yml (relevant) ---'
sed -n '1,140p' .github/workflows/ci.yml
echo
echo '--- package.json (relevant scripts) ---'
cat package.json
echo
echo '--- src/lib/schedules/store.ts (Prisma usage) ---'
sed -n '1,200p' src/lib/schedules/store.ts
echo
echo '--- scripts/seed_availability.js (more) ---'
sed -n '1,160p' scripts/seed_availability.jsRepository: Siul49/moim
Length of output: 13020
🏁 Script executed:
#!/bin/bash
set -eu
echo '--- .github/workflows/ci.yml ---'
sed -n '1,220p' .github/workflows/ci.yml
echo
echo '--- package.json ---'
cat package.json
echo
echo '--- src/lib/prisma.ts ---'
sed -n '1,120p' src/lib/prisma.ts
echo
echo '--- scripts/ensure-sqlite-schema.mjs (tail) ---'
tail -n 60 scripts/ensure-sqlite-schema.mjs
echo
echo '--- prisma 관련 config 확인 (있는 경우) ---'
ls -la prisma || true
find prisma -maxdepth 2 -type f -print || true
echo
echo '--- Prisma 사용 파일들 빠른 목록 ---'
rg -n --glob '!node_modules' 'from "`@prisma/client`"|new PrismaClient\(|prisma\.' src scripts prisma | head -n 80Repository: Siul49/moim
Length of output: 11436
🏁 Script executed:
#!/bin/bash
set -eu
echo '--- .github/workflows/ci.yml ---'
sed -n '1,220p' .github/workflows/ci.yml
echo
echo '--- package.json ---'
cat package.json
echo
echo '--- src/lib/prisma.ts ---'
sed -n '1,120p' src/lib/prisma.ts
echo
echo '--- scripts/ensure-sqlite-schema.mjs (tail) ---'
tail -n 60 scripts/ensure-sqlite-schema.mjs
echo
echo '--- prisma 관련 config 확인 (있는 경우) ---'
ls -la prisma || true
find prisma -maxdepth 2 -type f -print || true
echo
echo '--- Prisma 사용 파일들 빠른 목록 ---'
rg -n --glob '!node_modules' 'from "`@prisma/client`"|new PrismaClient\(|prisma\.' src scripts prisma | head -n 80Repository: Siul49/moim
Length of output: 11436
🏁 Script executed:
# 예: SQLite 스키마를 쓰는 명령은 --schema로 분기
npx prisma db push --schema=prisma/schema.sqlite.prisma --skip-generateRepository: Siul49/moim
Length of output: 371
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '1,200p' scripts/with-database-url.mjsRepository: Siul49/moim
Length of output: 1476
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '1,200p' scripts/with-database-url.mjsRepository: Siul49/moim
Length of output: 1476
🏁 Script executed:
# 예: 모드별로 generate/db push를 schema로 분리
prisma generate --schema=prisma/schema.prisma
prisma db push --schema=prisma/schema.prisma
prisma generate --schema=prisma/schema.sqlite.prisma
prisma db push --schema=prisma/schema.sqlite.prismaRepository: Siul49/moim
Length of output: 237
🏁 Script executed:
# 예: SQLite용 작업만 --schema로 분기
npx prisma db push --skip-generate --schema=prisma/schema.sqlite.prismaRepository: Siul49/moim
Length of output: 371
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '180,320p' src/lib/supabase/server.tsRepository: Siul49/moim
Length of output: 4147
🏁 Script executed:
# 모드별로 schema를 명시
npx prisma generate --schema=prisma/schema.prisma
npx prisma db push --schema=prisma/schema.prisma --skip-generate
npx prisma generate --schema=prisma/schema.sqlite.prisma
npx prisma db push --schema=prisma/schema.sqlite.prisma --skip-generateRepository: Siul49/moim
Length of output: 141
GitHub Actions에서 DATABASE_URL을 PostgreSQL로 주입하는데 Prisma 스키마는 provider="sqlite"로 고정돼 실행 경로가 어긋납니다.
왜: postinstall에서 prisma/schema.prisma(sqlite provider) 기준으로 Prisma Client가 생성되고, 테스트 런타임은 scripts/with-database-url.mjs가 CI의 DATABASE_URL을 그대로 유지합니다. 그런데 Actions는 DATABASE_URL/DIRECT_URL을 postgresql://...로 주입하고, scripts/ensure-sqlite-schema.mjs는 PostgreSQL URL이면 SQLite 스키마 생성 로직을 스킵합니다. 즉, sqlite provider 기준 Prisma Client가 PostgreSQL URL로 붙는 구조가 됩니다.
어떻게: Actions의 DB 모드를 현재 Prisma 스키마와 동일하게 맞추거나(권장: sqlite로 통일), PostgreSQL 모드용 Prisma schema/생성을 분리하세요.
수정 방향(권장: Actions를 sqlite로 통일)
# .github/workflows/ci.yml
env:
DATABASE_URL: file:./dev.db
# DIRECT_URL 제거 또는 동일 sqlite 값// (방어) scripts/with-database-url.mjs 또는 pretest 직전에 가드 추가
if (process.env.DATABASE_URL?.startsWith("postgres")) {
throw new Error("DATABASE_URL이 PostgreSQL인데 prisma/schema.prisma는 sqlite provider로 생성됩니다. 모드를 통일하세요.");
}🤖 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 `@prisma/schema.prisma` around lines 6 - 10, The Prisma schema is set to
provider="sqlite" (datasource db in prisma/schema.prisma) while CI injects a
PostgreSQL DATABASE_URL, causing mismatched client generation; fix by making CI
use a SQLite DATABASE_URL (e.g., set env DATABASE_URL to file:./dev.db in
.github/workflows/ci.yml and remove/align DIRECT_URL) or else create a separate
Postgres Prisma schema and generation flow, and add a defensive guard in
scripts/with-database-url.mjs (and/or scripts/ensure-sqlite-schema.mjs) that
throws if process.env.DATABASE_URL startsWith("postgres") to fail fast when
modes disagree.
Source: Coding guidelines
| it("EverytimeError 및 그 하위 에러들은 세부 타입과 상태코드를 올바르게 매핑한다", () => { | ||
| const authError = new EverytimeAuthError("Invalid ID"); | ||
| expect(authError).toBeInstanceOf(EverytimeError); | ||
| expect(authError).toBeInstanceOf(EverytimeAuthError); | ||
| expect(authError.statusCode).toBe(401); | ||
| expect(authError.code).toBe("EVERYTIME_ERROR"); | ||
|
|
||
| const fetchError = new EverytimeFetchError("Network block"); | ||
| expect(fetchError).toBeInstanceOf(EverytimeFetchError); | ||
| expect(fetchError.statusCode).toBe(500); | ||
|
|
||
| const scrapeError = new EverytimeScrapeError("Invalid share URL"); | ||
| expect(scrapeError).toBeInstanceOf(EverytimeScrapeError); | ||
| expect(scrapeError.statusCode).toBe(400); | ||
| }); |
There was a problem hiding this comment.
details.type 충돌 회귀 케이스가 빠져 있습니다.
Why: 지금 테스트는 Everytime*Error의 기본 매핑만 확인합니다. 그래서 new EverytimeError(..., { type: ... })처럼 추가 details가 들어올 때 분류 정보가 덮어써지는 버그를 못 잡습니다. 이 계층을 공통화한 목적상 이런 불변식 테스트는 바로 붙어 있어야 합니다.
How: 생성자 인수의 type이 항상 최종 details.type이 되는지 한 케이스만 추가하세요.
최소 수정 예시
it("EverytimeError 및 그 하위 에러들은 세부 타입과 상태코드를 올바르게 매핑한다", () => {
const authError = new EverytimeAuthError("Invalid ID");
expect(authError).toBeInstanceOf(EverytimeError);
expect(authError).toBeInstanceOf(EverytimeAuthError);
expect(authError.statusCode).toBe(401);
expect(authError.code).toBe("EVERYTIME_ERROR");
@@
const scrapeError = new EverytimeScrapeError("Invalid share URL");
expect(scrapeError).toBeInstanceOf(EverytimeScrapeError);
expect(scrapeError.statusCode).toBe(400);
});
+
+ it("EverytimeError는 추가 details가 있어도 생성자 type을 유지한다", () => {
+ const error = new EverytimeError("x", "AUTH", 500, {
+ type: "FETCH",
+ traceId: "t-1",
+ });
+
+ expect(error.details).toEqual({
+ traceId: "t-1",
+ type: "AUTH",
+ });
+ });
});As per coding guidelines, **/__tests__/**의 "경계값, 에러 케이스, 빈 입력 등 엣지 케이스 커버리지를 평가하세요."를 근거로 했습니다.
🤖 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/__tests__/errors.test.ts` around lines 72 - 86, Add a regression test
that constructs an EverytimeError (and/or specific subclass like
EverytimeAuthError) by passing a top-level type argument plus a details object
containing a conflicting type, then assert the final error.details.type equals
the constructor's type (not the provided details.type) and that
statusCode/instance checks still hold; specifically exercise EverytimeError's
constructor behavior (and EverytimeAuthError instantiation) to ensure the
constructor-enforced type wins over details.type.
Source: Coding guidelines
| const redirectUri = | ||
| process.env.NAVER_REDIRECT_URI || `${base}/api/auth/naver/callback`; | ||
| if (!clientId || !redirectUri) { | ||
| throw new Error( | ||
| "NAVER_CLIENT_ID 또는 NAVER_REDIRECT_URI 환경변수가 설정되지 않았습니다.", | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
redirectUri fallback 패턴으로 인한 dead code 조건 (src/lib/auth/naver.ts)
getNaverAuthUrl (Line 42-48)과 getNaverToken (Line 73-79) 모두 redirectUri에 || fallback 패턴이 적용되어 항상 truthy 값을 갖는다. 따라서 !redirectUri 조건은 도달 불가하며, 에러 메시지가 실제 검증 대상과 불일치한다. 각 함수에서 실제로 검증이 필요한 환경변수만 체크하도록 정리하면 된다.
🤖 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/auth/naver.ts` around lines 42 - 48, The error check uses redirectUri
= process.env.NAVER_REDIRECT_URI || `${base}/api/auth/naver/callback`, making
!redirectUri unreachable; update getNaverAuthUrl and getNaverToken to only
validate the actual required environment variables: in getNaverAuthUrl remove
the redundant !redirectUri check and only throw if NAVER_CLIENT_ID is missing
(reference: getNaverAuthUrl, redirectUri); in getNaverToken ensure you validate
NAVER_CLIENT_ID and NAVER_CLIENT_SECRET (reference: getNaverToken, clientId,
clientSecret) and either validate process.env.NAVER_REDIRECT_URI explicitly if
you require it or adjust the thrown message to reflect that a fallback redirect
is used.
| describe("manualAdapter — ManualCalendarAdapter class direct usage", () => { | ||
| test("toCalendarEvents를 직접 호출하여 동일한 환산 동작을 검증한다", () => { | ||
| const weekStart = new Date(2026, 4, 4, 0, 0, 0, 0); | ||
| const slots = [{ day: "MON", startHour: 9, endHour: 11 }]; | ||
|
|
||
| const result = manualAdapter.toCalendarEvents({ slots, weekStart }); | ||
| expect(result).toHaveLength(1); | ||
| expect(result[0].id).toBe("manual:0:MON-9-11"); | ||
| expect(result[0].title).toBe("가용"); | ||
| expect(result[0].startAt.getHours()).toBe(9); | ||
| expect(result[0].endAt.getHours()).toBe(11); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
테스트 슬롯 변수에 타입 명시 권장
Line 50의 slots 변수가 암시적 타입 추론에 의존. day: "MON"이 리터럴 타입으로 추론되지 않으면 TimeSlot[]과 불일치할 수 있음. 명시적 타입으로 테스트 의도를 명확히.
타입 명시 수정
- const slots = [{ day: "MON", startHour: 9, endHour: 11 }];
+ const slots: TimeSlot[] = [{ day: "MON", startHour: 9, endHour: 11 }];🤖 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/calendar/adapters/__tests__/manual.test.ts` around lines 47 - 59,
테스트의 slots 변수가 암시적 타입으로 인해 'day: "MON"' 리터럴이 유지되지 않을 수 있으니, slots를 명시적으로
TimeSlot[] 타입으로 선언해 테스트 의도를 분명히 하세요; 예를 들어 테스트 내에서 slots 변수에 대해 TimeSlot[] 타입
주석을 추가하여 'slots'와 관련된 타입 불일치(수정 대상: slots 변수 선언) 문제를 제거하고
manualAdapter.toCalendarEvents 호출이 기대한 리터럴 타입을 받도록 하세요.
🚀 작업 내용 (What)
캘린더 어댑터 및 에러 핸들링 계층을 객체 지향 상속 구조(Template Method 패턴)로 리팩터링하고 Supabase 테스트 환경 검증 버그를 수정했습니다.
상세 변경 내역 및 검증 결과 (접기/펼치기)
1) 캘린더 어댑터 상속 구조화 (Template Method Pattern)
googleAdapter,icloudAdapter등)에서 중복 구현하던 ID 생성, 타이틀 기본값 설정, 공통 속성 매핑 로직을 추상 클래스인BaseCalendarAdapter로 일원화하였습니다.ArrayCalendarAdapter를 도입했습니다.GoogleCalendarAdapter,ICloudCalendarAdapter,PhotoCalendarAdapter,ManualCalendarAdapter)를 정의하여 책임을 명확히 구분하고 중복 코드를 제거했습니다.googleAdapter,icloudAdapter,photoAdapter,manualSlotsToFreeEvents) 또한 기존 모듈에서 그대로 사용할 수 있게 보존하였습니다.2) 에러 핸들링 구조화 (Custom Error Hierarchy)
UnauthorizedError,CalDAVError,EverytimeAuthError등)들을 공통 상속 에러 클래스인MoimError하위 계층 구조로 개편하였습니다.MoimError및ExternalServiceError하위 클래스로 확장하여 API 에러 처리와 외부 서비스 에러 관리를 일관성 있게 구성하였습니다.3) Supabase 테스트 환경 검증 실패 해결
typeof window여부에만 의존하여 발생하던vitest(JSDOM) 환경의 Supabase 서버 클라이언트 검증 누락 오류를 해결했습니다.getSupabaseConfig에options: { isServer?: boolean }매개변수를 지원하여, 서버용 환경 변수 조회 시 window Mocking 값을 참조하지 않고 엄격하게 환경 변수를 검증하도록 고쳤습니다.4) 단위 테스트 및 빌드 검증 결과
MoimError계층 구조 및 신규manualAdapter를 검증하는 단위 테스트를 작성/보강하였습니다.npm run build를 통한 프로덕션 빌드 타입 검사 및 컴파일을 완벽하게 성공하였습니다.📣 핵심 변경 이유 (Why)
📸 스크린샷 (Visuals, 선택)
🔗 관련 이슈 (Issue)
Close #