diff --git a/src/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx b/src/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx index b5987014..0c7a4dac 100644 --- a/src/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx +++ b/src/pages/AllowedServicePage/AllowedServiceGroupDetail/Content/AllowedServiceGroupDetailContent.tsx @@ -1,11 +1,16 @@ -import { ReactNode } from 'react'; +import { ReactNode, useRef } from 'react'; +import Dropdown from '@/shared/components/Dropdown/Dropdown'; import FaviconImage from '@/shared/components/FaviconImage/FaviconImage'; +import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper'; import Spacer from '@/shared/components/Spacer/Spacer'; import { AllowedServiceGroupDetailSiteType } from '@/shared/types/allowedService'; -import MinusBtn from '@/shared/assets/svgs/minus_btn.svg?react'; +import MeatBallDefaultIcon from '@/shared/assets/svgs/common/ic_meatball_default.svg?react'; + +import ModalContentsAlert from '@/pages/AllowedServicePage/ModalContentsAlert/ModalContentsAlert'; +import { usePostMergeToParentDomain } from '@/shared/apisV2/allowedService/allowedService.mutations'; export interface AllowedServiceGroupDetailContentProps { children: ReactNode; @@ -53,12 +58,35 @@ export const AllowedServiceGroupDetailContentTable = ({ export interface AllowedServiceGroupDetailContentRootTableRowProps extends AllowedServiceGroupDetailSiteType { onDeleteAllowedSite: () => void; + activeGroupId: number; } export const AllowedServiceGroupDetailContentTableRow = ({ onDeleteAllowedSite, + activeGroupId, ...allowedSiteData }: AllowedServiceGroupDetailContentRootTableRowProps) => { + const domainAllowModalRef = useRef(null); + const confirmDeleteModalRef = useRef(null); + + const handleOpenDomainAllowModal = () => { + domainAllowModalRef.current?.open(); + }; + + const handleCloseDomainAllowModal = () => { + domainAllowModalRef.current?.close(); + }; + + const handleOpenConfirmDeleteModal = () => { + confirmDeleteModalRef.current?.open(); + }; + + const handleCloseConfirmDeleteModal = () => { + confirmDeleteModalRef.current?.close(); + }; + + const allowToMergeParentDomain = usePostMergeToParentDomain(); + return (
@@ -77,11 +105,49 @@ export const AllowedServiceGroupDetailContentTableRow = ({
- + + + + + + + { + handleOpenConfirmDeleteModal(); + }} + /> + +
+ + {() => ( + { + allowToMergeParentDomain.mutate({ + allowedGroupId: activeGroupId, + siteUrl: allowedSiteData.siteUrl, + }); + handleCloseDomainAllowModal(); + }} + onCancel={handleCloseDomainAllowModal} + /> + )} + + + {() => ( + { + handleCloseConfirmDeleteModal(); + onDeleteAllowedSite(); + }} + pageName={allowedSiteData.pageName} + /> + )} +
); }; diff --git a/src/pages/AllowedServicePage/AllowedServiceList/AllowedServiceList.tsx b/src/pages/AllowedServicePage/AllowedServiceList/AllowedServiceList.tsx index 25017fee..11a09dda 100644 --- a/src/pages/AllowedServicePage/AllowedServiceList/AllowedServiceList.tsx +++ b/src/pages/AllowedServicePage/AllowedServiceList/AllowedServiceList.tsx @@ -63,29 +63,26 @@ const AllowedServiceListContent = ({ children }: { children: ReactNode }) => { }; interface AllowedServiceListItemProps extends AllowedServiceGroupType { - index: number; activeGroupId: number | null; activeGroupTitleInput: string; onSelectActiveGroup: (activeGroupId: number | null) => void; - onDeleteAllowedServiceGroup: (groupId: number, isActive: boolean, currentIndex: number) => void; + onDeleteAllowedServiceGroup: (groupId: number) => void; isEditingTitle: boolean; } const AllowedServiceListItem = ({ - index, activeGroupId, activeGroupTitleInput, onSelectActiveGroup, onDeleteAllowedServiceGroup, isEditingTitle, - ...allowedServiceGroupData }: AllowedServiceListItemProps) => { const isActive = activeGroupId === allowedServiceGroupData.id; const handleDeleteAllowedServiceGroup = (e: MouseEvent) => { e.stopPropagation(); - onDeleteAllowedServiceGroup(allowedServiceGroupData.id, isActive, index); + onDeleteAllowedServiceGroup(allowedServiceGroupData.id); }; const handleSelectActiveGroupId = () => { diff --git a/src/pages/AllowedServicePage/AllowedServicePage.tsx b/src/pages/AllowedServicePage/AllowedServicePage.tsx index ea5beba0..fbb0b8c7 100644 --- a/src/pages/AllowedServicePage/AllowedServicePage.tsx +++ b/src/pages/AllowedServicePage/AllowedServicePage.tsx @@ -11,11 +11,11 @@ import TextField from '@/shared/components/TextField/TextField'; import { isUrlValid } from '@/shared/utils/validation'; import { ColorPaletteType } from '@/shared/types/allowedService'; -import { GetAllowedServiceListRes } from '@/shared/types/api/allowedService'; import BellIcon from '@/shared/assets/svgs/bell.svg?react'; import FriendSettingIcon from '@/shared/assets/svgs/friend_setting.svg?react'; +import ModalContentsAlert from '@/pages/AllowedServicePage/ModalContentsAlert/ModalContentsAlert'; import { allowedServiceKeys } from '@/shared/apisV2/allowedService/allowedService.keys'; import { useDeleteAllowedService, @@ -47,6 +47,7 @@ const AllowedServicePage = () => { const queryClient = useQueryClient(); const friendsModalRef = useRef(null); + const requireTitleModalRef = useRef(null); const handleChangeTitleInput = (e: ChangeEvent) => { setTitleInput(e.target.value); @@ -150,35 +151,24 @@ const AllowedServicePage = () => { } }; - const handleDeleteAllowedServiceGroup = (groupId: number, isActive: boolean, currentIndex: number) => { + const handleDeleteAllowedServiceGroup = (groupId: number) => { deleteAllowedServiceGroup( { allowedGroupId: groupId }, { onSuccess: () => { - queryClient.setQueryData( - allowedServiceKeys.allowedServiceList({ connectType: currentTap }), - (oldData: GetAllowedServiceListRes) => { - if (!oldData) return oldData; - return { - ...oldData, - data: oldData.data.filter((group) => group.id !== groupId), - }; - }, - ); - - if (isActive) { - if (allowedServiceList && allowedServiceList.data.length > 1) { - setActiveGroupId(allowedServiceList.data[currentIndex + 1].id); - } else { - handleEnableAddingAllowedServiceGroup(); - } - } + queryClient.invalidateQueries({ + queryKey: allowedServiceKeys.allowedServiceList({ connectType: currentTap }), + }); }, }, ); }; const handleAddAllowedService = (urlInput: string, activeGroupId: number | null) => { + if (!activeGroupId) { + handleOpenRequireTitleModal(); + return; + } if (activeGroupId && !isPending) { postAddAllowedService( { @@ -216,14 +206,23 @@ const AllowedServicePage = () => { const handleKeyDownUrlInput = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + e.preventDefault(); handleAddAllowedService(urlInput, activeGroupId); } }; // NOTE: 첫 렌더링 시 api를 통해 받은 첫번째 allowed service group id를 activeGroupId로 설정 + // 리스트 삭제 후, 현재 active 그룹이 리스트에 없는 경우 첫 번째 그룹으로 설정 useEffect(() => { - if (activeGroupId === null && allowedServiceList && allowedServiceList?.data.length > 0) { - setActiveGroupId(allowedServiceList.data[0].id); + if (allowedServiceList && allowedServiceList.data.length > 0) { + const activeGroupExists = activeGroupId && allowedServiceList.data.some((group) => group.id === activeGroupId); + + if (!activeGroupId || !activeGroupExists) { + setActiveGroupId(allowedServiceList.data[0].id); + } + } else if (allowedServiceList && allowedServiceList.data.length === 0 && activeGroupId !== null) { + // 리스트가 비어있고 선택된 그룹이 있으면 입력 모드로 전환 + handleEnableAddingAllowedServiceGroup(); } }, [allowedServiceList]); @@ -239,6 +238,14 @@ const AllowedServicePage = () => { friendsModalRef.current?.open(); }; + const handleOpenRequireTitleModal = () => { + requireTitleModalRef.current?.open(); + }; + + const handleCloseRequireTitleModal = () => { + requireTitleModalRef.current?.close(); + }; + return (
@@ -261,10 +268,9 @@ const AllowedServicePage = () => { {activeGroupId === null && ( )} - {allowedServiceList?.data.map((allowedServiceGroupData, index) => ( + {allowedServiceList?.data.map((allowedServiceGroupData) => ( { setCurrentTap('WEB')} isActive={currentTap === 'WEB'}> 웹사이트 - - 앱 - @@ -322,15 +325,16 @@ const AllowedServicePage = () => { 사이트 등록하기 - {allowedServiceGroupDetail && + activeGroupId && allowedServiceGroupDetail.data.allowedSites.map((allowedSiteData, index) => ( - handleDeleteAllowedService(allowedSiteData.id, allowedSiteData.siteUrl) - } + activeGroupId={activeGroupId} + onDeleteAllowedSite={() => { + handleDeleteAllowedService(allowedSiteData.id, allowedSiteData.siteUrl); + }} {...allowedSiteData} /> ))} @@ -352,6 +356,9 @@ const AllowedServicePage = () => { {({ isModalOpen }) => } + + {() => } + ); }; diff --git a/src/pages/AllowedServicePage/ModalContentsAlert/ModalContentsAlert.tsx b/src/pages/AllowedServicePage/ModalContentsAlert/ModalContentsAlert.tsx new file mode 100644 index 00000000..0b1a1a3c --- /dev/null +++ b/src/pages/AllowedServicePage/ModalContentsAlert/ModalContentsAlert.tsx @@ -0,0 +1,99 @@ +import { forwardRef } from 'react'; + +import ButtonRadius5 from '@/shared/components/ButtonRadius5/ButtonRadius5'; + +interface RequireTitleProps { + onClick: () => void; +} + +const RequireTitle = forwardRef(({ onClick }, ref) => { + return ( +
+

+ 허용서비스 리스트의 이름을 +
+ 먼저 입력해주세요. +

+ + 확인 + +
+ ); +}); + +interface ConfirmDeleteProps { + onClick: () => void; + pageName: string; +} + +const ConfirmDelete = forwardRef(({ onClick, pageName }, ref) => { + return ( +
+

+ ' + + {pageName} + + ' 허용 사이트가 +
+ 삭제되었습니다. +

+ + 확인 + +
+ ); +}); + +interface DomainAllowConfirmProps { + onConfirm: () => void; + onCancel: () => void; + siteName?: string; +} + +const DomainAllowConfirm = forwardRef( + ({ onConfirm, onCancel, siteName }, ref) => { + return ( +
+

+ ' + + {siteName} + + '의
+ 상위 도메인을 허용할까요? +

+

해당 사이트 이름을 가진 링크들이 하나로 통합돼요.

+
+ + 허용 + + + 취소하기 + +
+
+ ); + }, +); + +RequireTitle.displayName = 'RequireTitle'; +ConfirmDelete.displayName = 'ConfirmDelete'; +DomainAllowConfirm.displayName = 'DomainAllowConfirm'; + +const ModalContentsAlert = { + RequireTitle, + DomainAllowConfirm, + ConfirmDelete, +}; + +export default ModalContentsAlert; diff --git a/src/pages/AllowedServicePage/RecommendService/RecommendService.tsx b/src/pages/AllowedServicePage/RecommendService/RecommendService.tsx index d567af2d..86996815 100644 --- a/src/pages/AllowedServicePage/RecommendService/RecommendService.tsx +++ b/src/pages/AllowedServicePage/RecommendService/RecommendService.tsx @@ -8,8 +8,6 @@ import useCarousel from '@/shared/hooks/useCarousel'; import { RecommendSiteType } from '@/shared/types/allowedService'; import { Direction } from '@/shared/types/global'; -import { getServiceFavicon } from '@/pages/OnboardingPage/utils/serviceUrl'; - interface RecommendServiceItemProps extends ButtonHTMLAttributes { recommendSite: RecommendSiteType; } @@ -20,8 +18,8 @@ const RecommendServiceItem = ({ recommendSite, ...props }: RecommendServiceItemP className="flex w-[23.9rem] flex-shrink-0 items-center gap-[1.5rem] rounded-[8px] bg-gray-bg-01 p-[2rem] hover:bg-gray-bg-04 active:bg-gray-bg-02" > favicon

{recommendSite.siteName}

diff --git a/src/shared/apisV2/allowedService/allowedService.api.ts b/src/shared/apisV2/allowedService/allowedService.api.ts index 0a7f7c22..b754b743 100644 --- a/src/shared/apisV2/allowedService/allowedService.api.ts +++ b/src/shared/apisV2/allowedService/allowedService.api.ts @@ -11,6 +11,7 @@ import { PatchChangeAllowedServiceGroupNameReq, PostAddAllowedServiceGroupReq, PostAddAllowedServiceReq, + PostMergeAllowedSiteReq, } from '@/shared/types/api/allowedService'; import { authClient } from '../client'; @@ -25,6 +26,7 @@ const ALLOWED_SERVICE_ENDPOINT = { GET_RECOMMENDED_SITES: 'api/v2/recommendSite', POST_ADD_ALLOWED_SERVICE: 'api/v2/allowedSite/:allowedGroupId', DELETE_ALLOWED_SERVICE: 'api/v2/allowedSite/:allowedSiteId', + POST_MERGE_ALLOWED_SITE: 'api/v2/allowedSite/merge/:allowedGroupId', }; export const postAddAllowedServiceGroup = async ({ name, colorCode }: PostAddAllowedServiceGroupReq) => { @@ -103,3 +105,16 @@ export const deleteAllowedService = async ({ allowedSiteId }: DeleteAllowedServi ); return data; }; + +export const postMergeAllowedSite = async ({ allowedGroupId, siteUrl }: PostMergeAllowedSiteReq) => { + const { data } = await authClient.post( + ALLOWED_SERVICE_ENDPOINT.POST_MERGE_ALLOWED_SITE.replace(':allowedGroupId', String(allowedGroupId)), + {}, + { + params: { + siteUrl, + }, + }, + ); + return data; +}; diff --git a/src/shared/apisV2/allowedService/allowedService.mutations.ts b/src/shared/apisV2/allowedService/allowedService.mutations.ts index dfb55c44..8ad6269f 100644 --- a/src/shared/apisV2/allowedService/allowedService.mutations.ts +++ b/src/shared/apisV2/allowedService/allowedService.mutations.ts @@ -10,6 +10,7 @@ import { patchChangeAllowedServiceGroupName, postAddAllowedService, postAddAllowedServiceGroup, + postMergeAllowedSite, } from './allowedService.api'; import { allowedServiceKeys } from './allowedService.keys'; @@ -77,3 +78,14 @@ export const useDeleteAllowedService = () => { }, }); }; + +export const usePostMergeToParentDomain = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: postMergeAllowedSite, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: allowedServiceKeys.allowedService }); + }, + }); +}; diff --git a/src/shared/assets/svgs/default_url_favicon.svg b/src/shared/assets/svgs/default_url_favicon.svg new file mode 100644 index 00000000..487dce24 --- /dev/null +++ b/src/shared/assets/svgs/default_url_favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/shared/components/Dropdown/Dropdown.tsx b/src/shared/components/Dropdown/Dropdown.tsx index 882e012f..2e54f21b 100644 --- a/src/shared/components/Dropdown/Dropdown.tsx +++ b/src/shared/components/Dropdown/Dropdown.tsx @@ -49,7 +49,7 @@ const DropdownRoot = ({ children, onOpenChange }: DropdownRootProps) => { return ( -
+
{children}
@@ -95,7 +95,7 @@ const DropdownContent = forwardRef(
    {open && children}
diff --git a/src/shared/components/FaviconImage/FaviconImage.tsx b/src/shared/components/FaviconImage/FaviconImage.tsx index ce48cfba..52bebb70 100644 --- a/src/shared/components/FaviconImage/FaviconImage.tsx +++ b/src/shared/components/FaviconImage/FaviconImage.tsx @@ -1,15 +1,15 @@ import { ImgHTMLAttributes } from 'react'; -import LogoPath from '@/shared/assets/svgs/logo_icon.svg'; +import DefaultFaviconImage from '@/shared/assets/svgs/default_url_favicon.svg'; export const FaviconImage = ({ src, className, ...rest }: ImgHTMLAttributes) => { return ( { - e.currentTarget.src = LogoPath; + e.currentTarget.src = DefaultFaviconImage; e.currentTarget.alt = '모립 로고 아이콘'; }} /> diff --git a/src/shared/types/api/allowedService.ts b/src/shared/types/api/allowedService.ts index bc16922a..90cda72c 100644 --- a/src/shared/types/api/allowedService.ts +++ b/src/shared/types/api/allowedService.ts @@ -64,6 +64,7 @@ export interface GetRecommendedSitesRes { recommendSites: { siteName: string; siteUrl: string; + favicon: string; }[]; }; } @@ -80,3 +81,8 @@ export interface DeleteAllowedServiceReq { export interface GetRecommendedSitesReq { allowedGroupId: number; } + +export interface PostMergeAllowedSiteReq { + allowedGroupId: number; + siteUrl: string; +}