├── .gitignore ├── LICENSE ├── README.md ├── babel.config.json ├── dist ├── mission-utils.cjs └── mission-utils.js ├── package-lock.json ├── package.json ├── src ├── console.js ├── index.js └── random.js ├── test ├── console.test.js └── random.test.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 woowacourse-projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mission-utils 2 | 3 | ![npm](https://img.shields.io/npm/v/@woowacourse/mission-utils) 4 | [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 5 | 6 | ## Install 7 | 8 | ### with CDN 9 | 10 | 1. 스크립트 삽입하기 11 | 12 | ```html 13 | 14 | 15 | ``` 16 | 17 | - `index.html`에 해당 스크립트 태그를 삽입해주세요. 18 | - 유틸 라이브러리의 경우 어플리케이션 스크립트 이전에 작성해야 합니다. 19 | 20 | 2. 유틸 사용하기 21 | 22 | ```js 23 | // example 24 | console.log(MissionUtils.Random.pickNumberInList([1, 2, 3])); 25 | ``` 26 | 27 | - 스크립트 태그로 삽입된 경우 전역에 할당되어 `MissionUtils.[util]` 형태로 사용할 수 있습니다. 28 | 29 | ### with npm 30 | 31 | 1. 모듈 다운로드 32 | 33 | ```sh 34 | npm i @woowacourse/mission-utils 35 | ``` 36 | 37 | 2. 모듈 사용하기 38 | 39 | #### ES Modules 방식으로 사용하는 경우 40 | 41 | ```js 42 | import { MissionUtils } from "@woowacourse/mission-utils"; 43 | 44 | console.log(MissionUtils.Random.pickNumberInList([1, 2, 3])); 45 | ``` 46 | 47 | #### CommonJS 방식으로 사용하는 경우 48 | 49 | ```js 50 | const MissionUtils = require("@woowacourse/mission-utils"); 51 | 52 | console.log(MissionUtils.Random.pickNumberInList([1, 2, 3])); 53 | ``` 54 | 55 | ## Features 56 | 57 | ### Console 58 | 59 | #### `readLine(query, callback)` 60 | 61 | 주어진 질문을 화면에 출력하고, 사용자가 답변을 입력할 때까지 기다린 다음 입력된 답변을 인수로 전달하는 콜백 함수를 호출한다. 62 | 63 | ```js 64 | Console.readLine('닉네임을 입력해주세요.', (answer) => { 65 | console.log(`닉네임: ${answer}`); 66 | }); 67 | ``` 68 | 69 | #### `readLineAsync(query)` 70 | 71 | 주어진 질문을 화면에 출력하고, 사용자가 입력한 답변을 Promise를 통해 반환한다. 72 | 73 | ```js 74 | async function getUsername() { 75 | try { 76 | const username = await Console.readLineAsync('닉네임을 입력해주세요.'); 77 | } catch (error) { 78 | // reject 되는 경우 79 | } 80 | } 81 | ``` 82 | 83 | #### `print(message)` 84 | 85 | 주어진 문자열을 콘솔에 출력한다. 86 | 87 | ```js 88 | Console.print('안녕하세요.'); 89 | ``` 90 | 91 | ### Random 92 | 93 | #### `pickNumberInRange(startInclusive, endInclusive)` 94 | 95 | 숫자 범위를 지정하면 시작 또는 끝 숫자를 포함하여 범위의 숫자를 반환한다. 96 | 97 | ```js 98 | Random.pickNumberInRange(1, 10); // 1 99 | Random.pickNumberInRange(1, 10); // 10 100 | Random.pickNumberInRange(1, 10); // 4 101 | Random.pickNumberInRange(1, 10); // 5 102 | ``` 103 | 104 | #### `pickNumberInList(array)` 105 | 106 | 목록에 있는 숫자 중 하나를 반환한다. 107 | 108 | ```js 109 | Random.pickNumberInList([1, 3, 10]); // 1 110 | Random.pickNumberInList([1, 3, 10]); // 10 111 | Random.pickNumberInList([1, 3, 10]); // 3 112 | ``` 113 | 114 | #### `pickUniqueNumbersInRange(startInclusive, endInclusive, count)` 115 | 116 | 숫자 범위 내에서 지정된 개수만큼 겹치지 않는 숫자를 반환한다. 117 | 118 | ```js 119 | Random.pickUniqueNumbersInRange(1, 10, 2); // [1, 2] 120 | Random.pickUniqueNumbersInRange(1, 10, 5); // [1, 10, 7, 8, 5] 121 | ``` 122 | 123 | #### `shuffle(array)` 124 | 125 | 무작위로 섞인 새 목록을 반환한다. 126 | 127 | ```js 128 | Random.shuffle([1, 2, 3, 4, 5]); // [2, 4, 1, 3, 5] 129 | ``` 130 | 131 | ## Contributors 132 | 133 | [2SOOY](https://github.com/2SOOY) 134 | [zereight](https://github.com/zereight) 135 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /dist/mission-utils.cjs: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var r,e={d:(r,t)=>{for(var n in t)e.o(t,n)&&!e.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:t[n]})},o:(r,e)=>Object.prototype.hasOwnProperty.call(r,e),r:r=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(r,"__esModule",{value:!0})}},t={};function n(r){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(r){return typeof r}:function(r){return r&&"function"==typeof Symbol&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},n(r)}function o(r,e){for(var t=0;tp,MissionUtils:()=>v,Random:()=>s});var u=function(){function r(){!function(r,e){if(!(r instanceof e))throw new TypeError("Cannot call a class as a function")}(this,r)}var e,t;return e=r,t=[{key:"pickNumberInRange",value:function(e,t){return i(r,r,c).call(r,e,t),e=Math.ceil(e),Math.floor(Math.random()*(t+1-e))+e}},{key:"pickNumberInList",value:function(e){return i(r,r,l).call(r,e),e[r.pickNumberInRange(0,e.length-1)]}},{key:"pickUniqueNumbersInRange",value:function(e,t,n){i(r,r,f).call(r,e,t,n);for(var o=[],u=e;u<=t;u++)o.push(u);return r.shuffle(o).slice(0,n)}},{key:"shuffle",value:function(e){return i(r,r,l).call(r,e),e.sort((function(){return Math.random()-.5}))}}],null&&o(e.prototype,null),t&&o(e,t),Object.defineProperty(e,"prototype",{writable:!1}),r}();function a(r){return"number"==typeof r}function c(e,t){if(!i(r,r,a).call(r,e)||!i(r,r,a).call(r,t))throw new Error("arguments must be numbers.");if(eNumber.MAX_SAFE_INTEGER)throw new Error("endInclusive cannot be greater than Number.MAX_SAFE_INTEGER.");if(e>t)throw new Error("startInclusive ".concat(e," cannot be greater than endInclusive ").concat(t,"."));if(t-e>=Number.MAX_VALUE)throw new Error("the input range is too large.")}function l(e){if(!Array.isArray(e))throw new Error("the argument must be an array.");if(!e.every((function(e){return i(r,r,a).call(r,e)})))throw new Error("array elements must be numbers.");if(0===e.length)throw new Error("argument array cannot be empty.")}function f(e,t,n){if(!i(r,r,a).call(r,e)||!i(r,r,a).call(r,t)||!i(r,r,a).call(r,n))throw new Error("arguments must be numbers.");if(n<0)throw new Error("count cannot be less than zero.");if(t-e+1{for(var n in e)t.o(e,n)&&!t.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:e[n]})},o:(r,e)=>Object.prototype.hasOwnProperty.call(r,e)},n={};function o(r){return o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(r){return typeof r}:function(r){return r&&"function"==typeof Symbol&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},o(r)}function i(r,e){for(var t=0;tv,HN:()=>w,kk:()=>m});var a=function(){function r(){!function(r,e){if(!(r instanceof e))throw new TypeError("Cannot call a class as a function")}(this,r)}var e,t;return e=r,t=[{key:"pickNumberInRange",value:function(e,t){return u(r,r,l).call(r,e,t),e=Math.ceil(e),Math.floor(Math.random()*(t+1-e))+e}},{key:"pickNumberInList",value:function(e){return u(r,r,f).call(r,e),e[r.pickNumberInRange(0,e.length-1)]}},{key:"pickUniqueNumbersInRange",value:function(e,t,n){u(r,r,s).call(r,e,t,n);for(var o=[],i=e;i<=t;i++)o.push(i);return r.shuffle(o).slice(0,n)}},{key:"shuffle",value:function(e){return u(r,r,f).call(r,e),e.sort((function(){return Math.random()-.5}))}}],null&&i(e.prototype,null),t&&i(e,t),Object.defineProperty(e,"prototype",{writable:!1}),r}();function c(r){return"number"==typeof r}function l(r,t){if(!u(e,e,c).call(e,r)||!u(e,e,c).call(e,t))throw new Error("arguments must be numbers.");if(rNumber.MAX_SAFE_INTEGER)throw new Error("endInclusive cannot be greater than Number.MAX_SAFE_INTEGER.");if(r>t)throw new Error("startInclusive ".concat(r," cannot be greater than endInclusive ").concat(t,"."));if(t-r>=Number.MAX_VALUE)throw new Error("the input range is too large.")}function f(r){if(!Array.isArray(r))throw new Error("the argument must be an array.");if(!r.every((function(r){return u(e,e,c).call(e,r)})))throw new Error("array elements must be numbers.");if(0===r.length)throw new Error("argument array cannot be empty.")}function s(r,t,n){if(!u(e,e,c).call(e,r)||!u(e,e,c).call(e,t)||!u(e,e,c).call(e,n))throw new Error("arguments must be numbers.");if(n<0)throw new Error("count cannot be less than zero.");if(t-r+1=9.6.7", 40 | "node": ">=18.17.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/console.js: -------------------------------------------------------------------------------- 1 | import readline from "readline"; 2 | 3 | class Console { 4 | constructor() {} 5 | 6 | static readLine(query, callback) { 7 | if (arguments.length !== 2) { 8 | throw new Error("arguments must be 2."); 9 | } 10 | 11 | if (typeof query !== "string") { 12 | throw new Error("query must be string"); 13 | } 14 | 15 | if (typeof callback !== "function") { 16 | throw new Error("callback must be function"); 17 | } 18 | 19 | if (callback.length !== 1) { 20 | throw new Error("callback must have 1 argument"); 21 | } 22 | 23 | const rl = readline.createInterface({ 24 | input: process.stdin, 25 | output: process.stdout, 26 | }); 27 | 28 | rl.question(query, callback); 29 | } 30 | 31 | static readLineAsync(query) { 32 | return new Promise((resolve, reject) => { 33 | if (arguments.length !== 1) { 34 | reject(new Error("arguments must be 1")); 35 | } 36 | 37 | if (typeof query !== "string") { 38 | reject(new Error("query must be string")); 39 | } 40 | 41 | const rl = readline.createInterface({ 42 | input: process.stdin, 43 | output: process.stdout, 44 | }); 45 | 46 | rl.question(query, (input) => { 47 | rl.close(); 48 | resolve(input); 49 | }); 50 | }); 51 | } 52 | 53 | static print(message) { 54 | console.log(message); 55 | } 56 | } 57 | 58 | export default Console; 59 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Random from "./random.js"; 2 | import Console from "./console.js"; 3 | 4 | export { Random, Console }; 5 | 6 | export const MissionUtils = { 7 | Random, 8 | Console, 9 | }; 10 | -------------------------------------------------------------------------------- /src/random.js: -------------------------------------------------------------------------------- 1 | class Random { 2 | constructor() {} 3 | 4 | static pickNumberInRange(startInclusive, endInclusive) { 5 | Random.#validateRange(startInclusive, endInclusive); 6 | 7 | startInclusive = Math.ceil(startInclusive); 8 | 9 | return ( 10 | Math.floor(Math.random() * (endInclusive + 1 - startInclusive)) + 11 | startInclusive 12 | ); 13 | } 14 | 15 | static #isNumber(value) { 16 | return typeof value === "number"; 17 | } 18 | 19 | static #validateRange(startInclusive, endInclusive) { 20 | if (!Random.#isNumber(startInclusive) || !Random.#isNumber(endInclusive)) { 21 | throw new Error("arguments must be numbers."); 22 | } 23 | 24 | if (startInclusive < Number.MIN_SAFE_INTEGER) { 25 | throw new Error( 26 | "startInclusive cannot be less than Number.MIN_SAFE_INTEGER" 27 | ); 28 | } 29 | 30 | if (endInclusive > Number.MAX_SAFE_INTEGER) { 31 | throw new Error( 32 | "endInclusive cannot be greater than Number.MAX_SAFE_INTEGER." 33 | ); 34 | } 35 | 36 | if (startInclusive > endInclusive) { 37 | throw new Error( 38 | `startInclusive ${startInclusive} cannot be greater than endInclusive ${endInclusive}.` 39 | ); 40 | } 41 | 42 | if (endInclusive - startInclusive >= Number.MAX_VALUE) { 43 | throw new Error("the input range is too large."); 44 | } 45 | } 46 | 47 | static pickNumberInList(array) { 48 | Random.#validateEmptyArray(array); 49 | 50 | return array[Random.pickNumberInRange(0, array.length - 1)]; 51 | } 52 | 53 | static #validateEmptyArray(array) { 54 | if (!Array.isArray(array)) { 55 | throw new Error("the argument must be an array."); 56 | } 57 | 58 | if (!array.every((v) => Random.#isNumber(v))) { 59 | throw new Error("array elements must be numbers."); 60 | } 61 | 62 | if (array.length === 0) { 63 | throw new Error("argument array cannot be empty."); 64 | } 65 | } 66 | 67 | static pickUniqueNumbersInRange(startInclusive, endInclusive, count) { 68 | Random.#validateIntsRange(startInclusive, endInclusive, count); 69 | 70 | const result = []; 71 | 72 | for (let i = startInclusive; i <= endInclusive; i++) { 73 | result.push(i); 74 | } 75 | 76 | return Random.shuffle(result).slice(0, count); 77 | } 78 | 79 | static #validateIntsRange(startInclusive, endInclusive, count) { 80 | if ( 81 | !Random.#isNumber(startInclusive) || 82 | !Random.#isNumber(endInclusive) || 83 | !Random.#isNumber(count) 84 | ) { 85 | throw new Error("arguments must be numbers."); 86 | } 87 | 88 | if (count < 0) { 89 | throw new Error("count cannot be less than zero."); 90 | } 91 | 92 | if (endInclusive - startInclusive + 1 < count) { 93 | throw new Error( 94 | `count: ${count} cannot be greater than the input range (endInclusive - startInclusive): ${ 95 | endInclusive - startInclusive 96 | }.` 97 | ); 98 | } 99 | } 100 | 101 | static shuffle(array) { 102 | Random.#validateEmptyArray(array); 103 | 104 | return array.sort(() => Math.random() - 0.5); 105 | } 106 | } 107 | 108 | export default Random; 109 | -------------------------------------------------------------------------------- /test/console.test.js: -------------------------------------------------------------------------------- 1 | import readline from "readline"; 2 | import * as MissionUtils from "../src"; 3 | 4 | describe('Console.print', () => { 5 | test('주어진 메시지를 콘솔에 출력해야 한다.', () => { 6 | // given 7 | const message = 'test'; 8 | const logSpy = jest.spyOn(console, "log"); 9 | 10 | // when 11 | MissionUtils.Console.print(message) 12 | 13 | // then 14 | expect(logSpy).toHaveBeenCalledWith(message); 15 | }); 16 | }); 17 | 18 | describe('Console.readLine', () => { 19 | const query = "test"; 20 | const callback = jest.fn(); 21 | 22 | test('인자가 2개보다 적게 주어진 경우 예외가 발생해야 한다.', () => { 23 | // given 24 | // when 25 | // then 26 | expect(() => { 27 | MissionUtils.Console.readLine(query); 28 | }).toThrow(); 29 | }); 30 | 31 | test('인자가 2개보다 많이 주어진 경우 예외가 발생해야 한다.', () => { 32 | // given 33 | // when 34 | // then 35 | expect(() => { 36 | MissionUtils.Console.readLine(query, callback, 1); 37 | }).toThrow(); 38 | }); 39 | 40 | test('query가 문자열이 아닌 경우 예외가 발생해야 한다.', () => { 41 | // given 42 | const invalidQuery = 1; 43 | 44 | // when 45 | // then 46 | expect(() => { 47 | MissionUtils.Console.readLine(invalidQuery, callback); 48 | }).toThrow(); 49 | }); 50 | 51 | test('callback이 함수가 아닌 경우 예외가 발생해야 한다.', () => { 52 | // given 53 | const invalidCallback = "callback"; 54 | 55 | // when 56 | // then 57 | expect(() => { 58 | MissionUtils.Console.readLine(query, invalidCallback); 59 | }).toThrow(); 60 | }); 61 | 62 | test('callback에 인자가 1개가 아닌 경우 예외가 발생해야 한다.', () => { 63 | // given 64 | const invalidCallback = (a, b) => {}; 65 | 66 | // when 67 | // then 68 | expect(() => { 69 | MissionUtils.Console.readLine(query, invalidCallback); 70 | }).toThrow(); 71 | }); 72 | }); 73 | 74 | describe('Console.readLineAsync', () => { 75 | const query = "test"; 76 | 77 | test('인자가 주어지지 않은 경우 예외가 발생해야 한다.', async () => { 78 | // given 79 | // when 80 | // then 81 | await expect(MissionUtils.Console.readLineAsync()).rejects.toThrow(); 82 | }); 83 | 84 | test('인자가 1개보다 많이 주어진 경우 예외가 발생해야 한다.', async () => { 85 | // given 86 | // when 87 | // then 88 | await expect(MissionUtils.Console.readLineAsync(query, 1)).rejects.toThrow(); 89 | }); 90 | 91 | test('query가 문자열이 아닌 경우 예외가 발생해야 한다.', async () => { 92 | // given 93 | const invalidQuery = 1; 94 | 95 | // when 96 | // then 97 | await expect(MissionUtils.Console.readLineAsync(invalidQuery)).rejects.toThrow(); 98 | }); 99 | 100 | test('사용자가 입력한 값을 반환해야 한다.', async () => { 101 | // given 102 | const userInput = "user input"; 103 | const createInterfaceMock = jest.spyOn(readline, "createInterface"); 104 | const readlineMock = { 105 | question: jest.fn((query, callback) => { 106 | callback(userInput); 107 | }), 108 | close: jest.fn(), 109 | }; 110 | createInterfaceMock.mockReturnValue(readlineMock); 111 | 112 | // when 113 | const result = await MissionUtils.Console.readLineAsync(query); 114 | 115 | // then 116 | expect(result).toBe(userInput); 117 | expect(readlineMock.question).toHaveBeenCalledWith(query, expect.any(Function)); 118 | expect(readlineMock.close).toHaveBeenCalled(); 119 | 120 | createInterfaceMock.mockRestore(); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/random.test.js: -------------------------------------------------------------------------------- 1 | import * as MissionUtils from "../src"; 2 | 3 | expect.extend({ 4 | toBeDistinct(received) { 5 | const pass = 6 | Array.isArray(received) && new Set(received).size === received.length; 7 | if (pass) { 8 | return { 9 | message: () => `expected [${received}] array is unique`, 10 | pass: true, 11 | }; 12 | } else { 13 | return { 14 | message: () => `expected [${received}] array is not to unique`, 15 | pass: false, 16 | }; 17 | } 18 | }, 19 | }); 20 | 21 | describe("Random.pickNumberInRange", () => { 22 | test("시작 또는 끝 숫자를 포함한 범위 내의 숫자를 반환해야 한다.", () => { 23 | // given 24 | const start = 1; 25 | const end = 5; 26 | 27 | // when 28 | const number = MissionUtils.Random.pickNumberInRange(start, end); 29 | 30 | // then 31 | expect(number).toBeGreaterThanOrEqual(start); 32 | expect(number).toBeLessThanOrEqual(end); 33 | }); 34 | 35 | test("시작 숫자가 끝 숫자보다 크면 예외가 발생해야 한다.", () => { 36 | // given 37 | const start = 1; 38 | const end = start - 1; 39 | 40 | // when 41 | // then 42 | expect(() => { 43 | MissionUtils.Random.pickNumberInRange(start, end); 44 | }).toThrow(); 45 | }); 46 | 47 | test("시작 숫자가 Number.MIN_SAFE_INTEGER 작으면 예외가 발생해야 한다.", () => { 48 | // given 49 | const start = Number.MIN_SAFE_INTEGER - 1; 50 | const end = start + 10; 51 | 52 | // when 53 | // then 54 | expect(() => { 55 | MissionUtils.Random.pickNumberInRange(start, end); 56 | }).toThrow(); 57 | }); 58 | 59 | test("끝 숫자가 Number.MAX_SAFE_INTEGER보다 크면 예외가 발생해야 한다.", () => { 60 | // given 61 | const start = 1; 62 | const end = Number.MAX_SAFE_INTEGER + 1; 63 | 64 | // when 65 | // then 66 | expect(() => { 67 | MissionUtils.Random.pickNumberInRange(start, end); 68 | }).toThrow(); 69 | }); 70 | }); 71 | 72 | describe("Random.pickNumberInList", () => { 73 | test("주어진 목록에 있는 숫자 중 하나를 반환해야 한다.", () => { 74 | // given 75 | const list = [1, 2, 3, 4, 5]; 76 | 77 | // when 78 | const number = MissionUtils.Random.pickNumberInList(list); 79 | 80 | // then 81 | expect(list).toContain(number); 82 | }); 83 | 84 | test("목록이 비어 있을 경우 예외가 발생해야 한다.", () => { 85 | // given 86 | const list = []; 87 | 88 | // when 89 | // then 90 | expect(() => { 91 | MissionUtils.Random.pickNumberInList(list); 92 | }).toThrow(); 93 | }); 94 | 95 | test("숫자로만 이루어진 목록이 아닌 경우 예외가 발생해야 한다.", () => { 96 | // given 97 | const list = [2, "text", 4]; 98 | 99 | // when 100 | // then 101 | expect(() => { 102 | MissionUtils.Random.pickNumberInList(list); 103 | }).toThrow(); 104 | }); 105 | 106 | test("주어진 목록이 배열이 아닐 경우 예외가 발생해야 한다.", () => { 107 | // given 108 | // when 109 | // then 110 | expect(() => { 111 | MissionUtils.Random.pickNumberInList(""); 112 | }).toThrow(); 113 | 114 | expect(() => { 115 | MissionUtils.Random.pickNumberInList({}); 116 | }).toThrow(); 117 | 118 | expect(() => { 119 | MissionUtils.Random.pickNumberInList(1); 120 | }).toThrow(); 121 | 122 | expect(() => { 123 | MissionUtils.Random.pickNumberInList(); 124 | }).toThrow(); 125 | }); 126 | }); 127 | 128 | describe("Random.pickUniqueNumbersInRange", () => { 129 | test("범위 내에서 지정된 개수만큼 겹치지 않는 숫자를 반환해야 한다.", () => { 130 | // given 131 | const start = 1; 132 | const end = 5; 133 | const count = 2; 134 | 135 | // when 136 | const numbers = MissionUtils.Random.pickUniqueNumbersInRange( 137 | start, 138 | end, 139 | count 140 | ); 141 | 142 | // then 143 | expect(numbers).toHaveLength(count); 144 | numbers.forEach( 145 | (number) => 146 | expect(number).toBeGreaterThanOrEqual(start) && 147 | expect(number).toBeLessThanOrEqual(end) 148 | ); 149 | expect(numbers).toBeDistinct(); 150 | }); 151 | 152 | test("개수가 음수이면 예외가 발생해야 한다.", () => { 153 | // given 154 | const start = 1; 155 | const end = 5; 156 | const count = -1; 157 | 158 | // when 159 | // then 160 | expect(() => { 161 | MissionUtils.Random.pickUniqueNumbersInRange(start, end, count); 162 | }).toThrow(); 163 | }); 164 | 165 | test("개수가 숫자 범위의 크기보다 크면 예외가 발생해야 한다.", () => { 166 | // given 167 | const start = 1; 168 | const end = 5; 169 | const count = 10; 170 | 171 | // when 172 | // then 173 | expect(() => { 174 | MissionUtils.Random.pickUniqueNumbersInRange(start, end, count); 175 | }).toThrow(); 176 | }); 177 | 178 | test("시작 숫자가 끝 숫자보다 크면 예외가 발생해야 한다.", () => { 179 | // given 180 | const start = 1; 181 | const end = start - 1; 182 | 183 | // when 184 | // then 185 | expect(() => { 186 | MissionUtils.Random.pickUniqueNumbersInRange(start, end); 187 | }).toThrow(); 188 | }); 189 | 190 | test("시작 숫자가 Number.MIN_SAFE_INTEGER 작으면 예외가 발생해야 한다.", () => { 191 | // given 192 | const start = Number.MIN_SAFE_INTEGER - 1; 193 | const end = start + 10; 194 | 195 | // when 196 | // then 197 | expect(() => { 198 | MissionUtils.Random.pickUniqueNumbersInRange(start, end); 199 | }).toThrow(); 200 | }); 201 | 202 | test("끝 숫자가 Number.MAX_SAFE_INTEGER보다 크면 예외가 발생해야 한다.", () => { 203 | // given 204 | const start = 1; 205 | const end = Number.MAX_SAFE_INTEGER + 1; 206 | 207 | // when 208 | // then 209 | expect(() => { 210 | MissionUtils.Random.pickUniqueNumbersInRange(start, end); 211 | }).toThrow(); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | 4 | const __filename = fileURLToPath(import.meta.url); 5 | const __dirname = path.dirname(__filename); 6 | 7 | const config = { 8 | entry: "./src/index.js", 9 | target: "node", 10 | resolve: { 11 | extensions: [".js"], 12 | }, 13 | devtool: false, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | exclude: [/node_modules/], 19 | use: { 20 | loader: "babel-loader", 21 | options: { 22 | presets: ["@babel/preset-env"] 23 | }, 24 | }, 25 | }, 26 | ], 27 | }, 28 | }; 29 | 30 | export default [ 31 | // CJS 빌드 32 | { 33 | ...config, 34 | output: { 35 | path: path.resolve(__dirname, "dist"), 36 | filename: "mission-utils.cjs", 37 | library: { 38 | type: "commonjs", 39 | }, 40 | }, 41 | }, 42 | // ESM 빌드 43 | { 44 | ...config, 45 | output: { 46 | path: path.resolve(__dirname, "dist"), 47 | filename: "mission-utils.js", 48 | library: { 49 | type: "module", 50 | }, 51 | chunkFormat: "module", 52 | }, 53 | experiments: { 54 | outputModule: true, // ESM 출력 모듈 활성화 55 | }, 56 | }, 57 | ]; 58 | --------------------------------------------------------------------------------