Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
74fe111
chore: Daum 우편번호 검색 μ„œλΉ„μŠ€ λ¦¬μ•‘νŠΈ 라이브러리 및 shadcn μΆ”κ°€
Seoje1405 Feb 9, 2026
88a58b1
feat: order νƒ€μž… μ •μ˜
Seoje1405 Feb 9, 2026
61690a7
feat: μ£Όλ¬Έ, μƒν’ˆ, μœ μ € μ„œλ²„ μ•‘μ…˜ μ •μ˜
Seoje1405 Feb 9, 2026
106131e
chore: 결제 νŽ˜μ΄μ§€λ‘œ λΌμš°νŒ… λ˜λ„λ‘ μˆ˜μ •
Seoje1405 Feb 9, 2026
0d601d1
feat: 슀크둀 슀파이용 μ„Ήμ…˜ μƒμˆ˜ μ •μ˜
Seoje1405 Feb 9, 2026
30021f1
feat: 상단 헀더와 λ„€λΉ„κ²Œμ΄μ…˜ μ»΄ν¬λ„ŒνŠΈ κ΅¬ν˜„
Seoje1405 Feb 9, 2026
94d19ef
feat: μ£Όλ¬Έ 정보 μ»΄ν¬λ„ŒνŠΈ κ΅¬ν˜„
Seoje1405 Feb 9, 2026
8c3c340
feat: 배솑지 μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ κ΅¬ν˜„(μΆ”ν›„ 배솑지 API κ΅¬ν˜„μ‹œ μˆ˜μ •)
Seoje1405 Feb 9, 2026
975bdfb
feat: 적립금/μΊμ‹œ μ»΄ν¬λ„ŒνŠΈ κ΅¬ν˜„
Seoje1405 Feb 9, 2026
db2c61e
feat: 결제 νŽ˜μ΄μ§€μš© 슀크둀 슀파이 κ΅¬ν˜„
Seoje1405 Feb 9, 2026
6aea8d4
feat: 결제 νŽ˜μ΄μ§€ μƒνƒœ κ΄€λ¦¬μš© Context κ΅¬ν˜„
Seoje1405 Feb 9, 2026
0e78a7c
feat: 결제 νŽ˜μ΄μ§€ κ΅¬ν˜„
Seoje1405 Feb 9, 2026
5c318cc
chore: μ•„μ΄μ½˜ 에셋 μΆ”κ°€
Seoje1405 Feb 9, 2026
222ac29
feat: μ£Όλ¬Έ μ™„λ£Œ νŽ˜μ΄μ§€ κ΅¬ν˜„
Seoje1405 Feb 9, 2026
b433bff
refactor: 이미지 에셋 경둜 μˆ˜μ •
Seoje1405 Feb 9, 2026
c95be73
fix: 폴더λͺ… μˆ˜μ •
Seoje1405 Feb 9, 2026
94f02a7
fix: μ½”λ“œλž˜λΉ— 반영
Seoje1405 Feb 10, 2026
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"next-auth": "5.0.0-beta.30",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-daum-postcode": "^3.2.0",
"react-dom": "19.2.3",
"react-hook-form": "^7.71.1",
"tailwind-merge": "^3.4.0",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions public/icons/bag-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 97 additions & 1 deletion src/app/actions/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@

import { api } from '@/lib/api-client';
import { revalidatePath } from 'next/cache';
import { redirect, notFound } from 'next/navigation';
import {
OrderFromCartRequest,
OrderFromProductRequest,
OrderDetail,
OrderListResponse,
OrderListParams,
} from '@/types/domain/order';
import type { PaymentDisplayItem } from '@/app/payment/_components/order-items';
import { getCartItems } from '@/app/actions/cart';
import { getProductDetail } from '@/app/actions/product';

