diff --git a/package.json b/package.json
index d9668f98..64087ec0 100644
--- a/package.json
+++ b/package.json
@@ -49,15 +49,18 @@
]
},
"devDependencies": {
+ "@draft-js-plugins/editor": "^4.1.0",
"@storybook/addon-actions": "^6.1.21",
"@storybook/addon-docs": "^6.1.21",
"@storybook/addon-knobs": "^6.1.21",
"@storybook/addon-links": "^6.1.21",
"@storybook/preset-create-react-app": "^3.1.7",
"@storybook/react": "^6.1.21",
+ "@types/draft-js": "^0.11.1",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.1",
"@types/react-router-dom": "^5.1.7",
+ "draft-js": "^0.11.7",
"eslint-config-naver": "^2.1.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
diff --git a/src/dumbs/Layout/index.tsx b/src/dumbs/Layout/index.tsx
index b656b9f6..8f5b1a76 100644
--- a/src/dumbs/Layout/index.tsx
+++ b/src/dumbs/Layout/index.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import styled from 'styled-components';
+import { theme } from 'themes';
interface Props {
children: React.ReactNode;
@@ -7,14 +8,21 @@ interface Props {
const Content = styled.section`
max-width: 1100px;
- margin: 36px auto;
+ height: 100vh;
+ display: flex;
+ margin: auto;
+
+ > div {
+ margin: ${theme.spacing(4)} 0;
+ width: 100%;
+ }
`;
const Layout = ({ children }: Props): React.ReactElement => {
return (
- <>
- {children}
- >
+
+ {children}
+
);
};
diff --git a/src/pages/CreateDesign/Pattern/index.tsx b/src/pages/CreateDesign/Pattern/index.tsx
index d8bbfacf..73177eb9 100644
--- a/src/pages/CreateDesign/Pattern/index.tsx
+++ b/src/pages/CreateDesign/Pattern/index.tsx
@@ -1,7 +1,79 @@
-import React from 'react';
+import Editor from '@draft-js-plugins/editor';
+import { EditorState } from 'draft-js';
+import createUnitDecoratorPlugin from 'plugins/unitDecorator';
+import { UnitDecoratorStyleMap } from 'plugins/unitDecorator/types';
+import { useRef, useState } from 'react';
+import styled, { css } from 'styled-components';
+import { theme } from 'themes';
+import { palette } from 'themes/palatte';
+
+const stitcheDecoratorPlugin = createUnitDecoratorPlugin({ unit: '코' });
+const rowDecoratorPlugin = createUnitDecoratorPlugin({ unit: '단' });
+
+interface EditorWrapperProps {
+ isFocused: boolean;
+}
+
+const EditorWrapper = styled.div`
+ min-height: 50%;
+ padding: ${theme.spacing(1.5)};
+ margin: ${theme.spacing(4, 1.5)};
+ border: 1.5px solid transparent;
+ border-radius: ${theme.spacing(1)};
+ background: ${palette.grey[200]};
+
+ ${({ isFocused }) =>
+ isFocused &&
+ css`
+ border: 1.5px solid ${palette.grey[400]};
+ `}
+
+ .public-DraftEditorPlaceholder-root {
+ display: inline;
+ div {
+ display: inline;
+ }
+ color: ${palette.action.active};
+ }
+
+ .DraftEditor-editorContainer {
+ display: inline-block;
+ }
+`;
const Pattern = (): React.ReactElement => {
- return 도안 작성 페이지
;
+ const [editorState, setEditorState] = useState(
+ EditorState.createEmpty(),
+ );
+ const [isFocused, setIsFocused] = useState(false);
+ const editor = useRef(null);
+
+ const plugins = [stitcheDecoratorPlugin, rowDecoratorPlugin];
+
+ const focusEditor = (): void => {
+ editor?.current?.focus();
+ };
+
+ const customStyleMap = {
+ ...UnitDecoratorStyleMap,
+ };
+
+ return (
+ <>
+
+ setIsFocused(true)}
+ onBlur={(): void => setIsFocused(false)}
+ placeholder="도안을 입력하세요"
+ />
+
+ >
+ );
};
export default Pattern;
diff --git a/src/plugins/unitDecorator/index.tsx b/src/plugins/unitDecorator/index.tsx
new file mode 100644
index 00000000..5f855254
--- /dev/null
+++ b/src/plugins/unitDecorator/index.tsx
@@ -0,0 +1,34 @@
+import { EditorPlugin } from '@draft-js-plugins/editor';
+import { ContentBlock } from 'draft-js';
+import { ComponentType, ReactElement } from 'react';
+
+import UnitDecorator, { UnitDecoratorProps } from './unitDecorator';
+import unitDecoratorStrategy from './unitDecoratorStrategy';
+
+export type { UnitDecoratorProps };
+export interface UnitDecoratorPluginConfig {
+ unit?: string;
+ unitDecoratorComponent?: ComponentType;
+}
+
+export default (config: UnitDecoratorPluginConfig = {}): EditorPlugin => {
+ const {
+ unit = '코',
+ unitDecoratorComponent: UnitDecoratorComponent = UnitDecorator,
+ } = config;
+ const DecoratedUnitDecorator = (props: UnitDecoratorProps): ReactElement => (
+
+ );
+
+ return {
+ decorators: [
+ {
+ strategy: (
+ contentBlock: ContentBlock,
+ callback: (begin: number, end: number) => void,
+ ) => unitDecoratorStrategy(unit, contentBlock, callback),
+ component: DecoratedUnitDecorator,
+ },
+ ],
+ };
+};
diff --git a/src/plugins/unitDecorator/types.ts b/src/plugins/unitDecorator/types.ts
new file mode 100644
index 00000000..691e1700
--- /dev/null
+++ b/src/plugins/unitDecorator/types.ts
@@ -0,0 +1,12 @@
+import { palette } from 'themes/palatte';
+
+export const CustomInline = {
+ NOT_CALCULATE: 'NOT_CALCULATE',
+};
+
+export const UnitDecoratorStyleMap = {
+ NOT_CALCULATE: {
+ background: palette.action.disabledBackground,
+ color: palette.text.primary,
+ },
+};
diff --git a/src/plugins/unitDecorator/unitDecorator.tsx b/src/plugins/unitDecorator/unitDecorator.tsx
new file mode 100644
index 00000000..9f34e193
--- /dev/null
+++ b/src/plugins/unitDecorator/unitDecorator.tsx
@@ -0,0 +1,96 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { ContentState, EditorState, RichUtils } from 'draft-js';
+import { ReactElement, ReactNode } from 'react';
+import styled from 'styled-components';
+import { theme } from 'themes';
+import { palette } from 'themes/palatte';
+
+import { CustomInline } from './types';
+
+export interface UnitDecoratorProps {
+ className?: string;
+ children?: ReactNode;
+
+ unit?: string;
+ decoratedText?: string;
+ entityKey?: string;
+ offsetKey?: string;
+ contentState?: ContentState;
+ blockKey?: string;
+ start?: number;
+ end?: number;
+
+ setEditorState?(editorState: EditorState): void;
+ getEditorState?(): EditorState;
+}
+
+const DecoratorWrapper = styled.span`
+ > span {
+ background: ${palette.primary.main};
+ margin: ${theme.spacing(0, 0.5)};
+ padding: ${theme.spacing(0.5, 1)};
+ border-radius: ${theme.spacing(0.5)};
+ color: ${theme.palette.background.paper};
+ box-shadow: ${theme.shadows[2]};
+ cursor: pointer;
+
+ &:hover {
+ box-shadow: ${theme.shadows[4]};
+ }
+ }
+`;
+
+export default function UnitDecorator(props: UnitDecoratorProps): ReactElement {
+ const {
+ className,
+ children,
+ unit,
+ decoratedText,
+ entityKey,
+ getEditorState,
+ offsetKey,
+ setEditorState,
+ contentState,
+ blockKey,
+ start,
+ end,
+ ...otherProps
+ } = props;
+
+ const handleClick = (): void => {
+ const editorState = getEditorState?.();
+
+ if (editorState != null && children != null && setEditorState != null) {
+ const selectionState = editorState.getSelection();
+ const newSelection = selectionState.merge({
+ anchorOffset: start,
+ focusOffset: end,
+ });
+
+ const editorStateWithNewSelection = EditorState.forceSelection(
+ editorState,
+ newSelection,
+ );
+ const editorStateWithStyles = RichUtils.toggleInlineStyle(
+ editorStateWithNewSelection,
+ CustomInline.NOT_CALCULATE,
+ );
+ const editorStateWithStylesAndPreviousSelection = EditorState.forceSelection(
+ editorStateWithStyles,
+ selectionState,
+ );
+
+ setEditorState(editorStateWithStylesAndPreviousSelection);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/plugins/unitDecorator/unitDecoratorStrategy.ts b/src/plugins/unitDecorator/unitDecoratorStrategy.ts
new file mode 100644
index 00000000..18cdaec9
--- /dev/null
+++ b/src/plugins/unitDecorator/unitDecoratorStrategy.ts
@@ -0,0 +1,18 @@
+import { ContentBlock } from 'draft-js';
+
+import { extractUnitDecoratorsWithIndices } from './utils/extractUnitDecorator';
+
+export default (
+ unit: string,
+ contentBlock: ContentBlock,
+ callback: (begin: number, end: number) => void,
+): void => {
+ const text = contentBlock.getText();
+ const results = extractUnitDecoratorsWithIndices(unit, text);
+
+ results.forEach((unitDecorator) => {
+ const { indices } = unitDecorator;
+
+ callback(indices[0], indices[1]);
+ });
+};
diff --git a/src/plugins/unitDecorator/utils/extractUnitDecorator.ts b/src/plugins/unitDecorator/utils/extractUnitDecorator.ts
new file mode 100644
index 00000000..2ebee2ab
--- /dev/null
+++ b/src/plugins/unitDecorator/utils/extractUnitDecorator.ts
@@ -0,0 +1,50 @@
+import {
+ getEndHashtagMatch,
+ getHashSigns,
+ getUnitDecoratorBoundary,
+} from './unitDecoratorRegex';
+
+interface UnitDecoratorIndice {
+ unitDecorator: string;
+ indices: [number, number];
+}
+
+export function extractUnitDecoratorsWithIndices(
+ unit: string,
+ text: string,
+): UnitDecoratorIndice[] {
+ if (!text || !text.match(getHashSigns(unit))) {
+ return [];
+ }
+
+ const tags: UnitDecoratorIndice[] = [];
+
+ function replacer(
+ match: string,
+ before: string,
+ _hash: string,
+ hashText: string,
+ offset: number,
+ chunk: string,
+ ): string {
+ const after = chunk.slice(offset + match.length);
+
+ if (after.match(getEndHashtagMatch(unit))) {
+ return '';
+ }
+
+ const startPosition = offset + before.length;
+ const endPosition =
+ startPosition + (offset === 0 ? match.length : match.length - 1);
+
+ tags.push({
+ unitDecorator: hashText,
+ indices: [startPosition, endPosition],
+ });
+ return '';
+ }
+
+ text.replace(getUnitDecoratorBoundary(unit), replacer);
+
+ return tags;
+}
diff --git a/src/plugins/unitDecorator/utils/unitDecoratorRegex.ts b/src/plugins/unitDecorator/utils/unitDecoratorRegex.ts
new file mode 100644
index 00000000..092551a1
--- /dev/null
+++ b/src/plugins/unitDecorator/utils/unitDecoratorRegex.ts
@@ -0,0 +1,13 @@
+const numbers = '0-9';
+
+export const getEndHashtagMatch = (unit: string): RegExp =>
+ // eslint-disable-next-line no-useless-escape
+ new RegExp(`^(?:[${numbers}]+[${unit}]|:\/\/)`);
+
+export const getHashSigns = (unit: string): RegExp => new RegExp(unit);
+
+export const getUnitDecoratorBoundary = (unit: string): RegExp =>
+ new RegExp(
+ `((?:^|$|[^${numbers}]))([${numbers}]*)([${numbers}]+[${unit}])`,
+ 'gi',
+ );
diff --git a/yarn.lock b/yarn.lock
index e8610d6d..82901c7a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1448,6 +1448,14 @@
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
+"@draft-js-plugins/editor@^4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@draft-js-plugins/editor/-/editor-4.1.0.tgz#1861fb257ba51ecbd031b8a3d84ee0809de1504e"
+ integrity sha512-i95uGF1GOFwP99qRCjtocGAYanULtkX9/XPeXKwjKx65vnOt/0dvBBdSvzSgrJ9pncHHLUqDfFQKXy/09fTmvg==
+ dependencies:
+ immutable "~3.7.4"
+ prop-types "^15.5.8"
+
"@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
@@ -2713,6 +2721,14 @@
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb"
integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==
+"@types/draft-js@^0.11.1":
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/@types/draft-js/-/draft-js-0.11.1.tgz#f67920c9583054143e263704e250dd3086de3ef3"
+ integrity sha512-jV4LAXYdVvS0ahIROZehkKqHgfLxaDBl3fzfEVqho8NxFAtEaObdIiu7FpPUu/Y97PlJVxGajar7aSikQqz9sQ==
+ dependencies:
+ "@types/react" "*"
+ immutable "~3.7.4"
+
"@types/eslint@^7.2.6":
version "7.2.6"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.6.tgz#5e9aff555a975596c03a98b59ecd103decc70c3c"
@@ -3679,7 +3695,7 @@ arrify@^2.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
-asap@~2.0.6:
+asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@@ -5268,6 +5284,11 @@ core-js@^3.0.1, core-js@^3.0.4:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae"
integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
+core-js@^3.6.4:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.10.0.tgz#9a020547c8b6879f929306949e31496bbe2ae9b3"
+ integrity sha512-MQx/7TLgmmDVamSyfE+O+5BHvG1aUGj/gHhLn1wVtm2B5u1eVIPvh7vkfjwWKNCjrTJB8+He99IntSQ1qP+vYQ==
+
core-js@^3.6.5:
version "3.9.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.0.tgz#790b1bb11553a2272b36e2625c7179db345492f8"
@@ -5374,6 +5395,13 @@ create-react-context@0.3.0:
gud "^1.0.0"
warning "^4.0.3"
+cross-fetch@^3.0.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
+ integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
+ dependencies:
+ node-fetch "2.6.1"
+
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -6099,6 +6127,15 @@ downshift@^6.0.6:
prop-types "^15.7.2"
react-is "^17.0.1"
+draft-js@^0.11.7:
+ version "0.11.7"
+ resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.11.7.tgz#be293aaa255c46d8a6647f3860aa4c178484a206"
+ integrity sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg==
+ dependencies:
+ fbjs "^2.0.0"
+ immutable "~3.7.4"
+ object-assign "^4.1.1"
+
duplexer@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
@@ -7018,6 +7055,25 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
+fbjs-css-vars@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8"
+ integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==
+
+fbjs@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-2.0.0.tgz#01fb812138d7e31831ed3e374afe27b9169ef442"
+ integrity sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ==
+ dependencies:
+ core-js "^3.6.4"
+ cross-fetch "^3.0.4"
+ fbjs-css-vars "^1.0.0"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
figgy-pudding@^3.5.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
@@ -8114,6 +8170,11 @@ immer@8.0.1:
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
+immutable@~3.7.4:
+ version "3.7.6"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
+ integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks=
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -10307,7 +10368,7 @@ node-dir@^0.1.10:
dependencies:
minimatch "^3.0.2"
-node-fetch@^2.6.0:
+node-fetch@2.6.1, node-fetch@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -11865,6 +11926,13 @@ promise.prototype.finally@^3.1.0:
es-abstract "^1.17.0-next.0"
function-bind "^1.1.1"
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+ dependencies:
+ asap "~2.0.3"
+
promise@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e"
@@ -13247,7 +13315,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
-setimmediate@^1.0.4:
+setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -14434,6 +14502,11 @@ typescript@4.1.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
+ua-parser-js@^0.7.18:
+ version "0.7.27"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.27.tgz#b54f8ce9eb6c7abf3584edeaf9a3d8b3bd92edba"
+ integrity sha512-eXMaRYK2skomGocoX0x9sBXzx5A1ZVQgXfrW4mTc8dT0zS7olEcyfudAzRC5tIIRgLxQ69B6jut3DI+n5hslPA==
+
unbox-primitive@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"