Conversation
into fix/#111/sp1-qa-popup
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough팝업 입력 검증(공백·길이·중복)과 draft 상태를 추가하고, 생성/수정/삭제 핸들러로 제어 흐름을 분리했습니다. 실패 시 토스트를 표시하도록 PopupPortal과 Sidebar 간에 토스트 상태와 카테고리 목록을 전달하도록 인터페이스가 확장되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant Sidebar as Sidebar
participant Popup as PopupPortal
participant Validator as Validation
participant API as Category API
participant Toast as AutoDismissToast
User->>Popup: 입력/확인 클릭 (create/edit/delete)
alt create/edit
Popup->>Validator: 이름 검증(트림, 길이≤10, 중복제외)
Validator-->>Popup: 검증 결과
alt valid
Popup->>API: mutate(create|edit)
API-->>Sidebar: onSuccess
Sidebar-->>User: 팝업 닫음
else invalid
Popup->>Toast: show(helperText, actionLabel)
Toast-->>User: 오류 메시지(자동 닫힘)
end
else delete
Popup->>API: mutate(delete)
API-->>Sidebar: onSuccess / onError
alt onError
Sidebar->>Popup: isToastOpen=true
Popup->>Toast: show(delete 실패)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (5)
apps/client/src/shared/components/sidebar/PopupPortal.tsx (3)
2-3: draft 초기화용 useEffect 누락 — import 보강 필요편집 모달이 열릴 때 초깃값 동기화를 위해
useEffect가 필요합니다. 먼저useEffectimport를 추가해 주세요.적용 diff:
-import { useState } from 'react'; +import { useEffect, useState } from 'react';
33-47: 빈 입력 허용으로 인해 불필요한 요청 가능현재 검증 함수는
value가 비어 있으면null을 반환하여 에러가 없다고 판단합니다. 이 상태에서 생성/수정 버튼을 누르면 빈 이름으로 API 호출이 갈 수 있습니다(실패 → 토스트). UX/에러율을 줄이려면 액션 이전에 공백-only를 차단하세요.적용 diff(검증 메시지는 그대로 두되, 액션 단에서 차단):
- if (!value) return null; + if (!value) return null; // UI 에러 노출은 최소화하되, 액션에서 별도 가드추가로 아래 핸들러 가드 패치를 함께 적용해 주세요(다음 코멘트 참조).
41-47: 중복 검사 시 좌우 공백 정규화 권장좌변도
trim()하여"이름"vs"이름 "같은 케이스를 동일하게 취급하도록 하는 편이 안전합니다.적용 diff:
- category.name === value && + category.name.trim() === value &&apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
71-73: 생성 실패 전, 공백 입력 사전 차단 권장빈/공백-only 이름이면 API 호출 전에 빠르게 리턴하고 토스트만 노출하는 편이 UX와 서버 부하에 유리합니다.
적용 diff:
const handleCreateCategory = () => { - createCategory(newCategoryName, { + if (!newCategoryName.trim()) { + setToastIsOpen(true); + return; + } + createCategory(newCategoryName.trim(), { onSuccess: () => {
101-102: 삭제 실패 토스트 재사용성을 위한 헬퍼 함수화 제안
setToastIsOpen(true)호출이 세 곳에서 반복됩니다. 작은 헬퍼(openErrorToast)로 통일하면 후속 변경(예: 트래킹 추가)이 쉬워집니다.적용 예:
const openErrorToast = () => setToastIsOpen(true); /* ... onError: () => openErrorToast() ... */
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/shared/components/sidebar/PopupPortal.tsx(6 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/client/src/shared/components/sidebar/PopupPortal.tsxapps/client/src/shared/components/sidebar/Sidebar.tsx
🧬 Code graph analysis (1)
apps/client/src/shared/components/sidebar/PopupPortal.tsx (2)
packages/design-system/src/components/toast/hooks/uesFadeOut.tsx (1)
AutoDismissToast(12-49)packages/design-system/src/components/toast/Toast.tsx (1)
Toast(7-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (2)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
145-157: 기본 카테고리('안 읽은 정보') 삭제 방지 검증 — 재실행 필요이전 스크립트가 "unrecognized file type: tsx" 오류로 실패했습니다. 아래 수정된 스크립트로 재실행하여 옵션 메뉴에서 기본 카테고리('안 읽은 정보')가 삭제되지 않도록 비활성화(또는 가드)되어 있는지 확인.
#!/bin/bash # .ts/.tsx 파일에서 기본 카테고리 문자열 및 삭제 핸들러 검색 rg -n -C3 "안 읽은 정보" -g "*.ts" -g "*.tsx" || true rg -n -C3 -e "openDelete\(" -e "onDelete" -e "deleteCategory" -g "*.ts" -g "*.tsx" || true대상 파일: apps/client/src/shared/components/sidebar/Sidebar.tsx (라인 145–157) 및 옵션 메뉴 관련 코드 전역 검색.
apps/client/src/shared/components/sidebar/PopupPortal.tsx (1)
64-68: 기본 카테고리("안 읽은 정보") 삭제 방지 로직 확인 필요리포지토리에서 '안 읽은 정보'·삭제 금지 관련 문자열은 검색되지 않았고, PopupPortal.handleDelete는 onDeleteConfirm?.(popup.id)만 호출합니다 — 상위(OptionsMenuPortal → Sidebar/useCategoryPopups)에서 차단 중인지 확인하거나, 없다면 삭제 가드(예: category.name === '안 읽은 정보' 검사)를 추가하세요. 참고 파일: apps/client/src/shared/components/sidebar/PopupPortal.tsx (handleDelete), apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx (onDelete 호출), apps/client/src/shared/components/sidebar/Sidebar.tsx (PopupPortal 사용), apps/client/src/shared/apis/axios.ts (deleteCategory API).
| const [draft, setDraft] = useState(''); | ||
|
|
||
| if (!popup) return null; |
There was a problem hiding this comment.
편집 모달에서 입력을 변경하지 않으면 빈 문자열이 전파됨
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
‼️ 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.
| 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 handleCreate = () => { | ||
| if (error) return; | ||
| onCreateConfirm?.(); | ||
| }; | ||
|
|
||
| const handleEdit = () => { | ||
| if (error || popup.kind !== 'edit') return; | ||
| onEditConfirm?.(popup.id, draft.trim()); | ||
| }; | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| {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> | ||
| )} |
There was a problem hiding this comment.
토스트 재표시가 한 번만 되고 이후 재등장하지 않는 이슈
AutoDismissToast는 내부 타이머 종료 후 onClose를 호출하지만, 부모에서 isToastOpen을 false로 되돌려주지 않으면 다음 오류 때 재마운트가 되지 않아 토스트가 다시 보이지 않습니다. 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.
| onError: (error) => { | ||
| console.error('카테고리 수정 실패:', error); | ||
| setToastIsOpen(true); | ||
| }, | ||
| } |
There was a problem hiding this comment.
편집 시 입력 미변경이면 빈 문자열 전송 위험
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.
| onEditConfirm={(id) => handlePatchCategory(id)} | ||
| onDeleteConfirm={(id) => handleDeleteCategory(id)} |
There was a problem hiding this comment.
🛠️ 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.
| 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.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
96-106: 기본 카테고리('안 읽은 정보') 삭제 차단 로직 없음 — UI에서 삭제 호출 차단 필요레포지토리에서 '삭제 불가' 관련 로직/문구가 검색되지 않았으므로, OptionsMenu/Popup 단계에서 삭제 버튼을 비활성화/비표시하고 handleDeleteCategory에서도 기본 카테고리(id 또는 name 검사)면 삭제 호출을 early-return 하세요.
참고 위치:
- apps/client/src/shared/components/sidebar/OptionsMenuPortal.tsx: ~35–41 (onDelete 호출)
- apps/client/src/shared/components/sidebar/Sidebar.tsx: ~94–100 (handleDeleteCategory), ~183–187 (openDelete → onDelete 경로)
- apps/client/src/shared/apis/queries.ts: ~50–55 (useDeleteCategory mutation)
♻️ Duplicate comments (4)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
213-216: AutoDismissToast onClose → 상태 리셋 연결 완료
onToastClose={() => setToastIsOpen(false)}로 재노출 문제 해결되었습니다.
206-216: 편집 확정 시 draft 무시 → 빈 이름 PATCH 위험PopupPortal이
onEditConfirm(id, draft)로 정상 전달하지만 여기서 draft를 버리고newCategoryName을 사용해 공백/미변경 시 빈 문자열 PATCH가 발생할 수 있습니다. draft를 수용하고 trim/검증 후 전송하세요.패치 핸들러 보강:
- 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 }, { onSuccess: () => { setNewCategoryName(''); queryClient.invalidateQueries({ queryKey: ['dashboardCategories'] }); close(); }, onError: () => { setToastIsOpen(true); }, } ); };draft 전달:
- onEditConfirm={(id) => handlePatchCategory(id)} + onEditConfirm={(id, draft) => handlePatchCategory(id, draft)}Also applies to: 80-94
apps/client/src/shared/components/sidebar/PopupPortal.tsx (2)
37-41: 편집 모달 초기값 동기화 LGTM — 부모 상태도 동기화 권장초깃값을 draft로 세팅한 점 좋습니다. 부모의
newCategoryName과도 맞추면 불일치가 사라집니다.useEffect(() => { if (!popup) return; - setDraft(popup.kind === 'edit' ? (popup.name ?? '') : ''); + const init = popup.kind === 'edit' ? (popup.name ?? '') : ''; + setDraft(init); + onChange?.(init); }, [popup]);
78-86: 확정 전 부모 onChange에 정규화 값 전달로 상태 일관성 확보생성/수정 모두 confirm 시
value(trimmed)를 부모에도 반영하면 Sidebar의 내부 상태와 일치합니다(특히 생성 시 후단으로 공백이 포함되지 않도록).const handleCreate = () => { - if (blocked) return; - onCreateConfirm?.(); + if (blocked) return; + onChange?.(value); + onCreateConfirm?.(); }; const handleEdit = () => { - if (blocked || popup.kind !== 'edit') return; - onEditConfirm?.(popup.id, value); + if (blocked || popup.kind !== 'edit') return; + onChange?.(value); + onEditConfirm?.(popup.id, value); };Also applies to: 83-86
🧹 Nitpick comments (3)
apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx (1)
25-27: role="menu" 사용 시 ARIA 시맨틱 보완 필요현재 컨테이너가 menu 역할이지만 내부 버튼은 menuitem/키보드 내비게이션(방향키, roving tabindex)이 없습니다. 메뉴 시맨틱을 유지하지 않을 거라면 group 으로 낮추는 편이 안전합니다.
적용 예:
- role="menu" + role="group"혹은 menu 유지 시 각 버튼에 role="menuitem"과 적절한 키보드 핸들링을 추가하세요.
apps/client/src/shared/components/sidebar/PopupPortal.tsx (2)
44-69: 검증 로직 적절 — 공백 트리밍 비교로 중복 판정 강화 제안기존/신규 이름의 사소한 공백 차이를 동일하게 취급하려면 비교 시
trim()을 적용하세요.- !!categoryList?.some( - (c) => c.name === value && (popup.kind === 'create' || c.id !== popup.id) - ); + !!categoryList?.some( + (c) => c.name.trim() === value && (popup.kind === 'create' || c.id !== popup.id) + );
144-155: 토스트 표시 시간이 1초로 다소 짧음가독/인지 시간을 고려해 2–3초로 늘리는 것을 검토하세요.
- duration={1000} + duration={2000}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx(1 hunks)apps/client/src/shared/components/sidebar/PopupPortal.tsx(4 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsxapps/client/src/shared/components/sidebar/PopupPortal.tsx
🧬 Code graph analysis (2)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
apps/client/src/shared/components/sidebar/CreateItem.tsx (1)
CreateItem(9-27)
apps/client/src/shared/components/sidebar/PopupPortal.tsx (3)
apps/client/src/shared/hooks/useCategoryPopups.ts (1)
PopupState(3-7)packages/design-system/src/components/toast/hooks/uesFadeOut.tsx (1)
AutoDismissToast(12-49)packages/design-system/src/components/toast/Toast.tsx (1)
Toast(7-21)
🔇 Additional comments (4)
apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx (1)
12-12: 버튼 내용 중앙 정렬 변경 LGTM디자인 정렬 목적에 부합하며 다른 동작에는 영향 없습니다.
apps/client/src/shared/components/sidebar/Sidebar.tsx (3)
63-66: 팝업 전환 시 토스트 상태 리셋 적절함팝업 변경마다 토스트를 닫아 재노출이 보장됩니다.
166-171: 팝업 오픈 전 토스트 상태 초기화 LGTM팝업 열기 전에 토스트를 닫아 잔존 토스트가 새 동작을 가리는 문제를 예방합니다.
Also applies to: 180-187
108-111: 팝업 닫힘 시 토스트도 함께 리셋하는 결합 처리 Good일관된 종료 동작입니다.
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
📷 Screenshot
2025-09-18.3.42.05.mov
2025-09-17.4.33.54.mov
Summary by CodeRabbit
New Features
UI/UX
Style