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
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { formatKoreanNumber } from '@/utils/numberUtils'
import { BiErrorCircle } from 'react-icons/bi'

interface Props {
isVisible: boolean
overRatio: number
remainingRatio: number
remainder: number
}

const BannerLayout = ({ children }: { children: React.ReactNode }) => {
Expand All @@ -21,7 +19,6 @@ const AllocationStatusBanner = ({
isVisible,
overRatio,
remainingRatio,
remainder,
}: Props) => {
if (!isVisible) {
return <div className="w-full bg-gray-300 dark:bg-neutral-700 my-4" />
Expand All @@ -41,7 +38,6 @@ const AllocationStatusBanner = ({
<BannerLayout>
<div className="flex gap-3">
<span>남은 비율 : {remainingRatio}</span>
<span>남은 메소 : {formatKoreanNumber(remainder)}</span>
</div>
</BannerLayout>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,17 @@ const PartyMemberCard = ({
<span className="shrink-0 font-light">%</span>
</div>
</div>
<div className="text-[13px] text-gray-600 dark:text-gray-300">
송금할 금액:{' '}
<span className="text-[16px] font-semibold text-blue-700 dark:text-blue-400">
{formatKoreanNumber(member.transferAmount)}
</span>{' '}
메소
</div>
<div className="text-[13px] text-gray-600 dark:text-gray-300">
최종 분배금:{' '}
<span className="text-[16px] font-semibold text-blue-700 dark:text-blue-400">
{formatKoreanNumber(member.amount)}
{formatKoreanNumber(member.finalReceivedAmount)}
</span>{' '}
메소
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const PartyMemberControl = ({ netAmount, feeRate }: Props) => {
const {
mode,
members,
remainder,
overRatio,
remainingRatio,
canAddMember,
Expand Down Expand Up @@ -51,7 +50,6 @@ const PartyMemberControl = ({ netAmount, feeRate }: Props) => {
isVisible={isAllocationBannerVisible}
overRatio={overRatio}
remainingRatio={remainingRatio}
remainder={remainder}
/>
<PartyMemberList
members={members}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ export const usePartyDistribution = (totalProfit: number, feeRate: number) => {
return {
mode: state.mode,

members: distribution.results,
remainder: distribution.remainder,
members: distribution,

canAddMember: canAddMember,
remainingRatio: remainingRatio,
Expand Down
86 changes: 71 additions & 15 deletions src/app/calculator/partyProfit/_utils/distributeProfitByPercent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,89 @@ export type DistributionResult = {
id: string
name: string
ratio: number
amount: number
transferAmount: number // 입력 금액 (송금액)
finalReceivedAmount: number // 실수령액
}

type Member = { id: string; name: string; ratio: number }
type Mode = 'MANUAL' | 'EQUAL'

const normalizeMembers = (members: Member[], mode: Mode): Member[] => {
if (mode === 'EQUAL') {
const equalRatio = 100 / members.length
return members.map((m) => ({ ...m, ratio: equalRatio }))
}
return members
}

const calculateFairBaseProfit = (
totalProfit: number,
ownerRatio: number,
feeRate: number,
): number => {
const r = feeRate / 100
const wOwner = ownerRatio / 100

const denominator = wOwner + (1 - wOwner) / (1 - r)

if (denominator <= 0) return 0
return totalProfit / denominator
}

const computeMemberShare = (
member: Member,
fairBaseProfit: number,
feeRate: number,
isOwner: boolean,
): DistributionResult => {
const r = feeRate / 100
const w = member.ratio / 100

const targetFinal = Math.floor(fairBaseProfit * w)

if (isOwner) {
return {
...member,
transferAmount: 0,
finalReceivedAmount: targetFinal,
}
}

const transfer = Math.floor(targetFinal / (1 - r))
const finalReceived = Math.floor(transfer * (1 - r))

return {
...member,
transferAmount: transfer,
finalReceivedAmount: finalReceived,
}
}

export const distributeProfitByPercent = (
totalProfit: number,
feeRate: number,
members: Member[],
mode: Mode,
) => {
const memberCount = members.length
): DistributionResult[] => {
const normalizedMembers = normalizeMembers(members, mode)

const fee = memberCount >= 2 ? Math.floor((totalProfit * feeRate) / 100) : 0
const distributable = totalProfit - fee
const activeOwnerIndex = 0
const ownerMember = normalizedMembers[activeOwnerIndex]
Comment on lines +71 to +72

Choose a reason for hiding this comment

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

medium

분배 주체인 파티장이 항상 배열의 첫 번째(index 0) 멤버라고 가정하고 있습니다. 이는 유연하지 못한 설계이며, 향후 멤버 순서를 변경하거나 다른 파티장을 지정하는 기능이 추가될 경우 버그를 유발할 수 있습니다. 파티장의 ID를 함수에 전달하거나 Member 타입에 isOwner 같은 플래그를 추가하여 명시적으로 파티장을 식별하는 것이 더 견고한 접근 방식입니다.


const results: DistributionResult[] = members.map((m) => ({
...m,
amount:
mode === 'MANUAL'
? Math.floor((distributable * m.ratio) / 100)
: Math.floor(distributable / memberCount),
}))
const fairBaseProfit = calculateFairBaseProfit(
totalProfit,
ownerMember.ratio,
feeRate,
)

const distributed = results.reduce((sum, r) => sum + r.amount, 0)
const remainder = distributable - distributed
const results = normalizedMembers.map((member, idx) =>
computeMemberShare(
member,
fairBaseProfit,
feeRate,
idx === activeOwnerIndex, // isOwner check
),
)

return { results, remainder }
return results
}
Comment on lines 63 to 90

Choose a reason for hiding this comment

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

critical

이 함수는 더 단순하고 견고하게 리팩토링할 수 있습니다. 현재 normalizeMembers 함수에 의존하고 있는데, 이 함수는 상위 훅의 로직을 중복 구현하고 있으며 빈 멤버 배열이 전달될 경우 크래시를 유발할 수 있습니다. 또한, 함수가 불필요하게 mode에 의존하고 있습니다.

modenormalizeMembers에 대한 의존성을 제거하여 이 함수를 순수한 계산 유틸리티로 만들고, 상위 훅이 멤버 비율을 설정하도록 책임을 위임하는 것이 좋습니다. 이렇게 하면 관심사가 명확히 분리되고 잠재적인 크래시를 해결할 수 있습니다. 아래 제안을 적용한 후에는 normalizeMembers 함수를 삭제하고, usePartyDistribution.ts에서 distributeProfitByPercent를 호출하는 부분도 mode를 전달하지 않도록 수정해야 합니다.

export const distributeProfitByPercent = (
  totalProfit: number,
  feeRate: number,
  members: Member[],
): DistributionResult[] => {
  if (members.length === 0) {
    return [];
  }

  const activeOwnerIndex = 0;
  const ownerMember = members[activeOwnerIndex];

  const fairBaseProfit = calculateFairBaseProfit(
    totalProfit,
    ownerMember.ratio,
    feeRate,
  );

  const results = members.map((member, idx) =>
    computeMemberShare(
      member,
      fairBaseProfit,
      feeRate,
      idx === activeOwnerIndex, // isOwner check
    ),
  );

  return results;
};