Skip to content

[JDDEV-26] [JDDEV-30] [JDDEV-33] Feat: 기본 로그인/회원가입/구글 로그인 API 연결#59

Merged
minnngo merged 3 commits into
developfrom
feature/JDDEV-26-basic_login-api-feat
May 22, 2026
Merged

[JDDEV-26] [JDDEV-30] [JDDEV-33] Feat: 기본 로그인/회원가입/구글 로그인 API 연결#59
minnngo merged 3 commits into
developfrom
feature/JDDEV-26-basic_login-api-feat

Conversation

@minnngo
Copy link
Copy Markdown
Collaborator

@minnngo minnngo commented May 22, 2026

🔗 관련 이슈

  • JDDEV-26
  • JDDEV-30
  • JDDEV-33

📝 개요

  • 로그인/회원가입/Google OAuth 인증 API를 백엔드와 연결했습니다.
  • 로그인 성공 시 토큰과 사용자 이메일을 저장하고 /apply로 이동하도록 처리했습니다.
  • LNB 하단에 로그인한 사용자 이메일과 이메일 이니셜이 표시되도록 수정했습니다.

⌨️ 작업 상세 내용

  • 일반 로그인 API 연결
  • 회원가입 이메일 인증번호 발송/검증 및 회원가입 API 연결
  • 회원가입 API 요청값에 맞춰 이름 입력 필드 추가
  • Google OAuth 시작 URL 연결 및 /oauth2/redirect 콜백 페이지 추가
  • accessToken, refreshToken, 로그인 이메일 localStorage 저장
  • LNB 하단 이메일/이니셜을 로그인 사용자 정보 기준으로 표시

💡 코드 설명 및 참고사항

인증 관련 API 호출 로직은 src/lib/auth.ts에 분리했습니다.
백엔드 기본 주소는 https://api.jobdri.site를 사용하며, 필요 시 NEXT_PUBLIC_API_BASE_URL로 대체할 수 있습니다.
Google OAuth는 백엔드에서 프론트 콜백으로 accessToken, refreshToken을 query string으로 전달하는 흐름을 기준으로 구현했습니다.

🔍 리뷰 요구사항 (Reviewers)

  • 로그인, 회원가입, Google 로그인 기능 모두 정상 동작하는지 확인 부탁해용..
  • Google 로그인은 이따 머지하고 푸쉬한 후에 Vercel에서 확인 부탁..!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

This PR establishes OAuth2 redirect handling and comprehensive email-based authentication. It adds a new auth library with API integration, token persistence, and JWT parsing; implements OAuth2 callback handling with conditional routing; enhances the login/signup screen with async flows and error management; and syncs authenticated user email across the sidebar component.

Changes

OAuth2 and Email Authentication Integration

Layer / File(s) Summary
Auth library foundation
jobdri/src/lib/auth.ts
Introduces centralized authentication module with API base URL, storage keys for tokens/email, request/response types, AuthApiError class, postAuth helper for API calls, endpoint wrappers (loginWithEmail, sendEmailVerification, confirmEmailVerification, signupWithEmail), token persistence/retrieval, JWT email extraction, and Google OAuth URL generation.
Route constants
jobdri/src/constants/routes.ts
Adds OAUTH_REDIRECT route constant mapping to /oauth2/redirect.
OAuth2 redirect handler
jobdri/src/app/oauth2/redirect/OAuthRedirectClient.tsx, jobdri/src/app/oauth2/redirect/page.tsx
OAuthRedirectClient parses query parameters, derives user message, saves tokens and redirects to apply route on success, or schedules redirect to login after 2 seconds if tokens missing. OAuthRedirectPage wraps client component in Suspense with loading fallback.
Email login/signup and verification flows
jobdri/src/components/login/EmailLoginScreen.tsx
Replaces placeholder handlers with async flows: login validates inputs, calls loginWithEmail, saves tokens, and routes to apply; signup sends verification code then collects user input; verification confirms code, signs up user, and saves tokens. Adds error messages, submission/resend state flags, input disabling during submission, and per-step error rendering. Google login initiates OAuth flow via getGoogleAuthorizationUrl().
Sidebar email display sync
jobdri/src/components/common/lnb/Lnb.tsx
Updates Lnb to derive displayed email from prop, browser storage (useSyncExternalStore), or default fallback. Computes email initial from display value and renders initial in avatar and email in expanded sidebar.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant OAuthRedirectPage
    participant OAuthRedirectClient
    participant AuthLib
    participant Router
    Browser->>OAuthRedirectPage: Load redirect page
    OAuthRedirectPage->>OAuthRedirectClient: Render with Suspense
    OAuthRedirectClient->>OAuthRedirectClient: Parse query params
    alt Has accessToken and refreshToken
        OAuthRedirectClient->>AuthLib: saveAuthTokens()
        AuthLib->>Browser: Store tokens in localStorage
        OAuthRedirectClient->>Router: router.replace(ROUTES.APPLY)
        Router->>Browser: Redirect to /apply
    else Missing tokens
        OAuthRedirectClient->>OAuthRedirectClient: Schedule redirect
        OAuthRedirectClient->>Router: router.replace(ROUTES.LOGIN) after 2s
        Router->>Browser: Redirect to /login
    end
