Skip to content

[FEAT] 애플 로그인 구현#606

Open
Chaejy wants to merge 3 commits into
developfrom
feat/apple-login
Open

[FEAT] 애플 로그인 구현#606
Chaejy wants to merge 3 commits into
developfrom
feat/apple-login

Conversation

@Chaejy

@Chaejy Chaejy commented May 27, 2026

Copy link
Copy Markdown
Contributor

✅ 체크리스트

  • 코드에 any가 사용되지 않았습니다
  • 스토리북에 추가했습니다 (해당 시)
  • 이슈와 커밋이 명확히 연결되어 있습니다

🔗 관련 이슈

📌 작업 목적

Apple OAuth를 통한 소셜 로그인을 구현합니다.

🔨 주요 작업 내용

타입

  • OAuthLoginData 공통 타입 추가 (nickname, email, profileImageUrl)
  • KakaoLoginDataOAuthLoginData를 extends하도록 리팩토링
  • Amplitude 이벤트에 CLICK_LOGIN_APPLE 추가

버튼 / 트리거

  • AppleLoginButton 컴포넌트 추가
  • appleLogin()/api/proxy/login/apple로 리다이렉트하는 트리거 함수 추가
  • 로그인 페이지(LoginPage)에 AppleLoginButton 노출

콜백 플로우

  • POST /api/auth/apple/callback — Apple이 보내는 form_post 수신, 백엔드 프록시에 code/state 전달
  • exchangeAppleLogin() — 백엔드 프록시 응답에서 로그인 데이터 파싱 및 에러 메시지 처리
  • applyProxyAuthToResponse() — 프록시 응답의 Set-Cookie를 Next.js response에 전파, accessToken 쿠키 세팅
  • /login/apple/callback 라우트 및 AppleCallbackPage — 쿼리스트링으로 받은 유저 정보를 온보딩 스토어에 주입 후 홈으로 이동

⚠️ 기존 문제 (선택)

☑️ 해결 방법 (선택)

🧪 테스트 방법

  1. 로그인 페이지에서 애플로 로그인하기 버튼 클릭
  2. Apple 계정 인증 완료 후 홈 화면으로 이동하는지 확인
  3. 잘못된 접근(code/state 누락) 시 로그인 페이지로 리다이렉트되는지 확인
  4. 이미 가입된 카카오 계정으로 애플 로그인 시도 시 토스트 에러 메시지 노출 확인

✔️ 설치 라이브러리

📷 실행 영상 또는 스크린샷

💬 논의할 점

  • 카카오 로그인과 카카오콜백 로직은 추후 정리 예정입니다.

🙋🏻 참고 자료

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 애플 로그인 기능 추가로 기존 카카오 로그인과 함께 다중 인증 옵션 제공
    • 로그인 페이지에 애플 로그인 버튼 추가
    • 인증 과정에서 사용자 피드백을 위한 토스트 알림 메시지 개선
    • 애플 OAuth 콜백 처리로 사용자 정보 자동 동기화

Review Change Stack

Chaejy and others added 3 commits May 27, 2026 19:25
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
surf-admin Ready Ready Preview, Comment May 27, 2026 10:31am
surf-web Ready Ready Preview, Comment May 27, 2026 10:31am

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

이 PR은 Apple OAuth 로그인 기능을 완전히 구현합니다. 클라이언트의 AppleLoginButton 컴포넌트에서 인증을 시작하고, 서버의 /api/auth/apple/callback 라우트에서 Apple 폼데이터를 받아 exchangeAppleLogin으로 토큰을 교환합니다. 교환 성공 시 applyProxyAuthToResponse로 업스트림 인증 정보를 프록시 환경에 맞게 재설정한 후, 사용자 정보를 쿼리로 AppleCallbackPage로 리다이렉트합니다. 콜백 페이지에서는 사용자 정보를 온보딩 스토어에 저장하고 홈으로 이동하며, 데이터 누락 시 로그인 페이지로 돌려보냅니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


상세 코드 리뷰 피드백

1. 안정성 & 예외 처리

🔴 exchangeAppleLogin.ts - 네트워크 타임아웃 미처리

// 현재 코드 (위험)
const upstream = await fetch(url, { ... });

