Skip to content

Commit 41ff45d

Browse files
authored
Merge branch 'develop' into feat/mypage
2 parents 006e9bb + 5a3c066 commit 41ff45d

24 files changed

Lines changed: 806 additions & 81 deletions

File tree

auth.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
import NextAuth from 'next-auth';
22
import Credentials from 'next-auth/providers/credentials';
33
import { z } from 'zod';
4+
import { JWT } from 'next-auth/jwt';
5+
import { TokenRefreshReqDto, TokenRefreshResDto } from '@/types/domain/auth';
6+
import { ApiResponse } from '@/types/common';
7+
8+
const TOKEN_REFRESH_BUFFER = 60 * 1000;
9+
10+
async function refreshAccessToken(token: JWT): Promise<JWT> {
11+
try {
12+
const response = await fetch(
13+
`${process.env.BACKEND_API_URL}/auth/token/refresh`,
14+
{
15+
method: 'POST',
16+
headers: { 'Content-Type': 'application/json' },
17+
body: JSON.stringify({
18+
refreshToken: token.refreshToken,
19+
} satisfies TokenRefreshReqDto),
20+
},
21+
);
22+
23+
if (!response.ok) {
24+
throw new Error('Failed to refresh access token');
25+
}
26+
27+
const { data }: ApiResponse<TokenRefreshResDto> = await response.json();
28+
29+
return {
30+
...token,
31+
accessToken: data.accessToken,
32+
refreshToken: data.refreshToken,
33+
accessTokenExpires: Date.now() + 60 * 60 * 1000,
34+
};
35+
} catch (error) {
36+
console.error('Refresh token error:', error);
37+
return {
38+
...token,
39+
error: 'RefreshAccessTokenError',
40+
};
41+
}
42+
}
443

544
export const { handlers, signIn, signOut, auth } = NextAuth({
645
providers: [
@@ -41,6 +80,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
4180
accessToken: data.accessToken,
4281
refreshToken: data.refreshToken,
4382
profileUrl: data.profileUrl || null,
83+
expiresIn: data.expires_in,
4484
};
4585
} catch (error) {
4686
console.error('Social Auth Error:', error);
@@ -83,6 +123,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
83123
accessToken: data.accessToken,
84124
refreshToken: data.refreshToken,
85125
profileUrl: data.profileUrl,
126+
expiresIn: data.expires_in,
86127
};
87128
} catch (error) {
88129
console.error('Login Error:', error);
@@ -95,21 +136,31 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
95136
callbacks: {
96137
async jwt({ token, user }) {
97138
if (user) {
98-
const u = user;
99-
token.accessToken = u.accessToken;
100-
token.refreshToken = u.refreshToken;
101-
token.userId = u.userId;
102-
token.nickName = u.nickName;
103-
token.profileImageUrl = u.profileUrl;
139+
const expiresIn = user.expiresIn ?? 60 * 60;
140+
return {
141+
...token,
142+
accessToken: user.accessToken,
143+
refreshToken: user.refreshToken,
144+
userId: user.userId,
145+
nickName: user.nickName,
146+
profileUrl: user.profileUrl,
147+
accessTokenExpires: Date.now() + expiresIn * 1000,
148+
};
149+
}
150+
151+
if (Date.now() < (token.accessTokenExpires ?? 0) - TOKEN_REFRESH_BUFFER) {
152+
return token;
104153
}
105-
return token;
154+
155+
return refreshAccessToken(token);
106156
},
107157
async session({ session, token }) {
108158
session.user.userId = token.userId as string;
109159
session.user.nickName = token.nickName as string;
110-
session.user.profileUrl = token.profileImageUrl;
111-
session.accessToken = token.accessToken;
112-
session.refreshToken = token.refreshToken;
160+
session.user.profileUrl = token.profileUrl;
161+
session.accessToken = token.accessToken as string;
162+
session.refreshToken = token.refreshToken as string;
163+
session.error = token.error;
113164
return session;
114165
},
115166
},

next-auth.d.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,29 @@ declare module 'next-auth' {
1010
profileUrl: string | null;
1111
accessToken: string;
1212
refreshToken: string;
13+
expiresIn?: number;
1314
}
1415

1516
interface Session extends DefaultSession {
1617
user: {
1718
userId: string;
1819
nickName: string;
19-
profileImageUrl: string | null;
20+
profileUrl: string | null;
2021
} & DefaultSession['user'];
2122
accessToken: string;
2223
refreshToken: string;
24+
error?: 'RefreshAccessTokenError';
2325
}
2426
}
2527

