Skip to content

Commit 8640d40

Browse files
authored
Merge pull request #8 from Moaguide-develop/feat/editor
Feat/editor
2 parents 92aad33 + 3436428 commit 8640d40

File tree

8 files changed

+192
-36
lines changed

8 files changed

+192
-36
lines changed

src/components/LoginMiddleware.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useEffect, ReactNode } from 'react';
2+
import { isLoggedIn } from '../utils/isloggedIn';
3+
4+
const LoginMiddleware: React.FC<{ children: ReactNode }> = ({ children }) => {
5+
useEffect(() => {
6+
if (!isLoggedIn() && window.location.pathname !== '/login') {
7+
window.location.href = '/login';
8+
}
9+
}, []);
10+
11+
return <>{children}</>;
12+
};
13+
14+
export default LoginMiddleware;

src/components/editor/Editor.tsx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { authors, types, categories } from '../../types/options';
66
import SelectComponent from './SelectComponent';
77
import CustomToolbar from './toolbar/CustomToolbar';
88
import { saveArticle } from '../../api/article';
9+
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
910

1011
// Tiptap 기본 확장
1112
import StarterKit from '@tiptap/starter-kit';
@@ -22,17 +23,15 @@ import Table from '@tiptap/extension-table';
2223
import TableHeader from '@tiptap/extension-table-header';
2324
import TableCell from '@tiptap/extension-table-cell';
2425
import TableRow from '@tiptap/extension-table-row';
25-
// import Image from '@tiptap/extension-image';
2626
import Link from '@tiptap/extension-link';
27-
import { getLinkOptions } from './common/Link';
28-
2927
// List Extension
3028
import ListItem from '@tiptap/extension-list-item';
3129
import Blockquote from '@tiptap/extension-blockquote';
3230
import BulletList from '@tiptap/extension-bullet-list';
3331
import OrderedList from '@tiptap/extension-ordered-list';
3432

3533
// Custom Extension
34+
import { getLinkOptions } from './common/Link';
3635
import CustomPaywall from './customComponent/CustomPaywall';
3736
import CustomPhoto from './customComponent/CustomPhoto';
3837
import CustomFile from './customComponent/CustomFile';
@@ -87,14 +86,10 @@ const Editor = ({ content }: { content: JSONContent[] | null }) => {
8786
className: 'rounded-3 border border-blue-500',
8887
mode: 'all',
8988
}),
90-
91-
// 텍스트
9289
Color.configure({ types: [TextStyle.name, ListItem.name] }),
9390
Placeholder.configure({
9491
placeholder: '내용을 입력하세요.',
9592
}),
96-
TextStyle,
97-
Underline,
9893
Highlight.configure({ multicolor: true }),
9994
TextAlign.configure({
10095
types: ['paragraph', 'image', 'blockquote', 'horizontal_rule', 'file'],
@@ -104,20 +99,41 @@ const Editor = ({ content }: { content: JSONContent[] | null }) => {
10499
class: 'border-l-3 border-gray-300 pl-4 m-6',
105100
},
106101
}),
107-
108-
// 커스텀 콘텐츠
109102
Link.configure(getLinkOptions()),
110-
// Image,
111-
CustomPhoto,
112-
CustomFile,
113-
CustomPaywall,
114103
Table.configure({
115104
resizable: true,
116105
}),
106+
TextStyle,
107+
Underline,
117108
TableHeader,
118109
TableRow,
119110
TableCell,
111+
112+
// 커스텀 콘텐츠
113+
CustomPhoto,
114+
CustomFile,
115+
CustomPaywall,
120116
],
117+
editorProps: {
118+
handlePaste(view, event) {
119+
const html = event.clipboardData?.getData('text/html');
120+
console.log(html);
121+
if (html) {
122+
const parser = new DOMParser();
123+
const doc = parser.parseFromString(html, 'text/html');
124+
const body = doc.body;
125+
126+
const fragment = ProseMirrorDOMParser.fromSchema(
127+
view.state.schema,
128+
).parse(body);
129+
130+
const transaction = view.state.tr.replaceSelectionWith(fragment);
131+
view.dispatch(transaction);
132+
return true;
133+
}
134+
return false;
135+
},
136+
},
121137
});
122138