문제점:

  • Fetch API에 타임아웃 설정이 없어 무한 대기 가능
  • 업스트림 서버 응답 지연 시 클라이언트 타이머 초과 가능

개선안:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10초

try {
  const upstream = await fetch(url, {
    signal: controller.signal,
    // ...
  });
} finally {
  clearTimeout(timeoutId);
}

참고: MDN Fetch API - Signal & AbortController


🔴 applyProxyAuthResponse.ts - 쿠키 파싱 에러 핸들링 부족

// 현재 코드
function parseCookie(cookieString: string): CookieInfo | null {
  // 파싱 로직...
}

문제점:

  • 잘못된 쿠키 형식이나 특수 문자 처리 미흡
  • 무효한 Max-Age 값에 대한 Number 변환 실패 미처리

개선안:

function parseCookie(cookieString: string): CookieInfo | null {
  try {
    const [cookiePair, ...attributes] = cookieString.split(';');
    const [name, value] = cookiePair.split('=');
    
    if (!name || !value) return null;
    
    const maxAgeStr = attributes.find(a => a.trim().toLowerCase().startsWith('max-age='))?.split('=')[1];
    const maxAge = maxAgeStr ? parseInt(maxAgeStr, 10) : undefined;
    
    if (maxAgeStr && isNaN(maxAge)) {
      console.warn(`Invalid Max-Age value: ${maxAgeStr}`);
      return null;
    }
    
    // ... 나머지 파싱
  } catch (error) {
    console.error('Cookie parsing error:', error);
    return null;
  }
}

🟡 route.ts - FormData 추출 방어 코드 강화 필요

const getFormString = (form: FormData, key: string): string => {
  const value = form.get(key);
  return typeof value === 'string' ? value : '';
};

개선안:

const getFormString = (form: FormData, key: string): string => {
  const value = form.get(key);
  if (typeof value !== 'string') {
    console.warn(`Expected string for key '${key}', got ${typeof value}`);
  }
  return typeof value === 'string' ? value.trim() : '';
};

2. 가독성 & 유지보수성

🟡 AppleCallbackPage.tsx - 매직 넘버 상수화

// 현재 코드
delay={index * 100}  // 왜 100?
style={{ animationDelay: `${index * 100}ms` }}

개선안:

const ANIMATION_DELAY_MS = 100;
const CHARACTER_COUNT = 5;

{characters.map((Character, index) => (
  <Character
    key={index}
    delay={index * ANIMATION_DELAY_MS}
    className="animate-bounce"
  />
))}

🟡 exchangeAppleLogin.ts - 함수 이름 명확화

// 현재: extractLoginData - 무엇에서 추출하는지 불명확
export function extractLoginData(parsed: unknown): OAuthLoginData | null

// 개선안:
export function extractOAuthLoginDataFromUpstream(
  parsed: unknown
): OAuthLoginData | null

3. 타입 안정성

🔴 exchangeAppleLogin.ts - isRecord 타입 가드 강화

// 현재 코드
function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

문제점:

  • Array도 object이므로 Array.isArray() 체크 누락
  • null이 이미 제외되었지만, prototype chain 확인 없음

개선안:

function isRecord(value: unknown): value is Record<string, unknown> {
  return (
    typeof value === 'object' &&
    value !== null &&
    !Array.isArray(value) &&
    Object.getPrototypeOf(value) === Object.prototype
  );
}

참고: TypeScript Handbook - Type Guards


🟡 types.ts - OAuthLoginData 필드 선택성 검토

export type OAuthLoginData = {
  nickname: string;
  email: string;
  profileImageUrl: string;
};

고려사항:

  • Apple은 초회 가입 시에만 이메일 제공 (재방문 시 null)
  • profileImageUrl은 Apple에서 제공하지 않을 수 있음

개선안:

export type OAuthLoginData = {
  email: string;  // 필수 (검증됨)
  nickname?: string;  // 선택
  profileImageUrl?: string;  // 선택
};

4. 보안

🔴 route.ts - CSRF 토큰 검증 누락

// 현재 코드: state 값을 받기만 함
const state = getFormString(form, 'state');