Loading
sequenceDiagram
    participant User
    participant EmailLoginScreen
    participant AuthLib
    participant Router
    User->>EmailLoginScreen: Enter email and password
    alt Login flow
        EmailLoginScreen->>EmailLoginScreen: Validate inputs
        EmailLoginScreen->>AuthLib: loginWithEmail(credentials)
        AuthLib->>AuthLib: POST to /auth/login
        AuthLib->>EmailLoginScreen: Return tokens or error
        alt Success
            EmailLoginScreen->>AuthLib: saveAuthTokens(tokens)
            EmailLoginScreen->>Router: router.replace(ROUTES.APPLY)
        else Error
            EmailLoginScreen->>EmailLoginScreen: Display loginErrorMessage
        end
    else Signup flow
        EmailLoginScreen->>EmailLoginScreen: Validate name, email, password
        EmailLoginScreen->>AuthLib: sendEmailVerification(email)
        AuthLib->>EmailLoginScreen: Success or error
        EmailLoginScreen->>EmailLoginScreen: Switch to verification mode
        User->>EmailLoginScreen: Enter verification code
        EmailLoginScreen->>AuthLib: confirmEmailVerification(code)
        EmailLoginScreen->>AuthLib: signupWithEmail(credentials)
        alt Success
            EmailLoginScreen->>AuthLib: saveAuthTokens(tokens)
            EmailLoginScreen->>Router: router.replace(ROUTES.APPLY)
        else Error
            EmailLoginScreen->>EmailLoginScreen: Display verificationErrorMessage
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A new auth path emerges,
OAuth tokens dance and converge,
Email flows gently through the screen,
With tokens stored, not yet seen,
Sidebar glows with user's email keen!

🚥 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 The title clearly summarizes the main changes: connecting login, signup, and Google OAuth APIs. It directly maps to the PR's primary objectives and reflects the significant implementation work across authentication flows.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/JDDEV-26-basic_login-api-feat

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
jobdri/src/components/common/lnb/Lnb.tsx (1)

40-48: ⚡ Quick win

storage-only subscription ignores same-tab userEmail writes, but current auth routing likely avoids the stale-case
Lnb.tsx subscribes only to the storage event (lines 40-48). userEmail is only written in saveAuthTokens (localStorage.setItem) and saveAuthTokens is only called from jobdri/src/components/login/EmailLoginScreen.tsx (/login) and jobdri/src/app/oauth2/redirect/OAuthRedirectClient.tsx (/oauth2/redirect). LayoutShell only renders Lnb for LAYOUT_ROUTES (/ and /credit), so during /login and /oauth2/redirect the useSyncExternalStore subscription isn’t mounted—when routing reaches /apply, Lnb mounts fresh via MockApplicationHomePageClient and the initial snapshot should be current.
Recommend adding a same-tab notification (e.g., custom event) if userEmail can change while Lnb remains mounted (future in-app email changes).

