From 2d11a7bae983430db835f0c4b19df0f372b35e2d Mon Sep 17 00:00:00 2001 From: djaco Date: Wed, 13 Aug 2025 13:27:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ• ์ปค๋ฐ‹ ์‹œ๊ฐ„: 2025. 08. 13. 13:27:54 ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„: โ€ข ์ด ํŒŒ์ผ: 3๊ฐœ ๐Ÿ“ ์ถ”๊ฐ€๋œ ํŒŒ์ผ: + com.twin.app.shoptime/src/utils/fp.js + com.twin.app.shoptime/src/utils/lodash.js + com.twin.app.shoptime/src/utils/lodashFpEx.js ๐Ÿ”ง ์ฃผ์š” ๋ณ€๊ฒฝ ๋‚ด์šฉ: โ€ข ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ์ตœ์ ํ™” --- com.twin.app.shoptime/src/utils/fp.js | 12 + com.twin.app.shoptime/src/utils/lodash.js | 4 + com.twin.app.shoptime/src/utils/lodashFpEx.js | 468 ++++++++++++++++++ 3 files changed, 484 insertions(+) create mode 100644 com.twin.app.shoptime/src/utils/fp.js create mode 100644 com.twin.app.shoptime/src/utils/lodash.js create mode 100644 com.twin.app.shoptime/src/utils/lodashFpEx.js diff --git a/com.twin.app.shoptime/src/utils/fp.js b/com.twin.app.shoptime/src/utils/fp.js new file mode 100644 index 00000000..569e44b3 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/fp.js @@ -0,0 +1,12 @@ +// FP bootstrap: use locally-extended lodash instance +// './lodash' already mixes in our custom extensions from './lodashFpEx' +import fp from './lodash'; + +export const { + pipe, flow, curry, compose, + map, filter, reduce, get, set, + isEmpty, isNotEmpty, isNil, isNotNil, + mapAsync, reduceAsync, filterAsync, +} = fp; + +export default fp; diff --git a/com.twin.app.shoptime/src/utils/lodash.js b/com.twin.app.shoptime/src/utils/lodash.js new file mode 100644 index 00000000..ac1389e5 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/lodash.js @@ -0,0 +1,4 @@ +import fp from 'lodash/fp'; +import fpEx from './lodashFpEx'; + +export default fp.mixin(fpEx); diff --git a/com.twin.app.shoptime/src/utils/lodashFpEx.js b/com.twin.app.shoptime/src/utils/lodashFpEx.js new file mode 100644 index 00000000..cb136ef3 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/lodashFpEx.js @@ -0,0 +1,468 @@ +import fp from 'lodash/fp'; + +/** + * ๋Œ€์ƒ ์ธ์ž๊ฐ€ promise(thenable)์ธ์ง€ ์—ฌ๋ถ€ + * @param {*} x + */ +const isPromise = (x) => fp.isFunction(fp.get('then', x)) && fp.isFunction(fp.get('catch', x)); + +const fnPromisify = (fn, ...args) => { + return new Promise((resolve, reject) => { + try { + resolve(fn(...args)); + } catch (e) { + reject(e); + } + }); +}; + +/** + * ๋Œ€์ƒ ์ธ์ž๋ฅผ promise๋กœ lift (like monad lift) + */ +const promisify = (a, ...args) => { + const cond = fp.cond([ + [fp.isFunction, () => fnPromisify(a, ...args)], + [isPromise, fp.identity], + [fp.T, (a) => Promise.resolve(a)], + ]); + const result = cond(a); + + return result; +}; + +/** + * promise๊ฐ€ ๋˜๋‹ค๋ฅธ promise๋ฅผ resolveํ•˜๋Š” ๊ฒฝ์šฐ, promise์˜ ์ค‘์ฒฉ์„ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•œ helper ํ•จ์ˆ˜ + * + * @param {*} thenable + */ +const flatPromise = (thenable) => + isPromise(thenable) ? thenable.then((x) => flatPromise(x)) : thenable; + +/** + * lodash ํ˜•ํƒœ์˜ promise then + */ +const then = fp.curry((fn, thenable) => promisify(thenable).then(flatPromise(fn))); + +/** + * lodash ํ˜•ํƒœ์˜ promise catch + */ +const otherwise = fp.curry((fn, thenable) => promisify(thenable).catch(flatPromise(fn))); + +/** + * lodash ํ˜•ํƒœ์˜ promise finally + */ +const _finally = fp.curry((fn, thenable) => promisify(thenable).finally(flatPromise(fn))); + +/** + * invert boolean + * @param {*} x + */ +const not = (x) => !x; + +/** + * ๋Œ€์ƒ์ด ๋น„์–ด์žˆ์ง€ ์•Š์€์ง€ ์—ฌ๋ถ€ + */ +const isNotEmpty = fp.pipe(fp.isEmpty, not); + +/** + * ๋Œ€์ƒ ์ธ์ž๋ฅผ boolean ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜\ + * (์˜ˆ์™ธ)'true'๋ฌธ์ž์—ด์ด๋ฉด true, 'false'๋ฌธ์ž์—ด์ด๋ฉด false + * + * @param {*} a + */ +const toBool = (a) => + fp.cond([ + [fp.equals('true'), fp.T], + [fp.equals('false'), fp.F], + [fp.T, (a) => !!a], + ])(a); + +/** + * ์‚ผํ•ญ์‹ helper ํ•จ์ˆ˜\ + * (isTrue๊ฐ€ true๋ฉด t(์‹คํ–‰)๋ฐ˜ํ™˜, false๋ฉด f(์‹คํ–‰)๋ฐ˜ํ™˜) + */ +const ternary = fp.curry((evaluator, trueHandler, falseHandler, a) => { + const executor = fp.curry((t, f, a, isTrue) => { + const result = isTrue ? (fp.isFunction(t) ? t(a) : t) : fp.isFunction(f) ? f(a) : f; + return result; + }); + const getEvaluator = (fn) => (fp.isNil(fn) ? fp.identity : fn); + const result = executor(trueHandler, falseHandler, a, getEvaluator(evaluator)(a)); + + return result; +}); + +/** + * a์ธ์ž๊ฐ€ tํƒ€์ž…์ธ์ง€ ์—ฌ๋ถ€ + */ +const instanceOf = fp.curry((t, a) => a instanceof t); + +/** + * ๋Œ€์ƒ ๋ฌธ์ž์—ด์„ pascalcase ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + */ +const pascalCase = fp.pipe(fp.camelCase, fp.upperFirst); + +/** + * (collection) fp.map์˜ ๋น„๋™๊ธฐ ํ•จ์ˆ˜\ + * mapper ํ•จ์ˆ˜๋กœ ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค. + */ +const mapAsync = fp.curry(async (asyncMapper, arr) => { + const composer = fp.pipe( + fp.flatMapDeep(fp.pipe(asyncMapper, promisify)), + async (a) => await Promise.all(a), + ); + const result = await composer(arr); + + return result; +}); + +/** + * (collection) fp.filter์˜ ๋น„๋™๊ธฐ ํ•จ์ˆ˜\ + * ํ•„ํ„ฐํ•จ์ˆ˜๋กœ ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค. + */ +const filterAsync = fp.curry(async (asyncFilter, arr) => { + const composer = fp.pipe( + mapAsync(async (item) => ((await asyncFilter(item)) ? item : false)), + then(fp.filter(fp.pipe(fp.equals(false), not))), + ); + const result = await composer(arr); + + return result; +}); +/** + * (collection) fp.find์˜ ๋น„๋™๊ธฐ ํ•จ์ˆ˜ + */ +const findAsync = fp.curry(async (asyncFn, arr) => { + const composer = fp.pipe( + mapAsync(asyncFn), + then(fp.indexOf(true)), + then((idx) => fp.get(`[${idx}]`, arr)), + otherwise(fp.always(undefined)), + ); + const result = await composer(arr); + + return result; +}); + +/** + * asyncFn์˜ ์‹œ์ž‘์€ await accPromise๊ฐ€ ๋˜์–ด์•ผ ํ•œ๋‹ค.\ + * ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰๋œ๋‹ค.\ + * (ex 300ms์ด ๊ฑธ๋ฆฌ๋Š” 5๊ฐœ์˜ promise๊ฐ€ ์žˆ๋‹ค๋ฉด, ์ตœ์†Œ 1500ms+alpah์˜ ์‹œ๊ฐ„์ด ์†Œ์š”๋œ๋‹ค.\ + * ์ƒ๊ธฐ์˜ mapAsync์˜ ๊ฒฝ์šฐ 300+alpah์˜ ์‹œ๊ฐ„๋งŒ ์†Œ์š”๋œ๋‹ค.(Promise.all๊ณผ Promise.resolve์˜ ์ฐจ์ด)) + */ +const reduceAsync = fp.curry((asyncFn, initAcc, dest) => { + const initAccPromise = Promise.resolve(initAcc); + const result = fp.reduce(asyncFn, initAccPromise, dest); + return result; +}); + +/** + * ๋น„๋™๊ธฐ forEach + * ์‹คํ–‰ํ•จ์ˆ˜๋กœ ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค + * ์ˆœ์ฐจ์‹คํ–‰ + */ +const forEachAsync = fp.curry(async (cb, collection) => { + const loopResults = []; + const iterator = fp.entries(collection); + + for (const e of iterator) { + loopResults.push(await cb(e[1], e[0])); + } + + return loopResults; +}); + +/** + * value๋กœ object key ์กฐํšŒ + */ +const key = fp.curry((a, v) => { + const composer = fp.pipe( + fp.entries, + fp.find(([k, val]) => fp.equals(v, val)), + fp.head, + ); + const result = composer(a); + return result; +}); + +/** + * ๋Œ€์ƒ ๋ฌธ์ž์—ด์ด jsonํ˜•์‹ ๋ฌธ์ž์—ด์ธ์ง€ ์—ฌ๋ถ€ + * @param {String} a + */ +const isJson = (a) => { + const composer = fp.pipe(fp.attempt, fp.isError); + return fp.isString(a) && !composer(() => JSON.parse(a)); +}; + +/** + * shallow freeze ๋ณด์™„ + * (๋Œ€์ƒ object์˜ refence ํƒ€์ž…์˜ properties๊นŒ์ง€ object.freeze ์ฒ˜๋ฆฌ) + * @param {*} obj + */ +const deepFreeze = (obj) => { + const freezeRecursively = (v) => (isRef(v) && !Object.isFrozen(v) ? deepFreeze(v) : v); + const composer = fp.pipe(Object.freeze, fp.forOwn(freezeRecursively)); + const result = composer(obj); + + return result; +}; + +const transformObjectKey = fp.curry((transformFn, dest) => { + const convertRecursively = (dest) => { + const convertTo = (o) => { + const composer = fp.pipe( + fp.entries, + fp.reduce((acc, [k, v]) => { + const cond = fp.cond([ + [fp.isPlainObject, convertTo], + [fp.isArray, (v) => v.map(cond)], + [fp.T, (a) => a], + ]); + const transformedKey = transformFn(k); + if (!fp.has(transformedKey, acc)) { + acc[transformedKey] = cond(v); + return acc; + } else { + throw new Error( + `${transformedKey} already exist. duplicated property name is not supported.`, + ); + } + }, {}), + ); + const result = composer(o); + return result; + }; + const result = convertTo(dest); + return result; + }; + + const result = fp.isObject(dest) || fp.isArray(dest) ? convertRecursively(dest) : dest; + + return result; +}); + +/** + * ๋Œ€์ƒ object์˜ property key๋ฌธ์ž์—ด์„ camelcase ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + */ +const toCamelcase = transformObjectKey(fp.camelCase); + +/** + * ๋Œ€์ƒ object์˜ property key๋ฌธ์ž์—ด์„ snakecase ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + */ +const toSnakecase = transformObjectKey(fp.snakeCase); +const toPascalcase = transformObjectKey(pascalCase); + +/** + * dateํ˜•์‹ ๋ฌธ์ž์—ด ์—ฌ๋ถ€ + * @param {string} str dateํ˜•์‹ ๋ฌธ์ž์—ด + */ +const isDatetimeString = (str) => isNaN(str) && !isNaN(Date.parse(str)); +/** + * applicative functor pattern ๊ตฌํ˜„์ฒด + * (์ฃผ๋กœ fp.pipeํ•จ์ˆ˜์—์„œ ํ•จ์ˆ˜์˜ ์ธ์ž ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ) + */ +const ap = fp.curry((a, curried) => curried(a)); + +/** + * ๋Œ€์ƒ ์ธ์ž๊ฐ€ undefined ๋˜๋Š” null์ด ์•„๋‹Œ์ง€ ์—ฌ๋ถ€ + */ +const isNotNil = fp.pipe(fp.isNil, not); + +/** + * a์ธ์ž๋ฅผ ์ธ์ž๋กœ, evaluatorํ•จ์ˆ˜ ์‹คํ–‰, + * true๋ฉด trueHandler์— a์ธ์ž ๋Œ€์ž… + * false๋ฉด a ๋ฐ˜ํ™˜ + */ +const ifT = fp.curry((evaluator, trueHandler, a) => { + const isValidParams = fp.every(fp.isFunction, [evaluator, trueHandler]); + + if (isValidParams) { + return fp.pipe(evaluator, fp.equals(true))(a) ? trueHandler(a) : a; + } else { + throw new Error('invalid parameter'); + } +}); + +/** + * a์ธ์ž๋ฅผ ์ธ์ž๋กœ, evaluatorํ•จ์ˆ˜ ์‹คํ–‰, + * false๋ฉด falseHandler์— a์ธ์ž ๋Œ€์ž… + * true๋ฉด a ๋ฐ˜ํ™˜ + */ +const ifF = fp.curry((evaluator, falseHandler, a) => { + const isValidParams = fp.every(fp.isFunction, [evaluator, falseHandler]); + + if (isValidParams) { + return fp.pipe(evaluator, fp.equals(false))(a) ? falseHandler(a) : a; + } else { + throw new Error('invalid parameter(s)'); + } +}); + +/** + * arr์ธ์ž ๋ฐฐ์—ด์— a์ธ์ž๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ์—ฌ๋ถ€ + */ +const notIncludes = fp.curry((a, arr) => { + const composer = fp.pipe(fp.includes, ap(arr), not); + const result = composer(a); + + return result; +}); + +/** + * a์ธ์ž์™€ b์ธ์ž๊ฐ€ ๋‹ค๋ฅธ์ง€ ์—ฌ๋ถ€ (deep equal) + */ +const notEquals = fp.curry((a, b) => fp.pipe(fp.equals(a), not)(b)); + +/** + * arr์ธ์ž์˜ idx์ธ์ž์˜ index์— ํ•ด๋‹นํ•˜๋Š” ์š”์†Œ ์ œ๊ฑฐ + */ +const removeByIndex = fp.curry((idx, arr) => { + if (fp.isArray(arr)) { + const cloned = fp.cloneDeep(arr); + cloned.splice(fp.toNumber(idx), 1); + + return cloned; + } + return arr; +}); + +/** + * arr ์ธ์ž์˜ ๋งˆ์ง€๋ง‰ ์š”์†Œ ์ œ๊ฑฐ (immutable) + * + * @param {*} arr + */ +const removeLast = (a) => { + const nextA = fp.cloneDeep(a); + if (fp.isArray(a)) { + nextA.pop(); + } + if (fp.isString(a)) { + return nextA.substring(0, fp.size(a) - 1); + } + return nextA; +}; + +/** + * fp.concat alias + */ +const append = fp.concat; + +/** + * array ์ธ์ž์˜ (index์ƒ)์•ž์ชฝ์— value์ธ์ž๋ฅผ ์ถ”๊ฐ€ + */ +const prepend = fp.curry((array, value) => + fp.isArray(value) ? fp.concat(value, array) : fp.concat([value], array), +); + +/** + * key(index)๋ฅผ ํฌํ•จํ•œ fp.map + */ +const mapWithKey = fp.curry((f, a) => fp.map.convert({ cap: false })(f, a)); + +/** + * key(index)๋ฅผ ํฌํ•จํ•œ fp.forEach + */ +const forEachWithKey = fp.curry((f, a) => fp.forEach.convert({ cap: false })(f, a)); + +/** + * key(index)๋ฅผ ํฌํ•จํ•œ reduce + */ +const reduceWithKey = fp.curry((f, acc, a) => fp.reduce.convert({ cap: false })(f, acc, a)); + +/** + * null, undefined, Boolean, Number, String + * + */ +const isVal = (a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a); + +/** + * Array, Object, Function + */ +const isRef = fp.pipe(isVal, not); + +const isFalsy = (a) => { + return fp.isNil(a) || fp.some(fp.equals(a), [0, -0, NaN, false, '']); +}; + +const isTruthy = (a) => !isFalsy(a); + +/** + * fp.getOr override + * + * fp.getOr์˜ ๋ฐ˜ํ™˜๊ฐ’์ด null์ธ ๊ฒฝ์šฐ, ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜๋˜๊ฒŒ ์ˆ˜์ •ํ•œ ๋ฒ„์ „ + * circular dependency ๋•Œ๋ฌธ์— closure๋กœ ์ž‘์„ฑ + */ +const getOr = (({ curry, getOr }) => { + const _getOr = curry((defaultValue, path, target) => { + const val = fp.get(path, target); + return fp.isNil(val) ? defaultValue : val; + }); + return _getOr; +})(fp); + +export default { + mapAsync, + filterAsync, + reduceAsync, + findAsync, + forEachAsync, + promisify, + then, + andThen: then, + otherwise, + catch: otherwise, + finally: _finally, + + isPromise, + isNotEmpty, + isNotNil, + isJson, + notEquals, + isNotEqual: notEquals, + isVal, + isPrimitive: isVal, + isRef, + isReference: isRef, + not, + notIncludes, + toBool, + + deepFreeze, + key, + keyByVal: key, + + // string + transformObjectKey, + toCamelcase, + toCamelKey: toCamelcase, + toSnakecase, + toSnakeKey: toSnakecase, + toPascalcase, + pascalCase, + isDatetimeString, + + ap, + instanceOf, + + ternary, + ifT, + ifF, + + // array + removeByIndex, + removeByIdx: removeByIndex, + removeLast, + append, + prepend, + + mapWithKey, + mapWithIdx: mapWithKey, + forEachWithKey, + forEachWithIdx: forEachWithKey, + reduceWithKey, + reduceWithIdx: reduceWithKey, + isFalsy, + isTruthy, + + getOr, +};