From 57ff5c1045bd6cee5df99ba3595e96057bf19a3d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gromit=20=28=EC=A0=84=EB=AF=BC=EC=9E=AC=29?=
<64779472+ssi02014@users.noreply.github.com>
Date: Sun, 12 Jan 2025 18:21:12 +0900
Subject: [PATCH] =?UTF-8?q?fix(utils):=20formatNumberWithUnits,=20formatNu?=
=?UTF-8?q?mberWithCurrency=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20?=
=?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?=
=?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#666)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(utils): formatNumberWithUnits, formatNumberWithCurrency 로직 개선 및 인터페이스 변경
* feat: 소수점 처리 추가
* fix: formatNumberWithUnits/Currency 로직 변경
* refactor: formatNumberWithUnits 리팩토링
---
.changeset/curly-timers-hope.md | 5 +
.../utils/formatter/formatNumberByUnits.md | 76 ---------
.../utils/formatter/formatNumberCurrency.md | 109 ------------
.../formatter/formatNumberWithCurrency.md | 75 +++++++++
.../utils/formatter/formatNumberWithUnits.md | 91 ++++++++++
.../formatNumberByUnits.spec.ts | 75 ---------
.../formatter/formatNumberByUnits/index.ts | 72 --------
.../formatNumberCurrency.spec.ts | 94 -----------
.../formatter/formatNumberCurrency/index.ts | 37 -----
.../formatNumberWithCurrency.spec.ts | 133 +++++++++++++++
.../formatNumberWithCurrency.types.ts | 9 +
.../formatNumberWithCurrency.utils.ts | 63 +++++++
.../formatNumberWithCurrency/index.ts | 76 +++++++++
.../formatNumberWithUnits.spec.ts | 156 ++++++++++++++++++
.../formatNumberWithUnits.types.ts | 27 +++
.../formatNumberWithUnits.utils.ts | 78 +++++++++
.../formatter/formatNumberWithUnits/index.ts | 97 +++++++++++
packages/utils/src/formatter/index.ts | 4 +-
.../reverseString/reverseString.spec.ts | 7 +
packages/utils/vitest.config.ts | 1 +
20 files changed, 820 insertions(+), 465 deletions(-)
create mode 100644 .changeset/curly-timers-hope.md
delete mode 100644 docs/docs/utils/formatter/formatNumberByUnits.md
delete mode 100644 docs/docs/utils/formatter/formatNumberCurrency.md
create mode 100644 docs/docs/utils/formatter/formatNumberWithCurrency.md
create mode 100644 docs/docs/utils/formatter/formatNumberWithUnits.md
delete mode 100644 packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts
delete mode 100644 packages/utils/src/formatter/formatNumberByUnits/index.ts
delete mode 100644 packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts
delete mode 100644 packages/utils/src/formatter/formatNumberCurrency/index.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithCurrency/index.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts
create mode 100644 packages/utils/src/formatter/formatNumberWithUnits/index.ts
diff --git a/.changeset/curly-timers-hope.md b/.changeset/curly-timers-hope.md
new file mode 100644
index 000000000..8744c96d7
--- /dev/null
+++ b/.changeset/curly-timers-hope.md
@@ -0,0 +1,5 @@
+---
+'@modern-kit/utils': minor
+---
+
+fix(utils): formatNumberWithUnits, formatNumberWithCurrency 로직 개선 및 인터페이스 변경 - @ssi02014
diff --git a/docs/docs/utils/formatter/formatNumberByUnits.md b/docs/docs/utils/formatter/formatNumberByUnits.md
deleted file mode 100644
index 486c9c0e6..000000000
--- a/docs/docs/utils/formatter/formatNumberByUnits.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# formatNumberByUnits
-
-인자로 받은 숫자를 `단위 별로 구분`해주는 함수입니다.
-
-
-
-## Code
-[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberByUnits/index.ts)
-
-## Interface
-```ts title="typescript"
-interface Unit {
- unit: string;
- value: number;
-}
-
-type FloorUnit =
- | 1
- | 10
- | 100
- | 1000
- | 10000
- | 100000
- | 1000000
- | 10000000
- | 100000000;
-
-interface FormatNumberByUnitsOption {
- units?: Unit[]; // default: []
- withCommas?: boolean; // default: false,
- floorUnit?: FloorUnit; // default: 1,
- insertSpace?: boolean; // default: false,
-}
-
-const formatNumberByUnits: (
- value: number,
- options?: FormatNumberByUnitsOption
-) => string;
-```
-
-## Usage
-```ts title="typescript"
-import { formatNumberByUnits } from '@modern-kit/utils';
-
-const ONE_HUNDRED_MILLION = 100000000;
-const TEN_THOUSAND = 10000;
-
-const units = [
- { unit: '억', value: ONE_HUNDRED_MILLION },
- { unit: '만', value: TEN_THOUSAND },
-];
-
-const value1 = formatNumberByUnits(450000000, {
- units: units,
-}); // '4억5000만'
-
-// withCommas옵션으로 천 단위마다 콤마를 추가할 수 있습니다.
-const value2 = formatNumberByUnits(450000000, {
- withCommas: true,
- units: units,
-}); // '4억5,000만'
-
-// insertSpace옵션으로 단위 마다 띄어쓰기를 추가할 수 있습니다.
-const value3 = formatNumberByUnits(450000000, {
- units: units,
- withCommas: true,
- insertSpace: true,
-}); // '4억 5,000만'
-
-// floorUnit 옵션으로 해당 단위 미만의 값들을 버릴 수 있습니다.
-const value4 = formatNumberByUnits(459325300, {
- units: units,
- withCommas: false,
- floorUnit: 10000000,
-}); // '4억5000만'
-```
\ No newline at end of file
diff --git a/docs/docs/utils/formatter/formatNumberCurrency.md b/docs/docs/utils/formatter/formatNumberCurrency.md
deleted file mode 100644
index 2a2c07e2d..000000000
--- a/docs/docs/utils/formatter/formatNumberCurrency.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# formatNumberCurrency
-
-인자로 받은 숫자를 `단위 별로 구분`하고 `통화 단위`를 추가할 수 있습니다.
-
-
-
-## Code
-[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberCurrency/index.ts)
-
-## Interface
-```ts title="typescript"
-interface CurrencyOption {
- currency: string;
- currencyPosition: 'prefix' | 'suffix';
-}
-
-/*
- units?: Unit[]; // default: []
- floorUnit?: FloorUnit; // default: 1,
- withCommas?: boolean; // default: false,
- insertSpace?: boolean; // default: false,
- currency?: string; // default: ''
- currencyPosition?: 'prefix' | 'suffix'; // default: 'suffix'
-*/
-type FormatNumberCurrencyOptions = FormatNumberByUnitsOption &
- Partial;
-
-const formatNumberCurrency: (
- value: number,
- options?: FormatNumberCurrencyOptions
-) => string;
-```
-
-## Usage
-### Basic
-기본적인 사용법은 아래와 같습니다.
-```ts title="typescript"
-import { formatNumberCurrency } from '@modern-kit/utils';
-
-const ONE_HUNDRED_MILLION = 100000000;
-const TEN_THOUSAND = 10000;
-
-const units = [
- { unit: '억', value: ONE_HUNDRED_MILLION },
- { unit: '만', value: TEN_THOUSAND },
-];
-
-const value1 = formatNumberCurrency(450000000, {
- units: units,
- currency: '원',
- currencyPosition: 'suffix',
-}); // '4억5000만원'
-
-const value2 = formatNumberCurrency(4500, {
- currency: '$',
- currencyPosition: 'prefix',
-}); // '$4500'
-
-// withCommas 옵션으로 천 단위마다 콤마를 추가할 수 있습니다.
-const value2 = formatNumberCurrency(450000000, {
- units: units,
- withCommas: true,
- currency: '원',
- currencyPosition: 'suffix',
-}); // '4억5,000만원'
-
-// insertSpace 옵션으로 단위 마다 띄어쓰기를 추가할 수 있습니다.
-const value3 = formatNumberCurrency(450000000, {
- units: units,
- withCommas: true,
- currency: '원',
- currencyPosition: 'suffix',
- insertSpace: true,
-}); // '4억 5,000만원'
-
-// floorUnit 옵션으로 해당 단위 미만의 값들을 버릴 수 있습니다.
-const value4 = formatNumberCurrency(459325300, {
- units: units,
- currency: '원',
- currencyPosition: 'suffix',
- floorUnit: 10000000,
-}); // '4억5000만원'
-```
-
-
-
-### ⭐️ Best Practice
-아래와 같이 필요에 맞게 `추상화`해서 사용하는 것을 추천드립니다.
-
-```ts title="typescript"
-const ONE_HUNDRED_MILLION = 100000000;
-const TEN_THOUSAND = 10000;
-
-const units = [
- { unit: '억', value: ONE_HUNDRED_MILLION },
- { unit: '만', value: TEN_THOUSAND },
-];
-
-const formatToKRW = (value: number) => {
- return formatNumberCurrency(value, {
- units: units,
- currency: '원',
- currencyPosition: 'suffix',
- });
-}
-
-const value1 = formatToKRW(42000000); // 4200만원
-const value2 = formatToKRW(425000000); // 4억2500만원
-```
\ No newline at end of file
diff --git a/docs/docs/utils/formatter/formatNumberWithCurrency.md b/docs/docs/utils/formatter/formatNumberWithCurrency.md
new file mode 100644
index 000000000..97465fda0
--- /dev/null
+++ b/docs/docs/utils/formatter/formatNumberWithCurrency.md
@@ -0,0 +1,75 @@
+# formatNumberWithCurrency
+
+`숫자` 혹은 `숫자로 이루어진 문자열`을 주어진 `통화 기호`를 추가하는 함수입니다.
+
+
+
+## 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 {
+ symbol?: string; // default: ''
+ position?: 'prefix' | 'suffix'; // default: 'suffix'
+ space?: boolean; // default: false
+ commas?: boolean; // default: true
+ locale?: Locale;
+}
+```
+```ts title="typescript"
+function formatNumberWithCurrency(
+ value: number | string,
+ options?: CurrencyOptions
+): string;
+```
+
+## Usage
+### 기본 동작
+```ts title="typescript"
+import { formatNumberWithCurrency } from '@modern-kit/utils';
+
+// 기본 동작
+formatNumberWithCurrency(1000) // '1,000'
+
+// 통화 기호 추가 (기본 값: '')
+formatNumberWithCurrency(1000, { symbol: '원' }) // '1,000원'
+formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix' }) // '$1,000'
+
+// 숫자로 이루어진 문자열도 허용
+formatNumberWithCurrency('1000', { symbol: '원' }) // '1,000원'
+formatNumberWithCurrency('1000', { symbol: '$', position: 'prefix' }) // '$1,000'
+
+// 음수 처리
+formatNumberWithCurrency(-1000, { symbol: '$', position: 'prefix' }) // '-$1,000'
+formatNumberWithCurrency(-1000, { symbol: '원', position: 'suffix' }) // '-1,000원'
+```
+
+
+
+### 옵션 사용
+```ts title="typescript"
+import { formatNumberWithCurrency } from '@modern-kit/utils';
+
+// 통호 기호 위치 변경 (기본값: 'suffix')
+formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix' }) // '$1,000'
+
+// 공백 추가 (기본값: false)
+formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix', space: false }) // '$1000'
+formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix', space: true }) // '$ 1000'
+
+// 천의 단위 구분 여부 (기본값: 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'
+```
diff --git a/docs/docs/utils/formatter/formatNumberWithUnits.md b/docs/docs/utils/formatter/formatNumberWithUnits.md
new file mode 100644
index 000000000..1bd6558e3
--- /dev/null
+++ b/docs/docs/utils/formatter/formatNumberWithUnits.md
@@ -0,0 +1,91 @@
+# formatNumberWithUnits
+
+`숫자` 혹은 `숫자로 이루어진 문자열`을 주어진 `단위` 별로 포맷팅하는 함수입니다.
+
+
+
+## Code
+[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/formatter/formatNumberWithUnits/index.ts)
+
+## Interface
+```ts title="typescript"
+interface Unit {
+ unit: string;
+ value: number;
+}
+
+type FloorUnit =
+ | 1
+ | 10
+ | 100
+ | 1_000
+ | 10_000
+ | 100_000
+ | 1_000_000
+ | 10_000_000
+ | 100_000_000
+ | 1_000_000_000
+ | 10_000_000_000
+ | 100_000_000_000
+ | 1_000_000_000_000;
+
+interface FormatNumberWithUnitsOptions {
+ units?: Unit[] | readonly Unit[]; // default: []
+ commas?: boolean; // default: true
+ floorUnit?: FloorUnit; // default: 1
+ space?: boolean; // default: true
+}
+```
+```ts title="typescript"
+function formatNumberWithUnits(
+ value: number | string,
+ options?: FormatNumberWithUnitsOptions
+): string;
+```
+
+## Usage
+### 기본 동작
+```ts title="typescript"
+import { formatNumberWithUnits } from '@modern-kit/utils';
+
+const KRW_UNITS = [
+ { unit: '조', value: 1_000_000_000_000 },
+ { unit: '억', value: 100_000_000 },
+ { unit: '만', value: 10_000 },
+] as const;
+
+// 기본 동작
+formatNumberWithUnits(1234567) // "1,234,567"
+
+formatNumberWithUnits(1234567, { units: KRW_UNITS }) // "123만 4,567"
+formatNumberWithUnits(-1234567, { units: KRW_UNITS }) // "-123만 4,567", 음수 처리
+
+formatNumberWithUnits('1234567', { units: KRW_UNITS }) // "123만 4,567", 숫자로 이루어진 문자열 허용
+```
+
+
+
+### 옵션 사용
+```ts title="typescript"
+import { formatNumberWithUnits } from '@modern-kit/utils';
+
+const KRW_UNITS = [
+ { unit: '조', value: 1_000_000_000_000 },
+ { unit: '억', value: 100_000_000 },
+ { unit: '만', value: 10_000 },
+] as const;
+
+// 단위 사이 공백 추가 (기본값: true)
+formatNumberWithUnits(1234567, { units: KRW_UNITS, space: true }) // "123만 4,567"
+formatNumberWithUnits(1234567, { units: KRW_UNITS, space: false }) // "123만4,567"
+
+// 쉼표 사용 여부 (기본값: true)
+formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: false }) // "123만 4567"
+formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: true }) // "123만 4,567"
+
+// 버림 단위 (기본값: 1)
+formatNumberWithUnits(1234567, { units: KRW_UNITS, floorUnit: 10000 }) // "123만"
+
+// 소수점 자리수 (기본값: 0)
+formatNumberWithUnits(1234567.123, { units: KRW_UNITS, decimal: 2 }) // "123만 4,567.12"
+```
\ No newline at end of file
diff --git a/packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts b/packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts
deleted file mode 100644
index 4904386a2..000000000
--- a/packages/utils/src/formatter/formatNumberByUnits/formatNumberByUnits.spec.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { formatNumberByUnits } from '.';
-
-const ONE_HUNDRED_MILLION = 100000000;
-const TEN_THOUSAND = 10000;
-
-const testUnits = [
- { unit: '억', value: ONE_HUNDRED_MILLION },
- { unit: '만', value: TEN_THOUSAND },
-];
-
-describe('formatNumberByUnits', () => {
- it('should format numbers based on the provided options', () => {
- const testValue1 = formatNumberByUnits(4500000, {
- withCommas: false,
- units: testUnits,
- });
- expect(testValue1).toBe('450만');
-
- const testValue2 = formatNumberByUnits(450000000, {
- withCommas: false,
- units: testUnits,
- });
- expect(testValue2).toBe('4억5000만');
-
- const testValue3 = formatNumberByUnits(459005300, {
- withCommas: false,
- units: testUnits,
- });
- expect(testValue3).toBe('4억5900만5300');
-
- const testValue4 = formatNumberByUnits(459005300, {
- withCommas: true,
- units: testUnits,
- });
- expect(testValue4).toBe('4억5,900만5,300');
- });
-
- it('should add spaces between units when the insertSpace option is provided', () => {
- const testValue2 = formatNumberByUnits(450000000, {
- withCommas: true,
- units: testUnits,
- insertSpace: true,
- });
- expect(testValue2).toBe('4억 5,000만');
- });
-
- it('should discard values below the specified floorUnit when provided', () => {
- const testValue1 = formatNumberByUnits(459325300, {
- floorUnit: 10000,
- withCommas: false,
- units: testUnits,
- });
- expect(testValue1).toBe('4억5932만');
-
- const testValue2 = formatNumberByUnits(459325300, {
- floorUnit: 10000000,
- withCommas: false,
- units: testUnits,
- });
- expect(testValue2).toBe('4억5000만');
-
- const testValue4 = formatNumberByUnits(4000000, {
- floorUnit: 100000000,
- withCommas: false,
- units: testUnits,
- });
- expect(testValue4).toBe('0');
- });
-
- it('should format using default values if no options are provided', () => {
- const testValue1 = formatNumberByUnits(450000000);
- expect(testValue1).toBe('450000000');
- });
-});
diff --git a/packages/utils/src/formatter/formatNumberByUnits/index.ts b/packages/utils/src/formatter/formatNumberByUnits/index.ts
deleted file mode 100644
index cf03b7a92..000000000
--- a/packages/utils/src/formatter/formatNumberByUnits/index.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { formatNumberWithCommas } from '../formatNumberWithCommas';
-
-interface Unit {
- unit: string;
- value: number;
-}
-
-type FloorUnit =
- | 1
- | 10
- | 100
- | 1000
- | 10000
- | 100000
- | 1000000
- | 10000000
- | 100000000;
-
-export interface FormatNumberByUnitsOption {
- units?: Unit[];
- withCommas?: boolean;
- floorUnit?: FloorUnit;
- insertSpace?: boolean;
-}
-
-const getNumberWithConditionalCommas = (value: number, withCommas: boolean) => {
- return withCommas ? formatNumberWithCommas(value) : String(value);
-};
-
-export function formatNumberByUnits(
- value: number,
- options: FormatNumberByUnitsOption = {}
-) {
- const {
- units = [],
- insertSpace = false,
- withCommas = false,
- floorUnit = 1,
- } = options;
-
- if (value < floorUnit) {
- return String(0);
- }
-
- if (units.length === 0) {
- return getNumberWithConditionalCommas(value, withCommas);
- }
-
- const sortedUnits = [...units].sort((a, b) => b.value - a.value);
- let result = '';
- let remainder = Math.floor(value / floorUnit) * floorUnit;
-
- sortedUnits.forEach(({ unit, value: unitValue }) => {
- const quotient = Math.floor(remainder / unitValue);
-
- if (quotient > 0) {
- result += `${getNumberWithConditionalCommas(
- quotient,
- withCommas
- )}${unit}`;
-
- if (insertSpace) result += ' ';
- remainder %= unitValue;
- }
- });
-
- if (remainder > 0) {
- result += `${getNumberWithConditionalCommas(remainder, withCommas)}`;
- }
-
- return result.trim();
-}
diff --git a/packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts b/packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts
deleted file mode 100644
index 40924182c..000000000
--- a/packages/utils/src/formatter/formatNumberCurrency/formatNumberCurrency.spec.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { formatNumberCurrency } from '.';
-
-const ONE_HUNDRED_MILLION = 100000000;
-const TEN_THOUSAND = 10000;
-
-const testUnits = [
- { unit: '억', value: ONE_HUNDRED_MILLION },
- { unit: '만', value: TEN_THOUSAND },
-];
-
-describe('formatNumberCurrency', () => {
- it('should format numbers based on the provided options', () => {
- const testValue1 = formatNumberCurrency(4500000, {
- withCommas: false,
- units: testUnits,
- currency: '원',
- currencyPosition: 'suffix',
- });
- expect(testValue1).toBe('450만원');
-
- const testValue2 = formatNumberCurrency(459005300, {
- withCommas: false,
- units: testUnits,
- currency: '원',
- currencyPosition: 'suffix',
- });
- expect(testValue2).toBe('4억5900만5300원');
-
- const testValue3 = formatNumberCurrency(4500, {
- withCommas: false,
- currency: '$',
- currencyPosition: 'prefix',
- });
- expect(testValue3).toBe('$4500');
-
- const testValue4 = formatNumberCurrency(459005300, {
- withCommas: true,
- units: testUnits,
- currency: '원',
- currencyPosition: 'suffix',
- });
- expect(testValue4).toBe('4억5,900만5,300원');
-
- const testValue5 = formatNumberCurrency(4500, {
- withCommas: true,
- currency: '$',
- currencyPosition: 'prefix',
- });
- expect(testValue5).toBe('$4,500');
- });
-
- it('should add spaces between units when the insertSpace option is provided', () => {
- const testValue2 = formatNumberCurrency(450000000, {
- withCommas: true,
- units: testUnits,
- insertSpace: true,
- currency: '원',
- currencyPosition: 'suffix',
- });
- expect(testValue2).toBe('4억 5,000만원');
- });
-
- it('should discard values below the specified floorUnit when provided', () => {
- const testValue1 = formatNumberCurrency(459325300, {
- floorUnit: 10000,
- withCommas: false,
- units: testUnits,
- currency: '원',
- });
- expect(testValue1).toBe('4억5932만원');
-
- const testValue2 = formatNumberCurrency(459325300, {
- floorUnit: 10000000,
- withCommas: false,
- units: testUnits,
- currency: '원',
- });
- expect(testValue2).toBe('4억5000만원');
-
- const testValue4 = formatNumberCurrency(4000000, {
- floorUnit: 100000000,
- withCommas: false,
- units: testUnits,
- currency: '원',
- });
- expect(testValue4).toBe('0원');
- });
-
- it('should format using default values if no options are provided', () => {
- const testValue = formatNumberCurrency(450000000);
- expect(testValue).toBe('450000000');
- });
-});
diff --git a/packages/utils/src/formatter/formatNumberCurrency/index.ts b/packages/utils/src/formatter/formatNumberCurrency/index.ts
deleted file mode 100644
index bd4cbe873..000000000
--- a/packages/utils/src/formatter/formatNumberCurrency/index.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import {
- formatNumberByUnits,
- FormatNumberByUnitsOption,
-} from '../formatNumberByUnits';
-
-interface CurrencyOption {
- currency: string;
- currencyPosition: 'prefix' | 'suffix';
-}
-
-type FormatNumberCurrencyOptions = FormatNumberByUnitsOption &
- Partial;
-
-const addCurrency = (value: string, currencyOption: CurrencyOption) => {
- const { currency, currencyPosition } = currencyOption;
-
- if (currencyPosition === 'prefix') {
- return currency + value;
- }
- return value + currency;
-};
-
-export function formatNumberCurrency(
- value: number,
- options: FormatNumberCurrencyOptions = {}
-) {
- const {
- currency = '',
- currencyPosition = 'suffix',
- ...restOptions
- } = options;
-
- return addCurrency(formatNumberByUnits(value, restOptions), {
- currency,
- currencyPosition,
- });
-}
diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts
new file mode 100644
index 000000000..cf63fc08c
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.spec.ts
@@ -0,0 +1,133 @@
+import { describe, it, expect } from 'vitest';
+import { formatNumberWithCurrency } from './index';
+
+describe('formatNumberWithCurrency', () => {
+ describe('기본 동작', () => {
+ it('옵션 없이 호출하면 숫자만 반환해야 합니다.', () => {
+ expect(formatNumberWithCurrency(1000)).toBe('1,000');
+ expect(formatNumberWithCurrency('1000')).toBe('1,000');
+ });
+
+ it('기본적으로 접미사(suffix) 위치에 통화 기호를 추가해야 합니다.', () => {
+ expect(formatNumberWithCurrency(1000, { symbol: '원' })).toBe('1,000원');
+ expect(formatNumberWithCurrency('1000', { symbol: '원' })).toBe(
+ '1,000원'
+ );
+ });
+
+ it('음수일 때 통화 기호를 앞에 추가해야 합니다.', () => {
+ expect(
+ formatNumberWithCurrency(-1000, { symbol: '$', position: 'prefix' })
+ ).toBe('-$1,000');
+ expect(
+ formatNumberWithCurrency('-1000', { symbol: '$', position: 'prefix' })
+ ).toBe('-$1,000');
+ });
+ });
+
+ describe('옵션 적용', () => {
+ it('position 옵션을 기반으로 기호를 추가해야 합니다.', () => {
+ expect(
+ formatNumberWithCurrency(1000, {
+ symbol: '$',
+ position: 'prefix',
+ })
+ ).toBe('$1,000');
+
+ expect(
+ formatNumberWithCurrency(1000, {
+ symbol: '$',
+ position: 'suffix',
+ })
+ ).toBe('1,000$');
+ });
+
+ it('space가 true일 때 통화 기호와 숫자 사이에 공백을 추가해야 합니다.', () => {
+ expect(
+ formatNumberWithCurrency(1000, {
+ symbol: '$',
+ position: 'prefix',
+ space: true,
+ })
+ ).toBe('$ 1,000');
+
+ expect(
+ formatNumberWithCurrency(1000, {
+ symbol: '$',
+ position: 'prefix',
+ space: false,
+ })
+ ).toBe('$1,000');
+ });
+
+ it('commas 옵션을 기반으로 쉼표를 추가해야 합니다.', () => {
+ expect(
+ formatNumberWithCurrency(1000, {
+ symbol: '$',
+ position: 'prefix',
+ commas: true,
+ })
+ ).toBe('$1,000');
+
+ expect(
+ formatNumberWithCurrency(1000, {
+ symbol: '$',
+ position: 'prefix',
+ commas: false,
+ })
+ ).toBe('$1000');
+ });
+
+ it('locale 옵션으로 통화 기호를 자동 적용해야 합니다.', () => {
+ expect(
+ formatNumberWithCurrency(1000, {
+ locale: 'USD',
+ })
+ ).toBe('$1,000');
+
+ expect(
+ formatNumberWithCurrency(1000, {
+ locale: 'KRW',
+ })
+ ).toBe('1,000원');
+ });
+
+ it('locale 옵션이 있으면 commas 옵션을 제외한 나머지 옵션들은 무시되어야 합니다.', () => {
+ expect(
+ formatNumberWithCurrency(1000, {
+ locale: 'USD',
+ commas: true,
+ symbol: '*', // 무시
+ position: 'prefix', // 무시
+ space: true, // 무시
+ })
+ ).toBe('$1,000');
+
+ expect(
+ formatNumberWithCurrency(1000, {
+ locale: 'USD',
+ commas: false,
+ symbol: '*', // 무시
+ position: 'prefix', // 무시
+ space: true, // 무시
+ })
+ ).toBe('$1000');
+ });
+ });
+
+ describe('에러 처리', () => {
+ it('숫자가 아닌 값이 주어지면 에러를 던져야 합니다.', () => {
+ expect(() => formatNumberWithCurrency('10d00')).toThrow(
+ 'value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.'
+ );
+ });
+
+ it('지원하지 않는 locale 입력시 에러를 던져야 합니다.', () => {
+ expect(() =>
+ formatNumberWithCurrency(1000, {
+ locale: 'INVALID' as unknown as 'KRW',
+ })
+ ).toThrow('유효하지 않은 locale 입니다.');
+ });
+ });
+});
diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts
new file mode 100644
index 000000000..581b1e372
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.types.ts
@@ -0,0 +1,9 @@
+type Locale = 'KRW' | 'KRW_SYMBOL' | 'USD' | 'JPY' | 'CNY' | 'EUR';
+
+export interface FormatNumberCurrencyOptions {
+ symbol?: string;
+ position?: 'prefix' | 'suffix';
+ space?: boolean;
+ commas?: boolean;
+ locale?: Locale;
+}
diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts
new file mode 100644
index 000000000..b6b94707d
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithCurrency/formatNumberWithCurrency.utils.ts
@@ -0,0 +1,63 @@
+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} options - 통화 옵션
+ * @returns {Omit} locale을 제외한 통화 옵션
+ */
+const getCurrencyOption = (
+ options: Omit
+): Omit => {
+ 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] - 숫자와 통화 기호 사이 공백 여부
+ * @returns {string} 통화 기호가 추가된 문자열
+ */
+export const getFormattedNumberWithCurrency = (
+ value: number,
+ options: FormatNumberCurrencyOptions & { isNegative: boolean }
+): string => {
+ const { commas, ...restOption } = options;
+ const { symbol, position, space } = getCurrencyOption(restOption);
+
+ const valueToUse = commas ? formatNumberWithCommas(value) : String(value);
+
+ if (position === 'prefix') {
+ // 음수 처리
+ if (options.isNegative) {
+ const numericPart = valueToUse.slice(1);
+ return '-' + symbol + (space ? ' ' : '') + numericPart;
+ }
+ return symbol + (space ? ' ' : '') + valueToUse;
+ }
+ return valueToUse + (space ? ' ' : '') + symbol;
+};
diff --git a/packages/utils/src/formatter/formatNumberWithCurrency/index.ts b/packages/utils/src/formatter/formatNumberWithCurrency/index.ts
new file mode 100644
index 000000000..7b7db37e2
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithCurrency/index.ts
@@ -0,0 +1,76 @@
+import { isNumber } from '../../validator/isNumber';
+import { FormatNumberCurrencyOptions } from './formatNumberWithCurrency.types';
+import { getFormattedNumberWithCurrency } from './formatNumberWithCurrency.utils';
+
+/**
+ * @description `숫자 혹은 숫자로 이뤄진 문자열`을 주어진 `통화 기호`를 추가하는 함수입니다.
+ *
+ * @param {number | string} value - 포맷팅할 숫자 값
+ * @param {FormatNumberCurrencyOptions} options - 포맷팅 옵션
+ * @param {string} [options.symbol=''] - 통화 기호
+ * @param {'prefix' | 'suffix'} [options.position='suffix'] - 통화 기호 위치
+ * @param {boolean} [options.space=false] - 숫자와 통화 기호 사이 공백 여부
+ * @param {boolean} [options.commas=true] - 천의 단위 구분 여부
+ * @param {'KRW' | 'KRW_SYMBOL' | 'USD' | 'JPY' | 'CNY' | 'EUR'} [options.locale] - 통화 단위
+ * @returns 통화 기호가 포함된 포맷팅된 문자열
+ *
+ * @example
+ * // 기본 사용법
+ * formatNumberWithCurrency(1000) // '1,000'
+ *
+ * // 통화 기호 추가 (기본값: '')
+ * formatNumberWithCurrency(1000, { symbol: '원' }) // '1,000원'
+ *
+ * // 음수 처리
+ * formatNumberWithCurrency(-1000, { symbol: '$', position: 'prefix' }) // '-$1,000'
+ *
+ * // 숫자로 이뤄진 문자열 허용
+ * formatNumberWithCurrency('1000', { symbol: '원' }) // '1,000원'
+ *
+ * @example
+ * // 통호 기호 위치 변경 (기본값: 'suffix')
+ * formatNumberWithCurrency(1000, { symbol: '$', position: 'prefix' }) // '$1,000'
+ *
+ * // 공백 추가 (기본값: false)
+ * formatNumberWithCurrency(1000, { symbol: '₩', space: true }) // '1,000 원'
+ *
+ * // 천의 단위 구분 여부 (기본값: true)
+ * formatNumberWithCurrency(1000, { symbol: '원', commas: false }) // '1000원'
+ * formatNumberWithCurrency(1000, { symbol: '원', commas: true }) // '1,000원'
+ *
+ * // locale 사용
+ * formatNumberWithCurrency(1000, { locale: 'USD' }) // '$1,000'
+ * formatNumberWithCurrency(1000, { locale: 'KRW' }) // '1,000원'
+ *
+ * // locale 옵션이 있으면 commas 옵션을 제외한 나머지 옵션들은 무시됩니다.
+ * formatNumberWithCurrency(1000, { locale: 'KRW', commas: false }) // '1000원'
+ */
+export function formatNumberWithCurrency(
+ value: number | string,
+ options: FormatNumberCurrencyOptions = {}
+) {
+ const {
+ symbol = '',
+ position = 'suffix',
+ space = false,
+ commas = true,
+ locale,
+ } = options;
+ const valueToUse = isNumber(value) ? value : Number(value);
+ const isNegative = valueToUse < 0;
+
+ if (isNaN(valueToUse)) {
+ throw new Error('value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.');
+ }
+
+ const formattedResult = getFormattedNumberWithCurrency(valueToUse, {
+ symbol,
+ position,
+ space,
+ locale,
+ commas,
+ isNegative,
+ });
+
+ return formattedResult;
+}
diff --git a/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts
new file mode 100644
index 000000000..7f2cb36eb
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.spec.ts
@@ -0,0 +1,156 @@
+import { describe, it, expect } from 'vitest';
+import { formatNumberWithUnits } from './index';
+
+const KRW_UNITS = [
+ { unit: '조', value: 1_000_000_000_000 },
+ { unit: '억', value: 100_000_000 },
+ { unit: '만', value: 10_000 },
+] as const;
+
+describe('formatNumberWithUnits', () => {
+ describe('기본 동작', () => {
+ it('units 옵션이 주어지면 해당 단위로 숫자를 포맷팅한다', () => {
+ expect(formatNumberWithUnits(1234567890000, { units: KRW_UNITS })).toBe(
+ '1조 2,345억 6,789만'
+ );
+ expect(formatNumberWithUnits(1200000000, { units: KRW_UNITS })).toBe(
+ '12억'
+ );
+ expect(formatNumberWithUnits(4010000, { units: KRW_UNITS })).toBe(
+ '401만'
+ );
+
+ // 문자열
+ expect(formatNumberWithUnits('1234567890000', { units: KRW_UNITS })).toBe(
+ '1조 2,345억 6,789만'
+ );
+ expect(formatNumberWithUnits('1200000000', { units: KRW_UNITS })).toBe(
+ '12억'
+ );
+ expect(formatNumberWithUnits('4010000', { units: KRW_UNITS })).toBe(
+ '401만'
+ );
+ });
+
+ it('음수를 처리할 수 있다', () => {
+ expect(formatNumberWithUnits(-1234567890000, { units: KRW_UNITS })).toBe(
+ '-1조 2,345억 6,789만'
+ );
+ expect(formatNumberWithUnits(-1000000000, { units: KRW_UNITS })).toBe(
+ '-10억'
+ );
+
+ // 문자열
+ expect(
+ formatNumberWithUnits('-1234567890000', { units: KRW_UNITS })
+ ).toBe('-1조 2,345억 6,789만');
+ expect(formatNumberWithUnits('-1000000000', { units: KRW_UNITS })).toBe(
+ '-10억'
+ );
+ });
+
+ it('0과 작은 숫자를 처리한다', () => {
+ expect(formatNumberWithUnits(0)).toBe('0');
+ expect(formatNumberWithUnits(9)).toBe('9');
+ });
+ });
+
+ // 문자열 케이스는 제외
+ describe('옵션 적용', () => {
+ it('floorUnit 주어진 값에 따라 버림 단위를 적용해야 합니다.', () => {
+ expect(
+ formatNumberWithUnits(12345, { floorUnit: 10000, units: KRW_UNITS })
+ ).toBe('1만');
+
+ // value보다 floorUnit이 크면 '0'을 반환
+ expect(
+ formatNumberWithUnits(12345, { floorUnit: 100000, units: KRW_UNITS })
+ ).toBe('0');
+
+ // units 옵션이 주어지지 않으면 기본 단위로 포맷팅
+ expect(formatNumberWithUnits(12345, { floorUnit: 10000 })).toBe('10,000');
+ });
+
+ it('commas가 true라면 ","를 추가하며, false라면 제외해야 합니다.', () => {
+ expect(
+ formatNumberWithUnits(1234567890000, {
+ commas: false,
+ units: KRW_UNITS,
+ })
+ ).toBe('1조 2345억 6789만');
+
+ expect(
+ formatNumberWithUnits(1234567890000, {
+ commas: true,
+ units: KRW_UNITS,
+ })
+ ).toBe('1조 2,345억 6,789만');
+
+ // units 옵션이 주어지지 않으면 기본 단위로 포맷팅
+ expect(formatNumberWithUnits(1234567890000, { commas: true })).toBe(
+ '1,234,567,890,000'
+ );
+ });
+
+ it('space가 true라면 unit 사이 공백을 추가하며, false라면 제외해야 합니다.', () => {
+ expect(
+ formatNumberWithUnits(1234567890000, {
+ space: false,
+ units: KRW_UNITS,
+ })
+ ).toBe('1조2,345억6,789만');
+
+ expect(
+ formatNumberWithUnits(1234567890000, {
+ space: true,
+ units: KRW_UNITS,
+ })
+ ).toBe('1조 2,345억 6,789만');
+
+ // units 옵션이 주어지지 않으면 기본 단위로 포맷팅
+ expect(formatNumberWithUnits(1234567890000, { space: true })).toBe(
+ '1,234,567,890,000'
+ );
+ });
+
+ it('decimal가 주어진 값에 따라 소수점 자리수를 적용해야 합니다.', () => {
+ expect(
+ formatNumberWithUnits(1234567.123, { decimal: 2, units: KRW_UNITS })
+ ).toBe('123만 4,567.12');
+
+ // 만약, floorUnit가 1보다 크면 소수점 자리수를 적용하지 않습니다.
+ expect(
+ formatNumberWithUnits(1234567.123, {
+ decimal: 2,
+ floorUnit: 1000,
+ units: KRW_UNITS,
+ })
+ ).toBe('123만 4,000');
+
+ // units 옵션이 주어지지 않으면 기본 단위로 포맷팅
+ expect(formatNumberWithUnits(1234567.123, { decimal: 2 })).toBe(
+ '1,234,567.12'
+ );
+ });
+ });
+
+ describe('예외 케이스', () => {
+ it('value가 숫자 혹은 숫자로 이뤄진 문자열이 아니면 예외를 발생시킵니다.', () => {
+ expect(() => formatNumberWithUnits('d123ㅇ4567')).toThrow(
+ 'value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.'
+ );
+ });
+
+ it('floorUnit가 1 이상의 정수가 아니면 예외를 발생시킵니다.', () => {
+ expect(() =>
+ formatNumberWithUnits(1234567, { floorUnit: -1 as unknown as 10 })
+ ).toThrow('floorUnit은 1을 포함한 10의 제곱수여야 합니다.');
+ });
+
+ it('decimal이 0보다 작으면 예외를 발생시킵니다.', () => {
+ expect(() =>
+ formatNumberWithUnits(1234567, { decimal: -1 as unknown as number })
+ ).toThrow('decimal은 0 이상의 정수여야 합니다.');
+ });
+ });
+});
diff --git a/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts
new file mode 100644
index 000000000..17df2a98c
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.types.ts
@@ -0,0 +1,27 @@
+type FloorUnit =
+ | 1
+ | 10
+ | 100
+ | 1_000
+ | 10_000
+ | 100_000
+ | 1_000_000
+ | 10_000_000
+ | 100_000_000
+ | 1_000_000_000
+ | 10_000_000_000
+ | 100_000_000_000
+ | 1_000_000_000_000;
+
+export interface Unit {
+ unit: string;
+ value: number;
+}
+
+export interface FormatNumberWithUnitsOptions {
+ units?: Unit[] | readonly Unit[];
+ commas?: boolean;
+ floorUnit?: FloorUnit;
+ space?: boolean;
+ decimal?: number;
+}
diff --git a/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts
new file mode 100644
index 000000000..cdad89803
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithUnits/formatNumberWithUnits.utils.ts
@@ -0,0 +1,78 @@
+import { formatNumberWithCommas } from '../../formatter/formatNumberWithCommas';
+import {
+ FormatNumberWithUnitsOptions,
+ Unit,
+} from './formatNumberWithUnits.types';
+
+/**
+ * @description 쉼표 사용 여부에 따라 숫자를 포맷팅하는 함수
+ *
+ * @param {number} value - 포맷팅할 숫자
+ * @param {boolean} commas - 쉼표 사용 여부
+ * @returns {string} 포맷팅된 문자열
+ */
+const getNumberWithConditionalCommas = (
+ value: number | string,
+ commas: boolean
+): string => {
+ return commas ? formatNumberWithCommas(value) : String(value);
+};
+
+/**
+ * @description 주어진 단위(units)에 따라 숫자를 포맷팅하는 함수
+ *
+ * @param {number} value - 포맷팅할 숫자 값
+ * @param {Unit[] | readonly Unit[]} units - 변환할 단위 배열
+ * @param {Omit, 'units'>} options - 포맷팅 옵션
+ * @param {boolean} options.commas - 천 단위 구분 쉼표 사용 여부입니다.
+ * @param {boolean} options.space - 단위 사이 공백 추가 여부입니다.
+ * @param {number} options.decimal - 소수점 자릿수입니다.
+ * @returns {string} 포맷팅된 문자열
+ */
+export const getFormattedValueWithUnits = (
+ value: number,
+ units: Unit[] | readonly Unit[],
+ options: Omit, 'units'>
+): string => {
+ const { commas, space, decimal, floorUnit } = options;
+
+ const absoluteValue = Math.abs(value);
+ const isNegative = value < 0;
+
+ // value가 floorUnit(버림 단위)보다 작으면 '0'을 반환
+ if (absoluteValue < floorUnit) {
+ return '0';
+ }
+
+ let formattedResult = '';
+ let remainingValue =
+ floorUnit > 1
+ ? Math.floor(absoluteValue / floorUnit) * floorUnit
+ : absoluteValue;
+
+ // unit 별로 나누기
+ for (let i = 0; i < units.length; i++) {
+ const { unit, value } = units[i];
+ const quotient = Math.floor(remainingValue / value);
+
+ if (quotient <= 0) continue;
+
+ formattedResult += `${getNumberWithConditionalCommas(
+ quotient,
+ commas
+ )}${unit}${space ? ' ' : ''}`;
+
+ remainingValue %= value;
+ }
+
+ // 남은 remainingValue가 있으면 추가
+ if (remainingValue > 0) {
+ formattedResult += `${getNumberWithConditionalCommas(
+ remainingValue.toFixed(decimal),
+ commas
+ )}`;
+ }
+
+ // 음수일 경우 앞에 '-' 붙이며, 앞/뒤 공백 제거
+ return (isNegative ? '-' : '') + formattedResult.trim();
+};
diff --git a/packages/utils/src/formatter/formatNumberWithUnits/index.ts b/packages/utils/src/formatter/formatNumberWithUnits/index.ts
new file mode 100644
index 000000000..2c9b82f7e
--- /dev/null
+++ b/packages/utils/src/formatter/formatNumberWithUnits/index.ts
@@ -0,0 +1,97 @@
+import { getFormattedValueWithUnits } from './formatNumberWithUnits.utils';
+import { isNumber } from '../../validator/isNumber';
+import { FormatNumberWithUnitsOptions } from './formatNumberWithUnits.types';
+
+/**
+ * @description `숫자` 혹은 `숫자로 이루어진 문자열`을 주어진 `단위` 별로 포맷팅하는 함수입니다.
+ *
+ * @param {number | string} value - 포맷팅할 숫자 또는 숫자로 이루어진 문자열
+ * @param {FormatNumberWithUnitsOptions} options - 포맷팅 옵션
+ * @param {Unit[]} [options.units=DEFAULT_UNITS] - 사용할 단위 배열(조,억,만). 직접 정의해서 사용할 수 있습니다.
+ * @param {boolean} [options.commas=true] - 천 단위 구분 쉼표 사용 여부입니다.
+ * @param {number} [options.floorUnit=1] - 버림 단위이며, 10의 제곱수를 단위로 사용합니다. 만약, 버림 단위보다 작은 숫자는 '0'으로 반환합니다.
+ * @param {boolean} [options.space=true] - 단위 사이 공백 추가 여부입니다.
+ * @param {number} [options.decimal=0] - 소수점 자리수입니다. 버림 단위가 1보다 클 경우 소수점 자리수를 적용하지 않습니다.
+ * @returns {string} 포맷팅된 문자열
+ * @throws 입력값이 올바른 형식이 아니거나 floorUnit, decimal이 올바른 형식이 아닐 경우 에러 발생
+ *
+ * @example
+ * // 기본 동작
+ * const KRW_UNITS = [
+ * { unit: '조', value: 1_000_000_000_000 },
+ * { unit: '억', value: 100_000_000 },
+ * { unit: '만', value: 10_000 },
+ * ] as const;
+ *
+ * formatNumberWithUnits(1234567) // "1,234,567"
+ * formatNumberWithUnits(1234567, { units: KRW_UNITS }) // "123만 4,567"
+ * formatNumberWithUnits('1234567', { units: KRW_UNITS }) // "123만 4,567"
+ *
+ * @example
+ * // 단위 사이 공백 추가 (기본값: true)
+ * formatNumberWithUnits(1234567, { units: KRW_UNITS, space: true }) // "123만 4,567"
+ * formatNumberWithUnits(1234567, { units: KRW_UNITS, space: false }) // "123만4,567"
+ *
+ * @example
+ * // 쉼표 사용 여부 (기본값: true)
+ * formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: false }) // "123만 4567"
+ * formatNumberWithUnits(1234567, { units: KRW_UNITS, commas: true }) // "123만 4,567"
+ *
+ * @example
+ * // 버림 단위 (기본값: 1)
+ * formatNumberWithUnits(1234567, { units: KRW_UNITS, floorUnit: 10000 }) // "123만"
+ *
+ * @example
+ * // 소수점 자리수 (기본값: 0)
+ * formatNumberWithUnits(1234567.123, { units: KRW_UNITS, decimal: 2 }) // "123만 4,567.12"
+ *
+ * // floorUnit이 1보다 크면 소수점 자리수를 적용하지 않습니다.
+ * formatNumberWithUnits(1234567.123, {
+ * units: KRW_UNITS,
+ * decimal: 3,
+ * floorUnit: 1000
+ * }) // "123만 4,000"
+ */
+export function formatNumberWithUnits(
+ value: number | string,
+ options: FormatNumberWithUnitsOptions = {}
+): string {
+ const {
+ units = [],
+ space = true,
+ commas = true,
+ floorUnit = 1,
+ decimal = 0,
+ } = options;
+
+ // value 값을 기준으로 내림차순 정렬
+ const sortedUnits = [...units].sort((a, b) => b.value - a.value);
+
+ const valueToUse = isNumber(value) ? value : Number(value);
+ // 에러 처리
+ if (isNaN(valueToUse)) {
+ throw new Error('value는 숫자 혹은 숫자로 이뤄진 문자열이여야 합니다.');
+ }
+
+ const isValidFloorUnit =
+ !Number.isInteger(floorUnit) ||
+ floorUnit < 1 ||
+ (floorUnit !== 1 && floorUnit % 10 !== 0);
+
+ if (isValidFloorUnit) {
+ throw new Error('floorUnit은 1을 포함한 10의 제곱수여야 합니다.');
+ }
+
+ if (!Number.isInteger(decimal) || decimal < 0) {
+ throw new Error('decimal은 0 이상의 정수여야 합니다.');
+ }
+
+ const formattedResult = getFormattedValueWithUnits(valueToUse, sortedUnits, {
+ commas,
+ space,
+ decimal,
+ floorUnit,
+ });
+
+ return formattedResult;
+}
diff --git a/packages/utils/src/formatter/index.ts b/packages/utils/src/formatter/index.ts
index d9c13d3b7..4c9192339 100644
--- a/packages/utils/src/formatter/index.ts
+++ b/packages/utils/src/formatter/index.ts
@@ -1,4 +1,4 @@
-export * from './formatNumberByUnits';
-export * from './formatNumberCurrency';
export * from './formatNumberWithCommas';
+export * from './formatNumberWithCurrency';
+export * from './formatNumberWithUnits';
export * from './formatPhoneNumber';
diff --git a/packages/utils/src/string/reverseString/reverseString.spec.ts b/packages/utils/src/string/reverseString/reverseString.spec.ts
index 20dad26f2..262c7648e 100644
--- a/packages/utils/src/string/reverseString/reverseString.spec.ts
+++ b/packages/utils/src/string/reverseString/reverseString.spec.ts
@@ -9,6 +9,13 @@ describe('reverseString', () => {
expect(reversedString).toBe('다나가CBA');
});
+ it('빈 문자열에 대해 빈 문자열을 반환해야 합니다.', () => {
+ const emptyString = '';
+ const reversedString = reverseString(emptyString);
+
+ expect(reversedString).toBe('');
+ });
+
it('특수 문자가 포함된 문자열에 대해 반전된 문자열을 반환해야 합니다.', () => {
const stringWithSpecialCharacter = 'A!B@C';
const reversedString = reverseString(stringWithSpecialCharacter);
diff --git a/packages/utils/vitest.config.ts b/packages/utils/vitest.config.ts
index c586873e0..85132775c 100644
--- a/packages/utils/vitest.config.ts
+++ b/packages/utils/vitest.config.ts
@@ -17,6 +17,7 @@ export default defineConfig({
'src/file',
'src/**/internal.ts',
'src/**/*.bench.ts',
+ 'src/**/*.utils.ts',
...coverageConfigDefaults.exclude,
],
},