Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c2a39fd
refactor(mentor): PropsWithChildren 교체
cksrlcks Jan 14, 2025
76d0ec2
refactor(mentor): 상품 이미지 업로드 서비스 함수 개선
cksrlcks Jan 14, 2025
a22e42c
refactor(mentor): util 함수들 인풋에대한 검증 추가
cksrlcks Jan 14, 2025
55172c7
feat: article type, schema, service 작성
cksrlcks Jan 15, 2025
75e9bfc
feat: Article action 훅 작성
cksrlcks Jan 15, 2025
a9f61a0
feat: 이미지업로드 컴포넌트 개선
cksrlcks Jan 15, 2025
d4dc45c
fix: 아바타 컴포넌트 수정 (Next Image width 설정)
cksrlcks Jan 15, 2025
b754f36
feat: Comment 관련 컴포넌트 개선
cksrlcks Jan 15, 2025
ee13623
feat: 중고마켓 작성, 수정시 해당 페이지로 이동하도록 수정
cksrlcks Jan 15, 2025
5dcc2f1
feat: 자유게시판 상세페이지 작업
cksrlcks Jan 15, 2025
1a73a67
feat: 자유게시판 작성페이지 작업
cksrlcks Jan 15, 2025
22032fb
design: 플레이스홀더 메세지 줄바꿈 css 추가
cksrlcks Jan 15, 2025
f3607ed
fix: 로딩메세지 수정
cksrlcks Jan 15, 2025
d7f0cd3
feat: 목록으로 돌아가기 버튼 작업
cksrlcks Jan 15, 2025
f82f654
feat: 게시물 작성 페이지 작업
cksrlcks Jan 15, 2025
099a884
fix: 네비게이션 활성화 수정
cksrlcks Jan 15, 2025
fea5f65
fix: placeholder 수정
cksrlcks Jan 15, 2025
ce97f3e
fix: middleware match 경로 수정
cksrlcks Jan 15, 2025
89fa285
refactor: 페이지 에러핸들링 수정(최상위 error로 가도록)
cksrlcks Jan 15, 2025
3def569
refactor: 자동으로 static되는 페이지에 dynamic 키워트 제거
cksrlcks Jan 16, 2025
1c14751
refactor: 공용 코멘트 컴포넌트 위치 변경
cksrlcks Jan 16, 2025
871c1cd
fix: 메세지 상수 파일 수정
cksrlcks Jan 16, 2025
42b5ffe
refactor: ssr페이지 에러 처리 개선
cksrlcks Jan 16, 2025
6d2b65d
refactor: try문안에서 redirect를 사용하도록 수정
cksrlcks Jan 16, 2025
4721252
chore: readme 수정
cksrlcks Jan 16, 2025
c1aa081
refactor(mentor): axios config params에 쿼리파람 옮기기
cksrlcks Jan 16, 2025
e346262
refactor: 게시물 작성완료시 페이지이동 개선
cksrlcks Jan 16, 2025
b3d1b3c
fix: axios 에러 타입체크 변경
cksrlcks Jan 16, 2025
9adef1f
feat: 코멘트에 리액트쿼리 사용해보기, 코멘트 리팩토링
cksrlcks Jan 16, 2025
81f8663
chore: console.log 지우기
cksrlcks Jan 17, 2025
d55cbf5
refactor: 회원가입, 로그인 서버액션 사용해보기
cksrlcks Jan 17, 2025
9438b13
refactor: force-static 지우기
cksrlcks Jan 17, 2025
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ state 관리하고 있는 params과 usePageSize로 관리하는 pageSize의 변
- 직접 값을 업데이트해줄때는 (특수한 필드의 경우), useForm의 반환값중 setValue를 이용하면 값 업데이트와 벨리데이션이 작동한다.
- react hook form에 컴포넌트를 연결할때에는 한겹의 어뎁터 레이어를 설정하여 컴포넌트 내부에서 react hook form의 의존성이 없도록 작성하는 방법을 사용하자.
- watch를 통해 각 필드에 value값을 전달하면 하나의 필드가 업데이트 될때마다 전체가 리랜더링이 되어버린다. (Controller, useController 등을 통해서 전달해야함)

