Skip to content

[OMF-279] 견적 계산기 가격 구조 변경#148

Open
chldsbdud wants to merge 2 commits intomainfrom
OMF-279
Open

[OMF-279] 견적 계산기 가격 구조 변경#148
chldsbdud wants to merge 2 commits intomainfrom
OMF-279

Conversation

@chldsbdud
Copy link
Collaborator

@chldsbdud chldsbdud commented Mar 24, 2026

📌 주요 변경 사항

  • 견적 계산기 가격 구조 변경

🔗 관련 이슈

Jira 링크: https://onsurvey.atlassian.net/browse/OMF-279

✅ 리뷰 요청 사항 (Need Review)

  • 🙂 크게 우려되는 사항은 없어요. 가볍게 리뷰 부탁드려요.

🎨 스크린샷 (선택)

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 설문 생성 메뉴를 애니메이션 플로팅 액션 버튼으로 개선
    • 견적 단계에서 문항 수 선택 기능 추가
    • 성별/연령대 정보를 통합 표시
  • Refactor

    • 견적 상세 정보 표시 간소화
    • 가격 계산 로직 최적화

@chldsbdud chldsbdud self-assigned this Mar 24, 2026
@chldsbdud chldsbdud added the ✨Feat 새로운 기능 추가 label Mar 24, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 24, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'project', 'rules', 'files', 'ignore'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

MySurvey 페이지에 애니메이션 플로팅 액션 버튼(FAB) 메뉴를 추가하여 기존 단일 버튼을 대체했습니다. 동시에 결제 시스템에 문항 수 필드를 추가하고, 예상 가격 계산 로직을 기존의 개별 수수료 합산 방식에서 테이블 기반 조회 시스템으로 전면 변경했습니다.

Changes

코호트 / 파일 요약
MySurvey 페이지 FAB 메뉴
src/features/mysurvey/pages/index.tsx
기존 설문 추가 버튼을 framer-motion 기반 애니메이션 FAB 메뉴로 대체. 백드롭 오버레이, 상태 관리(isFabOpen), 네비게이션 핸들러(Google 양식 변환/일반 폼 생성) 추가.
결제 상수 및 타입
src/features/payment/constants/payment.ts
문항 수 범위를 나타내는 QuestionCountRange 타입("1~30" | "31~50")과 QUESTION_COUNT_OPTIONS 배열 추가. EstimateField 열거형에 QuestionCount 멤버 추가.
결제 페이지 및 컴포넌트
src/features/payment/pages/EstimatePage.tsx, src/features/payment/components/estimate/EstimateDetailBottomSheet.tsx
결제 페이지에 문항 수 필드 선택 UI 추가. 하단 시트의 items 배열을 간단한 rows 배열로 재구성하고, 성별/연령대 개별 가격 표시 제거 및 targetingSummary 통합.
결제 컨텍스트
src/shared/contexts/PaymentContext.tsx
Estimate 타입에 questionCount 필드 추가("1~30" 기본값). 초기 상태 및 리셋 로직에 반영.
가격 계산 핵심 로직
src/shared/lib/estimatePrice.ts, src/shared/lib/paymentCalculator.ts, src/shared/lib/estimatePricingTable.ts (신규)
기존 개별 수수료 계산 함수 제거. 테이블 기반 조회 시스템 도입: 참여자 규모, 문항 수, 타겟팅 프리셋을 조합하여 ESTIMATE_PRICE_TABLE에서 가격 조회. PriceBreakdown 구조 단순화 (개별 가격 필드 제거, targetingSummary 추가).

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant FAB as FAB 메뉴
    participant Router as 라우터
    participant EstimatePage as 결제 페이지

    User->>FAB: FAB 토글 버튼 클릭
    activate FAB
    FAB->>FAB: isFabOpen 상태 전환
    FAB->>User: 메뉴 애니메이션 표시
    deactivate FAB

    User->>FAB: "Google 양식 변환" 또는 "설문 생성" 선택
    activate FAB
    FAB->>Router: 네비게이션 (state.source 설정)
    Router->>EstimatePage: 라우팅 + source 파라미터 전달
    EstimatePage->>EstimatePage: 문항 수, 참여자 규모, 타겟팅 정보 입력
    EstimatePage->>EstimatePage: lookupEstimateTablePrice()로 예상 가격 계산
    EstimatePage->>User: 예상 가격 표시
    deactivate FAB
