Skip to content

[이승헌] sprint10 #660

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

Open
wants to merge 42 commits into
base: Next.js-이승헌
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1bf8fdd
remove: 초기 세팅 파일 중 필요없는 파일, 코드 제거
Jun 6, 2024
c6c8f11
chore: prettier 설치 및 설정
Jun 6, 2024
cfb3206
chore: tailwindcss 설치 및 설정
Jun 6, 2024
f7efed7
chore: import & tailwindcss 순서 정렬, import alias 설정
Jun 6, 2024
d043dd6
style: 기본 색상 추가
Jun 6, 2024
449a096
feat: 버튼 컴포넌트 구현
Jun 7, 2024
ae39b95
feat: 헤더 컴포넌트 구현
Jun 7, 2024
7ad8833
feat: 헤더를 모든 페이지에 적용
Jun 7, 2024
aa77fc8
style: 헤더 아래 여백 추가
Jun 7, 2024
d3c64b6
remove: 사용되지 않는 pages폴더의 api 삭제
Jun 7, 2024
32afc20
chore: axios 설치 및 초기 세팅
Jun 7, 2024
8d20bb1
feat: 자유게시판 특정 글 받아오는 api 구현
Jun 7, 2024
39e97dc
feat: html 메타 태그 전체 페이지에 적용
Jun 7, 2024
129ed7a
feat: 헤더 네비게이션 메뉴 표시 수정
Jun 7, 2024
fa69022
fix: html에 실수로 들어간 세미콜론 제거
Jun 7, 2024
6fd62ba
style: pre 태그 속 내용물 넘치지 않게 스타일 적용
Jun 8, 2024
1e1a8ec
chore: 외부 이미지를 사용하기 위한 next config 설정
Jun 8, 2024
31cf35d
feat: 게시판 댓글 관련 api 추가
Jun 8, 2024
20507c6
feat: 게시글 상세 페이지 구현
Jun 8, 2024
9b76206
refactor: Button Prop을 좀 더 상세한 타입으로 수정
Jun 8, 2024
d4a9a5d
feat: favicon 수정
Jun 8, 2024
13621d4
feat: 로그인, 회원가입 페이지는 헤더 표시 안함
Jun 8, 2024
4423751
feat: Pretendard 폰트 적용
Jun 8, 2024
73f32c9
style: 댓글 여백 수정
Jun 8, 2024
1e4cdb5
refactor: 반응형 브레이크포인트 변수로 관리
Jun 9, 2024
162d168
feat: 레이아웃 수정
Jun 9, 2024
f8b00b6
feat: 홈페이지에 footer 추가
Jun 9, 2024
e717ca2
refactor: 이미지 파일들은 public/images 폴더로 이동
Jun 9, 2024
207bf37
refactor: 로고만 있는 헤더를 컴포넌트로 분리
Jun 9, 2024
ee67b01
feat: 메인 페이지 구현
Jun 10, 2024
4f3ea82
style: 데스크탑 브레이크포인트 1280px로 변경
Jun 10, 2024
d92c5e5
style: 헤더 네비게이바 글자 수직 가운데 정렬
Jun 10, 2024
3780dab
feat: 게시글 댓글 없을 때 대체 UI 표시
heony704 Jun 12, 2024
ea5addb
style: 입력창 focus outline 스타일 수정
heony704 Jun 12, 2024
311b754
refactor: type들 따로 파일로 분리해서 관리
heony704 Jun 13, 2024
ef33223
refactor: import path 룰 수정
heony704 Jun 13, 2024
657456a
refactor: Comment 타입, Writer 타입 분리
heony704 Jun 13, 2024
27f2aac
refactor: 댓글 구분선을 댓글이 아닌 댓글 외부에서 스타일하도록 수정
heony704 Jun 13, 2024
6afda06
refactor: 댓글폼, 댓글, 목록으로 돌아가기 버튼을 컴포넌트로 분리
heony704 Jun 13, 2024
47a9bbe
style: 게시글과 댓글 사이에 가로선 추가
heony704 Jun 13, 2024
48763da
feat: 제품 상세 페이지 구현
heony704 Jun 13, 2024
ba2abd9
fix: 이미지 오류 수정
heony704 Jun 13, 2024
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
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": "next/core-web-vitals"
"extends": ["next/core-web-vitals", "prettier"]
}
30 changes: 30 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"arrowParens": "always",
"endOfLine": "lf",
"printWidth": 80,
"quoteProps": "as-needed",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",

"importOrder": [
"<THIRD_PARTY_MODULES>",

"^@/apis/(.*)$",
"^@/hooks/(.*)$",
"^@/components/(.*)$",
"^@/utils/(.*)$",
"^@/types/(.*)$",

"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,

"plugins": [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
]
}
16 changes: 16 additions & 0 deletions apis/board.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from './instance';

