diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c007a62..ad002918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Fixed + +- 링크 추가에서 URL을 붙여넣으면 즉시 중복을 확인하고, 메타정보 수집 실패는 토스트와 제목 입력 안내로 표시하도록 수정했습니다. +- 자동 수집한 제목을 100자로 제한하고, 사용자가 수정하거나 지운 제목을 자동 수집 결과가 덮어쓰지 않도록 수정했습니다. 변경 전 URL의 늦은 응답도 무시합니다. +- 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..def22295 --- /dev/null +++ b/src/components/layout/SideNavigation/components/MenuSection/AddLink/hooks/useAddLinkForm.browser.spec.tsx @@ -0,0 +1,279 @@ +import type { Link } from '@/types/link'; +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'; +import { useDuplicateLinkPrefill } from './useDuplicateLinkPrefill'; + +const mocks = vi.hoisted(() => ({ + scrape: vi.fn(), + duplicate: vi.fn(), + fetchLink: 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: mocks.fetchLink })); +vi.mock('@/stores/toastStore', () => ({ showToast: mocks.toast })); + +let form: ReturnType; +function FormHarness() { + form = useAddLinkForm(); + const duplicate = useDuplicateCheck(form.trimmedUrl, form.isValidUrl); + useDuplicateLinkPrefill( + form.form, + duplicate.isDuplicate, + duplicate.duplicateLinkData, + form.metaLoading, + form.hasEditedTitle + ); + return ( + <> +