From 9936d9edade48ce02b4521198773f716402281c1 Mon Sep 17 00:00:00 2001 From: thelightway Date: Mon, 28 Apr 2025 14:37:07 +0900 Subject: [PATCH 01/22] =?UTF-8?q?=F0=9F=93=A6=20Chore:=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-deploy.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/dev-deploy.yml diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml new file mode 100644 index 0000000..b4d858b --- /dev/null +++ b/.github/workflows/dev-deploy.yml @@ -0,0 +1,27 @@ +name: Frontend Deploy + +on: + push: + branches: + - dev + paths-ignore: + - '.github/workflows/**' + +jobs: + deploy: + runs-on: self-hosted + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Copy frontend code to shared volume + run: | + rm -rf /deploy/app/* + cp -r . /deploy/app/ + + - name: Restart Docker (Frontend Node.js) + run: | + cd /deploy + docker-compose down + docker-compose up -d --build From 1127be7107a4ddf9500a9687e1cdceab4461ee12 Mon Sep 17 00:00:00 2001 From: thelightway Date: Mon, 28 Apr 2025 14:44:06 +0900 Subject: [PATCH 02/22] =?UTF-8?q?=F0=9F=93=A6=20Chore:=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 66bb426..6b0dfd5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +Yak+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started From 89c2110214669cdffde086dbff28a9aac07d771d Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Mon, 28 Apr 2025 16:07:39 +0900 Subject: [PATCH 03/22] =?UTF-8?q?=F0=9F=93=A6=20Chore:=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-deploy.yml | 2 +- Dockerfile | 33 ++++++++++++++++++++++++++++++++ docker-compose.yml | 13 +++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index b4d858b..2c69a00 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -22,6 +22,6 @@ jobs: - name: Restart Docker (Frontend Node.js) run: | - cd /deploy + cd /deploy/app docker-compose down docker-compose up -d --build diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a9f653a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# 1) Builder 단계: 의존 설치 + 빌드 +FROM node:18-alpine AS builder +WORKDIR /app + +# package-lock.json까지 복사해서 정확히 설치 +COPY package.json package-lock.json ./ +RUN npm ci + +# 소스 전체 복사 +COPY . . + +# Next.js 빌드 (App Router 기준으로 .next + static 페이지 생성) +RUN npm run build + +# 2) Production 단계: 런타임용으로 경량화 +FROM node:18-alpine +WORKDIR /app + +# production 환경 변수 +ENV NODE_ENV=production + +# 빌드 결과물만 복사 +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/package-lock.json ./package-lock.json +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/public ./public + +# 컨테이너가 열 포트 +EXPOSE 3000 + +# next start 로 서버 구동 +CMD ["npm", "start"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3f99176 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + next-app: + build: + context: . + dockerfile: Dockerfile + container_name: yakplus-frontend + environment: + - NODE_ENV=production + ports: + - "13000:3000" + restart: unless-stopped From 89934c9ad302be259ae68ce8340fed853ab656f9 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:11:03 +0900 Subject: [PATCH 04/22] =?UTF-8?q?=F0=9F=90=9B=20Bug:=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/logo.svg | 1 + ...26\264\352\267\270\353\236\250.drawio.svg" | 4 + src/app/search/symptom/page.js | 331 +++++++++++++++ src/components/Footer.js | 22 + src/components/Header.js | 70 ++++ src/components/NoImage.js | 24 ++ src/components/SearchBar.js | 379 ++++++++++++++++++ 7 files changed, 831 insertions(+) create mode 100644 public/logo.svg create mode 100644 "public/\354\240\234\353\252\251 \354\227\206\353\212\224 \353\213\244\354\235\264\354\226\264\352\267\270\353\236\250.drawio.svg" create mode 100644 src/app/search/symptom/page.js create mode 100644 src/components/Footer.js create mode 100644 src/components/Header.js create mode 100644 src/components/NoImage.js create mode 100644 src/components/SearchBar.js diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..70f14e8 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git "a/public/\354\240\234\353\252\251 \354\227\206\353\212\224 \353\213\244\354\235\264\354\226\264\352\267\270\353\236\250.drawio.svg" "b/public/\354\240\234\353\252\251 \354\227\206\353\212\224 \353\213\244\354\235\264\354\226\264\352\267\270\353\236\250.drawio.svg" new file mode 100644 index 0000000..7424f10 --- /dev/null +++ "b/public/\354\240\234\353\252\251 \354\227\206\353\212\224 \353\213\244\354\235\264\354\226\264\352\267\270\353\236\250.drawio.svg" @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/app/search/symptom/page.js b/src/app/search/symptom/page.js new file mode 100644 index 0000000..7f3b348 --- /dev/null +++ b/src/app/search/symptom/page.js @@ -0,0 +1,331 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { useState, useEffect, Suspense } from 'react'; +import Header from '../../../components/Header'; +import Footer from '../../../components/Footer'; +import NoImage from '../../../components/NoImage'; +import SearchBar from '../../../components/SearchBar'; + +// SearchParams를 사용하는 컴포넌트를 분리 +const SearchResults = () => { + const searchParams = useSearchParams(); + const query = searchParams.get('q'); + const mode = searchParams.get('mode'); + const [selectedType, setSelectedType] = useState('all'); + const [sortBy, setSortBy] = useState('name'); + const [showTypeDropdown, setShowTypeDropdown] = useState(false); + const [showSortDropdown, setShowSortDropdown] = useState(false); + + // 페이지네이션 관련 상태 + const [currentPage, setCurrentPage] = useState(1); + const [totalResults, setTotalResults] = useState(0); // 전체 검색 결과 수 + const itemsPerPage = 10; // 페이지당 항목 수 + + // 임시 검색 결과 데이터 + const mockResults = [ + { + id: 1, + name: "타이레놀", + category: "일반 의약품", + company: "(주)한국얀센 / Janssen Korea", + effect: "아세트아미노펜 과립", + symptoms: ["감기로 인한 발열 및 동통(통증), 두통, 신경통, 근육통, 월경통, 염좌통(삔 통증)"], + image: null + }, + { + id: 2, + name: "부루펜", + category: "일반 의약품", + company: "삼일제약(주) / Samil", + effect: "이부프로펜", + symptoms: ["류마티양 관절염, 연소성 류마티양 관절염, 골관절염(퇴행성 관절질환), 감기로 인한 발열 및 동통, 요통, 월경곤란증, 수술후 동통"], + image: null + }, + { + id: 3, + name: "트라펜정", + category: "전문 의약품", + company: "명문제약(주) / Myungmoon Pharm", + effect: "트라마돌염산염,아세트아미노펜", + symptoms: ["중등도-중증의 급ㆍ만성 통증"], + image: null + }, + ]; + + // 검색어에 따른 결과 필터링 (임시로 "두통" 검색어만 결과 표시) + const searchResults = query?.toLowerCase() === "두통" ? mockResults : []; + + // 선택된 타입에 따라 결과 필터링 + const filteredResults = selectedType === 'all' + ? searchResults + : searchResults.filter(medicine => { + if (selectedType === 'general') return medicine.category === "일반 의약품"; + if (selectedType === 'prescription') return medicine.category === "전문 의약품"; + return true; + }); + + // 정렬 기준에 따라 결과 정렬 + const sortedResults = [...filteredResults].sort((a, b) => { + switch (sortBy) { + case 'name': + return a.name.localeCompare(b.name, 'ko'); + case 'effect': + return a.effect.localeCompare(b.effect, 'ko'); + case 'company': + return a.company.localeCompare(b.company, 'ko'); + default: + return 0; + } + }); + + // 검색 결과 수 업데이트 + useEffect(() => { + setTotalResults(sortedResults.length); + }, [sortedResults.length]); + + // 드롭다운 외부 클릭 시 닫기 + const handleClickOutside = () => { + setShowTypeDropdown(false); + setShowSortDropdown(false); + }; + + const getTypeLabel = () => { + switch (selectedType) { + case 'all': return '전체'; + case 'general': return '일반 의약품'; + case 'prescription': return '전문 의약품'; + default: return '전체'; + } + }; + + const getSortLabel = () => { + switch (sortBy) { + case 'name': return '제품명'; + case 'effect': return '성분명'; + case 'company': return '제약회사'; + default: return '제품명'; + } + }; + + // 현재 필터 상태 텍스트 생성 + const getFilterStatusText = () => { + const typeText = selectedType === 'all' ? '' : `${getTypeLabel()} · `; + const sortText = `${getSortLabel()} 순`; + return `${typeText}${sortText}`; + }; + + return ( +
+
+
+
+ {/* 검색창 영역 */} +
+ +
+ + {/* 필터 영역 - 결과가 있을 때만 표시 */} + {sortedResults.length > 0 && ( +
+ {/* 검색 결과 정보 */} +
+ + 총 {totalResults}개의 검색결과 + + | + {getFilterStatusText()} +
+ + {/* 필터 버튼들 */} +
+ {/* 의약품 구분 드롭다운 */} +
+ + {showTypeDropdown && ( +
+
+ + + +
+
+ )} +
+ + {/* 정렬 기준 드롭다운 */} +
+ + {showSortDropdown && ( +
+
+ + + +
+
+ )} +
+
+
+ )} + + {/* 검색 결과 목록 */} +
+ {sortedResults.length > 0 ? ( + sortedResults.map((medicine) => ( +
+
+
+ {medicine.image ? ( + {medicine.name} + ) : ( + + )} +
+
+
+
+

명칭: {medicine.name}

+

구분: {medicine.category}

+

제약사: {medicine.company}

+

성분: {medicine.effect}

+

+ 효능: {medicine.symptoms.join(', ')} +

+
+
+
+
+
+ )) + ) : ( +
+

검색 결과가 없습니다.

+

다른 검색어로 다시 시도해 보세요.

+
+ )} +
+ + {/* 페이지네이션 - 결과가 있을 때만 표시 */} + {sortedResults.length > 0 && ( +
+ {/* 실제 구현 시에는 totalResults와 itemsPerPage를 기반으로 페이지 수 계산 필요 */} + {[1, 2, 3, 4, 5].map((page) => ( + + ))} + +
+ )} +
+
+
+
+ ); +}; + +// 메인 페이지 컴포넌트 +const SymptomSearchResults = () => { + return ( + Loading...}> + + + ); +}; + +export default SymptomSearchResults; \ No newline at end of file diff --git a/src/components/Footer.js b/src/components/Footer.js new file mode 100644 index 0000000..e75de6a --- /dev/null +++ b/src/components/Footer.js @@ -0,0 +1,22 @@ +'use client'; + +const Footer = () => { + return ( +
+
+
+

YAK+

+

+ 더 나은 약품 정보를 위한 약품 검색 플랫폼, YAK+ + {/* 나에게 딱 맞는 약품 검색, YAK+ */} +

+

+ © {new Date().getFullYear()} YAK+. All rights reserved. +

