Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9e8689d
♻️ refactor(mentor): Item 외부 태그 Link로 변경
Moon-ju-young Jun 1, 2025
6e8f0e2
📦 build: typescript 추가
Moon-ju-young Jun 1, 2025
ef6c045
📦 build: tsconfig 파일 수정
Moon-ju-young Jun 2, 2025
55cc07a
📦 build: build command 수정
Moon-ju-young Jun 2, 2025
fedb887
✨ feat: ts module 설정 파일 추가
Moon-ju-young Jun 2, 2025
256e81c
🐛 fix: 내용 같은 태그 key 중복 오류 해결
Moon-ju-young Jun 2, 2025
4852f5c
🐛 fix: 공백 태그 생성 방지
Moon-ju-young Jun 2, 2025
c13d5cc
🐛 fix: 태그 입력 시 isComposing 확인
Moon-ju-young Jun 2, 2025
aa7a4b1
♻️ refactor: main 파일 jsx > tsx 변경
Moon-ju-young Jun 2, 2025
232d126
♻️ refactor: App 파일 jsx > tsx 변경
Moon-ju-young Jun 2, 2025
6145429
✨ feat: getProducts 함수 keyword parameter 기본값 추가
Moon-ju-young Jun 2, 2025
69d3a32
♻️ refactor: Inpex Page 파일 jsx > tsx 변경
Moon-ju-young Jun 2, 2025
d029caa
♻️ refactor: Auth 파일 jsx > tsx 변경
Moon-ju-young Jun 2, 2025
d74651a
♻️ refactor: Login Signup Page 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
285ef71
♻️ refactor: Input Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
8cd8d53
♻️ refactor: AuthInput Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
24a3734
🐛 fix: prop 전체 optional로 변경
Moon-ju-young Jun 3, 2025
4ae723b
🐛 fix: PasswordInput label prop 누락 수정
Moon-ju-young Jun 3, 2025
88ee07f
♻️ refactor: Button Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
64f9003
♻️ refactor: Nav Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
8824a67
♻️ refactor: SimpleLogin Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
2e342bd
♻️ refactor: Items Page 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
f434aa2
♻️ refactor: Tag Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
ec8e128
♻️ refactor: AddItem page 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
f5db528
✨ feat: apiTypes 파일 추가
Moon-ju-young Jun 3, 2025
cdb81ed
♻️ refactor: api 함수 파일 js > ts 변경
Moon-ju-young Jun 3, 2025
71bd156
🐛 fix: Items state type 설정
Moon-ju-young Jun 3, 2025
3b5984e
♻️ refactor: Items mode 구체적 type 설정
Moon-ju-young Jun 3, 2025
1988541
♻️ refactor: Dropdown Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
df91800
♻️ refactor: ButtonHeart Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
2f706ea
♻️ refactor: DropdownSort Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
c85b125
♻️ refactor: ItemList Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
1345370
♻️ refactor: Pagenation Component 파일 jsx > tsx 변경
Moon-ju-young Jun 3, 2025
4349be0
♻️ refactor: ItemProduct Page 파일 jsx > tsx 변경
Moon-ju-young Jun 4, 2025
8f01942
🐛 fix: 파일 업로드 버그 수정
Moon-ju-young Jun 4, 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
59 changes: 50 additions & 9 deletions package-lock.json

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

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
Expand All @@ -17,13 +17,15 @@
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/node": "^22.15.29",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "^5.8.3",
"vite": "^6.2.0"
}
}
14 changes: 7 additions & 7 deletions src/App.jsx → src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Index from './pages/Index.jsx';
import Items from './pages/Items.jsx';
import ItemProduct from './pages/ItemProduct.jsx';
import AddItem from './pages/AddItem.jsx';
import Auth from './pages/Auth.jsx';
import Login from './pages/Login.jsx';
import Signup from './pages/Signup.jsx';
import Index from './pages/Index';
import Items from './pages/Items';
import ItemProduct from './pages/ItemProduct';
import AddItem from './pages/AddItem';
import Auth from './pages/Auth';
import Login from './pages/Login';
import Signup from './pages/Signup';
import './reset.css';
import './color.css';

Expand Down
20 changes: 0 additions & 20 deletions src/api/api.js

This file was deleted.

31 changes: 31 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Comments, Product, Products } from "./apiTypes";

const BASE_URL = "https://panda-market-api.vercel.app";

export async function getProducts({ page=1, pageSize=10, orderBy="recent", keyword='' }: {
page?: number;
pageSize?: number;
orderBy?: "recent" | "favorite";
keyword?: string;
}): Promise<Products> {
const response = await fetch(`${BASE_URL}/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}${keyword && "&keyword="+keyword}`);
if (!response.ok) { throw Error("Request Error"); }
return (await response.json()) as Products;
}
Comment on lines +5 to +14
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 여담
keyword는 옵셔널 파람인데 기본값으로 빈 문자열을 줄 필요가 있을까요?
위의 코드도 동작상 문제가 없을 것 같지만 가독성 측면에서 기본값이 없는것이 더 좋을 것 같아요~

