+
{[0.4, 0.8, 1.2].map((delay) => (
wellItems, [wellItems]);
+ const [items, setItems] = useState([]);
+ const [orderChanges, setOrderChanges] = useState(
+ []
+ );
+
+ useEffect(() => {
+ if (originalItems.length) {
+ setItems(originalItems);
+ }
+ }, [originalItems]);
+
+ const handleMoveItem = (result: any) => {
+ const movedItem = items.find((item) => item.id === result.draggableId);
+ if (!movedItem) return;
+
+ const newOrder = items.length - result.destination.index - 1;
+
+ const prevChanges = [...orderChanges];
+ prevChanges.push({
+ well_id: wellData.id,
+ id: result.draggableId,
+ order: newOrder,
+ });
+
+ console.log(prevChanges);
+ setOrderChanges(prevChanges);
+
+ setItems((prevItems) => {
+ const updated = [...prevItems];
+ const [moved] = updated.splice(result.source.index, 1);
+ updated.splice(result.destination.index, 0, moved);
+ return updated;
+ });
+ };
+
+ const handleOrderChange = async () => {
+ try {
+ await updateWellItemOrder({
+ well_id: wellData.id,
+ changes: orderChanges,
+ });
+ setOrderChanges([]);
+ router.replace(getPath.wellDetail(userId, wellData.id));
+ } catch (error) {
+ console.error('순서 저장 실패:', error);
+ }
+ };
+
+ return (
+ <>
+ {isMovable ? (
+ router.replace(pathname)}
+ onClickDone={handleOrderChange}
+ />
+ ) : (
+
+ )}
+
+
+
+ >
+ );
+}
+
+export default WellDetail;
diff --git a/src/features/Well/components/Well/WellDetailPage.tsx b/src/features/Well/components/Well/WellDetailPage.tsx
index 7bad205b..2c047fbf 100644
--- a/src/features/Well/components/Well/WellDetailPage.tsx
+++ b/src/features/Well/components/Well/WellDetailPage.tsx
@@ -4,10 +4,11 @@ import React from 'react';
import ScrollToTop from '@/components/Gesture/ScrollToTop';
import NavigationBar from '@/components/NavigationBar/NavigationBar';
import { useScrollToTop } from '@/hooks/gesture/useScrollToTop';
-import { GetWellRes, SearchWellItemRes } from '@frolog/frolog-api';
+import { useSearchParams } from 'next/navigation';
import MainLayout from '@/layouts/MainLayout';
-import WellHeader from './WellHeader';
-import WellItemList from './WellItem/WellItemList';
+import { useWell } from '../../hooks/useWell';
+import WellDetail from './WellDetail';
+import { GetWellRes, SearchWellItemRes } from '@frolog/frolog-api';
interface Props {
/** 우물 소유 유저 id */
@@ -33,23 +34,21 @@ function WellDetailPage({
const isRootUser = userId === sessionUserId;
const isDefaultWell = defaultWellId === wellDetail.id;
const { isRendering } = useScrollToTop();
+ const { well } = useWell(wellDetail.id);
+ const isMovable = useSearchParams().get('mode') === 'movable';
return (
<>
-
- {wellDetail && (
-
)}
diff --git a/src/features/Well/components/Well/WellFrog/FrogOnBook.tsx b/src/features/Well/components/Well/WellFrog/FrogOnBook.tsx
index 3ee933f8..0308a97b 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);
@@ -21,19 +23,27 @@ interface Props {
message?: string;
/** 개구리 z-index */
zIndex: number;
+ /** 우물 순서 변경 모드 여부 */
+ isMovable?: boolean;
}
/** 우물 내 도서 최상단에 있는 개구리 컴포넌트 */
-function FrogOnBook({ message, frogId = 'default', zIndex }: Props) {
+function FrogOnBook({
+ message,
+ frogId = 'default',
+ zIndex,
+ isMovable = false,
+}: Props) {
const newItemId = useNewItemStore((state) => state.newItemId);
const userId = useUserId();
+ const isGotFirstFrog = localStorage.getItem(STORAGE_KEY.gotFirstFrog);
return (
-
-
-
+
+
+ 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 (
void;
+ userId?: string;
+ wellId?: string;
+}
+
+function WellEditSheet({ isOpen, closeSheet, userId, wellId }: Props) {
+ return (
+
+ {isOpen && userId && wellId && (
+
+
+
+
+ 우물 수정
+
+
+
+
+
+ 도서 순서 수정
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+export default WellEditSheet;
diff --git a/src/features/Well/components/Well/WellHeader.tsx b/src/features/Well/components/Well/WellHeader/WellHeader.tsx
similarity index 52%
rename from src/features/Well/components/Well/WellHeader.tsx
rename to src/features/Well/components/Well/WellHeader/WellHeader.tsx
index cbd0f768..ffa32760 100644
--- a/src/features/Well/components/Well/WellHeader.tsx
+++ b/src/features/Well/components/Well/WellHeader/WellHeader.tsx
@@ -1,12 +1,10 @@
'use client';
-import React from 'react';
-import { EditIcon } from 'public/icons';
-import BackButton from '@/components/Button/BackButton';
-import { useRouter } from 'next/navigation';
+import React, { useState } from 'react';
+import { EditIcon, WellListIcon } from 'public/icons';
import Link from 'next/link';
import { PAGES } from '@/constants/page';
-import { getPath } from '@/utils/getPath';
+import WellEditSheet from './WellEditSheet';
interface Props {
/** 우물 소유 유저 id */
@@ -16,7 +14,9 @@ interface Props {
/** 현재 로그인한 유저인지 여부 */
isRootUser: boolean;
/** 뒤로가기 버튼 유무 */
- hasBackButton?: boolean;
+ hasHomeButton?: boolean;
+ /** 기본 우물인지 여부 */
+ isDefaultWell?: boolean;
}
/** 우물 헤더 컴포넌트 */
@@ -24,30 +24,37 @@ function WellHeader({
userId,
wellId,
isRootUser,
- hasBackButton = true,
+ hasHomeButton = true,
+ isDefaultWell,
}: Props) {
- const router = useRouter();
const isMyWell = isRootUser && userId && wellId;
+ const [isOpen, setIsOpen] = useState(false);
return (
- {hasBackButton && (
- router.push(PAGES.HOME) : () => router.back()
- }
- fill='#B3B6C5'
- extraClass='absolute top-[28px] left-[28px] z-20'
- />
- )}
- {isMyWell && (
+ {hasHomeButton && (
+
+
+ )}
+ {isMyWell && !isDefaultWell && (
+
)}
+ setIsOpen(false)}
+ userId={userId}
+ wellId={wellId}
+ />
);
}
diff --git a/src/features/Well/components/Well/WellHeader/WellOrderEditHeader.tsx b/src/features/Well/components/Well/WellHeader/WellOrderEditHeader.tsx
new file mode 100644
index 00000000..709a938c
--- /dev/null
+++ b/src/features/Well/components/Well/WellHeader/WellOrderEditHeader.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import React from 'react';
+
+interface Props {
+ onClickDone: () => void;
+ onClickCancel: () => void;
+}
+
+function WellOrderEditHeader({ onClickDone, onClickCancel }: Props) {
+ return (
+
+
+
+
+ );
+}
+
+export default WellOrderEditHeader;
diff --git a/src/features/Well/components/Well/WellItem/WellItem.tsx b/src/features/Well/components/Well/WellItem/WellItem.tsx
index e5a19213..e7cd1cbe 100644
--- a/src/features/Well/components/Well/WellItem/WellItem.tsx
+++ b/src/features/Well/components/Well/WellItem/WellItem.tsx
@@ -1,9 +1,12 @@
+'use client';
+
/* eslint-disable arrow-body-style */
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { useUserId } from '@/store/sessionStore';
import { AnimatePresence, motion } from 'framer-motion';
import useNewItemStore from '@/store/newItemStore';
+import { WellItemMoverIcon } from 'public/icons';
import { staggerItemVariants } from '@/styles/variants/variants';
import { useCustomRouter } from '@/hooks/useCustomRouter';
import { STORAGE_KEY } from '@/constants/storage';
@@ -15,33 +18,31 @@ import { getPath } from '@/utils/getPath';
import MemoLeaf from './MemoLeaf';
interface Props {
- /** 도서 데이터 객체 */
wellBook: GetWellItemRes;
- /** 우물 id */
wellId: string;
- /** 최상단 아이템인지 여부 */
isTopItem: boolean;
- /** 아이템의 z-index */
- zIndex: number;
- /** 로딩 시작 핸들러 */
+ index: number;
startLoading: () => void;
- /** 최하단 아이템인지 여부 */
isLastItem?: boolean;
- /** 무한스크롤을 위한 observer 타겟 세팅 핸들러 */
setTarget?: React.Dispatch<
React.SetStateAction
>;
+ isMovable?: boolean;
+ draggableHandle: any;
+ isDragging?: boolean;
}
-/** 우물 도서 아이템 컴포넌트 */
function WellItem({
wellId,
wellBook,
isTopItem,
- zIndex,
+ index,
isLastItem,
setTarget,
startLoading,
+ draggableHandle,
+ isMovable = false,
+ isDragging = false,
}: Props) {
const userId = useUserId();
const { navigate } = useCustomRouter('well');
@@ -51,6 +52,7 @@ function WellItem({
const height = page > 400 ? page * 0.15 : 55;
const isReading = status === 'reading';
const hasMemo = memo_cnt > 0;
+ const fallbackCategory = 'economic_business';
useEffect(() => {
let timeoutId: NodeJS.Timeout;
@@ -73,7 +75,12 @@ function WellItem({
}, []);
return (
-
+
{
@@ -88,8 +95,8 @@ function WellItem({
variants={
newItemId === id && isTopItem ? staggerItemVariants : undefined
}
- style={{ zIndex, height }}
- className={`flex h-fit w-full bg-category-bg-${category} relative z-auto box-border justify-center pt-[12px]`}
+ className={`flex h-fit w-full bg-category-bg-${category || fallbackCategory} relative z-auto box-border justify-center pt-[12px]`}
+ style={{ zIndex: index + 1, height }}
>
{isLastItem && (
)}
{isReading && (
)}
- {hasMemo && (
-
+ {isMovable && (
+
+ )}
+ {!isMovable && hasMemo && (
+
)}
- {title}
+ {title || '책 정보 불러오는 중...'}
{isFirstMemo && (
diff --git a/src/features/Well/components/Well/WellItem/WellItemList.tsx b/src/features/Well/components/Well/WellItem/WellItemList.tsx
index 9fb69b6c..f5e0e41f 100644
--- a/src/features/Well/components/Well/WellItem/WellItemList.tsx
+++ b/src/features/Well/components/Well/WellItem/WellItemList.tsx
@@ -2,49 +2,72 @@
import React, { useEffect, useState } from 'react';
import { staggerContainerVariants } from '@/styles/variants/variants';
-import { motion } from 'framer-motion';
-import { GetWellRes, SearchWellItemRes } from '@frolog/frolog-api';
+import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
+import { GetWellItemRes, GetWellRes } from '@frolog/frolog-api';
+import { AnimatePresence, motion } from 'framer-motion';
import { getRandomEmptyMessage } from '@/features/Well/utils/getRandomMessage';
import WellItemSkeleton from '@/components/Fallback/Skeleton/Well/WellItemSkeleton';
import WithConditionalRendering from '@/components/HOC/WithConditionalRendering';
-import { useObserver } from '@/hooks/gesture/useObserver';
import LoadingOverlay from '@/components/Spinner/LoadingOverlay';
-import { useWellItems } from '@/features/Well/hooks/useWellItems';
import { chat } from '@/features/Well/data/chat';
-import WellTitle from '../WellTitle';
-import WellActionButton from '../Pointing/WellActionButton';
import FrogOnBook from '../WellFrog/FrogOnBook';
import WellItem from './WellItem';
import EmptyWellItem from './EmptyWellItem';
+import GettingNewFrog from '../NewFrog/GettingNewFrog';
+import { useWellItemCount } from '@/features/Well/hooks/useWellItemCount';
+import { useUserFrogsCount } from '@/features/Store/hooks/useUserFrogsCount';
+import SurveyFormSheet from '../NewFrog/SurveyFormSheet';
+import { STORAGE_KEY } from '@/constants/storage';
+import { isSurveyCompleted } from '@/hooks/useSurvey';
interface Props {
/** 우물 정보 데이터 객체 */
wellData: GetWellRes;
+ /** 우물 아이템 (순서 변경 모드) */
+ items: GetWellItemRes[];
+ /** 우물 아이템 */
+ wellItems: GetWellItemRes[];
/** 로그인한 유저인지 여부 */
isRootUser: boolean;
/** 첫 우물인지 여부 */
isDefaultWell?: boolean;
- /** 우물 아이템 리스트 */
- initialWellItemList: SearchWellItemRes;
+ /** 우물 순서 변경 모드 여부 */
+ isMovable: boolean;
+ isFetchingNextPage: boolean;
+ isEmpty: boolean;
+ isFetched: boolean;
+ setTarget: React.Dispatch<
+ React.SetStateAction
+ >;
+ handleMoveItem: (result: any) => void;
+ userId: string;
}
/** 우물 아이템 리스트 컴포넌트 */
const WellItemList = React.memo(
- ({ wellData, isRootUser, isDefaultWell, initialWellItemList }: Props) => {
- const {
- wellItems,
- fetchNextPage,
- hasNextPage,
- isFetchingNextPage,
- isEmpty,
- isFetched,
- } = useWellItems(wellData.id, initialWellItemList);
- const { id, name, item_cnt } = wellData;
+ ({
+ wellData,
+ items,
+ wellItems,
+ isRootUser,
+ isDefaultWell,
+ isMovable,
+ isFetchingNextPage,
+ isEmpty,
+ userId,
+ setTarget,
+ handleMoveItem,
+ }: Props) => {
+ 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 +75,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 +87,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);
@@ -80,15 +113,23 @@ const WellItemList = React.memo(
}
}, [wellItems]);
+ const [enabled, setEnabled] = useState(false);
+
+ useEffect(() => {
+ const animation = requestAnimationFrame(() => setEnabled(true));
+
+ return () => {
+ cancelAnimationFrame(animation);
+ setEnabled(false);
+ };
+ }, []);
+
+ if (!enabled) {
+ return null;
+ }
+
return (
<>
-
}
>
{isFetchingNextPage && }
-
- {wellItems?.map((item, i) => (
- setIsLoading(true)}
- />
- ))}
-
+
+
+ {(rootProvided: any) => (
+
+
+ {items?.map((item, i) => (
+
+ {(provided: any, snapshot: any) => (
+
+ setIsLoading(true)}
+ isMovable={isMovable}
+ />
+
+ )}
+
+ ))}
+ {rootProvided.placeholder}
+
+
+ )}
+
+
- {isTimeToMakeSecond && (
-
- )}
+
+ {isOpenNewFrogSheet && !isGotFirstFrog && (
+ setIsOpenNewFrogSheet(false)} />
+ )}
+
+
+ {isOpenSurveySheet && (
+ setIsOpenSurveySheet(false)} />
+ )}
+
{isLoading && }
>
);
diff --git a/src/features/Well/components/Well/WellTitle.tsx b/src/features/Well/components/Well/WellTitle.tsx
index fd0d976c..e0f3ba22 100644
--- a/src/features/Well/components/Well/WellTitle.tsx
+++ b/src/features/Well/components/Well/WellTitle.tsx
@@ -17,6 +17,8 @@ interface Props {
isPointing?: boolean;
/** 현재 로그인한 유저인지 여부 */
isRootUser?: boolean;
+ /** 우물 순서 변경 모드인지 여부 */
+ isMovable?: boolean;
}
/** 우물 타이틀 컴포넌트 */
@@ -26,6 +28,7 @@ function WellTitle({
itemCount,
isPointing = false,
isRootUser = false,
+ isMovable = false,
}: Props) {
return (
@@ -34,23 +37,36 @@ function WellTitle({
-
- {title}
- {isRootUser && (
-
- )}
-
+ {isMovable ? (
+
+
{title}
+
+ 책을 원하는 순서대로 바꿔보세요!
+
+ 바꾼 뒤에 꼭{' '}
+ 완료를
+ 눌러주세요!
+
+
+ ) : (
+
+ {title}
+ {isRootUser && (
+
+ )}
+
+ )}
);
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/features/Well/hooks/useWellItems.ts b/src/features/Well/hooks/useWellItems.ts
index 38cfde5b..76da7d2f 100644
--- a/src/features/Well/hooks/useWellItems.ts
+++ b/src/features/Well/hooks/useWellItems.ts
@@ -17,7 +17,7 @@ export const useWellItems = (
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: [QUERY_KEY.wellItems, wellId],
- queryFn: ({ pageParam }) => getWellItems(pageParam, wellId),
+ queryFn: ({ pageParam }) => getWellItems(pageParam, wellId!),
initialPageParam: 0,
getNextPageParam: (lastPage) => {
const totalPages = Math.ceil(lastPage.count / lastPage.limit);
diff --git a/src/features/Well/index.ts b/src/features/Well/index.ts
index cbd7143f..8f2911e7 100644
--- a/src/features/Well/index.ts
+++ b/src/features/Well/index.ts
@@ -6,7 +6,7 @@ export { default as FrogOnBook } from './components/Well/WellFrog/FrogOnBook';
export { default as WellActionButton } from './components/Well/Pointing/WellActionButton';
export { default as SideWellHeader } from './components/WellList/SideWellHeader';
export { default as WellForm } from './components/WellForm/WellForm';
-export { default as WellHeader } from './components/Well/WellHeader';
+export { default as WellHeader } from './components/Well/WellHeader/WellHeader';
export { default as WellIcon } from './components/WellList/WellIcon/WellIcon';
export { default as WellTitle } from './components/Well/WellTitle';
export { default as WellList } from './components/WellList/WellList';
diff --git a/src/hooks/useCustomRouter.ts b/src/hooks/useCustomRouter.ts
index 7c94cefe..3599556e 100644
--- a/src/hooks/useCustomRouter.ts
+++ b/src/hooks/useCustomRouter.ts
@@ -1,6 +1,7 @@
import { NAV_ITEM } from '@/constants/nav';
import { NavItemLabel } from '@/types/nav';
import { useRouter, useSearchParams } from 'next/navigation';
+import useNavigateStore from '@/store/navigateStore';
/** nav 상태를 붙여주는 커스텀 라우터
* @param defaultNav - 기본으로 적용될 nav key
@@ -14,6 +15,7 @@ export const useCustomRouter = (
const searchParams = useSearchParams();
const currentNav =
searchParams.get('nav') ?? (defaultNav ? NAV_ITEM[defaultNav].key : '');
+ const setNavigateState = useNavigateStore((state) => state.setNavigateState);
const generatePath = (path: string) => {
const separator = path.includes('?') ? '&' : '?';
@@ -25,7 +27,10 @@ export const useCustomRouter = (
}
};
- const navigate = (path: string) => router.push(generatePath(path));
+ const navigate = (path: string, state?: any) => {
+ router.push(generatePath(path));
+ setNavigateState(state);
+ };
const replace = (path: string) => router.replace(generatePath(path));
return { navigate, replace, router };
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/layouts/BackDrop.tsx b/src/layouts/BackDrop.tsx
index e0f8b394..95ae3ec6 100644
--- a/src/layouts/BackDrop.tsx
+++ b/src/layouts/BackDrop.tsx
@@ -29,7 +29,7 @@ function BackDrop({ children, align }: Props) {
animate='animate'
exit='exit'
className={`${alignmentClass} safe-screen fixed inset-x-0 left-0 top-0 z-90 mx-auto w-[450px] mobile:inset-0 mobile:left-0 mobile:w-full`}
- style={{ zIndex: '90' }}
+ style={{ zIndex: 90 }}
>
{children}
diff --git a/src/middleware.ts b/src/middleware.ts
index 6f9d16a2..e3348d7e 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,4 +1,4 @@
-import { RefreshToken, RefreshTokenRes } from '@frolog/frolog-api';
+import { RefreshTokenRes } from '@frolog/frolog-api';
import { encode, getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
@@ -7,15 +7,23 @@ import { getExpFromToken } from './utils/auth/decodeToken';
const protectedRoutes: string[] = [
'/frolog-test',
'/profile',
- '/join/finish',
'/flash',
'/well',
'/comments',
'/new-memo',
'/new-review',
+ '/join/finish',
'/quit',
'/terms',
'/store',
+ '/feed',
+ '/search',
+ '/memo',
+ '/review',
+ '/explore',
+ '/book',
+ '/search-home',
+ '/mission',
]; // 로그인이 필요한 페이지 목록
const publicRoutes: string[] = [
'/onboarding',
@@ -98,10 +106,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 && (
-
+