From 61b3c064273ef9d6064a43a2f656eb23b9c6de63 Mon Sep 17 00:00:00 2001 From: seojeongm Date: Thu, 6 Nov 2025 11:59:22 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EA=B2=80=EC=83=89=EC=96=B4=202?= =?UTF-8?q?=EA=B8=80=EC=9E=90=20=EB=AF=B8=EB=A7=8C=EC=9D=BC=20=EB=95=8C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/search/SearchPage.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/pages/search/SearchPage.tsx b/src/pages/search/SearchPage.tsx index 8dfdfb9..5f0bf89 100644 --- a/src/pages/search/SearchPage.tsx +++ b/src/pages/search/SearchPage.tsx @@ -25,6 +25,14 @@ const SearchPage = () => { setArticles([]); return; } + + // 검색어 길이 확인 + if (q.length < 2) { + setError('2글자 이상으로 검색해 주세요.'); + setArticles([]); + return; + } + setLoading(true); setError(null); console.log('[SearchPage] runSearch:start', { q, page, size, threshold }); @@ -39,6 +47,11 @@ const SearchPage = () => { totalCount: result.totalCount, }); + // 검색 결과가 없을 때 문구 표시 + if (result.articles.length === 0) { + setError('검색 결과가 없습니다.'); + } + setArticles(result.articles); } catch (e) { console.log('[SearchPage] runSearch:error', e); From e7eaf18eb1b411d955aff5a8c69aa1825f7dd0ad Mon Sep 17 00:00:00 2001 From: seojeongm Date: Thu, 6 Nov 2025 12:19:42 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20204=20=EC=97=90=EB=9F=AC=20=EB=94=B0?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20api?= =?UTF-8?q?.ts=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=9D=98=EB=AF=B8=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=80=EC=83=89=EC=96=B4=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=AC=B8=EA=B5=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/search/SearchPage.tsx | 34 ++++++++------- src/shared/apis/api.ts | 75 +++++++++------------------------ 2 files changed, 40 insertions(+), 69 deletions(-) diff --git a/src/pages/search/SearchPage.tsx b/src/pages/search/SearchPage.tsx index 5f0bf89..00999ed 100644 --- a/src/pages/search/SearchPage.tsx +++ b/src/pages/search/SearchPage.tsx @@ -23,10 +23,11 @@ const SearchPage = () => { const run = async () => { if (!q) { setArticles([]); + setError(null); return; } - // 검색어 길이 확인 + // 1) 2글자 미만 → 에러 문구 if (q.length < 2) { setError('2글자 이상으로 검색해 주세요.'); setArticles([]); @@ -35,26 +36,29 @@ const SearchPage = () => { setLoading(true); setError(null); - console.log('[SearchPage] runSearch:start', { q, page, size, threshold }); try { const result = await semanticSearch({ query: q, page, size, threshold }); + setArticles(result.articles ?? []); + } catch (e: any) { + const status = e?.response?.status; + const message = e?.response?.data?.message; + + // 1️⃣ 검색 결과 없음 (빈 데이터) + if (status === 204) { + setArticles([]); + setError(null); // 에러 아님 → "검색 결과 없음" 표시 + return; + } - console.log('[SearchPage] runSearch:done', { - q_from_url: q, - q_in_result: result.query, - articles_len: result.articles.length, - totalCount: result.totalCount, - }); - - // 검색 결과가 없을 때 문구 표시 - if (result.articles.length === 0) { - setError('검색 결과가 없습니다.'); + // 2️⃣ 백엔드가 검색어 부적절 판단 + if (status === 400 && message?.includes('유효한 검색어')) { + setError('유효한 검색어가 없습니다. 의미 있는 단어를 입력하세요.'); + setArticles([]); + return; } - setArticles(result.articles); - } catch (e) { - console.log('[SearchPage] runSearch:error', e); + // 3️⃣ 그 외 일반 오류 setError('검색 중 오류가 발생했습니다.'); setArticles([]); } finally { diff --git a/src/shared/apis/api.ts b/src/shared/apis/api.ts index 8714f36..bc9e569 100644 --- a/src/shared/apis/api.ts +++ b/src/shared/apis/api.ts @@ -3,91 +3,58 @@ import type { AxiosResponse } from 'axios'; import type { ApiRequestOptionsTypes, ApiResponseTypes } from '../types/apiTypes'; import axiosInstance from './axios'; -/** - * 기본 API 객체 - * - * 사용 예시: - * - GET: api.get('/users') - * - POST: api.post('/users', userData) - * - PUT: api.put('/users/1', userData) - * - DELETE: api.delete('/users/1') - */ +function extract(response: AxiosResponse): T { + if (response.status === 204 || response.data == null || response.data === '') { + return undefined as T; + } + const data = response.data as ApiResponseTypes; + if (!data.isSuccess) { + throw new Error(data.message || 'API 요청 실패'); + } + return data.result as T; +} + const api = { - /** - * GET 요청 - */ get: async (url: string, options?: ApiRequestOptionsTypes): Promise => { - const response: AxiosResponse> = await axiosInstance.get(url, { + const res = await axiosInstance.get(url, { params: options?.params, headers: options?.headers, timeout: options?.timeout, }); - if (!response.data.isSuccess) { - throw new Error(response.data.message || 'API 요청 실패'); - } - - return response.data.result as T; + return extract(res); }, - /** - * POST 요청 - */ post: async (url: string, data?: unknown, options?: ApiRequestOptionsTypes): Promise => { - const response: AxiosResponse> = await axiosInstance.post(url, data, { + const res = await axiosInstance.post(url, data, { headers: options?.headers, timeout: options?.timeout, }); - if (!response.data.isSuccess) { - throw new Error(response.data.message || 'API 요청 실패'); - } - - return response.data.result as T; + return extract(res); }, - /** - * PUT 요청 - */ put: async (url: string, data?: unknown, options?: ApiRequestOptionsTypes): Promise => { - const response: AxiosResponse> = await axiosInstance.put(url, data, { + const res = await axiosInstance.put(url, data, { headers: options?.headers, timeout: options?.timeout, }); - if (!response.data.isSuccess) { - throw new Error(response.data.message || 'API 요청 실패'); - } - - return response.data.result as T; + return extract(res); }, - /** - * PATCH 요청 - */ patch: async (url: string, data?: unknown, options?: ApiRequestOptionsTypes): Promise => { - const response: AxiosResponse> = await axiosInstance.patch(url, data, { + const res = await axiosInstance.patch(url, data, { headers: options?.headers, timeout: options?.timeout, }); - if (!response.data.isSuccess) { - throw new Error(response.data.message || 'API 요청 실패'); - } - - return response.data.result as T; + return extract(res); }, - /** - * DELETE 요청 - */ delete: async (url: string, options?: ApiRequestOptionsTypes): Promise => { - const response: AxiosResponse> = await axiosInstance.delete(url, { + const res = await axiosInstance.delete(url, { params: options?.params, headers: options?.headers, timeout: options?.timeout, }); - if (!response.data.isSuccess) { - throw new Error(response.data.message || 'API 요청 실패'); - } - - return response.data.result as T; + return extract(res); }, }; From 2968b3fd0ea073afe4791d46f9600b1ed1779a77 Mon Sep 17 00:00:00 2001 From: seojeongm Date: Thu, 6 Nov 2025 14:38:28 +0900 Subject: [PATCH 3/3] style: fix prettier formatting in SearchPage (#213) --- src/pages/search/SearchPage.tsx | 238 ++++++++++++++++---------------- 1 file changed, 119 insertions(+), 119 deletions(-) diff --git a/src/pages/search/SearchPage.tsx b/src/pages/search/SearchPage.tsx index c0c466c..a1dd97e 100644 --- a/src/pages/search/SearchPage.tsx +++ b/src/pages/search/SearchPage.tsx @@ -10,127 +10,127 @@ const MIN_LENGTH_MESSAGE = '검색어는 두 글자 이상이어야 합니다.'; const EMPTY_RESULT_MESSAGE = '검색 결과가 없습니다.'; const SearchPage = () => { - const [params] = useSearchParams(); - const q = (params.get('query') ?? '').trim(); - - const threshold = 0.7; - const page = 0; - const size = 10; - - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [showLengthPopup, setShowLengthPopup] = useState(false); - const [articles, setArticles] = useState([]); - - useEffect(() => { - const run = async () => { - if (!q) { - setLoading(false); - setError(null); - setShowLengthPopup(false); - setArticles([]); - return; - } - - if (q.length < 2) { - setLoading(false); - setShowLengthPopup(true); - setError(null); - setArticles([]); - return; - } - - setLoading(true); - setShowLengthPopup(false); - setError(null); - - try { - const result = await semanticSearch({ query: q, page, size, threshold }); - setArticles(result.articles ?? []); - } catch (e: any) { - const status = e?.response?.status; - const message = e?.response?.data?.message; - - if (status === 204) { - setArticles([]); - setError(null); - return; - } - - if (status === 400 && message?.includes('유효한 검색어')) { - setError('유효한 검색어가 없습니다. 의미 있는 단어를 입력하세요.'); - setArticles([]); - return; - } - - setError('검색 중 오류가 발생했습니다.'); - setArticles([]); - } finally { - setLoading(false); - } - }; - - void run(); - }, [q, page, threshold]); - - const errorClassName = useMemo(() => { - if (!error) return ''; - return error === EMPTY_RESULT_MESSAGE ? 'text-black' : 'text-red-500'; - }, [error]); - - const resultsContainerSpacing = showLengthPopup ? 'mt-14' : 'mt-0'; - - return ( -
-
- {showLengthPopup && ( -
- {MIN_LENGTH_MESSAGE} -
- )} - -
- {loading && } - {error &&
{error}
} - {!loading && !error && articles.length === 0 && ( -
{EMPTY_RESULT_MESSAGE}
- )} - {!loading && - !error && - articles.map((a, idx) => ( -
- -
-

- {a.title} -

-

- {a.summary} -

-
- - - {idx < articles.length - 1 && ( - <> - -
- + const [params] = useSearchParams(); + const q = (params.get('query') ?? '').trim(); + + const threshold = 0.7; + const page = 0; + const size = 10; + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showLengthPopup, setShowLengthPopup] = useState(false); + const [articles, setArticles] = useState([]); + + useEffect(() => { + const run = async () => { + if (!q) { + setLoading(false); + setError(null); + setShowLengthPopup(false); + setArticles([]); + return; + } + + if (q.length < 2) { + setLoading(false); + setShowLengthPopup(true); + setError(null); + setArticles([]); + return; + } + + setLoading(true); + setShowLengthPopup(false); + setError(null); + + try { + const result = await semanticSearch({ query: q, page, size, threshold }); + setArticles(result.articles ?? []); + } catch (e: any) { + const status = e?.response?.status; + const message = e?.response?.data?.message; + + if (status === 204) { + setArticles([]); + setError(null); + return; + } + + if (status === 400 && message?.includes('유효한 검색어')) { + setError('유효한 검색어가 없습니다. 의미 있는 단어를 입력하세요.'); + setArticles([]); + return; + } + + setError('검색 중 오류가 발생했습니다.'); + setArticles([]); + } finally { + setLoading(false); + } + }; + + void run(); + }, [q, page, threshold]); + + const errorClassName = useMemo(() => { + if (!error) return ''; + return error === EMPTY_RESULT_MESSAGE ? 'text-black' : 'text-red-500'; + }, [error]); + + const resultsContainerSpacing = showLengthPopup ? 'mt-14' : 'mt-0'; + + return ( +
+
+ {showLengthPopup && ( +
+ {MIN_LENGTH_MESSAGE} +
)} -
- ))} + +
+ {loading && } + {error &&
{error}
} + {!loading && !error && articles.length === 0 && ( +
{EMPTY_RESULT_MESSAGE}
+ )} + {!loading && + !error && + articles.map((a, idx) => ( +
+ +
+

+ {a.title} +

+

+ {a.summary} +

+
+ + + {idx < articles.length - 1 && ( + <> + +
+ + )} +
+ ))} +
+
-
-
- ); + ); }; export default SearchPage;