123139
useEffect(() => {
@@ -189,7 +205,7 @@ const Editor = ({ content }: { content: JSONContent[] | null }) => {
189205
<h1 className="p-4 pl-20 border-b-2 border-b-gray-200">
190206
<input
191207
type="text"
192-
className="w-full text-2xl font-bold"
208+
className="w-full text-[40px] font-bold font-['Pretendard'] leading-[56px]"
193209
placeholder="제목"
194210
value={articleData.title}
195211
onChange={(e) =>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Node, mergeAttributes } from '@tiptap/core';
2+
3+
const CustomLinkNode = Node.create({
4+
name: 'customLink',
5+
6+
group: 'inline',
7+
8+
inline: true,
9+
10+
selectable: false,
11+
12+
addAttributes() {
13+
return {
14+
href: {
15+
default: null,
16+
},
17+
title: {
18+
default: null,
19+
},
20+
description: {
21+
default: null,
22+
},
23+
};
24+
},
25+
26+
parseHTML() {
27+
return [
28+
{
29+
tag: 'a[data-custom-link]',
30+
},
31+
];
32+
},
33+
34+
renderHTML({ HTMLAttributes }) {
35+
return ['a', mergeAttributes({ 'data-custom-link': true }, HTMLAttributes), HTMLAttributes.title];
36+
},
37+
});
38+
39+
export default CustomLinkNode;
Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,109 @@
1-
import { Node, mergeAttributes } from '@tiptap/core';
2-
import { ReactNodeViewRenderer } from '@tiptap/react';
3-
import PaywallComponent from './PaywallComponent'; // React 컴포넌트로 렌더링
1+
import { Node } from '@tiptap/core';
42

53
const CustomPaywall = Node.create({
64
name: 'paywall',
75

8-
group: 'block', // 블록 요소로 처리
9-
atom: true, // 독립적 요소
6+
group: 'block',
7+
atom: true,
108

119
addAttributes() {
1210
return {
11+
alignment: { default: 'mr-auto ml-0' },
1312
title: { default: '프리미엄 구독자 전용 콘텐츠입니다.' },
1413
description: {
1514
default: '모아가이드 구독으로 더 많은 콘텐츠를 만나보세요!',
1615
},
1716
buttonText: { default: '프리미엄 구독하기' },
1817
info: {
19-
default: '콘텐츠 이용권한이 없는 경우 여기까지만 확인 가능합니다.',
20-
},
21-
brInfo: {
22-
default: '콘텐츠 판매 설정에 따라 문구 및 버튼이 변경될 수 있습니다.',
18+
default: `콘텐츠 이용권한이 없는 경우 여기까지만 확인 가능합니다.\n콘텐츠 판매 설정에 따라 문구 및 버튼이 변경될 수 있습니다.`,
2319
},
2420
};
2521
},
2622

2723
parseHTML() {
2824
return [
2925
{
30-
tag: 'div.se_paywall',
26+
tag: 'div.se-section.se-section-custom.se-l-default.se-section-align-left',
27+
getAttrs: (element) => {
28+
const alignment = element.classList.contains(
29+
'se-section-align-center',
30+
)
31+
? 'mx-auto'
32+
: element.classList.contains('se-section-align-right')
33+
? 'ml-auto mr-0'
34+
: 'mr-auto ml-0';
35+
36+
const paywallElement = element.querySelector('.se_paywall');
37+
if (!paywallElement) {
38+
return false;
39+
}
40+
41+
const title =
42+
paywallElement.querySelector('.se_paywall_title')?.textContent ||
43+
'';
44+
const description =
45+
paywallElement.querySelector('.se_paywall_desc')?.textContent || '';
46+
const buttonText =
47+
paywallElement.querySelector('.se_paywall_subscribe')
48+
?.textContent || '';
49+
const info = Array.from(
50+
paywallElement.querySelector('.se_paywall_info')?.childNodes || [],
51+
)
52+
.map((node) => (node.nodeName === 'BR' ? '\n' : node.textContent))
53+
.join('');
54+
55+
return { alignment, title, description, buttonText, info };
56+
},
3157
},
3258
];
3359
},
3460

3561
renderHTML({ HTMLAttributes }) {
62+
const { alignment, title, description, buttonText, info } = HTMLAttributes;
63+
3664
return [
3765
'div',
38-
mergeAttributes(HTMLAttributes, { class: 'se_paywall' }),
66+
{
67+
class: `block mx-[-20px] relative ${alignment}`,
68+
},
3969
[
4070
'div',
41-
{ class: 'se_paywall_text' },
42-
['strong', { class: 'se_paywall_title' }, HTMLAttributes.title],
43-
['p', { class: 'se_paywall_desc' }, HTMLAttributes.description],
44-
['a', { class: 'se_paywall_subscribe' }, HTMLAttributes.buttonText],
71+
{
72+
class: `p-[28px_20px_0] tracking-[-0.5px] text-center`,
73+
},
74+
[
75+
'div',
76+
{ class: 'px-[11px] py-[20px] text-[#303038]' },
77+
['strong', { class: 'text-[18px] leading-[24px]' }, title],
78+
[
79+
'p',
80+
{ class: 'mt-[3px] text-[14px] leading-[20px] opacity-75' },
81+
description,
82+
],
83+
[
84+
'a',
85+
{
86+
class:
87+
'overflow-hidden block mt-[31px] px-[15px] py-[13px] text-[17px] font-semibold leading-[20px] text-[#222] tracking-[-0.5px] rounded-[3px] shadow-md border border-[rgba(255,255,255,0.09)] bg-gradient-to-r from-[#e6b459] to-[#e9a750]',
88+
},
89+
buttonText,
90+
],
91+
],
92+
[
93+
'p',
94+
{
95+
class:
96+
'pt-[20px] pb-[18px] text-[14px] leading-[20px] text-[#999] border-t border-[rgba(0,0,0,0.1)]',
97+
},
98+
...info
99+
.split('\n')
100+
.flatMap((line: string, index: number, array: string[]) =>
101+
index < array.length - 1 ? [line, ['br']] : [line],
102+
),
103+
],
45104
],
46-
['p', { class: 'se_paywall_info' }, HTMLAttributes.info],
47105
];
48106
},
49-
50-
addNodeView() {
51-
return ReactNodeViewRenderer(PaywallComponent); // React 컴포넌트와 연결
52-
},
53107
});
54108

55109
export default CustomPaywall;

src/components/editor/icons/CustomButtons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const CustomIcon = {
4343
AddLink: ({ editor }: { editor: Editor }) => (
4444
<button
4545
onClick={() => {
46-
const url = window.prompt('Enter the URL');
46+
const url = prompt('링크를 입력하세요:');
4747
if (url) {
4848
editor
4949
.chain()

src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import { createRoot } from 'react-dom/client';
33
import './index.css';
44
import App from './App.tsx';
55
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6+
import LoginMiddleware from './components/LoginMiddleware.tsx';
67

78
const queryClient = new QueryClient();
89

910
createRoot(document.getElementById('root')!).render(
1011
<StrictMode>
1112
<QueryClientProvider client={queryClient}>
12-
<App />
13+
<LoginMiddleware>
14+
<App />
15+
</LoginMiddleware>
1316
</QueryClientProvider>
1417
</StrictMode>,
1518
);

src/utils/isLoggedIn.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { getCookie } from './useCookie';
2+
export const isLoggedIn = () => {
3+
const token = getCookie('token');
4+
if (token) {
5+
return true;
6+
}
7+
return false;
8+
};

yarn.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,11 @@
808808
dependencies:
809809
tippy.js "^6.3.7"
810810

811+
"@tiptap/extension-focus@^2.11.2":
812+
version "2.11.2"
813+
resolved "https://registry.yarnpkg.com/@tiptap/extension-focus/-/extension-focus-2.11.2.tgz#bad95040e9466051add7a515c83702babf40140a"
814+
integrity sha512-BLmd7tED5FJUwyMmz8jLsok16SxBwAvavS2TwExVVCX9ABSBc3nHA9vv5sGNbTOOS9QDWYTzVH5Zq0cbgj86Gg==
815+
811816
"@tiptap/extension-gapcursor@^2.11.2":
812817
version "2.11.2"
813818
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.2.tgz#9a44175bca5eb13e5281f22c6c64d6b9e3db326e"
@@ -848,6 +853,13 @@
848853
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.11.2.tgz#f74051e6c767680bf43039b8f152736c0553f13e"
849854
integrity sha512-652oTa+iDiR7sMtmePSy+303HSNJxvxmV/6IvQoMdffJU0oPiWcWnCCL0qrWgtHh15dplj36EtB/znENWbvVOw==
850855

856+
"@tiptap/extension-link@^2.11.2":
857+
version "2.11.2"
858+
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.11.2.tgz#46096d6862a2c4f8dd18e9db9204959e4b1dbe0d"
859+
integrity sha512-Mbre+JotLMUg9jdWWrwIReiRVMkA2kMzmtD2Aqy/n5P+wuI84898qIZSkhPEzDOGzp0mluUO/iGsz0NdTto/JQ==
860+
dependencies:
861+
linkifyjs "^4.2.0"
862+
851863
"@tiptap/extension-list-item@^2.11.2":
852864
version "2.11.2"
853865
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.11.2.tgz#4b81f934bdbe98ee5e1dabdb799f2f3bf095c44e"
@@ -863,6 +875,11 @@
863875
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.11.2.tgz#b4e99129b1c5959e2926b9563ced89755e0d4d81"
864876
integrity sha512-iydTjeZbPJuqctOaAx7QebLPvz9J/hBxPptuhe4GZmqInknAk7+SFJagYeGNb14wfXKOvDZ9DMqv6mBiqSA90Q==
865877

878+
"@tiptap/extension-placeholder@^2.11.2":
879+
version "2.11.2"
880+
resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.11.2.tgz#de68ef4932141d6344b0c75f18916249c387883b"
881+
integrity sha512-7rv6nylqX57Q+K+AH794Kg9U7OrLyujhXXqQvd9iZdBP7bTCNUlFu0cGlIyHdM/eWJjoUblZs0VLV2IApk4xjQ==
882+
866883
"@tiptap/extension-strike@^2.11.2":
867884
version "2.11.2"
868885
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.11.2.tgz#9b5b698da5a024219e2ecf438c3b503d9b812fbe"
@@ -1937,6 +1954,11 @@ linkify-it@^5.0.0:
19371954
dependencies:
19381955
uc.micro "^2.0.0"
19391956

1957+
linkifyjs@^4.2.0:
1958+
version "4.2.0"
1959+
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08"
1960+
integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==
1961+
19401962
locate-path@^6.0.0:
19411963
version "6.0.0"
19421964
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"

0 commit comments

Comments
 (0)