Skip to content

Commit

Permalink
Merge branch 'develop' into bugfix/DEV-82
Browse files Browse the repository at this point in the history
  • Loading branch information
Happhee authored Aug 6, 2024
2 parents 9390bb8 + 0cabbf3 commit 68c3ae1
Show file tree
Hide file tree
Showing 115 changed files with 3,914 additions and 177 deletions.
8 changes: 8 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const nextConfig = {
"oopy.lazyrockets.com",
"eehhqckznniu25210545.cdn.ntruss.com",
"d3ex4vlh373syu.cloudfront.net",
"storage.mrblog.net",
"d3ex4vlh373syu.cloudfront.net",
"www.fig1.kr",
"cdn.maily.so",
"maily.so",
"pension.inmostadvisor.com",
"localhost",

],
},
webpack: (config, context) => {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"@hookform/resolvers": "^3.6.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-query": "^5.40.0",
"@tanstack/react-query-devtools": "^5.40.1",
Expand All @@ -33,6 +35,7 @@
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.5",
"react-intersection-observer": "^9.13.0",
"react-lottie-player": "^2.0.0",
"shadcn-ui": "^0.8.0",
"tailwind-merge": "^2.3.0",
Expand Down
272 changes: 272 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions public/.well-known/apple-app-site-association
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"applinks": {
"details": [
{
"appIDs": [
"X64MATB2CC.sju.D3N"
],
"components": [
{
"/": "*"
}
]
}
]
}
}
6 changes: 6 additions & 0 deletions public/assets/icon/cardFewLogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/assets/icon/eye.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/assets/icon/fewlogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/assets/icon/hamburgerMenu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/assets/icon/skeletonFewLogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/assets/icon/up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/assets/icon/x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/enterlogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 80 additions & 5 deletions src/api/fewFetch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { COOKIES } from "@shared/constants/token";
import { getTokenCookie } from "@shared/utils/getTokenCookie";
import { setCookie } from "cookies-next";

export type FewResponse<DataType extends object> = {
data: DataType;
message: string;
Expand Down Expand Up @@ -29,7 +33,7 @@ type Interceptor = {
const processApiResponse = async <T extends object>(
response: Response,
config: RequestInit,
) => {
): Promise<ApiResponse<T>> => {
const data = (await response.json().catch(() => ({}))) as FewResponse<T>;
const { headers, ok, redirected, status, statusText, type, url } = response;
return {
Expand All @@ -45,12 +49,82 @@ const processApiResponse = async <T extends object>(
};
};

// refreshToken 으로 새로운 accessToken 발급 받기
const refreshAccessToken = async (): Promise<string> => {
const refreshToken = document.cookie
.split("; ")
.find((row) => row.startsWith(`${COOKIES.REFRESH_TOKEN}=`))
?.split("=")[1];

if (!refreshToken) {
throw new Error("No refresh token found");
}

const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/memebers/token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refreshToken }),
},
);

if (!response.ok) {
throw new Error("Failed to refresh access token");
}

const data = await response.json();

setCookie(COOKIES.ACCESS_TOKEN, data.accessToken, {
maxAge: 24 * 60 * 60, // 30 days
path: "/",
});

setCookie(COOKIES.REFRESH_TOKEN, data.refreshToken, {
maxAge: 30 * 24 * 60 * 60, // 30 days
path: "/",
});

return data.accessToken;
};

const fetInterceptor: Interceptor = {
onRequest: (config) => config,
onResponse: (response) => response,
onRequest: (config) => {
const accessToken = getTokenCookie();

if (accessToken) {
config.headers = {
...config.headers,
Authorization: `Bearer ${accessToken}`,
};
}
return config;
},
onResponse: async <T extends object>(response: ApiResponse<T>) => {
if (!response.ok && response.status === 401) {
try {
const newAccessToken = await refreshAccessToken();
if (typeof document !== "undefined") {
response.config.headers = {
...response.config.headers,
Authorization: `Bearer ${newAccessToken}`,
};
}
const retryResponse = await fetch(response.url, response.config);
return processApiResponse<T>(retryResponse, response.config);
} catch (error) {
console.error("Failed to refresh token", error);
return Promise.reject(error);
}
}
return response;
},
onRequestError: (reason) => Promise.reject(reason),
onResponseError: (reason) => Promise.reject(reason),
};

