Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/app/(pages)/albaTalk/[id]/page.tsx
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;
146 changes: 146 additions & 0 deletions src/app/(pages)/albaTalk/write/page.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지 경로는 가이드 문서를 따라주세요
image

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";
Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
</>
);
}
8 changes: 7 additions & 1 deletion src/app/components/card/board/BoardComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ const BoardComment: React.FC<BoardCommentProps> = ({
className="hover:text-grayscale-700 flex items-center justify-center text-grayscale-500"
aria-label="Options"
>
<Image src={kebabSrc} alt="Kebab Menu Icon" width={28} height={28} /> {/* 크기 조정 */}
<Image
src={kebabSrc}
alt="Kebab Menu Icon"
width={isLargeScreen ? 28 : 24}
height={isLargeScreen ? 28 : 24}
/>{" "}
{/* 크기 조정 */}
</button>
</div>

Expand Down
8 changes: 7 additions & 1 deletion src/app/components/card/board/CardBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ const CardBoard: React.FC<CardBoardProps> = ({
className="hover:text-grayscale-700 flex items-center justify-center text-grayscale-500"
aria-label="Options"
>
<Image src={kebabSrc} alt="Kebab Menu Icon" width={28} height={28} /> {/* 크기 조정 */}
<Image
src={kebabSrc}
alt="Kebab Menu Icon"
width={isLargeScreen ? 28 : 24}
height={isLargeScreen ? 28 : 24}
/>{" "}
{/* 크기 조정 */}
</button>
</div>
{/* Content */}
Expand Down
66 changes: 46 additions & 20 deletions src/app/components/card/board/Comment.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,63 @@
"use client";

import React from "react";
import React, { useEffect, useState } from "react";
import Image from "next/image";

export interface CommentProps {
userName: string;
date: string;
comment: string;
onKebabClick?: () => void; // 케밥 버튼 클릭 핸들러
}

const Comment: React.FC<CommentProps> = ({ userName, date, comment }) => {
const Comment: React.FC<CommentProps> = ({ userName, date, comment, onKebabClick }) => {
const [isLargeScreen, setIsLargeScreen] = useState(false);

useEffect(() => {
const handleResize = () => setIsLargeScreen(window.innerWidth >= 600);
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);

// 케밥 아이콘 경로 설정
const kebabSrc = `/icons/menu/${isLargeScreen ? "kebab-menu-md.svg" : "kebab-menu-sm.svg"}`;
const kebabSize = isLargeScreen ? 28 : 24;

return (
<div className="flex h-[128px] w-[327px] flex-col border border-line-200 p-4 sm:h-[122px] sm:w-[600px] xl:h-[148px] xl:w-[1480px]">
<div className="relative flex h-[128px] w-[327px] flex-col border border-line-200 p-4 sm:h-[122px] sm:w-[600px] xl:h-[148px] xl:w-[1480px]">
{/* Header */}
<div className="flex items-center gap-2">
<Image
src="/icons/user/user-profile-sm.svg"
alt="User Icon"
className="rounded-full"
width={24}
height={24}
sizes="(max-width: 600px) 24px, (max-width: 1480px) 28px, 30px"
/>
<div className="flex items-center gap-1 truncate">
<span className="truncate font-nexon text-[14px] text-grayscale-500 sm:text-[16px] xl:text-[20px]">
{userName}
</span>
<span className="text-grayscale-500">|</span>
<span className="font-nexon text-[14px] text-grayscale-500 sm:text-[16px] xl:text-[20px]">{date}</span>
</div>

<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Image
src="/icons/user/user-profile-sm.svg"
alt="User Icon"
className="rounded-full"
width={24}
height={24}
sizes="(max-width: 600px) 24px, (max-width: 1480px) 28px, 30px"
/>
<div className="flex items-center gap-1 truncate">
<span className="truncate font-nexon text-[14px] text-gray-500 sm:text-[16px] xl:text-[20px]">
{userName}
</span>
<span className="text-gray-500">|</span>
<span className="font-nexon text-[14px] text-gray-500 sm:text-[16px] xl:text-[20px]">{date}</span>
</div>


{/* Right Content: Kebab */}
<button
onClick={onKebabClick}
className="flex items-center justify-center text-gray-500 hover:text-gray-700"
aria-label="Options"
>
<Image src={kebabSrc} alt="Kebab Menu Icon" width={kebabSize} height={kebabSize} />
</button>
</div>

{/* Comment */}
{/* Comment */}
<div className="mt-2 flex h-[96px] w-full flex-1 items-center overflow-hidden text-[14px] leading-[1.5] sm:text-[16px] xl:text-[20px]">
<p className="line-clamp-2 font-nexon text-black-400">{comment}</p>
</div>
Expand Down
27 changes: 15 additions & 12 deletions src/app/components/input/textarea/BaseTextArea.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트에 value, onChange prop을 추가하지 않고 페이지에서 리액트 훅 폼을 사용해주세요
컴포넌트에 prop을 추가하면 리액트 훅폼 쓰는 페이지에서 에러가 발생합니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 - 값 변경 핸들러
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어노테이션 추가 잘 해주셨군요 ~


const BaseTextArea = forwardRef<HTMLTextAreaElement, BaseTextAreaProps>((props, ref) => {
const variantStyles = {
Expand All @@ -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" : "";

Expand All @@ -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}
Expand Down
5 changes: 4 additions & 1 deletion src/app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gray 색상 쓸때는 primary 없이 bg-grayscale-숫자 요렇게 써주셔야돼요

onClick={() => window.history.back()}

>
뒤로 가기
</Button>
Expand Down
36 changes: 36 additions & 0 deletions src/hooks/usePost.ts
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("네트워크 오류가 발생했습니다.");
}
},
});
};
Loading