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 */}
+
+ {/* */}
+
+
+
+ {/* 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({