export async function getBoardById(id: number) {
const res = await axios.get(`/articles/${id}`);
return res.data;
}

export async function getBoardComments(boardId: number) {
const res = await axios.get(`/articles/${boardId}/comments?limit=100`);
return res.data.list;
}

export async function postBoardComment(boardId: number, content: string) {
const res = await axios.post(`/articles/${boardId}/comments`, { content });
return res.data;
}
7 changes: 7 additions & 0 deletions apis/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import axios from 'axios';

Copy link
Collaborator

Choose a reason for hiding this comment

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

axios.defaults.baseURL = "https://panda";
axios.defaults.headers.common['Content-Type'] = "application/json";
axios.defaults.timeout = 5000;

const instance = axios.create();

// [Client] ------ [interceptor] --------> [Server]
instance.interceptors.request.use( req => { }, err => { } );

// [Client] <------ [interceptor] -------- [Server]
instance.interceptors.response.use( res => { }, erro => { } );

const instance = axios.create({
baseURL: 'https://panda-market-api.vercel.app',
});

export default instance;
16 changes: 16 additions & 0 deletions apis/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from './instance';

export async function getProductById(id: number) {
const res = await axios.get(`/products/${id}`);
return res.data;
}

export async function getProductComments(productId: number) {
const res = await axios.get(`/products/${productId}/comments?limit=100`);
return res.data.list;
}

export async function postProductComment(productId: number, content: string) {
const res = await axios.post(`/products/${productId}/comments`, { content });
return res.data;
}
33 changes: 33 additions & 0 deletions components/BackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Url } from 'next/dist/shared/lib/router/router';
import Image from 'next/image';
import { useRouter } from 'next/router';

import Button from './Button';

type BackButtonProp = {
to: Url;
};

export default function BackButton({ to }: BackButtonProp) {
const { push } = useRouter();

const handleButtonClick = () => {
push(to);
};

return (
<Button
style={{ shape: 'rounded', size: 'large' }}
onClick={handleButtonClick}
>
목록으로 돌아가기
<Image
src="/images/ic_back.svg"
width={24}
height={24}
className="ml-[10px]"
alt="뒤로가기 아이콘"
/>
</Button>
);
}
28 changes: 28 additions & 0 deletions components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MouseEventHandler, ReactNode } from 'react';

type Props = {
style?: {
shape?: 'square' | 'rounded';
size?: 'small' | 'large';
};
disabled?: boolean;
onClick: MouseEventHandler<HTMLButtonElement>;
children: ReactNode;
};

Copy link
Collaborator

Choose a reason for hiding this comment

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

Button, Input 같이 최소 단위 컴포넌트 같은 경우, 보통 forwardRef 적용을 많이 합니다.

이유는 해당 tag의 native 이벤트(클릭, 포커스, 블러 등)를 컴포넌트 외부에서도 조정할 수 있게 끔 하기 위해서입니다.

interface ButtonProps extends PropsWithChildren {};
const Button = forwardRef<HTMLButtonElement, ButtonProps>( (props, ref) => {
   const {children} = props;
   return (
     <button ref={ref}>{children}</button> 
  )
})


// ./page or ./component
const DummyComponent= () => {
   const buttonRef = useRef<HTMLButtonElement>(null);
   const hanldeClickTrigger = () => {
      buttonRef.current?.click();
   }
   return ( 
      <>
       <Button ref={buttonRef} onClick={() => console.log("cliced by trigger-button")} />
       <button onClick={hanldeClickTrigger}>trigger-button</button> 
      <> 
    )
}