/** μž₯λ°”κ΅¬λ‹ˆ μ£Όλ¬Έ 생성 */
export async function createOrderFromCart(
data: OrderFromCartRequest,
): Promise<number> {
Expand All @@ -21,11 +28,14 @@ export async function createOrderFromCart(
} catch (error) {
console.error('μž₯λ°”κ΅¬λ‹ˆ μƒν’ˆ μ£Όλ¬Έ μ‹€νŒ¨:', error);
throw new Error(
error instanceof Error ? error.message : 'μž₯λ°”κ΅¬λ‹ˆ μƒν’ˆ 주문에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.',
error instanceof Error
? error.message
: 'μž₯λ°”κ΅¬λ‹ˆ μƒν’ˆ 주문에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.',
);
}
}

/** μƒν’ˆ 직접 μ£Όλ¬Έ 생성 */
export async function createOrderFromProduct(
data: OrderFromProductRequest,
): Promise<number> {
Expand All @@ -43,6 +53,92 @@ export async function createOrderFromProduct(
}
}

/** μž₯λ°”κ΅¬λ‹ˆ μ£Όλ¬Έ μ•„μ΄ν…œ 쑰회 */
export async function fetchCartOrderItems(
params: Record<string, string | string[] | undefined>,
): Promise<PaymentDisplayItem[]> {
const rawIds = params.items;
const cartItemIds = (
typeof rawIds === 'string' ? rawIds.split(',') : (rawIds ?? [])
)
.map(Number)
.filter((id) => !isNaN(id));

if (cartItemIds.length === 0) redirect('/cart');

const cartItems = await getCartItems();
const selected = cartItems.filter((c) => cartItemIds.includes(c.cartId));

if (selected.length === 0) redirect('/cart');

return selected.map((c) => ({
productId: c.productId,
productName: c.productName,
brandName: c.brandName,
thumbnailImageUrl: c.thumbnailImageUrl,
selectedSize: c.selectedSize,
selectedColor: c.selectedColor,
quantity: c.quantity,
originalPrice: c.price,
price: c.price,
totalPrice: c.totalPrice,
cartItemId: c.cartId,
}));
Comment on lines +74 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "interface CartResponse|type CartResponse" -A 20 --type=ts

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 2229


🏁 Script executed:

# Also check the import statements and CartResponse usage in the file
cat -n src/app/actions/order.ts | head -30

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 1128


🏁 Script executed:

# Find where CartResponse is defined
fd -e ts -e tsx | xargs rg -l "CartResponse" | head -10

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 248


🏁 Script executed:

# Check the full function around lines 74-86
sed -n '50,95p' src/app/actions/order.ts

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 1352


🏁 Script executed:

# Look at the OrderFromCartRequest type definition
rg -n "interface OrderFromCartRequest|type OrderFromCartRequest" -A 30 --type=ts

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 1627


🏁 Script executed:

# Check the PaymentDisplayItem type to understand what's expected
rg -n "type PaymentDisplayItem|interface PaymentDisplayItem" -A 20 --type=ts

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 1568


CartResponse에 originalPrice ν•„λ“œκ°€ μ—†μ–΄ 할인 κΈ°λŠ₯ λ™μž‘ λΆˆκ°€

CartResponseλŠ” originalPrice ν•„λ“œλ₯Ό μ œκ³΅ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ, 82-83번 μ€„μ—μ„œ originalPrice: c.price, price: c.price둜 λ™μΌν•œ 값을 λ§€ν•‘ν•  μˆ˜λ°–μ— μ—†μŠ΅λ‹ˆλ‹€. 이둜 인해 PaymentDisplayItem이 κΈ°λŒ€ν•˜λŠ” 정가와 νŒλ§€κ°€μ˜ ꡬ뢄이 λΆˆκ°€λŠ₯ν•˜κ³ , OrderItemsSection의 할인 계산(originalTotal - finalTotal)이 항상 0이 λ˜μ–΄ 할인 ν‘œμ‹œκ°€ 정상 μž‘λ™ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

APIμ—μ„œ CartResponse에 originalPrice ν•„λ“œλ₯Ό μΆ”κ°€ν•˜κ±°λ‚˜, λ³„λ„μ˜ μ •κ°€ 데이터 μ†ŒμŠ€λ₯Ό 확보해야 ν•©λ‹ˆλ‹€.

πŸ€– Prompt for AI Agents
In `@src/app/actions/order.ts` around lines 74 - 86, The mapping that builds
PaymentDisplayItem sets originalPrice and price to the same value because
CartResponse lacks originalPrice, breaking discount calculations; update the
data flow so originalPrice contains the product's list/was price (either by
adding originalPrice to the CartResponse from the API or by fetching the
product's MSRP/list price from the product/catalog service using productId when
building the mapped objects in the selected.map block), then assign
originalPrice to that sourced value (leave price as the current sale price) so
PaymentDisplayItem and OrderItemsSection can compute discounts correctly.

}

