diff --git a/README.md b/README.md
index 0e8c08fd..328dd8c4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
모두의 글을 엮어 만드는 나만의 책, 노티클 📒
Documentation
-Explore Knoticle
+Explore Knoticle
diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js
index 4af6a157..76891f57 100644
--- a/backend/.eslintrc.js
+++ b/backend/.eslintrc.js
@@ -25,6 +25,12 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
+ 'padding-line-between-statements': [
+ 'error',
+ { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
+ { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },
+ { blankLine: 'always', prev: '*', next: 'return' },
+ ],
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'import/order': [
'error',
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index 4ad8e7e0..70d9fdf0 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -52,8 +52,6 @@ model Article {
book_id Int
scraps Scrap[]
- @@fulltext([content])
- @@fulltext([title])
@@fulltext([content, title])
}
diff --git a/backend/src/apis/articles/articles.controller.ts b/backend/src/apis/articles/articles.controller.ts
index b6033450..b38b5ca6 100644
--- a/backend/src/apis/articles/articles.controller.ts
+++ b/backend/src/apis/articles/articles.controller.ts
@@ -10,14 +10,14 @@ const searchArticles = async (req: Request, res: Response) => {
const searchResult = await articlesService.searchArticles({ query, page, take: +take, userId });
- res.status(200).send(searchResult);
+ return res.status(200).send(searchResult);
};
const getArticle = async (req: Request, res: Response) => {
const articleId = Number(req.params.articleId);
const articleData = await articlesService.getArticle(articleId);
- res.status(200).send(articleData);
+ return res.status(200).send(articleData);
};
const createArticle = async (req: Request, res: Response) => {
@@ -28,23 +28,46 @@ const createArticle = async (req: Request, res: Response) => {
content: article.content,
book_id: article.book_id,
});
+
// forEach와 async,await을 같이사용하는 것이 맞나? 다른방법은 없나?
+ scraps.forEach(async (scrap: IScrap) => {
+ if (scrap.id === 0) {
+ await scrapsService.createScrap({
+ order: scrap.order,
+ is_original: true,
+ book_id: article.book_id,
+ article_id: createdArticle.id,
+ });
+ } else {
+ await scrapsService.updateScrapOrder(scrap);
+ }
+ });
+
+ return res.status(201).send({ createdArticle });
+};
+
+const updateArticle = async (req: Request, res: Response) => {
+ const { article, scraps } = req.body;
+
+ const articleId = Number(req.params.articleId);
+
+ const modifiedArticle = await articlesService.updateArticle(articleId, {
+ title: article.title,
+ content: article.content,
+ book_id: article.book_id,
+ });
+
const result: any[] = [];
+
scraps.forEach(async (scrap: IScrap) => {
if (scrap.id === 0) {
- result.push(
- await scrapsService.createScrap({
- order: scrap.order,
- is_original: true,
- book_id: article.book_id,
- article_id: createdArticle.id,
- })
- );
+ result.push(await scrapsService.updateScrapBookId(articleId, article.book_id, scrap));
} else {
- result.push(await scrapsService.updateScraps(scrap));
+ result.push(await scrapsService.updateScrapOrder(scrap));
}
});
- res.status(201).send({ createdArticle, result });
+
+ return res.status(201).send({ modifiedArticle, result });
};
const deleteArticle = async (req: Request, res: Response) => {
@@ -52,34 +75,39 @@ const deleteArticle = async (req: Request, res: Response) => {
await articlesService.deleteArticle(articleId);
- res.status(204).send();
+ return res.status(204).send();
};
const getTemporaryArticle = async (req: Request, res: Response) => {
- const userId = Number(req.params.userId);
+ if (!res.locals.user) return res.status(200).send();
+ const userId = res.locals.user.id;
const temporaryArticle = await articlesService.getTemporaryArticle(userId);
- res.status(200).send({ temporaryArticle });
+ return res.status(200).send(temporaryArticle);
};
-const craeteTemporaryArticle = async (req: Request, res: Response) => {
- const { title, content, user_id } = req.body;
+const createTemporaryArticle = async (req: Request, res: Response) => {
+ const { title, content } = req.body;
+
+ const userId = res.locals.user.id;
const temporaryArticle = await articlesService.createTemporaryArticle({
title,
content,
- user_id,
+ user_id: userId,
});
- res.status(201).send({ temporaryArticle });
+ return res.status(201).send(temporaryArticle);
+ res.status(201).send(temporaryArticle);
};
export default {
searchArticles,
getArticle,
createArticle,
+ updateArticle,
deleteArticle,
getTemporaryArticle,
- craeteTemporaryArticle,
+ createTemporaryArticle,
};
diff --git a/backend/src/apis/articles/articles.service.ts b/backend/src/apis/articles/articles.service.ts
index ffca3951..89899603 100644
--- a/backend/src/apis/articles/articles.service.ts
+++ b/backend/src/apis/articles/articles.service.ts
@@ -49,6 +49,13 @@ const searchArticles = async (searchArticles: SearchArticles) => {
deleted_at: null,
...matchUserCondition,
},
+ orderBy: {
+ _relevance: {
+ fields: ['title', 'content'],
+ sort: 'desc',
+ search: `${query}*`,
+ },
+ },
take,
skip,
});
@@ -100,6 +107,7 @@ const createArticle = async (dto: CreateArticle) => {
},
},
});
+
return article;
};
@@ -149,10 +157,32 @@ const createTemporaryArticle = async (dto: CreateTemporaryArticle) => {
return temporaryArticle;
};
+const updateArticle = async (articleId: number, dto: CreateArticle) => {
+ const { title, content, book_id } = dto;
+
+ const article = await prisma.article.update({
+ where: {
+ id: articleId,
+ },
+ data: {
+ title,
+ content,
+ book: {
+ connect: {
+ id: book_id,
+ },
+ },
+ },
+ });
+
+ return article;
+};
+
export default {
searchArticles,
getArticle,
createArticle,
+ updateArticle,
deleteArticle,
getTemporaryArticle,
createTemporaryArticle,
diff --git a/backend/src/apis/auth/auth.controller.ts b/backend/src/apis/auth/auth.controller.ts
index cc37c24e..3b8de7fb 100644
--- a/backend/src/apis/auth/auth.controller.ts
+++ b/backend/src/apis/auth/auth.controller.ts
@@ -20,7 +20,7 @@ const signIn = async (req: Request, res: Response) => {
res.cookie('access_token', accessToken, { httpOnly: true });
res.cookie('refresh_token', refreshToken, { httpOnly: true });
- res.status(200).send({ id: user.id, nickname: user.nickname });
+ return res.status(200).send({ id: user.id, nickname: user.nickname });
};
const signInGithub = async (req: Request, res: Response) => {
@@ -41,7 +41,7 @@ const signInGithub = async (req: Request, res: Response) => {
res.cookie('access_token', accessToken, { httpOnly: true });
res.cookie('refresh_token', refreshToken, { httpOnly: true });
- res.status(200).send({ id: githubUser.id, nickname: githubUser.nickname });
+ return res.status(200).send({ id: githubUser.id, nickname: githubUser.nickname });
};
const signUp = async (req: Request, res: Response) => {
@@ -51,7 +51,7 @@ const signUp = async (req: Request, res: Response) => {
await authService.signUpLocalUser(username, password, nickname);
- res.status(201).send();
+ return res.status(201).send();
};
const checkSignInStatus = async (req: Request, res: Response) => {
@@ -62,14 +62,15 @@ const checkSignInStatus = async (req: Request, res: Response) => {
return res.status(200).send({ id: user.id, nickname: user.nickname });
}
- res.status(200).send({ id: 0, nickname: '' });
+
+ return res.status(200).send({ id: 0, nickname: '' });
};
const signOut = async (req: Request, res: Response) => {
res.clearCookie('access_token');
res.clearCookie('refresh_token');
- res.status(200).send({ id: 0, nickname: '' });
+ return res.status(200).send({ id: 0, nickname: '' });
};
export default {
diff --git a/backend/src/apis/auth/auth.service.ts b/backend/src/apis/auth/auth.service.ts
index 7b39194b..62827764 100644
--- a/backend/src/apis/auth/auth.service.ts
+++ b/backend/src/apis/auth/auth.service.ts
@@ -38,6 +38,7 @@ const getGithubAccessToken = async (code: string) => {
},
}
);
+
return data.access_token;
};
@@ -60,6 +61,7 @@ const getUserByLocalDB = async (provider_id: string) => {
nickname: true,
},
});
+
return user;
};
@@ -72,6 +74,11 @@ const signUpGithubUser = async (username: string, provider_id: string) => {
provider: 'github',
password: '',
description: `안녕하세요 ${nickname}입니다.`,
+ books: {
+ create: {
+ title: '새로운 책',
+ },
+ },
},
});
@@ -129,6 +136,11 @@ const signUpLocalUser = async (username: string, password: string, nickname: str
provider: 'local',
password: encryptedPassword,
description: `안녕하세요 ${nickname}입니다.`,
+ books: {
+ create: {
+ title: '새로운 책',
+ },
+ },
},
});
};
diff --git a/backend/src/apis/bookmarks/bookmarks.controller.ts b/backend/src/apis/bookmarks/bookmarks.controller.ts
index 5c0fa70f..8191114e 100644
--- a/backend/src/apis/bookmarks/bookmarks.controller.ts
+++ b/backend/src/apis/bookmarks/bookmarks.controller.ts
@@ -7,13 +7,16 @@ const createBookmark = async (req: Request, res: Response) => {
const userId = res.locals.user.id;
const bookmarkId = await bookmarksService.createBookmark(Number(userId), Number(book_id));
- res.status(200).send({ bookmarkId });
+
+ return res.status(200).send({ bookmarkId });
};
const deleteBookmark = async (req: Request, res: Response) => {
const bookmarkId = Number(req.params.bookmarkId);
+
await bookmarksService.deleteBookmark(bookmarkId);
- res.status(200).send();
+
+ return res.status(200).send();
};
export default {
diff --git a/backend/src/apis/bookmarks/bookmarks.service.ts b/backend/src/apis/bookmarks/bookmarks.service.ts
index d39b8474..bb4b8ce5 100644
--- a/backend/src/apis/bookmarks/bookmarks.service.ts
+++ b/backend/src/apis/bookmarks/bookmarks.service.ts
@@ -8,6 +8,7 @@ const createBookmark = async (user_id: number, book_id: number) => {
book_id,
},
});
+
return id;
};
diff --git a/backend/src/apis/books/books.controller.ts b/backend/src/apis/books/books.controller.ts
index da536dec..a7ddd4d8 100644
--- a/backend/src/apis/books/books.controller.ts
+++ b/backend/src/apis/books/books.controller.ts
@@ -12,9 +12,9 @@ const getBook = async (req: Request, res: Response) => {
if (!userId) userId = 0;
- const book = await booksService.findBook(+bookId, userId);
+ const book = await booksService.getBook(+bookId, userId);
- res.status(200).send(book);
+ return res.status(200).send(book);
};
const getBooks = async (req: Request, res: Response) => {
@@ -24,9 +24,9 @@ const getBooks = async (req: Request, res: Response) => {
if (!userId) userId = 0;
- const books = await booksService.findBooks({ order, take: +take, userId, editor, type });
+ const books = await booksService.getBooks({ order, take: +take, userId, editor, type });
- res.status(200).send(books);
+ return res.status(200).send(books);
};
const searchBooks = async (req: Request, res: Response) => {
@@ -34,7 +34,7 @@ const searchBooks = async (req: Request, res: Response) => {
const searchResult = await booksService.searchBooks({ query, userId, take: +take, page });
- res.status(200).send(searchResult);
+ return res.status(200).send(searchResult);
};
const createBook = async (req: Request, res: Response) => {
@@ -44,26 +44,25 @@ const createBook = async (req: Request, res: Response) => {
const book = await booksService.createBook({ title, userId });
- const bookData = await booksService.findBook(book.id, userId);
+ const bookData = await booksService.getBook(book.id, userId);
- res.status(201).send(bookData);
+ return res.status(201).send(bookData);
};
-const editBook = async (req: Request, res: Response) => {
+const updateBook = async (req: Request, res: Response) => {
const { id, title, thumbnail_image, scraps } = req.body;
const userId = res.locals.user.id;
- const book = await booksService.editBook({ id, title, thumbnail_image });
+ const book = await booksService.updateBook({ id, title, thumbnail_image });
- const result: any[] = [];
scraps.forEach(async (scrap: IScrap) => {
- result.push(await scrapsService.updateScraps(scrap));
+ await scrapsService.updateScrapOrder(scrap);
});
- const bookData = await booksService.findBook(book.id, userId);
+ const bookData = await booksService.getBook(book.id, userId);
- res.status(200).send(bookData);
+ return res.status(200).send(bookData);
};
const deleteBook = async (req: Request, res: Response) => {
@@ -73,7 +72,7 @@ const deleteBook = async (req: Request, res: Response) => {
const book = await booksService.deleteBook(bookId, userId);
- res.status(200).send(book);
+ return res.status(200).send(book);
};
export default {
@@ -81,6 +80,6 @@ export default {
getBooks,
searchBooks,
createBook,
- editBook,
+ updateBook,
deleteBook,
};
diff --git a/backend/src/apis/books/books.service.ts b/backend/src/apis/books/books.service.ts
index 1ca235a0..1a2f0145 100644
--- a/backend/src/apis/books/books.service.ts
+++ b/backend/src/apis/books/books.service.ts
@@ -2,34 +2,35 @@ import { FindBooks, SearchBooks, CreateBook } from '@apis/books/books.interface'
import { prisma } from '@config/orm.config';
import { Message, NotFound } from '@errors';
-const findBook = async (bookId: number, userId: number) => {
- const book = await prisma.book.findFirst({
+const searchBooks = async ({ query, userId, take, page }: SearchBooks) => {
+ const skip = (page - 1) * take;
+
+ const books = await prisma.book.findMany({
select: {
id: true,
title: true,
thumbnail_image: true,
+ created_at: true,
user: {
select: {
nickname: true,
- profile_image: true,
},
},
scraps: {
- orderBy: { order: 'asc' },
select: {
+ id: true,
order: true,
article: {
select: {
id: true,
title: true,
- deleted_at: true,
},
},
},
},
bookmarks: {
where: {
- user_id: userId,
+ user_id: Number(userId) ? Number(userId) : 0,
},
},
_count: {
@@ -37,39 +38,50 @@ const findBook = async (bookId: number, userId: number) => {
},
},
where: {
- id: bookId,
deleted_at: null,
+ user_id: Number(userId) ? Number(userId) : undefined,
+ title: {
+ search: `${query}*`,
+ },
+ },
+ orderBy: {
+ _relevance: {
+ fields: ['title'],
+ sort: 'desc',
+ search: `${query}*`,
+ },
},
+ skip,
+ take,
});
- return book;
+ return {
+ data: books,
+ hasNextPage: books.length === take,
+ };
};
-const findBooks = async ({ order, take, userId, editor, type }: FindBooks) => {
- const sortOptions = [];
- if (order === 'bookmark') sortOptions.push({ bookmarks: { _count: 'desc' as const } });
- if (order === 'newest') sortOptions.push({ created_at: 'desc' as const });
-
- const books = await prisma.book.findMany({
+const getBook = async (bookId: number, userId: number) => {
+ const book = await prisma.book.findFirst({
select: {
id: true,
title: true,
thumbnail_image: true,
- created_at: true,
user: {
select: {
nickname: true,
+ profile_image: true,
},
},
scraps: {
orderBy: { order: 'asc' },
select: {
- id: true,
order: true,
article: {
select: {
id: true,
title: true,
+ deleted_at: true,
},
},
},
@@ -84,37 +96,19 @@ const findBooks = async ({ order, take, userId, editor, type }: FindBooks) => {
},
},
where: {
+ id: bookId,
deleted_at: null,
- user:
- type === 'bookmark'
- ? {}
- : {
- is: {
- nickname: editor ? editor : undefined,
- },
- },
- bookmarks:
- type === 'bookmark'
- ? {
- some: {
- user: {
- is: {
- nickname: editor ? editor : undefined,
- },
- },
- },
- }
- : {},
},
- orderBy: sortOptions,
- take,
});
- return books;
+ return book;
};
-const searchBooks = async ({ query, userId, take, page }: SearchBooks) => {
- const skip = (page - 1) * take;
+const getBooks = async ({ order, take, userId, editor, type }: FindBooks) => {
+ const sortOptions = [];
+
+ if (order === 'bookmark') sortOptions.push({ bookmarks: { _count: 'desc' as const } });
+ if (order === 'newest') sortOptions.push({ created_at: 'desc' as const });
const books = await prisma.book.findMany({
select: {
@@ -128,9 +122,11 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => {
},
},
scraps: {
+ orderBy: { order: 'asc' },
select: {
id: true,
order: true,
+ is_original: true,
article: {
select: {
id: true,
@@ -141,7 +137,7 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => {
},
bookmarks: {
where: {
- user_id: Number(userId) ? Number(userId) : 0,
+ user_id: userId,
},
},
_count: {
@@ -150,19 +146,32 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => {
},
where: {
deleted_at: null,
- user_id: Number(userId) ? Number(userId) : undefined,
- title: {
- search: `${query}*`,
- },
+ user:
+ type === 'bookmark'
+ ? {}
+ : {
+ is: {
+ nickname: editor ? editor : undefined,
+ },
+ },
+ bookmarks:
+ type === 'bookmark'
+ ? {
+ some: {
+ user: {
+ is: {
+ nickname: editor ? editor : undefined,
+ },
+ },
+ },
+ }
+ : {},
},
- skip,
+ orderBy: sortOptions,
take,
});
- return {
- data: books,
- hasNextPage: books.length === take,
- };
+ return books;
};
const createBook = async ({ title, userId }: CreateBook) => {
@@ -182,7 +191,7 @@ const createBook = async ({ title, userId }: CreateBook) => {
return book;
};
-const editBook = async (dto: any) => {
+const updateBook = async (dto: any) => {
const { id, title, thumbnail_image } = dto;
const book = await prisma.book.update({
where: {
@@ -224,10 +233,10 @@ const checkBookOwnerCorrect = async (id: number, userId: number) => {
};
export default {
- findBook,
- findBooks,
searchBooks,
+ getBook,
+ getBooks,
createBook,
- editBook,
+ updateBook,
deleteBook,
};
diff --git a/backend/src/apis/images/images.controller.ts b/backend/src/apis/images/images.controller.ts
index 444e178c..a3b1e2f3 100644
--- a/backend/src/apis/images/images.controller.ts
+++ b/backend/src/apis/images/images.controller.ts
@@ -5,7 +5,7 @@ import imagesService from '@apis/images/images.service';
const createImage = async (req: Request, res: Response) => {
const imagePath = await imagesService.createImage({ file: req.file });
- res.status(201).send({ imagePath });
+ return res.status(201).send({ imagePath });
};
export default {
diff --git a/backend/src/apis/index.ts b/backend/src/apis/index.ts
index 47cfaa2e..312219cf 100644
--- a/backend/src/apis/index.ts
+++ b/backend/src/apis/index.ts
@@ -21,26 +21,29 @@ router.post('/auth/signup', catchAsync(authController.signUp));
router.get('/auth/signout', catchAsync(authController.signOut));
router.get('/auth', decoder, 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/: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.get('/articles/temporary/:userId', catchAsync(articlesController.getTemporaryArticle));
-router.post('/articles/temporary', catchAsync(articlesController.craeteTemporaryArticle));
router.post('/image', multer().single('image'), catchAsync(imagesController.createImage));
router.get('/books/search', catchAsync(booksController.searchBooks));
router.get('/books/:bookId', decoder, catchAsync(booksController.getBook));
-router.delete('/books/:bookId', catchAsync(guard), catchAsync(booksController.deleteBook));
router.get('/books', decoder, catchAsync(booksController.getBooks));
-router.post('/books', catchAsync(guard), catchAsync(booksController.createBook));
-router.patch('/books', catchAsync(guard), catchAsync(booksController.editBook));
+router.post('/books', guard, catchAsync(booksController.createBook));
+router.patch('/books', guard, catchAsync(booksController.updateBook));
+router.delete('/books/:bookId', guard, catchAsync(booksController.deleteBook));
-router.post('/bookmarks', catchAsync(guard), catchAsync(bookmarksController.createBookmark));
+router.post('/bookmarks', guard, catchAsync(bookmarksController.createBookmark));
router.delete('/bookmarks/:bookmarkId', catchAsync(bookmarksController.deleteBookmark));
+router.get('/scraps', catchAsync(scrapsController.getScraps));
router.post('/scraps', catchAsync(scrapsController.createScrap));
+router.delete('/scraps/:scrapId', guard, catchAsync(scrapsController.deleteScrap));
router.get('/users', catchAsync(usersController.getUserProfile));
router.patch('/users/:userId', catchAsync(usersController.editUserProfile));
diff --git a/backend/src/apis/scraps/scraps.controller.ts b/backend/src/apis/scraps/scraps.controller.ts
index f1dfe3e5..80e765e4 100644
--- a/backend/src/apis/scraps/scraps.controller.ts
+++ b/backend/src/apis/scraps/scraps.controller.ts
@@ -7,25 +7,37 @@ import scrapsService from './scraps.service';
const createScrap = async (req: Request, res: Response) => {
const { book_id, article_id, scraps } = req.body;
- const result: any[] = [];
scraps.forEach(async (scrap: IScrap) => {
if (scrap.id === 0) {
- result.push(
- await scrapsService.createScrap({
- order: scrap.order,
- is_original: true,
- book_id,
- article_id: article_id,
- })
- );
+ await scrapsService.createScrap({
+ order: scrap.order,
+ is_original: false,
+ book_id,
+ article_id: article_id,
+ });
} else {
- result.push(await scrapsService.updateScraps(scrap));
+ await scrapsService.updateScrapOrder(scrap);
}
});
- res.status(201).send();
+ return res.status(201).send();
+};
+
+const deleteScrap = async (req: Request, res: Response) => {
+ const scrapId = Number(req.params.scrapId);
+
+ await scrapsService.deleteScrap(scrapId);
+
+ return res.status(200).send();
+};
+const getScraps = async (req: Request, res: Response) => {
+ const scraps = await scrapsService.getScraps();
+
+ return res.status(200).send(scraps);
};
export default {
createScrap,
+ deleteScrap,
+ getScraps,
};
diff --git a/backend/src/apis/scraps/scraps.service.ts b/backend/src/apis/scraps/scraps.service.ts
index 7751e7fc..14cbfb6b 100644
--- a/backend/src/apis/scraps/scraps.service.ts
+++ b/backend/src/apis/scraps/scraps.service.ts
@@ -39,7 +39,7 @@ const checkScrapExists = async (dto: CreateScrap) => {
if (scrap) throw new ResourceConflict(Message.SCRAP_OVERLAP);
};
-const updateScraps = async (scraps: IScrap) => {
+const updateScrapOrder = async (scraps: IScrap) => {
const scrap = await prisma.scrap.update({
where: {
id: scraps.id,
@@ -48,11 +48,64 @@ const updateScraps = async (scraps: IScrap) => {
order: scraps.order,
},
});
+
+ return scrap;
+};
+
+const deleteScrap = async (scrapId: number) => {
+ await prisma.scrap.delete({
+ where: {
+ id: scrapId,
+ },
+ });
+};
+
+const getScraps = async () => {
+ const scraps = await prisma.scrap.findMany({
+ select: {
+ book_id: true,
+ article_id: true,
+ },
+ where: {
+ is_original: true,
+ book: {
+ deleted_at: null,
+ },
+ article: {
+ deleted_at: null,
+ },
+ },
+ });
+
+ return scraps;
+};
+
+const updateScrapBookId = async (articleId: number, bookId: number, scraps: IScrap) => {
+ const originalScrap = await prisma.scrap.findFirst({
+ where: {
+ is_original: true,
+ article_id: articleId,
+ },
+ });
+
+ const scrap = await prisma.scrap.update({
+ where: {
+ id: originalScrap.id,
+ },
+ data: {
+ book_id: bookId,
+ order: scraps.order,
+ },
+ });
+
return scrap;
};
export default {
createScrap,
checkScrapExists,
- updateScraps,
+ updateScrapOrder,
+ updateScrapBookId,
+ deleteScrap,
+ getScraps,
};
diff --git a/backend/src/apis/users/users.controller.ts b/backend/src/apis/users/users.controller.ts
index c44d8eb7..0283f077 100644
--- a/backend/src/apis/users/users.controller.ts
+++ b/backend/src/apis/users/users.controller.ts
@@ -7,7 +7,7 @@ const getUserProfile = async (req: Request, res: Response) => {
const userProfile = await usersService.findUserProfile(userNickname);
- res.status(200).send(userProfile);
+ return res.status(200).send(userProfile);
};
// PATCH인데 id가 req.body에 담겨서 오다보니 params를 확인할 일이 없는데...
@@ -16,7 +16,7 @@ const editUserProfile = async (req: Request, res: Response) => {
const userProfile = await usersService.updateUserProfile(req.body);
- res.status(200).send(userProfile);
+ return res.status(200).send(userProfile);
};
export default {
diff --git a/backend/src/apis/users/users.service.ts b/backend/src/apis/users/users.service.ts
index 8661f6c7..40555b47 100644
--- a/backend/src/apis/users/users.service.ts
+++ b/backend/src/apis/users/users.service.ts
@@ -25,6 +25,7 @@ const updateUserProfile = async (dto: UpdateUserProfile) => {
const { id, nickname, profile_image, description } = dto;
const user = await getUserByNickname(nickname);
+
if (user && user.id !== id) throw new ResourceConflict(Message.AUTH_NICKNAME_OVERLAP);
const userProfile = await prisma.user.update({
diff --git a/backend/src/errors/message.ts b/backend/src/errors/message.ts
index be7b4838..3e0006e2 100644
--- a/backend/src/errors/message.ts
+++ b/backend/src/errors/message.ts
@@ -6,7 +6,7 @@ export default {
ARTICLE_NOTFOUND: '일치하는 글이 없습니다.',
BOOK_NOTFOUND: '일치하는 책이 없습니다.',
USER_NOTFOUND: '일치하는 유저가 없습니다.',
- TOKEN_EXPIRED: '다시 로그인 해주세요',
- TOKEN_MALFORMED: '다시 로그인 해주세요',
+ TOKEN_EXPIRED: '로그인이 필요합니다.',
+ TOKEN_MALFORMED: '로그인이 필요합니다.',
BOOKMARK_NOTFOUND: '북마크된 책이 아닙니다.',
};
diff --git a/backend/src/loaders/express.loader.ts b/backend/src/loaders/express.loader.ts
index 0f99cbda..1ca12a28 100644
--- a/backend/src/loaders/express.loader.ts
+++ b/backend/src/loaders/express.loader.ts
@@ -6,6 +6,7 @@ import logger from 'morgan';
import router from '@apis';
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
const { status, message } = err;
diff --git a/backend/src/middlewares/tokenValidator.ts b/backend/src/middlewares/tokenValidator.ts
index 8cb745a9..64b9fe99 100644
--- a/backend/src/middlewares/tokenValidator.ts
+++ b/backend/src/middlewares/tokenValidator.ts
@@ -1,12 +1,15 @@
import { NextFunction, Request, Response } from 'express';
import { Unauthorized, Message } from '@errors';
+import { catchAsync } from '@utils/catch-async';
import token from '@utils/token';
const guard = async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = token.verifyJWT(req.cookies.access_token);
+
res.locals.user = { id };
+
next();
} catch (err) {
if (err.message === 'jwt expired') {
@@ -28,4 +31,4 @@ const guard = async (req: Request, res: Response, next: NextFunction) => {
}
};
-export default guard;
+export default catchAsync(guard);
diff --git a/backend/src/utils/token.ts b/backend/src/utils/token.ts
index 2974c234..9f6bb354 100644
--- a/backend/src/utils/token.ts
+++ b/backend/src/utils/token.ts
@@ -1,18 +1,18 @@
-import jwt, { JwtPayload } from 'jsonwebtoken';
+import { decode, JwtPayload, sign, verify } from 'jsonwebtoken';
import { prisma } from '@config/orm.config';
import { Message, Unauthorized } from '@errors';
const generateJWT = (expiresIn: '3h' | '7d', payload: { id?: number } = {}) => {
- return jwt.sign(payload, process.env.JWT_SECRET_KEY, { expiresIn });
+ return sign(payload, process.env.JWT_SECRET_KEY, { expiresIn });
};
const verifyJWT = (token: string) => {
- return jwt.verify(token, process.env.JWT_SECRET_KEY) as JwtPayload;
+ return verify(token, process.env.JWT_SECRET_KEY) as JwtPayload;
};
const decodeJWT = (token: string) => {
- return jwt.decode(token) as JwtPayload;
+ return decode(token) as JwtPayload;
};
const getTokens = (userId: number) => {
diff --git a/frontend/apis/articleApi.ts b/frontend/apis/articleApi.ts
index fe5f71eb..d817a6ba 100644
--- a/frontend/apis/articleApi.ts
+++ b/frontend/apis/articleApi.ts
@@ -43,3 +43,40 @@ export const createArticleApi = async (data: CreateArticleApi) => {
return response.data;
};
+
+export const modifyArticleApi = async (articleId: number, data: CreateArticleApi) => {
+ const url = `/api/articles/${articleId}`;
+
+ const response = await api({ url, method: 'PATCH', data });
+
+ return response.data;
+};
+
+export const deleteArticleApi = async (articleId: string) => {
+ const url = `/api/articles/${articleId}`;
+
+ const response = await api({ url, method: 'DELETE' });
+
+ return response.data;
+};
+
+export const getTemporaryArticleApi = async () => {
+ const url = '/api/articles/temporary';
+
+ const response = await api({ url, method: 'GET' });
+
+ return response.data;
+};
+
+interface CreateTemporaryArticleApi {
+ title: string;
+ content: string;
+}
+
+export const createTemporaryArticleApi = async (data: CreateTemporaryArticleApi) => {
+ const url = '/api/articles/temporary';
+
+ const response = await api({ url, method: 'POST', data });
+
+ return response.data;
+};
diff --git a/frontend/apis/bookApi.ts b/frontend/apis/bookApi.ts
index c2563212..1ccf0e09 100644
--- a/frontend/apis/bookApi.ts
+++ b/frontend/apis/bookApi.ts
@@ -30,7 +30,7 @@ interface GetBooksApi {
interface EditBookApi {
id: number;
title: string;
- thumbnail_image: any;
+ thumbnail_image: string;
scraps: IScrap[];
}
diff --git a/frontend/apis/scrapApi.ts b/frontend/apis/scrapApi.ts
index 2c79576b..2ac50ba2 100644
--- a/frontend/apis/scrapApi.ts
+++ b/frontend/apis/scrapApi.ts
@@ -1,5 +1,13 @@
import api from '@utils/api';
+export const getScrapsApi = async () => {
+ const url = '/api/scraps';
+
+ const response = await api({ url, method: 'GET' });
+
+ return response.data;
+};
+
interface CreateScrapApi {
order: number;
is_original: boolean;
@@ -7,7 +15,6 @@ interface CreateScrapApi {
article_id: number;
}
-// eslint-disable-next-line import/prefer-default-export
export const createScrapApi = async (data: CreateScrapApi) => {
const url = `/api/scraps`;
@@ -15,3 +22,11 @@ export const createScrapApi = async (data: CreateScrapApi) => {
return response.data;
};
+
+export const deleteScrapApi = async (scrapId: string) => {
+ const url = `/api/scraps/${scrapId}`;
+
+ const response = await api({ url, method: 'DELETE' });
+
+ return response.data;
+};
diff --git a/frontend/atoms/article.ts b/frontend/atoms/article.ts
index 7487a1c8..0762b96e 100644
--- a/frontend/atoms/article.ts
+++ b/frontend/atoms/article.ts
@@ -3,6 +3,7 @@ import { atom } from 'recoil';
const articleState = atom({
key: 'articleState',
default: {
+ id: -1,
title: '',
content: '',
book_id: -1,
diff --git a/frontend/atoms/articleBuffer.ts b/frontend/atoms/articleBuffer.ts
new file mode 100644
index 00000000..2cee747d
--- /dev/null
+++ b/frontend/atoms/articleBuffer.ts
@@ -0,0 +1,11 @@
+import { atom } from 'recoil';
+
+const articleBuffer = atom({
+ key: 'articleBuffer',
+ default: {
+ title: '',
+ content: '',
+ },
+});
+
+export default articleBuffer;
diff --git a/frontend/atoms/editInfo.ts b/frontend/atoms/editInfo.ts
index de56e0df..0d41faa5 100644
--- a/frontend/atoms/editInfo.ts
+++ b/frontend/atoms/editInfo.ts
@@ -10,6 +10,8 @@ interface EditInfoState {
thumbnail_image: string;
scraps: IScrap[];
}[];
+ deletedArticle: number[];
+ deletedScraps: number[];
}
const editInfoState = atom({
@@ -17,6 +19,8 @@ const editInfoState = atom({
default: {
deleted: [],
editted: [],
+ deletedArticle: [],
+ deletedScraps: [],
},
});
diff --git a/frontend/atoms/scrap.ts b/frontend/atoms/scrap.ts
index 4b0ea75c..3ed43b66 100644
--- a/frontend/atoms/scrap.ts
+++ b/frontend/atoms/scrap.ts
@@ -1,6 +1,8 @@
import { atom } from 'recoil';
-const scrapState = atom({
+import { IScrap } from '@interfaces';
+
+const scrapState = atom({
key: 'scrapState',
default: [],
});
diff --git a/frontend/components/CheckSignInByToken/index.tsx b/frontend/components/auth/CheckSignInStatus/index.tsx
similarity index 82%
rename from frontend/components/CheckSignInByToken/index.tsx
rename to frontend/components/auth/CheckSignInStatus/index.tsx
index ffbe1b07..9eaefe80 100644
--- a/frontend/components/CheckSignInByToken/index.tsx
+++ b/frontend/components/auth/CheckSignInStatus/index.tsx
@@ -6,11 +6,11 @@ import { checkSignInApi } from '@apis/authApi';
import signInStatusState from '@atoms/signInStatus';
import useFetch from '@hooks/useFetch';
-interface CheckSignInByTokenProps {
+interface CheckSignInStatus {
children: React.ReactNode;
}
-export default function CheckSignInByToken({ children }: CheckSignInByTokenProps) {
+export default function CheckSignInStatus({ children }: CheckSignInStatus) {
const { data: user, execute: checkSignIn } = useFetch(checkSignInApi);
const setSignInStatus = useSetRecoilState(signInStatusState);
diff --git a/frontend/components/SignInModal/index.tsx b/frontend/components/auth/SignInModal/index.tsx
similarity index 100%
rename from frontend/components/SignInModal/index.tsx
rename to frontend/components/auth/SignInModal/index.tsx
diff --git a/frontend/components/SignInModal/styled.ts b/frontend/components/auth/SignInModal/styled.ts
similarity index 79%
rename from frontend/components/SignInModal/styled.ts
rename to frontend/components/auth/SignInModal/styled.ts
index 27028780..6bd3bb45 100644
--- a/frontend/components/SignInModal/styled.ts
+++ b/frontend/components/auth/SignInModal/styled.ts
@@ -1,6 +1,6 @@
import styled from 'styled-components';
-import { FlexColumnCenter } from '@styles/layout';
+import { FlexColumnAlignCenter } from '@styles/layout';
export const SignInModalWrapper = styled.div`
margin-top: 56px;
@@ -10,7 +10,7 @@ export const SignInModalWrapper = styled.div`
box-sizing: border-box;
`;
-export const SignUpContainer = styled(FlexColumnCenter)`
+export const SignUpContainer = styled(FlexColumnAlignCenter)`
padding: 20px;
gap: 10px;
diff --git a/frontend/components/SignUpModal/index.tsx b/frontend/components/auth/SignUpModal/index.tsx
similarity index 100%
rename from frontend/components/SignUpModal/index.tsx
rename to frontend/components/auth/SignUpModal/index.tsx
diff --git a/frontend/components/SignUpModal/styled.ts b/frontend/components/auth/SignUpModal/styled.ts
similarity index 100%
rename from frontend/components/SignUpModal/styled.ts
rename to frontend/components/auth/SignUpModal/styled.ts
diff --git a/frontend/components/common/Book/index.tsx b/frontend/components/common/Book/index.tsx
index 75aa183b..9e4fc0fb 100644
--- a/frontend/components/common/Book/index.tsx
+++ b/frontend/components/common/Book/index.tsx
@@ -20,6 +20,7 @@ import {
ArticleLink,
AuthorLink,
BookmarkIcon,
+ BookLink,
} from './styled';
interface BookProps {
@@ -36,17 +37,27 @@ export default function Book({ book }: BookProps) {
return (
// 수정모드일때만 아래 onclick이 실행되도록 수정해야함 -> 민형님 작업 후
-
+
+
+
- {title}
+
+ {title}
+
by {user.nickname}
@@ -74,11 +85,11 @@ export default function Book({ book }: BookProps) {
)}
- {scraps.length > 4 && (
+ {/* {scraps.length > 4 && (
- )}
+ )} */}
);
diff --git a/frontend/components/common/Book/styled.ts b/frontend/components/common/Book/styled.ts
index 9b462b2a..9e564be3 100644
--- a/frontend/components/common/Book/styled.ts
+++ b/frontend/components/common/Book/styled.ts
@@ -7,8 +7,7 @@ import { TextXSmall } from '@styles/common';
import { FlexColumn } from '@styles/layout';
export const BookWrapper = styled(FlexColumn)`
- min-width: 280px;
- max-width: 280px;
+ width: 280px;
height: 480px;
margin: 0 10px;
box-sizing: border-box;
@@ -19,12 +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;
+ }
`;
export const BookThumbnail = styled(Image)`
width: 280px;
- height: 200px;
min-height: 200px;
+ object-fit: cover;
+ aspect-ratio: 280/200;
+
+ @media ${(props) => props.theme.tablet} {
+ width: 100%;
+ min-height: auto;
+ }
`;
export const BookInfoContainer = styled(FlexColumn)`
@@ -100,3 +111,8 @@ export const AuthorLink = styled(Link)`
display: block;
margin-top: 2px;
`;
+
+export const BookLink = styled(Link)<{ isArticleExists: boolean }>`
+ text-decoration: none;
+ ${(props) => (props.isArticleExists ? '' : 'pointer-events: none;')}
+`;
diff --git a/frontend/components/common/DragDrop/Container/index.tsx b/frontend/components/common/DragDrop/Container/index.tsx
index f741ef02..e1c50ef8 100644
--- a/frontend/components/common/DragDrop/Container/index.tsx
+++ b/frontend/components/common/DragDrop/Container/index.tsx
@@ -5,6 +5,7 @@ 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';
@@ -13,21 +14,17 @@ const ItemTypes = {
Scrap: 'scrap',
};
-export interface EditScrap {
- id: number;
- order: number;
- article: {
- id: number;
- title: string;
- };
-}
export interface ContainerState {
- data: EditScrap[];
+ data: IScrap[];
isContentsShown: boolean;
+ isDeleteBtnShown: boolean;
}
-
-export const Container = memo(function Container({ data, isContentsShown }: ContainerState) {
- const [scraps, setScraps] = useRecoilState(scrapState);
+const DragContainer = memo(function Container({
+ data,
+ isContentsShown,
+ isDeleteBtnShown,
+}: ContainerState) {
+ const [scraps, setScraps] = useRecoilState(scrapState);
useEffect(() => {
if (!data) return;
@@ -35,15 +32,8 @@ export const Container = memo(function Container({ data, isContentsShown }: Cont
}, []);
const findScrap = useCallback(
- (id: string) => {
- const scrap = scraps.filter((c) => `${c.article.id}` === id)[0] as {
- id: number;
- order: number;
- article: {
- id: number;
- title: string;
- };
- };
+ (id: number) => {
+ const scrap = scraps.filter((c) => c.article.id === id)[0];
return {
scrap,
index: scraps.indexOf(scrap),
@@ -53,7 +43,7 @@ export const Container = memo(function Container({ data, isContentsShown }: Cont
);
const moveScrap = useCallback(
- (id: string, atIndex: number) => {
+ (id: number, atIndex: number) => {
const { scrap, index } = findScrap(id);
setScraps(
update(scraps, {
@@ -73,14 +63,19 @@ export const Container = memo(function Container({ data, isContentsShown }: Cont
{scraps.map((scrap, index) => (
))}
);
});
+
+export default DragContainer;
diff --git a/frontend/components/common/DragDrop/Container/styled.ts b/frontend/components/common/DragDrop/Container/styled.ts
index 7dbec1b6..8b22b090 100644
--- a/frontend/components/common/DragDrop/Container/styled.ts
+++ b/frontend/components/common/DragDrop/Container/styled.ts
@@ -4,11 +4,6 @@ const ContainerWapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
-
- div {
- border-bottom: 1px solid var(--grey-02-color);
- height: 28px;
- }
`;
export default ContainerWapper;
diff --git a/frontend/components/common/DragDrop/ListItem/index.tsx b/frontend/components/common/DragDrop/ListItem/index.tsx
index bdfba4ce..1175f52b 100644
--- a/frontend/components/common/DragDrop/ListItem/index.tsx
+++ b/frontend/components/common/DragDrop/ListItem/index.tsx
@@ -1,35 +1,49 @@
import { memo } from 'react';
import { useDrag, useDrop } from 'react-dnd';
-import Article from './styled';
+import { useRecoilState } from 'recoil';
+
+import MinusWhite from '@assets/ico_minus_white.svg';
+import editInfoState from '@atoms/editInfo';
+import scrapState from '@atoms/scrap';
+
+import { Article, Text, MinusButton, MinusIcon, OriginalBadge, TextWapper } from './styled';
const ItemTypes = {
Scrap: 'scrap',
};
export interface ScrapProps {
- id: string;
+ id: number;
+ scrapId: number;
text: string;
- moveScrap: (id: string, to: number) => void;
- findScrap: (id: string) => { index: number };
+ isOriginal: boolean;
+ moveScrap: (id: number, to: number) => void;
+ findScrap: (id: number) => { index: number };
isShown: boolean;
isContentsShown: boolean;
+ isDeleteBtnShown: boolean;
}
interface Item {
- id: string;
+ id: number;
originalIndex: number;
}
export const ListItem = memo(function Scrap({
id,
+ scrapId,
text,
+ isOriginal,
moveScrap,
findScrap,
isShown,
isContentsShown,
+ isDeleteBtnShown,
}: ScrapProps) {
const originalIndex = findScrap(id).index;
+ const [scraps, setScraps] = useRecoilState(scrapState);
+ const [editInfo, setEditInfo] = useRecoilState(editInfoState);
// Drag
const [{ isDragging }, drag] = useDrag(
@@ -67,9 +81,39 @@ export const ListItem = memo(function Scrap({
[findScrap, moveScrap]
);
+ const handleMinusBtnClick = () => {
+ // 원본글이 아니면 스크랩에서만 삭제
+ // 원본글이면 실제로 삭제
+ if (window.confirm('글을 책에서 삭제하시겠습니까?')) {
+ if (isOriginal) {
+ setEditInfo({
+ ...editInfo,
+ deletedArticle: [...editInfo.deletedArticle, id],
+ deletedScraps: [...editInfo.deletedScraps, scrapId],
+ });
+ setScraps(scraps.filter((v) => v.article.id !== id));
+ return;
+ }
+
+ setEditInfo({
+ ...editInfo,
+ deletedScraps: [...editInfo.deletedScraps, scrapId],
+ });
+ setScraps(scraps.filter((v) => v.article.id !== id));
+ }
+ };
+
return (
drag(drop(node))} isShown={isContentsShown ? true : isShown}>
- {text}
+
+ {text}
+ {isOriginal && isDeleteBtnShown && 원본}
+
+ {isDeleteBtnShown && (
+
+
+
+ )}
);
});
diff --git a/frontend/components/common/DragDrop/ListItem/styled.ts b/frontend/components/common/DragDrop/ListItem/styled.ts
index 746f128a..4e0e27eb 100644
--- a/frontend/components/common/DragDrop/ListItem/styled.ts
+++ b/frontend/components/common/DragDrop/ListItem/styled.ts
@@ -1,13 +1,39 @@
+import Image from 'next/image';
+
import styled from 'styled-components';
-const Article = styled.div<{ isShown: true | false }>`
+import { TextXSmall } from '@styles/common';
+import { Flex } from '@styles/layout';
+
+export const Text = styled.span``;
+export const Article = styled.div<{ isShown: true | false }>`
font-size: 14px;
line-height: 20px;
text-decoration: none;
color: inherit;
- display: ${(props) => (props.isShown ? 'block' : 'none')};
- padding: 2px 0;
+ display: ${(props) => (props.isShown ? 'flex' : 'none')};
+ justify-content: space-between;
+ align-items: center;
border-bottom: 1px solid var(--grey-02-color);
+ padding: 5px;
+`;
+
+export const MinusButton = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 30px;
+ border-radius: 50%;
+ background-color: var(--red-color);
`;
-export default Article;
+export const MinusIcon = styled(Image)``;
+export const TextWapper = styled(Flex)``;
+
+export const OriginalBadge = styled(TextXSmall)`
+ // background-color: var(--grey-02-color);
+ border: 1px solid var(--grey-02-color);
+ border-radius: 10px;
+ padding: 1px 3px;
+ margin-left: 5px;
+`;
diff --git a/frontend/components/common/DragDrop/dndInterface.ts b/frontend/components/common/DragDrop/dndInterface.ts
deleted file mode 100644
index 5cb406a1..00000000
--- a/frontend/components/common/DragDrop/dndInterface.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export interface EditScrap {
- id: number;
- order: number;
- article: {
- id: number;
- title: string;
- };
-}
-export interface ContainerState {
- data: EditScrap[];
- isContentsShown: boolean;
-}
diff --git a/frontend/components/common/DragDrop/index.tsx b/frontend/components/common/DragDrop/index.tsx
index fa1ed574..ed4d57d7 100644
--- a/frontend/components/common/DragDrop/index.tsx
+++ b/frontend/components/common/DragDrop/index.tsx
@@ -1,11 +1,12 @@
import { DndProvider } from 'react-dnd';
-import { HTML5Backend } from 'react-dnd-html5-backend';
+import { TouchBackend } from 'react-dnd-touch-backend';
-import { Container } from '@components/common/DragDrop/Container';
+import DragContainer from '@components/common/DragDrop/Container';
export interface EditScrap {
id: number;
order: number;
+ is_original: boolean;
article: {
id: number;
title: string;
@@ -14,12 +15,17 @@ export interface EditScrap {
export interface ContainerState {
data: EditScrap[];
isContentsShown: boolean;
+ isDeleteBtnShown: boolean;
}
-export default function DragArticle({ data, isContentsShown }: any) {
+export default function DragArticle({ data, isContentsShown, isDeleteBtnShown }: ContainerState) {
return (
-
-
+
+
);
}
diff --git a/frontend/components/common/GNB/index.tsx b/frontend/components/common/GNB/index.tsx
index 5d7f591d..9879bdcf 100644
--- a/frontend/components/common/GNB/index.tsx
+++ b/frontend/components/common/GNB/index.tsx
@@ -8,9 +8,9 @@ import ArticleIcon from '@assets/ico_article.svg';
import PersonIcon from '@assets/ico_person.svg';
import SearchIcon from '@assets/ico_search.svg';
import signInStatusState from '@atoms/signInStatus';
+import SignInModal from '@components/auth/SignInModal';
+import SignUpModal from '@components/auth/SignUpModal';
import Modal from '@components/common/Modal';
-import SignInModal from '@components/SignInModal';
-import SignUpModal from '@components/SignUpModal';
import { GNBbar, Icon, IconsContainer, Logo } from './styled';
diff --git a/frontend/components/common/Modal/styled.ts b/frontend/components/common/Modal/styled.ts
index 8025d329..ee46f1d7 100644
--- a/frontend/components/common/Modal/styled.ts
+++ b/frontend/components/common/Modal/styled.ts
@@ -27,6 +27,11 @@ export const ModalInner = styled.div`
background: var(--white-color);
border-radius: 30px;
z-index: 100;
+
+ @media ${(props) => props.theme.mobile} {
+ width: 320px;
+ padding: 32px 20px;
+ }
`;
export const ButtonWrapper = styled.div<{ hasBackward?: boolean }>`
diff --git a/frontend/components/common/SkeletonBook/index.tsx b/frontend/components/common/SkeletonBook/index.tsx
new file mode 100644
index 00000000..23ca92f3
--- /dev/null
+++ b/frontend/components/common/SkeletonBook/index.tsx
@@ -0,0 +1,38 @@
+import { FlexColumn, FlexSpaceBetween } from '@styles/layout';
+
+import {
+ BookWrapper,
+ BookInfoContainer,
+ BookTitle,
+ BookContentsInfo,
+ BookContents,
+ BookThumbnail,
+ BookAuthor,
+ Bookmark,
+} from './styled';
+
+export default function SkeletonBook() {
+ const bookContentsList = Array.from({ length: 4 }, (_, i) => i + 1);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {bookContentsList.map((key) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/components/common/SkeletonBook/styled.ts b/frontend/components/common/SkeletonBook/styled.ts
new file mode 100644
index 00000000..8211d17e
--- /dev/null
+++ b/frontend/components/common/SkeletonBook/styled.ts
@@ -0,0 +1,90 @@
+import Image from 'next/image';
+
+import styled from 'styled-components';
+
+import { FlexColumn } from '@styles/layout';
+
+const SkeletonItem = styled.div`
+ width: 100%;
+ height: 30px;
+ background-color: #f2f2f2;
+ position: relative;
+ overflow: hidden;
+ border-radius: 4px;
+
+ @keyframes skeleton-gradient {
+ 0% {
+ background-color: rgba(165, 165, 165, 0.1);
+ }
+ 50% {
+ background-color: rgba(165, 165, 165, 0.3);
+ }
+ 100% {
+ background-color: rgba(165, 165, 165, 0.1);
+ }
+ }
+
+ &:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ animation: skeleton-gradient 1.5s infinite ease-in-out;
+ }
+`;
+
+export const BookWrapper = styled(FlexColumn)`
+ min-width: 280px;
+ max-width: 280px;
+ height: 480px;
+ margin: 0 10px;
+ box-sizing: border-box;
+
+ background-color: var(--white-color);
+ border: 1px solid var(--primary-color);
+ border-radius: 10px;
+ overflow: hidden;
+`;
+
+export const BookThumbnail = styled(SkeletonItem)`
+ width: 280px;
+ height: 200px;
+ min-height: 200px;
+`;
+
+export const BookInfoContainer = styled(FlexColumn)`
+ padding: 15px 24px;
+ gap: 18px;
+`;
+
+export const BookTitle = styled(SkeletonItem)`
+ height: 30px;
+ width: 140px;
+`;
+
+export const BookAuthor = styled(SkeletonItem)`
+ height: 20px;
+ width: 60px;
+ margin-top: 10px;
+`;
+
+export const Bookmark = styled(SkeletonItem)`
+ height: 30px;
+ width: 30px;
+`;
+
+export const BookmarkIcon = styled(Image)`
+ cursor: pointer;
+`;
+
+export const BookContentsInfo = styled(FlexColumn)`
+ gap: 8px;
+ margin-top: 30px;
+`;
+
+export const BookContents = styled(SkeletonItem)`
+ height: 20px;
+ width: 100%;
+`;
diff --git a/frontend/components/common/Spinner/index.tsx b/frontend/components/common/Spinner/index.tsx
new file mode 100644
index 00000000..60fa0e24
--- /dev/null
+++ b/frontend/components/common/Spinner/index.tsx
@@ -0,0 +1,19 @@
+import { SpinnerInner, SpinnerWrapper } from './styled';
+
+interface SpinnerStyle {
+ width: number;
+ height: number;
+ borderWidth: number;
+}
+
+interface SpinnerProps {
+ style: SpinnerStyle;
+}
+
+export default function Spinner({ style }: SpinnerProps) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/components/common/Spinner/styled.ts b/frontend/components/common/Spinner/styled.ts
new file mode 100644
index 00000000..668c9ac5
--- /dev/null
+++ b/frontend/components/common/Spinner/styled.ts
@@ -0,0 +1,37 @@
+import styled from 'styled-components';
+
+interface SpinnerWrapperProps {
+ width: number;
+ height: number;
+}
+
+export const SpinnerWrapper = styled.div`
+ width: ${(props) => props.width}px;
+ height: ${(props) => props.height}px;
+`;
+
+interface SpinnerInnerProps {
+ borderWidth: number;
+}
+
+export const SpinnerInner = styled.div`
+ width: 100%;
+ height: 100%;
+ background-color: transparent;
+ border: ${(props) => props.borderWidth}px solid var(--grey-02-color);
+ border-top: ${(props) => props.borderWidth}px solid var(--primary-color);
+ border-radius: 50%;
+ box-sizing: border-box;
+
+ @keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+
+ animation: spin 1s infinite ease;
+`;
diff --git a/frontend/components/edit/EditBar/index.tsx b/frontend/components/edit/EditBar/index.tsx
index 5f8ae692..18127792 100644
--- a/frontend/components/edit/EditBar/index.tsx
+++ b/frontend/components/edit/EditBar/index.tsx
@@ -1,19 +1,65 @@
+import { useRouter } from 'next/router';
+
+import { useEffect } from 'react';
+
+import { useRecoilValue, useSetRecoilState } from 'recoil';
+
+import { createTemporaryArticleApi, getTemporaryArticleApi } from '@apis/articleApi';
+import articleState from '@atoms/article';
+import articleBuffer from '@atoms/articleBuffer';
+import useFetch from '@hooks/useFetch';
+
import { Bar, ButtonGroup, ExitButton, PublishButton, TemporaryButton } from './styled';
interface EditBarProps {
handleModalOpen: () => void;
+ isModifyMode: boolean;
}
-export default function EditBar({ handleModalOpen }: EditBarProps) {
+export default function EditBar({ handleModalOpen, isModifyMode }: EditBarProps) {
+ const article = useRecoilValue(articleState);
+ const setBuffer = useSetRecoilState(articleBuffer);
+ const router = useRouter();
+
+ const { data: temporaryArticle, execute: getTemporaryArticle } = useFetch(getTemporaryArticleApi);
+ const { execute: createTemporaryArticle } = useFetch(createTemporaryArticleApi);
+
+ const handleLoadButton = () => {
+ getTemporaryArticle();
+ };
+
+ const handleSaveButton = () => {
+ createTemporaryArticle({ title: article.title, content: article.content });
+ };
+
+ const handleExitButton = () => {
+ const confirm = window.confirm('정말 나가시겠습니까?');
+
+ if (confirm) router.push('/');
+ };
+
+ useEffect(() => {
+ if (!temporaryArticle) return;
+
+ setBuffer({
+ title: temporaryArticle.title,
+ content: temporaryArticle.content,
+ });
+ }, [temporaryArticle]);
+
return (
- 나가기
+ handleExitButton()}>
+ 나가기
+
- 불러오기
- 임시 저장
- 발행
+ handleLoadButton()}>불러오기
+ handleSaveButton()}>저장
+
+ {isModifyMode ? '수정하기' : '발행'}
+
);
diff --git a/frontend/components/edit/EditHead/index.tsx b/frontend/components/edit/EditHead/index.tsx
new file mode 100644
index 00000000..f5deb269
--- /dev/null
+++ b/frontend/components/edit/EditHead/index.tsx
@@ -0,0 +1,9 @@
+import Head from 'next/head';
+
+export default function EditHead() {
+ return (
+
+ Knoticle
+
+ );
+}
diff --git a/frontend/components/edit/Editor/core/theme.ts b/frontend/components/edit/Editor/core/theme.ts
index 362f83a8..8ba08326 100644
--- a/frontend/components/edit/Editor/core/theme.ts
+++ b/frontend/components/edit/Editor/core/theme.ts
@@ -3,21 +3,23 @@ import { tags } from '@lezer/highlight';
export default function theme() {
const highlightStyle = HighlightStyle.define([
- {
- tag: tags.heading1,
- 'font-size': '24px',
- 'font-weight': '700',
- },
- {
- tag: tags.heading2,
- 'font-size': '20px',
- 'font-weight': '700',
- },
- {
- tag: tags.heading3,
- 'font-size': '16px',
- 'font-weight': '700',
- },
+ { tag: tags.heading1, fontSize: '24px', fontWeight: '700' },
+ { tag: tags.heading2, fontSize: '20px', fontWeight: '700' },
+ { tag: tags.heading3, fontSize: '16px', fontWeight: '700' },
+ { tag: tags.link, textDecoration: 'underline' },
+ { tag: tags.strikethrough, textDecoration: 'line-through' },
+ { tag: tags.invalid, color: '#cb2431' },
+ { tag: [tags.string, tags.meta, tags.regexp], color: '#222222', fontWeight: 700 },
+ { tag: [tags.heading, tags.strong], color: '#222222', fontWeight: '700' },
+ { tag: [tags.emphasis], color: '#24292e', fontStyle: 'italic' },
+ { tag: [tags.comment, tags.bracket], color: '#6a737d' },
+ { tag: [tags.className, tags.propertyName], color: '#6f42c1' },
+ { tag: [tags.variableName, tags.attributeName, tags.number, tags.operator], color: '#005cc5' },
+ { tag: [tags.keyword, tags.typeName, tags.typeOperator, tags.typeName], color: '#d73a49' },
+ { tag: [tags.name, tags.quote], color: '#22863a' },
+ { tag: [tags.deleted], color: '#b31d28', backgroundColor: 'ffeef0' },
+ { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: '#e36209' },
+ { tag: [tags.url, tags.escape, tags.regexp, tags.link], color: '#222222' },
]);
return [syntaxHighlighting(highlightStyle)];
diff --git a/frontend/components/edit/Editor/core/useCodeMirror.ts b/frontend/components/edit/Editor/core/useCodeMirror.ts
index 832fed35..b2ab2cb4 100644
--- a/frontend/components/edit/Editor/core/useCodeMirror.ts
+++ b/frontend/components/edit/Editor/core/useCodeMirror.ts
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
+import { languages } from '@codemirror/language-data';
import { EditorState } from '@codemirror/state';
import { placeholder } from '@codemirror/view';
import { EditorView } from 'codemirror';
@@ -15,7 +16,7 @@ export default function useCodeMirror() {
const [editorView, setEditorView] = useState();
- const [value, setValue] = useState('');
+ const [document, setDocument] = useState('');
const [element, setElement] = useState();
const ref = useCallback((node: HTMLElement | null) => {
@@ -24,9 +25,21 @@ export default function useCodeMirror() {
setElement(node);
}, []);
+ const replaceDocument = (insert: string) => {
+ if (!editorView) return;
+
+ editorView.dispatch({
+ changes: {
+ from: 0,
+ to: editorView.state.doc.length,
+ insert,
+ },
+ });
+ };
+
const onChange = () => {
return EditorView.updateListener.of(({ view, docChanged }) => {
- if (docChanged) setValue(view.state.doc.toString());
+ if (docChanged) setDocument(view.state.doc.toString());
});
};
@@ -60,7 +73,7 @@ export default function useCodeMirror() {
const markdownImage = (path: string) => `![image](${path})\n`;
- const insert = markdownImage(image.imagePath);
+ const insert = markdownImage(image?.imagePath);
editorView.dispatch({
changes: {
@@ -77,11 +90,15 @@ export default function useCodeMirror() {
const editorState = EditorState.create({
extensions: [
- markdown({ base: markdownLanguage }),
+ markdown({
+ base: markdownLanguage,
+ codeLanguages: languages,
+ }),
placeholder('내용을 입력해주세요.'),
theme(),
onChange(),
onPaste(),
+ EditorView.lineWrapping,
],
});
@@ -96,5 +113,5 @@ export default function useCodeMirror() {
return () => view?.destroy();
}, [element]);
- return { ref, value };
+ return { ref, document, replaceDocument };
}
diff --git a/frontend/components/edit/Editor/index.tsx b/frontend/components/edit/Editor/index.tsx
index 6d1297a8..357b92e5 100644
--- a/frontend/components/edit/Editor/index.tsx
+++ b/frontend/components/edit/Editor/index.tsx
@@ -1,47 +1,57 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
-import rehypeStringify from 'rehype-stringify';
-import remarkParse from 'remark-parse';
-import remarkRehype from 'remark-rehype';
-import { unified } from 'unified';
import articleState from '@atoms/article';
+import articleBuffer from '@atoms/articleBuffer';
import Content from '@components/common/Content';
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 { CodeMirrorWrapper, EditorInner, EditorWrapper, TitleInput } from './styled';
interface EditorProps {
handleModalOpen: () => void;
+ originalArticle?: IArticle;
}
-export default function Editor({ handleModalOpen }: EditorProps) {
- const { ref, value } = useCodeMirror();
+export default function Editor({ handleModalOpen, originalArticle }: EditorProps) {
+ const { ref, document, replaceDocument } = useCodeMirror();
+ const [buffer, setBuffer] = useRecoilState(articleBuffer);
+ const [isModifyMode, setIsModifyMode] = useState(false);
const [article, setArticle] = useRecoilState(articleState);
const title = useInput();
useEffect(() => {
- setArticle({
- ...article,
- title: title.value,
- });
- }, [title.value]);
+ if (originalArticle) {
+ setIsModifyMode(true);
+ setBuffer({
+ title: originalArticle.title,
+ content: originalArticle.content,
+ });
+ }
+ }, [originalArticle]);
+
+ useEffect(() => {
+ if (!buffer.title && !buffer.content) return;
+
+ title.setValue(buffer.title);
+ replaceDocument(html2markdown(buffer.content));
+
+ setBuffer({ title: '', content: '' });
+ }, [buffer]);
useEffect(() => {
setArticle({
...article,
- content: unified()
- .use(remarkParse)
- .use(remarkRehype)
- .use(rehypeStringify)
- .processSync(value)
- .toString(),
+ title: title.value,
+ content: markdown2html(document),
});
- }, [value]);
+ }, [title.value, document]);
return (
@@ -50,7 +60,7 @@ export default function Editor({ handleModalOpen }: EditorProps) {
-
+
@@ -58,3 +68,7 @@ export default function Editor({ handleModalOpen }: EditorProps) {
);
}
+
+Editor.defaultProps = {
+ originalArticle: '',
+};
diff --git a/frontend/components/edit/Editor/styled.ts b/frontend/components/edit/Editor/styled.ts
index 117dfb5f..2d5c4d8f 100644
--- a/frontend/components/edit/Editor/styled.ts
+++ b/frontend/components/edit/Editor/styled.ts
@@ -15,6 +15,12 @@ export const EditorInner = styled.div`
overflow: auto;
padding: 32px;
position: relative;
+
+ @media ${(props) => props.theme.tablet} {
+ &:nth-child(2) {
+ display: none;
+ }
+ }
`;
export const CodeMirrorWrapper = styled.div`
diff --git a/frontend/components/edit/ModifyModal/index.tsx b/frontend/components/edit/ModifyModal/index.tsx
new file mode 100644
index 00000000..6c9714dc
--- /dev/null
+++ b/frontend/components/edit/ModifyModal/index.tsx
@@ -0,0 +1,128 @@
+import { useRouter } from 'next/router';
+
+import { useEffect, useState } from 'react';
+
+import { useRecoilState } from 'recoil';
+
+import { modifyArticleApi } from '@apis/articleApi';
+import articleState from '@atoms/article';
+import scrapState from '@atoms/scrap';
+import DragArticle from '@components/common/DragDrop';
+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 { ArticleWrapper, Label, ModifyModalWrapper, WarningLabel } from './styled';
+
+interface ModifyModalProps {
+ books: IBookScraps[];
+ originalArticle: IArticle;
+}
+
+export default function ModifyModal({ books, originalArticle }: ModifyModalProps) {
+ const router = useRouter();
+
+ const { id: originalArticleId, book_id: originalBookId } = originalArticle as IArticle;
+
+ const { data: modifiedArticle, execute: modifyArticle } = useFetch(modifyArticleApi);
+
+ const [article, setArticle] = useRecoilState(articleState);
+
+ const [selectedBookIndex, setSelectedBookIndex] = useState(-1);
+ const [filteredScraps, setFilteredScraps] = useState([]);
+ const [scrapList, setScrapList] = useRecoilState(scrapState);
+
+ const [isSelectedBookUnavailable, setSelectedBookUnavailable] = useState(false);
+
+ const createBookDropdownItems = (items: IBook[]) =>
+ items.map((item) => {
+ return {
+ id: item.id,
+ name: item.title,
+ };
+ });
+
+ const checkArticleExistsInBook = (articleId: number, items: IScrap[]) => {
+ return items.some((item) => item.article.id === articleId);
+ };
+
+ const createScrapDropdownItems = (items: IScrap[]) => {
+ const itemList = [...items];
+
+ if (selectedBookIndex !== originalBookId)
+ itemList.push({
+ id: 0,
+ order: items.length + 1,
+ is_original: false,
+ article: { id: 0, title: article.title },
+ });
+ return itemList;
+ };
+
+ useEffect(() => {
+ if (selectedBookIndex === -1) return;
+
+ const selectedBook = books.find((book) => book.id === selectedBookIndex);
+
+ if (
+ !selectedBook ||
+ (selectedBookIndex !== originalBookId &&
+ checkArticleExistsInBook(originalArticleId, selectedBook.scraps))
+ ) {
+ setSelectedBookIndex(-1);
+ setSelectedBookUnavailable(true);
+ setFilteredScraps([]);
+ return;
+ }
+
+ setSelectedBookUnavailable(false);
+ setFilteredScraps(selectedBook.scraps);
+
+ setArticle({
+ ...article,
+ book_id: selectedBookIndex,
+ });
+ }, [selectedBookIndex]);
+
+ useEffect(() => {
+ setScrapList(createScrapDropdownItems(filteredScraps));
+ }, [filteredScraps]);
+
+ const handleModifyBtnClick = () => {
+ const scraps = scrapList.map((v: IScrap, i: number) => ({ ...v, order: i + 1 }));
+ modifyArticle(originalArticleId, { article, scraps });
+ };
+
+ useEffect(() => {
+ if (modifiedArticle) router.push('/');
+ }, [modifiedArticle]);
+
+ return (
+
+
+ setSelectedBookIndex(id)}
+ />
+ {isSelectedBookUnavailable && (
+ 선택하신 책에 본 글이 스크랩되어 있습니다.
+ )}
+ {filteredScraps.length !== 0 && (
+
+
+
+
+ )}
+
+ 수정하기
+
+
+ );
+}
diff --git a/frontend/components/edit/ModifyModal/styled.ts b/frontend/components/edit/ModifyModal/styled.ts
new file mode 100644
index 00000000..32010994
--- /dev/null
+++ b/frontend/components/edit/ModifyModal/styled.ts
@@ -0,0 +1,22 @@
+import styled from 'styled-components';
+
+import { TextLarge, TextMedium } from '@styles/common';
+
+export const ModifyModalWrapper = styled.div`
+ margin-top: 32px;
+
+ > div {
+ margin-bottom: 16px;
+ }
+`;
+
+export const Label = styled(TextLarge)``;
+export const ArticleWrapper = styled.div`
+ width: 100%;
+ height: 300px;
+ overflow: auto;
+`;
+
+export const WarningLabel = styled(TextMedium)`
+ color: var(--red-color);
+`;
diff --git a/frontend/components/edit/PublishModal/index.tsx b/frontend/components/edit/PublishModal/index.tsx
index f0504cb3..bb7de33c 100644
--- a/frontend/components/edit/PublishModal/index.tsx
+++ b/frontend/components/edit/PublishModal/index.tsx
@@ -12,7 +12,6 @@ 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 { IEditScrap } from 'interfaces/scrap.interface';
import { ArticleWrapper, Label, PublishModalWrapper } from './styled';
@@ -23,14 +22,14 @@ interface PublishModalProps {
export default function PublishModal({ books }: PublishModalProps) {
const router = useRouter();
- const { execute: createArticle } = useFetch(createArticleApi);
+ const { data: createdArticle, execute: createArticle } = useFetch(createArticleApi);
// 전역으로 관리해야할까?
const [article, setArticle] = useRecoilState(articleState);
const [selectedBookIndex, setSelectedBookIndex] = useState(-1);
const [filteredScraps, setFilteredScraps] = useState([]);
- const [scrapList, setScrapList] = useRecoilState(scrapState);
+ const [scrapList, setScrapList] = useRecoilState(scrapState);
const createBookDropdownItems = (items: IBook[]) =>
items.map((item) => {
@@ -40,12 +39,16 @@ export default function PublishModal({ books }: PublishModalProps) {
};
});
- const createScrapDropdownItems = (items: IEditScrap[]) => {
- // 깔끔하게 리팩토릭 필요
- const itemList = [...items];
-
- itemList.push({ id: 0, order: items.length + 1, article: { id: 0, title: article.title } });
- return itemList;
+ const createScrapDropdownItems = (items: IScrap[]) => {
+ return [
+ ...items,
+ {
+ id: 0,
+ order: items.length + 1,
+ is_original: true,
+ article: { id: article.id, title: article.title },
+ },
+ ];
};
useEffect(() => {
@@ -58,17 +61,21 @@ export default function PublishModal({ books }: PublishModalProps) {
book_id: selectedBookIndex,
});
}, [selectedBookIndex]);
+
useEffect(() => {
setScrapList(createScrapDropdownItems(filteredScraps));
}, [filteredScraps]);
const handlePublishBtnClick = () => {
- const scraps = scrapList.map((v: IEditScrap, i: number) => ({ ...v, order: i + 1 }));
+ const scraps = scrapList.map((v, i) => ({ ...v, order: i + 1 }));
createArticle({ article, scraps });
- router.push('/');
};
+ useEffect(() => {
+ if (createdArticle) router.push('/');
+ }, [createdArticle]);
+
return (
@@ -82,7 +89,11 @@ export default function PublishModal({ books }: PublishModalProps) {
{filteredScraps.length !== 0 && (
-
+
)}
diff --git a/frontend/components/home/HomeHead/index.tsx b/frontend/components/home/HomeHead/index.tsx
new file mode 100644
index 00000000..e01c0384
--- /dev/null
+++ b/frontend/components/home/HomeHead/index.tsx
@@ -0,0 +1,16 @@
+import Head from 'next/head';
+
+export default function HomeHead() {
+ return (
+
+ Knoticle
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/components/home/Slider/index.tsx b/frontend/components/home/Slider/index.tsx
index d65f4fe8..8a2b3970 100644
--- a/frontend/components/home/Slider/index.tsx
+++ b/frontend/components/home/Slider/index.tsx
@@ -6,6 +6,7 @@ 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 { IBookScraps } from '@interfaces';
import {
@@ -18,19 +19,24 @@ import {
SliderBookContainer,
SliderInfoContainer,
SliderIcon,
+ SliderTrack,
+ SliderBookWrapper,
} from './styled';
interface SliderProps {
bookList: IBookScraps[];
title: string;
+ isLoading: boolean;
+ numberPerPage: number;
}
-function Slider({ bookList, title }: SliderProps) {
+function Slider({ bookList, title, isLoading, numberPerPage }: SliderProps) {
const [curBookIndex, setCurBookIndex] = useState(0);
const [sliderNumber, setSliderNumber] = useState(1);
- const numberPerPage = 4;
- const sliderIndicatorCount = Math.ceil(bookList.length / numberPerPage);
+ const SkeletonList = Array.from({ length: numberPerPage }, (_, i) => i + 1);
+
+ const sliderIndicatorCount = bookList ? Math.ceil(bookList.length / numberPerPage) : 0;
const sliderIndicatorNumbersList = Array.from({ length: sliderIndicatorCount }, (_, i) => i + 1);
const handleLeftArrowClick = () => {
@@ -51,23 +57,31 @@ function Slider({ bookList, title }: SliderProps) {
isvisible={(sliderNumber !== 1).toString()}
/>
-
+
{title}
-
- {sliderIndicatorNumbersList.map((number) => {
- return ;
- })}
-
+ {numberPerPage !== 1 && (
+
+ {sliderIndicatorNumbersList.map((number) => {
+ return ;
+ })}
+
+ )}
-
- {bookList.map((book) => (
-
- ))}
+
+
+ {isLoading
+ ? SkeletonList.map((key) => )
+ : bookList.map((book) => (
+
+
+
+ ))}
+
diff --git a/frontend/components/home/Slider/styled.ts b/frontend/components/home/Slider/styled.ts
index f6d5cf98..aed12aa2 100644
--- a/frontend/components/home/Slider/styled.ts
+++ b/frontend/components/home/Slider/styled.ts
@@ -10,11 +10,18 @@ export const SliderWrapper = styled.div`
gap: 10px;
`;
-export const SliderContent = styled(FlexColumn)`
+export const SliderContent = styled(FlexColumn)<{ numberPerPage: number }>`
max-width: 1200px;
overflow: hidden;
gap: 10px;
margin-top: 30px;
+
+ max-width: ${(props) => {
+ if (props.numberPerPage === 1) return '300px';
+ if (props.numberPerPage === 2) return '600px';
+ if (props.numberPerPage === 3) return '900px';
+ return '1200px';
+ }};
`;
export const SliderInfoContainer = styled(FlexSpaceBetween)`
@@ -32,11 +39,14 @@ export const SliderTitle = styled.div`
font-weight: 700;
`;
-export const SliderBookContainer = styled.div<{ curBookIndex: number }>`
+export const SliderBookContainer = styled.div`
+ position: relative;
+`;
+
+export const SliderTrack = styled.div<{ curBookIndex: number }>`
display: flex;
${(props) => `transform: translateX(-${300 * props.curBookIndex}px);`}
transition: transform 700ms ease 0ms;
- z-index: 0;
`;
export const SliderIndicatorContainer = styled.div`
@@ -45,6 +55,29 @@ export const SliderIndicatorContainer = styled.div`
gap: 4px;
`;
+export const SliderBookWrapper = styled.div<{ numberPerPage: number }>`
+ min-width: 300px;
+ @media ${(props) => props.theme.desktop} {
+ min-width: 300px;
+ }
+
+ @media ${(props) => props.theme.tablet} {
+ min-width: 280px;
+ margin: 0 10px;
+ }
+
+ @media ${(props) => props.theme.mobile} {
+ min-width: 280px;
+ margin: 0 10px;
+ }
+
+ ${(props) => {
+ if (props.numberPerPage === 1) return 'min-width: 280px; margin: 0 10px;';
+ if (props.numberPerPage === 2) return 'min-width: 280px; margin: 0 10px;';
+ return 'min-width: 300px;';
+ }};
+`;
+
export const SliderIndicator = styled.div<{ isActive: boolean }>`
width: 40px;
height: 8px;
diff --git a/frontend/components/home/Slider/tempBookData.ts b/frontend/components/home/Slider/tempBookData.ts
deleted file mode 100644
index 98f78d44..00000000
--- a/frontend/components/home/Slider/tempBookData.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-const tempBookDatas = [
- {
- id: 1,
- title: '리액트 완전 정복',
- user: {
- id: 1,
- nickname: 'MinHK4',
- profile_image: '',
- },
- scraps: [
- {
- order: 1,
- article: {
- id: 5,
- title: 'Create-react-app',
- },
- },
- {
- order: 2,
- article: {
- id: 6,
- title: 'JSX',
- },
- },
- ],
- _count: {
- bookmarks: 398,
- },
- bookmark: [
- {
- id: 1,
- },
- ],
- },
- {
- id: 2,
- title: '리액트 완전 정복',
- user: {
- id: 1,
- nickname: 'MinHK4',
- profile_image: '',
- },
- scraps: [
- {
- order: 1,
- article: {
- id: 5,
- title: 'Create-react-app',
- },
- },
- {
- order: 2,
- article: {
- id: 6,
- title: 'JSX',
- },
- },
- ],
- _count: {
- bookmarks: 398,
- },
- bookmark: [],
- },
-];
-
-export default tempBookDatas;
diff --git a/frontend/components/search/ArticleItem/styled.ts b/frontend/components/search/ArticleItem/styled.ts
index b3eb30da..d59040dc 100644
--- a/frontend/components/search/ArticleItem/styled.ts
+++ b/frontend/components/search/ArticleItem/styled.ts
@@ -7,6 +7,8 @@ export const ItemWrapper = styled.div`
display: flex;
gap: 32px;
border-bottom: 1px solid var(--grey-02-color);
+ width: 100%;
+ box-sizing: border-box;
`;
export const ItemGroup = styled.div`
diff --git a/frontend/components/search/BookList/styled.ts b/frontend/components/search/BookList/styled.ts
index 391cf947..19f56885 100644
--- a/frontend/components/search/BookList/styled.ts
+++ b/frontend/components/search/BookList/styled.ts
@@ -9,7 +9,25 @@ export const BookListWrapper = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
- grid-row-gap: 30px;
+ box-sizing: border-box;
+ grid-gap: 20px 10px;
+ padding: 20px 0px;
margin-bottom: 30px;
+
+ & div {
+ justify-self: center;
+ }
+
+ @media ${(props) => props.theme.desktop} {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ @media ${(props) => props.theme.tablet} {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media ${(props) => props.theme.mobile} {
+ grid-template-columns: repeat(1, 1fr);
+ }
`;
diff --git a/frontend/components/search/SearchBar/styled.ts b/frontend/components/search/SearchBar/styled.ts
index 860acffb..d8b90b0b 100644
--- a/frontend/components/search/SearchBar/styled.ts
+++ b/frontend/components/search/SearchBar/styled.ts
@@ -16,4 +16,5 @@ export const SearchBarInput = styled.input`
outline: none;
font-size: 32px;
font-family: Noto Sans KR;
+ width: 100%;
`;
diff --git a/frontend/components/search/SearchFilter/index.tsx b/frontend/components/search/SearchFilter/index.tsx
index 1a581b73..28a7cf13 100644
--- a/frontend/components/search/SearchFilter/index.tsx
+++ b/frontend/components/search/SearchFilter/index.tsx
@@ -28,13 +28,15 @@ export default function SearchFilter({ handleFilter }: SearchFilterProps) {
책
-
- handleFilter({ userId: e.target.checked ? signInStatus.id : 0 })}
- />
- 내 책에서 검색
-
+ {signInStatus.id !== 0 && (
+
+ handleFilter({ userId: e.target.checked ? signInStatus.id : 0 })}
+ />
+ 내 책에서 검색
+
+ )}
);
}
diff --git a/frontend/components/search/SearchHead/index.tsx b/frontend/components/search/SearchHead/index.tsx
new file mode 100644
index 00000000..3ea5bebf
--- /dev/null
+++ b/frontend/components/search/SearchHead/index.tsx
@@ -0,0 +1,9 @@
+import Head from 'next/head';
+
+export default function SearchHead() {
+ return (
+
+ Knoticle
+
+ );
+}
diff --git a/frontend/components/study/AddBook/index.tsx b/frontend/components/study/AddBook/index.tsx
index 6f2c88d2..7602bd88 100644
--- a/frontend/components/study/AddBook/index.tsx
+++ b/frontend/components/study/AddBook/index.tsx
@@ -9,7 +9,6 @@ import signInStatusState from '@atoms/signInStatus';
import Button from '@components/common/Modal/ModalButton';
import useFetch from '@hooks/useFetch';
import useInput from '@hooks/useInput';
-import { IBookScraps } from '@interfaces';
import { FlexSpaceBetween } from '@styles/layout';
import { toastSuccess } from '@utils/toast';
@@ -31,8 +30,7 @@ interface AddBookProps {
}
export default function AddBook({ handleModalClose }: AddBookProps) {
- const [curKnottedBookList, setCurKnottedBookList] =
- useRecoilState(curKnottedBookListState);
+ const [curKnottedBookList, setCurKnottedBookList] = useRecoilState(curKnottedBookListState);
const user = useRecoilValue(signInStatusState);
const title = useInput('');
const { data: addBookData, execute: addBook } = useFetch(addBookApi);
@@ -41,7 +39,7 @@ export default function AddBook({ handleModalClose }: AddBookProps) {
if (!addBookData) return;
setCurKnottedBookList([...curKnottedBookList, addBookData]);
handleModalClose();
- toastSuccess(`${addBookData.title}책이 추가되었습니다!`);
+ toastSuccess(`<${addBookData.title}>책이 생성되었습니다!`);
}, [addBookData]);
const handleAddBookBtnClick = () => {
diff --git a/frontend/components/study/BookListTab/index.tsx b/frontend/components/study/BookListTab/index.tsx
index 281a1571..f659d3a6 100644
--- a/frontend/components/study/BookListTab/index.tsx
+++ b/frontend/components/study/BookListTab/index.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
@@ -43,11 +43,11 @@ export default function BookListTab({
const [isEditing, setIsEditing] = useState(false);
const handleEditBookModalOpen = (id: number) => {
- const curbook = knottedBookList?.find((v) => v.id === id);
- if (!curbook) return;
+ const curBook = knottedBookList?.find((v) => v.id === id);
+ if (!curBook) return;
setModalShown(true);
- setCurEditBook(curbook);
+ setCurEditBook(curBook);
};
const handleModalClose = () => {
@@ -55,16 +55,25 @@ export default function BookListTab({
};
const handleMinusBtnClick = (e: React.MouseEvent, id: number) => {
- e.stopPropagation();
- setCurKnottedBookList([...curKnottedBookList.filter((book) => id !== book.id)]);
- setEditInfo({
- ...editInfo,
- deleted: [...editInfo.deleted, id],
+ const curBook = knottedBookList.find((book) => book.id === id);
+ if (!curBook) return;
+ const originalArticleList: number[] = [];
+
+ curBook.scraps.forEach((scrap) => {
+ if (scrap.is_original) originalArticleList.push(scrap.article.id);
});
+
+ if (window.confirm('이 책에는 원본글이 포함되어 있습니다. 정말로 삭제하시겠습니까?')) {
+ setCurKnottedBookList([...curKnottedBookList.filter((book) => id !== book.id)]);
+ setEditInfo({
+ ...editInfo,
+ deleted: [...editInfo.deleted, id],
+ deletedArticle: [...editInfo.deletedArticle, ...originalArticleList],
+ });
+ }
};
const handleEditModalOpenerClick = (e: React.MouseEvent, bookId: number) => {
- e.stopPropagation();
handleEditBookModalOpen(bookId);
};
diff --git a/frontend/components/study/BookListTab/styled.ts b/frontend/components/study/BookListTab/styled.ts
index 725c48f9..f4d7a920 100644
--- a/frontend/components/study/BookListTab/styled.ts
+++ b/frontend/components/study/BookListTab/styled.ts
@@ -24,8 +24,22 @@ export const TabTitleContent = styled(TextLinkMedium)<{ isActive: boolean }>`
export const BookGrid = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
- gap: 30px;
+ box-sizing: border-box;
+ gap: 20px 0;
padding: 20px;
+
+ @media ${(props) => props.theme.desktop} {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ @media ${(props) => props.theme.tablet} {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+ }
+
+ @media ${(props) => props.theme.mobile} {
+ grid-template-columns: repeat(1, 1fr);
+ }
`;
export const EditModeIndicator = styled(TextLinkMedium)`
diff --git a/frontend/components/study/EditBook/index.tsx b/frontend/components/study/EditBook/index.tsx
index 35b6a5d6..79cccf24 100644
--- a/frontend/components/study/EditBook/index.tsx
+++ b/frontend/components/study/EditBook/index.tsx
@@ -16,7 +16,6 @@ import useFetch from '@hooks/useFetch';
import useInput from '@hooks/useInput';
import { IBookScraps } from '@interfaces';
import { FlexSpaceBetween } from '@styles/layout';
-import { IEditScrap } from 'interfaces/scrap.interface';
import {
BookWrapper,
@@ -47,7 +46,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) {
const [editInfo, setEditInfo] = useRecoilState(editInfoState);
const [curKnottedBookList, setCurKnottedBookList] = useRecoilState(curKnottedBookListState);
- const [scrapList] = useRecoilState(scrapState);
+ const [scrapList] = useRecoilState(scrapState);
const [isContentsShown, setIsContentsShown] = useState(false);
@@ -70,7 +69,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) {
};
const handleCompletedBtnClick = () => {
- const editScraps = scrapList.map((v: IEditScrap, i: number) => ({ ...v, order: i + 1 }));
+ const editScraps = scrapList.map((v, i) => ({ ...v, order: i + 1 }));
// 해당하는 책을 찾아서 전역에서 관리하고 있는 애를 변경해서 업데이트
setCurKnottedBookList([
@@ -91,7 +90,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) {
setEditInfo({
...editInfo,
editted: [
- ...editInfo.editted,
+ ...editInfo.editted.filter((edit) => edit.id !== id),
{
id,
title: titleData,
@@ -144,7 +143,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) {
Contents
-
+
diff --git a/frontend/components/study/EditBook/styled.ts b/frontend/components/study/EditBook/styled.ts
index dff242f5..61aa7f40 100644
--- a/frontend/components/study/EditBook/styled.ts
+++ b/frontend/components/study/EditBook/styled.ts
@@ -3,11 +3,15 @@ import Image from 'next/image';
import styled from 'styled-components';
import { TextXSmall, TextSmall } from '@styles/common';
-import { FlexCenter, FlexColumn, FlexColumnCenter } from '@styles/layout';
+import { FlexCenter, FlexColumn, FlexColumnAlignCenter } from '@styles/layout';
-export const EditBookWapper = styled(FlexColumnCenter)`
+export const EditBookWapper = styled(FlexColumnAlignCenter)`
width: 320px;
margin: auto;
+
+ @media ${(props) => props.theme.mobile} {
+ width: 300px;
+ }
`;
export const BookWrapper = styled(FlexColumn)`
@@ -111,6 +115,6 @@ export const MoreContentsIconWrapper = styled(FlexCenter)``;
export const DragArticleWrapper = styled.div<{ isContentsShown: true | false }>`
${(props) =>
props.isContentsShown
- ? { overflow: 'auto', height: '400px' }
+ ? { overflow: 'auto', height: '415px' }
: { overflow: 'none', height: '120px' }};
`;
diff --git a/frontend/components/study/EditUserProfile/index.tsx b/frontend/components/study/EditUserProfile/index.tsx
index 533984e5..0018dcfb 100644
--- a/frontend/components/study/EditUserProfile/index.tsx
+++ b/frontend/components/study/EditUserProfile/index.tsx
@@ -19,6 +19,8 @@ import {
UserThumbnail,
EditThumbnailIcon,
UserThumbnailGroup,
+ RedNotice,
+ UsernameGroup,
} from './styled';
interface EditUserProfileProps {
@@ -91,15 +93,22 @@ export default function EditUserProfile({
-
+
+
+ {nicknameValue === '' && 빈 공백은 닉네임으로 설정할 수 없습니다}
+
-
- 수정 완료
+
+ {nicknameValue === '' ? '수정 불가' : '수정 완료'}
diff --git a/frontend/components/study/EditUserProfile/styled.ts b/frontend/components/study/EditUserProfile/styled.ts
index c10293c3..f2c6260e 100644
--- a/frontend/components/study/EditUserProfile/styled.ts
+++ b/frontend/components/study/EditUserProfile/styled.ts
@@ -2,12 +2,19 @@ import Image from 'next/image';
import styled from 'styled-components';
+import { TextSmall } from '@styles/common';
+import { Flex } from '@styles/layout';
+
export const UserProfileWrapper = styled.div`
- width: 100%;
- margin: 40px 0 20px;
+ margin: 40px 0 20px 0;
+ width: 78%;
display: flex;
- align-items: flex-end;
- /* justify-content: flex-start; */
+ @media ${(props) => props.theme.mobile} {
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ margin: 20px;
+ }
`;
export const UserThumbnailGroup = styled.div``;
@@ -23,7 +30,9 @@ export const UserDetailGroup = styled.div`
display: flex;
flex-direction: column;
margin-left: 30px;
- /* background-color: red; */
+ @media ${(props) => props.theme.mobile} {
+ margin-top: 20px;
+ }
`;
export const Input = styled.input`
@@ -40,6 +49,10 @@ export const Input = styled.input`
margin: 5px 0;
`;
+export const UsernameGroup = styled(Flex)`
+ align-items: center;
+`;
+
export const EditUsername = styled(Input)`
font-size: 18px;
line-height: 24px;
@@ -49,7 +62,10 @@ export const EditUsername = styled(Input)`
export const EditUserDescription = styled(Input)`
font-size: 14px;
line-height: 20px;
- width: 340px;
+ width: 400px;
+ @media ${(props) => props.theme.tablet} {
+ width: 250px;
+ }
`;
export const ButtonGroup = styled.div<{ isVisible: boolean }>`
@@ -66,16 +82,16 @@ const Button = styled.button`
`;
export const ProfileEditButton = styled(Button)`
- padding: 0 10px;
- height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
+ padding: 0 10px;
+ height: 40px;
border-radius: 10px;
- border: 1px solid rgba(148, 173, 46, 1);
- background-color: var(--green-color);
color: var(--white-color);
+ border: 1px solid ${(props) => (props.disabled ? 'var(--red-color)' : 'rgba(148, 173, 46, 1)')};
+ background-color: ${(props) => (props.disabled ? 'var(--red-color)' : 'var(--green-color)')};
`;
export const EditThumbnailIcon = styled.div`
@@ -91,3 +107,8 @@ export const EditThumbnailIcon = styled.div`
background-color: var(--light-yellow-color);
cursor: pointer;
`;
+
+export const RedNotice = styled(TextSmall)`
+ color: var(--red-color);
+ margin-left: 10px;
+`;
diff --git a/frontend/components/study/FAB/index.tsx b/frontend/components/study/FAB/index.tsx
index 4e29a301..c89a46c5 100644
--- a/frontend/components/study/FAB/index.tsx
+++ b/frontend/components/study/FAB/index.tsx
@@ -4,7 +4,9 @@ import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
+import { deleteArticleApi } from '@apis/articleApi';
import { deleteBookApi, editBookApi } from '@apis/bookApi';
+import { deleteScrapApi } from '@apis/scrapApi';
import Add from '@assets/ico_add.svg';
import CheckWhite from '@assets/ico_check_white.svg';
import EditWhite from '@assets/ico_edit_white.svg';
@@ -24,6 +26,8 @@ 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 [editInfo, setEditInfo] = useRecoilState(editInfoState);
@@ -44,6 +48,14 @@ 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(() => {
@@ -64,15 +76,15 @@ export default function FAB({ isEditing, setIsEditing }: FabProps) {
});
}, [editBookData]);
- // useEffect(() => {
- // if (
- // deletedBook &&
- // editInfo.deleted.length === 0 &&
- // editBookData &&
- // editInfo.editted.length === 0
- // )
- // toastSuccess(`수정 완료되었습니다`);
- // }, [deletedBook, editBookData, editInfo]);
+ useEffect(() => {
+ if (
+ (deletedBook || editBookData) &&
+ editInfo.deleted.length === 0 &&
+ editInfo.editted.length === 0
+ ) {
+ toastSuccess(`수정 완료되었습니다`);
+ }
+ }, [editInfo]);
return (
diff --git a/frontend/components/study/StudyHead/index.tsx b/frontend/components/study/StudyHead/index.tsx
new file mode 100644
index 00000000..720608ba
--- /dev/null
+++ b/frontend/components/study/StudyHead/index.tsx
@@ -0,0 +1,22 @@
+import Head from 'next/head';
+
+interface StudyHeadProps {
+ userNickname: string;
+ userDescription: string;
+ userImage: string;
+}
+
+export default function StudyHead({ userNickname, userDescription, userImage }: StudyHeadProps) {
+ return (
+
+ {`${userNickname} - Knoticle`}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/components/study/UserProfile/styled.ts b/frontend/components/study/UserProfile/styled.ts
index 97df613d..2b89c6bf 100644
--- a/frontend/components/study/UserProfile/styled.ts
+++ b/frontend/components/study/UserProfile/styled.ts
@@ -5,11 +5,15 @@ import styled from 'styled-components';
import { TextLarge, TextSmall } from '@styles/common';
export const UserProfileWrapper = styled.div`
- width: 100%;
- margin: 40px 0 20px;
+ margin: 40px 0 20px 0;
+ width: 78%;
display: flex;
- align-items: flex-end;
- /* justify-content: flex-start; */
+ @media ${(props) => props.theme.mobile} {
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ margin: 20px;
+ }
`;
export const UserThumbnail = styled(Image)`
@@ -23,12 +27,22 @@ export const UserDetailGroup = styled.div`
display: flex;
flex-direction: column;
margin-left: 30px;
- /* background-color: red; */
+ @media ${(props) => props.theme.mobile} {
+ margin-top: 20px;
+ }
`;
-export const Username = styled(TextLarge)``;
+export const Username = styled(TextLarge)`
+ font-size: 24px;
+ margin-bottom: 10px;
+`;
-export const UserDescription = styled(TextSmall)``;
+export const UserDescription = styled(TextSmall)`
+ width: 400px;
+ @media ${(props) => props.theme.tablet} {
+ width: 300px;
+ }
+`;
export const ButtonGroup = styled.div<{ isVisible: boolean }>`
display: flex;
diff --git a/frontend/components/viewer/ArticleContent/Button/styled.ts b/frontend/components/viewer/ArticleContent/Button/styled.ts
index 821193dd..fc811df4 100644
--- a/frontend/components/viewer/ArticleContent/Button/styled.ts
+++ b/frontend/components/viewer/ArticleContent/Button/styled.ts
@@ -7,7 +7,7 @@ export const ViewerButton = styled.button`
border-radius: 10px;
border: 1px solid var(--grey-02-color);
background-color: white;
- margin-left: 10px;
+ width: fit-content;
`;
export const ViewerLabel = styled(TextSmall)`
diff --git a/frontend/components/viewer/ArticleContent/index.tsx b/frontend/components/viewer/ArticleContent/index.tsx
index 2ffca8c9..7536c14d 100644
--- a/frontend/components/viewer/ArticleContent/index.tsx
+++ b/frontend/components/viewer/ArticleContent/index.tsx
@@ -3,13 +3,16 @@ import { useRouter } from 'next/router';
import { useEffect } from 'react';
-import axios from 'axios';
+import { useRecoilValue } from 'recoil';
+import { deleteArticleApi } from '@apis/articleApi';
import LeftBtnIcon from '@assets/ico_leftBtn.svg';
import Original from '@assets/ico_original.svg';
import RightBtnIcon from '@assets/ico_rightBtn.svg';
import Scrap from '@assets/ico_scrap.svg';
+import signInStatusState from '@atoms/signInStatus';
import Content from '@components/common/Content';
+import useFetch from '@hooks/useFetch';
import { IArticleBook, IScrap } from '@interfaces';
import { TextLarge } from '@styles/common';
@@ -21,100 +24,112 @@ import {
ArticleRightBtn,
ArticleTitle,
ArticleTitleBtnBox,
+ ArticleContentsWrapper,
} from './styled';
interface ArticleProps {
article: IArticleBook;
scraps: IScrap[];
bookId: number;
+ bookAuthor: string;
handleScrapBtnClick: () => void;
}
-const user = {
- id: 1,
- nickname: 'moc1ha',
-};
+export default function Article({
+ article,
+ scraps,
+ bookId,
+ bookAuthor,
+ handleScrapBtnClick,
+}: ArticleProps) {
+ const user = useRecoilValue(signInStatusState);
-export default function Article({ article, scraps, bookId, handleScrapBtnClick }: ArticleProps) {
+ const { data: deleteArticleData, execute: deleteArticle } = useFetch(deleteArticleApi);
const router = useRouter();
+
const handleOriginalBtnOnClick = () => {
router.push(`/viewer/${article.book_id}/${article.id}`);
};
+
const handleLeftBtnOnClick = () => {
- const prevOrder =
- scraps.filter((scrap: IScrap) => scrap.article.id === article.id)[0].order - 1;
- const prevArticleId = scraps.filter((scrap: IScrap) => scrap.order === prevOrder)[0].article.id;
+ const prevOrder = scraps.filter((scrap) => scrap.article.id === article.id)[0].order - 1;
+ const prevArticleId = scraps.filter((scrap) => scrap.order === prevOrder)[0].article.id;
router.push(`/viewer/${bookId}/${prevArticleId}`);
};
+
const handleRightBtnOnClick = () => {
- const nextOrder =
- scraps.filter((scrap: IScrap) => scrap.article.id === article.id)[0].order + 1;
- const nextArticleId = scraps.filter((scrap: IScrap) => scrap.order === nextOrder)[0].article.id;
+ const nextOrder = scraps.filter((scrap) => scrap.article.id === article.id)[0].order + 1;
+ const nextArticleId = scraps.filter((scrap) => scrap.order === nextOrder)[0].article.id;
router.push(`/viewer/${bookId}/${nextArticleId}`);
};
+
const handleDeleteBtnOnClick = () => {
if (window.confirm('해당 글을 삭제하시겠습니까?')) {
- axios
- .delete(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/articles/${article.id}`)
- .catch((err) => {
- // 추후 에러 핸들링 추가 예정
- console.log(err);
- });
-
- router.push('/');
+ deleteArticle(article.id);
}
};
- const checkArticleAuthority = (id: number) => {
- if (scraps.find((v: IScrap) => v.article.id === id)) {
- return true;
+ const handleScrapDeleteBtnOnClick = () => {
+ if (window.confirm('해당 글을 책에서 삭제하시겠습니까?')) {
+ //
}
- // alert 두번뜨는 현상...
- // 404 페이지로 처리? 고민 중
- // alert('잘못된 접근입니다.');
- router.push('/');
- return false;
+ };
+
+ const handleModifyBtnOnClick = () => {
+ router.push(`/editor?id=${article.id}`);
};
useEffect(() => {
- checkArticleAuthority(article.id);
- }, []);
+ if (deleteArticleData !== undefined) router.push('/');
+ }, [deleteArticleData]);
return (
{article.id === scraps.at(0)?.article.id ? null : (
-
+
)}
{!article.deleted_at ? (
-
- {/* Global style Large의 크기가 너무 작음 -> 월요일 회의 후 반영 */}
- {article.title}
-
- {article.book.user.nickname === user.nickname ? (
- 삭제
- ) : (
-
-
- 원본 글 보기
-
- )}
+
+
+ {article.title}
+
+
+
+
+
+ {article.book_id !== bookId && (
+
+
+ 원본 글 보기
+
+ )}
+ {article.book_id === bookId && article.book.user.nickname === user.nickname && (
+ <>
+ 글 삭제
+ 글 수정
+ >
+ )}
+ {/* {article.book_id !== bookId && bookAuthor === user.nickname && (
+ 스크랩 삭제
+ )} */}
+ {user.id !== 0 && (
스크랩
-
-
-
+ )}
+
) : (
삭제된 글입니다.
)}
+
{article.id === scraps.at(-1)?.article.id ? null : (
-
+
)}
diff --git a/frontend/components/viewer/ArticleContent/styled.ts b/frontend/components/viewer/ArticleContent/styled.ts
index 7ccc2807..739a0980 100644
--- a/frontend/components/viewer/ArticleContent/styled.ts
+++ b/frontend/components/viewer/ArticleContent/styled.ts
@@ -1,6 +1,6 @@
import styled from 'styled-components';
-import { Flex } from '@styles/layout';
+import { Flex, FlexColumn } from '@styles/layout';
export const ArticleContainer = styled(Flex)`
flex: 1;
@@ -11,27 +11,56 @@ export const ArticleLeftBtn = styled.div`
top: 50%;
margin-left: 20px;
cursor: pointer;
+
+ @media ${(props) => props.theme.mobile} {
+ top: 97px;
+ }
`;
export const ArticleRightBtn = styled.div`
position: fixed;
top: 50%;
right: 25px;
cursor: pointer;
+
+ @media ${(props) => props.theme.mobile} {
+ top: 97px;
+ }
`;
export const ArticleMain = styled(Flex)`
flex-direction: column;
width: 100%;
padding: 50px;
- overflow: auto;
+ overflow-y: scroll;
+ box-sizing: border-box;
+
+ ::-webkit-scrollbar {
+ width: 10px;
+ }
+ ::-webkit-scrollbar-thumb {
+ background-color: var(--grey-02-color);
+ border-radius: 10px;
+ }
+
+ @media ${(props) => props.theme.mobile} {
+ padding: 50px 16px;
+ }
`;
-export const ArticleTitle = styled(Flex)`
+export const ArticleTitle = styled.div`
width: 100%;
border-bottom: 1px solid black;
padding: 25px 0;
- justify-content: space-between;
+ text-align: left;
+`;
+export const ArticleTitleBtnBox = styled(Flex)`
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 16px;
+ border-top: 1px solid var(--grey-02-color);
+ padding-top: 10px;
`;
-export const ArticleTitleBtnBox = styled(Flex)``;
export const ArticleContents = styled.div`
margin-top: 20px;
height: 481px;
`;
+
+export const ArticleContentsWrapper = styled(FlexColumn)``;
diff --git a/frontend/components/viewer/ClosedSideBar/styled.ts b/frontend/components/viewer/ClosedSideBar/styled.ts
index 67b4925b..1ff3f7a8 100644
--- a/frontend/components/viewer/ClosedSideBar/styled.ts
+++ b/frontend/components/viewer/ClosedSideBar/styled.ts
@@ -1,9 +1,9 @@
-import styled, { keyframes } from 'styled-components';
+import styled from 'styled-components';
import { FlexCenter } from '@styles/layout';
export const ClosedSideBarWrapper = styled.div`
- min-width: 50px;
+ min-width: 30px;
height: calc(100vh - 67px);
background-color: var(--primary-color);
`;
diff --git a/frontend/components/viewer/ScrapModal/index.tsx b/frontend/components/viewer/ScrapModal/index.tsx
index dcf0125b..88e860fc 100644
--- a/frontend/components/viewer/ScrapModal/index.tsx
+++ b/frontend/components/viewer/ScrapModal/index.tsx
@@ -8,10 +8,9 @@ import DragArticle from '@components/common/DragDrop';
import Dropdown from '@components/common/Dropdown';
import ModalButton from '@components/common/Modal/ModalButton';
import useFetch from '@hooks/useFetch';
-import { IBook, IBookScraps, IScrap, IArticle } from '@interfaces';
-import { IEditScrap } from 'interfaces/scrap.interface';
+import { IBook, IArticle, IScrap, IBookScraps } from '@interfaces';
-import { ArticleWrapper, Label, ScrapModalWrapper } from './styled';
+import { ArticleWrapper, Label, ScrapModalWrapper, WarningLabel } from './styled';
interface ScrapModalProps {
books: IBookScraps[];
@@ -24,7 +23,9 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo
const [filteredScraps, setFilteredScraps] = useState([]);
const { execute: createScrap } = useFetch(createScrapApi);
- const [scrapList, setScrapList] = useRecoilState(scrapState);
+ const [scrapList, setScrapList] = useRecoilState(scrapState);
+
+ const [isSelectedBookUnavailable, setSelectedBookUnavailable] = useState(false);
const createBookDropdownItems = (items: IBook[]) =>
items.map((item) => {
@@ -34,20 +35,37 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo
};
});
- const createScrapDropdownItems = (items: IEditScrap[]) => {
- const itemList = [...items];
+ const createScrapDropdownItems = (items: IScrap[]) => {
+ return [
+ ...items,
+ {
+ id: 0,
+ order: items.length + 1,
+ is_original: true,
+ article: { id: article.id, title: article.title },
+ },
+ ];
+ };
- itemList.push({
- id: 0,
- order: items.length + 1,
- article: { id: article.id, title: article.title },
- });
- return itemList;
+ const checkArticleExistsInBook = (articleId: number, items: IScrap[]) => {
+ return items.some((item) => item.article.id === articleId);
};
useEffect(() => {
+ if (selectedBookIndex === -1) return;
+
const selectedBook = books.find((book) => book.id === selectedBookIndex);
+ if (!selectedBook || checkArticleExistsInBook(article.id, selectedBook.scraps)) {
+ setSelectedBookIndex(-1);
+ setSelectedBookUnavailable(true);
+ setFilteredScraps([]);
+ return;
+ }
+
+ setSelectedBookUnavailable(false);
+ setFilteredScraps(selectedBook.scraps);
+
setFilteredScraps(selectedBook ? selectedBook.scraps : []);
}, [selectedBookIndex]);
@@ -58,7 +76,7 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo
const handleScrapBtnClick = () => {
if (selectedBookIndex === -1) return;
- const scraps = scrapList.map((v: IEditScrap, i: number) => ({ ...v, order: i + 1 }));
+ const scraps = scrapList.map((v, i) => ({ ...v, order: i + 1 }));
createScrap({ book_id: selectedBookIndex, article_id: article.id, scraps });
handleModalClose();
@@ -73,10 +91,17 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo
selectedId={selectedBookIndex}
handleItemSelect={(id) => setSelectedBookIndex(id)}
/>
+ {isSelectedBookUnavailable && (
+ 선택하신 책에 이미 동일한 글이 존재합니다.
+ )}
{filteredScraps.length !== 0 && (
-
+
)}
diff --git a/frontend/components/viewer/ScrapModal/styled.ts b/frontend/components/viewer/ScrapModal/styled.ts
index ac45a476..121ccf05 100644
--- a/frontend/components/viewer/ScrapModal/styled.ts
+++ b/frontend/components/viewer/ScrapModal/styled.ts
@@ -1,6 +1,6 @@
import styled from 'styled-components';
-import { TextLarge } from '@styles/common';
+import { TextLarge, TextMedium } from '@styles/common';
export const ScrapModalWrapper = styled.div`
margin-top: 32px;
@@ -16,3 +16,7 @@ export const ArticleWrapper = styled.div`
height: 300px;
overflow: auto;
`;
+
+export const WarningLabel = styled(TextMedium)`
+ color: var(--red-color);
+`;
diff --git a/frontend/components/viewer/TOC/index.tsx b/frontend/components/viewer/TOC/index.tsx
index f9197bd8..242c9e3c 100644
--- a/frontend/components/viewer/TOC/index.tsx
+++ b/frontend/components/viewer/TOC/index.tsx
@@ -68,7 +68,7 @@ export default function TOC({ articleId, book, handleSideBarOnClick }: TocProps)
-
+
Written by
{user.nickname}
diff --git a/frontend/components/viewer/TOC/styled.ts b/frontend/components/viewer/TOC/styled.ts
index 9580c0ce..bea16deb 100644
--- a/frontend/components/viewer/TOC/styled.ts
+++ b/frontend/components/viewer/TOC/styled.ts
@@ -24,6 +24,12 @@ export const TocWrapper = styled(Flex)`
flex-direction: column;
justify-content: space-between;
// animation: ${slide} 1s ease-in-out;
+
+ @media ${(props) => props.theme.mobile} {
+ position: absolute;
+ z-index: 5;
+ width: 100%;
+ }
`;
export const TocSideBar = styled.div`
@@ -74,10 +80,13 @@ export const TocArticle = styled(Link)`
}
`;
-export const TocProfile = styled(Flex)`
+export const TocProfile = styled(Link)`
+ display: flex;
justify-content: end;
align-items: end;
padding: 20px;
+ text-decoration: none;
+ color: inherit;
`;
export const TocProfileText = styled(Flex)`
diff --git a/frontend/components/viewer/ViewerHead/index.tsx b/frontend/components/viewer/ViewerHead/index.tsx
new file mode 100644
index 00000000..052c5093
--- /dev/null
+++ b/frontend/components/viewer/ViewerHead/index.tsx
@@ -0,0 +1,21 @@
+import Head from 'next/head';
+
+interface ViewerHeadProps {
+ articleTitle: string;
+ articleContent: string;
+}
+
+export default function ViewerHead({ articleTitle, articleContent }: ViewerHeadProps) {
+ return (
+
+ {articleTitle}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/hooks/useFetch.ts b/frontend/hooks/useFetch.ts
index 8e3ce5cd..4711d1aa 100644
--- a/frontend/hooks/useFetch.ts
+++ b/frontend/hooks/useFetch.ts
@@ -5,10 +5,12 @@ import { toastError } from '@utils/toast';
const useFetch = (api: (...args: any[]) => Promise) => {
const [data, setData] = useState();
+ const [isLoading, setIsLoading] = useState(true);
const execute = useCallback(async (...args: any[]) => {
try {
setData(await api(...args));
+ setIsLoading(false);
} catch (error: any) {
const { message } = error.response.data;
@@ -16,7 +18,7 @@ const useFetch = (api: (...args: any[]) => Promise) => {
}
}, []);
- return { data, execute };
+ return { data, isLoading, execute };
};
export default useFetch;
diff --git a/frontend/hooks/useInput.ts b/frontend/hooks/useInput.ts
index 87da5f94..2d639483 100644
--- a/frontend/hooks/useInput.ts
+++ b/frontend/hooks/useInput.ts
@@ -7,7 +7,7 @@ const useInput = (initialValue = '') => {
setValue(event.target.value);
};
- return { value, onChange };
+ return { value, setValue, onChange };
};
export default useInput;
diff --git a/frontend/interfaces/index.ts b/frontend/interfaces/index.ts
index 994b98e9..2cd751bd 100644
--- a/frontend/interfaces/index.ts
+++ b/frontend/interfaces/index.ts
@@ -5,4 +5,4 @@ import { IArticleBook, IBookScraps } from './combined.interface';
import { IScrap } from './scrap.interface';
import { IUser } from './user.interface';
-export type { IArticle, IBook, IBookmark, IScrap, IUser, IBookScraps, IArticleBook };
+export type { IArticle, IBook, IBookmark, IScrap, IUser, IArticleBook, IBookScraps };
diff --git a/frontend/interfaces/scrap.interface.ts b/frontend/interfaces/scrap.interface.ts
index 2975de36..27a3e0c5 100644
--- a/frontend/interfaces/scrap.interface.ts
+++ b/frontend/interfaces/scrap.interface.ts
@@ -1,13 +1,7 @@
-import { IArticle } from './article.interface';
-
export interface IScrap {
id: number;
order: number;
- article: IArticle;
-}
-export interface IEditScrap {
- id: number;
- order: number;
+ is_original: boolean;
article: {
id: number;
title: string;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8eb40a38..58dad363 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@codemirror/lang-markdown": "^6.0.5",
"@codemirror/language": "^6.3.1",
+ "@codemirror/language-data": "^6.1.0",
"@codemirror/state": "^6.1.4",
"@codemirror/view": "^6.6.0",
"@lezer/highlight": "^1.1.2",
@@ -24,15 +25,20 @@
"eslint-config-next": "13.0.3",
"immutability-helper": "^3.1.1",
"next": "13.0.3",
+ "next-sitemap": "^3.1.32",
"react": "18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
+ "react-dnd-touch-backend": "^16.0.1",
"react-dom": "18.2.0",
"react-toastify": "^9.1.1",
"recoil": "^0.7.6",
+ "rehype-parse": "^8.0.4",
+ "rehype-remark": "^9.1.2",
"rehype-stringify": "^9.0.3",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
+ "remark-stringify": "^10.0.2",
"styled-components": "^5.3.6",
"styled-reset": "^4.4.2",
"typescript": "4.8.4",
@@ -352,6 +358,15 @@
"@lezer/common": "^1.0.0"
}
},
+ "node_modules/@codemirror/lang-cpp": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz",
+ "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/cpp": "^1.0.0"
+ }
+ },
"node_modules/@codemirror/lang-css": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.1.tgz",
@@ -378,6 +393,15 @@
"@lezer/html": "^1.1.0"
}
},
+ "node_modules/@codemirror/lang-java": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz",
+ "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/java": "^1.0.0"
+ }
+ },
"node_modules/@codemirror/lang-javascript": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.1.tgz",
@@ -392,6 +416,15 @@
"@lezer/javascript": "^1.0.0"
}
},
+ "node_modules/@codemirror/lang-json": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
+ "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/json": "^1.0.0"
+ }
+ },
"node_modules/@codemirror/lang-markdown": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.5.tgz",
@@ -405,6 +438,71 @@
"@lezer/markdown": "^1.0.0"
}
},
+ "node_modules/@codemirror/lang-php": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz",
+ "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==",
+ "dependencies": {
+ "@codemirror/lang-html": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/php": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-python": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.0.tgz",
+ "integrity": "sha512-a/JhyPYn5qz5T8WtAfZCuAZcfClgNVb7UZzdLr76bWUeG7Usd3Un5o8UQOkZ/5Xw+EM5YGHHG+T6ickMYkDcRQ==",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.3.2",
+ "@codemirror/language": "^6.0.0",
+ "@lezer/python": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-rust": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz",
+ "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/rust": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-sql": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.3.3.tgz",
+ "integrity": "sha512-VNsHju8500fkiDyDU8jZyGQ8M0iXU0SmfeCoCeAYkACcEFlX63BOT8311pICXyw43VYRbS23w54RgSEQmixGjQ==",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-wast": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz",
+ "integrity": "sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-xml": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.1.tgz",
+ "integrity": "sha512-0tvycUTElajCcRKgsszhKjWX+uuOogdu5+enpfqYA+j0gnP8ek7LRxujh2/XMPRdXt/hwOML4slJLE7r2eX3yQ==",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/xml": "^1.0.0"
+ }
+ },
"node_modules/@codemirror/language": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.1.tgz",
@@ -418,6 +516,36 @@
"style-mod": "^4.0.0"
}
},
+ "node_modules/@codemirror/language-data": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.1.0.tgz",
+ "integrity": "sha512-g9V23fuLRI9AEbpM6bDy1oquqgpFlIDHTihUhL21NPmxp+x67ZJbsKk+V71W7/Bj8SCqEO1PtqQA/tDGgt1nfw==",
+ "dependencies": {
+ "@codemirror/lang-cpp": "^6.0.0",
+ "@codemirror/lang-css": "^6.0.0",
+ "@codemirror/lang-html": "^6.0.0",
+ "@codemirror/lang-java": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
+ "@codemirror/lang-json": "^6.0.0",
+ "@codemirror/lang-markdown": "^6.0.0",
+ "@codemirror/lang-php": "^6.0.0",
+ "@codemirror/lang-python": "^6.0.0",
+ "@codemirror/lang-rust": "^6.0.0",
+ "@codemirror/lang-sql": "^6.0.0",
+ "@codemirror/lang-wast": "^6.0.0",
+ "@codemirror/lang-xml": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/legacy-modes": "^6.1.0"
+ }
+ },
+ "node_modules/@codemirror/legacy-modes": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.1.tgz",
+ "integrity": "sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0"
+ }
+ },
"node_modules/@codemirror/lint": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz",
@@ -453,6 +581,11 @@
"w3c-keyname": "^2.2.4"
}
},
+ "node_modules/@corex/deepmerge": {
+ "version": "4.0.29",
+ "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.29.tgz",
+ "integrity": "sha512-q/yVUnqckA8Do+EvAfpy7RLdumnBy9ZsducMUtZTvpdbJC7azEf1hGtnYYxm0QfphYxjwggv6XtH64prvS1W+A=="
+ },
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
@@ -586,6 +719,15 @@
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz",
"integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw=="
},
+ "node_modules/@lezer/cpp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz",
+ "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==",
+ "dependencies": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"node_modules/@lezer/css": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.1.tgz",
@@ -613,6 +755,15 @@
"@lezer/lr": "^1.0.0"
}
},
+ "node_modules/@lezer/java": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz",
+ "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==",
+ "dependencies": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"node_modules/@lezer/javascript": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.1.1.tgz",
@@ -622,6 +773,15 @@
"@lezer/lr": "^1.0.0"
}
},
+ "node_modules/@lezer/json": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz",
+ "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==",
+ "dependencies": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"node_modules/@lezer/lr": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
@@ -639,6 +799,42 @@
"@lezer/highlight": "^1.0.0"
}
},
+ "node_modules/@lezer/php": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz",
+ "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==",
+ "dependencies": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/python": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.1.tgz",
+ "integrity": "sha512-ArUGh9kvdaOVu6IkSaYUS9WFQeMAFVWKRuZo6vexnxoeCLnxf0Y9DCFEAMMa7W9SQBGYE55OarSpPqSkdOXSCA==",
+ "dependencies": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/rust": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz",
+ "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==",
+ "dependencies": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/xml": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz",
+ "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==",
+ "dependencies": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"node_modules/@next/env": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.3.tgz",
@@ -969,6 +1165,11 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz",
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="
},
+ "node_modules/@types/extend": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.1.tgz",
+ "integrity": "sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw=="
+ },
"node_modules/@types/hast": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
@@ -1015,6 +1216,11 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
},
+ "node_modules/@types/parse5": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
+ "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g=="
+ },
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@@ -3147,6 +3353,60 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hast-util-embedded": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-2.0.0.tgz",
+ "integrity": "sha512-vEr54rDu2CheBM4nLkWbW8Rycf8HhkA/KsrDnlyKnvBTyhyO+vAG6twHnfUbiRGo56YeUBNCI4HFfHg3Wu+tig==",
+ "dependencies": {
+ "hast-util-is-element": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-parse5": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz",
+ "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "@types/parse5": "^6.0.0",
+ "@types/unist": "^2.0.0",
+ "hastscript": "^7.0.0",
+ "property-information": "^6.0.0",
+ "vfile": "^5.0.0",
+ "vfile-location": "^4.0.0",
+ "web-namespaces": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-has-property": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz",
+ "integrity": "sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w==",
+ "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",
+ "integrity": "sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-is-element": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-is-element": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz",
@@ -3160,6 +3420,33 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hast-util-parse-selector": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz",
+ "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==",
+ "dependencies": {
+ "@types/hast": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-phrasing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-2.0.1.tgz",
+ "integrity": "sha512-Lw+gVihgE0Ye1TsToZqui0puQnHbZ0dFQe0c/Z2QJWGYRIc72DpH3UHLV8zU48sjIPord88MTSeYEbLQMp5A9g==",
+ "dependencies": {
+ "hast-util-embedded": "^2.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-is-body-ok-link": "^2.0.0",
+ "hast-util-is-element": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-to-html": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz",
@@ -3181,6 +3468,46 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hast-util-to-mdast": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-8.4.1.tgz",
+ "integrity": "sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==",
+ "dependencies": {
+ "@types/extend": "^3.0.0",
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "extend": "^3.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-is-element": "^2.0.0",
+ "hast-util-phrasing": "^2.0.0",
+ "hast-util-to-text": "^3.0.0",
+ "mdast-util-phrasing": "^3.0.0",
+ "mdast-util-to-string": "^3.0.0",
+ "rehype-minify-whitespace": "^5.0.0",
+ "trim-trailing-lines": "^2.0.0",
+ "unist-util-is": "^5.0.0",
+ "unist-util-visit": "^4.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",
+ "integrity": "sha512-7S3mOBxACy8syL45hCn3J7rHqYaXkxRfsX6LXEU5Shz4nt4GxdjtMUtG+T6G/ZLUHd7kslFAf14kAN71bz30xA==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "hast-util-is-element": "^2.0.0",
+ "unist-util-find-after": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-whitespace": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
@@ -3190,6 +3517,22 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hastscript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.1.0.tgz",
+ "integrity": "sha512-uBjaTTLN0MkCZxY/R2fWUOcu7FRtUVzKRO5P/RAfgsu3yFiMB1JWCO4AjeVkgHxAira1f2UecHK5WfS9QurlWA==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^3.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -3688,6 +4031,15 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -3747,6 +4099,19 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdast-util-phrasing": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.0.tgz",
+ "integrity": "sha512-S+QYsDRLkGi8U7o5JF1agKa/sdP+CNGXXLqC17pdTVL8FHHgQEiwFGa9yE5aYtUxNiFGYoaDy9V1kC85Sz86Gg==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "unist-util-is": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-to-hast": {
"version": "12.2.4",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.2.4.tgz",
@@ -3767,6 +4132,24 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdast-util-to-markdown": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz",
+ "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-to-string": "^3.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "unist-util-visit": "^4.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-to-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz",
@@ -4355,6 +4738,31 @@
}
}
},
+ "node_modules/next-sitemap": {
+ "version": "3.1.32",
+ "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.32.tgz",
+ "integrity": "sha512-jkIKpwLXpWWTPfmDO46+6nu4+qpar4CjvUwCR9rYZHWtzE/wFfaCVFKpGtFMl6MFjpu8GjiE6kWFEa7uF3bzzg==",
+ "funding": [
+ {
+ "url": "https://github.com/iamvishnusankar/next-sitemap.git"
+ }
+ ],
+ "dependencies": {
+ "@corex/deepmerge": "^4.0.29",
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "next-sitemap": "bin/next-sitemap.mjs",
+ "next-sitemap-cjs": "bin/next-sitemap.cjs"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "peerDependencies": {
+ "@next/env": "*",
+ "next": "*"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
@@ -4539,6 +4947,11 @@
"node": ">=6"
}
},
+ "node_modules/parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4763,6 +5176,15 @@
"dnd-core": "^16.0.1"
}
},
+ "node_modules/react-dnd-touch-backend": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz",
+ "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==",
+ "dependencies": {
+ "@react-dnd/invariant": "^4.0.1",
+ "dnd-core": "^16.0.1"
+ }
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -4851,21 +5273,68 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
- "node_modules/rehype-stringify": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz",
- "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==",
+ "node_modules/rehype-minify-whitespace": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz",
+ "integrity": "sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==",
"dependencies": {
"@types/hast": "^2.0.0",
- "hast-util-to-html": "^8.0.0",
- "unified": "^10.0.0"
+ "hast-util-embedded": "^2.0.0",
+ "hast-util-is-element": "^2.0.0",
+ "hast-util-whitespace": "^2.0.0",
+ "unified": "^10.0.0",
+ "unist-util-is": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/remark-parse": {
+ "node_modules/rehype-parse": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz",
+ "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "hast-util-from-parse5": "^7.0.0",
+ "parse5": "^6.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-remark": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-9.1.2.tgz",
+ "integrity": "sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "hast-util-to-mdast": "^8.3.0",
+ "unified": "^10.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",
+ "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "hast-util-to-html": "^8.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz",
"integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==",
@@ -4894,6 +5363,20 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remark-stringify": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.2.tgz",
+ "integrity": "sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "mdast-util-to-markdown": "^1.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -5456,6 +5939,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/trim-trailing-lines": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz",
+ "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/trough": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz",
@@ -5578,6 +6070,19 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/unist-util-find-after": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.0.tgz",
+ "integrity": "sha512-gfpsxKQde7atVF30n5Gff2fQhAc4/HTOV4CvkXpTg9wRfQhZWdXitpyXHWB6YcYgnsxLx+4gGHeVjCTAAp9sjw==",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/unist-util-generated": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz",
@@ -5721,6 +6226,19 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/vfile-location": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz",
+ "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "vfile": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/vfile-message": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.3.tgz",
@@ -5752,6 +6270,15 @@
"node": ">=10.13.0"
}
},
+ "node_modules/web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/webpack": {
"version": "5.75.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
@@ -5887,6 +6414,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
},
"dependencies": {
@@ -6113,6 +6649,15 @@
"@lezer/common": "^1.0.0"
}
},
+ "@codemirror/lang-cpp": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz",
+ "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/cpp": "^1.0.0"
+ }
+ },
"@codemirror/lang-css": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.1.tgz",
@@ -6139,6 +6684,15 @@
"@lezer/html": "^1.1.0"
}
},
+ "@codemirror/lang-java": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz",
+ "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/java": "^1.0.0"
+ }
+ },
"@codemirror/lang-javascript": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.1.tgz",
@@ -6153,6 +6707,15 @@
"@lezer/javascript": "^1.0.0"
}
},
+ "@codemirror/lang-json": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
+ "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/json": "^1.0.0"
+ }
+ },
"@codemirror/lang-markdown": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.5.tgz",
@@ -6166,6 +6729,71 @@
"@lezer/markdown": "^1.0.0"
}
},
+ "@codemirror/lang-php": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz",
+ "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==",
+ "requires": {
+ "@codemirror/lang-html": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/php": "^1.0.0"
+ }
+ },
+ "@codemirror/lang-python": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.0.tgz",
+ "integrity": "sha512-a/JhyPYn5qz5T8WtAfZCuAZcfClgNVb7UZzdLr76bWUeG7Usd3Un5o8UQOkZ/5Xw+EM5YGHHG+T6ickMYkDcRQ==",
+ "requires": {
+ "@codemirror/autocomplete": "^6.3.2",
+ "@codemirror/language": "^6.0.0",
+ "@lezer/python": "^1.0.0"
+ }
+ },
+ "@codemirror/lang-rust": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz",
+ "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/rust": "^1.0.0"
+ }
+ },
+ "@codemirror/lang-sql": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.3.3.tgz",
+ "integrity": "sha512-VNsHju8500fkiDyDU8jZyGQ8M0iXU0SmfeCoCeAYkACcEFlX63BOT8311pICXyw43VYRbS23w54RgSEQmixGjQ==",
+ "requires": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "@codemirror/lang-wast": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz",
+ "integrity": "sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "@codemirror/lang-xml": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.1.tgz",
+ "integrity": "sha512-0tvycUTElajCcRKgsszhKjWX+uuOogdu5+enpfqYA+j0gnP8ek7LRxujh2/XMPRdXt/hwOML4slJLE7r2eX3yQ==",
+ "requires": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/xml": "^1.0.0"
+ }
+ },
"@codemirror/language": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.1.tgz",
@@ -6179,6 +6807,36 @@
"style-mod": "^4.0.0"
}
},
+ "@codemirror/language-data": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.1.0.tgz",
+ "integrity": "sha512-g9V23fuLRI9AEbpM6bDy1oquqgpFlIDHTihUhL21NPmxp+x67ZJbsKk+V71W7/Bj8SCqEO1PtqQA/tDGgt1nfw==",
+ "requires": {
+ "@codemirror/lang-cpp": "^6.0.0",
+ "@codemirror/lang-css": "^6.0.0",
+ "@codemirror/lang-html": "^6.0.0",
+ "@codemirror/lang-java": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
+ "@codemirror/lang-json": "^6.0.0",
+ "@codemirror/lang-markdown": "^6.0.0",
+ "@codemirror/lang-php": "^6.0.0",
+ "@codemirror/lang-python": "^6.0.0",
+ "@codemirror/lang-rust": "^6.0.0",
+ "@codemirror/lang-sql": "^6.0.0",
+ "@codemirror/lang-wast": "^6.0.0",
+ "@codemirror/lang-xml": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/legacy-modes": "^6.1.0"
+ }
+ },
+ "@codemirror/legacy-modes": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.1.tgz",
+ "integrity": "sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw==",
+ "requires": {
+ "@codemirror/language": "^6.0.0"
+ }
+ },
"@codemirror/lint": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz",
@@ -6214,6 +6872,11 @@
"w3c-keyname": "^2.2.4"
}
},
+ "@corex/deepmerge": {
+ "version": "4.0.29",
+ "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.29.tgz",
+ "integrity": "sha512-q/yVUnqckA8Do+EvAfpy7RLdumnBy9ZsducMUtZTvpdbJC7azEf1hGtnYYxm0QfphYxjwggv6XtH64prvS1W+A=="
+ },
"@emotion/is-prop-valid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
@@ -6322,6 +6985,15 @@
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz",
"integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw=="
},
+ "@lezer/cpp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz",
+ "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"@lezer/css": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.1.tgz",
@@ -6349,6 +7021,15 @@
"@lezer/lr": "^1.0.0"
}
},
+ "@lezer/java": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz",
+ "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"@lezer/javascript": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.1.1.tgz",
@@ -6358,6 +7039,15 @@
"@lezer/lr": "^1.0.0"
}
},
+ "@lezer/json": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz",
+ "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"@lezer/lr": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
@@ -6375,6 +7065,42 @@
"@lezer/highlight": "^1.0.0"
}
},
+ "@lezer/php": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz",
+ "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "@lezer/python": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.1.tgz",
+ "integrity": "sha512-ArUGh9kvdaOVu6IkSaYUS9WFQeMAFVWKRuZo6vexnxoeCLnxf0Y9DCFEAMMa7W9SQBGYE55OarSpPqSkdOXSCA==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "@lezer/rust": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz",
+ "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "@lezer/xml": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz",
+ "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"@next/env": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.3.tgz",
@@ -6573,6 +7299,11 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz",
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="
},
+ "@types/extend": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.1.tgz",
+ "integrity": "sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw=="
+ },
"@types/hast": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
@@ -6619,6 +7350,11 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
},
+ "@types/parse5": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
+ "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g=="
+ },
"@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@@ -8180,6 +8916,44 @@
"has-symbols": "^1.0.2"
}
},
+ "hast-util-embedded": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-2.0.0.tgz",
+ "integrity": "sha512-vEr54rDu2CheBM4nLkWbW8Rycf8HhkA/KsrDnlyKnvBTyhyO+vAG6twHnfUbiRGo56YeUBNCI4HFfHg3Wu+tig==",
+ "requires": {
+ "hast-util-is-element": "^2.0.0"
+ }
+ },
+ "hast-util-from-parse5": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz",
+ "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "@types/parse5": "^6.0.0",
+ "@types/unist": "^2.0.0",
+ "hastscript": "^7.0.0",
+ "property-information": "^6.0.0",
+ "vfile": "^5.0.0",
+ "vfile-location": "^4.0.0",
+ "web-namespaces": "^2.0.0"
+ }
+ },
+ "hast-util-has-property": {
+ "version": "2.0.0",
+ "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-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",
+ "integrity": "sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-is-element": "^2.0.0"
+ }
+ },
"hast-util-is-element": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz",
@@ -8189,6 +8963,25 @@
"@types/unist": "^2.0.0"
}
},
+ "hast-util-parse-selector": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz",
+ "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==",
+ "requires": {
+ "@types/hast": "^2.0.0"
+ }
+ },
+ "hast-util-phrasing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-2.0.1.tgz",
+ "integrity": "sha512-Lw+gVihgE0Ye1TsToZqui0puQnHbZ0dFQe0c/Z2QJWGYRIc72DpH3UHLV8zU48sjIPord88MTSeYEbLQMp5A9g==",
+ "requires": {
+ "hast-util-embedded": "^2.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-is-body-ok-link": "^2.0.0",
+ "hast-util-is-element": "^2.0.0"
+ }
+ },
"hast-util-to-html": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz",
@@ -8206,11 +8999,55 @@
"unist-util-is": "^5.0.0"
}
},
+ "hast-util-to-mdast": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-8.4.1.tgz",
+ "integrity": "sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==",
+ "requires": {
+ "@types/extend": "^3.0.0",
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "extend": "^3.0.0",
+ "hast-util-has-property": "^2.0.0",
+ "hast-util-is-element": "^2.0.0",
+ "hast-util-phrasing": "^2.0.0",
+ "hast-util-to-text": "^3.0.0",
+ "mdast-util-phrasing": "^3.0.0",
+ "mdast-util-to-string": "^3.0.0",
+ "rehype-minify-whitespace": "^5.0.0",
+ "trim-trailing-lines": "^2.0.0",
+ "unist-util-is": "^5.0.0",
+ "unist-util-visit": "^4.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",
+ "integrity": "sha512-7S3mOBxACy8syL45hCn3J7rHqYaXkxRfsX6LXEU5Shz4nt4GxdjtMUtG+T6G/ZLUHd7kslFAf14kAN71bz30xA==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "hast-util-is-element": "^2.0.0",
+ "unist-util-find-after": "^4.0.0"
+ }
+ },
"hast-util-whitespace": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
"integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg=="
},
+ "hastscript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.1.0.tgz",
+ "integrity": "sha512-uBjaTTLN0MkCZxY/R2fWUOcu7FRtUVzKRO5P/RAfgsu3yFiMB1JWCO4AjeVkgHxAira1f2UecHK5WfS9QurlWA==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^3.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0"
+ }
+ },
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -8546,6 +9383,11 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
+ "longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="
+ },
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -8591,6 +9433,15 @@
"uvu": "^0.5.0"
}
},
+ "mdast-util-phrasing": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.0.tgz",
+ "integrity": "sha512-S+QYsDRLkGi8U7o5JF1agKa/sdP+CNGXXLqC17pdTVL8FHHgQEiwFGa9yE5aYtUxNiFGYoaDy9V1kC85Sz86Gg==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "unist-util-is": "^5.0.0"
+ }
+ },
"mdast-util-to-hast": {
"version": "12.2.4",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.2.4.tgz",
@@ -8607,6 +9458,20 @@
"unist-util-visit": "^4.0.0"
}
},
+ "mdast-util-to-markdown": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz",
+ "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-to-string": "^3.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "unist-util-visit": "^4.0.0",
+ "zwitch": "^2.0.0"
+ }
+ },
"mdast-util-to-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz",
@@ -8928,6 +9793,15 @@
"use-sync-external-store": "1.2.0"
}
},
+ "next-sitemap": {
+ "version": "3.1.32",
+ "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.32.tgz",
+ "integrity": "sha512-jkIKpwLXpWWTPfmDO46+6nu4+qpar4CjvUwCR9rYZHWtzE/wFfaCVFKpGtFMl6MFjpu8GjiE6kWFEa7uF3bzzg==",
+ "requires": {
+ "@corex/deepmerge": "^4.0.29",
+ "minimist": "^1.2.6"
+ }
+ },
"node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
@@ -9055,6 +9929,11 @@
"callsites": "^3.0.0"
}
},
+ "parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
+ },
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -9192,6 +10071,15 @@
"dnd-core": "^16.0.1"
}
},
+ "react-dnd-touch-backend": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz",
+ "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==",
+ "requires": {
+ "@react-dnd/invariant": "^4.0.1",
+ "dnd-core": "^16.0.1"
+ }
+ },
"react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -9250,6 +10138,41 @@
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="
},
+ "rehype-minify-whitespace": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz",
+ "integrity": "sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "hast-util-embedded": "^2.0.0",
+ "hast-util-is-element": "^2.0.0",
+ "hast-util-whitespace": "^2.0.0",
+ "unified": "^10.0.0",
+ "unist-util-is": "^5.0.0"
+ }
+ },
+ "rehype-parse": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz",
+ "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "hast-util-from-parse5": "^7.0.0",
+ "parse5": "^6.0.0",
+ "unified": "^10.0.0"
+ }
+ },
+ "rehype-remark": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-9.1.2.tgz",
+ "integrity": "sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "hast-util-to-mdast": "^8.3.0",
+ "unified": "^10.0.0"
+ }
+ },
"rehype-stringify": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz",
@@ -9281,6 +10204,16 @@
"unified": "^10.0.0"
}
},
+ "remark-stringify": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.2.tgz",
+ "integrity": "sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "mdast-util-to-markdown": "^1.0.0",
+ "unified": "^10.0.0"
+ }
+ },
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -9635,6 +10568,11 @@
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="
},
+ "trim-trailing-lines": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz",
+ "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg=="
+ },
"trough": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz",
@@ -9722,6 +10660,15 @@
"@types/unist": "^2.0.0"
}
},
+ "unist-util-find-after": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.0.tgz",
+ "integrity": "sha512-gfpsxKQde7atVF30n5Gff2fQhAc4/HTOV4CvkXpTg9wRfQhZWdXitpyXHWB6YcYgnsxLx+4gGHeVjCTAAp9sjw==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0"
+ }
+ },
"unist-util-generated": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz",
@@ -9813,6 +10760,15 @@
"vfile-message": "^3.0.0"
}
},
+ "vfile-location": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz",
+ "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "vfile": "^5.0.0"
+ }
+ },
"vfile-message": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.3.tgz",
@@ -9837,6 +10793,11 @@
"graceful-fs": "^4.1.2"
}
},
+ "web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="
+ },
"webpack": {
"version": "5.75.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
@@ -9932,6 +10893,11 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+ },
+ "zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 3e1addc6..e3450499 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"@codemirror/lang-markdown": "^6.0.5",
"@codemirror/language": "^6.3.1",
+ "@codemirror/language-data": "^6.1.0",
"@codemirror/state": "^6.1.4",
"@codemirror/view": "^6.6.0",
"@lezer/highlight": "^1.1.2",
@@ -25,15 +26,20 @@
"eslint-config-next": "13.0.3",
"immutability-helper": "^3.1.1",
"next": "13.0.3",
+ "next-sitemap": "^3.1.32",
"react": "18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
+ "react-dnd-touch-backend": "^16.0.1",
"react-dom": "18.2.0",
"react-toastify": "^9.1.1",
"recoil": "^0.7.6",
+ "rehype-parse": "^8.0.4",
+ "rehype-remark": "^9.1.2",
"rehype-stringify": "^9.0.3",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
+ "remark-stringify": "^10.0.2",
"styled-components": "^5.3.6",
"styled-reset": "^4.4.2",
"typescript": "4.8.4",
diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx
index 06bb6780..2abfb6fc 100644
--- a/frontend/pages/_app.tsx
+++ b/frontend/pages/_app.tsx
@@ -4,18 +4,24 @@ import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
import { RecoilRoot } from 'recoil';
+import { ThemeProvider } from 'styled-components';
-import CheckSignInByToken from '@components/CheckSignInByToken';
+import CheckSignInStatus from '@components/auth/CheckSignInStatus';
import GlobalStyle from '@styles/GlobalStyle';
+import responsive from '@styles/responsive';
+
+import '@styles/font.css';
export default function App({ Component, pageProps }: AppProps) {
return (
-
+
-
-
-
+
+
+
+
+
);
}
diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx
index 78973364..1c9f6eef 100644
--- a/frontend/pages/_document.tsx
+++ b/frontend/pages/_document.tsx
@@ -43,7 +43,7 @@ export default class MyDocument extends Document {
diff --git a/frontend/pages/booktest.tsx b/frontend/pages/booktest.tsx
new file mode 100644
index 00000000..1f7a26b6
--- /dev/null
+++ b/frontend/pages/booktest.tsx
@@ -0,0 +1,26 @@
+import { useEffect } from 'react';
+
+import { getOrderedBookListApi } from '@apis/bookApi';
+import Book from '@components/common/Book';
+import useFetch from '@hooks/useFetch';
+
+export default function Booktest() {
+ const {
+ data: popularBookList,
+ isLoading: isPopularBookListLoading,
+ execute: getPopularBookList,
+ } = useFetch(getOrderedBookListApi);
+
+ useEffect(() => {
+ getPopularBookList('bookmark');
+ }, []);
+ return (
+
+ {popularBookList && (
+ <>
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/pages/editor.tsx b/frontend/pages/editor.tsx
index de8f66ce..b82246e8 100644
--- a/frontend/pages/editor.tsx
+++ b/frontend/pages/editor.tsx
@@ -1,36 +1,69 @@
+import { useRouter } from 'next/router';
+
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
+import { getArticleApi } from '@apis/articleApi';
import { getUserKnottedBooksApi } from '@apis/bookApi';
import signInStatusState from '@atoms/signInStatus';
import Modal from '@components/common/Modal';
+import EditHead from '@components/edit/EditHead';
import Editor from '@components/edit/Editor';
+import ModifyModal from '@components/edit/ModifyModal';
import PublishModal from '@components/edit/PublishModal';
import useFetch from '@hooks/useFetch';
+import { IArticle } from '@interfaces';
+import { toastError } from '@utils/toast';
export default function EditorPage() {
const [isModalShown, setModalShown] = useState(false);
+ const [originalArticle, setOriginalArticle] = useState(undefined);
const handleModalOpen = () => setModalShown(true);
const handleModalClose = () => setModalShown(false);
const { data: books, execute: getUserKnottedBooks } = useFetch(getUserKnottedBooksApi);
+ const { data: article, execute: getArticle } = useFetch(getArticleApi);
const user = useRecoilValue(signInStatusState);
+ const router = useRouter();
+
useEffect(() => {
getUserKnottedBooks(user.nickname);
- }, []);
+ }, [user.nickname]);
+
+ useEffect(() => {
+ if (router.query.id) getArticle(router.query.id);
+ }, [router.query]);
+
+ useEffect(() => {
+ if (!article) return;
+
+ if (article.book.user.nickname !== user.nickname) {
+ toastError('수정 권한이 없습니다.');
+ router.push('/');
+ return;
+ }
+ setOriginalArticle(article);
+ }, [article]);
return (
<>
-
- {isModalShown && (
-
-
-
- )}
+
+
+
+ {isModalShown &&
+ ((originalArticle && (
+
+
+
+ )) || (
+
+
+
+ ))}
>
);
}
diff --git a/frontend/pages/github.tsx b/frontend/pages/github.tsx
index 23e22725..48d6ad75 100644
--- a/frontend/pages/github.tsx
+++ b/frontend/pages/github.tsx
@@ -6,29 +6,43 @@ import { useSetRecoilState } from 'recoil';
import { githubSignInApi } from '@apis/authApi';
import signInStatusState from '@atoms/signInStatus';
+import Spinner from '@components/common/Spinner';
import useFetch from '@hooks/useFetch';
+import { FlexColumnCenter, FullPageWrapper } from '@styles/layout';
+import { toastError } from '@utils/toast';
export default function Github() {
const router = useRouter();
- const { data: user, execute: githubSignIn } = useFetch(githubSignInApi);
+
const setSignInStatus = useSetRecoilState(signInStatusState);
+ const { data: user, execute: githubSignIn } = useFetch(githubSignInApi);
useEffect(() => {
- if (router.query.code) {
- githubSignIn({
- code: router.query.code,
- });
+ if (router.query.error) {
+ toastError('GitHub 로그인에 실패했습니다.');
+
+ router.push('/');
}
+ }, [router.query.error]);
+
+ useEffect(() => {
+ if (router.query.code) githubSignIn({ code: router.query.code });
}, [router.query.code]);
useEffect(() => {
if (!user) return;
- setSignInStatus({
- ...user,
- });
+ setSignInStatus({ ...user });
+
router.push('/');
}, [user]);
- return Github 로그인 시동 중입니다.....
;
+ return (
+
+
+
+ GitHub 로그인 중 입니다.
+
+
+ );
}
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx
index 17ea70cb..efc4d888 100644
--- a/frontend/pages/index.tsx
+++ b/frontend/pages/index.tsx
@@ -1,15 +1,53 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { getOrderedBookListApi } from '@apis/bookApi';
import Footer from '@components/common/Footer';
import GNB from '@components/common/GNB';
+import HomeHead from '@components/home/HomeHead';
import Slider from '@components/home/Slider';
import useFetch from '@hooks/useFetch';
import { PageInnerLarge, PageWrapper } from '@styles/layout';
export default function Home() {
- const { data: newestBookList, execute: getNewestBookList } = useFetch(getOrderedBookListApi);
- const { data: popularBookList, execute: getPopularBookList } = useFetch(getOrderedBookListApi);
+ const {
+ data: newestBookList,
+ isLoading: isNewBookListLoading,
+ execute: getNewestBookList,
+ } = useFetch(getOrderedBookListApi);
+
+ const [numberPerPage, setNumberPerPage] = useState(0);
+
+ const resizingHandler = () => {
+ switch (true) {
+ case window.innerWidth > 1300:
+ setNumberPerPage(4);
+ break;
+ case window.innerWidth > 1000:
+ setNumberPerPage(3);
+ break;
+ case window.innerWidth > 700:
+ setNumberPerPage(2);
+ break;
+ default:
+ setNumberPerPage(1);
+ break;
+ }
+ };
+
+ useEffect(() => {
+ resizingHandler();
+ window.addEventListener('resize', resizingHandler);
+
+ return () => {
+ window.removeEventListener('resize', resizingHandler);
+ };
+ }, []);
+
+ const {
+ data: popularBookList,
+ isLoading: isPopularBookListLoading,
+ execute: getPopularBookList,
+ } = useFetch(getOrderedBookListApi);
useEffect(() => {
getNewestBookList('newest');
@@ -18,12 +56,27 @@ export default function Home() {
return (
<>
+
- {newestBookList && }
- {popularBookList && }
-
+ {numberPerPage !== 0 && (
+ <>
+
+
+
+ >
+ )}
>
diff --git a/frontend/pages/search.tsx b/frontend/pages/search.tsx
index 7acc5e71..6ed1eb17 100644
--- a/frontend/pages/search.tsx
+++ b/frontend/pages/search.tsx
@@ -7,6 +7,7 @@ import ArticleList from '@components/search/ArticleList';
import BookList from '@components/search/BookList';
import SearchBar from '@components/search/SearchBar';
import SearchFilter from '@components/search/SearchFilter';
+import SearchHead from '@components/search/SearchHead';
import SearchNoResult from '@components/search/SearchNoResult';
import useDebounce from '@hooks/useDebounce';
import useFetch from '@hooks/useFetch';
@@ -33,6 +34,9 @@ export default function Search() {
const [filter, setFilter] = useState({ type: 'article', userId: 0 });
+ const [isArticleNoResult, setIsArticleNoResult] = useState(false);
+ const [isBookNoResult, setIsBookNoResult] = useState(false);
+
const highlightWord = (text: string, words: string[]): React.ReactNode => {
let wordIndexList = words.map((word) => text.toLowerCase().indexOf(word.toLowerCase()));
@@ -57,9 +61,14 @@ export default function Search() {
};
useEffect(() => {
- if (!debouncedKeyword) return;
+ if (debouncedKeyword === '') {
+ setArticles([]);
+ setBooks([]);
+ setIsArticleNoResult(false);
+ setIsBookNoResult(false);
+ return;
+ }
- setArticles([]);
searchArticles({
query: debouncedKeyword,
userId: filter.userId,
@@ -70,7 +79,7 @@ export default function Search() {
hasNextPage: true,
pageNumber: 2,
});
- setBooks([]);
+
searchBooks({
query: debouncedKeyword,
userId: filter.userId,
@@ -86,7 +95,7 @@ export default function Search() {
useEffect(() => {
if (!isIntersecting || !debouncedKeyword) return;
- if (filter.type === 'article') {
+ if (filter.type === 'article' && !isArticleNoResult) {
if (!articlePage.hasNextPage) return;
searchArticles({
query: debouncedKeyword,
@@ -98,7 +107,7 @@ export default function Search() {
...articlePage,
pageNumber: articlePage.pageNumber + 1,
});
- } else if (filter.type === 'book') {
+ } else if (filter.type === 'book' && !isBookNoResult) {
if (!bookPage.hasNextPage) return;
searchBooks({
query: debouncedKeyword,
@@ -115,19 +124,28 @@ export default function Search() {
useEffect(() => {
if (!newArticles) return;
- setArticles(
- articles.concat(
- newArticles.data.map((article: IArticle) => {
- const keywords = debouncedKeyword.trim().split(' ');
-
- return {
- ...article,
- title: highlightWord(article.title, keywords),
- content: highlightWord(article.content, keywords),
- };
- })
- )
- );
+
+ if (newArticles.data.length === 0 && articlePage.pageNumber === 2) {
+ setArticles([]);
+ setIsArticleNoResult(true);
+ return;
+ }
+
+ setIsArticleNoResult(false);
+
+ const newArticlesHighlighted = newArticles.data.map((article: IArticle) => {
+ const keywords = debouncedKeyword.trim().split(' ');
+
+ return {
+ ...article,
+ title: highlightWord(article.title, keywords),
+ content: highlightWord(article.content, keywords),
+ };
+ });
+
+ if (articlePage.pageNumber === 2) setArticles(newArticlesHighlighted);
+ else setArticles(articles.concat(newArticlesHighlighted));
+
setArticlePage({
...articlePage,
hasNextPage: newArticles.hasNextPage,
@@ -136,18 +154,27 @@ export default function Search() {
useEffect(() => {
if (!newBooks) return;
- setBooks(
- books.concat(
- newBooks.data.map((book: IBook) => {
- const keywords = debouncedKeyword.trim().split(' ');
-
- return {
- ...book,
- title: highlightWord(book.title, keywords),
- };
- })
- )
- );
+
+ if (newBooks.data.length === 0 && bookPage.pageNumber === 2) {
+ setBooks([]);
+ setIsBookNoResult(true);
+ return;
+ }
+
+ setIsBookNoResult(false);
+
+ const newBooksHighlighted = newBooks.data.map((book: IBook) => {
+ const keywords = debouncedKeyword.trim().split(' ');
+
+ return {
+ ...book,
+ title: highlightWord(book.title, keywords),
+ };
+ });
+
+ if (bookPage.pageNumber === 2) setBooks(newBooksHighlighted);
+ setBooks(books.concat(newBooksHighlighted));
+
setBookPage({
...bookPage,
hasNextPage: newBooks.hasNextPage,
@@ -163,16 +190,18 @@ export default function Search() {
return (
<>
+
- {articles?.length > 0 && filter.type === 'article' && }
- {books?.length > 0 && filter.type === 'book' && }
{debouncedKeyword !== '' &&
- ((articles?.length === 0 && filter.type === 'article') ||
- (books?.length === 0 && filter.type === 'book')) && }
+ filter.type === 'article' &&
+ (isArticleNoResult ? : )}
+ {debouncedKeyword !== '' &&
+ filter.type === 'book' &&
+ (isBookNoResult ? : )}
diff --git a/frontend/pages/sitemap.xml.tsx b/frontend/pages/sitemap.xml.tsx
new file mode 100644
index 00000000..e844db6e
--- /dev/null
+++ b/frontend/pages/sitemap.xml.tsx
@@ -0,0 +1,35 @@
+import { GetServerSidePropsContext } from 'next';
+import { getServerSideSitemap } from 'next-sitemap';
+
+import { getScrapsApi } from '@apis/scrapApi';
+
+export default function SiteMapXML() {
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>>;
+}
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const scraps = await getScrapsApi();
+
+ const lastmod = new Date().toISOString();
+
+ const defaultFields = [
+ {
+ loc: `${process.env.NEXT_PUBLIC_NEXT_URL}`,
+ changefreq: 'daily',
+ priority: '1.0',
+ lastmod,
+ },
+ ];
+
+ const scrapFields = scraps.map((scrap: { book_id: number; article_id: number }) => ({
+ loc: `${process.env.NEXT_PUBLIC_NEXT_URL}/viewer/${scrap.book_id}/${scrap.article_id}`,
+ changefreq: 'daily',
+ priority: '1.0',
+ lastmod,
+ }));
+
+ const fields = [...defaultFields, ...scrapFields];
+
+ return getServerSideSitemap(context, fields);
+};
diff --git a/frontend/pages/study/[...data].tsx b/frontend/pages/study/[...data].tsx
index d68149eb..bd2aac79 100644
--- a/frontend/pages/study/[...data].tsx
+++ b/frontend/pages/study/[...data].tsx
@@ -1,3 +1,4 @@
+import { GetServerSideProps } from 'next';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -11,15 +12,24 @@ import signInStatusState from '@atoms/signInStatus';
import GNB from '@components/common/GNB';
import BookListTab from '@components/study/BookListTab';
import EditUserProfile from '@components/study/EditUserProfile';
+import StudyHead from '@components/study/StudyHead';
import UserProfile from '@components/study/UserProfile';
import useFetch from '@hooks/useFetch';
import { IUser } from '@interfaces';
import { PageInnerLarge, PageWrapper } from '@styles/layout';
-export default function Study() {
+interface StudyProps {
+ userProfile: {
+ id: number;
+ profile_image: string;
+ nickname: string;
+ description: string;
+ };
+}
+
+export default function Study({ userProfile }: StudyProps) {
const router = useRouter();
- const { data: userProfile, execute: getUserProfile } = useFetch(getUserProfileApi);
const { data: updatedUserProfile, execute: updateUserProfile } = useFetch(updateUserProfileApi);
const { data: knottedBookList, execute: getKnottedBookList } = useFetch(getUserKnottedBooksApi);
const { data: bookmarkedBookList, execute: getBookmarkedBookList } =
@@ -41,7 +51,6 @@ export default function Study() {
if (!router.query.data) return;
const nickname = router.query.data;
- getUserProfile(nickname);
getKnottedBookList(nickname);
getBookmarkedBookList(nickname);
}, [router.query.data]);
@@ -73,6 +82,11 @@ export default function Study() {
return (
<>
+
{curUserProfile && (
@@ -102,3 +116,10 @@ export default function Study() {
>
);
}
+
+export const getServerSideProps: GetServerSideProps = async (context) => {
+ const nickname = context.query.data as string;
+ const data = await getUserProfileApi(nickname);
+
+ return { props: { userProfile: data } };
+};
diff --git a/frontend/pages/viewer/[...data].tsx b/frontend/pages/viewer/[...data].tsx
index daf2a349..1d62a19e 100644
--- a/frontend/pages/viewer/[...data].tsx
+++ b/frontend/pages/viewer/[...data].tsx
@@ -1,3 +1,4 @@
+import { GetServerSideProps } from 'next';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -13,41 +14,54 @@ import ArticleContainer from '@components/viewer/ArticleContent';
import ClosedSideBar from '@components/viewer/ClosedSideBar';
import ScrapModal from '@components/viewer/ScrapModal';
import TOC from '@components/viewer/TOC';
+import ViewerHead from '@components/viewer/ViewerHead';
import useFetch from '@hooks/useFetch';
+import { IArticleBook, IBookScraps } from '@interfaces';
import { Flex } from '@styles/layout';
-export default function Viewer() {
- const { data: article, execute: getArticle } = useFetch(getArticleApi);
- const { data: book, execute: getBook } = useFetch(getBookApi);
+interface ViewerProps {
+ book: IBookScraps;
+ article: IArticleBook;
+}
+
+export default function Viewer({ book, article }: ViewerProps) {
const { data: userBooks, execute: getUserKnottedBooks } = useFetch(getUserKnottedBooksApi);
const user = useRecoilValue(signInStatusState);
+ const router = useRouter();
- const [isOpened, setIsOpened] = useState(true);
+ const [isOpened, setIsOpened] = useState(false);
const [isModalShown, setModalShown] = useState(false);
const handleModalOpen = () => setModalShown(true);
const handleModalClose = () => setModalShown(false);
- const router = useRouter();
-
const handleSideBarToggle = () => {
setIsOpened((prev) => !prev);
};
useEffect(() => {
- if (Array.isArray(router.query.data) && router.query.data?.length === 2) {
- const [bookId, articleId] = router.query.data;
+ getUserKnottedBooks(user.nickname);
+ }, [user.nickname]);
- getBook(bookId);
- getArticle(articleId);
- getUserKnottedBooks(user.nickname);
+ const checkArticleAuthority = (id: number) => {
+ if (book.scraps.find((scrap) => scrap.article.id === id)) {
+ return true;
}
- }, [router.query.data]);
+ return false;
+ };
+
+ useEffect(() => {
+ if (!checkArticleAuthority(article.id)) router.push('/404');
+ });
+ useEffect(() => {
+ if (window.innerWidth > 576) setIsOpened(true);
+ }, []);
return (
<>
+
{book && article ? (
@@ -56,12 +70,17 @@ export default function Viewer() {
) : (
)}
-
+ {book.scraps.find((scrap) => scrap.article.id === article.id) ? (
+
+ ) : (
+ 올바르지 않은 접근입니다.
+ )}
) : (
loading
@@ -74,3 +93,11 @@ export default function Viewer() {
>
);
}
+
+export const getServerSideProps: GetServerSideProps = async (context) => {
+ const [bookId, articleId] = context.query.data as string[];
+ const book = await getBookApi(bookId);
+ const article = await getArticleApi(articleId);
+
+ return { props: { book, article } };
+};
diff --git a/frontend/public/assets/ico_bookmark_black.svg b/frontend/public/assets/ico_bookmark_black.svg
index 4bb42b78..9ebe8a84 100644
--- a/frontend/public/assets/ico_bookmark_black.svg
+++ b/frontend/public/assets/ico_bookmark_black.svg
@@ -1,10 +1,3 @@
-