-
Notifications
You must be signed in to change notification settings - Fork 39
React 염휘건 sprint6 #187
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
The head ref may contain hidden characters: "React-\uC5FC\uD718\uAC74-sprint6"
React 염휘건 sprint6 #187
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BEM 방식으로 잘 nesting 되어있네요. 굳굳 👍 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| /* block */ | ||
| .product-add-page { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 1.6rem; | ||
| padding: 2rem; | ||
| font-size: 1.4rem; | ||
|
|
||
| // ----- header ----- | ||
| &__header { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| margin-bottom: 1.2rem; | ||
| } | ||
|
|
||
| &__title { | ||
| font-size: 2rem; | ||
| font-weight: 600; | ||
| } | ||
|
|
||
| &__submit-btn { | ||
| background: #a6abb7; | ||
| color: #fff; | ||
| border: none; | ||
| border-radius: 6px; | ||
| padding: 0.6rem 1.4rem; | ||
| cursor: pointer; | ||
|
|
||
| &:disabled { | ||
| opacity: 0.4; | ||
| cursor: not-allowed; | ||
| } | ||
| } | ||
|
|
||
| // ----- images ----- | ||
| &__images { | ||
| display: flex; | ||
| gap: 1rem; | ||
| flex-wrap: wrap; | ||
| } | ||
|
|
||
| // 업로더 카드 | ||
| &__uploader { | ||
| width: 160px; | ||
| height: 160px; | ||
| border: 2px dashed #d9d9d9; | ||
| border-radius: 8px; | ||
| background: #fafafa; | ||
| display: flex; | ||
| flex-direction: column; | ||
| justify-content: center; | ||
| align-items: center; | ||
| cursor: pointer; | ||
| text-align: center; | ||
| transition: background 0.15s; | ||
|
|
||
| &:hover { | ||
| background: #f0f0f0; | ||
| } | ||
| } | ||
|
|
||
| &__uploader-plus { | ||
| font-size: 3.2rem; | ||
| line-height: 1; | ||
| } | ||
| &__uploader-text { | ||
| margin-top: 0.4rem; | ||
| font-size: 1.2rem; | ||
| color: #777; | ||
| } | ||
| &__images-error { | ||
| margin-top: 0.4rem; | ||
| font-size: 1.2rem; | ||
| color: #ff4d4f; // 빨간색 | ||
| } | ||
|
|
||
| // 썸네일 | ||
| &__thumb { | ||
| position: relative; | ||
| width: 160px; | ||
| height: 160px; | ||
| border-radius: 8px; | ||
| overflow: hidden; | ||
|
|
||
| img { | ||
| width: 100%; | ||
| height: 100%; | ||
| object-fit: cover; | ||
| } | ||
| } | ||
|
|
||
| &__thumb-delete { | ||
| position: absolute; | ||
| top: 4px; | ||
| right: 4px; | ||
| width: 24px; | ||
| height: 24px; | ||
| border: none; | ||
| border-radius: 50%; | ||
| background: rgba(0, 0, 0, 0.6); | ||
| color: #fff; | ||
| line-height: 24px; | ||
| font-size: 1.4rem; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| // 일반 인풋 / textarea | ||
| &__input, | ||
| &__textarea { | ||
| width: 100%; | ||
| border: none; | ||
| border-radius: 6px; | ||
| background: #f5f6f8; | ||
| padding: 1.2rem 1.4rem; | ||
| font-size: 1.4rem; | ||
| resize: none; | ||
|
|
||
| &:focus { | ||
| outline: 2px solid #4f7cff; | ||
| background: #fff; | ||
| } | ||
| } | ||
|
|
||
| // textarea만 별도 | ||
| &__textarea { | ||
| min-height: 140px; | ||
| } | ||
|
|
||
| // ----- tags ----- | ||
| &__tags { | ||
| display: flex; | ||
| gap: 0.6rem; | ||
| flex-wrap: wrap; | ||
| } | ||
|
|
||
| &__tag { | ||
| background: #f1f1f1; | ||
| border-radius: 9999px; | ||
| padding: 0.4rem 0.8rem 0.4rem 1rem; | ||
| font-size: 1.2rem; | ||
| position: relative; | ||
| display: flex; | ||
| align-items: center; | ||
| } | ||
|
|
||
| &__tag-delete { | ||
| margin-left: 0.4rem; | ||
| border: none; | ||
| background: transparent; | ||
| cursor: pointer; | ||
| font-size: 1.2rem; | ||
| line-height: 1; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import React, { useRef, useState, ChangeEvent, KeyboardEvent } from 'react'; | ||
| import style from './ProductAddPage.module.scss'; | ||
|
|
||
| interface ImagePreview { | ||
| file: File; | ||
| url: string; | ||
| } | ||
|
|
||
| const MAX_IMAGES = 1; | ||
|
|
||
| export default function ProductAddPage() { | ||
| const [images, setImages] = useState<ImagePreview[]>([]); | ||
| const [errorMsg, setErrorMsg] = useState(''); | ||
| const [name, setName] = useState(''); | ||
| const [description, setDescription] = useState(''); | ||
| const [price, setPrice] = useState(''); | ||
| const [tagInput, setTagInput] = useState(''); | ||
| const [tags, setTags] = useState<string[]>([]); | ||
|
|
||
| const fileInputRef = useRef<HTMLInputElement | null>(null); | ||
|
|
||
| const handleSelectImages = (e: ChangeEvent<HTMLInputElement>) => { | ||
| const files = e.target.files; | ||
| if (!files) return; | ||
|
|
||
| if (images.length >= MAX_IMAGES) { | ||
| setErrorMsg('*이미지 등록은 최대 1개까지 가능합니다.'); | ||
| e.target.value = ''; | ||
| return; | ||
| } | ||
|
|
||
| const [file] = files; | ||
| setImages([{ file, url: URL.createObjectURL(file) }]); | ||
| setErrorMsg(''); | ||
| e.target.value = ''; | ||
| }; | ||
|
|
||
| const handleRemoveImage = () => { | ||
| images.forEach((i) => URL.revokeObjectURL(i.url)); | ||
| setImages([]); | ||
| setErrorMsg(''); | ||
| }; | ||
|
|
||
| const handleTagKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { | ||
| if (e.nativeEvent.isComposing) return; | ||
|
|
||
| if (e.key !== 'Enter' && e.key !== ',') return; | ||
| e.preventDefault(); | ||
|
|
||
| const value = tagInput.trim(); | ||
| if (!value || tags.includes(value)) return; | ||
|
|
||
| setTags((prev) => [...prev, value]); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. spread 연산자를 사용해 배열을 복사 후 새 아이템을 추가하는군요! 이 과정에서 배열을 다룰때 발생할 수 있는 예외 케이스 (예를 들어, 중복값을 허용할것인지 등) 를 꼼꼼하게 더 처리해볼까요? |
||
| setTagInput(''); | ||
| }; | ||
| const removeTag = (t: string) => | ||
|
Comment on lines
+55
to
+56
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 55, 56 사이 공백 한칸 띄워줄까요? 함수와 함수 사이엔 공백 한칸씩 꼭 띄우도록합시다 :) |
||
| setTags((prev) => prev.filter((x) => x !== t)); | ||
|
|
||
| const handleSubmit = () => { | ||
| console.log({ images, name, description, price: Number(price), tags }); | ||
| }; | ||
|
Comment on lines
+59
to
+61
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 함수는 실제 제출을 담당하지는 않고 로그만 찍는 용도인가요? :) |
||
|
|
||
| return ( | ||
| <section className={style['product-add-page']}> | ||
| {/* === 헤더 === */} | ||
| <header className={style['product-add-page__header']}> | ||
| <h3 className={style['product-add-page__title']}>상품 등록하기</h3> | ||
| <button | ||
| type="button" | ||
| className={style['product-add-page__submit-btn']} | ||
| onClick={handleSubmit} | ||
| disabled={!name || !price || !images.length} | ||
| > | ||
| 등록 | ||
| </button> | ||
| </header> | ||
| 상품 이미지 | ||
| <div className={style['product-add-page__images']}> | ||
| <label className={style['product-add-page__uploader']}> | ||
| <input | ||
| ref={fileInputRef} | ||
| type="file" | ||
| accept="image/*" | ||
| onChange={handleSelectImages} | ||
| hidden | ||
| /> | ||
| <span className={style['product-add-page__uploader-plus']}>+</span> | ||
| <span className={style['product-add-page__uploader-text']}> | ||
| 이미지 등록 | ||
| </span> | ||
| </label> | ||
|
|
||
| {images.map(({ url }) => ( | ||
| <div key={url} className={style['product-add-page__thumb']}> | ||
| <img src={url} alt="preview" /> | ||
| <button | ||
| type="button" | ||
| className={style['product-add-page__thumb-delete']} | ||
| onClick={handleRemoveImage} | ||
| > | ||
| × | ||
| </button> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| {errorMsg && ( | ||
| <p className={style['product-add-page__images-error']}>{errorMsg}</p> | ||
| )} | ||
| 상품명 | ||
| <input | ||
| className={style['product-add-page__input']} | ||
| placeholder="상품명을 입력해주세요" | ||
| value={name} | ||
| onChange={(e) => setName(e.target.value)} | ||
| /> | ||
|
Comment on lines
+110
to
+115
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ProductAddPage의 복잡도를 감소시키기위해, |
||
| 상품 소개 | ||
| <textarea | ||
| className={style['product-add-page__textarea']} | ||
| placeholder="상품 소개를 입력해주세요" | ||
| value={description} | ||
| onChange={(e) => setDescription(e.target.value)} | ||
| rows={6} | ||
| /> | ||
| 판매 가격 | ||
| <input | ||
| type="number" | ||
| min={0} | ||
| className={style['product-add-page__input']} | ||
| placeholder="판매 가격을 입력해주세요" | ||
| value={price} | ||
| onChange={(e) => setPrice(e.target.value)} | ||
| /> | ||
| 태그 | ||
| <input | ||
| className={style['product-add-page__input']} | ||
| placeholder="태그를 입력해주세요" | ||
| value={tagInput} | ||
| onChange={(e) => setTagInput(e.target.value)} | ||
| onKeyDown={handleTagKeyDown} | ||
| /> | ||
| <ul className={style['product-add-page__tags']}> | ||
| {tags.map((t) => ( | ||
| <li key={t} className={style['product-add-page__tag']}> | ||
| #{t} | ||
| <button | ||
| type="button" | ||
| className={style['product-add-page__tag-delete']} | ||
| onClick={() => removeTag(t)} | ||
| > | ||
| × | ||
| </button> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </section> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,14 @@ | ||
| // src/router/routes.ts | ||
| //라우터 경로 정의 | ||
| import MarketPage from '@/pages/MarketPage/MarketPage'; | ||
|
|
||
| import ProductAddPage from '@/pages/ProductAddPage/ProductAddPage'; | ||
| //라우터 인터페이스 설정 | ||
| export interface AppRoute { | ||
| path: string; | ||
| element: React.ComponentType; | ||
| } | ||
| export const routes: AppRoute[] = [ | ||
| { path: '/items', element: MarketPage }, | ||
|
|
||
| { path: '/additem', element: ProductAddPage }, // 상품 등록 페이지 | ||
| // …추가 라우트 | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
불필요한 로그는 PR 올리실때 지워봅시다! :)