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
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<!-- HTML Meta Tags -->
<title>판다마켓</title>
<meta name="description" content="일상의 모든 물건을 거래해보세요">
Expand Down
4 changes: 4 additions & 0 deletions public/images/common/ic_plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/images/common/ic_tag_x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/components/Common/CommonButton.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import styles from './styles/CommonButton.module.css';
import { useNavigate, useLocation } from 'react-router-dom';

const CommonButton = ({buttonType, path=""}) => {
const CommonButton = ({buttonType, path="", disabled=false}) => {
const navigate = useNavigate();
const location = useLocation();
const currentPath = location.pathname;
console.log(currentPath);
const isItems = currentPath === "/items" ? styles.isItems : "";
return (
<button
type={buttonType.buttonType}
className={`${styles.commonButton} ${styles[buttonType.buttonStyle]} ${isItems}`}
onClick={() => path && navigate(path)}
disabled={disabled}
>
{buttonType.buttonText}
</button>
Expand Down
4 changes: 4 additions & 0 deletions src/components/Common/styles/CommonButton.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
background-color: var(--blue);
color: #fff;
}
&:disabled {
background-color: var(--gray400);
cursor: not-allowed;
}
}

@media (max-width: 768px) {
Expand Down
11 changes: 9 additions & 2 deletions src/components/Header/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const Header = () => {
const navigate = useNavigate();
const location = useLocation();
return (
<header className={`header ${location.pathname === '/items' || location.pathname === '/board' ? 'header02' : ''}`}>
<header
className={`header ${location.pathname === '/items' || location.pathname === '/additem' ? 'header02' : ''}`}>
<div className='inner02'>
<h1 className='header-logo'>
<button onClick={() => navigate('/')}>
Expand All @@ -15,7 +16,13 @@ const Header = () => {
</h1>
<nav className='header-nav'>
<NavLink to='/board'>자유게시판</NavLink>
<NavLink to='/items'>중고마켓</NavLink>
<NavLink
to='/items'
className={({ isActive }) =>
location.pathname === '/items' || location.pathname === '/additem' ? 'active' : ''
}>
중고마켓
</NavLink>
</nav>
<button onClick={() => navigate('/login')} className='header-login'>
로그인
Expand Down
11 changes: 6 additions & 5 deletions src/components/Header/styles/Header.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,7 @@
}
}
@media all and (max-width: 1024px) {
.header .inner02 {
max-width: none;
margin-inline: 3.23vw;
height: 9.41vw;
}

.header-login {
max-width: 17.2vw;
height: 6.45vw;
Expand All @@ -104,6 +100,11 @@
}
}
@media all and (max-width: 768px) {
.header .inner02 {
max-width: none;
margin-inline: 3.23vw;
height: 9.41vw;
}
.header-logo button {
max-height: 5.21vw;
max-width: 19.92vw;
Expand Down
31 changes: 31 additions & 0 deletions src/contexts/AddItemFormContext.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 여담
contextAPI를 사용하실 때는 주의해야할 점이 있습니다.
context를 통해 값을 전달할때는 해당 값이 변경되면 구독하는 컴포넌트에서 리렌더링이 발생할 수 있다는 것을 염두에 두고 작업을 해야합니다. (아래 코드의 경우 객체를 value로 공유하고 있어, 렌더링시 새로운 객체가 생성되어 참조가 변경되므로 object.is로 단순 비교시 변경으로 판단되어 리렌더링을 유발합니다. )
이를 해결하실 수 있는 다양한 방법이 있으나 가장 중요한 것은 동작에 대해 이해하는 것이므로 아래의 글들을 읽어보시는 것을 추천드립니다. 추후에는 상태관리 라이브러리를 사용해서 이러한 기능을 구현하실 가능성이 큰데 그때도 context 동작에 대해 이해하고 계시는 것이 도움이 되실거에요!

https://ko.react.dev/reference/react/useContext#optimizing-re-renders-when-passing-objects-and-functions
https://velog.io/@velopert/react-context-tutorial

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createContext, useContext, useState } from 'react';

export const AddItemFormContext = createContext();

export function AddItemFormProvider({ children }) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [price, setPrice] = useState('');
const [tags, setTags] = useState([]);

const isFormValid = name && description && price && tags.length > 0;

return (
<AddItemFormContext.Provider
value={{
name,
setName,
description,
setDescription,
price,
setPrice,
tags,
setTags,
isFormValid,
}}>
{children}
</AddItemFormContext.Provider>
);
}

export const useAddItemForm = () => useContext(AddItemFormContext);
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
해당 context를 사용하는 훅을 정의하셔서 사용성을 높으신 것 좋습니다.
다만 아래처럼 context 내부에서 해당 context에 접근하지 않을때의 에러도 추가해주시면 더 좋을 것 같습니다!

Suggested change
export const useAddItemForm = () => useContext(AddItemFormContext);
export const useAddItemForm = () => {
const _context = useContext(AddItemFormContext);
if (!_context) throw new Error(/** error msg */);
return _context;
}

40 changes: 40 additions & 0 deletions src/pages/AddItem/components/AddItemImage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useState } from 'react';
import styles from '../styles/AddItemImage.module.css';

export default function AddItemImage() {
const [fileImg, setFileImg] = useState(null);
const [showFileLimitAlert , setShowFileLimitAlert ] = useState(false);
const handleFileChange = (e) => {
if (fileImg) {
setShowFileLimitAlert (true);
e.target.value = '';
return;
}
setFileImg(e.target.files[0]);
setShowFileLimitAlert (false);
}
const handleRemoveFile = () => {
setFileImg(null);
setShowFileLimitAlert(false);
};
return (
<>
<div className={styles.addItemImage}>
<input type='file' name='file' id='file' onChange={handleFileChange} />
<label htmlFor='file'>
<img src='/images/common/ic_plus.svg' alt='이미지 등록' />
이미지 등록
</label>
{fileImg && (
<div className={styles.addItemImagePreview}>
<img src={fileImg ? URL.createObjectURL(fileImg) : ''} alt='이미지 미리보기' />
<button type='button' onClick={handleRemoveFile}>
<img src='/images/common/ic_tag_x.svg' alt='이미지 삭제' />
</button>
</div>
)}
</div>
{showFileLimitAlert && <span className={styles.addItemImageAlert}>*이미지 등록은 최대 1개까지 가능합니다.</span>}
</>
);
}
86 changes: 86 additions & 0 deletions src/pages/AddItem/components/AddItemLists.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import styles from '../styles/AddItemLists.module.css';
import AddItemImage from './AddItemImage';
import AddItemTag from './AddItemTag';
import { useState, useRef } from 'react';
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
import { useState, useRef } from 'react';
import { useRef } from 'react';

import { useAddItemForm } from '@/contexts/AddItemFormContext';

export default function AddItemsLists() {
const inputRef = useRef(null);
const { name, setName, description, setDescription, price, setPrice } = useAddItemForm();

const handlePriceChange = (e) => {
const el = inputRef.current;
const rawValue = e.target.value;
const cursorPos = el.selectionStart;

// 쉼표 제거
const cleanValue = rawValue.replace(/[^0-9]/g, '');
const numericValue = cleanValue === '' ? 0 : Number(cleanValue);

// 상태 업데이트
changePrice(numericValue, rawValue, cursorPos);
};
const changePrice = (value, rawValue, prevCursorPos) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
value === 0 ? '' : value.toLocaleString() 부분이 반복되니 변수에 저장하시고 사용하시면 더 좋을 것 같아요.

const changePrice = (value, rawValue, prevCursorPos) => {
    const formatted = value ? value.toLocaleString() : "";

    setPrice(formatted);
    setTimeout(() => { 
        ...
        // 새 포맷된 문자열 <= ❗️ 불필요
        // const formatted = value === 0 ? '' : value.toLocaleString();

        for (let i = 0; i < formatted.length; i++) { ... }
    }
}

또한 setTimeout에 콜백도 따로 이름을 붙여 분리해주시면 가독성에 더 좋을 것 같습니다~

setPrice(value === 0 ? '' : value.toLocaleString());
Comment on lines +11 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
변수 el, cursorPos의 경우 changePrice 함수에 인수로 사용되기 위한 값이므로 아래처럼 작성하시는 것이
더 가독성 측면에서 좋을 것 같습니다~

Suggested change
const handlePriceChange = (e) => {
const el = inputRef.current;
const rawValue = e.target.value;
const cursorPos = el.selectionStart;
// 쉼표 제거
const cleanValue = rawValue.replace(/[^0-9]/g, '');
const numericValue = cleanValue === '' ? 0 : Number(cleanValue);
// 상태 업데이트
changePrice(numericValue, rawValue, cursorPos);
};
const changePrice = (value, rawValue, prevCursorPos) => {
setPrice(value === 0 ? '' : value.toLocaleString());
const handlePriceChange = (e) => {
const rawValue = e.target.value;
const cleanValue = rawValue.replace(/[^0-9]/g, '');
const numericValue = cleanValue === '' ? 0 : Number(cleanValue);
changePrice(numericValue, rawValue);
};
const changePrice = (value, rawValue) => {
const prevCursorPos = inputRef.current.selectionStart;
setPrice(value === 0 ? '' : value.toLocaleString());

// 커서 복원
setTimeout(() => {
if (!inputRef.current) return;

// 새 포맷된 문자열
const formatted = value === 0 ? '' : value.toLocaleString();

// 이전 값에서 커서까지 몇 개의 숫자가 있었는지 계산
const numbersBeforeCursor = rawValue.slice(0, prevCursorPos).replace(/[^0-9]/g, '').length;

// 새 포맷 문자열에서 그 숫자 위치를 다시 찾음
let newCursorPos = 0;
let digitsSeen = 0;

for (let i = 0; i < formatted.length; i++) {
if (/\d/.test(formatted[i])) digitsSeen++;
if (digitsSeen === numbersBeforeCursor) {
newCursorPos = i + 1;
break;
}
}

inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
}, 0);
};




return (
<>
<li className={styles.addItemListItem}>
<p>상품 이미지</p>
<AddItemImage />
</li>
<li className={styles.addItemListItem}>
<label htmlFor='name'>상품명</label>
<input type='text' name='name' id='name' placeholder='상품명을 입력해주세요' value={name} onChange={(e) => setName(e.target.value)} />
</li>
<li className={styles.addItemListItem}>
<label htmlFor='description'>상품 소개</label>
<textarea name='description' id='description' placeholder='상품 소개를 입력해주세요' value={description} onChange={(e) => setDescription(e.target.value)} />
</li>
<li className={styles.addItemListItem}>
<label htmlFor='price'>판매가격</label>
<input
ref={inputRef}
type='text'
name='price'
id='price'
placeholder='판매 가격을 입력해주세요'
value={price}
onChange={handlePriceChange}
/>
</li>
<li className={styles.addItemListItem}>
<AddItemTag />
</li>
</>
);
}

54 changes: 54 additions & 0 deletions src/pages/AddItem/components/AddItemTag.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
한글로된 태그를 생성할 때 두번 입력이 발생하는 것 같은 현상이 있습니다. 이는 자모음으로 이루어진 한글을 입력중에 발생하는 현상입니다~
배포사이트에서 한글로 태그를 생성해보시고, 위의 동작을 고쳐보세요~

https://toby2009.tistory.com/53

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import styles from '../styles/AddItemTag.module.css';
import { useState } from 'react';
import { useAddItemForm } from '@/contexts/AddItemFormContext';

export default function AddItemTag() {
const { tags, setTags } = useAddItemForm();
const [tag, setTag] = useState('');
const handleTagChange = (e) => {
setTag(e.target.value);
}
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault(); // 폼 submit 방지
const newTag = tag.trim(); // 공백 제거
if (newTag.includes(' ')) {
alert('태그에는 띄어쓰기가 포함될 수 없습니다.');
return;
}
if (newTag && !tags.includes(newTag)) {
Comment on lines +15 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
태그에 공백이 포함되면 alert로 유저에게 피드백을 주신 것처럼 중복의 경우도 안내를 해주시면 일관성과 UX 측면에서 더 좋을 것 같아요.

// 태그 중복, 띄어쓰기 방지
setTags([...tags, newTag]);
setTag('');
}
}
};
const handleTagDelete = (tag) => {
setTags([...tags].filter((ele) => ele !== tag));
};

return (
<>
<label htmlFor=''>태그</label>
<input
type='text'
name=''
id=''
Comment on lines +35 to +36
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
사용하지 않는 속성일 경우 지워주시고, label이랑 input은 연결해주세요~

placeholder='태그를 입력해주세요'
value={tag}
onChange={handleTagChange}
onKeyDown={handleKeyDown}
/>
<ul className={styles.addItemTagList}>
{tags.map((tag, index) => (
<li key={`${tag}-${index}`}>
<span>&#35;{tag}</span>
<button type='button' onClick={() => handleTagDelete(tag)}>
<img src='/images/common/ic_tag_x.svg' alt='삭제' />
</button>
</li>
))}
</ul>
</>
);
}
38 changes: 37 additions & 1 deletion src/pages/AddItem/index.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
import Footer from '@/components/Footer/Footer';
import Header from '@/components/Header/Header';
import styles from './styles/index.module.css';
import CommonButton from '@/components/Common/CommonButton';
import '@/styles/items.css';
import AddItemLists from './components/AddItemLists';
import { AddItemFormProvider, useAddItemForm } from '@/contexts/AddItemFormContext';

export default function AddItem() {
return <div>AddItem</div>;
return (
<>
<Header />
<div id='container' className={`${styles.addItemPage} itemsPage`}>
<AddItemFormProvider>
<AddItemContents />
</AddItemFormProvider>
</div>
<Footer />
</>
);
}

function AddItemContents() {
const { isFormValid } = useAddItemForm();
return (
<form action='' className='inner04'>
<div className='contentHeader'>
<h3>상품 등록하기</h3>
<CommonButton
buttonType={{ buttonType: 'submit', buttonStyle: 'primary', buttonText: '등록' }}
disabled={!isFormValid}
/>
</div>
<ul className={styles.addItemList}>
<AddItemLists />
</ul>
</form>
);
}
Loading
Loading