Loading
sequenceDiagram
    participant User as 사용자
    participant EstimatePage as 결제 페이지
    participant PaymentContext as 결제 컨텍스트
    participant PricingTable as 가격 테이블 모듈

    User->>EstimatePage: 문항 수, 참여자, 성별/연령대 선택
    EstimatePage->>PaymentContext: estimate 상태 업데이트<br/>(questionCount, desiredParticipants, gender, ages)
    PaymentContext->>PricingTable: lookupEstimateTablePrice(estimate) 호출
    
    activate PricingTable
    PricingTable->>PricingTable: parseParticipantTier()<br/>(참여자 규모 → 구간 매핑)
    PricingTable->>PricingTable: getEstimateTargetingPreset()<br/>(성별/연령대 → 타겟팅 프리셋)
    PricingTable->>PricingTable: ESTIMATE_PRICE_TABLE 조회<br/>[구간][문항범위][프리셋]
    deactivate PricingTable
    
    PricingTable-->>EstimatePage: 예상 가격 반환
    EstimatePage->>User: 계산된 가격 및 타겟팅 요약 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 견적 계산기 가격 구조 변경이라는 주요 변경사항을 명확하게 설명하고 있으며, 실제 변경 내용과 일치합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch OMF-279

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +131 to +132
<span
className={`${FAB_SIZE_CLASS} inline-flex items-center justify-center rounded-full! bg-[#191F28]`}
Copy link
Contributor

Choose a reason for hiding this comment

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

