Skip to content

Commit

Permalink
fix(utils): formatNumberWithCurrency locale 값 타입을 locale 포맷에 맞게 변경 (#671
Browse files Browse the repository at this point in the history
)
  • Loading branch information
ssi02014 authored Jan 12, 2025
1 parent 89579bd commit 37ef40a
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 100 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-jobs-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modern-kit/utils': patch
---

fix(utils): formatNumberWithCurrency locale 값 타입을 locale 포맷에 맞게 변경 - @ssi02014
57 changes: 45 additions & 12 deletions docs/docs/utils/formatter/formatNumberWithCurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,43 @@

`숫자` 혹은 `숫자로 이루어진 문자열`을 주어진 `통화 기호`를 추가하는 함수입니다.

기본 옵션으로 직접 원하는 형태로 숫자에 통화 기호를 추가해 포맷팅할 수 있습니다.
- 기본 옵션: `symbol`, `position`, `space`, `commas`, `decimal`

`locale` 옵션이 있으면 `locale` 형식에 따라 포맷팅됩니다. `소수점 자리(decimal)` 옵션은 포함됩니다.
- `locale`, `decimal` 옵션을 제외한 나머지 옵션들은 무시됩니다.

<br />

## Code
[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberWithCurrency/index.ts)

## Interface
```ts title="typescript"
type Locale = 'KRW' | 'KRW_SYMBOL' | 'USD' | 'JPY' | 'CNY' | 'EUR';

interface CurrencyOptions {
interface FormatNumberCurrencyOptions {
symbol?: string; // default: ''
position?: 'prefix' | 'suffix'; // default: 'suffix'
space?: boolean; // default: false
commas?: boolean; // default: true
decimal?: number; // default: 0
locale?: Locale;
}
```
```ts title="typescript"
// 옵션 없이 사용
function formatNumberWithCurrency(value: number | string): string;

// locale 옵션을 제외한, 기본 옵션이 주어지면 주어진 옵션에 따라 포맷팅됩니다.
// (기본 옵션: `symbol`, `position`, `space`, `commas`, `decimal`)
function formatNumberWithCurrency(
value: number | string,
options: Omit<FormatNumberCurrencyOptions, 'locale'>
): string;

// locale 옵션이 있으면 locale 형식에 따라 포맷팅됩니다.
function formatNumberWithCurrency(
value: number | string,
options?: CurrencyOptions
options: { locale: Locale; decimal?: number }
): string;
```

Expand Down Expand Up @@ -50,6 +66,7 @@ formatNumberWithCurrency(-1000, { symbol: '원', position: 'suffix' }) // '-1,00
<br />

### 옵션 사용
- 기본 옵션 사용(`symbol`, `position`, `space`, `commas`, `decimal`)
```ts title="typescript"
import { formatNumberWithCurrency } from '@modern-kit/utils';

Expand All @@ -64,12 +81,28 @@ formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix', space: true })
formatNumberWithCurrency(1000, { symbol: '', commas: false }) // '1000원'
formatNumberWithCurrency(1000, { symbol: '', commas: true }) // '1,000원'

// locale 사용
// locale 옵션이 있으면 commas 옵션을 제외한 나머지 옵션들은 무시됩니다.
formatNumberWithCurrency(1000, { locale: 'USD' }) // '$1,000'
formatNumberWithCurrency(1000, { locale: 'KRW' }) // '1,000원'
formatNumberWithCurrency(1000, { locale: 'KRW_SYMBOL' }) // '1,000₩'
formatNumberWithCurrency(1000, { locale: 'JPY' }) // '¥1,000'
formatNumberWithCurrency(1000, { locale: 'CNY' }) // '¥1,000'
formatNumberWithCurrency(1000, { locale: 'EUR' }) // '€1,000'
// 소숫점 자리수 포맷팅 (기본값: 0)
formatNumberWithCurrency(1000.234, { symbol: '', decimal: 3 }) // '1,000.234원'
formatNumberWithCurrency(1000.234, { symbol: '', decimal: 0 }) // '1,000원'
```

<br />