export default function Button({
style = { shape: 'square', size: 'small' },
disabled = false,
onClick,
children,
}: Props) {
return (
<button
className={`${style.size === 'small' ? 'h-[42px] px-6' : 'h-12 px-9'} ${style.shape === 'square' ? 'rounded-lg' : 'rounded-full'} flex cursor-pointer items-center justify-center bg-primary-400 text-base font-semibold text-white hover:bg-primary-600 active:bg-primary-700 disabled:cursor-default disabled:bg-gray-400`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
41 changes: 41 additions & 0 deletions components/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Image from 'next/image';

import { formatDateToTimeAgo } from '@/utils/formatDateToString';

import { BoardComment } from '@/types/board';

type CommentProp = {
comment: BoardComment;
};

export default function Comment({ comment }: CommentProp) {
const { content, createdAt, writer } = comment;

return (
<div className="relative my-4 flex flex-col tablet:my-6">
<Image
src="/images/ic_kebab.svg"
width={24}
height={24}
className="absolute right-0 top-0"
alt="댓글 메뉴 아이콘"
/>
<p className="text-sm font-normal text-gray-800">{content}</p>
<div className="mt-4 flex items-center tablet:mt-6">
<Image
src={writer.image ?? '/images/img_default_profile.svg'}
width={32}
height={32}
className="max-h-8 rounded-full"
alt="댓글쓴이 프로필 이미지"
/>
<div className="ml-2 flex flex-col">
<p className="text-xs font-normal text-gray-600">{writer.nickname}</p>
<p className="text-xs font-normal text-gray-400">
{formatDateToTimeAgo(new Date(createdAt))}
</p>
</div>
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions components/CommentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ChangeEvent, useState } from 'react';

import Button from '@/components/Button';

type CommentFormProp = {
placeholder?: string;
onSubmit: (inputValue: string) => void;
};

export default function CommentForm({
placeholder = '',
onSubmit,
}: CommentFormProp) {
const [value, setValue] = useState<string>('');

const handleTextareaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setValue(e.target.value);
};

const handleSubmitButtonClick = () => {
onSubmit(value);
setValue('');
};

return (
<>
<textarea
className="h-[104px] resize-none rounded-xl bg-gray-100 px-6 py-4 text-base font-normal text-gray-800 placeholder:text-gray-400"
placeholder={placeholder}
value={value}
onChange={handleTextareaChange}
/>
<div className="flex justify-end">
<Button
style={{ shape: 'square', size: 'small' }}
disabled={value === ''}
onClick={handleSubmitButtonClick}
>
등록
</Button>
</div>
</>
);
}
87 changes: 87 additions & 0 deletions components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Image from 'next/image';
import Link from 'next/link';

type Nav = {
name: string;
url: string;
};

type Social = {
name: string;
url: string;
iconSrc: string;
};

const NAVS: Nav[] = [
{
name: 'Privacy Policy',
url: '/privacy',
},
{
name: 'FAQ',
url: '/faq',
},
];

const SOCIALS: Social[] = [
{
name: '페이스북',
url: 'https://www.facebook.com/',
iconSrc: '/images/ic_facebook.svg',
},
{
name: '트위터',
url: 'https://twitter.com/?lang=ko',
iconSrc: '/images/ic_twitter.svg',
},
{
name: '유튜브',
url: 'https://www.youtube.com/',
iconSrc: '/images/ic_youtube.svg',
},
{
name: '인스타그램',
url: 'https://www.instagram.com/',
iconSrc: '/images/ic_instagram.svg',
},
];

export default function Footer() {
return (
<footer className="h-40 bg-gray-900">
<div className="m-auto flex h-full max-w-[1920px] flex-wrap-reverse items-end justify-between p-8 text-base font-normal tablet:px-[104px] desktop:px-[200px]">
<div className="grow basis-full self-start text-gray-400 tablet:grow-0 tablet:basis-auto tablet:self-auto">
©codeit - 2024
</div>
<div className="flex gap-[30px]">
{NAVS.map((nav) => (
<Link
href={nav.url}
className="text-gray-200 active:text-primary-400"
key={nav.name}
>
{nav.name}
</Link>
))}
</div>
<div className="flex gap-3">
{SOCIALS.map((social) => (
<Link
href={social.url}
target="_blank"
rel="noopener noreferrer"
key={social.name}
>
<Image
width={20}
height={20}
src={social.iconSrc}
alt={`${social.name} 아이콘`}
/>
</Link>
))}
</div>
</div>
</footer>
);
}
45 changes: 45 additions & 0 deletions components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useRouter } from 'next/router';

import Footer from './Footer';
import LogoHeader from './LogoHeader';
import MainHeader from './MainHeader';

type Props = {
children: React.ReactNode;
};

export default function Layout({ children }: Props) {
const { asPath } = useRouter();

if (asPath === '/') {
return (
<>
<MainHeader />
<main className={`mt-[70px]`}>{children}</main>
<Footer />
</>
);
}

if (asPath === '/signin' || asPath === '/signup') {
return (
<>
<LogoHeader />
<main className="m-auto px-4 pb-20 pt-6 tablet:px-12 tablet:pb-24 desktop:px-16 desktop:pb-16">
{children}
</main>
</>
);
}

return (
<>
<MainHeader />
<main
className={`m-auto mt-[70px] px-4 pb-24 pt-8 tablet:px-6 desktop:max-w-[1200px] desktop:px-0`}
>
{children}
</main>
</>
);
}
19 changes: 19 additions & 0 deletions components/LogoHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Image from 'next/image';
import Link from 'next/link';

export default function LogoHeader() {
return (
<header className="flex justify-center pt-6 tablet:pt-12 desktop:pt-16">
<Link href="/">
<Image
width={198}
height={66}
className="tablet:h-[132px] tablet:w-[396px]"
src="/images/logo_big.svg"
alt="판다마켓 로고"
priority
/>
</Link>
</header>
);
}
Loading