### nextjs server component에서 try/catch문 안에서 redirect 사용하기

- 서버컴포넌트에서 try,catch문안에서 redirect사용시 문제가 됬던점에 대해서 정리했습니다.
- https://heavy-bear.tistory.com/16
5 changes: 2 additions & 3 deletions src/app/(auth)/_components/AuthContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ReactNode } from "react";
import { PropsWithChildren } from "react";
import Link from "next/link";
import Image from "next/image";
import logo from "@assets/img/common/logo_full.svg";
import Oauth from "./Oauth";
import styles from "./AuthContainer.module.scss";

interface AuthContainerProps {
children: ReactNode;
interface AuthContainerProps extends PropsWithChildren {
mode?: "login" | "signup";
}

Expand Down
113 changes: 113 additions & 0 deletions src/app/(common)/(board)/_components/ArticleForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";

import { Section } from "@components/Section";
import {
FieldItem,
Form,
ImageUpload,
Input,
Textarea,
} from "@components/Field";
import { Button } from "@components/ui";
import useFormWithError from "@hooks/useFormWithError";
import { zodResolver } from "@hookform/resolvers/zod";
import { Article } from "@/types/article";
import { FieldAdapter } from "@components/adaptor/rhf";
import { useRouter } from "next/navigation";
import useArticleActions from "./useArticleActions";
import { ArticleFormSchema, ArticleFormType } from "@/schemas/article";

interface ArticleFormProps {
initialData?: Article;
mode?: "add" | "edit";
articleId?: number;
Copy link
Collaborator

Choose a reason for hiding this comment

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

articleId 값은 initialData 에 포함되어있지 않을까요...?

Copy link
Collaborator

Choose a reason for hiding this comment

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

요거는 타입 추론을 이용하면 좋을것같아요.

mode가 add 이면 딱히 initialData나 articleId가 필요없지 않나요...?

type ArticleFormProps = ArticleEditFormProps | ArticleAddFormProps;

type ArticleEditFormProps = {
  initialData?: any;
  mode: "edit";
  articleId?: number;
};

type ArticleAddFormProps = {
  mode: "add";
};

이런식으로 합성해서 사용할 수 있어요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오 이거 엄청 고민하던건데 감사합니다.;;;

}

export default function ArticleForm({
initialData,
mode = "add",
articleId,
}: ArticleFormProps) {
const router = useRouter();
const { handleArticleAdd, handleArticleModify } =
useArticleActions(articleId);
const onFormSubmit = mode === "add" ? handleArticleAdd : handleArticleModify;
Comment on lines +32 to +34
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 좀 취향을 탈수도 있는데... 저는 컴포넌트에서 API 통신하는걸 그다지 선호하진 않거든요...

왜냐하면 이 ArticleForm은 단순 Form인데, 결과적으로 이게 submit 되었을떄 add할지 edit할지는 이 컴포넌트를 가져다가 사용하는 사람이 결정하는게 좋다고 생각해서요ㅎㅎ;;

그래서 차라리 ArticleFormProps 에 onSubmit 메소드를 받는게 어떨까 싶어요!

Copy link
Collaborator Author

@cksrlcks cksrlcks Jan 17, 2025

Choose a reason for hiding this comment

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

form컴포넌트를 불러서 사용하는쪽에서 원하는 submit기능을 주입하도록 변경하는거네요. 감사합니다. ^^


const {
control,
formError,
handleSubmit,
formState: { isSubmitting, isValid },
} = useFormWithError<ArticleFormType>({
mode: "onBlur",
resolver: zodResolver(ArticleFormSchema),
defaultValues: initialData || {
title: "",
content: "",
},
});

async function onSubmit(data: ArticleFormType) {
try {
const result = await onFormSubmit(data);
const id = result?.id;

alert(
mode === "add" ? "성공적으로 작성했습니다." : "성공적으로 수정했습니다."
);
router.replace(id ? `/boards/${id}` : "boards");
} catch (err) {
throw err;
}
}

return (
<Section>
<Form
isLoading={isSubmitting}
error={formError}
onSubmit={handleSubmit(onSubmit)}
>
<Section.Header title="게시글 쓰기">
<Button type="submit" size="sm" disabled={!isValid}>
{mode === "add" ? "등록" : "수정"}
</Button>
</Section.Header>
<Section.Content>
<FieldItem>
<FieldItem.Label htmlFor="title">제목</FieldItem.Label>
<FieldAdapter
name="title"
control={control}
render={(props) => (
<Input
type="text"
placeholder="제목을 입력해주세요"
{...props}
/>
)}
/>
</FieldItem>
<FieldItem>
<FieldItem.Label htmlFor="content">내용</FieldItem.Label>
<FieldAdapter
name="content"
control={control}
render={(props) => (
<Textarea placeholder="내용을 입력해주세요" {...props} />
)}
/>
</FieldItem>
<FieldItem>
<FieldItem.Label htmlFor="image">이미지</FieldItem.Label>
<FieldAdapter
name="image"
control={control}
render={(props) => <ImageUpload {...props} />}
/>
</FieldItem>
</Section.Content>
</Form>
</Section>
);
}
38 changes: 38 additions & 0 deletions src/app/(common)/(board)/_components/BoardDetail.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.detail {
margin-bottom: 3.2rem;
}

.header {
margin-bottom: 1.6rem;
display: flex;
justify-content: space-between;

.title {
font-size: 2rem;
font-weight: 700;
word-break: break-all;
}
}

.meta {
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--color-secondary-200);
padding-bottom: 1.6rem;
margin-bottom: 2.4rem;

.controls {
margin-left: 3.2rem;
padding-left: 3.2rem;
border-left: 1px solid var(--color-secondary-200);
}
}

