diff --git a/package.json b/package.json index 9bf022b6..6646e598 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@apollo/client": "^3.8.1", "@playwright/test": "^1.29.1", + "@tanstack/react-query": "^4.35.3", "@tinymce/tinymce-react": "^4.3.0", "@types/d3": "^7.4.0", "@types/node": "18.16.19", @@ -22,8 +23,12 @@ "@types/styled-components": "^5.1.26", "@types/tinymce": "^4.6.5", "@typescript-eslint/eslint-plugin": "^5.43.0", + "@uiw/react-markdown-preview": "^4.1.16", + "@uiw/react-md-editor": "3.6.0", "axios": "^1.4.0", + "babel-loader": "^9.1.3", "babel-plugin-styled-components": "^2.1.4", + "babel-plugin-transform-remove-imports": "^1.7.0", "d3": "^7.8.5", "dayjs": "^1.11.9", "dotenv": "^16.3.1", @@ -33,14 +38,14 @@ "graphql": "^16.8.0", "jest": "^29.3.1", "msw": "^1.2.3", - "next": "13.4.4", + "next": "13.5.2", + "next-remove-imports": "^1.0.12", "prettier": "^2.8.8", "react": "18.2.0", "react-circular-progressbar": "^2.1.0", "react-d3-radar": "^1.0.0-rc6", "react-dom": "18.2.0", "react-infinite-scroll-component": "^6.1.0", - "react-query": "^3.39.3", "react-scroll-horizontal": "^1.6.6", "react-slick": "^0.29.0", "react-spinner": "^0.2.7", @@ -49,8 +54,8 @@ "react-toastify": "^9.1.3", "recoil": "^0.7.7", "slick-carousel": "^1.8.1", - "styled-components": "^5.3.11", - "sweetalert2": "^11.7.8", + "styled-components": "^6.0.8", + "sweetalert2": "^11.7.28", "tinymce": "^6.6.0", "typescript": "5.0.4" }, @@ -63,7 +68,6 @@ "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^9.0.1", "@semantic-release/release-notes-generator": "^10.0.3", - "@tanstack/react-query-devtools": "^4.22.0", "@types/graphql": "^14.5.0", "@types/jest": "^29.2.4", "@types/react-beautiful-dnd": "^13.1.3", diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 36a99274..0f24d515 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -2,7 +2,7 @@ /* tslint:disable */ /** - * Mock Service Worker (1.2.3). + * Mock Service Worker (1.3.1). * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. diff --git a/src/apis/httpClient/httpClient.ts b/src/apis/httpClient/httpClient.ts index e1f8343b..2fe5851d 100644 --- a/src/apis/httpClient/httpClient.ts +++ b/src/apis/httpClient/httpClient.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; import { requestInterceptors, responseInterceptors } from "@/apis/interceptor"; import { KEY, TOKEN } from "@/constants/"; -import { QueryClient } from "react-query"; +import { QueryClient } from "@tanstack/react-query"; import Storage from "../storage"; export interface HttpClientConfig { @@ -161,4 +161,5 @@ export default { bamboo: new HttpClient("api/bamboo", axiosConfig), admin: new HttpClient("api/bamboo/admin", axiosConfig), like: new HttpClient("api/likes/update", axiosConfig), + image: new HttpClient("api/image/save", axiosConfig), }; diff --git a/src/apis/storage/index.ts b/src/apis/storage/index.ts index 826c5dc2..cab8d660 100644 --- a/src/apis/storage/index.ts +++ b/src/apis/storage/index.ts @@ -4,7 +4,7 @@ type StorageKey = StorageSettingKey | StorageTokenKey; export default class Storage { static getItem(key: StorageKey) { - return typeof window !== "undefined" ? localStorage.getItem(key) : null; + return typeof window !== "undefined" ? localStorage.getItem(key) : ""; } static setItem(key: StorageKey, value: string) { diff --git a/src/app/lostfound/[state]/[id]/page.tsx b/src/app/lostfound/[state]/[id]/page.tsx deleted file mode 100644 index b46d31dc..00000000 --- a/src/app/lostfound/[state]/[id]/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import LostFoundPostPage from "@/page/lostfound-post"; - -const LostFoundPost = () => { - return ; -}; - -export default LostFoundPost; diff --git a/src/app/lostfound/page.tsx b/src/app/lostfound/page.tsx deleted file mode 100644 index 50537b81..00000000 --- a/src/app/lostfound/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import LostFoundPage from "@/page/lostfound"; - -const LostFound = () => { - return ; -}; - -export default LostFound; diff --git a/src/app/lostfound/write/page.tsx b/src/app/lostfound/write/page.tsx deleted file mode 100644 index c4364a38..00000000 --- a/src/app/lostfound/write/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import LostFoundWritePage from "@/page/lostfound-write"; - -const LostFoundWrite = () => { - return ; -}; - -export default LostFoundWrite; diff --git a/src/app/post/[id]/update/page.tsx b/src/app/post/[id]/update/page.tsx new file mode 100644 index 00000000..41f70d27 --- /dev/null +++ b/src/app/post/[id]/update/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import UpdatePage from "@/page/forum-edit"; + +interface IPostUpdateAppPageParams { + params: { + id: number; + }; +} + +const Update = ({ params }: IPostUpdateAppPageParams) => { + return ; +}; + +export default Update; diff --git a/src/app/registry.tsx b/src/app/registry.tsx deleted file mode 100644 index 5a7f4112..00000000 --- a/src/app/registry.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { useServerInsertedHTML } from "next/navigation"; -import { ServerStyleSheet, StyleSheetManager } from "styled-components"; - -export default function StyledComponentsRegistry({ - children, -}: { - children: React.ReactNode; -}) { - // Only create stylesheet once with lazy initial state - // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state - const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); - - useServerInsertedHTML(() => { - const styles = styledComponentsStyleSheet.getStyleElement(); - styledComponentsStyleSheet.instance.seal(); - return styles; - }); - - if (typeof window !== "undefined") return children; - - return ( - - {children} - - ); -} diff --git a/src/assets/data/emptyCategories.ts b/src/assets/data/emptyCategories.ts index b670c529..6de6ad50 100644 --- a/src/assets/data/emptyCategories.ts +++ b/src/assets/data/emptyCategories.ts @@ -18,8 +18,12 @@ const categories = [ name: FORUM.CODE_REVIEW.NAME, }, { - type: FORUM.LOST_FOUND.TYPE, - name: FORUM.LOST_FOUND.NAME, + type: FORUM.LOST.TYPE, + name: FORUM.LOST.NAME, + }, + { + type: FORUM.FOUND.TYPE, + name: FORUM.FOUND.NAME, }, ]; diff --git a/src/assets/data/emptyInputPost.ts b/src/assets/data/emptyInputPost.ts new file mode 100644 index 00000000..47c07278 --- /dev/null +++ b/src/assets/data/emptyInputPost.ts @@ -0,0 +1,19 @@ +import { POST } from "@/constants"; +import { IInputPost } from "@/interfaces"; + +const emptyInputPost: IInputPost = { + id: "", + title: "", + category: POST.COMMON, + content: "", + prUrl: "", + isFinished: false, + lostThingImage: "", + place: "", + keepingPlace: "", + startTime: "", + endTime: "", + field: "", +}; + +export default emptyInputPost; diff --git a/src/assets/data/index.ts b/src/assets/data/index.ts index 7bca69dc..1bf7607e 100644 --- a/src/assets/data/index.ts +++ b/src/assets/data/index.ts @@ -2,3 +2,4 @@ export { default as emptyCategories } from "./emptyCategories"; export { default as emptyClassInfo } from "./emptyClassInfo"; export { default as emptyClassLevel } from "./emptyClassLevel"; export { default as emptyTimetable } from "./emptyTimetable"; +export { default as emptyInputPost } from "./emptyInputPost"; diff --git a/src/assets/icons/LikeIcon.tsx b/src/assets/icons/LikeIcon.tsx index 0a758631..3168fde9 100644 --- a/src/assets/icons/LikeIcon.tsx +++ b/src/assets/icons/LikeIcon.tsx @@ -1,16 +1,17 @@ import { SVGAttribute } from "@/interfaces"; +import Like from "./Like"; interface ILikeIconProps extends SVGAttribute { - color?: string; + isLiked?: boolean; } const LikeIcon = ({ width = 15, height = 13, - color = "#E54F5A", + isLiked, isPointable, }: ILikeIconProps) => { - return ( + return isLiked ? ( + ) : ( + ); }; diff --git a/src/assets/icons/UploadIcon.tsx b/src/assets/icons/UploadIcon.tsx new file mode 100644 index 00000000..c3d4862d --- /dev/null +++ b/src/assets/icons/UploadIcon.tsx @@ -0,0 +1,40 @@ +import { color, flex } from "@/styles"; +import React from "react"; +import styled from "styled-components"; + +const UploadIcon = ({ + ...props +}: React.ButtonHTMLAttributes) => { + return ( + + + + + + ); +}; + +const StyledButton = styled.button` + padding: 4px; + ${flex.CENTER}; + + &:hover { + background-color: ${color.gray}; + svg { + path { + fill: #0066cc; + } + } + } +`; + +export default UploadIcon; diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 27934dc7..9f2fdc29 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -18,3 +18,4 @@ export { default as Laughing } from "./emojis/Laughing"; export { default as Relaxed } from "./emojis/Relaxed"; export { default as Curve } from "./Curve"; export { default as HistorySeparator } from "./HistorySeparator"; +export { default as UploadIcon } from "./UploadIcon"; diff --git a/src/components/atoms/CustomEditor.tsx b/src/components/atoms/CustomEditor.tsx index e7aa2885..851b6c92 100644 --- a/src/components/atoms/CustomEditor.tsx +++ b/src/components/atoms/CustomEditor.tsx @@ -1,107 +1,60 @@ +import "@uiw/react-md-editor/markdown-editor.css"; +import "@uiw/react-markdown-preview/markdown.css"; import React from "react"; -import styled from "styled-components"; -import { Editor as TinymcEditor } from "@tinymce/tinymce-react"; -import { font } from "@/styles"; -import useEmoji from "@/hooks/useEmoji"; -import { EmojiModal } from "@/components/common"; - -interface IBlobInfo { - id: () => string; - name: () => string; - filename: () => string; - blob: () => Blob; - base64: () => string; - blobUri: () => string; - uri: () => string | undefined; +import { toast } from "react-toastify"; +import MDEditor, { + ContextStore, + ICommand, + MDEditorProps, + getCommands, +} from "@uiw/react-md-editor"; +import { UploadIcon } from "@/assets/icons"; +import { getImageUrl } from "@/helpers"; +import useModal from "@/hooks/useModal"; +import DragDrop from "./DragDrop"; + +type CustomEditorPropsType = MDEditorProps & React.RefAttributes; + +interface ICustomEditorProps extends CustomEditorPropsType { + value: string; } -const CustomEditor = () => { - const [content, setContent] = React.useState(""); - const { openEmoji, closeEmoji, visible } = useEmoji(); +const CustomEditor = ({ ...props }: ICustomEditorProps) => { + const { openModal, closeModal } = useModal(); + + const handleImageSelected = async (file: File | undefined) => { + closeModal(); + const imageUrl = await getImageUrl(file); + + try { + await navigator.clipboard.writeText(imageUrl); + toast.success("이미지 주소를 클립보드에 저장했어요."); + } catch (err) { + toast.error("이미지를 업로드를 실패했습니다."); + } + }; - const imagesUploadHandler = async (blobInfo: IBlobInfo): Promise => { - return new Promise(() => { - const file = new FormData(); - file.append("file", blobInfo.blob()); + const handleImageUploaderClick = () => { + openModal({ + component: ( + + ), }); }; + const imageUploader: ICommand = { + name: "imageUploader", + keyCommand: "imageUploader", + icon: , + }; + return ( - - {visible && } - { - tinymceEditor.ui.registry.addButton("emoticon", { - icon: "emoji", - onAction: openEmoji, - }); - }, - relative_urls: false, - convert_urls: false, - extended_valid_elements: "img[src|class|alt|e_id|e_idx|e_type]", - images_upload_handler: imagesUploadHandler, - init_instance_callback: (e) => { - const css = document.createElement("style"); - css.innerHTML = StyledCSS; - e.contentDocument.head.appendChild(css); - }, - }} - value={content} - onEditorChange={(props) => setContent(props)} - /> - + ); }; -const StyledCSS = ` - html { - ${font.p3}; - } - - * { - border-color: green !important; - list-style: none; - text-decoration: none; - margin: 0; - padding: 0; - } - - .emoticon { - width: 100px !important; - height: 100px !important; - } - - body { - padding: 18px !important; - } -`; - -const Container = styled.div` - position: relative; -`; - export default CustomEditor; diff --git a/src/components/atoms/CustomViewer.tsx b/src/components/atoms/CustomViewer.tsx new file mode 100644 index 00000000..765c1292 --- /dev/null +++ b/src/components/atoms/CustomViewer.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import MDViewer from "@uiw/react-markdown-preview"; + +interface MDViewerPropsType { + content?: string; +} + +const CustomViewer = ({ content }: MDViewerPropsType) => { + return ( + + ); +}; + +export default CustomViewer; diff --git a/src/components/atoms/DragDrop.tsx b/src/components/atoms/DragDrop.tsx new file mode 100644 index 00000000..0b408c38 --- /dev/null +++ b/src/components/atoms/DragDrop.tsx @@ -0,0 +1,186 @@ +import Image from "next/image"; +import { color } from "@/styles"; +import React from "react"; +import styled, { css } from "styled-components"; +import { Column, Row } from "../Flex"; + +interface DragDropFunctionPropsType { + handler: (file: File | undefined) => void; + previewImage?: string; + width?: string; + height?: string; + isDontNeedPreview?: boolean; + isDontNeedChangeText?: boolean; +} + +const DragDrop = ({ + width, + height, + handler, + previewImage, + isDontNeedPreview, + isDontNeedChangeText, +}: DragDropFunctionPropsType) => { + const [isDragging, setIsDragging] = React.useState(false); + const [file, setFile] = React.useState(); + + const dragRef = React.useRef(null); + + const onChangeFiles = React.useCallback( + ( + e: + | React.ChangeEvent + | React.DragEvent, + ) => { + let selectFiles: File[] = []; + + if (e.type === "drop") { + const dragEvent = e as React.DragEvent; + const fileList = dragEvent.dataTransfer.files; + selectFiles = fileList ? Array.from(fileList) : []; // FileList가 null이면 빈 배열 할당 + } else { + const changeEvent = e as React.ChangeEvent; + const fileList = changeEvent.target.files; + selectFiles = fileList ? Array.from(fileList) : []; // FileList가 null이면 빈 배열 할당 + } + + const [currentFile] = selectFiles; + handler(currentFile); + setFile(currentFile); + }, + // eslint-disable-next-line + [], + ); + + const handleDragIn = React.useCallback((e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDragOut = React.useCallback((e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + setIsDragging(false); + }, []); + + const handleDragOver = React.useCallback((e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (e.dataTransfer) setIsDragging(true); + }, []); + + const handleDrop = React.useCallback( + (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + onChangeFiles(e as unknown as React.DragEvent); + setIsDragging(false); + }, + [onChangeFiles], + ); + + const initDragEvents = React.useCallback(() => { + if (dragRef.current) { + dragRef.current.addEventListener("dragenter", handleDragIn); + dragRef.current.addEventListener("dragleave", handleDragOut); + dragRef.current.addEventListener("dragover", handleDragOver); + dragRef.current.addEventListener("drop", handleDrop); + } + }, [handleDragIn, handleDragOut, handleDragOver, handleDrop]); + + const resetDragEvents = React.useCallback(() => { + if (dragRef.current) { + dragRef.current.removeEventListener("dragenter", handleDragIn); + dragRef.current.removeEventListener("dragleave", handleDragOut); + dragRef.current.removeEventListener("dragover", handleDragOver); + dragRef.current.removeEventListener("drop", handleDrop); + } + }, [handleDragIn, handleDragOut, handleDragOver, handleDrop]); + + React.useEffect(() => { + initDragEvents(); + return () => resetDragEvents(); + }, [initDragEvents, resetDragEvents]); + + return ( + + + + + + {file && !isDontNeedChangeText + ? file.name + : "드래그하여 파일 업로드"} + + + + {file && !isDontNeedPreview && previewImage && ( + + )} + + ); +}; + +const NoneDisplayInput = styled.input` + display: none; +`; + +const DragDropTitle = styled.div` + font-weight: 600; + font-size: 14px; +`; + +const StyledImage = styled(Image)` + border-radius: 6px; + width: 28vw; + height: 30vh; + padding: 6px; +`; + +const DragDropButton = styled.label<{ + width?: string; + height?: string; + isDragging: boolean; +}>` + width: ${({ width }) => width || "24vw"}; + height: ${({ height }) => height || "30vh"}; + border: 2px solid ${color.container}; + ${({ isDragging }) => + isDragging + ? css` + background-color: ${color.primary_blue}; + color: ${color.white}; + ` + : css` + background-color: ${color.white}; + color: ${color.black}; + `}; + box-shadow: 4px 4px 10px 0 rgba(0, 0, 0, 0.05); + border-radius: 10px; + cursor: pointer; + transition: 0.12s ease-in; + display: flex; + justify-content: center; + align-items: center; +`; + +export default DragDrop; diff --git a/src/components/atoms/ImageWithFallback.tsx b/src/components/atoms/ImageWithFallback.tsx index 865ca6a7..9d8aa7df 100644 --- a/src/components/atoms/ImageWithFallback.tsx +++ b/src/components/atoms/ImageWithFallback.tsx @@ -22,6 +22,10 @@ const ImageWithFallback = ({ }: ImageWithFallbackProps) => { const [imgSrc, setImgSrc] = React.useState(src); + React.useEffect(() => { + setImgSrc(src); + }, [src]); + return ( { - return gql` - query { +export const GET_POST = ({ id }: IPostUpdateQueryProps) => gql` + query GetPost { readOne ( id: ${id} ) { - ${posts[type]} + id + ${DEFAULT_POST} + ${ALL_POST} } } `; -}; -export const GET_POST_LIST = ({ type, page, size }: IPostProps) => { - return gql` - query { +export const GET_UPDATE_POST = ({ id }: IPostUpdateQueryProps) => gql` + query GetPost { + readOne ( id: ${id} ) { + id + ${DEFAULT_POST} + ${ALL_POST} + } + } +`; + +export const GET_POST_LIST = ({ type, page, size }: IPostProps) => + gql` + query GetPostList { readByCategory ( category: "${type}" page: ${page} size: ${size} ) { entity { id ${DEFAULT_POST} + ${posts[type]} } totalPage currentPage } } `; -}; -export const UPDATE_POST = ({ type, data }: IPostMutateProps) => { - return gql` - mutation { - update ( input: ${data} ) { - ${posts[type]} - } +export const UPDATE_POST = gql` + mutation UpdatePost($data: PostInput) { + update(input: $data) { + id } - `; -}; + } +`; -export const CREATE_POST = ({ type, data }: IPostMutateProps) => { - return gql` - mutation { - create ( input: ${data} ) { - ${posts[type]} - } +export const CREATE_POST = gql` + mutation CreatePost($data: PostInput) { + create(input: $data) { + id } - `; -}; + } +`; + +export const DELETE_POST = gql` + mutation DeletePost($id: Int) { + delete(id: $id) { + deletedId + } + } +`; diff --git a/src/helpers/checkPostValid.helper.ts b/src/helpers/checkPostValid.helper.ts new file mode 100644 index 00000000..1551c360 --- /dev/null +++ b/src/helpers/checkPostValid.helper.ts @@ -0,0 +1,49 @@ +import { POST } from "@/constants"; +import { IInputPost } from "@/interfaces"; +import { toast } from "react-toastify"; + +const checkPostValid = (post: IInputPost) => { + const { + title, + category, + content, + prUrl, + place, + keepingPlace, + startTime, + endTime, + field, + } = post; + const is일반혹은공지사항카테고리라면 = + category === POST.COMMON || category === POST.NOTICE; + const is분실물찾기카테고리라면 = + category === POST.LOST || category === POST.FOUND; + + // 공통사항 + if (!title) return toast.error("글 제목을 입력해주세요."); + + // 일반 혹은 공지사항 카테고리일 경우 content 유효성 검사 + if (is일반혹은공지사항카테고리라면 && !content) + return toast.error("글 내용을 입력해주세요."); + + // 코드리뷰 카테고리일 경우 PR 링크 유효성 검사 + if (category === POST.CODE_REVIEW && !prUrl) + return toast.error("PR 링크를 입력해주세요."); + + // 프로젝트 카테고리일 경우 + if (category === POST.PROJECT) { + if (!startTime) return toast.error("프로젝트 시작 기한을 입력해주세요."); + if (!endTime) return toast.error("프로젝트 마감 기한을 입력해주세요."); + if (!field) return toast.error("프로젝트 분야를 입력해주세요."); + } + + // 분실물찾기 카테고리일 경우 + if (is분실물찾기카테고리라면 && !place) + return toast.error("장소를 입력해주세요."); + if (category === POST.FOUND && !keepingPlace) + return toast.error("보관 장소를 입력해주세요."); + + return true; +}; + +export default checkPostValid; diff --git a/src/helpers/filterInputPost.helper.ts b/src/helpers/filterInputPost.helper.ts new file mode 100644 index 00000000..4f21d4d7 --- /dev/null +++ b/src/helpers/filterInputPost.helper.ts @@ -0,0 +1,36 @@ +import { POST } from "@/constants"; +import { IInputPost } from "@/interfaces"; + +const filterInputPost = (post: IInputPost) => { + const { + id, + title, + content, + prUrl, + isFinished, + startTime, + endTime, + field, + lostThingImage, + place, + keepingPlace, + category, + } = post; + + const defaultPost = { id, title, content, category }; + + if (category === POST.COMMON || category === POST.NOTICE) return defaultPost; + + if (category === POST.CODE_REVIEW) + return { ...defaultPost, prUrl, isFinished }; + + if (category === POST.PROJECT) + return { ...defaultPost, startTime, endTime, field }; + + if (category === POST.LOST) return { ...defaultPost, lostThingImage, place }; + + if (category === POST.FOUND) + return { ...defaultPost, lostThingImage, place, keepingPlace }; +}; + +export default filterInputPost; diff --git a/src/helpers/getCategory.helper.ts b/src/helpers/getCategory.helper.ts index 4eed5e91..b705360c 100644 --- a/src/helpers/getCategory.helper.ts +++ b/src/helpers/getCategory.helper.ts @@ -3,7 +3,8 @@ import { PostCategoryType } from "@/types"; const POSTNAME = { COMMON: "일반", CODE_REVIEW: "코드 리뷰", - LOST_FOUND: "분실물 찾기", + LOST: "분실했어요", + FOUND: "습득했어요", NOTICE: "공지사항", PROJECT: "팀원 모집", }; diff --git a/src/helpers/getImageUrl.helper.ts b/src/helpers/getImageUrl.helper.ts new file mode 100644 index 00000000..d5fe1348 --- /dev/null +++ b/src/helpers/getImageUrl.helper.ts @@ -0,0 +1,13 @@ +import httpClient from "@/apis/httpClient"; + +const getImageUrl = async (file: File | undefined) => { + if (file) { + const formData = new FormData(); + formData.append("image", file, file.name); + + const { data: imageUrl } = await httpClient.image.post(formData); + return imageUrl; + } +}; + +export default getImageUrl; diff --git a/src/helpers/getToken.helper.ts b/src/helpers/getToken.helper.ts new file mode 100644 index 00000000..c5d8f128 --- /dev/null +++ b/src/helpers/getToken.helper.ts @@ -0,0 +1,8 @@ +import Storage from "@/apis/storage"; +import { TOKEN } from "@/constants"; + +const getToken = () => { + return Storage.getItem(TOKEN.ACCESS) || ""; +}; + +export default getToken; diff --git a/src/helpers/getWriteContentLabel.helper.ts b/src/helpers/getWriteContentLabel.helper.ts new file mode 100644 index 00000000..371b2b6c --- /dev/null +++ b/src/helpers/getWriteContentLabel.helper.ts @@ -0,0 +1,13 @@ +import { POST } from "@/constants"; +import { PostCategoryType } from "@/types"; + +const getWriteContentLabel = (category: PostCategoryType) => { + if (category === POST.PROJECT) return "프로젝트에 대해 설명해주세요."; + if (category === POST.LOST) return "잃어버린 물건에 대해 설명해주세요."; + if (category === POST.FOUND) return "습득한 물건에 대해 설명해주세요."; + if (category === POST.CODE_REVIEW) + return "PR에 대한 요구사항 등을 설명해주세요."; + return "글의 내용을 입력해주세요."; +}; + +export default getWriteContentLabel; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 8c606d30..c8eced3c 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,3 +1,8 @@ export { default as getUserRole } from "./getUserRole.helper"; export { default as getStatusColor } from "./getStatusColor.helper"; +export { default as getToken } from "./getToken.helper"; +export { default as getImageUrl } from "./getImageUrl.helper"; export { default as isAdmin } from "./isAdmin.helper"; +export { default as filterInputPost } from "./filterInputPost.helper"; +export { default as getWriteContentLabel } from "./getWriteContentLabel.helper"; +export { default as checkPostValid } from "./checkPostValid.helper"; diff --git a/src/hooks/useDate.ts b/src/hooks/useDate.ts index 904299ec..a312d244 100644 --- a/src/hooks/useDate.ts +++ b/src/hooks/useDate.ts @@ -22,6 +22,10 @@ interface TimeDiffType { startTime: string; } +interface IFormatDateOptions { + summary: boolean; +} + const weekdaysENG = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; const weekdaysKOR = ["일", "월", "화", "수", "목", "금", "토"]; @@ -32,10 +36,16 @@ const useDate = () => { (_, i) => user.enroll + i, ); - const formatDate = (date: string) => { + const formatDate = (date?: string, option?: IFormatDateOptions) => { + if (option?.summary) return dayjs(date).locale("ko").format("YYYY.MM.DD."); return dayjs(date).locale("ko").format("YYYY.MM.DD. A hh:mm"); }; + const unformatDate = (date: string, option?: IFormatDateOptions) => { + if (option?.summary) return dayjs(date).format("YYYY-MM-DD"); + return dayjs(date).format("YYYY-MM-DDThh:mm:ss"); + }; + const getHMSDate = () => { const date = dayjs(); const HMSDate = dayjs(date).locale("ko").format("A h:mm:ss"); @@ -93,6 +103,7 @@ const useDate = () => { weekdaysENG, weekdaysKOR, currentYearsWithSchool, + unformatDate, formatDate, getHMSDate, getDate, diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 1063226d..2df16d3c 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -1,6 +1,6 @@ import React from "react"; import { useRouter } from "next/navigation"; -import { useQuery } from "react-query"; +import { useQuery } from "@tanstack/react-query"; import { useRecoilState } from "recoil"; import httpClient, { HttpClient } from "@/apis/httpClient/httpClient"; import KEY from "@/constants/key.constant"; diff --git a/src/hooks/useWindow.ts b/src/hooks/useWindow.ts index d891b4fa..1b4ad133 100644 --- a/src/hooks/useWindow.ts +++ b/src/hooks/useWindow.ts @@ -1,11 +1,15 @@ -import { useEffect, useState } from "react"; +"use client"; -export default function useWindow() { - const [isWindow, setIsWindow] = useState(false); +import React from "react"; - useEffect(() => { +const useWindow = () => { + const [isWindow, setIsWindow] = React.useState(false); + + React.useEffect(() => { if (typeof window !== "undefined") setIsWindow(true); }, []); return { isWindow }; -} +}; + +export default useWindow; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 431e30f3..606c6bb1 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -14,3 +14,4 @@ export type { default as IComment } from "./comment.interface"; export type { default as IBambooPost } from "./bambooPost.interface"; export type { default as IPostInfiniteList } from "./postInfiniteList.interface"; export type { default as IInfiniteResult } from "./infiniteResult.interface"; +export type { default as IInputPost } from "./inputPost.interface"; diff --git a/src/interfaces/infiniteResult.interface.ts b/src/interfaces/infiniteResult.interface.ts index 8efb26e9..683d0e42 100644 --- a/src/interfaces/infiniteResult.interface.ts +++ b/src/interfaces/infiniteResult.interface.ts @@ -1,4 +1,4 @@ -import { QueryStatus } from "react-query"; +import { QueryStatus } from "@tanstack/react-query"; import { AxiosError } from "axios"; import IPostInfiniteList from "./postInfiniteList.interface"; diff --git a/src/interfaces/inputPost.interface.ts b/src/interfaces/inputPost.interface.ts new file mode 100644 index 00000000..40e7e791 --- /dev/null +++ b/src/interfaces/inputPost.interface.ts @@ -0,0 +1,16 @@ +import { PostCategoryType } from "@/types"; + +export default interface IInputPost { + id: string; + title: string; + category: PostCategoryType; + content: string; + prUrl: string; + isFinished: boolean; + lostThingImage: string; + place: string; + keepingPlace: string; + startTime: Date | string; + endTime: Date | string; + field: string; +} diff --git a/src/interfaces/post.interface.ts b/src/interfaces/post.interface.ts index 6a6f0174..cb33f446 100644 --- a/src/interfaces/post.interface.ts +++ b/src/interfaces/post.interface.ts @@ -1,18 +1,37 @@ import { PostCategoryType } from "@/types"; export default interface IPost { - id: number; + id: string; + title: string; + content: string; user: { id: number; nickName: string; profileImage: string; }; category: PostCategoryType; - title: string; createdAt: string; - view: number; likeCount: number; commentCount: number; - content: string; - isMyLike: boolean; + doesLike: boolean; + + // POST.PROJECT + startTime?: string; + endTime?: string; + field?: string; + + // POST.CODE_REVIEW + prUrl?: string; + isFinished?: boolean; + + // POST.LOST & POST.FOUND + lostThingImage?: string; + place?: string; + foundUser?: { + id: number; + nickName: string; + }; + + // POST.FOUND + keepingPlace?: string; } diff --git a/src/interfaces/postQuery.interface.ts b/src/interfaces/postQuery.interface.ts index 8d9afcf3..d3bd6ea0 100644 --- a/src/interfaces/postQuery.interface.ts +++ b/src/interfaces/postQuery.interface.ts @@ -1,6 +1,3 @@ -import { PostCategoryType } from "@/types"; - export default interface IPostQuery { - type: PostCategoryType; id: number; } diff --git a/src/page/bamboo/services/mutation.service.ts b/src/page/bamboo/services/mutation.service.ts index a3be9ea7..a8da6443 100644 --- a/src/page/bamboo/services/mutation.service.ts +++ b/src/page/bamboo/services/mutation.service.ts @@ -1,4 +1,4 @@ -import { useMutation, useQueryClient } from "react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "react-toastify"; import { KEY } from "@/constants"; import useModal from "@/hooks/useModal"; diff --git a/src/page/bamboo/services/query.service.ts b/src/page/bamboo/services/query.service.ts index e606d2b3..bdf3f986 100644 --- a/src/page/bamboo/services/query.service.ts +++ b/src/page/bamboo/services/query.service.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery, useQuery } from "react-query"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { KEY } from "@/constants"; import IBambooPendingPost from "@/interfaces/bambooPendingPost.interface"; import { getBambooPendingPostList, getBambooPostList } from "./api.service"; diff --git a/src/page/lostfound/index.tsx b/src/page/forum-edit/index.tsx similarity index 65% rename from src/page/lostfound/index.tsx rename to src/page/forum-edit/index.tsx index b807c239..9788cea8 100644 --- a/src/page/lostfound/index.tsx +++ b/src/page/forum-edit/index.tsx @@ -1,12 +1,16 @@ import styled from "styled-components"; import { Aside } from "@/components/common"; -import LostFoundBox from "./layouts/LostFoundBox"; +import UpdateBox from "./layouts/UpdateBox"; -const LostFoundPage = () => { +interface IPostPageParams { + id: number; +} + +const UpdatePage = (params: IPostPageParams) => { return ( - +