diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index 326bf0cc..31b68e75 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -14,7 +14,7 @@ model User {
password String @db.VarChar(100)
nickname String @unique @db.VarChar(20)
description String @db.VarChar(100)
- profile_image String @default("https://kr.object.ncloudstorage.com/j027/522da4f3-c9d7-403d-a98f-2b09cabefc47.png") @db.VarChar(255)
+ profile_image String @default("https://kr.object.ncloudstorage.com/j027/20e92b3e-af66-4eab-8272-a40ea9212930.png") @db.VarChar(255)
provider String @db.VarChar(20)
created_at DateTime @default(now())
deleted_at DateTime?
diff --git a/backend/src/apis/articles/articles.controller.ts b/backend/src/apis/articles/articles.controller.ts
index c213bd21..62ad9a0d 100644
--- a/backend/src/apis/articles/articles.controller.ts
+++ b/backend/src/apis/articles/articles.controller.ts
@@ -7,9 +7,19 @@ import scrapsService from '@apis/scraps/scraps.service';
import { Forbidden, Message } from '@errors';
const searchArticles = async (req: Request, res: Response) => {
- const { query, page, take, userId } = req.query as unknown as SearchArticles;
+ const { query, page, take, isUsers } = req.query as unknown as SearchArticles;
- const searchResult = await articlesService.searchArticles({ query, page, take: +take, userId });
+ let userId = res.locals.user?.id;
+
+ if (!userId) userId = 0;
+
+ const searchResult = await articlesService.searchArticles({
+ query,
+ page,
+ take: +take,
+ isUsers,
+ userId,
+ });
return res.status(200).send(searchResult);
};
@@ -78,9 +88,9 @@ const updateArticle = async (req: Request, res: Response) => {
const deleteArticle = async (req: Request, res: Response) => {
const articleId = Number(req.params.articleId);
- await articlesService.deleteArticle(articleId);
+ const article = await articlesService.deleteArticle(articleId);
- return res.status(204).send();
+ return res.status(200).send(article);
};
const getTemporaryArticle = async (req: Request, res: Response) => {
diff --git a/backend/src/apis/articles/articles.interface.ts b/backend/src/apis/articles/articles.interface.ts
index 5abd0f1b..e8195302 100644
--- a/backend/src/apis/articles/articles.interface.ts
+++ b/backend/src/apis/articles/articles.interface.ts
@@ -2,7 +2,8 @@ export interface SearchArticles {
query: string;
page: number;
take: number;
- userId: number;
+ userId?: number;
+ isUsers?: string;
}
export interface CreateArticle {
diff --git a/backend/src/apis/articles/articles.service.ts b/backend/src/apis/articles/articles.service.ts
index 89899603..5d331649 100644
--- a/backend/src/apis/articles/articles.service.ts
+++ b/backend/src/apis/articles/articles.service.ts
@@ -6,19 +6,20 @@ import {
import { prisma } from '@config/orm.config';
const searchArticles = async (searchArticles: SearchArticles) => {
- const { query, page, take, userId } = searchArticles;
+ const { query, page, take, userId, isUsers } = searchArticles;
const skip = (page - 1) * take;
- const matchUserCondition = Number(userId)
- ? {
- book: {
- user: {
- id: Number(userId),
+ const matchUserCondition =
+ isUsers === 'true'
+ ? {
+ book: {
+ user: {
+ id: Number(userId),
+ },
},
- },
- }
- : {};
+ }
+ : {};
const articles = await prisma.article.findMany({
select: {
@@ -112,7 +113,7 @@ const createArticle = async (dto: CreateArticle) => {
};
const deleteArticle = async (articleId: number) => {
- await prisma.article.update({
+ const article = await prisma.article.update({
where: {
id: articleId,
},
@@ -120,6 +121,8 @@ const deleteArticle = async (articleId: number) => {
deleted_at: new Date(),
},
});
+
+ return article;
};
const getTemporaryArticle = async (userId: number) => {
diff --git a/backend/src/apis/books/books.controller.ts b/backend/src/apis/books/books.controller.ts
index dc94588a..3b28a0f1 100644
--- a/backend/src/apis/books/books.controller.ts
+++ b/backend/src/apis/books/books.controller.ts
@@ -31,9 +31,19 @@ const getBooks = async (req: Request, res: Response) => {
};
const searchBooks = async (req: Request, res: Response) => {
- const { query, page, take, userId } = req.query as unknown as SearchBooks;
+ const { query, page, take, isUsers } = req.query as unknown as SearchBooks;
- const searchResult = await booksService.searchBooks({ query, userId, take: +take, page });
+ let userId = res.locals.user?.id;
+
+ if (!userId) userId = 0;
+
+ const searchResult = await booksService.searchBooks({
+ query,
+ isUsers,
+ userId,
+ take: +take,
+ page,
+ });
return res.status(200).send(searchResult);
};
diff --git a/backend/src/apis/books/books.interface.ts b/backend/src/apis/books/books.interface.ts
index 6d58090a..979b6daf 100644
--- a/backend/src/apis/books/books.interface.ts
+++ b/backend/src/apis/books/books.interface.ts
@@ -3,6 +3,7 @@ export interface SearchBooks {
page: number;
take: number;
userId?: number;
+ isUsers?: string;
}
export interface FindBooks {
diff --git a/backend/src/apis/books/books.service.ts b/backend/src/apis/books/books.service.ts
index 15965734..8652ce5c 100644
--- a/backend/src/apis/books/books.service.ts
+++ b/backend/src/apis/books/books.service.ts
@@ -2,7 +2,7 @@ import { FindBooks, SearchBooks, CreateBook } from '@apis/books/books.interface'
import { prisma } from '@config/orm.config';
import { Message, NotFound } from '@errors';
-const searchBooks = async ({ query, userId, take, page }: SearchBooks) => {
+const searchBooks = async ({ query, userId, isUsers, take, page }: SearchBooks) => {
const skip = (page - 1) * take;
const books = await prisma.book.findMany({
@@ -40,7 +40,7 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => {
},
where: {
deleted_at: null,
- user_id: Number(userId) ? Number(userId) : undefined,
+ user_id: isUsers === 'true' ? Number(userId) : undefined,
title: {
search: `${query}*`,
},
diff --git a/backend/src/apis/index.ts b/backend/src/apis/index.ts
index 172ca29a..3f179cbd 100644
--- a/backend/src/apis/index.ts
+++ b/backend/src/apis/index.ts
@@ -10,6 +10,7 @@ import imagesController from '@apis/images/images.controller';
import scrapsController from '@apis/scraps/scraps.controller';
import usersController from '@apis/users/users.controller';
import decoder from '@middlewares/tokenDecoder';
+import { tokenErrorHandler } from '@middlewares/tokenErrorHandler';
import guard from '@middlewares/tokenValidator';
import { catchAsync } from '@utils/catch-async';
@@ -19,19 +20,19 @@ router.post('/auth/signin/local', catchAsync(authController.signIn));
router.post('/auth/signin/github', catchAsync(authController.signInGithub));
router.post('/auth/signup', catchAsync(authController.signUp));
router.get('/auth/signout', catchAsync(authController.signOut));
-router.get('/auth', decoder, catchAsync(authController.checkSignInStatus));
+router.get('/auth', guard, tokenErrorHandler, catchAsync(authController.checkSignInStatus));
router.get('/articles/temporary', guard, catchAsync(articlesController.getTemporaryArticle));
router.post('/articles/temporary', guard, catchAsync(articlesController.createTemporaryArticle));
-router.get('/articles/search', catchAsync(articlesController.searchArticles));
+router.get('/articles/search', decoder, catchAsync(articlesController.searchArticles));
router.get('/articles/:articleId', catchAsync(articlesController.getArticle));
-router.post('/articles', catchAsync(articlesController.createArticle));
-router.patch('/articles/:articleId', catchAsync(articlesController.updateArticle));
-router.delete('/articles/:articleId', catchAsync(articlesController.deleteArticle));
+router.post('/articles', guard, catchAsync(articlesController.createArticle));
+router.patch('/articles/:articleId', guard, catchAsync(articlesController.updateArticle));
+router.delete('/articles/:articleId', guard, catchAsync(articlesController.deleteArticle));
-router.post('/image', multer().single('image'), catchAsync(imagesController.createImage));
+router.post('/image', guard, multer().single('image'), catchAsync(imagesController.createImage));
-router.get('/books/search', catchAsync(booksController.searchBooks));
+router.get('/books/search', decoder, catchAsync(booksController.searchBooks));
router.get('/books/:bookId', decoder, catchAsync(booksController.getBook));
router.get('/books', decoder, catchAsync(booksController.getBooks));
router.post('/books', guard, catchAsync(booksController.createBook));
@@ -39,14 +40,14 @@ router.patch('/books', guard, catchAsync(booksController.updateBook));
router.delete('/books/:bookId', guard, catchAsync(booksController.deleteBook));
router.post('/bookmarks', guard, catchAsync(bookmarksController.createBookmark));
-router.delete('/bookmarks/:bookmarkId', catchAsync(bookmarksController.deleteBookmark));
+router.delete('/bookmarks/:bookmarkId', guard, catchAsync(bookmarksController.deleteBookmark));
router.get('/scraps', catchAsync(scrapsController.getScraps));
-router.patch('/scraps', catchAsync(guard), catchAsync(scrapsController.updateScrapsOrder));
-router.post('/scraps', catchAsync(scrapsController.createScrap));
+router.patch('/scraps', guard, catchAsync(scrapsController.updateScrapsOrder));
+router.post('/scraps', guard, catchAsync(scrapsController.createScrap));
router.delete('/scraps/:scrapId', guard, catchAsync(scrapsController.deleteScrap));
router.get('/users', catchAsync(usersController.getUserProfile));
-router.patch('/users/:userId', catchAsync(usersController.editUserProfile));
+router.patch('/users/:userId', guard, catchAsync(usersController.editUserProfile));
export default router;
diff --git a/backend/src/apis/scraps/scraps.controller.ts b/backend/src/apis/scraps/scraps.controller.ts
index 99307850..076de041 100644
--- a/backend/src/apis/scraps/scraps.controller.ts
+++ b/backend/src/apis/scraps/scraps.controller.ts
@@ -26,10 +26,11 @@ const createScrap = async (req: Request, res: Response) => {
const deleteScrap = async (req: Request, res: Response) => {
const scrapId = Number(req.params.scrapId);
- await scrapsService.deleteScrap(scrapId);
+ const scrap = await scrapsService.deleteScrap(scrapId);
- return res.status(200).send();
+ return res.status(200).send(scrap);
};
+
const getScraps = async (req: Request, res: Response) => {
const scraps = await scrapsService.getScraps();
diff --git a/backend/src/apis/scraps/scraps.service.ts b/backend/src/apis/scraps/scraps.service.ts
index 14cbfb6b..3d6e54a3 100644
--- a/backend/src/apis/scraps/scraps.service.ts
+++ b/backend/src/apis/scraps/scraps.service.ts
@@ -53,11 +53,13 @@ const updateScrapOrder = async (scraps: IScrap) => {
};
const deleteScrap = async (scrapId: number) => {
- await prisma.scrap.delete({
+ const scrap = await prisma.scrap.delete({
where: {
id: scrapId,
},
});
+
+ return scrap;
};
const getScraps = async () => {
diff --git a/backend/src/middlewares/tokenErrorHandler.ts b/backend/src/middlewares/tokenErrorHandler.ts
new file mode 100644
index 00000000..1e21c5dc
--- /dev/null
+++ b/backend/src/middlewares/tokenErrorHandler.ts
@@ -0,0 +1,7 @@
+import { ErrorRequestHandler } from 'express';
+
+export const tokenErrorHandler: ErrorRequestHandler = (err, req, res, next) => {
+ if (!err) return next();
+
+ return res.status(200).send({ id: 0 });
+};
diff --git a/frontend/apis/articleApi.ts b/frontend/apis/articleApi.ts
index d817a6ba..866f1c38 100644
--- a/frontend/apis/articleApi.ts
+++ b/frontend/apis/articleApi.ts
@@ -2,7 +2,7 @@ import api from '@utils/api';
interface SearchArticlesApi {
query: string;
- userId: number;
+ isUsers: boolean;
page: number;
take: number;
}
@@ -11,7 +11,7 @@ export const searchArticlesApi = async (data: SearchArticlesApi) => {
const url = `/api/articles/search`;
const params = {
query: data.query,
- userId: data.userId,
+ isUsers: data.isUsers,
page: data.page,
take: data.take,
};
@@ -52,7 +52,7 @@ export const modifyArticleApi = async (articleId: number, data: CreateArticleApi
return response.data;
};
-export const deleteArticleApi = async (articleId: string) => {
+export const deleteArticleApi = async (articleId: number) => {
const url = `/api/articles/${articleId}`;
const response = await api({ url, method: 'DELETE' });
diff --git a/frontend/apis/bookApi.ts b/frontend/apis/bookApi.ts
index 1ccf0e09..988fd8b3 100644
--- a/frontend/apis/bookApi.ts
+++ b/frontend/apis/bookApi.ts
@@ -3,7 +3,7 @@ import api from '@utils/api';
interface SearchBooksApi {
query: string;
- userId: number;
+ isUsers: boolean;
page: number;
take: number;
}
@@ -12,7 +12,7 @@ export const searchBooksApi = async (data: SearchBooksApi) => {
const url = `/api/books/search`;
const params = {
query: data.query,
- userId: data.userId,
+ isUsers: data.isUsers,
page: data.page,
take: data.take,
};
diff --git a/frontend/components/common/Book/styled.ts b/frontend/components/common/Book/styled.ts
index e82fa156..27a88bff 100644
--- a/frontend/components/common/Book/styled.ts
+++ b/frontend/components/common/Book/styled.ts
@@ -18,24 +18,24 @@ export const BookWrapper = styled(FlexColumn)`
overflow: hidden;
color: var(--grey-01-color);
- aspect-ratio: 280/480;
- @media ${(props) => props.theme.tablet} {
- width: 100%;
- height: auto;
- overflow: none;
- }
+ // aspect-ratio: 280/480;
+ // @media ${(props) => props.theme.tablet} {
+ // width: 100%;
+ // height: auto;
+ // overflow: none;
+ // }
`;
export const BookThumbnail = styled(Image)`
width: 280px;
min-height: 200px;
object-fit: cover;
- aspect-ratio: 280/200;
+ // aspect-ratio: 280/200;
- @media ${(props) => props.theme.tablet} {
- width: 100%;
- min-height: auto;
- }
+ // @media ${(props) => props.theme.tablet} {
+ // width: 100%;
+ // min-height: auto;
+ // }
`;
export const BookInfoContainer = styled(FlexColumn)`
diff --git a/frontend/components/common/Content/index.tsx b/frontend/components/common/Content/index.tsx
index c62932a2..f1569cee 100644
--- a/frontend/components/common/Content/index.tsx
+++ b/frontend/components/common/Content/index.tsx
@@ -1,3 +1,5 @@
+import { markdown2html } from '@utils/parser';
+
import { ContentBody, ContentTitle, ContentWrapper } from './styled';
import 'highlight.js/styles/github.css';
@@ -13,7 +15,7 @@ export default function Content({ title, content }: ContentProps) {
{title && {title}}
diff --git a/frontend/components/common/Content/styled.ts b/frontend/components/common/Content/styled.ts
index 14e8e97d..e9f55c85 100644
--- a/frontend/components/common/Content/styled.ts
+++ b/frontend/components/common/Content/styled.ts
@@ -56,7 +56,11 @@ export const ContentBody = styled.div`
p {
img {
- width: 100%;
+ max-width: 720px;
+
+ @media ${(props) => props.theme.mobile} {
+ width: 100%;
+ }
}
}
diff --git a/frontend/components/common/DragDrop/Container/index.tsx b/frontend/components/common/DragDrop/Container/index.tsx
index e1c50ef8..11849e36 100644
--- a/frontend/components/common/DragDrop/Container/index.tsx
+++ b/frontend/components/common/DragDrop/Container/index.tsx
@@ -1,11 +1,10 @@
-import { useEffect, memo, useCallback } from 'react';
+import { memo, useCallback } from 'react';
import { useDrop } from 'react-dnd';
import update from 'immutability-helper';
import { useRecoilState } from 'recoil';
import scrapState from '@atoms/scrap';
-import { IScrap } from '@interfaces';
import { ListItem } from '../ListItem';
import ContainerWapper from './styled';
@@ -15,22 +14,15 @@ const ItemTypes = {
};
export interface ContainerState {
- data: IScrap[];
isContentsShown: boolean;
isDeleteBtnShown: boolean;
}
const DragContainer = memo(function Container({
- data,
isContentsShown,
isDeleteBtnShown,
}: ContainerState) {
const [scraps, setScraps] = useRecoilState(scrapState);
- useEffect(() => {
- if (!data) return;
- setScraps(data);
- }, []);
-
const findScrap = useCallback(
(id: number) => {
const scrap = scraps.filter((c) => c.article.id === id)[0];
diff --git a/frontend/components/common/DragDrop/ListItem/index.tsx b/frontend/components/common/DragDrop/ListItem/index.tsx
index 08d0e279..143c7ecd 100644
--- a/frontend/components/common/DragDrop/ListItem/index.tsx
+++ b/frontend/components/common/DragDrop/ListItem/index.tsx
@@ -82,8 +82,6 @@ export const ListItem = memo(function Scrap({
);
const handleMinusBtnClick = () => {
- // 원본글이 아니면 스크랩에서만 삭제
- // 원본글이면 실제로 삭제
if (isOriginal) {
if (window.confirm('이 글은 원본 글입니다. 정말로 삭제하시겠습니까?')) {
setEditInfo({
diff --git a/frontend/components/common/DragDrop/ListItem/styled.ts b/frontend/components/common/DragDrop/ListItem/styled.ts
index d6be6f76..dbee668e 100644
--- a/frontend/components/common/DragDrop/ListItem/styled.ts
+++ b/frontend/components/common/DragDrop/ListItem/styled.ts
@@ -16,6 +16,7 @@ export const Article = styled.div<{ isShown: true | false }>`
align-items: center;
border-bottom: 1px solid var(--grey-02-color);
padding: 5px;
+ cursor: pointer;
`;
export const MinusButton = styled.div`
diff --git a/frontend/components/common/DragDrop/index.tsx b/frontend/components/common/DragDrop/index.tsx
index ed4d57d7..73be9ce4 100644
--- a/frontend/components/common/DragDrop/index.tsx
+++ b/frontend/components/common/DragDrop/index.tsx
@@ -13,19 +13,14 @@ export interface EditScrap {
};
}
export interface ContainerState {
- data: EditScrap[];
isContentsShown: boolean;
isDeleteBtnShown: boolean;
}
-export default function DragArticle({ data, isContentsShown, isDeleteBtnShown }: ContainerState) {
+export default function DragArticle({ isContentsShown, isDeleteBtnShown }: ContainerState) {
return (
-
+
);
}
diff --git a/frontend/components/common/LabeledInput/styled.ts b/frontend/components/common/LabeledInput/styled.ts
index 4cc7f700..7c297653 100644
--- a/frontend/components/common/LabeledInput/styled.ts
+++ b/frontend/components/common/LabeledInput/styled.ts
@@ -13,4 +13,14 @@ export const Input = styled.input`
font-size: 16px;
border: 1px solid var(--grey-02-color);
border-radius: 10px;
+
+ ::placeholder {
+ font-size: 14px;
+ }
+
+ @media ${(props) => props.theme.mobile} {
+ ::placeholder {
+ font-size: 12px;
+ }
+ }
`;
diff --git a/frontend/components/edit/EditBar/index.tsx b/frontend/components/edit/EditBar/index.tsx
index 18127792..04fa80c2 100644
--- a/frontend/components/edit/EditBar/index.tsx
+++ b/frontend/components/edit/EditBar/index.tsx
@@ -1,3 +1,4 @@
+import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
@@ -5,9 +6,11 @@ import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { createTemporaryArticleApi, getTemporaryArticleApi } from '@apis/articleApi';
+import ExitIcon from '@assets/ico_exit.svg';
import articleState from '@atoms/article';
import articleBuffer from '@atoms/articleBuffer';
import useFetch from '@hooks/useFetch';
+import { toastSuccess } from '@utils/toast';
import { Bar, ButtonGroup, ExitButton, PublishButton, TemporaryButton } from './styled';
@@ -25,11 +28,19 @@ export default function EditBar({ handleModalOpen, isModifyMode }: EditBarProps)
const { execute: createTemporaryArticle } = useFetch(createTemporaryArticleApi);
const handleLoadButton = () => {
- getTemporaryArticle();
+ const confirm = window.confirm('현재 작성하신 글이 사라집니다.\n정말 불러오시겠습니까?');
+
+ if (confirm) getTemporaryArticle();
};
const handleSaveButton = () => {
- createTemporaryArticle({ title: article.title, content: article.content });
+ const confirm = window.confirm('기존에 임시 저장한 글이 사라집니다.\n정말 저장하시겠습니까?');
+
+ if (confirm) {
+ createTemporaryArticle({ title: article.title, content: article.content });
+
+ toastSuccess('글을 임시 저장했습니다.');
+ }
};
const handleExitButton = () => {
@@ -45,20 +56,22 @@ export default function EditBar({ handleModalOpen, isModifyMode }: EditBarProps)
title: temporaryArticle.title,
content: temporaryArticle.content,
});
+
+ toastSuccess('임시 저장된 글을 불러왔습니다.');
}, [temporaryArticle]);
return (
handleExitButton()}>
- 나가기
+
handleLoadButton()}>불러오기
- handleSaveButton()}>저장
+ handleSaveButton()}>저장하기
- {isModifyMode ? '수정하기' : '발행'}
+ {isModifyMode ? '수정하기' : '발행하기'}
diff --git a/frontend/components/edit/EditBar/styled.ts b/frontend/components/edit/EditBar/styled.ts
index d4d5dc70..6e90e5c6 100644
--- a/frontend/components/edit/EditBar/styled.ts
+++ b/frontend/components/edit/EditBar/styled.ts
@@ -20,15 +20,17 @@ export const ButtonGroup = styled.div`
`;
const Button = styled.button`
- padding: 8px 16px;
- border-radius: 10px;
+ padding: 4px;
+ border-radius: 8px;
+ line-height: 16px;
font-family: 'Noto Sans KR';
`;
-export const ExitButton = styled(Button)`
- color: var(--title-active-color);
- background-color: var(--light-orange-color);
- border: 1px solid var(--grey-01-color);
+export const ExitButton = styled.button`
+ > img {
+ width: 32px;
+ height: 32px;
+ }
`;
export const TemporaryButton = styled(Button)`
diff --git a/frontend/components/edit/Editor/index.tsx b/frontend/components/edit/Editor/index.tsx
index 90b3d8cd..99fab747 100644
--- a/frontend/components/edit/Editor/index.tsx
+++ b/frontend/components/edit/Editor/index.tsx
@@ -20,7 +20,6 @@ import EditBar from '@components/edit/EditBar';
import useCodeMirror from '@components/edit/Editor/core/useCodeMirror';
import useInput from '@hooks/useInput';
import { IArticle } from '@interfaces';
-import { html2markdown, markdown2html } from '@utils/parser';
import {
EditorButtonWrapper,
@@ -68,7 +67,7 @@ export default function Editor({ handleModalOpen, originalArticle }: EditorProps
if (!buffer.title && !buffer.content) return;
title.setValue(buffer.title);
- replaceDocument(html2markdown(buffer.content));
+ replaceDocument(buffer.content);
setBuffer({ title: '', content: '' });
}, [buffer]);
@@ -77,7 +76,7 @@ export default function Editor({ handleModalOpen, originalArticle }: EditorProps
setArticle({
...article,
title: title.value,
- content: markdown2html(document),
+ content: document,
});
}, [title.value, document]);
diff --git a/frontend/components/edit/ModifyModal/index.tsx b/frontend/components/edit/ModifyModal/index.tsx
index 8c56b093..0ae32357 100644
--- a/frontend/components/edit/ModifyModal/index.tsx
+++ b/frontend/components/edit/ModifyModal/index.tsx
@@ -12,6 +12,7 @@ import Dropdown from '@components/common/Dropdown';
import ModalButton from '@components/common/Modal/ModalButton';
import useFetch from '@hooks/useFetch';
import { IArticle, IBook, IBookScraps, IScrap } from '@interfaces';
+import { toastSuccess } from '@utils/toast';
import { ArticleWrapper, Label, ModifyModalWrapper, WarningLabel } from './styled';
@@ -95,7 +96,11 @@ export default function ModifyModal({ books, originalArticle }: ModifyModalProps
};
useEffect(() => {
- if (modifiedArticle) router.push('/');
+ if (modifiedArticle) {
+ const { id, title } = modifiedArticle.modifiedArticle;
+ router.push(`/viewer/${selectedBookIndex}/${id}`);
+ toastSuccess(`${title}글이 수정되었습니다.`);
+ }
}, [modifiedArticle]);
return (
@@ -113,11 +118,7 @@ export default function ModifyModal({ books, originalArticle }: ModifyModalProps
{filteredScraps.length !== 0 && (
-
+
)}
diff --git a/frontend/components/edit/PublishModal/index.tsx b/frontend/components/edit/PublishModal/index.tsx
index bb7de33c..95460e5d 100644
--- a/frontend/components/edit/PublishModal/index.tsx
+++ b/frontend/components/edit/PublishModal/index.tsx
@@ -12,6 +12,7 @@ import Dropdown from '@components/common/Dropdown';
import ModalButton from '@components/common/Modal/ModalButton';
import useFetch from '@hooks/useFetch';
import { IBook, IBookScraps, IScrap } from '@interfaces';
+import { toastSuccess } from '@utils/toast';
import { ArticleWrapper, Label, PublishModalWrapper } from './styled';
@@ -73,7 +74,11 @@ export default function PublishModal({ books }: PublishModalProps) {
};
useEffect(() => {
- if (createdArticle) router.push('/');
+ if (createdArticle) {
+ const { id, title } = createdArticle.createdArticle;
+ router.push(`/viewer/${selectedBookIndex}/${id}`);
+ toastSuccess(`${title}글이 발행되었습니다.`);
+ }
}, [createdArticle]);
return (
@@ -89,11 +94,7 @@ export default function PublishModal({ books }: PublishModalProps) {
{filteredScraps.length !== 0 && (
-
+
)}
diff --git a/frontend/components/home/Slider/index.tsx b/frontend/components/home/Slider/index.tsx
index 8f17cd51..f5dc184e 100644
--- a/frontend/components/home/Slider/index.tsx
+++ b/frontend/components/home/Slider/index.tsx
@@ -1,13 +1,15 @@
import Image from 'next/image';
-import { useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import LeftArrowIcon from '@assets/ico_arrow_left.svg';
import RightArrowIcon from '@assets/ico_arrow_right.svg';
import ListIcon from '@assets/ico_flower.svg';
import Book from '@components/common/Book';
import SkeletonBook from '@components/common/SkeletonBook';
+import useSessionStorage from '@hooks/useSessionStorage';
import { IBookScraps } from '@interfaces';
+import { Flex } from '@styles/layout';
import {
SliderContent,
@@ -37,8 +39,19 @@ const setNumBetween = (val: number, min: number, max: number) => {
};
function Slider({ bookList, title, isLoading, numberPerPage }: SliderProps) {
- const [curBookIndex, setCurBookIndex] = useState(0);
- const [sliderNumber, setSliderNumber] = useState(1);
+ const {
+ value: curBookIndex,
+ isValueSet: isCurBookIndexSet,
+ setValue: setCurBookIndex,
+ } = useSessionStorage(`${title}_curBookIndex`, 0);
+
+ const {
+ value: sliderNumber,
+ isValueSet: isSliderNumberSet,
+ setValue: setSliderNumber,
+ } = useSessionStorage(`${title}_sliderNumber`, 1);
+
+ const [touchPositionX, setTouchPositionX] = useState(0);
const SkeletonList = Array.from({ length: numberPerPage }, (_, i) => i + 1);
@@ -55,6 +68,20 @@ function Slider({ bookList, title, isLoading, numberPerPage }: SliderProps) {
setSliderNumber(sliderNumber + 1);
};
+ const handleSliderTrackTouchStart = (e: React.TouchEvent) => {
+ setTouchPositionX(e.changedTouches[0].pageX);
+ };
+
+ const handleSliderTrackTouchEnd = (e: React.TouchEvent) => {
+ const distanceX = touchPositionX - e.changedTouches[0].pageX;
+ if (distanceX > 30 && sliderNumber !== sliderIndicatorCount) {
+ handleRightArrowClick();
+ }
+ if (distanceX < -30 && sliderNumber !== 1) {
+ handleLeftArrowClick();
+ }
+ };
+
useEffect(() => {
if (!bookList) return;
@@ -93,15 +120,25 @@ function Slider({ bookList, title, isLoading, numberPerPage }: SliderProps) {
-
- {isLoading
- ? SkeletonList.map((key) => )
- : bookList.map((book) => (
-
-
-
- ))}
-
+ {!isLoading && isCurBookIndexSet && isSliderNumberSet ? (
+
+ {bookList.map((book) => (
+
+
+
+ ))}
+
+ ) : (
+
+ {SkeletonList.map((key) => (
+
+ ))}
+
+ )}
diff --git a/frontend/components/home/Slider/styled.ts b/frontend/components/home/Slider/styled.ts
index eee8827c..40fa5bf7 100644
--- a/frontend/components/home/Slider/styled.ts
+++ b/frontend/components/home/Slider/styled.ts
@@ -51,7 +51,7 @@ export const SliderBookContainer = styled.div`
export const SliderTrack = styled.div<{ curBookIndex: number }>`
display: flex;
${(props) => `transform: translateX(-${300 * props.curBookIndex}px);`}
- transition: transform 700ms ease 0ms;
+ transition: transform 500ms ease 0ms;
`;
export const SliderIndicatorContainer = styled.div`
diff --git a/frontend/components/search/ArticleList/index.tsx b/frontend/components/search/ArticleList/index.tsx
index e59f4919..55779c4b 100644
--- a/frontend/components/search/ArticleList/index.tsx
+++ b/frontend/components/search/ArticleList/index.tsx
@@ -1,5 +1,6 @@
import ArticleItem from '@components/search/ArticleItem';
import { IArticleBook } from '@interfaces';
+import { markdown2text } from '@utils/parser';
interface ArticleListProps {
articles: IArticleBook[];
@@ -24,7 +25,7 @@ export default function ArticleList({ articles, keywords }: ArticleListProps) {
let paddingIndex = 0;
if (isFirst) {
- const regex = /(<([^>]+)>)/g;
+ const regex = /\n/g;
while (regex.test(text.slice(0, startIndex))) paddingIndex = regex.lastIndex;
}
@@ -33,7 +34,7 @@ export default function ArticleList({ articles, keywords }: ArticleListProps) {
<>
{text.slice(paddingIndex, startIndex)}
{text.slice(startIndex, endIndex)}
- {highlightWord(text.slice(endIndex).replace(/(<([^>]+)>)/gi, ''), words)}
+ {highlightWord(text.slice(endIndex), words)}
>
);
};
@@ -44,7 +45,7 @@ export default function ArticleList({ articles, keywords }: ArticleListProps) {
void;
+ handleFilter: (value: { [value: string]: string | boolean }) => void;
filter: Filter;
}
@@ -43,8 +43,8 @@ export default function SearchFilter({ handleFilter, filter }: SearchFilterProps
handleFilter({ userId: e.target.checked ? signInStatus.id : 0 })}
- checked={filter.userId !== 0}
+ onChange={() => handleFilter({ isUsers: !filter.isUsers })}
+ checked={filter.isUsers}
/>
내 책에서 검색
diff --git a/frontend/components/study/BookListTab/index.tsx b/frontend/components/study/BookListTab/index.tsx
index 76cd6fd4..ba16c4df 100644
--- a/frontend/components/study/BookListTab/index.tsx
+++ b/frontend/components/study/BookListTab/index.tsx
@@ -1,16 +1,17 @@
-import dynamic from 'next/dynamic';
-
import React, { useState } from 'react';
-import { useRecoilState } from 'recoil';
+import { useRecoilState, useSetRecoilState } from 'recoil';
import MinusWhite from '@assets/ico_minus_white.svg';
import curKnottedBookListState from '@atoms/curKnottedBookList';
import editInfoState from '@atoms/editInfo';
+import scrapState from '@atoms/scrap';
import Book from '@components/common/Book';
+import Modal from '@components/common/Modal';
import FAB from '@components/study/FAB';
import { IBookScraps } from '@interfaces';
+import EditBookModal from '../EditBookModal';
import {
BookGrid,
BookListTabWrapper,
@@ -34,11 +35,9 @@ export default function BookListTab({
bookmarkedBookList,
isUserMatched,
}: BookListTabProps) {
- const Modal = dynamic(() => import('@components/common/Modal'));
- const EditBookModal = dynamic(() => import('@components/study/EditBookModal'));
-
const [curKnottedBookList, setCurKnottedBookList] = useRecoilState(curKnottedBookListState);
const [editInfo, setEditInfo] = useRecoilState(editInfoState);
+ const setScraps = useSetRecoilState(scrapState);
const [isModalShown, setModalShown] = useState(false);
const [curEditBook, setCurEditBook] = useState(null);
@@ -51,10 +50,12 @@ export default function BookListTab({
setModalShown(true);
setCurEditBook(curBook);
+ setScraps(curBook.scraps);
};
const handleModalClose = () => {
setModalShown(false);
+ setScraps([]);
};
const handleMinusBtnClick = (e: React.MouseEvent, id: number) => {
diff --git a/frontend/components/study/EditBookModal/index.tsx b/frontend/components/study/EditBookModal/index.tsx
index e3033ccd..803e208d 100644
--- a/frontend/components/study/EditBookModal/index.tsx
+++ b/frontend/components/study/EditBookModal/index.tsx
@@ -41,7 +41,7 @@ interface BookProps {
}
export default function EditBookModal({ book, handleModalClose }: BookProps) {
- const { id, title, user, scraps } = book;
+ const { id, title, user } = book;
const { data: imgFile, execute: createImage } = useFetch(createImageApi);
const { value: titleData, onChange: onTitleChange } = useInput(title);
@@ -106,7 +106,6 @@ export default function EditBookModal({ book, handleModalClose }: BookProps) {
},
],
});
-
handleModalClose();
};
@@ -151,11 +150,11 @@ export default function EditBookModal({ book, handleModalClose }: BookProps) {
Contents
- {isContentsShown ? '완료' : '수정'}
+ {isContentsShown ? '순서저장' : '순서수정'}
-
+
{isContentsShown && (
드래그앤드롭으로 글의 순서를 변경할 수 있습니다.
diff --git a/frontend/components/study/FAB/index.tsx b/frontend/components/study/FAB/index.tsx
index 65563d63..cc3f1bf0 100644
--- a/frontend/components/study/FAB/index.tsx
+++ b/frontend/components/study/FAB/index.tsx
@@ -1,6 +1,6 @@
import Image from 'next/image';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
import { useRecoilState } from 'recoil';
@@ -24,10 +24,15 @@ interface FabProps {
}
export default function FAB({ isEditing, setIsEditing }: FabProps) {
- const { data: deletedBook, execute: deleteBook } = useFetch(deleteBookApi);
- const { data: editBookData, execute: editBook } = useFetch(editBookApi);
- const { data: deleteArticleData, execute: deleteArticle } = useFetch(deleteArticleApi);
- const { data: deleteScrapData, execute: deleteScrap } = useFetch(deleteScrapApi);
+ const { execute: deleteBook } = useFetch(deleteBookApi);
+ const { execute: editBook } = useFetch(editBookApi);
+ const { execute: deleteArticle } = useFetch(deleteArticleApi);
+ const { execute: deleteScrap } = useFetch(deleteScrapApi);
+
+ // const { data: deletedBook, execute: deleteBook } = useFetch(deleteBookApi);
+ // const { data: editBookData, execute: editBook } = useFetch(editBookApi);
+ // const { data: deleteArticleData, execute: deleteArticle } = useFetch(deleteArticleApi);
+ // const { data: deleteScrapData, execute: deleteScrap } = useFetch(deleteScrapApi);
const [editInfo, setEditInfo] = useRecoilState(editInfoState);
@@ -48,43 +53,74 @@ export default function FAB({ isEditing, setIsEditing }: FabProps) {
editInfo.editted.forEach((edit) => {
editBook(edit);
});
- // 원본글 삭제
editInfo.deletedArticle.forEach((articleId) => {
deleteArticle(articleId);
});
- // 스크랩 삭제
editInfo.deletedScraps.forEach((scrapId) => {
deleteScrap(scrapId);
});
- };
-
- useEffect(() => {
- if (!deletedBook) return;
-
setEditInfo({
- ...editInfo,
- deleted: editInfo.deleted.filter((id) => id !== deletedBook.id),
+ deleted: [],
+ editted: [],
+ deletedArticle: [],
+ deletedScraps: [],
});
- }, [deletedBook]);
-
- useEffect(() => {
- if (!editBookData) return;
+ toastSuccess(`수정 완료되었습니다`);
+ };
- setEditInfo({
- ...editInfo,
- editted: editInfo.editted.filter((edit) => edit.id !== editBookData.id),
- });
- }, [editBookData]);
-
- useEffect(() => {
- if (
- (deletedBook || editBookData) &&
- editInfo.deleted.length === 0 &&
- editInfo.editted.length === 0
- ) {
- toastSuccess(`수정 완료되었습니다`);
- }
- }, [editInfo]);
+ // useEffect(() => {
+ // if (!deletedBook) return;
+
+ // setEditInfo({
+ // ...editInfo,
+ // deleted: editInfo.deleted.filter((id) => id !== deletedBook.id),
+ // });
+ // }, [deletedBook]);
+
+ // useEffect(() => {
+ // console.log('editBookData', editBookData);
+ // if (!editBookData) return;
+
+ // setEditInfo({
+ // ...editInfo,
+ // editted: editInfo.editted.filter((edit) => edit.id !== editBookData.id),
+ // deletedScraps: editInfo.deletedScraps.filter((scrapId) => scrapId !== deleteScrapData.id),
+ // });
+ // }, [editBookData, deleteScrapData]);
+
+ // useEffect(() => {
+ // if (!deleteArticleData) return;
+ // console.log('deleteArticleData', deleteArticleData);
+
+ // setEditInfo({
+ // ...editInfo,
+ // deletedArticle: editInfo.deletedArticle.filter(
+ // (articleId) => articleId !== deleteArticleData.id
+ // ),
+ // });
+ // }, [deleteArticleData]);
+
+ // useEffect(() => {
+ // if (!deleteScrapData) return;
+ // console.log('deleteScrapData', deleteScrapData);
+
+ // setEditInfo({
+ // ...editInfo,
+ // deletedScraps: editInfo.deletedScraps.filter((scrapId) => scrapId !== deleteScrapData.id),
+ // });
+ // }, [deleteScrapData]);
+
+ // useEffect(() => {
+ // console.log(deletedBook, editBookData, deleteArticleData, deleteScrapData, editInfo);
+ // if (
+ // (deletedBook || editBookData || deleteArticleData || deleteScrapData) &&
+ // editInfo.deleted.length === 0 &&
+ // editInfo.editted.length === 0 &&
+ // editInfo.deletedArticle.length === 0 &&
+ // editInfo.deletedScraps.length === 0
+ // ) {
+ // }
+ // }, [editInfo]);
return (
diff --git a/frontend/components/study/UserProfile/styled.ts b/frontend/components/study/UserProfile/styled.ts
index 2b89c6bf..6cb558a4 100644
--- a/frontend/components/study/UserProfile/styled.ts
+++ b/frontend/components/study/UserProfile/styled.ts
@@ -40,7 +40,7 @@ export const Username = styled(TextLarge)`
export const UserDescription = styled(TextSmall)`
width: 400px;
@media ${(props) => props.theme.tablet} {
- width: 300px;
+ width: 250px;
}
`;
diff --git a/frontend/components/viewer/ArticleContent/index.tsx b/frontend/components/viewer/ArticleContent/index.tsx
index c33e9602..7cb51f11 100644
--- a/frontend/components/viewer/ArticleContent/index.tsx
+++ b/frontend/components/viewer/ArticleContent/index.tsx
@@ -16,6 +16,7 @@ import Content from '@components/common/Content';
import useFetch from '@hooks/useFetch';
import useScrollDetector from '@hooks/useScrollDetector';
import { IArticleBook, IScrap } from '@interfaces';
+import { toastSuccess } from '@utils/toast';
import ArticleButton from './Button';
import {
@@ -117,6 +118,19 @@ export default function Article({
setIsScrollDown(isScrollDown ? 'true' : 'false');
}, [isScrollDown]);
+ useEffect(() => {
+ if (scrollTarget.current) {
+ scrollTarget.current.scrollTop = 0;
+ }
+ }, [router.query.data]);
+
+ useEffect(() => {
+ if (!deleteArticleData) return;
+
+ toastSuccess(`<${article.title}> 글이 삭제되었습니다`);
+ router.push('/');
+ }, [deleteArticleData]);
+
return (
diff --git a/frontend/components/viewer/ScrapModal/index.tsx b/frontend/components/viewer/ScrapModal/index.tsx
index 3d307bc9..1c7a4c54 100644
--- a/frontend/components/viewer/ScrapModal/index.tsx
+++ b/frontend/components/viewer/ScrapModal/index.tsx
@@ -13,6 +13,7 @@ import Dropdown from '@components/common/Dropdown';
import ModalButton from '@components/common/Modal/ModalButton';
import useFetch from '@hooks/useFetch';
import { IBook, IArticle, IScrap, IBookScraps } from '@interfaces';
+import { toastSuccess } from '@utils/toast';
import { ArticleWrapper, Label, ScrapModalWrapper, WarningLabel } from './styled';
@@ -97,6 +98,7 @@ export default function ScrapModal({ bookId, handleModalClose, article }: ScrapM
useEffect(() => {
if (createScrapData === undefined) return;
router.push(`/viewer/${bookId}/${article.id}`);
+ toastSuccess(`${article.title}글이 스크랩되었습니다.`);
handleModalClose();
}, [createScrapData]);
@@ -117,11 +119,7 @@ export default function ScrapModal({ bookId, handleModalClose, article }: ScrapM
{filteredScraps.length !== 0 && (
-
+
)}
diff --git a/frontend/components/viewer/TOC/index.tsx b/frontend/components/viewer/TOC/index.tsx
index 8390b2dc..f0f3844c 100644
--- a/frontend/components/viewer/TOC/index.tsx
+++ b/frontend/components/viewer/TOC/index.tsx
@@ -10,6 +10,7 @@ import useBookmark from '@hooks/useBookmark';
import { IBookScraps } from '@interfaces';
import { TextMedium, TextSmall } from '@styles/common';
import { FlexCenter, FlexSpaceBetween } from '@styles/layout';
+import { text2link } from '@utils/toc';
import {
TocWrapper,
@@ -25,6 +26,7 @@ import {
TocArticleTitle,
TocCurrentArticle,
TocOpenButton,
+ TocCurrentText,
} from './styled';
interface TocProps {
@@ -86,13 +88,13 @@ export default function TOC({
) : (
-
+
{v.order}.{v.article.title}
-
+
{isArticleShown &&
articleToc.map((article) => (
diff --git a/frontend/components/viewer/TOC/styled.ts b/frontend/components/viewer/TOC/styled.ts
index 6bdc3846..f91c4635 100644
--- a/frontend/components/viewer/TOC/styled.ts
+++ b/frontend/components/viewer/TOC/styled.ts
@@ -3,6 +3,7 @@ import Link from 'next/link';
import styled from 'styled-components';
+import { TextSmall } from '@styles/common';
import { Flex, FlexColumn, FlexColumnSpaceBetween } from '@styles/layout';
interface TocWrapperProps {
@@ -63,11 +64,13 @@ export const TocContainer = styled.div`
color: var(--grey-01-color);
background-color: var(--white-color);
border-radius: 16px;
- overflow: auto;
+ overflow-x: hidden;
+ overflow-y: scroll;
::-webkit-scrollbar {
width: 10px;
}
+
::-webkit-scrollbar-thumb {
background-color: var(--grey-02-color);
border-radius: 10px;
@@ -89,23 +92,30 @@ export const TocArticle = styled(Link)`
color: inherit;
display: block;
margin-bottom: 5px;
+ font-weight: 600;
`;
export const TocCurrentArticle = styled.div`
font-size: 14px;
line-height: 20px;
display: block;
margin-bottom: 5px;
-
- color: #ca7647;
+ color: var(--primary-color);
+`;
+export const TocCurrentText = styled(TextSmall)`
+ cursor: 'pointer';
+ font-weight: 600;
`;
export const TocArticleTitle = styled(Link)<{ count: number | undefined }>`
font-size: 14px;
line-height: 20px;
text-decoration: none;
- color: #c29880;
display: block;
margin-bottom: 5px;
padding-left: ${(props) => `${props.count}px`};
+
+ &:hover {
+ color: var(--primary-color);
+ }
`;
export const TocProfile = styled(Link)`
diff --git a/frontend/hooks/useSessionStorage.ts b/frontend/hooks/useSessionStorage.ts
index 2151e819..855d9ceb 100644
--- a/frontend/hooks/useSessionStorage.ts
+++ b/frontend/hooks/useSessionStorage.ts
@@ -6,9 +6,9 @@ const useSessionStorage = (key: string, initialValue: T) => {
useEffect(() => {
if (typeof window !== 'undefined') {
- setIsValueSet(true);
const savedValue = sessionStorage.getItem(key);
if (savedValue) setStateValue(JSON.parse(savedValue));
+ setIsValueSet(true);
}
}, [typeof window]);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 08e0cf5b..eec9f602 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -35,14 +35,18 @@
"react-dom": "18.2.0",
"react-toastify": "^9.1.1",
"recoil": "^0.7.6",
+ "rehype": "^12.0.1",
"rehype-highlight": "^6.0.0",
"rehype-parse": "^8.0.4",
"rehype-remark": "^9.1.2",
+ "rehype-slug": "^5.1.0",
"rehype-stringify": "^9.0.3",
+ "remark": "^14.0.2",
"remark-breaks": "^3.0.2",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"remark-stringify": "^10.0.2",
+ "strip-markdown": "^5.0.0",
"styled-components": "^5.3.6",
"styled-reset": "^4.4.2",
"typescript": "4.8.4",
@@ -3218,6 +3222,11 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/github-slugger": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
+ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="
+ },
"node_modules/glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
@@ -3417,6 +3426,18 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hast-util-heading-rank": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-2.1.0.tgz",
+ "integrity": "sha512-w+Rw20Q/iWp2Bcnr6uTrYU6/ftZLbHKhvc8nM26VIWpDqDMlku2iXUVTeOlsdoih/UKQhY7PHQ+vZ0Aqq8bxtQ==",
+ "dependencies": {
+ "@types/hast": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-is-body-ok-link": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz",
@@ -3518,6 +3539,18 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hast-util-to-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz",
+ "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==",
+ "dependencies": {
+ "@types/hast": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-to-text": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.1.tgz",
@@ -5319,6 +5352,21 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
+ "node_modules/rehype": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz",
+ "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "rehype-parse": "^8.0.0",
+ "rehype-stringify": "^9.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/rehype-highlight": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-6.0.0.tgz",
@@ -5382,6 +5430,24 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/rehype-slug": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.1.0.tgz",
+ "integrity": "sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "github-slugger": "^2.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-heading-rank": "^2.0.0",
+ "hast-util-to-string": "^2.0.0",
+ "unified": "^10.0.0",
+ "unist-util-visit": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/rehype-stringify": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz",
@@ -5396,6 +5462,21 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remark": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.2.tgz",
+ "integrity": "sha512-A3ARm2V4BgiRXaUo5K0dRvJ1lbogrbXnhkJRmD0yw092/Yl0kOCZt1k9ZeElEwkZsWGsMumz6qL5MfNJH9nOBA==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "remark-parse": "^10.0.0",
+ "remark-stringify": "^10.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/remark-breaks": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.2.tgz",
@@ -5783,6 +5864,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strip-markdown": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/strip-markdown/-/strip-markdown-5.0.0.tgz",
+ "integrity": "sha512-PXSts6Ta9A/TwGxVVSRlQs1ukJTAwwtbip2OheJEjPyfykaQ4sJSTnQWjLTI2vYWNts/R/91/csagp15W8n9gA==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.6",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
@@ -8891,6 +8986,11 @@
"integrity": "sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==",
"dev": true
},
+ "github-slugger": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
+ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="
+ },
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
@@ -9033,6 +9133,14 @@
"resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz",
"integrity": "sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w=="
},
+ "hast-util-heading-rank": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-2.1.0.tgz",
+ "integrity": "sha512-w+Rw20Q/iWp2Bcnr6uTrYU6/ftZLbHKhvc8nM26VIWpDqDMlku2iXUVTeOlsdoih/UKQhY7PHQ+vZ0Aqq8bxtQ==",
+ "requires": {
+ "@types/hast": "^2.0.0"
+ }
+ },
"hast-util-is-body-ok-link": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz",
@@ -9110,6 +9218,14 @@
"unist-util-visit": "^4.0.0"
}
},
+ "hast-util-to-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz",
+ "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==",
+ "requires": {
+ "@types/hast": "^2.0.0"
+ }
+ },
"hast-util-to-text": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.1.tgz",
@@ -10242,6 +10358,17 @@
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="
},
+ "rehype": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz",
+ "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "rehype-parse": "^8.0.0",
+ "rehype-stringify": "^9.0.0",
+ "unified": "^10.0.0"
+ }
+ },
"rehype-highlight": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-6.0.0.tgz",
@@ -10289,6 +10416,20 @@
"unified": "^10.0.0"
}
},
+ "rehype-slug": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.1.0.tgz",
+ "integrity": "sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "github-slugger": "^2.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-heading-rank": "^2.0.0",
+ "hast-util-to-string": "^2.0.0",
+ "unified": "^10.0.0",
+ "unist-util-visit": "^4.0.0"
+ }
+ },
"rehype-stringify": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz",
@@ -10299,6 +10440,17 @@
"unified": "^10.0.0"
}
},
+ "remark": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.2.tgz",
+ "integrity": "sha512-A3ARm2V4BgiRXaUo5K0dRvJ1lbogrbXnhkJRmD0yw092/Yl0kOCZt1k9ZeElEwkZsWGsMumz6qL5MfNJH9nOBA==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "remark-parse": "^10.0.0",
+ "remark-stringify": "^10.0.0",
+ "unified": "^10.0.0"
+ }
+ },
"remark-breaks": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.2.tgz",
@@ -10557,6 +10709,16 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
},
+ "strip-markdown": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/strip-markdown/-/strip-markdown-5.0.0.tgz",
+ "integrity": "sha512-PXSts6Ta9A/TwGxVVSRlQs1ukJTAwwtbip2OheJEjPyfykaQ4sJSTnQWjLTI2vYWNts/R/91/csagp15W8n9gA==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.6",
+ "unified": "^10.0.0"
+ }
+ },
"style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 7622954b..cdcfaf54 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -36,14 +36,18 @@
"react-dom": "18.2.0",
"react-toastify": "^9.1.1",
"recoil": "^0.7.6",
+ "rehype": "^12.0.1",
"rehype-highlight": "^6.0.0",
"rehype-parse": "^8.0.4",
"rehype-remark": "^9.1.2",
+ "rehype-slug": "^5.1.0",
"rehype-stringify": "^9.0.3",
+ "remark": "^14.0.2",
"remark-breaks": "^3.0.2",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"remark-stringify": "^10.0.2",
+ "strip-markdown": "^5.0.0",
"styled-components": "^5.3.6",
"styled-reset": "^4.4.2",
"typescript": "4.8.4",
diff --git a/frontend/pages/search.tsx b/frontend/pages/search.tsx
index 037b92b2..05981657 100644
--- a/frontend/pages/search.tsx
+++ b/frontend/pages/search.tsx
@@ -31,20 +31,12 @@ export default function Search() {
pageNumber: 2,
});
- const {
- value: filter,
- isValueSet: isFilterSet,
- setValue: setFilter,
- } = useSessionStorage('filter', {
+ const { value: filter, setValue: setFilter } = useSessionStorage('filter', {
type: 'article',
- userId: 0,
+ isUsers: false,
});
- const {
- value: keyword,
- isValueSet: isKeywordSet,
- setValue: setKeyword,
- } = useSessionStorage('keyword', '');
+ const { value: keyword, setValue: setKeyword } = useSessionStorage('keyword', '');
const debouncedKeyword = useDebounce(keyword, 300);
const [keywords, setKeywords] = useState([]);
@@ -70,7 +62,7 @@ export default function Search() {
}, [debouncedKeyword]);
useEffect(() => {
- if (!isKeywordSet || !isFilterSet || isInitialRendering) return;
+ if (isInitialRendering) return;
if (debouncedKeyword === '') {
setArticles([]);
@@ -90,7 +82,7 @@ export default function Search() {
searchArticles({
query: debouncedKeyword,
- userId: filter.userId,
+ isUsers: filter.isUsers,
page: 1,
take: 12,
});
@@ -101,7 +93,7 @@ export default function Search() {
searchBooks({
query: debouncedKeyword,
- userId: filter.userId,
+ isUsers: filter.isUsers,
page: 1,
take: 12,
});
@@ -109,7 +101,7 @@ export default function Search() {
hasNextPage: true,
pageNumber: 2,
});
- }, [debouncedKeyword, filter.userId]);
+ }, [debouncedKeyword, filter.isUsers]);
useEffect(() => {
if (!isIntersecting || !debouncedKeyword) return;
@@ -118,7 +110,7 @@ export default function Search() {
if (!articlePage.hasNextPage) return;
searchArticles({
query: debouncedKeyword,
- userId: filter.userId,
+ isUsers: filter.isUsers,
page: articlePage.pageNumber,
take: 12,
});
@@ -130,7 +122,7 @@ export default function Search() {
if (!bookPage.hasNextPage) return;
searchBooks({
query: debouncedKeyword,
- userId: filter.userId,
+ isUsers: filter.isUsers,
page: bookPage.pageNumber,
take: 12,
});
@@ -200,7 +192,8 @@ export default function Search() {
};
}, []);
- const handleFilter = (value: { [value: string]: string | number }) => {
+ const handleFilter = (value: { [value: string]: string | boolean }) => {
+ setIsInitialRendering(false);
setFilter({
...filter,
...value,
diff --git a/frontend/pages/viewer/[...data].tsx b/frontend/pages/viewer/[...data].tsx
index c6a7e488..b5097ed5 100644
--- a/frontend/pages/viewer/[...data].tsx
+++ b/frontend/pages/viewer/[...data].tsx
@@ -13,7 +13,7 @@ import ViewerHead from '@components/viewer/ViewerHead';
import useFetch from '@hooks/useFetch';
import { IArticleBook, IBookScraps } from '@interfaces';
import { Flex, PageGNBHide, PageNoScrollWrapper } from '@styles/layout';
-import { articleToc, articleConversion } from '@utils/articleConversion';
+import { parseHeadings } from '@utils/toc';
interface ViewerProps {
article: IArticleBook;
@@ -38,6 +38,7 @@ export default function Viewer({ article }: ViewerProps) {
};
const checkArticleAuthority = (targetBook: IBookScraps, id: number) => {
+ if (!targetBook) return false;
if (targetBook.scraps.find((scrap) => scrap.article.id === id)) return true;
return false;
};
@@ -64,7 +65,7 @@ export default function Viewer({ article }: ViewerProps) {
}, [router.query.data]);
useEffect(() => {
- if (!book) return;
+ if (book === undefined) return;
if (!checkArticleAuthority(book, article.id)) router.push('/404');
}, [book]);
@@ -81,7 +82,7 @@ export default function Viewer({ article }: ViewerProps) {
diff --git a/frontend/public/assets/ico_exit.svg b/frontend/public/assets/ico_exit.svg
new file mode 100644
index 00000000..e7efdf8e
--- /dev/null
+++ b/frontend/public/assets/ico_exit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
index 718d6fea..8923f4ab 100644
Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ
diff --git a/frontend/utils/articleConversion.ts b/frontend/utils/articleConversion.ts
index 94e01841..27dfc7f7 100644
--- a/frontend/utils/articleConversion.ts
+++ b/frontend/utils/articleConversion.ts
@@ -29,7 +29,7 @@ export const articleConversion = (content: string) => {
if (v.includes('h1') || v.includes('h2') || v.includes('h3')) {
const title = v.replace(/<[^>]*>?/g, '');
const result = v.split('');
- result.splice(3, 0, ' ', `id=${title}`);
+ result.splice(3, 0, ' ', `id="${title}"`);
return result.join('');
}
return v;
diff --git a/frontend/utils/parser.ts b/frontend/utils/parser.ts
index 42fea91b..6803d3a0 100644
--- a/frontend/utils/parser.ts
+++ b/frontend/utils/parser.ts
@@ -1,11 +1,15 @@
+import { rehype } from 'rehype';
import rehypeHighlight from 'rehype-highlight';
import rehypeParse from 'rehype-parse';
import rehypeRemark from 'rehype-remark';
+import rehypeSlug from 'rehype-slug';
import rehypeStringify from 'rehype-stringify';
+import { remark } from 'remark';
import remarkBreaks from 'remark-breaks';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkStringify from 'remark-stringify';
+import stripMarkdown from 'strip-markdown';
import { unified } from 'unified';
export const markdown2html = (markdown: string) => {
@@ -17,19 +21,28 @@ export const markdown2html = (markdown: string) => {
.processSync(markdown)
.toString();
- return unified()
- .use(rehypeParse)
+ const htmlWithSyntaxHighlight = rehype()
+ .use(rehypeSlug)
.use(rehypeHighlight, { ignoreMissing: true })
- .use(rehypeStringify)
.processSync(html)
.toString();
+
+ return htmlWithSyntaxHighlight;
};
export const html2markdown = (html: string) => {
- return unified()
+ const markdown = unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkStringify)
.processSync(html)
.toString();
+
+ return markdown;
+};
+
+export const markdown2text = (markdown: string) => {
+ const text = remark().use(stripMarkdown).processSync(markdown).toString();
+
+ return text;
};
diff --git a/frontend/utils/toast.ts b/frontend/utils/toast.ts
index 9b2b20b2..d1172285 100644
--- a/frontend/utils/toast.ts
+++ b/frontend/utils/toast.ts
@@ -3,7 +3,7 @@ import { toast } from 'react-toastify';
export const toastError = (message: string) => {
toast.error(message, {
position: 'top-right',
- autoClose: 3000,
+ autoClose: 1000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
@@ -16,7 +16,7 @@ export const toastError = (message: string) => {
export const toastSuccess = (message: string) => {
toast.success(message, {
position: 'top-right',
- autoClose: 3000,
+ autoClose: 1000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
diff --git a/frontend/utils/toc.ts b/frontend/utils/toc.ts
new file mode 100644
index 00000000..505550f8
--- /dev/null
+++ b/frontend/utils/toc.ts
@@ -0,0 +1,27 @@
+export const parseHeadings = (content: string) => {
+ // 게시물 본문을 줄바꿈 기준으로 나누고, 제목 요소인 것만 저장
+ const headings = content.split('\n').filter((line) => line.includes('# '));
+
+ // 예외처리 - 제목은 문자열 시작부터 #을 써야함
+ const parsedHeadings = headings
+ .filter((heading) => heading.startsWith('#'))
+ .map((heading) => {
+ // #의 갯수에 따라 제목의 크기가 달라지므로 갯수를 센다.
+ let count = heading.match(/#/g)?.length;
+
+ // 갯수에 따라 목차에 그릴때 들여쓰기 하기위해 *10을 함.
+ if (count) count *= 16;
+
+ // 제목의 내용물만 꺼내기 위해 '# '을 기준으로 나누고, 백틱과 공백을 없애주고 count와 묶어서 리턴
+ return {
+ title: heading.split('# ')[1].trim(),
+ count,
+ };
+ });
+
+ return parsedHeadings;
+};
+
+export const text2link = (text: string) => {
+ return `#${text.replace(/ /g, '-').replace(/[^\uAC00-\uD7A30-9a-zA-Z_-]/g, '')}`;
+};