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 }) => (