Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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