-
Notifications
You must be signed in to change notification settings - Fork 37
[최제원] sprint7 #270
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-\uCD5C\uC81C\uC6D0-sprint7"
[최제원] sprint7 #270
Changes from all commits
db45b99
2fd73b2
6c30b66
66f6832
9c47467
c7cb73c
c8e22ef
b4b9ce3
944208c
7104975
702b080
ad38edb
fc914ca
94d4274
37dba11
48e9d67
63114f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import React from "react"; | ||
| import styled from "styled-components"; | ||
| import { ReactComponent as CloseIcon } from "../../assets/images/icons/ic_x.svg"; | ||
|
|
||
| const Button = styled.button` | ||
| background-color: ${({ theme }) => theme.colors.gray[0]}; | ||
|
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. 👍👍👍 |
||
| width: 20px; | ||
| height: 20px; | ||
| border-radius: 50%; | ||
| display: flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| &:hover { | ||
| background-color: ${({ theme }) => theme.colors.blue[0]}; | ||
| } | ||
| `; | ||
|
|
||
| function DeleteButton({ onClick, label }) { | ||
| return ( | ||
| <Button aria-label={`${label} 삭제`} onClick={onClick}> | ||
|
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. Type도 명시되면 더 좋겠네요 ㅎ |
||
| <CloseIcon /> | ||
| </Button> | ||
| ); | ||
| } | ||
|
|
||
| export default DeleteButton; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| .sortButtonWrapper { | ||
| position: relative; | ||
| } | ||
|
|
||
| .sortDropdownTriggerButton { | ||
| border: 1px solid #e5e7eb; | ||
| border-radius: 12px; | ||
| padding: 9px; | ||
| margin-left: 8px; | ||
| } | ||
|
|
||
| .dropdownMenu { | ||
| position: absolute; | ||
| top: 110%; | ||
| right: 0; | ||
| background: #fff; | ||
| border-radius: 8px; | ||
| border: 1px solid #e5e7eb; | ||
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||
| z-index: 99; | ||
| } | ||
|
|
||
| .dropdownItem { | ||
| padding: 12px 44px; | ||
| border-bottom: 1px solid #e5e7eb; | ||
| font-size: 16px; | ||
| color: #1f2937; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .dropdownItem:last-child { | ||
| border-bottom: none; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import React, { useState } from "react"; | ||
| import "./DropdownMenu.css"; | ||
| import { ReactComponent as SortIcon } from "../../assets/images/icons/ic_sort.svg"; | ||
|
|
||
| function DropdownMenu({ onSortSelection }) { | ||
| const [isDropdownVisible, setIsDropdownVisible] = useState(false); | ||
|
|
||
| const toggleDropdown = () => { | ||
| setIsDropdownVisible(!isDropdownVisible); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="sortButtonWrapper"> | ||
| <button className="sortDropdownTriggerButton" onClick={toggleDropdown}> | ||
| <SortIcon /> | ||
| </button> | ||
|
|
||
| {isDropdownVisible && ( | ||
| <div className="dropdownMenu"> | ||
| <div | ||
| className="dropdownItem" | ||
| onClick={() => { | ||
| onSortSelection("recent"); | ||
| setIsDropdownVisible(false); | ||
| }} | ||
| > | ||
| 최신순 | ||
| </div> | ||
| <div | ||
| className="dropdownItem" | ||
| onClick={() => { | ||
| onSortSelection("favorite"); | ||
| setIsDropdownVisible(false); | ||
| }} | ||
| > | ||
| 인기순 | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| export default DropdownMenu; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import React, { useState } from "react"; | ||
| import { Label } from "./InputItem"; | ||
| import styled, { css } from "styled-components"; | ||
| import { ReactComponent as PlusIcon } from "../../assets/images/icons/ic_plus.svg"; | ||
| import DeleteButton from "./DeleteButton"; | ||
|
|
||
| const ImageUploadContainer = styled.div` | ||
| display: flex; | ||
| gap: 8px; | ||
|
|
||
| @media ${({ theme }) => theme.mediaQuery.tablet} { | ||
| gap: 18px; | ||
| } | ||
|
|
||
| @media ${({ theme }) => theme.mediaQuery.desktop} { | ||
| gap: 24px; | ||
| } | ||
| `; | ||
|
|
||
| const squareStyles = css` | ||
| // 작은 화면에서는 max-width가 되기 전까지는 UploadButton과 ImagePreview가 각각 gap을 포함해 컨테이너 너비의 절반을 차지하도록 함 | ||
| width: calc(50% - 4px); | ||
| max-width: 200px; | ||
| aspect-ratio: 1 / 1; // 정사각형 비율 유지 | ||
| border-radius: 12px; | ||
|
|
||
| @media ${({ theme }) => theme.mediaQuery.tablet} { | ||
| width: 162px; | ||
| } | ||
|
|
||
| @media ${({ theme }) => theme.mediaQuery.desktop} { | ||
| width: 282px; | ||
| } | ||
| `; | ||
|
|
||
| // file input과 연관 짓기 위해 버튼이 대신 label로 설정 | ||
| const UploadButton = styled.label` | ||
| background-color: ${({ theme }) => theme.colors.gray[1]}; | ||
| color: ${({ theme }) => theme.colors.gray[0]}; | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| gap: 12px; | ||
| font-size: 16px; | ||
| cursor: pointer; // 버튼이 아닌 label을 사용한 경우 별도로 추가해 주세요 | ||
|
|
||
| &:hover { | ||
| background-color: ${({ theme }) => theme.colors.gray[2]}; | ||
| } | ||
|
|
||
| ${squareStyles} | ||
| `; | ||
|
|
||
| const ImagePreview = styled.div` | ||
| background-image: url(${({ src }) => src}); | ||
| background-size: cover; | ||
| background-position: center; | ||
| position: relative; // DeleteButton 포지셔닝을 위해 추가 | ||
|
|
||
| ${squareStyles} | ||
| `; | ||
|
|
||
| const DeleteButtonWrapper = styled.div` | ||
| position: absolute; | ||
| top: 12px; | ||
| right: 12px; | ||
| `; | ||
|
|
||
| // 브라우저 기본 '파일 선택' 버튼 대신 커스텀 버튼을 사용하기 위해 file input을 숨김 처리 | ||
| const HiddenFileInput = styled.input` | ||
| display: none; | ||
| `; | ||
|
|
||
| function ImageUpload({ title }) { | ||
| const [imagePreviewUrl, setImagePreviewUrl] = useState(""); | ||
|
|
||
| const handleImageChange = (event) => { | ||
| const file = event.target.files[0]; | ||
| if (file) { | ||
| // 미리보기 주소 값(Object URL) 생성 | ||
| const imageUrl = URL.createObjectURL(file); | ||
| setImagePreviewUrl(imageUrl); | ||
| } | ||
| }; | ||
|
|
||
| const handleDelete = () => { | ||
| setImagePreviewUrl(""); // 미리보기 URL 리셋 | ||
| }; | ||
|
|
||
| return ( | ||
| <div> | ||
| {title && <Label>{title}</Label>} | ||
|
|
||
| <ImageUploadContainer> | ||
| {/* HiddenFileInput의 id와 label의 htmlFor 값을 매칭해 주세요 */} | ||
| <UploadButton htmlFor="image-upload"> | ||
| <PlusIcon /> | ||
| 이미지 등록 | ||
| </UploadButton> | ||
|
|
||
| <HiddenFileInput | ||
| id="image-upload" | ||
| type="file" | ||
| onChange={handleImageChange} | ||
| accept="image/*" // 이미지 파일만 업로드 가능하도록 제한 | ||
| /> | ||
|
|
||
| {/* 업로드된 이미지가 있으면 썸네일 렌더링 */} | ||
| {imagePreviewUrl && ( | ||
| <ImagePreview src={imagePreviewUrl}> | ||
| <DeleteButtonWrapper> | ||
| <DeleteButton onClick={handleDelete} label="이미지 파일" /> | ||
| </DeleteButtonWrapper> | ||
| </ImagePreview> | ||
| )} | ||
| </ImageUploadContainer> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default ImageUpload; |
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.
👍👍