Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes
File renamed without changes
24 changes: 24 additions & 0 deletions public/assets/Logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src/pages/auth/callback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

'use client' 디렉티브 제거 필요

이 프로젝트는 Next.js Pages Router를 사용하고 있어 'use client' 디렉티브가 불필요합니다. App Router에서만 필요한 디렉티브입니다.

Based on learnings

이 diff를 적용하세요:

-'use client';
 import { useEffect } from 'react';
🤖 Prompt for AI Agents
In src/pages/auth/callback.tsx around line 1, remove the top-level "'use
client';" directive because this file lives in the Pages Router and does not
require the App Router client directive; delete that line and verify no other
client-only hooks or browser-only APIs remain that would require converting the
file to a client component.

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { exchangeTempToken } from '@/shared/api/auth';
import Loading from '@/pages/loading';

export default function AuthCallbackPage() {
const router = useRouter();

useEffect(() => {
if (!router.isReady) return;

const searchParams = new URLSearchParams(router.asPath.split('?')[1]);
const tempToken = searchParams.get('temp_token');
console.log('tempToken:', tempToken);

if (!tempToken) return;

const handleLogin = async () => {
try {
const res = await exchangeTempToken(tempToken);
console.log('API 응답:', res);

if (res.success) {
console.log('redirect to main 페이지');
router.replace('/main');
} else {
alert(res.message || '로그인에 실패했습니다.');
router.replace('/auth');
}
} catch (error) {
console.error('오류:', error);
router.replace('/auth');
}
};

handleLogin();
}, [router.isReady, router.asPath]);

return <Loading />;
}
72 changes: 47 additions & 25 deletions src/pages/auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
'use client';
import { useState } from 'react';
import { Icon } from '@/shared/icons';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { cn } from '@/shared/lib';
import LoginButton from '@/pages/auth/components/LoginButton';
import RecentLoginBubble from '@/pages/auth/components/RecentLoginBubble';
import LoginButton from '@/shared/components/auth/LoginButton';
import RecentLoginBubble from '@/shared/components/auth/RecentLoginBubble';
import { useRecentLogin } from '@/shared/hooks/useRecentLogin';

