+
{name}
{type === 'store' && (
)}
+ {type === 'well' && is_owned && (
+ 획득완료
+ )}
+ {hasAcquireButton && (
+
+ )}
);
diff --git a/src/constants/query.ts b/src/constants/query.ts
index 75796ecb..01093204 100644
--- a/src/constants/query.ts
+++ b/src/constants/query.ts
@@ -23,4 +23,6 @@ export const QUERY_KEY = {
searchWells: 'searchWells',
firstMemoDetail: 'firstMemoDetail',
profileFeed: 'profileFeed',
+ wellItemCount: 'wellItemCount',
+ userFrogs: 'userFrogs',
} as const;
diff --git a/src/constants/storage.ts b/src/constants/storage.ts
index 5ae64cc9..3185bf36 100644
--- a/src/constants/storage.ts
+++ b/src/constants/storage.ts
@@ -9,4 +9,6 @@ export const STORAGE_KEY = {
profileEditFormKey: '@FROLOG_PROFILE_EDIT_FORM',
profileIsEdited: '@FROLOG_PROFILE_IS_EDITED',
selectedWellItemId: '@FROLOG_SELECTED_WELL_ITEM_ID',
+ surveyCompleted: '@FROLOG_SURVEY_COMPLETED',
+ gotFirstFrog: '@FROLOG_GOT_FIRST_FROG',
} as const;
diff --git a/src/data/ui/bottomSheet.tsx b/src/data/ui/bottomSheet.tsx
index 5ea513c4..220d1cca 100644
--- a/src/data/ui/bottomSheet.tsx
+++ b/src/data/ui/bottomSheet.tsx
@@ -11,9 +11,7 @@ export interface AlertSheet {
export type BottomSheetKeys = keyof typeof sheetData;
-export const sheetData: {
- [key: string]: AlertSheet;
-} = {
+export const sheetData: { [key: string]: AlertSheet } = {
leave_while_write: {
getTitle: () => (
<>
@@ -122,10 +120,7 @@ export const sheetData: {
frog: SHEET_FROG.wink,
description: () => <>추후에 상점이 오픈될 때, 알려드릴게요.>,
},
- add_book: {
- getTitle: () => <>지금 이 책은...>,
- type: 'normal',
- },
+ add_book: { getTitle: () => <>지금 이 책은...>, type: 'normal' },
select_books: {
getTitle: () => <>기존 리뷰를 우물에 담을까요?>,
type: 'normal',
@@ -222,6 +217,11 @@ export const sheetData: {
extraButtonText: '취소',
description: () => <>포인트가 충분하면 캐릭터가 보여요>,
},
+ survey_form: {
+ getTitle: () => <>프롤로그를 얼마나 만족하셨나요?>,
+ type: 'normal',
+ buttonText: '제출하기',
+ },
delete_profile_feed: {
getTitle: () => (
<>
diff --git a/src/data/ui/textareaType.ts b/src/data/ui/textareaType.ts
index 66d0ffb0..4e029a50 100644
--- a/src/data/ui/textareaType.ts
+++ b/src/data/ui/textareaType.ts
@@ -5,25 +5,27 @@ export interface TextareaType {
| keyof ReviewFormType
| keyof MemoFormType
| keyof FirstMemoFormType
- | 'self_intro';
+ | 'self_intro'
+ | 'reason'
+ | 'wish';
title: string;
maxLength: number;
minLength: number;
minRow: number;
errorMessage: string;
placeholder: string;
+ hasCounter: boolean;
required: boolean | string;
}
-export const textareaType: {
- [key: string]: TextareaType;
-} = {
+export const textareaType: { [key: string]: TextareaType } = {
oneLiner: {
fieldName: 'oneLiner',
title: '한 줄평',
maxLength: 40,
minLength: 10,
minRow: 1,
+ hasCounter: true,
required: '한 줄평을 작성해주세요',
errorMessage: '멋진 문장이에요! 좀 더 남겨주세요. (최소 10자)',
placeholder: '책을 한 문장으로 표현해주세요! (10자 이상)',
@@ -34,6 +36,7 @@ export const textareaType: {
maxLength: 400,
minLength: 10,
minRow: 2,
+ hasCounter: true,
required: '리뷰를 작성해주세요',
errorMessage: '좋은 시작이에요! 생각이 더 궁금해요. (최소 10자)',
placeholder:
@@ -45,6 +48,7 @@ export const textareaType: {
maxLength: 400,
minLength: 0,
minRow: 1,
+ hasCounter: true,
required: false,
errorMessage: '',
placeholder: '인상깊은 구절을 메모하세요',
@@ -55,6 +59,7 @@ export const textareaType: {
maxLength: 400,
minLength: 0,
minRow: 2,
+ hasCounter: true,
required: false,
errorMessage: '',
placeholder:
@@ -66,8 +71,31 @@ export const textareaType: {
maxLength: 50,
minLength: 1,
minRow: 1,
+ hasCounter: true,
required: '자기소개를 입력해주세요',
errorMessage: '1-50자로 입력하세요.',
placeholder: '자기소개를 입력해주세요',
},
+ reason: {
+ fieldName: 'reason',
+ title: '프롤로그를 사용하는 이유는?',
+ maxLength: 400,
+ minLength: 0,
+ minRow: 1,
+ required: false,
+ errorMessage: '',
+ hasCounter: false,
+ placeholder: '사용하는 이유를 알려주세요',
+ },
+ wish: {
+ fieldName: 'wish',
+ title: '프롤로그에게 바라는 점은?',
+ maxLength: 400,
+ minLength: 0,
+ minRow: 1,
+ required: false,
+ errorMessage: '',
+ hasCounter: false,
+ placeholder: '자유롭게 피드백을 입력하세요',
+ },
};
diff --git a/src/features/Store/hooks/useUserFrogsCount.ts b/src/features/Store/hooks/useUserFrogsCount.ts
new file mode 100644
index 00000000..ead7a27e
--- /dev/null
+++ b/src/features/Store/hooks/useUserFrogsCount.ts
@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+import { getStoreItems } from '../api/store.api';
+import { QUERY_KEY } from '@/constants/query';
+
+export const useUserFrogsCount = () => {
+ const baseFrogs = ['roro', 'fro', 'rogi'];
+
+ const { data } = useQuery({
+ queryKey: [QUERY_KEY.userFrogs],
+ queryFn: () => getStoreItems({ type: 'frog', limit: 100 }),
+ refetchOnWindowFocus: false,
+ staleTime: Infinity,
+ });
+
+ const baseFrogsCount = data?.items.filter(
+ (item) => baseFrogs.includes(item.key) && item.is_owned
+ ).length;
+
+ return {
+ baseFrogsCount,
+ };
+};
diff --git a/src/features/Well/api/frog.api.ts b/src/features/Well/api/frog.api.ts
index 75bdbbef..0fa5887d 100644
--- a/src/features/Well/api/frog.api.ts
+++ b/src/features/Well/api/frog.api.ts
@@ -1,9 +1,16 @@
import { baseOptions } from '@/api/options';
-import { GetFrogs } from '@frolog/frolog-api';
+import { GrantInitialStoreItem } from '@frolog/frolog-api';
-const getFrogs = new GetFrogs(baseOptions);
+export const getFirstFrog = async (key: string, userId?: string) => {
+ if (!userId) {
+ return;
+ }
-export const getFrogList = async (id: string) => {
- const response = await getFrogs.fetch({ id });
- return response.frogs;
+ const data = await new GrantInitialStoreItem(baseOptions).fetch({
+ id: userId,
+ key,
+ count: 1,
+ });
+
+ return data;
};
diff --git a/src/features/Well/api/well.api.ts b/src/features/Well/api/well.api.ts
index 018a56ee..549c9411 100644
--- a/src/features/Well/api/well.api.ts
+++ b/src/features/Well/api/well.api.ts
@@ -23,6 +23,7 @@ import {
SearchUserWellReq,
SearchWell,
SearchWellItem,
+ GetUserWellItemCount,
} from '@frolog/frolog-api';
const postWell = new PostWell(baseOptions);
@@ -33,6 +34,7 @@ const editWellObj = new EditWell(baseOptions);
const postWellItem = new PostWellItem(baseOptions);
const getWellNameAvailability = new GetWellNameAvailability(baseOptions);
const searchUserWell = new SearchUserWell(baseOptions);
+const getUserWellItems = new GetUserWellItemCount(baseOptions);
export const addNewWell = async (req: PostWellReq) => {
const response = await postWell.fetch(req);
@@ -127,3 +129,8 @@ export const deleteThisBook = async (req: DeleteWellItemsByConditionReq) => {
const response = await new DeleteWellItemsByCondition(baseOptions).fetch(req);
return response;
};
+
+export const getUserWellItemsCount = async (userId: string) => {
+ const response = await getUserWellItems.fetch({ id: userId });
+ return response;
+};
diff --git a/src/features/Well/components/Well/NewFrog/FrogList.tsx b/src/features/Well/components/Well/NewFrog/FrogList.tsx
new file mode 100644
index 00000000..4ee22a2e
--- /dev/null
+++ b/src/features/Well/components/Well/NewFrog/FrogList.tsx
@@ -0,0 +1,61 @@
+import FrologItem from '@/components/FrologItem/FrologItem';
+import React from 'react';
+
+interface Props {
+ ownedFrog?: string;
+ onAcquire?: (key: string) => void;
+}
+
+function FrogList({ ownedFrog, onAcquire }: Props) {
+ return (
+
+ onAcquire?.('fro')}
+ />
+ onAcquire?.('roro')}
+ />
+ onAcquire?.('rogy')}
+ />
+
+ );
+}
+
+export default FrogList;
diff --git a/src/features/Well/components/Well/NewFrog/FrogSelectSheet.tsx b/src/features/Well/components/Well/NewFrog/FrogSelectSheet.tsx
new file mode 100644
index 00000000..5f793857
--- /dev/null
+++ b/src/features/Well/components/Well/NewFrog/FrogSelectSheet.tsx
@@ -0,0 +1,42 @@
+import Image from 'next/image';
+import React from 'react';
+import { SHEET_FROG } from '@/constants/frogs';
+import { motion } from 'framer-motion';
+import FrogList from './FrogList';
+
+interface Props {
+ onAcquire: (key: string) => void;
+}
+
+function FrogSelectSheet({ onAcquire }: Props) {
+ return (
+
+
+
+
+ 책 1권 추가 완료!
+
+ 보상으로 개구리를 골라주세요
+
+
+
+
+
+ );
+}
+
+export default FrogSelectSheet;
diff --git a/src/features/Well/components/Well/NewFrog/GettingNewFrog.tsx b/src/features/Well/components/Well/NewFrog/GettingNewFrog.tsx
new file mode 100644
index 00000000..b30cddc3
--- /dev/null
+++ b/src/features/Well/components/Well/NewFrog/GettingNewFrog.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import BackDrop from '@/layouts/BackDrop';
+import React, { useState } from 'react';
+import FrogSelectSheet from './FrogSelectSheet';
+import NewFrogCongrats from './NewFrogCongrats';
+import GuideSheet from './GuideSheet';
+import { useMutation } from '@tanstack/react-query';
+import { getFirstFrog } from '@/features/Well/api/frog.api';
+import { useSession } from 'next-auth/react';
+import { STORAGE_KEY } from '@/constants/storage';
+
+interface Props {
+ onClose: () => void;
+}
+
+/** 최초 우물에서 개구리를 획득하는 프로세스를 진행하는 컴포넌트 */
+function GettingNewFrog({ onClose }: Props) {
+ const { data: session } = useSession();
+ const [ownedFrog, setOwnedFrog] = useState
('');
+ const [isAcquired, setIsAcquired] = useState(false);
+ const [isOpenGuideSheet, setIsOpenGuideSheet] = useState(false);
+
+ const { mutate: handleAcquireFrog } = useMutation({
+ mutationFn: (key: string) => getFirstFrog(key, session?.user.id),
+ onSuccess: (_, key) => {
+ setIsAcquired(true);
+ setOwnedFrog(key);
+ },
+ });
+
+ const handleClose = () => {
+ localStorage.setItem(STORAGE_KEY.gotFirstFrog, 'true');
+ onClose();
+ };
+
+ return (
+
+ {!isAcquired && (
+ handleAcquireFrog(key)} />
+ )}
+ {isAcquired && !isOpenGuideSheet && (
+ setIsOpenGuideSheet(true)}
+ acquiredFrog={{
+ key: ownedFrog,
+ type: 'frog',
+ name: '개꾸리',
+ price: 100,
+ disabled: false,
+ is_available: true,
+ is_owned: true,
+ }}
+ />
+ )}
+ {isOpenGuideSheet && (
+
+ )}
+
+ );
+}
+
+export default GettingNewFrog;
diff --git a/src/features/Well/components/Well/NewFrog/GuideSheet.tsx b/src/features/Well/components/Well/NewFrog/GuideSheet.tsx
new file mode 100644
index 00000000..f49a2a98
--- /dev/null
+++ b/src/features/Well/components/Well/NewFrog/GuideSheet.tsx
@@ -0,0 +1,66 @@
+import Image from 'next/image';
+import React, { useRef } from 'react';
+import { SHEET_FROG } from '@/constants/frogs';
+import { motion } from 'framer-motion';
+import Button from '@/components/Button/Button';
+import FrogList from './FrogList';
+import { useClickOutside } from '@/hooks/popup/useClickOutside';
+import { useRouter } from 'next/navigation';
+import { PAGES } from '@/constants/page';
+import { STORAGE_KEY } from '@/constants/storage';
+
+interface Props {
+ ownedFrog: string;
+ onClose: () => void;
+}
+
+function GuideSheet({ ownedFrog, onClose }: Props) {
+ const router = useRouter();
+ const ref = useRef(null);
+
+ useClickOutside(ref, onClose);
+
+ return (
+
+
+
+
+ 책을 1권 더 추가하고,
+
세 개구리 모두 얻으세요!
+
+
+
+
+
+
+
+
+ );
+}
+
+export default GuideSheet;
diff --git a/src/features/Well/components/Well/NewFrog/NewFrogCongrats.tsx b/src/features/Well/components/Well/NewFrog/NewFrogCongrats.tsx
new file mode 100644
index 00000000..62e39f0b
--- /dev/null
+++ b/src/features/Well/components/Well/NewFrog/NewFrogCongrats.tsx
@@ -0,0 +1,77 @@
+import React, { useEffect } from 'react';
+import { motion } from 'framer-motion';
+import Image from 'next/image';
+import FrologItem from '@/components/FrologItem/FrologItem';
+import { GetStoreItemRes } from '@frolog/frolog-api';
+
+interface Props {
+ acquiredFrog: GetStoreItemRes;
+ onNext: () => void;
+}
+
+function NewFrogCongrats({ acquiredFrog, onNext }: Props) {
+ useEffect(() => {
+ setTimeout(() => {
+ onNext();
+ }, 3000);
+ }, []);
+
+ const topConfettiVariants = {
+ initial: { y: 0, opacity: 0, scale: 0.3 },
+ animate: {
+ y: -140,
+ opacity: 1,
+ scale: 1,
+ transition: { type: 'spring', stiffness: 100, damping: 12, delay: 0.1 },
+ },
+ };
+
+ const bottomConfettiVariants = {
+ initial: { y: 0, opacity: 0, scale: 0.3 },
+ animate: {
+ y: 140,
+ opacity: 1,
+ scale: 1,
+ transition: { type: 'spring', stiffness: 100, damping: 12, delay: 0.1 },
+ },
+ };
+
+ return (
+
+ );
+}
+
+export default NewFrogCongrats;
diff --git a/src/features/Well/components/Well/NewFrog/SurveyFormSheet.tsx b/src/features/Well/components/Well/NewFrog/SurveyFormSheet.tsx
new file mode 100644
index 00000000..36344cdd
--- /dev/null
+++ b/src/features/Well/components/Well/NewFrog/SurveyFormSheet.tsx
@@ -0,0 +1,82 @@
+import Textarea from '@/components/Form/Input/Textarea';
+import { textareaType } from '@/data/ui/textareaType';
+import { useSurvey } from '@/hooks/useSurvey';
+import BottomSheet from '@/modules/BottomSheet/BottomSheet';
+import React from 'react';
+import { FormProvider } from 'react-hook-form';
+
+interface Props {
+ onClose: () => void;
+}
+
+/** 누적 권수 3권인 경우 렌더링되는 설문조사 폼 */
+function SurveyFormSheet({ onClose }: Props) {
+ const { methods, handleSurvey, handleCloseSurvey } = useSurvey();
+
+ const buttonStyle =
+ 'rounded-[12px] border px-[16px] py-[18px] text-body-lg border-gray-200 bg-gray-200 text-gray-800';
+ const selected = 'border-main text-body-lg-bold';
+
+ const { watch, setValue } = methods;
+
+ const handleClick = (value: 'yes' | 'no' | 'soso') => {
+ setValue('recommend', value);
+ };
+
+ const handleSubmit = async () => {
+ await handleSurvey();
+ onClose();
+ };
+
+ const handleClose = () => {
+ handleCloseSurvey();
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default SurveyFormSheet;
diff --git a/src/features/Well/components/Well/Pointing/Pointing.tsx b/src/features/Well/components/Well/Pointing/Pointing.tsx
index d631ca64..c54fcf75 100644
--- a/src/features/Well/components/Well/Pointing/Pointing.tsx
+++ b/src/features/Well/components/Well/Pointing/Pointing.tsx
@@ -7,7 +7,7 @@ import { pointing } from '@/styles/variants/variants';
/** 우물 내 클릭 유도 애니메이션 */
function Pointing() {
return (
-
+
{[0.4, 0.8, 1.2].map((delay) => (
{wellDetail && (
)}
{isRendering && }
diff --git a/src/features/Well/components/Well/WellFrog/FrogOnBook.tsx b/src/features/Well/components/Well/WellFrog/FrogOnBook.tsx
index 3ee933f8..9e66ec41 100644
--- a/src/features/Well/components/Well/WellFrog/FrogOnBook.tsx
+++ b/src/features/Well/components/Well/WellFrog/FrogOnBook.tsx
@@ -11,6 +11,8 @@ import useNewItemStore from '@/store/newItemStore';
import { leafVariants, frogVariants } from '@/styles/variants/variants';
import { PAGES } from '@/constants/page';
import GuideChat from './GuideChat';
+import Pointing from '../Pointing/Pointing';
+import { STORAGE_KEY } from '@/constants/storage';
const MotionImage = motion.create(Image);
@@ -27,6 +29,7 @@ interface Props {
function FrogOnBook({ message, frogId = 'default', zIndex }: Props) {
const newItemId = useNewItemStore((state) => state.newItemId);
const userId = useUserId();
+ const isGotFirstFrog = localStorage.getItem(STORAGE_KEY.gotFirstFrog);
return (
@@ -48,13 +51,27 @@ function FrogOnBook({ message, frogId = 'default', zIndex }: Props) {
message={message}
marginBottom={FROGS[frogId].marginBottom}
/>
-
-
-
+
+
+ localStorage.removeItem(STORAGE_KEY.gotFirstFrog)}
+ >
+
+
+
+ {isGotFirstFrog && (
+
+ )}
+
);
diff --git a/src/features/Well/components/Well/WellFrog/GuideChat.tsx b/src/features/Well/components/Well/WellFrog/GuideChat.tsx
index a0331c44..bdcc7a65 100644
--- a/src/features/Well/components/Well/WellFrog/GuideChat.tsx
+++ b/src/features/Well/components/Well/WellFrog/GuideChat.tsx
@@ -42,7 +42,7 @@ function GuideChat({ message, marginBottom }: Props) {
return () => clearTimeout(timer);
}
- }, []);
+ }, [message]);
return (
- {hasBackButton && (
- router.push(PAGES.HOME) : () => router.back()
- }
- fill='#B3B6C5'
- extraClass='absolute top-[28px] left-[28px] z-20'
- />
+ {hasHomeButton && (
+
+
+
)}
- {isMyWell && (
+ {isMyWell && !isDefaultWell && (
{
+ ({
+ wellData,
+ isRootUser,
+ isDefaultWell,
+ initialWellItemList,
+ userId,
+ }: Props) => {
const {
wellItems,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isEmpty,
- isFetched,
} = useWellItems(wellData.id, initialWellItemList);
const { id, name, item_cnt } = wellData;
+
+ const { wellItemCount, isLoading: isWellItemCountLoading } =
+ useWellItemCount(userId);
+ const { baseFrogsCount } = useUserFrogsCount();
+
+ const isGotFirstFrog = localStorage.getItem(STORAGE_KEY.gotFirstFrog);
+
+ const [isOpenSurveySheet, setIsOpenSurveySheet] = useState(false);
+ const [isOpenNewFrogSheet, setIsOpenNewFrogSheet] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState(undefined);
const { setTarget } = useObserver({ hasNextPage, fetchNextPage });
- const isTimeToMakeSecond =
- isDefaultWell && isFetched && wellItems.length >= 2;
/** 우물 내 개구리 말풍선 메세지를 구하는 함수 */
const getMessage = (count: number) => {
@@ -52,10 +70,8 @@ const WellItemList = React.memo(
return undefined;
} else if (isDefaultWell) {
if (count === 0) {
- return getRandomEmptyMessage();
- } else if (count === 1) {
- return chat.first_book;
- } else if (count === 2) {
+ return chat.default_well_empty;
+ } else if (wellItemCount === 1 && isGotFirstFrog) {
return chat.second_book;
}
} else {
@@ -66,6 +82,18 @@ const WellItemList = React.memo(
return undefined;
};
+ useEffect(() => {
+ // 누적 권수가 1권이고, 개구리를 지급받지 않은 경우
+ if (wellItemCount === 1 && baseFrogsCount === 0) {
+ setIsOpenNewFrogSheet(true);
+ }
+
+ // 누적 권수가 3권 이상이고, 설문조사를 아직 완료하지 않은 경우
+ if (wellItemCount && wellItemCount >= 3 && !isSurveyCompleted()) {
+ setIsOpenSurveySheet(true);
+ }
+ }, [wellItems, isWellItemCountLoading, wellItemCount, baseFrogsCount]);
+
useEffect(
() => () => {
setIsLoading(false);
@@ -87,7 +115,7 @@ const WellItemList = React.memo(
wellId={id}
itemCount={item_cnt}
isRootUser={isRootUser}
- isPointing={isDefaultWell && wellItems.length < 2}
+ isPointing={isDefaultWell && wellItemCount === 0}
/>
- {isTimeToMakeSecond && (
-
- )}
+
+ {isOpenNewFrogSheet && !isGotFirstFrog && (
+ setIsOpenNewFrogSheet(false)} />
+ )}
+
+
+ {isOpenSurveySheet && (
+ setIsOpenSurveySheet(false)} />
+ )}
+
{isLoading && }
>
);
diff --git a/src/features/Well/components/WellList/WellAddButton.tsx b/src/features/Well/components/WellList/WellAddButton.tsx
index e0394dba..a36ccf92 100644
--- a/src/features/Well/components/WellList/WellAddButton.tsx
+++ b/src/features/Well/components/WellList/WellAddButton.tsx
@@ -1,31 +1,28 @@
'use client';
-import CustomMotionLink from '@/components/Link/CustomMotionLink';
-import { NAV_ITEM } from '@/constants/nav';
-
-import { getPath } from '@/utils/getPath';
-import { WellAddIcon } from 'public/icons';
+import { motion } from 'framer-motion';
import React from 'react';
+import { WellAddIcon } from 'public/icons';
+import Link from 'next/link';
+import { getPath } from '@/utils/getPath';
+
+const MotionLink = motion.create(Link);
interface Props {
- /** 유저 id */
userId: string;
}
/** 우물 추가 버튼 컴포넌트 */
function WellAddButton({ userId }: Props) {
return (
-
-
-
-
- 새 우물 파기
-
+
+
+
);
}
diff --git a/src/features/Well/components/WellList/WellList.tsx b/src/features/Well/components/WellList/WellList.tsx
index 95fa402c..7dcaeb6f 100644
--- a/src/features/Well/components/WellList/WellList.tsx
+++ b/src/features/Well/components/WellList/WellList.tsx
@@ -5,7 +5,6 @@ import WellItemsSkeleton from '@/components/Fallback/Skeleton/Well/WellItemsSkel
import { useObserver } from '@/hooks/gesture/useObserver';
import Observer from '@/components/Gesture/Observer';
import { SearchWellRes } from '@frolog/frolog-api';
-import WellListMessage from '@/features/Well/components/WellList/WellListMessage';
import WellAddButton from './WellAddButton';
import WellIcon from './WellIcon/WellIcon';
import { useWells } from '../../hooks/useWells';
@@ -31,7 +30,6 @@ function WellList({ userId, isRootUser, initialWells }: Props) {
className={`relative flex w-full flex-col pb-[48px] text-gray-800 ${isRootUser ? 'bg-gray-300' : 'bg-white'}`}
>
- {isRootUser && }
{wells?.map((well) => )}
}
/>
- {isRootUser && (
-
- )}
+ {isRootUser && }
);
}
diff --git a/src/features/Well/components/WellList/WellListMessage.tsx b/src/features/Well/components/WellList/WellListMessage.tsx
deleted file mode 100644
index e692b853..00000000
--- a/src/features/Well/components/WellList/WellListMessage.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ToastPlus } from 'public/icons';
-import React from 'react';
-
-interface Props {
- /** 메세지 */
- message: string;
-}
-
-/** 우물 리스트 하단에 위치한 토스트 형태의 메시지 컴포넌트 */
-function WellListMessage({ message }: Props) {
- return (
-
-
- {message}
-
- );
-}
-
-export default WellListMessage;
diff --git a/src/features/Well/data/chat.tsx b/src/features/Well/data/chat.tsx
index 8c6da6cd..4b8fb1bd 100644
--- a/src/features/Well/data/chat.tsx
+++ b/src/features/Well/data/chat.tsx
@@ -1,8 +1,9 @@
export const chat = {
not_loggedIn: '로그인을 해야\n책을 기록할 수 있어요',
+ default_well_empty: '책을 한 권 추가해보세요!\n놀라운 일이 생길지도? 🐸',
first_book:
'🚨특급 정보 입수!🚨\n책을 하나만 더 추가하면\n새로운 개구리가 나온대요!',
- second_book: '새로운 개구리가 왔나봐요!\n또 다른 우물을 파볼까요?',
+ second_book: '새로운 개구리가 왔나봐요!\n저를 클릭해보세요!',
};
export const emptyMessage = [
diff --git a/src/features/Well/hooks/useWellForm.ts b/src/features/Well/hooks/useWellForm.ts
index 05f8d484..00cd2f4d 100644
--- a/src/features/Well/hooks/useWellForm.ts
+++ b/src/features/Well/hooks/useWellForm.ts
@@ -59,9 +59,10 @@ export const useWellForm = ({ type, setError, wellId, wellData }: Props) => {
setIsLoading(false);
},
onSuccess: async () => {
+ await update({ defaultWellId: null });
+ router.refresh();
+
if (isSecond) {
- await update({ defaultWellId: null });
- router.refresh();
openFlash({ type: 'first_new_well', callbackUrl: PAGES.HOME });
} else {
openFlash({ type: 'new_well', callbackUrl: PAGES.HOME });
diff --git a/src/features/Well/hooks/useWellItemCount.ts b/src/features/Well/hooks/useWellItemCount.ts
new file mode 100644
index 00000000..7c31c52e
--- /dev/null
+++ b/src/features/Well/hooks/useWellItemCount.ts
@@ -0,0 +1,16 @@
+import { useQuery } from '@tanstack/react-query';
+import { getUserWellItemsCount } from '../api/well.api';
+import { QUERY_KEY } from '@/constants/query';
+
+export const useWellItemCount = (userId: string) => {
+ const { data, isLoading } = useQuery({
+ queryKey: [QUERY_KEY.wellItemCount, userId],
+ queryFn: () => getUserWellItemsCount(userId),
+ enabled: !!userId,
+ });
+
+ return {
+ wellItemCount: data?.total,
+ isLoading,
+ };
+};
diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts
new file mode 100644
index 00000000..52ad8031
--- /dev/null
+++ b/src/hooks/useSurvey.ts
@@ -0,0 +1,54 @@
+import { postSurvey } from '@/api/survey.api';
+import { STORAGE_KEY } from '@/constants/storage';
+import { toast } from '@/modules/Toast';
+import { useForm } from 'react-hook-form';
+
+interface SurveyForm {
+ recommend: 'yes' | 'no' | 'soso';
+ reason?: string;
+ wish?: string;
+}
+
+export const isSurveyCompleted = () => {
+ if (typeof window === 'undefined') return false;
+ return localStorage.getItem(STORAGE_KEY.surveyCompleted) === 'true';
+};
+
+const markSurveyAsCompleted = () => {
+ if (typeof window === 'undefined') return;
+ localStorage.setItem(STORAGE_KEY.surveyCompleted, 'true');
+};
+
+export const useSurvey = () => {
+ const methods = useForm
({
+ defaultValues: { recommend: 'yes', reason: '', wish: '' },
+ });
+
+ const handleSurvey = async () => {
+ const req = methods.getValues();
+
+ try {
+ const response = await postSurvey(req);
+
+ if (response.result) {
+ toast.normal('설문조사가 정상적으로 제출되었습니다');
+ markSurveyAsCompleted();
+ } else {
+ toast.error('다시 시도해주세요');
+ }
+ } catch (error) {
+ toast.error('다시 시도해주세요');
+ }
+ };
+
+ const handleCloseSurvey = () => {
+ markSurveyAsCompleted();
+ };
+
+ return {
+ methods,
+ handleSurvey,
+ handleCloseSurvey,
+ isSurveyCompleted,
+ };
+};
diff --git a/src/middleware.ts b/src/middleware.ts
index 6f9d16a2..96c24bf6 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -98,10 +98,12 @@ export async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const defaultWellId = sessionToken ? sessionToken.defaultWellId : undefined;
+ const referer = req.headers.get('referer');
+
if (pathname === '/') {
if (!sessionToken) {
return NextResponse.redirect(new URL('/default', req.url));
- } else if (defaultWellId) {
+ } else if (defaultWellId && !referer?.includes('/well')) {
// 재발급
const redirectResponse = NextResponse.redirect(
new URL(`/${sessionToken.id}/well/${defaultWellId}`, req.url)
diff --git a/src/modules/BottomSheet/BottomSheet.tsx b/src/modules/BottomSheet/BottomSheet.tsx
index 85a9f528..3c69930a 100644
--- a/src/modules/BottomSheet/BottomSheet.tsx
+++ b/src/modules/BottomSheet/BottomSheet.tsx
@@ -19,6 +19,7 @@ function BottomSheet({
onClickSubButton,
onClose,
titleProp,
+ padding = 'px-[24px]',
}: BottomSheetProps) {
useScrollFreeze();
const { getTitle, type, frog, buttonText, extraButtonText, description } =
@@ -41,7 +42,7 @@ function BottomSheet({
animate={{ y: '0%' }}
exit={{ y: '120%' }}
transition={{ duration: 0.3 }}
- className='safe-bottom relative flex h-fit w-full flex-col items-center gap-[40px] rounded-t-[20px] bg-white px-[24px] pb-[20px] pt-[40px] text-gray-800'
+ className={`safe-bottom relative flex h-fit w-full flex-col items-center gap-[40px] rounded-t-[20px] bg-white pb-[20px] pt-[40px] text-gray-800 ${padding}`}
style={{ paddingTop: '40px', gap: '40px' }}
>
{buttonText && (
-
+