2628
declare module 'next-auth/jwt' {
2729
interface JWT extends DefaultJWT {
2830
userId: string;
29-
nickname: string;
30-
profileImageUrl: string | null;
31+
nickName: string;
32+
profileUrl: string | null;
3133
accessToken: string;
3234
refreshToken: string;
35+
accessTokenExpires: number;
36+
error?: 'RefreshAccessTokenError';
3337
}
3438
}

public/icons/heart-filled.svg

Lines changed: 16 additions & 0 deletions
Loading

src/app/actions/cart.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ export async function addToCart(data: CartCreateRequest) {
2525
await api.post<CartResponse, CartCreateRequest>('/carts', data);
2626
revalidatePath('/cart');
2727
return { success: true, message: '장바구니에 상품을 담았습니다.' };
28-
} catch (error: any) {
28+
} catch (error) {
2929
console.error('장바구니 담기 실패:', error);
3030
return {
3131
success: false,
32-
message: error.message || '장바구니 담기에 실패했습니다.',
32+
message:
33+
error instanceof Error ? error.message : '장바구니 담기에 실패했습니다.',
3334
};
3435
}
3536
}
@@ -40,9 +41,12 @@ export async function updateCartItem(cartId: number, data: CartUpdateRequest) {
4041
await api.patch(`/carts/${cartId}`, data);
4142
revalidatePath('/cart');
4243
return { success: true, message: '장바구니가 수정되었습니다.' };
43-
} catch (error: any) {
44+
} catch (error) {
4445
console.error('장바구니 수정 실패:', error);
45-
return { success: false, message: error.message || '수정에 실패했습니다.' };
46+
return {
47+
success: false,
48+
message: error instanceof Error ? error.message : '수정에 실패했습니다.',
49+
};
4650
}
4751
}
4852

@@ -52,9 +56,12 @@ export async function deleteCartItem(cartId: number) {
5256
await api.delete(`/carts/${cartId}`);
5357
revalidatePath('/cart');
5458
return { success: true, message: '상품이 삭제되었습니다.' };
55-
} catch (error: any) {
59+
} catch (error) {
5660
console.error('장바구니 삭제 실패:', error);
57-
return { success: false, message: error.message || '삭제에 실패했습니다.' };
61+
return {
62+
success: false,
63+
message: error instanceof Error ? error.message : '삭제에 실패했습니다.',
64+
};
5865
}
5966
}
6067

@@ -70,9 +77,12 @@ export async function deleteCartItems(cartIds: number[]) {
7077

7178
revalidatePath('/cart');
7279
return { success: true, message: '선택한 상품이 삭제되었습니다.' };
73-
} catch (error: any) {
80+
} catch (error) {
7481
console.error('장바구니 일괄 삭제 실패:', error);
75-
return { success: false, message: error.message || '삭제에 실패했습니다.' };
82+
return {
83+
success: false,
84+
message: error instanceof Error ? error.message : '삭제에 실패했습니다.',
85+
};
7686
}
7787
}
7888