Suggested change
export async function getProducts({ page=1, pageSize=10, orderBy="recent", keyword='' }: {
page?: number;
pageSize?: number;
orderBy?: "recent" | "favorite";
keyword?: string;
}): Promise<Products> {
const response = await fetch(`${BASE_URL}/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}${keyword && "&keyword="+keyword}`);
if (!response.ok) { throw Error("Request Error"); }
return (await response.json()) as Products;
}
export async function getProducts({ page=1, pageSize=10, orderBy="recent", keyword }: {
page?: number;
pageSize?: number;
orderBy?: "recent" | "favorite";
keyword?: string;
}): Promise<Products> {
const response = await fetch(`${BASE_URL}/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}${keyword && "&keyword="+keyword}`);
if (!response.ok) { throw Error("Request Error"); }
return (await response.json()) as Products;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

기본값을 주지 않으면 query parameter에 그대로 undefined가 뒤에 붙어서 기본값을 줬습니다~

Comment on lines +5 to +14
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
export async function getProducts({ page=1, pageSize=10, orderBy="recent", keyword='' }: {
page?: number;
pageSize?: number;
orderBy?: "recent" | "favorite";
keyword?: string;
}): Promise<Products> {
const response = await fetch(`${BASE_URL}/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}${keyword && "&keyword="+keyword}`);
if (!response.ok) { throw Error("Request Error"); }
return (await response.json()) as Products;
}
export async function getProducts({ page=1, pageSize=10, orderBy="recent", keyword='' }: {
page?: number;
pageSize?: number;
orderBy?: "recent" | "favorite";
keyword?: string;
}): Promise<Products> {
const response = await fetch(`${BASE_URL}/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}${keyword && "&keyword="+keyword}`);
if (!response.ok) {
const errorText = await response.text(); // 서버에서 온 응답을 활용
throw new Error(`Failed to fetch products: ${response.status} - ${errorText}`);
}
const data: Products = await response.json();
return data;
}


export async function getProduct({ productId }: {
productId: number | string;
}): Promise<Product> {
const response = await fetch(`${BASE_URL}/products/${productId}`);
if (!response.ok) { throw Error("Request Error"); }
return (await response.json()) as Product;
}

export async function getProductComments({ productId, limit }: {
productId: number | string;
limit: number;
}): Promise<Comments> {
const response = await fetch(`${BASE_URL}/products/${productId}/comments?limit=${limit}`);
if (!response.ok) { throw Error("Request Error"); }
return (await response.json()) as Comments;
}
35 changes: 35 additions & 0 deletions src/api/apiTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface Product {
createdAt: string;
favoriteCount: number;
ownerNickname: string;
ownerId: number;
images: string[];
tags: string[];
price: number;
description: string;
name: string,
id: number;
isFavorite: boolean;
}

export interface Products {
totalCount: number;
list: Product[];
}
Comment on lines +15 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 칭찬
Product와 Products를 분리해서 타입 선언하신 것 좋습니다!


export interface Comment {
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
추후 Component 정의시 중복될 수 있고 이름을 통해 타입을 추론할 수 있도록 Comment보다 CommentProps 같은 이름이 좋습니다.

writer: {
image: string;
nickname: string;
id: number;
};
Comment on lines +21 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.

💬 여담
Comment의 writer 타입은 추후 재사용될 가능성이 높기 때문에, 분리해두시는 것도 좋을 것 같아요.

export interface CommentWriterProps {
    image: string;
    nickname: string;
    id: number;
}

export interface CommentProps {
    writer: WriterProps;
}

위에처럼 분리하시면 아래처럼 작성이 가능합니다.

// 분리된 타입을 사용하는 경우
export const CommentWriter = ({ image, nickname, id }: CommentWriterProps) => { ... }
// 또는 유틸리티 타입을 사용하는 방법도 있습니다
export const CommentWriter = ({ image, nickname, id }: Pick<Comment, 'writer'>) => { ... }

updatedAt: string;
createdAt: string;
content: string;
id: number;
}

export interface Comments {
nextCursor: number;
list: Comment[]
}
56 changes: 32 additions & 24 deletions src/components/AuthInput.jsx → src/components/AuthInput.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,61 @@
import { useState } from "react";
import { useState, type FocusEvent, type FocusEventHandler, type InputHTMLAttributes } from "react";
import Input from "./Input";
import icEyeVisible from "../assets/ic_eye_visible.svg";
import icEyeInvisible from "../assets/ic_eye_invisible.svg";
import "./AuthInput.css";

function PasswordInput ({ name, className, onFocusout, valid, wrongMessage, ...props }) {
interface PasswordProps extends InputHTMLAttributes<HTMLInputElement> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
기본 Input 타입에서 확장해서 선언하신 점 좋습니다.
다만 이렇게 하신 경우 name, className이 InputHTMLAttributes에 선언되어 있는데 겹치게 되네요.
타입을 더 명확하게 하기 위해 중복되는 속성들을 제거하시거나 Omit을 사용하시는 것을 추천드려요.

interface PasswordProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string;
  wrongMessage: string;
  onFocusout?: FocusEventHandler;
  valid?: string;
}

interface PasswordProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'name' | 'className'> {
  name: string;
  label: string;
  className?: string;
  onFocusout?: FocusEventHandler;
  valid?: string;
  wrongMessage: string;
}

name: string;
label: string;
className?: string;
onFocusout?: FocusEventHandler;
valid?: string;
wrongMessage: string;
}

