diff --git a/package-lock.json b/package-lock.json index 1db16bb..a0a819c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16518,6 +16518,11 @@ "shallowequal": "^1.1.0" } }, + "react-image-gallery": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/react-image-gallery/-/react-image-gallery-1.2.7.tgz", + "integrity": "sha512-ts7yMWykvZhslGMuvVAUk7a1dJIx9QtIwK6Clv8dOGxPsJid92FRrLvXbZDvkYcUyj48sHalnBhqU6mZNCB2jg==" + }, "react-inspector": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz", diff --git a/package.json b/package.json index 4ccb466..f632103 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "next-plugin-antd-less": "1.5.2", "react": "17.0.2", "react-dom": "17.0.2", + "react-image-gallery": "1.2.7", "styled-components": "5.3.3" }, "devDependencies": { diff --git a/src/components/atoms/Textarea.tsx b/src/components/atoms/Textarea.tsx index f5a06b0..8d30e6c 100644 --- a/src/components/atoms/Textarea.tsx +++ b/src/components/atoms/Textarea.tsx @@ -8,6 +8,7 @@ export const Textarea = styled.textarea` border-radius: 9px; border: 1px solid ${theme.color.primary}; padding: 8px; + margin-bottom: 0; &:placeholder-shown { border: 1px solid ${theme.color.gray3}; diff --git a/src/components/icon/Coin.icon.tsx b/src/components/icon/Coin.icon.tsx new file mode 100644 index 0000000..a9f9324 --- /dev/null +++ b/src/components/icon/Coin.icon.tsx @@ -0,0 +1,99 @@ +function CoinIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default CoinIcon; diff --git a/src/components/icon/ColoredCamera.icon.tsx b/src/components/icon/ColoredCamera.icon.tsx new file mode 100644 index 0000000..cc752fb --- /dev/null +++ b/src/components/icon/ColoredCamera.icon.tsx @@ -0,0 +1,32 @@ +function ColoredCameraIcon() { + return ( + + + + + + ); +} + +export default ColoredCameraIcon; diff --git a/src/components/icon/ForkRate.icon.tsx b/src/components/icon/ForkRate.icon.tsx new file mode 100644 index 0000000..46c36cb --- /dev/null +++ b/src/components/icon/ForkRate.icon.tsx @@ -0,0 +1,24 @@ +function ForkRateIcon() { + return ( + + + + + ); +} + +export default ForkRateIcon; diff --git a/src/components/icon/RightArrow.icon.tsx b/src/components/icon/RightArrow.icon.tsx new file mode 100644 index 0000000..ad6e898 --- /dev/null +++ b/src/components/icon/RightArrow.icon.tsx @@ -0,0 +1,15 @@ +function RightArrowIcon() { + return ( + + + + ); +} + +export default RightArrowIcon; diff --git a/src/components/molecules/AutoCompleteInput.component.tsx b/src/components/molecules/AutoCompleteInput.component.tsx index d7728a7..7e1168c 100644 --- a/src/components/molecules/AutoCompleteInput.component.tsx +++ b/src/components/molecules/AutoCompleteInput.component.tsx @@ -66,21 +66,26 @@ function AutoCompleteInputComponent({ style={{ maxHeight: "300px", overflowY: "auto" }} > {inputValue === "" && options.length === 0 ? ( - + 검색어를 입력해주세요. ) : ( "" )} {inputValue !== "" && options.length === 0 ? ( - + 일치하는 항목을 찾지 못했습니다. ) : ( "" )} {options.map((x) => ( - + {x.label} ))} diff --git a/src/components/molecules/CategorySelectComponent.tsx b/src/components/molecules/CategorySelectComponent.tsx index c1b0de6..fe30b27 100644 --- a/src/components/molecules/CategorySelectComponent.tsx +++ b/src/components/molecules/CategorySelectComponent.tsx @@ -11,12 +11,22 @@ const CategoryWrapper = styled.div` width: fit-content; margin: 0; `; -const CategoryImageWrapper = styled.div` +const CategoryImageWrapper = styled.div` border-radius: 12px; background-color: ${theme.color.gray2}; - height: calc((100vw - 2 * ${Padding.pageX} - 30px) / 4); - width: calc((100vw - 2 * ${Padding.pageX} - 30px) / 4); + ${({ size = "full" }) => { + switch (size) { + case "grid": + return `height: auto; width: 25%;`; + case "full": + default: + return ` + height: calc((100vw - 2 * ${Padding.pageX} - 30px) / 4); + width: calc((100vw - 2 * ${Padding.pageX} - 30px) / 4); + `; + } + }}; overflow: hidden; display: flex; @@ -65,22 +75,29 @@ type CategoryComponentProp = { img: string | null; name: string; onClick: MouseEventHandler; + size?: "full" | "grid"; +}; + +type CategoryImageWrapperProp = { + size?: "full" | "grid"; }; type CategoryWrapperProp = { onClick: MouseEventHandler; }; - +CategorySelectComponent.defaultProps = { + size: "full", +}; function CategorySelectComponent({ selected = false, img = null, name = "", onClick = null, + size, }: CategoryComponentProp) { return ( - - {/* TODO:: 여기에 아이콘 대신 이미지를 삽입 */} + {img ? : } diff --git a/src/components/molecules/ImageGallery.component.tsx b/src/components/molecules/ImageGallery.component.tsx new file mode 100644 index 0000000..95227cc --- /dev/null +++ b/src/components/molecules/ImageGallery.component.tsx @@ -0,0 +1,74 @@ +import { useCallback, useMemo, useState } from "react"; +import ImageGallery from "react-image-gallery"; +import "react-image-gallery/styles/css/image-gallery.css"; +import styled from "styled-components"; + +const Container = styled.div` + .image-gallery { + width: 100vw; + } + .image-gallery-image { + height: 250px; + } +`; + +const Popup = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.3); + z-index: 10000; + display: flex; + justify-content: center; + align-items: center; + button { + position: absolute; + width: fit-content; + top: 5px; + right: 5px; + } + img { + width: 100%; + height: auto; + } +`; + +function ImageGalleryComponent({ images = [] as string[] }) { + const [popupImgSrc, setPopupImgSrc] = useState(null); + const imageList = useMemo( + () => + images.map((x) => ({ + original: x, + originalHeight: "250px", + originalWidth: "auto", + })), + [images], + ); + const onClick = useCallback((e) => { + setPopupImgSrc(e.target.src); + }, []); + const onClickPopup = useCallback(() => { + setPopupImgSrc(null); + }, []); + return ( + + + {popupImgSrc && ( + + popup + + )} + + ); +} + +export default ImageGalleryComponent; diff --git a/src/components/molecules/MultipleImageUpload.component.tsx b/src/components/molecules/MultipleImageUpload.component.tsx new file mode 100644 index 0000000..e2ecba4 --- /dev/null +++ b/src/components/molecules/MultipleImageUpload.component.tsx @@ -0,0 +1,91 @@ +import UploadComponent from "@src/components/molecules/Upload.component"; +import { useCallback, useState } from "react"; +import styled from "styled-components"; +import { LoadingOutlined } from "@ant-design/icons"; + +// components +import UploadedImageFrameComponent from "@src/components/molecules/UploadedImageFrame.component"; +import ColoredCameraIcon from "@src/components/icon/ColoredCamera.icon"; + +// styles +import theme, { FontSize } from "@src/styles/theme"; + +const Container = styled.div` + position: relative; + display: flex; + align-items: center; + gap: 10px; + overflow-x: auto; +`; +export const ImageFrame = styled.div` + width: 80px; + height: 80px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid ${theme.color.point}; + border-radius: 9px; + position: relative; + cursor: pointer; + img { + min-width: 80px; + } +`; +export const Message = styled.p` + margin-bottom: 0; + font-size: ${FontSize.PrimaryDescription}; +`; +function MultipleImageUploadComponent({ + messageOnEmpty, + uploaded, + setUploaded, + folder, +}) { + const [loading, setLoading] = useState(false); + + const onUploaded = useCallback( + (resources) => { + setUploaded([...uploaded, ...resources]); + }, + [setUploaded, uploaded], + ); + + const onRemove = useCallback( + (path) => { + setUploaded(uploaded.filter((x) => x.path !== path)); + }, + [setUploaded, uploaded], + ); + + return ( + + {uploaded.map((x) => ( + + ))} + {loading && ( + + + + )} + + + + } + folder={folder} + setLoading={setLoading} + onUploaded={onUploaded} + multiple + /> + {uploaded.length === 0 && !loading && {messageOnEmpty}} + + ); +} + +export default MultipleImageUploadComponent; diff --git a/src/components/molecules/ProfileFrame.component.tsx b/src/components/molecules/ProfileFrame.component.tsx index aeec497..bf35edf 100644 --- a/src/components/molecules/ProfileFrame.component.tsx +++ b/src/components/molecules/ProfileFrame.component.tsx @@ -6,13 +6,15 @@ import { BaseProps, BaseStyleProps } from "@src/styles/common"; import { Resource } from "@src/models/dto/api-response"; import { useMemo, useState } from "react"; import { LoadingOutlined } from "@ant-design/icons"; +import CameraIcon from "@src/components/icon/Camera.icon"; +import { FolderPathType } from "@src/constant/enum.constant"; interface ProfileFrameProps { mb?: string; - size?: "large" | "small"; + size?: "large" | "small" | "medium"; allowUpload?: boolean; imgSrc?: string; - onUploadImage?: (e: Resource) => void; + onUploadImage?: (e: Resource[]) => void; } const ProfileImageWrapper = styled.div` @@ -26,12 +28,34 @@ const CircleImageFrame = styled.div` img { width: auto; - height: ${({ size }) => (size === "large" ? "80px" : "32px")}; + height: ${({ size }) => { + switch (size) { + case "large": + return "80px"; + case "medium": + return "72px"; + case "small": + return "32px"; + default: + return "80px"; + } + }}; } svg { width: auto; - height: ${({ size }) => (size === "large" ? "48px" : "18px")}; + height: ${({ size }) => { + switch (size) { + case "large": + return "48px"; + case "medium": + return "48px"; + case "small": + return "18px"; + default: + return "48px"; + } + }}; } ${({ size }) => { @@ -41,6 +65,12 @@ const CircleImageFrame = styled.div` width: 80px; height: 80px; `; + case "medium": + return ` + width: 72px; + height: 72px; + border-width: 5px; + `; case "small": return ` width: 32px; @@ -53,6 +83,7 @@ const CircleImageFrame = styled.div` height: 80px;`; } }} + border-radius: 50%; display: flex; @@ -65,6 +96,26 @@ const CircleImageFrame = styled.div` background-color: ${theme.color.gray0}; `; +const CameraButtonWrapper = styled.div` + position: absolute; + bottom: 0; + right: 0; + z-index: 10; + + border: 1px solid ${theme.color.gray2}; + border-radius: 50%; + background-color: #fff; + + display: flex; + justify-content: center; + align-items: center; + + height: 32px; + width: 32px; + + cursor: pointer; +`; + const S = { ProfileImageWrapper, CircleImageFrame, @@ -97,7 +148,16 @@ function ProfileFrameComponent({ {image} {allowUpload && ( - + + + + } + setLoading={setLoading} + onUploaded={onUploadImage} + /> )} ); diff --git a/src/components/molecules/Upload.component.tsx b/src/components/molecules/Upload.component.tsx index e83a906..fb17994 100644 --- a/src/components/molecules/Upload.component.tsx +++ b/src/components/molecules/Upload.component.tsx @@ -1,29 +1,16 @@ -import { useStores } from "@src/store/root.store"; import { useCallback } from "react"; -import CameraIcon from "@src/components/icon/Camera.icon"; import styled from "styled-components"; -import { ButtonStyleProps } from "@src/components/atoms/Button"; -import theme from "@src/styles/theme"; -const IconButton = styled.label` - position: absolute; - bottom: 0; - right: 0; - z-index: 10; +// lib +import { useStores } from "@src/store/root.store"; - border: 1px solid ${theme.color.gray2}; - border-radius: 50%; - background-color: #fff; +// component +import { ButtonStyleProps } from "@src/components/atoms/Button"; +const IconButton = styled.label` display: flex; justify-content: center; align-items: center; - - height: 32px; - width: 32px; - - cursor: pointer; - input { display: none; } @@ -31,25 +18,42 @@ const IconButton = styled.label` const S = { IconButton }; -function UploadComponent({ onUploaded, setLoading }) { +function UploadComponent({ + icon, + folder, + onUploaded, + setLoading, + multiple = false, +}) { const rootStore = useStores(); const onUploadFile = useCallback( async (e) => { setLoading(true); - const file = e.target.files[0]; - const uploaded = await rootStore.uploadImage(file); + + const uploaded = await Promise.all( + Object.entries(e.target.files).map((file) => + rootStore.uploadImage(file[1], folder), + ), + ); + onUploaded(uploaded); setLoading(false); }, - [setLoading, rootStore, onUploaded], + [setLoading, onUploaded, rootStore, folder], ); return (
- - + {icon} +
); diff --git a/src/components/molecules/UploadedImageFrame.component.tsx b/src/components/molecules/UploadedImageFrame.component.tsx new file mode 100644 index 0000000..4f046f8 --- /dev/null +++ b/src/components/molecules/UploadedImageFrame.component.tsx @@ -0,0 +1,38 @@ +import CloseIcon from "@src/components/icon/Close.icon"; +import { memo, useCallback } from "react"; +import { ImageFrame } from "@src/components/molecules/MultipleImageUpload.component"; +import styled from "styled-components"; + +const IconButton = styled.button` + position: absolute; + right: 0; + top: 0; + cursor: pointer; + z-index: 5; + + background-color: transparent; + border: none; + &:focus { + outline: none; + } +`; +const Container = styled.div` + position: relative; + height: fit-content; +`; +function UploadedImageFrameComponent({ resource, onRemove }) { + const onClickRemove = useCallback(() => { + onRemove(resource.path); + }, [onRemove, resource]); + return ( + + + {resource.path} + + + + + + ); +} +export default memo(UploadedImageFrameComponent); diff --git a/src/components/organs/StudyManageListElement.component.tsx b/src/components/organs/StudyManageListElement.component.tsx index a5c774e..b5a6850 100644 --- a/src/components/organs/StudyManageListElement.component.tsx +++ b/src/components/organs/StudyManageListElement.component.tsx @@ -8,6 +8,7 @@ import { import styled from "styled-components"; import { Padding } from "@src/styles/theme"; import { useMemo } from "react"; +import SimpleProfileComponent from "@src/components/molecules/SimpleProfile.component"; const BottomWrapper = styled.div` display: flex; @@ -34,7 +35,7 @@ function StudyManageListElementComponent({ study }) { -
+
{study.user && }
+ + {error} + diff --git a/src/templates/MyPage.template.tsx b/src/templates/MyPage.template.tsx new file mode 100644 index 0000000..c15ae62 --- /dev/null +++ b/src/templates/MyPage.template.tsx @@ -0,0 +1,161 @@ +import styled from "styled-components"; + +// lib +import { NaviType } from "@src/constant/enum.constant"; + +// components +import RightArrowIcon from "@src/components/icon/RightArrow.icon"; +import StarIcon from "@src/components/icon/Star.icon"; +import { TextButton } from "@src/components/atoms/TextButton"; +import { BoldDivider } from "@src/components/atoms/Divider"; +import BottomNavigationComponent from "@src/components/organs/BottomNavigation.component"; +import DetailUserInfoComponent from "@src/components/organs/profile/DetailUserInfo.component"; +import PointDetailComponent from "@src/components/organs/profile/PointDetail.component"; +import RateSliderComponent from "@src/components/organs/profile/RateSlider.component"; +import ProfileCategoryListBox from "@src/components/organs/profile/ProfileCategoryListBox"; + +// styles +import { BaseProps, BaseStyleProps } from "@src/styles/common"; +import theme, { FontSize, Padding } from "@src/styles/theme"; + +const Container = styled.div` + padding-top: 20px; + padding-bottom: 80px; + + h3 { + font-size: ${FontSize.PrimaryDescription}; + } +`; + +const InfoBox = styled.div` + //padding: 10px; + p { + font-size: ${FontSize.SecondaryLabel}; + } +`; + +const InfoBoxContainer = styled.div` + ${BaseStyleProps}; + + margin: ${Padding.page}; + display: grid; + grid-template-columns: 1fr 1fr; + border: 1px solid ${theme.color.point}; + border-radius: 9px; + padding: 10px; + + ${InfoBox}:nth-child(odd) { + border-right: 1px solid ${theme.color.gray3}; + } + ${InfoBox}:nth-child(even) { + padding-left: 10px; + } + ${InfoBox}:nth-child(1) { + border-bottom: 1px solid ${theme.color.gray3}; + } + ${InfoBox}:nth-child(2) { + border-bottom: 1px solid ${theme.color.gray3}; + } +`; + +const InfoBoxTitleWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + h3 { + margin-bottom: 0; + } + p { + margin-bottom: 0; + text-align: left; + } +`; + +const RateWrapper = styled.div` + display: flex; + gap: 5px; + align-items: center; + font-size: ${FontSize.Small}; +`; + +const BottomWrapper = styled.div` + ${TextButton} { + text-align: left; + } + + padding: ${Padding.page}; + height: 300px; +`; + +const BottomButtonWrapper = styled.div` + position: absolute; + bottom: 100px; +`; + +function MyPageTemplate({ profile }) { + return ( + + {/* Profile */} + + {/* Point */} + + {/* InfoBox */} + + {/* 신뢰도 */} + + +

신뢰도

+
+ + + {profile.user.rate} + + {/* Slider */} + +
+ {/* 스터디 횟수 */} + + +

스터디 횟수

+
+

{profile.studyCount}회

+
+ {/* 카테고리 - GIVE */} + + + + {/* 카테고리 - TAKE */} + + + +
+ + + + + + +

이용 약관

+
+
+ +
+
+ + + 로그아웃 + + 회원탈퇴 + +
+ + +
+ ); +} + +export default MyPageTemplate; diff --git a/src/templates/StudyCreate.template.tsx b/src/templates/StudyCreate.template.tsx index f655bd6..65806e3 100644 --- a/src/templates/StudyCreate.template.tsx +++ b/src/templates/StudyCreate.template.tsx @@ -16,14 +16,18 @@ import { Textarea } from "@src/components/atoms/Textarea"; // styles import theme, { Padding } from "@src/styles/theme"; -import { LightUnderline } from "@src/styles/common"; +import { BaseProps, BaseStyleProps, LightUnderline } from "@src/styles/common"; import CategorySelectDrawerComponent from "@src/components/organs/CategorySelectDrawer.component"; import { Category } from "@src/components/atoms/Category"; import useVisibleHook from "@src/hooks/useVisible.hook"; import TradeIcon from "@src/components/icon/Trade.icon"; import dayjs from "dayjs"; +import MultipleImageUploadComponent from "@src/components/molecules/MultipleImageUpload.component"; +import { FolderPathType } from "@src/constant/enum.constant"; +import { Checkbox } from "antd"; -const FormWrapper = styled.div` +const FormWrapper = styled.div` + ${BaseStyleProps}; padding: 0 ${Padding.pageX}; input, select { @@ -42,7 +46,7 @@ const WithUnderline = styled.div` display: flex; align-items: center; padding-top: 8px; - padding-bottom: 0; + padding-bottom: 8px; margin-bottom: 5px; `; @@ -62,6 +66,8 @@ function StudyCreateTemplate({ onSubmit, values, onChange, + allowDatepicker, + toggleUseDatepicker, startDate, onChangeStartDate, endDate, @@ -70,6 +76,8 @@ function StudyCreateTemplate({ setSelectedMine, selectedYours, setSelectedYours, + uploaded, + setUploaded, }) { const studyTypeList = useMemo(() => getStudyTypeList(), []); const [mineVisible, setMineVisible, setMineInvisible] = useVisibleHook(false); @@ -166,6 +174,7 @@ function StudyCreateTemplate({ - + + + 참가자와 날짜 결정 + + {/* - +