diff --git a/src/app/mocks/browser.ts b/src/app/mocks/browser.ts index d0d6e01..414d0ed 100644 --- a/src/app/mocks/browser.ts +++ b/src/app/mocks/browser.ts @@ -7,11 +7,13 @@ import { followHandler } from './handler/follow'; import { searchHandler } from './handler/search'; import { userHandler } from './handler/user'; import { bookmarkHandlers } from './handler/bookmark'; +import { feedHidesHandlers } from './handler/hide'; // 브라우저에서 실행하기 위한 mocking worker 초기화 export const worker = setupWorker( ...commentHandlers, ...feedHandlers, + ...feedHidesHandlers, ...bookmarkHandlers, ...likeHandlers, ...followHandler, diff --git a/src/app/mocks/consts/feed.ts b/src/app/mocks/consts/feed.ts index 52ca7be..06430b1 100644 --- a/src/app/mocks/consts/feed.ts +++ b/src/app/mocks/consts/feed.ts @@ -4,6 +4,7 @@ import { comments } from './comment'; import { likes } from './like'; import { users } from './user'; import { reports } from './report'; +import { hiddens } from './hidden'; interface Feeds { [feedId: number]: Feed; @@ -273,6 +274,7 @@ export const feeds: Feeds = { for (let i = 10; i < 100; i++) { reports[i] = false; + hiddens[i] = false; comments[i] = []; likes[i] = { totalCount: i, isLiked: false }; feeds[i] = { diff --git a/src/app/mocks/consts/hidden.ts b/src/app/mocks/consts/hidden.ts new file mode 100644 index 0000000..6f32313 --- /dev/null +++ b/src/app/mocks/consts/hidden.ts @@ -0,0 +1,17 @@ +import { feeds } from './feed'; + +interface Hiddens { + [feedId: keyof typeof feeds]: boolean; +} + +export const hiddens: Hiddens = { + 1: false, + 2: false, + 3: false, + 4: false, + 5: false, + 6: false, + 7: false, + 8: false, + 9: false, +}; diff --git a/src/app/mocks/handler/feed.ts b/src/app/mocks/handler/feed.ts index ae0e24d..d583e85 100644 --- a/src/app/mocks/handler/feed.ts +++ b/src/app/mocks/handler/feed.ts @@ -5,6 +5,7 @@ import { reports } from '../consts/report'; import { users } from '../consts/user'; import { likes } from '../consts/like'; import { comments } from '../consts/comment'; +import { hiddens } from '../consts/hidden'; import { getCurrentDate } from '../dir/date'; import { createHttpSuccessResponse, @@ -39,7 +40,7 @@ export const feedHandlers = [ const feedsData = Object.values(feeds) .slice((formattedPage - 1) * pageCount, formattedPage * pageCount) - .filter((feed) => !reports[feed.id]); + .filter((feed) => !reports[feed.id] && !hiddens[feed.id]); const totalFeeds = Object.values(feeds).length; const endOfPageRange = formattedPage * pageCount; @@ -181,8 +182,9 @@ export const feedHandlers = [ return createHttpErrorResponse('4003'); } + reports[formattedFeedId] = true; if (isBlind) { - reports[formattedFeedId] = true; + hiddens[formattedFeedId] = true; } return createHttpSuccessResponse({}); diff --git a/src/app/mocks/handler/hide.ts b/src/app/mocks/handler/hide.ts new file mode 100644 index 0000000..fa1995d --- /dev/null +++ b/src/app/mocks/handler/hide.ts @@ -0,0 +1,46 @@ +import { http } from 'msw'; + +import { feeds } from '../consts/feed'; +import { hiddens } from '../consts/hidden'; +import { + createHttpErrorResponse, + createHttpSuccessResponse, +} from '../dir/response'; + +export const feedHidesHandlers = [ + // 1️⃣ 피드 숨기기 + http.put('/feeds/:feed_id/hides', ({ params }) => { + const { feed_id } = params; + + if (isNaN(Number(feed_id))) { + return createHttpErrorResponse('4220'); + } + + const formattedFeedId = Number(feed_id); + + if (!feeds[formattedFeedId]) { + return createHttpErrorResponse('4040'); + } + + hiddens[formattedFeedId] = true; + return createHttpSuccessResponse({ isHidden: true }); + }), + + // 2️⃣ 피드 숨기기 취소 + http.delete('/feeds/:feed_id/hides', ({ params }) => { + const { feed_id } = params; + + if (isNaN(Number(feed_id))) { + return createHttpErrorResponse('4220'); + } + + const formattedFeedId = Number(feed_id); + + if (!feeds[formattedFeedId]) { + return createHttpErrorResponse('4040'); + } + + hiddens[formattedFeedId] = false; + return createHttpSuccessResponse({ isHidden: false }); + }), +]; diff --git a/src/entitites/feed/hide-store.ts b/src/entitites/feed/hide-store.ts new file mode 100644 index 0000000..c6cc393 --- /dev/null +++ b/src/entitites/feed/hide-store.ts @@ -0,0 +1,46 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +const HIDDEN = true; +const VISIBLE = false; + +interface HiddenFeedState { + hiddenFeeds: Map; +} + +export const useHiddenFeedStore = create()( + devtools( + (): HiddenFeedState => ({ + hiddenFeeds: new Map(), + }), + { name: 'feed-hidden-store' }, + ), +); + +/** + * 숨김 피드 목록에 피드를 추가합니다. + * @param feedId 피드 아이디 + */ +export function addHiddenFeed(feedId: number) { + useHiddenFeedStore.setState( + ({ hiddenFeeds: prevHiddenFeeds }) => ({ + hiddenFeeds: new Map(prevHiddenFeeds).set(feedId, HIDDEN), + }), + false, + 'feed/addHiddenFeed', + ); +} + +/** + * 숨김 피드 목록에서 피드를 제거합니다. + * @param feedId 피드 아이디 + */ +export function cancleHiddenFeed(feedId: number) { + useHiddenFeedStore.setState( + ({ hiddenFeeds: prevHiddenFeeds }) => ({ + hiddenFeeds: new Map(prevHiddenFeeds).set(feedId, VISIBLE), + }), + false, + 'feed/cancleHiddenFeed', + ); +} diff --git a/src/entitites/feed/index.ts b/src/entitites/feed/index.ts new file mode 100644 index 0000000..03acf1e --- /dev/null +++ b/src/entitites/feed/index.ts @@ -0,0 +1 @@ +export * from './hide-store'; diff --git a/src/features/feed-hides/api/index.ts b/src/features/feed-hides/api/index.ts new file mode 100644 index 0000000..4711d0e --- /dev/null +++ b/src/features/feed-hides/api/index.ts @@ -0,0 +1,2 @@ +export { useHides } from './useHides'; +export { useHideCancel } from './useHideCancle'; diff --git a/src/features/feed-hides/api/useHideCancle.tsx b/src/features/feed-hides/api/useHideCancle.tsx new file mode 100644 index 0000000..7587a6a --- /dev/null +++ b/src/features/feed-hides/api/useHideCancle.tsx @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { cancleHiddenFeed } from '@/entitites/feed'; +import { axiosInstance } from '@/shared/axios'; + +async function requestHideCancelFeed(feedId: number) { + const { data } = await axiosInstance.delete(`/feeds/${feedId}/hides`); + + return data; +} + +export const useHideCancel = (feedId: number) => { + const { mutateAsync: hideCancelFeed, isPending } = useMutation({ + mutationFn: () => requestHideCancelFeed(feedId), + onSuccess: () => cancleHiddenFeed(feedId), + }); + + return { hideCancelFeed, isPending }; +}; diff --git a/src/features/feed-hides/api/useHides.tsx b/src/features/feed-hides/api/useHides.tsx new file mode 100644 index 0000000..8849a9e --- /dev/null +++ b/src/features/feed-hides/api/useHides.tsx @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import { addHiddenFeed } from '@/entitites/feed'; +import { axiosInstance } from '@/shared/axios'; + +async function requestHideFeed(feedId: number) { + const { data } = await axiosInstance.put(`/feeds/${feedId}/hides`); + + return data; +} + +export const useHides = (feedId: number) => { + const { mutateAsync: hideFeedAsync, isPending } = useMutation({ + mutationFn: () => requestHideFeed(feedId), + onSuccess: () => addHiddenFeed(feedId), + }); + + return { hideFeedAsync, isPending }; +}; diff --git a/src/features/feed-hides/index.ts b/src/features/feed-hides/index.ts new file mode 100644 index 0000000..5ecdd1f --- /dev/null +++ b/src/features/feed-hides/index.ts @@ -0,0 +1 @@ +export * from './ui'; diff --git a/src/widgets/feed-main-list/ui/HiddenFeed.scss b/src/features/feed-hides/ui/HiddenFeed.scss similarity index 100% rename from src/widgets/feed-main-list/ui/HiddenFeed.scss rename to src/features/feed-hides/ui/HiddenFeed.scss diff --git a/src/features/feed-hides/ui/HiddenFeed.tsx b/src/features/feed-hides/ui/HiddenFeed.tsx new file mode 100644 index 0000000..94cb120 --- /dev/null +++ b/src/features/feed-hides/ui/HiddenFeed.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@/shared/ui'; + +import './HiddenFeed.scss'; +import { useHideCancel } from '../api'; + +interface HiddenFeedProps { + feedId: number; + message: string; +} + +export const HiddenFeed: React.FC = ({ feedId, message }) => { + const { hideCancelFeed, isPending } = useHideCancel(feedId); + + return ( +
+
+ +