.image {
max-width: 24rem;
margin-bottom: 2.4rem;
}

.content {
font-size: 1.8rem;
}
99 changes: 99 additions & 0 deletions src/app/(common)/(board)/_components/BoardDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"use client";

import { Article } from "@/types/article";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import useArticleActions from "./useArticleActions";
import { More } from "@/components/Button";
import { Author, Fullscreen, LikeButton, Thumbnail } from "@/components/ui";
import styles from "./BoardDetail.module.scss";

interface BoardDetailProps {
detail: Article;
}

export default function BoardDetail({ detail }: BoardDetailProps) {
const {
id,
image,
title,
content,
writer: { nickname, id: ownerId },
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

updatedAt,
likeCount,
isLiked,
} = detail;

const { data: session } = useSession();
const router = useRouter();
const { handleLike, handleArticleDelete } = useArticleActions(id);
const isOwner = ownerId === Number(session?.user?.id);

async function handleToggleLike() {
if (!session?.user) {
return alert("로그인이 필요합니다.");
}
await handleLike(!isLiked);
router.refresh();
}

function handleModify() {
if (!isOwner) {
return alert("작성자만 수정이 가능합니다.");
}

router.push(`/modifyBoard/${id}`);
}

async function handleDelete() {
if (!isOwner) {
return alert("작성자만 삭제가 가능합니다.");
}

if (confirm("정말 삭제할까요?")) {
try {
await handleArticleDelete();
alert("상품을 삭제했습니다.");
router.replace("/boards");
} catch (err) {
console.log(err);
}
}
}

return (
<div className={styles.detail}>
<header className={styles.header}>
<h2 className={styles.title}>{title}</h2>
<div className={styles.controls}>
<More
options={[
{ label: "수정하기", action: handleModify },
{ label: "삭제하기", action: handleDelete },
]}
/>
</div>
</header>
<div className={styles.meta}>
<Author nickname={nickname} updatedAt={updatedAt} />
<div className={styles.controls}>
<LikeButton
count={likeCount}
isLiked={isLiked}
onClick={handleToggleLike}
/>
</div>
</div>
<div>
{image && (
<div className={styles.image}>
<Fullscreen>
<Thumbnail src={image} alt={title} />
</Fullscreen>
</div>
)}
<div className={styles.content}>{content}</div>
</div>
</div>
);
}
57 changes: 57 additions & 0 deletions src/app/(common)/(board)/_components/useArticleActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ArticleFormType } from "@/schemas/article";
import {
addArticle,
deleteArticle,
modifyArticle,
toggleLike,
uploadArticleImage,
} from "@service/article";

