diff --git a/packages/common/utils/package.json b/packages/common/utils/package.json new file mode 100644 index 0000000..72bd75d --- /dev/null +++ b/packages/common/utils/package.json @@ -0,0 +1,31 @@ +{ + "name": "@brgndy/utils", + "version": "1.0.0", + "sideEffects": false, + "type": "module", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "NODE_ENV=production rollup -c", + "build:dev": "rollup -c", + "dev": "rollup -c -w", + "test": "vitest --run" + }, + "author": { + "name": "brgndyy", + "url": "https://github.com/brgndyy" + }, + "license": "ISC", + "description": "", + "devDependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.6", + "rollup": "^4.13.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "typescript": "^5.6.2", + "vitest": "^2.1.1" + } +} diff --git a/packages/common/utils/rollup.config.js b/packages/common/utils/rollup.config.js new file mode 100644 index 0000000..5503ba2 --- /dev/null +++ b/packages/common/utils/rollup.config.js @@ -0,0 +1,32 @@ +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from '@rollup/plugin-typescript'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import terser from '@rollup/plugin-terser'; + +import packageJson from './package.json' assert { type: 'json' }; + +const isProduction = process.env.NODE_ENV === 'production'; + +export default { + input: 'src/index.ts', + output: [ + { + file: packageJson.main, + format: 'cjs', + sourcemap: !isProduction, + }, + { + file: packageJson.module, + format: 'esm', + sourcemap: !isProduction, + }, + ], + plugins: [ + peerDepsExternal(), + resolve(), + commonjs(), + typescript({ tsconfig: './tsconfig.json' }), + terser(), + ], +}; diff --git a/packages/common/utils/src/batchOfRequestOf.test.ts b/packages/common/utils/src/batchOfRequestOf.test.ts new file mode 100644 index 0000000..3a2711e --- /dev/null +++ b/packages/common/utils/src/batchOfRequestOf.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi } from 'vitest'; +import { batchRequestsOf } from './batchOfRequestOf'; + +describe('batchRequestsOf에 대한 테스트 코드 작성', () => { + it('동일한 인자를 넣었을때 동일한 프로미스가 반환되어야 한다.', async () => { + const mockFunc = vi.fn((a: number, b: number) => { + return new Promise((resolve) => { + setTimeout(() => resolve(a + b), 100); + }); + }); + + const batchedFunc = batchRequestsOf(mockFunc); + + const promise1 = batchedFunc(1, 2); + const promise2 = batchedFunc(1, 2); + + expect(promise1).toBe(promise2); + expect(mockFunc).toHaveBeenCalledTimes(1); + + const result = await promise1; + expect(result).toBe(3); + }); + + it('다른 인자를 넣었을때는 다른 프로미스가 반환 되어야 한다.', async () => { + const mockFunc = vi.fn((a: number, b: number) => { + return new Promise((resolve) => { + setTimeout(() => resolve(a + b), 100); + }); + }); + + const batchedFunc = batchRequestsOf(mockFunc); + + const promise1 = batchedFunc(1, 2); + const promise2 = batchedFunc(3, 4); + + expect(promise1).not.toBe(promise2); + expect(mockFunc).toHaveBeenCalledTimes(2); + + const result1 = await promise1; + const result2 = await promise2; + + expect(result1).toBe(3); + expect(result2).toBe(7); + }); + + it('프로미스가 resolve 된 후에는 새로운 프로미스가 반환 되어야 한다.', async () => { + const mockFunc = vi.fn((a: number, b: number) => { + return new Promise((resolve) => { + setTimeout(() => resolve(a + b), 100); + }); + }); + + const batchedFunc = batchRequestsOf(mockFunc); + + const promise1 = batchedFunc(1, 2); + await promise1; + + const promise2 = batchedFunc(1, 2); + expect(promise1).not.toBe(promise2); + expect(mockFunc).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/common/utils/src/batchOfRequestOf.ts b/packages/common/utils/src/batchOfRequestOf.ts new file mode 100644 index 0000000..3572ea0 --- /dev/null +++ b/packages/common/utils/src/batchOfRequestOf.ts @@ -0,0 +1,19 @@ +export function batchRequestsOf any>(func: F) { + const promiseByKey = new Map>>(); + + return function (...args: Parameters) { + const key = JSON.stringify(args); + + if (promiseByKey.has(key)) { + return promiseByKey.get(key)!; + } else { + const promise = func(...args); + promise.then(() => { + promiseByKey.delete(key); + }); + promiseByKey.set(key, promise); + + return promise; + } + } as F; +} diff --git a/packages/common/utils/src/index.ts b/packages/common/utils/src/index.ts new file mode 100644 index 0000000..535b611 --- /dev/null +++ b/packages/common/utils/src/index.ts @@ -0,0 +1 @@ +export * from './batchOfRequestOf'; diff --git a/packages/common/utils/tsconfig.json b/packages/common/utils/tsconfig.json new file mode 100644 index 0000000..ad664be --- /dev/null +++ b/packages/common/utils/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "target": "ESNext", + "module": "ESNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist", + "declaration": true, + "declarationDir": "types", + "sourceMap": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "emitDeclarationOnly": true, + "traceResolution": true, + "lib": ["ESNext", "DOM"] + }, + "include": ["src/**/*"], + "exclude": ["dist", "types"] +} diff --git a/packages/common/utils/vitest.config.ts b/packages/common/utils/vitest.config.ts new file mode 100644 index 0000000..8adfccd --- /dev/null +++ b/packages/common/utils/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + include: ['**/*.test.ts'], + environment: 'jsdom', + }, +}); diff --git a/packages/react/react/package.json b/packages/react/react/package.json index b26db7f..2918a2e 100644 --- a/packages/react/react/package.json +++ b/packages/react/react/package.json @@ -1,6 +1,6 @@ { "name": "@brgndy/react", - "version": "1.0.2", + "version": "1.0.3", "sideEffects": false, "main": "dist/index.js", "module": "dist/index.esm.js", diff --git a/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.md b/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.md new file mode 100644 index 0000000..f23660d --- /dev/null +++ b/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.md @@ -0,0 +1,19 @@ +# ScrollPreventer + +해당 컴포넌트로 감싼 컴포넌트가 렌더링시에 스크롤이 적용되지 않습니다. + +백드롭이 적용 된 모달 렌더링시에 사용할때 유용하게 적용됩니다. + +## - Example + +```tsx +import ScrollPreventer from '/@brgndy-react/ScrollPreventer'; + +export default function App() { + return ( + + + + ); +} +``` diff --git a/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.test.tsx b/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.test.tsx new file mode 100644 index 0000000..4879a8c --- /dev/null +++ b/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.test.tsx @@ -0,0 +1,60 @@ +import { render } from '@testing-library/react'; +import ScrollPreventer from './ScrollPreventer'; + +describe('ScrollPreventer 컴포넌트에 대한 테스트 코드 작성', () => { + beforeEach(() => { + document.body.style.overflow = ''; + document.body.style.height = ''; + document.documentElement.style.overflow = 'hidden'; + document.documentElement.style.height = '100%'; + window.scrollTo(0, 0); + }); + + const createLongContent = () => ( +
+
Very long content
+
+ ); + + it('만약 isOpen Prop의 상태값이 true라면 body의 overflow와 height에 스타일이 적용된다.', () => { + render( {createLongContent()}); + + expect(document.body.style.overflow).toBe('hidden'); + expect(document.body.style.height).toBe('100%'); + expect(document.documentElement.style.overflow).toBe('hidden'); + expect(document.documentElement.style.height).toBe('100%'); + }); + + it('isOpen이 false가 되면 body의 스타일은 초기화 된다.', () => { + render( {createLongContent()}); + + expect(document.body.style.overflow).toBe(''); + expect(document.body.style.height).toBe(''); + expect(document.documentElement.style.overflow).toBe(''); + expect(document.documentElement.style.height).toBe(''); + }); + + it('컴포넌트가 언마운트 되면 body 스타일이 초기화 된다.', () => { + const { unmount } = render( + {createLongContent()}, + ); + + expect(document.body.style.overflow).toBe('hidden'); + expect(document.body.style.height).toBe('100%'); + expect(document.documentElement.style.overflow).toBe('hidden'); + expect(document.documentElement.style.height).toBe('100%'); + + unmount(); + expect(document.body.style.overflow).toBe(''); + expect(document.body.style.height).toBe(''); + expect(document.documentElement.style.overflow).toBe(''); + expect(document.documentElement.style.height).toBe(''); + }); + + it('해당 컴포넌트로 감싼 컨텐츠는 스크롤이 적용되지 않는다.', () => { + render( {createLongContent()}); + + window.scrollTo(0, 100); + expect(window.scrollY).toBe(0); + }); +}); diff --git a/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.tsx b/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.tsx new file mode 100644 index 0000000..0e699e9 --- /dev/null +++ b/packages/react/react/src/components/ScrollPreventer/ScrollPreventer.tsx @@ -0,0 +1,31 @@ +import { useEffect } from 'react'; +import type { PropsWithChildren } from 'react'; + +interface PreventScrollObserverProps extends PropsWithChildren { + isOpen?: boolean; +} + +export default function ScrollPreventer({ isOpen = true, children }: PreventScrollObserverProps) { + useEffect(() => { + if (isOpen) { + document.documentElement.style.overflow = 'hidden'; + document.documentElement.style.height = '100%'; + document.body.style.overflow = 'hidden'; + document.body.style.height = '100%'; + } else { + document.documentElement.style.overflow = ''; + document.documentElement.style.height = ''; + document.body.style.overflow = ''; + document.body.style.height = ''; + } + + return () => { + document.documentElement.style.overflow = ''; + document.documentElement.style.height = ''; + document.body.style.overflow = ''; + document.body.style.height = ''; + }; + }, [isOpen]); + + return <>{children}; +} diff --git a/packages/react/react/src/components/ScrollPreventer/index.ts b/packages/react/react/src/components/ScrollPreventer/index.ts new file mode 100644 index 0000000..6530cc5 --- /dev/null +++ b/packages/react/react/src/components/ScrollPreventer/index.ts @@ -0,0 +1 @@ +export * from './ScrollPreventer'; diff --git a/packages/react/react/src/components/index.ts b/packages/react/react/src/components/index.ts new file mode 100644 index 0000000..6530cc5 --- /dev/null +++ b/packages/react/react/src/components/index.ts @@ -0,0 +1 @@ +export * from './ScrollPreventer'; diff --git a/packages/react/react/src/index.ts b/packages/react/react/src/index.ts index fd70c42..74766a7 100644 --- a/packages/react/react/src/index.ts +++ b/packages/react/react/src/index.ts @@ -1,2 +1,3 @@ export * from './hooks'; export * from './utils'; +export * from './components'; diff --git a/packages/react/react/tsconfig.json b/packages/react/react/tsconfig.json index 89ab638..35f908e 100644 --- a/packages/react/react/tsconfig.json +++ b/packages/react/react/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "jsx": "react", + "jsx": "react-jsx", "target": "ESNext", "module": "ESNext", "esModuleInterop": true, @@ -17,5 +17,5 @@ "emitDeclarationOnly": true, "traceResolution": true }, - "include": ["src/**/*"] + "include": ["src/**/*", "**/*.tsx"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d404cbb..fad4c8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,33 @@ importers: specifier: ^2.27.7 version: 2.27.7 + packages/common/utils: + devDependencies: + '@rollup/plugin-commonjs': + specifier: ^26.0.1 + version: 26.0.1(rollup@4.21.2) + '@rollup/plugin-node-resolve': + specifier: ^15.2.3 + version: 15.2.3(rollup@4.21.2) + '@rollup/plugin-terser': + specifier: ^0.4.4 + version: 0.4.4(rollup@4.21.2) + '@rollup/plugin-typescript': + specifier: ^11.1.6 + version: 11.1.6(rollup@4.21.2)(tslib@2.7.0)(typescript@5.6.2) + rollup: + specifier: ^4.13.0 + version: 4.21.2 + rollup-plugin-peer-deps-external: + specifier: ^2.2.4 + version: 2.2.4(rollup@4.21.2) + typescript: + specifier: ^5.6.2 + version: 5.6.2 + vitest: + specifier: ^2.1.1 + version: 2.1.1(@types/node@22.5.3)(jsdom@24.1.0)(terser@5.31.6) + packages/react-metronome: dependencies: react: @@ -269,6 +296,58 @@ importers: specifier: ^2.1.1 version: 2.1.1(@types/node@22.5.3)(jsdom@24.1.0)(terser@5.31.6) + packages/react/use-modal: + dependencies: + classnames: + specifier: ^2.5.1 + version: 2.5.1 + devDependencies: + '@rollup/plugin-commonjs': + specifier: ^26.0.1 + version: 26.0.1(rollup@4.21.2) + '@rollup/plugin-node-resolve': + specifier: ^15.2.3 + version: 15.2.3(rollup@4.21.2) + '@rollup/plugin-terser': + specifier: ^0.4.4 + version: 0.4.4(rollup@4.21.2) + '@rollup/plugin-typescript': + specifier: ^11.1.6 + version: 11.1.6(rollup@4.21.2)(tslib@2.7.0)(typescript@5.6.2) + '@testing-library/dom': + specifier: ^10.1.0 + version: 10.1.0 + '@testing-library/jest-dom': + specifier: ^6.5.0 + version: 6.5.0 + '@testing-library/react': + specifier: ^16.0.1 + version: 16.0.1(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.7)(react-dom@18.3.1(react@18.2.0))(react@18.2.0) + '@types/react': + specifier: ^18.3.7 + version: 18.3.7 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.2.0) + rollup: + specifier: ^4.13.0 + version: 4.21.2 + rollup-plugin-peer-deps-external: + specifier: ^2.2.4 + version: 2.2.4(rollup@4.21.2) + typescript: + specifier: ^5.6.2 + version: 5.6.2 + vitest: + specifier: ^2.1.1 + version: 2.1.1(@types/node@22.5.3)(jsdom@24.1.0)(terser@5.31.6) + packages: '@adobe/css-tools@4.4.0': @@ -2613,6 +2692,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -8818,6 +8900,8 @@ snapshots: dependencies: consola: 3.2.3 + classnames@2.5.1: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0