export default function LoginPage() {
const [recentPlatform, setRecentPlatform] = useState<string | null>(null);
const { recentPlatform, saveRecentPlatform } = useRecentLogin();
const router = useRouter();

//로그인
const handleLoginClick = (platform: string) => {
alert(`${platform} 로그인 준비중`);
console.log(`${platform} 로그인 버튼 클릭`);
setRecentPlatform(platform);
saveRecentPlatform(platform);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

최근 로그인 말풍선이 항상 숨겨집니다
saveRecentPlatform에 저장되는 값이 'kakao'/'google'인데 아래 렌더 조건은 '카카오'/'구글'과 비교하고 있어 항상 false가 됩니다. 결과적으로 최근 로그인 말풍선이 표시되지 않는 회귀가 발생합니다. 저장 값을 표시 문자열로 유지하거나 조건식을 PLATFORM 상수와 일치하도록 수정해 주세요.

-    saveRecentPlatform(platform);
+    saveRecentPlatform(platformDisplay);
🤖 Prompt for AI Agents
In src/pages/auth/index.tsx around line 30, the recent-login bubble never shows
because saveRecentPlatform stores 'kakao'/'google' but the render condition
compares against Korean display strings ('카카오'/'구글'), causing a permanent false;
fix by making the condition match the stored value—either change the render
check to use the PLATFORM constant or the lowercase keys ('kakao'/'google') (or
map stored keys to display labels before checking), or alternatively change
saveRecentPlatform to persist the display string consistently; update whichever
side is simplest to keep key/display usage consistent across save and render.

const base = process.env.NEXT_PUBLIC_BACKEND_URL;
const url =
platform === '카카오'
? `${base}/oauth2/authorization/kakao`
: `${base}/oauth2/authorization/google`;
window.location.href = url;
Comment on lines +32 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

환경 변수 누락 시 사용자 피드백 추가 필요
NEXT_PUBLIC_BACKEND_URL이 비어 있으면 콘솔 로그만 남기고 return되어 사용자는 클릭해도 아무 반응이 없는 것처럼 보입니다. 구성 실수 시 로그인 기능이 완전히 막히므로 토스트·알림 등을 통해 즉시 안내하고 개발 환경에서만 상세 로그를 출력해 주세요.

-    if (!base) {
-      console.error('NEXT_PUBLIC_BACKEND_URL is not defined');
-      return;
-    }
+    if (!base) {
+      const message = '로그인 서버 설정이 누락되어 로그인을 진행할 수 없습니다.';
+      if (process.env.NODE_ENV === 'development') {
+        console.error(message);
+      }
+      alert(message); // 앱의 알림/토스트 유틸이 있다면 그쪽을 사용해 주세요.
+      return;
+    }
🤖 Prompt for AI Agents
In src/pages/auth/index.tsx around lines 32 to 37 the code silently returns when
NEXT_PUBLIC_BACKEND_URL is missing; change this so users get immediate feedback
and developers still see details: detect missing base, call the app's
user-facing notification (toast/alert) to show a clear message like "Login
unavailable — backend URL not configured", then return; keep the detailed
console.error or console.debug but only when process.env.NODE_ENV !==
'production' (or use isDev flag) so devs see the error details; ensure you
import/ use the existing notification utility and keep the early return after
notifying the user.

};

//비회원 로그인
const handleGuestClick = () => {
if (document.referrer && document.referrer !== window.location.href) {
router.back();
} else {
router.push('/main');
}
};
Comment on lines +41 to 47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

document.referrer 사용의 신뢰성 문제

document.referrer는 개인정보 보호 설정, 직접 URL 접근, HTTPS→HTTP 전환 등의 경우 비어있을 수 있어 신뢰할 수 없습니다. 라우터 쿼리 파라미터를 사용하는 것이 더 안정적입니다.

이전 페이지 정보를 라우터 쿼리로 전달하는 방식을 권장합니다:

// 로그인 페이지로 이동할 때
router.push({ pathname: '/auth', query: { returnUrl: router.asPath } });

// handleGuestClick에서
const handleGuestClick = () => {
  const returnUrl = router.query.returnUrl as string;
  if (returnUrl) {
    router.push(returnUrl);
  } else {
    router.push('/main');
  }
};
🤖 Prompt for AI Agents
src/pages/auth/index.tsx around lines 25-31: document.referrer is unreliable;
change handleGuestClick to read a returnUrl from router.query (e.g., const
returnUrl = router.query.returnUrl as string) and if present
router.push(returnUrl) else router.push('/main'); also ensure callers that
navigate to the auth page include the returnUrl in the query (router.push({
pathname: '/auth', query: { returnUrl: router.asPath } })) so the previous page
is reliably preserved; keep a safe fallback to '/main'.


return (
Expand Down Expand Up @@ -48,25 +64,27 @@ export default function LoginPage() {
<section
className={cn(
'w-full flex flex-col items-center text-center',
'px-[6.8rem] pt-[5.2rem]',
'px-[6.8rem] pt-[6rem]',
)}
>
{/* 타이틀 */}
<div className='flex flex-col items-center gap-[3.3rem] mb-[3.2rem]'>
<h1 className='text-headline-lg-serif text-mint-900'>글다</h1>
<p className='text-label-md text-mint-900'>
{/* 로고 영역 */}
<div className='flex flex-col items-center'>
<Image
src='/assets/Logo.svg'
alt='글다 로고'
width={112}
height={140}
className='mb-[2.8rem]'
priority
/>
<p className='text-label-serif text-mint-900 mt-[5rem] mb-[2.8rem]'>
만화 속 부천 여행
<br />
10개 명소를 탐험하고 엽서를 모아보세요!
</p>
</div>

{/* 로고 */}
<div className='p-[3.2rem] mb-[3.2rem]'>
<Icon name='Stamp' size={132} color='mint-400' />
</div>

<div className='flex flex-col items-center gap-[2.1rem]'>
<div className='flex flex-col items-center mt-[5rem] gap-[2.1rem]'>
<p className='text-label-lg text-gray-400'>start with</p>

<div className='flex gap-[1.5rem] relative'>
Expand All @@ -78,7 +96,7 @@ export default function LoginPage() {
/>
{recentPlatform === '카카오' && (
<div
className='absolute -top-[3.8rem] left-1/2 -translate-x-1/2
className='absolute -top-[2.5rem] left-1/2 -translate-x-1/2
w-auto min-w-max h-auto flex-shrink-0 pointer-events-none'
>
<RecentLoginBubble />
Expand All @@ -94,7 +112,7 @@ export default function LoginPage() {
/>
{recentPlatform === '구글' && (
<div
className='absolute -top-[3.8rem] left-1/2 -translate-x-1/2
className='absolute -top-[2.5rem] left-1/2 -translate-x-1/2
w-auto min-w-max h-auto flex-shrink-0 pointer-events-none'
>
<RecentLoginBubble />
Expand All @@ -103,14 +121,18 @@ export default function LoginPage() {
</div>
</div>

<p className='text-label-md text-gray-400 cursor-pointer underline underline-offset-[0.25rem]'>
<p
className='text-label-md text-gray-400 cursor-pointer underline underline-offset-[0.25rem]'
onClick={handleGuestClick}
>
비회원 로그인
</p>
</div>

{/* 안내문 */}
<p className='mt-[5rem] text-label-sm text-gray-400'>
비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다.
</p>
<div className='mt-[5rem] text-label-md text-gray-400 whitespace-nowrap text-ellipsis overflow-hidden text-center'>
<p>비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다.</p>
</div>
Comment on lines +144 to +146
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

중요 안내문이 잘릴 수 있음

whitespace-nowraptext-ellipsis로 인해 좁은 화면에서 비회원 제한사항 안내문이 잘려서 표시될 수 있습니다. 사용자가 게스트 로그인을 선택하기 전에 이 정보를 완전히 읽을 수 있어야 합니다.

다음 diff를 적용하여 텍스트를 여러 줄로 표시하도록 수정하세요:

-        <div className='mt-[5rem] text-label-md text-gray-400 whitespace-nowrap text-ellipsis overflow-hidden text-center'>
+        <div className='mt-[5rem] text-label-md text-gray-400 text-center'>
           <p>비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다.</p>
         </div>
📝 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='mt-[5rem] text-label-md text-gray-400 whitespace-nowrap text-ellipsis overflow-hidden text-center'>
<p>비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다.</p>
</div>
<div className='mt-[5rem] text-label-md text-gray-400 text-center'>
<p>비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다.</p>
</div>
🤖 Prompt for AI Agents
In src/pages/auth/index.tsx around lines 127 to 129, the guest-notice div uses
whitespace-nowrap and text-ellipsis causing the important message to be
truncated on narrow screens; remove whitespace-nowrap and text-ellipsis and
allow wrapping by replacing them with whitespace-normal and break-words (you can
keep overflow-hidden and text-center if desired) so the paragraph flows to
multiple lines and remains fully readable on small viewports.

</section>
</main>
);
Expand Down
17 changes: 17 additions & 0 deletions src/shared/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { apiAuth } from '@/shared/api/instance';
import type { ApiResponse, TokenData } from '@/shared/types/authtypes';
import { setTokens } from '@/shared/utils/token';

export const exchangeTempToken = async (tempToken: string) => {
const { data } = await apiAuth.post<ApiResponse<TokenData>>(
'/api/auth/temp-token/exchange',
{ tempToken },
);

if (data.success) {
const { accessToken, refreshToken } = data.data;
setTokens(accessToken, refreshToken);
}

return data;
};
6 changes: 4 additions & 2 deletions src/shared/api/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
setTokens,
clearTokens,
} from '@/shared/utils/token';
import type { ApiResponse, TokenData } from '@/shared/api/types';
import type { ApiResponse, TokenData } from '@/shared/types/authtypes';

const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL;

Expand Down Expand Up @@ -50,7 +50,9 @@ const processQueue = (error: unknown, token: string | null = null) => {
apiWithToken.interceptors.response.use(
(response) => response,
async (error: AxiosError<ApiResponse<unknown>>) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};

if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,82 @@
// SVGO 해결후 Image → Icon으로 교체 예정
'use client';
import Image from 'next/image';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/shared/lib';


const loginButtonVariants = cva(
`
flex justify-center items-center flex-shrink-0
w-[5rem] h-[5rem] rounded-full
shadow-[0_0_4px_rgba(0,0,0,0.30)]
transition-all duration-150 active:scale-95
`,
{
variants: {
platform: {
google: 'bg-white',
kakao: 'bg-[#FEE500]',
},
},
defaultVariants: {
platform: 'google',
},
}
);

interface LoginButtonProps extends VariantProps<typeof loginButtonVariants> {
onClick: () => void;
className?: string;
}

export default function LoginButton({
onClick,
platform,
className,
}: LoginButtonProps) {
const iconData = {
google: {
src: '/svgs/GoogleIcon.svg',
alt: 'Google Logo',
width: 36,
height: 36,
label: '구글 로그인',
},
kakao: {
src: '/svgs/KakaoIcon.svg',
alt: 'Kakao Logo',
width: 28,
height: 28,
label: '카카오 로그인',
},
};

const { src, alt, width, height, label } = iconData[platform ?? 'google'];

return (
<button
type="button"
onClick={onClick}
aria-label={label}
className={cn(loginButtonVariants({ platform }), className)}
>
<Image src={src} alt={alt} width={width} height={height} priority />
</button>
);
}
'use client';
import Image from 'next/image';
import { useEffect } from 'react';
import { useRecentLogin } from '@/shared/hooks/useRecentLogin';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/shared/lib';


const loginButtonVariants = cva(
`
flex justify-center items-center flex-shrink-0
w-[5rem] h-[5rem] rounded-full
shadow-[0_0_4px_rgba(0,0,0,0.30)]
transition-all duration-150 active:scale-95
`,
{
variants: {
platform: {
google: 'bg-white',
kakao: 'bg-[#FEE500]',
},
},
defaultVariants: {
platform: 'google',
},
}
);

interface LoginButtonProps extends VariantProps<typeof loginButtonVariants> {
onClick: () => void;
className?: string;
}

export default function LoginButton({
onClick,
platform,
className,
}: LoginButtonProps) {
const iconData = {
google: {
src: '/assets/GoogleIcon.svg',
alt: 'Google Logo',
width: 36,
height: 36,
label: '구글 로그인',
},
kakao: {
src: '/assets/KakaoIcon.svg',
alt: 'Kakao Logo',
width: 28,
height: 28,
label: '카카오 로그인',
},
};

if (!platform || !(platform in iconData)) {
throw new Error(`Invalid platform: ${platform}`);
}
const { src, alt, width, height, label } = iconData[platform as keyof typeof iconData];

const handleClick = () => {
if (onClick) return onClick();
const base = process.env.NEXT_PUBLIC_BACKEND_URL;
const url =
platform === 'kakao'
? `${base}/oauth2/authorization/kakao`
: `${base}/oauth2/authorization/google`;

window.location.href = url;
};

return (
<button
type="button"
onClick={handleClick}
aria-label={label}
className={cn(loginButtonVariants({ platform }), className)}
>
<Image src={src} alt={alt} width={width} height={height} priority />
</button>
);
}
Loading
Loading