export default function useArticleActions(articleId?: number) {
async function handleLike(flag: boolean) {
if (!articleId) return;

return toggleLike(articleId, flag);
}

async function handleArticleAdd(formData: ArticleFormType) {
try {
if (formData.image instanceof File) {
const { url } = await uploadArticleImage(formData.image);
formData.image = url;
}

return await addArticle(formData);
} catch (err) {
throw err;
}
}

async function handleArticleModify(formData: ArticleFormType) {
if (!articleId) return;

try {
if (formData.image instanceof File) {
const { url } = await uploadArticleImage(formData.image);
formData.image = url;
}

return await modifyArticle(articleId, formData);
} catch (err) {
throw err;
}
}

async function handleArticleDelete() {
if (!articleId) return;

return deleteArticle(articleId);
}

return {
handleLike,
handleArticleAdd,
handleArticleModify,
handleArticleDelete,
};
}
9 changes: 7 additions & 2 deletions src/app/(common)/(board)/addBoard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Temporary } from "@/components/ui";
import { PageWrapper } from "@/components/Page";
import ArticleForm from "../_components/ArticleForm";

export default function AddBoardPage() {
return <Temporary title="게시글 작성 페이지" />;
return (
<PageWrapper>
<ArticleForm mode="add" />
</PageWrapper>
);
}
7 changes: 6 additions & 1 deletion src/app/(common)/(board)/boards/(lists)/@all/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getArticles } from "@/service/article";
import { Section } from "@/components/Section";
import BoardList from "../_components/BoardList";
import BoardFilter from "../_components/BoardFilter";
import { Button } from "@/components/ui";

type ItemsPageQueryParams = {
page?: string;
Expand All @@ -22,7 +23,11 @@ export default async function AllListPage({

return (
<Section>
<Section.Header title="게시글" />
<Section.Header title="게시글">
<Button href="/addBoard" size="sm">
글쓰기
</Button>
</Section.Header>
<Section.Content>
<BoardFilter />
<BoardList data={data} />
Expand Down
5 changes: 5 additions & 0 deletions src/app/(common)/(board)/boards/[id]/@comments/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Message } from "@/components/ui";

export default function Loading() {
return <Message>코멘트를 가져오는중입니다...</Message>;
}
Comment on lines +3 to +5
Copy link
Collaborator

Choose a reason for hiding this comment

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

오 loading.tsx 도 쓸줄아시는군요...! 👍

16 changes: 16 additions & 0 deletions src/app/(common)/(board)/boards/[id]/@comments/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Comments from "@/components/Comment/Comments";
import { getComments } from "@/service/comments";

export default async function ArticleCommentsPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const id = (await params).id;
const comments = await getComments("articles", {
id: Number(id),
limit: 5,
});

return <Comments name="articles" data={comments} />;
}
5 changes: 5 additions & 0 deletions src/app/(common)/(board)/boards/[id]/@detail/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Message } from "@/components/ui";

export default function Loading() {
return <Message>게시물 정보를 불러오는중입니다...</Message>;
}
Loading