### 옵션 사용2
- `locale` 옵션 사용
```ts title="typescript"
import { formatNumberWithCurrency } from '@modern-kit/utils';

// locale 옵션이 있으면 locale 형식에 따라 포맷팅됩니다.
formatNumberWithCurrency(1000, { locale: 'en-US' }) // '$1,000'
formatNumberWithCurrency(1000, { locale: 'ko-KR-UNIT' }) // '1,000원'
formatNumberWithCurrency(1000, { locale: 'ko-KR' }) // '₩1,000'
formatNumberWithCurrency(1000, { locale: 'ja-JP' }) // '¥1,000'
formatNumberWithCurrency(1000, { locale: 'zh-CN' }) // '¥1,000'
formatNumberWithCurrency(1000, { locale: 'zh-HK' }) // 'HK$1,000'
formatNumberWithCurrency(1000, { locale: 'en-GB' }) // '£1,000'

// 소숫점 자리수 포맷팅 (기본값: 0)
formatNumberWithCurrency(1000.234, { locale: 'en-US', decimal: 3 }) // '$1,000.234'
formatNumberWithCurrency(1000.234, { locale: 'en-US', decimal: 0 }) // '$1,000'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const LOCALE_CURRENCY_MAP = {
'ko-KR': { currency: 'KRW' }, // 대한민국
'en-US': { currency: 'USD' }, // 미국
'ja-JP': { currency: 'JPY' }, // 일본
'zh-CN': { currency: 'CNY' }, // 중국
'de-DE': { currency: 'EUR' }, // 독일
'en-GB': { currency: 'GBP' }, // 영국
'fr-FR': { currency: 'EUR' }, // 프랑스
'it-IT': { currency: 'EUR' }, // 이탈리아
'es-ES': { currency: 'EUR' }, // 스페인
'nl-NL': { currency: 'EUR' }, // 네덜란드
'pt-PT': { currency: 'EUR' }, // 포르투갈
'zh-HK': { currency: 'HKD' }, // 홍콩
'ru-RU': { currency: 'RUB' }, // 러시아
'ar-SA': { currency: 'SAR' }, // 사우디아라비아
'tr-TR': { currency: 'TRY' }, // 터키
'vi-VN': { currency: 'VND' }, // 베트남
'th-TH': { currency: 'THB' }, // 태국
'id-ID': { currency: 'IDR' }, // 인도네시아
'my-MY': { currency: 'MYR' }, // 말레이시아
'ph-PH': { currency: 'PHP' }, // 필리핀
'sg-SG': { currency: 'SGD' }, // 싱가포르
'nz-NZ': { currency: 'NZD' }, // 뉴질랜드
// 필요 시 추가
} as const;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { formatNumberWithCurrency } from './index';
import { Locale } from './formatNumberWithCurrency.types';

describe('formatNumberWithCurrency', () => {
describe('기본 동작', () => {
Expand Down Expand Up @@ -78,40 +79,44 @@ describe('formatNumberWithCurrency', () => {
).toBe('$1000');
});

it('locale 옵션으로 통화 기호를 자동 적용해야 합니다.', () => {
it('decimal 옵션을 기반으로 소숫점 자리수를 포맷팅해야 합니다.', () => {
expect(
formatNumberWithCurrency(1000, {
locale: 'USD',
formatNumberWithCurrency(1000.234, {
symbol: '$',
position: 'prefix',
decimal: 3,
})
).toBe('$1,000');
).toBe('$1,000.234');

expect(
formatNumberWithCurrency(1000, {
locale: 'KRW',
formatNumberWithCurrency(1000.23, {
symbol: '$',
position: 'prefix',
decimal: 0,
})
).toBe('1,000원');
).toBe('$1,000');
});

it('locale 옵션이 있으면 commas 옵션을 제외한 나머지 옵션들은 무시되어야 합니다.', () => {
it('locale 옵션이 있으면 locale 옵션에 따라 통화 기호가 적용되어야 합니다.', () => {
expect(
formatNumberWithCurrency(1000, {
locale: 'USD',
commas: true,
symbol: '*', // 무시
position: 'prefix', // 무시
space: true, // 무시
locale: 'en-US', // { symbol: '$', position: 'prefix', space: false, commas: true }
})
).toBe('$1,000');

expect(
formatNumberWithCurrency(1000, {
locale: 'USD',
commas: false,
symbol: '*', // 무시
position: 'prefix', // 무시
space: true, // 무시
locale: 'ko-KR', // { symbol: '₩', position: 'prefix', space: false, commas: true }
})
).toBe('$1000');
).toBe('₩1,000');

// 소숫점 자리수 포맷팅
expect(
formatNumberWithCurrency(1000.123, {
locale: 'en-US', // { symbol: '$', position: 'prefix', space: false, commas: true }
decimal: 2,
})
).toBe('$1,000.12');
});
});

Expand All @@ -122,10 +127,16 @@ describe('formatNumberWithCurrency', () => {
);
});

it('decimal이 0 이상의 정수가 아닌 경우 에러를 던져야 합니다.', () => {
expect(() => formatNumberWithCurrency(1000, { decimal: -1 })).toThrow(
'decimal은 0 이상의 정수여야 합니다.'
);
});

it('지원하지 않는 locale 입력시 에러를 던져야 합니다.', () => {
expect(() =>
formatNumberWithCurrency(1000, {
locale: 'INVALID' as unknown as 'KRW',
locale: 'INVALID' as unknown as Locale,
})
).toThrow('유효하지 않은 locale 입니다.');
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
type Locale = 'KRW' | 'KRW_SYMBOL' | 'USD' | 'JPY' | 'CNY' | 'EUR';
import { LOCALE_CURRENCY_MAP } from './formatNumberWithCurrency.constants';

export type Locale = keyof typeof LOCALE_CURRENCY_MAP;

export interface FormatNumberCurrencyOptions {
symbol?: string;
position?: 'prefix' | 'suffix';
space?: boolean;
commas?: boolean;
locale?: Locale;
decimal?: number;
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,29 @@
import { formatNumberWithCommas } from '../../formatter/formatNumberWithCommas';
import { FormatNumberCurrencyOptions } from './formatNumberWithCurrency.types';

const LOCALE_CURRENCY_MAP = {
KRW: { symbol: '원', position: 'suffix', space: false },
KRW_SYMBOL: { symbol: '₩', position: 'suffix', space: false },
USD: { symbol: '$', position: 'prefix', space: false },
JPY: { symbol: '¥', position: 'prefix', space: false },
CNY: { symbol: '¥', position: 'prefix', space: false },
EUR: { symbol: '€', position: 'prefix', space: false },
} as const;

/**
* @description 통화 옵션을 가져오는 함수
*
* @param {Omit<FormatNumberCurrencyOptions, 'commas'>} options - 통화 옵션
* @returns {Omit<FormatNumberCurrencyOptions, 'locale' | 'commas'>} locale을 제외한 통화 옵션
*/
const getCurrencyOption = (
options: Omit<FormatNumberCurrencyOptions, 'commas'>
): Omit<FormatNumberCurrencyOptions, 'locale' | 'commas'> => {
const { symbol, position, space, locale } = options;

if (locale) {
if (!LOCALE_CURRENCY_MAP[locale]) {
throw new Error('유효하지 않은 locale 입니다.');
}

return LOCALE_CURRENCY_MAP[locale];
}

return { symbol, position, space };
};

/**
* @description 숫자에 통화 기호를 추가하는 함수입니다.
*
* @param {number | string} value - 통화 기호를 추가할 숫자 또는 문자열
* @param {FormatNumberCurrencyOptions & { isNegative: boolean }} currencyOption - 통화 단위 옵션
* @param {string} [currencyOption.symbol=''] - 통화 기호
* @param {'prefix' | 'suffix'} [currencyOption.position='suffix'] - 통화 기호 위치
* @param {boolean} [currencyOption.space=false] - 숫자와 통화 기호 사이 공백 여부
* @param {FormatNumberCurrencyOptions & { isNegative: boolean }} options - 통화 단위 옵션
* @param {string} [options.symbol=''] - 통화 기호
* @param {'prefix' | 'suffix'} [options.position='suffix'] - 통화 기호 위치
* @param {boolean} [options.space=false] - 숫자와 통화 기호 사이 공백 여부
* @returns {string} 통화 기호가 추가된 문자열
*/
export const getFormattedNumberWithCurrency = (
value: number,
options: FormatNumberCurrencyOptions & { isNegative: boolean }
value: string,
options: Omit<FormatNumberCurrencyOptions, 'decimal'> & {
isNegative: boolean;
}
): string => {
const { commas, ...restOption } = options;
const { symbol, position, space } = getCurrencyOption(restOption);
const { symbol, position, space, commas, isNegative } = options;

const valueToUse = commas ? formatNumberWithCommas(value) : String(value);

if (position === 'prefix') {
// 음수 처리
if (options.isNegative) {
if (isNegative) {
const numericPart = valueToUse.slice(1);
return '-' + symbol + (space ? ' ' : '') + numericPart;
}
Expand Down
Loading

0 comments on commit 37ef40a

Please sign in to comment.