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
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MUSINSSAK</title>
<!-- 포트원 V2 SDK -->
<script src="https://cdn.portone.io/v2/browser-sdk.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import {
Home,
Login,
Mypage,
Order,
Payment,
PaymentSuccess,
ProductDetail,
ProductInquiry,
SignUp,
} from "./pages";

import type { Message } from "./types/types";

function GlobalAppSetup() {
Expand Down Expand Up @@ -71,6 +74,8 @@ export default function App() {
/>
<Route path="/cart" element={<Cart />} />
<Route path="/payment" element={<Payment />} />
<Route path="/payment/success" element={<PaymentSuccess />} />
<Route path="/order" element={<Order />} />
<Route path="/mypage/:tab?" element={<Mypage />} />
</Routes>

Expand Down
116 changes: 116 additions & 0 deletions src/api/cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { api } from "../lib/axios";

// Cart API 응답 타입들
type CartItem = {
cartItemId: number;
productId: number;
productName: string;
brandName: string;
productImageUrl: string;
size: string;
quantity: number;
originalPrice: number;
salePrice: number;
discountRate: number;
selected: boolean;
stock: number;
};

type CartGetResponse = {
userId: number;
cartItems: CartItem[];
selectedCount: number;
totalProductAmount: number;
totalCount: number;
totalPrice: number;
discountAmount: number;
deliveryFee: number;
finalAmount: number;
};

type CartChangeQuantityResponse = {
cartItemId: number;
oldQuantity: number;
newQuantity: number;
itemTotalPrice: number;
availableStock: number;
canIncrease: boolean;
canDecrease: boolean;
};

type CartDeleteResponse = {
deletedCount: number;
remainingItems: number;
};

type CartSelectResponse = {
selectedCount: number;
selectedTotalPrice: number;
};

type ApiSuccessMessage = {
message: string;
};

// 장바구니 조회
export async function getCart(): Promise<CartGetResponse> {
const res = await api.get("/cart");
return res.data.data as CartGetResponse;
}

// 장바구니에 상품 추가
export async function addToCart(
productOptionId: number,
quantity: number,
): Promise<ApiSuccessMessage> {
const res = await api.post("/cart", { productOptionId, quantity });
return { message: res.data.message };
}

// 장바구니 수량 변경
export async function changeCartQuantity(
cartItemId: number,
quantity: number,
): Promise<CartChangeQuantityResponse> {
const res = await api.put(`/cart/${cartItemId}/quantity`, { quantity });
return res.data.data as CartChangeQuantityResponse;
}

// 단일 장바구니 항목 삭제
export async function deleteCartItem(
cartItemId: number,
): Promise<CartDeleteResponse> {
const res = await api.delete(`/cart/items/${cartItemId}`);
return res.data.data as CartDeleteResponse;
}

// 여러 장바구니 항목 삭제
export async function deleteCartSelected(
cartItemIds: number[],
): Promise<CartDeleteResponse> {
const res = await api.delete("/cart/items", { data: { cartItemIds } });
return res.data.data as CartDeleteResponse;
}

// 장바구니 선택 상태 변경 (특정 항목들)
export async function selectCartItems(
cartItemIds: number[],
isSelected: boolean,
): Promise<CartSelectResponse> {
const res = await api.put("/cart/select", { cartItemIds, isSelected });
return res.data.data as CartSelectResponse;
}

// 장바구니 전체 선택/해제
export async function selectCartAll(
isSelected: boolean,
): Promise<CartSelectResponse> {
const res = await api.put("/cart/select", { selectAll: true, isSelected });
return res.data.data as CartSelectResponse;
}

// 장바구니에서 주문 생성
export async function createOrderFromCart(cartItemIds: number[]) {
const res = await api.post("/orders/create", { cartItemIds });
return res.data.data;
}
100 changes: 100 additions & 0 deletions src/api/orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { api } from "../lib/axios";

export type OrderItemsApiData = {
orderPk: number;
orderId: number;
orderNumber: string;
status: string;
reservationExpiresAt: string;
totalProductAmount: number;
discountAmount: number;
deliveryFee: number;
finalAmount: number;
items: Array<{
id: number;
isDefault: boolean;
// 추가 필드들이 백엔드에서 제공되면 여기에 추가
cartItemId?: number;
productId?: number;
productName?: string;
brandName?: string;
size?: string;
quantity?: number;
salePrice?: number;
originalPrice?: number;
imageUrl?: string | null;
thumbnailImageUrl?: string | null;
}>;
summary?: {
originalTotal: number;
cartDiscount: number;
couponDiscount: number;
pointsUsed: number;
shippingFee: number;
finalAmount: number;
};
remainingTimeSeconds?: number;
};

export async function getOrderItems(orderIdentifier: string | number) {
let url: string;

if (typeof orderIdentifier === "number") {
// 숫자 orderPk인 경우 기존 API 사용
url = `/orders/${orderIdentifier}/items`;
} else {
// 문자열 orderNumber인 경우 새로운 API 사용
const trimmed = orderIdentifier.trim();
if (!trimmed) {
throw new Error("orderIdentifier is required");
}
url = `/orders/number/${trimmed}/items`;
}

const response = await api.get<{ data: OrderItemsApiData }>(url);
return response.data.data;
}

type UpdateOrderInfoRequest = {
deliveryInfo: {
recipient: string;
phone: string;
address: string;
detailAddress: string;
postalCode?: string;
deliveryRequest: string;
};
ordererInfo: {
name: string;
email: string;
phone: string;
};
};

export async function updateOrderInfo(
orderIdentifier: string | number,
data: UpdateOrderInfoRequest,
) {
let resolved: string;

if (typeof orderIdentifier === "number") {
resolved = orderIdentifier.toString();
} else {
const trimmed = orderIdentifier.trim();

// 백엔드가 Long 타입만 받으므로 문자열에서 숫자 부분 추출
const match = trimmed.match(/\d+/);
if (match) {
resolved = match[0];
} else {
resolved = trimmed;
}
}

if (!resolved) {
throw new Error("orderIdentifier is required");
}

const response = await api.put(`/orders/${resolved}`, data);
return response.data;
}
133 changes: 133 additions & 0 deletions src/api/payments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { api } from "../lib/axios";

// 결제 정보 조회 API 응답 타입
export type PaymentInfoResponse = {
orderPk: number;
orderId: string;
orderItems: Array<{
id: number;
productName: string;
brandName: string;
size: string;
quantity: number;
price: number;
imageUrl?: string;
}>;
paymentSummary: {
finalAmount: number;
};
deliveryInfo: {
recipient: string;
phone: string;
address: string;
detailAddress: string;
deliveryRequest: string;
};
remainingTime: string;
};

// 결제 요청 API 응답 타입
type PaymentRequestResponse = {
paymentId: string;
merchantId: string;
channelKey: string; // V2 API용 추가
orderName: string;
totalAmount: number;
currency: string;
customerName: string;
customerEmail: string;
returnUrl: string;
notificationUrl: string;
};

// 결제 정보 조회
export async function getPaymentInfo(
orderId: string,
): Promise<PaymentInfoResponse> {
const response = await api.get<{ data: PaymentInfoResponse }>(
`/payments/${orderId}/info`,
);
return response.data.data;
}

// 결제 요청
export async function requestPayment(
orderId: string,
): Promise<PaymentRequestResponse> {
try {
console.log("결제 요청 API 호출:", {
orderId,
url: `/payments/${orderId}/request`,
});
const response = await api.post<{ data: PaymentRequestResponse }>(
`/payments/${orderId}/request`,
{},
);
console.log("결제 요청 API 성공:", response.data);
return response.data.data;
} catch (error: unknown) {
const apiError = error as {
response?: { status?: number; statusText?: string; data?: unknown };
message?: string;
};
console.error("결제 요청 API 오류:", {
orderId,
status: apiError.response?.status,
statusText: apiError.response?.statusText,
data: apiError.response?.data,
message: apiError.message,
});
throw error;
}
}

// 결제 완료 API 타입
type PaymentCompleteRequest = {
transactionId: string;
status: string;
};

type PaymentCompleteResponse = {
orderNumber: string;
paymentId: string;
transactionId: string;
paymentStatus: string;
completedAt: string;
finalAmount: number;
};

// 결제 완료 알림
export async function completePayment(
paymentId: string,
data: PaymentCompleteRequest,
): Promise<PaymentCompleteResponse> {
try {
console.log("=== 백엔드 결제 완료 API 호출 시작 ===");
console.log("결제 완료 API 호출:", {
paymentId,
url: `/payments/${paymentId}/complete`,
data,
});

const response = await api.post<{ data: PaymentCompleteResponse }>(
`/payments/${paymentId}/complete`,
data,
);

console.log("✅ 백엔드 결제 완료 처리 성공:", response.data);
return response.data.data;
} catch (error: unknown) {
const apiError = error as {
response?: { status?: number; statusText?: string; data?: unknown };
message?: string;
};
console.error("❌ 백엔드 결제 완료 처리 실패:", {
paymentId,
status: apiError.response?.status,
statusText: apiError.response?.statusText,
data: apiError.response?.data,
message: apiError.message,
});
throw error;
}
}
2 changes: 1 addition & 1 deletion src/components/atoms/NumberStepper/NumberStepper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Minus, Plus } from "lucide-react";
import styles from "./NumberStepper.module.css";

export type NumberStepperProps = {
type NumberStepperProps = {
value: number;
min: number;
max: number;
Expand Down
Loading