diff --git a/src/components/atoms/Input.tsx b/src/components/atoms/Input.tsx index 6c02dd8..6817891 100644 --- a/src/components/atoms/Input.tsx +++ b/src/components/atoms/Input.tsx @@ -22,6 +22,13 @@ const BaseInput = css` } `; +export const BoxInput = styled.input` + ${BaseInput}; + padding: 10px 20px; + border-radius: 50px; + border: 1px solid ${theme.color.point}; +`; + export const DividedInput = styled.input` ${BaseInput}; border: none; diff --git a/src/components/icon/Document.icon.tsx b/src/components/icon/Document.icon.tsx new file mode 100644 index 0000000..cb63046 --- /dev/null +++ b/src/components/icon/Document.icon.tsx @@ -0,0 +1,34 @@ +function DocumentIcon() { + return ( + + + + + + + + ); +} + +export default DocumentIcon; diff --git a/src/components/icon/Empty.icon.tsx b/src/components/icon/Empty.icon.tsx new file mode 100644 index 0000000..43dda07 --- /dev/null +++ b/src/components/icon/Empty.icon.tsx @@ -0,0 +1,73 @@ +function EmptyIcon() { + return ( + + + + + + + + + + + + + + + + + ); +} + +export default EmptyIcon; diff --git a/src/components/molecules/ApplierListElement.component.tsx b/src/components/molecules/ApplierListElement.component.tsx new file mode 100644 index 0000000..9688fc4 --- /dev/null +++ b/src/components/molecules/ApplierListElement.component.tsx @@ -0,0 +1,70 @@ +import { Switch } from "antd"; +import Link from "next/link"; +import styled from "styled-components"; + +// components +import ProfileFrameComponent from "@src/components/molecules/ProfileFrame.component"; +import StarIcon from "@src/components/icon/Star.icon"; +import DocumentIcon from "@src/components/icon/Document.icon"; +import { FontSize } from "@src/styles/theme"; +import { StudyState } from "@src/constant/enum.constant"; + +const Container = styled.div` + display: grid; + grid-template-columns: 52px auto 52px 52px; + align-items: center; + gap: 15px; +`; +const InfoWrapper = styled.div` + width: 100%; +`; + +const Nickname = styled.p` + margin-bottom: 5px; + font-size: ${FontSize.Default}; +`; +const RateWrapper = styled.div` + display: flex; + align-items: center; + svg { + margin-right: 5px; + } + span { + font-size: ${FontSize.Small}; + } +`; +const CenterWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; +function ApplierListElementComponent({ userInfo }) { + return ( + + + + {userInfo.nickname} + + + {userInfo.rate || "비공개"} + + + + + + + + + + + + + + ); +} + +export default ApplierListElementComponent; diff --git a/src/components/molecules/Empty.component.tsx b/src/components/molecules/Empty.component.tsx new file mode 100644 index 0000000..ab54940 --- /dev/null +++ b/src/components/molecules/Empty.component.tsx @@ -0,0 +1,27 @@ +import EmptyIcon from "@src/components/icon/Empty.icon"; +import styled from "styled-components"; +import { FontSize } from "@src/styles/theme"; + +const EmptyWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + margin-top: 60px; + svg { + margin-bottom: 20px; + } + p { + font-size: ${FontSize.PrimaryDescription}; + } +`; + +function EmptyComponent({ message }) { + return ( + + +

{message}

+
+ ); +} + +export default EmptyComponent; diff --git a/src/components/molecules/ProfileFrame.component.tsx b/src/components/molecules/ProfileFrame.component.tsx index bf35edf..5de9f0b 100644 --- a/src/components/molecules/ProfileFrame.component.tsx +++ b/src/components/molecules/ProfileFrame.component.tsx @@ -11,7 +11,7 @@ import { FolderPathType } from "@src/constant/enum.constant"; interface ProfileFrameProps { mb?: string; - size?: "large" | "small" | "medium"; + size?: "large" | "small" | "mid-small" | "medium"; allowUpload?: boolean; imgSrc?: string; onUploadImage?: (e: Resource[]) => void; @@ -34,6 +34,8 @@ const CircleImageFrame = styled.div` return "80px"; case "medium": return "72px"; + case "mid-small": + return "56px"; case "small": return "32px"; default: @@ -50,6 +52,8 @@ const CircleImageFrame = styled.div` return "48px"; case "medium": return "48px"; + case "mid-small": + return "32px"; case "small": return "18px"; default: @@ -71,6 +75,12 @@ const CircleImageFrame = styled.div` height: 72px; border-width: 5px; `; + case "mid-small": + return ` + width: 56px; + height: 56px; + border-width: 5px; + `; case "small": return ` width: 32px; diff --git a/src/components/molecules/SearchLikeFilterLinkButton.component.tsx b/src/components/molecules/SearchLikeFilterLinkButton.component.tsx new file mode 100644 index 0000000..37fd04b --- /dev/null +++ b/src/components/molecules/SearchLikeFilterLinkButton.component.tsx @@ -0,0 +1,38 @@ +import Link from "next/link"; +import { Button } from "@src/components/atoms/Button"; +import theme, { Padding } from "@src/styles/theme"; +import ColoredSearchIcon from "@src/components/icon/ColoredSearch.icon"; +import styled from "styled-components"; + +const SearchContentsWrapper = styled.div` + display: flex; + align-items: center; + padding: 0 ${Padding.pageX}; + svg { + margin-right: 10px; + } +`; + +function SearchLikeFilterLinkButtonComponent({ contents, mb = "0" }) { + return ( + + + + + + ); +} + +export default SearchLikeFilterLinkButtonComponent; diff --git a/src/components/organs/BottomNavigation.component.tsx b/src/components/organs/BottomNavigation.component.tsx index 189a7df..e5554a8 100644 --- a/src/components/organs/BottomNavigation.component.tsx +++ b/src/components/organs/BottomNavigation.component.tsx @@ -50,11 +50,11 @@ function BottomNavigationComponent({ selected = NaviType.MAIN }) { - 카테고리 + 스터디 필터링 - + diff --git a/src/components/organs/CategorySelectDrawer.component.tsx b/src/components/organs/CategorySelectDrawer.component.tsx index e24b573..990b6f6 100644 --- a/src/components/organs/CategorySelectDrawer.component.tsx +++ b/src/components/organs/CategorySelectDrawer.component.tsx @@ -5,8 +5,11 @@ import styled from "styled-components"; import { NoScroll } from "@src/styles/common"; import { useStores } from "@src/store/root.store"; import CategorySelectComponent from "@src/components/molecules/CategorySelectComponent"; -import { Padding } from "@src/styles/theme"; +import { FontSize, Padding } from "@src/styles/theme"; import TitleHeaderComponent from "@src/components/molecules/TitleHeader.component"; +import { BoxInput } from "@src/components/atoms/Input"; +import InputWithSuffixComponent from "@src/components/molecules/InputWithSuffix.component"; +import { ErrorMessage } from "@src/components/atoms/text/ErrorMessage"; interface ContainerProp { visible: boolean; @@ -24,15 +27,25 @@ const Container = styled.div` top: 0; bottom: 0; display: ${({ visible }) => (visible ? "block" : "none")}; +`; +const LabelWrapper = styled.div` + display: flex; + align-items: center; + margin-bottom: 20px; + margin-top: 80px; h3 { - padding-top: 70px; - text-align: left; - margin-bottom: 20px; - font-weight: 600; - word-break: keep-all; + margin-bottom: 0; + margin-right: 10px; + } + span { + margin-left: 10px; + font-size: ${FontSize.Small}; } `; +const PointWrapper = styled.div` + margin-bottom: 10px; +`; const ContentWrapper = styled.div` ${NoScroll}; @@ -51,15 +64,26 @@ function CategorySelectDrawerComponent({ selectedList, setSelectedList, title = "", + description = "", multiple = true, + showPoint = false, + remain = null, + pointValue = "", + onChangePointValue = null, }) { const { categoryStore } = useStores(); const [categoryList, setCategoryList] = useState([]); + const [pointSelected, setPointSelected] = useState(false); const onToggleCategory = useCallback( (selected) => { - const originalList = [...selectedList]; + let originalList = [...selectedList]; + if (pointSelected) { + originalList = []; + if (onChangePointValue) onChangePointValue({ target: { value: "" } }); + } + setPointSelected(false); const found = originalList.find((x) => x.id === selected.id); @@ -68,46 +92,123 @@ function CategorySelectDrawerComponent({ else if (multiple) setSelectedList([...originalList, selected]); else setSelectedList([selected]); }, - [multiple, selectedList, setSelectedList], + [ + multiple, + onChangePointValue, + pointSelected, + selectedList, + setSelectedList, + ], ); useEffect(() => { - categoryStore.getCategoryList().then((list) => setCategoryList(list)); - }, [categoryStore]); + categoryStore + .getCategoryList(showPoint) + .then((list) => setCategoryList(list)); + }, [categoryStore, showPoint]); + + useEffect(() => { + const found = selectedList.find((x) => x.name === "포인트"); + if (found) setPointSelected(true); + }, [selectedList]); const categoryListDom = useMemo( () => - categoryList.map((x) => { - const onClick = () => onToggleCategory(x); - return ( - y.id === x.id) !== undefined} - name={x.name} - onClick={onClick} - /> - ); - }), + categoryList + .filter((x) => x.name !== "포인트") + .map((x) => { + const onClick = () => onToggleCategory(x); + return ( + y.id === x.id) !== undefined} + name={x.name} + onClick={onClick} + /> + ); + }), [categoryList, onToggleCategory, selectedList], ); + const pointInfo = useMemo( + () => categoryList.find((x) => x.name === "포인트"), + [categoryList], + ); + const selectedNothing = useMemo( () => selectedList.length === 0, [selectedList], ); + const onClickPoint = useCallback(() => { + setSelectedList([pointInfo]); + setPointSelected(true); + }, [pointInfo, setSelectedList]); + + const disableButton = useMemo( + () => + !showPoint || + (pointSelected && pointValue === "") || + (remain !== null && pointValue > remain), + [pointSelected, pointValue, remain, showPoint], + ); + return ( -

{title}

+ +

{title}

+ {description} +
+ {pointSelected && onChangePointValue && ( + + + } + suffix={ + remain !== null ? ( + + 잔여 코인 {remain}코인 + + ) : ( + <> + ) + } + /> + + )} + {remain !== null && pointValue > remain && ( + + 입력한 코인이 남은 코인보다 많습니다! + + )} + {pointInfo && ( + y.id === pointInfo.id) !== undefined + } + name={pointInfo.name} + onClick={onClickPoint} + /> + )} + {categoryListDom} diff --git a/src/components/organs/Popup.component.tsx b/src/components/organs/Popup.component.tsx index 22f08fb..eb8fe86 100644 --- a/src/components/organs/Popup.component.tsx +++ b/src/components/organs/Popup.component.tsx @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { FontSize } from "@src/styles/theme"; const Wrapper = styled.div` position: fixed; @@ -22,12 +23,27 @@ const Modal = styled.div` padding: 12px; text-align: center; `; - -function PopupComponent({ content, bottom }) { +const contentPadding = { padding: "15px 0 10px 0" }; +const Title = styled.h3` + font-size: ${FontSize.Default}; + margin-bottom: 10px; +`; +const Description = styled.p` + font-size: ${FontSize.PrimaryDescription}; + margin-bottom: 10px; +`; +function PopupComponent({ + title = "", + description = "" as string | JSX.Element, + bottom, +}) { return ( -
{content}
+
+ {title && {title}} + {description && {description}} +
{bottom}
diff --git a/src/components/organs/StudyList.component.tsx b/src/components/organs/StudyList.component.tsx index ad13b31..9443d6b 100644 --- a/src/components/organs/StudyList.component.tsx +++ b/src/components/organs/StudyList.component.tsx @@ -1,9 +1,25 @@ import { BoldDivider, LightDivider } from "@src/components/atoms/Divider"; import { TextButton } from "@src/components/atoms/TextButton"; -import { useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import StudyListElementComponent from "@src/components/organs/StudyListElement.component"; +import useIntersectionObserver from "@src/hooks/useIntersectionObserver.hook"; +import styled from "styled-components"; + +const ListWrapper = styled.div` + min-height: 50vh; +`; +const ThresholdWrapper = styled.div` + height: 20px; +`; +function StudyListComponent({ studyList, hasMore, loading, onClickNext }) { + const ref = useRef(null); + const entry = useIntersectionObserver(ref, { threshold: 0 }, !hasMore); + + useEffect( + () => !loading && hasMore && entry && onClickNext(), + [entry, hasMore, loading, onClickNext], + ); -function StudyListComponent({ studyList, hasMore, onClickNext }) { const studyListDom = useMemo( () => studyList.map((study, index) => { @@ -21,16 +37,17 @@ function StudyListComponent({ studyList, hasMore, onClickNext }) { ); return ( -
+ -
{studyListDom}
+ {studyListDom} + {hasMore && ( 더보기 )} -
+ ); } diff --git a/src/components/organs/StudyManageListElement.component.tsx b/src/components/organs/StudyManageListElement.component.tsx index b5a6850..80c36c2 100644 --- a/src/components/organs/StudyManageListElement.component.tsx +++ b/src/components/organs/StudyManageListElement.component.tsx @@ -1,3 +1,5 @@ +import Link from "next/link"; + import StudyListElementComponent from "@src/components/organs/StudyListElement.component"; import { LightDivider } from "@src/components/atoms/Divider"; import { Button } from "@src/components/atoms/Button"; @@ -36,15 +38,19 @@ function StudyManageListElementComponent({ study }) {
{study.user && }
- + + + + +
); diff --git a/src/hooks/useInfiniteLoading.hook.ts b/src/hooks/useInfiniteLoading.hook.ts index 90c850c..71a69a5 100644 --- a/src/hooks/useInfiniteLoading.hook.ts +++ b/src/hooks/useInfiniteLoading.hook.ts @@ -10,6 +10,7 @@ const useInfiniteLoading = ({ const [page, setPage] = useState(pageToLoad); const initialPageLoaded = useRef(false); const [hasMore, setHasMore] = useState(true); + const [loading, setLoading] = useState(false); const loadItems = useCallback( async (nextPage) => { @@ -20,12 +21,14 @@ const useInfiniteLoading = ({ setHasMore(true); /* 4 */ setItems((prevItems) => [...prevItems, ...data[listKeyName]]); } + setLoading(false); }, [getItems, listKeyName], ); const onNext = useCallback(() => { setPage(page + 1); + setLoading(true); loadItems(page + 1); }, [loadItems, page]); @@ -47,6 +50,7 @@ const useInfiniteLoading = ({ hasMore, loadItems, onNext, + loading, }; }; diff --git a/src/hooks/useIntersectionObserver.hook.tsx b/src/hooks/useIntersectionObserver.hook.tsx new file mode 100644 index 0000000..8d83594 --- /dev/null +++ b/src/hooks/useIntersectionObserver.hook.tsx @@ -0,0 +1,50 @@ +import { MutableRefObject, useEffect, useRef, useState } from "react"; + +// https://dev.to/anxinyang/infinite-scroll-with-react-hook-intersection-observer-img 코드 참고 +function useIntersectionObserver( + ref: MutableRefObject, + options: IntersectionObserverInit = {}, + forward = true, +) { + const [element, setElement] = useState(null); + const [isIntersecting, setIsIntersecting] = useState(false); + const observer = useRef(null); + + const cleanOb = () => { + if (observer.current) { + observer.current.disconnect(); + } + }; + + useEffect(() => { + setElement(ref.current); + }, [ref]); + + useEffect(() => { + if (!element) return; + cleanOb(); + // eslint-disable-next-line no-multi-assign + const ob = (observer.current = new IntersectionObserver( + ([entry]) => { + const isElementIntersecting = entry.isIntersecting; + if (!forward) { + setIsIntersecting(isElementIntersecting); + } else if (forward && !isIntersecting && isElementIntersecting) { + setIsIntersecting(isElementIntersecting); + cleanOb(); + } + }, + { ...options }, + )); + ob.observe(element); + + // eslint-disable-next-line consistent-return + return () => { + cleanOb(); + }; + // eslint-disable-next-line + }, [element, options]); + + return isIntersecting; +} +export default useIntersectionObserver; diff --git a/src/models/dto/study.dto.d.ts b/src/models/dto/study.dto.d.ts index b6e0036..dc747a4 100644 --- a/src/models/dto/study.dto.d.ts +++ b/src/models/dto/study.dto.d.ts @@ -13,9 +13,9 @@ export interface CreateStudyDto { chatRoom: string; roomPwd: string; images: string[]; - give: string[]; - take: string[]; - point: number; + give: string[] | number[]; + take: string[] | number[]; + point: number | string; } export interface StudyListElement { @@ -30,6 +30,15 @@ export interface StudyListElement { user?: UserProfile; } +export interface StudyFilteringDto { + filter: { + give: string; + take: string; + type: StudyType; + }; + study: StudyListElement[]; +} + export interface StudyDetailDto extends StudyListElement { content: string; storeName: string; @@ -43,3 +52,17 @@ export interface StudyDetailDto extends StudyListElement { export interface StudyManageListElement extends StudyListElement { state: StudyState; } + +export interface StudyMemberInfo { + userId: number; + studyMemberId: number; + nickname: string; + profileImg: string; + rate: number | null; + state: StudyState; +} + +export interface StudyApplyDetail extends StudyMemberInfo { + msg: string; + applyFiles: string[]; +} diff --git a/src/pages/main.tsx b/src/pages/main.tsx index ad02908..ef8c4eb 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -15,6 +15,7 @@ function UserMainPage() { items: studyList, hasMore, onNext, + loading, } = useInfiniteLoading({ getItems: (p) => studyStore.getStudyFeed(p), pageToLoad: 0, @@ -23,6 +24,7 @@ function UserMainPage() { return ( - studyStore.getFilteredStudy( - page, - router.query.take, - router.query.give, - router.query.type, - ), + studyStore + .getFilteredStudy( + page, + router.query.take, + router.query.give, + router.query.type, + ) + .then((res) => { + setGive(res.filter.give); + setTake(res.filter.take); + return res; + }), [router, studyStore], ); @@ -25,6 +33,7 @@ function SearchResultPage() { items: studyList, hasMore, onNext, + loading, } = useInfiniteLoading({ ready: router.query.take !== undefined && @@ -37,9 +46,13 @@ function SearchResultPage() { return ( ); } diff --git a/src/pages/study/create.tsx b/src/pages/study/create.tsx index 2deaf29..5e0247a 100644 --- a/src/pages/study/create.tsx +++ b/src/pages/study/create.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/router"; // components @@ -16,9 +16,10 @@ import { CreateStudyDto } from "@src/models/dto/study.dto"; import { StudyType } from "@src/constant/enum.constant"; import { AuthPermissionType } from "@src/constant/api.constant"; import { Resource } from "@src/models/dto/api-response"; +import useInput from "@src/hooks/useInput.hook"; function CreateStudyPage() { - const { studyStore } = useStores(); + const { studyStore, userStore } = useStores(); const router = useRouter(); const { value: startDate, onChange: onChangeStartDate } = useDatepicker(); @@ -28,6 +29,13 @@ function CreateStudyPage() { const [uploaded, setUploaded] = useState([]); const [allowDatepicker, setUseDatepicker] = useState(true); const [selectedCafe, setSelectedCafe] = useState(null); + const { value: givePoint, handleChange: onChangeGivePoint } = useInput(""); + const { value: takePoint, handleChange: onChangeTakePoint } = useInput(""); + const [remainPoint, setRemainPoint] = useState(0); + + useEffect(() => { + userStore.getMyPoint().then((point) => setRemainPoint(point)); + }, [userStore]); const { values, handleChange, handleSubmit, submitted } = useForm({ @@ -56,8 +64,9 @@ function CreateStudyPage() { give: selectedMine.map((x) => x.id), take: selectedYours.map((x) => x.id), images: uploaded.map((x) => x.path), - storeName: selectedCafe.place_name, - storeAddress: selectedCafe.road_address_name, + storeName: selectedCafe?.place_name || "", + storeAddress: selectedCafe?.road_address_name || "", + point: givePoint || takePoint || 0, }) .then(() => router.push(`/`)); }, @@ -89,6 +98,11 @@ function CreateStudyPage() { setSelectedYours={setSelectedYours} selectedCafe={selectedCafe} setSelectedCafe={setSelectedCafe} + givePoint={givePoint} + remainPoint={remainPoint} + onChangeGivePoint={onChangeGivePoint} + takePoint={takePoint} + onChangeTakePoint={onChangeTakePoint} uploaded={uploaded} setUploaded={setUploaded} /> diff --git a/src/pages/study/manage/[id].tsx b/src/pages/study/manage/[id].tsx new file mode 100644 index 0000000..5af269b --- /dev/null +++ b/src/pages/study/manage/[id].tsx @@ -0,0 +1,27 @@ +import { useStores } from "@src/store/root.store"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +// components +import StudyApplierListTemplate from "@src/templates/StudyApplierList.template"; + +function StudyApplierListPage() { + const router = useRouter(); + const { studyStore } = useStores(); + + const [applierList, setApplierList] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (router.query.id) + studyStore.findAllStudyMember(router.query.id).then((list) => { + setApplierList(list); + setLoading(false); + }); + }, [router.query.id, studyStore]); + + return ( + + ); +} + +export default StudyApplierListPage; diff --git a/src/pages/study/manage/apply/[id].tsx b/src/pages/study/manage/apply/[id].tsx new file mode 100644 index 0000000..f0c433b --- /dev/null +++ b/src/pages/study/manage/apply/[id].tsx @@ -0,0 +1,48 @@ +import { useCallback, useEffect, useState } from "react"; +import { useStores } from "@src/store/root.store"; +import { useRouter } from "next/router"; +import StudyApplyDetailTemplate from "@src/templates/StudyApplyDetail.template"; +import { StudyState } from "@src/constant/enum.constant"; +import useVisibleHook from "@src/hooks/useVisible.hook"; + +function StudyApplyDetailPage() { + const { studyStore } = useStores(); + const router = useRouter(); + + const [detail, setDetail] = useState(null); + const [popupMessage, setPopupMessage] = useState(""); + const [popupVisible, setPopupVisible, setPopupInvisible] = + useVisibleHook(false); + + useEffect(() => { + if (router.query.id) + studyStore + .findStudyMemberDetail(router.query.id) + .then((res) => setDetail(res)); + }, [router.query.id, studyStore]); + + const onClickApproval = useCallback( + (nextState: StudyState) => { + studyStore + .approveStudyMember(detail.studyMemberId, nextState) + .then((res) => { + setPopupMessage(res); + setPopupVisible(); + setDetail({ ...detail, state: nextState }); + }); + }, + [detail, setPopupVisible, studyStore], + ); + + return ( + + ); +} + +export default StudyApplyDetailPage; diff --git a/src/pages/profile/study.tsx b/src/pages/study/manage/index.tsx similarity index 100% rename from src/pages/profile/study.tsx rename to src/pages/study/manage/index.tsx diff --git a/src/services/Category.service.ts b/src/services/Category.service.ts index 5dafd31..9966a17 100644 --- a/src/services/Category.service.ts +++ b/src/services/Category.service.ts @@ -4,9 +4,9 @@ import { Category } from "@src/models/dto/signup.dto"; export default class CategoryService extends BaseHttpService { prefix = "/category"; - async getCategoryList() { + async getCategoryList(containPoint = false) { return (await this.get( - `${this.prefix}?point=false`, + `${this.prefix}?point=${containPoint ? "true" : "false"}`, {}, )) as Category[]; } diff --git a/src/services/Study.service.ts b/src/services/Study.service.ts index 6760d86..ed0bd1e 100644 --- a/src/services/Study.service.ts +++ b/src/services/Study.service.ts @@ -1,10 +1,14 @@ import BaseHttpService from "@src/services/BaseHttp.service"; import { CreateStudyDto, + StudyApplyDetail, StudyDetailDto, + StudyFilteringDto, StudyListElement, StudyManageListElement, + StudyMemberInfo, } from "@src/models/dto/study.dto"; +import { StudyState } from "@src/constant/enum.constant"; export default class StudyService extends BaseHttpService { prefix = "/study"; @@ -24,10 +28,10 @@ export default class StudyService extends BaseHttpService { give: string | string[], take: string | string[], type: string | string[], - ): Promise { - return (await this.get( + ): Promise { + return (await this.get( `/page/filter?page=${page}&give=${give}&take=${take}&type=${type}`, - )) as StudyListElement[]; + )) as StudyFilteringDto; } async getMyStudyList(): Promise { @@ -48,6 +52,25 @@ export default class StudyService extends BaseHttpService { )) as StudyDetailDto; } + async findAllStudyMember(id) { + return (await this.get( + `/studymember/all?studyId=${id}`, + )) as StudyMemberInfo[]; + } + + async findStudyMemberDetail(memberId) { + return (await this.get( + `/studymember/detail?studyMemberId=${memberId}`, + )) as StudyApplyDetail; + } + + async approveStudyMember(studyMemberId: number, state: StudyState) { + return (await this.post(`/studymember/approval`, { + studyMemberId, + state, + })) as string; + } + async applyStudy( id: number, content: string, diff --git a/src/services/User.service.ts b/src/services/User.service.ts index 96982b4..6a2e638 100644 --- a/src/services/User.service.ts +++ b/src/services/User.service.ts @@ -8,9 +8,13 @@ export default class UserService extends BaseHttpService { return (await this.get(`${this.prefix}/univ`)) as string; } - async findMyPageProfile(): Promise { + async findMyPageProfile(id = null): Promise { return (await this.get( - `${this.prefix}/mypage`, + `${this.prefix}/mypage${id ? `?id=${id}` : ""}`, )) as UserMyPageDto; } + + async findMyRemainPoint(): Promise { + return (await this.get(`${this.prefix}/point`)) as number; + } } diff --git a/src/store/category.store.ts b/src/store/category.store.ts index a4487af..5382314 100644 --- a/src/store/category.store.ts +++ b/src/store/category.store.ts @@ -17,9 +17,11 @@ export default class CategoryStore { makeAutoObservable(this); } - async getCategoryList() { + async getCategoryList(containPoint = false) { if (this.categoryList.length === 0) { - const categoryList = await this.categoryService.getCategoryList(); + const categoryList = await this.categoryService.getCategoryList( + containPoint, + ); this.categoryList = [...categoryList]; } diff --git a/src/store/study.store.ts b/src/store/study.store.ts index 93024a1..e883865 100644 --- a/src/store/study.store.ts +++ b/src/store/study.store.ts @@ -3,10 +3,14 @@ import { makeAutoObservable } from "mobx"; import StudyService from "@src/services/Study.service"; import { CreateStudyDto, + StudyApplyDetail, StudyDetailDto, + StudyFilteringDto, StudyListElement, StudyManageListElement, + StudyMemberInfo, } from "@src/models/dto/study.dto"; +import { StudyState } from "@src/constant/enum.constant"; export default class StudyStore { private readonly rootStore: RootStore; @@ -33,13 +37,13 @@ export default class StudyStore { give: string | string[], take: string | string[], type: string | string[], - ): Promise { + ): Promise { return (await this.studyService.getFilteredStudy( page, give, take, type, - )) as StudyListElement[]; + )) as StudyFilteringDto; } async getMyStudyList(): Promise { @@ -54,6 +58,25 @@ export default class StudyStore { return (await this.studyService.getStudyDetail(id)) as StudyDetailDto; } + async findAllStudyMember(id) { + return (await this.studyService.findAllStudyMember( + id, + )) as StudyMemberInfo[]; + } + + async findStudyMemberDetail(memberId) { + return (await this.studyService.findStudyMemberDetail( + memberId, + )) as StudyApplyDetail; + } + + async approveStudyMember(studyMemberId: number, state: StudyState) { + return (await this.studyService.approveStudyMember( + studyMemberId, + state, + )) as string; + } + async applyStudy( id: number, content: string, diff --git a/src/store/user.store.ts b/src/store/user.store.ts index 1437ab2..87e48fb 100644 --- a/src/store/user.store.ts +++ b/src/store/user.store.ts @@ -81,4 +81,12 @@ export default class UserStore { this.myProfile = (await this.userService.findMyPageProfile()) as UserMyPageDto; } + + async getUserProfile(id) { + return (await this.userService.findMyPageProfile(id)) as UserMyPageDto; + } + + async getMyPoint() { + return (await this.userService.findMyRemainPoint()) as number; + } } diff --git a/src/stories/molecules/ApplierListElement.stories.tsx b/src/stories/molecules/ApplierListElement.stories.tsx new file mode 100644 index 0000000..f007713 --- /dev/null +++ b/src/stories/molecules/ApplierListElement.stories.tsx @@ -0,0 +1,24 @@ +import ApplierListElementComponent from "@src/components/molecules/ApplierListElement.component"; + +export default { + title: "molecules/Applier List Element", +}; + +const userSample = { + userId: 3, + studyMemberId: 7, + nickname: "예진", + profileImg: + "https://fork-fork-cake.s3.ap-northeast-2.amazonaws.com/profile/5750452df.jpeg", + rate: null, + state: 2, +}; + +const Template = ({ userInfo }) => ( + +); + +export const ApplierListElement = Template.bind({}); +ApplierListElement.args = { + userInfo: userSample, +}; diff --git a/src/stories/molcules/TitleHeader.stories.tsx b/src/stories/molecules/TitleHeader.stories.tsx similarity index 100% rename from src/stories/molcules/TitleHeader.stories.tsx rename to src/stories/molecules/TitleHeader.stories.tsx diff --git a/src/stories/organs/Popup.stories.tsx b/src/stories/organs/Popup.stories.tsx index 0c35c8e..5c4c1af 100644 --- a/src/stories/organs/Popup.stories.tsx +++ b/src/stories/organs/Popup.stories.tsx @@ -18,17 +18,19 @@ const bottom = ( ); -const content = ( -
-

스터디 참여 신청 완료!

-

- 신청 내역 및 스터디 선정 여부는 -
- 스터디 관리에서 확인하세요! -

-
+const Template = () => ( + + 신청 내역 및 스터디 선정 여부는 +
+ 스터디 관리에서 확인하세요! +

+ } + /> ); -const Template = () => ; export const Popup = Template.bind({}); diff --git a/src/stories/templates/StudyJoinSuccessModal.component.tsx b/src/stories/templates/StudyJoinSuccessModal.component.tsx index a65067d..7985e5b 100644 --- a/src/stories/templates/StudyJoinSuccessModal.component.tsx +++ b/src/stories/templates/StudyJoinSuccessModal.component.tsx @@ -1,12 +1,10 @@ import { Button } from "@src/components/atoms/Button"; import Link from "next/link"; import PopupComponent from "@src/components/organs/Popup.component"; -import styled from "styled-components"; -import { FontSize } from "@src/styles/theme"; const Bottom = () => ( ); -const contentPadding = { padding: "15px 0 10px 0" }; -const Title = styled.h3` - font-size: ${FontSize.Default}; - margin-bottom: 10px; -`; -const Description = styled.p` - font-size: ${FontSize.PrimaryDescription}; - margin-bottom: 10px; -`; -const content = ( -
- 스터디 참여 신청 완료! - - 신청 내역 및 스터디 선정 여부는 -
- 스터디 관리에서 확인하세요! -
-
-); function StudyJoinSuccessModalComponent() { - return } content={content} />; + return ( + } + title="스터디 참여 신청 완료!" + description={ +
+ 신청 내역 및 스터디 선정 여부는 +
+ 스터디 관리에서 확인하세요! +
+ } + /> + ); } export default StudyJoinSuccessModalComponent; diff --git a/src/stories/templates/UserMain.stories.tsx b/src/stories/templates/UserMain.stories.tsx index 4dc40ab..df31d71 100644 --- a/src/stories/templates/UserMain.stories.tsx +++ b/src/stories/templates/UserMain.stories.tsx @@ -17,7 +17,7 @@ const studySample: StudyListElementDto = { img: "https://cdn.pixabay.com/photo/2021/09/01/16/09/cake-6591719__340.jpg", }; const Template = ({ studyList = [] }) => ( - + ); export const UserMain = Template.bind({}); diff --git a/src/styles/common.ts b/src/styles/common.ts index 4e12d9e..0c8acbb 100644 --- a/src/styles/common.ts +++ b/src/styles/common.ts @@ -3,6 +3,7 @@ import theme, { FontSize } from "@src/styles/theme"; export interface BaseProps extends ThemeProps { mb?: string; + mr?: string; pt?: string; fontSize?: "small" | "default" | "large"; height?: string; @@ -13,6 +14,10 @@ export const BaseMarginBottom = css` margin-bottom: ${(props: BaseProps) => props.mb}; `; +export const BaseMarginRight = css` + margin-right: ${(props: BaseProps) => props.mr}; +`; + export const BasePaddingTop = css` padding-top: ${(props: BaseProps) => props.pt}; `; @@ -74,6 +79,7 @@ export const NoScroll = css` export const BaseStyleProps = css` ${BasePaddingTop}; ${BaseMarginBottom}; + ${BaseMarginRight}; ${BaseFontSize}; ${BaseHeight}; ${BaseTextAlign}; diff --git a/src/templates/Filtering.template.tsx b/src/templates/Filtering.template.tsx index 29a74be..42c1ef8 100644 --- a/src/templates/Filtering.template.tsx +++ b/src/templates/Filtering.template.tsx @@ -69,7 +69,7 @@ function FilteringTemplate({ const myCategoryDom = useMemo( () => selectedMine.length === 0 ? ( -

내가 잘 하는 것은?

+

다른 사람에게 줄 수 있는 것은?

) : (
{selectedMine.map((x) => ( @@ -97,22 +97,26 @@ function FilteringTemplate({ return ( diff --git a/src/templates/SearchResult.template.tsx b/src/templates/SearchResult.template.tsx index a87cf58..dbdbe16 100644 --- a/src/templates/SearchResult.template.tsx +++ b/src/templates/SearchResult.template.tsx @@ -1,24 +1,44 @@ import PageWrapperComponent from "@src/components/organs/PageWrapper.component"; import BottomNavigationComponent from "@src/components/organs/BottomNavigation.component"; -import { NaviType } from "@src/constant/enum.constant"; +import { NaviType, StudyTypeEnumToLabel } from "@src/constant/enum.constant"; import styled from "styled-components"; import { Padding } from "@src/styles/theme"; import StudyListComponent from "@src/components/organs/StudyList.component"; +import SearchLikeFilterLinkButtonComponent from "@src/components/molecules/SearchLikeFilterLinkButton.component"; +import EmptyComponent from "@src/components/molecules/Empty.component"; const Wrapper = styled.div` padding: 0 ${Padding.pageX}; `; -function SearchResultTemplate({ studyList, hasMore, onNext }) { +function SearchResultTemplate({ + give, + take, + type, + studyList, + hasMore, + onNext, + loading, +}) { return ( + + + {studyList.length === 0 && ( + + )} + {studyList.length > 0 && ( - + )} ); diff --git a/src/templates/StudyApplierList.template.tsx b/src/templates/StudyApplierList.template.tsx new file mode 100644 index 0000000..94f5f45 --- /dev/null +++ b/src/templates/StudyApplierList.template.tsx @@ -0,0 +1,52 @@ +import styled from "styled-components"; + +// lib +import { NaviType } from "@src/constant/enum.constant"; + +// components +import PageWrapperComponent from "@src/components/organs/PageWrapper.component"; +import BottomNavigationComponent from "@src/components/organs/BottomNavigation.component"; +import ApplierListElementComponent from "@src/components/molecules/ApplierListElement.component"; +import EmptyComponent from "@src/components/molecules/Empty.component"; + +// styles +import { FontSize, Padding } from "@src/styles/theme"; + +const ListWrapper = styled.div` + padding: 20px ${Padding.pageX}; +`; +const HeaderWrapper = styled.div` + display: grid; + grid-template-columns: 52px auto 52px 52px; + gap: 15px; + p { + margin-bottom: 0; + font-size: ${FontSize.Small}; + text-align: center; + } +`; +function StudyApplierListTemplate({ loading, applierList }) { + return ( + + + {!loading && applierList.length === 0 && ( + + )} + {applierList.length > 0 && ( + +
+
+

참여 여부

+

신청서

+ + )} + {applierList.map((user) => ( + + ))} + + + + ); +} + +export default StudyApplierListTemplate; diff --git a/src/templates/StudyApplyDetail.template.tsx b/src/templates/StudyApplyDetail.template.tsx new file mode 100644 index 0000000..783beeb --- /dev/null +++ b/src/templates/StudyApplyDetail.template.tsx @@ -0,0 +1,109 @@ +import { StudyState } from "@src/constant/enum.constant"; + +// components +import PageWrapperComponent from "@src/components/organs/PageWrapper.component"; +import ImageGalleryComponent from "@src/components/molecules/ImageGallery.component"; +import SimpleProfileComponent from "@src/components/molecules/SimpleProfile.component"; +import { useMemo } from "react"; +import LoadingComponent from "@src/components/molecules/Loading.component"; +import { + ImageWrapper, + StudyContentsWrapper, + StudyWrapper, +} from "@src/templates/StudyDetail.template"; +import { Button } from "@src/components/atoms/Button"; +import PopupComponent from "@src/components/organs/Popup.component"; + +function StudyApplyDetailTemplate({ + applyDetail, + onClickApproval, + popupVisible, + onClosePopup, + popupMessage, +}) { + const applyButton = useMemo(() => { + if (!applyDetail) return ""; + switch (applyDetail.state) { + case StudyState.APPLIED: + return ( + + ); + case StudyState.JOINED: + return ( + + ); + case StudyState.REJECTED: + return ( + + ); + default: + return ""; + } + }, [applyDetail, onClickApproval]); + + const appliedUser = useMemo( + () => + applyDetail + ? { + img: applyDetail.profileImg, + nickname: applyDetail.nickname, + rate: applyDetail.rate, + } + : null, + [applyDetail], + ); + return applyDetail ? ( + + {/* Thumbnail */} + + {/* {study.title} */} + + + + {/* Profile */} + + {/* Contents */} + {applyDetail.msg} + + {popupVisible && ( + + 확인 + + } + description={popupMessage} + /> + )} + + ) : ( + + ); +} + +export default StudyApplyDetailTemplate; diff --git a/src/templates/StudyCreate.template.tsx b/src/templates/StudyCreate.template.tsx index d1709b5..9e365f5 100644 --- a/src/templates/StudyCreate.template.tsx +++ b/src/templates/StudyCreate.template.tsx @@ -82,6 +82,11 @@ function StudyCreateTemplate({ setSelectedYours, selectedCafe, setSelectedCafe, + givePoint, + onChangeGivePoint, + takePoint, + onChangeTakePoint, + remainPoint, uploaded, setUploaded, }) { @@ -101,21 +106,20 @@ function StudyCreateTemplate({ (startDate && current < dayjs(startDate).add(-1, "day").endOf("day")), [startDate], ); - const myCategoryDom = useMemo( - () => - selectedMine.length === 0 - ? "나의 능력" - : selectedMine.map((x) => {x.name}), - [selectedMine], - ); + const myCategoryDom = useMemo(() => { + if (selectedMine.length === 0) return "Give"; + if (givePoint) + return {givePoint} 포인트; + return selectedMine.map((x) => {x.name}); + }, [givePoint, selectedMine]); + + const yourCategoryDom = useMemo(() => { + if (selectedYours.length === 0) return "Take"; + if (takePoint) + return {takePoint} 포인트; + return selectedYours.map((x) => {x.name}); + }, [selectedYours, takePoint]); - const yourCategoryDom = useMemo( - () => - selectedYours.length === 0 - ? "너의 능력" - : selectedYours.map((x) => {x.name}), - [selectedYours], - ); return (
diff --git a/src/templates/StudyDetail.template.tsx b/src/templates/StudyDetail.template.tsx index c7a7f3e..deaee75 100644 --- a/src/templates/StudyDetail.template.tsx +++ b/src/templates/StudyDetail.template.tsx @@ -20,7 +20,7 @@ import ImageGalleryComponent from "@src/components/molecules/ImageGallery.compon // styles import theme, { FontSize, Padding, windowSize } from "@src/styles/theme"; -const StudyContentsWrapper = styled.div` +export const StudyContentsWrapper = styled.div` padding: 20px ${Padding.pageX} 0; h3 { margin-bottom: 7px; @@ -31,7 +31,7 @@ const StudyContentsWrapper = styled.div` } `; -const ImageWrapper = styled.div` +export const ImageWrapper = styled.div` width: 100%; overflow: hidden; display: flex; @@ -71,7 +71,7 @@ const LocationWrapper = styled.div` } `; -const StudyWrapper = styled.div` +export const StudyWrapper = styled.div` white-space: pre-wrap; margin-top: 20px; padding-bottom: 20px; diff --git a/src/templates/UserMain.template.tsx b/src/templates/UserMain.template.tsx index 6bf142d..ed258f1 100644 --- a/src/templates/UserMain.template.tsx +++ b/src/templates/UserMain.template.tsx @@ -8,15 +8,15 @@ import { useStores } from "@src/store/root.store"; // components import { Button } from "@src/components/atoms/Button"; import { CategoryTag } from "@src/components/atoms/CategoryTag"; -import ColoredSearchIcon from "@src/components/icon/ColoredSearch.icon"; import PencilIcon from "@src/components/icon/Pencil.icon"; import CakeIcon from "@src/components/icon/Cake.icon"; import BottomNavigationComponent from "@src/components/organs/BottomNavigation.component"; import FloatingButtonComponent from "@src/components/molecules/FloatingButton.component"; import StudyListComponent from "@src/components/organs/StudyList.component"; +import SearchLikeFilterLinkButtonComponent from "@src/components/molecules/SearchLikeFilterLinkButton.component"; // styles -import theme, { Color, FontSize, Padding } from "@src/styles/theme"; +import { Color, FontSize, Padding } from "@src/styles/theme"; import styled, { css } from "styled-components"; import { NoScroll } from "@src/styles/common"; import { NaviType } from "@src/constant/enum.constant"; @@ -80,19 +80,11 @@ const CategoryListElementWrapper = styled.div` margin-right: 8px; } `; -const SearchContentsWrapper = styled.div` - display: flex; - align-items: center; - padding: 0 ${Padding.pageX}; - svg { - margin-right: 10px; - } -`; - function UserMainTemplate({ studyList = [], onClickNext = () => null, hasMore = false, + loading, }) { const { categoryStore } = useStores(); const [categoryList, setCategoryList] = useState([]); @@ -109,22 +101,7 @@ function UserMainTemplate({ return ( - - - - - +

@@ -151,6 +128,7 @@ function UserMainTemplate({