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
30 changes: 26 additions & 4 deletions apps/customer/src/app/payment/success/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { Suspense, useEffect, useMemo, useState } from "react";
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import type { StoreRandomBoxRespDTO, StoreRespDTO } from "@compasser/api";
import { useApproveKakaoPayMutation } from "@/shared/queries/mutation/payment/useApproveKakaoPayMutation";
import { useCompleteAdminSettlementMutation } from "@/shared/queries/mutation/admin-settlement/useCompleteAdminSettlementMutation";
import PurchaseCompleteModal from "@/app/(tabs)/main/store/[id]/purchase/_components/PurchaseCompleteModal";

interface PendingPayment {
Expand All @@ -15,7 +16,12 @@ interface PendingPayment {

function PaymentSuccessContent() {
const searchParams = useSearchParams();

const approveKakaoPayMutation = useApproveKakaoPayMutation();
const completeAdminSettlementMutation = useCompleteAdminSettlementMutation();

const hasRequestedRef = useRef(false);

const [pendingPayment, setPendingPayment] = useState<PendingPayment | null>(
null,
);
Expand All @@ -29,10 +35,14 @@ function PaymentSuccessContent() {
const pgToken = searchParams.get("pg_token");

useEffect(() => {
if (hasRequestedRef.current) return;

const savedPayment = sessionStorage.getItem("pendingPayment");

if (!savedPayment || !reservationId || !pgToken) return;

hasRequestedRef.current = true;

const parsedPayment = JSON.parse(savedPayment) as PendingPayment;

setPendingPayment(parsedPayment);
Expand All @@ -44,12 +54,24 @@ function PaymentSuccessContent() {
},
{
onSuccess: () => {
sessionStorage.removeItem("pendingPayment");
setIsCompleteModalOpen(true);
completeAdminSettlementMutation.mutate(
{
storeId: parsedPayment.store.storeId,
body: {
reservationIds: [reservationId],
},
},
Comment on lines +57 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 결제 성공 페이지에서 역직렬화/필드 접근 무가드 구간 확인
rg -n -C3 'JSON.parse\(savedPayment\)|parsedPayment\.store\.storeId|reservationIds:\s*\[' apps/customer/src/app/payment/success/page.tsx

Repository: CUK-Compasser/FE

Length of output: 519


🏁 Script executed:

cat -n apps/customer/src/app/payment/success/page.tsx

Repository: CUK-Compasser/FE

Length of output: 3646


🏁 Script executed:

# reservationId 변수 출처 확인
rg -n "reservationId" apps/customer/src/app/payment/success/page.tsx | head -20

Repository: CUK-Compasser/FE

Length of output: 357


🏁 Script executed:

# savedPayment 변수 출처 및 초기값 확인
rg -n "savedPayment|sessionStorage" apps/customer/src/app/payment/success/page.tsx | head -20

Repository: CUK-Compasser/FE

Length of output: 331


세션 데이터 파싱과 필드 접근이 무가드라 런타임 크래시 위험이 있습니다.

Line 46에서 JSON.parse(savedPayment)가 try-catch 없이 호출되고 있어, sessionStorage 데이터가 손상된 경우 SyntaxError로 결제 성공 플로우가 깨질 수 있습니다. 또한 Line 59의 parsedPayment.store.storeId 중첩 접근도 파싱된 객체의 구조를 검증하지 않아 TypeScript 타입 정보가 런타임에 제거되면 런타임 에러가 발생할 수 있습니다.

🔧 제안 수정안
-    const parsedPayment = JSON.parse(savedPayment) as PendingPayment;
+    let parsedPayment: PendingPayment;
+    try {
+      parsedPayment = JSON.parse(savedPayment) as PendingPayment;
+    } catch {
+      sessionStorage.removeItem("pendingPayment");
+      return;
+    }
+
+    if (!parsedPayment?.store?.storeId || !parsedPayment?.reservationId) {
+      sessionStorage.removeItem("pendingPayment");
+      return;
+    }

     setPendingPayment(parsedPayment);

     approveKakaoPayMutation.mutate(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/payment/success/page.tsx` around lines 57 - 63, Wrap
the JSON.parse of savedPayment in a try-catch and handle parse failures (e.g.,
log the error and abort/redirect), and before calling
completeAdminSettlementMutation.mutate ensure parsedPayment is an object with
the expected shape (check parsedPayment?.store and parsedPayment.store?.storeId)
and that reservationId is defined; if validations fail, bail out with a safe
fallback or error path instead of accessing nested fields directly in
completeAdminSettlementMutation.mutate to prevent runtime crashes.

{
onSettled: () => {
sessionStorage.removeItem("pendingPayment");
setIsCompleteModalOpen(true);
},
},
);
},
},
);
}, [reservationId, pgToken, approveKakaoPayMutation]);
}, [reservationId, pgToken]);

if (approveKakaoPayMutation.isPending) {
return <div>결제 승인 처리 중...</div>;
Expand Down
4 changes: 3 additions & 1 deletion apps/customer/src/shared/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createMemberModule,
createOrderModule,
createPaymentModule,
createAdminSettlementModule,
} from "@compasser/api";

const tokenStore: TokenStore = {
Expand Down Expand Up @@ -44,4 +45,5 @@ export const authModule = createAuthModule(compasserApi);
export const memberModule = createMemberModule(compasserApi);
export const storeModule = createStoreModule(compasserApi);
export const orderModule = createOrderModule(compasserApi);
export const paymentModule = createPaymentModule(compasserApi);
export const paymentModule = createPaymentModule(compasserApi);
export const adminSettlementModule = createAdminSettlementModule(compasserApi);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { adminSettlementModule } from "@/shared/api/api";

export const useCompleteAdminSettlementMutation = () => {
const queryClient = useQueryClient();

return useMutation({
...adminSettlementModule.mutations.completeSettlement(queryClient),
});
};
38 changes: 16 additions & 22 deletions apps/owner/src/app/signup/business/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Input, Button } from "@compasser/design-system";
import { useVerifyBusinessMutation } from "@/shared/queries/mutation/auth/useVerifyBusinessMutation";
import { useUpgradeToStoreManagerMutation } from "@/shared/queries/mutation/auth/useUpgradeToStoreManagerMutation";
import { useOwnerSignupStore } from "@/shared/stores/ownerSignup.store";
import {
normalizeBusinessNumber,
Expand All @@ -13,7 +13,7 @@ import {

export default function BusinessSignupPage() {
const router = useRouter();
const verifyMutation = useVerifyBusinessMutation();
const upgradeMutation = useUpgradeToStoreManagerMutation();

const signupCompleted = useOwnerSignupStore((s) => s.signupCompleted);
const email = useOwnerSignupStore((s) => s.email);
Expand Down Expand Up @@ -65,26 +65,20 @@ export default function BusinessSignupPage() {
return;
}

verifyMutation.mutate(
{
businessLicenseNumber: value,
email,
upgradeMutation.mutate(undefined, {
onSuccess: (res) => {
if (res.alreadyUpgraded) {
setError("이미 사업자 등록이 완료된 계정입니다.");
return;
}

setBusinessCompleted();
router.push("/signup/register");
},
{
onSuccess: (res) => {
if (res.alreadyUpgraded) {
setError("이미 사업자 등록이 완료된 계정입니다.");
return;
}

setBusinessCompleted();
router.push("/signup/register");
},
onError: () => {
setError("사업자 번호 인증에 실패했습니다.");
},
onError: () => {
setError("점장 승격 처리에 실패했습니다.");
},
);
});
};

return (
Expand Down Expand Up @@ -115,9 +109,9 @@ export default function BusinessSignupPage() {
size="lg"
variant="primary"
onClick={handleNext}
disabled={verifyMutation.isPending}
disabled={upgradeMutation.isPending}
>
{verifyMutation.isPending ? "확인 중..." : "다음으로"}
{upgradeMutation.isPending ? "확인 중..." : "다음으로"}
</Button>
</div>
</main>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ownerModule } from "@/shared/api/api";

export const useUpgradeToStoreManagerMutation = () => {
const queryClient = useQueryClient();

return useMutation({
...ownerModule.mutations.upgradeToStoreManager(queryClient),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ownerModule } from "@/shared/api/api";
export const useVerifyBusinessMutation = () => {
const queryClient = useQueryClient();

return useMutation(
ownerModule.mutations.verifyBizAndUpgrade(queryClient),
);
return useMutation({
...ownerModule.mutations.verifyBizAndUpgrade(queryClient),
});
};
59 changes: 59 additions & 0 deletions packages/api/src/domains/admin-settlement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// packages/api/src/domains/admin-settlement.ts

import type { QueryClient } from "@tanstack/react-query";

import { createMutationWithCache } from "../core/mutation";
import type { CompasserApi } from "../core/types";
import type {
AdminSettlementCompleteReqDTO,
AdminSettlementCompleteResponse,
} from "../models/admin-settlement";

export interface CompleteAdminSettlementParams {
storeId: number;
body: AdminSettlementCompleteReqDTO;
}

export const createAdminSettlementModule = (api: CompasserApi) => {
const keys = {
all: ["admin-settlement"] as const,
complete: (storeId: number) => [...keys.all, "complete", storeId] as const,
};

const requests = {
completeSettlement: async ({
storeId,
body,
}: CompleteAdminSettlementParams) => {
const { data } =
await api.privateClient.post<AdminSettlementCompleteResponse>(
`/admin/settlements/${storeId}/complete`,
body,
);

return data;
},
};

const mutations = {
completeSettlement: (queryClient: QueryClient) =>
createMutationWithCache({
queryClient,
mutationKey: [...keys.all, "complete"],
mutationFn: requests.completeSettlement,
getActions: (response, variables) => [
{
type: "set",
queryKey: keys.complete(variables.storeId),
value: response,
},
{
type: "invalidate",
queryKey: keys.all,
},
],
}),
};

return { keys, requests, mutations };
};
2 changes: 2 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./models/random-box";
export * from "./models/store";
export * from "./models/storeManager";
export * from "./models/store-image";
export * from "./models/admin-settlement";
export * from "./domains/auth";
export * from "./domains/health";
export * from "./domains/member";
Expand All @@ -23,4 +24,5 @@ export * from "./domains/random-box";
export * from "./domains/store";
export * from "./domains/store-image";
export * from "./domains/store-manager";
export * from "./domains/admin-settlement";
export * from "./domains/index";
16 changes: 16 additions & 0 deletions packages/api/src/models/admin-settlement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ApiResponse } from "../core/types";

export interface AdminSettlementCompleteReqDTO {
reservationIds: number[];
}

export interface AdminSettlementCompleteRespDTO {
storeId: number;
storeName: string;
count: number;
totalAmount: number;
message: string;
}

export type AdminSettlementCompleteResponse =
ApiResponse<AdminSettlementCompleteRespDTO>;
Loading