// 개선: 세션 저장 state와 비교 필요
if (state !== storedState) {
  return NextResponse.redirect(`${baseUrl}${PAGE_ROUTES.LOGIN}?msg=invalid_state`);
}

참고: OAuth 2.0 Security Best Practices - State Parameter


🟡 applyProxyAuthResponse.ts - SameSite=None 처리

// 현재: SameSite=none을 lax로 강제 변환
if (sameSite?.toLowerCase() === 'none') {
  sameSite = 'lax';
}

검토:

  • HTTPS 환경에서는 SameSite=None & Secure가 정상
  • 개발 환경(HTTP)에서는 SameSite=Lax가 필요
  • 현재 로직이 맞지만, 명확한 주석 필요
// SameSite=None은 HTTPS 필수이므로, 
// 프록시 환경에서는 Lax로 다운그레이드
if (sameSite?.toLowerCase() === 'none') {
  sameSite = 'lax';
}

5. 접근성 & UX

🟡 AppleLoginButton.tsx - 접근성 개선

// 현재 코드
<button type="button" onClick={appleLogin}>
  <AppleIcon />
  애플로 로그인하기
</button>

개선안:

<button
  type="button"
  onClick={appleLogin}
  aria-label="Apple 계정으로 로그인하기"
  title="Apple 계정으로 로그인"
  disabled={isLoading}  // 중복 클릭 방지
>
  <AppleIcon aria-hidden="true" />
  <span>애플로 로그인하기</span>
</button>

🟡 AppleCallbackPage.tsx - 로딩 상태 접근성

// 개선안: aria-live로 상태 변경 알림
<div aria-live="polite" aria-label="로그인 처리 중">
  <LoadingCharacter />
  <div className="animate-pulse">잠시만 기다려 주세요</div>
</div>

6. 테스트 전략

🔴 테스트 케이스 계획 필요

// exchangeAppleLogin.test.ts
describe('exchangeAppleLogin', () => {
  it('should return error when upstream responds with 400', async () => {
    // 예상: msg = "Apple 로그인 인증에 실패했습니다."
  });

  it('should return error when response lacks email', async () => {
    // 예상: msg = "로그인 응답이 비어있어요."
  });

  it('should extract email but provide default nickname', async () => {
    // nickname 누락 시 기본값 처리 검증
  });

  it('should handle network timeout', async () => {
    // AbortController 타임아웃 검증
  });
});

7. Clean Architecture 검토

레이어 분리 - 양호

  • UI Layer: AppleLoginButton, AppleCallbackPage
  • Domain/Business Logic: exchangeAppleLogin, appleLogin
  • Infrastructure: route.ts, applyProxyAuthToResponse
  • Types: 공통 타입 정의

🟡 의존성 흐름 개선 제안

UI (AppleLoginButton)
  ↓ (appleLogin)
    ↓ API 호출 (/api/proxy/login/apple)
      ↓ 서버 라우트 (route.ts)
        ↓ exchangeAppleLogin (비즈니스 로직)
          ↓ applyProxyAuthToResponse (인프라)
            ↓ 콜백 페이지 (UI)

개선안:

  • exchangeAppleLogin을 별도 서비스 레이어로 추상화
  • applyProxyAuthToResponse를 미들웨어 패턴으로 재구성
// 제안: middleware/authProxyMiddleware.ts
export async function withProxyAuth(
  handler: (req: NextRequest) => Promise<NextResponse>,
  options?: ProxyAuthOptions
) {
  return async (req: NextRequest) => {
    const res = await handler(req);
    // ...
  };
}

8. 브라우저 호환성

🟡 AppleLoginButton - 브라우저 지원 범위 확인

// Apple 로그인 가능 환경 검사 필요
const isAppleSignInSupported = () => {
  return (
    /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
    navigator.platform === 'MacIntel' ||
    navigator.platform === 'iPhone'
  );
};

9. 에러 메시지 일관성

🟡 한국어 에러 메시지 표준화

// getAppleLoginErrorMessage의 메시지들이 다양함
// 일관된 톤으로 통일 필요:
// - "로그인 응답이 비어있어요." (친근한 톤)
// - "Apple 로그인 인증에 실패했습니다." (격식적 톤)

