From 3c01c84914e1774544e02592e25b284e6c7e7dd8 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Fri, 23 Oct 2026 17:53:02 +0900 Subject: [PATCH 01/17] =?UTF-8?q?chore:=20=ED=8F=B4=EB=8D=94=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=ED=99=94=EB=A9=B4=EA=B3=BC=20dnd-kit=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 폴더 편집 화면(/wish/folder/edit) 삭제 - 원인: 개편 디자인에서 폴더 정렬을 생성순으로 고정 - 원인: 폴더 수정 진입점을 카드 더보기 메뉴로 이관 - @dnd-kit/core·@dnd-kit/sortable 제거 - 원인: 폴더 편집 화면 외 사용처 없음 --- apps/web/package.json | 2 - .../edit/_components/FolderEditContent.tsx | 132 ------------------ .../edit/_components/SortableFolderRow.tsx | 72 ---------- apps/web/src/app/wish/folder/edit/page.tsx | 11 -- pnpm-lock.yaml | 53 ------- 5 files changed, 270 deletions(-) delete mode 100644 apps/web/src/app/wish/folder/edit/_components/FolderEditContent.tsx delete mode 100644 apps/web/src/app/wish/folder/edit/_components/SortableFolderRow.tsx delete mode 100644 apps/web/src/app/wish/folder/edit/page.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 447b43ee..9893f4c9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,8 +14,6 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^16.2.9", "@piki/core": "workspace:*", diff --git a/apps/web/src/app/wish/folder/edit/_components/FolderEditContent.tsx b/apps/web/src/app/wish/folder/edit/_components/FolderEditContent.tsx deleted file mode 100644 index 4df60775..00000000 --- a/apps/web/src/app/wish/folder/edit/_components/FolderEditContent.tsx +++ /dev/null @@ -1,132 +0,0 @@ -'use client'; - -import { - DndContext, - type DragEndEvent, - KeyboardSensor, - PointerSensor, - closestCenter, - useSensor, - useSensors, -} from '@dnd-kit/core'; -import { - SortableContext, - arrayMove, - sortableKeyboardCoordinates, - verticalListSortingStrategy, -} from '@dnd-kit/sortable'; -import { josa } from 'es-hangul'; -import { useState } from 'react'; - -import { CloseIconFill, WarningIconFill } from '@/assets/icons'; -import ConfirmDialog from '@/components/common/confirm-dialog'; -import { Header } from '@/components/header'; -import { ROUTES } from '@/consts/route'; -import { Z_INDEX } from '@/consts/zIndex'; -import { useBackWithFallback } from '@/hooks/useBackWithFallback'; -import { cn } from '@/utils/cn'; - -import { MOCK_FOLDERS } from '../../../_common/_consts/wishMock'; -import type { WishFolderT } from '../../../_common/_types/wish'; -import SortableFolderRow from './SortableFolderRow'; - -function FolderEditContent() { - const backWithFallback = useBackWithFallback(); - - /** TEMP: API 연동 시 폴더 목록 조회 응답으로 교체 */ - const [folders, setFolders] = useState(MOCK_FOLDERS); - const [deleteTarget, setDeleteTarget] = useState(null); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) - ); - - const isChanged = folders.map(({ id }) => id).join() !== MOCK_FOLDERS.map(({ id }) => id).join(); - - const handleDragEnd = ({ active, over }: DragEndEvent) => { - if (!over || active.id === over.id) return; - - setFolders(prev => - arrayMove( - prev, - prev.findIndex(({ id }) => id === active.id), - prev.findIndex(({ id }) => id === over.id) - ) - ); - }; - - const handleClose = () => backWithFallback(ROUTES.WISHLIST_FOLDER_TAB); - - /** TODO: 폴더 삭제·순서 변경 API 연결 */ - const handleDone = () => handleClose(); - - const handleConfirmDelete = () => { - if (!deleteTarget) return; - - setFolders(prev => prev.filter(({ id }) => id !== deleteTarget.id)); - setDeleteTarget(null); - }; - - return ( - <> -
-
- - - } - center={

폴더 편집

} - right={ - - } - /> -
- - - -
    - {folders.map(folder => ( - - ))} -
