Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface OptionsMenuButtonProps {

const ITEM_STYLE =
'body4-r text-font-black-1 h-[3.6rem] w-full ' +
'flex items-center pl-[0.8rem] ' +
'flex items-center justify-center ' +
'hover:bg-gray100 focus-visible:bg-gray100 active:bg-gray200 ' +
'outline-none transition-colors';

Expand Down
107 changes: 99 additions & 8 deletions apps/client/src/shared/components/sidebar/PopupPortal.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,99 @@
import { createPortal } from 'react-dom';
import { Popup } from '@pinback/design-system/ui';
import { useEffect, useState } from 'react';
import { AutoDismissToast, Popup, Toast } from '@pinback/design-system/ui';
import type { PopupState } from '@shared/hooks/useCategoryPopups';

interface Props {
popup: PopupState;
popup: PopupState | null;
onClose: () => void;
onChange?: (value: string) => void;
onCreateConfirm?: () => void;
onEditConfirm?: (id: number, draft?: string) => void;
onDeleteConfirm?: (id: number) => void;
categoryList?: { id: number; name: string }[];
isToastOpen?: boolean;
onToastClose?: () => void;
toastKey?: number;
toastAction?: 'create' | 'edit' | 'delete';
}

const MAX_LEN = 10;

export default function PopupPortal({
popup,
onClose,
onChange,
onCreateConfirm,
onEditConfirm,
onDeleteConfirm,
categoryList,
isToastOpen,
onToastClose,
toastKey,
toastAction,
}: Props) {
const [draft, setDraft] = useState('');

useEffect(() => {
if (!popup) return;
setDraft(popup.kind === 'edit' ? (popup.name ?? '') : '');
}, [popup]);

if (!popup) return null;

const value = draft.trim();
const len = value.length;

const isEmpty = popup.kind !== 'delete' && len === 0;
const isDuplicate =
popup.kind !== 'delete' &&
!!categoryList?.some(
(c) => c.name === value && (popup.kind === 'create' || c.id !== popup.id)
);

let helperText = '';
let isErrorUI = false;

if (!isEmpty && popup.kind !== 'delete') {
if (isDuplicate) {
helperText = '이미 존재하는 카테고리 이름입니다.';
isErrorUI = true;
} else if (len > MAX_LEN) {
helperText = `카테고리 이름은 ${MAX_LEN}자 이내로 입력해주세요.`;
isErrorUI = true;
} else if (len === MAX_LEN) {
helperText = `최대 ${MAX_LEN}자까지 입력할 수 있어요.`;
isErrorUI = false;
}
}

const handleInputChange = (next: string) => {
setDraft(next);
onChange?.(next);
};

const blocked =
popup.kind !== 'delete' && (isEmpty || isDuplicate || len > MAX_LEN);

const handleCreate = () => {
if (blocked) return;
onCreateConfirm?.();
};

const handleEdit = () => {
if (blocked || popup.kind !== 'edit') return;
onEditConfirm?.(popup.id, value);
};

const handleDelete = () => {
if (popup.kind !== 'delete') return;
onDeleteConfirm?.(popup.id);
};

const action = toastAction ?? (popup.kind as 'create' | 'edit' | 'delete');
const actionLabel =
action === 'create' ? '추가' : action === 'edit' ? '수정' : '삭제';

return createPortal(
<div className="fixed inset-0 z-[11000]">
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
Expand All @@ -31,10 +104,13 @@ export default function PopupPortal({
title="카테고리 추가하기"
left="취소"
right="추가"
onInputChange={onChange}
isError={isErrorUI}
helperText={helperText}
inputValue={draft}
onInputChange={handleInputChange}
placeholder="카테고리 제목을 입력해주세요"
onLeftClick={onClose}
onRightClick={() => onCreateConfirm?.()}
onRightClick={handleCreate}
/>
)}

Expand All @@ -44,10 +120,12 @@ export default function PopupPortal({
title="카테고리 수정하기"
left="취소"
right="확인"
onInputChange={onChange}
defaultValue={popup.name}
isError={isErrorUI}
helperText={helperText}
inputValue={draft}
onInputChange={handleInputChange}
onLeftClick={onClose}
onRightClick={() => onEditConfirm?.(popup.id)}
onRightClick={handleEdit}
/>
)}

Expand All @@ -59,9 +137,22 @@ export default function PopupPortal({
left="취소"
right="삭제"
onLeftClick={onClose}
onRightClick={() => onDeleteConfirm?.(popup.id)}
onRightClick={handleDelete}
/>
)}

{isToastOpen && (
<div className="absolute bottom-[23.4rem] left-1/2 -translate-x-1/2">
<AutoDismissToast
key={toastKey}
duration={1000}
fadeMs={500}
onClose={onToastClose}
>
<Toast text={`${actionLabel}에 실패했어요.\n다시 시도해주세요`} />
</AutoDismissToast>
</div>
)}
</div>
</div>,
document.body
Expand Down
48 changes: 38 additions & 10 deletions apps/client/src/shared/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import {
usePutCategory,
useDeleteCategory,
} from '@shared/apis/queries';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';

export function Sidebar() {
const [newCategoryName, setNewCategoryName] = useState('');
const [toastIsOpen, setToastIsOpen] = useState(false);

const queryClient = useQueryClient();

const { data: categories } = useGetDashboardCategories();
Expand Down Expand Up @@ -58,18 +60,23 @@ export function Sidebar() {
setNewCategoryName(name);
};

useEffect(() => {
setToastIsOpen(false);
}, [popup]);

const handleCreateCategory = () => {
createCategory(newCategoryName, {
onSuccess: () => {
handleCategoryChange('');
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
close();
},
onError: (error) => {
console.error('카테고리 생성 실패:', error);
onError: () => {
setToastIsOpen(true);
},
});
};

const handlePatchCategory = (id: number) => {
patchCategory(
{ id, categoryName: newCategoryName },
Expand All @@ -79,7 +86,9 @@ export function Sidebar() {
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
close();
},
onError: (error) => console.error('카테고리 수정 실패:', error),
onError: () => {
setToastIsOpen(true);
},
}
);
};
Expand All @@ -90,12 +99,17 @@ export function Sidebar() {
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
close();
},
onError: (error) => {
console.error('카테고리 삭제 실패:', error);
onError: () => {
setToastIsOpen(true);
},
});
};

const handlePopupClose = () => {
setToastIsOpen(false);
close();
};

if (isPending) return <div></div>;
if (isError) return <div></div>;
const acornCount = data.acornCount;
Expand Down Expand Up @@ -149,7 +163,12 @@ export function Sidebar() {
/>
))}

