diff --git a/backend/package-lock.json b/backend/package-lock.json index e8b44764..fd694500 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@prisma/client": "^4.7.0", + "@prisma/client": "^4.7.1", "aws-sdk": "^2.1259.0", "axios": "^1.1.3", "bcrypt": "^5.1.0", @@ -37,7 +37,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", "prettier": "^2.7.1", - "prisma": "^4.7.0", + "prisma": "^4.7.1", "ts-node": "^10.9.1", "tsc-watch": "^5.0.3", "tsconfig-paths": "^4.1.0", @@ -264,12 +264,12 @@ "dev": true }, "node_modules/@prisma/client": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.7.0.tgz", - "integrity": "sha512-keXMa0oJWJGOzMEFKp+CEgzJPwnOtGSrnTWw6qMYxnypYrRFdNxqyA06EzELZexBhgM4oLooZ1jDJ3iy46wExA==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.7.1.tgz", + "integrity": "sha512-/GbnOwIPtjiveZNUzGXOdp7RxTEkHL4DZP3vBaFNadfr6Sf0RshU5EULFzVaSi9i9PIK9PYd+1Rn7z2B2npb9w==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "4.7.0-74.39190b250ebc338586e25e6da45e5e783bc8a635" + "@prisma/engines-version": "4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c" }, "engines": { "node": ">=14.17" @@ -284,16 +284,16 @@ } }, "node_modules/@prisma/engines": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.7.0.tgz", - "integrity": "sha512-afKrVFktaZ1pOK12/uFl2hRsBWIJZuC5FdDtacuKk5x/mR+rC5AbA+PlN3ZCZbmYTaeiBMHjcU5wbT5z2N3nSQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.7.1.tgz", + "integrity": "sha512-zWabHosTdLpXXlMefHmnouhXMoTB1+SCbUU3t4FCmdrtIOZcarPKU3Alto7gm/pZ9vHlGOXHCfVZ1G7OIrSbog==", "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "4.7.0-74.39190b250ebc338586e25e6da45e5e783bc8a635", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.7.0-74.39190b250ebc338586e25e6da45e5e783bc8a635.tgz", - "integrity": "sha512-ImczGEQ8NS1OUApEeyAGxC4uLTtQp0wI1+2wM4MeQLVwIQbyMHk1vOhWWE8Pwbi3rnzLcPvsIrd9sm6oNXhERw==" + "version": "4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c.tgz", + "integrity": "sha512-Bd4LZ+WAnUHOq31e9X/ihi5zPlr4SzTRwUZZYxvWOxlerIZ7HJlVa9zXpuKTKLpI9O1l8Ec4OYCKsivWCs5a3Q==" }, "node_modules/@tsconfig/node10": { "version": "1.0.9", @@ -3795,13 +3795,13 @@ } }, "node_modules/prisma": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.7.0.tgz", - "integrity": "sha512-VsecNo0Ca3+bDTzSpJqIpdupKVhhQ8aOYeWc09JlUM89knqvhSrlMrg0U8BiOD4tFrY1OPaCcraK8leDBxKMBg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.7.1.tgz", + "integrity": "sha512-CCQP+m+1qZOGIZlvnL6T3ZwaU0LAleIHYFPN9tFSzjs/KL6vH9rlYbGOkTuG9Q1s6Ki5D0LJlYlW18Z9EBUpGg==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "4.7.0" + "@prisma/engines": "4.7.1" }, "bin": { "prisma": "build/index.js", @@ -5017,23 +5017,23 @@ } }, "@prisma/client": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.7.0.tgz", - "integrity": "sha512-keXMa0oJWJGOzMEFKp+CEgzJPwnOtGSrnTWw6qMYxnypYrRFdNxqyA06EzELZexBhgM4oLooZ1jDJ3iy46wExA==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.7.1.tgz", + "integrity": "sha512-/GbnOwIPtjiveZNUzGXOdp7RxTEkHL4DZP3vBaFNadfr6Sf0RshU5EULFzVaSi9i9PIK9PYd+1Rn7z2B2npb9w==", "requires": { - "@prisma/engines-version": "4.7.0-74.39190b250ebc338586e25e6da45e5e783bc8a635" + "@prisma/engines-version": "4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c" } }, "@prisma/engines": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.7.0.tgz", - "integrity": "sha512-afKrVFktaZ1pOK12/uFl2hRsBWIJZuC5FdDtacuKk5x/mR+rC5AbA+PlN3ZCZbmYTaeiBMHjcU5wbT5z2N3nSQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.7.1.tgz", + "integrity": "sha512-zWabHosTdLpXXlMefHmnouhXMoTB1+SCbUU3t4FCmdrtIOZcarPKU3Alto7gm/pZ9vHlGOXHCfVZ1G7OIrSbog==", "devOptional": true }, "@prisma/engines-version": { - "version": "4.7.0-74.39190b250ebc338586e25e6da45e5e783bc8a635", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.7.0-74.39190b250ebc338586e25e6da45e5e783bc8a635.tgz", - "integrity": "sha512-ImczGEQ8NS1OUApEeyAGxC4uLTtQp0wI1+2wM4MeQLVwIQbyMHk1vOhWWE8Pwbi3rnzLcPvsIrd9sm6oNXhERw==" + "version": "4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c.tgz", + "integrity": "sha512-Bd4LZ+WAnUHOq31e9X/ihi5zPlr4SzTRwUZZYxvWOxlerIZ7HJlVa9zXpuKTKLpI9O1l8Ec4OYCKsivWCs5a3Q==" }, "@tsconfig/node10": { "version": "1.0.9", @@ -7640,12 +7640,12 @@ } }, "prisma": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.7.0.tgz", - "integrity": "sha512-VsecNo0Ca3+bDTzSpJqIpdupKVhhQ8aOYeWc09JlUM89knqvhSrlMrg0U8BiOD4tFrY1OPaCcraK8leDBxKMBg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.7.1.tgz", + "integrity": "sha512-CCQP+m+1qZOGIZlvnL6T3ZwaU0LAleIHYFPN9tFSzjs/KL6vH9rlYbGOkTuG9Q1s6Ki5D0LJlYlW18Z9EBUpGg==", "devOptional": true, "requires": { - "@prisma/engines": "4.7.0" + "@prisma/engines": "4.7.1" } }, "process-nextick-args": { diff --git a/backend/package.json b/backend/package.json index daac1602..10e0fcc5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "lint": "eslint ." }, "dependencies": { - "@prisma/client": "^4.7.0", + "@prisma/client": "^4.7.1", "aws-sdk": "^2.1259.0", "axios": "^1.1.3", "bcrypt": "^5.1.0", @@ -38,7 +38,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", "prettier": "^2.7.1", - "prisma": "^4.7.0", + "prisma": "^4.7.1", "ts-node": "^10.9.1", "tsc-watch": "^5.0.3", "tsconfig-paths": "^4.1.0", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 70d9fdf0..326bf0cc 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1,8 +1,5 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" previewFeatures = ["fullTextSearch", "fullTextIndex"] } @@ -12,77 +9,77 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - username String @db.VarChar(20) - password String @db.VarChar(100) - nickname String @db.VarChar(20) @unique - description String @db.VarChar(100) - profile_image String @db.VarChar(255) @default("https://kr.object.ncloudstorage.com/j027/522da4f3-c9d7-403d-a98f-2b09cabefc47.png") - provider String @db.VarChar(20) - created_at DateTime @default(now()) - deleted_at DateTime? - books Book[] - bookmarks Bookmark[] + id Int @id @default(autoincrement()) + username String @db.VarChar(20) + password String @db.VarChar(100) + nickname String @unique @db.VarChar(20) + description String @db.VarChar(100) + profile_image String @default("https://kr.object.ncloudstorage.com/j027/522da4f3-c9d7-403d-a98f-2b09cabefc47.png") @db.VarChar(255) + provider String @db.VarChar(20) + created_at DateTime @default(now()) + deleted_at DateTime? + books Book[] + bookmarks Bookmark[] temporary_article TemporaryArticle? - token Token? + token Token? } model Book { - id Int @id @default(autoincrement()) - thumbnail_image String @db.VarChar(255) @default("https://kr.object.ncloudstorage.com/j027/3947d647-f26e-43cc-9834-82d59703cd9c.png") - title String @db.VarChar(50) - created_at DateTime @default(now()) - deleted_at DateTime? - user User @relation(fields: [user_id], references: [id]) - user_id Int - articles Article[] - scraps Scrap[] - bookmarks Bookmark[] + id Int @id @default(autoincrement()) + thumbnail_image String @default("https://kr.object.ncloudstorage.com/j027/3947d647-f26e-43cc-9834-82d59703cd9c.png") @db.VarChar(255) + title String @db.VarChar(50) + created_at DateTime @default(now()) + deleted_at DateTime? + user User @relation(fields: [user_id], references: [id]) + user_id Int + articles Article[] + scraps Scrap[] + bookmarks Bookmark[] - @@fulltext([title]) + @@fulltext([title], map: "title") } model Article { - id Int @id @default(autoincrement()) - title String @db.VarChar(50) - content String @db.Text - created_at DateTime @default(now()) + id Int @id @default(autoincrement()) + title String @db.VarChar(100) + content String @db.Text + created_at DateTime @default(now()) deleted_at DateTime? - book Book @relation(fields: [book_id], references: [id]) - book_id Int - scraps Scrap[] + book Book @relation(fields: [book_id], references: [id]) + book_id Int + scraps Scrap[] - @@fulltext([content, title]) + @@fulltext([title, content], map: "title_content") } model Scrap { - id Int @id @default(autoincrement()) - order Int + id Int @id @default(autoincrement()) + order Int is_original Boolean - article Article @relation(fields: [article_id], references: [id]) - article_id Int - book Book @relation(fields: [book_id], references: [id]) - book_id Int + article Article @relation(fields: [article_id], references: [id]) + article_id Int + book Book @relation(fields: [book_id], references: [id]) + book_id Int } model Bookmark { - id Int @id @default(autoincrement()) - user User @relation(fields: [user_id], references: [id]) + id Int @id @default(autoincrement()) + user User @relation(fields: [user_id], references: [id]) user_id Int - book Book @relation(fields: [book_id], references: [id]) + book Book @relation(fields: [book_id], references: [id]) book_id Int } model TemporaryArticle { - id Int @id @default(autoincrement()) - title String @db.VarChar(50) + id Int @id @default(autoincrement()) + title String @db.VarChar(50) content String @db.Text - user User @relation(fields: [user_id], references: [id]) - user_id Int @unique + user User @relation(fields: [user_id], references: [id]) + user_id Int @unique } model Token { refresh_token String @db.VarChar(200) - user User @relation(fields: [user_id], references: [id]) - user_id Int @unique -} \ No newline at end of file + user User @relation(fields: [user_id], references: [id]) + user_id Int @unique +} diff --git a/backend/src/apis/articles/articles.controller.ts b/backend/src/apis/articles/articles.controller.ts index b38b5ca6..c213bd21 100644 --- a/backend/src/apis/articles/articles.controller.ts +++ b/backend/src/apis/articles/articles.controller.ts @@ -4,6 +4,7 @@ import { SearchArticles } from '@apis/articles/articles.interface'; import articlesService from '@apis/articles/articles.service'; import { IScrap } from '@apis/scraps/scraps.interface'; import scrapsService from '@apis/scraps/scraps.service'; +import { Forbidden, Message } from '@errors'; const searchArticles = async (req: Request, res: Response) => { const { query, page, take, userId } = req.query as unknown as SearchArticles; @@ -23,6 +24,8 @@ const getArticle = async (req: Request, res: Response) => { const createArticle = async (req: Request, res: Response) => { const { article, scraps } = req.body; + if (!article.title.length) throw new Forbidden(Message.ARTICLE_INVALID_TITLE); + const createdArticle = await articlesService.createArticle({ title: article.title, content: article.content, @@ -49,6 +52,8 @@ const createArticle = async (req: Request, res: Response) => { const updateArticle = async (req: Request, res: Response) => { const { article, scraps } = req.body; + if (!article.title.length) throw new Forbidden(Message.ARTICLE_INVALID_TITLE); + const articleId = Number(req.params.articleId); const modifiedArticle = await articlesService.updateArticle(articleId, { diff --git a/backend/src/apis/books/books.controller.ts b/backend/src/apis/books/books.controller.ts index a7ddd4d8..dc94588a 100644 --- a/backend/src/apis/books/books.controller.ts +++ b/backend/src/apis/books/books.controller.ts @@ -4,6 +4,7 @@ import { FindBooks, SearchBooks } from '@apis/books/books.interface'; import booksService from '@apis/books/books.service'; import { IScrap } from '@apis/scraps/scraps.interface'; import scrapsService from '@apis/scraps/scraps.service'; +import { Forbidden, Message } from '@errors'; const getBook = async (req: Request, res: Response) => { const { bookId } = req.params; @@ -40,6 +41,8 @@ const searchBooks = async (req: Request, res: Response) => { const createBook = async (req: Request, res: Response) => { const { title } = req.body; + if (!title.length) throw new Forbidden(Message.BOOK_INVALID_TITLE); + const userId = res.locals.user.id; const book = await booksService.createBook({ title, userId }); @@ -52,6 +55,8 @@ const createBook = async (req: Request, res: Response) => { const updateBook = async (req: Request, res: Response) => { const { id, title, thumbnail_image, scraps } = req.body; + if (!title.length) throw new Forbidden(Message.BOOK_INVALID_TITLE); + const userId = res.locals.user.id; const book = await booksService.updateBook({ id, title, thumbnail_image }); diff --git a/backend/src/apis/books/books.service.ts b/backend/src/apis/books/books.service.ts index 1a2f0145..15965734 100644 --- a/backend/src/apis/books/books.service.ts +++ b/backend/src/apis/books/books.service.ts @@ -20,6 +20,7 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => { select: { id: true, order: true, + is_original: true, article: { select: { id: true, @@ -76,7 +77,9 @@ const getBook = async (bookId: number, userId: number) => { scraps: { orderBy: { order: 'asc' }, select: { + id: true, order: true, + is_original: true, article: { select: { id: true, diff --git a/backend/src/apis/index.ts b/backend/src/apis/index.ts index 312219cf..172ca29a 100644 --- a/backend/src/apis/index.ts +++ b/backend/src/apis/index.ts @@ -42,6 +42,7 @@ router.post('/bookmarks', guard, catchAsync(bookmarksController.createBookmark)) router.delete('/bookmarks/:bookmarkId', catchAsync(bookmarksController.deleteBookmark)); router.get('/scraps', catchAsync(scrapsController.getScraps)); +router.patch('/scraps', catchAsync(guard), catchAsync(scrapsController.updateScrapsOrder)); router.post('/scraps', catchAsync(scrapsController.createScrap)); router.delete('/scraps/:scrapId', guard, catchAsync(scrapsController.deleteScrap)); diff --git a/backend/src/apis/scraps/scraps.controller.ts b/backend/src/apis/scraps/scraps.controller.ts index 80e765e4..99307850 100644 --- a/backend/src/apis/scraps/scraps.controller.ts +++ b/backend/src/apis/scraps/scraps.controller.ts @@ -20,7 +20,7 @@ const createScrap = async (req: Request, res: Response) => { } }); - return res.status(201).send(); + return res.status(201).send({ book_id, article_id }); }; const deleteScrap = async (req: Request, res: Response) => { @@ -36,8 +36,19 @@ const getScraps = async (req: Request, res: Response) => { return res.status(200).send(scraps); }; +const updateScrapsOrder = async (req: Request, res: Response) => { + const scraps = req.body; + + scraps.forEach(async (scrap: IScrap) => { + await scrapsService.updateScrapOrder(scrap); + }); + + res.status(200).send(scraps); +}; + export default { createScrap, deleteScrap, getScraps, + updateScrapsOrder, }; diff --git a/backend/src/errors/message.ts b/backend/src/errors/message.ts index 3e0006e2..7c3f94f6 100644 --- a/backend/src/errors/message.ts +++ b/backend/src/errors/message.ts @@ -2,11 +2,13 @@ export default { AUTH_WRONG: '아이디 또는 비밀번호가 일치하지 않습니다.', AUTH_USERNAME_OVERLAP: '중복되는 아이디가 존재합니다.', AUTH_NICKNAME_OVERLAP: '중복되는 닉네임이 존재합니다.', - SCRAP_OVERLAP: '이미 스크랩되어 있는 글입니다.', - ARTICLE_NOTFOUND: '일치하는 글이 없습니다.', - BOOK_NOTFOUND: '일치하는 책이 없습니다.', USER_NOTFOUND: '일치하는 유저가 없습니다.', TOKEN_EXPIRED: '로그인이 필요합니다.', TOKEN_MALFORMED: '로그인이 필요합니다.', + BOOK_NOTFOUND: '일치하는 책이 없습니다.', + BOOK_INVALID_TITLE: '책 제목이 비어있습니다.', BOOKMARK_NOTFOUND: '북마크된 책이 아닙니다.', + ARTICLE_NOTFOUND: '일치하는 글이 없습니다.', + ARTICLE_INVALID_TITLE: '글 제목이 비어있습니다.', + SCRAP_OVERLAP: '이미 스크랩되어 있는 글입니다.', }; diff --git a/frontend/apis/scrapApi.ts b/frontend/apis/scrapApi.ts index 2ac50ba2..47ece7ff 100644 --- a/frontend/apis/scrapApi.ts +++ b/frontend/apis/scrapApi.ts @@ -1,3 +1,4 @@ +import { IScrap } from '@interfaces'; import api from '@utils/api'; export const getScrapsApi = async () => { @@ -30,3 +31,9 @@ export const deleteScrapApi = async (scrapId: string) => { return response.data; }; +export const updateScrapsOrderApi = async (data: IScrap[]) => { + const url = `/api/scraps`; + const response = await api({ url, method: 'PATCH', data }); + + return response.data; +}; diff --git a/frontend/components/common/Book/index.tsx b/frontend/components/common/Book/index.tsx index 9e4fc0fb..baabada5 100644 --- a/frontend/components/common/Book/index.tsx +++ b/frontend/components/common/Book/index.tsx @@ -1,13 +1,10 @@ -import Image from 'next/image'; - import InactiveBookmarkIcon from '@assets/ico_bookmark_black.svg'; import ActiveBookmarkIcon from '@assets/ico_bookmark_grey_filled.svg'; -import MoreContentsIcon from '@assets/ico_more_contents.svg'; import sampleImage from '@assets/img_sample_thumbnail.jpg'; import useBookmark from '@hooks/useBookmark'; import { IBookScraps } from '@interfaces'; import { TextLarge, TextXSmall, TextSmall } from '@styles/common'; -import { FlexCenter, FlexSpaceBetween } from '@styles/layout'; +import { FlexSpaceBetween } from '@styles/layout'; import { BookWrapper, @@ -38,7 +35,7 @@ export default function Book({ book }: BookProps) { // 수정모드일때만 아래 onclick이 실행되도록 수정해야함 -> 민형님 작업 후 {title} diff --git a/frontend/components/common/Book/styled.ts b/frontend/components/common/Book/styled.ts index 9e564be3..e82fa156 100644 --- a/frontend/components/common/Book/styled.ts +++ b/frontend/components/common/Book/styled.ts @@ -112,7 +112,7 @@ export const AuthorLink = styled(Link)` margin-top: 2px; `; -export const BookLink = styled(Link)<{ isArticleExists: boolean }>` +export const BookLink = styled(Link)<{ isarticleexists: string }>` text-decoration: none; - ${(props) => (props.isArticleExists ? '' : 'pointer-events: none;')} + ${(props) => (props.isarticleexists === 'true' ? '' : 'pointer-events: none;')} `; diff --git a/frontend/components/common/Content/index.tsx b/frontend/components/common/Content/index.tsx index 3fb1f599..c62932a2 100644 --- a/frontend/components/common/Content/index.tsx +++ b/frontend/components/common/Content/index.tsx @@ -1,5 +1,7 @@ import { ContentBody, ContentTitle, ContentWrapper } from './styled'; +import 'highlight.js/styles/github.css'; + interface ContentProps { title?: string; content: string; diff --git a/frontend/components/common/Content/styled.ts b/frontend/components/common/Content/styled.ts index af2efe59..955335f8 100644 --- a/frontend/components/common/Content/styled.ts +++ b/frontend/components/common/Content/styled.ts @@ -42,12 +42,10 @@ export const ContentBody = styled.div` ol { list-style-type: decimal; - list-style-position: inside; } ul { list-style-type: disc; - list-style-position: inside; } p { @@ -68,8 +66,6 @@ export const ContentBody = styled.div` blockquote { margin: 24px 0; padding: 24px 16px; - /* background-color: var(--light-orange-color); */ - /* border-radius: 4px; */ border-left: 8px solid var(--light-orange-color); } @@ -90,6 +86,7 @@ export const ContentBody = styled.div` code { padding: 0; + white-space: pre-wrap; } } `; diff --git a/frontend/components/common/GNB/index.tsx b/frontend/components/common/GNB/index.tsx index 9879bdcf..d017a82d 100644 --- a/frontend/components/common/GNB/index.tsx +++ b/frontend/components/common/GNB/index.tsx @@ -12,7 +12,7 @@ import SignInModal from '@components/auth/SignInModal'; import SignUpModal from '@components/auth/SignUpModal'; import Modal from '@components/common/Modal'; -import { GNBbar, Icon, IconsContainer, Logo } from './styled'; +import { GNBbar, Icon, IconsContainer, Logo, LogoWrapper } from './styled'; export default function GNB() { const [isModalShown, setModalShown] = useState(false); @@ -29,8 +29,9 @@ export default function GNB() { return ( - - knoticle + + knoticle + props.theme.mobile} { + left: 0; + right: none; + margin-left: 30px; + } +`; + export const Logo = styled(Link)` + margin: auto; font-family: 'Sofia'; font-style: normal; font-weight: 500; @@ -27,7 +43,8 @@ export const Logo = styled(Link)` `; export const IconsContainer = styled.div` - width: 96px; + /* width: 96px; */ + gap: 20px; display: flex; justify-content: space-between; align-items: center; diff --git a/frontend/components/edit/Editor/styled.ts b/frontend/components/edit/Editor/styled.ts index 2d5c4d8f..b6395662 100644 --- a/frontend/components/edit/Editor/styled.ts +++ b/frontend/components/edit/Editor/styled.ts @@ -2,7 +2,7 @@ import styled from 'styled-components'; export const EditorWrapper = styled.div` width: 100%; - height: 100vh; + height: calc(var(--window-inner-height)); display: flex; > div:nth-child(2) { @@ -25,7 +25,7 @@ export const EditorInner = styled.div` export const CodeMirrorWrapper = styled.div` font-size: 16px; - height: calc(100vh - 160px); + height: calc(var(--window-inner-height) - 160px); overflow: auto; .cm-editor.cm-focused { diff --git a/frontend/components/home/Slider/styled.ts b/frontend/components/home/Slider/styled.ts index aed12aa2..eee8827c 100644 --- a/frontend/components/home/Slider/styled.ts +++ b/frontend/components/home/Slider/styled.ts @@ -2,12 +2,17 @@ import Image from 'next/image'; import styled from 'styled-components'; -import { FlexColumn, FlexSpaceBetween } from '@styles/layout'; +import { FlexCenter, FlexColumn, FlexSpaceBetween } from '@styles/layout'; export const SliderWrapper = styled.div` display: flex; + justify-content: space-between; align-items: center; gap: 10px; + + @media ${(props) => props.theme.mobile} { + gap: 0; + } `; export const SliderContent = styled(FlexColumn)<{ numberPerPage: number }>` @@ -55,21 +60,8 @@ export const SliderIndicatorContainer = styled.div` gap: 4px; `; -export const SliderBookWrapper = styled.div<{ numberPerPage: number }>` +export const SliderBookWrapper = styled(FlexCenter)<{ 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;'; diff --git a/frontend/components/study/BookListTab/index.tsx b/frontend/components/study/BookListTab/index.tsx index f659d3a6..98187a7d 100644 --- a/frontend/components/study/BookListTab/index.tsx +++ b/frontend/components/study/BookListTab/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useState } from 'react'; import { useRecoilState } from 'recoil'; diff --git a/frontend/components/viewer/ArticleContent/index.tsx b/frontend/components/viewer/ArticleContent/index.tsx index 7536c14d..67c2bf61 100644 --- a/frontend/components/viewer/ArticleContent/index.tsx +++ b/frontend/components/viewer/ArticleContent/index.tsx @@ -6,6 +6,7 @@ import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { deleteArticleApi } from '@apis/articleApi'; +import { deleteScrapApi, updateScrapsOrderApi } from '@apis/scrapApi'; import LeftBtnIcon from '@assets/ico_leftBtn.svg'; import Original from '@assets/ico_original.svg'; import RightBtnIcon from '@assets/ico_rightBtn.svg'; @@ -45,6 +46,9 @@ export default function Article({ const user = useRecoilValue(signInStatusState); const { data: deleteArticleData, execute: deleteArticle } = useFetch(deleteArticleApi); + const { execute: deleteScrap } = useFetch(deleteScrapApi); + const { data: updateScrapsData, execute: updateScrapsOrder } = useFetch(updateScrapsOrderApi); + const router = useRouter(); const handleOriginalBtnOnClick = () => { @@ -65,13 +69,21 @@ export default function Article({ const handleDeleteBtnOnClick = () => { if (window.confirm('해당 글을 삭제하시겠습니까?')) { + const curScrap = scraps.find((scrap) => scrap.article.id === article.id); + deleteScrap(curScrap?.id); deleteArticle(article.id); } }; const handleScrapDeleteBtnOnClick = () => { if (window.confirm('해당 글을 책에서 삭제하시겠습니까?')) { - // + const curScrap = scraps.find((scrap) => scrap.article.id === article.id); + if (!curScrap) return; + const newScraps = scraps + .filter((scrap) => scrap.id !== curScrap.id) + .map((v, i) => ({ ...v, order: i + 1 })); + updateScrapsOrder(newScraps); + deleteScrap(curScrap.id); } }; @@ -83,6 +95,16 @@ export default function Article({ if (deleteArticleData !== undefined) router.push('/'); }, [deleteArticleData]); + useEffect(() => { + if (updateScrapsData === undefined) return; + + if (updateScrapsData.length !== 0) { + router.push(`/viewer/${bookId}/${updateScrapsData[0].article.id}`); + return; + } + router.push('/'); + }, [updateScrapsData]); + return ( {article.id === scraps.at(0)?.article.id ? null : ( @@ -112,9 +134,9 @@ export default function Article({ 글 수정 )} - {/* {article.book_id !== bookId && bookAuthor === user.nickname && ( + {article.book_id !== bookId && bookAuthor === user.nickname && ( 스크랩 삭제 - )} */} + )} {user.id !== 0 && ( Scrap Icon diff --git a/frontend/components/viewer/ArticleContent/styled.ts b/frontend/components/viewer/ArticleContent/styled.ts index 739a0980..0fe75bc2 100644 --- a/frontend/components/viewer/ArticleContent/styled.ts +++ b/frontend/components/viewer/ArticleContent/styled.ts @@ -4,7 +4,7 @@ import { Flex, FlexColumn } from '@styles/layout'; export const ArticleContainer = styled(Flex)` flex: 1; - height: calc(100vh - 67px); + height: calc(var(--window-inner-height) - 67px); `; export const ArticleLeftBtn = styled.div` position: fixed; @@ -57,6 +57,7 @@ export const ArticleTitleBtnBox = styled(Flex)` margin-top: 16px; border-top: 1px solid var(--grey-02-color); padding-top: 10px; + margin-bottom: 30px; `; export const ArticleContents = styled.div` margin-top: 20px; diff --git a/frontend/components/viewer/ClosedSideBar/styled.ts b/frontend/components/viewer/ClosedSideBar/styled.ts index 1ff3f7a8..2b75804a 100644 --- a/frontend/components/viewer/ClosedSideBar/styled.ts +++ b/frontend/components/viewer/ClosedSideBar/styled.ts @@ -4,7 +4,7 @@ import { FlexCenter } from '@styles/layout'; export const ClosedSideBarWrapper = styled.div` min-width: 30px; - height: calc(100vh - 67px); + height: calc(var(--window-inner-height) - 67px); background-color: var(--primary-color); `; diff --git a/frontend/components/viewer/ScrapModal/index.tsx b/frontend/components/viewer/ScrapModal/index.tsx index 88e860fc..3d307bc9 100644 --- a/frontend/components/viewer/ScrapModal/index.tsx +++ b/frontend/components/viewer/ScrapModal/index.tsx @@ -1,9 +1,13 @@ +import { useRouter } from 'next/router'; + import { useEffect, useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { getUserKnottedBooksApi } from '@apis/bookApi'; import { createScrapApi } from '@apis/scrapApi'; import scrapState from '@atoms/scrap'; +import signInStatusState from '@atoms/signInStatus'; import DragArticle from '@components/common/DragDrop'; import Dropdown from '@components/common/Dropdown'; import ModalButton from '@components/common/Modal/ModalButton'; @@ -13,20 +17,26 @@ import { IBook, IArticle, IScrap, IBookScraps } from '@interfaces'; import { ArticleWrapper, Label, ScrapModalWrapper, WarningLabel } from './styled'; interface ScrapModalProps { - books: IBookScraps[]; + bookId: number; handleModalClose: () => void; article: IArticle; } -export default function ScrapModal({ books, handleModalClose, article }: ScrapModalProps) { +export default function ScrapModal({ bookId, handleModalClose, article }: ScrapModalProps) { const [selectedBookIndex, setSelectedBookIndex] = useState(-1); const [filteredScraps, setFilteredScraps] = useState([]); - const { execute: createScrap } = useFetch(createScrapApi); + const { data: createScrapData, execute: createScrap } = useFetch(createScrapApi); + const { data: books, execute: getUserKnottedBooks } = + useFetch(getUserKnottedBooksApi); + + const user = useRecoilValue(signInStatusState); const [scrapList, setScrapList] = useRecoilState(scrapState); const [isSelectedBookUnavailable, setSelectedBookUnavailable] = useState(false); + const router = useRouter(); + const createBookDropdownItems = (items: IBook[]) => items.map((item) => { return { @@ -51,9 +61,20 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo return items.some((item) => item.article.id === articleId); }; - useEffect(() => { + const handleScrapBtnClick = () => { if (selectedBookIndex === -1) return; + const scraps = scrapList.map((v, i) => ({ ...v, order: i + 1 })); + + createScrap({ book_id: selectedBookIndex, article_id: article.id, scraps }); + }; + useEffect(() => { + getUserKnottedBooks(user.nickname); + }, [user.nickname]); + + useEffect(() => { + if (selectedBookIndex === -1 || !books) return; + const selectedBook = books.find((book) => book.id === selectedBookIndex); if (!selectedBook || checkArticleExistsInBook(article.id, selectedBook.scraps)) { @@ -73,24 +94,23 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo setScrapList(createScrapDropdownItems(filteredScraps)); }, [filteredScraps]); - const handleScrapBtnClick = () => { - if (selectedBookIndex === -1) return; - - const scraps = scrapList.map((v, i) => ({ ...v, order: i + 1 })); - - createScrap({ book_id: selectedBookIndex, article_id: article.id, scraps }); + useEffect(() => { + if (createScrapData === undefined) return; + router.push(`/viewer/${bookId}/${article.id}`); handleModalClose(); - }; + }, [createScrapData]); return ( - setSelectedBookIndex(id)} - /> + {books && ( + setSelectedBookIndex(id)} + /> + )} {isSelectedBookUnavailable && ( 선택하신 책에 이미 동일한 글이 존재합니다. )} diff --git a/frontend/components/viewer/TOC/styled.ts b/frontend/components/viewer/TOC/styled.ts index bea16deb..09d1c924 100644 --- a/frontend/components/viewer/TOC/styled.ts +++ b/frontend/components/viewer/TOC/styled.ts @@ -17,7 +17,7 @@ const slide = keyframes` export const TocWrapper = styled(Flex)` /* 고정크기? %? */ flex-basis: 300px; - height: calc(100vh - 67px); + height: calc(var(--window-inner-height) - 67px); overflow: hidden; background-color: var(--primary-color); color: var(--white-color); @@ -52,7 +52,7 @@ export const TocContainer = styled.div` padding: 24px; margin-top: 10px; overflow: auto; - height: calc(100vh - 357px); + height: calc(var(--window-inner-height) - 357px); ::-webkit-scrollbar { width: 10px; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 58dad363..92adead7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "dotenv-webpack": "^8.0.1", "eslint": "8.27.0", "eslint-config-next": "13.0.3", + "highlight.js": "^11.7.0", "immutability-helper": "^3.1.1", "next": "13.0.3", "next-sitemap": "^3.1.32", @@ -33,6 +34,7 @@ "react-dom": "18.2.0", "react-toastify": "^9.1.1", "recoil": "^0.7.6", + "rehype-highlight": "^6.0.0", "rehype-parse": "^8.0.4", "rehype-remark": "^9.1.2", "rehype-stringify": "^9.0.3", @@ -3036,6 +3038,18 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3122,6 +3136,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3533,6 +3555,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/highlight.js": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz", + "integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==", + "engines": { + "node": ">=12.0.0" + } + }, "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", @@ -4051,6 +4081,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.8.0.tgz", + "integrity": "sha512-WeExw1IKEkel9ZcYwzpvcFzORIB0IlleTcxJYoEuUgHASuYe/OBYbV6ym/AetG7unNVCBU/SXpgTgs2nT93mhg==", + "dependencies": { + "@types/hast": "^2.0.0", + "fault": "^2.0.0", + "highlight.js": "~11.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -5273,6 +5317,22 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/rehype-highlight": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-6.0.0.tgz", + "integrity": "sha512-q7UtlFicLhetp7K48ZgZiJgchYscMma7XjzX7t23bqEJF8m6/s+viXQEe4oHjrATTIZpX7RG8CKD7BlNZoh9gw==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-to-text": "^3.0.0", + "lowlight": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-minify-whitespace": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz", @@ -8697,6 +8757,14 @@ "reusify": "^1.0.4" } }, + "fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "requires": { + "format": "^0.2.0" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8751,6 +8819,11 @@ "mime-types": "^2.1.12" } }, + "format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -9048,6 +9121,11 @@ "space-separated-tokens": "^2.0.0" } }, + "highlight.js": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz", + "integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==" + }, "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", @@ -9396,6 +9474,16 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowlight": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.8.0.tgz", + "integrity": "sha512-WeExw1IKEkel9ZcYwzpvcFzORIB0IlleTcxJYoEuUgHASuYe/OBYbV6ym/AetG7unNVCBU/SXpgTgs2nT93mhg==", + "requires": { + "@types/hast": "^2.0.0", + "fault": "^2.0.0", + "highlight.js": "~11.7.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -10138,6 +10226,18 @@ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" }, + "rehype-highlight": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-6.0.0.tgz", + "integrity": "sha512-q7UtlFicLhetp7K48ZgZiJgchYscMma7XjzX7t23bqEJF8m6/s+viXQEe4oHjrATTIZpX7RG8CKD7BlNZoh9gw==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-to-text": "^3.0.0", + "lowlight": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + } + }, "rehype-minify-whitespace": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e3450499..0b7c13c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "dotenv-webpack": "^8.0.1", "eslint": "8.27.0", "eslint-config-next": "13.0.3", + "highlight.js": "^11.7.0", "immutability-helper": "^3.1.1", "next": "13.0.3", "next-sitemap": "^3.1.32", @@ -34,6 +35,7 @@ "react-dom": "18.2.0", "react-toastify": "^9.1.1", "recoil": "^0.7.6", + "rehype-highlight": "^6.0.0", "rehype-parse": "^8.0.4", "rehype-remark": "^9.1.2", "rehype-stringify": "^9.0.3", diff --git a/frontend/pages/booktest.tsx b/frontend/pages/booktest.tsx deleted file mode 100644 index 1f7a26b6..00000000 --- a/frontend/pages/booktest.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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 b82246e8..cb83b70e 100644 --- a/frontend/pages/editor.tsx +++ b/frontend/pages/editor.tsx @@ -14,6 +14,7 @@ import ModifyModal from '@components/edit/ModifyModal'; import PublishModal from '@components/edit/PublishModal'; import useFetch from '@hooks/useFetch'; import { IArticle } from '@interfaces'; +import { PageNoScrollWrapper } from '@styles/layout'; import { toastError } from '@utils/toast'; export default function EditorPage() { @@ -49,8 +50,18 @@ export default function EditorPage() { setOriginalArticle(article); }, [article]); + const syncHeight = () => { + document.documentElement.style.setProperty('--window-inner-height', `${window.innerHeight}px`); + }; + + useEffect(() => { + syncHeight(); + window.addEventListener('resize', syncHeight); + return () => window.removeEventListener('resize', syncHeight); + }, []); + return ( - <> + @@ -64,6 +75,6 @@ export default function EditorPage() { ))} - + ); } diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index efc4d888..2665adab 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -62,18 +62,18 @@ export default function Home() { {numberPerPage !== 0 && ( <> - +