랜딩페이지 및 인증페이지 개발 완료#74
Conversation
…andingshell및 layout.tsx에서 코드 수정
Summary of ChangesHello @yooolleee, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 사용자에게 개선된 첫인상을 제공하고 핵심 인증 기능을 활성화하기 위해 새로운 랜딩 페이지와 로그인, 회원가입, 비밀번호 재설정 페이지를 도입합니다. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
이 PR은 랜딩 페이지와 인증 관련 페이지(로그인, 회원가입, 비밀번호 재설정) 개발을 포함하고 있습니다. 전반적으로 Zod와 React Hook Form을 사용한 폼 구현이 잘 되어 있으며, 카카오 소셜 로그인 흐름도 서버 사이드에서 안전하게 처리되었습니다. 랜딩 페이지 또한 반응형으로 잘 구현되었습니다. 몇 가지 개선점을 제안드립니다. next.config.js의 이미지 허용 호스트에서 http 프로토콜을 제거하여 보안을 강화하고, 일부 중복되거나 사용되지 않는 코드를 정리하여 유지보수성을 높이는 것이 좋겠습니다. 또한, 랜딩 페이지의 반응형 이미지 로딩 방식을 최적화하여 성능을 개선할 수 있습니다. 자세한 내용은 각 파일에 남긴 코멘트를 참고해주세요.
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { fetchApiServer } from '@/shared/apis/fetchApi.server'; | ||
| import { setAuthCookies } from '../../_lib/cookies'; | ||
|
|
||
| const TEAM_ID = process.env.API_TEAM_ID ?? '20-1'; | ||
|
|
||
| interface KakaoSignInResponse { | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| user: { | ||
| id: number; | ||
| email: string; | ||
| nickname: string; | ||
| image: string | null; | ||
| teamId: string; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/auth/kakao/callback | ||
| * | ||
| * 카카오가 인가코드를 전달하는 Redirect URI 핸들러입니다. | ||
| * | ||
| * 흐름: | ||
| * 1. 카카오가 ?code=인가코드 형태로 이 URL을 호출합니다. | ||
| * 2. 인가코드 + redirectUri를 백엔드 /auth/signIn/KAKAO로 전달합니다. | ||
| * 3. 백엔드가 카카오에 토큰 교환 후 accessToken/refreshToken을 발급합니다. | ||
| * 4. httpOnly 쿠키에 저장 후 적절한 페이지로 이동합니다. | ||
| * | ||
| * 왜 redirectUri를 다시 백엔드에 보내는가? | ||
| * - 카카오 보안 정책상, 인가코드 발급 시 사용한 redirectUri와 | ||
| * 토큰 교환 시 사용하는 redirectUri가 정확히 일치해야 합니다. | ||
| * - 백엔드가 카카오 토큰 API를 호출할 때 이 값이 필요합니다. | ||
| */ | ||
| export async function GET(req: NextRequest) { | ||
| const { searchParams } = new URL(req.url); | ||
| const code = searchParams.get('code'); | ||
| const error = searchParams.get('error'); | ||
|
|
||
| // 사용자가 카카오 로그인 동의를 거부한 경우 | ||
| if (error || !code) { | ||
| return NextResponse.redirect(new URL('/login', req.url)); | ||
| } | ||
|
|
||
| const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'; | ||
| const redirectUri = `${APP_URL}/oauth/kakao`; | ||
|
|
||
| try { | ||
| console.log('[kakao callback] code:', code); | ||
| console.log('[kakao callback] redirectUri:', redirectUri); | ||
|
|
||
| const response = await fetchApiServer(`/auth/signIn/KAKAO`, { | ||
| method: 'POST', | ||
| body: JSON.stringify({ | ||
| token: code, | ||
| redirectUri, | ||
| }), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorBody = await response.json().catch(() => ({})); | ||
| console.error('[kakao callback] 백엔드 응답 실패:', response.status, errorBody); | ||
| return NextResponse.redirect(new URL('/login?error=kakao_failed', req.url)); | ||
| } | ||
|
|
||
| const data: KakaoSignInResponse = await response.json(); | ||
| await setAuthCookies(data.accessToken, data.refreshToken); | ||
|
|
||
| // 소속 팀이 있으면 팀 페이지로, 없으면 팀 추가 페이지로 | ||
| const redirectPath = data.user?.teamId ? `/${data.user.teamId}` : '/addteam'; | ||
| return NextResponse.redirect(new URL(redirectPath, req.url)); | ||
| } catch (e) { | ||
| console.error('[kakao callback] 예외 발생:', e); | ||
| return NextResponse.redirect(new URL('/login?error=kakao_failed', req.url)); | ||
| } | ||
| } |
There was a problem hiding this comment.
이 파일(src/app/api/auth/kakao/callback/route.ts)은 src/app/oauth/kakao/route.ts와 거의 동일한 코드를 포함하고 있어 중복으로 보입니다. src/app/api/auth/kakao/route.ts에서 리다이렉트 URI를 /oauth/kakao로 설정했기 때문에, 실제로는 src/app/oauth/kakao/route.ts 파일만 사용될 것입니다. 중복된 코드는 혼란을 야기하고 유지보수를 어렵게 만들 수 있으므로, 사용되지 않는 이 파일은 삭제하는 것이 좋습니다. 또한, 이 파일 내에 선언된 TEAM_ID 상수도 사용되지 않고 있습니다.
There was a problem hiding this comment.
이제 사용되지 않아 삭제하는게 맞습니다 삭제하겠습니다.
| images: { | ||
| remotePatterns: [ | ||
| { | ||
| protocol: 'https', | ||
| hostname: '**.kakaocdn.net', | ||
| }, | ||
| { | ||
| protocol: 'http', | ||
| hostname: '**.kakaocdn.net', | ||
| }, | ||
| { | ||
| protocol: 'https', | ||
| hostname: '**.kakao.com', | ||
| }, | ||
| { | ||
| protocol: 'http', | ||
| hostname: '**.kakao.com', | ||
| }, | ||
| ], | ||
| }, |
| } | ||
| router.refresh(); | ||
| } catch { | ||
| setError('email', { message: '네트워크 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }); |
There was a problem hiding this comment.
네트워크 오류는 이메일 필드에만 국한된 문제가 아니므로, setError('email', ...) 대신 setError('root', ...)를 사용하여 폼 전체에 대한 오류로 처리하는 것이 더 적절합니다. 이렇게 하면 사용자에게 더 명확한 피드백을 제공할 수 있으며, 다른 폼(회원가입, 비밀번호 재설정)에서의 오류 처리 방식과 일관성을 유지할 수 있습니다.
| setError('email', { message: '네트워크 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }); | |
| setError('root', { message: '네트워크 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }); |
| @@ -0,0 +1,376 @@ | |||
| /* app/(root)/landing/LandingPage.module.css */ | |||
| <div className={[styles.media, mediaClass].join(' ')}> | ||
| <Image | ||
| src={imgPc} | ||
| alt="" | ||
| className={[styles.imgPc, imgPcClass].filter(Boolean).join(' ')} | ||
| /> | ||
| <Image src={imgTablet} alt="" className={styles.imgTablet} /> | ||
| <Image src={imgMobile} alt="" className={styles.imgMobile} /> | ||
| </div> |
There was a problem hiding this comment.
현재 반응형 이미지를 처리하기 위해 여러 개의 <Image> 컴포넌트를 렌더링하고 CSS로 숨기는 방식을 사용하고 계십니다. 이 방법은 구현이 간단하지만, 화면에 보이지 않는 이미지도 다운로드하게 되어 초기 로딩 성능에 영향을 줄 수 있습니다. 특히 여러 이미지가 사용되는 랜딩 페이지에서는 성능 저하가 두드러질 수 있습니다.
<picture> 태그와 source 엘리먼트를 사용하면 브라우저가 화면 크기에 맞는 이미지만 다운로드하도록 할 수 있어 성능 개선에 도움이 됩니다. Next.js에서 <picture>와 next/image를 함께 사용하는 것은 다소 복잡할 수 있지만, 성능 최적화를 위해 고려해볼 만한 방법입니다.
| @media (max-width: 480px) { | ||
| /* 오버레이를 하단 정렬로 변경 */ | ||
| .mobileOverlay { | ||
| align-items: flex-end !important; |
There was a problem hiding this comment.
.mobileOverlay 클래스에서 !important를 사용하고 있습니다. !important는 CSS의 명시도 규칙을 무시하여 스타일 충돌 시 디버깅을 어렵게 만들고 코드 유지보수를 힘들게 할 수 있습니다. 가능하면 !important 사용을 피하고, 더 구체적인 셀렉터를 사용하거나 Modal 컴포넌트 자체에서 스타일을 제어할 수 있는 prop(예: verticalAlign)을 추가하는 방안을 고려해보는 것이 좋습니다.
| align-items: flex-end !important; | |
| align-items: flex-end; |
Summary
우선 feature/login-signup 브랜치에서 인증 페이지 개발을 하고 feature/landing-v2 브랜치에서 랜딩페이지 개발을 하고 각각 다른 개발 브랜치에서 push를 할 예정이였으나 하나의 브랜치에서 두 기능을 push하는 것을 알립니다.
타임라인 순으로 말씀드리면 feature/landing-v2 브랜치에서 개발한 랜딩페이지를 push 했을 때 버셀 배포 환경에서 오류가 났었습니다. 저의 코드로 발생한 건 아니고 팀원의 코드에서 환경변수가 맞지않는 문제가 있어서 에러가 난 상태로 머지를 못하고 두고 시간이 촉박한 관계로 feature/login-signup 브랜치에서 인증페이지를 개발을 먼저 하면서 아무래도 유기성이 있는 페이지다 보니까 필요한 랜딩페이지 관련 파일을 가져와서 작업을 했고 여기서 특정 파일들이 랜딩과 로그인 양쪽에 걸쳐 있어서 어느 브랜치로 분기해서 각각 push 하기가 애매해 졌습니다. 굳이 분리를 하는 방향이 충돌 해결 비용이나 리뷰 난이도 상승이 있을 것 같아서 한 브랜치에서 push를 하고 팀원에게 알립니다.
Zod 와 React Hook Form 을 사용했습니다 pull 받으신 후에 pnpm install 한번 진행해 주시면 감사합니다.
현재
Issue
Scope
포함
특이사항