🤖 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 `@jobdri/src/components/common/lnb/Lnb.tsx` around lines 40 - 48,
subscribeToStoredEmail currently only listens to the window "storage" event so
it misses same-tab updates to AUTH_STORAGE_KEYS.userEmail; update the sync
subscription to also handle a custom same-tab event: add a listener in
subscribeToStoredEmail (function subscribeToStoredEmail) for a custom event name
(e.g., "userEmailChanged") that calls onStoreChange, and remove that listener in
the cleanup alongside the existing window.removeEventListener("storage", ...).
Then update saveAuthTokens (where
localStorage.setItem(AUTH_STORAGE_KEYS.userEmail, ... ) is called) to dispatch
the same custom event after writing to localStorage so Lnb (and other same-tab
subscribers) receive immediate updates.
🤖 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 `@jobdri/src/app/oauth2/redirect/OAuthRedirectClient.tsx`:
- Around line 11-35: The component OAuthRedirectClient must stop reading
accessToken/refreshToken from URL query (avoid
saveAuthTokens/getEmailFromAccessToken usage) and instead read an authorization
code (e.g., "code" and optional "state") and call a backend endpoint to exchange
that code server-side so the server issues HttpOnly cookies; update the
useEffect in OAuthRedirectClient to POST the code to an API (e.g.,
/auth/oauth/callback), handle success by router.replace(ROUTES.APPLY) and
failure by router.replace(ROUTES.LOGIN), and remove any client-side storage of
raw tokens; keep getEmailFromAccessToken only if used elsewhere but do not use
it here, and ensure the redirect replaces history (router.replace) so the code
param is not retained.

In `@jobdri/src/components/login/EmailLoginScreen.tsx`:
- Around line 236-241: The login validation currently blocks submission using
the signup password regex by checking passwordPattern.test(password); remove
that check from the EmailLoginScreen validation and replace it with a simple
presence (and/or minimal length) check (e.g., password.length > 0 or >= 6) so
existing users with legacy passwords are not rejected; update the conditional
that uses isLoginSubmitting, isLoginReady, emailPattern, passwordPattern (and
any variable named password) to only use emailPattern for format validation and
a simple non-empty/min-length check for password, keeping variable names
unchanged.

In `@jobdri/src/lib/auth.ts`:
- Around line 172-174: The code only writes AUTH_STORAGE_KEYS.userEmail when the
variable email is truthy, leaving a stale value if email becomes undefined; in
the function containing the snippet, add an else branch to clear the key (e.g.,
window.localStorage.removeItem(AUTH_STORAGE_KEYS.userEmail) or setItem with an
empty string) so that when email is falsy the stored userEmail is removed;
reference the existing AUTH_STORAGE_KEYS.userEmail, window.localStorage.setItem
call, and the email variable to locate and update the logic.

---

Nitpick comments:
In `@jobdri/src/components/common/lnb/Lnb.tsx`:
- Around line 40-48: subscribeToStoredEmail currently only listens to the window
"storage" event so it misses same-tab updates to AUTH_STORAGE_KEYS.userEmail;
update the sync subscription to also handle a custom same-tab event: add a
listener in subscribeToStoredEmail (function subscribeToStoredEmail) for a
custom event name (e.g., "userEmailChanged") that calls onStoreChange, and
remove that listener in the cleanup alongside the existing
window.removeEventListener("storage", ...). Then update saveAuthTokens (where
localStorage.setItem(AUTH_STORAGE_KEYS.userEmail, ... ) is called) to dispatch
the same custom event after writing to localStorage so Lnb (and other same-tab
subscribers) receive immediate updates.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e958f1a-1079-4007-acc1-465c2fcf9cae

📥 Commits

Reviewing files that changed from the base of the PR and between 6b1da3c and 8b9b534.

📒 Files selected for processing (6)
  • jobdri/src/app/oauth2/redirect/OAuthRedirectClient.tsx
  • jobdri/src/app/oauth2/redirect/page.tsx
  • jobdri/src/components/common/lnb/Lnb.tsx
  • jobdri/src/components/login/EmailLoginScreen.tsx
  • jobdri/src/constants/routes.ts
  • jobdri/src/lib/auth.ts

Comment on lines +11 to +35
const accessToken = searchParams.get("accessToken");
const refreshToken = searchParams.get("refreshToken");
const error = searchParams.get("error");
const errorMessage = searchParams.get("message");
const message =
accessToken && refreshToken
? "Google 로그인 처리 중입니다."
: errorMessage ||
(error ? "Google 로그인에 실패했습니다." : "로그인 정보를 확인할 수 없습니다.");

