-
Notifications
You must be signed in to change notification settings - Fork 1
Feat: 개발 QA 1차(카테고리 팝업 수정) #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
6dec825
a489fdf
7e2086f
a558439
e96026b
cf02700
67c0da9
80994d4
0901def
bfce6ae
fa49062
5c88464
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { createPortal } from 'react-dom'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Popup } from '@pinback/design-system/ui'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { AutoDismissToast, Popup, Toast } from '@pinback/design-system/ui'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import type { PopupState } from '@shared/hooks/useCategoryPopups'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| interface Props { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -9,6 +10,9 @@ interface Props { | |||||||||||||||||||||||||||||||||||||||||||||||
| onCreateConfirm?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||
| onEditConfirm?: (id: number, draft?: string) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||
| onDeleteConfirm?: (id: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||
| categoryList?: { id: number; name: string }[]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| isToastOpen?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||
| onToastClose?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export default function PopupPortal({ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -18,9 +22,54 @@ export default function PopupPortal({ | |||||||||||||||||||||||||||||||||||||||||||||||
| onCreateConfirm, | ||||||||||||||||||||||||||||||||||||||||||||||||
| onEditConfirm, | ||||||||||||||||||||||||||||||||||||||||||||||||
| onDeleteConfirm, | ||||||||||||||||||||||||||||||||||||||||||||||||
| categoryList, | ||||||||||||||||||||||||||||||||||||||||||||||||
| isToastOpen, | ||||||||||||||||||||||||||||||||||||||||||||||||
| onToastClose, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }: Props) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [draft, setDraft] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (!popup) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const error = (() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (popup.kind === 'delete') return null; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const value = draft.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!value) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (value.length > 10) return '카테고리 이름은 10자 이내로 입력해주세요.'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const isDuplicate = !!categoryList?.some( | ||||||||||||||||||||||||||||||||||||||||||||||||
| (category) => | ||||||||||||||||||||||||||||||||||||||||||||||||
| category.name === value && | ||||||||||||||||||||||||||||||||||||||||||||||||
| (popup.kind === 'create' || category.id !== popup.id) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return isDuplicate ? '이미 존재하는 카테고리 이름입니다.' : null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const handleInputChange = (value: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setDraft(value); | ||||||||||||||||||||||||||||||||||||||||||||||||
| onChange?.(value); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const handleCreate = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| onCreateConfirm?.(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const handleEdit = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (error || popup.kind !== 'edit') return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| onEditConfirm?.(popup.id, draft.trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CREATE/PATCH 시 공백값 차단 및 편집 기본값 폴백
적용 diff: const handleCreate = () => {
- if (error) return;
- onCreateConfirm?.();
+ const value = draft.trim();
+ if (!value || error) return;
+ onChange?.(value);
+ onCreateConfirm?.();
};
const handleEdit = () => {
- if (error || popup.kind !== 'edit') return;
- onEditConfirm?.(popup.id, draft.trim());
+ if (popup.kind !== 'edit') return;
+ const value = (draft || popup.name || '').trim();
+ if (!value || error) return;
+ onChange?.(value);
+ onEditConfirm?.(popup.id, value);
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| const handleDelete = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (popup.kind === 'delete') { | ||||||||||||||||||||||||||||||||||||||||||||||||
| onDeleteConfirm?.(popup.id); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const actionLabel = | ||||||||||||||||||||||||||||||||||||||||||||||||
| popup.kind === 'create' ? '추가' : popup.kind === 'edit' ? '수정' : '삭제'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return createPortal( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="fixed inset-0 z-[11000]"> | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute inset-0 bg-black/60" onClick={onClose} /> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,10 +80,12 @@ export default function PopupPortal({ | |||||||||||||||||||||||||||||||||||||||||||||||
| title="카테고리 추가하기" | ||||||||||||||||||||||||||||||||||||||||||||||||
| left="취소" | ||||||||||||||||||||||||||||||||||||||||||||||||
| right="추가" | ||||||||||||||||||||||||||||||||||||||||||||||||
| onInputChange={onChange} | ||||||||||||||||||||||||||||||||||||||||||||||||
| isError={Boolean(error)} | ||||||||||||||||||||||||||||||||||||||||||||||||
| helperText={error ?? ''} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onInputChange={handleInputChange} | ||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="카테고리 제목을 입력해주세요" | ||||||||||||||||||||||||||||||||||||||||||||||||
| onLeftClick={onClose} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onRightClick={() => onCreateConfirm?.()} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onRightClick={handleCreate} | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -44,10 +95,12 @@ export default function PopupPortal({ | |||||||||||||||||||||||||||||||||||||||||||||||
| title="카테고리 수정하기" | ||||||||||||||||||||||||||||||||||||||||||||||||
| left="취소" | ||||||||||||||||||||||||||||||||||||||||||||||||
| right="확인" | ||||||||||||||||||||||||||||||||||||||||||||||||
| onInputChange={onChange} | ||||||||||||||||||||||||||||||||||||||||||||||||
| isError={Boolean(error)} | ||||||||||||||||||||||||||||||||||||||||||||||||
| helperText={error ?? ''} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onInputChange={handleInputChange} | ||||||||||||||||||||||||||||||||||||||||||||||||
| defaultValue={popup.name} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onLeftClick={onClose} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onRightClick={() => onEditConfirm?.(popup.id)} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onRightClick={handleEdit} | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,9 +112,21 @@ 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 | ||||||||||||||||||||||||||||||||||||||||||||||||
| duration={1000} | ||||||||||||||||||||||||||||||||||||||||||||||||
| fadeMs={1000} | ||||||||||||||||||||||||||||||||||||||||||||||||
| onClose={onToastClose} | ||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||
| <Toast text={`${actionLabel}에 실패했어요.\n다시 시도해주세요`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| </AutoDismissToast> | ||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+119
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 토스트 재표시가 한 번만 되고 이후 재등장하지 않는 이슈
적용 예시는 Sidebar 코멘트 참고. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||
| </div>, | ||||||||||||||||||||||||||||||||||||||||||||||||
| document.body | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -58,6 +58,8 @@ export function Sidebar() { | |||||||||
| setNewCategoryName(name); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const [toastIsOpen, setToastIsOpen] = useState(false); | ||||||||||
|
|
||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
| const handleCreateCategory = () => { | ||||||||||
| createCategory(newCategoryName, { | ||||||||||
| onSuccess: () => { | ||||||||||
|
|
@@ -67,6 +69,7 @@ export function Sidebar() { | |||||||||
| }, | ||||||||||
| onError: (error) => { | ||||||||||
| console.error('카테고리 생성 실패:', error); | ||||||||||
| setToastIsOpen(true); | ||||||||||
| }, | ||||||||||
| }); | ||||||||||
| }; | ||||||||||
|
|
@@ -79,7 +82,10 @@ export function Sidebar() { | |||||||||
| queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] }); | ||||||||||
| close(); | ||||||||||
| }, | ||||||||||
| onError: (error) => console.error('카테고리 수정 실패:', error), | ||||||||||
| onError: (error) => { | ||||||||||
| console.error('카테고리 수정 실패:', error); | ||||||||||
| setToastIsOpen(true); | ||||||||||
| }, | ||||||||||
| } | ||||||||||
|
||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
@@ -92,6 +98,7 @@ export function Sidebar() { | |||||||||
| }, | ||||||||||
| onError: (error) => { | ||||||||||
| console.error('카테고리 삭제 실패:', error); | ||||||||||
| setToastIsOpen(true); | ||||||||||
| }, | ||||||||||
| }); | ||||||||||
| }; | ||||||||||
|
|
@@ -185,6 +192,8 @@ export function Sidebar() { | |||||||||
| onCreateConfirm={handleCreateCategory} | ||||||||||
| onEditConfirm={(id) => handlePatchCategory(id)} | ||||||||||
| onDeleteConfirm={(id) => handleDeleteCategory(id)} | ||||||||||
|
Comment on lines
211
to
212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion
위 대안 A를 택했다면, 여기서 적용 diff: - onEditConfirm={(id) => handlePatchCategory(id)}
+ onEditConfirm={(id, draft) => handlePatchCategory(id, draft)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| categoryList={categories?.categories ?? []} | ||||||||||
| isToastOpen={toastIsOpen} | ||||||||||
| /> | ||||||||||
| </aside> | ||||||||||
| ); | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
편집 모달에서 입력을 변경하지 않으면 빈 문자열이 전파됨
draft가 기본값''에서 시작하고, 편집 시 입력을 바꾸지 않으면onEditConfirm에 빈 문자열이 전달될 위험이 있습니다(아래 핸들러 코멘트 참조). 모달 오픈 시점에popup.name을draft와 상위onChange로 동기화해 주세요.적용 diff:
const [draft, setDraft] = useState(''); +// 모달 오픈/변경 시 초깃값 동기화 +useEffect(() => { + if (!popup) return; + if (popup.kind === 'edit') { + const v = popup.name?.trim() ?? ''; + setDraft(v); + onChange?.(v); + } else if (popup.kind === 'create') { + setDraft(''); + onChange?.(''); + } else { + setDraft(''); + } +}, [popup, onChange]);📝 Committable suggestion
🤖 Prompt for AI Agents