From 9ce840f091f54fd53c033b823ead1cef5acddfc6 Mon Sep 17 00:00:00 2001 From: Goder Date: Sun, 18 Oct 2026 11:47:44 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Fix:=20=EB=A7=81=ED=81=AC=20=EB=B6=99?= =?UTF-8?q?=EC=97=AC=EB=84=A3=EA=B8=B0=EC=99=80=20=EB=A9=94=ED=83=80?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=9E=85=EB=A0=A5=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84=20(#603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 붙여넣기 확정 시 중복을 즉시 확인하고 메타 수집 실패와 제목 길이를 일관되게 처리 --- CHANGELOG.md | 5 + package.json | 1 + .../AddLink/LinkThumbnailTitleSection.tsx | 12 +- .../hooks/useAddLinkForm.browser.spec.tsx | 187 ++++++++++++++++++ .../AddLink/hooks/useAddLinkForm.ts | 106 ++++++++-- .../AddLink/hooks/useDuplicateCheck.ts | 43 ++-- .../components/MenuSection/AddLink/index.tsx | 8 +- src/hooks/useLinkMetaScrape.ts | 56 ++++-- vitest.browser.config.ts | 27 +++ 9 files changed, 378 insertions(+), 67 deletions(-) create mode 100644 src/components/layout/SideNavigation/components/MenuSection/AddLink/hooks/useAddLinkForm.browser.spec.tsx create mode 100644 vitest.browser.config.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c007a62..90d09ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Fixed + +- 링크 추가에서 URL을 붙여넣으면 즉시 중복을 확인하고, 메타정보 수집 실패는 토스트와 제목 입력 안내로 표시하도록 수정했습니다. +- 자동 수집한 제목을 100자로 제한하고, 사용자가 수정하거나 지운 제목을 자동 수집 결과가 덮어쓰지 않도록 수정했습니다. 변경 전 URL의 늦은 응답도 무시합니다. + ## [v0.1.3] - 26.10.06 ### Changed diff --git a/package.json b/package.json index 74eac5a7..70b052fb 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "format": "prettier --plugin=@trivago/prettier-plugin-sort-imports --config .prettierrc --write \"src/**/*.{js,jsx,ts,tsx}\"", "chromatic": "node scripts/run-chromatic.js", "test:unit": "vitest run --config vitest.unit.config.ts", + "test:browser": "vitest run --config vitest.browser.config.ts", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, diff --git a/src/components/layout/SideNavigation/components/MenuSection/AddLink/LinkThumbnailTitleSection.tsx b/src/components/layout/SideNavigation/components/MenuSection/AddLink/LinkThumbnailTitleSection.tsx index cfc0918e..00b8bddb 100644 --- a/src/components/layout/SideNavigation/components/MenuSection/AddLink/LinkThumbnailTitleSection.tsx +++ b/src/components/layout/SideNavigation/components/MenuSection/AddLink/LinkThumbnailTitleSection.tsx @@ -12,6 +12,8 @@ import { AddLinkForm } from './hooks/useAddLinkForm'; interface Props { control: Control; errors: FieldErrors; + hasEditedTitle: boolean; + onTitleChange: (value: string) => void; metaLoading: boolean; isValidUrl: boolean; shouldDisableDetails: boolean; @@ -21,6 +23,8 @@ interface Props { export default function LinkThumbnailTitleSection({ control, errors, + hasEditedTitle, + onTitleChange, metaLoading, isValidUrl, shouldDisableDetails, @@ -65,15 +69,19 @@ export default function LinkThumbnailTitleSection({ maxHeightLines={2} showMax={isTitleFocused} onFocus={() => setIsTitleFocused(true)} + onBlur={() => { + setIsTitleFocused(false); + field.onBlur(); + }} maxLength={MAX_TITLE_LENGTH} isLoading={metaLoading && isValidUrl} disabled={shouldDisableDetails} value={field.value ?? ''} - onChange={e => field.onChange(e)} + onChange={e => onTitleChange(e.target.value)} /> )} /> - {errors.title && !shouldDisableDetails && ( + {hasEditedTitle && !isTitleFocused && errors.title && !shouldDisableDetails && ( {errors.title.message as string} )} diff --git a/src/components/layout/SideNavigation/components/MenuSection/AddLink/hooks/useAddLinkForm.browser.spec.tsx b/src/components/layout/SideNavigation/components/MenuSection/AddLink/hooks/useAddLinkForm.browser.spec.tsx new file mode 100644 index 00000000..4e4456d2 --- /dev/null +++ b/src/components/layout/SideNavigation/components/MenuSection/AddLink/hooks/useAddLinkForm.browser.spec.tsx @@ -0,0 +1,187 @@ +import { StrictMode, act } from 'react'; +import { type Root, createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useAddLinkForm } from './useAddLinkForm'; +import { useDuplicateCheck } from './useDuplicateCheck'; + +const mocks = vi.hoisted(() => ({ scrape: vi.fn(), duplicate: vi.fn(), toast: vi.fn() })); +vi.mock('@/hooks/usePostLinkMetaScrape', () => ({ + usePostLinkMetaScrape: () => ({ mutateAsync: mocks.scrape, status: 'idle', reset: vi.fn() }), +})); +vi.mock('@/hooks/useCheckDuplicateLink', () => ({ + useDuplicateLinkMutation: () => ({ mutate: mocks.duplicate }), +})); +vi.mock('@/apis/linkApi', () => ({ fetchLink: vi.fn() })); +vi.mock('@/stores/toastStore', () => ({ showToast: mocks.toast })); + +let form: ReturnType; +function FormHarness() { + form = useAddLinkForm(); + useDuplicateCheck(form.trimmedUrl, form.isValidUrl); + return ( + <> +