const APPLE_ERROR_MESSAGES = {
  INVALID_CREDENTIALS: '인증 정보가 올바르지 않습니다.',
  DUPLICATE_ACCOUNT: '이미 가입된 계정입니다.',
  SERVER_ERROR: '로그인 처리 중 오류가 발생했습니다. 다시 시도해주세요.',
} as const;

10. 성능 최적화

🟡 KakaoCallBackPage의 의존성 배열

// 현재: 많은 의존성
useEffect(() => { ... }, [provider, nickname, email, profileImageUrl, showToast, router])

개선안:

const params = useMemo(() => ({
  provider,
  nickname,
  email,
  profileImageUrl,
}), [provider, nickname, email, profileImageUrl]);

useEffect(() => {
  // params 사용
}, [params, showToast, router]);

종합 평가

항목 상태 우선순위
타임아웃 미처리 🔴 높음
CSRF 토큰 검증 🔴 높음
쿠키 파싱 에러 처리 🟡 중간
타입 안정성 강화 🟡 중간
접근성 개선 🟡 중간
테스트 작성 🔴 높음
문서화 🟡 낮음
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경 사항의 핵심을 명확하게 요약하고 있습니다. '[FEAT] 애플 로그인 구현'은 Apple OAuth 인증 기능 추가라는 주요 변경사항을 정확하게 반영합니다.
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 feat/apple-login

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
apps/web/src/app-pages/login/ui/AppleCallbackPage.tsx (1)

23-46: 🏗️ Heavy lift

페이지 레이어에 콜백 비즈니스 로직이 과도하게 포함되어 있습니다.

문제: Line 23-46에서 쿼리 파싱/검증/스토어 반영/리다이렉트를 모두 처리해 app-pages가 조립 레이어 역할을 넘었습니다.
원인: 콜백 처리 규칙이 UI 컴포넌트에 결합됨.
대안: features/authuseOAuthCallbackHandler(또는 service) 추출하고, 이 컴포넌트는 결과 상태 렌더링만 담당하세요.
장점: Apple/Kakao 분기 로직 재사용·일관성 확보, 변경 시 파급 범위 축소.
단점: 초기 분리 작업으로 파일/테스트 보강이 필요합니다.

As per coding guidelines, "apps/web/**: 페이지/라우트는 “조립”만: 복잡한 로직은 features/entities로 내려보낸다."

