diff --git a/frontend/techpick/package.json b/frontend/techpick/package.json index cd176072..7da2310b 100644 --- a/frontend/techpick/package.json +++ b/frontend/techpick/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "1.3.0", "@radix-ui/react-popover": "1.1.2", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-visually-hidden": "^1.1.0", "@tanstack/react-query": "^5.59.0", diff --git a/frontend/techpick/src/apis/apiConstants.ts b/frontend/techpick/src/apis/apiConstants.ts index 58136b4d..d7d32ed4 100644 --- a/frontend/techpick/src/apis/apiConstants.ts +++ b/frontend/techpick/src/apis/apiConstants.ts @@ -3,6 +3,7 @@ const API_ENDPOINTS = { LOCATION: 'location', BASIC: 'basic', PICKS: 'picks', + TAGS: 'tags', }; export const API_URLS = { @@ -24,4 +25,9 @@ export const API_URLS = { `${cursor ? '&cursor=' + cursor : ''}` + `${size ? '&size=' + size : ''}`, MOVE_PICKS: `${API_ENDPOINTS.PICKS}/${API_ENDPOINTS.LOCATION}`, + UPDATE_PICKS: `${API_ENDPOINTS.PICKS}`, + CREATE_TAGS: `${API_ENDPOINTS.TAGS}`, + DELETE_TAGS: `${API_ENDPOINTS.TAGS}`, + UPDATE_TAGS: `${API_ENDPOINTS.TAGS}`, + GET_TAGS: `${API_ENDPOINTS.TAGS}`, }; diff --git a/frontend/techpick/src/apis/pick/getPick/getPick.ts b/frontend/techpick/src/apis/pick/getPick/getPick.ts index b2fa2141..4620342d 100644 --- a/frontend/techpick/src/apis/pick/getPick/getPick.ts +++ b/frontend/techpick/src/apis/pick/getPick/getPick.ts @@ -1,12 +1,10 @@ import { HTTPError } from 'ky'; import { apiClient, returnErrorFromHTTPError } from '@/apis'; -import type { GetPickResponseType } from '../pickApi.type'; +import type { PickInfoType } from '@/types'; -export const getPick = async (pickId: number): Promise => { +export const getPick = async (pickId: number): Promise => { try { - const response = await apiClient.get( - `picks/${pickId}` - ); + const response = await apiClient.get(`picks/${pickId}`); const data = await response.json(); return data; diff --git a/frontend/techpick/src/apis/pick/getPick/useGetPickQuery.ts b/frontend/techpick/src/apis/pick/getPick/useGetPickQuery.ts index d805eb92..25c33d9c 100644 --- a/frontend/techpick/src/apis/pick/getPick/useGetPickQuery.ts +++ b/frontend/techpick/src/apis/pick/getPick/useGetPickQuery.ts @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { getPick } from './getPick'; -import type { GetPickResponseType } from '../pickApi.type'; +import type { PickInfoType } from '@/types'; // todo: suspense query로 바꾸기. export const useGetPickQuery = (pickId: number) => { - return useQuery({ + return useQuery({ queryKey: ['pick', pickId], queryFn: () => { return getPick(pickId); diff --git a/frontend/techpick/src/apis/pick/index.ts b/frontend/techpick/src/apis/pick/index.ts index a22ab748..7eb2088d 100644 --- a/frontend/techpick/src/apis/pick/index.ts +++ b/frontend/techpick/src/apis/pick/index.ts @@ -1,4 +1,4 @@ export { useGetPickQuery } from './getPick/useGetPickQuery'; -export { useUpdatePickMutation } from './updatePick/useUpdatePickMutation'; export { getPicksByFolderId } from './getPicks'; export { movePicks } from './movePicks'; +export { updatePick } from './updatePick'; diff --git a/frontend/techpick/src/apis/pick/pickApi.type.ts b/frontend/techpick/src/apis/pick/pickApi.type.ts deleted file mode 100644 index 58c2b952..00000000 --- a/frontend/techpick/src/apis/pick/pickApi.type.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Concrete } from '@/types/util.type'; -import type { components } from '@/schema'; - -export type GetPickResponseType = Concrete< - components['schemas']['techpick.api.application.pick.dto.PickApiResponse$Pick'] ->; -export type UpdatePickRequestType = - components['schemas']['techpick.api.application.pick.dto.PickApiRequest$Update']; diff --git a/frontend/techpick/src/apis/pick/updatePick/updatePick.ts b/frontend/techpick/src/apis/pick/updatePick.ts similarity index 62% rename from frontend/techpick/src/apis/pick/updatePick/updatePick.ts rename to frontend/techpick/src/apis/pick/updatePick.ts index 0320ca68..4264d334 100644 --- a/frontend/techpick/src/apis/pick/updatePick/updatePick.ts +++ b/frontend/techpick/src/apis/pick/updatePick.ts @@ -1,15 +1,16 @@ import { HTTPError } from 'ky'; import { apiClient, returnErrorFromHTTPError } from '@/apis'; -import type { - GetPickResponseType, - UpdatePickRequestType, -} from '../pickApi.type'; +import { API_URLS } from '../apiConstants'; +import type { UpdatePickRequestType, PickInfoType } from '@/types'; export const updatePick = async (pickInfo: UpdatePickRequestType) => { try { - const response = await apiClient.put('picks', { - json: pickInfo, - }); + const response = await apiClient.patch( + API_URLS.UPDATE_PICKS, + { + json: pickInfo, + } + ); const data = await response.json(); return data; diff --git a/frontend/techpick/src/apis/pick/updatePick/useUpdatePickMutation.ts b/frontend/techpick/src/apis/pick/updatePick/useUpdatePickMutation.ts deleted file mode 100644 index f30c8d70..00000000 --- a/frontend/techpick/src/apis/pick/updatePick/useUpdatePickMutation.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { updatePick } from './updatePick'; -import type { UpdatePickRequestType } from '../pickApi.type'; - -export const useUpdatePickMutation = (pickId: number) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (pickInfo: UpdatePickRequestType) => { - return updatePick(pickInfo); - }, - onSettled() { - queryClient.invalidateQueries({ - queryKey: ['pick', pickId], - }); - }, - }); -}; diff --git a/frontend/techpick/src/apis/tag/createTag.ts b/frontend/techpick/src/apis/tag/createTag.ts index 9a988d1f..df2f5776 100644 --- a/frontend/techpick/src/apis/tag/createTag.ts +++ b/frontend/techpick/src/apis/tag/createTag.ts @@ -1,10 +1,14 @@ -import { apiClient } from '@/apis'; +import { apiClient } from '../apiClient'; +import { API_URLS } from '../apiConstants'; import { CreateTagRequestType, CreateTagResponseType } from '@/types'; export const createTag = async (createTag: CreateTagRequestType) => { - const response = await apiClient.post('tag', { - body: JSON.stringify(createTag), - }); + const response = await apiClient.post( + API_URLS.CREATE_TAGS, + { + body: JSON.stringify(createTag), + } + ); const data = await response.json(); return data; diff --git a/frontend/techpick/src/apis/tag/deleteTag.ts b/frontend/techpick/src/apis/tag/deleteTag.ts index d819f53d..48fa6c1d 100644 --- a/frontend/techpick/src/apis/tag/deleteTag.ts +++ b/frontend/techpick/src/apis/tag/deleteTag.ts @@ -1,4 +1,5 @@ -import { apiClient } from '@/apis'; +import { apiClient } from '../apiClient'; +import { API_URLS } from '../apiConstants'; import { DeleteTagRequestType } from '@/types'; export const deleteTag = async (tagId: DeleteTagRequestType['id']) => { @@ -6,7 +7,7 @@ export const deleteTag = async (tagId: DeleteTagRequestType['id']) => { id: tagId, }; - await apiClient.delete(`tags`, { + await apiClient.delete(API_URLS.DELETE_TAGS, { json: deleteTagRequest, }); }; diff --git a/frontend/techpick/src/apis/tag/getTagList.ts b/frontend/techpick/src/apis/tag/getTagList.ts index 0fc5eb5d..df3434e4 100644 --- a/frontend/techpick/src/apis/tag/getTagList.ts +++ b/frontend/techpick/src/apis/tag/getTagList.ts @@ -1,8 +1,11 @@ -import { apiClient } from '@/apis'; +import { apiClient } from '../apiClient'; +import { API_URLS } from '../apiConstants'; import type { GetTagListResponseType } from '@/types'; export const getTagList = async () => { - const response = await apiClient.get('tags'); + const response = await apiClient.get( + API_URLS.GET_TAGS + ); const data = await response.json(); return data; diff --git a/frontend/techpick/src/apis/tag/updateTag.ts b/frontend/techpick/src/apis/tag/updateTag.ts index f0982205..3a925705 100644 --- a/frontend/techpick/src/apis/tag/updateTag.ts +++ b/frontend/techpick/src/apis/tag/updateTag.ts @@ -1,20 +1,15 @@ -import { apiClient } from '@/apis'; +import { apiClient } from '../apiClient'; +import { API_URLS } from '../apiConstants'; import { UpdateTagRequestType, UpdateTagResponseType } from '@/types'; -// const findTargetTag = ( -// tagList: UpdateTagResponseType, -// updateTag: TagUpdateType -// ) => { -// const targetTag = tagList.filter((tag) => tag.tagId === updateTag.tagId); -// return targetTag; -// }; - export const updateTag = async (updateTag: UpdateTagRequestType) => { - const response = await apiClient.put('tag', { - json: [updateTag], - }); + const response = await apiClient.put( + API_URLS.UPDATE_TAGS, + { + json: [updateTag], + } + ); const updatedTag = await response.json(); - //const updatedTag = findTargetTag(totalTagList, updateTag); return updatedTag; }; diff --git a/frontend/techpick/src/app/(signed)/folders/layout.css.ts b/frontend/techpick/src/app/(signed)/folders/layout.css.ts index 47aeb1ae..395e9931 100644 --- a/frontend/techpick/src/app/(signed)/folders/layout.css.ts +++ b/frontend/techpick/src/app/(signed)/folders/layout.css.ts @@ -8,11 +8,11 @@ export const pageContainerLayout = style({ }); export const ListViewerLayout = style({ - display: 'flex', - flexDirection: 'column', width: '100%', height: '100vh', padding: space['32'], + flexShrink: 1, + minWidth: 0, '@media': { 'screen and (min-width: 1440px)': { diff --git a/frontend/techpick/src/app/(signed)/folders/layout.tsx b/frontend/techpick/src/app/(signed)/folders/layout.tsx index ad746cff..3634b9af 100644 --- a/frontend/techpick/src/app/(signed)/folders/layout.tsx +++ b/frontend/techpick/src/app/(signed)/folders/layout.tsx @@ -1,5 +1,9 @@ import { PropsWithChildren, Suspense } from 'react'; -import { FolderTree, FolderAndPickDndContextProvider } from '@/components'; +import { + FolderTree, + FolderAndPickDndContextProvider, + PickRecordHeader, +} from '@/components'; import { CurrentPathIndicator } from '@/components/FolderPathIndicator/CurrentPathIndicator'; import { SearchWidget } from '@/components/SearchWidget/SearchWidget'; import { @@ -26,6 +30,7 @@ export default function FolderLayout({ children }: PropsWithChildren) { + {children} diff --git a/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx b/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx index fde9619f..e2b505a8 100644 --- a/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx +++ b/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx @@ -1,7 +1,8 @@ 'use client'; import { useEffect } from 'react'; -import { DraggablePickListViewer } from '@/components'; +import { PickDraggableListLayout } from '@/components/PickDraggableListLayout'; +import { PickDraggableRecord } from '@/components/PickRecord/PickDraggableRecord'; import { useTreeStore } from '@/stores/dndTreeStore/dndTreeStore'; import { usePickStore } from '@/stores/pickStore/pickStore'; @@ -37,11 +38,17 @@ export default function UnclassifiedFolderPage() { return
loading...
; } + const pickList = getOrderedPickListByFolderId( + basicFolderMap['UNCLASSIFIED'].id + ); return ( - + viewType="record" + > + {pickList.map((pickInfo) => { + return ; + })} + ); } diff --git a/frontend/techpick/src/components/Breadcrumb/Breadcumb.tsx b/frontend/techpick/src/components/Breadcrumb/Breadcumb.tsx index 1ac3acd7..9eb7a061 100644 --- a/frontend/techpick/src/components/Breadcrumb/Breadcumb.tsx +++ b/frontend/techpick/src/components/Breadcrumb/Breadcumb.tsx @@ -80,14 +80,14 @@ const BreadcrumbSeparator = ({ className, ...props }: React.ComponentProps<'li'>) => ( - + ); BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'; diff --git a/frontend/techpick/src/components/Button/Button.css.ts b/frontend/techpick/src/components/Button/Button.css.ts index f9c5ce66..57bcdee0 100644 --- a/frontend/techpick/src/components/Button/Button.css.ts +++ b/frontend/techpick/src/components/Button/Button.css.ts @@ -43,13 +43,13 @@ export type buttonColorVariantKeyTypes = keyof typeof buttonColorVariants; export const buttonBackgroundVariants = styleVariants({ primary: { - backgroundColor: colorVars.blue1, + backgroundColor: colorVars.blue8, }, secondary: { backgroundColor: colorVars.green1, }, warning: { - backgroundColor: colorVars.orange1, + backgroundColor: colorVars.orange8, }, }); @@ -71,11 +71,11 @@ export const buttonStyle = style({ selectors: { '&[data-variant="primary"]:hover, &[data-variant="primary"]:focus': { - backgroundColor: colorVars.amber1, + backgroundColor: colorVars.blue10, }, '&[data-variant="warning"]:hover, &[data-variant="warning"]:focus': { - backgroundColor: colorVars.orange1, + backgroundColor: colorVars.orange10, }, }, }); diff --git a/frontend/techpick/src/components/DeselectTagButton/DeselectTagButton.tsx b/frontend/techpick/src/components/DeselectTagButton/DeselectTagButton.tsx deleted file mode 100644 index 998742f7..00000000 --- a/frontend/techpick/src/components/DeselectTagButton/DeselectTagButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; - -import { X } from 'lucide-react'; -import { DeselectTagButtonStyle } from './DeselectTagButton.css'; - -export function DeselectTagButton({ - onClick = () => {}, -}: DeselectTagButtonProps) { - return ( - - ); -} - -interface DeselectTagButtonProps { - onClick?: () => void; -} diff --git a/frontend/techpick/src/components/DragOverlay.tsx b/frontend/techpick/src/components/DragOverlay.tsx index 404adc64..a6623bef 100644 --- a/frontend/techpick/src/components/DragOverlay.tsx +++ b/frontend/techpick/src/components/DragOverlay.tsx @@ -3,15 +3,13 @@ import { useEffect, useState } from 'react'; import type { CSSProperties } from 'react'; import { DragOverlay as DndKitDragOverlay } from '@dnd-kit/core'; -import { usePickRenderModeStore, usePickStore, useTreeStore } from '@/stores'; +import { usePickStore, useTreeStore } from '@/stores'; import { pickDragOverlayStyle } from './dragOverlay.css'; -import { PickCard } from './PickListViewer/PickCard'; -import { PickRecord } from './PickListViewer/PickRecord'; +import { PickRecord } from './PickRecord'; export function DargOverlay({ elementClickPosition }: DargOverlayProps) { const { isDragging: isFolderDragging, draggingFolderInfo } = useTreeStore(); const { isDragging: isPickDragging, draggingPickInfo } = usePickStore(); - const { pickRenderMode } = usePickRenderModeStore(); const [mousePosition, setMousePosition] = useState({ x: -1, y: -1 }); const overlayStyle: CSSProperties = { top: `${mousePosition.y}px`, @@ -74,18 +72,10 @@ export function DargOverlay({ elementClickPosition }: DargOverlayProps) { } if (isPickDragging && draggingPickInfo) { - if (pickRenderMode === 'list') { - return ( - - - - ); - } - return (
- +
); diff --git a/frontend/techpick/src/components/PickDraggableListLayout.tsx b/frontend/techpick/src/components/PickDraggableListLayout.tsx new file mode 100644 index 00000000..f3841417 --- /dev/null +++ b/frontend/techpick/src/components/PickDraggableListLayout.tsx @@ -0,0 +1,16 @@ +import { PickListSortableContextProvider } from './PickListSortableContextProvider'; +import { PickViewDraggableItemListLayoutComponentProps } from '@/types'; + +export function PickDraggableListLayout({ + viewType = 'record', + folderId, + children, +}: PickViewDraggableItemListLayoutComponentProps) { + return ( +
+ + {children} + +
+ ); +} diff --git a/frontend/techpick/src/components/PickListSortableContextProvider.tsx b/frontend/techpick/src/components/PickListSortableContextProvider.tsx new file mode 100644 index 00000000..7b424297 --- /dev/null +++ b/frontend/techpick/src/components/PickListSortableContextProvider.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { + SortableContext, + rectSortingStrategy, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { usePickStore } from '@/stores'; +import { PickViewDraggableItemListLayoutComponentProps } from '@/types'; + +export function PickListSortableContextProvider({ + folderId, + children, + viewType, +}: PickViewDraggableItemListLayoutComponentProps) { + const { + getOrderedPickIdListByFolderId, + selectedPickIdList, + isDragging, + focusPickId, + } = usePickStore(); + const orderedPickIdList = getOrderedPickIdListByFolderId(folderId); + const orderedPickIdListWithoutSelectedIdList = isDragging + ? orderedPickIdList.filter( + (orderedPickId) => + !selectedPickIdList.includes(orderedPickId) || + orderedPickId === focusPickId + ) + : orderedPickIdList; + + /** + * @description card일때와 vertical일 때(listItem, record) 렌더링이 다릅니다. + */ + const strategy = + viewType === 'record' ? verticalListSortingStrategy : rectSortingStrategy; + + return ( + + {children} + + ); +} diff --git a/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx b/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx index 81c2b66f..c9ba5415 100644 --- a/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx +++ b/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx @@ -1,8 +1,8 @@ import type { ReactNode } from 'react'; import { PickDnDCard } from './PickDnDCard'; import { PickDnDCardListLayout } from './PickDnDCardListLayout'; -import { PickDndRecord } from './PickDndRecord'; -import { PickDndRecordListLayout } from './PickDndRecordListLayout'; +import { PickDndListItem } from './PickDndListItem'; +import { PickDndListItemLayout } from './PickDndListItemLayout'; import type { PickViewItemComponentProps, PickViewItemListLayoutComponentProps, @@ -41,8 +41,8 @@ const DND_PICK_LIST_VIEW_TEMPLATES: Record< PickViewItemListLayoutComponent: PickDnDCardListLayout, }, list: { - PickViewItemComponent: PickDndRecord, - PickViewItemListLayoutComponent: PickDndRecordListLayout, + PickViewItemComponent: PickDndListItem, + PickViewItemListLayoutComponent: PickDndListItemLayout, }, }; diff --git a/frontend/techpick/src/components/PickListViewer/PickDndRecord.tsx b/frontend/techpick/src/components/PickListViewer/PickDndListItem.tsx similarity index 70% rename from frontend/techpick/src/components/PickListViewer/PickDndRecord.tsx rename to frontend/techpick/src/components/PickListViewer/PickDndListItem.tsx index 59f85f17..63b796fa 100644 --- a/frontend/techpick/src/components/PickListViewer/PickDndRecord.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickDndListItem.tsx @@ -1,19 +1,19 @@ 'use client'; -import { MouseEvent, useCallback, type CSSProperties } from 'react'; +import { MouseEvent, type CSSProperties } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { usePickStore } from '@/stores'; +import { usePickStore, useUpdatePickStore } from '@/stores'; import { isSelectionActive } from '@/utils'; import { isActiveDraggingItemStyle, selectedDragItemStyle, } from './pickDnDCard.css'; import { getSelectedPickRange } from './pickDnDCard.util'; -import { PickRecord } from './PickRecord'; +import { PickListItem } from './PickListItem'; import type { PickViewDnDItemComponentProps } from './PickListViewer'; -export function PickDndRecord({ pickInfo }: PickViewDnDItemComponentProps) { +export function PickDndListItem({ pickInfo }: PickViewDnDItemComponentProps) { const { selectedPickIdList, selectSinglePick, @@ -22,8 +22,8 @@ export function PickDndRecord({ pickInfo }: PickViewDnDItemComponentProps) { setSelectedPickIdList, isDragging, } = usePickStore(); - const { linkInfo, id: pickId, parentFolderId } = pickInfo; - const { url } = linkInfo; + const { setCurrentPickIdToNull } = useUpdatePickStore(); + const { id: pickId, parentFolderId } = pickInfo; const isSelected = selectedPickIdList.includes(pickId); const { attributes, @@ -48,10 +48,6 @@ export function PickDndRecord({ pickInfo }: PickViewDnDItemComponentProps) { opacity: 1, }; - const openUrl = useCallback(() => { - window.open(url, '_blank'); - }, [url]); - const handleShiftSelect = (parentFolderId: number, pickId: number) => { if (!focusPickId) { return; @@ -79,6 +75,7 @@ export function PickDndRecord({ pickInfo }: PickViewDnDItemComponentProps) { return; } + setCurrentPickIdToNull(); selectSinglePick(pickId); }; @@ -87,17 +84,14 @@ export function PickDndRecord({ pickInfo }: PickViewDnDItemComponentProps) { } return ( - <> -
-
handleClick(pickId, event)} - id={pickElementId} - > - -
+
+
handleClick(pickId, event)} + id={pickElementId} + > +
- +
); } diff --git a/frontend/techpick/src/components/PickListViewer/PickDndRecordListLayout.tsx b/frontend/techpick/src/components/PickListViewer/PickDndListItemLayout.tsx similarity index 70% rename from frontend/techpick/src/components/PickListViewer/PickDndRecordListLayout.tsx rename to frontend/techpick/src/components/PickListViewer/PickDndListItemLayout.tsx index c0f814f8..bd526b87 100644 --- a/frontend/techpick/src/components/PickListViewer/PickDndRecordListLayout.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickDndListItemLayout.tsx @@ -1,17 +1,17 @@ import { PickViewDnDItemListLayoutComponentProps } from './DraggablePickListViewer'; +import { PickListItemLayout } from './PickListItemLayout'; import { PickListSortableContext } from './PickListSortableContext'; -import { PickRecordListLayout } from './PickRecordListLayout'; -export function PickDndRecordListLayout({ +export function PickDndListItemLayout({ children, folderId, viewType, }: PickViewDnDItemListLayoutComponentProps) { return ( - + {children} - + ); } diff --git a/frontend/techpick/src/components/PickListViewer/PickListItem.tsx b/frontend/techpick/src/components/PickListViewer/PickListItem.tsx new file mode 100644 index 00000000..e0715be9 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickListItem.tsx @@ -0,0 +1,105 @@ +'use client'; + +import Image from 'next/image'; +import { SelectedTagItem, SelectedTagListLayout } from '@/components'; +import { useOpenUrlInNewTab } from '@/hooks'; +import { usePickStore, useTagStore, useUpdatePickStore } from '@/stores'; +import { formatDateString } from '@/utils'; +import { + pickListItemLayoutStyle, + pickImageSectionLayoutStyle, + pickImageStyle, + pickEmptyImageStyle, + pickContentSectionLayoutStyle, + pickTitleSectionStyle, + pickDetailInfoLayoutStyle, + dividerDot, + dateTextStyle, +} from './pickListItem.css'; +import { PickViewItemComponentProps } from './PickListViewer'; +import { PickTitleInput } from './PickTitleInput'; + +export function PickListItem({ pickInfo }: PickViewItemComponentProps) { + const pick = pickInfo; + const link = pickInfo.linkInfo; + const { findTagById } = useTagStore(); + const { updatePickInfo } = usePickStore(); + const { openUrlInNewTab } = useOpenUrlInNewTab(link.url); + const { + currentUpdatePickId, + setCurrentPickIdToNull, + setCurrentUpdatePickId, + } = useUpdatePickStore(); + const isUpdateTitle = currentUpdatePickId === pickInfo.id; + + return ( +
+
+ {link.imageUrl ? ( + + ) : ( +
+ )} +
+
+ {isUpdateTitle ? ( + { + updatePickInfo(pick.parentFolderId, { + ...pickInfo, + title: newTitle, + }); + setCurrentPickIdToNull(); + }} + onClickOutSide={() => { + setCurrentPickIdToNull(); + }} + /> + ) : ( +
{ + setCurrentUpdatePickId(pickInfo.id); + event.stopPropagation(); + }} + role="button" + > + {pick.title} +
+ )} +
+ {0 < pick.tagIdOrderedList.length && ( + + {pick.tagIdOrderedList + .map(findTagById) + .map( + (tag) => tag && + )} + + )} + {0 < pick.tagIdOrderedList.length && ( +

·

// divider + )} +

{formatDateString(pick.updatedAt)}

+
+
+ +
+ link +
+
+ ); +} diff --git a/frontend/techpick/src/components/PickListViewer/PickListItemLayout.tsx b/frontend/techpick/src/components/PickListViewer/PickListItemLayout.tsx new file mode 100644 index 00000000..8bf62026 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickListItemLayout.tsx @@ -0,0 +1,9 @@ +'use client'; +import { pickListItemLayoutStyle } from './pickListItemLayout.css'; +import { PickViewItemListLayoutComponentProps } from './PickListViewer'; + +export function PickListItemLayout({ + children, +}: PickViewItemListLayoutComponentProps) { + return
{children}
; +} diff --git a/frontend/techpick/src/components/PickListViewer/PickListViewer.tsx b/frontend/techpick/src/components/PickListViewer/PickListViewer.tsx index ac9e2eb7..db2c5dfc 100644 --- a/frontend/techpick/src/components/PickListViewer/PickListViewer.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickListViewer.tsx @@ -1,7 +1,9 @@ import type { PropsWithChildren, ReactNode } from 'react'; import { PickCard } from './PickCard'; import { PickCardListLayout } from './PickCardListLayout'; -import type { PickInfoType } from '@/types'; +import { PickListItem } from './PickListItem'; +import { PickListItemLayout } from './PickListItemLayout'; +import type { PickInfoType, PickRenderModeType } from '@/types'; export function PickListViewer({ pickList, @@ -21,24 +23,23 @@ export function PickListViewer({ interface PickListViewerProps { pickList: PickInfoType[]; - viewType?: ViewTemplateType; + viewType?: PickRenderModeType; } const NORMAL_PICK_LIST_VIEW_TEMPLATES: Record< - ViewTemplateType, + PickRenderModeType, ViewTemplateValueType > = { card: { PickViewItemListLayoutComponent: PickCardListLayout, PickViewItemComponent: PickCard, }, + list: { + PickViewItemListLayoutComponent: PickListItemLayout, + PickViewItemComponent: PickListItem, + }, }; -/** - * @description ViewTemplateType은 pickInfo를 어떤 UI로 보여줄지 나타냅니다. ex) card, list - */ -type ViewTemplateType = 'card'; - interface ViewTemplateValueType { PickViewItemListLayoutComponent: ( props: PickViewItemListLayoutComponentProps diff --git a/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx b/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx index 88fb7cff..f7ba767e 100644 --- a/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx @@ -4,11 +4,11 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import InfiniteLoader from 'react-window-infinite-loader'; import { colorVars } from 'techpick-shared'; -import { PickRecord } from '@/components/PickListViewer/PickRecord'; import { pickRecordListLayoutInlineStyle, RECORD_HEIGHT, } from '@/components/PickListViewer/pickRecordListLayout.css'; +import { PickSearchRecord } from '@/components/PickListViewer/PickSearchRecord'; import { usePickStore } from '@/stores'; import { PickListType } from '@/types'; @@ -44,7 +44,7 @@ export function PickListViewerInfiniteScroll( return (
{isItemLoaded(index) ? ( - + ) : ( )} diff --git a/frontend/techpick/src/components/PickListViewer/PickRecord.tsx b/frontend/techpick/src/components/PickListViewer/PickSearchRecord.tsx similarity index 95% rename from frontend/techpick/src/components/PickListViewer/PickRecord.tsx rename to frontend/techpick/src/components/PickListViewer/PickSearchRecord.tsx index 98d9bffd..1fc9ffec 100644 --- a/frontend/techpick/src/components/PickListViewer/PickRecord.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickSearchRecord.tsx @@ -14,9 +14,9 @@ import { recordBodySectionStyle, recordTitleAndBodySectionLayoutStyle, recordSubTextStyle, -} from './pickRecord.css'; +} from './pickSearchRecord.css'; -export function PickRecord({ pickInfo }: PickViewItemComponentProps) { +export function PickSearchRecord({ pickInfo }: PickViewItemComponentProps) { const { findTagById } = useTagStore(); const pick = pickInfo; const link = pickInfo.linkInfo; diff --git a/frontend/techpick/src/components/PickListViewer/PickTitleInput.tsx b/frontend/techpick/src/components/PickListViewer/PickTitleInput.tsx new file mode 100644 index 00000000..381e0ac3 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickTitleInput.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useCallback, useEffect, useRef } from 'react'; +import type { KeyboardEvent } from 'react'; +import { isEmptyString } from '@/utils'; +import { pickTitleInputStyle } from './pickTitleInput.css'; + +export function PickTitleInput({ + onSubmit, + onClickOutSide = () => {}, + initialValue = '', +}: PickTitleInputProps) { + const containerRef = useRef(null); + const inputRef = useRef(null); + + const submitIfNotEmptyString = useCallback(() => { + const pickTitle = inputRef.current?.value.trim() ?? ''; + if (isEmptyString(pickTitle)) return; + + onSubmit(pickTitle); + }, [onSubmit]); + + const onEnter = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + submitIfNotEmptyString(); + } + }; + + useEffect( + function detectOutsideClick() { + const handleClickOutside = (event: MouseEvent) => { + console.log('handleClickOutside work!'); + + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + submitIfNotEmptyString(); + onClickOutSide(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, + [onClickOutSide, submitIfNotEmptyString] + ); + + useEffect( + function initializeFolderInput() { + if (inputRef.current) { + inputRef.current.value = initialValue; + + // 타이밍 이슈 탓으로 인해 setTimeout 사용 + setTimeout(() => inputRef.current?.focus(), 0); + } + }, + [initialValue] + ); + + return ( +
e.stopPropagation()}> + +
+ ); +} + +interface PickTitleInputProps { + onSubmit: (value: string) => void; + onClickOutSide?: () => void; + initialValue?: string; +} diff --git a/frontend/techpick/src/components/PickListViewer/pickListItem.css.ts b/frontend/techpick/src/components/PickListViewer/pickListItem.css.ts new file mode 100644 index 00000000..9c8e7d77 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/pickListItem.css.ts @@ -0,0 +1,66 @@ +import { style } from '@vanilla-extract/css'; +import { sizes, typography, colorVars, space } from 'techpick-shared'; + +export const pickListItemLayoutStyle = style({ + position: 'relative', + display: 'flex', + flexDirection: 'row', + gap: space['16'], + minWidth: 0, + maxWidth: '100%', + height: '130px', + padding: space['16'], + borderTop: `1px solid ${colorVars.gray6}`, + cursor: 'pointer', +}); + +export const pickImageSectionLayoutStyle = style({ + position: 'relative', + width: sizes['8xs'], + flexShrink: 0, +}); + +export const pickImageStyle = style({ + objectFit: 'cover', + borderRadius: '2px', +}); + +export const pickEmptyImageStyle = style({ + border: '1px solid black', +}); + +export const pickContentSectionLayoutStyle = style({ + flexGrow: '1', + flexShrink: '1', + minWidth: '0', + maxWidth: '100%', +}); + +export const pickTitleSectionStyle = style({ + fontSize: typography.fontSize['2xl'], + fontWeight: typography.fontWeights['light'], + height: '32px', + lineHeight: '32px', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', +}); + +export const pickDetailInfoLayoutStyle = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: space['8'], +}); + +export const dividerDot = style({ + fontSize: typography.fontSize['sm'], + fontWeight: typography.fontWeights['normal'], + color: colorVars.gray11, +}); + +export const dateTextStyle = style({ + fontSize: typography.fontSize['sm'], + fontWeight: typography.fontWeights['normal'], + color: colorVars.gray11, +}); diff --git a/frontend/techpick/src/components/PickListViewer/pickListItemLayout.css.ts b/frontend/techpick/src/components/PickListViewer/pickListItemLayout.css.ts new file mode 100644 index 00000000..43e48b87 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/pickListItemLayout.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; +import { sizes } from 'techpick-shared'; + +export const pickListItemLayoutStyle = style({ + width: sizes['full'], + height: sizes['full'], + overflowY: 'scroll', +}); diff --git a/frontend/techpick/src/components/PickListViewer/pickRecord.css.ts b/frontend/techpick/src/components/PickListViewer/pickSearchRecord.css.ts similarity index 100% rename from frontend/techpick/src/components/PickListViewer/pickRecord.css.ts rename to frontend/techpick/src/components/PickListViewer/pickSearchRecord.css.ts diff --git a/frontend/techpick/src/components/PickListViewer/pickTitleInput.css.ts b/frontend/techpick/src/components/PickListViewer/pickTitleInput.css.ts new file mode 100644 index 00000000..905bf7fc --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/pickTitleInput.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css'; +import { typography } from 'techpick-shared'; + +export const pickTitleInputStyle = style({ + fontSize: typography.fontSize['2xl'], + fontWeight: typography.fontWeights['light'], + height: '32px', + margin: 0, + width: '100%', +}); diff --git a/frontend/techpick/src/components/PickRecord/PickDateColumnLayout.tsx b/frontend/techpick/src/components/PickRecord/PickDateColumnLayout.tsx new file mode 100644 index 00000000..5f1d4808 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickDateColumnLayout.tsx @@ -0,0 +1,11 @@ +import type { PropsWithChildren } from 'react'; +import { pickDateColumnLayoutStyle } from './pickDateColumnLayout.css'; +import { Gap } from '../Gap'; + +export function PickDateColumnLayout({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/techpick/src/components/PickRecord/PickDraggableRecord.tsx b/frontend/techpick/src/components/PickRecord/PickDraggableRecord.tsx new file mode 100644 index 00000000..2a5c3d55 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickDraggableRecord.tsx @@ -0,0 +1,104 @@ +import type { CSSProperties, MouseEvent } from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { usePickStore, useUpdatePickStore } from '@/stores'; +import { getSelectedPickRange, isSelectionActive } from '@/utils'; +import { + isActiveDraggingItemStyle, + selectedDragItemStyle, +} from './pickDraggableRecord.css'; +import { PickRecord } from './PickRecord'; +import { PickViewDraggableItemComponentProps } from '@/types'; + +export function PickDraggableRecord({ + pickInfo, +}: PickViewDraggableItemComponentProps) { + const { + selectedPickIdList, + selectSinglePick, + getOrderedPickIdListByFolderId, + focusPickId, + setSelectedPickIdList, + isDragging, + } = usePickStore(); + const { setCurrentPickIdToNull } = useUpdatePickStore(); + const { id: pickId, parentFolderId } = pickInfo; + const isSelected = selectedPickIdList.includes(pickId); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging: isActiveDragging, + } = useSortable({ + id: pickId, + data: { + id: pickId, + type: 'pick', + parentFolderId: parentFolderId, + }, + }); + const pickElementId = `pickId-${pickId}`; + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: 1, + }; + + const handleShiftSelect = (parentFolderId: number, pickId: number) => { + if (!focusPickId) { + return; + } + + // 새로운 선택된 배열 만들기. + const orderedPickIdList = getOrderedPickIdListByFolderId(parentFolderId); + + const newSelectedPickIdList = getSelectedPickRange({ + orderedPickIdList, + startPickId: focusPickId, + endPickId: pickId, + }); + + setSelectedPickIdList(newSelectedPickIdList); + }; + + const handleClick = ( + pickId: number, + event: MouseEvent + ) => { + if (event.shiftKey && isSelectionActive(selectedPickIdList.length)) { + event.preventDefault(); + handleShiftSelect(parentFolderId, pickId); + return; + } + + setCurrentPickIdToNull(); + selectSinglePick(pickId); + }; + + /** + * @description multi-select에 포함이 됐으나 dragging target이 아닐때. + */ + if (isDragging && isSelected && !isActiveDragging) { + return null; + } + + return ( +
+
handleClick(pickId, event)} + id={pickElementId} + > + +
+
+ ); +} diff --git a/frontend/techpick/src/components/PickRecord/PickImageColumnLayout.tsx b/frontend/techpick/src/components/PickRecord/PickImageColumnLayout.tsx new file mode 100644 index 00000000..938176ef --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickImageColumnLayout.tsx @@ -0,0 +1,11 @@ +import type { PropsWithChildren } from 'react'; +import { Gap } from '../Gap'; +import { pickImageColumnLayoutStyle } from './pickImageColumnLayout.css'; + +export function PickImageColumnLayout({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/techpick/src/components/PickRecord/PickRecord.tsx b/frontend/techpick/src/components/PickRecord/PickRecord.tsx new file mode 100644 index 00000000..497b16dc --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickRecord.tsx @@ -0,0 +1,120 @@ +'use client'; + +import Image from 'next/image'; +import { useOpenUrlInNewTab } from '@/hooks'; +import { usePickStore, useTagStore, useUpdatePickStore } from '@/stores'; +import { formatDateString } from '@/utils'; +import { PickDateColumnLayout } from './PickDateColumnLayout'; +import { PickImageColumnLayout } from './PickImageColumnLayout'; +import { + pickRecordLayoutStyle, + pickImageStyle, + pickEmptyImageStyle, + pickTitleSectionStyle, + dateTextStyle, +} from './pickRecord.css'; +import { PickRecordTitleInput } from './PickRecordTitleInput'; +import { PickTagColumnLayout } from './PickTagColumnLayout'; +import { PickTitleColumnLayout } from './PickTitleColumnLayout'; +import { Separator } from './Separator'; +import { TagPicker } from '../TagPicker'; +import { PickViewItemComponentProps, TagType } from '@/types'; + +export function PickRecord({ pickInfo }: PickViewItemComponentProps) { + const pick = pickInfo; + const link = pickInfo.linkInfo; + const { findTagById } = useTagStore(); + const { updatePickInfo } = usePickStore(); + const { openUrlInNewTab } = useOpenUrlInNewTab(link.url); + const { + currentUpdatePickId, + setCurrentPickIdToNull, + setCurrentUpdatePickId, + } = useUpdatePickStore(); + const isUpdateTitle = currentUpdatePickId === pickInfo.id; + + const filteredSelectedTagList: TagType[] = []; + + pickInfo.tagIdOrderedList.forEach((tagId) => { + const tagInfo = findTagById(tagId); + if (tagInfo) { + filteredSelectedTagList.push(tagInfo); + } + }); + + return ( +
+ +
+ {link.imageUrl ? ( + + ) : ( +
+ )} +
+ + + + + + {isUpdateTitle ? ( + { + updatePickInfo(pick.parentFolderId, { + ...pickInfo, + title: newTitle, + }); + setCurrentPickIdToNull(); + }} + onClickOutSide={() => { + setCurrentPickIdToNull(); + }} + /> + ) : ( +
{ + setCurrentUpdatePickId(pickInfo.id); + event.stopPropagation(); + }} + role="button" + > + {pick.title} +
+ )} +
+ + + + + + + + + + +
{formatDateString(pick.updatedAt)}
+
+ +
+ link +
+
+ ); +} diff --git a/frontend/techpick/src/components/PickRecord/PickRecordHeader.tsx b/frontend/techpick/src/components/PickRecord/PickRecordHeader.tsx new file mode 100644 index 00000000..2dcd13ee --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickRecordHeader.tsx @@ -0,0 +1,33 @@ +import { PickDateColumnLayout } from './PickDateColumnLayout'; +import { PickImageColumnLayout } from './PickImageColumnLayout'; +import { pickRecordHeaderLayoutStyle } from './pickRecordHeader.css'; +import { PickTagColumnLayout } from './PickTagColumnLayout'; +import { PickTitleColumnLayout } from './PickTitleColumnLayout'; +import { Separator } from './Separator'; + +export function PickRecordHeader() { + return ( +
+ +
Image
+
+ + + + +
Title
+
+ + + +
Tags
+
+ + + + +
date
+
+
+ ); +} diff --git a/frontend/techpick/src/components/PickRecord/PickRecordTitleInput.tsx b/frontend/techpick/src/components/PickRecord/PickRecordTitleInput.tsx new file mode 100644 index 00000000..c1c2c583 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickRecordTitleInput.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useCallback, useEffect, useRef } from 'react'; +import type { KeyboardEvent } from 'react'; +import { isEmptyString } from '@/utils'; +import { pickTitleInputStyle } from './pickRecordTitleInput.css'; + +export function PickRecordTitleInput({ + onSubmit, + onClickOutSide = () => {}, + initialValue = '', +}: PickRecordTitleInputProps) { + const containerRef = useRef(null); + const inputRef = useRef(null); + + const submitIfNotEmptyString = useCallback(() => { + const pickTitle = inputRef.current?.value.trim() ?? ''; + if (isEmptyString(pickTitle)) return; + + onSubmit(pickTitle); + }, [onSubmit]); + + const onEnter = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + submitIfNotEmptyString(); + } + }; + + useEffect( + function detectOutsideClick() { + const handleClickOutside = (event: MouseEvent) => { + console.log('handleClickOutside work!'); + + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + submitIfNotEmptyString(); + onClickOutSide(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, + [onClickOutSide, submitIfNotEmptyString] + ); + + useEffect( + function initializeFolderInput() { + if (inputRef.current) { + inputRef.current.value = initialValue; + + // 타이밍 이슈 탓으로 인해 setTimeout 사용 + setTimeout(() => inputRef.current?.focus(), 0); + } + }, + [initialValue] + ); + + return ( +
e.stopPropagation()}> + +
+ ); +} + +interface PickRecordTitleInputProps { + onSubmit: (value: string) => void; + onClickOutSide?: () => void; + initialValue?: string; +} diff --git a/frontend/techpick/src/components/PickRecord/PickTagColumnLayout.tsx b/frontend/techpick/src/components/PickRecord/PickTagColumnLayout.tsx new file mode 100644 index 00000000..8d9be73c --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickTagColumnLayout.tsx @@ -0,0 +1,11 @@ +import type { PropsWithChildren } from 'react'; +import { Gap } from '../Gap'; +import { pickTagColumnLayoutStyle } from './pickTagColumnLayout.css'; + +export function PickTagColumnLayout({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/techpick/src/components/PickRecord/PickTitleColumnLayout.tsx b/frontend/techpick/src/components/PickRecord/PickTitleColumnLayout.tsx new file mode 100644 index 00000000..ba9f6526 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/PickTitleColumnLayout.tsx @@ -0,0 +1,11 @@ +import type { PropsWithChildren } from 'react'; +import { Gap } from '../Gap'; +import { pickTitleColumnLayoutStyle } from './pickTitleColumnLayout.css'; + +export function PickTitleColumnLayout({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/techpick/src/components/PickRecord/Separator.tsx b/frontend/techpick/src/components/PickRecord/Separator.tsx new file mode 100644 index 00000000..be24306b --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/Separator.tsx @@ -0,0 +1,12 @@ +import * as RadixSeparator from '@radix-ui/react-separator'; +import { separatorStyle } from './separator.css'; + +export function Separator() { + return ( + + ); +} diff --git a/frontend/techpick/src/components/PickRecord/index.ts b/frontend/techpick/src/components/PickRecord/index.ts new file mode 100644 index 00000000..f5bb53e1 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/index.ts @@ -0,0 +1,2 @@ +export { PickRecordHeader } from './PickRecordHeader'; +export { PickRecord } from './PickRecord'; diff --git a/frontend/techpick/src/components/PickRecord/pickDateColumnLayout.css.ts b/frontend/techpick/src/components/PickRecord/pickDateColumnLayout.css.ts new file mode 100644 index 00000000..ee6c9fe9 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickDateColumnLayout.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; + +export const pickDateColumnLayoutStyle = style({ + width: 'fit-content', + height: '100%', +}); diff --git a/frontend/techpick/src/components/PickRecord/pickDraggableRecord.css.ts b/frontend/techpick/src/components/PickRecord/pickDraggableRecord.css.ts new file mode 100644 index 00000000..f62a0448 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickDraggableRecord.css.ts @@ -0,0 +1,11 @@ +import { style } from '@vanilla-extract/css'; +import { colorVars } from 'techpick-shared'; + +export const selectedDragItemStyle = style({ + backgroundColor: colorVars.primary, + userSelect: 'none', +}); + +export const isActiveDraggingItemStyle = style({ + opacity: 0, +}); diff --git a/frontend/techpick/src/components/PickRecord/pickImageColumnLayout.css.ts b/frontend/techpick/src/components/PickRecord/pickImageColumnLayout.css.ts new file mode 100644 index 00000000..77a452ba --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickImageColumnLayout.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; + +export const pickImageColumnLayoutStyle = style({ + width: '56px', + height: '100%', +}); diff --git a/frontend/techpick/src/components/PickRecord/pickRecord.css.ts b/frontend/techpick/src/components/PickRecord/pickRecord.css.ts new file mode 100644 index 00000000..e31f06aa --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickRecord.css.ts @@ -0,0 +1,41 @@ +import { style } from '@vanilla-extract/css'; +import { colorVars, typography } from 'techpick-shared'; + +export const pickRecordLayoutStyle = style({ + position: 'relative', + display: 'flex', + width: 'fit-content', + minHeight: '60px', + height: 'fit-content', + alignItems: 'center', +}); + +export const pickImageStyle = style({ + position: 'relative', + width: '48px', + height: '48px', + objectFit: 'cover', + borderRadius: '2px', +}); + +export const pickEmptyImageStyle = style({ + border: '1px solid black', +}); + +export const pickTitleSectionStyle = style({ + fontSize: typography.fontSize['lg'], + fontWeight: typography.fontWeights['light'], + height: '32px', + lineHeight: '32px', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + cursor: 'pointer', +}); + +export const dateTextStyle = style({ + fontSize: typography.fontSize['sm'], + fontWeight: typography.fontWeights['normal'], + color: colorVars.gray11, + whiteSpace: 'nowrap', +}); diff --git a/frontend/techpick/src/components/PickRecord/pickRecordHeader.css.ts b/frontend/techpick/src/components/PickRecord/pickRecordHeader.css.ts new file mode 100644 index 00000000..9714114b --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickRecordHeader.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; + +export const pickRecordHeaderLayoutStyle = style({ + display: 'flex', + height: '20px', +}); diff --git a/frontend/techpick/src/components/PickRecord/pickRecordTitleInput.css.ts b/frontend/techpick/src/components/PickRecord/pickRecordTitleInput.css.ts new file mode 100644 index 00000000..cd50d162 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickRecordTitleInput.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css'; +import { typography } from 'techpick-shared'; + +export const pickTitleInputStyle = style({ + fontSize: typography.fontSize['lg'], + fontWeight: typography.fontWeights['light'], + height: '32px', + margin: 0, + width: '100%', +}); diff --git a/frontend/techpick/src/components/PickRecord/pickTagColumnLayout.css.ts b/frontend/techpick/src/components/PickRecord/pickTagColumnLayout.css.ts new file mode 100644 index 00000000..ceaf0602 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickTagColumnLayout.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; + +export const pickTagColumnLayoutStyle = style({ + width: '320px', + height: 'fit-content', +}); diff --git a/frontend/techpick/src/components/PickRecord/pickTitleColumnLayout.css.ts b/frontend/techpick/src/components/PickRecord/pickTitleColumnLayout.css.ts new file mode 100644 index 00000000..0b554d53 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/pickTitleColumnLayout.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css'; + +export const pickTitleColumnLayoutStyle = style({ + width: '528px', +}); diff --git a/frontend/techpick/src/components/PickRecord/separator.css.ts b/frontend/techpick/src/components/PickRecord/separator.css.ts new file mode 100644 index 00000000..be2b4de4 --- /dev/null +++ b/frontend/techpick/src/components/PickRecord/separator.css.ts @@ -0,0 +1,11 @@ +import { style } from '@vanilla-extract/css'; + +export const separatorStyle = style({ + minHeight: '1px', + maxHeight: '100%', + width: '1px', + backgroundColor: 'black', + flexShrink: 0, + flexGrow: 0, + alignSelf: 'stretch', +}); diff --git a/frontend/techpick/src/components/SearchWidget/SearchInput.css b/frontend/techpick/src/components/SearchWidget/SearchInput.css index 8ad53057..1834d787 100644 --- a/frontend/techpick/src/components/SearchWidget/SearchInput.css +++ b/frontend/techpick/src/components/SearchWidget/SearchInput.css @@ -40,6 +40,10 @@ box-shadow: none; } +/** +* @todo: 현재 SearchWidget이 사용되는 페이지에 전체적으로 적용되고 있음. +* 클래스로 바꿔야함. +*/ input { margin: 8px; font-size: 24px; @@ -79,28 +83,41 @@ input { background-color: #e7e7e7; } -.token-input-container .token-input-token-list .token-input-token.token-input-token--error { +.token-input-container + .token-input-token-list + .token-input-token.token-input-token--error { color: #db3d44; background-color: #f9b5b5; } -.token-input-container .token-input-token-list .token-input-token.token-input-token--error:hover { +.token-input-container + .token-input-token-list + .token-input-token.token-input-token--error:hover { background-color: #ffdada; } -.token-input-container .token-input-token-list .token-input-token.token-input-token--read-only:hover { +.token-input-container + .token-input-token-list + .token-input-token.token-input-token--read-only:hover { background-color: #a9a9a9; } -.token-input-container .token-input-token-list .token-input-token.token-input-token--read-only.token-input-token--error:hover { +.token-input-container + .token-input-token-list + .token-input-token.token-input-token--read-only.token-input-token--error:hover { background-color: #f9b5b5; } -.token-input-container .token-input-token-list .token-input-token.token-input-token--editable:hover { +.token-input-container + .token-input-token-list + .token-input-token.token-input-token--editable:hover { cursor: pointer; } -.token-input-container .token-input-token-list .token-input-token.token-input-token--active .token-input-autosized-wrapper { +.token-input-container + .token-input-token-list + .token-input-token.token-input-token--active + .token-input-autosized-wrapper { display: flex; align-content: center; justify-content: center; @@ -109,19 +126,29 @@ input { margin: 4px 8px; } -.token-input-container .token-input-token-list .token-input-token.token-input-token--active .token-input-autosized-wrapper input { +.token-input-container + .token-input-token-list + .token-input-token.token-input-token--active + .token-input-autosized-wrapper + input { height: auto; border-bottom: 1px solid #aaa; } -.token-input-container .token-input-token-list .token-input-token .token-input-token__label-wrapper { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__label-wrapper { flex: 1 0 0; margin: 0 8px; overflow: hidden; text-overflow: ellipsis; } -.token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button { display: flex; flex: 0 0 0; align-content: center; @@ -137,14 +164,27 @@ input { } /* token */ -.token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button .token-input-delete-button__close-icon { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button + .token-input-delete-button__close-icon { position: relative; width: 14px; height: 14px; } /* delete btn */ -.token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button .token-input-delete-button__close-icon::before, .token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button .token-input-delete-button__close-icon::after { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button + .token-input-delete-button__close-icon::before, +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button + .token-input-delete-button__close-icon::after { position: absolute; left: 6px; height: 14px; @@ -152,20 +192,39 @@ input { border-left: 2px solid #222; } -.token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button .token-input-delete-button__close-icon::before { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button + .token-input-delete-button__close-icon::before { transform: rotate(-45deg); } -.token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button .token-input-delete-button__close-icon::after { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button + .token-input-delete-button__close-icon::after { transform: rotate(45deg); } -.token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button:hover { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button:hover { background-color: #aaa; opacity: 1; } -.token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button:hover .token-input-delete-button__close-icon::before, .token-input-container .token-input-token-list .token-input-token .token-input-token__delete-button:hover .token-input-delete-button__close-icon::after { +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button:hover + .token-input-delete-button__close-icon::before, +.token-input-container + .token-input-token-list + .token-input-token + .token-input-token__delete-button:hover + .token-input-delete-button__close-icon::after { border-color: #fff; } - diff --git a/frontend/techpick/src/components/SelectedTagListLayout/SelectedTagListLayout.css.ts b/frontend/techpick/src/components/SelectedTagListLayout/SelectedTagListLayout.css.ts index aedba435..111c3156 100644 --- a/frontend/techpick/src/components/SelectedTagListLayout/SelectedTagListLayout.css.ts +++ b/frontend/techpick/src/components/SelectedTagListLayout/SelectedTagListLayout.css.ts @@ -1,11 +1,11 @@ import { style, styleVariants } from '@vanilla-extract/css'; -import { colorVars, space } from 'techpick-shared'; +import { colorVars } from 'techpick-shared'; export const ListLayoutHeightVariants = styleVariants({ fixed: { overflow: 'hidden', minHeight: '30px', - maxHeight: '60px', + maxHeight: '58px', }, flexible: { overflow: 'visible', @@ -19,8 +19,6 @@ export type ListLayoutHeightVariantKeyTypes = export const SelectedTagListLayoutFocusStyleVariant = styleVariants({ focus: { border: `1px solid ${colorVars.color.inputBorderFocus}`, - borderTopLeftRadius: '4px', - borderTopRightRadius: '4px', }, none: {}, }); @@ -30,7 +28,9 @@ export type SelectedTagListLayoutFocusStyleVarianKeyTypes = export const SelectedTagListLayoutStyle = style({ display: 'flex', - alignItems: 'center', - gap: space['4'], + gap: '4px', flexWrap: 'wrap', + padding: '4px', + width: '288px', + overflowY: 'scroll', }); diff --git a/frontend/techpick/src/components/DeleteTagDialog/DeleteTagDialog.css.ts b/frontend/techpick/src/components/TagPicker/DeleteTagDialog.css.ts similarity index 100% rename from frontend/techpick/src/components/DeleteTagDialog/DeleteTagDialog.css.ts rename to frontend/techpick/src/components/TagPicker/DeleteTagDialog.css.ts diff --git a/frontend/techpick/src/components/DeleteTagDialog/index.tsx b/frontend/techpick/src/components/TagPicker/DeleteTagDialog.tsx similarity index 79% rename from frontend/techpick/src/components/DeleteTagDialog/index.tsx rename to frontend/techpick/src/components/TagPicker/DeleteTagDialog.tsx index ddfa7006..d964fc31 100644 --- a/frontend/techpick/src/components/DeleteTagDialog/index.tsx +++ b/frontend/techpick/src/components/TagPicker/DeleteTagDialog.tsx @@ -3,18 +3,19 @@ import { useRef, memo, KeyboardEvent, MouseEvent } from 'react'; import * as Dialog from '@radix-ui/react-dialog'; import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; -import { useQueryClient } from '@tanstack/react-query'; -import { Text, Button, Gap } from '@/components'; -import { useDeleteTagDialogStore } from '@/stores/deleteTagDialogStore'; -import { useTagStore } from '@/stores/tagStore'; -import { notifyError } from '@/utils'; +import { PORTAL_CONTAINER_ID } from '@/constants'; +import { useTagStore, useDeleteTagDialogStore } from '@/stores'; +import { getElementById } from '@/utils'; +import { Button } from '../Button'; +import { Gap } from '../Gap'; +import { Text } from '../Text'; import { dialogContentStyle, dialogOverlayStyle } from './DeleteTagDialog.css'; export const DeleteTagDialog = memo(function DeleteTagDialog() { const cancelButtonRef = useRef(null); const { deleteTag } = useTagStore(); const { deleteTagId, isOpen, setIsOpen } = useDeleteTagDialogStore(); - const queryClient = useQueryClient(); + const portalContainer = getElementById(PORTAL_CONTAINER_ID); const closeDialog = () => { setIsOpen(false); @@ -33,20 +34,13 @@ export const DeleteTagDialog = memo(function DeleteTagDialog() { return; } - try { - closeDialog(); - await deleteTag(deleteTagId); - } catch (error) { - if (error instanceof Error) { - notifyError(error.message); - } - } + closeDialog(); + await deleteTag(deleteTagId); }; const DeleteTagByClick = async (e: MouseEvent) => { e.stopPropagation(); await handleDeleteTag(); - queryClient.invalidateQueries({ queryKey: ['pick'] }); }; const DeleteTagByEnterKey = async (e: KeyboardEvent) => { @@ -59,7 +53,7 @@ export const DeleteTagDialog = memo(function DeleteTagDialog() { return ( - + 이 태그를 삭제하시겠습니까? diff --git a/frontend/techpick/src/components/DeselectTagButton/DeselectTagButton.css.ts b/frontend/techpick/src/components/TagPicker/DeselectTagButton.css.ts similarity index 78% rename from frontend/techpick/src/components/DeselectTagButton/DeselectTagButton.css.ts rename to frontend/techpick/src/components/TagPicker/DeselectTagButton.css.ts index afc87e90..4a640133 100644 --- a/frontend/techpick/src/components/DeselectTagButton/DeselectTagButton.css.ts +++ b/frontend/techpick/src/components/TagPicker/DeselectTagButton.css.ts @@ -2,6 +2,9 @@ import { style } from '@vanilla-extract/css'; import { colorVars } from 'techpick-shared'; export const DeselectTagButtonStyle = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', width: '20px', height: '20px', backgroundColor: 'transparent', diff --git a/frontend/techpick/src/components/TagPicker/DeselectTagButton.tsx b/frontend/techpick/src/components/TagPicker/DeselectTagButton.tsx new file mode 100644 index 00000000..2939c838 --- /dev/null +++ b/frontend/techpick/src/components/TagPicker/DeselectTagButton.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { X } from 'lucide-react'; +import { usePickStore } from '@/stores'; +import { DeselectTagButtonStyle } from './DeselectTagButton.css'; +import { PickInfoType, TagType } from '@/types'; + +export function DeselectTagButton({ + tag, + pickInfo, + selectedTagList, + onClick = () => {}, +}: DeselectTagButtonProps) { + const tagIdOrderedList = selectedTagList.map((tag) => tag.id); + const { updatePickInfo } = usePickStore(); + + return ( + + ); +} + +interface DeselectTagButtonProps { + tag: TagType; + pickInfo: PickInfoType; + selectedTagList: TagType[]; + onClick?: () => void; +} diff --git a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/PopoverOverlay.tsx b/frontend/techpick/src/components/TagPicker/PopoverOverlay.tsx similarity index 100% rename from frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/PopoverOverlay.tsx rename to frontend/techpick/src/components/TagPicker/PopoverOverlay.tsx diff --git a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/PopoverTriggerButton.css.ts b/frontend/techpick/src/components/TagPicker/PopoverTriggerButton.css.ts similarity index 100% rename from frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/PopoverTriggerButton.css.ts rename to frontend/techpick/src/components/TagPicker/PopoverTriggerButton.css.ts diff --git a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/PopoverTriggerButton.tsx b/frontend/techpick/src/components/TagPicker/PopoverTriggerButton.tsx similarity index 100% rename from frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/PopoverTriggerButton.tsx rename to frontend/techpick/src/components/TagPicker/PopoverTriggerButton.tsx diff --git a/frontend/techpick/src/components/TagPicker/ShowDeleteTagDialogButton.tsx b/frontend/techpick/src/components/TagPicker/ShowDeleteTagDialogButton.tsx new file mode 100644 index 00000000..ad3995af --- /dev/null +++ b/frontend/techpick/src/components/TagPicker/ShowDeleteTagDialogButton.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { useDeleteTagDialogStore } from '@/stores'; +import { Button } from '../Button'; +import type { TagType } from '@/types'; + +export function ShowDeleteTagDialogButton({ + tag, + onClick: parentOnClick = () => {}, +}: ShowDeleteTagDialogButtonProps) { + const { setIsOpen, setDeleteTagId } = useDeleteTagDialogStore(); + + const showDeleteTagDialog = () => { + setIsOpen(true); + setDeleteTagId(tag.id); + parentOnClick(); + }; + + return ( + + ); +} + +interface ShowDeleteTagDialogButtonProps { + tag: TagType; + onClick?: () => void; +} diff --git a/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/TagAutocompleteDialog.css.ts b/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.css.ts similarity index 65% rename from frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/TagAutocompleteDialog.css.ts rename to frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.css.ts index fecc06e8..af9bf879 100644 --- a/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/TagAutocompleteDialog.css.ts +++ b/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.css.ts @@ -1,12 +1,14 @@ import { style } from '@vanilla-extract/css'; -// import { SelectedTagCommonStyle } from '@/entities/tag'; -import { colorVars } from 'techpick-shared'; +import { colorVars, fontSize } from 'techpick-shared'; + +const { color } = colorVars; export const tagDialogPortalLayout = style({ position: 'absolute', top: '0', zIndex: '1', - backgroundColor: colorVars.color.inputBackground, + backgroundColor: colorVars.lightGray, + boxShadow: '4px 4px 0px 0px rgba(0, 0, 0, 0.2)', }); export const commandInputStyle = style({ @@ -17,15 +19,15 @@ export const commandInputStyle = style({ outline: 'none', border: 'none', padding: '0 4px', - color: colorVars.color.font, + fontSize: fontSize['sm'], + color: color.font, + margin: 0, }); export const tagListStyle = style({ - border: `1px solid ${colorVars.color.tagBorder}`, - borderRadius: '4px', - padding: '4px 0', - boxShadow: - 'rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, rgba(15, 15, 15, 0.2) 0px 3px 6px, rgba(15, 15, 15, 0.4) 0px 9px 24px', + maxHeight: '90px', + border: `1px solid black`, + borderTop: 'none', overflowY: 'auto', '::-webkit-scrollbar': { display: 'none', @@ -43,14 +45,13 @@ export const tagListItemStyle = style({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', - borderRadius: '4px', backgroundColor: 'transparent', padding: '4px', // 선택된 상태일 때 selectors: { '&[data-selected="true"]': { - backgroundColor: colorVars.color.tagSelectedBackground, + backgroundColor: colorVars.softPoint, }, '&[data-disabled="true"]': { display: 'none', @@ -59,7 +60,7 @@ export const tagListItemStyle = style({ }); export const tagListItemContentStyle = style({ - maxWidth: `calc('264px' - 34px)`, // 26px은 생성 텍스트의 영역 8px는 패딩 + maxWidth: `calc(288px - 38px)`, // 26px은 생성 텍스트의 영역 12px는 패딩 height: '20px', lineHeight: '20px', borderRadius: '4px', @@ -68,11 +69,11 @@ export const tagListItemContentStyle = style({ whiteSpace: 'nowrap', // 줄 바꿈 방지 overflow: 'hidden', // 넘치는 내용 숨김 textOverflow: 'ellipsis', // 생략 부호 추가 - color: colorVars.color.font, + color: color.font, }); export const tagCreateTextStyle = style({ - width: '26px', + width: '28px', fontSize: '14px', - color: colorVars.color.font, + color: color.font, }); diff --git a/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/TagAutocompleteDialog.lib.ts b/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.lib.ts similarity index 100% rename from frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/TagAutocompleteDialog.lib.ts rename to frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.lib.ts diff --git a/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/index.tsx b/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.tsx similarity index 62% rename from frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/index.tsx rename to frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.tsx index c30f2f5d..a4c07fe3 100644 --- a/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/index.tsx +++ b/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog.tsx @@ -1,20 +1,14 @@ 'use client'; -import { Dispatch, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Command } from 'cmdk'; import { BarLoader } from 'react-spinners'; import { colorVars } from 'techpick-shared'; -import { useUpdatePickMutation, useGetPickQuery } from '@/apis/pick'; -import { - SelectedTagItem, - SelectedTagListLayout, - DeleteTagDialog, - DeselectTagButton, -} from '@/components'; -import { useTagStore } from '@/stores/tagStore'; -import { useThemeStore } from '@/stores/themeStore'; +import { useThemeStore, useTagStore, usePickStore } from '@/stores'; import { notifyError, numberToRandomColor } from '@/utils'; -import { TagInfoEditPopoverButton } from '../TagInfoEditPopoverButton'; +import { DeleteTagDialog } from './DeleteTagDialog'; +import { DeselectTagButton } from './DeselectTagButton'; +import { SelectedTagItem } from '../SelectedTagItem'; import { tagDialogPortalLayout, commandInputStyle, @@ -29,103 +23,76 @@ import { CREATABLE_TAG_KEYWORD, getRandomInt, } from './TagAutocompleteDialog.lib'; -import { useCalculateCommandListHeight } from './useCalculateCommandListHeight'; -import type { TagType } from '@/types'; +import { TagInfoEditPopoverButton } from './TagInfoEditPopoverButton'; +import { SelectedTagListLayout } from '../SelectedTagListLayout/SelectedTagListLayout'; +import { PickInfoType, TagType } from '@/types'; export function TagAutocompleteDialog({ open, onOpenChange, container, - pickId, + pickInfo, selectedTagList, - setSelectedTagList, }: TagSelectionDialogProps) { const [tagInputValue, setTagInputValue] = useState(''); const [canCreateTag, setCanCreateTag] = useState(false); const tagInputRef = useRef(null); const selectedTagListRef = useRef(null); + const isCreateFetchPendingRef = useRef(false); const randomNumber = useRef(getRandomInt()); - const { tagList, fetchingTagState, fetchingTagList, createTag } = - useTagStore(); - const { commandListHeight } = - useCalculateCommandListHeight(selectedTagListRef); + const tagIdOrderedList = selectedTagList.map((tag) => tag.id); + + const { tagList, fetchingTagState, createTag } = useTagStore(); + const { updatePickInfo } = usePickStore(); const { isDarkMode } = useThemeStore(); - const { data: pickData } = useGetPickQuery(pickId); - const { mutate: updatePickInfo } = useUpdatePickMutation(pickId); const focusTagInput = () => { tagInputRef.current?.focus(); + tagInputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; const clearTagInputValue = () => { setTagInputValue(''); }; - const selectTag = (tag: TagType) => { - const index = selectedTagList.findIndex( - (selectedTag) => selectedTag.id === tag.id - ); - - if (index !== -1) { + const onSelectTag = (tag: TagType) => { + if (tagIdOrderedList.includes(tag.id)) { return; } - setSelectedTagList([...selectedTagList, tag]); - }; + const newTagIdOrderedList = [...tagIdOrderedList, tag.id]; - const deselectTag = (tag: TagType) => { - const filteredSelectedTagList = selectedTagList.filter( - (selectedTag) => selectedTag.id !== tag.id - ); - - setSelectedTagList([...filteredSelectedTagList]); - }; - - const onSelectTag = (tag: TagType) => { - selectTag(tag); focusTagInput(); clearTagInputValue(); + updatePickInfo(pickInfo.parentFolderId, { + id: pickInfo.id, + tagIdOrderedList: newTagIdOrderedList, + }); }; const onSelectCreatableTag = async () => { + if (isCreateFetchPendingRef.current) { + return; + } + try { + isCreateFetchPendingRef.current = true; + const newTag = await createTag({ name: tagInputValue, colorNumber: randomNumber.current, }); randomNumber.current = getRandomInt(); onSelectTag(newTag!); - - if (!pickData || !newTag) { - return; - } - - const { title, id } = pickData; - - const previousTagIdList = selectedTagList.map( - (selectedTag) => selectedTag.id - ); - - updatePickInfo({ - title, - - id, - tagIdOrderedList: [...previousTagIdList, newTag.id], - }); } catch (error) { if (error instanceof Error) { notifyError(error.message); } + } finally { + isCreateFetchPendingRef.current = false; } }; - useEffect( - function fetchTagList() { - fetchingTagList(); - }, - [fetchingTagList] - ); - useEffect( function checkIsCreatableTag() { const isUnique = !tagList.some((tag) => tag.name === tagInputValue); @@ -140,30 +107,7 @@ export function TagAutocompleteDialog({ return ( { - console.log('Command.Dialog click'); - e.stopPropagation(); - e.preventDefault(); - }} - onOpenChange={async (open) => { - onOpenChange(open); - - if (!open && pickData) { - const { title, id } = pickData; - - updatePickInfo({ - title, - - id, - tagIdOrderedList: selectedTagList.map( - (selectedTag) => selectedTag.id - ), - }); - } - - // updatePickInfo() - // 비동기 api 요청을 보내자! - }} + onOpenChange={onOpenChange} container={container?.current ?? undefined} className={tagDialogPortalLayout} filter={filterCommandItems} @@ -173,10 +117,10 @@ export function TagAutocompleteDialog({ {selectedTagList.map((tag) => ( { - focusTagInput(); - deselectTag(tag); - }} + tag={tag} + onClick={focusTagInput} + pickInfo={pickInfo} + selectedTagList={selectedTagList} /> ))} @@ -190,10 +134,7 @@ export function TagAutocompleteDialog({ {/**전체 태그 리스트 */} - + {fetchingTagState.isPending && ( @@ -214,11 +155,7 @@ export function TagAutocompleteDialog({ keywords={[tag.name]} > - + ))} @@ -256,7 +193,6 @@ interface TagSelectionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; container?: React.RefObject; - pickId: number; + pickInfo: PickInfoType; selectedTagList: TagType[]; - setSelectedTagList: Dispatch>; } diff --git a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/TagInfoEditPopoverButton.css.ts b/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton.css.ts similarity index 88% rename from frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/TagInfoEditPopoverButton.css.ts rename to frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton.css.ts index 4245884f..53ec1282 100644 --- a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/TagInfoEditPopoverButton.css.ts +++ b/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton.css.ts @@ -1,5 +1,5 @@ import { style } from '@vanilla-extract/css'; -import { colorVars } from 'techpick-shared'; +import { colorVars, fontSize } from 'techpick-shared'; export const tagInfoEditFormLayout = style({ position: 'relative', @@ -16,8 +16,10 @@ export const tagInfoEditFormLayout = style({ export const tagInputStyle = style({ outline: 'none', + margin: 0, border: `1px solid ${colorVars.color.font}`, color: colorVars.color.font, + fontSize: fontSize['md'], }); export const popoverOverlayStyle = style({ diff --git a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/index.tsx b/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton.tsx similarity index 70% rename from frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/index.tsx rename to frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton.tsx index 944f095e..138d173d 100644 --- a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/index.tsx +++ b/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton.tsx @@ -1,31 +1,26 @@ 'use client'; -import { Dispatch, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { useFloating, shift } from '@floating-ui/react'; import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; -import { useQueryClient } from '@tanstack/react-query'; import DOMPurify from 'dompurify'; -import { ShowDeleteTagDialogButton } from '@/components'; -import { useTagStore } from '@/stores/tagStore'; -import { notifyError } from '@/utils'; +import { useTagStore } from '@/stores'; +import { notifyError, isEmptyString, isShallowEqualValue } from '@/utils'; import { PopoverOverlay } from './PopoverOverlay'; import { PopoverTriggerButton } from './PopoverTriggerButton'; +import { ShowDeleteTagDialogButton } from './ShowDeleteTagDialogButton'; import { tagInfoEditFormLayout, tagInputStyle, } from './TagInfoEditPopoverButton.css'; -import { isEmptyString, isSameValue } from './TagInfoEditPopoverButton.lib'; -import { TagType } from '@/types'; +import type { TagType } from '@/types'; export function TagInfoEditPopoverButton({ tag, - selectedTagList, - setSelectedTagList, }: TagInfoEditPopoverButtonProps) { const tagNameInputRef = useRef(null); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const updateTag = useTagStore((state) => state.updateTag); - const queryClient = useQueryClient(); const { refs, floatingStyles } = useFloating({ open: isPopoverOpen, @@ -57,34 +52,20 @@ export function TagInfoEditPopoverButton({ const newTagName = DOMPurify.sanitize(tagNameInputRef.current.value.trim()); - if (isEmptyString(newTagName) || isSameValue(newTagName, tag.name)) { + if ( + isEmptyString(newTagName) || + isShallowEqualValue(newTagName, tag.name) + ) { closePopover(); return; } - const index = selectedTagList.findIndex( - (selectedTag) => selectedTag.id === tag.id - ); - - if (index !== -1) { - const tempSelectedTagList = [...selectedTagList]; - tempSelectedTagList[index] = { - id: tag.id, - name: newTagName, - colorNumber: tag.colorNumber, - }; - setSelectedTagList(tempSelectedTagList); - } - try { await updateTag({ id: tag.id, name: newTagName, colorNumber: tag.colorNumber, }); - queryClient.invalidateQueries({ - queryKey: ['pick'], - }); closePopover(); } catch (error) { if (error instanceof Error) { @@ -94,12 +75,11 @@ export function TagInfoEditPopoverButton({ }; return ( -
+ <> { e.stopPropagation(); // 옵션 버튼을 눌렀을 때, 해당 태그를 선택하는 onSelect를 막기 위헤서 전파 방지 - e.preventDefault(); setIsPopoverOpen(true); }} /> @@ -109,7 +89,6 @@ export function TagInfoEditPopoverButton({ onClick={(e) => { closePopover(); e.stopPropagation(); - e.preventDefault(); }} />
{ - e.stopPropagation(); - e.preventDefault(); - }} + onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > )} -
+ ); } interface TagInfoEditPopoverButtonProps { tag: TagType; - selectedTagList: TagType[]; - setSelectedTagList: Dispatch>; } diff --git a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/TagInfoEditPopoverButton.lib.ts b/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/TagInfoEditPopoverButton.lib.ts deleted file mode 100644 index 0c420b0f..00000000 --- a/frontend/techpick/src/components/TagPicker/TagInfoEditPopoverButton/TagInfoEditPopoverButton.lib.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const isEmptyString = (value: string) => { - return value === ''; -}; - -export const isSameValue = (value1: T, value2: T) => { - return value1 === value2; -}; diff --git a/frontend/techpick/src/components/TagPicker/TagPicker.css.ts b/frontend/techpick/src/components/TagPicker/TagPicker.css.ts index ea475069..1d7a3b91 100644 --- a/frontend/techpick/src/components/TagPicker/TagPicker.css.ts +++ b/frontend/techpick/src/components/TagPicker/TagPicker.css.ts @@ -7,21 +7,30 @@ export const tagPickerLayout = style({ position: 'relative', }); +export const tagPickerPlaceholderStyle = style({ + position: 'absolute', + top: '0', + left: '0', + width: '100%', + height: '30px', + paddingLeft: '8px', + lineHeight: '30px', + fontSize: '14px', + color: colorVars.gray11, +}); + export const tagDialogTriggerLayout = style({ position: 'relative', boxSizing: 'border-box', cursor: 'pointer', - width: '264px', - minHeight: '30px', - maxHeight: '60px', + width: '288px', border: '1px solid transparent', - borderRadius: '4px', - backgroundColor: color.inputBackground, transition: 'border 0.3s ease', ':focus': { border: `1px solid ${color.inputBorderFocus}`, outline: 'none', - backgroundColor: color.inputBackgroundFocus, + backgroundColor: colorVars.lightGray, + boxShadow: '4px 4px 0px 0px rgba(0, 0, 0, 0.2)', }, }); diff --git a/frontend/techpick/src/components/TagPicker/TagPicker.tsx b/frontend/techpick/src/components/TagPicker/TagPicker.tsx new file mode 100644 index 00000000..72cbd893 --- /dev/null +++ b/frontend/techpick/src/components/TagPicker/TagPicker.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { forwardRef, useRef, useState } from 'react'; +import { SelectedTagItem } from '../SelectedTagItem'; +import { TagAutocompleteDialog } from './TagAutocompleteDialog'; +import { + tagPickerLayout, + tagDialogTriggerLayout, + tagPickerPlaceholderStyle, +} from './TagPicker.css'; +import { SelectedTagListLayout } from '../SelectedTagListLayout/SelectedTagListLayout'; +import { PickInfoType, TagType } from '@/types'; + +export const TagPicker = forwardRef( + function TagPickerWithRef({ pickInfo, selectedTagList }, tabFocusRef) { + const [open, setOpen] = useState(false); + const tagInputContainerRef = useRef(null); + + const openDialog = () => { + setOpen(true); + }; + + const onEnterKeyDown = (e: React.KeyboardEvent) => { + if (e.key !== 'Enter') { + return; + } + + openDialog(); + }; + + return ( +
+
+ {selectedTagList.length === 0 && ( +

태그를 넣어주세요

+ )} + + {selectedTagList.map((tag) => ( + + ))} + +
+ + +
+ ); + } +); + +interface TagPickerProps { + pickInfo: PickInfoType; + selectedTagList: TagType[]; +} diff --git a/frontend/techpick/src/components/TagPicker/index.tsx b/frontend/techpick/src/components/TagPicker/index.tsx index 5b6cf90e..704c3e17 100644 --- a/frontend/techpick/src/components/TagPicker/index.tsx +++ b/frontend/techpick/src/components/TagPicker/index.tsx @@ -1,81 +1,3 @@ -'use client'; - -import { forwardRef, useEffect, useRef, useState } from 'react'; -import { useGetPickQuery } from '@/apis/pick'; -import { SelectedTagItem, SelectedTagListLayout } from '@/components'; -import { useTagStore } from '@/stores/tagStore'; -import { TagAutocompleteDialog } from './TagAutocompleteDialog'; -import { tagPickerLayout, tagDialogTriggerLayout } from './TagPicker.css'; -import type { TagType } from '@/types'; - -export const TagPicker = forwardRef( - function TagPickerWithRef({ pickId }, tabFocusRef) { - const [open, setOpen] = useState(false); - const [selectedTagList, setSelectedTagList] = useState([]); - const tagInputContainerRef = useRef(null); - const { data: pickData } = useGetPickQuery(pickId); - const { tagList } = useTagStore(); - - useEffect( - function tagPickerLoad() { - if (!pickData) { - return; - } - - const selectedTagList = tagList.filter((tag) => - pickData.tagIdOrderedList.includes(tag.id) - ); - - setSelectedTagList(selectedTagList); - }, - [pickData, tagList] - ); - - const openDialog = () => { - setOpen(true); - }; - - const onEnterKeyDown = (e: React.KeyboardEvent) => { - if (e.key !== 'Enter') { - return; - } - - openDialog(); - }; - - return ( -
-
{ - console.log('tagDialogTriggerLayout click'); - e.preventDefault(); - openDialog(); - }} - onKeyDown={onEnterKeyDown} - tabIndex={0} - ref={tabFocusRef} - > - - {selectedTagList.map((tag) => ( - - ))} - -
- - -
- ); - } -); - -interface TagPickerProps { - pickId: number; -} +export { TagPicker } from './TagPicker'; +export { DeleteTagDialog } from './DeleteTagDialog'; +export { DeselectTagButton } from './DeselectTagButton'; diff --git a/frontend/techpick/src/components/index.ts b/frontend/techpick/src/components/index.ts index 63cae72a..0102acdc 100644 --- a/frontend/techpick/src/components/index.ts +++ b/frontend/techpick/src/components/index.ts @@ -4,12 +4,11 @@ export * from './Gap'; export { DeferredComponent } from './DeferredComponent'; export { SelectedTagItem } from './SelectedTagItem'; export { SelectedTagListLayout } from './SelectedTagListLayout/SelectedTagListLayout'; -export { DeleteTagDialog } from './DeleteTagDialog'; -export { DeselectTagButton } from './DeselectTagButton/DeselectTagButton'; export { ShowDeleteTagDialogButton } from './ShowDeleteTagDialogButton'; export { ToggleThemeButton } from './ToggleThemeButton'; export { FeaturedSection } from './FeaturedSection/FeaturedSection'; -export { TagPicker } from './TagPicker'; +export { TagPicker, DeleteTagDialog, DeselectTagButton } from './TagPicker'; export { FolderTree } from './FolderTree'; export { PickListViewer, DraggablePickListViewer } from './PickListViewer'; export { FolderAndPickDndContextProvider } from './FolderAndPickDndContextProvider'; +export { PickRecordHeader } from './PickRecord'; diff --git a/frontend/techpick/src/hooks/index.ts b/frontend/techpick/src/hooks/index.ts index bea54c8b..46ef8f7d 100644 --- a/frontend/techpick/src/hooks/index.ts +++ b/frontend/techpick/src/hooks/index.ts @@ -2,3 +2,5 @@ export { useFolderToFolderDndMonitor } from './useFolderToFolderDndMonitor'; export { usePickToPickDndMonitor } from './usePickToPickDndMonitor'; export { useGetDndContextSensor } from './useGetDndContextSensor'; export { usePickToFolderDndMonitor } from './usePickToFolderDndMonitor'; +export { useOpenUrlInNewTab } from './useOpenUrlInNewTab'; +export { useCalculateCommandListHeight } from './useCalculateCommandListHeight'; diff --git a/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/useCalculateCommandListHeight.ts b/frontend/techpick/src/hooks/useCalculateCommandListHeight.ts similarity index 86% rename from frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/useCalculateCommandListHeight.ts rename to frontend/techpick/src/hooks/useCalculateCommandListHeight.ts index 70ac137e..4b60ca29 100644 --- a/frontend/techpick/src/components/TagPicker/TagAutocompleteDialog/useCalculateCommandListHeight.ts +++ b/frontend/techpick/src/hooks/useCalculateCommandListHeight.ts @@ -1,15 +1,12 @@ 'use client'; import { MutableRefObject, useEffect, useState } from 'react'; -import { useTagStore } from '@/stores/tagStore'; export function useCalculateCommandListHeight( selectedTagListRef: MutableRefObject ) { const [commandListHeight, setCommandListHeight] = useState(0); - const { selectedTagList } = useTagStore(); - useEffect( function calculateCommandListHeight() { const COMMAND_LIST_INITIAL_HEIGHT = 160; @@ -28,7 +25,7 @@ export function useCalculateCommandListHeight( ); setCommandListHeight(commandListHeight); }, - [selectedTagList, selectedTagListRef] + [selectedTagListRef] ); return { commandListHeight }; diff --git a/frontend/techpick/src/hooks/useOpenUrlInNewTab.ts b/frontend/techpick/src/hooks/useOpenUrlInNewTab.ts new file mode 100644 index 00000000..87816026 --- /dev/null +++ b/frontend/techpick/src/hooks/useOpenUrlInNewTab.ts @@ -0,0 +1,9 @@ +import { useCallback } from 'react'; + +export function useOpenUrlInNewTab(url: string) { + const openUrlInNewTab = useCallback(() => { + window.open(url, '_blank'); + }, [url]); + + return { openUrlInNewTab }; +} diff --git a/frontend/techpick/src/stores/index.ts b/frontend/techpick/src/stores/index.ts index 18c84e6a..4a5cf964 100644 --- a/frontend/techpick/src/stores/index.ts +++ b/frontend/techpick/src/stores/index.ts @@ -2,3 +2,6 @@ export { useTagStore } from './tagStore'; export { usePickStore } from './pickStore/pickStore'; export { useTreeStore } from './dndTreeStore/dndTreeStore'; export { usePickRenderModeStore } from './pickRenderModeStore'; +export { useDeleteTagDialogStore } from './deleteTagDialogStore'; +export { useThemeStore } from './themeStore'; +export { useUpdatePickStore } from './updatePickStore'; diff --git a/frontend/techpick/src/stores/pickStore/pickStore.ts b/frontend/techpick/src/stores/pickStore/pickStore.ts index eb0333c5..2d4b21ee 100644 --- a/frontend/techpick/src/stores/pickStore/pickStore.ts +++ b/frontend/techpick/src/stores/pickStore/pickStore.ts @@ -2,7 +2,7 @@ import { enableMapSet } from 'immer'; import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; -import { getPicksByFolderId, movePicks } from '@/apis/pick'; +import { getPicksByFolderId, movePicks, updatePick } from '@/apis/pick'; import { getPickListByQueryParam } from '@/apis/pick/getPicks'; import { isPickDraggableObject, reorderSortableIdList } from '@/utils'; import type { Active, Over } from '@dnd-kit/core'; @@ -14,6 +14,7 @@ import type { PickDraggableObjectType, PickToFolderDroppableObjectType, SearchPicksResponseType, + UpdatePickRequestType, } from '@/types'; enableMapSet(); @@ -60,6 +61,10 @@ type PickAction = { size?: number ) => Promise; getSearchResult: () => SearchPicksResponseType; + updatePickInfo: ( + pickParentFolderId: number, + pickInfo: UpdatePickRequestType + ) => Promise; }; const initialState: PickState = { @@ -365,6 +370,56 @@ export const usePickStore = create()( getSearchResult: () => { return get().searchResult; }, + updatePickInfo: async (pickParentFolderId, pickInfo) => { + const { + id: pickId, + tagIdOrderedList: newTagOrderedList, + title: newTitle, + } = pickInfo; + + const pickRecordValue = get().pickRecord[pickParentFolderId]; + + if (!get().hasPickRecordValue(pickRecordValue)) { + return; + } + + const { pickInfoRecord } = pickRecordValue; + + if (!pickInfoRecord[pickId]) { + return; + } + + const prevPickInfo = pickInfoRecord[pickId]; + const newPickInfo: PickInfoType = { + ...prevPickInfo, + title: newTitle ?? prevPickInfo.title, + tagIdOrderedList: newTagOrderedList ?? prevPickInfo.tagIdOrderedList, + }; + + // 미리 업데이트 + set((state) => { + if (!state.pickRecord[pickParentFolderId]) { + return; + } + + const { pickInfoRecord } = state.pickRecord[pickParentFolderId]; + pickInfoRecord[pickId] = newPickInfo; + }); + + try { + await updatePick(pickInfo); + } catch { + // 실패하면 원복하기. + set((state) => { + if (!state.pickRecord[pickParentFolderId]) { + return; + } + + const { pickInfoRecord } = state.pickRecord[pickParentFolderId]; + pickInfoRecord[pickId] = prevPickInfo; + }); + } + }, })) ) ); diff --git a/frontend/techpick/src/stores/tagStore.ts b/frontend/techpick/src/stores/tagStore.ts index 1103d0df..b832f676 100644 --- a/frontend/techpick/src/stores/tagStore.ts +++ b/frontend/techpick/src/stores/tagStore.ts @@ -11,7 +11,7 @@ import type { type TagState = { tagList: TagType[]; - selectedTagList: TagType[]; + fetchingTagState: { isError: boolean; isPending: boolean; @@ -20,10 +20,6 @@ type TagState = { }; type TagAction = { - replaceSelectedTagList: (tagList: TagType[]) => void; - selectTag: (tag: TagType) => void; - deselectTag: (tagId: TagType['id']) => void; - updateSelectedTagList: (tag: TagType) => void; fetchingTagList: () => Promise; createTag: (tagData: CreateTagRequestType) => Promise; deleteTag: (tagId: TagType['id']) => Promise; @@ -40,7 +36,7 @@ type TagAction = { const initialState: TagState = { tagList: [], - selectedTagList: [], + fetchingTagState: { isError: false, isPending: false, data: [] }, }; @@ -48,46 +44,6 @@ export const useTagStore = create()( immer((set, get) => ({ ...initialState, - replaceSelectedTagList: (tagList) => - set((state) => { - state.selectedTagList = tagList; - }), - - selectTag: (tag: TagType) => - set((state) => { - const exist = state.selectedTagList.some((t) => t.id === tag.id); - - // 이미 선택된 태그인지 확인 - if (exist) { - return; - } - - state.selectedTagList.push(tag); - }), - - deselectTag: (tagId) => - set((state) => { - state.selectedTagList = state.selectedTagList.filter( - (t) => t.id !== tagId - ); - }), - - updateSelectedTagList: (updatedTag) => { - set((state) => { - const index = state.selectedTagList.findIndex( - (tag) => tag.id === updatedTag.id - ); - - if (index === -1) { - return; - } - - state.selectedTagList[index] = { - ...updatedTag, - }; - }); - }, - fetchingTagList: async () => { try { set((state) => { @@ -135,17 +91,12 @@ export const useTagStore = create()( deleteTag: async (tagId: number) => { let temporalDeleteTargetTag: TagType | undefined; let deleteTargetTagIndex = -1; - let isSelected = false; - let deleteTargetSelectedIndex = -1; try { set((state) => { deleteTargetTagIndex = state.tagList.findIndex( (tag) => tag.id === tagId ); - deleteTargetSelectedIndex = state.selectedTagList.findIndex( - (tag) => tag.id === tagId - ); if (deleteTargetTagIndex !== -1) { temporalDeleteTargetTag = { @@ -153,11 +104,6 @@ export const useTagStore = create()( }; state.tagList.splice(deleteTargetTagIndex, 1); } - - if (deleteTargetSelectedIndex !== -1) { - isSelected = true; - state.selectedTagList.splice(deleteTargetSelectedIndex, 1); - } }); await deleteTag(tagId); @@ -172,14 +118,6 @@ export const useTagStore = create()( 0, temporalDeleteTargetTag ); - - if (isSelected) { - state.selectedTagList.splice( - deleteTargetSelectedIndex, - 0, - temporalDeleteTargetTag - ); - } }); if (error instanceof HTTPError) { @@ -190,7 +128,6 @@ export const useTagStore = create()( updateTag: async (updatedTag) => { let previousTag: TagType | undefined; - let previousSelectedTag: TagType | undefined; try { set((state) => { @@ -203,17 +140,6 @@ export const useTagStore = create()( state.tagList[index] = updatedTag; } - - const selectedTagListIndex = state.selectedTagList.findIndex( - (tag) => tag.id === updatedTag.id - ); - - if (selectedTagListIndex !== -1) { - previousSelectedTag = { - ...state.selectedTagList[selectedTagListIndex], - }; - state.selectedTagList[selectedTagListIndex] = updatedTag; - } }); await updateTag(updatedTag); @@ -225,13 +151,6 @@ export const useTagStore = create()( ); state.tagList[index] = previousTag; } - - if (previousSelectedTag) { - const selectedIndex = state.selectedTagList.findIndex( - (tag) => tag.id === previousSelectedTag?.id - ); - state.selectedTagList[selectedIndex] = previousSelectedTag; - } }); if (error instanceof HTTPError) { diff --git a/frontend/techpick/src/stores/updatePickStore.ts b/frontend/techpick/src/stores/updatePickStore.ts new file mode 100644 index 00000000..0d9116b6 --- /dev/null +++ b/frontend/techpick/src/stores/updatePickStore.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; + +type UpdatePickState = { + currentUpdatePickId: number | null; +}; + +type UpdatePickAction = { + setCurrentUpdatePickId: (nextUpdatePickId: number | null) => void; + setCurrentPickIdToNull: () => void; +}; + +const initialState: UpdatePickState = { + currentUpdatePickId: null, +}; + +export const useUpdatePickStore = create()( + immer((set) => ({ + ...initialState, + setCurrentUpdatePickId: (nextUpdatePickId) => { + set((state) => { + state.currentUpdatePickId = nextUpdatePickId; + }); + }, + setCurrentPickIdToNull: () => { + set((state) => { + state.currentUpdatePickId = null; + }); + }, + })) +); diff --git a/frontend/techpick/src/styles/reset.css.ts b/frontend/techpick/src/styles/reset.css.ts index 794cb9d7..ab4401e2 100644 --- a/frontend/techpick/src/styles/reset.css.ts +++ b/frontend/techpick/src/styles/reset.css.ts @@ -23,7 +23,7 @@ globalStyle( article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, - time, mark, audio, video, button + time, mark, audio, video, button,input `, { margin: 0, diff --git a/frontend/techpick/src/types/PickViewDraggableItemComponentProps.ts b/frontend/techpick/src/types/PickViewDraggableItemComponentProps.ts new file mode 100644 index 00000000..24c5981c --- /dev/null +++ b/frontend/techpick/src/types/PickViewDraggableItemComponentProps.ts @@ -0,0 +1,3 @@ +import { PickViewItemComponentProps } from './PickViewItemComponentProps'; + +export type PickViewDraggableItemComponentProps = PickViewItemComponentProps; diff --git a/frontend/techpick/src/types/PickViewDraggableItemListLayoutComponentProps.ts b/frontend/techpick/src/types/PickViewDraggableItemListLayoutComponentProps.ts new file mode 100644 index 00000000..71b0b02f --- /dev/null +++ b/frontend/techpick/src/types/PickViewDraggableItemListLayoutComponentProps.ts @@ -0,0 +1,10 @@ +import { PickViewItemListLayoutComponentProps } from './PickViewItemListLayoutComponentProps'; + +/** + * @description 현재는 record지만 추후에 PickRenderModeType와 병합될 예정입니다. + */ +export type PickViewDraggableItemListLayoutComponentProps = + PickViewItemListLayoutComponentProps<{ + folderId: number; + viewType: 'record'; + }>; diff --git a/frontend/techpick/src/types/PickViewItemComponentProps.ts b/frontend/techpick/src/types/PickViewItemComponentProps.ts new file mode 100644 index 00000000..c3c5a2e8 --- /dev/null +++ b/frontend/techpick/src/types/PickViewItemComponentProps.ts @@ -0,0 +1,5 @@ +import { PickInfoType } from './pick.type'; + +export type PickViewItemComponentProps = { + pickInfo: PickInfoType; +} & ExtraProps; diff --git a/frontend/techpick/src/types/PickViewItemListLayoutComponentProps.ts b/frontend/techpick/src/types/PickViewItemListLayoutComponentProps.ts new file mode 100644 index 00000000..cb195ef7 --- /dev/null +++ b/frontend/techpick/src/types/PickViewItemListLayoutComponentProps.ts @@ -0,0 +1,4 @@ +import type { PropsWithChildren } from 'react'; + +export type PickViewItemListLayoutComponentProps = + PropsWithChildren; diff --git a/frontend/techpick/src/types/UpdatePickRequestType.ts b/frontend/techpick/src/types/UpdatePickRequestType.ts new file mode 100644 index 00000000..8785d524 --- /dev/null +++ b/frontend/techpick/src/types/UpdatePickRequestType.ts @@ -0,0 +1,4 @@ +import { components } from '@/schema'; + +export type UpdatePickRequestType = + components['schemas']['techpick.api.application.pick.dto.PickApiRequest$Update']; diff --git a/frontend/techpick/src/types/index.ts b/frontend/techpick/src/types/index.ts index 5dac9653..ff44ad77 100644 --- a/frontend/techpick/src/types/index.ts +++ b/frontend/techpick/src/types/index.ts @@ -7,3 +7,7 @@ export type { PickDraggableObjectType } from './PickDraggableObjectType'; export type { FolderDraggableObjectType } from './FolderDraggableObjectType'; export type { PickToFolderDroppableObjectType } from './PickToFolderDroppableObjectType'; export type { PickRenderModeType } from './PickRenderModeType'; +export type { UpdatePickRequestType } from './UpdatePickRequestType'; +export type { PickViewItemComponentProps } from './PickViewItemComponentProps'; +export type { PickViewDraggableItemComponentProps } from './PickViewDraggableItemComponentProps'; +export type { PickViewDraggableItemListLayoutComponentProps } from './PickViewDraggableItemListLayoutComponentProps'; diff --git a/frontend/techpick/src/utils/formatDateString.ts b/frontend/techpick/src/utils/formatDateString.ts new file mode 100644 index 00000000..39c84725 --- /dev/null +++ b/frontend/techpick/src/utils/formatDateString.ts @@ -0,0 +1,7 @@ +export const formatDateString = (dateStringFromServer: string) => { + const date = new Date(dateStringFromServer); + const year = date.getFullYear(); // 2023 + const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 06 + const day = date.getDate().toString().padStart(2, '0'); // 18 + return year + '-' + month + '-' + day; +}; diff --git a/frontend/techpick/src/utils/getSelectedPickRange.ts b/frontend/techpick/src/utils/getSelectedPickRange.ts new file mode 100644 index 00000000..d253870a --- /dev/null +++ b/frontend/techpick/src/utils/getSelectedPickRange.ts @@ -0,0 +1,32 @@ +import { hasIndex } from '@/utils'; +import type { OrderedPickIdListType } from '@/types'; + +export const getSelectedPickRange = ({ + orderedPickIdList, + startPickId, + endPickId, +}: GetSelectedPickRangePayload) => { + const firstSelectedIndex = orderedPickIdList.findIndex( + (orderedPickId) => orderedPickId === startPickId + ); + const lastSelectedIndex = orderedPickIdList.findIndex( + (orderedPickId) => orderedPickId === endPickId + ); + + if (!hasIndex(firstSelectedIndex) || !hasIndex(lastSelectedIndex)) return []; + + const startIndex = Math.min(firstSelectedIndex, lastSelectedIndex); + const endIndex = Math.max(firstSelectedIndex, lastSelectedIndex); + const newSelectedPickIdList = orderedPickIdList.slice( + startIndex, + endIndex + 1 + ); + + return newSelectedPickIdList; +}; + +interface GetSelectedPickRangePayload { + orderedPickIdList: OrderedPickIdListType; + startPickId: number; + endPickId: number; +} diff --git a/frontend/techpick/src/utils/index.ts b/frontend/techpick/src/utils/index.ts index 78d3aab8..7e1e21ce 100644 --- a/frontend/techpick/src/utils/index.ts +++ b/frontend/techpick/src/utils/index.ts @@ -10,3 +10,6 @@ export { isFolderDraggableObject } from './isFolderDraggableObject'; export { isPickDraggableObject } from './isPickDraggableObjectType'; export { isPickToFolderDroppableObject } from './isPickToFolderDroppableObject'; export { getElementById } from './getElementById'; +export { formatDateString } from './formatDateString'; +export { isShallowEqualValue } from './isShallowEqualValue'; +export { getSelectedPickRange } from './getSelectedPickRange'; diff --git a/frontend/techpick/src/utils/isShallowEqualValue.ts b/frontend/techpick/src/utils/isShallowEqualValue.ts new file mode 100644 index 00000000..603f5109 --- /dev/null +++ b/frontend/techpick/src/utils/isShallowEqualValue.ts @@ -0,0 +1,3 @@ +export const isShallowEqualValue = (value1: T, value2: T) => { + return value1 === value2; +}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 22b3988a..e7141eb5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3443,6 +3443,25 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-separator@npm:^1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-separator@npm:1.1.0" + dependencies: + "@radix-ui/react-primitive": "npm:2.0.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/0ca9e25db27b6b001f3c0c50b2df9d6cf070b949f183043e263115d694a25b7268fecd670572469a512e556deca25ebb08b3aec4a870f0309eed728eef19ab8a + languageName: node + linkType: hard + "@radix-ui/react-slot@npm:1.1.0, @radix-ui/react-slot@npm:^1.1.0": version: 1.1.0 resolution: "@radix-ui/react-slot@npm:1.1.0" @@ -14042,6 +14061,7 @@ __metadata: "@radix-ui/react-dialog": "npm:^1.1.2" "@radix-ui/react-icons": "npm:1.3.0" "@radix-ui/react-popover": "npm:1.1.2" + "@radix-ui/react-separator": "npm:^1.1.0" "@radix-ui/react-slot": "npm:^1.1.0" "@radix-ui/react-visually-hidden": "npm:^1.1.0" "@storybook/addon-essentials": "npm:^8.2.9"