Conversation
iCloud 캘린더 연동을 위한 CalDAV 클라이언트, API 라우트, ICS 파서/빌더, 암호화 유틸 및 DB 마이그레이션 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crypto, ICS 빌더/파서, CalDAV 클라이언트/생성/쿼리 모듈의 단위 테스트 69개 추가. ICS 파서의 TZID 타임존 변환 시 오프셋 이중 적용 버그 수정. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Google OAuth 2.0 기반 캘린더 연동 (목록 조회, 일정 읽기/쓰기) 구현. 토큰 자동 갱신, 쿠키 기반 세션 관리, Zod 입력 검증 포함. 실제 Google Calendar 서버 통합 테스트 완료. 단위 테스트 30개 통과. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
package.json과 package-lock.json 불일치로 CI npm ci 실패 수정 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
체이닝된 메서드의 eslint-disable 주석 위치 문제 해결 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GeistVF.woff/GeistMonoVF.woff 파일 누락으로 빌드 실패 수정. Inter/JetBrains Mono Google Font으로 대체. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
macOS에서 생성 시 누락된 Linux optional dependencies 포함하여 재생성 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js 빌드 시 @vitejs/plugin-react 타입 에러 방지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
환경변수 미설정 CI 환경에서 정적 프리렌더링 시 에러 방지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 누락된 로컬 폰트를 Google Fonts(Inter, JetBrains Mono)로 대체 - tsconfig.json 추가 (@/* 경로 alias, vitest.config.ts exclude) - API 라우트에 force-dynamic 추가하여 프리렌더링 방지 - package-lock.json 재생성 (Linux 네이티브 바인딩 포함) - cn() 유틸리티 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@/shared/lib/supabase/server → @/lib/supabase/server 경로 수정. 빌드 통과를 위해 createClient 스켈레톤 export 추가. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat(google): Google Calendar OAuth 연동 기능 구현
feat(icloud): iCloud CalDAV 캘린더 연동 기능 구현
- CalendarEvent 표준 타입과 SearchWindow/TimeRange 정의 - DateTimeSlot, ParticipantDateAvailability 타입을 schedule.ts에 추가 - time-slot.ts: sortSlots/mergeOverlapping 구현 + Date 기반 변종 추가 - free-slots.ts: busy 이벤트를 윈도우 내 free 슬롯으로 반전 - findCommonDateSlots: durationMinutes 필터를 가진 Date 기반 교집합 - CalendarAdapter 인터페이스와 iCloud/Google/Manual 어댑터 구현 - photo 어댑터는 후속 이슈를 위한 스텁 Refs #15
- Prisma 5 + SQLite 로컬 DB 구성 및 User 모델 마이그레이션 - zod v4 기반 서버 사이드 유효성 검사 (이메일·전화번호·닉네임·비밀번호·약관) - bcryptjs를 이용한 비밀번호 해싱 저장 (평문 미저장) - email·phoneNumber·nickname 중복 시 409 응답 및 field 명시 - 전화번호 입력값 010-XXXX-XXXX 형식으로 정규화 저장 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 이메일 저장 전 trim·toLowerCase 정규화 처리 - bcryptjs.hash를 try 블록 안으로 이동하여 해싱 오류도 500 처리 - normalizePhoneNumber에 비숫자 제거 및 유효성 검사 추가, 잘못된 입력 시 예외 발생 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /api/auth/login: 이메일 또는 닉네임으로 로그인, httpOnly JWT 쿠키 발급 - GET /api/auth/me: accessToken 쿠키 검증 후 사용자 정보 반환 - POST /api/auth/logout: accessToken 쿠키 삭제 - jose 기반 JWT 서명/검증 유틸 (signAccessToken, verifyAccessToken) - bcryptjs comparePassword 유틸 분리 - zod loginSchema 작성 (loginId, password 필수 검증) - JWT payload에 userId, email, nickname만 포함 (민감정보 제외) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: 에브리타임 시간표 유효성 검사 강화 및 빌드 오류 수정
- SocialAccount 테이블 추가 (provider, providerUserId, userId FK) - User에 profileCompleted 필드 추가 (소셜 신규 가입 시 추가 정보 입력 유도) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…back) - getKakaoAuthUrl / getKakaoToken / getKakaoUser / extractKakaoUserInfo 유틸 구현 - JWT signAccessToken / verifyAccessToken (jose HS256, 7일 유효) - /api/auth/kakao/login: CSRF state 쿠키 발급 후 카카오 인가 URL 리다이렉트 - /api/auth/kakao/callback: state 검증, 토큰 교환, 신규/기존 유저 처리, JWT 쿠키 발급 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- jose 패키지 추가 (JWT 서명/검증) - .env.example에 KAKAO_REST_API_KEY, KAKAO_CLIENT_SECRET, KAKAO_REDIRECT_URI 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
애플 연동만 앱이 채택하지 않은 Supabase Auth 중심 설계 위에 구현되어 일반 로그인(커스텀 JWT) 유저는 connect에서 401, profiles FK 미충족으로 저장 실패했다. 구글/네이버와 동일하게 HttpOnly 쿠키 저장으로 전환한다. - src/lib/caldav/connection-cookie.ts 신설: 앱 전용 암호를 AES-256-GCM으로 암호화해 icloud_connection 쿠키에 저장/조회/삭제 - icloud 라우트 5개(connect/calendars/events·query/create/disconnect)에서 requireSession+Supabase 제거, 쿠키 기반으로 전환 - events 라우트 계약 calendarId(UUID) → calendarUrl로 변경 - calendar/status: Supabase/세션 게이트 제거, 쿠키 존재로 판단 (구글 분기도 빈 테이블 조회로 깨져 있어 함께 수정) - .env.example에 ENCRYPTION_SECRET 추가 (배포에 필수) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix: 애플(iCloud) 캘린더 연동을 JWT+쿠키 방식으로 재정렬 (#54)
…metadata 저장 및 자동 매핑, 가입 리디렉션 구현
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedToo many files! This PR contains 200 files, which is 50 over the limit of 150. To get a review, narrow the scope: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (44)
📒 Files selected for processing (200)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
|
@coderabbitai \src/lib/\ 디렉토리와 데이터베이스 관련 변경 사항에 대한 세부 리뷰를 요청합니다.
|
|
@coderabbitai \src/app/\ 디렉토리(특히 인증 및 로그인 리디렉션 흐름, \signup/page.tsx, \login/page.tsx, /api/auth/callback\ 등)에 대한 세부 리뷰를 요청합니다.
|
|
@coderabbitai UI 컴포넌트(\src/components/)와 모바일 조작 스크롤 간섭 제어, 그리고 Playwright E2E 테스트(\e2e/) 코드에 대한 세부 리뷰를 요청합니다.
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## Repository root files"
fd -d 2 '^(middleware|package|next\.config|tsconfig|supabase|README)' . || true
echo
echo "## Target auth/app files"
fd -i '^(page|route|middleware)\.(tsx|ts|js|jsx)$' . | rg -n '(^|/)src/(app|middleware)|signup|login|callback|auth|middleware' || true
echo
echo "## Supabase/auth utility files"
fd -i '(supabase|auth|middleware|session|redirect|callback).*\.(ts|tsx|js|jsx)$' . | sort || true
echo
echo "## Relevant source tree under src/app (auth/login/signup/callback only)"
fd . src/app -t f 2>/dev/null | rg -n '/(auth|login|signup|api/auth|callback)|middleware' || true
echo
echo "## Read target files if present"
for f in \
src/app/signup/page.tsx \
src/app/login/page.tsx \
src/app/api/auth/callback/route.ts \
src/middleware.ts \
middleware.ts
do
if [ -f "$f" ]; then
echo
echo "===== $f ====="
nl -ba "$f" | sed -n '1,260p'
fi
done
echo
echo "## Search auth redirect/session/cookie usage with context"
rg -n -C 3 --type-add 'tsx:*.tsx' --type-add 'ts:*.ts' \
'(signInWithOAuth|exchangeCodeForSession|getUser|getSession|cookies\(|redirectTo|next=|redirect=|NextResponse\.redirect|middleware|createServerClient|createBrowserClient|supabase\.auth|auth\.callback|callbackUrl|returnTo|origin|URLSearchParams)' \
src middleware.ts 2>/dev/null || trueLength of output: 4616 🏁 Script executed: #!/bin/bash
set -euo pipefail
print_file() {
f="$1"
if [ -f "$f" ]; then
echo
echo "===== $f ====="
awk '{printf "%5d %s\n", NR, $0}' "$f" | sed -n '1,320p'
else
echo "MISSING: $f"
fi
}
for f in \
'src/app/(auth)/signup/page.tsx' \
'src/app/(auth)/login/page.tsx' \
'src/app/(auth)/signup/additional-info/page.tsx' \
'src/app/api/auth/callback/route.ts' \
'src/app/api/auth/login/route.ts' \
'src/app/api/auth/signup/route.ts' \
'src/app/api/auth/logout/route.ts' \
'src/app/api/auth/me/route.ts' \
'src/app/api/auth/profile/complete/route.ts' \
'src/lib/auth/session.ts' \
'src/middleware.ts'
do
print_file "$f"
done
echo
echo "## Supabase client helpers"
fd -i 'supabase.*\.(ts|tsx|js|jsx)$' src | while read -r f; do print_file "$f"; done
echo
echo "## Auth/social component usage"
print_file 'src/components/moim/auth-social.tsx'
echo
echo "## Cross-file auth redirect/cookie references"
rg -n -C 4 --type-add 'tsx:*.tsx' --type-add 'ts:*.ts' \
'(signInWithOAuth|exchangeCodeForSession|getUser|getSession|setSession|refreshSession|cookies\(|redirectTo|next=|redirect=|NextResponse\.redirect|createServerClient|createBrowserClient|createMiddlewareClient|supabase\.auth|URLSearchParams|searchParams|callbackUrl|returnTo|origin|x-forwarded|DEV_BYPASS|dev-bypass)' \
src 2>/dev/null || trueLength of output: 122412 🏁 Script executed: #!/bin/bash
set -euo pipefail
print_file() {
f="$1"
if [ -f "$f" ]; then
echo
echo "===== $f ====="
awk '{printf "%5d %s\n", NR, $0}' "$f" | sed -n '1,260p'
else
echo "MISSING: $f"
fi
}
echo "## signup tail with social OAuth handler"
if [ -f 'src/app/(auth)/signup/page.tsx' ]; then
awk '{printf "%5d %s\n", NR, $0}' 'src/app/(auth)/signup/page.tsx' | sed -n '320,390p'
fi
for f in \
'src/lib/supabase/server.ts' \
'src/lib/supabase/client.ts' \
'src/lib/supabase/env.ts' \
'src/app/api/auth/dev-bypass/route.ts' \
'src/features/auth/signup.schema.ts' \
'src/features/auth/login.schema.ts' \
'src/features/auth/social-profile.schema.ts'
do
print_file "$f"
doneLength of output: 14904
핵심 결론
발견 사항1. [High]
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== src/lib/ directory structure ==="
find src/lib -type f | sort
echo ""
echo "=== src/app/api directory structure ==="
find src/app/api -type f | sort 2>/dev/null || echo "not found"
echo ""
echo "=== Search for addParticipantAvailability / upsert ==="
rg -rn "addParticipantAvailability\|upsert" src/ --type ts -A 5 -B 2
echo ""
echo "=== Search for participant submission API routes ==="
rg -rn "participant\|availability\|submit" src/app/api --type ts -l 2>/dev/null || echo "not found"Length of output: 3849 🏁 Script executed: #!/bin/bash
echo "=== All lib files content ==="
find src/lib -type f -name "*.ts" | sort | while read f; do
echo "======= FILE: $f ======="
cat "$f"
echo ""
doneLength of output: 192793 🏁 Script executed: #!/bin/bash
echo "=== API routes for participant/availability/schedule ==="
find src/app/api -type f -name "*.ts" | sort | while read f; do
echo "======= FILE: $f ======="
cat "$f"
echo ""
doneLength of output: 53703 🏁 Script executed: #!/bin/bash
echo "=== Search for supabase upsert calls ==="
rg -rn "\.upsert\(" src/ --type ts -A 8 -B 3
echo ""
echo "=== Search for transaction or BEGIN/COMMIT ==="
rg -rn "BEGIN\|COMMIT\|ROLLBACK\|rpc\|transaction" src/ --type ts -A 3 -B 2
echo ""
echo "=== Search for validation schemas (zod/yup/joi) ==="
rg -rn "z\.object\|z\.string\|schema\|validate\|safeParse\|parse(" src/ --type ts -lLength of output: 1792
🔴 Critical — 동시성 / Race Condition1.
|
| 시각 | 요청 A | 요청 B |
|---|---|---|
| T1 | findFirst → null |
|
| T2 | findFirst → null |
|
| T3 | create 성공 |
|
| T4 | create 성공 → 중복 참여자 |
해결 방법: Prisma 스키마에 @@unique([scheduleId, name]) 제약을 추가하고, upsert로 전환하거나 create 실패 시 고유 제약 위반을 catch하도록 처리해야 합니다.
// Prisma schema에 추가 (prisma/schema.prisma)
model ScheduleParticipant {
// ...
@@unique([scheduleId, name])
}// store.ts: upsert 방식으로 교체
return tx.scheduleParticipant.upsert({
where: { scheduleId_name: { scheduleId, name: normName } },
create: {
id: createToken(12),
scheduleId,
name: normName,
available: JSON.stringify(normalizeAvailability(schedule, input.available)),
},
update: {
available: JSON.stringify(normalizeAvailability(schedule, input.available)),
submittedAt: new Date(),
},
});2. updateMany 쓰기 잠금 패턴이 의미론적으로 취약
const updateResult = await tx.schedule.updateMany({
where: { id: scheduleId, status: "open" },
data: { status: "open" }, // 실제 변경 없음 — "터치" 전략
});status: "open" → status: "open" 쓰기는 PostgreSQL에서 행 수정으로 인식되어 직렬화 충돌 감지에 도움이 되지만, 일부 ORM/드라이버 최적화 또는 DB 버전에서 no-op 쓰기를 스킵할 수 있습니다. status: "processing" 같은 임시 상태로 실제 변경 후 롤백하거나, 별도 버전 카운터 컬럼(version)을 활용한 Optimistic Lock 패턴이 더 안전합니다.
🟠 High — 유효성 검증 / 에러 처리
3. API 라우트에서 요청 본문 스키마 검증 누락
파일: src/app/api/schedules/[id]/availability/route.ts
const participant = await addParticipantAvailability(
id,
await request.json(), // ← null, [], 또는 비객체가 그대로 전달됨
);request.json()이 반환할 수 있는 null, 배열, 원시형이 store.ts의 normalizeParticipantName(input.name) 등에서 TypeScript 타입 안전성 없이 처리됩니다. Zod 스키마 검증을 API 레이어에 추가해야 합니다.
import { z } from "zod";
const AvailabilitySchema = z.object({
name: z.string().min(1).max(40),
available: z.array(z.object({
day: z.enum(["MON","TUE","WED","THU","FRI","SAT","SUN"]),
startHour: z.number().int().min(0).max(23),
endHour: z.number().int().min(1).max(24),
})).min(1),
});
// POST 핸들러 내부:
const parsed = AvailabilitySchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
}
const participant = await addParticipantAvailability(id, parsed.data);4. 내부 에러 메시지 클라이언트 노출 (에러 메시지 유출)
파일: src/app/api/schedules/[id]/availability/route.ts, src/app/api/schedules/[id]/route.ts
const message = error instanceof Error ? error.message : "invalid request";
// Prisma 내부 오류, DB 연결 오류 메시지가 그대로 응답에 포함됨
return NextResponse.json({ error: message }, { status });Prisma 에러(PrismaClientKnownRequestError 등)나 내부 로직 오류 메시지가 클라이언트에 노출됩니다. 에러 타입을 구분하여 사용자 친화적 메시지만 반환하고 상세 내용은 서버 로그에만 기록해야 합니다.
} catch (error) {
if (error instanceof Error) {
const knownMessages = ["schedule not found", "schedule is not open", "availability must"];
const isKnown = knownMessages.some(m => error.message.includes(m));
if (isKnown) {
return NextResponse.json({ error: error.message }, { status: error.message === "schedule not found" ? 404 : 400 });
}
}
console.error("[availability] 서버 오류:", error);
return NextResponse.json({ error: "서버 오류가 발생했습니다." }, { status: 500 });
}🟡 Medium — 보안 / 데이터 안정성
5. dev-bypass 쿠키에 httpOnly 플래그 누락
파일: src/app/api/auth/dev-bypass/route.ts
res.cookies.set("e2e_mock_uid", user.id, {
path: "/",
maxAge: 60 * 60 * 24,
// httpOnly: true 누락 — JavaScript에서 접근 가능
});E2E 모드라도 세션 쿠키에 httpOnly: true를 추가해야 XSS로 인한 세션 탈취를 방지합니다.
6. 개발 모드에서 잔여 E2E 쿠키로 인한 인증 우회
파일: src/lib/supabase/server.ts
if (process.env.E2E_TEST === "true" || process.env.NODE_ENV === "development") {
const mockUid = cookieStore.get("e2e_mock_uid")?.value;
const mockEmail = cookieStore.get("e2e_mock_email")?.value;
if (process.env.E2E_TEST === "true" || (mockUid && mockEmail)) {
// ← 개발 모드에서 이전 테스트 잔여 쿠키만 있어도 목 인증 활성화개발 환경에서 이전 E2E 테스트 쿠키가 브라우저에 남아있으면 실제 Supabase 인증 없이 목 유저로 동작합니다. E2E_TEST === "true" 조건만으로 목 활성화를 제한하는 것을 권장합니다.
// 수정 제안: E2E_TEST 환경변수로만 제한
if (process.env.E2E_TEST === "true") {
// 목 인증 처리
}7. reset-password/complete — 빈 refresh_token 전달
파일: src/app/api/auth/reset-password/complete/route.ts
await supabase.auth.setSession({
access_token: token,
refresh_token: "", // ← 빈 문자열
});Supabase SDK 버전에 따라 빈 refresh_token으로 setSession 호출 시 예기치 않은 동작이 발생할 수 있습니다. 비밀번호 재설정 토큰 처리는 verifyOtp 또는 exchangeCodeForSession을 활용하는 것이 공식 권장 방식입니다.
🔵 Low — 코드 품질 / 잠재 위험
8. clearSchedules() — 프로덕션 가드 없음
export async function clearSchedules() {
await prisma.$transaction([
prisma.scheduleParticipant.deleteMany(), // 전체 삭제
prisma.schedule.deleteMany(), // 전체 삭제
]);
}테스트 유틸리티 함수가 export되어 있어 실수로 프로덕션 코드에서 호출될 위험이 있습니다. 테스트 전용 파일로 분리하거나, 환경 가드를 추가하는 것이 안전합니다.
export async function clearSchedules() {
if (process.env.NODE_ENV === "production") {
throw new Error("clearSchedules()는 프로덕션에서 사용할 수 없습니다.");
}
// ...
}9. createAdminClient — 모듈 레벨 싱글턴 캐싱
파일: src/lib/supabase/admin.ts
let cached: SupabaseClient | null = null;
export function createAdminClient(): SupabaseClient {
if (cached) return cached;
// ...
cached = createClient(url, serviceRoleKey, ...);
return cached;
}환경변수가 런타임에 교체되는 테스트 환경이나 핫리로딩 시 구 설정이 사용될 수 있습니다. Next.js의 서버 컴포넌트 환경에서는 허용되지만, 의도를 명시하는 주석 추가를 권장합니다.
요약 테이블
| 심각도 | 이슈 | 파일 |
|---|---|---|
| 🔴 Critical | DB 유니크 제약 없는 upsert 경쟁 조건 | src/lib/schedules/store.ts |
| 🔴 Critical | updateMany no-op 잠금 패턴 취약성 |
src/lib/schedules/store.ts |
| 🟠 High | API 레이어 요청 스키마 검증 누락 | src/app/api/schedules/[id]/availability/route.ts |
| 🟠 High | 내부 에러 메시지 클라이언트 노출 | src/app/api/schedules/[id]/availability/route.ts, [id]/route.ts |
| 🟡 Medium | 세션 쿠키 httpOnly 누락 |
src/app/api/auth/dev-bypass/route.ts |
| 🟡 Medium | 개발 모드 E2E 쿠키 잔존 시 인증 우회 | src/lib/supabase/server.ts |
| 🟡 Medium | 빈 refresh_token으로 세션 설정 |
src/app/api/auth/reset-password/complete/route.ts |
| 🔵 Low | clearSchedules 프로덕션 가드 없음 |
src/lib/schedules/store.ts |
| 🔵 Low | 관리자 클라이언트 싱글턴 캐싱 | src/lib/supabase/admin.ts |
가장 우선적으로 처리해야 할 항목은 ① DB 유니크 제약 추가 + upsert 전환 (중복 참여자 방지)과 ② API 레이어 Zod 스키마 검증 (비정상 입력 방어)입니다. 궁금한 점이 있으시면 말씀해 주세요!
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find grid/schedule related components with touch event handling
echo "=== Touch event related files ==="
rg -rn "touchmove|touchstart|touchend|onTouchMove|onTouchStart|onTouchEnd|passive" src/components/ --type tsx -l 2>/dev/null || \
rg -rn "touchmove|touchstart|touchend|onTouchMove|onTouchStart|onTouchEnd|passive" src/components/ -l
echo ""
echo "=== Grid component files ==="
fd -e tsx -e ts "grid|Grid|schedule|Schedule|timetable|Timetable" src/components/
echo ""
echo "=== E2E test files ==="
fd -e ts -e spec.ts "host-flow|participant-flow" e2e/
fd -e ts "spec" e2e/Length of output: 859 🏁 Script executed: #!/bin/bash
# Read E2E test files
echo "=== host-flow.spec.ts ==="
cat e2e/host-flow.spec.ts 2>/dev/null || fd "host-flow" e2e/ --exec cat {}
echo ""
echo "=== participant-flow.spec.ts ==="
cat e2e/participant-flow.spec.ts 2>/dev/null || fd "participant-flow" e2e/ --exec cat {}Length of output: 5592 🏁 Script executed: #!/bin/bash
# Find touch event handling in components
rg -rn "touchmove|touchstart|touchend|addEventListener|removeEventListener|passive" src/components/ -A 3 -B 1
echo ""
echo "=== All component files with drag logic ==="
rg -rn "isDragging|onMouseDown|onMouseMove|onTouchStart|dragStart" src/components/ -lLength of output: 597 🏁 Script executed: #!/bin/bash
echo "=== Directory structure of src/components/ ==="
tree src/components/ 2>/dev/null || find src/components/ -type f | head -60
echo ""
echo "=== Search for touch events anywhere in src/ ==="
rg -rn "touchmove|touchstart|touchend|addEventListener.*touch|passive.*false" src/ -l
echo ""
echo "=== Search for drag logic anywhere in src/ ==="
rg -rn "isDragging|dragStart|onMouseDown.*slot|onTouchStart.*slot|useRef.*drag" src/ -lLength of output: 994 🏁 Script executed: #!/bin/bash
echo "=== Full src directory structure ==="
find src/ -type f -name "*.tsx" -o -name "*.ts" | grep -v "node_modules" | sort | head -80
echo ""
echo "=== Playwright config ==="
cat playwright.config.ts 2>/dev/null || cat playwright.config.js 2>/dev/nullLength of output: 5094 🏁 Script executed: #!/bin/bash
echo "=== CreateScheduleClient.tsx ==="
cat src/app/schedule/create/CreateScheduleClient.tsx
echo ""
echo "=== ScheduleRoomClient.tsx ==="
cat src/app/schedule/[id]/ScheduleRoomClient.tsxLength of output: 83418
🟥 터치 드래그 — 치명적 Race Condition (양 파일 공통)문제:
|
| 심각도 | 위치 | 이슈 |
|---|---|---|
| 🟥 치명 | 양 파일 | preventDefaultTouch closure stale → 첫 touchmove에서 스크롤 방지 실패 |
| 🟧 높음 | CreateScheduleClient | 중복 mouseup 글로벌 리스너 |
| 🟧 높음 | host-flow.spec.ts | waitForTimeout 남용 + step 전환 미검증 |
| 🟧 높음 | host-flow.spec.ts | 비밀번호 input 인덱스 셀렉터 |
| 🟨 중간 | participant-flow.spec.ts | 제출 후 고정 2초 대기 |
| 🟨 중간 | 양 E2E 파일 | 테스트 데이터 cleanup 없음 |
| 🟩 낮음 | 양 파일 | 개별 셀 onTouchMove → 컨테이너로 통합 권장 |
[PR] MOIM V1 서비스 공식 런칭 및 UX/UI 고도화 통합 반영 (
dev->main)📌 개요
본 Pull Request는
dev브랜치에서 진행된 MOIM V1 전체 개발 내역을main브랜치에 병합하여 실서비스(Production) 환경에 배포하기 위한 통합 PR입니다.인증, 일정 조율 알고리즘, 외부 캘린더 연동, 모바일 UX 고도화, 그리고 최신 QA 피드백 결함 해결 사항이 모두 포함되어 있습니다.
🛠️ 주요 반영 사항 및 해결된 이슈
1. 인증 및 회원 관리 (Authentication & Onboarding)
redirect혹은next쿼리 파라미터를 파싱하여 유저가 원래 있던 흐름(예: 모임 저장 후 대시보드)으로 안전하게 진입하도록 보장합니다./signup) 내 소셜 로그인 버튼들이 잘못된 API 라우트로 매핑되어 있던 현상을 수정하고, 로그인 화면과 동일하게 클라이언트 사이드signInWithOAuth핸들러로 연동하여 안정성을 확보했습니다.2. 일정 생성 및 조율 동선 고도화 (Host Creation Flow)
CreateScheduleClient)로 구조화하여 주최자가 단계별로 명확하게 정보를 입력하도록 유도합니다.hostPath에 고유hostToken쿼리 파라미터를 추가하여, 참여자용 초대 링크와 호스트용 결과 화면을 완전히 격리했습니다.creatorId === user.id)인 경우 대시보드 및 모임방에서 호스트 권한을 보장하도록 API 인가 처리를 확장했습니다.3. 외부 캘린더 및 시간표 연동 (Calendar & Timetable Sync)
slots)를 Supabaseuser_metadata에 영구 보존하여 사용자가 기기를 이동해도 연동 상태를 온전하게 복원할 수 있습니다.4. 모바일 사용성 및 프리미엄 UX/UI 개선 (Mobile UX & Micro-Animations)
touchmove리스너(passive: false)를 연동했습니다.SubmissionDonePanel)에 라운딩 처리 및 프리미엄 섀도우를 적용하고 가입 유도 영역에 그라데이션 및 버튼 hover/click 마이크로 인터랙션을 반영했습니다.5. 데이터 안정성 및 멱등성 보장 (Data Consistency)
upsert): 모임 참여자가 동일한 이름으로 여러 번 일정을 수정 제출하거나 페이지를 새로고침할 때 발생하던 중복 참여자 누적 및 500 DB 에러를upsert로직(기존 제출 건 update)으로 해결했습니다.🧪 검증 결과 요약
1. 프로덕션 빌드 성공
npm run build결과 15개 정적/동적 라우트가 타입 에러 없이 성공적으로 최적화 빌드 완료되었습니다.2. 테스트 스위트 전체 통과
over_email_send_rate_limit)을 우회할 수 있도록 E2E 테스트 모드 전용 Mocking 세션 쿠키 제어를 설계하여 테스트 안정성을 확보했습니다.✅ 체크리스트