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
3 changes: 2 additions & 1 deletion src/entities/post/api/getPosts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { serverFetch } from "@/shared/api/fetcher.server";
import { apiFetch } from "@/shared/api/fetcher";
import { POST_PAGE_SIZE } from "@/entities/post/model/constants/api";
import type { Post } from "@/entities/post/model/types/post";

Expand All @@ -22,7 +23,7 @@ export async function getPosts({
if (keyword) query.append("keyword", keyword);
if (sort) query.append("sort", sort);

const { data } = await serverFetch<{ data: Post[] }>(
const { data } = await apiFetch<{ data: Post[] }>(
`/api/postings?${query.toString()}`,
{ method: "GET" },
);
Expand Down
7 changes: 6 additions & 1 deletion src/entities/user/ui/card/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Image from "next/image";
import { useAuthStore } from "@/features/auth/model/auth.store";
import Button from "@/shared/ui/Button/Button";
import DefaultProfileImage from "./assets/profile.jpg";
import { apiFetch } from "@/shared/api/fetcher";

export interface ProfileProps {
nickname: string;
Expand Down Expand Up @@ -80,7 +81,11 @@ const Profile = ({
<Button
variant="tertiary"
className="w-full md:w-full xl:w-full"
onClick={() => {
onClick={async () => {
await apiFetch("/auth/logout", {
method: "POST",
credentials: "include",
});
logout();
router.push("/login");
}}
Expand Down
1 change: 0 additions & 1 deletion src/features/auth/ui/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export const LoginForm = ({
}>("/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
noAuth: true,
});

setAccessToken(res.accessToken);
Expand Down
1 change: 0 additions & 1 deletion src/features/auth/ui/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => {
const res = await apiFetch("/api/users/", {
method: "POST",
body: JSON.stringify(body),
noAuth: true,
});

console.log("회원가입 성공:", res);
Expand Down
22 changes: 22 additions & 0 deletions src/shared/api/fetcher.server.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import "server-only";

import { cookies } from "next/headers";

const BASE_URL = process.env.NEXT_PUBLIC_API_URL;

export async function serverFetch<T>(
endpoint: string,
options?: RequestInit,
): Promise<T> {
const cookieStore = await cookies();
const cookieHeader = [
cookieStore.get("accessToken")
? `accessToken=${cookieStore.get("accessToken")?.value}`
: "",
cookieStore.get("refreshToken")
? `refreshToken=${cookieStore.get("refreshToken")?.value}`
: "",
]
.filter(Boolean)
.join("; ");

const res = await fetch(`${BASE_URL}${endpoint}`, {
headers: {
"Content-Type": "application/json",
Cookie: cookieHeader,
},
cache: "no-store",
credentials: "include",
...options,
});

if (res.status === 401) {
//TODO: SSR prefetch 시 에러 핸들링 로직 추가
}

if (!res.ok) {
const text = await res.text();
throw new Error(text || `Server API error: ${res.status}`);
Expand Down
51 changes: 12 additions & 39 deletions src/shared/api/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,35 @@
import { useAuthStore } from "@/features/auth/model/auth.store";
import { refreshAccessToken } from "./refresh";
import { useModalStore } from "@/shared/model/modal.store";

const BASE_URL = process.env.NEXT_PUBLIC_API_URL;

export async function apiFetch<T>(
endpoint: string,
options: RequestInit & { noAuth?: boolean },
options: RequestInit,
): Promise<T> {
const { headers, noAuth, ...restOptions } = options;
const { accessToken, setAccessToken, logout } = useAuthStore.getState();
const { headers, ...restOptions } = options;
const { openModal, closeModal } = useModalStore.getState();

const defaultHeaders: HeadersInit = {
"Content-Type": "application/json",
};

if (!noAuth && typeof window !== "undefined") {
defaultHeaders["Authorization"] = `Bearer ${accessToken}`;
}

let res = await fetch(`${BASE_URL}${endpoint}`, {
const res = await fetch(`${BASE_URL}${endpoint}`, {
headers: { ...defaultHeaders, ...headers },
cache: "no-store",
credentials: "include",
...restOptions,
});

//AccessToken 만료 처리
if (res.status === 401 && !noAuth) {
const newToken = await refreshAccessToken();

if (newToken) {
setAccessToken(newToken);
defaultHeaders["Authorization"] = `Bearer ${newToken}`;

//동일한 경로에 요청 재시도
res = await fetch(`${BASE_URL}${endpoint}`, {
headers: { ...defaultHeaders, ...headers },
cache: "no-store",
credentials: "include",
...restOptions,
});
} else {
logout();
openModal("normal", {
message: "세션이 만료되었습니다. 다시 로그인 해주세요.",
buttonText: "확인",
onClick: () => {
closeModal();
if (typeof window !== "undefined") {
location.replace("/login");
}
},
});

throw new Error("세션이 만료되었습니다. 다시 로그인 해주세요.");
}
if (res.status === 401) {
openModal("normal", {
message: "세션이 만료되었습니다. 다시 로그인 해주세요.",
buttonText: "확인",
onClick: () => {
closeModal();
location.replace("/login");
},
});
}

if (!res.ok) {
Expand Down
7 changes: 6 additions & 1 deletion src/widgets/header/ui/MobileSideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Link from "next/link";
import DeleteIcon from "@/shared/images/delete.svg";
import { useAuthStore } from "@/features/auth/model/auth.store";
import cn from "@/shared/lib/cn";
import { apiFetch } from "@/shared/api/fetcher";

export default function MobileSideMenu({
onClose,
Expand Down Expand Up @@ -105,7 +106,11 @@ export default function MobileSideMenu({
<li>
<button
type="button"
onClick={() => {
onClick={async () => {
await apiFetch("/auth/logout", {
method: "POST",
credentials: "include",
});
logout();
handleClose();
router.push("/");
Expand Down