useEffect(() => {
if (accessToken && refreshToken) {
saveAuthTokens(
{ accessToken, refreshToken },
searchParams.get("email") ||
getEmailFromAccessToken(accessToken) ||
undefined,
);
router.replace(ROUTES.APPLY);
return;
}

const timerId = window.setTimeout(() => {
router.replace(ROUTES.LOGIN);
}, 2000);
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 | 🟠 Major | 🏗️ Heavy lift

Avoid query-string token delivery in OAuth callback.

Reading accessToken/refreshToken from URL query on Line 11-12 exposes tokens to browser history and potential referrer leakage. This flow should move to a safer contract (e.g., backend-set HttpOnly cookies or one-time code exchange).

🛡️ Immediate client-side mitigation (partial)
   useEffect(() => {
     if (accessToken && refreshToken) {
+      window.history.replaceState(null, "", ROUTES.OAUTH_REDIRECT);
       saveAuthTokens(
         { accessToken, refreshToken },
         searchParams.get("email") ||
           getEmailFromAccessToken(accessToken) ||
           undefined,
       );
🤖 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 `@jobdri/src/app/oauth2/redirect/OAuthRedirectClient.tsx` around lines 11 - 35,
The component OAuthRedirectClient must stop reading accessToken/refreshToken
from URL query (avoid saveAuthTokens/getEmailFromAccessToken usage) and instead
read an authorization code (e.g., "code" and optional "state") and call a
backend endpoint to exchange that code server-side so the server issues HttpOnly
cookies; update the useEffect in OAuthRedirectClient to POST the code to an API
(e.g., /auth/oauth/callback), handle success by router.replace(ROUTES.APPLY) and
failure by router.replace(ROUTES.LOGIN), and remove any client-side storage of
raw tokens; keep getEmailFromAccessToken only if used elsewhere but do not use
it here, and ensure the redirect replaces history (router.replace) so the code
param is not retained.

Comment on lines 236 to 241
if (
isLoginSubmitting ||
!isLoginReady ||
!emailPattern.test(email) ||
!passwordPattern.test(password)
) {
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 | 🟠 Major | ⚡ Quick win

Do not apply signup password policy in login validation.

On Line 240, login is blocked unless the password matches the signup regex. This can reject legitimate existing users whose stored passwords don’t conform to the new client-side pattern.

✅ Suggested fix
     if (
       isLoginSubmitting ||
       !isLoginReady ||
-      !emailPattern.test(email) ||
-      !passwordPattern.test(password)
+      !emailPattern.test(email)
     ) {
🤖 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 `@jobdri/src/components/login/EmailLoginScreen.tsx` around lines 236 - 241, The
login validation currently blocks submission using the signup password regex by
checking passwordPattern.test(password); remove that check from the
EmailLoginScreen validation and replace it with a simple presence (and/or
minimal length) check (e.g., password.length > 0 or >= 6) so existing users with
legacy passwords are not rejected; update the conditional that uses
isLoginSubmitting, isLoginReady, emailPattern, passwordPattern (and any variable
named password) to only use emailPattern for format validation and a simple
non-empty/min-length check for password, keeping variable names unchanged.

Comment thread jobdri/src/lib/auth.ts
Comment on lines +172 to +174
if (email) {
window.localStorage.setItem(AUTH_STORAGE_KEYS.userEmail, email);
}
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 | 🟠 Major | ⚡ Quick win

Clear stale userEmail when email is unavailable.

On Line 172, userEmail is only written when email is truthy. If a later login flow cannot resolve email, the previous account email stays in storage and can be shown as the current user.

🔧 Suggested fix
   if (email) {
     window.localStorage.setItem(AUTH_STORAGE_KEYS.userEmail, email);
+  } else {
+    window.localStorage.removeItem(AUTH_STORAGE_KEYS.userEmail);
   }
📝 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
if (email) {
window.localStorage.setItem(AUTH_STORAGE_KEYS.userEmail, email);
}
if (email) {
window.localStorage.setItem(AUTH_STORAGE_KEYS.userEmail, email);
} else {
window.localStorage.removeItem(AUTH_STORAGE_KEYS.userEmail);
}
🤖 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 `@jobdri/src/lib/auth.ts` around lines 172 - 174, The code only writes
AUTH_STORAGE_KEYS.userEmail when the variable email is truthy, leaving a stale
value if email becomes undefined; in the function containing the snippet, add an
else branch to clear the key (e.g.,
window.localStorage.removeItem(AUTH_STORAGE_KEYS.userEmail) or setItem with an
empty string) so that when email is falsy the stored userEmail is removed;
reference the existing AUTH_STORAGE_KEYS.userEmail, window.localStorage.setItem
call, and the email variable to locate and update the logic.

Copy link
Copy Markdown
Contributor

@yiyoonseo yiyoonseo left a comment

Choose a reason for hiding this comment

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

수고해떠~~🩷🩷👍👍

@minnngo minnngo merged commit 19c3f63 into develop May 22, 2026
1 check 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