Skip to content

Commit f2303de

Browse files
authored
Feat: 개발 QA 1차(정민 | 카테고리 팝업 수정) (#113)
* feat: 헬퍼 텍스트 추가 * fix: 카테고리 수정 안됨 * feat: 카테고리 추가 및 수정 시 유효성 검사 로직 추가 * fix: rules-of-hooks * feat: Toast 기능 추가 및 카테고리 수정/삭제 오류 처리 개선 * fix: ToastIsOpen 분기처리 * feat: 옵션 메뉴버튼 수정 * fix: PopupPortal에서 null 체크 및 에러 처리 개선 * feat: PopupPortal에서 토스트 상태 관리 및 팝업 닫기 시 토스트 닫기 기능 추가 * chore: console log 삭제
1 parent 3df02a5 commit f2303de

File tree

3 files changed

+138
-19
lines changed

3 files changed

+138
-19
lines changed

apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface OptionsMenuButtonProps {
99

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

apps/client/src/shared/components/sidebar/PopupPortal.tsx

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,99 @@
11
import { createPortal } from 'react-dom';
2-
import { Popup } from '@pinback/design-system/ui';
2+
import { useEffect, useState } from 'react';
3+
import { AutoDismissToast, Popup, Toast } from '@pinback/design-system/ui';
34
import type { PopupState } from '@shared/hooks/useCategoryPopups';
45

56
interface Props {
6-
popup: PopupState;
7+
popup: PopupState | null;
78
onClose: () => void;
89
onChange?: (value: string) => void;
910
onCreateConfirm?: () => void;
1011
onEditConfirm?: (id: number, draft?: string) => void;
1112
onDeleteConfirm?: (id: number) => void;
13+
categoryList?: { id: number; name: string }[];
14+
isToastOpen?: boolean;
15+
onToastClose?: () => void;
16+
toastKey?: number;
17+
toastAction?: 'create' | 'edit' | 'delete';
1218
}
1319

20+
const MAX_LEN = 10;
21+
1422
export default function PopupPortal({
1523
popup,
1624
onClose,
1725
onChange,
1826
onCreateConfirm,
1927
onEditConfirm,
2028
onDeleteConfirm,
29+
categoryList,
30+
isToastOpen,
31+
onToastClose,
32+
toastKey,
33+
toastAction,
2134
}: Props) {
35+
const [draft, setDraft] = useState('');
36+
37+
useEffect(() => {
38+
if (!popup) return;
39+
setDraft(popup.kind === 'edit' ? (popup.name ?? '') : '');
40+
}, [popup]);
41+
2242
if (!popup) return null;
2343

44+
const value = draft.trim();
45+
const len = value.length;
46+
47+
const isEmpty = popup.kind !== 'delete' && len === 0;
48+
const isDuplicate =
49+
popup.kind !== 'delete' &&
50+
!!categoryList?.some(
51+
(c) => c.name === value && (popup.kind === 'create' || c.id !== popup.id)
52+
);
53+
54+
let helperText = '';
55+
let isErrorUI = false;
56+
57+
if (!isEmpty && popup.kind !== 'delete') {
58+
if (isDuplicate) {
59+
helperText = '이미 존재하는 카테고리 이름입니다.';
60+
isErrorUI = true;
61+
} else if (len > MAX_LEN) {
62+
helperText = `카테고리 이름은 ${MAX_LEN}자 이내로 입력해주세요.`;
63+
isErrorUI = true;
64+
} else if (len === MAX_LEN) {
65+
helperText = `최대 ${MAX_LEN}자까지 입력할 수 있어요.`;
66+
isErrorUI = false;
67+
}
68+
}
69+
70+
const handleInputChange = (next: string) => {
71+
setDraft(next);
72+
onChange?.(next);
73+
};
74+
75+
const blocked =
76+
popup.kind !== 'delete' && (isEmpty || isDuplicate || len > MAX_LEN);
77+
78+
const handleCreate = () => {
79+
if (blocked) return;
80+
onCreateConfirm?.();
81+
};
82+
83+
const handleEdit = () => {
84+
if (blocked || popup.kind !== 'edit') return;
85+
onEditConfirm?.(popup.id, value);
86+
};
87+
88+
const handleDelete = () => {
89+
if (popup.kind !== 'delete') return;
90+
onDeleteConfirm?.(popup.id);
91+
};
92+
93+
const action = toastAction ?? (popup.kind as 'create' | 'edit' | 'delete');
94+
const actionLabel =
95+
action === 'create' ? '추가' : action === 'edit' ? '수정' : '삭제';
96+
2497
return createPortal(
2598
<div className="fixed inset-0 z-[11000]">
2699
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
@@ -31,10 +104,13 @@ export default function PopupPortal({
31104
title="카테고리 추가하기"
32105
left="취소"
33106
right="추가"
34-
onInputChange={onChange}
107+
isError={isErrorUI}
108+
helperText={helperText}
109+
inputValue={draft}
110+
onInputChange={handleInputChange}
35111
placeholder="카테고리 제목을 입력해주세요"
36112
onLeftClick={onClose}
37-
onRightClick={() => onCreateConfirm?.()}
113+
onRightClick={handleCreate}
38114
/>
39115
)}
40116

@@ -44,10 +120,12 @@ export default function PopupPortal({
44120
title="카테고리 수정하기"
45121
left="취소"
46122
right="확인"
47-
onInputChange={onChange}
48-
defaultValue={popup.name}
123+
isError={isErrorUI}
124+
helperText={helperText}
125+
inputValue={draft}
126+
onInputChange={handleInputChange}
49127
onLeftClick={onClose}
50-
onRightClick={() => onEditConfirm?.(popup.id)}
128+
onRightClick={handleEdit}
51129
/>
52130
)}
53131

@@ -59,9 +137,22 @@ export default function PopupPortal({
59137
left="취소"
60138
right="삭제"
61139
onLeftClick={onClose}
62-
onRightClick={() => onDeleteConfirm?.(popup.id)}
140+
onRightClick={handleDelete}
63141
/>
64142
)}
143+
144+
{isToastOpen && (
145+
<div className="absolute bottom-[23.4rem] left-1/2 -translate-x-1/2">
146+
<AutoDismissToast
147+
key={toastKey}
148+
duration={1000}
149+
fadeMs={500}
150+
onClose={onToastClose}
151+
>
152+
<Toast text={`${actionLabel}에 실패했어요.\n다시 시도해주세요`} />
153+
</AutoDismissToast>
154+
</div>
155+
)}
65156
</div>
66157
</div>,
67158
document.body

apps/client/src/shared/components/sidebar/Sidebar.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ import {
1717
usePutCategory,
1818
useDeleteCategory,
1919
} from '@shared/apis/queries';
20-
import { useState } from 'react';
20+
import { useEffect, useState } from 'react';
2121
import { useQueryClient } from '@tanstack/react-query';
2222

2323
export function Sidebar() {
2424
const [newCategoryName, setNewCategoryName] = useState('');
25+
const [toastIsOpen, setToastIsOpen] = useState(false);
26+
2527
const queryClient = useQueryClient();
2628

2729
const { data: categories } = useGetDashboardCategories();
@@ -58,18 +60,23 @@ export function Sidebar() {
5860
setNewCategoryName(name);
5961
};
6062

63+
useEffect(() => {
64+
setToastIsOpen(false);
65+
}, [popup]);
66+
6167
const handleCreateCategory = () => {
6268
createCategory(newCategoryName, {
6369
onSuccess: () => {
6470
handleCategoryChange('');
6571
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
6672
close();
6773
},
68-
onError: (error) => {
69-
console.error('카테고리 생성 실패:', error);
74+
onError: () => {
75+
setToastIsOpen(true);
7076
},
7177
});
7278
};
79+
7380
const handlePatchCategory = (id: number) => {
7481
patchCategory(
7582
{ id, categoryName: newCategoryName },
@@ -79,7 +86,9 @@ export function Sidebar() {
7986
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
8087
close();
8188
},
82-
onError: (error) => console.error('카테고리 수정 실패:', error),
89+
onError: () => {
90+
setToastIsOpen(true);
91+
},
8392
}
8493
);
8594
};
@@ -90,12 +99,17 @@ export function Sidebar() {
9099
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
91100
close();
92101
},
93-
onError: (error) => {
94-
console.error('카테고리 삭제 실패:', error);
102+
onError: () => {
103+
setToastIsOpen(true);
95104
},
96105
});
97106
};
98107

108+
const handlePopupClose = () => {
109+
setToastIsOpen(false);
110+
close();
111+
};
112+
99113
if (isPending) return <div></div>;
100114
if (isError) return <div></div>;
101115
const acornCount = data.acornCount;
@@ -149,7 +163,12 @@ export function Sidebar() {
149163
/>
150164
))}
151165

152-
<CreateItem onClick={openCreate} />
166+
<CreateItem
167+
onClick={() => {
168+
setToastIsOpen(false);
169+
openCreate();
170+
}}
171+
/>
153172
</ul>
154173
</AccordionItem>
155174

@@ -158,8 +177,14 @@ export function Sidebar() {
158177
style={style ?? undefined}
159178
categoryId={menu.categoryId}
160179
getCategoryName={getCategoryName}
161-
onEdit={(id, name) => openEdit(id, name)}
162-
onDelete={(id, name) => openDelete(id, name)}
180+
onEdit={(id, name) => {
181+
setToastIsOpen(false);
182+
openEdit(id, name);
183+
}}
184+
onDelete={(id, name) => {
185+
setToastIsOpen(false);
186+
openDelete(id, name);
187+
}}
163188
onClose={closeMenu}
164189
containerRef={containerRef}
165190
/>
@@ -180,11 +205,14 @@ export function Sidebar() {
180205

181206
<PopupPortal
182207
popup={popup}
183-
onClose={close}
208+
onClose={handlePopupClose}
184209
onChange={handleCategoryChange}
185210
onCreateConfirm={handleCreateCategory}
186211
onEditConfirm={(id) => handlePatchCategory(id)}
187212
onDeleteConfirm={(id) => handleDeleteCategory(id)}
213+
categoryList={categories?.categories ?? []}
214+
isToastOpen={toastIsOpen}
215+
onToastClose={() => setToastIsOpen(false)}
188216
/>
189217
</aside>
190218
);

0 commit comments

Comments
 (0)