export const fewFetch = (
defaultConfig: RequestInit = {
headers: {
Expand All @@ -61,8 +135,8 @@ export const fewFetch = (
) => {
const request = async <T extends object = object>(
url: string,
config: RequestInit,
) => {
config: RequestInit = {},
): Promise<ApiResponse<T>> => {
if (!url.startsWith("http")) {
url = `${process.env.NEXT_PUBLIC_API_BASE_URL}${url}`;
} else {
Expand All @@ -81,6 +155,7 @@ export const fewFetch = (
return fetInterceptor.onRequestError(error);
}
};

return {
request,
get: <T extends object>(url: string, config: RequestInit = {}) =>
Expand Down
6 changes: 4 additions & 2 deletions src/app/auth/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import EmailForm from "@auth/components/EmailForm";
import LoginLogo from "@auth/components/LoginLogo";

export default function AuthPage() {
return (
<>
<div className="flex h-full flex-col gap-[41px]">
<LoginLogo />
<EmailForm />
</>
</div>
);
}
64 changes: 64 additions & 0 deletions src/app/auth/validation/complete/Login.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

import { SIGNUP_COMPLETED } from "@auth/constants/auth";
import { COOKIES } from "@shared/constants/token";
import { render, screen, waitFor } from "@testing-library/react";
import ValidationCompletePage from "./page";

// Mocking useRouter
const mockPush = vi.fn();

describe("ValidationCompletePage 페이지 테스트", () => {
const queryClient = new QueryClient();

const renderWithClient = () => {
return render(
<QueryClientProvider client={queryClient}>
<ValidationCompletePage />
</QueryClientProvider>,
);
};

beforeAll(() => {
vi.mock("next/navigation", async () => {
const actual =
await vi.importActual<typeof import("next/navigation")>(
"next/navigation",
);
return {
...actual,
useSearchParams: vi.fn(() => ({
get: vi.fn().mockReturnValueOnce(() => "test-token"),
})),
useRouter: vi.fn(() => ({
mockPush,
})),
};
});
});

beforeEach(() => {
renderWithClient();
});

it("인증을 완료하고 쿠키를 세팅한다.", async () => {
await waitFor(() => {
expect(document.cookie).toContain(
`${COOKIES.ACCESS_TOKEN}=${COOKIES.ACCESS_TOKEN}`,
);
expect(document.cookie).toContain(
`${COOKIES.REFRESH_TOKEN}=${COOKIES.REFRESH_TOKEN}`,
);
});
});

it("컴포넌트 내 요소들을 올바르게 렌더링 한다.", () => {
expect(screen.getByText(SIGNUP_COMPLETED.GRETTING)).toBeInTheDocument();
expect(screen.getByText(SIGNUP_COMPLETED.INTRO)).toBeInTheDocument();
expect(
screen.getByRole("button", { name: SIGNUP_COMPLETED.MAIN_BUTTON }),
).toBeInTheDocument();
});
});
35 changes: 35 additions & 0 deletions src/app/auth/validation/complete/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client"

import { useRouter, useSearchParams } from "next/navigation";

import LottieClient from "@shared/components/Lottie";
import { Button } from "@shared/components/ui/button";

import { SIGNUP_COMPLETED } from "@auth/constants/auth";
import { useAuth } from "@auth/hooks/useAuth";
import lottieJson from "public/assets/Problem_Complete.json";
import FewLogo from "public/enterlogo.svg";

export default function ValidationCompletePage() {
const searchParams = useSearchParams();
const router = useRouter();

const auth_token = searchParams.get("auth_token");
useAuth(auth_token? auth_token : "");

return (
<div className="flex h-auto flex-col items-center">
<LottieClient animationData={lottieJson} />
<FewLogo />
<span className="h3-bold mb-[20px] mt-[63px] text-black">
{SIGNUP_COMPLETED.GRETTING}
</span>
<span className="sub3-semibold text-text-gray5 mb-[123px]">
{SIGNUP_COMPLETED.INTRO}
</span>
<Button className="h-[56px] w-full rounded-none bg-main py-6 text-white cursor-pointer" onClick={() => router.push('/')}>
{SIGNUP_COMPLETED.MAIN_BUTTON}
</Button>
</div>
);
}
21 changes: 21 additions & 0 deletions src/app/auth/validation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";
import { useSearchParams } from "next/navigation";

import SignupProgress from "@auth/components/SignupProgress";
import FewLogo from "public/enterlogo.svg";

export default function ValidationPage() {
const searchParams = useSearchParams();

const email = searchParams.get("email");

return (
<div className="flex h-full flex-col items-center">
<FewLogo />
<span className="h3-bold mb-[20px] mt-[63px] text-text-gray1">
{email}
</span>
<SignupProgress />
</div>
);
}
8 changes: 7 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
@apply font-pretendard text-[15px]/[24.75px] font-normal;
}

.body3-bold {
@apply font-pretendard text-[14px]/[23.1px] font-bold;
}
.body3-medium {
@apply font-pretendard text-[14px]/[21px] font-medium;
}
Expand All @@ -119,7 +122,10 @@
@apply font-pretendard text-[16px]/[25.44px] font-medium;
}
.sub3-semibold {
@apply font-pretendard text-[12px] font-semibold
@apply font-pretendard text-[12px] font-semibold;
}
.sub3-medium {
@apply font-pretendard text-[12px]/[18px] font-medium;
}

.scrollbar-hide {
Expand Down
Loading

0 comments on commit 68c3ae1

Please sign in to comment.