Skip to content

Commit 1559a12

Browse files
authored
Merge pull request #295 from Nexters/develop
[운영 배포] 11차 MVP
2 parents ec59f5c + 4bd34c2 commit 1559a12

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+21424
-11287
lines changed

.pnp.cjs

Lines changed: 10550 additions & 5909 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/admin/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@
1616
"@boolti/bridge": "*",
1717
"@boolti/icon": "*",
1818
"@boolti/ui": "*",
19+
"@codemirror/language": "^6.10.8",
1920
"@dnd-kit/core": "^6.1.0",
2021
"@dnd-kit/modifiers": "^7.0.0",
2122
"@dnd-kit/sortable": "^8.0.0",
2223
"@dnd-kit/utilities": "^3.2.2",
2324
"@emotion/react": "^11.11.3",
2425
"@emotion/styled": "^11.11.0",
26+
"@lezer/highlight": "^1.2.1",
27+
"@mdxeditor/editor": "^3.23.2",
2528
"@react-pdf/renderer": "^3.4.4",
2629
"@tanstack/react-table": "^8.12.0",
2730
"@types/lodash.debounce": "^4.0.9",
2831
"date-fns": "^3.3.1",
2932
"framer-motion": "^11.2.10",
33+
"i18next": "^24.2.2",
3034
"jotai": "^2.8.3",
3135
"jwt-decode": "^4.0.0",
3236
"lodash.debounce": "^4.0.8",
@@ -37,7 +41,9 @@
3741
"react-dropzone": "^14.2.3",
3842
"react-error-boundary": "^4.1.2",
3943
"react-hook-form": "^7.50.0",
44+
"react-i18next": "^15.4.1",
4045
"react-intersection-observer": "^9.8.0",
46+
"react-naver-maps": "^0.1.3",
4147
"react-pdf": "^9.0.0",
4248
"react-router-dom": "^6.21.3",
4349
"react-select": "^5.8.0",
@@ -50,6 +56,7 @@
5056
"@boolti/typescript-config": "*",
5157
"@emotion/babel-plugin": "^11.11.0",
5258
"@types/js-cookie": "^3.0.6",
59+
"@types/navermaps": "^3.7.9",
5360
"@types/react": "^18.2.43",
5461
"@types/react-dom": "^18.2.17",
5562
"@vitejs/plugin-react": "^4.2.1",

apps/admin/src/App.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'the-new-css-reset/css/reset.css';
2+
import '@mdxeditor/editor/style.css';
23
import './index.css';
3-
4+
import './i18n';
5+
import { NavermapsProvider } from 'react-naver-maps';
46
import { QueryClientProvider } from '@boolti/api';
57
import { BooltiUIProvider } from '@boolti/ui';
68
import { setDefaultOptions } from 'date-fns';
@@ -41,6 +43,9 @@ import ShowSettlementPage from './pages/ShowSettlementPage';
4143
import ShowEnterancePage from './pages/ShowEnterancePage';
4244
import { initVConsole } from './utils/vConsole';
4345
import { checkIsWebView } from '@boolti/bridge';
46+
import { X_NCP_APIGW_API_KEY_ID } from './constants/ncp';
47+
import { IS_PRODUCTION_PHASE } from './constants/phase';
48+
import WebView from './pages/WebView';
4449

4550
setDefaultOptions({ locale: ko });
4651

@@ -155,20 +160,30 @@ const routes: RouteObject[] = [
155160
<QueryClientProvider>
156161
<BooltiUIProvider>
157162
<LazyMotion features={domAnimation}>
158-
<Outlet />
163+
<NavermapsProvider ncpClientId={X_NCP_APIGW_API_KEY_ID} submodules={['geocoder']}>
164+
<Outlet />
165+
</NavermapsProvider>
159166
</LazyMotion>
160167
</BooltiUIProvider>
161168
</QueryClientProvider>
162169
),
163170
errorElement: <GlobalErrorBoundary />,
164-
children: [...publicRoutes, ...privateRoutes],
171+
children: [
172+
...publicRoutes,
173+
...privateRoutes,
174+
...(IS_PRODUCTION_PHASE ? [] : [{ path: PATH.WEBVIEW, element: <WebView /> }]),
175+
],
165176
},
166177
];
167178

168179
const router = createBrowserRouter(routes);
169180

170181
const App = () => {
171-
return <RouterProvider router={router} />;
182+
return (
183+
<Suspense fallback={null}>
184+
<RouterProvider router={router} />
185+
</Suspense>
186+
);
172187
};
173188

