Skip to content

Commit 5a3c066

Browse files
authored
Merge pull request #53 from IT-Cotato/feat/heart,pay-api
Feat/heart-pay api
2 parents ede4ee4 + 2b9a950 commit 5a3c066

22 files changed

Lines changed: 738 additions & 68 deletions

File tree

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+
}

src/app/orders/[orderId]/page.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Metadata } from 'next';
2+
import Image from 'next/image';
3+
import Link from 'next/link';
4+
import { getOrderDetail } from '@/app/actions/order';
5+
import { CloseButton } from '@/components/ui/close-button';
6+
import { notFound } from 'next/navigation';
7+
8+
export const metadata: Metadata = {
9+
title: '주문 상세 | OnGil',
10+
description: '주문 상세 정보입니다.',
11+
};
12+
13+
interface OrderPageProps {
14+
params: Promise<{ orderId: string }>;
15+
}
16+
17+
export default async function OrderDetailPage({ params }: OrderPageProps) {
18+
const { orderId } = await params;
19+
const numericId = Number(orderId);
20+
if (Number.isNaN(numericId)) {
21+
notFound();
22+
}
23+
const order = await getOrderDetail(numericId);
24+
25+
const formatDate = (dateString: string) => {
26+
const date = new Date(dateString);
27+
return date.toLocaleDateString('ko-KR', {
28+
year: 'numeric',
29+
month: 'long',
30+
day: 'numeric',
31+
hour: '2-digit',
32+
minute: '2-digit',
33+
});
34+
};
35+
36+
const formatPrice = (price: number) => {
37+
return price.toLocaleString('ko-KR');
38+
};
39+
40+
return (
41+
<main className="mx-auto min-h-screen max-w-2xl bg-white">
42+
<header className="sticky top-0 z-20 flex h-24 w-full items-center justify-center bg-white">
43+
<div className="absolute top-1/2 left-4 ml-3 -translate-y-1/2">
44+
<CloseButton />
45+
</div>
46+
<h1 className="text-3xl leading-[18px] font-semibold">주문 상세</h1>
47+
</header>
48+
49+
<div className="px-6 pb-32">
50+
{/* 주문 번호 및 날짜 */}
51+
<section className="border-b border-gray-200 pb-6">
52+
<p className="text-sm text-gray-500">주문번호</p>
53+
<p className="mt-1 text-lg font-semibold">{order.orderNumber}</p>
54+
<p className="mt-2 text-sm text-gray-500">
55+
{formatDate(order.createdAt)}
56+
</p>
57+
</section>
58+
59+
{/* 주문 상품 목록 */}
60+
<section className="border-b border-gray-200 py-6">
61+
<h2 className="mb-4 text-xl font-semibold">주문 상품</h2>
62+
<ul className="space-y-4">
63+
{order.orderItems.map((item, index) => (
64+
<li key={index} className="flex gap-4">
65+
<Link href={`/product/${item.productId}`}>
66+
<div className="relative h-24 w-24 overflow-hidden rounded-lg bg-gray-100">
67+
<Image
68+
src={item.imageUrl}
69+
alt={item.productName}
70+
fill
71+
className="object-cover"
72+
/>
73+
</div>
74+
</Link>
75+
<div className="flex flex-1 flex-col justify-center">
76+
<p className="text-sm text-gray-500">{item.brandName}</p>
77+
<Link
78+
href={`/product/${item.productId}`}
79+
className="font-medium hover:underline"
80+
>
81+
{item.productName}
82+
</Link>
83+
<p className="mt-1 text-sm text-gray-500">
84+
{item.selectedColor} / {item.selectedSize} / {item.quantity}
85+
86+
</p>
87+
<p className="mt-1 font-semibold">
88+
{formatPrice(item.priceAtOrder * item.quantity)}
89+
</p>
90+
</div>
91+
</li>
92+
))}
93+
</ul>
94+
</section>
95+
96+
{/* 배송 정보 */}
97+
<section className="border-b border-gray-200 py-6">
98+
<h2 className="mb-4 text-xl font-semibold">배송 정보</h2>
99+
<dl className="space-y-2 text-sm">
100+
<div className="flex">
101+
<dt className="w-24 text-gray-500">받는 분</dt>
102+
<dd>{order.recipient}</dd>
103+
</div>
104+
<div className="flex">
105+
<dt className="w-24 text-gray-500">연락처</dt>
106+
<dd>{order.recipientPhone}</dd>
107+
</div>
108+
<div className="flex">
109+
<dt className="w-24 text-gray-500">배송지</dt>
110+
<dd>{order.deliveryAddress}</dd>
111+
</div>
112+
{order.deliveryMessage && (
113+
<div className="flex">
114+
<dt className="w-24 text-gray-500">배송 메모</dt>
115+
<dd>{order.deliveryMessage}</dd>
116+
</div>
117+
)}
118+
</dl>
119+
</section>
120+
121+
{/* 결제 금액 */}
122+
<section className="py-6">
123+
<h2 className="mb-4 text-xl font-semibold">결제 금액</h2>
124+
<div className="flex items-center justify-between text-2xl font-bold">
125+
<span>총 결제금액</span>
126+
<span className="text-blue-600">
127+
{formatPrice(order.totalAmount)}
128+
</span>
129+
</div>
130+
</section>
131+
</div>
132+
</main>
133+
);
134+
}

src/app/product/[id]/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation';
22
import { getProductById } from '@/components/product/product-service';
33
import { fetchUserBodyInfo, fetchSizeAnalysis } from '@/mocks/size';
44
import ProductDetailView from '@/components/product/product-detail-view';
5+
import { getMyWishlist } from '@/app/actions/wishlist';
56

67
interface PageProps {
78
params: Promise<{ id: string }>;
@@ -22,12 +23,20 @@ export default async function ProductPage({ params }: PageProps) {
2223
? await fetchSizeAnalysis(id, userInfo.height, userInfo.weight)
2324
: null;
2425

25-
// 3. 클라이언트 뷰에 데이터 전달하며 렌더링
26+
// 3. 찜 목록에서 현재 상품이 있는지 확인
27+
const wishlist = await getMyWishlist();
28+
const wishlistItem = wishlist.find((item) => item.productId === Number(id));
29+
const isLiked = !!wishlistItem;
30+
const wishlistId = wishlistItem?.wishlistId;
31+
32+
// 4. 클라이언트 뷰에 데이터 전달하며 렌더링
2633
return (
2734
<ProductDetailView
2835
product={product}
2936
userInfo={userInfo}
3037
analysisData={analysisData}
38+
isLiked={isLiked}
39+
wishlistId={wishlistId}
3140
/>
3241
);
3342
}

0 commit comments

Comments
 (0)