<CreateItem onClick={openCreate} />
<CreateItem
onClick={() => {
setToastIsOpen(false);
openCreate();
}}
/>
</ul>
</AccordionItem>

Expand All @@ -158,8 +177,14 @@ export function Sidebar() {
style={style ?? undefined}
categoryId={menu.categoryId}
getCategoryName={getCategoryName}
onEdit={(id, name) => openEdit(id, name)}
onDelete={(id, name) => openDelete(id, name)}
onEdit={(id, name) => {
setToastIsOpen(false);
openEdit(id, name);
}}
onDelete={(id, name) => {
setToastIsOpen(false);
openDelete(id, name);
}}
onClose={closeMenu}
containerRef={containerRef}
/>
Expand All @@ -180,11 +205,14 @@ export function Sidebar() {

<PopupPortal
popup={popup}
onClose={close}
onClose={handlePopupClose}
onChange={handleCategoryChange}
onCreateConfirm={handleCreateCategory}
onEditConfirm={(id) => handlePatchCategory(id)}
onDeleteConfirm={(id) => handleDeleteCategory(id)}
Comment on lines 211 to 212
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

onEditConfirm 시그니처 활용해 초안 값 직접 전달

위 대안 A를 택했다면, 여기서 draft를 전달해 주세요.

적용 diff:

-        onEditConfirm={(id) => handlePatchCategory(id)}
+        onEditConfirm={(id, draft) => handlePatchCategory(id, draft)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onEditConfirm={(id) => handlePatchCategory(id)}
onDeleteConfirm={(id) => handleDeleteCategory(id)}
onEditConfirm={(id, draft) => handlePatchCategory(id, draft)}
onDeleteConfirm={(id) => handleDeleteCategory(id)}
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/Sidebar.tsx around lines 193-194,
the onEditConfirm prop currently only forwards the id but the review requests
using the onEditConfirm signature to pass the draft as well; update the prop to
forward both parameters to your handler (e.g., call handlePatchCategory with id
and draft or bind the handler so it accepts both id and draft) so the draft
value from the edit UI is passed through to the patch function.

categoryList={categories?.categories ?? []}
isToastOpen={toastIsOpen}
onToastClose={() => setToastIsOpen(false)}
/>
</aside>
);
Expand Down
Loading