174189
export default App;
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import styled from '@emotion/styled';
2+
3+
const MarkdownEditorContainer = styled.div<{ disabled?: boolean; hasError?: boolean }>`
4+
.mdx-editor {
5+
width: 100%;
6+
max-width: 600px;
7+
min-height: 220px;
8+
max-height: 320px;
9+
overflow-y: auto;
10+
border: 1px solid ${({ theme }) => theme.palette.grey.g20};
11+
border-radius: 4px;
12+
13+
${({ disabled, hasError, theme }) => {
14+
if (disabled) {
15+
return `
16+
background-color: ${theme.palette.grey.g10};
17+
border: 1px solid ${theme.palette.grey.g20};
18+
color: ${theme.palette.grey.g40};
19+
pointer-events: none;
20+
`;
21+
}
22+
23+
if (hasError) {
24+
return `
25+
border-color: ${theme.palette.status.error1};
26+
`;
27+
}
28+
}}
29+
}
30+
31+
.mdxeditor-toolbar {
32+
border-radius: 0;
33+
contain: inline-size;
34+
}
35+
36+
.prose {
37+
p, ul, ol, li, blockquote {
38+
font-size: 16px;
39+
font-style: normal;
40+
font-weight: 400;
41+
line-height: 28px;
42+
}
43+
44+
blockquote {
45+
margin: 0;
46+
padding: 0 16px;
47+
border-left: 4px solid ${({ theme }) => theme.palette.grey.g20};
48+
}
49+
50+
h1 {
51+
font-size: 20px;
52+
font-style: normal;
53+
font-weight: 600;
54+
line-height: 28px;
55+
}
56+
57+
h2 {
58+
font-size: 18px;
59+
font-style: normal;
60+
font-weight: 600;
61+
line-height: 26px;
62+
}
63+
64+
h3 {
65+
font-size: 16px;
66+
font-style: normal;
67+
font-weight: 600;
68+
line-height: 24px;
69+
}
70+
71+
ul {
72+
list-style-type: disc;
73+
padding-left: 24px;
74+
}
75+
76+
ol {
77+
list-style-type: decimal;
78+
padding-left: 24px;
79+
}
80+
81+
li {
82+
margin: 0;
83+
}
84+
85+
a {
86+
color: #46a6ff;
87+
font-size: 16px;
88+
font-style: normal;
89+
font-weight: 400;
90+
line-height: 28px;
91+
text-decoration-line: underline;
92+
text-decoration-style: solid;
93+
text-decoration-skip-ink: none;
94+
text-decoration-thickness: auto;
95+
text-underline-offset: auto;
96+
text-underline-position: from-font;
97+
}
98+
99+
hr {
100+
border: none;
101+
border-top: 1px solid ${({ theme }) => theme.palette.grey.g20};
102+
margin: 16px 0;
103+
}
104+
}
105+
`;
106+
107+
const MarkdownEditorPlaceholder = styled.p`
108+
${({ theme }) => theme.typo.b3};
109+
color: ${({ theme }) => theme.palette.grey.g30};
110+
margin: 0;
111+
`
112+
113+
const YoutubeEmbedDeleteButton = styled.button`
114+
position: absolute;
115+
right: 0;
116+
top: 0;
117+
display: inline-flex;
118+
align-items: center;
119+
justify-content: center;
120+
width: 32px;
121+
height: 32px;
122+
123+
&:hover {
124+
path {
125+
stroke: ${({ theme }) => theme.palette.grey.g90};
126+
}
127+
}
128+
129+
svg {
130+
width: 20px;
131+
height: 20px;
132+
}
133+
134+
path {
135+
stroke: ${({ theme }) => theme.palette.grey.g20};
136+
}
137+
`;
138+
139+
const YoutubeEmbedContainer = styled.div`
140+
position: relative;
141+
display: flex;
142+
flex-direction: column;
143+
align-items: flex-start;
144+
padding: 32px 0 0;
145+
146+
&:hover {
147+
${YoutubeEmbedDeleteButton} {
148+
display: flex;
149+
}
150+
}
151+
`;
152+
153+
export default {
154+
MarkdownEditorContainer,
155+
MarkdownEditorPlaceholder,
156+
YoutubeEmbedDeleteButton,
157+
YoutubeEmbedContainer,
158+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { TrashIcon, YoutubeLinkIcon } from '@boolti/icon';
2+
import { MDXEditor, BoldItalicUnderlineToggles, headingsPlugin, toolbarPlugin, listsPlugin, thematicBreakPlugin, BlockTypeSelect, tablePlugin, linkPlugin, linkDialogPlugin, CreateLink, markdownShortcutPlugin, imagePlugin, InsertImage, directivesPlugin, usePublisher, insertDirective$, DialogButton, InsertThematicBreak, ListsToggle, Separator, DirectiveDescriptor, quotePlugin } from '@mdxeditor/editor';
3+
import { useUploadShowContentImage } from '@boolti/api';
4+
import { useTranslation } from 'react-i18next';
5+
6+
import Styled from './MarkdownEditor.styles';
7+
8+
const YoutubeDirectiveDescriptor: DirectiveDescriptor = {
9+
name: 'youtube',
10+
type: 'leafDirective',
11+
testNode(node) {
12+
return node.name === 'youtube'
13+
},
14+
attributes: ['id'],
15+
hasChildren: false,
16+
Editor: ({ mdastNode, lexicalNode, parentEditor }) => {
17+
return (
18+
<Styled.YoutubeEmbedContainer>
19+
<Styled.YoutubeEmbedDeleteButton
20+
onClick={() => {
21+
parentEditor.update(() => {
22+
lexicalNode.selectNext()
23+
lexicalNode.remove()
24+
})
25+
}}
26+
>
27+
<TrashIcon />
28+
</Styled.YoutubeEmbedDeleteButton>
29+
<iframe
30+
width="100%"
31+
style={{
32+
aspectRatio: '16 / 9',
33+
}}
34+
src={`https://www.youtube.com/embed/${mdastNode.attributes?.id}`}
35+
title="YouTube video player"
36+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
37+
></iframe>
38+
</Styled.YoutubeEmbedContainer>
39+
)
40+
}
41+
}
42+
43+
const InsertYoutubeVideo = () => {
44+
const insertDirective = usePublisher(insertDirective$)
45+
const { t } = useTranslation();
46+
47+
return (
48+
<DialogButton
49+
tooltipTitle={t('toolbar.youtube')}
50+
submitButtonTitle={t('youtube.urlPlaceholder')}
51+
dialogInputPlaceholder={t('youtube.urlPlaceholder')}
52+
buttonContent={<YoutubeLinkIcon />}
53+
onSubmit={(url) => {
54+
try {
55+
const videoId = new URL(url).searchParams.get('v')
56+
57+
if (videoId) {
58+
insertDirective({
59+
name: 'youtube',
60+
type: 'leafDirective',
61+
attributes: { id: videoId },
62+
})
63+
} else {
64+
throw new Error('Invalid URL')
65+
}
66+
} catch (error) {
67+
alert(t('youtube.invalidMessage'))
68+
}
69+
}}
70+
/>
71+
)
72+
}
73+
74+
interface MarkdownEditorProps {
75+
value: string;
76+
placeholder?: string;
77+
disabled?: boolean;
78+
hasError?: boolean;
79+
onChange: (value: string) => void;
80+
onBlur?: () => void;
81+
}
82+
83+
const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
84+
value, placeholder, disabled, hasError, onChange, onBlur
85+
}) => {
86+
const { t } = useTranslation();
87+
const uploadShowContentImageMutation = useUploadShowContentImage();
88+
89+
return (
90+
<Styled.MarkdownEditorContainer disabled={disabled} hasError={hasError}>
91+
<MDXEditor
92+
className="mdx-editor"
93+
contentEditableClassName="prose"
94+
markdown={value}
95+
placeholder={<Styled.MarkdownEditorPlaceholder>{placeholder}</Styled.MarkdownEditorPlaceholder>}
96+
translation={(key, _defaultValue, interpolations) => t(key, interpolations)}
97+
plugins={[
98+
toolbarPlugin({
99+
toolbarContents: () => (
100+
<>
101+
<BlockTypeSelect />
102+
<BoldItalicUnderlineToggles />
103+
<Separator />
104+
<ListsToggle options={["number", "bullet"]} />
105+
<InsertThematicBreak />
106+
<Separator />
107+
<InsertYoutubeVideo />
108+
<CreateLink />
109+
<InsertImage />
110+
</>
111+
)
112+
}),
113+
headingsPlugin({
114+
allowedHeadingLevels: [1, 2, 3],
115+
}),
116+
listsPlugin(),
117+
quotePlugin(),
118+
thematicBreakPlugin(),
119+
tablePlugin(),
120+
linkPlugin(),
121+
linkDialogPlugin(),
122+
markdownShortcutPlugin(),
123+
imagePlugin({
124+
imageUploadHandler: (file) => {
125+
const imageFile = {
126+
...file,
127+
preview: URL.createObjectURL(file),
128+
}
129+
return uploadShowContentImageMutation.mutateAsync(imageFile);
130+
},
131+
}),
132+
directivesPlugin({ directiveDescriptors: [YoutubeDirectiveDescriptor] }),
133+
]}
134+
onChange={onChange}
135+
onBlur={onBlur}
136+
/>
137+
</Styled.MarkdownEditorContainer>
138+
)
139+
}
140+
141+
export default MarkdownEditor

0 commit comments

Comments
 (0)