Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 1 addition & 3 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// output: "export",
reactStrictMode: true,
experimental: {
appDir: true, // ✅ App Router 활성화 (Next 13 이상)
},
images: {
remotePatterns: [
{
Expand Down
19 changes: 18 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"framer-motion": "^12.7.4",
"next": "^15.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-hook-form": "^7.56.3"
},
"devDependencies": {
"@types/node": "^20.17.30",
Expand Down
4 changes: 4 additions & 0 deletions public/assets/ic_medal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions public/assets/reply_empty.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 98 additions & 0 deletions src/app/addboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';
import Container from '@/components/layout/Container';
import Button from '@/components/ui/Button';
import ConfirmModal from '@/components/ui/ConfirmModal';
import FormField from '@/components/ui/form/FormField';
import ImageFileBox from '@/components/ui/form/ImageFileBox';
import { TextAreaField } from '@/components/ui/form/InputBox';
import Title from '@/components/ui/Title';
import { ArticleCreateRequest, usePostArticles } from '@/hooks/useArticles';
import { useConfirmModal } from '@/hooks/useModal';
import { validationRules } from '@/utils/validate';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';


const INITIAL_Article: ArticleCreateRequest = {
image: "",
content: "",
title: ""
}
type FormValues = {
title: string;
content: string;
image: string;
};
function PostArticles() {
const router = useRouter();
const { isConfirmOpen, confirmMessage, openConfirmModal, closeConfirmModal } = useConfirmModal();

const {
register,
handleSubmit,
getValues,
formState: { errors, isValid, isDirty },
} = useForm<FormValues>({
mode: 'onBlur',
});

const [addArticles, setAddArticles] = useState<ArticleCreateRequest>(INITIAL_Article);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이미 react-hook-form 을 사용중이라 중복해서 상태를 다시 정의할 필요 없습니다! 🤔
우리가 react-hook-form 사용하는 이유중 하나는 uncontrolled로 form을 관리하기 때문입니다. 직접 상태를 관리하실 필요 없는거죠!

controlled/uncontrolled 컴포넌트 개념을 좀 더 알아보셔도 좋습니다 :)


const { mutate: postArticles} = usePostArticles(openConfirmModal,router);

const handleFieldBlur = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

위와 마찬가지로 불필요한 함수입니다!

const values = getValues(); // 모든 필드 값 가져오기
setAddArticles((prev) => {
const updated = { ...prev, ...values };
return updated;
});
};

const onSubmit: SubmitHandler<FormValues> = (data) => {
setAddArticles((prev) => ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

애초에 불필요한 코드이기도 하지만, 다시 생각해 보시면 좋은 것은, setState는 비동기적으로 동작하죠!
여기서 setAddArticles 한다고 해도 addArticles 상태 값은 바로 변하지 않습니다!

아래에서 실행되는 postArticles(addArticles); 는 이전의 addArticles를 넣고 있는거죠 :)

...prev,
...addArticles, // form에서 온 title, content 등
}));
postArticles(addArticles);
};

function handleInputBlur(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>){
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분도 지워도 될 거 같네요 :)

const value = e.target.value;
setAddArticles((prev) => ({
...prev,
[e.target.id]: value
}));
}

return (
<Container className="relative mb-[130px]">
<Title titleTag='h1' text='게시물 쓰기'>
</Title>
<form className='flex flex-col gap-6' onSubmit={handleSubmit(onSubmit)}>
<Button
type="submit"
variant="roundedSS"
className="!absolute top-0 right-0"
disabled = { !isValid || !isDirty || !addArticles.content || !addArticles.image || !addArticles.title }
Copy link
Collaborator

Choose a reason for hiding this comment

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

required 옵션, validation을 잘 넣어주셨다면 !isValid 로도 충분할 거 같네요! :)

>등록</Button>
<FormField
id="title"
label="*제목"
type="text"
placeholder="제목을 입력해주세요"
error={errors.title?.message}
{...register('title', {
...validationRules.title,
Copy link
Collaborator

Choose a reason for hiding this comment

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

유효성 검사를 하실때 react-hook-form/tpyescript 와 zod 궁합이 좋습니다~! 한 번 써보셔도 좋을 거 같아요 :)

onBlur: handleFieldBlur,
})}
/>
<TextAreaField id='content' label='내용' height='282px' placeholder='내용를 입력해주세요' onBlur={handleInputBlur} />
<ImageFileBox<ArticleCreateRequest> setForm={setAddArticles} />
</form>
<ConfirmModal isOpen={isConfirmOpen} onClose={closeConfirmModal} errorMessage={confirmMessage} />
</Container>
);
}

export default PostArticles;
65 changes: 65 additions & 0 deletions src/app/boards/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import CommentSection from '@/components/Article/comment/CommentSection';
import Container from '@/components/layout/Container';
import ConfirmModal from '@/components/ui/ConfirmModal';
import LikeButton from '@/components/ui/LikeButton';
import UserInfo from '@/components/ui/UserInfo';
import { useArticleDetails, useToggleArticlesFavorite } from '@/hooks/useArticles';
import { useConfirmModal } from '@/hooks/useModal';
import { formatDate } from '@/utils/date';
import { useParams } from 'next/navigation';
import React from 'react';

function PostDetail() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

next를 좀 더 적극적으로 써보시면 좋을 거 같아요! 특히나 상세 페이지의 경우 정적 페이지로 만들기가 좋죠!!


const { id } = useParams(); // URL에서 [id] 추출
const articleId = Number(id);