/** λ°”λ‘œ ꡬ맀 μ•„μ΄ν…œ 쑰회 */
export async function fetchDirectOrderItems(
params: Record<string, string | string[] | undefined>,
): Promise<PaymentDisplayItem[]> {
const productId = Number(params.productId);
if (!productId || isNaN(productId)) notFound();

let selections: { color: string; size: string; quantity: number }[];
try {
const raw = Array.isArray(params.selections)
? params.selections[0]
: params.selections;
selections = JSON.parse(raw || '[]');
} catch {
notFound();
}
Comment on lines +96 to +104
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

params.selectionsκ°€ string[]일 λ•Œ as string 캐슀트 μœ„ν—˜

params νƒ€μž…μ΄ Record<string, string | string[] | undefined>μ΄λ―€λ‘œ selectionsκ°€ string[]일 수 μžˆμŠ΅λ‹ˆλ‹€. 이 경우 as string μΊμŠ€νŠΈλŠ” Array.toString()이 ν˜ΈμΆœλ˜μ–΄ μœ νš¨ν•˜μ§€ μ•Šμ€ JSON이 λ©λ‹ˆλ‹€. catchμ—μ„œ notFound()둜 μ²˜λ¦¬λ˜μ§€λ§Œ, μ˜λ„ν•˜μ§€ μ•Šμ€ μ‹€νŒ¨ κ²½λ‘œμž…λ‹ˆλ‹€.

-    selections = JSON.parse((params.selections as string) || '[]');
+    const raw = Array.isArray(params.selections)
+      ? params.selections[0]
+      : params.selections;
+    selections = JSON.parse(raw || '[]');
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let selections: { color: string; size: string; quantity: number }[];
try {
selections = JSON.parse((params.selections as string) || '[]');
} catch {
notFound();
}
let selections: { color: string; size: string; quantity: number }[];
try {
const raw = Array.isArray(params.selections)
? params.selections[0]
: params.selections;
selections = JSON.parse(raw || '[]');
} catch {
notFound();
}
πŸ€– Prompt for AI Agents
In `@src/app/actions/order.ts` around lines 96 - 101, params.selections can be a
string[] but the code force-casts it with as string before JSON.parse, causing
Array.toString() and invalid JSON; fix by normalizing the raw value before
parsing: read params.selections (reference the selections variable and
params.selections), if Array.isArray(params.selections) use the first element
(or join/choose an agreed behavior) as the JSON string, otherwise use it
directly or fallback to '[]', then JSON.parse that normalized string inside the
try/catch and call notFound() only on parse failure.


if (selections.length === 0) notFound();

const product = await getProductDetail(productId);

return selections.map((sel) => ({
productId: product.id,
productName: product.name,
brandName: product.brandName,
thumbnailImageUrl: product.thumbnailImageUrl,
selectedSize: sel.size,
selectedColor: sel.color,
quantity: sel.quantity,
originalPrice: product.price,
price: product.finalPrice,
totalPrice: product.finalPrice * sel.quantity,
}));
}

/** μ£Όλ¬Έ λ‚΄μ—­ 쑰회 */
export async function getOrders(
params?: OrderListParams,
): Promise<OrderListResponse> {
try {
const response = await api.get<OrderListResponse>('/orders', {
params: params as Record<string, string | number | boolean | undefined>,
});
return response;
} catch (error) {
console.error('μ£Όλ¬Έ λ‚΄μ—­ 쑰회 μ‹€νŒ¨:', error);
throw new Error(
error instanceof Error ? error.message : 'μ£Όλ¬Έ λ‚΄μ—­ μ‘°νšŒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.',
);
}
}

