-
Notifications
You must be signed in to change notification settings - Fork 5
feat: 글쓰기페이지 #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 글쓰기페이지 #100
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| "use client"; | ||
| import { useRouter } from "next/router"; | ||
|
|
||
| const DetailPage = () => { | ||
| const router = useRouter(); | ||
| const { id } = router.query; | ||
|
|
||
| return <div>글 상세 페이지 - ID: {id}</div>; | ||
| }; | ||
|
|
||
| export default DetailPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| "use client"; | ||
|
|
||
| import React, { useState } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import Button from "../../../components/button/default/Button"; | ||
| import BaseTextArea from "../../../components/input/textarea/BaseTextArea"; | ||
| import ImageInputwithPlaceHolder from "../../../components/input/file/ImageInput/ImageInputwithPlaceHolder"; | ||
| import { usePost } from "../../../../hooks/usePost"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "@/" 경로로 자동완성 안되던가요 ? |
||
|
|
||
| export default function WritePage() { | ||
| const [title, setTitle] = useState<string>(""); | ||
| const [content, setContent] = useState<string>(""); | ||
| const [imageList, setImageList] = useState<string[]>([]); | ||
| const router = useRouter(); | ||
|
|
||
| const { mutate: createPost, isPending, error } = usePost(); | ||
|
|
||
| const handleSubmit = () => { | ||
| if (!title || !content) { | ||
| alert("제목과 내용을 입력해주세요."); | ||
| return; | ||
| } | ||
|
|
||
| const imageUrl = imageList.length > 0 ? imageList.join(",") : undefined; | ||
|
|
||
| createPost( | ||
| { title, content, imageUrl }, | ||
| { | ||
| onSuccess: () => { | ||
| alert("게시글이 등록되었습니다."); | ||
| router.push("/albaTalk"); | ||
| }, | ||
| onError: (err: Error) => { | ||
| alert(err.message || "게시글 등록에 실패했습니다."); | ||
| }, | ||
| } | ||
| ); | ||
| }; | ||
|
|
||
| // DOM에서 업로드된 이미지의 URL을 수집하여 imageList 상태로 업데이트. | ||
| const syncImageList = () => { | ||
| const images = document.querySelectorAll<HTMLElement>(".image-preview-item"); | ||
| const urls = Array.from(images).map((img) => img.dataset.url || ""); | ||
| setImageList(urls.filter((url) => url)); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <div className="flex min-h-screen w-full items-start justify-center bg-gray-50 py-8 font-nexon"> | ||
| <div className="w-full max-w-[1480px] rounded-md bg-white px-6 md:px-4 lg:px-6"> | ||
| <div className="mb-[40px] flex h-[58px] w-full items-center justify-between border-b border-[#line-100] md:h-[78px] lg:h-[126px]"> | ||
| <h1 className="flex items-center text-[18px] font-semibold md:text-[20px] lg:text-[32px]">글쓰기</h1> | ||
| <div className="hidden space-x-1 md:flex md:space-x-2 lg:space-x-4"> | ||
| <Button | ||
| variant="solid" | ||
| className="bg-gray-100 text-gray-50 hover:bg-gray-200 md:h-[46px] md:w-[108px] md:text-[14px] lg:h-[58px] lg:w-[180px] lg:text-[18px]" | ||
| onClick={() => router.push("/albaTalk")} | ||
| > | ||
| 취소 | ||
| </Button> | ||
| <Button | ||
| variant="solid" | ||
| className="bg-primary-orange-300 text-gray-50 hover:bg-orange-400 md:h-[46px] md:w-[108px] md:text-[14px] lg:h-[58px] lg:w-[180px] lg:text-[18px]" | ||
| onClick={() => { | ||
| syncImageList(); | ||
| handleSubmit(); | ||
| }} | ||
| disabled={isPending} | ||
| > | ||
| {isPending ? "등록 중..." : "등록하기"} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="space-y-6 md:space-y-8"> | ||
| <div className="w-full"> | ||
| <label | ||
| htmlFor="title" | ||
| className="mb-2 flex items-center text-[16px] font-medium text-black-300 md:text-[18px] lg:text-[20px]" | ||
| > | ||
| 제목<span className="ml-1 text-primary-orange-300">*</span> | ||
| </label> | ||
| <BaseTextArea | ||
| variant="white" | ||
| name="title" | ||
| size="w-full h-[52px] md:h-[54px] lg:h-[64px] lg:w-[1432px]" | ||
| placeholder="제목을 입력하세요" | ||
| value={title} | ||
| onChange={(e) => setTitle(e.target.value)} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="w-full"> | ||
| <label | ||
| htmlFor="content" | ||
| className="mb-2 flex items-center text-[16px] font-medium text-black-300 md:text-[18px] lg:text-[20px]" | ||
| > | ||
| 내용<span className="ml-1 text-primary-orange-300">*</span> | ||
| </label> | ||
| <BaseTextArea | ||
| variant="white" | ||
| name="content" | ||
| size="w-full h-[180px] md:h-[200px] lg:h-[240px] lg:w-[1432px]" | ||
| placeholder="내용을 입력하세요" | ||
| value={content} | ||
| onChange={(e) => setContent(e.target.value)} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="w-full"> | ||
| <label | ||
| htmlFor="image" | ||
| className="mb-2 block text-[16px] font-medium text-black-300 md:text-[18px] lg:text-[20px]" | ||
| > | ||
| 이미지 | ||
| </label> | ||
| <ImageInputwithPlaceHolder /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* 모바일 버전 버튼 */} | ||
| <div className="w-full md:hidden"> | ||
| <div className="fixed bottom-4 left-0 right-0 flex flex-col items-center space-y-2 rounded-t-lg bg-white p-4"> | ||
| {/* 모바일 버튼 간격 조정 */} | ||
| <button | ||
| className="mb-2 h-[58px] w-[327px] rounded-[8px] bg-gray-200 text-white hover:bg-gray-300" | ||
| onClick={() => router.push("/albaTalk")} | ||
| > | ||
| 취소 | ||
| </button> | ||
| <button | ||
| className="h-[58px] w-[327px] rounded-[8px] bg-primary-orange-300 text-white hover:bg-orange-400" | ||
| onClick={() => { | ||
| syncImageList(); // 이미지 리스트 동기화 | ||
| handleSubmit(); | ||
| }} | ||
| > | ||
| 등록하기 | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </> | ||
| ); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컴포넌트에 value, onChange prop을 추가하지 않고 페이지에서 리액트 훅 폼을 사용해주세요
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정해서 다시 올리겠습니다 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,18 @@ | ||
| import { forwardRef } from "react"; | ||
| import { BaseTextAreaProps } from "@/types/textInput"; | ||
| /** | ||
| @param variant: "white" | "transparent" - 필수값 | ||
| @param name: string - 필수값 | ||
| @param size: "w-[00px] h-[00px] lg:w-[00px] lg:h-[00px]" - 기본값: "w-[327px] h-[132px] lg:w-[640px] lg:h-[160px]" | ||
| @param placeholder: string | ||
| @param errormessage: string - 에러메시지 + 테두리 색상 변경 | ||
| @param disabled: boolean | ||
| @param wrapperClassName?: string; - 부가적인 tailwind css 클래스 | ||
| @param innerClassName?: string; - 부가적인 tailwind css 클래스 | ||
| */ | ||
|
|
||
|
|
||
| /* | ||
| @params variant: "white" | "transparent" - 필수값 | ||
| @params name: string - 필수값 | ||
| @params size: "w-[00px] h-[00px] lg:w-[00px] lg:h-[00px]" - 기본값: "w-[327px] h-[132px] lg:w-[640px] lg:h-[160px]" | ||
| @params placeholder: string | ||
| @params errorMessage: string - 에러메시지 + 테두리 색상 변경 | ||
| @params disabled: boolean | ||
| @params wrapperClassName?: string; - 부가적인 tailwind css 클래스 | ||
| @params innerClassName?: string; - 부가적인 tailwind css 클래스 | ||
| @params value: string - 현재 입력된 값 | ||
| @params onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void - 값 변경 핸들러 | ||
|
||
|
|
||
| const BaseTextArea = forwardRef<HTMLTextAreaElement, BaseTextAreaProps>((props, ref) => { | ||
| const variantStyles = { | ||
|
|
@@ -29,12 +32,10 @@ const BaseTextArea = forwardRef<HTMLTextAreaElement, BaseTextAreaProps>((props, | |
| const defaultSize = "w-[327px] h-[132px] lg:w-[640px] lg:h-[160px]"; | ||
| const sizeStyles = props.size || defaultSize; | ||
|
|
||
| // textareaStyle | ||
| const baseStyle = "resize-none focus:outline-none h-full w-full"; | ||
| const textStyle = | ||
| "text-black-400 placeholder:text-grayscale-400 placeholder:text-base placeholder:leading-[26px] lg:placeholder:text-xl lg:placeholder:leading-8 lg:text-xl font-normal lg:leading-8 text-base leading-[26px]"; | ||
|
|
||
| // wrapperStyle | ||
| const variantStyle = `${variantStyles[props.variant].border} ${variantStyles[props.variant].hover} ${variantStyles[props.variant].focus}`; | ||
| const errorStyle = props.errormessage ? "!border-[0.5px] border-state-error" : ""; | ||
|
|
||
|
|
@@ -50,6 +51,8 @@ const BaseTextArea = forwardRef<HTMLTextAreaElement, BaseTextAreaProps>((props, | |
| id={props.name} | ||
| placeholder={props.placeholder} | ||
| disabled={props.disabled} | ||
| value={props.value} // value 추가 | ||
| onChange={props.onChange} // onChange 추가 | ||
| className={`${textareaStyle} scrollbar-custom`} | ||
| ref={ref} | ||
| {...props} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,8 +35,11 @@ export default function NotFound() { | |
| variant="solid" | ||
| width="sm" | ||
| radius="full" | ||
|
|
||
| className="bg-primary-orange-100 text-white hover:bg-primary-orange-300" | ||
|
|
||
| className="bg-primary-grayscale-500 hover:bg-primary-grayscale-600 text-white" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. gray 색상 쓸때는 primary 없이 bg-grayscale-숫자 요렇게 써주셔야돼요 |
||
| onClick={() => window.history.back()} | ||
|
|
||
| > | ||
| 뒤로 가기 | ||
| </Button> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { useMutation, UseMutationResult } from "@tanstack/react-query"; | ||
| import axios from "axios"; | ||
| import { PostDetailResponse } from "@/types/response/post"; | ||
|
|
||
| interface PostCreateRequest { | ||
| title: string; | ||
| content: string; | ||
| imageUrl?: string; | ||
| } | ||
|
|
||
| export const usePost = (): UseMutationResult<PostDetailResponse, Error, PostCreateRequest> => { | ||
| return useMutation<PostDetailResponse, Error, PostCreateRequest>({ | ||
| mutationFn: async (data: PostCreateRequest) => { | ||
| const apiUrl = `/api/posts`; // Next.js API 라우트로 요청 | ||
|
|
||
| try { | ||
| const response = await axios.post<PostDetailResponse>(apiUrl, data, { | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| withCredentials: true, // 쿠키 포함 | ||
| }); | ||
| return response.data; | ||
| } catch (error: any) { | ||
| console.error("Error during post request:", error); | ||
|
|
||
| if (error.response) { | ||
| console.error("Server response:", error.response.data); | ||
| throw new Error(error.response.data.message || "게시글 등록에 실패했습니다."); | ||
| } | ||
|
|
||
| throw new Error("네트워크 오류가 발생했습니다."); | ||
| } | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
페이지 경로는 가이드 문서를 따라주세요