src/app/actions/order.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use server';
2+
3+
import { api } from '@/lib/api-client';
4+
import { revalidatePath } from 'next/cache';
5+
import {
6+
OrderFromCartRequest,
7+
OrderFromProductRequest,
8+
OrderDetail,
9+
} from '@/types/domain/order';
10+
11+
export async function createOrderFromCart(
12+
data: OrderFromCartRequest,
13+
): Promise<number> {
14+
try {
15+
const orderId = await api.post<number, OrderFromCartRequest>(
16+
'/orders/cart',
17+
data,
18+
);
19+
revalidatePath('/cart');
20+
return orderId;
21+
} catch (error) {
22+
console.error('장바구니 상품 주문 실패:', error);
23+
throw new Error(
24+
error instanceof Error ? error.message : '장바구니 상품 주문에 실패했습니다.',
25+
);
26+
}
27+
}
28+
29+
export async function createOrderFromProduct(
30+
data: OrderFromProductRequest,
31+
): Promise<number> {
32+
try {
33+
const orderId = await api.post<number, OrderFromProductRequest>(
34+
'/orders',
35+
data,
36+
);
37+
return orderId;
38+
} catch (error) {
39+
console.error('상품 직접 주문 실패:', error);
40+
throw new Error(
41+
error instanceof Error ? error.message : '상품 직접 주문에 실패했습니다.',
42+
);
43+
}
44+
}
45+
46+
export async function getOrderDetail(orderId: number): Promise<OrderDetail> {
47+
try {
48+
const orderDetail = await api.get<OrderDetail>(`/orders/${orderId}`);
49+
return orderDetail;
50+
} catch (error) {
51+
console.error('주문 상세 조회 실패:', error);
52+
throw new Error(
53+
error instanceof Error ? error.message : '주문 상세 조회에 실패했습니다.',
54+
);
55+
}
56+
}

src/app/actions/wishlist.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use server';
2+
3+
import { revalidatePath } from 'next/cache';
4+
import { api } from '@/lib/api-client';
5+
import { WishlistItem } from '@/types/domain/wishlist';
6+
7+
/**
8+
* 찜하기 (POST)
9+
*/
10+
export async function addToWishlist(productId: number) {
11+
try {
12+
const data = await api.post<WishlistItem>(
13+
`/wishlists/products/${productId}`,
14+
{},
15+
);
16+
17+
revalidatePath('/me');
18+
revalidatePath(`/product/${productId}`);
19+
20+
return { success: true, data };
21+
} catch (error) {
22+
console.error('Add to wishlist error:', error);
23+
return { success: false, error: '찜하기에 실패했습니다.' };
24+
}
25+
}
26+
27+
/**
28+
* 찜 취소하기 (DELETE)
29+
*/
30+
export async function deleteFromWishlist(wishlistId: number) {
31+
try {
32+
await api.delete<string>(`/wishlists/${wishlistId}`);
33+
34+
revalidatePath('/me');
35+
36+
return { success: true, wishlistId };
37+
} catch (error) {
38+
console.error('Delete from wishlist error:', error);
39+
return { success: false, error: '찜 취소에 실패했습니다.' };
40+
}
41+
}
42+
43+
/**
44+
* 내 찜 목록 조회 (GET)
45+
*/
46+
export async function getMyWishlist(categoryId?: number) {
47+
try {
48+
const params = categoryId ? { categoryId } : undefined;
49+
const data = await api.get<WishlistItem[]>('/wishlists', { params });
50+
51+
return data;
52+
} catch (error) {
53+
console.error('Get wishlist error:', error);
54+
return [];
55+
}
56+
}

src/app/me/wishlist/page.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getMyWishlist } from '@/app/actions/wishlist';
2+
import WishlistGrid from '@/components/wishlist/wishlist-grid';
3+
import MainHeader from '@/components/layout/main-header';
4+
import MainNavBar from '@/components/layout/main-nav-bar';
5+
6+
export default async function WishlistPage() {
7+
const wishlistItems = await getMyWishlist();
8+
9+
return (
10+
<div className="flex min-h-screen flex-col bg-white">
11+
<MainHeader />
12+
<main className="mx-auto w-full max-w-screen-lg flex-grow px-4 pb-20">
13+
<h1 className="mt-4 mb-6 text-2xl font-bold">
14+
찜 목록
15+
{wishlistItems.length > 0 && (
16+
<span className="ml-2 text-lg font-normal text-gray-500">
17+
{wishlistItems.length}
18+
</span>
19+
)}
20+
</h1>
21+
<WishlistGrid items={wishlistItems} />
22+
</main>
23+
<MainNavBar />
24+
</div>
25+
);
26+
}

0 commit comments

Comments
 (0)