diff --git a/client/index.html b/client/index.html index b8ac8169..932b9a23 100644 --- a/client/index.html +++ b/client/index.html @@ -8,6 +8,7 @@
+ diff --git a/client/src/App.tsx b/client/src/App.tsx index 7e0ff77e..89a4844b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -7,6 +7,7 @@ import { isEmpty } from "@/utils/typeCheck"; import ReactRouter from "@/ReactRouter"; import CommonModalWrapper from "@/components/main/Modal/ModalWrapper/CommonModalWrapper"; import { useRefreshInterceptor } from "@/hooks/useRefreshInterceptor"; +import ModalProvider from "@/components/commons/Modal/ModalProvider/ModalProvider"; import "./App.scss"; @@ -21,6 +22,7 @@ function App(): JSX.Element { + ); diff --git a/client/src/apis/post.ts b/client/src/apis/post.ts index 8f060889..d39d5401 100644 --- a/client/src/apis/post.ts +++ b/client/src/apis/post.ts @@ -38,6 +38,11 @@ export const fetchBookmarkPost = async ( return data; }; +export const deletePost = async (postId: string): Promise => { + const { data } = await axiosInstance.delete(`/posts/${postId}`); + return data; +}; + export const uploadImage = async ({ preSignedData, imageUri, diff --git a/client/src/components/commons/Modal/ModalProvider/ModalProvider.tsx b/client/src/components/commons/Modal/ModalProvider/ModalProvider.tsx new file mode 100644 index 00000000..bcd9e669 --- /dev/null +++ b/client/src/components/commons/Modal/ModalProvider/ModalProvider.tsx @@ -0,0 +1,25 @@ +import React, { ReactPortal } from "react"; +import { createPortal } from "react-dom"; + +import useModalStore from "@/store/useModalStore"; +import PostMoreModal from "@/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModal"; +import { MODAL_KEY, ModalProps } from "@/types/modal"; + +const MODALS = new Map>([ + [MODAL_KEY.POST_MORE, PostMoreModal], +]); + +const ModalProvider = (): ReactPortal => { + const modals = useModalStore((state) => state.modals); + const Modals = modals.map(({ key, props }) => { + const Modal = MODALS.get(key) as React.FC; + return ; + }); + + return createPortal( + <>{Modals}, + document.getElementById("modal") as Element + ); +}; + +export default ModalProvider; diff --git a/client/src/components/commons/Modal/core/Modal/Modal.tsx b/client/src/components/commons/Modal/core/Modal/Modal.tsx new file mode 100644 index 00000000..5db96bbf --- /dev/null +++ b/client/src/components/commons/Modal/core/Modal/Modal.tsx @@ -0,0 +1,35 @@ +import React, { ReactNode, useRef } from "react"; + +import ModalContainer from "@/components/commons/Modal/core/ModalContainer/ModalContainer"; +import ModalWrapper from "@/components/commons/Modal/core/ModalWrapper/ModalWrapper"; +import ModalOverlay from "@/components/commons/Modal/core/ModalOverlay/ModalOverlay"; +import useOutsideClickHandler from "@/hooks/useOutsideClickHandler"; +import ModalTitle from "@/components/commons/Modal/core/ModalTitle/ModalTitle"; +import ModalContents from "@/components/commons/Modal/core/ModalContents/ModalContents"; + +interface ModalProps { + title?: string; + onClose?: Function; + children?: ReactNode; +} + +const Modal = ({ title, onClose, children }: ModalProps): JSX.Element => { + const modalRef = useRef(null); + const handleModalClose = (): void => { + onClose?.(); + }; + useOutsideClickHandler(modalRef, handleModalClose); + + return ( + + + + + {children} + + + + ); +}; + +export default Modal; diff --git a/client/src/components/commons/Modal/core/ModalContainer/ModalContainer.tsx b/client/src/components/commons/Modal/core/ModalContainer/ModalContainer.tsx new file mode 100644 index 00000000..8d9a9fe1 --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalContainer/ModalContainer.tsx @@ -0,0 +1,15 @@ +import React, { ReactNode, ReactPortal } from "react"; +import { createPortal } from "react-dom"; + +interface ModalContainerProps { + children: ReactNode; +} + +const ModalContainer = ({ children }: ModalContainerProps): ReactPortal => { + const modalElement = document.getElementById("modal") as HTMLElement; + const modalContents = <>{children}; + + return createPortal(modalContents, modalElement); +}; + +export default ModalContainer; diff --git a/client/src/components/commons/Modal/core/ModalContents/ModalContents.scss b/client/src/components/commons/Modal/core/ModalContents/ModalContents.scss new file mode 100644 index 00000000..f93467dc --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalContents/ModalContents.scss @@ -0,0 +1,6 @@ +@import "@/styles/_theme.scss"; + +.modal-contents { + font-size: $font-medium; + color: $text-color; +} diff --git a/client/src/components/commons/Modal/core/ModalContents/ModalContents.tsx b/client/src/components/commons/Modal/core/ModalContents/ModalContents.tsx new file mode 100644 index 00000000..287b5284 --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalContents/ModalContents.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from "react"; + +import "./ModalContents.scss"; + +interface ModalContentsProps { + children?: ReactNode; +} + +const ModalContents = ({ children }: ModalContentsProps): JSX.Element => { + return
{children}
; +}; + +export default ModalContents; diff --git a/client/src/components/commons/Modal/core/ModalOverlay/ModalOverlay.scss b/client/src/components/commons/Modal/core/ModalOverlay/ModalOverlay.scss new file mode 100644 index 00000000..6d93e63a --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalOverlay/ModalOverlay.scss @@ -0,0 +1,12 @@ +$highest-z-index: 90000; // 가장 높은 z-index 값으로 설정 + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + z-index: $highest-z-index; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.2); // 모달 배경 색 + backdrop-filter: blur(2px); +} diff --git a/client/src/components/commons/Modal/core/ModalOverlay/ModalOverlay.tsx b/client/src/components/commons/Modal/core/ModalOverlay/ModalOverlay.tsx new file mode 100644 index 00000000..c144ca07 --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalOverlay/ModalOverlay.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from "react"; + +import "./ModalOverlay.scss"; + +interface ModalOverlayProps { + children: ReactNode; +} + +const ModalOverlay = ({ children }: ModalOverlayProps): JSX.Element => { + return
{children}
; +}; + +export default ModalOverlay; diff --git a/client/src/components/commons/Modal/core/ModalTitle/ModalTitle.scss b/client/src/components/commons/Modal/core/ModalTitle/ModalTitle.scss new file mode 100644 index 00000000..27682647 --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalTitle/ModalTitle.scss @@ -0,0 +1,11 @@ +@import "@/styles/_theme.scss"; + +.modal-title { + width: 100%; + height: 5rem; + font-size: $font-large; + // div 텍스트 중앙 정렬 + line-height: 5rem; + text-align: center; + border-bottom: $border-small $line-color; +} diff --git a/client/src/components/commons/Modal/core/ModalTitle/ModalTitle.tsx b/client/src/components/commons/Modal/core/ModalTitle/ModalTitle.tsx new file mode 100644 index 00000000..f282ed15 --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalTitle/ModalTitle.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +import "./ModalTitle.scss"; + +interface ModalTitleProps { + title?: string; +} + +const ModalTitle = ({ title }: ModalTitleProps): JSX.Element => { + return
{title ?? ""}
; +}; + +export default ModalTitle; diff --git a/client/src/components/commons/Modal/core/ModalWrapper/ModalWrapper.scss b/client/src/components/commons/Modal/core/ModalWrapper/ModalWrapper.scss new file mode 100644 index 00000000..82cb05bb --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalWrapper/ModalWrapper.scss @@ -0,0 +1,19 @@ +@import "@/styles/_theme.scss"; + +.modal-wrapper { + position: absolute; + // 좌측 상단 위치를 modal 중앙에 위치 + top: 50%; + left: 50%; + box-sizing: border-box; + width: fit-content; + min-width: 20rem; + height: fit-content; + min-height: 5rem; + background-color: $weview-white; + border: $border-small $line-color; + border-radius: $radius-modal; + box-shadow: $shadow-box; + // wrapper 의 중앙을 modal 중앙에 위치 + transform: translate(-50%, -50%); +} diff --git a/client/src/components/commons/Modal/core/ModalWrapper/ModalWrapper.tsx b/client/src/components/commons/Modal/core/ModalWrapper/ModalWrapper.tsx new file mode 100644 index 00000000..f527cbfe --- /dev/null +++ b/client/src/components/commons/Modal/core/ModalWrapper/ModalWrapper.tsx @@ -0,0 +1,19 @@ +import React, { forwardRef, ReactNode } from "react"; + +import "./ModalWrapper.scss"; + +interface ModalWrapperProps { + children: ReactNode; +} + +const ModalWrapper = forwardRef( + ({ children }, ref): JSX.Element => { + return ( +
+ {children} +
+ ); + } +); + +export default ModalWrapper; diff --git a/client/src/components/commons/ProgressiveImage/ProgressiveImage.tsx b/client/src/components/commons/ProgressiveImage/ProgressiveImage.tsx index 177faff2..f83b8708 100644 --- a/client/src/components/commons/ProgressiveImage/ProgressiveImage.tsx +++ b/client/src/components/commons/ProgressiveImage/ProgressiveImage.tsx @@ -9,6 +9,7 @@ interface ProgressiveImageProps { width: number | "100%"; height: number | "100%"; alt: string; + handleClickImage?: () => void; } const ProgressiveImage = ({ @@ -18,6 +19,7 @@ const ProgressiveImage = ({ width, height, alt, + handleClickImage, }: ProgressiveImageProps): JSX.Element => { const { observeImage } = useImageIntersect(); @@ -26,6 +28,7 @@ const ProgressiveImage = ({ ref={observeImage} className={`progressive-image ${className}`} src={placeholder} + onClick={handleClickImage} data-lazysrc={src} width={width} height={height} diff --git a/client/src/components/main/CodeEditor/CodeEditor.tsx b/client/src/components/main/CodeEditor/CodeEditor.tsx index eacd5690..3ff5783c 100644 --- a/client/src/components/main/CodeEditor/CodeEditor.tsx +++ b/client/src/components/main/CodeEditor/CodeEditor.tsx @@ -7,9 +7,9 @@ import CodeViewer from "@/components/main/CodeViewer/CodeViewer"; import "./CodeEditor.scss"; const CodeEditor = (): JSX.Element => { - const { code, handleCodeChange, language, lineCount } = useCodeEditor(); const { lineRef, textRef, preRef, handleScrollChange } = useEditorScroll(); - + const { code, language, lineCount, handleCodeChange, handleKeyDown } = + useCodeEditor(textRef); useEffect(() => { textRef.current?.focus(); }, []); @@ -25,6 +25,7 @@ const CodeEditor = (): JSX.Element => { ref={textRef} onScroll={handleScrollChange} onChange={handleCodeChange} + onKeyDown={handleKeyDown} value={code} className="code__textarea" autoComplete="false" diff --git a/client/src/components/main/MainNav/NavContent/SearchContent/SearchContentHeader/SearchContentHeader.tsx b/client/src/components/main/MainNav/NavContent/SearchContent/SearchContentHeader/SearchContentHeader.tsx index ac1895b4..22f572fd 100644 --- a/client/src/components/main/MainNav/NavContent/SearchContent/SearchContentHeader/SearchContentHeader.tsx +++ b/client/src/components/main/MainNav/NavContent/SearchContent/SearchContentHeader/SearchContentHeader.tsx @@ -6,6 +6,7 @@ import ArrowDropDownCircleSharpIcon from "@mui/icons-material/ArrowDropDownCircl import useLabel from "@/hooks/useLabel"; import { Label } from "@/types/search"; import SearchLabel from "@/components/commons/SearchLabel/SearchLabel"; +import useNav from "@/hooks/useNav"; import DetailSearchForm from "./DetailSearchForm/DetailSearchForm"; @@ -20,6 +21,7 @@ const SearchContentHeader = (): JSX.Element => { removeLabel, handleSubmit, } = useLabel(); + const { handleNavClose } = useNav(); const [isDetailOpened, setIsDetailOpened] = useState(false); return ( @@ -36,7 +38,9 @@ const SearchContentHeader = (): JSX.Element => { /> { + handleNavClose(() => handleSubmit()); + }} />
diff --git a/client/src/components/main/MainNav/NavContent/SearchContent/SearchHistoryView/SearchHistoryView.tsx b/client/src/components/main/MainNav/NavContent/SearchContent/SearchHistoryView/SearchHistoryView.tsx index 3a7f5b1c..4d0fb8f6 100644 --- a/client/src/components/main/MainNav/NavContent/SearchContent/SearchHistoryView/SearchHistoryView.tsx +++ b/client/src/components/main/MainNav/NavContent/SearchContent/SearchHistoryView/SearchHistoryView.tsx @@ -60,6 +60,8 @@ const SearchHistoryView = (): JSX.Element => { fetchSearchHistory, { suspense: true, + refetchOnMount: true, // 렌더 시 업데이트 + staleTime: 2 * 1000, // 2초 } ); diff --git a/client/src/components/main/Modal/ModalContainer/ModalContainer.scss b/client/src/components/main/Modal/ModalContainer/ModalContainer.scss index a097a067..6ff2f5e0 100644 --- a/client/src/components/main/Modal/ModalContainer/ModalContainer.scss +++ b/client/src/components/main/Modal/ModalContainer/ModalContainer.scss @@ -1,21 +1,15 @@ @import "@/styles/theme"; +@import "@/styles/responsive"; @import "@/styles/global-style"; @import "@/styles/animation"; .modal-container { - // root 의 가운데 정렬 - position: absolute; - top: 0; - right: 0; - left: 0; - z-index: $modal-content-z-index; + z-index: $modal-content-z-index !important; box-sizing: border-box; - display: flex; - width: 100%; + width: fit-content; max-width: $desktop-main-width; - height: 100%; - + height: 80%; // 모달 내용들은 공통으로 2rem 패딩 내에 존재 padding: 2rem; margin: 0 auto; diff --git a/client/src/components/main/Modal/ModalWrapper/CommonModalWrapper.tsx b/client/src/components/main/Modal/ModalWrapper/CommonModalWrapper.tsx index c7659b4c..01b0bdeb 100644 --- a/client/src/components/main/Modal/ModalWrapper/CommonModalWrapper.tsx +++ b/client/src/components/main/Modal/ModalWrapper/CommonModalWrapper.tsx @@ -1,7 +1,9 @@ import React, { useCallback, MouseEvent } from "react"; +import CloseIcon from "@mui/icons-material/Close"; import ModalContainer from "@/components/main/Modal/ModalContainer/ModalContainer"; import useCommonModalStore from "@/store/useCommonModalStore"; +import { isCloseModalElement } from "@/utils/dom"; import "./ModalWrapper.scss"; @@ -12,7 +14,8 @@ const CommonModalWrapper = (): JSX.Element => { ]); const clickWrapperBackGround = useCallback( - (e: MouseEvent) => { + (e: MouseEvent) => { + if (!isCloseModalElement(e.target as HTMLElement)) return; closeModal(); }, [] @@ -20,8 +23,13 @@ const CommonModalWrapper = (): JSX.Element => { return modalContent !== null ? ( <> -
- +
+ + +
) : ( <> diff --git a/client/src/components/main/Modal/ModalWrapper/ModalWrapper.scss b/client/src/components/main/Modal/ModalWrapper/ModalWrapper.scss index a0025f83..22f2386f 100644 --- a/client/src/components/main/Modal/ModalWrapper/ModalWrapper.scss +++ b/client/src/components/main/Modal/ModalWrapper/ModalWrapper.scss @@ -1,9 +1,26 @@ @import "@/styles/_global-style.scss"; +@import "@/styles/theme.scss"; +@import "@/styles/responsive"; .modal-background { position: absolute; top: 0; z-index: $modal-background-z-index !important; + display: flex; + align-items: center; width: 100vw; height: 100vh; + min-height: 100vh; + background-color: rgba(0, 0, 0, 0.75); +} + +.modal-close-button { + position: absolute; + top: 2rem; + right: 2rem; + z-index: $modal-content-z-index; + font-size: 2rem; + color: $line-color; + cursor: pointer; + transform: scale(2); } diff --git a/client/src/components/main/Modal/ReviewModal/ReviewModal.scss b/client/src/components/main/Modal/ReviewModal/ReviewModal.scss index eeba416a..32278bd1 100644 --- a/client/src/components/main/Modal/ReviewModal/ReviewModal.scss +++ b/client/src/components/main/Modal/ReviewModal/ReviewModal.scss @@ -3,12 +3,11 @@ @import "@/styles/mixin"; $code-line-width: 4rem; -$review-code-width: calc($device-editor-width - $code-line-width); +$review-code-width: calc($device-editor-width - $code-line-width - 8rem); $github-dark-font-color: #c9d1d9; .review-modal { display: flex; - width: 100%; height: 100%; @@ -18,6 +17,7 @@ $github-dark-font-color: #c9d1d9; width: $review-code-width; min-width: $review-code-width; height: 100%; + background-color: $codeblock-color; border-radius: $radius-small; @@ -30,13 +30,24 @@ $github-dark-font-color: #c9d1d9; margin: 0; overflow-x: hidden; overflow-y: auto; - font-size: $font-medium; - white-space: pre-wrap; + + font-size: $font-large; + + &::-webkit-scrollbar { + display: none; + } } &--view { left: $code-line-width; + width: calc(100% - $code-line-width); + overflow-x: scroll; color: $github-dark-font-color; + letter-spacing: 1px; + + & > code { + font-family: D2Coding, "D2 coding", monospace; + } } &--lines { @@ -48,9 +59,9 @@ $github-dark-font-color: #c9d1d9; box-sizing: border-box; display: flex; flex-direction: column; - width: 100%; - max-width: $device-review-max-width; + width: 40rem; + max-width: 50rem; height: 100%; - margin-left: calc($device-review-modal-gap / 2); + margin-left: calc($device-review-modal-gap); } } diff --git a/client/src/components/main/Modal/ReviewModal/ReviewModal.tsx b/client/src/components/main/Modal/ReviewModal/ReviewModal.tsx index d36b2de5..26e09224 100644 --- a/client/src/components/main/Modal/ReviewModal/ReviewModal.tsx +++ b/client/src/components/main/Modal/ReviewModal/ReviewModal.tsx @@ -24,7 +24,7 @@ const ReviewModal = ({
- {Array.from(Array(getLineCount(code) + 1).keys()) + {Array.from(Array(getLineCount(code)).keys()) .slice(1) .join("\n")}
diff --git a/client/src/components/main/Modal/ReviewModal/ReviewScroll/Review/Review.scss b/client/src/components/main/Modal/ReviewModal/ReviewScroll/Review/Review.scss index 3dd72b10..1a22d1a6 100644 --- a/client/src/components/main/Modal/ReviewModal/ReviewScroll/Review/Review.scss +++ b/client/src/components/main/Modal/ReviewModal/ReviewScroll/Review/Review.scss @@ -12,7 +12,7 @@ $review-form-header-height: 4rem; box-sizing: border-box; display: flex; - gap: 1.6rem; + gap: 1rem; width: 100%; height: auto; diff --git a/client/src/components/main/Modal/ReviewModal/ReviewScroll/ReviewScroll.tsx b/client/src/components/main/Modal/ReviewModal/ReviewScroll/ReviewScroll.tsx index 9e1e4d10..8123304a 100644 --- a/client/src/components/main/Modal/ReviewModal/ReviewScroll/ReviewScroll.tsx +++ b/client/src/components/main/Modal/ReviewModal/ReviewScroll/ReviewScroll.tsx @@ -24,7 +24,7 @@ const ReviewScroll = ({ postId }: ReviewScrollProps): JSX.Element => { ); return ( -
+ <>
    {reviewInfos.map((reviewInfo: ReviewInfo) => ( @@ -36,7 +36,7 @@ const ReviewScroll = ({ postId }: ReviewScrollProps): JSX.Element => { />
-
+ ); }; diff --git a/client/src/components/main/Modal/WriteModal/CloseButton/CloseButton.tsx b/client/src/components/main/Modal/WriteModal/CloseButton/CloseButton.tsx index 028eb2e7..8bc15b76 100644 --- a/client/src/components/main/Modal/WriteModal/CloseButton/CloseButton.tsx +++ b/client/src/components/main/Modal/WriteModal/CloseButton/CloseButton.tsx @@ -1,13 +1,12 @@ import React, { useCallback } from "react"; - -import "./CloseButton.scss"; - import CloseIcon from "@mui/icons-material/Close"; -import useModalStore from "@/store/useModalStore"; +import useWritingModalStore from "@/store/useWritingModalStore"; + +import "./CloseButton.scss"; const CloseButton = (): JSX.Element => { - const { closeModal } = useModalStore((state) => ({ + const { closeModal } = useWritingModalStore((state) => ({ closeModal: state.closeWritingModal, })); diff --git a/client/src/components/main/Modal/WriteModal/SnapShotNav/SnapShotNav.scss b/client/src/components/main/Modal/WriteModal/SnapShotNav/SnapShotNav.scss index 3c046953..0ffa3151 100644 --- a/client/src/components/main/Modal/WriteModal/SnapShotNav/SnapShotNav.scss +++ b/client/src/components/main/Modal/WriteModal/SnapShotNav/SnapShotNav.scss @@ -5,7 +5,7 @@ flex-direction: column; flex-grow: 1; align-items: center; - width: auto !important; + width: 35rem !important; height: 100%; overflow-x: hidden; overflow-y: auto; @@ -14,16 +14,16 @@ box-shadow: $shadow-box; &__item { - width: 40rem; - height: 40rem; + width: 30rem; + height: 30rem; margin: 1rem 0; &--img { - width: 40rem; - height: 40rem; + width: 100%; + height: 100%; cursor: pointer; border-radius: $radius-medium; - object-fit: cover; + object-fit: contain; } } @media screen and (max-width: 720px) { diff --git a/client/src/components/main/Modal/WriteModal/SubmitModal/CloseButton/CloseButton.tsx b/client/src/components/main/Modal/WriteModal/SubmitModal/CloseButton/CloseButton.tsx index 9a9c499f..e93bcf3d 100644 --- a/client/src/components/main/Modal/WriteModal/SubmitModal/CloseButton/CloseButton.tsx +++ b/client/src/components/main/Modal/WriteModal/SubmitModal/CloseButton/CloseButton.tsx @@ -1,11 +1,11 @@ import React from "react"; -import "./CloseButton.scss"; +import useWritingModalStore from "@/store/useWritingModalStore"; -import useModalStore from "@/store/useModalStore"; +import "./CloseButton.scss"; const CloseButton = (): JSX.Element => { - const { closeModal } = useModalStore((state) => ({ + const { closeModal } = useWritingModalStore((state) => ({ closeModal: state.closeSubmitModal, })); diff --git a/client/src/components/main/Modal/WriteModal/SubmitModal/RegisterButton/RegisterButton.tsx b/client/src/components/main/Modal/WriteModal/SubmitModal/RegisterButton/RegisterButton.tsx index 79417370..dad5798c 100644 --- a/client/src/components/main/Modal/WriteModal/SubmitModal/RegisterButton/RegisterButton.tsx +++ b/client/src/components/main/Modal/WriteModal/SubmitModal/RegisterButton/RegisterButton.tsx @@ -3,7 +3,7 @@ import React from "react"; import useWritingStore from "@/store/useWritingStore"; import useCodeEditorStore from "@/store/useCodeEditorStore"; import { postWritingsAPI, uploadImage } from "@/apis/post"; -import useModalStore from "@/store/useModalStore"; +import useWritingModalStore from "@/store/useWritingModalStore"; import { isEmpty } from "@/utils/typeCheck"; import { fetchPreSignedData } from "@/apis/auth"; @@ -20,11 +20,13 @@ const RegisterButton = (): JSX.Element => { ]); const images = useCodeEditorStore((state) => state.images); const resetWritingStore = useWritingStore((state) => state.reset); - const { closeWritingModal, closeSubmitModal } = useModalStore((state) => ({ - isOpened: state.isSubmitModalOpened, - closeSubmitModal: state.closeSubmitModal, - closeWritingModal: state.closeWritingModal, - })); + const { closeWritingModal, closeSubmitModal } = useWritingModalStore( + (state) => ({ + isOpened: state.isSubmitModalOpened, + closeSubmitModal: state.closeSubmitModal, + closeWritingModal: state.closeWritingModal, + }) + ); // 제출 불가능 상태 판단 const isInvalidState = (): boolean => diff --git a/client/src/components/main/Modal/WriteModal/SubmitModal/SubmitModal.tsx b/client/src/components/main/Modal/WriteModal/SubmitModal/SubmitModal.tsx index 3a323546..22e95549 100644 --- a/client/src/components/main/Modal/WriteModal/SubmitModal/SubmitModal.tsx +++ b/client/src/components/main/Modal/WriteModal/SubmitModal/SubmitModal.tsx @@ -1,6 +1,6 @@ import React, { useCallback, MouseEvent } from "react"; -import useModalStore from "@/store/useModalStore"; +import useWritingModalStore from "@/store/useWritingModalStore"; import TagInput from "@/components/main/Modal/WriteModal/SubmitModal/TagInput/TagInput"; import TitleInput from "@/components/main/Modal/WriteModal/SubmitModal/TitleInput/TitleInput"; @@ -10,7 +10,7 @@ import RegisterButton from "./RegisterButton/RegisterButton"; import "./SubmitModal.scss"; const SubmitModal = (): JSX.Element => { - const { isOpened, closeModal } = useModalStore((state) => ({ + const { isOpened, closeModal } = useWritingModalStore((state) => ({ isOpened: state.isSubmitModalOpened, closeModal: state.closeSubmitModal, })); diff --git a/client/src/components/main/Modal/WriteModal/WriteModal.scss b/client/src/components/main/Modal/WriteModal/WriteModal.scss index 361fb8b7..0aad7f83 100644 --- a/client/src/components/main/Modal/WriteModal/WriteModal.scss +++ b/client/src/components/main/Modal/WriteModal/WriteModal.scss @@ -6,4 +6,7 @@ gap: 1rem; width: 100%; height: 100%; + @media screen and (max-width: 720px) { + flex-direction: column; + } } diff --git a/client/src/components/main/Modal/WriteModal/WritingForm/SubmitButton/SubmitButton.tsx b/client/src/components/main/Modal/WriteModal/WritingForm/SubmitButton/SubmitButton.tsx index 5387d212..def57cf3 100644 --- a/client/src/components/main/Modal/WriteModal/WritingForm/SubmitButton/SubmitButton.tsx +++ b/client/src/components/main/Modal/WriteModal/WritingForm/SubmitButton/SubmitButton.tsx @@ -1,11 +1,11 @@ import React, { FormEvent, useCallback } from "react"; -import useModalStore from "@/store/useModalStore"; +import useWritingModalStore from "@/store/useWritingModalStore"; import "./SubmitButton.scss"; const SubmitButton = (): JSX.Element => { - const { openSubmitModal } = useModalStore((state) => ({ + const { openSubmitModal } = useWritingModalStore((state) => ({ openSubmitModal: state.openSubmitModal, })); diff --git a/client/src/components/main/Modal/WriteModal/WritingForm/WritingForm.tsx b/client/src/components/main/Modal/WriteModal/WritingForm/WritingForm.tsx index ee465e78..ad0bd379 100644 --- a/client/src/components/main/Modal/WriteModal/WritingForm/WritingForm.tsx +++ b/client/src/components/main/Modal/WriteModal/WritingForm/WritingForm.tsx @@ -1,6 +1,6 @@ import React, { FormEvent, useCallback } from "react"; -import useModalStore from "@/store/useModalStore"; +import useWritingModalStore from "@/store/useWritingModalStore"; import LanguageSelector from "@/components/main/Modal/WriteModal/WritingForm/LanguageSelector/LanguageSelector"; import CodeEditor from "@/components/main/CodeEditor/CodeEditor"; @@ -9,7 +9,7 @@ import SubmitButton from "./SubmitButton/SubmitButton"; import "./WritingForm.scss"; const WritingForm = (): JSX.Element => { - const { openSubmitModal } = useModalStore((state) => ({ + const { openSubmitModal } = useWritingModalStore((state) => ({ openSubmitModal: state.openSubmitModal, })); diff --git a/client/src/components/main/PostScroll/Post/Post.scss b/client/src/components/main/PostScroll/Post/Post.scss index 687670bc..9fba7662 100644 --- a/client/src/components/main/PostScroll/Post/Post.scss +++ b/client/src/components/main/PostScroll/Post/Post.scss @@ -3,11 +3,9 @@ .post { box-sizing: border-box; - // 반응형 레이아웃 - display: grid; - grid-template-rows: 12fr 100fr 28fr 15fr; // 가로:세로 = 1:1.54 비율 기준 - width: 100%; - height: $main-post-height; + display: flex; + flex-direction: column; + width: 100%; // 54rem background-color: $weview-off-white; border: 1px solid $line-color; diff --git a/client/src/components/main/PostScroll/Post/PostBody/PostBody.scss b/client/src/components/main/PostScroll/Post/PostBody/PostBody.scss index 2f5e3b17..35d7337a 100644 --- a/client/src/components/main/PostScroll/Post/PostBody/PostBody.scss +++ b/client/src/components/main/PostScroll/Post/PostBody/PostBody.scss @@ -6,11 +6,9 @@ @include flex-start; box-sizing: border-box; flex-direction: column; - - grid-row: 3; gap: 0.35rem; width: 100%; - height: 100%; + min-height: 14rem; padding: $post-inline-padding; overflow: hidden; background-color: white; @@ -37,6 +35,21 @@ } &--content { + display: flex; + flex-direction: column; + flex-grow: 1; + align-items: flex-start; + width: 100%; + min-height: 8rem; font-size: $font-medium; + + &--more { + color: $placeholder-color-dark; + + &:hover { + font-weight: 700; + cursor: pointer; + } + } } } diff --git a/client/src/components/main/PostScroll/Post/PostBody/PostBody.tsx b/client/src/components/main/PostScroll/Post/PostBody/PostBody.tsx index e5f1687c..94ae0501 100644 --- a/client/src/components/main/PostScroll/Post/PostBody/PostBody.tsx +++ b/client/src/components/main/PostScroll/Post/PostBody/PostBody.tsx @@ -1,10 +1,46 @@ -import React, { useContext } from "react"; +import React, { MouseEventHandler, useContext, useState } from "react"; import { PostContext } from "@/components/main/PostScroll/Post/Post"; import TimeStamp from "@/components/commons/TimeStamp/TimeStamp"; +import { + MAXIMUM_CONTENT_ENTER, + MAXIMUM_CONTENT_LENGTH, +} from "@/constants/code"; import "./PostBody.scss"; +interface PostContentProps { + content: string; +} + +const PostContent = ({ content }: PostContentProps): JSX.Element => { + const contentSlice = content.slice(0, MAXIMUM_CONTENT_LENGTH); + const contentSliceEnterCount = contentSlice.split("\n").length - 1; + + const [isOpened, setIsOpened] = useState( + contentSlice.length < MAXIMUM_CONTENT_LENGTH && // 더보기로 나누지 않을 만큼 내용이 충분히 짧음 + contentSliceEnterCount < MAXIMUM_CONTENT_ENTER // 더보기로 나누지 않을 만큼 개행이 적음 + ); + + const handleOpenContent: MouseEventHandler = () => { + if (isOpened) { + return; + } + setIsOpened(true); + }; + + return ( +
+ {isOpened ? content : content.slice(0, MAXIMUM_CONTENT_LENGTH)} + {!isOpened && ( +

+ 더보기.. +

+ )} +
+ ); +}; + const PostBody = (): JSX.Element => { const { title, content, updatedAt } = useContext(PostContext); @@ -17,7 +53,7 @@ const PostBody = (): JSX.Element => { className={"post__body__title--time-stamp"} />
-
{content}
+
); }; diff --git a/client/src/components/main/PostScroll/Post/PostFooter/PostFooter.scss b/client/src/components/main/PostScroll/Post/PostFooter/PostFooter.scss index 41501323..eddcab87 100644 --- a/client/src/components/main/PostScroll/Post/PostFooter/PostFooter.scss +++ b/client/src/components/main/PostScroll/Post/PostFooter/PostFooter.scss @@ -2,11 +2,11 @@ $post-inline-padding: 0.75rem; // TODO 전역 분리 box-sizing: border-box; display: flex; - grid-row: 4; + flex-shrink: 0; align-items: center; justify-content: space-between; width: 100%; - height: 100%; + height: 7.5rem; padding-inline: $post-inline-padding $post-inline-padding; overflow: hidden; diff --git a/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/MoreButton.tsx b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/MoreButton.tsx index 88a323c2..048bf14f 100644 --- a/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/MoreButton.tsx +++ b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/MoreButton.tsx @@ -1,20 +1,31 @@ -import React, { MouseEventHandler } from "react"; +import React, { useContext } from "react"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import SvgIconButton from "@/components/commons/SvgIconButton/SvgIconButton"; +import useModal from "@/hooks/useModal"; +import { PostContext } from "@/components/main/PostScroll/Post/Post"; +import { MODAL_KEY } from "@/types/modal"; import "./RightBlockItems.scss"; const MoreButton = (): JSX.Element => { - const handleOpenMore: MouseEventHandler = () => {}; + const { id, author } = useContext(PostContext); + const { handleModalOpen } = useModal(); return ( - + <> + + handleModalOpen(MODAL_KEY.POST_MORE, { + postId: id, + authorId: author.id, + }) + } + className="post__footer__right-block--btn" + /> + ); }; diff --git a/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModal.scss b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModal.scss new file mode 100644 index 00000000..8921a374 --- /dev/null +++ b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModal.scss @@ -0,0 +1,13 @@ +@import "@/styles/_theme"; + +.post-more { + width: 25rem; + height: fit-content; + + &__menu { + &:last-of-type { + border-bottom-right-radius: $radius-modal; + border-bottom-left-radius: $radius-modal; + } + } +} diff --git a/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModal.tsx b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModal.tsx new file mode 100644 index 00000000..d2dde6c2 --- /dev/null +++ b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModal.tsx @@ -0,0 +1,50 @@ +import React from "react"; + +import Modal from "@/components/commons/Modal/core/Modal/Modal"; +import useModal from "@/hooks/useModal"; +import { ModalProps } from "@/types/modal"; +import PostMoreModalMenu from "@/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModalMenu/PostMoreModalMenu"; + +import "./PostMoreModal.scss"; + +import { deletePost } from "@/apis/post"; +import { queryClient } from "@/react-query/queryClient"; +import { QUERY_KEYS } from "@/react-query/queryKeys"; +import useAuth from "@/hooks/useAuth"; + +interface PostMoreModalProps extends ModalProps {} + +const PostMoreModal = ({ + postId, + authorId, +}: PostMoreModalProps): JSX.Element => { + const { isLoggedIn, myInfo } = useAuth(); + const { handleModalClose } = useModal(); + + const isDeletable = isLoggedIn && myInfo?.id === authorId; + + const handleDeletePost = (): void => { + void (async () => { + try { + await deletePost(postId as string); + await queryClient.invalidateQueries([QUERY_KEYS.POSTS]); + handleModalClose(); + } catch (e: any) { + alert(e.message); + } + })(); + }; + + return ( + +
+ {isDeletable && ( + + )} + +
+
+ ); +}; + +export default PostMoreModal; diff --git a/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModalMenu/PostMoreModalMenu.scss b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModalMenu/PostMoreModalMenu.scss new file mode 100644 index 00000000..9de8e3c0 --- /dev/null +++ b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModalMenu/PostMoreModalMenu.scss @@ -0,0 +1,16 @@ +@import "@/styles/_theme"; + +.post-more__menu { + box-sizing: border-box; + width: 100%; + height: 5rem; + padding: 0 1rem; + font-size: $font-large; + line-height: 5rem; + text-align: center; + + &:hover { + cursor: pointer; + background-color: $light-gray; + } +} diff --git a/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModalMenu/PostMoreModalMenu.tsx b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModalMenu/PostMoreModalMenu.tsx new file mode 100644 index 00000000..702de53b --- /dev/null +++ b/client/src/components/main/PostScroll/Post/PostFooter/RightBlockItems/PostMoreModal/PostMoreModalMenu/PostMoreModalMenu.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import "./PostMoreModalMenu.scss"; + +interface PostMoreModalMenuProps { + text: string; + onClick?: (e?: Event) => void; +} + +const PostMoreModalMenu = ({ + text, + onClick, +}: PostMoreModalMenuProps): JSX.Element => { + return ( +
onClick?.()}> + {text} +
+ ); +}; + +export default PostMoreModalMenu; diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss index c3980fed..82ea6e17 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss @@ -1,13 +1,14 @@ .post__image-slider { - grid-row: 2; + flex-shrink: 0; width: 100%; - height: 100%; + height: 51rem; overflow: hidden; background-color: black; &--image { width: inherit; height: inherit; + cursor: pointer; object-fit: cover; // 가로-세로 비율 맞춰서 꽉 차게 } -} \ No newline at end of file +} diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx index b0f1347a..8ea621de 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx @@ -3,11 +3,18 @@ import React, { useContext } from "react"; import { PostContext } from "@/components/main/PostScroll/Post/Post"; import ProgressiveImage from "@/components/commons/ProgressiveImage/ProgressiveImage"; import codePlaceholder from "@/assets/progressive-image.jpg"; +import useCommonModalStore from "@/store/useCommonModalStore"; +import ReviewModal from "@/components/main/Modal/ReviewModal/ReviewModal"; import "./PostImageSlider.scss"; const PostImageSlider = (): JSX.Element => { - const { images } = useContext(PostContext); + const { images, id: postId, code, language } = useContext(PostContext); + const [openModal] = useCommonModalStore((state) => [state.openModal]); + + const handleClickImage = (): void => { + openModal(); + }; return (
@@ -18,6 +25,7 @@ const PostImageSlider = (): JSX.Element => { width="100%" height="100%" alt="이미지 설명" + handleClickImage={handleClickImage} />
); diff --git a/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.scss b/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.scss index dfd5f6fd..844403cc 100644 --- a/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.scss +++ b/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.scss @@ -12,33 +12,19 @@ height: 80%; border-radius: 50%; object-fit: contain; + + &:hover { + cursor: pointer; + } } &__username { font-size: $font-medium; - } - - &__follow-button { - @include green-button; - width: fit-content; - height: 1.4rem; - - font-size: $font-form; - - &--on { - @include weview-button; - width: fit-content; - height: 1.5rem; - - font-size: $font-form; - - color: $line-color !important; - border: 1px solid $line-color; - &:hover { - color: $weview-off-white !important; - background-color: $line-color; - } + &:hover { + text-decoration: underline; + cursor: pointer; + background-color: $light-gray; } } } diff --git a/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.tsx b/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.tsx index bcb73a4a..8feee9a3 100644 --- a/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.tsx +++ b/client/src/components/main/PostScroll/Post/PostTitle/AuthorProfile/AuthorProfile.tsx @@ -1,19 +1,27 @@ import React, { useContext } from "react"; import { PostContext } from "@/components/main/PostScroll/Post/Post"; +import useSearchStore from "@/store/useSearchStore"; import "./AuthorProfile.scss"; const AuthorProfile = (): JSX.Element => { const { author } = useContext(PostContext); + const searchAuthorFilter = useSearchStore( + (state) => state.searchAuthorFilter + ); return (
searchAuthorFilter({ userId: author.id })} /> -
+
searchAuthorFilter({ userId: author.id })} + > {author.nickname}
diff --git a/client/src/components/main/PostScroll/Post/PostTitle/PostTitle.scss b/client/src/components/main/PostScroll/Post/PostTitle/PostTitle.scss index 73e1115a..2d538d15 100644 --- a/client/src/components/main/PostScroll/Post/PostTitle/PostTitle.scss +++ b/client/src/components/main/PostScroll/Post/PostTitle/PostTitle.scss @@ -3,11 +3,11 @@ .post__title { box-sizing: border-box; display: flex; - grid-row: 1; + flex-shrink: 0; align-items: center; justify-content: space-between; width: 100%; - height: 100%; + height: 6rem; padding-inline: 1.2rem; overflow: hidden; diff --git a/client/src/components/main/TagRanking/TagRankingItem/TagRankingItem.tsx b/client/src/components/main/TagRanking/TagRankingItem/TagRankingItem.tsx index 2ad474bc..6f278835 100644 --- a/client/src/components/main/TagRanking/TagRankingItem/TagRankingItem.tsx +++ b/client/src/components/main/TagRanking/TagRankingItem/TagRankingItem.tsx @@ -1,4 +1,5 @@ import React, { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; @@ -19,8 +20,10 @@ const TagRankingItem = ({ tagInfo }: PopularTagBoxProps): JSX.Element => { const [searchDefaultFilter] = useSearchStore((state) => [ state.searchDefaultFilter, ]); + const navigate = useNavigate(); const handleItemClick = useCallback((): void => { searchDefaultFilter({ tags: [tagInfo.name], lastId: "-1" }); + navigate("/"); }, []); return ( diff --git a/client/src/constants/code.ts b/client/src/constants/code.ts index 97966644..dd9ce8cc 100644 --- a/client/src/constants/code.ts +++ b/client/src/constants/code.ts @@ -1 +1,4 @@ export const ONE_SNAPSHOT_LINE_COUNT = 15; + +export const MAXIMUM_CONTENT_LENGTH = 200; +export const MAXIMUM_CONTENT_ENTER = 2; diff --git a/client/src/constants/options.ts b/client/src/constants/options.ts index 43f6a1b1..d882e432 100644 --- a/client/src/constants/options.ts +++ b/client/src/constants/options.ts @@ -19,13 +19,12 @@ export const LANGUAGES = [ export const DEFAULT_LANGUAGE = "JavaScript"; export const IMAGE_OPTIONS = { - width: 400, - height: 400, + width: 600, + height: 600, bgcolor: "#292c33", style: { display: "flex", - justifyContent: "center", alignItems: "center", - paddingInline: "15px", + paddingInline: "16px", }, }; diff --git a/client/src/hooks/useBookmark.ts b/client/src/hooks/useBookmark.ts index bd3ddecb..01e1be43 100644 --- a/client/src/hooks/useBookmark.ts +++ b/client/src/hooks/useBookmark.ts @@ -33,7 +33,9 @@ const useBookmark = ({ { onMutate: async () => { await queryClient.cancelQueries([QUERY_KEYS.POSTS]); - const previousPosts = queryClient.getQueryData([QUERY_KEYS.POSTS]); + const previousPosts = queryClient.getQueriesData([ + QUERY_KEYS.POSTS, + ])[0][1]; setIsBookmarkedState(!isBookmarkedState); return { previousPosts }; }, diff --git a/client/src/hooks/useCodeEditor.ts b/client/src/hooks/useCodeEditor.ts index cf4dec92..51401e58 100644 --- a/client/src/hooks/useCodeEditor.ts +++ b/client/src/hooks/useCodeEditor.ts @@ -1,4 +1,11 @@ -import { ChangeEvent, useCallback, useEffect, useState } from "react"; +import { + ChangeEvent, + KeyboardEvent, + useCallback, + useEffect, + useState, + RefObject, +} from "react"; import domtoimage from "dom-to-image"; import { ONE_SNAPSHOT_LINE_COUNT } from "@/constants/code"; @@ -10,10 +17,13 @@ interface UseCodeEditor { code: string; language: string; handleCodeChange: (e: ChangeEvent) => void; + handleKeyDown: (e: KeyboardEvent) => void; lineCount: number; } -const useCodeEditor = (): UseCodeEditor => { +const useCodeEditor = ( + textAreaRef: RefObject +): UseCodeEditor => { const { code, setCode, language, images, setImages, removeImage } = useCodeEditorStore((state) => ({ code: state.code, @@ -25,6 +35,22 @@ const useCodeEditor = (): UseCodeEditor => { })); const [lineCount, setLineCount] = useState(0); + const handleKeyDown = (e: KeyboardEvent): void => { + if (e.key === "Tab") { + if (textAreaRef.current !== null) { + e.preventDefault(); + const start = textAreaRef.current.selectionStart; + const end = textAreaRef.current.selectionEnd; + const value = code.substring(0, start) + " " + code.substring(end); + textAreaRef.current.value = value; + textAreaRef.current.selectionStart = textAreaRef.current.selectionEnd = + end + 2 - (end - start); + /* 커서가 뒤로 먼저 가지 않기 위해 value 먼저 변경하고 setCode 실행합니다. */ + setCode(value); + } + } + }; + const handleCodeChange = useCallback( (e: ChangeEvent) => { setCode(e.target.value); @@ -86,6 +112,7 @@ const useCodeEditor = (): UseCodeEditor => { handleCodeChange, language, lineCount, + handleKeyDown, }; }; diff --git a/client/src/hooks/useLabel.ts b/client/src/hooks/useLabel.ts index fd9d01ec..392fc763 100644 --- a/client/src/hooks/useLabel.ts +++ b/client/src/hooks/useLabel.ts @@ -1,10 +1,5 @@ -import { - ChangeEvent, - KeyboardEvent, - useCallback, - useEffect, - useState, -} from "react"; +import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { Label } from "@/types/search"; import useSearchStore from "@/store/useSearchStore"; @@ -28,6 +23,7 @@ const useLabel = (): UseLabelResult => { const [searchDefaultFilter] = useSearchStore((state) => [ state.searchDefaultFilter, ]); + const navigate = useNavigate(); const [word, setWord] = useState(""); // 입력중인 검색어 const [labels, setLabels] = useLabelStore((state) => [ state.labels, @@ -80,6 +76,7 @@ const useLabel = (): UseLabelResult => { // PostScroll 에 현재 검색 필터를 적용 const handleSubmit = (): void => { + navigate("/"); searchDefaultFilter(createSearchFilter(labels)); }; diff --git a/client/src/hooks/useModal.ts b/client/src/hooks/useModal.ts new file mode 100644 index 00000000..0096a313 --- /dev/null +++ b/client/src/hooks/useModal.ts @@ -0,0 +1,26 @@ +import useModalStore from "@/store/useModalStore"; +import { MODAL_KEY, ModalProps } from "@/types/modal"; + +interface UseModalResult { + handleModalOpen: (key: MODAL_KEY, props: ModalProps) => void; + handleModalClose: () => void; +} + +const useModal = (): UseModalResult => { + const [openModal, closeModal] = useModalStore((state) => [ + state.openModal, + state.closeModal, + ]); + + const handleModalOpen = (key: MODAL_KEY, props: ModalProps): void => { + openModal(key, props); + }; + + const handleModalClose = (): void => { + closeModal(); + }; + + return { handleModalOpen, handleModalClose }; +}; + +export default useModal; diff --git a/client/src/hooks/useOutsideClickHandler.ts b/client/src/hooks/useOutsideClickHandler.ts new file mode 100644 index 00000000..5c871145 --- /dev/null +++ b/client/src/hooks/useOutsideClickHandler.ts @@ -0,0 +1,24 @@ +import { RefObject, useEffect } from "react"; + +/** + * ref 외부의 요소를 클릭했을 경우 실행할 콜백 함수를 등록합니다. + */ +const useOutsideClickHandler = ( + ref: RefObject, + callback?: (event?: Event) => void +): void => { + useEffect(() => { + const handleClickOutside = (e: Event): void => { + if (ref.current === null || ref.current.contains(e.target as Node)) { + return; + } + callback?.(e); // 모달 외부 요소 클릭 시 실행 + }; + window.addEventListener("mousedown", handleClickOutside); + return () => { + window.removeEventListener("mousedown", handleClickOutside); + }; + }, [ref, callback]); +}; + +export default useOutsideClickHandler; diff --git a/client/src/hooks/usePostInfiniteScroll.ts b/client/src/hooks/usePostInfiniteScroll.ts index 5e1c2a42..b888102e 100644 --- a/client/src/hooks/usePostInfiniteScroll.ts +++ b/client/src/hooks/usePostInfiniteScroll.ts @@ -71,10 +71,6 @@ const usePostInfiniteScroll = (): PostInfiniteScrollResults => { { queryKey: [QUERY_KEYS.POSTS], exact: true }, { cancelRefetch: true } ); - await queryClient.refetchQueries({ - queryKey: [QUERY_KEYS.HISTORY], - type: "active", - }); })(); }, [filter, searchType]); diff --git a/client/src/hooks/usePostLike.ts b/client/src/hooks/usePostLike.ts index 669fb826..8e9a086d 100644 --- a/client/src/hooks/usePostLike.ts +++ b/client/src/hooks/usePostLike.ts @@ -28,7 +28,13 @@ const usePostLike = ({ { onMutate: async () => { await queryClient.cancelQueries([QUERY_KEYS.POSTS]); - const previousPosts = queryClient.getQueryData([QUERY_KEYS.POSTS]); + /* + * https://tanstack.com/query/v4/docs/reference/QueryClient#queryclientgetquerydata + * only one QUERY_KEYS.POSTS data exists, it's key [0], value [1] + */ + const previousPosts = queryClient.getQueriesData([ + QUERY_KEYS.POSTS, + ])[0][1]; setIsLikedState(!isLikedState); return { previousPosts }; }, diff --git a/client/src/hooks/useViewerScroll.ts b/client/src/hooks/useViewerScroll.ts index 3e47a3f4..f4c2caf1 100644 --- a/client/src/hooks/useViewerScroll.ts +++ b/client/src/hooks/useViewerScroll.ts @@ -12,6 +12,7 @@ const useViewerScroll = (): UseEditorScroll => { const handleScrollChange = useCallback((): void => { if (lineRef.current !== null && preRef.current !== null) { lineRef.current.scrollTop = preRef.current.scrollTop; + preRef.current.scrollTop = lineRef.current.scrollTop; } }, [preRef, lineRef]); return { preRef, lineRef, handleScrollChange }; diff --git a/client/src/mocks/datasource/mockConstants.ts b/client/src/mocks/datasource/mockConstants.ts new file mode 100644 index 00000000..02dd3d80 --- /dev/null +++ b/client/src/mocks/datasource/mockConstants.ts @@ -0,0 +1,79 @@ +export const LARGE_CONTENT = `애국가: +동해물과 백두산이 마르고 닳도록 +하느님이 보우하사 우리나라 만세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +남산위에 저 소나무 철갑을 두른듯 +바람서리 불변함은 우리 기상일세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +Hey~ 한국! Hey~ 한국! +oh~ oh~ oh~ oh! 한국! + +대~한민국! 짝짝!짝!짝!짝! +대~한민국! 짝짝!짝!짝!짝! + +가을하늘 공활한데 높고 구름없이 +밝은 달은 우리가슴 일편단심일세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +이 기상과 이 맘으로 충성을 다하여 +괴로우나 즐거우나 나라 사랑하세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +동해물과 백!두!산!이! +동해물과 백!두!산!이! +동해물과 백!두!산!이! + +동해물과 동해물과 동해물과 백!두!산!이! + +-애국가 - 윤도현 밴드 (MR 반주곡) + +애국가: + 동해물과 백두산이 마르고 닳도록 +하느님이 보우하사 우리나라 만세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +남산위에 저 소나무 철갑을 두른듯 +바람서리 불변함은 우리 기상일세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +Hey~ 한국! Hey~ 한국! +oh~ oh~ oh~ oh! 한국! + +대~한민국! 짝짝!짝!짝!짝! +대~한민국! 짝짝!짝!짝!짝! + +가을하늘 공활한데 높고 구름없이 +밝은 달은 우리가슴 일편단심일세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +이 기상과 이 맘으로 충성을 다하여 +괴로우나 즐거우나 나라 사랑하세 + +무궁화 삼천리 화려강산 +대한사람 대한으로 길이 보전하세 + +동해물과 백!두!산!이! +동해물과 백!두!산!이! +동해물과 백!두!산!이! + +동해물과 동해물과 동해물과 백!두!산!이! + +-애국가 - 윤도현 밴드 (MR 반주곡)`; + +export const SMALL_CONTENT = "코드리뷰 부탁드립니다."; diff --git a/client/src/mocks/datasource/mockDataSource.ts b/client/src/mocks/datasource/mockDataSource.ts index 02dc8113..639b06a4 100644 --- a/client/src/mocks/datasource/mockDataSource.ts +++ b/client/src/mocks/datasource/mockDataSource.ts @@ -2,6 +2,7 @@ import { PostInfo } from "@/types/post"; import { SearchHistory } from "@/types/search"; import { ReviewInfo } from "@/types/review"; import { MyInfo } from "@/types/auth"; +import { LARGE_CONTENT, SMALL_CONTENT } from "@/mocks/datasource/mockConstants"; export const mockUser: MyInfo = { id: "1", @@ -12,10 +13,11 @@ export const mockUser: MyInfo = { profileUrl: "http://placeimg.com/640/640/animals", }; -export const posts: PostInfo[] = Array.from(Array(1024).keys()).map((id) => ({ +export let posts: PostInfo[]; +posts = Array.from(Array(1024).keys()).map((id) => ({ id: `${id}`, title: `제목_${id}`, - content: `내용_${id}`, + content: id % 2 === 0 ? LARGE_CONTENT : SMALL_CONTENT, images: [ { src: "http://placeimg.com/640/640/animals", @@ -27,7 +29,7 @@ export const posts: PostInfo[] = Array.from(Array(1024).keys()).map((id) => ({ }, ], author: { - id: `${id}`, + id: `${id % 100}`, nickname: `sampleUser_${id + 10000}`, profileUrl: "http://placeimg.com/640/640/animals", email: `name_${id + 10000}@gmail.com`, @@ -37,11 +39,15 @@ export const posts: PostInfo[] = Array.from(Array(1024).keys()).map((id) => ({ likeCount: id % 10, lineCount: 1, updatedAt: "2022-11-16 12:26:56.124939", - code: `sourcecode: ~~~~~~~~~~~~~~~~~~`, + code: `sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~sourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\nsourcecode: ~~~~~~~~~~~~~~~~~~\n`, language: `javascript`, isLiked: false, })); +export const setPosts = (newPosts: PostInfo[]): void => { + posts = newPosts; +}; + export const reviews: ReviewInfo[] = Array.from(Array(1024).keys()).map( (id) => ({ id: String(id), diff --git a/client/src/mocks/handlers/postHandler.ts b/client/src/mocks/handlers/postHandler.ts index f557193a..1d3bebca 100644 --- a/client/src/mocks/handlers/postHandler.ts +++ b/client/src/mocks/handlers/postHandler.ts @@ -2,7 +2,12 @@ import { rest } from "msw"; import { API_SERVER_URL } from "@/constants/env"; import { parsePostQueryString } from "@/mocks/utils/mockUtils"; -import { posts, history, bookmarks } from "@/mocks/datasource/mockDataSource"; +import { + posts, + history, + bookmarks, + setPosts, +} from "@/mocks/datasource/mockDataSource"; // Backend API Server URL const baseUrl = API_SERVER_URL; @@ -69,6 +74,11 @@ export const postHandler = [ ctx.json({ message: "글 작성에 성공했습니다." }) ); }), + rest.delete(`${baseUrl}/posts/:postId`, (req, res, ctx) => { + const postId = String(req.params.postId); + setPosts(posts.filter((postInfo) => postInfo.id !== postId)); + return res(ctx.status(200)); + }), rest.post(`${baseUrl}/posts/:postId/likes`, (req, res, ctx) => { const postId = req.params.postId; posts diff --git a/client/src/mocks/handlers/searchHandler.ts b/client/src/mocks/handlers/searchHandler.ts index b3552e02..52bdde9c 100644 --- a/client/src/mocks/handlers/searchHandler.ts +++ b/client/src/mocks/handlers/searchHandler.ts @@ -8,7 +8,7 @@ const baseUrl = API_SERVER_URL; export const searchHandler = [ rest.get(`${baseUrl}/search/histories`, (req, res, ctx) => { - return res(ctx.status(200), ctx.delay(1000), ctx.json(history)); + return res(ctx.status(200), ctx.delay(500), ctx.json(history)); }), rest.delete(`${baseUrl}/search/histories/:id`, (req, res, ctx) => { setHistory( diff --git a/client/src/pages/Main/Main.tsx b/client/src/pages/Main/Main.tsx index 17f053d6..99feadec 100644 --- a/client/src/pages/Main/Main.tsx +++ b/client/src/pages/Main/Main.tsx @@ -2,13 +2,13 @@ import React from "react"; import MainNav from "@/components/main/MainNav/MainNav"; import PostScroll from "@/components/main/PostScroll/PostScroll"; -import useModalStore from "@/store/useModalStore"; +import useWritingModalStore from "@/store/useWritingModalStore"; import TagRanking from "@/components/main/TagRanking/TagRanking"; import "./Main.scss"; const Main = (): JSX.Element => { - const { isWritingModalOpened } = useModalStore((state) => ({ + const { isWritingModalOpened } = useWritingModalStore((state) => ({ isWritingModalOpened: state.isWritingModalOpened, })); diff --git a/client/src/pages/Post/Post.scss b/client/src/pages/Post/Post.scss index 80480bf7..66032d63 100644 --- a/client/src/pages/Post/Post.scss +++ b/client/src/pages/Post/Post.scss @@ -10,6 +10,10 @@ align-items: center; width: $main-post-bar-width; height: 100%; - margin-top: 4rem; + overflow-y: scroll; background-color: #fcfcfc; + + &::-webkit-scrollbar { + display: none; + } } diff --git a/client/src/pages/Post/Post.tsx b/client/src/pages/Post/Post.tsx index e1c21acc..b439762d 100644 --- a/client/src/pages/Post/Post.tsx +++ b/client/src/pages/Post/Post.tsx @@ -9,17 +9,17 @@ import PostItem from "@/components/main/PostScroll/Post/Post"; import TagRanking from "@/components/main/TagRanking/TagRanking"; import { PostInfo } from "@/types/post"; import LoadingSpinner from "@/components/commons/LoadingSpinner/LoadingSpinner"; -import useModalStore from "@/store/useModalStore"; +import useWritingModalStore from "@/store/useWritingModalStore"; import "./Post.scss"; const Post = (): JSX.Element => { - const [isWritingModalOpened] = useModalStore((state) => [ + const [isWritingModalOpened] = useWritingModalStore((state) => [ state.isWritingModalOpened, ]); const { postId } = useParams(); - const { isFetching, data } = useQuery( - [QUERY_KEYS.POSTS, postId], + const { isLoading, data } = useQuery( + [QUERY_KEYS.POSTS], async () => await getPostItem(postId as string) ); @@ -28,7 +28,7 @@ const Post = (): JSX.Element => {
- {isFetching ? ( + {isLoading ? ( ) : ( diff --git a/client/src/store/useModalStore.ts b/client/src/store/useModalStore.ts index 17b9cddb..b6d704a4 100644 --- a/client/src/store/useModalStore.ts +++ b/client/src/store/useModalStore.ts @@ -1,56 +1,39 @@ import create from "zustand"; import { devtools } from "zustand/middleware"; -interface ModalStates { - isWritingModalOpened: boolean; - isSubmitModalOpened: boolean; - isSearchModalOpened: boolean; -} - -interface ModalStore extends ModalStates, ModalActions {} - -interface ModalActions { - openWritingModal: () => void; - closeWritingModal: () => void; - - openSubmitModal: () => void; - closeSubmitModal: () => void; +import { MODAL_KEY, ModalContext, ModalProps } from "@/types/modal"; - openSearchModal: () => void; - closeSearchModal: () => void; - - closeRecentOpenedModal: (() => void) | null; - resetRecentOpenedModal: () => void; +interface ModalState { + modals: ModalContext[]; } -const useModalStore = create()( - devtools((set) => ({ - isWritingModalOpened: false, - openWritingModal: () => - set((state: ModalStore) => ({ - isWritingModalOpened: true, - closeRecentOpenedModal: state.closeWritingModal, - })), - closeWritingModal: () => set(() => ({ isWritingModalOpened: false })), - - isSubmitModalOpened: false, - openSubmitModal: () => set(() => ({ isSubmitModalOpened: true })), - closeSubmitModal: () => set(() => ({ isSubmitModalOpened: false })), - - isSearchModalOpened: false, - openSearchModal: () => - set((state: ModalStore) => ({ - isSearchModalOpened: true, - closeRecentOpenedModal: state.closeSearchModal, - })), - closeSearchModal: () => set(() => ({ isSearchModalOpened: false })), +interface ModalAction { + openModal: (key: MODAL_KEY, props: ModalProps) => void; + closeModal: () => void; +} - closeRecentOpenedModal: null, - resetRecentOpenedModal: () => - set(() => ({ - closeRecentOpenedModal: null, - })), - })) +const initialState: ModalState = { + modals: [], +}; + +const useModalStore = create()( + devtools( + (set) => ({ + ...initialState, + openModal: (key, props) => { + set((state) => ({ + modals: [...state.modals, { key, props }], + })); + }, + closeModal: () => + set((state) => ({ + modals: state.modals.slice(0, -1), + })), + }), + { + name: "modal-store", + } + ) ); export default useModalStore; diff --git a/client/src/store/useWritingModalStore.ts b/client/src/store/useWritingModalStore.ts new file mode 100644 index 00000000..37942364 --- /dev/null +++ b/client/src/store/useWritingModalStore.ts @@ -0,0 +1,44 @@ +import create from "zustand"; +import { devtools } from "zustand/middleware"; + +interface WritingModalStates { + isWritingModalOpened: boolean; + isSubmitModalOpened: boolean; +} + +interface WritingModalStore extends WritingModalStates, WritingModalActions {} + +interface WritingModalActions { + openWritingModal: () => void; + closeWritingModal: () => void; + + openSubmitModal: () => void; + closeSubmitModal: () => void; + + closeRecentOpenedModal: (() => void) | null; + resetRecentOpenedModal: () => void; +} + +const useWritingModalStore = create()( + devtools((set) => ({ + isWritingModalOpened: false, + openWritingModal: () => + set((state: WritingModalStore) => ({ + isWritingModalOpened: true, + closeRecentOpenedModal: state.closeWritingModal, + })), + closeWritingModal: () => set(() => ({ isWritingModalOpened: false })), + + isSubmitModalOpened: false, + openSubmitModal: () => set(() => ({ isSubmitModalOpened: true })), + closeSubmitModal: () => set(() => ({ isSubmitModalOpened: false })), + + closeRecentOpenedModal: null, + resetRecentOpenedModal: () => + set(() => ({ + closeRecentOpenedModal: null, + })), + })) +); + +export default useWritingModalStore; diff --git a/client/src/styles/_global-style.scss b/client/src/styles/_global-style.scss index 4d050cf2..4e979b4b 100644 --- a/client/src/styles/_global-style.scss +++ b/client/src/styles/_global-style.scss @@ -33,14 +33,13 @@ $desktop-main-width: $main-nav-width + $main-post-bar-width + $main-gap + */ $default-z-index: 0; -$modal-background-z-index: 1; -$modal-content-z-index: 2; -$modal-upper-z-index: 3; +$modal-background-z-index: 20000; +$modal-content-z-index: 20001; +$modal-upper-z-index: 20002; /** * editor */ - $editing-form-width: 72rem; $device-editor-width: 64rem; // device 최대 너비가 69.5rem - 6:4정도 유지를 위해 40rem 선정 $line-width: 4rem; diff --git a/client/src/styles/_mixin.scss b/client/src/styles/_mixin.scss index e3fdb97d..c282fa56 100644 --- a/client/src/styles/_mixin.scss +++ b/client/src/styles/_mixin.scss @@ -22,13 +22,16 @@ width: $line-width; height: 100%; overflow-x: hidden; - overflow-y: auto; + overflow-y: hidden; font-family: D2Coding, "D2 coding", monospace; color: $weview-white; text-align: end; white-space: pre-wrap; background-color: $codeblock-color; border-right: 1px solid $line-color; + &::-webkit-scrollbar{ + display: none; + } } @mixin flex-middle-start { diff --git a/client/src/types/modal.ts b/client/src/types/modal.ts new file mode 100644 index 00000000..4a3f7f8c --- /dev/null +++ b/client/src/types/modal.ts @@ -0,0 +1,13 @@ +export enum MODAL_KEY { + POST_MORE, +} + +export interface ModalProps { + postId?: string; + authorId?: string; +} + +export interface ModalContext { + key: MODAL_KEY; + props: ModalProps; +} diff --git a/client/src/utils/code.ts b/client/src/utils/code.ts index 7fb6e70b..bef76e1c 100644 --- a/client/src/utils/code.ts +++ b/client/src/utils/code.ts @@ -21,8 +21,10 @@ export const chunkHTML = (htmlData: string[]): string[][] => { export const wrapHTML = (chunkedHTML: string[][]): string => chunkedHTML - .map( - (arr, idx) => - `
${arr.join("\n")}
` - ) + .map((arr, idx) => { + return `
${arr + // 배열의 마지막 원소가 빈 문자열일 때 join하면 개행이 사라지므로 이를 확인하여 추가 + .map((item, idx2) => (item === "" && idx2 === 14 ? "
" : item)) + .join("\n")}
`; + }) .join(""); diff --git a/client/src/utils/dom.ts b/client/src/utils/dom.ts new file mode 100644 index 00000000..1fb85075 --- /dev/null +++ b/client/src/utils/dom.ts @@ -0,0 +1,4 @@ +export const isCloseModalElement = (element: HTMLElement): boolean => + element.matches(".modal-background") || + element.matches(".modal-close-button") || + element.matches("path"); diff --git a/server/src/domain/bookmark/bookmark.module.ts b/server/src/domain/bookmark/bookmark.module.ts index 40db779e..7df20136 100644 --- a/server/src/domain/bookmark/bookmark.module.ts +++ b/server/src/domain/bookmark/bookmark.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { LikesRepository } from '../likes/likes.repository'; import { PostRepository } from '../post/post.repository'; import { UserRepository } from '../user/user.repository'; import { BookmarkController } from './bookmark.controller'; @@ -12,6 +13,7 @@ import { BookmarkService } from './bookmark.service'; BookmarkRepository, UserRepository, PostRepository, + LikesRepository, ], exports: [BookmarkService, BookmarkRepository], }) diff --git a/server/src/domain/bookmark/bookmark.service.ts b/server/src/domain/bookmark/bookmark.service.ts index d1f3f1bd..9561f036 100644 --- a/server/src/domain/bookmark/bookmark.service.ts +++ b/server/src/domain/bookmark/bookmark.service.ts @@ -14,6 +14,7 @@ import { import { UserNotSameException } from '../../exception/user-not-same.exception'; import { PostNotFoundException } from '../../exception/post-not-found.exception'; import { LoadPostListResponseDto } from '../post/dto/service-response.dto'; +import { LikesRepository } from '../likes/likes.repository'; @Injectable() export class BookmarkService { @@ -21,6 +22,7 @@ export class BookmarkService { private bookmarkRepository: BookmarkRepository, private userRepository: UserRepository, private postRepository: PostRepository, + private likesRepository: LikesRepository, ) {} async getAll(userId: number, { lastId }: BookmarkGetAllRequestDto) { @@ -44,7 +46,14 @@ export class BookmarkService { isLast, ); - postList.posts.forEach((post) => (post.isBookmarked = true)); + const likes = await this.likesRepository.findBy({ + userId, + }); + postList.posts.forEach((post) => { + post.isBookmarked = true; + post.isLiked = likes.some((like) => like.postId === post.id); + }); + return postList; } @@ -123,4 +132,17 @@ export class BookmarkService { return postsYouBookmarked.map((bookmarkInfo) => bookmarkInfo.post.id); } + + async getIsBookmarked(postId: number, userId: number) { + const bookmark = await this.bookmarkRepository.findOneBy({ + post: { + id: postId, + }, + user: { + id: userId, + }, + }); + + return !!bookmark; + } } diff --git a/server/src/domain/likes/likes.service.ts b/server/src/domain/likes/likes.service.ts index fc81eb1a..4f03af1b 100644 --- a/server/src/domain/likes/likes.service.ts +++ b/server/src/domain/likes/likes.service.ts @@ -45,6 +45,15 @@ export class LikesService { return postsYouLiked.map((likesInfo) => likesInfo.postId); } + async getIsLiked(postId: number, userId: number) { + const likes = await this.likesRepository.findOneBy({ + postId, + userId, + }); + + return !!likes; + } + async countLikesCntByPostId(postId: number) { return this.likesRepository.count({ where: { diff --git a/server/src/domain/post/post.controller.ts b/server/src/domain/post/post.controller.ts index 27c638bb..801f701f 100644 --- a/server/src/domain/post/post.controller.ts +++ b/server/src/domain/post/post.controller.ts @@ -76,7 +76,7 @@ export class PostController { await this.addLikesToPostIfLogin(headers['authorization'], returnValue); await this.addBookmarksToPostIfLogin(headers['authorization'], returnValue); - this.addSearchHistory(headers['authorization'], inqueryDto); + await this.addSearchHistory(headers['authorization'], inqueryDto); this.applyTags(tags, lastId); @@ -130,7 +130,7 @@ export class PostController { } } - private addSearchHistory(token, inqueryDto: InqueryDto) { + private async addSearchHistory(token, inqueryDto: InqueryDto) { if (!token) { return; } @@ -142,7 +142,7 @@ export class PostController { // TODO 로그인한 유저가 처음 글 검색시 검색 기록을 추가해야 하기 때문에 글 검색이 있는 PostController에 위치 // 후에 글 검색 리팩토링시 같이 리팩토링이 필요할 듯 - this.userService.addSearchHistory(userId, inqueryDto); + await this.userService.addSearchHistory(userId, inqueryDto); } /** @@ -214,9 +214,24 @@ export class PostController { @ApiNotFoundResponse({ description: '유저 혹은 게시물이 존재하지 않습니다', }) - async inqueryPost(@Param('postId') postId: number) { + async inqueryPost(@Param('postId') postId: number, @Headers() header) { try { - return { post: await this.postService.inqueryPost(postId) }; + const post = await this.postService.inqueryPost(postId); + post.likesCount = await this.likesService.countLikesCntByPostId(post.id); + + const token = header['authorization']; + if (token) { + const userId = this.authService.authenticate(token); + if (userId) { + post.isLiked = await this.likesService.getIsLiked(postId, userId); + post.isBookmarked = await this.bookmarkService.getIsBookmarked( + postId, + userId, + ); + } + } + + return { post: post }; } catch (err) { if (err instanceof PostNotFoundException) { throw new NotFoundException(err.message); diff --git a/server/src/domain/search/search-history.mongo.repository.ts b/server/src/domain/search/search-history.mongo.repository.ts index 06fef170..2e76eb97 100644 --- a/server/src/domain/search/search-history.mongo.repository.ts +++ b/server/src/domain/search/search-history.mongo.repository.ts @@ -44,6 +44,6 @@ export class SearchHistoryMongoRepository extends MongoRepository ); } - this.save(searchHistory); + await this.save(searchHistory); } } diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index 2ce6afdb..dde21639 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -51,7 +51,7 @@ export class UserService { }); } - addSearchHistory( + async addSearchHistory( userId: number, { lastId, tags, reviewCount, likeCount, details }: InqueryDto, ) { @@ -65,7 +65,7 @@ export class UserService { return; } - this.searchHistoryRepository.addSearchHistory( + await this.searchHistoryRepository.addSearchHistory( userId, tags, reviewCount,