/** μ£Όλ¬Έ 상세 쑰회 */
export async function getOrderDetail(orderId: number): Promise<OrderDetail> {
try {
const orderDetail = await api.get<OrderDetail>(`/orders/${orderId}`);
Expand Down
19 changes: 19 additions & 0 deletions src/app/actions/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use server';

import { api } from '@/lib/api-client';
import type { ProductDetail } from '@/types/domain/product';

/** μƒν’ˆ 상세 쑰회 */
export async function getProductDetail(
productId: number,
): Promise<ProductDetail> {
try {
const product = await api.get<ProductDetail>(`/products/${productId}`);
return product;
} catch (error) {
console.error('μƒν’ˆ 상세 쑰회 μ‹€νŒ¨:', error);
throw new Error(
error instanceof Error ? error.message : 'μƒν’ˆ 상세 μ‘°νšŒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.',
);
}
}
20 changes: 20 additions & 0 deletions src/app/actions/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use server';

import { api } from '@/lib/api-client';
import type { UserInfoResDto } from '@/types/domain/user';

/** λ‚΄ 정보 쑰회 */
export async function getUserInfo(): Promise<UserInfoResDto> {
try {
const user = await api.get<UserInfoResDto>('/users/me');
if (typeof user.points === 'string') {
user.points = Number((user.points as string).replace(/,/g, '')) || 0;
}
return user;
} catch (error) {
console.error('μœ μ € 정보 쑰회 μ‹€νŒ¨:', error);
throw new Error(
error instanceof Error ? error.message : 'μœ μ € 정보 μ‘°νšŒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.',
);
}
}
7 changes: 7 additions & 0 deletions src/app/payment/_components/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const SECTIONS = {
ITEMS: 'section-items',
SHIPPING: 'section-shipping',
PAYMENT: 'section-payment',
} as const;

export const SECTION_IDS = Object.values(SECTIONS);
96 changes: 96 additions & 0 deletions src/app/payment/_components/order-items.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Image from 'next/image';

export interface PaymentDisplayItem {
productId: number;
productName: string;
brandName: string;
thumbnailImageUrl: string;
selectedSize: string;
selectedColor: string;
quantity: number;
originalPrice: number; // μ •κ°€
price: number; // μ΅œμ’…κ°€(νŒλ§€κ°€)
totalPrice: number;
cartItemId?: number;
}

interface Props {
items: PaymentDisplayItem[];
}

export default function OrderItemsSection({ items }: Props) {
const originalTotal = items.reduce(
(acc, item) => acc + item.originalPrice * item.quantity,
0,
);
const discountTotal = items.reduce(
(acc, item) => acc + (item.originalPrice - item.price) * item.quantity,
0,
);
const finalTotal = items.reduce((acc, item) => acc + item.totalPrice, 0);
Comment on lines +22 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

finalTotalκ³Ό originalTotal - discountTotal 뢈일치 κ°€λŠ₯μ„±

finalTotal은 item.totalPrice의 합이고, originalTotal - discountTotal은 item.price * item.quantity의 ν•©μž…λ‹ˆλ‹€. totalPrice !== price * quantity인 경우(예: 포인트 적용, 쿠폰 λ“±) ν‘œμ‹œ κΈˆμ•‘μ΄ λ§žμ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. totalPrice의 μ‚°μΆœ 기쀀을 λͺ…ν™•νžˆ ν•˜κ±°λ‚˜, ν•œ κ°€μ§€ 계산 λ°©μ‹μœΌλ‘œ ν†΅μΌν•˜μ„Έμš”.

πŸ€– Prompt for AI Agents
In `@src/app/payment/_components/order-items.tsx` around lines 22 - 30, finalTotal
(items.reduce(... item.totalPrice)) can diverge from originalTotal -
discountTotal (which effectively sums item.price * item.quantity) when
totalPrice includes extra adjustments (points, coupons, shipping, rounding).
Pick one canonical source of truth and use it consistently: either compute
totals from originalPrice/price/quantity and discount logic (update
originalTotal/discountTotal to mirror all adjustments) or derive discountTotal
and originalTotal from item.totalPrice so finalTotal equals originalTotal -
discountTotal. Update the functions/variables originalTotal, discountTotal, and
finalTotal and the place that sets item.totalPrice/item.price to ensure the same
calculation path is used and document which field (item.totalPrice vs
item.price*item.quantity) is authoritative.

const hasDiscount = discountTotal > 0;

return (
<div className="mt-8 flex flex-col gap-6 px-5">
<p className="text-center text-3xl leading-normal font-medium">
μ£Όλ¬Έ 정보 ν™•μΈν•΄μ£Όμ„Έμš”
</p>
<ul className="mt-10 space-y-4">
{items.map((item, index) => (
<li key={`${item.productId}-${index}`} className="flex gap-4">
<div className="flex h-full w-full items-center justify-center gap-4 rounded-2xl border border-black p-5">
{item.thumbnailImageUrl ? (
<Image
src={item.thumbnailImageUrl}
alt={item.productName}
width={110}
height={110}
/>
) : (
<div className="flex h-[110px] w-[110px] items-center justify-center text-xl">
No Image
</div>
)}
<div className="flex flex-col justify-center gap-7 text-xl leading-[18px] font-medium">
<span>{item.brandName}</span>
<p>{item.productName}</p>
<span>
{item.selectedColor} / {item.selectedSize}
</span>
<span>{item.quantity}개</span>
</div>
</div>
</li>
))}
</ul>
{hasDiscount && (
<p className="text-center text-xl leading-normal font-medium text-red-500">
μ΅œλŒ€ 할인이 적용 λμ–΄μš”
</p>
)}

<div className="mt-16 flex flex-col justify-center gap-6 text-xl leading-normal font-medium">
<div className="flex items-center justify-between">
<span>μƒν’ˆ κΈˆμ•‘</span>
<span className="text-2xl leading-[18px]">
{originalTotal.toLocaleString()}원
</span>
</div>
{hasDiscount && (
<div className="flex items-center justify-between">
<span>할인 κΈˆμ•‘</span>
<span className="text-2xl leading-[18px]">
{discountTotal.toLocaleString()}원
</span>
</div>
Comment on lines +79 to +85
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

할인 κΈˆμ•‘μ— λΆ€ν˜Έ ν‘œμ‹œ λˆ„λ½

할인 κΈˆμ•‘μ΄ μ–‘μˆ˜λ‘œ ν‘œμ‹œλ˜μ–΄ μΆ”κ°€ λΉ„μš©μ²˜λŸΌ 보일 수 μžˆμŠ΅λ‹ˆλ‹€. - 접두사λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

μˆ˜μ • μ œμ•ˆ
             <span className="text-2xl leading-[18px]">
-              {discountTotal.toLocaleString()}원
+              -{discountTotal.toLocaleString()}원
             </span>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{hasDiscount && (
<div className="flex items-center justify-between">
<span>할인 κΈˆμ•‘</span>
<span className="text-2xl leading-[18px]">
{discountTotal.toLocaleString()}원
</span>
</div>
{hasDiscount && (
<div className="flex items-center justify-between">
<span>할인 κΈˆμ•‘</span>
<span className="text-2xl leading-[18px]">
-{discountTotal.toLocaleString()}원
</span>
</div>
πŸ€– Prompt for AI Agents
In `@src/app/payment/_components/order-items.tsx` around lines 79 - 85, The
discount amount is rendered without a negative sign making it look like an extra
charge; update the rendering in the component that uses hasDiscount and
discountTotal (in order-items.tsx) to prefix the displayed value with a minus
sign, e.g., render "-" + discountTotal.toLocaleString() (or use string
interpolation) so the UI shows "-{amount}원" when hasDiscount is true; ensure
this change only affects the displayed text and not the raw discountTotal value
used in calculations.

)}
<div className="flex items-center justify-between">
<span>총 κΈˆμ•‘</span>
<span className="text-3xl leading-[18px]">
{finalTotal.toLocaleString()}원
</span>
</div>
</div>
</div>
);
}
Loading