diff --git a/apps/web/package.json b/apps/web/package.json
index a65128f9..447b43ee 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -14,6 +14,8 @@
"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:*",
@@ -25,6 +27,7 @@
"clsx": "^2.1.1",
"cropperjs": "1.6.2",
"embla-carousel-react": "^8.6.0",
+ "es-hangul": "^2.4.0",
"gsap": "^3.15.0",
"html-to-image": "^1.11.13",
"next": "16.2.0",
diff --git a/apps/web/src/app/wish/_common/_assets/folder-empty.svg b/apps/web/src/app/wish/_common/_assets/folder-empty.svg
new file mode 100644
index 00000000..16e25294
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_assets/folder-empty.svg
@@ -0,0 +1,5 @@
+
diff --git a/apps/web/src/app/wish/_common/_components/FolderListThumbnail.tsx b/apps/web/src/app/wish/_common/_components/FolderListThumbnail.tsx
new file mode 100644
index 00000000..fd1e03dd
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_components/FolderListThumbnail.tsx
@@ -0,0 +1,28 @@
+import BaseImage from '@/components/base-image';
+import { cn } from '@/utils/cn';
+
+import FolderEmptyImage from '../_assets/folder-empty.svg';
+
+type FolderListThumbnailProps = {
+ url: string | null;
+ className?: string;
+};
+
+function FolderListThumbnail({ url, className }: FolderListThumbnailProps) {
+ return (
+
+ {url ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default FolderListThumbnail;
diff --git a/apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx b/apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx
new file mode 100644
index 00000000..e1fdf25b
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx
@@ -0,0 +1,138 @@
+'use client';
+
+import { type FormEvent, useState } from 'react';
+
+import { ChevronBackwardIconFill, FolderAddIconFill } from '@/assets/icons';
+import Button from '@/components/button';
+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';
+
+type FolderSelectSheetProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ folders: WishFolderT[];
+ onSelect: (folder: WishFolderT) => void;
+ onAddFolder: (name: string) => void;
+};
+
+function FolderSelectSheet({
+ open,
+ onOpenChange,
+ folders,
+ onSelect,
+ 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();
+ onOpenChange(isOpen);
+ };
+
+ const handleAddSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ if (!trimmedFolderName) return;
+
+ onAddFolder(trimmedFolderName);
+ handleBackToList();
+ };
+
+ return (
+
+
+
+ {isAddView ? '새 폴더 이름을 입력하세요.' : '상품을 옮길 폴더를 선택하세요.'}
+
+ {isAddView ? (
+
+ ) : (
+
+
+ 폴더 선택
+
+ }
+ right={
+
+ }
+ />
+
+ {folders.map(folder => (
+ -
+
+
+ ))}
+
+
+ )}
+
+
+ );
+}
+
+export default FolderSelectSheet;
diff --git a/apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx b/apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx
new file mode 100644
index 00000000..619077f8
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx
@@ -0,0 +1,47 @@
+'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.tsx b/apps/web/src/app/wish/_common/_components/WishEditActionBar.tsx
new file mode 100644
index 00000000..88fe8393
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_components/WishEditActionBar.tsx
@@ -0,0 +1,120 @@
+'use client';
+
+import { type ComponentType, type SVGProps, useState } from 'react';
+
+import { FolderIconFill, TrashIconFill } from '@/assets/icons';
+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';
+
+type WishEditActionBarProps = {
+ selectedCount: number;
+ currentFolderId?: number;
+ onMove: (folder: WishFolderT) => void;
+ onDelete: () => void;
+};
+
+function ActionButton({
+ label,
+ Icon,
+ disabled,
+ onClick,
+}: {
+ label: string;
+ Icon: ComponentType>;
+ disabled: boolean;
+ onClick: () => void;
+}) {
+ return (
+
+ );
+}
+
+function WishEditActionBar({
+ selectedCount,
+ currentFolderId,
+ onMove,
+ onDelete,
+}: WishEditActionBarProps) {
+ const [isFolderSheetOpen, setIsFolderSheetOpen] = useState(false);
+ const [isDeleteSheetOpen, setIsDeleteSheetOpen] = useState(false);
+ /** TEMP: API 연동 시 폴더 목록 조회·생성 API 로 교체 */
+ const [folders, setFolders] = useState(() =>
+ MOCK_FOLDERS.filter(({ id }) => id !== currentFolderId)
+ );
+
+ const isDisabled = selectedCount === 0;
+
+ const handleAddFolder = (name: string) => {
+ setFolders(prev => [{ id: Date.now(), name, wishIds: [] }, ...prev]);
+ };
+
+ const handleSelectFolder = (folder: WishFolderT) => {
+ setIsFolderSheetOpen(false);
+ onMove(folder);
+ };
+
+ const handleConfirmDelete = () => {
+ setIsDeleteSheetOpen(false);
+ onDelete();
+ };
+
+ return (
+ <>
+
+
setIsFolderSheetOpen(true)}
+ />
+ setIsDeleteSheetOpen(true)}
+ />
+
+
+
+
+ >
+ );
+}
+
+export default WishEditActionBar;
diff --git a/apps/web/src/app/wish/_common/_components/WishEditHeader.tsx b/apps/web/src/app/wish/_common/_components/WishEditHeader.tsx
new file mode 100644
index 00000000..337c094b
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_components/WishEditHeader.tsx
@@ -0,0 +1,29 @@
+import { Header } from '@/components/header';
+
+type WishEditHeaderProps = {
+ selectedCount: number;
+ onDone: () => void;
+};
+
+function WishEditHeader({ selectedCount, onDone }: WishEditHeaderProps) {
+ return (
+
+ {selectedCount > 0 ? `상품 ${selectedCount}개 선택` : '상품 선택'}
+
+ }
+ right={
+
+ }
+ />
+ );
+}
+
+export default WishEditHeader;
diff --git a/apps/web/src/app/wish/_common/_components/WishGrid.tsx b/apps/web/src/app/wish/_common/_components/WishGrid.tsx
index ba2515e3..766d35db 100644
--- a/apps/web/src/app/wish/_common/_components/WishGrid.tsx
+++ b/apps/web/src/app/wish/_common/_components/WishGrid.tsx
@@ -1,19 +1,24 @@
+import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons';
+import { Z_INDEX } from '@/consts/zIndex';
+
import type { WishListItemT } from '../_types/wish';
import WishCard from './WishCard';
type WishGridProps = {
wishes: WishListItemT[];
+ selectedWishIds?: Set;
+ onToggleSelect?: (wishId: number) => void;
};
/** 이미지 preload 개수 */
const PRELOAD_COUNT = 6;
/** 위시 3열 그리드 **/
-function WishGrid({ wishes }: WishGridProps) {
+function WishGrid({ wishes, selectedWishIds, onToggleSelect }: WishGridProps) {
return (
- {wishes.map(({ wish, item }, index) => (
- -
+ {wishes.map(({ wish, item }, index) => {
+ const card = (
-
- ))}
+ );
+
+ if (!selectedWishIds) return - {card}
;
+
+ const isSelected = selectedWishIds.has(wish.id);
+ return (
+ -
+
+
+ );
+ })}
);
}
diff --git a/apps/web/src/app/wish/_common/_consts/wishMock.ts b/apps/web/src/app/wish/_common/_consts/wishMock.ts
index f7935663..44a3a394 100644
--- a/apps/web/src/app/wish/_common/_consts/wishMock.ts
+++ b/apps/web/src/app/wish/_common/_consts/wishMock.ts
@@ -25,4 +25,10 @@ export const MOCK_WISHES: WishListItemT[] = Array.from({ length: 12 }, (_, index
export const MOCK_FOLDERS: WishFolderT[] = [
{ id: 1, name: '가방 위시템', wishIds: [4, 1, 2, 3, 5, 6] },
+ { id: 2, name: '위시', wishIds: [1, 7] },
+ { id: 3, name: '여름 코디', wishIds: [5, 8, 9] },
+ { id: 4, name: '빈 폴더', wishIds: [] },
];
+
+export const getMockWishImageUrl = (wishId: number) =>
+ MOCK_WISHES.find(({ wish }) => wish.id === wishId)?.item.imageUrl ?? null;
diff --git a/apps/web/src/app/wish/_common/_consts/wishMode.ts b/apps/web/src/app/wish/_common/_consts/wishMode.ts
new file mode 100644
index 00000000..cd0a2744
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_consts/wishMode.ts
@@ -0,0 +1,4 @@
+export const WISH_MODE = {
+ DEFAULT: 'default',
+ EDIT: 'edit',
+} as const;
diff --git a/apps/web/src/app/wish/_common/_hooks/useWishEditMode.ts b/apps/web/src/app/wish/_common/_hooks/useWishEditMode.ts
new file mode 100644
index 00000000..2c838df9
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_hooks/useWishEditMode.ts
@@ -0,0 +1,36 @@
+import { useState } from 'react';
+
+import { WISH_MODE } from '../_consts/wishMode';
+import type { WishModeT } from '../_types/wishMode';
+
+export const useWishEditMode = () => {
+ const [mode, setMode] = useState(WISH_MODE.DEFAULT);
+ const [selectedWishIds, setSelectedWishIds] = useState>(new Set());
+
+ const handleToggleSelect = (wishId: number) => {
+ setSelectedWishIds(prev => {
+ const next = new Set(prev);
+ if (next.has(wishId)) next.delete(wishId);
+ else next.add(wishId);
+ return next;
+ });
+ };
+
+ const handleClearSelection = () => setSelectedWishIds(new Set());
+
+ const handleEnterEditMode = () => setMode(WISH_MODE.EDIT);
+
+ const handleExitEditMode = () => {
+ setMode(WISH_MODE.DEFAULT);
+ handleClearSelection();
+ };
+
+ return {
+ mode,
+ selectedWishIds,
+ handleToggleSelect,
+ handleClearSelection,
+ handleEnterEditMode,
+ handleExitEditMode,
+ };
+};
diff --git a/apps/web/src/app/wish/_common/_types/wishMode.ts b/apps/web/src/app/wish/_common/_types/wishMode.ts
new file mode 100644
index 00000000..4581d87d
--- /dev/null
+++ b/apps/web/src/app/wish/_common/_types/wishMode.ts
@@ -0,0 +1,3 @@
+import type { WISH_MODE } from '../_consts/wishMode';
+
+export type WishModeT = (typeof WISH_MODE)[keyof typeof WISH_MODE];
diff --git a/apps/web/src/app/wish/_components/AllWishTabContent.tsx b/apps/web/src/app/wish/_components/AllWishTabContent.tsx
index 92622de3..9405f1ff 100644
--- a/apps/web/src/app/wish/_components/AllWishTabContent.tsx
+++ b/apps/web/src/app/wish/_components/AllWishTabContent.tsx
@@ -1,9 +1,12 @@
+import type { ComponentProps } from 'react';
+
import WishGrid from '../_common/_components/WishGrid';
-import { MOCK_WISHES } from '../_common/_consts/wishMock';
-function AllWishTabContent() {
+export type AllWishTabContentProps = ComponentProps;
+
+function AllWishTabContent(props: AllWishTabContentProps) {
/** TODO: 위시 상세 링크 연결 시 saveScrollAnchor·useScrollRestoration(scope: all) 추가 */
- return ;
+ return ;
}
export default AllWishTabContent;
diff --git a/apps/web/src/app/wish/_components/FolderTabContent.tsx b/apps/web/src/app/wish/_components/FolderTabContent.tsx
index 21a5da5e..15cdd02a 100644
--- a/apps/web/src/app/wish/_components/FolderTabContent.tsx
+++ b/apps/web/src/app/wish/_components/FolderTabContent.tsx
@@ -3,14 +3,11 @@
import { useScrollRestoration } from '@/hooks/useScrollRestoration';
import { SCROLL_NAMESPACE } from '@/utils/scrollRestoration';
-import { MOCK_FOLDERS, MOCK_WISHES } from '../_common/_consts/wishMock';
+import { MOCK_FOLDERS, getMockWishImageUrl } from '../_common/_consts/wishMock';
import { WISH_TAB } from '../_consts/wishTab';
import FolderAddCard from './FolderAddCard';
import FolderCard from './FolderCard';
-const getImageUrl = (wishId: number) =>
- MOCK_WISHES.find(({ wish }) => wish.id === wishId)?.item.imageUrl ?? null;
-
function FolderTabContent() {
useScrollRestoration({ namespace: SCROLL_NAMESPACE.WISH, scope: WISH_TAB.FOLDER });
@@ -25,7 +22,7 @@ function FolderTabContent() {
folderId={id}
name={name}
wishCount={wishIds.length}
- thumbnailUrls={wishIds.slice(0, 3).map(getImageUrl)}
+ thumbnailUrls={wishIds.slice(0, 3).map(getMockWishImageUrl)}
/>
))}
diff --git a/apps/web/src/app/wish/_components/WishListContent.tsx b/apps/web/src/app/wish/_components/WishListContent.tsx
new file mode 100644
index 00000000..c5cd5cc6
--- /dev/null
+++ b/apps/web/src/app/wish/_components/WishListContent.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+import { Z_INDEX } from '@/consts/zIndex';
+
+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 { WISH_MODE } from '../_common/_consts/wishMode';
+import { useWishEditMode } from '../_common/_hooks/useWishEditMode';
+import type { WishFolderT } from '../_common/_types/wish';
+import WishTabBar from './WishTabBar';
+import WishTabContent from './WishTabContent';
+import WishTabSummary from './WishTabSummary';
+
+function WishListContent() {
+ /** TEMP: API 연동 시 위시 목록 조회 응답으로 교체 */
+ const [wishes, setWishes] = useState(MOCK_WISHES);
+ const {
+ mode,
+ selectedWishIds,
+ handleToggleSelect,
+ handleClearSelection,
+ handleEnterEditMode,
+ handleExitEditMode,
+ } = useWishEditMode();
+
+ /** TODO: 폴더 추가 API 연결. 기존 소속 폴더는 유지됨 */
+ const handleMove = (folder: WishFolderT) => {
+ handleClearSelection();
+ toast.success(`상품을 '${folder.name}' 폴더로 이동했어요.`);
+ };
+
+ /** TODO: 위시 삭제 API 연결 */
+ const handleDelete = () => {
+ setWishes(prev => prev.filter(({ wish }) => !selectedWishIds.has(wish.id)));
+ handleClearSelection();
+ toast.success(`상품 ${selectedWishIds.size}개를 삭제했어요.`);
+ };
+
+ return (
+ <>
+
+ {mode === WISH_MODE.EDIT ? (
+
+ ) : (
+ <>
+
+
내 위시리스트
+
+
+
+ >
+ )}
+
+
+
+
+ {mode === WISH_MODE.EDIT && (
+
+ )}
+ >
+ );
+}
+
+export default WishListContent;
diff --git a/apps/web/src/app/wish/_components/WishTabContent.tsx b/apps/web/src/app/wish/_components/WishTabContent.tsx
index 319adae5..92339052 100644
--- a/apps/web/src/app/wish/_components/WishTabContent.tsx
+++ b/apps/web/src/app/wish/_components/WishTabContent.tsx
@@ -8,10 +8,10 @@ import { getScrollContainer } from '@/utils/scrollRestoration';
import { WISH_TAB, WISH_TAB_PANEL_ID } from '../_consts/wishTab';
import type { WishTabT } from '../_types/wishTab';
import { parseWishTabParam } from '../_utils/wishSearchParams';
-import AllWishTabContent from './AllWishTabContent';
+import AllWishTabContent, { type AllWishTabContentProps } from './AllWishTabContent';
import FolderTabContent from './FolderTabContent';
-function WishTabContent() {
+function WishTabContent(allWishTabContentProps: AllWishTabContentProps) {
const searchParams = useSearchParams();
const activeTab = parseWishTabParam(searchParams.get('tab'));
@@ -41,7 +41,11 @@ function WishTabContent() {
return (
- {activeTab === WISH_TAB.FOLDER ?
:
}
+ {activeTab === WISH_TAB.FOLDER ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/apps/web/src/app/wish/_components/WishTabSummary.tsx b/apps/web/src/app/wish/_components/WishTabSummary.tsx
index 6282ce12..bf0583ba 100644
--- a/apps/web/src/app/wish/_components/WishTabSummary.tsx
+++ b/apps/web/src/app/wish/_components/WishTabSummary.tsx
@@ -1,18 +1,29 @@
'use client';
+import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
-import { MOCK_FOLDERS, MOCK_WISHES } from '../_common/_consts/wishMock';
+import { ROUTES } from '@/consts/route';
+
+import { MOCK_FOLDERS } from '../_common/_consts/wishMock';
+import { WISH_MODE } from '../_common/_consts/wishMode';
+import type { WishModeT } from '../_common/_types/wishMode';
import { WISH_TAB } from '../_consts/wishTab';
import { parseWishTabParam } from '../_utils/wishSearchParams';
const MAX_DISPLAY_COUNT = 999;
-function WishTabSummary() {
+type WishTabSummaryProps = {
+ wishCount: number;
+ mode: WishModeT;
+ onEdit: () => void;
+};
+
+function WishTabSummary({ wishCount, mode, onEdit }: WishTabSummaryProps) {
const searchParams = useSearchParams();
const isFolderTab = parseWishTabParam(searchParams.get('tab')) === WISH_TAB.FOLDER;
- const wishCount = MOCK_WISHES.length;
+ const editClassName = 'cursor-pointer body-2-medium text-text-neutral-secondary';
return (
@@ -21,9 +32,16 @@ function WishTabSummary() {
? `폴더 ${MOCK_FOLDERS.length}개`
: `상품 ${wishCount > MAX_DISPLAY_COUNT ? `${MAX_DISPLAY_COUNT}+` : wishCount}`}
-
+ {isFolderTab && (
+
+ 편집
+
+ )}
+ {!isFolderTab && mode === WISH_MODE.DEFAULT && (
+
+ )}
);
}
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 8f368bbe..9e300708 100644
--- a/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx
+++ b/apps/web/src/app/wish/folder/[folderId]/_components/FolderDetailContent.tsx
@@ -1,7 +1,7 @@
'use client';
import { useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { ChevronBackwardIconFill } from '@/assets/icons';
@@ -11,9 +11,14 @@ import { ROUTES } from '@/consts/route';
import { Z_INDEX } from '@/consts/zIndex';
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';
type FolderDetailContentProps = {
folderId: number | null;
@@ -25,6 +30,19 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) {
const folder = MOCK_FOLDERS.find(({ id }) => id === folderId);
+ /** TEMP: API 연동 시 폴더 위시 조회 응답으로 교체 */
+ const [wishes, setWishes] = useState(() =>
+ MOCK_WISHES.filter(({ wish }) => folder?.wishIds.includes(wish.id))
+ );
+ const {
+ mode,
+ selectedWishIds,
+ handleToggleSelect,
+ handleClearSelection,
+ handleEnterEditMode,
+ handleExitEditMode,
+ } = useWishEditMode();
+
/** 없는 폴더는 안내 후 폴더 탭으로 — 기록에 남기지 않는다 */
useEffect(() => {
if (folder) return;
@@ -35,7 +53,19 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) {
if (!folder) return null;
- const wishes = MOCK_WISHES.filter(({ wish }) => folder.wishIds.includes(wish.id));
+ /** TODO: 폴더 이동 API 연결 */
+ const handleMove = (targetFolder: WishFolderT) => {
+ setWishes(prev => prev.filter(({ wish }) => !selectedWishIds.has(wish.id)));
+ handleClearSelection();
+ toast.success(`상품을 '${targetFolder.name}' 폴더로 이동했어요.`);
+ };
+
+ /** TODO: 폴더에서 빼기·위시 삭제 중 동작 확정 후 API 연결 */
+ const handleDelete = () => {
+ setWishes(prev => prev.filter(({ wish }) => !selectedWishIds.has(wish.id)));
+ handleClearSelection();
+ toast.success(`폴더에서 상품 ${selectedWishIds.size}개를 삭제했어요.`);
+ };
return (
<>
@@ -43,26 +73,32 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) {
className="sticky top-0 bg-bg-layer-basement pt-padding-top"
style={{ zIndex: Z_INDEX.STICKY_HEADER }}
>
-
- backWithFallback(ROUTES.WISHLIST_FOLDER_TAB)}
- className="cursor-pointer"
- >
-
-
- }
- center={
-
- {folder.name}
-
- }
- right={}
- />
-
+ {mode === WISH_MODE.EDIT ? (
+
+
+
+ ) : (
+
+ backWithFallback(ROUTES.WISHLIST_FOLDER_TAB)}
+ className="cursor-pointer"
+ >
+
+
+ }
+ center={
+
+ {folder.name}
+
+ }
+ right={}
+ />
+
+ )}
@@ -70,18 +106,33 @@ function FolderDetailContent({ folderId }: FolderDetailContentProps) {
전체({wishes.length})
-
+ {mode === WISH_MODE.DEFAULT && (
+
+ )}
-
+
{/* TODO: 위시 상세 링크 연결 시 saveScrollAnchor·useScrollRestoration(scope: folderId) 추가 */}
-
+
+
+ {mode === WISH_MODE.EDIT && (
+
+ )}
>
);
}
diff --git a/apps/web/src/app/wish/folder/edit/_components/FolderEditContent.tsx b/apps/web/src/app/wish/folder/edit/_components/FolderEditContent.tsx
new file mode 100644
index 00000000..4df60775
--- /dev/null
+++ b/apps/web/src/app/wish/folder/edit/_components/FolderEditContent.tsx
@@ -0,0 +1,132 @@
+'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
new file mode 100644
index 00000000..4c28d279
--- /dev/null
+++ b/apps/web/src/app/wish/folder/edit/_components/SortableFolderRow.tsx
@@ -0,0 +1,72 @@
+'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
new file mode 100644
index 00000000..5986a898
--- /dev/null
+++ b/apps/web/src/app/wish/folder/edit/page.tsx
@@ -0,0 +1,11 @@
+import FolderEditContent from './_components/FolderEditContent';
+
+function FolderEditPage() {
+ return (
+
+
+
+ );
+}
+
+export default FolderEditPage;
diff --git a/apps/web/src/app/wish/page.tsx b/apps/web/src/app/wish/page.tsx
index b41dd867..aa57d4e0 100644
--- a/apps/web/src/app/wish/page.tsx
+++ b/apps/web/src/app/wish/page.tsx
@@ -1,25 +1,9 @@
-import { Z_INDEX } from '@/consts/zIndex';
-
-import WishSearchLink from './_common/_components/WishSearchLink';
-import WishTabBar from './_components/WishTabBar';
-import WishTabContent from './_components/WishTabContent';
-import WishTabSummary from './_components/WishTabSummary';
+import WishListContent from './_components/WishListContent';
function WishPage() {
return (
-
-
+
);
}
diff --git a/apps/web/src/assets/icons/fill/align-equal.svg b/apps/web/src/assets/icons/fill/align-equal.svg
index b738b3e3..b7087826 100644
--- a/apps/web/src/assets/icons/fill/align-equal.svg
+++ b/apps/web/src/assets/icons/fill/align-equal.svg
@@ -1,11 +1,5 @@
-