-
+
css`
- ${flexGenerator('column')};
+ ${flexGenerator('column', 'flex-start', 'flex-start')};
gap: ${gapSize}rem;
width: 100%;
`;
+
export const referTextStyle = (theme: Theme) => css`
${flexGenerator('row', 'flex-start')};
width: 100%;
diff --git a/src/pages/class/components/StepOne/StepOne.tsx b/src/pages/class/components/StepOne/StepOne.tsx
index 73e7796f..0f9b4979 100644
--- a/src/pages/class/components/StepOne/StepOne.tsx
+++ b/src/pages/class/components/StepOne/StepOne.tsx
@@ -149,6 +149,7 @@ const StepOne = ({ onNext }: StepProps) => {
onStartTimeChange={handleStartTimeChange}
onEndTimeChange={handleEndTimeChange}
/>
+
*픽플은 최소 10분 이상의 네트워킹 시간을 권장합니다.
몇 명의 게스트와 함께하고 싶으신가요?
diff --git a/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.style.ts b/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.style.ts
index 2862028a..82444435 100644
--- a/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.style.ts
+++ b/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.style.ts
@@ -39,11 +39,15 @@ export const questionMainStyle = css`
export const questionDataStyle = css`
${flexGenerator('column', 'flex-start', 'flex-start')}
- /* padding: 1.2rem 1.6rem 1rem 1rem; */
- gap: 1.5rem;
+ gap: 3rem;
width: 100%;
`;
+export const eachQuestionWrapper = css`
+ ${flexGenerator('column')}
+ gap: 1rem;
+`;
+
export const questionCautionStyle = (theme: Theme) => css`
${flexGenerator('row', 'flex-start', 'flex-start')};
gap: 1rem;
@@ -65,6 +69,12 @@ export const questionCautionTextStyle = (theme: Theme) => css`
${theme.font['body02-r-14']}
`;
+export const question4WrapperStyle = css`
+ ${flexGenerator('column', 'flex-start', 'flex-start')}
+ width: 100%;
+ gap: 1.5rem;
+`;
+
export const questionRefundStyle = () => css`
${flexGenerator('row', 'flex-start', 'flex-start')};
gap: 1.5rem;
diff --git a/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.tsx b/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.tsx
index 3dd07217..9fe52d4c 100644
--- a/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.tsx
+++ b/src/pages/class/page/ClassApply/ClassApplyQuestion/ClassApplyQuestion.tsx
@@ -11,7 +11,9 @@ import { IcCaution } from '@svg';
import AccountNumberInput from 'src/components/common/inputs/AccountNumberInput/AccountNumberInput';
import {
+ eachQuestionWrapper,
headerStyle,
+ question4WrapperStyle,
questionArticleLayout,
questionCautionIconStyle,
questionCautionStyle,
@@ -125,7 +127,7 @@ const ClassApplyQuestion = ({ handlePageChange }: ClassApplyProps) => {
{questionList.map((question, index) => (
{question && (
- <>
+
{question}
)}
))}
@@ -152,7 +154,7 @@ const ClassApplyQuestion = ({ handlePageChange }: ClassApplyProps) => {
-
+
승인 거절 시 환불 받을 계좌를 알려주세요
@@ -192,7 +194,7 @@ const ClassApplyQuestion = ({ handlePageChange }: ClassApplyProps) => {
diff --git a/src/pages/host/components/HostClassEmptyView/HostClassEmptyView.style.ts b/src/pages/host/components/HostClassEmptyView/HostClassEmptyView.style.ts
new file mode 100644
index 00000000..25c74e78
--- /dev/null
+++ b/src/pages/host/components/HostClassEmptyView/HostClassEmptyView.style.ts
@@ -0,0 +1,23 @@
+import { css, Theme } from '@emotion/react';
+
+import { flexGenerator } from '@styles/generator';
+
+export const emptyClassContainer = css`
+ ${flexGenerator('column')};
+ gap: 1rem;
+ height: 24rem;
+`;
+
+export const emptyClassImageStyle = css`
+ width: 15rem;
+ height: 15rem;
+`;
+
+export const emptyClassTextWrapper = css`
+ ${flexGenerator('column')}
+`;
+
+export const emptyClassTextStyle = (theme: Theme) => css`
+ ${theme.font['subhead02-sb-16']};
+ color: ${theme.color.lightgray2};
+`;
diff --git a/src/pages/host/components/HostClassEmptyView/HostClassEmptyView.tsx b/src/pages/host/components/HostClassEmptyView/HostClassEmptyView.tsx
new file mode 100644
index 00000000..cf9a258e
--- /dev/null
+++ b/src/pages/host/components/HostClassEmptyView/HostClassEmptyView.tsx
@@ -0,0 +1,23 @@
+import { graphicImage } from '@constants';
+import {
+ emptyClassContainer,
+ emptyClassImageStyle,
+ emptyClassTextStyle,
+ emptyClassTextWrapper,
+} from '@pages/host/components/HostClassEmptyView/HostClassEmptyView.style';
+
+const HostClassEmptyView = () => {
+ return (
+
+
+
+
+
스픽커가 클래스를
+
준비하고 있어요!
+
+
+
+ );
+};
+
+export default HostClassEmptyView;
diff --git a/src/pages/host/components/StepTwo/StepTwo.tsx b/src/pages/host/components/StepTwo/StepTwo.tsx
index cb6177e4..9a84da7e 100644
--- a/src/pages/host/components/StepTwo/StepTwo.tsx
+++ b/src/pages/host/components/StepTwo/StepTwo.tsx
@@ -114,7 +114,7 @@ const StepTwo = ({ onNext }: StepProps) => {
diff --git a/src/pages/host/page/HostInfoEditPage/HostInfoEditPage.style.ts b/src/pages/host/page/HostInfoEditPage/HostInfoEditPage.style.ts
new file mode 100644
index 00000000..5c6f1743
--- /dev/null
+++ b/src/pages/host/page/HostInfoEditPage/HostInfoEditPage.style.ts
@@ -0,0 +1,67 @@
+import { Theme, css } from '@emotion/react';
+
+import { flexGenerator } from '@styles/generator';
+
+export const hostInfoLayout = css`
+ ${flexGenerator('column', 'flex-start', 'flex-start')}
+ gap: 7.2rem;
+ padding-top: 6rem;
+ width: 100%;
+`;
+
+export const hostInfoContainer = css`
+ width: 100%;
+`;
+
+export const hostImageWrapper = css`
+ position: relative;
+`;
+
+export const hostBackgroundImage = css`
+ width: 100%;
+ height: 11.6rem;
+`;
+
+export const hostProfileImage = css`
+ position: absolute;
+ top: 7.4rem;
+ left: 2rem;
+`;
+
+export const hostInfoEditIcon = css`
+ width: 2.4rem;
+ height: 2.4rem;
+ position: absolute;
+ top: 12.6rem;
+ left: 8.7rem;
+
+ cursor: pointer;
+`;
+
+export const hostInfoEditInput = css`
+ display: none;
+`;
+
+export const hostInputContainer = css`
+ ${flexGenerator('column', 'flex-start', 'flex-start')};
+ gap: 5.4rem;
+ width: 100%;
+ padding: 0 2rem 3rem 2rem;
+`;
+
+export const hostInputWrapper = css`
+ ${flexGenerator('column', 'flex-start', 'flex-start')};
+ gap: 2.2rem;
+ width: 100%;
+`;
+
+export const hostTextAreaWrapper = css`
+ ${flexGenerator('column', 'flex-start', 'flex-start')};
+ gap: 0.8rem;
+ width: 100%;
+`;
+
+export const hostTextAreaLabelStyle = (theme: Theme) => css`
+ ${theme.font['subhead05-sb-14']};
+ color: ${theme.color.midgray2};
+`;
diff --git a/src/pages/host/page/HostInfoEditPage/HostInfoEditPage.tsx b/src/pages/host/page/HostInfoEditPage/HostInfoEditPage.tsx
new file mode 100644
index 00000000..b9c5317f
--- /dev/null
+++ b/src/pages/host/page/HostInfoEditPage/HostInfoEditPage.tsx
@@ -0,0 +1,202 @@
+import { useRef, useState } from 'react';
+import { useParams } from 'react-router-dom';
+
+import { useFetchHostInfo } from '@apis/domains/host/useFetchHostInfo';
+import { usePatchHostInfo } from '@apis/domains/host/usePatchHostInfo';
+import { usePutS3Upload } from '@apis/domains/presignedUrl';
+
+import { Button, Header, Image, Input, TextArea } from '@components';
+import { images } from '@constants';
+import {
+ hostBackgroundImage,
+ hostImageWrapper,
+ hostInfoContainer,
+ hostInfoEditIcon,
+ hostInfoEditInput,
+ hostInfoLayout,
+ hostInputContainer,
+ hostInputWrapper,
+ hostProfileImage,
+ hostTextAreaLabelStyle,
+ hostTextAreaWrapper,
+} from '@pages/host/page/HostInfoEditPage/HostInfoEditPage.style';
+import { IcCamera } from '@svg';
+import { handleUpload } from '@utils';
+
+import { components } from '@schema';
+type HostUpdateRequest = components['schemas']['HostUpdateRequest'];
+
+const HostInfoEditPage = () => {
+ const { hostId } = useParams();
+ const { data: hostInfoData } = useFetchHostInfo(Number(hostId));
+ const { profileUrl, nickName, keyword, description, socialLink } = hostInfoData ?? {};
+ const nicknameRef = useRef
(null);
+ const [isNicknameDuplicate, setIsNicknameDuplicate] = useState(false);
+ const { mutate } = usePatchHostInfo(Number(hostId), setIsNicknameDuplicate, nicknameRef);
+ const { mutateAsync: putS3UploadMutateAsync } = usePutS3Upload();
+
+ const fileInputRef = useRef(null);
+ const [selectedFiles, setSelectedFiles] = useState();
+ const [hostInfoValue, setHostInfoValue] = useState({
+ profileUrl: profileUrl || images.HostProfileImage,
+ nickname: `${nickName}`,
+ keyword: `${keyword}`,
+ description: `${description}`,
+ socialLink: `${socialLink}`,
+ });
+
+ const allInputFilled = Object.values(hostInfoValue).every((value) => value?.trim() !== '');
+
+ const handleProfileImageIconClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleProfileImageChange = (e: React.ChangeEvent) => {
+ const file = e.target.files && e.target.files[0];
+ if (!file) return;
+
+ // 파일 프리뷰 설정하기
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setHostInfoValue((prevState) => ({
+ ...prevState,
+ profileUrl: reader.result as string,
+ }));
+ };
+
+ reader.readAsDataURL(file);
+ setSelectedFiles([file]);
+ };
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ key: keyof HostUpdateRequest
+ ) => {
+ const value = e.target.value;
+ setHostInfoValue((prevState) => ({
+ ...prevState,
+ [key]: value,
+ }));
+ };
+
+ const isValid = (value: string) => {
+ return value.trim().length >= 1;
+ };
+
+ const handleButtonClick = async (): Promise => {
+ let imageUrl: string = '';
+
+ // 파일이 선택된 경우 Presigned URL을 얻고 S3에 파일 업로드
+ if (selectedFiles?.length === 1) {
+ try {
+ const imageUrlList = await handleUpload({
+ selectedFiles,
+ putS3Upload: putS3UploadMutateAsync,
+ type: 'HOST_PROFILE_PREFIX',
+ });
+ imageUrl = imageUrlList[0];
+ } catch (error) {
+ console.error('S3 업로드 실패: ', error);
+ return;
+ }
+ }
+
+ // 업로드된 이미지 URL로 hostInfoValue 업데이트
+ if (imageUrl !== '') {
+ const updateHostInfoValue = { ...hostInfoValue, profileUrl: imageUrl };
+ mutate({ hostId: Number(hostId), hostInfoValue: updateHostInfoValue });
+ } else {
+ mutate({ hostId: Number(hostId), hostInfoValue });
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ handleProfileImageChange(e)}
+ css={hostInfoEditInput}
+ />
+
+
+
+
+
+
+ >
+ );
+};
+
+export default HostInfoEditPage;
diff --git a/src/pages/host/page/HostInfoPage/HostInfoPage.style.ts b/src/pages/host/page/HostInfoPage/HostInfoPage.style.ts
new file mode 100644
index 00000000..4a1c6be6
--- /dev/null
+++ b/src/pages/host/page/HostInfoPage/HostInfoPage.style.ts
@@ -0,0 +1,148 @@
+import { Theme, css } from '@emotion/react';
+
+import { flexGenerator } from '@styles/generator';
+
+export const hostInfoLayout = css`
+ padding-top: 6rem;
+ ${flexGenerator('column', 'flex-start', 'flex-start')}
+ gap: 4.8rem;
+ width: 100%;
+ min-width: 37.5rem;
+`;
+
+export const hostInfoContainer = css`
+ ${flexGenerator('column', 'center', 'start')}
+ width: 100%;
+`;
+
+export const hostImageWrapper = css`
+ position: relative;
+ width: 100%;
+ height: 11.6rem;
+`;
+
+export const hostBackgroundImage = css`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 11.6rem;
+`;
+
+export const hostProfileImage = css`
+ position: absolute;
+ top: 7.4rem;
+ left: 2rem;
+`;
+
+export const hostInfoEditIcon = css`
+ width: 2.4rem;
+ height: 2.4rem;
+ position: absolute;
+ top: 12.6rem;
+ left: 8.7rem;
+`;
+
+export const hostProfileContainer = css`
+ ${flexGenerator('column', 'center', 'flex-start')};
+ gap: 1rem;
+ padding: 5rem 2rem 0rem;
+ width: 100%;
+`;
+
+export const hostProfileWrapper = css`
+ ${flexGenerator('column', 'center', 'flex-start')};
+ gap: 2rem;
+ width: 100%;
+`;
+
+export const hostProfileStyle = css`
+ ${flexGenerator('column', 'center', 'flex-start')}
+ gap: 0.4rem;
+`;
+
+export const hostNameMarkWrapper = css`
+ ${flexGenerator('row', 'flex-start')}
+ gap: 0.8rem;
+`;
+
+export const hostNameWrapper = css`
+ ${flexGenerator()}
+ gap: 0.4rem
+`;
+
+export const hostNameStyle = (theme: Theme) => css`
+ color: ${theme.color.black};
+ ${theme.font['head01-b-22']};
+`;
+
+export const hostMarkIconStyle = css`
+ width: 1.9rem;
+ height: 1.9rem;
+`;
+
+export const hostMarkMessageWrapper = (theme: Theme) => css`
+ ${flexGenerator()}
+ width: 4.8rem;
+ height: 2.1rem;
+ background-color: ${theme.color.purple5};
+ border-radius: 5px;
+`;
+
+export const hostMarkMessageStyle = (theme: Theme) => css`
+ color: ${theme.color.purple1};
+ ${theme.font['body03-r-12']};
+`;
+
+export const hostKeywordStyle = (theme: Theme) => css`
+ color: ${theme.color.midgray2};
+ ${theme.font['subhead06-m-14']};
+`;
+
+export const hostDescriptionWrapper = (theme: Theme) => css`
+ background-color: ${theme.color.bg_white0};
+ border-radius: 10px;
+ padding: 1.4rem 2rem;
+ width: 100%;
+`;
+
+export const hostDescriptionStyle = (theme: Theme) => css`
+ ${theme.font['body02-r-14']};
+ color: ${theme.color.darkgray};
+`;
+
+export const hostTabContainer = css`
+ width: 100%;
+`;
+
+export const hostTabWrapper = css`
+ ${flexGenerator()}
+`;
+
+export const hostTabTextStyle = (theme: Theme) => css`
+ ${flexGenerator()}
+ width: 50%;
+ color: ${theme.color.midgray1};
+ ${theme.font['subhead01-sb-18']}
+ padding-bottom: 1.5rem;
+ border-bottom: 1px solid ${theme.color.midgray1};
+`;
+
+export const hostActiveTabTextStyle = (theme: Theme) => css`
+ ${flexGenerator()}
+ width: 50%;
+ color: ${theme.color.blackgray};
+ ${theme.font['head03-b-18']}
+ padding-bottom: 1.4rem;
+ border-bottom: 2px solid ${theme.color.blackgray};
+`;
+
+export const hostTabContentWrapper = (theme: Theme) => css`
+ background-color: ${theme.color.bg_white0};
+ padding: 3rem 2rem;
+`;
+
+export const hostClassCardWrapper = css`
+ ${flexGenerator('column')};
+ gap: 1.2rem;
+`;
diff --git a/src/pages/host/page/HostInfoPage/HostInfoPage.tsx b/src/pages/host/page/HostInfoPage/HostInfoPage.tsx
new file mode 100644
index 00000000..64f7a21e
--- /dev/null
+++ b/src/pages/host/page/HostInfoPage/HostInfoPage.tsx
@@ -0,0 +1,174 @@
+import { useAtom } from 'jotai';
+import { useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+
+import { useFetchHostInfo } from '@apis/domains/host/useFetchHostInfo';
+import { useFetchMoimListByHost } from '@apis/domains/moim/useFetchMoimListByHost';
+import { useFetchReviewByHost } from '@apis/domains/review/useFetchReviewByHost';
+
+import { Button, Image, LogoHeader } from '@components';
+import { images } from '@constants';
+import { ClassReviewEmptyView } from '@pages/class/components';
+import { hostNameStyle } from '@pages/class/components/HostInfoCard/HostInfoCard.style';
+import { ClassListCard } from '@pages/classList/components';
+import HostClassEmptyView from '@pages/host/components/HostClassEmptyView/HostClassEmptyView';
+import {
+ hostActiveTabTextStyle,
+ hostBackgroundImage,
+ hostClassCardWrapper,
+ hostDescriptionStyle,
+ hostDescriptionWrapper,
+ hostImageWrapper,
+ hostInfoContainer,
+ hostInfoEditIcon,
+ hostInfoLayout,
+ hostKeywordStyle,
+ hostMarkIconStyle,
+ hostMarkMessageStyle,
+ hostMarkMessageWrapper,
+ hostNameMarkWrapper,
+ hostNameWrapper,
+ hostProfileContainer,
+ hostProfileImage,
+ hostProfileStyle,
+ hostProfileWrapper,
+ hostTabContainer,
+ hostTabContentWrapper,
+ hostTabTextStyle,
+ hostTabWrapper,
+} from '@pages/host/page/HostInfoPage/HostInfoPage.style';
+import { userAtom } from '@stores';
+import { IcEdit, IcSpickerMark } from '@svg';
+
+const HostInfoPage = () => {
+ const [activeTab, setActiveTab] = useState<'클래스' | '리뷰'>('클래스');
+
+ const [user] = useAtom(userAtom);
+ const { hostId } = useParams();
+ const navigate = useNavigate();
+
+ const handleTabClick = (tab: '클래스' | '리뷰') => {
+ setActiveTab(tab);
+ };
+
+ const handleHostLinkButtonClick = () => {
+ window.open(socialLink);
+ };
+
+ const handleEditIconClick = () => {
+ navigate(`/host/info/edit/${hostId}`);
+ };
+
+ const { data: hostInfoData } = useFetchHostInfo(Number(hostId));
+ const { nickName, profileUrl, count, keyword, description, socialLink } = hostInfoData ?? {};
+
+ const { data: hostInfoClassData } = useFetchMoimListByHost(Number(hostId));
+ const { data: hostInfoReviewData } = useFetchReviewByHost(Number(hostId));
+
+ return (
+
+
+
+
+
+
+
+
+
+ {Number(user.hostId) === Number(hostId) && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {count !== undefined && count >= 2 && (
+
+ 베테랑
+
+ )}
+
+
+
{keyword}
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+ handleTabClick('클래스')}
+ css={activeTab === '클래스' ? hostActiveTabTextStyle : hostTabTextStyle}>
+ 클래스
+
+ handleTabClick('리뷰')}
+ css={activeTab === '리뷰' ? hostActiveTabTextStyle : hostTabTextStyle}>
+ 리뷰
+
+
+
+
+ {activeTab === '클래스' ? (
+
+ {hostInfoClassData?.length === 0 ? (
+
+ ) : (
+
+ {hostInfoClassData &&
+ hostInfoClassData.map((data) => (
+
+ ))}
+
+ )}
+
+ ) : (
+
+ {hostInfoReviewData?.length === 0 ? (
+
+ ) : (
+
+ {hostInfoReviewData &&
+ hostInfoReviewData.map((data) => (
+
+ {data.content} {data.moimTitle}
+
+ ))}
+
+ )}
+
+ //수정 필요: 리뷰카드 머지되면 반영하기
+ )}
+
+
+
+
+ );
+};
+
+export default HostInfoPage;
diff --git a/src/pages/myPage/components/HostInfoCardWithLink/HostInfoCardWithLink.tsx b/src/pages/myPage/components/HostInfoCardWithLink/HostInfoCardWithLink.tsx
index 04a3afcd..6c582578 100644
--- a/src/pages/myPage/components/HostInfoCardWithLink/HostInfoCardWithLink.tsx
+++ b/src/pages/myPage/components/HostInfoCardWithLink/HostInfoCardWithLink.tsx
@@ -33,13 +33,17 @@ interface hostInfoCardWithLinkListProps {
}
const HostInfoCardWithLink = ({ hostInfoCardWithLinkList }: hostInfoCardWithLinkListProps) => {
const navigate = useNavigate();
- const { hostNickName, hostImageUrl, keyword, moimCount, attendeeCount } =
+ const { hostNickName, hostImageUrl, keyword, moimCount, attendeeCount, hostId } =
hostInfoCardWithLinkList;
const handleButtonClick = () => {
navigate('/class/post/step1');
};
+ const handleHostInfoButtonClick = () => {
+ navigate(`/host/info/${hostId}`);
+ };
+
return (
@@ -71,8 +75,7 @@ const HostInfoCardWithLink = ({ hostInfoCardWithLinkList }: hostInfoCardWithLink
-