🤖 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 `@apps/web/src/app-pages/login/ui/AppleCallbackPage.tsx` around lines 23 - 46,
The AppleCallbackPage contains too much callback business logic (query parsing,
validation, store updates and redirects); extract this into a reusable handler
in features/auth (e.g., create useOAuthCallbackHandler or OAuthCallbackService)
that accepts the URL/searchParams and returns a result/status and any onboarding
data, then update AppleCallbackPage to call that hook/service for side effects
and only render based on its result; specifically move logic from
AppleCallbackPage (the useEffect block that reads searchParams, validates email,
calls setOnboarding, shows toast, and router.push) into the new
useOAuthCallbackHandler and have AppleCallbackPage just call/use that hook and
render loading/error/success UI accordingly so parsing/validation/store mutation
and routing live in features/auth instead of the page layer.
🤖 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 `@apps/web/src/app-pages/login/ui/AppleCallbackPage.tsx`:
- Around line 49-65: The loading UI in AppleCallbackPage.tsx lacks accessibility
announcements; add role="status" and aria-live="polite" to the loading text
container (the div with className "text-body-body8 text-foreground-tertiary
...") so screen readers receive updates, and mark decorative character SVGs as
aria-hidden="true" when rendering them in the characters.map (the Character
components) — ensure any purely decorative markup inside Character is flagged
aria-hidden to avoid redundant announcements.

In `@apps/web/src/app-pages/login/ui/KakaoCallBackPage.tsx`:
- Around line 53-67: The Apple callback branch in KakaoCallBackPage.tsx is
rejecting logins when appleNickname or appleProfileImageUrl are missing; change
the validation to require only appleEmail and treat missing appleNickname and
appleProfileImageUrl as defaults (e.g., empty string or null) before calling
setOnboarding and router.push; update the branch that checks provider ===
'apple' to only show the error and redirect if appleEmail is absent, and
populate setOnboarding({ nickname: appleNickname ?? '', email: appleEmail,
profileImageUrl: appleProfileImageUrl ?? '' }) so behavior matches
AppleCallbackPage and allows logins when nickname/profileImageUrl are omitted.

In `@apps/web/src/app/api/auth/apple/callback/route.ts`:
- Around line 15-18: The getBaseUrl function currently trusts request headers
('x-forwarded-proto' and 'host') which allows header-based redirect tampering;
change getBaseUrl to use a fixed trusted origin (from configuration or an
environment variable) or validate the combined origin against a server-side
allowlist, and if the origin is not recognized throw/log and refuse to use it;
specifically stop using req.headers.get('x-forwarded-proto') and
req.headers.get('host') directly in getBaseUrl and instead return the
configured/trusted origin or a validated/allowlisted origin.
- Around line 50-55: The code is exposing PII by placing
nickname/email/profileImageUrl into the redirectUrl query; instead generate a
server-side temporary session key or signed short-lived token (e.g., via a new
helper like createTempAuthSession or signShortLivedToken) that stores the
result.data (nickname, email, profileImageUrl) server-side and map it to that
key, then set only provider='apple' and the temp session key/token on
redirectUrl (keep LOGIN_CALLBACK and baseUrl usage), avoid adding raw PII to
query params, and ensure whatever storage/mechanism you add includes an
expiration and lookup by the callback handler to retrieve the PII.
- Around line 38-41: The code calls exchangeAppleLogin with a hardcoded origin
'http://localhost:3000' which breaks in production; replace that literal by
deriving the origin dynamically (e.g., compute const origin = new
URL(req.url).origin or read a configured env var like NEXT_PUBLIC_APP_URL /
NEXTAUTH_URL with a sensible fallback) and pass that origin variable into
exchangeAppleLogin instead of the hardcoded string so the upstream call uses the
actual deployment domain; update the call site in route.ts where
exchangeAppleLogin({ code, state, user }, ...) is invoked.

In `@apps/web/src/features/auth/api/exchangeAppleLogin.ts`:
- Around line 38-42: The fetch call that assigns to upstream in
exchangeAppleLogin.ts currently has no timeout/abort and can hang; add an
AbortSignal with a sensible timeout (e.g., using AbortSignal.timeout or an
AbortController + setTimeout) and pass the signal option into the fetch call,
then handle the abort/timeout case in the surrounding try/catch to log the
timeout (including details) and throw a user-facing error or retry as
appropriate; update the upstream fetch invocation and its error handling paths
in the exchangeAppleLogin function to use the signal and detect
DOMException/AbortError to differentiate timeout vs other failures.

In `@apps/web/src/features/auth/lib/applyProxyAuthResponse.ts`:
- Around line 68-71: The toProxyCookiePath function currently returns '/' for
path === '/' which lets the cookie propagate app-wide; update to map the root
and undefined cases to the proxy boundary instead. Specifically, in
toProxyCookiePath(path?: string) return '/api/proxy' when path is undefined or
path === '/', keep returning path if it already startsWith('/api/proxy'), and
otherwise prepend '/api/proxy' to the supplied path; adjust only the logic
inside toProxyCookiePath so cookie scope is constrained to the proxy boundary.

---

Nitpick comments:
In `@apps/web/src/app-pages/login/ui/AppleCallbackPage.tsx`:
- Around line 23-46: The AppleCallbackPage contains too much callback business
logic (query parsing, validation, store updates and redirects); extract this
into a reusable handler in features/auth (e.g., create useOAuthCallbackHandler
or OAuthCallbackService) that accepts the URL/searchParams and returns a
result/status and any onboarding data, then update AppleCallbackPage to call
that hook/service for side effects and only render based on its result;
specifically move logic from AppleCallbackPage (the useEffect block that reads
searchParams, validates email, calls setOnboarding, shows toast, and
router.push) into the new useOAuthCallbackHandler and have AppleCallbackPage
just call/use that hook and render loading/error/success UI accordingly so
parsing/validation/store mutation and routing live in features/auth instead of
the page layer.
🪄 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: CHILL

Plan: Pro

Run ID: b0fb3006-7e11-4111-aa5a-3d72b0f622e8

📥 Commits

Reviewing files that changed from the base of the PR and between 335d0ef and dbd1dad.

⛔ Files ignored due to path filters (1)
  • apps/web/public/images/apple.svg is excluded by !**/*.svg
📒 Files selected for processing (11)
  • apps/web/src/app-pages/login/ui/AppleCallbackPage.tsx
  • apps/web/src/app-pages/login/ui/KakaoCallBackPage.tsx
  • apps/web/src/app-pages/login/ui/LoginPage.tsx
  • apps/web/src/app/(public)/login/apple/callback/page.tsx
  • apps/web/src/app/api/auth/apple/callback/route.ts
  • apps/web/src/features/auth/api/exchangeAppleLogin.ts
  • apps/web/src/features/auth/api/types.ts
  • apps/web/src/features/auth/lib/appleLogin.ts
  • apps/web/src/features/auth/lib/applyProxyAuthResponse.ts
  • apps/web/src/features/auth/model/types.ts
  • apps/web/src/features/auth/ui/AppleLoginButton.tsx

Comment on lines +49 to +65
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="flex flex-col items-center gap-10">
<div className="flex flex-row items-center">
{characters.map((Character, i) => (
<Character
key={i}
className="animate-float"
style={{ animationDelay: `${i * 0.1}s` }}
/>
))}
</div>
<div className="text-body-body8 text-foreground-tertiary flex items-center gap-2">
<span>잠시만 기다려 주세요</span>
<span className="animate-dot-appear-1 inline-block">.</span>
<span className="animate-dot-appear-2 inline-block">.</span>
<span className="animate-dot-appear-3 inline-block">.</span>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

로딩 화면 접근성 상태 알림이 누락되어 있습니다.

Line 49-65 로딩 UI는 시각적 애니메이션만 있고, 스크린리더에 진행 상태를 전달하지 못합니다. role="status" + aria-live="polite"를 추가하고 장식용 SVG는 aria-hidden="true" 처리하는 게 좋습니다.

접근성 보완 예시
-    <div className="flex h-full w-full flex-col items-center justify-center">
+    <div
+      className="flex h-full w-full flex-col items-center justify-center"
+      role="status"
+      aria-live="polite"
+      aria-label="로그인 처리 중입니다. 잠시만 기다려 주세요."
+    >
...
-        <div className="flex flex-row items-center">
+        <div className="flex flex-row items-center" aria-hidden="true">

As per coding guidelines, "**: 접근성: aria-label, role, 키보드 내비게이션, 포커스 트랩, 대비/색상 의존 금지."

📝 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
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="flex flex-col items-center gap-10">
<div className="flex flex-row items-center">
{characters.map((Character, i) => (
<Character
key={i}
className="animate-float"
style={{ animationDelay: `${i * 0.1}s` }}
/>
))}
</div>
<div className="text-body-body8 text-foreground-tertiary flex items-center gap-2">
<span>잠시만 기다려 주세요</span>
<span className="animate-dot-appear-1 inline-block">.</span>
<span className="animate-dot-appear-2 inline-block">.</span>
<span className="animate-dot-appear-3 inline-block">.</span>
</div>
<div
className="flex h-full w-full flex-col items-center justify-center"
role="status"
aria-live="polite"
aria-label="로그인 처리 중입니다. 잠시만 기다려 주세요."
>
<div className="flex flex-col items-center gap-10">
<div className="flex flex-row items-center" aria-hidden="true">
{characters.map((Character, i) => (
<Character
key={i}
className="animate-float"
style={{ animationDelay: `${i * 0.1}s` }}
/>
))}
</div>
<div className="text-body-body8 text-foreground-tertiary flex items-center gap-2">
<span>잠시만 기다려 주세요</span>
<span className="animate-dot-appear-1 inline-block">.</span>
<span className="animate-dot-appear-2 inline-block">.</span>
<span className="animate-dot-appear-3 inline-block">.</span>
</div>
🤖 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 `@apps/web/src/app-pages/login/ui/AppleCallbackPage.tsx` around lines 49 - 65,
The loading UI in AppleCallbackPage.tsx lacks accessibility announcements; add
role="status" and aria-live="polite" to the loading text container (the div with
className "text-body-body8 text-foreground-tertiary ...") so screen readers
receive updates, and mark decorative character SVGs as aria-hidden="true" when
rendering them in the characters.map (the Character components) — ensure any
purely decorative markup inside Character is flagged aria-hidden to avoid
redundant announcements.

Comment thread apps/web/src/app-pages/login/ui/KakaoCallBackPage.tsx
Comment on lines +15 to +18
function getBaseUrl(req: NextRequest): string {
const proto = req.headers.get('x-forwarded-proto') ?? 'http';
const host = req.headers.get('host') ?? 'localhost:3000';
return `${proto}://${host}`;

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

Host/Proto 헤더를 그대로 신뢰하면 리다이렉트 대상 변조 위험이 있습니다.

외부 입력 헤더 기반으로 base URL을 조합하면, 악의적 Host 헤더로 의도치 않은 도메인 리다이렉트가 발생할 수 있습니다. 서버 설정의 고정 origin(또는 allowlist 검증된 origin)만 사용하세요.

🤖 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 `@apps/web/src/app/api/auth/apple/callback/route.ts` around lines 15 - 18, The
getBaseUrl function currently trusts request headers ('x-forwarded-proto' and
'host') which allows header-based redirect tampering; change getBaseUrl to use a
fixed trusted origin (from configuration or an environment variable) or validate
the combined origin against a server-side allowlist, and if the origin is not
recognized throw/log and refuse to use it; specifically stop using
req.headers.get('x-forwarded-proto') and req.headers.get('host') directly in
getBaseUrl and instead return the configured/trusted origin or a
validated/allowlisted origin.

Comment on lines +38 to +41
const result = await exchangeAppleLogin(
{ code, state, user },
'http://localhost:3000',
req.headers.get('cookie') ?? undefined,

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 | 🔴 Critical | ⚡ Quick win

토큰 교환 origin이 localhost로 고정되어 운영 환경에서 실패합니다.

현재는 어떤 환경에서도 http://localhost:3000으로 업스트림 호출합니다. 실제 배포 도메인 기준으로 호출하도록 바꿔야 합니다.

🔧 제안 코드
   const result = await exchangeAppleLogin(
     { code, state, user },
-    'http://localhost:3000',
+    baseUrl,
     req.headers.get('cookie') ?? 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 `@apps/web/src/app/api/auth/apple/callback/route.ts` around lines 38 - 41, The
code calls exchangeAppleLogin with a hardcoded origin 'http://localhost:3000'
which breaks in production; replace that literal by deriving the origin
dynamically (e.g., compute const origin = new URL(req.url).origin or read a
configured env var like NEXT_PUBLIC_APP_URL / NEXTAUTH_URL with a sensible
fallback) and pass that origin variable into exchangeAppleLogin instead of the
hardcoded string so the upstream call uses the actual deployment domain; update
the call site in route.ts where exchangeAppleLogin({ code, state, user }, ...)
is invoked.

Comment on lines +50 to +55
const { nickname, email, profileImageUrl } = result.data;
const redirectUrl = new URL(LOGIN_CALLBACK, baseUrl);
redirectUrl.searchParams.set('provider', 'apple');
redirectUrl.searchParams.set('nickname', nickname);
redirectUrl.searchParams.set('email', email);
redirectUrl.searchParams.set('profileImageUrl', profileImageUrl);

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

이메일/닉네임을 URL 쿼리로 전달해 PII 노출 위험이 큽니다.

email, nickname을 쿼리로 넘기면 브라우저 히스토리/로그/리퍼러로 유출될 수 있습니다. 서버 측 임시 세션 키(또는 서명된 단기 토큰)만 전달하고, 실제 PII는 서버에서 조회하는 방식으로 전환하는 게 안전합니다.

🤖 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 `@apps/web/src/app/api/auth/apple/callback/route.ts` around lines 50 - 55, The
code is exposing PII by placing nickname/email/profileImageUrl into the
redirectUrl query; instead generate a server-side temporary session key or
signed short-lived token (e.g., via a new helper like createTempAuthSession or
signShortLivedToken) that stores the result.data (nickname, email,
profileImageUrl) server-side and map it to that key, then set only
provider='apple' and the temp session key/token on redirectUrl (keep
LOGIN_CALLBACK and baseUrl usage), avoid adding raw PII to query params, and
ensure whatever storage/mechanism you add includes an expiration and lookup by
the callback handler to retrieve the PII.

Comment on lines +38 to +42
const upstream = await fetch(url.toString(), {
method: 'POST',
headers,
cache: 'no-store',
});

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 | 🔴 Critical | ⚡ Quick win

외부 인증 호출에 timeout/abort가 없어 요청 스레드가 장시간 점유될 수 있습니다.

Line 38의 fetch가 무제한 대기라서 업스트림 지연 시 로그인 라우트가 묶일 수 있습니다. AbortSignal.timeout(...) 같은 명시적 제한을 넣어주세요.

🔧 제안 코드
-    const upstream = await fetch(url.toString(), {
+    const upstream = await fetch(url.toString(), {
       method: 'POST',
       headers,
       cache: 'no-store',
+      signal: AbortSignal.timeout(8000),
     });

As per coding guidelines, 5) 에러/예외 처리 기준: 사용자-facing 에러 메시지, 로깅, 재시도 전략, timeout, abort 등을 일관되게.

📝 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
const upstream = await fetch(url.toString(), {
method: 'POST',
headers,
cache: 'no-store',
});
const upstream = await fetch(url.toString(), {
method: 'POST',
headers,
cache: 'no-store',
signal: AbortSignal.timeout(8000),
});
🤖 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 `@apps/web/src/features/auth/api/exchangeAppleLogin.ts` around lines 38 - 42,
The fetch call that assigns to upstream in exchangeAppleLogin.ts currently has
no timeout/abort and can hang; add an AbortSignal with a sensible timeout (e.g.,
using AbortSignal.timeout or an AbortController + setTimeout) and pass the
signal option into the fetch call, then handle the abort/timeout case in the
surrounding try/catch to log the timeout (including details) and throw a
user-facing error or retry as appropriate; update the upstream fetch invocation
and its error handling paths in the exchangeAppleLogin function to use the
signal and detect DOMException/AbortError to differentiate timeout vs other
failures.

Comment on lines +68 to +71
function toProxyCookiePath(path?: string) {
if (!path || path === '/') return path ?? '/';
if (path.startsWith('/api/proxy')) return path;
return `/api/proxy${path}`;

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

루트 경로 쿠키가 프록시 경계를 벗어나도록 설정됩니다.

path === '/'일 때 그대로 /를 반환해서 쿠키가 앱 전역으로 전파됩니다. 프록시 전용 경계 의도라면 루트도 /api/proxy로 매핑해야 합니다.

🔧 제안 코드
 function toProxyCookiePath(path?: string) {
-  if (!path || path === '/') return path ?? '/';
+  if (!path || path === '/') return '/api/proxy';
   if (path.startsWith('/api/proxy')) return path;
   return `/api/proxy${path}`;
 }

As per coding guidelines, 3) workspace 경계를 넘는 import는 반드시 목적(재사용/표준화)을 설명하고, 사이드이펙트/전역 상태 공유를 최소화.

📝 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
function toProxyCookiePath(path?: string) {
if (!path || path === '/') return path ?? '/';
if (path.startsWith('/api/proxy')) return path;
return `/api/proxy${path}`;
function toProxyCookiePath(path?: string) {
if (!path || path === '/') return '/api/proxy';
if (path.startsWith('/api/proxy')) return path;
return `/api/proxy${path}`;
}
🤖 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 `@apps/web/src/features/auth/lib/applyProxyAuthResponse.ts` around lines 68 -
71, The toProxyCookiePath function currently returns '/' for path === '/' which
lets the cookie propagate app-wide; update to map the root and undefined cases
to the proxy boundary instead. Specifically, in toProxyCookiePath(path?: string)
return '/api/proxy' when path is undefined or path === '/', keep returning path
if it already startsWith('/api/proxy'), and otherwise prepend '/api/proxy' to
the supplied path; adjust only the logic inside toProxyCookiePath so cookie
scope is constrained to the proxy boundary.

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.

[FEAT] 애플 로그인 구현

1 participant