const { isConfirmOpen, confirmMessage, openConfirmModal, closeConfirmModal } = useConfirmModal();
const { mutate: toggleFavorite } = useToggleArticlesFavorite(openConfirmModal, {
onSuccess: (data) => {
openConfirmModal(data.isFavorited ? "관심상품 등록되었습니다" : "관심상품 취소되었습니다");
},
});
const { data } = useArticleDetails(articleId);
if ( data === undefined) return;

const createdAtString = formatDate( data.createdAt );

return (
<div>
<div className='flex flex-col mt-[32px]'>
<Container className='flex flex-col w-full gap-6 '>
{ data && (
<>
<div className="flex flex-col gap-4 border-b border-b-secondary-200 pb-4">
<strong className="text-xl font-bold">{data.title}</strong>
<div className="flex flex-row gap-6 items-center">
<UserInfo ownerNickname={data.writer.nickname} createdAtString={createdAtString} width={40} className="!text-[18px]" childrenClassName=" !flex-row"/>
<span className='w-[1px] h-[34px] bg-secondary-200'></span>
<LikeButton
id={data.id}
favoriteCount={data.likeCount}
toggleFavorite={toggleFavorite}
isFavorite={false}
className="border rounded-full border-secondary-200 py-[7px] px-[12px]"
childrenClassName='gap-0 text-[16px]' width="24" height="24"/>
</div>
</div>
<div>
<span>{data.content}</span>
</div>
</>
)}
</Container>
<Container className="mb-[151px] mt-8">
<CommentSection articleId={articleId} />
</Container>
<ConfirmModal isOpen={isConfirmOpen} onClose={closeConfirmModal} errorMessage={confirmMessage} />
</div>
</div>
);
}

export default PostDetail;
17 changes: 4 additions & 13 deletions src/app/boards/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import BoardsClient from "@/components/Article/BoardsClient";



import React from 'react';

function Boards() {
return (
<>
<h2>Boards</h2>
</>
);
}

export default Boards;
export default function BoardsPage() {
return <BoardsClient />;
}
1 change: 1 addition & 0 deletions src/app/faq/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import React from 'react';

function Faq() {
Expand Down
3 changes: 0 additions & 3 deletions src/app/items/[id]/ItemsDetail.module.css

This file was deleted.

31 changes: 24 additions & 7 deletions src/app/items/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
'use client';
import React from 'react';
import styles from './ItemsDetail.module.css';
import ProductDetails from '@/components/ItemsDetail/ProductDetails';
import { useParams } from 'next/navigation';
import ProductDescription from '@/components/productDetail/ProductDescription';
import ProductOverview from '@/components/productDetail/ProductOverview';
import Container from '@/components/layout/Container';
import CommentSection from '@/components/productDetail/comment/CommentSection';
import { useProductsDetails } from '@/hooks/useItems';

function ItemsDetail() {

const { id } = useParams(); // URL에서 [id] 추출
const productId = Number(id);

const { data } = useProductsDetails(productId);

return (
<>
<div className={styles.items_detail}>
<ProductDetails />
</div>
</>
<div className='flex flex-col mt-8'>
<Container className='flex flex-row w-full gap-6 pb-[40px] mb-[40px] border-b border-b-[var(--Cool_Gray_200)] mobile:flex-col'>
{ data && (
<>
<ProductOverview img={data?.images}/>
<ProductDescription {...data} />
</>
)}
</Container>
<Container>
<CommentSection productId={productId} />
</Container>
</div>
);
}

Expand Down
7 changes: 3 additions & 4 deletions src/app/items/apply/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
'use client';

import React, { useEffect } from 'react';
import React from 'react';
import { useState } from 'react';
import styles from './Additem.module.css';
import Container from 'components/layout/Container';
import Button from 'components/ui/Button';
import Title from 'components/ui/Title';
import { InputField, TextAreaField } from '@/components/ui/form/InputBox';
import TagBox from '@/components/ui/TagBox';
import { CreateProductRequest, ProductSummary, usePostProduct } from '@/hooks/useItems';
import { CreateProductRequest, usePostProduct } from '@/hooks/useItems';
import ImageFileBox from '@/components/ui/form/ImageFileBox';
import { useConfirmModal } from '@/hooks/useModal';
import ConfirmModal from '@/components/ui/ConfirmModal';
Expand Down Expand Up @@ -53,7 +52,7 @@ function Additem() {
</Title>

<form className={styles.formBox}>
<ImageFileBox product={addProduct} setProduct={setAddProduct}/>
<ImageFileBox<CreateProductRequest> setForm={setAddProduct} />
<InputField id='name' label='상품명' inputBoxType='text' placeholder='상품명을 입력해주세요' onBlur={handleInputBlur} />
<TextAreaField id='description' label='상품 소개' height='282px' placeholder='상품 소개를 입력해주세요' onBlur={handleInputBlur} />
<InputField id='price' label='판매가격' inputBoxType='number' placeholder='판매 가격을 입력해주세요' onBlur={handleInputBlur} />
Expand Down
26 changes: 4 additions & 22 deletions src/app/items/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
'use client';
import ProductClient from "@/components/Product/ProductClient";

import React from 'react';
import Container from 'components/layout/Container';
import Title from 'components/ui/Title';
import { BestItems } from '@/components/Product/BestItems';
import { AllItems } from '@/components/Product/AllItems';


function ItemsBox() {

return (
<>
<Container>
<Title titleTag='h1' text='베스트 상품' />
</Container>
<BestItems />
<AllItems />
</>
);
}

export default ItemsBox;
export default function BoardsPage() {
return <ProductClient />;
}
Loading
Loading