From 319203cf98cca5e7b9ef9ed7e017847abe39c7cd Mon Sep 17 00:00:00 2001 From: kanghaeun Date: Sat, 1 Aug 2026 23:25:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=A7=81=ED=81=AC=20=EB=8B=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=9B=85=EC=97=90=20=EC=97=90=EB=9F=AC=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B9=84=ED=99=9C=EC=84=B1=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[id]/create/_hooks/usePostTournamentItemLink.ts | 12 ++++++++++-- apps/web/src/hooks/usePostWishLink.ts | 9 +++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.ts b/apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.ts index a40755c3..caa50bd2 100644 --- a/apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.ts +++ b/apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.ts @@ -6,7 +6,15 @@ import type { ApiErrorResponseT } from '@/types/api'; import { postTournamentItemLink } from '../_apis/postTournamentItemLink'; -export const usePostTournamentItemLink = (tournamentId: number) => { +type UsePostTournamentItemLinkOptionsT = { + /** 입력 폼처럼 에러를 화면 안에서 안내하는 경우 false — 4xx 토스트를 끈다 */ + showErrorToast?: boolean; +}; + +export const usePostTournamentItemLink = ( + tournamentId: number, + { showErrorToast = true }: UsePostTournamentItemLinkOptionsT = {} +) => { const queryClient = useQueryClient(); const { mutate: postTournamentItemLinkMutation, isPending: isPostTournamentItemLinkPending } = @@ -25,7 +33,7 @@ export const usePostTournamentItemLink = (tournamentId: number) => { if (status < 500) { const clientErrorMessage = detail ?? '요청을 처리하지 못했습니다.'; - toast.error(clientErrorMessage); + if (showErrorToast) toast.error(clientErrorMessage); return; } diff --git a/apps/web/src/hooks/usePostWishLink.ts b/apps/web/src/hooks/usePostWishLink.ts index 373f52fb..382d1d86 100644 --- a/apps/web/src/hooks/usePostWishLink.ts +++ b/apps/web/src/hooks/usePostWishLink.ts @@ -9,7 +9,12 @@ import { ROUTES } from '@/consts/route'; import type { ApiErrorResponseT } from '@/types/api'; import { logAnalyticsEvent } from '@/utils/analytics'; -export const usePostWishLink = () => { +type UsePostWishLinkOptionsT = { + /** 입력 폼처럼 에러를 화면 안에서 안내하는 경우 false — 4xx 토스트를 끈다 */ + showErrorToast?: boolean; +}; + +export const usePostWishLink = ({ showErrorToast = true }: UsePostWishLinkOptionsT = {}) => { const router = useRouter(); const pathname = usePathname(); const queryClient = useQueryClient(); @@ -35,7 +40,7 @@ export const usePostWishLink = () => { if (status < 500) { const clientErrorMessage = detail ?? '요청을 처리하지 못했습니다.'; - toast.error(clientErrorMessage); + if (showErrorToast) toast.error(clientErrorMessage); return; } From e62eebd83976f064bbc861a3c1ac7c6232d87d20 Mon Sep 17 00:00:00 2001 From: kanghaeun Date: Sat, 1 Aug 2026 23:27:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=A7=81=ED=81=AC=EB=A1=9C=20?= =?UTF-8?q?=EB=8B=B4=EA=B8=B0=20=EC=8B=A4=ED=8C=A8=20=EC=82=AC=EC=9C=A0?= =?UTF-8?q?=EB=A5=BC=20helper=20text=20=EB=A1=9C=20=EC=95=88=EB=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../get-item-dialog/ByLinkDialog.tsx | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/apps/web/src/components/get-item-dialog/ByLinkDialog.tsx b/apps/web/src/components/get-item-dialog/ByLinkDialog.tsx index e26f6169..81be1ebb 100644 --- a/apps/web/src/components/get-item-dialog/ByLinkDialog.tsx +++ b/apps/web/src/components/get-item-dialog/ByLinkDialog.tsx @@ -11,6 +11,7 @@ import Input from '@/components/input'; import { usePostWishLink } from '@/hooks/usePostWishLink'; import type { ItemTypeT } from '@/types/item'; import { URL_PATTERN, extractUrlFromText } from '@/utils/extractUrl'; +import { getApiErrorMessage } from '@/utils/getApiErrorMessage'; type ByLinkProps = { type: ItemTypeT; @@ -20,12 +21,15 @@ type ByLinkProps = { function ByLinkDialog({ type, open, onOpenChange }: ByLinkProps) { const { id: tournamentId } = useParams<{ id: string }>(); - const { postWishLinkMutation, isPostWishLinkPending } = usePostWishLink(); + + const { postWishLinkMutation, isPostWishLinkPending } = usePostWishLink({ + showErrorToast: false, + }); const { postTournamentItemLinkMutation, isPostTournamentItemLinkPending } = - usePostTournamentItemLink(Number(tournamentId)); + usePostTournamentItemLink(Number(tournamentId), { showErrorToast: false }); const [url, setUrl] = useState(''); - const [hasError, setHasError] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); const trimmedUrl = url.trim(); const isEmpty = trimmedUrl.length === 0; @@ -33,7 +37,7 @@ function ByLinkDialog({ type, open, onOpenChange }: ByLinkProps) { const resetState = () => { setUrl(''); - setHasError(false); + setErrorMessage(null); }; const handleSubmit = () => { @@ -43,30 +47,31 @@ function ByLinkDialog({ type, open, onOpenChange }: ByLinkProps) { const submitUrl = URL_PATTERN.test(trimmedUrl) ? trimmedUrl : extractUrlFromText(trimmedUrl); if (!submitUrl) { - setHasError(true); + setErrorMessage('올바른 URL 형식으로 입력해주세요.'); + return; + } + + if (!submitUrl.startsWith('https://')) { + setErrorMessage('https 링크만 등록할 수 있어요'); return; } /** 닫기/초기화는 성공 시에만 — 실패 시 URL을 고칠 수 있게 유지. 위시리스트 이동은 usePostWishLink 훅이 조건부로 처리 */ - if (type === 'wish') - postWishLinkMutation(submitUrl, { - onSuccess: () => { - onOpenChange(false); - resetState(); - }, - }); - else - postTournamentItemLinkMutation(submitUrl, { - onSuccess: () => { - onOpenChange(false); - resetState(); - }, - }); + const mutationOptions = { + onSuccess: () => { + onOpenChange(false); + resetState(); + }, + onError: (error: Error) => setErrorMessage(getApiErrorMessage(error)), + }; + + if (type === 'wish') postWishLinkMutation(submitUrl, mutationOptions); + else postTournamentItemLinkMutation(submitUrl, mutationOptions); }; const handleChange = (value: string) => { setUrl(value); - if (hasError) setHasError(false); + if (errorMessage) setErrorMessage(null); }; /** 상품 설명 + URL 형태로 붙여넣으면 URL 만 입력창에 반영한다 */ @@ -98,8 +103,8 @@ function ByLinkDialog({ type, open, onOpenChange }: ByLinkProps) { onChange={event => handleChange(event.target.value)} onPaste={handlePaste} left={} - aria-invalid={hasError} - {...(hasError ? { helperText: '올바른 URL 형식으로 입력해주세요.' } : {})} + aria-invalid={Boolean(errorMessage)} + {...(errorMessage ? { helperText: errorMessage } : {})} autoFocus />