function PasswordInput ({ name, label, className='', onFocusout, valid, wrongMessage, ...props }: PasswordProps) {
const [isVisible, setIsVisible] = useState(false);
const onClick = () => {
setIsVisible((prev) => !prev);
}
const passwordMatch = () => {
try {
const password = document.querySelector("input#password");
const passwordCheck = document.querySelector("input#password-check");
const password = document.querySelector("input#password") as HTMLInputElement;
const passwordCheck = document.querySelector("input#password-check") as HTMLInputElement;
if (password.value === passwordCheck.value) {
passwordCheck.classList.add("correct");
passwordCheck.classList.remove("wrong");
passwordCheck.nextElementSibling.nextElementSibling.textContent = null;
(passwordCheck.nextElementSibling as HTMLElement).textContent = null;
} else if (passwordCheck.value) {
passwordCheck.classList.add("wrong");
passwordCheck.classList.remove("correct");
passwordCheck.nextElementSibling.nextElementSibling.textContent = "비밀번호가 일치하지 않습니다.";
(passwordCheck.nextElementSibling as HTMLElement).textContent = "비밀번호가 일치하지 않습니다.";
}
} catch (e) {}
}

return (
<Input required name={name}
className={"password "+className}
type={isVisible ? "text" : "password"}
onChange={passwordMatch}
{...(name === "password-check"
? { onChange: passwordMatch }
: { inputClassName: valid, minLength: 8, onBlur: onFocusout })}
{...props}
>
<button className="eye-btn" type="button" onClick={onClick}>
<img src={isVisible ? icEyeVisible : icEyeInvisible} />
</button>
<div className="wrong-message">{wrongMessage}</div>
</Input>
<Input required name={name} label={label}
className={"password "+className}
type={isVisible ? "text" : "password"}
onChange={passwordMatch}
{...(name === "password-check" ? {}
: { inputClassName: valid, minLength: 8, onBlur: onFocusout })}
{...props}
>
<div className="wrong-message">{wrongMessage}</div>
<button className="eye-btn" type="button" onClick={onClick}>
<img src={isVisible ? icEyeVisible : icEyeInvisible} />
</button>
</Input>
);
}

export default function AuthInput ({ label, name, type="text", placeholder='', emptyWrongMessage='', invalidWrongMessage='' }) {
export default function AuthInput ({ label='', name='', type="text", placeholder='', emptyWrongMessage='', invalidWrongMessage='' }) {
const [valid, setValid] = useState('');
const [wrongMessage, setWrongMessage] = useState(null);
const [wrongMessage, setWrongMessage] = useState('');

const onFocusout = (e) => {
const onFocusout = (e: FocusEvent<HTMLInputElement>) => {
if (!e.target.value) {
setValid("wrong");
setWrongMessage(emptyWrongMessage);
Expand All @@ -56,7 +64,7 @@ export default function AuthInput ({ label, name, type="text", placeholder='', e
setWrongMessage(invalidWrongMessage);
} else {
setValid("correct");
setWrongMessage(null);
setWrongMessage('');
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/components/Button.jsx → src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { ButtonHTMLAttributes, ReactNode } from "react";
import styles from "./Button.module.css";

//styleType은 small, medium, large로 나뉜다
function Button ({ styleType="small", className='', children, ...props }) {
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
styleType?: "small" | "medium" | "large";
className?: string;
children?: ReactNode;
}

function Button ({ styleType="small", className='', children, ...props }: Props) {
return (
<button className={`${styles.btn} ${styles[styleType]} ${className}`} {...props}>
{children}
Expand Down
17 changes: 0 additions & 17 deletions src/components/ButtonHeart.jsx

This file was deleted.

24 changes: 24 additions & 0 deletions src/components/ButtonHeart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ButtonHTMLAttributes, ReactNode } from "react";
import icHeartLargeActive from "../assets/heart/ic_heart_large_active.svg";
import icHeartLargeInactive from "../assets/heart/ic_heart_large_inactive.svg";
import icHeartMediumActive from "../assets/heart/ic_heart_medium_active.svg";
import icHeartMediumInactive from "../assets/heart/ic_heart_medium_inactive.svg";
import styles from "./ButtonHeart.module.css";

interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
isActive?: boolean;
className?: string;
children?: ReactNode;
}

function ButtonHeart({ isActive=false, className='', children, ...props }: Props) {
return (<button className={styles.btn+' '+className} {...props}>
<img className={styles.large+' '+styles[String(isActive)]} src={icHeartLargeActive} />
<img className={styles.large+' '+styles[String(!isActive)]} src={icHeartLargeInactive} />
<img className={styles.medium+' '+styles[String(isActive)]} src={icHeartMediumActive} />
<img className={styles.medium+' '+styles[String(!isActive)]} src={icHeartMediumInactive} />
{children}
</button>);
}

export default ButtonHeart;
Loading
Loading