+
+
+
+ ); +}; + +export default Footer; \ No newline at end of file diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000..0138b3c --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,70 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; + +const Header = () => { + return ( +
+
+ {/* 로고 */} + + {/* Yak+ Logo */} + YAK+ + + + {/* 건강 문구 */} + {/*
+
+

+ {'당신의 건강한 하루를 응원합니다'.split('').map((char, i) => ( + + {char === ' ' ? '\u00A0' : char} + + ))} +

+
+ +
*/} +
+ + +
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/components/NoImage.js b/src/components/NoImage.js new file mode 100644 index 0000000..743d6b6 --- /dev/null +++ b/src/components/NoImage.js @@ -0,0 +1,24 @@ +const NoImage = () => { + return ( +
+
+ + + +

No Image

+
+
+ ); +}; + +export default NoImage; \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js new file mode 100644 index 0000000..eaae61c --- /dev/null +++ b/src/components/SearchBar.js @@ -0,0 +1,379 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +const SearchIcon = ({ color = '#2BA89C', onClick }) => ( + +); + +const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' }) => { + const router = useRouter(); + const [searchMode, setSearchMode] = useState(initialMode); + const [searchType, setSearchType] = useState('symptom'); + const [searchQuery, setSearchQuery] = useState(initialQuery); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [suggestions, setSuggestions] = useState([]); + const [selectedSuggestion, setSelectedSuggestion] = useState(-1); + const [isFocused, setIsFocused] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [autoCompleteResults, setAutoCompleteResults] = useState([]); + + // 임시 데이터 - 실제로는 API에서 받아올 예정 + const mockSuggestions = { + symptom: [ + { text: '두통', category: '증상' }, + { text: '두근거림', category: '증상' }, + { text: '두드러기', category: '증상' }, + ], + company: [ + { text: '동아제약', category: '제조사' }, + { text: '동화약품', category: '제조사' }, + ], + medicine: [ + { text: '타이레놀', category: '약품명' }, + { text: '타이레놀이브', category: '약품명' }, + ], + }; + + const searchTypes = { + symptom: '증상', + company: '제조사', + medicine: '약품명' + }; + + const getPlaceholder = () => { + if (searchMode === 'natural') { + return '예) 머리가 아프고 열이 나요 (20자 이내)'; + } + + switch (searchType) { + case 'symptom': + return '증상을 입력하세요'; + case 'company': + return '제조사를 입력하세요'; + case 'medicine': + return '약품명을 입력하세요'; + default: + return '검색어를 입력하세요'; + } + }; + + // 자동완성 목록 필터링 + useEffect(() => { + if (!isFocused) { + setSuggestions([]); + return; + } + + if (searchMode === 'keyword' && searchQuery.trim()) { + const filtered = mockSuggestions[searchType] + .filter(item => + item.text.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .slice(0, 6); // 최대 6개까지만 표시 + setSuggestions(filtered); + } else { + setSuggestions([]); + } + }, [searchQuery, searchType, searchMode, isFocused]); + + // 포커스 아웃 시 자동완성 닫기 + const handleBlur = () => { + // 약간의 지연을 주어 클릭 이벤트가 처리될 수 있도록 함 + setTimeout(() => { + setIsFocused(false); + }, 200); + }; + + // 최근 검색어 저장 함수 + const saveRecentSearch = (query, type, mode) => { + try { + const savedSearches = sessionStorage.getItem('recentSearches'); + const searches = savedSearches ? JSON.parse(savedSearches) : []; + + const newSearch = { + query: query.trim(), + type: type, + mode: mode + }; + + const filteredSearches = searches.filter(item => item.query !== newSearch.query); + const updatedSearches = [newSearch, ...filteredSearches].slice(0, 5); + + sessionStorage.setItem('recentSearches', JSON.stringify(updatedSearches)); + } catch (error) { + console.error('Failed to save recent search:', error); + } + }; + + const handleSearch = (e) => { + e.preventDefault(); + if (!searchQuery.trim()) return; + + if (searchMode === 'keyword') { + switch (searchType) { + case 'symptom': + saveRecentSearch(searchQuery, searchType, searchMode); + router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}`); + break; + case 'company': + // 추후 구현 + break; + case 'medicine': + // 추후 구현 + break; + } + } else { + // 자연어 검색 처리 + saveRecentSearch(searchQuery, 'natural', searchMode); + router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=natural`); + } + }; + + const handleInputChange = (e) => { + const value = e.target.value; + if (searchMode === 'natural' && value.length > 20) { + return; + } + setSearchQuery(value); + setSelectedSuggestion(-1); + }; + + const handleKeyDown = (e) => { + if (searchMode === 'natural' && e.key === 'Enter') { + e.preventDefault(); + handleSearch(e); + return; + } + + if (!suggestions.length) return; + + switch (e.key) { + case 'ArrowDown':ㄹ + e.preventDefault(); + setSelectedSuggestion(prev => + prev < suggestions.length - 1 ? prev + 1 : prev + ); + break; + case 'ArrowUp': + e.preventDefault(); + setSelectedSuggestion(prev => prev > 0 ? prev - 1 : -1); + break; + case 'Enter': + e.preventDefault(); + if (selectedSuggestion >= 0) { + setSearchQuery(suggestions[selectedSuggestion].text); + setSuggestions([]); + } else { + handleSearch(e); + } + break; + case 'Escape': + setSuggestions([]); + setSelectedSuggestion(-1); + break; + } + }; + + // 검색어의 일치하는 부분을 하이라이트하는 함수 + const highlightMatch = (text) => { + if (!searchQuery) return text; + const parts = text.split(new RegExp(`(${searchQuery})`, 'gi')); + return parts.map((part, i) => + part.toLowerCase() === searchQuery.toLowerCase() ? + {part} : part + ); + }; + + // 선택된 모드에 따른 색상 테마 + const getThemeColor = () => { + return searchMode === 'keyword' ? '#2BA89C' : '#2978F2'; + }; + + const handleSuggestionClick = (suggestion) => { + setSearchQuery(suggestion.text); + setSuggestions([]); + if (searchType === 'symptom') { + saveRecentSearch(suggestion.text, searchType, 'keyword'); + router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}`); + } + }; + + return ( +
+ {/* 검색 모드 선택 탭 - showTabs가 true일 때만 표시 */} + {showTabs && ( +
+ + +
+ )} + + {/* 검색 폼 */} +
+
+ + setIsFocused(true)} + onBlur={handleBlur} + maxLength={searchMode === 'natural' ? 20 : undefined} + className={`w-full pl-16 pr-4 py-4 border rounded-lg focus:outline-none transition-all hover:shadow-md ${ + searchMode === 'keyword' + ? 'border-[#2BA89C] focus:ring-2 focus:ring-[#2BA89C]/20' + : 'border-[#2978F2] focus:ring-2 focus:ring-[#2978F2]/20' + }`} + /> + {searchMode === 'natural' && ( +
+ {searchQuery.length}/20 +
+ )} + + {/* 자동완성 드롭다운 */} + {isFocused && searchMode === 'keyword' && suggestions.length > 0 && ( +
+ {suggestions.map((suggestion, index) => ( + + ))} +
+ )} +
+ + {/* 검색 유형 선택 드롭다운 (키워드 모드일 때만 표시) */} + {searchMode === 'keyword' && ( +
+ + + {/* 드롭다운 메뉴 */} + {isDropdownOpen && ( +
+ {Object.entries(searchTypes).map(([type, label]) => ( + + ))} +
+ )} +
+ )} +
+ + {/* 자동완성 결과 */} + {searchQuery && !isLoading && autoCompleteResults.length > 0 && ( +
+ {/* ... existing autocomplete results code ... */} +
+ )} +
+ ); +}; + +export default SearchBar; \ No newline at end of file From 9401659258d9e00ed289c13b156b6afc075adaaa Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:27:06 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=F0=9F=90=9B=20Bug:=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=EB=A9=94=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?Push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.js | 237 +++++++++++++++++++++++++++++------------------- 1 file changed, 143 insertions(+), 94 deletions(-) diff --git a/src/app/page.js b/src/app/page.js index d625a20..51a129c 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,103 +1,152 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; import Image from "next/image"; +import SearchBar from '../components/SearchBar'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; -export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.js - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+const MAX_RECENT_SEARCHES = 5; // 최대 저장할 최근 검색어 수 + +const displayTypes = { + symptom: '증상', + company: '제조사', + medicine: '약품명', + natural: '자연어' +}; + +const Home = () => { + const [recentSearches, setRecentSearches] = useState([]); + const [searchBarProps, setSearchBarProps] = useState({ + initialQuery: '', + initialMode: 'keyword', + initialType: 'symptom' + }); + const searchBarRef = useRef(null); + + // 최근 검색어 클릭 핸들러 + const handleRecentSearchClick = (searchItem) => { + setSearchBarProps({ + initialQuery: searchItem.query, + initialMode: searchItem.mode, + initialType: searchItem.type + }); + // setTimeout을 사용하여 다음 렌더링 사이클에서 포커스 + setTimeout(() => { + searchBarRef.current?.focus(); + }, 0); + }; -
- - Vercel logomark - Deploy now - - - Read our docs - + // 최근 검색어 로드 + useEffect(() => { + const savedSearches = sessionStorage.getItem('recentSearches'); + if (savedSearches) { + setRecentSearches(JSON.parse(savedSearches)); + } + }, []); + + // 최근 검색어 삭제 + const removeSearch = (searchToRemove) => { + const newSearches = recentSearches.filter(search => search.query !== searchToRemove.query); + setRecentSearches(newSearches); + sessionStorage.setItem('recentSearches', JSON.stringify(newSearches)); + }; + + // 모든 최근 검색어 삭제 + const clearAllSearches = () => { + setRecentSearches([]); + sessionStorage.removeItem('recentSearches'); + }; + + const RecentSearches = () => { + if (recentSearches.length === 0) return null; + + return ( +
+
+ {recentSearches.map((searchItem, index) => ( +
+ + +
+ ))}
-
-
+ ); + }; + + return ( +
+
+
+
Window icon - Examples - - - Globe icon + +
+ - Go to nextjs.org → - - + + {/* 최근 검색어 */} + {recentSearches.length > 0 && ( +
+
+

최근 검색어

+ +
+ +
+ )} +
+
+
); -} +}; + +export default Home; From 643ebb41f5b87b1596b67af742dcff84b8f839f8 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:00:53 +0900 Subject: [PATCH 06/22] =?UTF-8?q?=E2=9C=A8=20Feature:=20=EC=A6=9D=EC=83=81?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=B0=B1=EC=97=94=EB=93=9C=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: 증상 자동완성 기능 연동 * 💄 Style: Head Metadata 변경 * ✨ Feat: 증상기반 검색기능 연동 * 📦️ Chore: api 엔드포인트 변경 * 📦️ Chore: nginx 구성에 따른 호스트 주소 삭제 --- src/app/layout.js | 7 +- src/app/search/symptom/page.js | 412 +++++++++++++-------------------- src/components/SearchBar.js | 214 ++++++++++++++--- 3 files changed, 352 insertions(+), 281 deletions(-) diff --git a/src/app/layout.js b/src/app/layout.js index 7bf337d..da6c022 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -12,8 +12,11 @@ const geistMono = Geist_Mono({ }); export const metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "약품 검색 플랫폼 Yak+", + description: "약품 검색 플랫폼 Yak+", + icons: { + icon: '/logo.svg', // SVG 경로 + }, }; export default function RootLayout({ children }) { diff --git a/src/app/search/symptom/page.js b/src/app/search/symptom/page.js index 7f3b348..047c657 100644 --- a/src/app/search/symptom/page.js +++ b/src/app/search/symptom/page.js @@ -11,112 +11,146 @@ import SearchBar from '../../../components/SearchBar'; const SearchResults = () => { const searchParams = useSearchParams(); const query = searchParams.get('q'); - const mode = searchParams.get('mode'); - const [selectedType, setSelectedType] = useState('all'); - const [sortBy, setSortBy] = useState('name'); - const [showTypeDropdown, setShowTypeDropdown] = useState(false); - const [showSortDropdown, setShowSortDropdown] = useState(false); - + // 페이지네이션 관련 상태 const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); // 전체 검색 결과 수 const itemsPerPage = 10; // 페이지당 항목 수 - // 임시 검색 결과 데이터 - const mockResults = [ - { - id: 1, - name: "타이레놀", - category: "일반 의약품", - company: "(주)한국얀센 / Janssen Korea", - effect: "아세트아미노펜 과립", - symptoms: ["감기로 인한 발열 및 동통(통증), 두통, 신경통, 근육통, 월경통, 염좌통(삔 통증)"], - image: null - }, - { - id: 2, - name: "부루펜", - category: "일반 의약품", - company: "삼일제약(주) / Samil", - effect: "이부프로펜", - symptoms: ["류마티양 관절염, 연소성 류마티양 관절염, 골관절염(퇴행성 관절질환), 감기로 인한 발열 및 동통, 요통, 월경곤란증, 수술후 동통"], - image: null - }, - { - id: 3, - name: "트라펜정", - category: "전문 의약품", - company: "명문제약(주) / Myungmoon Pharm", - effect: "트라마돌염산염,아세트아미노펜", - symptoms: ["중등도-중증의 급ㆍ만성 통증"], - image: null - }, - ]; - - // 검색어에 따른 결과 필터링 (임시로 "두통" 검색어만 결과 표시) - const searchResults = query?.toLowerCase() === "두통" ? mockResults : []; - - // 선택된 타입에 따라 결과 필터링 - const filteredResults = selectedType === 'all' - ? searchResults - : searchResults.filter(medicine => { - if (selectedType === 'general') return medicine.category === "일반 의약품"; - if (selectedType === 'prescription') return medicine.category === "전문 의약품"; - return true; - }); + // 서버에서 받아올 실제 검색 결과 + const [fetchedResults, setFetchedResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); - // 정렬 기준에 따라 결과 정렬 - const sortedResults = [...filteredResults].sort((a, b) => { - switch (sortBy) { - case 'name': - return a.name.localeCompare(b.name, 'ko'); - case 'effect': - return a.effect.localeCompare(b.effect, 'ko'); - case 'company': - return a.company.localeCompare(b.company, 'ko'); - default: - return 0; - } - }); - - // 검색 결과 수 업데이트 + // query 또는 currentPage가 바뀔 때마다 서버 요청 useEffect(() => { - setTotalResults(sortedResults.length); - }, [sortedResults.length]); - - // 드롭다운 외부 클릭 시 닫기 - const handleClickOutside = () => { - setShowTypeDropdown(false); - setShowSortDropdown(false); - }; - - const getTypeLabel = () => { - switch (selectedType) { - case 'all': return '전체'; - case 'general': return '일반 의약품'; - case 'prescription': return '전문 의약품'; - default: return '전체'; + if (!query) { + setFetchedResults([]); + return; } - }; + setIsLoading(true); + setError(null); + const apiPage = currentPage - 1; // API에 0부터 시작하는 페이지 인덱스 전달 + fetch( + `/api/api/drugs/search/symptom` + + `?q=${encodeURIComponent(query)}` + + `&page=${apiPage}&size=${itemsPerPage}` + ) + .then(res => { + if (!res.ok) throw new Error('서버 에러'); + return res.json(); + }) + .then(data => { + const list = data.data.searchResponseList; + setFetchedResults(list); + setTotalResults(list.length); + }) + .catch(err => setError(err.message)) + .finally(() => setIsLoading(false)); + }, [query, currentPage]); + + // const [selectedType, setSelectedType] = useState('all'); + // const [sortBy, setSortBy] = useState('name'); + // const [showTypeDropdown, setShowTypeDropdown] = useState(false); + // const [showSortDropdown, setShowSortDropdown] = useState(false); + + - const getSortLabel = () => { - switch (sortBy) { - case 'name': return '제품명'; - case 'effect': return '성분명'; - case 'company': return '제약회사'; - default: return '제품명'; - } - }; + // // 임시 검색 결과 데이터 + // const mockResults = [ + // { + // id: 1, + // name: "타이레놀", + // category: "일반 의약품", + // company: "(주)한국얀센 / Janssen Korea", + // effect: "아세트아미노펜 과립", + // symptoms: ["감기로 인한 발열 및 동통(통증), 두통, 신경통, 근육통, 월경통, 염좌통(삔 통증)"], + // image: null + // }, + // { + // id: 2, + // name: "부루펜", + // category: "일반 의약품", + // company: "삼일제약(주) / Samil", + // effect: "이부프로펜", + // symptoms: ["류마티양 관절염, 연소성 류마티양 관절염, 골관절염(퇴행성 관절질환), 감기로 인한 발열 및 동통, 요통, 월경곤란증, 수술후 동통"], + // image: null + // }, + // { + // id: 3, + // name: "트라펜정", + // category: "전문 의약품", + // company: "명문제약(주) / Myungmoon Pharm", + // effect: "트라마돌염산염,아세트아미노펜", + // symptoms: ["중등도-중증의 급ㆍ만성 통증"], + // image: null + // }, + // ]; + + // // 검색어에 따른 결과 필터링 (임시로 "두통" 검색어만 결과 표시) + // const searchResults = query?.toLowerCase() === "두통" ? mockResults : []; + + // // 선택된 타입에 따라 결과 필터링 + // const filteredResults = selectedType === 'all' + // ? fetchedResults + // : fetchedResults.filter(medicine => { + // if (selectedType === 'general') return medicine.category === "일반 의약품"; + // if (selectedType === 'prescription') return medicine.category === "전문 의약품"; + // return true; + // }); + + // // 정렬 기준에 따라 결과 정렬 + // const sortedResults = [...filteredResults].sort((a, b) => { + // switch (sortBy) { + // case 'name': + // return a.name.localeCompare(b.name, 'ko'); + // case 'effect': + // return a.effect.localeCompare(b.effect, 'ko'); + // case 'company': + // return a.company.localeCompare(b.company, 'ko'); + // default: + // return 0; + // } + // }); - // 현재 필터 상태 텍스트 생성 - const getFilterStatusText = () => { - const typeText = selectedType === 'all' ? '' : `${getTypeLabel()} · `; - const sortText = `${getSortLabel()} 순`; - return `${typeText}${sortText}`; - }; + // 검색 결과 수 업데이트 + // useEffect(() => { + // setTotalResults(sortedResults.length); + // }, [sortedResults.length]); + + // // 드롭다운 외부 클릭 시 닫기 + // const handleClickOutside = () => { + // setShowTypeDropdown(false); + // setShowSortDropdown(false); + // }; + + // const getTypeLabel = () => { + // switch (selectedType) { + // case 'all': return '전체'; + // case 'general': return '일반 의약품'; + // case 'prescription': return '전문 의약품'; + // default: return '전체'; + // } + // }; + + // const getSortLabel = () => { + // switch (sortBy) { + // case 'name': return '제품명'; + // case 'effect': return '성분명'; + // case 'company': return '제약회사'; + // default: return '제품명'; + // } + // }; + + // // 현재 필터 상태 텍스트 생성 + // const getFilterStatusText = () => { + // const typeText = selectedType === 'all' ? '' : `${getTypeLabel()} · `; + // const sortText = `${getSortLabel()} 순`; + // return `${typeText}${sortText}`; + // }; return ( -
+
@@ -125,159 +159,46 @@ const SearchResults = () => {
- {/* 필터 영역 - 결과가 있을 때만 표시 */} - {sortedResults.length > 0 && ( -
- {/* 검색 결과 정보 */} -
- - 총 {totalResults}개의 검색결과 - - | - {getFilterStatusText()} -
- - {/* 필터 버튼들 */} -
- {/* 의약품 구분 드롭다운 */} -
- - {showTypeDropdown && ( -
-
- - - -
-
- )} -
- - {/* 정렬 기준 드롭다운 */} -
- - {showSortDropdown && ( -
-
- - - -
-
- )} -
-
-
- )} - {/* 검색 결과 목록 */} -
- {sortedResults.length > 0 ? ( - sortedResults.map((medicine) => ( +
+ {isLoading ? ( +
로딩 중...
+ ) : error ? ( +
에러: {error}
+ ) : fetchedResults.length > 0 ? ( + fetchedResults.map((medicine) => (
-
-
- {medicine.image ? ( +
+ {/*
*/} + {/*
*/} +
+ {medicine.imageUrl ? ( {medicine.name} ) : ( )}
-
-
-
-

명칭: {medicine.name}

-

구분: {medicine.category}

-

제약사: {medicine.company}

-

성분: {medicine.effect}

-

- 효능: {medicine.symptoms.join(', ')} -

-
-
+ + {/* 정보영역 */} +
+ {/*
*/} +

명칭: {medicine.drugName}

+

제약회사: {medicine.company}

+

+ 효능: {medicine.efficacy.join(', ')} +

@@ -289,27 +210,26 @@ const SearchResults = () => {
)}
+ + + - {/* 페이지네이션 - 결과가 있을 때만 표시 */} - {sortedResults.length > 0 && ( + {/* 페이지네이션 */} + {fetchedResults.length > 0 && (
- {/* 실제 구현 시에는 totalResults와 itemsPerPage를 기반으로 페이지 수 계산 필요 */} - {[1, 2, 3, 4, 5].map((page) => ( + {[...Array(Math.ceil(totalResults / itemsPerPage)).keys()].map(i => ( ))} -
)}
diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index eaae61c..91dd448 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; const SearchIcon = ({ color = '#2BA89C', onClick }) => ( @@ -47,24 +47,22 @@ const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' const [selectedSuggestion, setSelectedSuggestion] = useState(-1); const [isFocused, setIsFocused] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [autoCompleteResults, setAutoCompleteResults] = useState([]); - - // 임시 데이터 - 실제로는 API에서 받아올 예정 - const mockSuggestions = { - symptom: [ - { text: '두통', category: '증상' }, - { text: '두근거림', category: '증상' }, - { text: '두드러기', category: '증상' }, - ], - company: [ - { text: '동아제약', category: '제조사' }, - { text: '동화약품', category: '제조사' }, - ], - medicine: [ - { text: '타이레놀', category: '약품명' }, - { text: '타이레놀이브', category: '약품명' }, - ], - }; + const [autoCompleteCache, setAutoCompleteCache] = useState({}); // 자동완성 결과 캐시 + const MAX_CACHE_SIZE = 100; + +// 캐시 업데이트 헬퍼 함수 +// (MAX_CACHE_SIZE를 초과하면 가장 오래된 것 삭제) +const updateCache = (prevCache, key, value) => { + const newCache = { ...prevCache }; + newCache[key] = value; + + if (Object.keys(newCache).length > MAX_CACHE_SIZE) { + const oldestKey = Object.keys(newCache)[0]; // 가장 먼저 들어간 키를 삭제 + delete newCache[oldestKey]; + } + + return newCache; +}; const searchTypes = { symptom: '증상', @@ -89,24 +87,174 @@ const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' } }; - // 자동완성 목록 필터링 + + // 자동완성 데이터를 가져오는 함수 + const fetchSuggestions = async (query, type) => { + try { + setIsLoading(true); + + const url = `/api/api/drugs/autocomplete/${type}?q=${encodeURIComponent(query)}`; + + console.log('자동완성 요청 URL:', url); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + console.log('자동완성 응답:', response); + + if (!response.ok) { + throw new Error('자동완성 데이터를 가져오는데 실패했습니다.'); + } + + const data = await response.json(); + console.log('자동완성 데이터:', data); + + // 캐시 업데이트 - 기존 결과를 새로운 결과로 덮어씌움 + const cacheKey = `${type}:${query}`; + const results = data.data.autoCompleteList || []; + // setAutoCompleteCache(prev => { + // const newCache = { ...prev }; + // newCache[cacheKey] = results; // 기존 키가 있으면 덮어씌움 + // return newCache; + // }); + // 캐시 무한정 커지지 않게 개수 체크하면서 추가 (MAX_CACHE_SIZE를 초과하면 가장 오래된 것 삭제) + setAutoCompleteCache(prev => updateCache(prev, cacheKey, results)); + + return results; + } catch (error) { + console.error('자동완성 에러:', error); + return []; + } finally { + setIsLoading(false); + } + }; + + // debounce된 자동완성 함수 + // const debouncedFetchSuggestions = useCallback( + // debounce(async (query, type) => { + // if (query === prevQuery) { + // console.log('같은 검색어, 요청 스킵:', query); + // return; + // } + // const results = await fetchSuggestions(query, type); + // setSuggestions(results.map(text => ({ text, category: searchTypes[type] }))); + // }, 1000), + // [fetchSuggestions] + // ); + + // 포커스 상태가 변경될 때 실행 + // useEffect(() => { + // if (!isFocused) { + // setSuggestions([]); + // return; + // } + + // // 포커스가 들어왔을 때 현재 검색어에 대한 캐시 확인 + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + // if (autoCompleteCache[cacheKey]) { + // console.log('포커스 시 캐시된 결과 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } + // // 포커스 시에는 API 요청을 보내지 않음 + // } + // }, [isFocused]); + + // // 검색어나 타입이 변경될 때 실행 + // useEffect(() => { + // if (!isFocused) return; + + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + // if (autoCompleteCache[cacheKey]) { + // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } else { + // debouncedFetchSuggestions(trimmedQuery, searchType); + // } + // } else { + // setSuggestions([]); + // } + // }, [searchQuery, searchType, searchMode, autoCompleteCache]); + + // useEffect(() => { + // if (!isFocused) return; + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + // if (autoCompleteCache[cacheKey]) { + // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } else if (trimmedQuery !== prevQuery) { + // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); + // debouncedFetchSuggestions(trimmedQuery, searchType); + // setPrevQuery(trimmedQuery); + // } + // } else { + // setSuggestions([]); + // } + // }, [searchQuery, searchType, searchMode, autoCompleteCache, isFocused, prevQuery]); + + // 검색어, 타입, 모드, 포커스 변화에 따라 자동완성 처리 + // useEffect(() => { + // if (!isFocused) return; + + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + + // if (autoCompleteCache[cacheKey]) { + // console.log('검색어 변경 시 캐시 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } else { + // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); + // debouncedFetchSuggestions(trimmedQuery, searchType); + // } + // } else { + // setSuggestions([]); + // } + // }, [searchQuery, searchType, searchMode, autoCompleteCache, debouncedFetchSuggestions]); + useEffect(() => { if (!isFocused) { setSuggestions([]); return; } - - if (searchMode === 'keyword' && searchQuery.trim()) { - const filtered = mockSuggestions[searchType] - .filter(item => - item.text.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .slice(0, 6); // 최대 6개까지만 표시 - setSuggestions(filtered); - } else { + + const trimmedQuery = searchQuery.trim(); + const cacheKey = `${searchType}:${trimmedQuery}`; + + if (searchMode !== 'keyword' || trimmedQuery.length === 0) { setSuggestions([]); + return; + } + + if (autoCompleteCache[cacheKey]) { + console.log('캐시 사용:', cacheKey); + setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + return; } - }, [searchQuery, searchType, searchMode, isFocused]); + + // 300ms 후에 서버 요청 예약 + const handler = setTimeout(() => { + console.log('서버 요청:', trimmedQuery); + fetchSuggestions(trimmedQuery, searchType) + .then(results => { + setSuggestions(results.map(text => ({ text, category: searchTypes[searchType] }))); + }); + }, 300); + + // 다음 입력 시 기존 요청 취소 + return () => clearTimeout(handler); + + }, [searchQuery, searchType, searchMode, isFocused, autoCompleteCache]); + // 포커스 아웃 시 자동완성 닫기 const handleBlur = () => { @@ -157,7 +305,7 @@ const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' } else { // 자연어 검색 처리 saveRecentSearch(searchQuery, 'natural', searchMode); - router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=natural`); + // router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=natural`); } }; @@ -180,7 +328,7 @@ const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' if (!suggestions.length) return; switch (e.key) { - case 'ArrowDown':ㄹ + case 'ArrowDown': e.preventDefault(); setSelectedSuggestion(prev => prev < suggestions.length - 1 ? prev + 1 : prev @@ -367,7 +515,7 @@ const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' {/* 자동완성 결과 */} - {searchQuery && !isLoading && autoCompleteResults.length > 0 && ( + {searchQuery && !isLoading && autoCompleteCache[`${searchType}:${searchQuery}`] && autoCompleteCache[`${searchType}:${searchQuery}`].length > 0 && (
{/* ... existing autocomplete results code ... */}
From 7ac9dfd00820def2c35ec55733d1027ab1ea9671 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:51:16 +0900 Subject: [PATCH 07/22] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=9E=90=EC=97=B0?= =?UTF-8?q?=EC=96=B4=20=EA=B2=80=EC=83=89=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: 증상 자연어 검색 백엔드 연동 * ✨ Feat: 자연어 검색 백엔드 연동 --- src/app/search/page.js | 259 ++++++++++++++++++++++++++++++++++++ src/components/SearchBar.js | 2 +- 2 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/app/search/page.js diff --git a/src/app/search/page.js b/src/app/search/page.js new file mode 100644 index 0000000..0082f7e --- /dev/null +++ b/src/app/search/page.js @@ -0,0 +1,259 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { useState, useEffect, Suspense } from 'react'; +import Header from '../../components/Header'; +import Footer from '../../components/Footer'; +import NoImage from '../../components/NoImage'; +import SearchBar from '../../components/SearchBar'; + +// SearchParams를 사용하는 컴포넌트를 분리 +const SearchResults = () => { + const searchParams = useSearchParams(); + const query = searchParams.get('q'); + + // 페이지네이션 관련 상태 + const [currentPage, setCurrentPage] = useState(1); + const [totalResults, setTotalResults] = useState(0); // 전체 검색 결과 수 + const itemsPerPage = 10; // 페이지당 항목 수 + + // 서버에서 받아올 실제 검색 결과 + const [fetchedResults, setFetchedResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // query 또는 currentPage가 바뀔 때마다 서버 요청 + useEffect(() => { + if (!query) { + setFetchedResults([]); + return; + } + setIsLoading(true); + setError(null); + const apiPage = currentPage - 1; // API에 0부터 시작하는 페이지 인덱스 전달 + const url = `/api/api/drugs/search`; + const body = { + query: query, + page: apiPage, + size: itemsPerPage, + }; + + fetch( + url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(body), + }) + .then(res => { + if (!res.ok) throw new Error('서버 에러'); + return res.json(); + }) + .then(data => { + const list = data.data; + setFetchedResults(list); + setTotalResults(list.length); + }) + .catch(err => setError(err.message)) + .finally(() => setIsLoading(false)); + }, [query, currentPage]); + + // const [selectedType, setSelectedType] = useState('all'); + // const [sortBy, setSortBy] = useState('name'); + // const [showTypeDropdown, setShowTypeDropdown] = useState(false); + // const [showSortDropdown, setShowSortDropdown] = useState(false); + + + + // // 임시 검색 결과 데이터 + // const mockResults = [ + // { + // id: 1, + // name: "타이레놀", + // category: "일반 의약품", + // company: "(주)한국얀센 / Janssen Korea", + // effect: "아세트아미노펜 과립", + // symptoms: ["감기로 인한 발열 및 동통(통증), 두통, 신경통, 근육통, 월경통, 염좌통(삔 통증)"], + // image: null + // }, + // { + // id: 2, + // name: "부루펜", + // category: "일반 의약품", + // company: "삼일제약(주) / Samil", + // effect: "이부프로펜", + // symptoms: ["류마티양 관절염, 연소성 류마티양 관절염, 골관절염(퇴행성 관절질환), 감기로 인한 발열 및 동통, 요통, 월경곤란증, 수술후 동통"], + // image: null + // }, + // { + // id: 3, + // name: "트라펜정", + // category: "전문 의약품", + // company: "명문제약(주) / Myungmoon Pharm", + // effect: "트라마돌염산염,아세트아미노펜", + // symptoms: ["중등도-중증의 급ㆍ만성 통증"], + // image: null + // }, + // ]; + + // // 검색어에 따른 결과 필터링 (임시로 "두통" 검색어만 결과 표시) + // const searchResults = query?.toLowerCase() === "두통" ? mockResults : []; + + // // 선택된 타입에 따라 결과 필터링 + // const filteredResults = selectedType === 'all' + // ? fetchedResults + // : fetchedResults.filter(medicine => { + // if (selectedType === 'general') return medicine.category === "일반 의약품"; + // if (selectedType === 'prescription') return medicine.category === "전문 의약품"; + // return true; + // }); + + // // 정렬 기준에 따라 결과 정렬 + // const sortedResults = [...filteredResults].sort((a, b) => { + // switch (sortBy) { + // case 'name': + // return a.name.localeCompare(b.name, 'ko'); + // case 'effect': + // return a.effect.localeCompare(b.effect, 'ko'); + // case 'company': + // return a.company.localeCompare(b.company, 'ko'); + // default: + // return 0; + // } + // }); + + // 검색 결과 수 업데이트 + // useEffect(() => { + // setTotalResults(sortedResults.length); + // }, [sortedResults.length]); + + // // 드롭다운 외부 클릭 시 닫기 + // const handleClickOutside = () => { + // setShowTypeDropdown(false); + // setShowSortDropdown(false); + // }; + + // const getTypeLabel = () => { + // switch (selectedType) { + // case 'all': return '전체'; + // case 'general': return '일반 의약품'; + // case 'prescription': return '전문 의약품'; + // default: return '전체'; + // } + // }; + + // const getSortLabel = () => { + // switch (sortBy) { + // case 'name': return '제품명'; + // case 'effect': return '성분명'; + // case 'company': return '제약회사'; + // default: return '제품명'; + // } + // }; + + // // 현재 필터 상태 텍스트 생성 + // const getFilterStatusText = () => { + // const typeText = selectedType === 'all' ? '' : `${getTypeLabel()} · `; + // const sortText = `${getSortLabel()} 순`; + // return `${typeText}${sortText}`; + // }; + + return ( +
+
+
+
+ {/* 검색창 영역 */} +
+ +
+ + {/* 검색 결과 목록 */} +
+ {isLoading ? ( +
로딩 중...
+ ) : error ? ( +
에러: {error}
+ ) : fetchedResults.length > 0 ? ( + fetchedResults.map((medicine) => ( +
+
+ {/*
*/} + {/*
*/} +
+ {medicine.imageUrl ? ( + {medicine.drugName} + ) : ( + + )} +
+ + {/* 정보영역 */} +
+ {/*
*/} +

명칭: {medicine.drugName}

+

제약회사: {medicine.company}

+

+ 효능: {medicine.efficacy.join(', ')} +

+
+
+
+ )) + ) : ( +
+

검색 결과가 없습니다.

+

다른 검색어로 다시 시도해 보세요.

+
+ )} +
+ + + + + {/* 페이지네이션 */} + {fetchedResults.length > 0 && ( +
+ {[...Array(Math.ceil(totalResults / itemsPerPage)).keys()].map(i => ( + + ))} +
+ )} +
+
+
+
+ ); +}; + +// 메인 페이지 컴포넌트 +const SymptomSearchResults = () => { + return ( + Loading...
}> + + + ); +}; + +export default SymptomSearchResults; \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 91dd448..a465154 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -305,7 +305,7 @@ const updateCache = (prevCache, key, value) => { } else { // 자연어 검색 처리 saveRecentSearch(searchQuery, 'natural', searchMode); - // router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=natural`); + router.push(`/search?q=${encodeURIComponent(searchQuery)}`); } }; From c4610df45d4172e2a8071d10b8ddbe890997a750 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:29:21 +0900 Subject: [PATCH 08/22] =?UTF-8?q?=F0=9F=92=84=20Style:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EA=B2=B0=EA=B3=BC=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 💄 Style: 검색결과 페이지 컨테이너 디자인 개선 * 🐛 Bug: 검색 시 검색타입 유지되지 않던 문제 수정 --- src/app/search/page.js | 257 +-------------------------------- src/app/search/symptom/page.js | 249 +------------------------------- src/components/SearchBar.js | 10 +- src/components/SearchPage.js | 165 +++++++++++++++++++++ 4 files changed, 181 insertions(+), 500 deletions(-) create mode 100644 src/components/SearchPage.js diff --git a/src/app/search/page.js b/src/app/search/page.js index 0082f7e..20aee7d 100644 --- a/src/app/search/page.js +++ b/src/app/search/page.js @@ -1,259 +1,12 @@ 'use client'; -import { useSearchParams } from 'next/navigation'; -import { useState, useEffect, Suspense } from 'react'; -import Header from '../../components/Header'; -import Footer from '../../components/Footer'; -import NoImage from '../../components/NoImage'; -import SearchBar from '../../components/SearchBar'; +import { Suspense } from 'react'; +import SearchPage from '../../components/SearchPage'; -// SearchParams를 사용하는 컴포넌트를 분리 -const SearchResults = () => { - const searchParams = useSearchParams(); - const query = searchParams.get('q'); - - // 페이지네이션 관련 상태 - const [currentPage, setCurrentPage] = useState(1); - const [totalResults, setTotalResults] = useState(0); // 전체 검색 결과 수 - const itemsPerPage = 10; // 페이지당 항목 수 - - // 서버에서 받아올 실제 검색 결과 - const [fetchedResults, setFetchedResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - // query 또는 currentPage가 바뀔 때마다 서버 요청 - useEffect(() => { - if (!query) { - setFetchedResults([]); - return; - } - setIsLoading(true); - setError(null); - const apiPage = currentPage - 1; // API에 0부터 시작하는 페이지 인덱스 전달 - const url = `/api/api/drugs/search`; - const body = { - query: query, - page: apiPage, - size: itemsPerPage, - }; - - fetch( - url, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(body), - }) - .then(res => { - if (!res.ok) throw new Error('서버 에러'); - return res.json(); - }) - .then(data => { - const list = data.data; - setFetchedResults(list); - setTotalResults(list.length); - }) - .catch(err => setError(err.message)) - .finally(() => setIsLoading(false)); - }, [query, currentPage]); - - // const [selectedType, setSelectedType] = useState('all'); - // const [sortBy, setSortBy] = useState('name'); - // const [showTypeDropdown, setShowTypeDropdown] = useState(false); - // const [showSortDropdown, setShowSortDropdown] = useState(false); - - - - // // 임시 검색 결과 데이터 - // const mockResults = [ - // { - // id: 1, - // name: "타이레놀", - // category: "일반 의약품", - // company: "(주)한국얀센 / Janssen Korea", - // effect: "아세트아미노펜 과립", - // symptoms: ["감기로 인한 발열 및 동통(통증), 두통, 신경통, 근육통, 월경통, 염좌통(삔 통증)"], - // image: null - // }, - // { - // id: 2, - // name: "부루펜", - // category: "일반 의약품", - // company: "삼일제약(주) / Samil", - // effect: "이부프로펜", - // symptoms: ["류마티양 관절염, 연소성 류마티양 관절염, 골관절염(퇴행성 관절질환), 감기로 인한 발열 및 동통, 요통, 월경곤란증, 수술후 동통"], - // image: null - // }, - // { - // id: 3, - // name: "트라펜정", - // category: "전문 의약품", - // company: "명문제약(주) / Myungmoon Pharm", - // effect: "트라마돌염산염,아세트아미노펜", - // symptoms: ["중등도-중증의 급ㆍ만성 통증"], - // image: null - // }, - // ]; - - // // 검색어에 따른 결과 필터링 (임시로 "두통" 검색어만 결과 표시) - // const searchResults = query?.toLowerCase() === "두통" ? mockResults : []; - - // // 선택된 타입에 따라 결과 필터링 - // const filteredResults = selectedType === 'all' - // ? fetchedResults - // : fetchedResults.filter(medicine => { - // if (selectedType === 'general') return medicine.category === "일반 의약품"; - // if (selectedType === 'prescription') return medicine.category === "전문 의약품"; - // return true; - // }); - - // // 정렬 기준에 따라 결과 정렬 - // const sortedResults = [...filteredResults].sort((a, b) => { - // switch (sortBy) { - // case 'name': - // return a.name.localeCompare(b.name, 'ko'); - // case 'effect': - // return a.effect.localeCompare(b.effect, 'ko'); - // case 'company': - // return a.company.localeCompare(b.company, 'ko'); - // default: - // return 0; - // } - // }); - - // 검색 결과 수 업데이트 - // useEffect(() => { - // setTotalResults(sortedResults.length); - // }, [sortedResults.length]); - - // // 드롭다운 외부 클릭 시 닫기 - // const handleClickOutside = () => { - // setShowTypeDropdown(false); - // setShowSortDropdown(false); - // }; - - // const getTypeLabel = () => { - // switch (selectedType) { - // case 'all': return '전체'; - // case 'general': return '일반 의약품'; - // case 'prescription': return '전문 의약품'; - // default: return '전체'; - // } - // }; - - // const getSortLabel = () => { - // switch (sortBy) { - // case 'name': return '제품명'; - // case 'effect': return '성분명'; - // case 'company': return '제약회사'; - // default: return '제품명'; - // } - // }; - - // // 현재 필터 상태 텍스트 생성 - // const getFilterStatusText = () => { - // const typeText = selectedType === 'all' ? '' : `${getTypeLabel()} · `; - // const sortText = `${getSortLabel()} 순`; - // return `${typeText}${sortText}`; - // }; - - return ( -
-
-
-
- {/* 검색창 영역 */} -
- -
- - {/* 검색 결과 목록 */} -
- {isLoading ? ( -
로딩 중...
- ) : error ? ( -
에러: {error}
- ) : fetchedResults.length > 0 ? ( - fetchedResults.map((medicine) => ( -
-
- {/*
*/} - {/*
*/} -
- {medicine.imageUrl ? ( - {medicine.drugName} - ) : ( - - )} -
- - {/* 정보영역 */} -
- {/*
*/} -

명칭: {medicine.drugName}

-

제약회사: {medicine.company}

-

- 효능: {medicine.efficacy.join(', ')} -

-
-
-
- )) - ) : ( -
-

검색 결과가 없습니다.

-

다른 검색어로 다시 시도해 보세요.

-
- )} -
- - - - - {/* 페이지네이션 */} - {fetchedResults.length > 0 && ( -
- {[...Array(Math.ceil(totalResults / itemsPerPage)).keys()].map(i => ( - - ))} -
- )} -
-
-
-
- ); -}; - -// 메인 페이지 컴포넌트 -const SymptomSearchResults = () => { +export default function Page() { return ( Loading...
}> - + ); -}; - -export default SymptomSearchResults; \ No newline at end of file +} \ No newline at end of file diff --git a/src/app/search/symptom/page.js b/src/app/search/symptom/page.js index 047c657..67f085f 100644 --- a/src/app/search/symptom/page.js +++ b/src/app/search/symptom/page.js @@ -1,251 +1,12 @@ 'use client'; -import { useSearchParams } from 'next/navigation'; -import { useState, useEffect, Suspense } from 'react'; -import Header from '../../../components/Header'; -import Footer from '../../../components/Footer'; -import NoImage from '../../../components/NoImage'; -import SearchBar from '../../../components/SearchBar'; +import { Suspense } from 'react'; +import SearchPage from '../../../components/SearchPage'; -// SearchParams를 사용하는 컴포넌트를 분리 -const SearchResults = () => { - const searchParams = useSearchParams(); - const query = searchParams.get('q'); - - // 페이지네이션 관련 상태 - const [currentPage, setCurrentPage] = useState(1); - const [totalResults, setTotalResults] = useState(0); // 전체 검색 결과 수 - const itemsPerPage = 10; // 페이지당 항목 수 - - // 서버에서 받아올 실제 검색 결과 - const [fetchedResults, setFetchedResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - // query 또는 currentPage가 바뀔 때마다 서버 요청 - useEffect(() => { - if (!query) { - setFetchedResults([]); - return; - } - setIsLoading(true); - setError(null); - const apiPage = currentPage - 1; // API에 0부터 시작하는 페이지 인덱스 전달 - fetch( - `/api/api/drugs/search/symptom` + - `?q=${encodeURIComponent(query)}` + - `&page=${apiPage}&size=${itemsPerPage}` - ) - .then(res => { - if (!res.ok) throw new Error('서버 에러'); - return res.json(); - }) - .then(data => { - const list = data.data.searchResponseList; - setFetchedResults(list); - setTotalResults(list.length); - }) - .catch(err => setError(err.message)) - .finally(() => setIsLoading(false)); - }, [query, currentPage]); - - // const [selectedType, setSelectedType] = useState('all'); - // const [sortBy, setSortBy] = useState('name'); - // const [showTypeDropdown, setShowTypeDropdown] = useState(false); - // const [showSortDropdown, setShowSortDropdown] = useState(false); - - - - // // 임시 검색 결과 데이터 - // const mockResults = [ - // { - // id: 1, - // name: "타이레놀", - // category: "일반 의약품", - // company: "(주)한국얀센 / Janssen Korea", - // effect: "아세트아미노펜 과립", - // symptoms: ["감기로 인한 발열 및 동통(통증), 두통, 신경통, 근육통, 월경통, 염좌통(삔 통증)"], - // image: null - // }, - // { - // id: 2, - // name: "부루펜", - // category: "일반 의약품", - // company: "삼일제약(주) / Samil", - // effect: "이부프로펜", - // symptoms: ["류마티양 관절염, 연소성 류마티양 관절염, 골관절염(퇴행성 관절질환), 감기로 인한 발열 및 동통, 요통, 월경곤란증, 수술후 동통"], - // image: null - // }, - // { - // id: 3, - // name: "트라펜정", - // category: "전문 의약품", - // company: "명문제약(주) / Myungmoon Pharm", - // effect: "트라마돌염산염,아세트아미노펜", - // symptoms: ["중등도-중증의 급ㆍ만성 통증"], - // image: null - // }, - // ]; - - // // 검색어에 따른 결과 필터링 (임시로 "두통" 검색어만 결과 표시) - // const searchResults = query?.toLowerCase() === "두통" ? mockResults : []; - - // // 선택된 타입에 따라 결과 필터링 - // const filteredResults = selectedType === 'all' - // ? fetchedResults - // : fetchedResults.filter(medicine => { - // if (selectedType === 'general') return medicine.category === "일반 의약품"; - // if (selectedType === 'prescription') return medicine.category === "전문 의약품"; - // return true; - // }); - - // // 정렬 기준에 따라 결과 정렬 - // const sortedResults = [...filteredResults].sort((a, b) => { - // switch (sortBy) { - // case 'name': - // return a.name.localeCompare(b.name, 'ko'); - // case 'effect': - // return a.effect.localeCompare(b.effect, 'ko'); - // case 'company': - // return a.company.localeCompare(b.company, 'ko'); - // default: - // return 0; - // } - // }); - - // 검색 결과 수 업데이트 - // useEffect(() => { - // setTotalResults(sortedResults.length); - // }, [sortedResults.length]); - - // // 드롭다운 외부 클릭 시 닫기 - // const handleClickOutside = () => { - // setShowTypeDropdown(false); - // setShowSortDropdown(false); - // }; - - // const getTypeLabel = () => { - // switch (selectedType) { - // case 'all': return '전체'; - // case 'general': return '일반 의약품'; - // case 'prescription': return '전문 의약품'; - // default: return '전체'; - // } - // }; - - // const getSortLabel = () => { - // switch (sortBy) { - // case 'name': return '제품명'; - // case 'effect': return '성분명'; - // case 'company': return '제약회사'; - // default: return '제품명'; - // } - // }; - - // // 현재 필터 상태 텍스트 생성 - // const getFilterStatusText = () => { - // const typeText = selectedType === 'all' ? '' : `${getTypeLabel()} · `; - // const sortText = `${getSortLabel()} 순`; - // return `${typeText}${sortText}`; - // }; - - return ( -
-
-
-
- {/* 검색창 영역 */} -
- -
- - {/* 검색 결과 목록 */} -
- {isLoading ? ( -
로딩 중...
- ) : error ? ( -
에러: {error}
- ) : fetchedResults.length > 0 ? ( - fetchedResults.map((medicine) => ( -
-
- {/*
*/} - {/*
*/} -
- {medicine.imageUrl ? ( - {medicine.drugName} - ) : ( - - )} -
- - {/* 정보영역 */} -
- {/*
*/} -

명칭: {medicine.drugName}

-

제약회사: {medicine.company}

-

- 효능: {medicine.efficacy.join(', ')} -

-
-
-
- )) - ) : ( -
-

검색 결과가 없습니다.

-

다른 검색어로 다시 시도해 보세요.

-
- )} -
- - - - - {/* 페이지네이션 */} - {fetchedResults.length > 0 && ( -
- {[...Array(Math.ceil(totalResults / itemsPerPage)).keys()].map(i => ( - - ))} -
- )} -
-
-
-
- ); -}; - -// 메인 페이지 컴포넌트 -const SymptomSearchResults = () => { +export default function Page() { return ( Loading...
}> - + ); -}; - -export default SymptomSearchResults; \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index a465154..243f8d1 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -293,7 +293,7 @@ const updateCache = (prevCache, key, value) => { switch (searchType) { case 'symptom': saveRecentSearch(searchQuery, searchType, searchMode); - router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}`); + router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); break; case 'company': // 추후 구현 @@ -305,7 +305,7 @@ const updateCache = (prevCache, key, value) => { } else { // 자연어 검색 처리 saveRecentSearch(searchQuery, 'natural', searchMode); - router.push(`/search?q=${encodeURIComponent(searchQuery)}`); + router.push(`/search?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); } }; @@ -372,9 +372,11 @@ const updateCache = (prevCache, key, value) => { const handleSuggestionClick = (suggestion) => { setSearchQuery(suggestion.text); setSuggestions([]); + saveRecentSearch(suggestion.text, searchType, 'keyword'); if (searchType === 'symptom') { - saveRecentSearch(suggestion.text, searchType, 'keyword'); - router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}`); + router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); + } else { + router.push(`/search?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); } }; diff --git a/src/components/SearchPage.js b/src/components/SearchPage.js new file mode 100644 index 0000000..a71e9f5 --- /dev/null +++ b/src/components/SearchPage.js @@ -0,0 +1,165 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import Header from './Header'; +import Footer from './Footer'; +import NoImage from './NoImage'; +import SearchBar from './SearchBar'; + +const SearchPage = ({ searchType }) => { + const searchParams = useSearchParams(); + const query = searchParams.get('q'); + const mode = searchParams.get('mode') || 'keyword'; + + const [currentPage, setCurrentPage] = useState(1); + const [totalResults, setTotalResults] = useState(0); + const itemsPerPage = 10; + + const [fetchedResults, setFetchedResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!query) { + setFetchedResults([]); + setTotalResults(0); + return; + } + setIsLoading(true); + setError(null); + const apiPage = currentPage - 1; + + let url; + let options = { method: 'GET' }; + + if (searchType === 'keyword') { + url = '/api/api/drugs/search'; + options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) + }; + } else if (searchType === 'symptom') { + url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + } + + fetch(url, options) + .then(res => { + if (!res.ok) throw new Error('서버 에러'); + return res.json(); + }) + .then(data => { + let list = searchType === 'keyword' ? data.data : data.data.searchResponseList; + setFetchedResults(list); + setTotalResults(list.length); + }) + .catch(err => setError(err.message)) + .finally(() => setIsLoading(false)); + }, [query, currentPage, searchType]); + + return ( +
+
+
+
+
+ +
+
+ {isLoading ? ( +
로딩 중...
+ ) : error ? ( +
에러: {error}
+ ) : fetchedResults.length > 0 ? ( + fetchedResults.map(medicine => ( +
+
+ {/* 이미지 영역 */} +
+ {medicine.imageUrl ? ( + {medicine.drugName} + ) : ( + + )} +
+ + {/* 정보 영역 */} +
+ {/* 명칭 */} +
+ + 명 칭 + + + {medicine.drugName} + +
+ {/* 제약회사 */} +
+ + 제약회사 + + + {medicine.company} + +
+ {/* 효능 */} +
+ + 효 능 + + + {medicine.efficacy.join(', ')} + +
+
+
+
+ + + + )) + ) : ( +
+

검색 결과가 없습니다.

+

다른 검색어로 다시 시도해 보세요.

+
+ )} +
+ {fetchedResults.length > 0 && ( +
+ {[...Array(Math.ceil(totalResults / itemsPerPage)).keys()].map(i => ( + + ))} +
+ )} +
+
+
+
+ ); +}; + +export default SearchPage; \ No newline at end of file From ec12ad0a75c4021453de977aa6ff4df35603a2bd Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Thu, 1 May 2025 09:55:40 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=BD=ED=92=88=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=A0=95=EB=B3=B4=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=97=B0=EB=8F=99=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/drugs/[id]/page.js | 261 +++++++++++++++++++++++++++++++++++ src/app/search/name/page.js | 12 ++ src/components/NoImage.js | 47 ++++++- src/components/SearchBar.js | 134 +++--------------- src/components/SearchPage.js | 102 ++++++++------ 5 files changed, 397 insertions(+), 159 deletions(-) create mode 100644 src/app/drugs/[id]/page.js create mode 100644 src/app/search/name/page.js diff --git a/src/app/drugs/[id]/page.js b/src/app/drugs/[id]/page.js new file mode 100644 index 0000000..72079f7 --- /dev/null +++ b/src/app/drugs/[id]/page.js @@ -0,0 +1,261 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import Header from '@/components/Header'; +import Footer from '@/components/Footer'; +import NoImage from '@/components/NoImage'; + +export default function DrugDetailPage() { + const params = useParams(); + const drugId = params.id; + + const [drug, setDrug] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // API 개발이 완료되면 아래 주석을 해제하고 mock 데이터 대신 실제 API를 사용하면 됩니다. + + const fetchDrugDetail = async () => { + try { + setLoading(true); + const response = await fetch(`/api/api/drugs/search/detail/${drugId}`); + if (!response.ok) { + throw new Error('약품 정보를 불러오는 데 실패했습니다.'); + } + + const data = await response.json(); + setDrug(data.data); + } catch (err) { + console.error('약품 상세 정보 조회 오류:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchDrugDetail(); + + + // Mock 데이터 사용 +// setTimeout(() => { +// setDrug({ +// ...mockDrugData, +// item_seq: drugId, // URL의 ID 반영 +// }); +// setLoading(false); +// }, 500); // 로딩 효과를 위한 지연 시간 + }, [drugId]); + + if (loading) { + return ( +
+
+
+
로딩 중...
+
+
+
+ ); + } + + if (error || !drug) { + return ( +
+
+
+
+ {error || '약품 정보를 찾을 수 없습니다.'} +
+
+
+
+ ); + } + + // 약품 전문/일반 구분 + const getEtcOtcName = (isGeneral) => { + return isGeneral ? "일반의약품" : "전문의약품"; + }; + + // 주의사항 키 값 추출 + const precautionKeys = drug.precaution ? Object.keys(drug.precaution) : []; + + return ( +
+
+
+
+
+ {/* 상단 영역: 약품 기본 정보 */} +
+ {/* 약품 이미지 */} +
+ {drug.imageUrl ? ( + {drug.drugName} + ) : ( + + )} +
+ + {/* 약품 기본 정보 */} +
+

{drug.drugName}

+
+
+
+ 제약회사 + {drug.company} +
+
+ 품목기준코드 + {drug.drugId} +
+
+ 보관방법 + {drug.storeMethod} +
+
+ 의약품 구분 + {getEtcOtcName(drug.isGeneral)} / {drug.isHerbal ? '한약' : '양약'} +
+
+
+ +
+ 허가일 + {drug.permitDate} +
+
+ 유효기간 + + {drug.validTerm ? drug.validTerm : '정보 없음'} + +
+
+ 취소일자 + + {drug.cancelDate ? drug.cancelDate : '해당 없음'} + +
+
+ 취소사유 + + {drug.cancelName ? drug.cancelName : '해당 없음'} + +
+
+
+
+
+ + {/* 성분 정보 */} + {drug.materialInfo && drug.materialInfo.length > 0 && ( +
+

+ 성분 정보 +

+
+ + + + + + + + + + + + {drug.materialInfo.map((material, index) => ( + + + + + + + + ))} + +
성분명분량단위총량규격
{material.성분명}{material.분량}{material.단위}{material.총량}{material.규격}
+
+
+ )} + + {/* 상세 정보 섹션 */} +
+ {/* 효능효과 */} +
+

+ 효능효과 +

+
+ {drug.efficacy?.length > 0 ? ( +
    + {drug.efficacy.map((item, i) => ( +
  • {item}
  • + ))} +
+ ) : ( +

정보가 없습니다.

+ )} +
+
+ + {/* 용법용량 */} +
+

+ 용법용량 +

+
+ {drug.usage?.length > 0 ? ( +
    + {drug.usage.map((item, i) => ( +
  • {item}
  • + ))} +
+ ) : ( +

정보가 없습니다.

+ )} +
+
+ + {/* 주의사항 및 기타 정보 */} + {precautionKeys.length > 0 && ( +
+

+ 주의사항 +

+ {precautionKeys.map((key) => ( +
+

+ {key} +

+
+ {drug.precaution[key]?.length > 0 ? ( +
    + {drug.precaution[key].map((item, i) => ( +
  • {item}
  • + ))} +
+ ) : ( +

정보가 없습니다.

+ )} +
+
+ ))} +
+ )} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/search/name/page.js b/src/app/search/name/page.js new file mode 100644 index 0000000..76dbed9 --- /dev/null +++ b/src/app/search/name/page.js @@ -0,0 +1,12 @@ +'use client'; + +import { Suspense } from 'react'; +import SearchPage from '../../../components/SearchPage'; + +export default function Page() { + return ( + Loading...
}> + + + ); +} \ No newline at end of file diff --git a/src/components/NoImage.js b/src/components/NoImage.js index 743d6b6..fd24929 100644 --- a/src/components/NoImage.js +++ b/src/components/NoImage.js @@ -1,21 +1,56 @@ -const NoImage = () => { +const NoImage = ({ className = "" }) => { return ( -
+
+ {/* 약병 몸체 */} + + + {/* 약병 뚜껑 */} + + + {/* 약병 라벨 */} + {/* */} + + {/* X 표시 - 우측 하단으로 이동 및 크기 확대 */} -

No Image

+

약품 이미지 없음

); diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 243f8d1..87b488e 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; const SearchIcon = ({ color = '#2BA89C', onClick }) => ( ); -const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' }) => { +const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword', initialSearchType = 'symptom'}) => { const router = useRouter(); + const searchParams = useSearchParams(); const [searchMode, setSearchMode] = useState(initialMode); - const [searchType, setSearchType] = useState('symptom'); + const urlType = searchParams.get('type'); + const [searchType, setSearchType] = useState(urlType || initialSearchType); const [searchQuery, setSearchQuery] = useState(initialQuery); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); const [selectedSuggestion, setSelectedSuggestion] = useState(-1); const [isFocused, setIsFocused] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [autoCompleteCache, setAutoCompleteCache] = useState({}); // 자동완성 결과 캐시 const MAX_CACHE_SIZE = 100; @@ -67,7 +70,7 @@ const updateCache = (prevCache, key, value) => { const searchTypes = { symptom: '증상', company: '제조사', - medicine: '약품명' + name: '약품명' }; const getPlaceholder = () => { @@ -80,7 +83,7 @@ const updateCache = (prevCache, key, value) => { return '증상을 입력하세요'; case 'company': return '제조사를 입력하세요'; - case 'medicine': + case 'name': return '약품명을 입력하세요'; default: return '검색어를 입력하세요'; @@ -116,11 +119,7 @@ const updateCache = (prevCache, key, value) => { // 캐시 업데이트 - 기존 결과를 새로운 결과로 덮어씌움 const cacheKey = `${type}:${query}`; const results = data.data.autoCompleteList || []; - // setAutoCompleteCache(prev => { - // const newCache = { ...prev }; - // newCache[cacheKey] = results; // 기존 키가 있으면 덮어씌움 - // return newCache; - // }); + // 캐시 무한정 커지지 않게 개수 체크하면서 추가 (MAX_CACHE_SIZE를 초과하면 가장 오래된 것 삭제) setAutoCompleteCache(prev => updateCache(prev, cacheKey, results)); @@ -133,94 +132,6 @@ const updateCache = (prevCache, key, value) => { } }; - // debounce된 자동완성 함수 - // const debouncedFetchSuggestions = useCallback( - // debounce(async (query, type) => { - // if (query === prevQuery) { - // console.log('같은 검색어, 요청 스킵:', query); - // return; - // } - // const results = await fetchSuggestions(query, type); - // setSuggestions(results.map(text => ({ text, category: searchTypes[type] }))); - // }, 1000), - // [fetchSuggestions] - // ); - - // 포커스 상태가 변경될 때 실행 - // useEffect(() => { - // if (!isFocused) { - // setSuggestions([]); - // return; - // } - - // // 포커스가 들어왔을 때 현재 검색어에 대한 캐시 확인 - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - // if (autoCompleteCache[cacheKey]) { - // console.log('포커스 시 캐시된 결과 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } - // // 포커스 시에는 API 요청을 보내지 않음 - // } - // }, [isFocused]); - - // // 검색어나 타입이 변경될 때 실행 - // useEffect(() => { - // if (!isFocused) return; - - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - // if (autoCompleteCache[cacheKey]) { - // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } else { - // debouncedFetchSuggestions(trimmedQuery, searchType); - // } - // } else { - // setSuggestions([]); - // } - // }, [searchQuery, searchType, searchMode, autoCompleteCache]); - - // useEffect(() => { - // if (!isFocused) return; - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - // if (autoCompleteCache[cacheKey]) { - // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } else if (trimmedQuery !== prevQuery) { - // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); - // debouncedFetchSuggestions(trimmedQuery, searchType); - // setPrevQuery(trimmedQuery); - // } - // } else { - // setSuggestions([]); - // } - // }, [searchQuery, searchType, searchMode, autoCompleteCache, isFocused, prevQuery]); - - // 검색어, 타입, 모드, 포커스 변화에 따라 자동완성 처리 - // useEffect(() => { - // if (!isFocused) return; - - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - - // if (autoCompleteCache[cacheKey]) { - // console.log('검색어 변경 시 캐시 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } else { - // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); - // debouncedFetchSuggestions(trimmedQuery, searchType); - // } - // } else { - // setSuggestions([]); - // } - // }, [searchQuery, searchType, searchMode, autoCompleteCache, debouncedFetchSuggestions]); - useEffect(() => { if (!isFocused) { setSuggestions([]); @@ -298,8 +209,9 @@ const updateCache = (prevCache, key, value) => { case 'company': // 추후 구현 break; - case 'medicine': - // 추후 구현 + case 'name': + saveRecentSearch(searchQuery, searchType, searchMode); + router.push(`/search/name?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); break; } } else { @@ -373,10 +285,15 @@ const updateCache = (prevCache, key, value) => { setSearchQuery(suggestion.text); setSuggestions([]); saveRecentSearch(suggestion.text, searchType, 'keyword'); - if (searchType === 'symptom') { - router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); - } else { - router.push(`/search?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); + switch (searchType) { + case 'symptom': + router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); + break; + case 'name': + router.push(`/search/name?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); + break; + default: + router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); } }; @@ -503,7 +420,7 @@ const updateCache = (prevCache, key, value) => { }} className={`w-full text-left px-4 py-3 hover:bg-[#2BA89C]/5 transition-colors ${ searchType === type ? 'text-[#2BA89C] font-medium bg-[#2BA89C]/10' : 'text-gray-700' - } ${type === 'medicine' ? 'rounded-b-lg' : ''} ${ + } ${type === 'name' ? 'rounded-b-lg' : ''} ${ type === 'symptom' ? 'rounded-t-lg' : '' }`} > @@ -516,12 +433,7 @@ const updateCache = (prevCache, key, value) => { )} - {/* 자동완성 결과 */} - {searchQuery && !isLoading && autoCompleteCache[`${searchType}:${searchQuery}`] && autoCompleteCache[`${searchType}:${searchQuery}`].length > 0 && ( -
- {/* ... existing autocomplete results code ... */} -
- )} +
); }; diff --git a/src/components/SearchPage.js b/src/components/SearchPage.js index a71e9f5..56e8c80 100644 --- a/src/components/SearchPage.js +++ b/src/components/SearchPage.js @@ -1,6 +1,6 @@ 'use client'; -import { useSearchParams } from 'next/navigation'; +import { useSearchParams, useRouter } from 'next/navigation'; import { useState, useEffect } from 'react'; import Header from './Header'; import Footer from './Footer'; @@ -9,8 +9,9 @@ import SearchBar from './SearchBar'; const SearchPage = ({ searchType }) => { const searchParams = useSearchParams(); + const router = useRouter(); const query = searchParams.get('q'); - const mode = searchParams.get('mode') || 'keyword'; + const mode = searchParams.get('mode'); const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); @@ -20,6 +21,11 @@ const SearchPage = ({ searchType }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + // 약품 상세 페이지로 이동하는 함수 + const navigateToDrugDetail = (drugId) => { + router.push(`/drugs/${drugId}`); + }; + useEffect(() => { if (!query) { setFetchedResults([]); @@ -32,31 +38,45 @@ const SearchPage = ({ searchType }) => { let url; let options = { method: 'GET' }; - - if (searchType === 'keyword') { + if(mode === 'natural') { //자연어 검색 url = '/api/api/drugs/search'; options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) }; - } else if (searchType === 'symptom') { - url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + } else { // 키워드 검색 모드 분기 + if(searchType === 'symptom') { // 증상 검색 + url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + } else if(searchType === 'name') { // 약품명 검색 + url = `/api/api/drugs/search/name?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + } } - fetch(url, options) + // if (searchType === 'natural') { + // url = '/api/api/drugs/search'; + // options = { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) + // }; + // } else if (searchType === 'symptom') { + // url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + // } + + fetch(url, options) // 검색 타입에 따라 데이터 받아오는 방식 분기됨. !!! response 형식 정해지면 수정 필요 .then(res => { if (!res.ok) throw new Error('서버 에러'); return res.json(); }) .then(data => { - let list = searchType === 'keyword' ? data.data : data.data.searchResponseList; + let list = mode === 'keyword' ? data.data.searchResponseList : data.data; setFetchedResults(list); setTotalResults(list.length); }) .catch(err => setError(err.message)) .finally(() => setIsLoading(false)); - }, [query, currentPage, searchType]); + }, [query, currentPage, mode]); return (
@@ -78,9 +98,10 @@ const SearchPage = ({ searchType }) => { ) : fetchedResults.length > 0 ? ( fetchedResults.map(medicine => (
+ key={medicine.drugId} + className="bg-white rounded-lg shadow-sm p-4 border border-transparent hover:shadow-md hover:border-[#2BA89C] transition cursor-pointer" + onClick={() => navigateToDrugDetail(medicine.drugId)} + >
{/* 이미지 영역 */}
@@ -97,39 +118,36 @@ const SearchPage = ({ searchType }) => { {/* 정보 영역 */}
- {/* 명칭 */} -
- - 명 칭 - - - {medicine.drugName} - -
- {/* 제약회사 */} -
- - 제약회사 - - - {medicine.company} - -
- {/* 효능 */} -
- - 효 능 - - - {medicine.efficacy.join(', ')} - + {/* 명칭 */} +
+ + 명 칭 + + + {medicine.drugName} + +
+ {/* 제약회사 */} +
+ + 제약회사 + + + {medicine.company} + +
+ {/* 효능 */} +
+ + 효 능 + + + {medicine.efficacy.join(', ')} + +
-
- - - )) ) : (
From 3d06f65eccfda4a3d4ad1df63a44296beff7e4eb Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Thu, 1 May 2025 10:03:28 +0900 Subject: [PATCH 10/22] =?UTF-8?q?Revert=20"=E2=9C=A8=20Feat:=20=EC=95=BD?= =?UTF-8?q?=ED=92=88=20=EC=83=81=EC=84=B8=EC=A0=95=EB=B3=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=97=B0=EB=8F=99=20(#10)"=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ec12ad0a75c4021453de977aa6ff4df35603a2bd. --- src/app/drugs/[id]/page.js | 261 ----------------------------------- src/app/search/name/page.js | 12 -- src/components/NoImage.js | 47 +------ src/components/SearchBar.js | 134 +++++++++++++++--- src/components/SearchPage.js | 102 ++++++-------- 5 files changed, 159 insertions(+), 397 deletions(-) delete mode 100644 src/app/drugs/[id]/page.js delete mode 100644 src/app/search/name/page.js diff --git a/src/app/drugs/[id]/page.js b/src/app/drugs/[id]/page.js deleted file mode 100644 index 72079f7..0000000 --- a/src/app/drugs/[id]/page.js +++ /dev/null @@ -1,261 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; -import Header from '@/components/Header'; -import Footer from '@/components/Footer'; -import NoImage from '@/components/NoImage'; - -export default function DrugDetailPage() { - const params = useParams(); - const drugId = params.id; - - const [drug, setDrug] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - // API 개발이 완료되면 아래 주석을 해제하고 mock 데이터 대신 실제 API를 사용하면 됩니다. - - const fetchDrugDetail = async () => { - try { - setLoading(true); - const response = await fetch(`/api/api/drugs/search/detail/${drugId}`); - if (!response.ok) { - throw new Error('약품 정보를 불러오는 데 실패했습니다.'); - } - - const data = await response.json(); - setDrug(data.data); - } catch (err) { - console.error('약품 상세 정보 조회 오류:', err); - setError(err.message); - } finally { - setLoading(false); - } - }; - - fetchDrugDetail(); - - - // Mock 데이터 사용 -// setTimeout(() => { -// setDrug({ -// ...mockDrugData, -// item_seq: drugId, // URL의 ID 반영 -// }); -// setLoading(false); -// }, 500); // 로딩 효과를 위한 지연 시간 - }, [drugId]); - - if (loading) { - return ( -
-
-
-
로딩 중...
-
-
-
- ); - } - - if (error || !drug) { - return ( -
-
-
-
- {error || '약품 정보를 찾을 수 없습니다.'} -
-
-
-
- ); - } - - // 약품 전문/일반 구분 - const getEtcOtcName = (isGeneral) => { - return isGeneral ? "일반의약품" : "전문의약품"; - }; - - // 주의사항 키 값 추출 - const precautionKeys = drug.precaution ? Object.keys(drug.precaution) : []; - - return ( -
-
-
-
-
- {/* 상단 영역: 약품 기본 정보 */} -
- {/* 약품 이미지 */} -
- {drug.imageUrl ? ( - {drug.drugName} - ) : ( - - )} -
- - {/* 약품 기본 정보 */} -
-

{drug.drugName}

-
-
-
- 제약회사 - {drug.company} -
-
- 품목기준코드 - {drug.drugId} -
-
- 보관방법 - {drug.storeMethod} -
-
- 의약품 구분 - {getEtcOtcName(drug.isGeneral)} / {drug.isHerbal ? '한약' : '양약'} -
-
-
- -
- 허가일 - {drug.permitDate} -
-
- 유효기간 - - {drug.validTerm ? drug.validTerm : '정보 없음'} - -
-
- 취소일자 - - {drug.cancelDate ? drug.cancelDate : '해당 없음'} - -
-
- 취소사유 - - {drug.cancelName ? drug.cancelName : '해당 없음'} - -
-
-
-
-
- - {/* 성분 정보 */} - {drug.materialInfo && drug.materialInfo.length > 0 && ( -
-

- 성분 정보 -

-
- - - - - - - - - - - - {drug.materialInfo.map((material, index) => ( - - - - - - - - ))} - -
성분명분량단위총량규격
{material.성분명}{material.분량}{material.단위}{material.총량}{material.규격}
-
-
- )} - - {/* 상세 정보 섹션 */} -
- {/* 효능효과 */} -
-

- 효능효과 -

-
- {drug.efficacy?.length > 0 ? ( -
    - {drug.efficacy.map((item, i) => ( -
  • {item}
  • - ))} -
- ) : ( -

정보가 없습니다.

- )} -
-
- - {/* 용법용량 */} -
-

- 용법용량 -

-
- {drug.usage?.length > 0 ? ( -
    - {drug.usage.map((item, i) => ( -
  • {item}
  • - ))} -
- ) : ( -

정보가 없습니다.

- )} -
-
- - {/* 주의사항 및 기타 정보 */} - {precautionKeys.length > 0 && ( -
-

- 주의사항 -

- {precautionKeys.map((key) => ( -
-

- {key} -

-
- {drug.precaution[key]?.length > 0 ? ( -
    - {drug.precaution[key].map((item, i) => ( -
  • {item}
  • - ))} -
- ) : ( -

정보가 없습니다.

- )} -
-
- ))} -
- )} -
-
-
-
-
-
- ); -} \ No newline at end of file diff --git a/src/app/search/name/page.js b/src/app/search/name/page.js deleted file mode 100644 index 76dbed9..0000000 --- a/src/app/search/name/page.js +++ /dev/null @@ -1,12 +0,0 @@ -'use client'; - -import { Suspense } from 'react'; -import SearchPage from '../../../components/SearchPage'; - -export default function Page() { - return ( - Loading...
}> - - - ); -} \ No newline at end of file diff --git a/src/components/NoImage.js b/src/components/NoImage.js index fd24929..743d6b6 100644 --- a/src/components/NoImage.js +++ b/src/components/NoImage.js @@ -1,56 +1,21 @@ -const NoImage = ({ className = "" }) => { +const NoImage = () => { return ( -
+
- {/* 약병 몸체 */} - - - {/* 약병 뚜껑 */} - - - {/* 약병 라벨 */} - {/* */} - - {/* X 표시 - 우측 하단으로 이동 및 크기 확대 */} -

약품 이미지 없음

+

No Image

); diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 87b488e..243f8d1 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; const SearchIcon = ({ color = '#2BA89C', onClick }) => ( ); -const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword', initialSearchType = 'symptom'}) => { +const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' }) => { const router = useRouter(); - const searchParams = useSearchParams(); const [searchMode, setSearchMode] = useState(initialMode); - const urlType = searchParams.get('type'); - const [searchType, setSearchType] = useState(urlType || initialSearchType); + const [searchType, setSearchType] = useState('symptom'); const [searchQuery, setSearchQuery] = useState(initialQuery); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); const [selectedSuggestion, setSelectedSuggestion] = useState(-1); const [isFocused, setIsFocused] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [autoCompleteCache, setAutoCompleteCache] = useState({}); // 자동완성 결과 캐시 const MAX_CACHE_SIZE = 100; @@ -70,7 +67,7 @@ const updateCache = (prevCache, key, value) => { const searchTypes = { symptom: '증상', company: '제조사', - name: '약품명' + medicine: '약품명' }; const getPlaceholder = () => { @@ -83,7 +80,7 @@ const updateCache = (prevCache, key, value) => { return '증상을 입력하세요'; case 'company': return '제조사를 입력하세요'; - case 'name': + case 'medicine': return '약품명을 입력하세요'; default: return '검색어를 입력하세요'; @@ -119,7 +116,11 @@ const updateCache = (prevCache, key, value) => { // 캐시 업데이트 - 기존 결과를 새로운 결과로 덮어씌움 const cacheKey = `${type}:${query}`; const results = data.data.autoCompleteList || []; - + // setAutoCompleteCache(prev => { + // const newCache = { ...prev }; + // newCache[cacheKey] = results; // 기존 키가 있으면 덮어씌움 + // return newCache; + // }); // 캐시 무한정 커지지 않게 개수 체크하면서 추가 (MAX_CACHE_SIZE를 초과하면 가장 오래된 것 삭제) setAutoCompleteCache(prev => updateCache(prev, cacheKey, results)); @@ -132,6 +133,94 @@ const updateCache = (prevCache, key, value) => { } }; + // debounce된 자동완성 함수 + // const debouncedFetchSuggestions = useCallback( + // debounce(async (query, type) => { + // if (query === prevQuery) { + // console.log('같은 검색어, 요청 스킵:', query); + // return; + // } + // const results = await fetchSuggestions(query, type); + // setSuggestions(results.map(text => ({ text, category: searchTypes[type] }))); + // }, 1000), + // [fetchSuggestions] + // ); + + // 포커스 상태가 변경될 때 실행 + // useEffect(() => { + // if (!isFocused) { + // setSuggestions([]); + // return; + // } + + // // 포커스가 들어왔을 때 현재 검색어에 대한 캐시 확인 + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + // if (autoCompleteCache[cacheKey]) { + // console.log('포커스 시 캐시된 결과 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } + // // 포커스 시에는 API 요청을 보내지 않음 + // } + // }, [isFocused]); + + // // 검색어나 타입이 변경될 때 실행 + // useEffect(() => { + // if (!isFocused) return; + + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + // if (autoCompleteCache[cacheKey]) { + // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } else { + // debouncedFetchSuggestions(trimmedQuery, searchType); + // } + // } else { + // setSuggestions([]); + // } + // }, [searchQuery, searchType, searchMode, autoCompleteCache]); + + // useEffect(() => { + // if (!isFocused) return; + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + // if (autoCompleteCache[cacheKey]) { + // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } else if (trimmedQuery !== prevQuery) { + // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); + // debouncedFetchSuggestions(trimmedQuery, searchType); + // setPrevQuery(trimmedQuery); + // } + // } else { + // setSuggestions([]); + // } + // }, [searchQuery, searchType, searchMode, autoCompleteCache, isFocused, prevQuery]); + + // 검색어, 타입, 모드, 포커스 변화에 따라 자동완성 처리 + // useEffect(() => { + // if (!isFocused) return; + + // const trimmedQuery = searchQuery.trim(); + // if (searchMode === 'keyword' && trimmedQuery.length > 0) { + // const cacheKey = `${searchType}:${trimmedQuery}`; + + // if (autoCompleteCache[cacheKey]) { + // console.log('검색어 변경 시 캐시 사용:', cacheKey); + // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); + // } else { + // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); + // debouncedFetchSuggestions(trimmedQuery, searchType); + // } + // } else { + // setSuggestions([]); + // } + // }, [searchQuery, searchType, searchMode, autoCompleteCache, debouncedFetchSuggestions]); + useEffect(() => { if (!isFocused) { setSuggestions([]); @@ -209,9 +298,8 @@ const updateCache = (prevCache, key, value) => { case 'company': // 추후 구현 break; - case 'name': - saveRecentSearch(searchQuery, searchType, searchMode); - router.push(`/search/name?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); + case 'medicine': + // 추후 구현 break; } } else { @@ -285,15 +373,10 @@ const updateCache = (prevCache, key, value) => { setSearchQuery(suggestion.text); setSuggestions([]); saveRecentSearch(suggestion.text, searchType, 'keyword'); - switch (searchType) { - case 'symptom': - router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); - break; - case 'name': - router.push(`/search/name?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); - break; - default: - router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); + if (searchType === 'symptom') { + router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); + } else { + router.push(`/search?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); } }; @@ -420,7 +503,7 @@ const updateCache = (prevCache, key, value) => { }} className={`w-full text-left px-4 py-3 hover:bg-[#2BA89C]/5 transition-colors ${ searchType === type ? 'text-[#2BA89C] font-medium bg-[#2BA89C]/10' : 'text-gray-700' - } ${type === 'name' ? 'rounded-b-lg' : ''} ${ + } ${type === 'medicine' ? 'rounded-b-lg' : ''} ${ type === 'symptom' ? 'rounded-t-lg' : '' }`} > @@ -433,7 +516,12 @@ const updateCache = (prevCache, key, value) => { )} - + {/* 자동완성 결과 */} + {searchQuery && !isLoading && autoCompleteCache[`${searchType}:${searchQuery}`] && autoCompleteCache[`${searchType}:${searchQuery}`].length > 0 && ( +
+ {/* ... existing autocomplete results code ... */} +
+ )}
); }; diff --git a/src/components/SearchPage.js b/src/components/SearchPage.js index 56e8c80..a71e9f5 100644 --- a/src/components/SearchPage.js +++ b/src/components/SearchPage.js @@ -1,6 +1,6 @@ 'use client'; -import { useSearchParams, useRouter } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { useState, useEffect } from 'react'; import Header from './Header'; import Footer from './Footer'; @@ -9,9 +9,8 @@ import SearchBar from './SearchBar'; const SearchPage = ({ searchType }) => { const searchParams = useSearchParams(); - const router = useRouter(); const query = searchParams.get('q'); - const mode = searchParams.get('mode'); + const mode = searchParams.get('mode') || 'keyword'; const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); @@ -21,11 +20,6 @@ const SearchPage = ({ searchType }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - // 약품 상세 페이지로 이동하는 함수 - const navigateToDrugDetail = (drugId) => { - router.push(`/drugs/${drugId}`); - }; - useEffect(() => { if (!query) { setFetchedResults([]); @@ -38,45 +32,31 @@ const SearchPage = ({ searchType }) => { let url; let options = { method: 'GET' }; - if(mode === 'natural') { //자연어 검색 + + if (searchType === 'keyword') { url = '/api/api/drugs/search'; options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) }; - } else { // 키워드 검색 모드 분기 - if(searchType === 'symptom') { // 증상 검색 - url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; - } else if(searchType === 'name') { // 약품명 검색 - url = `/api/api/drugs/search/name?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; - } + } else if (searchType === 'symptom') { + url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; } - // if (searchType === 'natural') { - // url = '/api/api/drugs/search'; - // options = { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) - // }; - // } else if (searchType === 'symptom') { - // url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; - // } - - fetch(url, options) // 검색 타입에 따라 데이터 받아오는 방식 분기됨. !!! response 형식 정해지면 수정 필요 + fetch(url, options) .then(res => { if (!res.ok) throw new Error('서버 에러'); return res.json(); }) .then(data => { - let list = mode === 'keyword' ? data.data.searchResponseList : data.data; + let list = searchType === 'keyword' ? data.data : data.data.searchResponseList; setFetchedResults(list); setTotalResults(list.length); }) .catch(err => setError(err.message)) .finally(() => setIsLoading(false)); - }, [query, currentPage, mode]); + }, [query, currentPage, searchType]); return (
@@ -98,10 +78,9 @@ const SearchPage = ({ searchType }) => { ) : fetchedResults.length > 0 ? ( fetchedResults.map(medicine => (
navigateToDrugDetail(medicine.drugId)} - > + key={medicine.drugId} + className="bg-white rounded-lg shadow-sm p-4 border border-transparent hover:shadow-md hover:border-[#2BA89C] transition" + >
{/* 이미지 영역 */}
@@ -118,36 +97,39 @@ const SearchPage = ({ searchType }) => { {/* 정보 영역 */}
- {/* 명칭 */} -
- - 명 칭 - - - {medicine.drugName} - -
- {/* 제약회사 */} -
- - 제약회사 - - - {medicine.company} - -
- {/* 효능 */} -
- - 효 능 - - - {medicine.efficacy.join(', ')} - -
+ {/* 명칭 */} +
+ + 명 칭 + + + {medicine.drugName} + +
+ {/* 제약회사 */} +
+ + 제약회사 + + + {medicine.company} + +
+ {/* 효능 */} +
+ + 효 능 + + + {medicine.efficacy.join(', ')} +
+
+ + + )) ) : (
From dbc3706e42e2da4114663fdd41eb6f843b40347c Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Thu, 1 May 2025 10:35:33 +0900 Subject: [PATCH 11/22] =?UTF-8?q?=E2=9C=A8=20Feature/#9=20=EC=95=BD?= =?UTF-8?q?=ED=92=88=20=EC=83=81=EC=84=B8=EC=A0=95=EB=B3=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: 약품 상세정보 페이지 구현 * ✨ Feat: 약품 상세정보 페이지 백엔드 연동 * ✨ Feat: 한약 구분 및 취소정보 추가 * 📦️ Chore: 레이아웃 확인용 mock 객체 삭제 * 🐛 Fix: useSearchParams의 Server 컴포넌트 사용 문제 해결 --------- Co-authored-by: HaechangLee <112938092+HaechangLee@users.noreply.github.com> --- src/app/drugs/[id]/page.js | 261 +++++++++++++++++++++++++++++++++++ src/app/page.js | 9 +- src/app/search/name/page.js | 12 ++ src/components/NoImage.js | 47 ++++++- src/components/SearchBar.js | 134 +++--------------- src/components/SearchPage.js | 102 ++++++++------ 6 files changed, 404 insertions(+), 161 deletions(-) create mode 100644 src/app/drugs/[id]/page.js create mode 100644 src/app/search/name/page.js diff --git a/src/app/drugs/[id]/page.js b/src/app/drugs/[id]/page.js new file mode 100644 index 0000000..72079f7 --- /dev/null +++ b/src/app/drugs/[id]/page.js @@ -0,0 +1,261 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import Header from '@/components/Header'; +import Footer from '@/components/Footer'; +import NoImage from '@/components/NoImage'; + +export default function DrugDetailPage() { + const params = useParams(); + const drugId = params.id; + + const [drug, setDrug] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // API 개발이 완료되면 아래 주석을 해제하고 mock 데이터 대신 실제 API를 사용하면 됩니다. + + const fetchDrugDetail = async () => { + try { + setLoading(true); + const response = await fetch(`/api/api/drugs/search/detail/${drugId}`); + if (!response.ok) { + throw new Error('약품 정보를 불러오는 데 실패했습니다.'); + } + + const data = await response.json(); + setDrug(data.data); + } catch (err) { + console.error('약품 상세 정보 조회 오류:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchDrugDetail(); + + + // Mock 데이터 사용 +// setTimeout(() => { +// setDrug({ +// ...mockDrugData, +// item_seq: drugId, // URL의 ID 반영 +// }); +// setLoading(false); +// }, 500); // 로딩 효과를 위한 지연 시간 + }, [drugId]); + + if (loading) { + return ( +
+
+
+
로딩 중...
+
+
+
+ ); + } + + if (error || !drug) { + return ( +
+
+
+
+ {error || '약품 정보를 찾을 수 없습니다.'} +
+
+
+
+ ); + } + + // 약품 전문/일반 구분 + const getEtcOtcName = (isGeneral) => { + return isGeneral ? "일반의약품" : "전문의약품"; + }; + + // 주의사항 키 값 추출 + const precautionKeys = drug.precaution ? Object.keys(drug.precaution) : []; + + return ( +
+
+
+
+
+ {/* 상단 영역: 약품 기본 정보 */} +
+ {/* 약품 이미지 */} +
+ {drug.imageUrl ? ( + {drug.drugName} + ) : ( + + )} +
+ + {/* 약품 기본 정보 */} +
+

{drug.drugName}

+
+
+
+ 제약회사 + {drug.company} +
+
+ 품목기준코드 + {drug.drugId} +
+
+ 보관방법 + {drug.storeMethod} +
+
+ 의약품 구분 + {getEtcOtcName(drug.isGeneral)} / {drug.isHerbal ? '한약' : '양약'} +
+
+
+ +
+ 허가일 + {drug.permitDate} +
+
+ 유효기간 + + {drug.validTerm ? drug.validTerm : '정보 없음'} + +
+
+ 취소일자 + + {drug.cancelDate ? drug.cancelDate : '해당 없음'} + +
+
+ 취소사유 + + {drug.cancelName ? drug.cancelName : '해당 없음'} + +
+
+
+
+
+ + {/* 성분 정보 */} + {drug.materialInfo && drug.materialInfo.length > 0 && ( +
+

+ 성분 정보 +

+
+ + + + + + + + + + + + {drug.materialInfo.map((material, index) => ( + + + + + + + + ))} + +
성분명분량단위총량규격
{material.성분명}{material.분량}{material.단위}{material.총량}{material.규격}
+
+
+ )} + + {/* 상세 정보 섹션 */} +
+ {/* 효능효과 */} +
+

+ 효능효과 +

+
+ {drug.efficacy?.length > 0 ? ( +
    + {drug.efficacy.map((item, i) => ( +
  • {item}
  • + ))} +
+ ) : ( +

정보가 없습니다.

+ )} +
+
+ + {/* 용법용량 */} +
+

+ 용법용량 +

+
+ {drug.usage?.length > 0 ? ( +
    + {drug.usage.map((item, i) => ( +
  • {item}
  • + ))} +
+ ) : ( +

정보가 없습니다.

+ )} +
+
+ + {/* 주의사항 및 기타 정보 */} + {precautionKeys.length > 0 && ( +
+

+ 주의사항 +

+ {precautionKeys.map((key) => ( +
+

+ {key} +

+
+ {drug.precaution[key]?.length > 0 ? ( +
    + {drug.precaution[key].map((item, i) => ( +
  • {item}
  • + ))} +
+ ) : ( +

정보가 없습니다.

+ )} +
+
+ ))} +
+ )} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/page.js b/src/app/page.js index 51a129c..642cab4 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,11 +1,16 @@ 'use client'; - +import dynamic from 'next/dynamic'; import { useState, useEffect, useRef } from 'react'; import Image from "next/image"; -import SearchBar from '../components/SearchBar'; import Header from '../components/Header'; import Footer from '../components/Footer'; +const SearchBar = dynamic(() => import('../components/SearchBar'), { + ssr: false, + loading: () =>
검색창 로딩 중...
, +}); + + const MAX_RECENT_SEARCHES = 5; // 최대 저장할 최근 검색어 수 const displayTypes = { diff --git a/src/app/search/name/page.js b/src/app/search/name/page.js new file mode 100644 index 0000000..76dbed9 --- /dev/null +++ b/src/app/search/name/page.js @@ -0,0 +1,12 @@ +'use client'; + +import { Suspense } from 'react'; +import SearchPage from '../../../components/SearchPage'; + +export default function Page() { + return ( + Loading...
}> + + + ); +} \ No newline at end of file diff --git a/src/components/NoImage.js b/src/components/NoImage.js index 743d6b6..fd24929 100644 --- a/src/components/NoImage.js +++ b/src/components/NoImage.js @@ -1,21 +1,56 @@ -const NoImage = () => { +const NoImage = ({ className = "" }) => { return ( -
+
+ {/* 약병 몸체 */} + + + {/* 약병 뚜껑 */} + + + {/* 약병 라벨 */} + {/* */} + + {/* X 표시 - 우측 하단으로 이동 및 크기 확대 */} -

No Image

+

약품 이미지 없음

); diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 243f8d1..87b488e 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; const SearchIcon = ({ color = '#2BA89C', onClick }) => ( ); -const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword' }) => { +const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword', initialSearchType = 'symptom'}) => { const router = useRouter(); + const searchParams = useSearchParams(); const [searchMode, setSearchMode] = useState(initialMode); - const [searchType, setSearchType] = useState('symptom'); + const urlType = searchParams.get('type'); + const [searchType, setSearchType] = useState(urlType || initialSearchType); const [searchQuery, setSearchQuery] = useState(initialQuery); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); const [selectedSuggestion, setSelectedSuggestion] = useState(-1); const [isFocused, setIsFocused] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [autoCompleteCache, setAutoCompleteCache] = useState({}); // 자동완성 결과 캐시 const MAX_CACHE_SIZE = 100; @@ -67,7 +70,7 @@ const updateCache = (prevCache, key, value) => { const searchTypes = { symptom: '증상', company: '제조사', - medicine: '약품명' + name: '약품명' }; const getPlaceholder = () => { @@ -80,7 +83,7 @@ const updateCache = (prevCache, key, value) => { return '증상을 입력하세요'; case 'company': return '제조사를 입력하세요'; - case 'medicine': + case 'name': return '약품명을 입력하세요'; default: return '검색어를 입력하세요'; @@ -116,11 +119,7 @@ const updateCache = (prevCache, key, value) => { // 캐시 업데이트 - 기존 결과를 새로운 결과로 덮어씌움 const cacheKey = `${type}:${query}`; const results = data.data.autoCompleteList || []; - // setAutoCompleteCache(prev => { - // const newCache = { ...prev }; - // newCache[cacheKey] = results; // 기존 키가 있으면 덮어씌움 - // return newCache; - // }); + // 캐시 무한정 커지지 않게 개수 체크하면서 추가 (MAX_CACHE_SIZE를 초과하면 가장 오래된 것 삭제) setAutoCompleteCache(prev => updateCache(prev, cacheKey, results)); @@ -133,94 +132,6 @@ const updateCache = (prevCache, key, value) => { } }; - // debounce된 자동완성 함수 - // const debouncedFetchSuggestions = useCallback( - // debounce(async (query, type) => { - // if (query === prevQuery) { - // console.log('같은 검색어, 요청 스킵:', query); - // return; - // } - // const results = await fetchSuggestions(query, type); - // setSuggestions(results.map(text => ({ text, category: searchTypes[type] }))); - // }, 1000), - // [fetchSuggestions] - // ); - - // 포커스 상태가 변경될 때 실행 - // useEffect(() => { - // if (!isFocused) { - // setSuggestions([]); - // return; - // } - - // // 포커스가 들어왔을 때 현재 검색어에 대한 캐시 확인 - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - // if (autoCompleteCache[cacheKey]) { - // console.log('포커스 시 캐시된 결과 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } - // // 포커스 시에는 API 요청을 보내지 않음 - // } - // }, [isFocused]); - - // // 검색어나 타입이 변경될 때 실행 - // useEffect(() => { - // if (!isFocused) return; - - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - // if (autoCompleteCache[cacheKey]) { - // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } else { - // debouncedFetchSuggestions(trimmedQuery, searchType); - // } - // } else { - // setSuggestions([]); - // } - // }, [searchQuery, searchType, searchMode, autoCompleteCache]); - - // useEffect(() => { - // if (!isFocused) return; - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - // if (autoCompleteCache[cacheKey]) { - // console.log('검색어 변경 시 캐시된 결과 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } else if (trimmedQuery !== prevQuery) { - // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); - // debouncedFetchSuggestions(trimmedQuery, searchType); - // setPrevQuery(trimmedQuery); - // } - // } else { - // setSuggestions([]); - // } - // }, [searchQuery, searchType, searchMode, autoCompleteCache, isFocused, prevQuery]); - - // 검색어, 타입, 모드, 포커스 변화에 따라 자동완성 처리 - // useEffect(() => { - // if (!isFocused) return; - - // const trimmedQuery = searchQuery.trim(); - // if (searchMode === 'keyword' && trimmedQuery.length > 0) { - // const cacheKey = `${searchType}:${trimmedQuery}`; - - // if (autoCompleteCache[cacheKey]) { - // console.log('검색어 변경 시 캐시 사용:', cacheKey); - // setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); - // } else { - // console.log('검색어 변경 시 서버 재요청:', trimmedQuery); - // debouncedFetchSuggestions(trimmedQuery, searchType); - // } - // } else { - // setSuggestions([]); - // } - // }, [searchQuery, searchType, searchMode, autoCompleteCache, debouncedFetchSuggestions]); - useEffect(() => { if (!isFocused) { setSuggestions([]); @@ -298,8 +209,9 @@ const updateCache = (prevCache, key, value) => { case 'company': // 추후 구현 break; - case 'medicine': - // 추후 구현 + case 'name': + saveRecentSearch(searchQuery, searchType, searchMode); + router.push(`/search/name?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); break; } } else { @@ -373,10 +285,15 @@ const updateCache = (prevCache, key, value) => { setSearchQuery(suggestion.text); setSuggestions([]); saveRecentSearch(suggestion.text, searchType, 'keyword'); - if (searchType === 'symptom') { - router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); - } else { - router.push(`/search?q=${encodeURIComponent(suggestion.text)}&mode=keyword`); + switch (searchType) { + case 'symptom': + router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); + break; + case 'name': + router.push(`/search/name?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); + break; + default: + router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); } }; @@ -503,7 +420,7 @@ const updateCache = (prevCache, key, value) => { }} className={`w-full text-left px-4 py-3 hover:bg-[#2BA89C]/5 transition-colors ${ searchType === type ? 'text-[#2BA89C] font-medium bg-[#2BA89C]/10' : 'text-gray-700' - } ${type === 'medicine' ? 'rounded-b-lg' : ''} ${ + } ${type === 'name' ? 'rounded-b-lg' : ''} ${ type === 'symptom' ? 'rounded-t-lg' : '' }`} > @@ -516,12 +433,7 @@ const updateCache = (prevCache, key, value) => { )} - {/* 자동완성 결과 */} - {searchQuery && !isLoading && autoCompleteCache[`${searchType}:${searchQuery}`] && autoCompleteCache[`${searchType}:${searchQuery}`].length > 0 && ( -
- {/* ... existing autocomplete results code ... */} -
- )} +
); }; diff --git a/src/components/SearchPage.js b/src/components/SearchPage.js index a71e9f5..56e8c80 100644 --- a/src/components/SearchPage.js +++ b/src/components/SearchPage.js @@ -1,6 +1,6 @@ 'use client'; -import { useSearchParams } from 'next/navigation'; +import { useSearchParams, useRouter } from 'next/navigation'; import { useState, useEffect } from 'react'; import Header from './Header'; import Footer from './Footer'; @@ -9,8 +9,9 @@ import SearchBar from './SearchBar'; const SearchPage = ({ searchType }) => { const searchParams = useSearchParams(); + const router = useRouter(); const query = searchParams.get('q'); - const mode = searchParams.get('mode') || 'keyword'; + const mode = searchParams.get('mode'); const [currentPage, setCurrentPage] = useState(1); const [totalResults, setTotalResults] = useState(0); @@ -20,6 +21,11 @@ const SearchPage = ({ searchType }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + // 약품 상세 페이지로 이동하는 함수 + const navigateToDrugDetail = (drugId) => { + router.push(`/drugs/${drugId}`); + }; + useEffect(() => { if (!query) { setFetchedResults([]); @@ -32,31 +38,45 @@ const SearchPage = ({ searchType }) => { let url; let options = { method: 'GET' }; - - if (searchType === 'keyword') { + if(mode === 'natural') { //자연어 검색 url = '/api/api/drugs/search'; options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) }; - } else if (searchType === 'symptom') { - url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + } else { // 키워드 검색 모드 분기 + if(searchType === 'symptom') { // 증상 검색 + url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + } else if(searchType === 'name') { // 약품명 검색 + url = `/api/api/drugs/search/name?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + } } - fetch(url, options) + // if (searchType === 'natural') { + // url = '/api/api/drugs/search'; + // options = { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) + // }; + // } else if (searchType === 'symptom') { + // url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + // } + + fetch(url, options) // 검색 타입에 따라 데이터 받아오는 방식 분기됨. !!! response 형식 정해지면 수정 필요 .then(res => { if (!res.ok) throw new Error('서버 에러'); return res.json(); }) .then(data => { - let list = searchType === 'keyword' ? data.data : data.data.searchResponseList; + let list = mode === 'keyword' ? data.data.searchResponseList : data.data; setFetchedResults(list); setTotalResults(list.length); }) .catch(err => setError(err.message)) .finally(() => setIsLoading(false)); - }, [query, currentPage, searchType]); + }, [query, currentPage, mode]); return (
@@ -78,9 +98,10 @@ const SearchPage = ({ searchType }) => { ) : fetchedResults.length > 0 ? ( fetchedResults.map(medicine => (
+ key={medicine.drugId} + className="bg-white rounded-lg shadow-sm p-4 border border-transparent hover:shadow-md hover:border-[#2BA89C] transition cursor-pointer" + onClick={() => navigateToDrugDetail(medicine.drugId)} + >
{/* 이미지 영역 */}
@@ -97,39 +118,36 @@ const SearchPage = ({ searchType }) => { {/* 정보 영역 */}
- {/* 명칭 */} -
- - 명 칭 - - - {medicine.drugName} - -
- {/* 제약회사 */} -
- - 제약회사 - - - {medicine.company} - -
- {/* 효능 */} -
- - 효 능 - - - {medicine.efficacy.join(', ')} - + {/* 명칭 */} +
+ + 명 칭 + + + {medicine.drugName} + +
+ {/* 제약회사 */} +
+ + 제약회사 + + + {medicine.company} + +
+ {/* 효능 */} +
+ + 효 능 + + + {medicine.efficacy.join(', ')} + +
-
- - - )) ) : (
From 37e855c8135bd3cb7c6d3d5e39a2401f2ec4698a Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Sat, 3 May 2025 18:45:20 +0900 Subject: [PATCH 12/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Refactor:=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EA=B2=80=EC=83=89=EC=B0=BD=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 Bug: SearchBar 컴포넌트 검색타입 반영하지 못하는 오류 수정 * ✨ Feat: 검색결과 페이지네이션 구현 * ✨ Feat: 검색결과 페이지네이션 구현 * 💄 Style: 검색바 로딩 시 화면 변하지 않도록 수정 * 💄 Style: NoImage 컴포넌트 디자인 변경 * 📦️ Chore: 검색결과 페이지 주소 파라미터 추가 및 에러 메시지 출력 수정 * ♻️ Refactor: 백엔드 API 변경 반영 * ♻️ Refactor: 백엔드 api 엔드포인트 변경내용 반영. --- src/app/drugs/[id]/page.js | 13 +--- src/app/page.js | 26 ++++++- src/components/NoImage.js | 6 +- src/components/SearchBar.js | 42 +++++------ src/components/SearchPage.js | 134 ++++++++++++++++++++++++++--------- 5 files changed, 148 insertions(+), 73 deletions(-) diff --git a/src/app/drugs/[id]/page.js b/src/app/drugs/[id]/page.js index 72079f7..0e82481 100644 --- a/src/app/drugs/[id]/page.js +++ b/src/app/drugs/[id]/page.js @@ -15,12 +15,11 @@ export default function DrugDetailPage() { const [error, setError] = useState(null); useEffect(() => { - // API 개발이 완료되면 아래 주석을 해제하고 mock 데이터 대신 실제 API를 사용하면 됩니다. const fetchDrugDetail = async () => { try { setLoading(true); - const response = await fetch(`/api/api/drugs/search/detail/${drugId}`); + const response = await fetch(`/api/drugs/search/detail/${drugId}`); if (!response.ok) { throw new Error('약품 정보를 불러오는 데 실패했습니다.'); } @@ -36,16 +35,6 @@ export default function DrugDetailPage() { }; fetchDrugDetail(); - - - // Mock 데이터 사용 -// setTimeout(() => { -// setDrug({ -// ...mockDrugData, -// item_seq: drugId, // URL의 ID 반영 -// }); -// setLoading(false); -// }, 500); // 로딩 효과를 위한 지연 시간 }, [drugId]); if (loading) { diff --git a/src/app/page.js b/src/app/page.js index 642cab4..06a453e 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -5,18 +5,38 @@ import Image from "next/image"; import Header from '../components/Header'; import Footer from '../components/Footer'; +// 스켈레톤 UI 컴포넌트 +const SearchBarSkeleton = () => ( +
+ {/* 검색 모드 선택 탭을 위한 스켈레톤 */} +
+
+
+
+ + {/* 검색 입력 필드를 위한 스켈레톤 */} +
+
+
+
+ + {/* 드롭다운 버튼을 위한 스켈레톤 */} +
+
+
+); + const SearchBar = dynamic(() => import('../components/SearchBar'), { ssr: false, - loading: () =>
검색창 로딩 중...
, + loading: () => , }); - const MAX_RECENT_SEARCHES = 5; // 최대 저장할 최근 검색어 수 const displayTypes = { symptom: '증상', company: '제조사', - medicine: '약품명', + name: '약품명', natural: '자연어' }; diff --git a/src/components/NoImage.js b/src/components/NoImage.js index fd24929..508ffea 100644 --- a/src/components/NoImage.js +++ b/src/components/NoImage.js @@ -42,15 +42,15 @@ const NoImage = ({ className = "" }) => { /> */} {/* X 표시 - 우측 하단으로 이동 및 크기 확대 */} - + /> */} -

약품 이미지 없음

+

약품 이미지 준비 중

); diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 87b488e..01dc816 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -37,12 +37,10 @@ const SearchIcon = ({ color = '#2BA89C', onClick }) => ( ); -const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword', initialSearchType = 'symptom'}) => { +const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword', initialType = 'symptom'}) => { const router = useRouter(); - const searchParams = useSearchParams(); const [searchMode, setSearchMode] = useState(initialMode); - const urlType = searchParams.get('type'); - const [searchType, setSearchType] = useState(urlType || initialSearchType); + const [searchType, setSearchType] = useState(initialType); const [searchQuery, setSearchQuery] = useState(initialQuery); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); @@ -96,7 +94,7 @@ const updateCache = (prevCache, key, value) => { try { setIsLoading(true); - const url = `/api/api/drugs/autocomplete/${type}?q=${encodeURIComponent(query)}`; + const url = `/api/drugs/autocomplete/${type}?q=${encodeURIComponent(query)}`; console.log('자동완성 요청 URL:', url); @@ -110,7 +108,7 @@ const updateCache = (prevCache, key, value) => { console.log('자동완성 응답:', response); if (!response.ok) { - throw new Error('자동완성 데이터를 가져오는데 실패했습니다.'); + throw new Error(response.message); } const data = await response.json(); @@ -132,6 +130,7 @@ const updateCache = (prevCache, key, value) => { } }; + // 자동완성 결과 업데이트 useEffect(() => { if (!isFocused) { setSuggestions([]); @@ -176,17 +175,17 @@ const updateCache = (prevCache, key, value) => { }; // 최근 검색어 저장 함수 - const saveRecentSearch = (query, type, mode) => { + const saveRecentSearch = (query, mode, type) => { try { const savedSearches = sessionStorage.getItem('recentSearches'); const searches = savedSearches ? JSON.parse(savedSearches) : []; const newSearch = { query: query.trim(), - type: type, - mode: mode + mode: mode, + type: type }; - + const filteredSearches = searches.filter(item => item.query !== newSearch.query); const updatedSearches = [newSearch, ...filteredSearches].slice(0, 5); @@ -196,6 +195,7 @@ const updateCache = (prevCache, key, value) => { } }; + // 검색 모드(키워드,자연어) 감지하여 모드별 검색 결과창 라우팅 const handleSearch = (e) => { e.preventDefault(); if (!searchQuery.trim()) return; @@ -203,24 +203,25 @@ const updateCache = (prevCache, key, value) => { if (searchMode === 'keyword') { switch (searchType) { case 'symptom': - saveRecentSearch(searchQuery, searchType, searchMode); - router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); + saveRecentSearch(searchQuery, searchMode, searchType); + router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=${searchType}`); break; case 'company': // 추후 구현 break; case 'name': - saveRecentSearch(searchQuery, searchType, searchMode); - router.push(`/search/name?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); + saveRecentSearch(searchQuery, searchMode, searchType); + router.push(`/search/name?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=${searchType}`); break; } } else { // 자연어 검색 처리 - saveRecentSearch(searchQuery, 'natural', searchMode); - router.push(`/search?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}`); + saveRecentSearch(searchQuery, searchMode, 'natural'); + router.push(`/search?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=natural`); } }; + const handleInputChange = (e) => { const value = e.target.value; if (searchMode === 'natural' && value.length > 20) { @@ -230,6 +231,7 @@ const updateCache = (prevCache, key, value) => { setSelectedSuggestion(-1); }; + const handleKeyDown = (e) => { if (searchMode === 'natural' && e.key === 'Enter') { e.preventDefault(); @@ -253,8 +255,8 @@ const updateCache = (prevCache, key, value) => { case 'Enter': e.preventDefault(); if (selectedSuggestion >= 0) { - setSearchQuery(suggestions[selectedSuggestion].text); - setSuggestions([]); + const selectedItem = suggestions[selectedSuggestion]; + handleSuggestionClick(selectedItem); } else { handleSearch(e); } @@ -284,7 +286,7 @@ const updateCache = (prevCache, key, value) => { const handleSuggestionClick = (suggestion) => { setSearchQuery(suggestion.text); setSuggestions([]); - saveRecentSearch(suggestion.text, searchType, 'keyword'); + saveRecentSearch(suggestion.text, 'keyword', searchType); switch (searchType) { case 'symptom': router.push(`/search/symptom?q=${encodeURIComponent(suggestion.text)}&mode=keyword&type=${searchType}`); @@ -423,7 +425,7 @@ const updateCache = (prevCache, key, value) => { } ${type === 'name' ? 'rounded-b-lg' : ''} ${ type === 'symptom' ? 'rounded-t-lg' : '' }`} - > + > {label} ))} diff --git a/src/components/SearchPage.js b/src/components/SearchPage.js index 56e8c80..3c9e90b 100644 --- a/src/components/SearchPage.js +++ b/src/components/SearchPage.js @@ -12,8 +12,11 @@ const SearchPage = ({ searchType }) => { const router = useRouter(); const query = searchParams.get('q'); const mode = searchParams.get('mode'); + const type = searchParams.get('type') || searchType; + const pageParam = searchParams.get('page'); - const [currentPage, setCurrentPage] = useState(1); + // URL에 페이지 파라미터가 있으면 사용하고, 없으면 기본값 1 사용 + const [currentPage, setCurrentPage] = useState(pageParam ? parseInt(pageParam) : 1); const [totalResults, setTotalResults] = useState(0); const itemsPerPage = 10; @@ -36,47 +39,53 @@ const SearchPage = ({ searchType }) => { setError(null); const apiPage = currentPage - 1; - let url; + let url = `/api/drugs/search/${type}?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; let options = { method: 'GET' }; - if(mode === 'natural') { //자연어 검색 - url = '/api/api/drugs/search'; - options = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) - }; - } else { // 키워드 검색 모드 분기 - if(searchType === 'symptom') { // 증상 검색 - url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; - } else if(searchType === 'name') { // 약품명 검색 - url = `/api/api/drugs/search/name?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; - } - } - - // if (searchType === 'natural') { - // url = '/api/api/drugs/search'; + // if(mode === 'natural') { //자연어 검색 + // url = '/api/drugs/search'; // options = { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) // }; - // } else if (searchType === 'symptom') { - // url = `/api/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + // } else { // 키워드 검색 모드 분기 + // if(searchType === 'symptom') { // 증상 검색 + // url = `/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + // } else if(searchType === 'name') { // 약품명 검색 + // url = `/api/drugs/search/name?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; + // } // } + fetch(url, options) // 검색 타입에 따라 데이터 받아오는 방식 분기됨. !!! response 형식 정해지면 수정 필요 .then(res => { - if (!res.ok) throw new Error('서버 에러'); + if (!res.ok) throw new Error(res.message); return res.json(); }) .then(data => { let list = mode === 'keyword' ? data.data.searchResponseList : data.data; setFetchedResults(list); - setTotalResults(list.length); + + // totalResponseCount가 있으면 그 값을 사용하고, 없으면 현재 목록 길이를 사용 + const totalCount = data.data.totalResponseCount || list.length; + setTotalResults(totalCount); }) .catch(err => setError(err.message)) .finally(() => setIsLoading(false)); - }, [query, currentPage, mode]); + }, [query, currentPage, mode, searchType, itemsPerPage]); + + // 페이지 변경 함수 + const handlePageChange = (pageNumber) => { + setCurrentPage(pageNumber); + + // URL 업데이트 + const params = new URLSearchParams(searchParams); + params.set('page', pageNumber.toString()); + router.push(`${window.location.pathname}?${params.toString()}`); + + // 페이지 상단으로 스크롤 + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; return (
@@ -88,8 +97,20 @@ const SearchPage = ({ searchType }) => { initialQuery={query || ''} showTabs={true} initialMode={mode} + initialType={type} />
+ + {/* 검색 결과 정보 헤더 */} + {!isLoading && !error && fetchedResults.length > 0 && ( +
+

+ {query} 검색 결과 + {totalResults.toLocaleString()}건 +

+
+ )} +
{isLoading ? (
로딩 중...
@@ -156,21 +177,64 @@ const SearchPage = ({ searchType }) => {
)}
- {fetchedResults.length > 0 && ( + + {/* 페이지네이션 개선 */} + {totalResults > 0 && (
- {[...Array(Math.ceil(totalResults / itemsPerPage)).keys()].map(i => ( + {/* 이전 페이지 버튼 */} + {currentPage > 1 && ( + + )} + + {/* 페이지 번호 버튼 */} + {Array.from({ length: Math.min(5, Math.ceil(totalResults / itemsPerPage)) }, (_, i) => { + // 현재 페이지를 중심으로 페이지 번호 표시 + const totalPages = Math.ceil(totalResults / itemsPerPage); + let pageNum; + + if (totalPages <= 5) { + // 전체 페이지가 5개 이하면 모든 페이지 표시 + pageNum = i + 1; + } else if (currentPage <= 3) { + // 현재 페이지가 1,2,3이면 1~5 표시 + pageNum = i + 1; + } else if (currentPage >= totalPages - 2) { + // 현재 페이지가 마지막 3페이지 안에 있으면 마지막 5페이지 표시 + pageNum = totalPages - 4 + i; + } else { + // 그 외에는 현재 페이지 중심으로 앞뒤 2페이지씩 표시 + pageNum = currentPage - 2 + i; + } + + return ( + + ); + })} + + {/* 다음 페이지 버튼 */} + {currentPage < Math.ceil(totalResults / itemsPerPage) && ( - ))} + )}
)}
From 933afb8e4859a1220d486754dcef437918c53571 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Sun, 4 May 2025 12:34:05 +0900 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=90=9B=20=20Fix:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드 자연어 검색 API 형식 변경에 맞춰 프론트엔드 검색결과 처리방식을 수정했습니다. --- src/app/search/page.js | 2 +- src/components/SearchPage.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/search/page.js b/src/app/search/page.js index 20aee7d..814b394 100644 --- a/src/app/search/page.js +++ b/src/app/search/page.js @@ -6,7 +6,7 @@ import SearchPage from '../../components/SearchPage'; export default function Page() { return ( Loading...
}> - + ); } \ No newline at end of file diff --git a/src/components/SearchPage.js b/src/components/SearchPage.js index 3c9e90b..f8e8f25 100644 --- a/src/components/SearchPage.js +++ b/src/components/SearchPage.js @@ -63,11 +63,11 @@ const SearchPage = ({ searchType }) => { return res.json(); }) .then(data => { - let list = mode === 'keyword' ? data.data.searchResponseList : data.data; + let list = data.data.searchResponseList; setFetchedResults(list); // totalResponseCount가 있으면 그 값을 사용하고, 없으면 현재 목록 길이를 사용 - const totalCount = data.data.totalResponseCount || list.length; + const totalCount = data.data.totalResponseCount; setTotalResults(totalCount); }) .catch(err => setError(err.message)) From 2252d9d2cd7c401ca0e6d604ef143a62fc491788 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 6 May 2025 01:50:10 +0900 Subject: [PATCH 14/22] =?UTF-8?q?=E2=9C=A8=20=20Feat:=20=EC=84=B1=EB=B6=84?= =?UTF-8?q?=EB=AA=85=20=EA=B2=80=EC=83=89=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/drugs/[id]/page.js | 16 +++++++++++----- src/app/search/material/page.js | 12 ++++++++++++ src/components/SearchBar.js | 11 ++++++----- 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 src/app/search/material/page.js diff --git a/src/app/drugs/[id]/page.js b/src/app/drugs/[id]/page.js index 0e82481..4016c66 100644 --- a/src/app/drugs/[id]/page.js +++ b/src/app/drugs/[id]/page.js @@ -1,10 +1,11 @@ 'use client'; import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; +import { useParams, useRouter } from 'next/navigation'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; import NoImage from '@/components/NoImage'; +import Link from 'next/link'; export default function DrugDetailPage() { const params = useParams(); @@ -111,7 +112,7 @@ export default function DrugDetailPage() {
의약품 구분 - {getEtcOtcName(drug.isGeneral)} / {drug.isHerbal ? '한약' : '양약'} + {getEtcOtcName(drug.isGeneral)}
@@ -157,17 +158,22 @@ export default function DrugDetailPage() { 분량 단위 총량 - 규격 {drug.materialInfo.map((material, index) => ( - {material.성분명} + + + {material.성분명} + + {material.분량} {material.단위} {material.총량} - {material.규격} ))} diff --git a/src/app/search/material/page.js b/src/app/search/material/page.js new file mode 100644 index 0000000..d3f0e68 --- /dev/null +++ b/src/app/search/material/page.js @@ -0,0 +1,12 @@ +'use client'; + +import { Suspense } from 'react'; +import SearchPage from '../../../components/SearchPage'; + +export default function Page() { + return ( + Loading...
}> + + + ); +} \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 01dc816..263f886 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -67,7 +67,7 @@ const updateCache = (prevCache, key, value) => { const searchTypes = { symptom: '증상', - company: '제조사', + material: '성분명', name: '약품명' }; @@ -79,8 +79,8 @@ const updateCache = (prevCache, key, value) => { switch (searchType) { case 'symptom': return '증상을 입력하세요'; - case 'company': - return '제조사를 입력하세요'; + case 'material': + return '성분명을 입력하세요'; case 'name': return '약품명을 입력하세요'; default: @@ -206,8 +206,9 @@ const updateCache = (prevCache, key, value) => { saveRecentSearch(searchQuery, searchMode, searchType); router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=${searchType}`); break; - case 'company': - // 추후 구현 + case 'material': + saveRecentSearch(searchQuery, searchMode, searchType); + router.push(`/search/material?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=${searchType}`); break; case 'name': saveRecentSearch(searchQuery, searchMode, searchType); From 2aa66409385362d9e3fb2c34b12474880adb04d2 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 6 May 2025 02:01:11 +0900 Subject: [PATCH 15/22] Update docker-compose.yml --- docker-compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3f99176..527f155 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,3 +11,9 @@ services: ports: - "13000:3000" restart: unless-stopped + networks: + - deploy_yakplus + +networks: + deploy_yakplus: + external: true From 3ed54ed981289da011191320479b55634cfe8dc7 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 6 May 2025 02:03:31 +0900 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=90=9B=20=20Bug:=20=EC=84=B1?= =?UTF-8?q?=EB=B6=84=EA=B2=80=EC=83=89=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 Bug: 성분검색 엔드포인트 수정 * 🐛 Bug: 성분검색 상세페이지 엔드포인트 수정 --- src/app/drugs/[id]/page.js | 2 +- src/components/SearchBar.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/drugs/[id]/page.js b/src/app/drugs/[id]/page.js index 4016c66..250568e 100644 --- a/src/app/drugs/[id]/page.js +++ b/src/app/drugs/[id]/page.js @@ -165,7 +165,7 @@ export default function DrugDetailPage() { {material.성분명} diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 263f886..07a7747 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -67,7 +67,7 @@ const updateCache = (prevCache, key, value) => { const searchTypes = { symptom: '증상', - material: '성분명', + ingredient: '성분명', name: '약품명' }; @@ -79,7 +79,7 @@ const updateCache = (prevCache, key, value) => { switch (searchType) { case 'symptom': return '증상을 입력하세요'; - case 'material': + case 'ingredient': return '성분명을 입력하세요'; case 'name': return '약품명을 입력하세요'; @@ -206,9 +206,9 @@ const updateCache = (prevCache, key, value) => { saveRecentSearch(searchQuery, searchMode, searchType); router.push(`/search/symptom?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=${searchType}`); break; - case 'material': + case 'ingredient': saveRecentSearch(searchQuery, searchMode, searchType); - router.push(`/search/material?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=${searchType}`); + router.push(`/search/ingredient?q=${encodeURIComponent(searchQuery)}&mode=${searchMode}&type=${searchType}`); break; case 'name': saveRecentSearch(searchQuery, searchMode, searchType); From 53d7deb4e7014e4049a42e6ad0d5a4a8b950771c Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 6 May 2025 02:16:48 +0900 Subject: [PATCH 17/22] =?UTF-8?q?=E2=9C=A8=20=20Feat:=20=EC=84=B1=EB=B6=84?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85=20=EC=A0=95=EC=83=81=ED=99=94=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.js | 2 +- src/app/search/{material => ingredient}/page.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/app/search/{material => ingredient}/page.js (83%) diff --git a/src/app/page.js b/src/app/page.js index 06a453e..628aa05 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -35,7 +35,7 @@ const MAX_RECENT_SEARCHES = 5; // 최대 저장할 최근 검색어 수 const displayTypes = { symptom: '증상', - company: '제조사', + ingredient: '성분명', name: '약품명', natural: '자연어' }; diff --git a/src/app/search/material/page.js b/src/app/search/ingredient/page.js similarity index 83% rename from src/app/search/material/page.js rename to src/app/search/ingredient/page.js index d3f0e68..3ee6f6b 100644 --- a/src/app/search/material/page.js +++ b/src/app/search/ingredient/page.js @@ -6,7 +6,7 @@ import SearchPage from '../../../components/SearchPage'; export default function Page() { return ( Loading...
}> - + ); } \ No newline at end of file From 346cb863a73a9264979cd12ddefc5eca92d684b5 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 6 May 2025 11:51:18 +0900 Subject: [PATCH 18/22] =?UTF-8?q?=F0=9F=90=9B=20=20Fix:=20=EC=9E=90?= =?UTF-8?q?=EC=97=B0=EC=96=B4=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=91=9C=EC=B6=9C=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=8C=80=EC=9D=91=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 자연어 검색 시 검색결과를 상위 100건만 표출하도록 수정하였습니다. 2. 모바일 페이지에 대응하여, 검색결과 페이지 디자인을 수정하였습니다. --- src/components/SearchPage.js | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/components/SearchPage.js b/src/components/SearchPage.js index f8e8f25..00ba8e6 100644 --- a/src/components/SearchPage.js +++ b/src/components/SearchPage.js @@ -41,20 +41,6 @@ const SearchPage = ({ searchType }) => { let url = `/api/drugs/search/${type}?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; let options = { method: 'GET' }; - // if(mode === 'natural') { //자연어 검색 - // url = '/api/drugs/search'; - // options = { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify({ query, page: apiPage, size: itemsPerPage }) - // }; - // } else { // 키워드 검색 모드 분기 - // if(searchType === 'symptom') { // 증상 검색 - // url = `/api/drugs/search/symptom?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; - // } else if(searchType === 'name') { // 약품명 검색 - // url = `/api/drugs/search/name?q=${encodeURIComponent(query)}&page=${apiPage}&size=${itemsPerPage}`; - // } - // } fetch(url, options) // 검색 타입에 따라 데이터 받아오는 방식 분기됨. !!! response 형식 정해지면 수정 필요 @@ -69,6 +55,9 @@ const SearchPage = ({ searchType }) => { // totalResponseCount가 있으면 그 값을 사용하고, 없으면 현재 목록 길이를 사용 const totalCount = data.data.totalResponseCount; setTotalResults(totalCount); + if(mode === 'natural') { + setTotalResults(100); + } }) .catch(err => setError(err.message)) .finally(() => setIsLoading(false)); @@ -106,8 +95,14 @@ const SearchPage = ({ searchType }) => {

{query} 검색 결과 - {totalResults.toLocaleString()}건 + {mode !== 'natural' && ( + {totalResults.toLocaleString()} + )} + {mode !== 'natural' && '건'}

+ {mode === 'natural' && ( +

자연어 검색결과는 상위 {totalResults.toLocaleString()}건만 표시됩니다

+ )}
)} @@ -123,9 +118,9 @@ const SearchPage = ({ searchType }) => { className="bg-white rounded-lg shadow-sm p-4 border border-transparent hover:shadow-md hover:border-[#2BA89C] transition cursor-pointer" onClick={() => navigateToDrugDetail(medicine.drugId)} > -
+
{/* 이미지 영역 */} -
+
{medicine.imageUrl ? ( {
{/* 정보 영역 */} -
+
{/* 명칭 */}
From 4331fbfb0188cba685d2e2aed056ffd035133142 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 6 May 2025 17:25:23 +0900 Subject: [PATCH 19/22] =?UTF-8?q?=F0=9F=92=84=20=20Style:=20placeholder=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 검색창 placeholder의 글씨 색상을 진하게 수정하였습니다. 2. 최근 검색어 클릭 시 즉시 검색이 되도록 수정하였습니다. 3. 검색 타입 변경 시 검색어가 초기화 되지 않도록 수정하였습니다. 4. 불필요한 콘솔 로그 표시를 주석처리 하였습니다. --- src/app/page.js | 17 ++++++++--------- src/components/SearchBar.js | 13 ++++++------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/app/page.js b/src/app/page.js index 628aa05..b18d93b 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from 'react'; import Image from "next/image"; import Header from '../components/Header'; import Footer from '../components/Footer'; +import { useRouter } from 'next/navigation'; // 스켈레톤 UI 컴포넌트 const SearchBarSkeleton = () => ( @@ -48,18 +49,16 @@ const Home = () => { initialType: 'symptom' }); const searchBarRef = useRef(null); + const router = useRouter(); // 최근 검색어 클릭 핸들러 const handleRecentSearchClick = (searchItem) => { - setSearchBarProps({ - initialQuery: searchItem.query, - initialMode: searchItem.mode, - initialType: searchItem.type - }); - // setTimeout을 사용하여 다음 렌더링 사이클에서 포커스 - setTimeout(() => { - searchBarRef.current?.focus(); - }, 0); + // 검색어 클릭 시 바로 검색 페이지로 이동 + if (searchItem.mode === 'natural') { + router.push(`/search?q=${encodeURIComponent(searchItem.query)}&mode=${searchItem.mode}&type=natural`); + } else { + router.push(`/search/${searchItem.type}?q=${encodeURIComponent(searchItem.query)}&mode=${searchItem.mode}&type=${searchItem.type}`); + } }; // 최근 검색어 로드 diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 07a7747..9109d89 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -96,7 +96,7 @@ const updateCache = (prevCache, key, value) => { const url = `/api/drugs/autocomplete/${type}?q=${encodeURIComponent(query)}`; - console.log('자동완성 요청 URL:', url); + // console.log('자동완성 요청 URL:', url); const response = await fetch(url, { method: 'GET', @@ -105,14 +105,14 @@ const updateCache = (prevCache, key, value) => { }, }); - console.log('자동완성 응답:', response); + // console.log('자동완성 응답:', response); if (!response.ok) { throw new Error(response.message); } const data = await response.json(); - console.log('자동완성 데이터:', data); + // console.log('자동완성 데이터:', data); // 캐시 업데이트 - 기존 결과를 새로운 결과로 덮어씌움 const cacheKey = `${type}:${query}`; @@ -146,14 +146,14 @@ const updateCache = (prevCache, key, value) => { } if (autoCompleteCache[cacheKey]) { - console.log('캐시 사용:', cacheKey); + // console.log('캐시 사용:', cacheKey); setSuggestions(autoCompleteCache[cacheKey].map(text => ({ text, category: searchTypes[searchType] }))); return; } // 300ms 후에 서버 요청 예약 const handler = setTimeout(() => { - console.log('서버 요청:', trimmedQuery); + // console.log('서버 요청:', trimmedQuery); fetchSuggestions(trimmedQuery, searchType) .then(results => { setSuggestions(results.map(text => ({ text, category: searchTypes[searchType] }))); @@ -350,7 +350,7 @@ const updateCache = (prevCache, key, value) => { onFocus={() => setIsFocused(true)} onBlur={handleBlur} maxLength={searchMode === 'natural' ? 20 : undefined} - className={`w-full pl-16 pr-4 py-4 border rounded-lg focus:outline-none transition-all hover:shadow-md ${ + className={`w-full pl-16 pr-4 py-4 border rounded-lg focus:outline-none transition-all hover:shadow-md placeholder-gray-900 placeholder-opacity-100 ${ searchMode === 'keyword' ? 'border-[#2BA89C] focus:ring-2 focus:ring-[#2BA89C]/20' : 'border-[#2978F2] focus:ring-2 focus:ring-[#2978F2]/20' @@ -419,7 +419,6 @@ const updateCache = (prevCache, key, value) => { setSearchType(type); setIsDropdownOpen(false); setSuggestions([]); - setSearchQuery(''); }} className={`w-full text-left px-4 py-3 hover:bg-[#2BA89C]/5 transition-colors ${ searchType === type ? 'text-[#2BA89C] font-medium bg-[#2BA89C]/10' : 'text-gray-700' From 6814f4c747ecabc4d86b1f4f67b107ade3a0561c Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 6 May 2025 18:26:17 +0900 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=92=84=20Style:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=96=B4=20=EC=83=89=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 💄 Style: 검색어 색상 수정 * 💄 Style: 자동완성 검색어 색상 수정 --- src/components/SearchBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 9109d89..0adc9de 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -350,7 +350,7 @@ const updateCache = (prevCache, key, value) => { onFocus={() => setIsFocused(true)} onBlur={handleBlur} maxLength={searchMode === 'natural' ? 20 : undefined} - className={`w-full pl-16 pr-4 py-4 border rounded-lg focus:outline-none transition-all hover:shadow-md placeholder-gray-900 placeholder-opacity-100 ${ + className={`w-full pl-16 pr-4 py-4 border rounded-lg focus:outline-none transition-all hover:shadow-md placeholder-gray-900 placeholder-opacity-100 text-black ${ searchMode === 'keyword' ? 'border-[#2BA89C] focus:ring-2 focus:ring-[#2BA89C]/20' : 'border-[#2978F2] focus:ring-2 focus:ring-[#2978F2]/20' @@ -377,7 +377,7 @@ const updateCache = (prevCache, key, value) => { index === selectedSuggestion ? 'bg-[#2BA89C]/10' : '' }`} > -
+
{highlightMatch(suggestion.text)}
From c71ba349131d017d0fe569cbc5b4a5427648473b Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 20 May 2025 15:32:33 +0900 Subject: [PATCH 21/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EA=B2=80=EC=83=89=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=90=EC=97=B0=EC=96=B4=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SearchBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index 0adc9de..c1de851 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -37,7 +37,7 @@ const SearchIcon = ({ color = '#2BA89C', onClick }) => ( ); -const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'keyword', initialType = 'symptom'}) => { +const SearchBar = ({ initialQuery = '', showTabs = true, initialMode = 'natural', initialType = 'symptom'}) => { const router = useRouter(); const [searchMode, setSearchMode] = useState(initialMode); const [searchType, setSearchType] = useState(initialType); @@ -440,4 +440,4 @@ const updateCache = (prevCache, key, value) => { ); }; -export default SearchBar; \ No newline at end of file +export default SearchBar; From ea613ba928181e2fbe526332ee77064d8ff32c17 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 20 May 2025 16:39:57 +0900 Subject: [PATCH 22/22] =?UTF-8?q?=F0=9F=92=84=20=20Style:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EB=AA=A8=EB=93=9C=20=EC=A0=95=EB=A0=AC=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EB=B3=80=EA=B2=BD=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.js | 2 +- src/components/SearchBar.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/page.js b/src/app/page.js index b18d93b..90c2351 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -45,7 +45,7 @@ const Home = () => { const [recentSearches, setRecentSearches] = useState([]); const [searchBarProps, setSearchBarProps] = useState({ initialQuery: '', - initialMode: 'keyword', + initialMode: 'natural', initialType: 'symptom' }); const searchBarRef = useRef(null); diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index c1de851..df8a0dc 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -307,29 +307,29 @@ const updateCache = (prevCache, key, value) => {
)}