bg-[#191F28] 고정값으로 들어가 있는데 이 컬러값을 토큰으로 관리해도 좋을 것 같습니다

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/features/payment/pages/EstimatePage.tsx (1)

51-61: desiredParticipants 초기값 불일치

PaymentContext에서는 desiredParticipants"50명"으로 초기화하지만, 여기서는 "50"으로 설정합니다. parseParticipantTier는 숫자만 추출하므로 기능상 문제는 없지만, 일관성을 위해 값 형식을 통일하는 것이 좋습니다.

♻️ 일관성 개선 제안
 		handleEstimateChange({
 			...estimate,
-			desiredParticipants: "50",
+			desiredParticipants: "50명",
 			gender: "ALL",
 			ages: ["ALL"],
 			location: "ALL",
 			questionCount: estimate.questionCount ?? "1~30",
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/payment/pages/EstimatePage.tsx` around lines 51 - 61, The
default desiredParticipants value set in this component is "50" but
PaymentContext uses "50명"; update the initialization in the useEffect (where
estimate.desiredParticipants is checked and handleEstimateChange is called) to
use the same string format ("50명") so it matches PaymentContext and
parseParticipantTier expectations; ensure you update the value in the
handleEstimateChange payload (and any nearby references to
estimate.desiredParticipants) to maintain consistent formatting across the app.
src/shared/lib/estimatePricingTable.ts (1)

152-161: 가격 조회 실패 시 0원 반환의 잠재적 문제

lookupEstimateTablePrice가 유효하지 않은 응답자 수로 인해 조회 실패 시 조용히 0을 반환합니다. 이로 인해 사용자에게 "0원"이 표시될 수 있으며, 이는 혼란을 줄 수 있습니다.

운영 환경에서 이 상황이 발생하면 추적이 어려울 수 있으므로, 코딩 가이드라인에 따라 예외 상황을 로깅하는 것을 고려해 주세요.

♻️ 로깅 추가 제안
+import * as Sentry from "@sentry/react";
+
 // 견적서 표 기준 총액 (원). 조합이 표에 없으면 0.
 export const lookupEstimateTablePrice = (estimate: Estimate): number => {
 	const tier = parseParticipantTier(estimate.desiredParticipants);
-	if (!tier) return 0;
+	if (!tier) {
+		const { logger } = Sentry;
+		logger.warn(
+			logger.fmt`Invalid participant tier for desiredParticipants=${estimate.desiredParticipants}`,
+		);
+		return 0;
+	}

 	const q = estimate.questionCount;
 	const preset = getEstimateTargetingPreset(estimate);

 	return ESTIMATE_PRICE_TABLE[tier][q][preset] ?? 0;
 };

As per coding guidelines, "Use logger.fmt template literal function to bring variables into structured logs for better log formatting and variable interpolation" 및 "Use appropriate logger methods based on severity: logger.warn()".

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

In `@src/shared/lib/estimatePricingTable.ts` around lines 152 - 161,
lookupEstimateTablePrice silently returns 0 when the ESTIMATE_PRICE_TABLE lookup
fails which can mask misconfiguration; update lookupEstimateTablePrice to detect
a missing entry (after using parseParticipantTier and
getEstimateTargetingPreset) and call logger.warn(logger.fmt`Missing price for
tier=${tier} questionCount=${q} preset=${preset}`) before returning 0 so
failures are recorded; reference the lookup in
ESTIMATE_PRICE_TABLE[tier][q][preset] and use logger.fmt/ logger.warn for
structured logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/mysurvey/pages/index.tsx`:
- Around line 44-48: The navigate call in goToGoogleFormConversion sets state {
source: "mysurvey_fab" } but that state is never read or forwarded by
GoogleFormConversionLandingPage (GoogleFormConversionLandingPage and its
subsequent navigation to /payment/google-form-conversion), so the source is
lost; either read location.state.source in GoogleFormConversionLandingPage (use
the component that consumes location.state) and re-dispatch it when navigating
to /payment/google-form-conversion, or emit the analytics/event immediately in
goToGoogleFormConversion instead of relying on navigation state—update the
GoogleFormConversionLandingPage to accept and forward location.state (or to call
the analytics util) and ensure navigate(...) to /payment/google-form-conversion
includes the same state if forwarding.
- Around line 69-79: The backdrop currently renders as motion.button
(key="fab-backdrop") which creates a hidden focusable control; change it to
motion.div (preserve className, initial/animate/exit/transition props and
onClick that calls setIsFabOpen(false)) so it is not focusable, or if you must
keep a button keep key="fab-backdrop" but add tabIndex={-1} to remove it from
the tab order; ensure you remove the type="button" prop when switching to
motion.div and retain aria-hidden if still desired.
- Around line 44-54: The three click handlers (goToGoogleFormConversion,
goToCreateSurvey and the FAB background-close / toggle handlers) must be
instrumented with Sentry traces: import Sentry if not present, wrap each handler
body in a Sentry.startSpan({ op: "ui.click", description: "<handler name>" })
call, perform the existing actions (setIsFabOpen, navigate) inside the span,
call span.finish() after navigation (or in a finally block) and ensure any
thrown errors are rethrown or handled; use the handler names
(goToGoogleFormConversion, goToCreateSurvey, and the background/FAB toggle
functions) as the span description to help identify the traces.

---

Nitpick comments:
In `@src/features/payment/pages/EstimatePage.tsx`:
- Around line 51-61: The default desiredParticipants value set in this component
is "50" but PaymentContext uses "50명"; update the initialization in the
useEffect (where estimate.desiredParticipants is checked and
handleEstimateChange is called) to use the same string format ("50명") so it
matches PaymentContext and parseParticipantTier expectations; ensure you update
the value in the handleEstimateChange payload (and any nearby references to
estimate.desiredParticipants) to maintain consistent formatting across the app.

In `@src/shared/lib/estimatePricingTable.ts`:
- Around line 152-161: lookupEstimateTablePrice silently returns 0 when the
ESTIMATE_PRICE_TABLE lookup fails which can mask misconfiguration; update
lookupEstimateTablePrice to detect a missing entry (after using
parseParticipantTier and getEstimateTargetingPreset) and call
logger.warn(logger.fmt`Missing price for tier=${tier} questionCount=${q}
preset=${preset}`) before returning 0 so failures are recorded; reference the
lookup in ESTIMATE_PRICE_TABLE[tier][q][preset] and use logger.fmt/ logger.warn
for structured logging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ebb5cb2-3a73-4d01-9400-759e02944ac5

📥 Commits

Reviewing files that changed from the base of the PR and between 6f3a3c3 and 09f9a40.

⛔ Files ignored due to path filters (4)
  • src/assets/mysurveyFloating/icon-clip.svg is excluded by !**/*.svg
  • src/assets/mysurveyFloating/icon-document-lines-mono-docs.svg is excluded by !**/*.svg
  • src/assets/mysurveyFloating/icon-navigation-x-mono.svg is excluded by !**/*.svg
  • src/assets/mysurveyFloating/icon-plus-btn.svg is excluded by !**/*.svg
📒 Files selected for processing (8)
  • src/features/mysurvey/pages/index.tsx
  • src/features/payment/components/estimate/EstimateDetailBottomSheet.tsx
  • src/features/payment/constants/payment.ts
  • src/features/payment/pages/EstimatePage.tsx
  • src/shared/contexts/PaymentContext.tsx
  • src/shared/lib/estimatePrice.ts
  • src/shared/lib/estimatePricingTable.ts
  • src/shared/lib/paymentCalculator.ts

Comment on lines +44 to +48
const goToGoogleFormConversion = () => {
setIsFabOpen(false);
navigate("/google-form-conversion-landing", {
state: { source: "mysurvey_fab" },
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

mysurvey_fab source가 현재 전환 흐름에서 유실됩니다.

src/features/google-form-conversion/pages/GoogleFormConversionLandingPage.tsx:1-35location.state를 읽지 않고, 다음 /payment/google-form-conversion 이동에도 상태를 넘기지 않아서 여기서 넣은 source 값은 어디에서도 소비되지 않습니다. 유입 출처를 보려는 의도였다면 랜딩 페이지에서 읽어 재전달하거나, 이 지점에서 바로 이벤트를 남기도록 맞춰 두는 편이 안전합니다.

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

In `@src/features/mysurvey/pages/index.tsx` around lines 44 - 48, The navigate
call in goToGoogleFormConversion sets state { source: "mysurvey_fab" } but that
state is never read or forwarded by GoogleFormConversionLandingPage
(GoogleFormConversionLandingPage and its subsequent navigation to
/payment/google-form-conversion), so the source is lost; either read
location.state.source in GoogleFormConversionLandingPage (use the component that
consumes location.state) and re-dispatch it when navigating to
/payment/google-form-conversion, or emit the analytics/event immediately in
goToGoogleFormConversion instead of relying on navigation state—update the
GoogleFormConversionLandingPage to accept and forward location.state (or to call
the analytics util) and ensure navigate(...) to /payment/google-form-conversion
includes the same state if forwarding.

Comment on lines +44 to +54
const goToGoogleFormConversion = () => {
setIsFabOpen(false);
navigate("/google-form-conversion-landing", {
state: { source: "mysurvey_fab" },
});
};

const handleAddSurvey = () =>
const goToCreateSurvey = () => {
setIsFabOpen(false);
navigate("/createFormStart", { state: { source: "mysurvey_button" } });
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "index.tsx" -path "*/features/mysurvey/pages/*" 2>/dev/null | head -5

Repository: On-Survey/Frontend

Length of output: 44


🏁 Script executed:

git ls-files "src/features/mysurvey/pages/index.tsx"

Repository: On-Survey/Frontend

Length of output: 99


🏁 Script executed:

wc -l src/features/mysurvey/pages/index.tsx

Repository: On-Survey/Frontend

Length of output: 103


🏁 Script executed:

cat -n src/features/mysurvey/pages/index.tsx

Repository: On-Survey/Frontend

Length of output: 5292


새로 추가한 FAB 클릭 핸들러들에 Sentry 계측이 필요합니다.

이 컴포넌트의 세 가지 클릭 핸들러(goToGoogleFormConversion, goToCreateSurvey, 배경 닫기, FAB 토글)가 모두 Sentry.startSpan으로 감싸져 있지 않습니다. 저장소 가이드라인에 따라 컴포넌트 액션 핸들러는 Sentry.startSpan({ op: "ui.click", ... })을 사용하여 클릭 trace를 기록해야 합니다.

적용 예시
+import * as Sentry from "@sentry/react";
 import { BottomNavigation } from "@shared/components/BottomNavigation";
 // ... other imports

 const goToGoogleFormConversion = () => {
-	setIsFabOpen(false);
-	navigate("/google-form-conversion-landing", {
-		state: { source: "mysurvey_fab" },
-	});
+	Sentry.startSpan(
+		{ op: "ui.click", name: "MySurvey FAB open google-form-conversion" },
+		(span) => {
+			span.setAttribute("destination", "/google-form-conversion-landing");
+			setIsFabOpen(false);
+			navigate("/google-form-conversion-landing", {
+				state: { source: "mysurvey_fab" },
+			});
+		},
+	);
 };

 const goToCreateSurvey = () => {
-	setIsFabOpen(false);
-	navigate("/createFormStart", { state: { source: "mysurvey_button" } });
+	Sentry.startSpan(
+		{ op: "ui.click", name: "MySurvey FAB open create-survey" },
+		(span) => {
+			span.setAttribute("destination", "/createFormStart");
+			setIsFabOpen(false);
+			navigate("/createFormStart", { state: { source: "mysurvey_button" } });
+		},
+	);
 };

-			onClick={() => setIsFabOpen(false)}
+			onClick={() =>
+				Sentry.startSpan({ op: "ui.click", name: "MySurvey FAB backdrop close" }, () => {
+					setIsFabOpen(false);
+				})
+			}

-		onClick={() => setIsFabOpen((o) => !o)}
+		onClick={() =>
+			Sentry.startSpan(
+				{ op: "ui.click", name: "MySurvey FAB toggle" },
+				(span) => {
+					span.setAttribute("next_state", !isFabOpen);
+					setIsFabOpen((o) => !o);
+				},
+			)
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/mysurvey/pages/index.tsx` around lines 44 - 54, The three click
handlers (goToGoogleFormConversion, goToCreateSurvey and the FAB
background-close / toggle handlers) must be instrumented with Sentry traces:
import Sentry if not present, wrap each handler body in a Sentry.startSpan({ op:
"ui.click", description: "<handler name>" }) call, perform the existing actions
(setIsFabOpen, navigate) inside the span, call span.finish() after navigation
(or in a finally block) and ensure any thrown errors are rethrown or handled;
use the handler names (goToGoogleFormConversion, goToCreateSurvey, and the
background/FAB toggle functions) as the span description to help identify the
traces.

Comment on lines +69 to +79
<motion.button
type="button"
key="fab-backdrop"
className="fixed inset-0 z-40 bg-black/65"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
aria-hidden
onClick={() => setIsFabOpen(false)}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the specific file and lines
cat -n src/features/mysurvey/pages/index.tsx | sed -n '60,85p'

Repository: On-Survey/Frontend

Length of output: 925


🏁 Script executed:

# Check the full context of the file to understand component structure
wc -l src/features/mysurvey/pages/index.tsx

Repository: On-Survey/Frontend

Length of output: 103


🏁 Script executed:

# Search for similar motion.button patterns in the codebase
rg "motion\.button" -A 5 -B 2 --max-count=10

Repository: On-Survey/Frontend

Length of output: 6394


🏁 Script executed:

# Check for aria-hidden usage patterns with interactive elements
rg "aria-hidden" -B 3 -A 3 --max-count=15

Repository: On-Survey/Frontend

Length of output: 50374


배경 레이어를 aria-hiddenbutton으로 두면 숨겨진 포커스 대상이 생깁니다.

aria-hidden은 접근성 트리에서만 숨기고 탭 포커스는 막지 않아서, 키보드 사용자나 스크린리더 사용자가 불일치하는 경험을 할 수 있습니다. 이 레이어는 motion.div로 바꾸거나 최소한 tabIndex={-1}을 추가해 포커스 대상에서 제외해 주세요.

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

In `@src/features/mysurvey/pages/index.tsx` around lines 69 - 79, The backdrop
currently renders as motion.button (key="fab-backdrop") which creates a hidden
focusable control; change it to motion.div (preserve className,
initial/animate/exit/transition props and onClick that calls
setIsFabOpen(false)) so it is not focusable, or if you must keep a button keep
key="fab-backdrop" but add tabIndex={-1} to remove it from the tab order; ensure
you remove the type="button" prop when switching to motion.div and retain
aria-hidden if still desired.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨Feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants