diff --git a/src/components/LoginMiddleware.tsx b/src/components/LoginMiddleware.tsx
new file mode 100644
index 0000000..29e0cd4
--- /dev/null
+++ b/src/components/LoginMiddleware.tsx
@@ -0,0 +1,14 @@
+import { useEffect, ReactNode } from 'react';
+import { isLoggedIn } from '../utils/isloggedIn';
+
+const LoginMiddleware: React.FC<{ children: ReactNode }> = ({ children }) => {
+ useEffect(() => {
+ if (!isLoggedIn() && window.location.pathname !== '/login') {
+ window.location.href = '/login';
+ }
+ }, []);
+
+ return <>{children}>;
+};
+
+export default LoginMiddleware;
diff --git a/src/components/editor/Editor.tsx b/src/components/editor/Editor.tsx
index 7a62e62..b473a70 100644
--- a/src/components/editor/Editor.tsx
+++ b/src/components/editor/Editor.tsx
@@ -6,6 +6,7 @@ import { authors, types, categories } from '../../types/options';
import SelectComponent from './SelectComponent';
import CustomToolbar from './toolbar/CustomToolbar';
import { saveArticle } from '../../api/article';
+import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
// Tiptap 기본 확장
import StarterKit from '@tiptap/starter-kit';
@@ -22,10 +23,7 @@ import Table from '@tiptap/extension-table';
import TableHeader from '@tiptap/extension-table-header';
import TableCell from '@tiptap/extension-table-cell';
import TableRow from '@tiptap/extension-table-row';
-// import Image from '@tiptap/extension-image';
import Link from '@tiptap/extension-link';
-import { getLinkOptions } from './common/Link';
-
// List Extension
import ListItem from '@tiptap/extension-list-item';
import Blockquote from '@tiptap/extension-blockquote';
@@ -33,6 +31,7 @@ import BulletList from '@tiptap/extension-bullet-list';
import OrderedList from '@tiptap/extension-ordered-list';
// Custom Extension
+import { getLinkOptions } from './common/Link';
import CustomPaywall from './customComponent/CustomPaywall';
import CustomPhoto from './customComponent/CustomPhoto';
import CustomFile from './customComponent/CustomFile';
@@ -87,14 +86,10 @@ const Editor = ({ content }: { content: JSONContent[] | null }) => {
className: 'rounded-3 border border-blue-500',
mode: 'all',
}),
-
- // 텍스트
Color.configure({ types: [TextStyle.name, ListItem.name] }),
Placeholder.configure({
placeholder: '내용을 입력하세요.',
}),
- TextStyle,
- Underline,
Highlight.configure({ multicolor: true }),
TextAlign.configure({
types: ['paragraph', 'image', 'blockquote', 'horizontal_rule', 'file'],
@@ -104,20 +99,41 @@ const Editor = ({ content }: { content: JSONContent[] | null }) => {
class: 'border-l-3 border-gray-300 pl-4 m-6',
},
}),
-
- // 커스텀 콘텐츠
Link.configure(getLinkOptions()),
- // Image,
- CustomPhoto,
- CustomFile,
- CustomPaywall,
Table.configure({
resizable: true,
}),
+ TextStyle,
+ Underline,
TableHeader,
TableRow,
TableCell,
+
+ // 커스텀 콘텐츠
+ CustomPhoto,
+ CustomFile,
+ CustomPaywall,
],
+ editorProps: {
+ handlePaste(view, event) {
+ const html = event.clipboardData?.getData('text/html');
+ console.log(html);
+ if (html) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const body = doc.body;
+
+ const fragment = ProseMirrorDOMParser.fromSchema(
+ view.state.schema,
+ ).parse(body);
+
+ const transaction = view.state.tr.replaceSelectionWith(fragment);
+ view.dispatch(transaction);
+ return true;
+ }
+ return false;
+ },
+ },
});
useEffect(() => {
@@ -189,7 +205,7 @@ const Editor = ({ content }: { content: JSONContent[] | null }) => {
diff --git a/src/components/editor/customComponent/CustomLink.ts b/src/components/editor/customComponent/CustomLink.ts
new file mode 100644
index 0000000..fc17df3
--- /dev/null
+++ b/src/components/editor/customComponent/CustomLink.ts
@@ -0,0 +1,39 @@
+import { Node, mergeAttributes } from '@tiptap/core';
+
+const CustomLinkNode = Node.create({
+ name: 'customLink',
+
+ group: 'inline',
+
+ inline: true,
+
+ selectable: false,
+
+ addAttributes() {
+ return {
+ href: {
+ default: null,
+ },
+ title: {
+ default: null,
+ },
+ description: {
+ default: null,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'a[data-custom-link]',
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['a', mergeAttributes({ 'data-custom-link': true }, HTMLAttributes), HTMLAttributes.title];
+ },
+});
+
+export default CustomLinkNode;
diff --git a/src/components/editor/customComponent/CustomPaywall.ts b/src/components/editor/customComponent/CustomPaywall.ts
index 6a5aa3a..bc3877f 100644
--- a/src/components/editor/customComponent/CustomPaywall.ts
+++ b/src/components/editor/customComponent/CustomPaywall.ts
@@ -1,25 +1,21 @@
-import { Node, mergeAttributes } from '@tiptap/core';
-import { ReactNodeViewRenderer } from '@tiptap/react';
-import PaywallComponent from './PaywallComponent'; // React 컴포넌트로 렌더링
+import { Node } from '@tiptap/core';
const CustomPaywall = Node.create({
name: 'paywall',
- group: 'block', // 블록 요소로 처리
- atom: true, // 독립적 요소
+ group: 'block',
+ atom: true,
addAttributes() {
return {
+ alignment: { default: 'mr-auto ml-0' },
title: { default: '프리미엄 구독자 전용 콘텐츠입니다.' },
description: {
default: '모아가이드 구독으로 더 많은 콘텐츠를 만나보세요!',
},
buttonText: { default: '프리미엄 구독하기' },
info: {
- default: '콘텐츠 이용권한이 없는 경우 여기까지만 확인 가능합니다.',
- },
- brInfo: {
- default: '콘텐츠 판매 설정에 따라 문구 및 버튼이 변경될 수 있습니다.',
+ default: `콘텐츠 이용권한이 없는 경우 여기까지만 확인 가능합니다.\n콘텐츠 판매 설정에 따라 문구 및 버튼이 변경될 수 있습니다.`,
},
};
},
@@ -27,29 +23,87 @@ const CustomPaywall = Node.create({
parseHTML() {
return [
{
- tag: 'div.se_paywall',
+ tag: 'div.se-section.se-section-custom.se-l-default.se-section-align-left',
+ getAttrs: (element) => {
+ const alignment = element.classList.contains(
+ 'se-section-align-center',
+ )
+ ? 'mx-auto'
+ : element.classList.contains('se-section-align-right')
+ ? 'ml-auto mr-0'
+ : 'mr-auto ml-0';
+
+ const paywallElement = element.querySelector('.se_paywall');
+ if (!paywallElement) {
+ return false;
+ }
+
+ const title =
+ paywallElement.querySelector('.se_paywall_title')?.textContent ||
+ '';
+ const description =
+ paywallElement.querySelector('.se_paywall_desc')?.textContent || '';
+ const buttonText =
+ paywallElement.querySelector('.se_paywall_subscribe')
+ ?.textContent || '';
+ const info = Array.from(
+ paywallElement.querySelector('.se_paywall_info')?.childNodes || [],
+ )
+ .map((node) => (node.nodeName === 'BR' ? '\n' : node.textContent))
+ .join('');
+
+ return { alignment, title, description, buttonText, info };
+ },
},
];
},
renderHTML({ HTMLAttributes }) {
+ const { alignment, title, description, buttonText, info } = HTMLAttributes;
+
return [
'div',
- mergeAttributes(HTMLAttributes, { class: 'se_paywall' }),
+ {
+ class: `block mx-[-20px] relative ${alignment}`,
+ },
[
'div',
- { class: 'se_paywall_text' },
- ['strong', { class: 'se_paywall_title' }, HTMLAttributes.title],
- ['p', { class: 'se_paywall_desc' }, HTMLAttributes.description],
- ['a', { class: 'se_paywall_subscribe' }, HTMLAttributes.buttonText],
+ {
+ class: `p-[28px_20px_0] tracking-[-0.5px] text-center`,
+ },
+ [
+ 'div',
+ { class: 'px-[11px] py-[20px] text-[#303038]' },
+ ['strong', { class: 'text-[18px] leading-[24px]' }, title],
+ [
+ 'p',
+ { class: 'mt-[3px] text-[14px] leading-[20px] opacity-75' },
+ description,
+ ],
+ [
+ 'a',
+ {
+ class:
+ '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]',
+ },
+ buttonText,
+ ],
+ ],
+ [
+ 'p',
+ {
+ class:
+ 'pt-[20px] pb-[18px] text-[14px] leading-[20px] text-[#999] border-t border-[rgba(0,0,0,0.1)]',
+ },
+ ...info
+ .split('\n')
+ .flatMap((line: string, index: number, array: string[]) =>
+ index < array.length - 1 ? [line, ['br']] : [line],
+ ),
+ ],
],
- ['p', { class: 'se_paywall_info' }, HTMLAttributes.info],
];
},
-
- addNodeView() {
- return ReactNodeViewRenderer(PaywallComponent); // React 컴포넌트와 연결
- },
});
export default CustomPaywall;
diff --git a/src/components/editor/icons/CustomButtons.tsx b/src/components/editor/icons/CustomButtons.tsx
index 6331e0f..8e5f6df 100644
--- a/src/components/editor/icons/CustomButtons.tsx
+++ b/src/components/editor/icons/CustomButtons.tsx
@@ -43,7 +43,7 @@ const CustomIcon = {
AddLink: ({ editor }: { editor: Editor }) => (