goodchuck-utils 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/form/__tests__/formatter.test.d.ts +2 -0
- package/dist/form/__tests__/formatter.test.d.ts.map +1 -0
- package/dist/form/__tests__/formatter.test.js +74 -0
- package/dist/form/__tests__/helpers.test.d.ts +2 -0
- package/dist/form/__tests__/helpers.test.d.ts.map +1 -0
- package/dist/form/__tests__/helpers.test.js +42 -0
- package/dist/form/__tests__/validation.test.d.ts +2 -0
- package/dist/form/__tests__/validation.test.d.ts.map +1 -0
- package/dist/form/__tests__/validation.test.js +67 -0
- package/dist/form/formatter.d.ts +34 -0
- package/dist/form/formatter.d.ts.map +1 -0
- package/dist/form/formatter.js +76 -0
- package/dist/form/helpers.d.ts +16 -0
- package/dist/form/helpers.d.ts.map +1 -0
- package/dist/form/helpers.js +34 -0
- package/dist/form/index.d.ts +9 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +11 -0
- package/dist/form/validation.d.ts +33 -0
- package/dist/form/validation.d.ts.map +1 -0
- package/dist/form/validation.js +56 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +12 -0
- package/dist/hooks/useClickOutside.d.ts +49 -0
- package/dist/hooks/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/useClickOutside.js +94 -0
- package/dist/hooks/useLocalStorage.d.ts +19 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.js +91 -0
- package/dist/hooks/useMediaQuery.d.ts +56 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +104 -0
- package/dist/hooks/useSessionStorage.d.ts +19 -0
- package/dist/hooks/useSessionStorage.d.ts.map +1 -0
- package/dist/hooks/useSessionStorage.js +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/string/__tests__/case.test.d.ts +2 -0
- package/dist/string/__tests__/case.test.d.ts.map +1 -0
- package/dist/string/__tests__/case.test.js +61 -0
- package/dist/string/__tests__/manipulation.test.d.ts +2 -0
- package/dist/string/__tests__/manipulation.test.d.ts.map +1 -0
- package/dist/string/__tests__/manipulation.test.js +109 -0
- package/dist/string/__tests__/validation.test.d.ts +2 -0
- package/dist/string/__tests__/validation.test.d.ts.map +1 -0
- package/dist/string/__tests__/validation.test.js +101 -0
- package/dist/string/case.d.ts +42 -0
- package/dist/string/case.d.ts.map +1 -0
- package/dist/string/case.js +71 -0
- package/dist/string/index.d.ts +9 -0
- package/dist/string/index.d.ts.map +1 -0
- package/dist/string/index.js +11 -0
- package/dist/string/manipulation.d.ts +61 -0
- package/dist/string/manipulation.d.ts.map +1 -0
- package/dist/string/manipulation.js +106 -0
- package/dist/string/validation.d.ts +79 -0
- package/dist/string/validation.d.ts.map +1 -0
- package/dist/string/validation.js +115 -0
- package/package.json +15 -1
- package/src/form/__tests__/formatter.test.ts +97 -0
- package/src/form/__tests__/helpers.test.ts +53 -0
- package/src/form/__tests__/validation.test.ts +84 -0
- package/src/form/formatter.ts +85 -0
- package/src/form/helpers.ts +44 -0
- package/src/form/index.ts +14 -0
- package/src/form/validation.ts +72 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useClickOutside.ts +114 -0
- package/src/hooks/useLocalStorage.ts +112 -0
- package/src/hooks/useMediaQuery.ts +116 -0
- package/src/hooks/useSessionStorage.ts +106 -0
- package/src/index.ts +9 -2
- package/src/string/__tests__/case.test.ts +78 -0
- package/src/string/__tests__/manipulation.test.ts +142 -0
- package/src/string/__tests__/validation.test.ts +128 -0
- package/src/string/case.ts +76 -0
- package/src/string/index.ts +14 -0
- package/src/string/manipulation.ts +124 -0
- package/src/string/validation.ts +126 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String Validation Utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 빈 문자열 또는 공백만 있는지 확인
|
|
6
|
+
* @example isEmpty('') // true
|
|
7
|
+
* @example isEmpty(' ') // true
|
|
8
|
+
* @example isEmpty('hello') // false
|
|
9
|
+
*/
|
|
10
|
+
export function isEmpty(str) {
|
|
11
|
+
return !str || str.trim().length === 0;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 문자열이 특정 문자로 시작하는지 확인
|
|
15
|
+
* @example startsWith('hello', 'he') // true
|
|
16
|
+
*/
|
|
17
|
+
export function startsWith(str, searchString) {
|
|
18
|
+
return str.startsWith(searchString);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 문자열이 특정 문자로 끝나는지 확인
|
|
22
|
+
* @example endsWith('hello', 'lo') // true
|
|
23
|
+
*/
|
|
24
|
+
export function endsWith(str, searchString) {
|
|
25
|
+
return str.endsWith(searchString);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 문자열에 특정 문자열이 포함되어 있는지 확인
|
|
29
|
+
* @example contains('hello world', 'world') // true
|
|
30
|
+
*/
|
|
31
|
+
export function contains(str, searchString) {
|
|
32
|
+
return str.includes(searchString);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 알파벳만 포함하는지 확인
|
|
36
|
+
* @example isAlpha('hello') // true
|
|
37
|
+
* @example isAlpha('hello123') // false
|
|
38
|
+
*/
|
|
39
|
+
export function isAlpha(str) {
|
|
40
|
+
return /^[a-zA-Z]+$/.test(str);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 숫자만 포함하는지 확인
|
|
44
|
+
* @example isNumeric('123') // true
|
|
45
|
+
* @example isNumeric('123.45') // false
|
|
46
|
+
*/
|
|
47
|
+
export function isNumeric(str) {
|
|
48
|
+
return /^[0-9]+$/.test(str);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 알파벳과 숫자만 포함하는지 확인
|
|
52
|
+
* @example isAlphanumeric('hello123') // true
|
|
53
|
+
* @example isAlphanumeric('hello_123') // false
|
|
54
|
+
*/
|
|
55
|
+
export function isAlphanumeric(str) {
|
|
56
|
+
return /^[a-zA-Z0-9]+$/.test(str);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 소문자만 포함하는지 확인
|
|
60
|
+
* @example isLowerCase('hello') // true
|
|
61
|
+
* @example isLowerCase('Hello') // false
|
|
62
|
+
*/
|
|
63
|
+
export function isLowerCase(str) {
|
|
64
|
+
return str === str.toLowerCase() && str !== str.toUpperCase();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 대문자만 포함하는지 확인
|
|
68
|
+
* @example isUpperCase('HELLO') // true
|
|
69
|
+
* @example isUpperCase('Hello') // false
|
|
70
|
+
*/
|
|
71
|
+
export function isUpperCase(str) {
|
|
72
|
+
return str === str.toUpperCase() && str !== str.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* JSON 형식인지 확인
|
|
76
|
+
* @example isJSON('{"name":"John"}') // true
|
|
77
|
+
* @example isJSON('not json') // false
|
|
78
|
+
*/
|
|
79
|
+
export function isJSON(str) {
|
|
80
|
+
try {
|
|
81
|
+
JSON.parse(str);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 16진수 색상 코드인지 확인
|
|
90
|
+
* @example isHexColor('#fff') // true
|
|
91
|
+
* @example isHexColor('#ffffff') // true
|
|
92
|
+
* @example isHexColor('fff') // false
|
|
93
|
+
*/
|
|
94
|
+
export function isHexColor(str) {
|
|
95
|
+
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(str);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* UUID 형식인지 확인
|
|
99
|
+
* @example isUUID('123e4567-e89b-12d3-a456-426614174000') // true
|
|
100
|
+
*/
|
|
101
|
+
export function isUUID(str) {
|
|
102
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Base64 인코딩된 문자열인지 확인
|
|
106
|
+
* @example isBase64('SGVsbG8gV29ybGQ=') // true
|
|
107
|
+
*/
|
|
108
|
+
export function isBase64(str) {
|
|
109
|
+
try {
|
|
110
|
+
return btoa(atob(str)) === str;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goodchuck-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"import": "./dist/index.js",
|
|
20
20
|
"require": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./hooks": {
|
|
23
|
+
"types": "./dist/hooks/index.d.ts",
|
|
24
|
+
"import": "./dist/hooks/index.js",
|
|
25
|
+
"require": "./src/hooks/index.js"
|
|
21
26
|
}
|
|
22
27
|
},
|
|
23
28
|
"keywords": [],
|
|
@@ -33,7 +38,16 @@
|
|
|
33
38
|
"src/**/*.ts"
|
|
34
39
|
],
|
|
35
40
|
"devDependencies": {
|
|
41
|
+
"@types/react": "^19.2.8",
|
|
36
42
|
"@vitest/ui": "^4.0.16",
|
|
37
43
|
"vitest": "^4.0.16"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=16.8.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
formatPhoneNumber,
|
|
4
|
+
formatBusinessNumber,
|
|
5
|
+
formatCreditCard,
|
|
6
|
+
extractNumbers,
|
|
7
|
+
formatCurrency,
|
|
8
|
+
maskResidentNumber,
|
|
9
|
+
maskCreditCard,
|
|
10
|
+
} from '../formatter';
|
|
11
|
+
|
|
12
|
+
describe('Form Formatters', () => {
|
|
13
|
+
describe('formatPhoneNumber', () => {
|
|
14
|
+
it('should format phone numbers correctly', () => {
|
|
15
|
+
expect(formatPhoneNumber('01012345678')).toBe('010-1234-5678');
|
|
16
|
+
expect(formatPhoneNumber('0212345678')).toBe('021-2345-678');
|
|
17
|
+
expect(formatPhoneNumber('010')).toBe('010');
|
|
18
|
+
expect(formatPhoneNumber('0101234')).toBe('010-1234');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should handle already formatted numbers', () => {
|
|
22
|
+
expect(formatPhoneNumber('010-1234-5678')).toBe('010-1234-5678');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should limit to 11 digits', () => {
|
|
26
|
+
expect(formatPhoneNumber('010123456789999')).toBe('010-1234-5678');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('formatBusinessNumber', () => {
|
|
31
|
+
it('should format business numbers correctly', () => {
|
|
32
|
+
expect(formatBusinessNumber('1234567890')).toBe('123-45-67890');
|
|
33
|
+
expect(formatBusinessNumber('123')).toBe('123');
|
|
34
|
+
expect(formatBusinessNumber('12345')).toBe('123-45');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should limit to 10 digits', () => {
|
|
38
|
+
expect(formatBusinessNumber('12345678901234')).toBe('123-45-67890');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('formatCreditCard', () => {
|
|
43
|
+
it('should format credit card numbers', () => {
|
|
44
|
+
expect(formatCreditCard('1234567890123456')).toBe('1234-5678-9012-3456');
|
|
45
|
+
expect(formatCreditCard('1234')).toBe('1234');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('extractNumbers', () => {
|
|
50
|
+
it('should extract only numbers', () => {
|
|
51
|
+
expect(extractNumbers('abc123def456')).toBe('123456');
|
|
52
|
+
expect(extractNumbers('010-1234-5678')).toBe('01012345678');
|
|
53
|
+
expect(extractNumbers('no numbers')).toBe('');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('formatCurrency', () => {
|
|
58
|
+
it('should format numbers with commas', () => {
|
|
59
|
+
expect(formatCurrency(1234567)).toBe('1,234,567원');
|
|
60
|
+
expect(formatCurrency(1000)).toBe('1,000원');
|
|
61
|
+
expect(formatCurrency(0)).toBe('0원');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle string input', () => {
|
|
65
|
+
expect(formatCurrency('1234567')).toBe('1,234,567원');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should allow custom currency', () => {
|
|
69
|
+
expect(formatCurrency(1000, '달러')).toBe('1,000달러');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle invalid input', () => {
|
|
73
|
+
expect(formatCurrency('invalid')).toBe('0원');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('maskResidentNumber', () => {
|
|
78
|
+
it('should mask resident registration number', () => {
|
|
79
|
+
expect(maskResidentNumber('1234561234567')).toBe('123456-*******');
|
|
80
|
+
expect(maskResidentNumber('123456-1234567')).toBe('123456-*******');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle partial input', () => {
|
|
84
|
+
expect(maskResidentNumber('12345')).toBe('12345');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('maskCreditCard', () => {
|
|
89
|
+
it('should mask credit card number', () => {
|
|
90
|
+
expect(maskCreditCard('1234567890123456')).toBe('1234-****-****-3456');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle partial input', () => {
|
|
94
|
+
expect(maskCreditCard('1234567')).toBe('1234567');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
hasFormErrors,
|
|
4
|
+
getChangedFields,
|
|
5
|
+
removeEmptyValues,
|
|
6
|
+
} from '../helpers';
|
|
7
|
+
|
|
8
|
+
describe('Form State Helpers', () => {
|
|
9
|
+
describe('hasFormErrors', () => {
|
|
10
|
+
it('should detect errors', () => {
|
|
11
|
+
expect(hasFormErrors({ email: 'error' })).toBe(true);
|
|
12
|
+
expect(hasFormErrors({})).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('getChangedFields', () => {
|
|
17
|
+
it('should return only changed fields', () => {
|
|
18
|
+
const original = { name: 'John', email: 'john@example.com', age: 30 };
|
|
19
|
+
const current = { name: 'Jane', email: 'john@example.com', age: 31 };
|
|
20
|
+
const changed = getChangedFields(original, current);
|
|
21
|
+
|
|
22
|
+
expect(changed).toEqual({ name: 'Jane', age: 31 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return empty object when nothing changed', () => {
|
|
26
|
+
const original = { name: 'John' };
|
|
27
|
+
const current = { name: 'John' };
|
|
28
|
+
expect(getChangedFields(original, current)).toEqual({});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('removeEmptyValues', () => {
|
|
33
|
+
it('should remove null, undefined, and empty strings', () => {
|
|
34
|
+
const input = {
|
|
35
|
+
name: 'John',
|
|
36
|
+
email: '',
|
|
37
|
+
age: null,
|
|
38
|
+
phone: undefined,
|
|
39
|
+
address: 'Seoul',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
expect(removeEmptyValues(input)).toEqual({
|
|
43
|
+
name: 'John',
|
|
44
|
+
address: 'Seoul',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should keep zero values', () => {
|
|
49
|
+
const input = { count: 0, active: false };
|
|
50
|
+
expect(removeEmptyValues(input)).toEqual({ count: 0, active: false });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isEmail,
|
|
4
|
+
isPhoneNumber,
|
|
5
|
+
isUrl,
|
|
6
|
+
isStrongPassword,
|
|
7
|
+
isBusinessNumber,
|
|
8
|
+
} from '../validation';
|
|
9
|
+
|
|
10
|
+
describe('Form Validation', () => {
|
|
11
|
+
describe('isEmail', () => {
|
|
12
|
+
it('should validate correct email formats', () => {
|
|
13
|
+
expect(isEmail('test@example.com')).toBe(true);
|
|
14
|
+
expect(isEmail('user.name@domain.co.kr')).toBe(true);
|
|
15
|
+
expect(isEmail('test+tag@example.com')).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should reject invalid email formats', () => {
|
|
19
|
+
expect(isEmail('invalid')).toBe(false);
|
|
20
|
+
expect(isEmail('test@')).toBe(false);
|
|
21
|
+
expect(isEmail('@example.com')).toBe(false);
|
|
22
|
+
expect(isEmail('test @example.com')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('isPhoneNumber', () => {
|
|
27
|
+
it('should validate Korean phone numbers', () => {
|
|
28
|
+
expect(isPhoneNumber('010-1234-5678')).toBe(true);
|
|
29
|
+
expect(isPhoneNumber('01012345678')).toBe(true);
|
|
30
|
+
expect(isPhoneNumber('02-1234-5678')).toBe(true);
|
|
31
|
+
expect(isPhoneNumber('031-123-4567')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should reject invalid phone numbers', () => {
|
|
35
|
+
expect(isPhoneNumber('123-456-7890')).toBe(false);
|
|
36
|
+
expect(isPhoneNumber('010-12-3456')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('isUrl', () => {
|
|
41
|
+
it('should validate URLs', () => {
|
|
42
|
+
expect(isUrl('https://example.com')).toBe(true);
|
|
43
|
+
expect(isUrl('http://test.co.kr')).toBe(true);
|
|
44
|
+
expect(isUrl('https://example.com/path?query=1')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should reject invalid URLs', () => {
|
|
48
|
+
expect(isUrl('not-a-url')).toBe(false);
|
|
49
|
+
expect(isUrl('example.com')).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('isStrongPassword', () => {
|
|
54
|
+
it('should validate strong passwords', () => {
|
|
55
|
+
expect(isStrongPassword('Password123!')).toBe(true);
|
|
56
|
+
expect(isStrongPassword('Str0ng#Pass')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should reject weak passwords', () => {
|
|
60
|
+
expect(isStrongPassword('short')).toBe(false);
|
|
61
|
+
expect(isStrongPassword('NoNumbers!')).toBe(false);
|
|
62
|
+
expect(isStrongPassword('nouppercas3!')).toBe(false);
|
|
63
|
+
expect(isStrongPassword('NOLOWERCASE1!')).toBe(false);
|
|
64
|
+
expect(isStrongPassword('NoSpecialChar1')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should respect custom options', () => {
|
|
68
|
+
expect(isStrongPassword('simple', { minLength: 6, requireUppercase: false, requireNumbers: false, requireSpecialChars: false })).toBe(true);
|
|
69
|
+
expect(isStrongPassword('PASSWORD', { requireLowercase: false, requireNumbers: false, requireSpecialChars: false })).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('isBusinessNumber', () => {
|
|
74
|
+
it('should validate 10-digit business numbers', () => {
|
|
75
|
+
expect(isBusinessNumber('1234567890')).toBe(true);
|
|
76
|
+
expect(isBusinessNumber('123-45-67890')).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should reject invalid business numbers', () => {
|
|
80
|
+
expect(isBusinessNumber('123456789')).toBe(false);
|
|
81
|
+
expect(isBusinessNumber('12345678901')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Formatter Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 전화번호 포맷팅 (010-1234-5678)
|
|
7
|
+
*/
|
|
8
|
+
export function formatPhoneNumber(value: string): string {
|
|
9
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
10
|
+
|
|
11
|
+
if (cleaned.length <= 3) return cleaned;
|
|
12
|
+
if (cleaned.length <= 7) {
|
|
13
|
+
return `${cleaned.slice(0, 3)}-${cleaned.slice(3)}`;
|
|
14
|
+
}
|
|
15
|
+
if (cleaned.length <= 11) {
|
|
16
|
+
return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 7)}-${cleaned.slice(7)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 7)}-${cleaned.slice(7, 11)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 사업자등록번호 포맷팅 (123-45-67890)
|
|
24
|
+
*/
|
|
25
|
+
export function formatBusinessNumber(value: string): string {
|
|
26
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
27
|
+
|
|
28
|
+
if (cleaned.length <= 3) return cleaned;
|
|
29
|
+
if (cleaned.length <= 5) {
|
|
30
|
+
return `${cleaned.slice(0, 3)}-${cleaned.slice(3)}`;
|
|
31
|
+
}
|
|
32
|
+
if (cleaned.length <= 10) {
|
|
33
|
+
return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 5)}-${cleaned.slice(5)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 5)}-${cleaned.slice(5, 10)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 신용카드 번호 포맷팅 (1234-5678-9012-3456)
|
|
41
|
+
*/
|
|
42
|
+
export function formatCreditCard(value: string): string {
|
|
43
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
44
|
+
const groups = cleaned.match(/.{1,4}/g);
|
|
45
|
+
return groups ? groups.join('-') : cleaned;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 숫자만 추출
|
|
50
|
+
*/
|
|
51
|
+
export function extractNumbers(value: string): string {
|
|
52
|
+
return value.replace(/[^0-9]/g, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 통화 포맷팅 (천 단위 콤마)
|
|
57
|
+
* @param value - 숫자 값
|
|
58
|
+
* @param currency - 통화 기호 (기본값: '원')
|
|
59
|
+
*/
|
|
60
|
+
export function formatCurrency(value: number | string, currency: string = '원'): string {
|
|
61
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
62
|
+
if (isNaN(num)) return '0' + currency;
|
|
63
|
+
|
|
64
|
+
return num.toLocaleString('ko-KR') + currency;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 주민등록번호 앞자리만 표시 (123456-*******)
|
|
69
|
+
*/
|
|
70
|
+
export function maskResidentNumber(value: string): string {
|
|
71
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
72
|
+
if (cleaned.length < 6) return cleaned;
|
|
73
|
+
|
|
74
|
+
return `${cleaned.slice(0, 6)}-${'*'.repeat(7)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 카드번호 마스킹 (1234-****-****-5678)
|
|
79
|
+
*/
|
|
80
|
+
export function maskCreditCard(value: string): string {
|
|
81
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
82
|
+
if (cleaned.length < 12) return value;
|
|
83
|
+
|
|
84
|
+
return `${cleaned.slice(0, 4)}-****-****-${cleaned.slice(-4)}`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form State Helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 폼 에러가 있는지 확인
|
|
7
|
+
*/
|
|
8
|
+
export function hasFormErrors(errors: Record<string, any>): boolean {
|
|
9
|
+
return Object.keys(errors).length > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 변경된 필드만 추출
|
|
14
|
+
*/
|
|
15
|
+
export function getChangedFields<T extends Record<string, any>>(
|
|
16
|
+
original: T,
|
|
17
|
+
current: T
|
|
18
|
+
): Partial<T> {
|
|
19
|
+
const changed: Partial<T> = {};
|
|
20
|
+
|
|
21
|
+
for (const key in current) {
|
|
22
|
+
if (current[key] !== original[key]) {
|
|
23
|
+
changed[key] = current[key];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return changed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 빈 값 제거 (null, undefined, 빈 문자열)
|
|
32
|
+
*/
|
|
33
|
+
export function removeEmptyValues<T extends Record<string, any>>(obj: T): Partial<T> {
|
|
34
|
+
const result: Partial<T> = {};
|
|
35
|
+
|
|
36
|
+
for (const key in obj) {
|
|
37
|
+
const value = obj[key];
|
|
38
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
39
|
+
result[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for form validation, formatting, and state management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Validation utilities
|
|
8
|
+
export * from './validation';
|
|
9
|
+
|
|
10
|
+
// Formatter utilities
|
|
11
|
+
export * from './formatter';
|
|
12
|
+
|
|
13
|
+
// Helper utilities
|
|
14
|
+
export * from './helpers';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Validation Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 이메일 형식 검증
|
|
7
|
+
*/
|
|
8
|
+
export function isEmail(value: string): boolean {
|
|
9
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10
|
+
return emailRegex.test(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 한국 전화번호 형식 검증
|
|
15
|
+
* 010-1234-5678, 01012345678, 02-1234-5678 등 허용
|
|
16
|
+
*/
|
|
17
|
+
export function isPhoneNumber(value: string): boolean {
|
|
18
|
+
const phoneRegex = /^(01[016789]|02|0[3-9]{1}[0-9]{1})-?[0-9]{3,4}-?[0-9]{4}$/;
|
|
19
|
+
return phoneRegex.test(value.replace(/\s/g, ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* URL 형식 검증
|
|
24
|
+
*/
|
|
25
|
+
export function isUrl(value: string): boolean {
|
|
26
|
+
try {
|
|
27
|
+
new URL(value);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 비밀번호 강도 검증
|
|
36
|
+
* @param value - 검증할 비밀번호
|
|
37
|
+
* @param options - 검증 옵션
|
|
38
|
+
*/
|
|
39
|
+
export function isStrongPassword(
|
|
40
|
+
value: string,
|
|
41
|
+
options: {
|
|
42
|
+
minLength?: number;
|
|
43
|
+
requireUppercase?: boolean;
|
|
44
|
+
requireLowercase?: boolean;
|
|
45
|
+
requireNumbers?: boolean;
|
|
46
|
+
requireSpecialChars?: boolean;
|
|
47
|
+
} = {}
|
|
48
|
+
): boolean {
|
|
49
|
+
const {
|
|
50
|
+
minLength = 8,
|
|
51
|
+
requireUppercase = true,
|
|
52
|
+
requireLowercase = true,
|
|
53
|
+
requireNumbers = true,
|
|
54
|
+
requireSpecialChars = true,
|
|
55
|
+
} = options;
|
|
56
|
+
|
|
57
|
+
if (value.length < minLength) return false;
|
|
58
|
+
if (requireUppercase && !/[A-Z]/.test(value)) return false;
|
|
59
|
+
if (requireLowercase && !/[a-z]/.test(value)) return false;
|
|
60
|
+
if (requireNumbers && !/[0-9]/.test(value)) return false;
|
|
61
|
+
if (requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(value)) return false;
|
|
62
|
+
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 한국 사업자등록번호 형식 검증 (10자리)
|
|
68
|
+
*/
|
|
69
|
+
export function isBusinessNumber(value: string): boolean {
|
|
70
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
71
|
+
return cleaned.length === 10;
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Hooks
|
|
3
|
+
*
|
|
4
|
+
* This module provides React hooks for common use cases.
|
|
5
|
+
* Note: Requires React as a peer dependency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Storage hooks
|
|
9
|
+
export * from './useLocalStorage';
|
|
10
|
+
export * from './useSessionStorage';
|
|
11
|
+
|
|
12
|
+
// UI/UX hooks
|
|
13
|
+
export * from './useMediaQuery';
|
|
14
|
+
export * from './useClickOutside';
|