-
-
- - !open && setDeleteTarget(null)} - icon={} - title={ - deleteTarget - ? `'${deleteTarget.name}'${josa.pick(deleteTarget.name, '을/를')} 삭제할까요?` - : '' - } - description="폴더를 삭제해도 담아둔 위시는 그대로 유지돼요." - confirmLabel="삭제하기" - onConfirm={handleConfirmDelete} - /> - - ); -} - -export default FolderEditContent; diff --git a/apps/web/src/app/wish/folder/edit/_components/SortableFolderRow.tsx b/apps/web/src/app/wish/folder/edit/_components/SortableFolderRow.tsx deleted file mode 100644 index 4c28d279..00000000 --- a/apps/web/src/app/wish/folder/edit/_components/SortableFolderRow.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import { useSortable } from '@dnd-kit/sortable'; - -import { AlignEqualIconFill, CloseCircularIconFill } from '@/assets/icons'; -import { cn } from '@/utils/cn'; - -import FolderListThumbnail from '../../../_common/_components/FolderListThumbnail'; -import { getMockWishImageUrl } from '../../../_common/_consts/wishMock'; -import type { WishFolderT } from '../../../_common/_types/wish'; - -type SortableFolderRowProps = { - folder: WishFolderT; - onDelete: (folder: WishFolderT) => void; -}; - -function SortableFolderRow({ folder, onDelete }: SortableFolderRowProps) { - const { - attributes, - listeners, - setNodeRef, - setActivatorNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: folder.id }); - - return ( -
  • - -
    - -

    - {folder.name} - ({folder.wishIds.length}) -

    -
    - -
  • - ); -} - -export default SortableFolderRow; diff --git a/apps/web/src/app/wish/folder/edit/page.tsx b/apps/web/src/app/wish/folder/edit/page.tsx deleted file mode 100644 index 5986a898..00000000 --- a/apps/web/src/app/wish/folder/edit/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import FolderEditContent from './_components/FolderEditContent'; - -function FolderEditPage() { - return ( -
    - -
    - ); -} - -export default FolderEditPage; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55c1af51..231a7db9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,12 +230,6 @@ importers: apps/web: dependencies: - '@dnd-kit/core': - specifier: ^6.3.1 - version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@dnd-kit/sortable': - specifier: ^10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) '@microsoft/fetch-event-source': specifier: ^2.0.1 version: 2.0.1 @@ -1093,28 +1087,6 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@dnd-kit/accessibility@3.1.1': - resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} - peerDependencies: - react: 19.1.0 - - '@dnd-kit/core@6.3.1': - resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} - peerDependencies: - react: 19.1.0 - react-dom: 19.1.0 - - '@dnd-kit/sortable@10.0.0': - resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} - peerDependencies: - '@dnd-kit/core': ^6.3.0 - react: 19.1.0 - - '@dnd-kit/utilities@3.2.2': - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} - peerDependencies: - react: 19.1.0 - '@dotenvx/dotenvx@1.66.0': resolution: {integrity: sha512-qlQFhHUjhRDybrinqLAD0MClVZDOrsq80O8eD5iSjz3Qa/4f3Jg7SQrOaSobrRyP1QaWIYLGtGpj2c7H0D8NUw==} hasBin: true @@ -9403,31 +9375,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@dnd-kit/accessibility@3.1.1(react@19.1.0)': - dependencies: - react: 19.1.0 - tslib: 2.8.1 - - '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@dnd-kit/accessibility': 3.1.1(react@19.1.0) - '@dnd-kit/utilities': 3.2.2(react@19.1.0) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - tslib: 2.8.1 - - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': - dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@dnd-kit/utilities': 3.2.2(react@19.1.0) - react: 19.1.0 - tslib: 2.8.1 - - '@dnd-kit/utilities@3.2.2(react@19.1.0)': - dependencies: - react: 19.1.0 - tslib: 2.8.1 - '@dotenvx/dotenvx@1.66.0': dependencies: commander: 11.1.0 From ab9d5934f835eea6e2b47b03dce9f60ae313fea7 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Fri, 23 Oct 2026 17:53:02 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EB=AA=A8=EB=93=9C=20=EC=95=A1=EC=85=98=EB=B0=94?= =?UTF-8?q?=EC=99=80=20=EC=82=AD=EC=A0=9C=20=ED=99=95=EC=9D=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 편집 헤더 문구를 `위시 N개 선택`으로 변경 - 하단 액션바를 아이콘·라벨 가로 배치로 변경 - 삭제 확인을 바텀시트에서 ConfirmDialog 로 교체 - 전체 위시 탭은 /archive/wish 의 기존 삭제 모달 문구 재사용 - 폴더 상세는 폴더에서만 빠진다는 디자인 문구 사용 - 액션바가 떠 있는 동안 하단 탭바 숨김 - 원인: 액션바 높이를 디자인 기준 84px 로 줄이자 탭바 윗부분이 노출됨 - 대안: 탭바에 편집 상태 전달 - 기각: 탭바가 루트 레이아웃에 있어 전역 상태 필요 - 규칙은 액션바 옆 CSS 파일에 둠 - useWishEditMode 에 전체 선택 핸들러 추가 --- .../_common/_components/WishDeleteSheet.tsx | 47 ------------------- .../_common/_components/WishEditActionBar.css | 4 ++ .../_common/_components/WishEditActionBar.tsx | 30 +++++++----- .../_common/_components/WishEditHeader.tsx | 2 +- .../wish/_common/_hooks/useWishEditMode.ts | 3 ++ 5 files changed, 26 insertions(+), 60 deletions(-) delete mode 100644 apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx create mode 100644 apps/web/src/app/wish/_common/_components/WishEditActionBar.css diff --git a/apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx b/apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx deleted file mode 100644 index 619077f8..00000000 --- a/apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import Button from '@/components/button'; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerHeader, - DrawerTitle, -} from '@/components/drawer'; - -type WishDeleteSheetProps = { - open: boolean; - onOpenChange: (open: boolean) => void; - selectedCount: number; - onConfirm: () => void; -}; - -function WishDeleteSheet({ open, onOpenChange, selectedCount, onConfirm }: WishDeleteSheetProps) { - return ( - - - - - 상품을 삭제할까요? - - - 선택하신 상품 {selectedCount}개가 삭제됩니다. - - -
    - - - - -
    -
    -
    - ); -} - -export default WishDeleteSheet; diff --git a/apps/web/src/app/wish/_common/_components/WishEditActionBar.css b/apps/web/src/app/wish/_common/_components/WishEditActionBar.css new file mode 100644 index 00000000..0a702919 --- /dev/null +++ b/apps/web/src/app/wish/_common/_components/WishEditActionBar.css @@ -0,0 +1,4 @@ +/** 탭바가 루트 레이아웃에 있어 편집 중에도 렌더되므로 액션바가 떠 있는 동안 숨김 */ +body:has([data-hide-bottom-tab-bar]) [data-bottom-tab-bar] { + display: none; +} diff --git a/apps/web/src/app/wish/_common/_components/WishEditActionBar.tsx b/apps/web/src/app/wish/_common/_components/WishEditActionBar.tsx index 88fe8393..bc1daa38 100644 --- a/apps/web/src/app/wish/_common/_components/WishEditActionBar.tsx +++ b/apps/web/src/app/wish/_common/_components/WishEditActionBar.tsx @@ -1,21 +1,23 @@ 'use client'; -import { type ComponentType, type SVGProps, useState } from 'react'; +import { type ComponentProps, type ComponentType, type SVGProps, useState } from 'react'; import { FolderIconFill, TrashIconFill } from '@/assets/icons'; +import ConfirmDialog from '@/components/common/confirm-dialog'; import { Z_INDEX } from '@/consts/zIndex'; import { cn } from '@/utils/cn'; import { MOCK_FOLDERS } from '../_consts/wishMock'; import type { WishFolderT } from '../_types/wish'; import FolderSelectSheet from './FolderSelectSheet'; -import WishDeleteSheet from './WishDeleteSheet'; +import './WishEditActionBar.css'; type WishEditActionBarProps = { selectedCount: number; currentFolderId?: number; onMove: (folder: WishFolderT) => void; onDelete: () => void; + deleteConfirm: Pick, 'title' | 'description' | 'icon'>; }; function ActionButton({ @@ -35,13 +37,13 @@ function ActionButton({ disabled={disabled} onClick={onClick} className={cn( - 'flex cursor-pointer flex-col items-center gap-[7px] body-2-semibold disabled:cursor-not-allowed', + 'flex w-25 cursor-pointer items-center justify-center gap-[7px] body-2-semibold disabled:cursor-not-allowed', disabled ? 'text-text-neutral-tertiary' : 'text-text-neutral-secondary' )} > @@ -55,9 +57,10 @@ function WishEditActionBar({ currentFolderId, onMove, onDelete, + deleteConfirm, }: WishEditActionBarProps) { const [isFolderSheetOpen, setIsFolderSheetOpen] = useState(false); - const [isDeleteSheetOpen, setIsDeleteSheetOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); /** TEMP: API 연동 시 폴더 목록 조회·생성 API 로 교체 */ const [folders, setFolders] = useState(() => MOCK_FOLDERS.filter(({ id }) => id !== currentFolderId) @@ -75,7 +78,7 @@ function WishEditActionBar({ }; const handleConfirmDelete = () => { - setIsDeleteSheetOpen(false); + setIsDeleteDialogOpen(false); onDelete(); }; @@ -83,7 +86,8 @@ function WishEditActionBar({ <>
    setIsFolderSheetOpen(true)} /> + setIsDeleteSheetOpen(true)} + onClick={() => setIsDeleteDialogOpen(true)} />
    @@ -107,10 +112,11 @@ function WishEditActionBar({ onSelect={handleSelectFolder} onAddFolder={handleAddFolder} /> - diff --git a/apps/web/src/app/wish/_common/_components/WishEditHeader.tsx b/apps/web/src/app/wish/_common/_components/WishEditHeader.tsx index 337c094b..e43fd613 100644 --- a/apps/web/src/app/wish/_common/_components/WishEditHeader.tsx +++ b/apps/web/src/app/wish/_common/_components/WishEditHeader.tsx @@ -10,7 +10,7 @@ function WishEditHeader({ selectedCount, onDone }: WishEditHeaderProps) {
    - {selectedCount > 0 ? `상품 ${selectedCount}개 선택` : '상품 선택'} + {selectedCount > 0 ? `위시 ${selectedCount}개 선택` : '위시 선택'} } right={ diff --git a/apps/web/src/app/wish/_common/_hooks/useWishEditMode.ts b/apps/web/src/app/wish/_common/_hooks/useWishEditMode.ts index 2c838df9..9029c9c6 100644 --- a/apps/web/src/app/wish/_common/_hooks/useWishEditMode.ts +++ b/apps/web/src/app/wish/_common/_hooks/useWishEditMode.ts @@ -18,6 +18,8 @@ export const useWishEditMode = () => { const handleClearSelection = () => setSelectedWishIds(new Set()); + const handleSelectAll = (wishIds: number[]) => setSelectedWishIds(new Set(wishIds)); + const handleEnterEditMode = () => setMode(WISH_MODE.EDIT); const handleExitEditMode = () => { @@ -30,6 +32,7 @@ export const useWishEditMode = () => { selectedWishIds, handleToggleSelect, handleClearSelection, + handleSelectAll, handleEnterEditMode, handleExitEditMode, }; From 01ecbd9d67f0331baf7fa337178bc2815d78aa4d Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Fri, 23 Oct 2026 17:53:02 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=ED=8F=B4=EB=8D=94=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=9E=85=EB=A0=A5=20=ED=8F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 폴더·폴더명 변경·이동 중 새 폴더가 같은 폼 사용 - 최대 20자 - 이모지도 1자로 세도록 grapheme 단위로 셈 - 20자 도달 시 추가 입력 막힘·안내 문구 노출 - 앞뒤 공백 제거 - 0자로 완료 시 `폴더 이름을 입력해 주세요.` 노출 - 폴더명 변경은 기존 이름과 같으면 완료 비활성 - 폴더 선택 시트의 폴더 추가 화면을 이 폼으로 교체 --- .../_common/_components/FolderNameForm.tsx | 83 +++++++++++++++++++ .../_common/_components/FolderSelectSheet.tsx | 62 +++----------- 2 files changed, 95 insertions(+), 50 deletions(-) create mode 100644 apps/web/src/app/wish/_common/_components/FolderNameForm.tsx diff --git a/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx new file mode 100644 index 00000000..ae4a5f8b --- /dev/null +++ b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { type ChangeEvent, type FormEvent, useState } from 'react'; + +import { ChevronBackwardIconFill } from '@/assets/icons'; +import Button from '@/components/button'; +import { DrawerTitle } from '@/components/drawer'; +import { Header } from '@/components/header'; +import Input from '@/components/input'; + +const MAX_FOLDER_NAME_LENGTH = 20; + +const graphemeSegmenter = new Intl.Segmenter('ko', { granularity: 'grapheme' }); + +/** NOTE: 이모지도 1자로 세야 해서 length 대신 grapheme 단위로 셈 */ +const splitGraphemes = (value: string) => + Array.from(graphemeSegmenter.segment(value), ({ segment }) => segment); + +type FolderNameFormProps = { + title: string; + initialName?: string; + onBack: () => void; + onSubmit: (name: string) => void; +}; + +function FolderNameForm({ title, initialName = '', onBack, onSubmit }: FolderNameFormProps) { + const [name, setName] = useState(initialName); + const [errorMessage, setErrorMessage] = useState(null); + + const trimmedName = name.trim(); + const isUnchanged = initialName !== '' && trimmedName === initialName; + + const handleChange = (event: ChangeEvent) => { + const graphemes = splitGraphemes(event.target.value); + const isOverLimit = graphemes.length >= MAX_FOLDER_NAME_LENGTH; + + setName(graphemes.slice(0, MAX_FOLDER_NAME_LENGTH).join('')); + setErrorMessage(isOverLimit ? `최대 ${MAX_FOLDER_NAME_LENGTH}자까지 입력할 수 있어요.` : null); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + if (!trimmedName) { + setErrorMessage('폴더 이름을 입력해 주세요.'); + return; + } + + onSubmit(trimmedName); + }; + + return ( +
    +
    + + + } + center={ + + {title} + + } + /> +
    + + +
    + + ); +} + +export default FolderNameForm; diff --git a/apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx b/apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx index e1fdf25b..f4698597 100644 --- a/apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx +++ b/apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx @@ -1,16 +1,15 @@ 'use client'; -import { type FormEvent, useState } from 'react'; +import { useState } from 'react'; -import { ChevronBackwardIconFill, FolderAddIconFill } from '@/assets/icons'; -import Button from '@/components/button'; +import { FolderAddIconFill } from '@/assets/icons'; import { Drawer, DrawerContent, DrawerDescription, DrawerTitle } from '@/components/drawer'; import { Header } from '@/components/header'; -import Input from '@/components/input'; import { getMockWishImageUrl } from '../_consts/wishMock'; import type { WishFolderT } from '../_types/wish'; import FolderListThumbnail from './FolderListThumbnail'; +import FolderNameForm from './FolderNameForm'; type FolderSelectSheetProps = { open: boolean; @@ -28,26 +27,15 @@ function FolderSelectSheet({ onAddFolder, }: FolderSelectSheetProps) { const [isAddView, setIsAddView] = useState(false); - const [folderName, setFolderName] = useState(''); - - const trimmedFolderName = folderName.trim(); - - const handleBackToList = () => { - setIsAddView(false); - setFolderName(''); - }; const handleOpenChange = (isOpen: boolean) => { - if (!isOpen) handleBackToList(); + if (!isOpen) setIsAddView(false); onOpenChange(isOpen); }; - const handleAddSubmit = (event: FormEvent) => { - event.preventDefault(); - if (!trimmedFolderName) return; - - onAddFolder(trimmedFolderName); - handleBackToList(); + const handleAddSubmit = (name: string) => { + onAddFolder(name); + setIsAddView(false); }; return ( @@ -57,37 +45,11 @@ function FolderSelectSheet({ {isAddView ? '새 폴더 이름을 입력하세요.' : '상품을 옮길 폴더를 선택하세요.'} {isAddView ? ( -
    -
    - - - } - center={ - - 폴더 추가 - - } - /> -
    - setFolderName(event.target.value)} - autoFocus - /> - -
    - + setIsAddView(false)} + onSubmit={handleAddSubmit} + /> ) : (
    Date: Fri, 23 Oct 2026 17:53:03 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 위시 탭 위에 덮는 검색 레이어로 구현 1. 처음에 /wish/search 페이지로 분리 2. 디자인상 입력 중 배경이 전체 위시 탭 그대로라 레이어로 전환 - 아래 탭 레이아웃은 그대로 둠 - 입력 중: 흰 패널·배경 dim - 검색 후: `검색 결과 n개`·그리드 - 결과 0개면 `일치하는 상품이 없어요.` - 상품명만 검색 대상 - 검색 API 연결은 TODO --- .../_common/_components/WishSearchLink.tsx | 15 -- .../src/app/wish/_components/WishSearch.tsx | 132 ++++++++++++++++++ 2 files changed, 132 insertions(+), 15 deletions(-) delete mode 100644 apps/web/src/app/wish/_common/_components/WishSearchLink.tsx create mode 100644 apps/web/src/app/wish/_components/WishSearch.tsx diff --git a/apps/web/src/app/wish/_common/_components/WishSearchLink.tsx b/apps/web/src/app/wish/_common/_components/WishSearchLink.tsx deleted file mode 100644 index bb499efc..00000000 --- a/apps/web/src/app/wish/_common/_components/WishSearchLink.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Link from 'next/link'; - -import { SearchIconOutline } from '@/assets/icons'; -import { ROUTES } from '@/consts/route'; - -function WishSearchLink() { - return ( - // TODO: 경로 변경 필요 - - - - ); -} - -export default WishSearchLink; diff --git a/apps/web/src/app/wish/_components/WishSearch.tsx b/apps/web/src/app/wish/_components/WishSearch.tsx new file mode 100644 index 00000000..0a0de4e0 --- /dev/null +++ b/apps/web/src/app/wish/_components/WishSearch.tsx @@ -0,0 +1,132 @@ +'use client'; + +import { type FormEvent, useRef, useState } from 'react'; + +import { ChevronBackwardIconFill, CloseCircularIconFill } from '@/assets/icons'; +import BasketGuestIcon from '@/assets/images/basket-guest.svg'; +import { Header } from '@/components/header'; +import Input from '@/components/input'; +import { Z_INDEX } from '@/consts/zIndex'; +import { cn } from '@/utils/cn'; + +import WishGrid from '../_common/_components/WishGrid'; +import type { WishListItemT } from '../_common/_types/wish'; + +type WishSearchProps = { + wishes: WishListItemT[]; + onClose: () => void; +}; + +function WishSearch({ wishes, onClose }: WishSearchProps) { + const inputRef = useRef(null); + + const [keyword, setKeyword] = useState(''); + const [submittedKeyword, setSubmittedKeyword] = useState(null); + const [isFocused, setIsFocused] = useState(true); + + /** TODO: API 연동 시 위시 검색 API 로 교체 */ + const results = + submittedKeyword === null + ? [] + : wishes.filter(({ item }) => + item.name?.toLowerCase().includes(submittedKeyword.toLowerCase()) + ); + const isSearching = isFocused || submittedKeyword === null; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + const trimmedKeyword = keyword.trim(); + if (!trimmedKeyword) return; + + setSubmittedKeyword(trimmedKeyword); + inputRef.current?.blur(); + }; + + const handleClear = () => { + setKeyword(''); + inputRef.current?.focus(); + }; + + /** NOTE: 아래 전체 위시 탭 레이아웃을 건드리지 않도록 그 위에 고정 레이어로 덮음 */ + return ( +
    +
    +
    + + + } + center={

    내 위시 검색

    } + /> +
    + setKeyword(event.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + autoFocus + right={ + keyword && + !isFocused && ( + + ) + } + /> +
    +
    + + {isSearching ? ( +
    + ) : ( +
    +

    + 검색 결과 {results.length}개 +

    + {results.length > 0 ? ( + + ) : ( +
    + +

    일치하는 상품이 없어요.

    +
    + )} +
    + )} +
    + ); +} + +export default WishSearch; From 2c737307540488597be620b13f39c7d4468ed318 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Fri, 23 Oct 2026 17:53:03 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=ED=8F=B4=EB=8D=94=20=EB=8D=94?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EB=A9=94=EB=89=B4=EC=99=80=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=9E=85=EB=A0=A5=20=EC=8B=9C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 폴더 카드 더보기 메뉴: `폴더명 변경하기`·`삭제하기` - 메뉴가 닫힐 때 트리거로 포커스 복귀 막음 - 원인: 이어 열리는 시트 입력창의 autoFocus 를 뺏음 - 새 폴더·폴더명 변경용 바텀시트 - 폴더 0개 빈 상태와 `폴더 추가하기` 버튼 --- .../app/wish/_components/FolderEmptyState.tsx | 25 +++++++++++ .../src/app/wish/_components/FolderMenu.tsx | 41 +++++++++++++++++++ .../app/wish/_components/FolderNameSheet.tsx | 37 +++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 apps/web/src/app/wish/_components/FolderEmptyState.tsx create mode 100644 apps/web/src/app/wish/_components/FolderMenu.tsx create mode 100644 apps/web/src/app/wish/_components/FolderNameSheet.tsx diff --git a/apps/web/src/app/wish/_components/FolderEmptyState.tsx b/apps/web/src/app/wish/_components/FolderEmptyState.tsx new file mode 100644 index 00000000..ef56563b --- /dev/null +++ b/apps/web/src/app/wish/_components/FolderEmptyState.tsx @@ -0,0 +1,25 @@ +import BasketGuestIcon from '@/assets/images/basket-guest.svg'; +import Button from '@/components/button'; + +type FolderEmptyStateProps = { + onAdd: () => void; +}; + +function FolderEmptyState({ onAdd }: FolderEmptyStateProps) { + return ( +
    + +

    + 아직 생성한 폴더가 없어요. + + 폴더를 생성하고 위시를 관리해보세요. + +

    + +
    + ); +} + +export default FolderEmptyState; diff --git a/apps/web/src/app/wish/_components/FolderMenu.tsx b/apps/web/src/app/wish/_components/FolderMenu.tsx new file mode 100644 index 00000000..dd6f293c --- /dev/null +++ b/apps/web/src/app/wish/_components/FolderMenu.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { FolderIconFill, ThreeDotVerticalIconFill, TrashIconFill } from '@/assets/icons'; +import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@/components/popover'; + +type FolderMenuProps = { + folderName: string; + onRename: () => void; + onDelete: () => void; +}; + +const MENU_ITEM_CLASS_NAME = + 'flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-3 body-2-medium text-text-neutral-primary hover:bg-bg-layer-basement focus-visible:bg-bg-layer-basement focus-visible:outline-none'; + +function FolderMenu({ folderName, onRename, onDelete }: FolderMenuProps) { + return ( + + + + + {/** NOTE: 메뉴가 닫히며 트리거로 포커스를 되돌리면 이어 열리는 시트 입력창의 autoFocus 를 뺏음 */} + event.preventDefault()} + className="w-[191px] p-2 ring-0" + > + + + 폴더명 변경하기 + + + + 삭제하기 + + + + ); +} + +export default FolderMenu; diff --git a/apps/web/src/app/wish/_components/FolderNameSheet.tsx b/apps/web/src/app/wish/_components/FolderNameSheet.tsx new file mode 100644 index 00000000..14b029c4 --- /dev/null +++ b/apps/web/src/app/wish/_components/FolderNameSheet.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { Drawer, DrawerContent, DrawerDescription } from '@/components/drawer'; + +import FolderNameForm from '../_common/_components/FolderNameForm'; + +type FolderNameSheetProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + initialName?: string; + onSubmit: (name: string) => void; +}; + +function FolderNameSheet({ + open, + onOpenChange, + title, + initialName, + onSubmit, +}: FolderNameSheetProps) { + return ( + + + 폴더 이름을 입력하세요. + onOpenChange(false)} + onSubmit={onSubmit} + /> + + + ); +} + +export default FolderNameSheet; From b34e1555108be271f1d2ed80f763ec0f12b70c58 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Fri, 23 Oct 2026 17:53:03 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=ED=8F=B4=EB=8D=94=EC=97=90=20?= =?UTF-8?q?=EC=9C=84=EC=8B=9C=20=EC=B6=94=EA=B0=80=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 담기 방법 선택 모달: 내 위시에서 가져오기·링크·이미지 - 링크·이미지는 기존 담기 다이얼로그 재사용 - 이 폴더에도 저장하는 API 연결은 TODO - 저장한 위시 선택 화면(/wish/folder/[folderId]/add) - 이미 이 폴더에 있는 위시는 숨김 - 0개 선택 시 추가 버튼 비활성 --- .../_components/FolderWishAddDialog.tsx | 69 +++++++++++++++ .../_components/FolderWishSelectContent.tsx | 86 +++++++++++++++++++ .../app/wish/folder/[folderId]/add/page.tsx | 19 ++++ 3 files changed, 174 insertions(+) create mode 100644 apps/web/src/app/wish/folder/[folderId]/_components/FolderWishAddDialog.tsx create mode 100644 apps/web/src/app/wish/folder/[folderId]/add/_components/FolderWishSelectContent.tsx create mode 100644 apps/web/src/app/wish/folder/[folderId]/add/page.tsx diff --git a/apps/web/src/app/wish/folder/[folderId]/_components/FolderWishAddDialog.tsx b/apps/web/src/app/wish/folder/[folderId]/_components/FolderWishAddDialog.tsx new file mode 100644 index 00000000..b0c827c2 --- /dev/null +++ b/apps/web/src/app/wish/folder/[folderId]/_components/FolderWishAddDialog.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useState } from 'react'; + +import { HeartIconFill, ImageIconFill, LinkIconFill } from '@/assets/icons'; +import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/dialog'; +import ByImageDialog from '@/components/get-item-dialog/ByImageDialog'; +import ByLinkDialog from '@/components/get-item-dialog/ByLinkDialog'; +import OptionButton from '@/components/get-item-dialog/OptionButton'; +import { ROUTES } from '@/consts/route'; + +type FolderWishAddDialogProps = { + folderId: number; + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +/** TODO: 링크·이미지로 담은 위시를 이 폴더에도 저장하는 API 연결 */ +function FolderWishAddDialog({ folderId, open, onOpenChange }: FolderWishAddDialogProps) { + const [subDialog, setSubDialog] = useState<'link' | 'image' | null>(null); + + return ( + <> + + + + 폴더에 위시 추가 + + + 저장한 위시, 링크, 이미지 중 하나를 선택해 폴더에 위시를 추가합니다. + +
      + + setSubDialog('link')} + /> + setSubDialog('image')} + /> +
    +
    +
    + + setSubDialog(isOpen ? 'link' : null)} + /> + setSubDialog(isOpen ? 'image' : null)} + /> + + ); +} + +export default FolderWishAddDialog; diff --git a/apps/web/src/app/wish/folder/[folderId]/add/_components/FolderWishSelectContent.tsx b/apps/web/src/app/wish/folder/[folderId]/add/_components/FolderWishSelectContent.tsx new file mode 100644 index 00000000..1b4e11ad --- /dev/null +++ b/apps/web/src/app/wish/folder/[folderId]/add/_components/FolderWishSelectContent.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { toast } from 'sonner'; + +import { CloseIconFill } from '@/assets/icons'; +import BottomCta from '@/components/bottom-cta'; +import Button from '@/components/button'; +import { Header } from '@/components/header'; +import { ROUTES } from '@/consts/route'; +import { Z_INDEX } from '@/consts/zIndex'; +import { useBackWithFallback } from '@/hooks/useBackWithFallback'; + +import WishGrid from '../../../../_common/_components/WishGrid'; +import { MOCK_FOLDERS, MOCK_WISHES } from '../../../../_common/_consts/wishMock'; +import { useWishEditMode } from '../../../../_common/_hooks/useWishEditMode'; + +const MAX_DISPLAY_COUNT = 999; + +type FolderWishSelectContentProps = { + folderId: number | null; +}; + +function FolderWishSelectContent({ folderId }: FolderWishSelectContentProps) { + const router = useRouter(); + const backWithFallback = useBackWithFallback(); + + const folder = MOCK_FOLDERS.find(({ id }) => id === folderId); + /** TEMP: API 연동 시 위시 목록 조회 응답으로 교체 */ + const candidates = MOCK_WISHES.filter(({ wish }) => !folder?.wishIds.includes(wish.id)); + + const { selectedWishIds, handleToggleSelect } = useWishEditMode(); + + useEffect(() => { + if (folder) return; + + toast.error('폴더를 찾을 수 없어요.'); + router.replace(ROUTES.WISHLIST_FOLDER_TAB); + }, [folder, router]); + + if (!folder) return null; + + /** TODO: 폴더에 위시 추가 API 연결 */ + const handleSubmit = () => backWithFallback(ROUTES.WISHLIST_FOLDER(folder.id)); + + return ( + <> +
    +
    폴더에 위시 추가} + right={ + + } + /> +

    + 상품 {candidates.length > MAX_DISPLAY_COUNT ? `${MAX_DISPLAY_COUNT}+` : candidates.length} +

    +
    + + + + + + + + ); +} + +export default FolderWishSelectContent; diff --git a/apps/web/src/app/wish/folder/[folderId]/add/page.tsx b/apps/web/src/app/wish/folder/[folderId]/add/page.tsx new file mode 100644 index 00000000..dbc427a5 --- /dev/null +++ b/apps/web/src/app/wish/folder/[folderId]/add/page.tsx @@ -0,0 +1,19 @@ +import { parseIdParam } from '@/utils/parseIdParam'; + +import FolderWishSelectContent from './_components/FolderWishSelectContent'; + +type FolderWishAddPageProps = { + params: Promise<{ folderId: string }>; +}; + +async function FolderWishAddPage({ params }: FolderWishAddPageProps) { + const { folderId } = await params; + + return ( +
    + +
    + ); +} + +export default FolderWishAddPage; From 6ff64bd35341d597d6c66e9d5865e6a698c8f2dd Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Fri, 23 Oct 2026 17:53:03 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EA=B3=BC=20=ED=8F=B4=EB=8D=94=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 위시 탭 - 타이틀 옆 검색·탭 아이콘 제거 - 요약 줄에 `검색 | 편집` 배치 - 그리드 첫 칸에 `상품 추가하기` 카드 추가 - 헤더 배경을 흰색으로 변경 - 폴더 탭 - 요약 줄 `편집` 제거 - 폴더 카드에 더보기 메뉴 연결 - 위시 0-2개 폴더 표지의 빈 칸을 빈 표지 패턴으로 채움 - 위시 1개 폴더는 카드 전체에 1장 - 원인: 정책 메모와 시안이 다름·시안 기준 - 폴더 생성·이름 변경·삭제 연결 - 삭제 모달 조사는 es-hangul 로 처리 - 폴더 상세 - 헤더 검색 제거 - 요약 줄을 `상품 N`·`추가 | 편집`으로 변경 - 편집 중 `폴더명 | 전체(n)`·`전체 선택` 노출 - 폴더 목업을 표지 경우별(3개 이상·3·2·1·0개)로 정리 - 폴더 탭 상태를 목록 화면으로 끌어올림 - 원인: 요약 줄 폴더 수와 폴더 탭이 같은 상태 필요 --- .../app/wish/_assets/folder-cover-empty.svg | 5 + .../app/wish/_common/_components/WishGrid.tsx | 6 +- .../src/app/wish/_common/_consts/wishMock.ts | 7 +- .../wish/_components/AllWishTabContent.tsx | 10 +- .../app/wish/_components/FolderAddCard.tsx | 15 ++- .../src/app/wish/_components/FolderCard.tsx | 76 +++++++++++---- .../app/wish/_components/FolderTabContent.tsx | 93 +++++++++++++++---- .../src/app/wish/_components/WishAddCard.tsx | 18 ++++ .../app/wish/_components/WishListContent.tsx | 59 ++++++++++-- .../src/app/wish/_components/WishTabBar.tsx | 25 +---- .../app/wish/_components/WishTabContent.tsx | 13 ++- .../app/wish/_components/WishTabSummary.tsx | 34 ++++--- apps/web/src/app/wish/_types/wishTab.ts | 14 +++ .../_components/FolderDetailContent.tsx | 73 +++++++++++---- apps/web/src/consts/route.ts | 2 +- 15 files changed, 333 insertions(+), 117 deletions(-) create mode 100644 apps/web/src/app/wish/_assets/folder-cover-empty.svg create mode 100644 apps/web/src/app/wish/_components/WishAddCard.tsx diff --git a/apps/web/src/app/wish/_assets/folder-cover-empty.svg b/apps/web/src/app/wish/_assets/folder-cover-empty.svg new file mode 100644 index 00000000..733d0d80 --- /dev/null +++ b/apps/web/src/app/wish/_assets/folder-cover-empty.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/app/wish/_common/_components/WishGrid.tsx b/apps/web/src/app/wish/_common/_components/WishGrid.tsx index d684aa7f..e4dd74c2 100644 --- a/apps/web/src/app/wish/_common/_components/WishGrid.tsx +++ b/apps/web/src/app/wish/_common/_components/WishGrid.tsx @@ -1,3 +1,5 @@ +import type { ReactNode } from 'react'; + import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons'; import { Z_INDEX } from '@/consts/zIndex'; @@ -8,15 +10,17 @@ type WishGridProps = { wishes: WishListItemT[]; selectedWishIds?: Set; onToggleSelect?: (wishId: number) => void; + leadingItem?: ReactNode; }; /** 이미지 preload 개수 */ const PRELOAD_COUNT = 6; /** 위시 3열 그리드 **/ -function WishGrid({ wishes, selectedWishIds, onToggleSelect }: WishGridProps) { +function WishGrid({ wishes, selectedWishIds, onToggleSelect, leadingItem }: WishGridProps) { return (
      + {leadingItem &&
    • {leadingItem}
    • } {wishes.map(({ wish, item, winnerTournamentIds }, index) => { const card = ( diff --git a/apps/web/src/app/wish/_components/AllWishTabContent.tsx b/apps/web/src/app/wish/_components/AllWishTabContent.tsx index 9405f1ff..8a494890 100644 --- a/apps/web/src/app/wish/_components/AllWishTabContent.tsx +++ b/apps/web/src/app/wish/_components/AllWishTabContent.tsx @@ -1,12 +1,12 @@ -import type { ComponentProps } from 'react'; - import WishGrid from '../_common/_components/WishGrid'; - -export type AllWishTabContentProps = ComponentProps; +import type { AllWishTabContentProps } from '../_types/wishTab'; +import WishAddCard from './WishAddCard'; function AllWishTabContent(props: AllWishTabContentProps) { + const isEditMode = Boolean(props.selectedWishIds); + /** TODO: 위시 상세 링크 연결 시 saveScrollAnchor·useScrollRestoration(scope: all) 추가 */ - return ; + return } />; } export default AllWishTabContent; diff --git a/apps/web/src/app/wish/_components/FolderAddCard.tsx b/apps/web/src/app/wish/_components/FolderAddCard.tsx index 6eda7fca..0aa05c95 100644 --- a/apps/web/src/app/wish/_components/FolderAddCard.tsx +++ b/apps/web/src/app/wish/_components/FolderAddCard.tsx @@ -1,11 +1,18 @@ import { AddIconOutline } from '@/assets/icons'; -// TODO: 폴더 추가 동작 연결 -function FolderAddCard() { +type FolderAddCardProps = { + onClick: () => void; +}; + +function FolderAddCard({ onClick }: FolderAddCardProps) { return ( - + ); +} + +export default WishAddCard; diff --git a/apps/web/src/app/wish/_components/WishListContent.tsx b/apps/web/src/app/wish/_components/WishListContent.tsx index c5cd5cc6..af7c3ed7 100644 --- a/apps/web/src/app/wish/_components/WishListContent.tsx +++ b/apps/web/src/app/wish/_components/WishListContent.tsx @@ -1,24 +1,34 @@ 'use client'; +import { useSearchParams } from 'next/navigation'; import { useState } from 'react'; import { toast } from 'sonner'; import { Z_INDEX } from '@/consts/zIndex'; +import { cn } from '@/utils/cn'; import WishEditActionBar from '../_common/_components/WishEditActionBar'; import WishEditHeader from '../_common/_components/WishEditHeader'; -import WishSearchLink from '../_common/_components/WishSearchLink'; -import { MOCK_WISHES } from '../_common/_consts/wishMock'; +import { MOCK_FOLDERS, MOCK_WISHES } from '../_common/_consts/wishMock'; import { WISH_MODE } from '../_common/_consts/wishMode'; import { useWishEditMode } from '../_common/_hooks/useWishEditMode'; import type { WishFolderT } from '../_common/_types/wish'; +import { WISH_TAB } from '../_consts/wishTab'; +import { parseWishTabParam } from '../_utils/wishSearchParams'; +import WishSearch from './WishSearch'; import WishTabBar from './WishTabBar'; import WishTabContent from './WishTabContent'; import WishTabSummary from './WishTabSummary'; function WishListContent() { + const searchParams = useSearchParams(); + const isFolderTab = parseWishTabParam(searchParams.get('tab')) === WISH_TAB.FOLDER; + /** TEMP: API 연동 시 위시 목록 조회 응답으로 교체 */ const [wishes, setWishes] = useState(MOCK_WISHES); + /** TEMP: API 연동 시 폴더 목록 조회·생성·수정·삭제 API 로 교체 */ + const [folders, setFolders] = useState(MOCK_FOLDERS); + const [isSearchOpen, setIsSearchOpen] = useState(false); const { mode, selectedWishIds, @@ -41,28 +51,51 @@ function WishListContent() { toast.success(`상품 ${selectedWishIds.size}개를 삭제했어요.`); }; + const handleCreateFolder = (name: string) => + setFolders(prev => [...prev, { id: Date.now(), name, wishIds: [] }]); + + const handleRenameFolder = (folderId: number, name: string) => + setFolders(prev => prev.map(folder => (folder.id === folderId ? { ...folder, name } : folder))); + + const handleDeleteFolder = (folderId: number) => + setFolders(prev => prev.filter(({ id }) => id !== folderId)); + return ( <>
      {mode === WISH_MODE.EDIT ? ( ) : ( <> -
      -

      내 위시리스트

      - -
      +

      내 위시리스트

      )} - + setIsSearchOpen(true)} + />
      {mode === WISH_MODE.EDIT && ( @@ -70,8 +103,14 @@ function WishListContent() { selectedCount={selectedWishIds.size} onMove={handleMove} onDelete={handleDelete} + deleteConfirm={{ + title: '상품을 정말 삭제할까요?', + description: '삭제 후에는 다시 복구할 수 없어요.', + }} /> )} + + {isSearchOpen && setIsSearchOpen(false)} />} ); } diff --git a/apps/web/src/app/wish/_components/WishTabBar.tsx b/apps/web/src/app/wish/_components/WishTabBar.tsx index ed45e32d..db0b47af 100644 --- a/apps/web/src/app/wish/_components/WishTabBar.tsx +++ b/apps/web/src/app/wish/_components/WishTabBar.tsx @@ -1,9 +1,7 @@ 'use client'; import { useSearchParams } from 'next/navigation'; -import type { ComponentType, SVGProps } from 'react'; -import { FolderIconOutline, HeartIconOutline } from '@/assets/icons'; import { ROUTES } from '@/consts/route'; import { cn } from '@/utils/cn'; import { replaceUrlKeepingState } from '@/utils/replaceUrlKeepingState'; @@ -12,19 +10,9 @@ import { WISH_TAB, WISH_TAB_PANEL_ID } from '../_consts/wishTab'; import type { WishTabT } from '../_types/wishTab'; import { parseWishTabParam } from '../_utils/wishSearchParams'; -const TABS: { - value: WishTabT; - label: string; - Icon: ComponentType>; - path: string; -}[] = [ - { value: WISH_TAB.ALL, label: '전체 위시', Icon: HeartIconOutline, path: ROUTES.WISH }, - { - value: WISH_TAB.FOLDER, - label: '폴더', - Icon: FolderIconOutline, - path: ROUTES.WISHLIST_FOLDER_TAB, - }, +const TABS: { value: WishTabT; label: string; path: string }[] = [ + { value: WISH_TAB.ALL, label: '전체 위시', path: ROUTES.WISH }, + { value: WISH_TAB.FOLDER, label: '폴더', path: ROUTES.WISHLIST_FOLDER_TAB }, ]; function WishTabBar() { @@ -33,7 +21,7 @@ function WishTabBar() { return (
      - {TABS.map(({ value, label, Icon, path }) => { + {TABS.map(({ value, label, path }) => { const isActive = activeTab === value; return ( +
      + + + +
      )}
      ); diff --git a/apps/web/src/app/wish/_types/wishTab.ts b/apps/web/src/app/wish/_types/wishTab.ts index 780caa6f..fb7775ab 100644 --- a/apps/web/src/app/wish/_types/wishTab.ts +++ b/apps/web/src/app/wish/_types/wishTab.ts @@ -1,3 +1,17 @@ +import type { WishFolderT, WishListItemT } from '../_common/_types/wish'; import type { WISH_TAB } from '../_consts/wishTab'; export type WishTabT = (typeof WISH_TAB)[keyof typeof WISH_TAB]; + +export type AllWishTabContentProps = { + wishes: WishListItemT[]; + selectedWishIds?: Set; + onToggleSelect?: (wishId: number) => void; +}; + +export type FolderTabContentProps = { + folders: WishFolderT[]; + onCreate: (name: string) => void; + onRename: (folderId: number, name: string) => void; + onDelete: (folderId: number) => void; +}; diff --git a/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx b/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx index 9e300708..a8c5e092 100644 --- a/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx +++ b/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx @@ -14,11 +14,13 @@ import { useBackWithFallback } from '@/hooks/useBackWithFallback'; import WishEditActionBar from '../../../_common/_components/WishEditActionBar'; import WishEditHeader from '../../../_common/_components/WishEditHeader'; import WishGrid from '../../../_common/_components/WishGrid'; -import WishSearchLink from '../../../_common/_components/WishSearchLink'; import { MOCK_FOLDERS, MOCK_WISHES } from '../../../_common/_consts/wishMock'; import { WISH_MODE } from '../../../_common/_consts/wishMode'; import { useWishEditMode } from '../../../_common/_hooks/useWishEditMode'; import type { WishFolderT } from '../../../_common/_types/wish'; +import FolderWishAddDialog from './FolderWishAddDialog'; + +const MAX_DISPLAY_COUNT = 999; type FolderDetailContentProps = { folderId: number | null; @@ -34,11 +36,13 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) { const [wishes, setWishes] = useState(() => MOCK_WISHES.filter(({ wish }) => folder?.wishIds.includes(wish.id)) ); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const { mode, selectedWishIds, handleToggleSelect, handleClearSelection, + handleSelectAll, handleEnterEditMode, handleExitEditMode, } = useWishEditMode(); @@ -60,7 +64,7 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) { toast.success(`상품을 '${targetFolder.name}' 폴더로 이동했어요.`); }; - /** TODO: 폴더에서 빼기·위시 삭제 중 동작 확정 후 API 연결 */ + /** TODO: 폴더에서 빼기 API 연결 */ const handleDelete = () => { setWishes(prev => prev.filter(({ wish }) => !selectedWishIds.has(wish.id))); handleClearSelection(); @@ -70,7 +74,7 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) { return ( <>
      {mode === WISH_MODE.EDIT ? ( @@ -95,31 +99,51 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) { {folder.name} } - right={} />
      )} -
      -

      - {folder.name} - - 전체({wishes.length}) -

      - {mode === WISH_MODE.DEFAULT && ( - +
      + {mode === WISH_MODE.EDIT ? ( + <> +

      + {folder.name} + + 전체({wishes.length}) +

      + + + ) : ( + <> +

      + 상품 {wishes.length > MAX_DISPLAY_COUNT ? `${MAX_DISPLAY_COUNT}+` : wishes.length} +

      +
      + + + +
      + )}
      - {/* TODO: 위시 상세 링크 연결 시 saveScrollAnchor·useScrollRestoration(scope: folderId) 추가 */} + {/** TODO: 위시 상세 링크 연결 시 saveScrollAnchor·useScrollRestoration(scope: folderId) 추가 */} )} + + ); } diff --git a/apps/web/src/consts/route.ts b/apps/web/src/consts/route.ts index 02328070..231aff35 100644 --- a/apps/web/src/consts/route.ts +++ b/apps/web/src/consts/route.ts @@ -25,7 +25,7 @@ export const ROUTES = { WISH: '/wish', // TEMP WISHLIST_FOLDER_TAB: '/wish?tab=folder', WISHLIST_FOLDER: (folderId: number) => `/wish/folder/${folderId}`, - WISHLIST_FOLDER_EDIT: '/wish/folder/edit', + WISHLIST_FOLDER_ADD: (folderId: number) => `/wish/folder/${folderId}/add`, NOTIFICATION: '/notification', MYPAGE_EDIT: '/mypage/edit', MYPAGE_WITHDRAW: '/mypage/withdraw', From 28dd7065e49fd6beac22b11c895c0f0a6efedae7 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 26 Oct 2026 10:58:50 +0900 Subject: [PATCH 08/17] =?UTF-8?q?style:=20=ED=99=95=EC=9D=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=ED=81=AC=EA=B8=B0=EC=99=80=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=ED=8F=AC=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=95=88=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 너비를 콘텐츠 영역 기준 양쪽 36px 마진으로 변경 - 이전: 화면 너비 - 70px, 최대 410px - 아이콘 없는 모달 상단 패딩 32px - 제목 heading-2-semibold·설명 body-2-medium 으로 변경 - 공통 컴포넌트라 ConfirmDialog 사용처 전체에 적용 --- apps/web/src/components/common/confirm-dialog/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/common/confirm-dialog/index.tsx b/apps/web/src/components/common/confirm-dialog/index.tsx index f3d7fbbd..489a1ab0 100644 --- a/apps/web/src/components/common/confirm-dialog/index.tsx +++ b/apps/web/src/components/common/confirm-dialog/index.tsx @@ -45,11 +45,14 @@ function ConfirmDialog({ return ( - + {icon &&
      {icon}
      } - {title} - + {title} + {description} From 7c52f25da283039dddc534b96bf1fbf08d89068d Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 26 Oct 2026 10:58:50 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=8B=9C=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=ED=8B=80=20=EC=A0=91=ED=9E=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스크롤이 52px 초과하면 `내 위시리스트` 타이틀 접힘 - 탭·요약 줄만 상단에 고정 - 맨 위로 돌아오면 타이틀 펼침 - 숨김·노출 기준을 분리 - 원인: 타이틀이 접히며 헤더 높이가 줄어 같은 기준이면 깜빡임 --- .../app/wish/_components/WishListContent.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/wish/_components/WishListContent.tsx b/apps/web/src/app/wish/_components/WishListContent.tsx index af7c3ed7..333dc213 100644 --- a/apps/web/src/app/wish/_components/WishListContent.tsx +++ b/apps/web/src/app/wish/_components/WishListContent.tsx @@ -1,11 +1,12 @@ 'use client'; import { useSearchParams } from 'next/navigation'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { Z_INDEX } from '@/consts/zIndex'; import { cn } from '@/utils/cn'; +import { getScrollContainer } from '@/utils/scrollRestoration'; import WishEditActionBar from '../_common/_components/WishEditActionBar'; import WishEditHeader from '../_common/_components/WishEditHeader'; @@ -20,6 +21,8 @@ import WishTabBar from './WishTabBar'; import WishTabContent from './WishTabContent'; import WishTabSummary from './WishTabSummary'; +const TITLE_HEIGHT_WITH_GAP = 52; + function WishListContent() { const searchParams = useSearchParams(); const isFolderTab = parseWishTabParam(searchParams.get('tab')) === WISH_TAB.FOLDER; @@ -29,6 +32,7 @@ function WishListContent() { /** TEMP: API 연동 시 폴더 목록 조회·생성·수정·삭제 API 로 교체 */ const [folders, setFolders] = useState(MOCK_FOLDERS); const [isSearchOpen, setIsSearchOpen] = useState(false); + const [isTitleHidden, setIsTitleHidden] = useState(false); const { mode, selectedWishIds, @@ -60,6 +64,19 @@ function WishListContent() { const handleDeleteFolder = (folderId: number) => setFolders(prev => prev.filter(({ id }) => id !== folderId)); + /** 스크롤 시 제목 부분을 숨김/노출 처리 함 */ + useEffect(() => { + const scroller = getScrollContainer(); + if (!scroller) return; + + const handleScroll = () => { + if (scroller.scrollTop > TITLE_HEIGHT_WITH_GAP) setIsTitleHidden(true); + else if (scroller.scrollTop <= 0) setIsTitleHidden(false); + }; + scroller.addEventListener('scroll', handleScroll, { passive: true }); + return () => scroller.removeEventListener('scroll', handleScroll); + }, []); + return ( <>
      ) : ( <> -

      내 위시리스트

      +
      +

      + 내 위시리스트 +

      +
      )} From 265ed4f4a9c6e1fbfeebed028d5e9c03eef7fb68 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 26 Oct 2026 13:37:24 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20=ED=8F=B4=EB=8D=94=EB=AA=85=2020?= =?UTF-8?q?=EC=9E=90=20=EC=B4=88=EA=B3=BC=20=EC=9E=85=EB=A0=A5=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 20자를 넘겨도 입력은 그대로 받고 안내 문구 표시 - 완료 버튼은 20자 이하가 될 때까지 비활성 - 이전: 20자에서 입력을 잘라냄 - 원인: 붙여넣기로 넘친 이름을 지우면서 고칠 수 있어야 함 - 빈 이름 에러는 다시 입력하면 사라짐 --- .../_common/_components/FolderNameForm.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx index ae4a5f8b..81df7cfd 100644 --- a/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx +++ b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { type ChangeEvent, type FormEvent, useState } from 'react'; +import { type FormEvent, useState } from 'react'; import { ChevronBackwardIconFill } from '@/assets/icons'; import Button from '@/components/button'; @@ -13,8 +13,7 @@ const MAX_FOLDER_NAME_LENGTH = 20; const graphemeSegmenter = new Intl.Segmenter('ko', { granularity: 'grapheme' }); /** NOTE: 이모지도 1자로 세야 해서 length 대신 grapheme 단위로 셈 */ -const splitGraphemes = (value: string) => - Array.from(graphemeSegmenter.segment(value), ({ segment }) => segment); +const countGraphemes = (value: string) => Array.from(graphemeSegmenter.segment(value)).length; type FolderNameFormProps = { title: string; @@ -25,23 +24,20 @@ type FolderNameFormProps = { function FolderNameForm({ title, initialName = '', onBack, onSubmit }: FolderNameFormProps) { const [name, setName] = useState(initialName); - const [errorMessage, setErrorMessage] = useState(null); + const [isEmptySubmitted, setIsEmptySubmitted] = useState(false); const trimmedName = name.trim(); const isUnchanged = initialName !== '' && trimmedName === initialName; + const isOverLimit = countGraphemes(trimmedName) > MAX_FOLDER_NAME_LENGTH; - const handleChange = (event: ChangeEvent) => { - const graphemes = splitGraphemes(event.target.value); - const isOverLimit = graphemes.length >= MAX_FOLDER_NAME_LENGTH; - - setName(graphemes.slice(0, MAX_FOLDER_NAME_LENGTH).join('')); - setErrorMessage(isOverLimit ? `최대 ${MAX_FOLDER_NAME_LENGTH}자까지 입력할 수 있어요.` : null); - }; + let errorMessage: string | null = null; + if (isOverLimit) errorMessage = `최대 ${MAX_FOLDER_NAME_LENGTH}자까지 입력할 수 있어요.`; + else if (isEmptySubmitted) errorMessage = '폴더 이름을 입력해 주세요.'; const handleSubmit = (event: FormEvent) => { event.preventDefault(); if (!trimmedName) { - setErrorMessage('폴더 이름을 입력해 주세요.'); + setIsEmptySubmitted(true); return; } @@ -67,12 +63,15 @@ function FolderNameForm({ title, initialName = '', onBack, onSubmit }: FolderNam aria-label="폴더명" placeholder="폴더명을 입력해주세요." value={name} - onChange={handleChange} + onChange={event => { + setName(event.target.value); + setIsEmptySubmitted(false); + }} aria-invalid={errorMessage !== null} {...(errorMessage && { helperText: errorMessage })} autoFocus /> -
      From cfa5f22127181412b83a9ca0a7af67063b3ed1d0 Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 26 Oct 2026 13:37:24 +0900 Subject: [PATCH 11/17] =?UTF-8?q?style:=20=ED=8F=B4=EB=8D=94=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=8B=9C=ED=8A=B8=20=EC=97=AC=EB=B0=B1=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 손잡이와 헤더 사이 간격 20px 에서 12px 로 줄임 - 에러 문구가 떠도 완료 버튼 위치 유지 - 에러가 있으면 입력창 아래 간격을 gap-9 에서 gap-2 로 바꿈 - 문구 위 8px · 문구 20px · 문구 아래 8px 합이 36px 라 버튼이 밀리지 않음 --- apps/web/src/app/wish/_common/_components/FolderNameForm.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx index 81df7cfd..06c21637 100644 --- a/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx +++ b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx @@ -7,6 +7,7 @@ import Button from '@/components/button'; import { DrawerTitle } from '@/components/drawer'; import { Header } from '@/components/header'; import Input from '@/components/input'; +import { cn } from '@/utils/cn'; const MAX_FOLDER_NAME_LENGTH = 20; @@ -45,7 +46,7 @@ function FolderNameForm({ title, initialName = '', onBack, onSubmit }: FolderNam }; return ( -
      +
      @@ -58,7 +59,7 @@ function FolderNameForm({ title, initialName = '', onBack, onSubmit }: FolderNam } /> -
      +
      Date: Mon, 26 Oct 2026 13:37:24 +0900 Subject: [PATCH 12/17] =?UTF-8?q?style:=20=EC=9C=84=EC=8B=9C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=C2=B7=EB=8B=AB?= =?UTF-8?q?=EA=B8=B0=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=83=89=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 폴더 이름 시트 · 폴더 상세 · 폴더에 위시 추가 아이콘은 icon-neutral-primary - 위시 검색 뒤로가기만 icon-neutral-secondary --- apps/web/src/app/wish/_common/_components/FolderNameForm.tsx | 2 +- apps/web/src/app/wish/_components/WishSearch.tsx | 2 +- .../wish/folder/[folderId]/_components/FolderDetailContent.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx index 06c21637..ab936d91 100644 --- a/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx +++ b/apps/web/src/app/wish/_common/_components/FolderNameForm.tsx @@ -50,7 +50,7 @@ function FolderNameForm({ title, initialName = '', onBack, onSubmit }: FolderNam
      - + } center={ diff --git a/apps/web/src/app/wish/_components/WishSearch.tsx b/apps/web/src/app/wish/_components/WishSearch.tsx index 0a0de4e0..a9851336 100644 --- a/apps/web/src/app/wish/_components/WishSearch.tsx +++ b/apps/web/src/app/wish/_components/WishSearch.tsx @@ -70,7 +70,7 @@ function WishSearch({ wishes, onClose }: WishSearchProps) { onClick={onClose} className="cursor-pointer" > - + } center={

      내 위시 검색

      } diff --git a/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx b/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx index a8c5e092..6eaae9fc 100644 --- a/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx +++ b/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx @@ -91,7 +91,7 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) { onClick={() => backWithFallback(ROUTES.WISHLIST_FOLDER_TAB)} className="cursor-pointer" > - + } center={ From 0c84fa2bccd9f21fc7121d72f89d29051bc97fae Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 26 Oct 2026 14:04:58 +0900 Subject: [PATCH 13/17] =?UTF-8?q?fix:=20=EC=9C=84=EC=8B=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=9E=85=EB=A0=A5=EC=B0=BD=20=EC=A7=80=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입력창 type="search" 제거 - 원인: 브라우저 기본 지우기 버튼이 직접 만든 버튼과 함께 표시됨 - 모바일 키보드의 검색 키는 enterKeyHint 로 유지 --- apps/web/src/app/wish/_components/WishSearch.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/app/wish/_components/WishSearch.tsx b/apps/web/src/app/wish/_components/WishSearch.tsx index a9851336..e60b6472 100644 --- a/apps/web/src/app/wish/_components/WishSearch.tsx +++ b/apps/web/src/app/wish/_components/WishSearch.tsx @@ -78,7 +78,6 @@ function WishSearch({ wishes, onClose }: WishSearchProps) { Date: Mon, 26 Oct 2026 14:04:58 +0900 Subject: [PATCH 14/17] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=9E=85=EB=A0=A5=20=EC=A4=91=20=EC=A7=80=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검색어가 있으면 입력 중에도 지우기 버튼 표시 - 이전: 검색 실행 뒤에만 표시 - 지우기 버튼을 눌러도 입력창 포커스 유지 - 결과: 키보드가 내려가지 않고 바로 다시 입력할 수 있음 --- apps/web/src/app/wish/_components/WishSearch.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/wish/_components/WishSearch.tsx b/apps/web/src/app/wish/_components/WishSearch.tsx index e60b6472..8ec410f6 100644 --- a/apps/web/src/app/wish/_components/WishSearch.tsx +++ b/apps/web/src/app/wish/_components/WishSearch.tsx @@ -87,11 +87,11 @@ function WishSearch({ wishes, onClose }: WishSearchProps) { onBlur={() => setIsFocused(false)} autoFocus right={ - keyword && - !isFocused && ( + keyword && ( - } - center={

      내 위시 검색

      } - /> - - setKeyword(event.target.value)} - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} - autoFocus - right={ - keyword && ( - - ) + + 상품명을 입력하고 검색하면 일치하는 위시를 보여줘요. + +
      +
      + + + } + center={ + +

      내 위시 검색

      +
      } /> - -
      +
      + setKeyword(event.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + right={ + keyword && ( + + ) + } + /> +
      +
      - {isSearching ? ( -
      - ) : ( -
      -

      - 검색 결과 {results.length}개 -

      - {results.length > 0 ? ( - - ) : ( -
      - -

      일치하는 상품이 없어요.

      -
      - )} -
      - )} -
      + {isSearching ? ( +
      + ) : ( +
      +

      + 검색 결과 {results.length}개 +

      + {results.length > 0 ? ( + + ) : ( +
      + +

      일치하는 상품이 없어요.

      +
      + )} +
      + )} + +
      ); } From f18fbc9539de19a5adf15b6aa3edae85919675ac Mon Sep 17 00:00:00 2001 From: Jung Sun A Date: Mon, 26 Oct 2026 15:55:20 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix:=20=ED=8F=B4=EB=8D=94=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EB=8B=AB=ED=9E=98=20=EC=95=A0=EB=8B=88=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A4=91=20=EB=82=B4=EC=9A=A9=20=EA=B9=9C?= =?UTF-8?q?=EB=B9=A1=EC=9E=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이름 변경 시트 · 삭제 확인창의 열림 상태를 대상 폴더와 분리 - 닫을 때 열림 상태만 끄고 대상 폴더는 다음 열기 때 교체 - 원인: 닫히며 대상을 비워서 애니메이션 동안 제목이 undefined 로 바뀌고 입력창이 비었음 --- .../app/wish/_components/FolderTabContent.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/wish/_components/FolderTabContent.tsx b/apps/web/src/app/wish/_components/FolderTabContent.tsx index 48fe1b2d..c9d83c60 100644 --- a/apps/web/src/app/wish/_components/FolderTabContent.tsx +++ b/apps/web/src/app/wish/_components/FolderTabContent.tsx @@ -21,7 +21,9 @@ function FolderTabContent({ folders, onCreate, onRename, onDelete }: FolderTabCo const [isCreateSheetOpen, setIsCreateSheetOpen] = useState(false); const [renameTarget, setRenameTarget] = useState(null); + const [isRenameSheetOpen, setIsRenameSheetOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const handleCreate = (name: string) => { onCreate(name); @@ -30,12 +32,12 @@ function FolderTabContent({ folders, onCreate, onRename, onDelete }: FolderTabCo const handleRename = (name: string) => { if (renameTarget) onRename(renameTarget.id, name); - setRenameTarget(null); + setIsRenameSheetOpen(false); }; const handleDelete = () => { if (deleteTarget) onDelete(deleteTarget.id); - setDeleteTarget(null); + setIsDeleteDialogOpen(false); }; return ( @@ -54,8 +56,14 @@ function FolderTabContent({ folders, onCreate, onRename, onDelete }: FolderTabCo name={folder.name} wishCount={folder.wishIds.length} thumbnailUrls={folder.wishIds.slice(0, 3).map(getMockWishImageUrl)} - onRename={() => setRenameTarget(folder)} - onDelete={() => setDeleteTarget(folder)} + onRename={() => { + setRenameTarget(folder); + setIsRenameSheetOpen(true); + }} + onDelete={() => { + setDeleteTarget(folder); + setIsDeleteDialogOpen(true); + }} /> ))} @@ -70,15 +78,15 @@ function FolderTabContent({ folders, onCreate, onRename, onDelete }: FolderTabCo /> !isOpen && setRenameTarget(null)} + open={isRenameSheetOpen} + onOpenChange={setIsRenameSheetOpen} title="폴더명 변경" {...(renameTarget && { initialName: renameTarget.name })} onSubmit={handleRename} /> !isOpen && setDeleteTarget(null)} + open={isDeleteDialogOpen} + onOpenChange={setIsDeleteDialogOpen} title={`‘${deleteTarget?.name}’${josa.pick(deleteTarget?.name ?? '', '을/를')} 삭제할까요?`} description="폴더를 삭제해도 담아둔 위시는 그대로 유지돼요." confirmLabel="삭제하기"