Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
77 changes: 71 additions & 6 deletions apps/client/src/shared/components/sidebar/PopupPortal.tsx
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 {
Expand All @@ -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({
Expand All @@ -18,9 +22,54 @@ export default function PopupPortal({
onCreateConfirm,
onEditConfirm,
onDeleteConfirm,
categoryList,
isToastOpen,
onToastClose,
}: Props) {
const [draft, setDraft] = useState('');

if (!popup) return null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

편집 모달에서 입력을 변경하지 않으면 빈 문자열이 전파됨

draft가 기본값 ''에서 시작하고, 편집 시 입력을 바꾸지 않으면 onEditConfirm에 빈 문자열이 전달될 위험이 있습니다(아래 핸들러 코멘트 참조). 모달 오픈 시점에 popup.namedraft와 상위 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

‼️ 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
const [draft, setDraft] = useState('');
if (!popup) return null;
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]);
if (!popup) return null;
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/PopupPortal.tsx around lines 29-31,
draft is initialized to '' and if the user opens the edit modal but doesn't
change the input an empty string can be propagated on confirm; update the
component to synchronize draft with popup.name when the modal opens or when
popup changes (e.g., add a useEffect that sets setDraft(popup.name ?? '')
whenever popup becomes non-null) so the initial input reflects the current name
and onEditConfirm will receive the existing name if the user makes no edits.


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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

CREATE/PATCH 시 공백값 차단 및 편집 기본값 폴백

  • 생성: 공백-only면 요청 자체를 막습니다.
  • 수정: 입력 변경이 없으면 popup.name으로 폴백, 최종 값은 상위 onChange로도 반영해 일관성을 유지합니다.

적용 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

‼️ 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
const handleCreate = () => {
if (error) return;
onCreateConfirm?.();
};
const handleEdit = () => {
if (error || popup.kind !== 'edit') return;
onEditConfirm?.(popup.id, draft.trim());
};
const handleCreate = () => {
const value = draft.trim();
if (!value || error) return;
onChange?.(value);
onCreateConfirm?.();
};
const handleEdit = () => {
if (popup.kind !== 'edit') return;
const value = (draft || popup.name || '').trim();
if (!value || error) return;
onChange?.(value);
onEditConfirm?.(popup.id, value);
};
🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/PopupPortal.tsx around lines 54 to
63, the create and edit handlers must prevent blank-only submissions and ensure
edits fall back to the original name and sync parent state: for handleCreate,
check trimmed draft and return early if it's empty (do not call
onCreateConfirm); for handleEdit, if popup.kind === 'edit' compute finalName =
draft.trim() || popup.name, call onEditConfirm?.(popup.id, finalName) and also
call onChange?.(finalName) so the parent receives the normalized value; keep
existing error checks.

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} />
Expand All @@ -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}
/>
)}

Expand All @@ -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}
/>
)}

Expand All @@ -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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

토스트 재표시가 한 번만 되고 이후 재등장하지 않는 이슈

AutoDismissToast는 내부 타이머 종료 후 onClose를 호출하지만, 부모에서 isToastOpenfalse로 되돌려주지 않으면 다음 오류 때 재마운트가 되지 않아 토스트가 다시 보이지 않습니다. Sidebar에서 onToastClose를 내려 받아 setToastIsOpen(false)를 호출하세요.

적용 예시는 Sidebar 코멘트 참고.

🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/PopupPortal.tsx around lines
119-129 the AutoDismissToast calls onClose when its internal timer finishes but
the parent boolean isToastOpen is never reset, preventing the toast from
remounting on subsequent errors; update the Sidebar (parent) to accept the
onToastClose callback and in that handler call setToastIsOpen(false) (i.e., pass
a function down that flips the toast state to false when invoked) so the toast
can be reopened on future failures.

</div>
</div>,
document.body
Expand Down
11 changes: 10 additions & 1 deletion apps/client/src/shared/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export function Sidebar() {
setNewCategoryName(name);
};

const [toastIsOpen, setToastIsOpen] = useState(false);

const handleCreateCategory = () => {
createCategory(newCategoryName, {
onSuccess: () => {
Expand All @@ -67,6 +69,7 @@ export function Sidebar() {
},
onError: (error) => {
console.error('카테고리 생성 실패:', error);
setToastIsOpen(true);
},
});
};
Expand All @@ -79,7 +82,10 @@ export function Sidebar() {
queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] });
close();
},
onError: (error) => console.error('카테고리 수정 실패:', error),
onError: (error) => {
console.error('카테고리 수정 실패:', error);
setToastIsOpen(true);
},
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

편집 시 입력 미변경이면 빈 문자열 전송 위험

onEditConfirm에서 전달되는 두 번째 인자를 무시하고, 로컬 상태(newCategoryName)만 사용 중입니다. 편집 모달에서 입력을 바꾸지 않으면 newCategoryName이 빈 문자열이라 빈 이름으로 PATCH가 나갈 수 있습니다. 콜백의 draft를 사용하거나, 최소한 공백-only를 차단하세요.

두 가지 대안 중 택1:

  • 대안 A(권장): draft 사용
-  const handlePatchCategory = (id: number) => {
-    patchCategory(
-      { id, categoryName: newCategoryName },
+  const handlePatchCategory = (id: number, name?: string) => {
+    const next = (name ?? newCategoryName).trim();
+    if (!next) { setToastIsOpen(true); return; }
+    patchCategory(
+      { id, categoryName: next },
       {
  • 대안 B(최소 수정): 모달 오픈 시 newCategoryName을 현재 이름으로 동기화
+ // popup 변경 시 편집 초기값을 동기화
+ useEffect(() => {
+   if (popup?.kind === 'edit') {
+     setNewCategoryName(getCategoryName(popup.id));
+   } else if (popup?.kind === 'create') {
+     setNewCategoryName('');
+   }
+ }, [popup]);

(대안 B 사용 시 useEffect import 필요)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/client/src/shared/components/sidebar/Sidebar.tsx around lines 85-89, the
edit-confirm handler ignores the provided draft and relies on local
newCategoryName which can be empty, causing an empty-name PATCH; fix by using
the callback's second argument (draft) as the source of truth when sending the
PATCH (preferred), trimming and validating it to block blank-only names before
calling the API, and falling back to the existing name if draft is falsy;
alternatively, if you choose the minimal change, initialize/sync newCategoryName
to the current category name when opening the modal (requires importing
useEffect) and also trim/validate before submitting to prevent whitespace-only
names.

);
};
Expand All @@ -92,6 +98,7 @@ export function Sidebar() {
},
onError: (error) => {
console.error('카테고리 삭제 실패:', error);
setToastIsOpen(true);
},
});
};
Expand Down Expand Up @@ -185,6 +192,8 @@ export function Sidebar() {
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}
/>
</aside>
);
Expand Down
Loading