{message}

+ +
+
+ ); +}; diff --git a/src/features/feed-hides/ui/HideButton.tsx b/src/features/feed-hides/ui/HideButton.tsx new file mode 100644 index 0000000..df9c094 --- /dev/null +++ b/src/features/feed-hides/ui/HideButton.tsx @@ -0,0 +1,25 @@ +import { useHides } from '../api'; + +interface HideButtonProps { + feedId: number; + onClose: () => void; +} + +export const HideButton: React.FC = ({ feedId, onClose }) => { + const { hideFeedAsync, isPending } = useHides(feedId); + + const handleClickHideBtn = async () => { + await hideFeedAsync(); + onClose(); + }; + + return ( + + ); +}; diff --git a/src/features/feed-hides/ui/index.ts b/src/features/feed-hides/ui/index.ts new file mode 100644 index 0000000..f579823 --- /dev/null +++ b/src/features/feed-hides/ui/index.ts @@ -0,0 +1,2 @@ +export { HideButton } from './HideButton'; +export { HiddenFeed } from './HiddenFeed'; diff --git a/src/widgets/feed-kebab/ui/KebabMenu.tsx b/src/widgets/feed-kebab/ui/KebabMenu.tsx index 0f058e6..ed68172 100644 --- a/src/widgets/feed-kebab/ui/KebabMenu.tsx +++ b/src/widgets/feed-kebab/ui/KebabMenu.tsx @@ -1,5 +1,7 @@ +import { HideButton } from '@/features/feed-hides'; import { FeedReportsForm } from '@/features/feed-reports'; import { useToggle } from '@/shared/hooks'; + import './KebabMenu.scss'; interface KebabMenuProps { @@ -15,7 +17,7 @@ export const KebabMenu: React.FC = ({ feedId, onClose }) => { <>
  • - +
  • - - - ); -}; - -export default HiddenFeed;