feat: 로그인/회원가입 페이지 구현 (#43)#46
Conversation
📝 WalkthroughWalkthrough로그인·회원가입 페이지와 인증 콜백 라우트가 추가됐다. 이메일·비밀번호, OAuth, 이메일 OTP 가입을 지원하며, 미들웨어는 인증 상태에 따라 리다이렉트한다. 인증 성공 시 사용자 프로필을 저장하고 워크스페이스로 이동한다. Changes인증 라우팅 및 페이지 진입
로그인 및 OAuth 시작
OTP 회원가입 흐름
OAuth 콜백 및 프로필 저장
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant LoginView
participant SupabaseAuth
participant AuthCallback
participant Profiles
participant Workspaces
LoginView->>SupabaseAuth: OAuth 로그인 요청
SupabaseAuth->>AuthCallback: 인증 code 전달
AuthCallback->>SupabaseAuth: 세션 교환
AuthCallback->>Profiles: 사용자 프로필 upsert
AuthCallback->>Workspaces: 성공 리다이렉트
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 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/auth/callback/route.ts`:
- Around line 19-24: Update the profiles upsert in the auth callback so repeated
OAuth logins do not overwrite user-edited real_name or avatar_url values;
preserve existing profile fields when the record already exists while still
initializing missing values for new profiles.
- Around line 14-24: 인증 콜백의 data.user.email 비어 있음 처리를 명확히 정의하고, email이 없는 사용자는
profiles upsert 전에 안전하게 거부하거나 적절한 인증 오류 흐름으로 반환하세요. realName 계산의 비-null 단언과
profiles upsert의 email 비-null 단언을 제거해 런타임 크래시 및 NOT NULL 위반을 방지하고, 이메일이 있는 기존
경로는 그대로 유지하세요.
- Around line 5-7: Validate the next redirect in the auth callback using new
URL(next, origin), and only allow it when the parsed URL has the same origin as
origin; otherwise fall back to /workspaces. Update the redirect construction to
use the validated URL so values such as `@evil.com` cannot redirect to an external
host.
In `@src/shared/lib/middleware.ts`:
- Around line 46-50: Update the unauthenticated redirect in the middleware
condition `if (!user && !isPublic)` to append the originally requested
destination as a `redirect` query parameter before returning
`NextResponse.redirect`. Build it from the current request URL so the login flow
can restore the protected path, query string, and other relevant destination
details.
- Around line 41-57: Move the PUBLIC_PATHS and AUTH_PAGES arrays from the
middleware request-handling scope to module-level constants, while preserving
the existing isPublic and authenticated-page redirect behavior.
In `@src/views/login/ui/LoginView.tsx`:
- Around line 173-179: Update the password visibility toggle button in LoginView
to include an accessible aria-label describing its current action or state,
distinguishing between showing and hiding the password while preserving the
existing toggle behavior.
- Around line 58-70: Update signInWithGoogle and signInWithGitHub to capture the
signInWithOAuth result, check its error, and pass any failure to setError
consistently with signInWithEmail so social-login failures are shown to the
user.
- Around line 43-49: Update the email and rememberEmail state initialization in
LoginPage to use SSR-safe empty defaults, then read SAVED_EMAIL_KEY in a
mount-time useEffect and populate both states after hydration. Preserve the
current password initialization and controlled input behavior.
In `@src/views/signup/ui/SignupView.tsx`:
- Around line 146-165: Update handleInfoSubmit to trim name before any Supabase
calls, return early with the existing error state when the normalized name is
empty, and use the trimmed value for both updateUser’s full_name and
profiles.upsert’s real_name.
- Around line 156-167: SignupView의 getUser 및 profiles upsert 흐름에서 반환된 오류를 검사하고,
어느 작업이든 실패하면 오류를 표시한 뒤 즉시 중단하세요. 두 작업이 성공하고 유효한 user가 확인된 경우에만 profiles upsert를
완료한 후 setStep('done')을 호출하도록 수정하세요.
- Around line 85-97: Update signInWithGoogle and signInWithGitHub to use a
shared OAuth handler that checks the signInWithOAuth result and passes any error
to setError. Track the request state and disable both OAuth buttons while
authentication is in progress, preserving the existing provider and redirect
configuration.
- Around line 115-127: Update the signup flow around handleOtpVerify and the
authenticated /signup routing so a newly verified user remains in onboarding
until the profiles record is completed. Add or reuse a distinct onboarding path,
or make the server-side redirect check profile completion status, and preserve
the transition to the info step without redirecting to /workspaces prematurely.
🪄 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: fd437e1a-e93c-4921-a664-a993c655369d
⛔ Files ignored due to path filters (3)
public/images/auth/icon-github.svgis excluded by!**/*.svg,!public/**public/images/auth/icon-google.svgis excluded by!**/*.svg,!public/**public/images/auth/login-symbol.pngis excluded by!**/*.png,!public/**
📒 Files selected for processing (10)
src/app/auth/callback/route.tssrc/app/login/page.tsxsrc/app/signup/page.tsxsrc/middleware.tssrc/shared/lib/middleware.tssrc/views/login/index.tssrc/views/login/ui/LoginView.tsxsrc/views/signup/index.tssrc/views/signup/ui/SignupView.tsxsrc/widgets/landing/landing-header/ui/LandingHeader.tsx
| if (!user && !isPublic) { | ||
| const url = request.nextUrl.clone(); | ||
| url.pathname = '/login'; | ||
| return NextResponse.redirect(url); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
비로그인 리다이렉트 시 원래 목적지 보존 안 됨.
보호된 경로 접근 시 /login으로만 리다이렉트되고 원래 경로가 보존되지 않습니다. LoginView(다른 파일)가 로그인 성공 후 항상 /workspaces로 고정 이동한다면, 사용자는 원래 방문하려던 페이지로 돌아가지 못합니다. ?redirect= 쿼리 파라미터를 추가해 로그인 후 원래 경로로 복귀시키는 것을 고려해보세요.
🤖 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/shared/lib/middleware.ts` around lines 46 - 50, Update the
unauthenticated redirect in the middleware condition `if (!user && !isPublic)`
to append the originally requested destination as a `redirect` query parameter
before returning `NextResponse.redirect`. Build it from the current request URL
so the login flow can restore the protected path, query string, and other
relevant destination details.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/views/signup/ui/SignupView.tsx`:
- Around line 85-96: Extract the duplicated handleOAuthSignIn logic from
SignupView and LoginView into a shared useOAuthSignIn hook under src/shared/lib,
preserving the existing provider type, redirect URL, Korean error conversion,
and loading/error state transitions. Update both views to use the shared hook
and remove their local handlers.
🪄 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: 6963d0ec-c820-4e14-8faa-adaa834ed065
📒 Files selected for processing (4)
src/app/auth/callback/route.tssrc/shared/lib/middleware.tssrc/views/login/ui/LoginView.tsxsrc/views/signup/ui/SignupView.tsx
seongjinss555
left a comment
There was a problem hiding this comment.
리뷰 하나 달아드렸는데 그거 수정 후에 머지하시면 될 것 같습니다~
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/views/signup/ui/SignupView.tsx`:
- Around line 66-84: Update SignupView’s session-check flow to track a
checkingSession state initialized as true, and render only a loading/empty state
while checkSession is pending. Set checkingSession to false after the
user/profile check completes, while preserving the existing
router.push('/workspaces') and setStep('info') outcomes.
- Around line 66-84: Update the signup session-routing logic around checkSession
to match the middleware behavior: authenticated users should be routed to
/workspaces regardless of whether their profile has real_name. Remove the
profile-dependent setStep('info') branch and preserve the unauthenticated signup
flow.
🪄 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: 021bc162-9e57-41da-99b1-f4392012887b
📒 Files selected for processing (3)
src/shared/lib/use-oauth-sign-in.tssrc/views/login/ui/LoginView.tsxsrc/views/signup/ui/SignupView.tsx
| const { signInWithOAuth, oauthError, oauthLoading } = useOAuthSignIn(); | ||
|
|
||
| useEffect(() => { | ||
| const checkSession = async () => { | ||
| const { data: { user } } = await supabase.auth.getUser(); | ||
| if (!user) return; | ||
| const { data: profile } = await supabase | ||
| .from('profiles') | ||
| .select('real_name') | ||
| .eq('id', user.id) | ||
| .maybeSingle(); | ||
| if (profile?.real_name) { | ||
| router.push('/workspaces'); | ||
| } else { | ||
| setStep('info'); | ||
| } | ||
| }; | ||
| checkSession(); | ||
| }, []); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
세션 확인이 끝나기 전 email 단계 화면이 순간 노출됩니다.
step 초기값이 'email'이라 checkSession이 비동기로 완료되기 전까지 "Syncly 시작하기" 폼이 먼저 그려집니다. OTP 인증 후 새로고침 등 이 로직이 노리는 바로 그 시나리오에서 사용자가 잘못된 화면을 잠깐 보게 됩니다. 확인 중임을 나타내는 상태로 초기 렌더를 가려주는 것이 좋습니다.
🩹 제안 수정
+ const [checkingSession, setCheckingSession] = useState(true);
+
useEffect(() => {
const checkSession = async () => {
const { data: { user } } = await supabase.auth.getUser();
- if (!user) return;
+ if (!user) {
+ setCheckingSession(false);
+ return;
+ }
const { data: profile } = await supabase
.from('profiles')
.select('real_name')
.eq('id', user.id)
.maybeSingle();
if (profile?.real_name) {
router.push('/workspaces');
} else {
setStep('info');
+ setCheckingSession(false);
}
};
checkSession();
}, []);그리고 checkingSession이 true인 동안에는 아무것도(또는 로딩 스피너만) 렌더링하도록 return 문을 감싸주세요.
🤖 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/views/signup/ui/SignupView.tsx` around lines 66 - 84, Update SignupView’s
session-check flow to track a checkingSession state initialized as true, and
render only a loading/empty state while checkSession is pending. Set
checkingSession to false after the user/profile check completes, while
preserving the existing router.push('/workspaces') and setStep('info') outcomes.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/shared/lib/middleware.ts =="
cat -n src/shared/lib/middleware.ts
echo
echo "== src/middleware.ts =="
cat -n src/middleware.tsRepository: TeampleRun/syncly
Length of output: 3858
checkSession 복귀 경로를 미들웨어와 맞춰야 합니다. src/shared/lib/middleware.ts가 인증된 사용자를 /signup에서 바로 /workspaces로 보내고 있어서, 프로필이 아직 없는 상태로 돌아와 info 단계를 복구하는 이 로직은 실행되지 않습니다. signup 접근 조건을 프로필 완료 여부에 맞게 분기하세요.
🤖 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/views/signup/ui/SignupView.tsx` around lines 66 - 84, Update the signup
session-routing logic around checkSession to match the middleware behavior:
authenticated users should be routed to /workspaces regardless of whether their
profile has real_name. Remove the profile-dependent setStep('info') branch and
preserve the unauthenticated signup flow.
Source: Learnings
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/views/signup/ui/SignupView.tsx (1)
69-80: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
getUser()/maybeSingle()의error를 처리하세요. 조회 실패도 프로필 없음으로 간주되어info단계로 넘어갑니다. 실패 시에는 오류를 표시하고 현재 단계로 유지/복구해야 합니다.🤖 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/views/signup/ui/SignupView.tsx` around lines 69 - 80, Update the useEffect checkSession flow to inspect errors from both supabase.auth.getUser() and the profiles query using maybeSingle(). On either lookup failure, display the error and keep or restore the current signup stage; only treat a successful response with no user or profile as the no-profile path that advances to the info stage.
🤖 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/views/signup/ui/SignupView.tsx`:
- Line 67: Update the email submit button in SignupView to use both loading
states, setting its disabled condition to account for loading and oauthLoading.
Keep the existing email submission behavior unchanged while preventing
submission during an active OAuth flow.
---
Outside diff comments:
In `@src/views/signup/ui/SignupView.tsx`:
- Around line 69-80: Update the useEffect checkSession flow to inspect errors
from both supabase.auth.getUser() and the profiles query using maybeSingle(). On
either lookup failure, display the error and keep or restore the current signup
stage; only treat a successful response with no user or profile as the
no-profile path that advances to the info stage.
🪄 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: 3d974c2c-d94d-48ae-9f5b-bf6e771079a6
📒 Files selected for processing (1)
src/views/signup/ui/SignupView.tsx
| const [checkingSession, setCheckingSession] = useState(true); | ||
|
|
||
| const timerRef = useRef<ReturnType<typeof setInterval> | null>(null); | ||
| const { signInWithOAuth, oauthError, oauthLoading } = useOAuthSignIn(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== SignupView outline =="
ast-grep outline src/views/signup/ui/SignupView.tsx --view expanded || true
echo
echo "== Relevant lines in SignupView =="
nl -ba src/views/signup/ui/SignupView.tsx | sed -n '1,320p'
echo
echo "== OAuth hook usage =="
rg -n "useOAuthSignIn|oauthLoading|loading|handleEmailNext|disabled" src/views/signup/ui/SignupView.tsx src -g '*.ts' -g '*.tsx'Repository: TeampleRun/syncly
Length of output: 611
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Requesting file inspection for src/views/signup/ui/SignupView.tsx\n'Repository: TeampleRun/syncly
Length of output: 221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path('src/views/signup/ui/SignupView.tsx')
text = path.read_text()
lines = text.splitlines()
for start, end in [(1, 220), (220, 340)]:
print(f"\n--- lines {start}-{end} ---")
for i in range(start, min(end, len(lines)) + 1):
print(f"{i:4d}: {lines[i-1]}")
PYRepository: TeampleRun/syncly
Length of output: 14076
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path('src/shared/lib/use-oauth-sign-in.ts')
text = path.read_text()
lines = text.splitlines()
for i, line in enumerate(lines, 1):
print(f"{i:4d}: {line}")
PYRepository: TeampleRun/syncly
Length of output: 1557
이메일 제출도 OAuth 진행 중에는 비활성화하세요.
handleEmailNext가 oauthLoading을 보지 않아서 소셜 로그인 시도 중에도 OTP 요청이 들어갈 수 있습니다. 이메일 submit 버튼도 disabled={loading || oauthLoading}로 맞추면 흐름이 겹치지 않습니다.
🤖 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/views/signup/ui/SignupView.tsx` at line 67, Update the email submit
button in SignupView to use both loading states, setting its disabled condition
to account for loading and oauthLoading. Keep the existing email submission
behavior unchanged while preventing submission during an active OAuth flow.
Pull Request
작업 내용
작업 결과
/login리다이렉트, 로그인 상태에서 auth 페이지 접근 시/workspaces리다이렉트변경 사항
Added
src/views/login,src/views/signup— LoginView, SignupView 컴포넌트src/app/login/page.tsx,src/app/signup/page.tsx— 페이지 라우트src/middleware.ts— Next.js 미들웨어 진입점src/app/auth/callback/route.ts— 소셜 로그인 콜백 처리Changed
src/widgets/landing/landing-header/ui/LandingHeader.tsx— 로그인/회원가입 링크 경로 수정Fixed
실행화면
테스트
리뷰 체크리스트
feature/*->develop, 배포 시develop또는release/*->main)Type/#issue-number/description형식을 따릅니다.console.log, 주석, 임시 코드를 제거했습니다.리뷰 요청사항
src/shared/lib/middleware.ts)src/app/auth/callback/route.ts)관련 이슈
Closes #43
Summary by CodeRabbit
/signup으로 수정했습니다.