Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/wish/_assets/folder-cover-empty.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions apps/web/src/app/wish/_common/_components/FolderNameForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client';

import { 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';
import { cn } from '@/utils/cn';

const MAX_FOLDER_NAME_LENGTH = 20;

const graphemeSegmenter = new Intl.Segmenter('ko', { granularity: 'grapheme' });

/** NOTE: 이모지도 1자로 세야 해서 length 대신 grapheme 단위로 셈 */
const countGraphemes = (value: string) => Array.from(graphemeSegmenter.segment(value)).length;

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 [isEmptySubmitted, setIsEmptySubmitted] = useState(false);

const trimmedName = name.trim();
const isUnchanged = initialName !== '' && trimmedName === initialName;
const isOverLimit = countGraphemes(trimmedName) > MAX_FOLDER_NAME_LENGTH;

let errorMessage: string | null = null;
if (isOverLimit) errorMessage = `최대 ${MAX_FOLDER_NAME_LENGTH}자까지 입력할 수 있어요.`;
else if (isEmptySubmitted) errorMessage = '폴더 이름을 입력해 주세요.';

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!trimmedName) {
setIsEmptySubmitted(true);
return;
}

onSubmit(trimmedName);
};

return (
<form onSubmit={handleSubmit} className="-mt-2 flex flex-col gap-6">
<Header
left={
<button type="button" aria-label="뒤로가기" onClick={onBack} className="cursor-pointer">
<ChevronBackwardIconFill className="size-7.5 text-icon-neutral-primary" />
</button>
}
center={
<DrawerTitle className="heading-2-semibold text-text-neutral-primary">
{title}
</DrawerTitle>
}
/>
<div className={cn('flex flex-col pb-1.5', errorMessage ? 'gap-2' : 'gap-9')}>
<Input
aria-label="폴더명"
placeholder="폴더명을 입력해주세요."
value={name}
onChange={event => {
setName(event.target.value);
setIsEmptySubmitted(false);
}}
aria-invalid={errorMessage !== null}
{...(errorMessage && { helperText: errorMessage })}
autoFocus
/>
<Button type="submit" size="lg" disabled={name === '' || isUnchanged || isOverLimit}>
완료
</Button>
</div>
</form>
);
}

export default FolderNameForm;
62 changes: 12 additions & 50 deletions apps/web/src/app/wish/_common/_components/FolderSelectSheet.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<HTMLFormElement>) => {
event.preventDefault();
if (!trimmedFolderName) return;

onAddFolder(trimmedFolderName);
handleBackToList();
const handleAddSubmit = (name: string) => {
onAddFolder(name);
setIsAddView(false);
};

return (
Expand All @@ -57,37 +45,11 @@ function FolderSelectSheet({
{isAddView ? '새 폴더 이름을 입력하세요.' : '상품을 옮길 폴더를 선택하세요.'}
</DrawerDescription>
{isAddView ? (
<form onSubmit={handleAddSubmit} className="flex flex-col gap-6">
<Header
left={
<button
type="button"
aria-label="폴더 선택으로 돌아가기"
onClick={handleBackToList}
className="cursor-pointer"
>
<ChevronBackwardIconFill className="size-7.5 text-text-neutral-primary" />
</button>
}
center={
<DrawerTitle className="heading-2-semibold text-text-neutral-primary">
폴더 추가
</DrawerTitle>
}
/>
<div className="flex flex-col gap-3 pb-1.5">
<Input
aria-label="폴더명"
placeholder="폴더명을 입력해주세요."
value={folderName}
onChange={event => setFolderName(event.target.value)}
autoFocus
/>
<Button type="submit" size="lg" disabled={!trimmedFolderName}>
완료
</Button>
</div>
</form>
<FolderNameForm
title="폴더 추가"
onBack={() => setIsAddView(false)}
onSubmit={handleAddSubmit}
/>
) : (
<div className="flex flex-col gap-3">
<Header
Expand Down
47 changes: 0 additions & 47 deletions apps/web/src/app/wish/_common/_components/WishDeleteSheet.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** 탭바가 루트 레이아웃에 있어 편집 중에도 렌더되므로 액션바가 떠 있는 동안 숨김 */
body:has([data-hide-bottom-tab-bar]) [data-bottom-tab-bar] {
display: none;
}
30 changes: 18 additions & 12 deletions apps/web/src/app/wish/_common/_components/WishEditActionBar.tsx
Original file line number Diff line number Diff line change
@@ -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<ComponentProps<typeof ConfirmDialog>, 'title' | 'description' | 'icon'>;
};

function ActionButton({
Expand All @@ -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'
)}
>
<Icon
className={cn(
'size-6',
'size-5',
disabled ? 'text-icon-neutral-secondary' : 'text-icon-neutral-primary'
)}
/>
Expand All @@ -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)
Expand All @@ -75,15 +78,16 @@ function WishEditActionBar({
};

const handleConfirmDelete = () => {
setIsDeleteSheetOpen(false);
setIsDeleteDialogOpen(false);
onDelete();
};

return (
<>
<div
data-bottom-cta
className="fixed right-0 bottom-0 left-0 mx-auto flex w-full max-w-120 justify-center gap-[82px] rounded-t-2xl bg-white pt-4 pb-[34px]"
data-hide-bottom-tab-bar
className="fixed right-0 bottom-0 left-0 mx-auto flex w-full max-w-120 items-center justify-center gap-10 rounded-t-2xl bg-white px-2.5 pt-6 pb-9"
style={{ zIndex: Z_INDEX.BOTTOM_CTA }}
>
<ActionButton
Expand All @@ -92,11 +96,12 @@ function WishEditActionBar({
disabled={isDisabled}
onClick={() => setIsFolderSheetOpen(true)}
/>
<span aria-hidden className="h-5.5 w-[1.4px] bg-gray-75" />
<ActionButton
label="삭제"
Icon={TrashIconFill}
disabled={isDisabled}
onClick={() => setIsDeleteSheetOpen(true)}
onClick={() => setIsDeleteDialogOpen(true)}
/>
</div>

Expand All @@ -107,10 +112,11 @@ function WishEditActionBar({
onSelect={handleSelectFolder}
onAddFolder={handleAddFolder}
/>
<WishDeleteSheet
open={isDeleteSheetOpen}
onOpenChange={setIsDeleteSheetOpen}
selectedCount={selectedCount}
<ConfirmDialog
{...deleteConfirm}
open={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
confirmLabel="삭제하기"
onConfirm={handleConfirmDelete}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function WishEditHeader({ selectedCount, onDone }: WishEditHeaderProps) {
<Header
center={
<h1 className="heading-1-bold text-text-neutral-primary">
{selectedCount > 0 ? `상품 ${selectedCount}개 선택` : '상품 선택'}
{selectedCount > 0 ? `위시 ${selectedCount}개 선택` : '위시 선택'}
</h1>
}
right={
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/app/wish/_common/_components/WishGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ReactNode } from 'react';

import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons';
import { Z_INDEX } from '@/consts/zIndex';

Expand All @@ -8,15 +10,17 @@ type WishGridProps = {
wishes: WishListItemT[];
selectedWishIds?: Set<number>;
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 (
<ul className="grid grid-cols-3 gap-x-[1.5px] gap-y-6">
{leadingItem && <li>{leadingItem}</li>}
{wishes.map(({ wish, item, winnerTournamentIds }, index) => {
const card = (
<WishCard
Expand Down
15 changes: 0 additions & 15 deletions apps/web/src/app/wish/_common/_components/WishSearchLink.tsx

This file was deleted.

Loading
Loading