Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 8 additions & 1 deletion apps/web/src/app/_components/SplashClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ function SplashClient() {

return (
<main
className={`relative h-dvh w-full ${isBackgroundShifted ? 'splash-bg-shift bg-gray-50' : 'bg-[#A2DEFF]'}`}
className="relative"
/** FOUC 방지하기 위해 인라인 스타일로 적용 */
style={{
height: '100dvh',
width: '100%',
backgroundColor: isBackgroundShifted ? 'var(--color-gray-50)' : '#A2DEFF',
transition: isBackgroundShifted ? 'background-color 0.7s ease-in-out' : 'none',
}}
>
{/**
* 로그인 페이지와 동일한 레이아웃 앵커. 보이지 않지만 로고가 이동할 최종 좌표를 측정한다.
Expand Down
9 changes: 0 additions & 9 deletions apps/web/src/app/_components/splash.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,9 @@
animation: splash-fade-in 1.5s ease-in forwards;
}

/** SplashClient SPLASH_SHRINK_MS(700)와 동기화 */
.splash-bg-shift {
transition: background-color 0.7s ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
.splash-logo {
opacity: 1;
animation: none;
}

.splash-bg-shift {
transition: none;
}
}
4 changes: 2 additions & 2 deletions apps/web/src/app/archive/wish/_hooks/useShareIntentWish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export const useShareIntentWish = () => {
if (processedUrlsRef.current.has(url)) return;

processedUrlsRef.current.add(url);
/** 실패한 URL은 잠금 해제해 재공유 시 다시 시도되도록 */
/** 처리 완료 후 잠금 해제 — in-flight 중복만 차단하고 재공유는 허용 */
postWishLinkMutation(url, {
onError: () => processedUrlsRef.current.delete(url),
onSettled: () => processedUrlsRef.current.delete(url),
});
},
[postWishLinkMutation]
Expand Down
31 changes: 10 additions & 21 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,12 @@
import { GoogleAnalytics } from '@next/third-parties/google';
import { WEBVIEW_UA_TOKEN } from '@piki/core';
import type { Metadata, Viewport } from 'next';
import localFont from 'next/font/local';
import { headers } from 'next/headers';
import { GoogleAnalytics } from '@next/third-parties/google';
import React from 'react';

import Providers from '../components/Providers';
import '../styles/globals.css';

const pretendard = localFont({
src: '../assets/fonts/PretendardVariable.woff2',
display: 'swap',
weight: '45 920',
preload: true,
fallback: [
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Helvetica Neue',
'sans-serif',
],
});

export const metadata: Metadata = {
title: 'PiKi - 같이 고르는 쇼핑 토너먼트',
description: '흩어진 위시를 한곳에 모아 토너먼트로 결정해보세요.',
Expand Down Expand Up @@ -54,10 +35,18 @@ async function RootLayout({
return (
<html
lang="ko"
className={`${pretendard.className} h-full overflow-hidden antialiased`}
className="h-full overflow-hidden antialiased"
{...(isWebview && { 'data-app': '' })}
>
<head>
{/** Pretendard Dynamic Subset CSS */}
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossOrigin="anonymous" />
<link
rel="stylesheet"
as="style"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
/>

{process.env.NODE_ENV === 'development' && !isWebview && (
<>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
Expand Down
36 changes: 16 additions & 20 deletions apps/web/src/app/login/_components/LoginButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,11 @@ import SocialLoginButton from './SocialLoginButton';
type LoginButtonsProps = {
redirect: string | null;
action: string | null;
/** 살아있는 게스트 세션 보유 여부 — 서버에서 httpOnly 쿠키 확인 후 전달 */
canReuseGuestSession: boolean;
/** Android 웹뷰에서는 false — 네이티브 Apple 로그인이 iOS 전용이라 미노출 */
showAppleLogin: boolean;
};

function LoginButtons({
redirect,
action,
canReuseGuestSession,
showAppleLogin,
}: LoginButtonsProps) {
function LoginButtons({ redirect, action, showAppleLogin }: LoginButtonsProps) {
const router = useRouter();
const validRedirect = isValidLoginRedirectPath(redirect) ? redirect : null;

Expand Down Expand Up @@ -106,21 +99,24 @@ function LoginButtons({
const handleGoogleLogin = () => handleSocialLogin('google');
const handleAppleLogin = () => handleSocialLogin('apple');

/**
* 게스트 로그인
*
* - 기존 게스트 세션 재활용 시도
* - 재활용 불가 시 새 게스트 발급
*/
const handleGuestLogin = async () => {
setLoginRedirectPath(validRedirect);

/** 살아있는 게스트 세션이면 토큰만 갱신 */
if (canReuseGuestSession) {
setIsGuestRefreshing(true);
try {
await refreshClientToken();
router.replace(getPostLoginRedirectPath());
return;
} catch {
/** 갱신 실패 시 새 게스트 발급 */
} finally {
setIsGuestRefreshing(false);
}
setIsGuestRefreshing(true);
try {
await refreshClientToken();
router.replace(getPostLoginRedirectPath());
return;
} catch {
/** 세션 재활용 불가 */
} finally {
setIsGuestRefreshing(false);
}

postGuestLoginMutation();
Expand Down
23 changes: 0 additions & 23 deletions apps/web/src/app/login/layout.tsx

This file was deleted.

21 changes: 10 additions & 11 deletions apps/web/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { WEBVIEW_UA_TOKEN } from '@piki/core';
import { headers } from 'next/headers';
import { cookies, headers } from 'next/headers';
import Link from 'next/link';
import { redirect } from 'next/navigation';

import { getMe } from '@/apis/getMe';
import PikiLogo from '@/assets/images/piki-logo.svg';
import { QUERY_ACTION } from '@/consts/queryAction';
import { ROUTES } from '@/consts/route';
import { getQueryClient } from '@/utils/queryClient';
import { getRoleFromToken } from '@/utils/auth';

import LoginButtons from './_components/LoginButtons';

Expand All @@ -16,11 +17,10 @@ type LoginPageProps = {
async function LoginPage({ searchParams }: LoginPageProps) {
const { redirect: redirectParam, action } = await searchParams;

/** 게스트 세션 재활용 가능 여부 판단 */
const user = await getQueryClient()
.fetchQuery({ queryKey: ['me'], queryFn: getMe })
.catch(() => null);
const canReuseGuestSession = user?.identityType === 'GUEST';
/** 멤버가 로그인 페이지 직접 진입 시 홈으로 리다이렉트 */
const accessToken = (await cookies()).get('access_token')?.value;
const role = getRoleFromToken(accessToken);
if (role === 'MEMBER' && action !== QUERY_ACTION.VALUE.SESSION_EXPIRED) redirect(ROUTES.HOME);

/**
* Android 웹뷰에서는 Apple 로그인 미노출.
Expand All @@ -34,16 +34,15 @@ async function LoginPage({ searchParams }: LoginPageProps) {
<div className="flex min-h-dvh flex-col items-center bg-gray-50 px-4 pt-padding-top pb-10">
<div className="mt-15 flex flex-col items-center gap-6">
<PikiLogo aria-label="PIKI" />
<p className="text-center body-1-bold whitespace-pre-line text-text-neutral-secondary">
<p className="text-center body-1-bold whitespace-pre-line text-text-neutral-secondary animate-in fade-in-0 duration-500">
{'매일 쌓여만 가던\n위시리스트가 오늘의 결정으로'}
</p>
Comment thread
iOdiO89 marked this conversation as resolved.
</div>

<div className="mt-[90px] w-full">
<div className="mt-[90px] w-full animate-in fade-in-0 duration-500">
<LoginButtons
redirect={redirectParam ?? null}
action={action ?? null}
canReuseGuestSession={canReuseGuestSession}
showAppleLogin={!isAndroidWebview}
/>

Expand Down
Binary file removed apps/web/src/assets/fonts/PretendardVariable.woff2
Binary file not shown.
16 changes: 14 additions & 2 deletions apps/web/src/components/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import dynamic from 'next/dynamic';
import { type ReactNode } from 'react';

import NavigationOverlay from '@/components/navigation-overlay';
Expand All @@ -12,6 +12,18 @@ import { useDeepLink } from '@/hooks/useDeepLink';
import { useFcmTokenSync } from '@/hooks/useFcmTokenSync';
import { getQueryClient } from '@/utils/queryClient';

/** Production 빌드에서는 React Query Devtools 미포함 */
const ReactQueryDevtools =
process.env.NODE_ENV === 'development'
? dynamic(
() =>
import('@tanstack/react-query-devtools').then(mod => ({
default: mod.ReactQueryDevtools,
})),
{ ssr: false }
)
: null;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
function FcmTokenSyncer() {
useFcmTokenSync();
return null;
Expand All @@ -38,7 +50,7 @@ function Providers({ children }: Readonly<{ children: ReactNode }>) {
{children}
<NotificationSSEProvider />
<Toaster />
<ReactQueryDevtools initialIsOpen={false} />
{ReactQueryDevtools && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
);
}
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,17 @@
html,
body {
background-color: var(--color-bg-layer-basement);
font-family:
'Pretendard Variable',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Helvetica Neue',
sans-serif;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { UserIdentityTypeT } from '@/types/user';

/** JWT 토큰 페이로드 추출 */
const decodeJwt = (token: string) => {
try {
Expand Down Expand Up @@ -29,3 +31,11 @@ export const isTokenValid = (token: string) => {

return currentTime < expiryTime;
};

/** access token 의 role(GUEST/MEMBER) 추출 — 만료·손상 토큰은 null */
export const getRoleFromToken = (token?: string): UserIdentityTypeT | null => {
if (!token || !isTokenValid(token)) return null;

const role = decodeJwt(token)?.role;
return role === 'GUEST' || role === 'MEMBER' ? role : null;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading