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
18 changes: 15 additions & 3 deletions components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import clsx from "clsx";
import { ReactNode } from "react";
//
interface Props {
onClick: onClick;
onClick?: onClick;
Copy link
Collaborator

Choose a reason for hiding this comment

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

(선택) 타입은 보통 파스칼 케이스로 사용하는게 일반적이예요 !

Suggested change
onClick?: onClick;
onClick?: OnClick;

global.d.ts에서 위와 같이 명칭을 바꾸는건 어떨까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

또한, 해당 타입은 필요 없을 수도 있겠네요 !

Suggested change
onClick?: onClick;

(이어서)

children: ReactNode;
disabled?: boolean;
className?: string;
}
export default function Button({ onClick, children, ...props }: Props) {
export default function Button({
onClick,
children,
className,
disabled,
...props
}: Props) {
return (
<>
<button
onClick={onClick}
className="flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer"
className={clsx(
"flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer",
disabled ? "bg-gray-400" : "",
className
)}
Comment on lines +10 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

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

(이어서) onClick을 지우고 다음과 같이 작성해볼 수 있겠어요

Suggested change
export default function Button({
onClick,
children,
className,
disabled,
...props
}: Props) {
return (
<>
<button
onClick={onClick}
className="flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer"
className={clsx(
"flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer",
disabled ? "bg-gray-400" : "",
className
)}
export default function Button({
children,
className,
disabled,
...props
}: Props) {
return (
<>
<button
className={clsx(
"flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer",
disabled ? "bg-gray-400" : "",
className
)}

Copy link
Collaborator

Choose a reason for hiding this comment

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

추가로 disabled도 지워볼 수 있을 것 같네요 !:

export default function Button({
  children,
  className,
  ...props
}: Props) {
  return (
    <>
      <button
        disabled={disabled}
        className={clsx(
          "flex  bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer disabled:bg-gray-400", className
        )}

disabled를 버튼의 속성으로 넣게 되면 덩달아 접근성도 좋아지겠어요 !
또한, disabled를 상태 선택자를 통해서 스타일도 입힐 수 있구요 😊😊

>
{children}
</button>
Expand Down
24 changes: 11 additions & 13 deletions components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import ProfileImg from "@/public/assets/icons/default.profile.icon.svg";
import { useFormatDate } from "@/hooks/useFormatting";

import BtnHeart from "./BtnHeart";
import Profile from "./common/Profile";
//
interface Props {
article: Article;
isDetailArticle?: boolean;
}
export function BestArticle({ article }: Props) {
const formattedDate = useFormatDate(article.createdAt);
Expand Down Expand Up @@ -41,27 +43,23 @@ export function BestArticle({ article }: Props) {
);
}

export function Articles({ article }: Props) {
const formattedDate = useFormatDate(article.createdAt);
export function Articles({ article, isDetailArticle }: Props) {
const articleImg = article.image || ProfileImg;

return (
<div className=" flex flex-col gap-4 w-full h-[138px] bg-light-gray px-6 border-b border-gray-200">
<div className="flex justify-between font-Pretendard text-H3Bold">
<p>{article.content}</p>
<img
className="w-[72px] h-[72px] text-H8"
src={articleImg}
alt="상품이미지"
/>
{!isDetailArticle && (
<img
className="w-[72px] h-[72px] text-H8"
src={article.image || ProfileImg}
alt="상품이미지"
/>
)}
</div>
<div className="flex justify-between text-gray-500 font-Pretendard text-H7Regular ">
<div className="flex gap-2">
<Image src={ProfileImg} width={24} height={24} alt="프로필" />

<div>{article.writer.nickname}</div>
{formattedDate}
</div>
<Profile value={article} />
<BtnHeart value={article.likeCount} />
</div>
</div>
Expand Down
124 changes: 74 additions & 50 deletions components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import PlusIcon from "@/public/assets/icons/plusIcon.svg";
import DeleteIcon from "@/public/assets/icons/DeleteIcon.svg";
import SearchIcon from "@/public/assets/icons/search.icon.svg";
import Image from "next/image";
import { ChangeEvent, useRef, useState } from "react";
import { useRef, useState } from "react";
import { uploadImage } from "@/lib/image";
import { renameFile } from "@/hooks/useRenameFile";
//

interface Props {
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onChange: (value: string) => void;
label?: string;
placeholder?: string;
name: string;
Expand All @@ -17,8 +19,16 @@ interface Props {
$edit?: boolean;
onKeyUp?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
type?: "text" | "number";
isAsterisk?: boolean;
}
export function Input({ onChange, type = "text", ...props }: Props) {

export function Input({
onChange,
placeholder,
type = "text",
isAsterisk,
...props
}: Props) {
const { label, onKeyUp, $edit, $textArea, $comment } = props;

return (
Expand All @@ -28,6 +38,7 @@ export function Input({ onChange, type = "text", ...props }: Props) {
$comment && "text-H5Bold"
} w-full h-[26px] font-Pretendard text-gray-800 text-H4Bold`}
>
{isAsterisk && <span>*</span>}
{label}
</div>
<input
Expand All @@ -37,78 +48,89 @@ export function Input({ onChange, type = "text", ...props }: Props) {
$edit && "text-H7Regular h-[80px]"
}relative w-full h-[56px] bg-gray-100 font-Pretendard text-H6Regular border-none rounded-xl text-gray-800 px-4 py-6 placeholder-gray-400 placeholder:text-H5Regular placeholder:absolute focus:outline-none`}
type={type}
onChange={onChange}
onChange={(e) => {
if (e.nativeEvent.composed) return onChange(e.target.value);
}}
onKeyUp={onKeyUp || undefined}
placeholder={placeholder}
/>
</div>
);
}

//
interface ImgProps extends Omit<Props, "onChange"> {
onChange: (value: File | string) => void;
onChange: (value: null | string) => void;
}
export function ImgInput({ onChange, ...props }: ImgProps) {
export function ImgInput({ onChange, label, ...props }: ImgProps) {
const imgRef = useRef<HTMLInputElement>(null);
const [imgPreview, setImgPreview] = useState<string>("");

const handlePreviewImg = () => {
if (!imgRef.current || !imgRef.current.files?.length) return;
const file = imgRef.current.files[0];

const renamedFile = renameFile(file);

const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
reader.readAsDataURL(renamedFile);

reader.onloadend = async () => {
if (typeof reader.result === "string") {
setImgPreview(reader.result);
onChange(file);
const formattedImage = await uploadImage(renamedFile);
onChange(formattedImage);
}
};
};
//

const handleClickImgDelete = () => {
setImgPreview("");
onChange("");
onChange(null);
};

return (
<div className="flex gap-6 mobile:gap-[10px] ">
<div className="mobile:w-[168px] w-[282px] h-[282px] bg-gray-100 rounded-xl border-none text-gray-800">
<input
className="relative w-full h-full bg-gray-100 font-Pretendard text-H6Regular border-none rounded-xl text-gray-800 px-4 py-6
<div className="flex flex-col gap-6 mobile:gap-[10px] ">
<p className="text-[18px] font-Pretendard text-H4Bold">{label}</p>
Copy link
Collaborator

Choose a reason for hiding this comment

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

<p> 태그는 단락을 나타냅니다 !

HTML <p> 요소는 하나의 문단을 나타냅니다. 시각적인 매체에서, 문단은 보통 인접 블록과의 여백과 첫 줄의 들여쓰기로 구분하지만, HTML에서 문단은 이미지나 입력 폼 등 서로 관련있는 콘텐츠 무엇이나 될 수 있습니다.

예를 들어 다음과 같은 값이 <p>태그에 적절할 수 있어요 😊:

Geckos are a group of usually small, usually nocturnal lizards. They are found on every continent except Antarctica.

✨ 대체 제안: <label> 태그로 변경해보세요!

Suggested change
<p className="text-[18px] font-Pretendard text-H4Bold">{label}</p>
<label htmlFor="fileUpload" className="text-[18px] font-Pretendard text-H4Bold">
{label}
</label>

혹은 이 텍스트가 진짜 단순한 스타일용 텍스트라면 도 괜찮아요. 하지만 입력 폼의 설명이나 타이틀 역할을 하고 있다면 <label>을 쓰는 걸 추천드려요!

<div className="relative overflow-hidden gap-6 w-[282px] mobile:w-[168px] mobile:h-[168px] h-[282px] bg-gray-100 rounded-xl border-none text-gray-800">
{!imgPreview ? (
<>
<input
className="hidden w-full h-full font-Pretendard text-H6Regular border-none rounded-xl text-gray-800 px-4 py-6
placeholder-gray-400 placeholder:text-H5Regular placeholder:top-[10px] placeholder:text-left placeholder:absolute focus:outline-none"
type="file"
id="fileUpload"
accept="image/*"
ref={imgRef}
onChange={handlePreviewImg}
{...props}
/>
<label
htmlFor="fileUpload"
className="absolute text-center text-gray-400 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-H5Regular"
>
<Image width={48} height={48} src={PlusIcon} alt="추가" />
<p>{props.placeholder}</p>
</label>
type="file"
id="fileUpload"
accept="image/*"
ref={imgRef}
onChange={handlePreviewImg}
{...props}
/>
<label
htmlFor="fileUpload"
className="absolute flex flex-col items-center justify-center text-gray-400 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-H5Regular"
>
<Image width={48} height={48} src={PlusIcon} alt="추가" />
<p className="text-center">{props.placeholder}</p>
</label>
</>
) : (
<div className="relative">
<img
className="z-10 w-full rounded-[6px] object-fill aspect-square"
src={imgPreview}
alt="이미지 미리보기"
/>
<Image
className="absolute top-[15px] right-[15px]"
src={DeleteIcon}
width={22}
height={24}
alt="x"
onClick={handleClickImgDelete}
/>
</div>
)}
</div>

{imgPreview && (
<div className="relative">
<Image
className=" aspect-square w-[282px] h-[282px] mobile:w-[168px] "
src={imgPreview}
width={282}
height={282}
alt="이미지 미리보기"
/>
<Image
className="absolute top-[15px] right-[15px]"
src={DeleteIcon}
width={22}
height={24}
alt="x"
onClick={handleClickImgDelete}
/>
</div>
)}
</div>
);
}
Expand All @@ -117,7 +139,9 @@ export function SearchInput({ onChange, ...props }: Props) {
return (
<div className="relative w-full">
<input
onChange={onChange}
onChange={(e) => {
onChange(e.target.value);
}}
className="
rounded-xl border-none px-[44px] py-[16px] w-full h-[42px] bg-gray-100
font-Pretendard text-gray-800 text-H5Regular
Expand Down
29 changes: 23 additions & 6 deletions components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@ import pandaLogo from "@/public/assets/Logo/pandaLogo.svg";
import textLogo from "@/public/assets/Logo/textLogo.svg";
import myLogo from "@/public/assets/icons/default.profile.icon.svg";
import Image from "next/image";
import useWindowSize from "@/hooks/useWindowSize";
import { useEffect, useState } from "react";
import Button from "./Button";
import { signIn } from "@/lib/auth";

export default function Nav() {
const device: string = useWindowSize();
const [isLogin, setIsLogin] = useState(false);
useEffect(() => {
const hasAccessToken = Boolean(localStorage.getItem("accessToken"));
setIsLogin(hasAccessToken);
}, []);

const handleClickLogin = async () => {
await signIn();
setIsLogin(true);
};
Comment on lines +12 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

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

isLogin이라는 상태는 nav 뿐 아니라 여러 곳에서 사용될 수 있겠네요 !

이럴 때, Context API와 훅을 하나 만들어두는건 어떨까요? 추가로 isLoggin 보다 userData 상태를 가지고 있어도 되겠어요. 😊

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    const token = localStorage.getItem("accessToken");
    if (token) {
      const userData = await getMy(); // 예시 입니다 !
      setUserData({ name: userData.name, email: userData.email }); // 예시 입니다 !
    }
  }, []);

  const signIn = async () => {
    const token = await login(); // 기존 코드에 `signIn` 함수와 같음
    localStorage.setItem("accessToken", token);
    const userData = await getMy(); // 예시 입니다 !
    setUserData({ name: userData.name, email: userData.email }); // 예시 입니다 !
  };

  // 추가로 signOut 함수도 필요하겠어요 !

  return (
    <AuthContext.Provider value={{ userData, signIn }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);


return (
<div className="fixed top-0 left-0 w-screen h-[70px] bg-white py-[10px] px-[200px] NavPadding:px-[24px] NavPadding:py-[10px] mobile:px-[15px] mobile:py-[15px] z-50">
<div className="flex justify-between w-screen max-w-full ">
Expand All @@ -27,19 +40,23 @@ export default function Nav() {
/>
</div>
<div className="flex w-auto gap-7 mobile:gap-2 font-Pretendard text-H4Bold">
<Link href="/docs">자유게시판</Link>
<Link href="/items">중고마켓</Link>
<Link href="/">자유게시판</Link>
<Link href="/">중고마켓</Link>
</div>
</div>
<Link href="/mypage">
{!isLogin ? (
<div className="w-[128px] h-12">
<Button onClick={() => handleClickLogin()}>로그인</Button>
</div>
) : (
<Image
className="w-10 h-10"
src={myLogo}
alt="프로필 이미지"
width={40}
height={40}
/>
</Link>
)}
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion components/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function EditSelect({ onChange, ...props }: Props) {
onClick={() => setIsOpen(!isOpen)}
className="h-6 bg-white border-none cursor-pointer"
>
<img src={kebabIcon} alt="케밥" />
<Image src={kebabIcon} width={24} height={24} alt="케밥" />
</div>
<ul
className={`${
Expand Down
55 changes: 55 additions & 0 deletions components/board/comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { KeyboardEvent, useState } from "react";
import Profile from "../common/Profile";
import { EditSelect } from "../Select";
import { deleteArticleComment, editArticleComment } from "@/lib/comments.api";

interface Props {
comment: Comment;
}
export default function Comment({ comment }: Props) {
const [isEdit, setIsEdit] = useState(false);
const [currentValue, setCurrentValue] = useState(comment.content);
const handleSelect = async (option: string) => {
switch (option) {
case "수정하기": {
setIsEdit(true);
break;
}
case "삭제하기": {
await deleteArticleComment(comment.id);
break;
}
}
};
const handleChange = async (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
await editArticleComment(comment.id, currentValue);
setCurrentValue(currentValue);
}
};
return (
<>
<div className="flex flex-col w-full gap-6 mb-3 h-fit">
<div className="flex justify-between w-full">
{isEdit ? (
<input
className="w-full"
value={currentValue}
onChange={(e) => {
setCurrentValue(e.currentTarget.value);
}}
onKeyDown={(e) => handleChange(e)}
/>
) : (
<p>{currentValue}</p>
)}
<EditSelect onChange={(option) => handleSelect(option)} />
</div>
<div>
<Profile isComment value={comment} />
</div>
</div>
<div className="w-full h-px bg-gray-300" />
</>
);
}
Loading
Loading