Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.

Commit dbc3706

Browse files
✨ Feature/#9 약품 상세정보 페이지 (#12)
* ✨ Feat: 약품 상세정보 페이지 구현 * ✨ Feat: 약품 상세정보 페이지 백엔드 연동 * ✨ Feat: 한약 구분 및 취소정보 추가 * 📦️ Chore: 레이아웃 확인용 mock 객체 삭제 * 🐛 Fix: useSearchParams의 Server 컴포넌트 사용 문제 해결 --------- Co-authored-by: HaechangLee <112938092+HaechangLee@users.noreply.github.com>
1 parent 3d06f65 commit dbc3706

File tree

6 files changed

+404
-161
lines changed

6 files changed

+404
-161
lines changed

src/app/drugs/[id]/page.js

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { useParams } from 'next/navigation';
5+
import Header from '@/components/Header';
6+
import Footer from '@/components/Footer';
7+
import NoImage from '@/components/NoImage';
8+
9+
export default function DrugDetailPage() {
10+
const params = useParams();
11+
const drugId = params.id;
12+
13+
const [drug, setDrug] = useState(null);
14+
const [loading, setLoading] = useState(true);
15+
const [error, setError] = useState(null);
16+
17+
useEffect(() => {
18+
// API 개발이 완료되면 아래 주석을 해제하고 mock 데이터 대신 실제 API를 사용하면 됩니다.
19+
20+
const fetchDrugDetail = async () => {
21+
try {
22+
setLoading(true);
23+
const response = await fetch(`/api/api/drugs/search/detail/${drugId}`);
24+
if (!response.ok) {
25+
throw new Error('약품 정보를 불러오는 데 실패했습니다.');
26+
}
27+
28+
const data = await response.json();
29+
setDrug(data.data);
30+
} catch (err) {
31+
console.error('약품 상세 정보 조회 오류:', err);
32+
setError(err.message);
33+
} finally {
34+
setLoading(false);
35+
}
36+
};
37+
38+
fetchDrugDetail();
39+
40+
41+
// Mock 데이터 사용
42+
// setTimeout(() => {
43+
// setDrug({
44+
// ...mockDrugData,
45+
// item_seq: drugId, // URL의 ID 반영
46+
// });
47+
// setLoading(false);
48+
// }, 500); // 로딩 효과를 위한 지연 시간
49+
}, [drugId]);
50+
51+
if (loading) {
52+
return (
53+
<div className="min-h-screen flex flex-col">
54+
<Header />
55+
<main className="flex-1 flex items-center justify-center">
56+
<div className="text-center py-16">로딩 중...</div>
57+
</main>
58+
<Footer />
59+
</div>
60+
);
61+
}
62+
63+
if (error || !drug) {
64+
return (
65+
<div className="min-h-screen flex flex-col">
66+
<Header />
67+
<main className="flex-1 flex items-center justify-center">
68+
<div className="text-center py-16 text-red-500">
69+
{error || '약품 정보를 찾을 수 없습니다.'}
70+
</div>
71+
</main>
72+
<Footer />
73+
</div>
74+
);
75+
}
76+
77+
// 약품 전문/일반 구분
78+
const getEtcOtcName = (isGeneral) => {
79+
return isGeneral ? "일반의약품" : "전문의약품";
80+
};
81+
82+
// 주의사항 키 값 추출
83+
const precautionKeys = drug.precaution ? Object.keys(drug.precaution) : [];
84+
85+
return (
86+
<div className="min-h-screen flex flex-col bg-gray-50">
87+
<Header />
88+
<main className="flex-1 w-full mt-[64px]">
89+
<div className="max-w-5xl mx-auto w-full px-4 py-8">
90+
<div className="bg-white rounded-lg shadow-md p-6">
91+
{/* 상단 영역: 약품 기본 정보 */}
92+
<div className="flex flex-col md:flex-row gap-8 mb-8">
93+
{/* 약품 이미지 */}
94+
<div className="w-full md:w-80 h-80 flex-shrink-0">
95+
{drug.imageUrl ? (
96+
<img
97+
src={drug.imageUrl}
98+
alt={drug.drugName}
99+
className="w-full h-full rounded-lg object-contain border border-gray-200"
100+
/>
101+
) : (
102+
<NoImage className="w-full h-full" />
103+
)}
104+
</div>
105+
106+
{/* 약품 기본 정보 */}
107+
<div className="flex-1 space-y-4">
108+
<h1 className="text-2xl font-bold text-gray-800">{drug.drugName}</h1>
109+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
110+
<div className="space-y-2">
111+
<div className="flex items-center">
112+
<span className="w-24 text-sm font-medium text-gray-500">제약회사</span>
113+
<span className="text-gray-700">{drug.company}</span>
114+
</div>
115+
<div className="flex items-center">
116+
<span className="w-24 text-sm font-medium text-gray-500">품목기준코드</span>
117+
<span className="text-gray-700">{drug.drugId}</span>
118+
</div>
119+
<div className="flex items-center">
120+
<span className="w-24 text-sm font-medium text-gray-500">보관방법</span>
121+
<span className="text-gray-700">{drug.storeMethod}</span>
122+
</div>
123+
<div className="flex items-center">
124+
<span className="w-24 text-sm font-medium text-gray-500">의약품 구분</span>
125+
<span className="text-gray-700">{getEtcOtcName(drug.isGeneral)} / {drug.isHerbal ? '한약' : '양약'}</span>
126+
</div>
127+
</div>
128+
<div className="space-y-2">
129+
130+
<div className="flex items-center">
131+
<span className="w-24 text-sm font-medium text-gray-500">허가일</span>
132+
<span className="text-gray-700">{drug.permitDate}</span>
133+
</div>
134+
<div className="flex items-center">
135+
<span className="w-24 text-sm font-medium text-gray-500">유효기간</span>
136+
<span className="text-gray-700">
137+
{drug.validTerm ? drug.validTerm : '정보 없음'}
138+
</span>
139+
</div>
140+
<div className="flex items-center">
141+
<span className="w-24 text-sm font-medium text-gray-500">취소일자</span>
142+
<span className="text-gray-700">
143+
{drug.cancelDate ? drug.cancelDate : '해당 없음'}
144+
</span>
145+
</div>
146+
<div className="flex items-center">
147+
<span className="w-24 text-sm font-medium text-gray-500">취소사유</span>
148+
<span className="text-gray-700">
149+
{drug.cancelName ? drug.cancelName : '해당 없음'}
150+
</span>
151+
</div>
152+
</div>
153+
</div>
154+
</div>
155+
</div>
156+
157+
{/* 성분 정보 */}
158+
{drug.materialInfo && drug.materialInfo.length > 0 && (
159+
<div className="mb-6">
160+
<h2 className="text-lg font-bold px-4 py-2 rounded-md mb-3 border-b border-gray-300 pb-2">
161+
성분 정보
162+
</h2>
163+
<div className="overflow-x-auto">
164+
<table className="min-w-full divide-y divide-gray-200">
165+
<thead>
166+
<tr>
167+
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">성분명</th>
168+
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">분량</th>
169+
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">단위</th>
170+
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">총량</th>
171+
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">규격</th>
172+
</tr>
173+
</thead>
174+
<tbody className="bg-white divide-y divide-gray-200">
175+
{drug.materialInfo.map((material, index) => (
176+
<tr key={index}>
177+
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.성분명}</td>
178+
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.분량}</td>
179+
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.단위}</td>
180+
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.총량}</td>
181+
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.규격}</td>
182+
</tr>
183+
))}
184+
</tbody>
185+
</table>
186+
</div>
187+
</div>
188+
)}
189+
190+
{/* 상세 정보 섹션 */}
191+
<div className="border-t pt-6 space-y-6">
192+
{/* 효능효과 */}
193+
<div className="space-y-3">
194+
<h2 className="text-lg font-bold px-4 py-2 text-[#2BA89C] rounded-md border-b border-gray-300 pb-2">
195+
효능효과
196+
</h2>
197+
<div className="px-4 py-2 text-gray-700">
198+
{drug.efficacy?.length > 0 ? (
199+
<ul className="list-none pl-0 space-y-1">
200+
{drug.efficacy.map((item, i) => (
201+
<li key={i}>{item}</li>
202+
))}
203+
</ul>
204+
) : (
205+
<p>정보가 없습니다.</p>
206+
)}
207+
</div>
208+
</div>
209+
210+
{/* 용법용량 */}
211+
<div className="space-y-3">
212+
<h2 className="text-lg font-bold px-4 py-2 text-[#2BA89C] rounded-md border-b border-gray-300 pb-2">
213+
용법용량
214+
</h2>
215+
<div className="px-4 py-2 text-gray-700">
216+
{drug.usage?.length > 0 ? (
217+
<ul className="list-none pl-0 space-y-1">
218+
{drug.usage.map((item, i) => (
219+
<li key={i}>{item}</li>
220+
))}
221+
</ul>
222+
) : (
223+
<p>정보가 없습니다.</p>
224+
)}
225+
</div>
226+
</div>
227+
228+
{/* 주의사항 및 기타 정보 */}
229+
{precautionKeys.length > 0 && (
230+
<div className="space-y-3">
231+
<h2 className="text-lg font-bold px-4 py-2 text-[#2BA89C] rounded-md border-b border-gray-300 pb-2">
232+
주의사항
233+
</h2>
234+
{precautionKeys.map((key) => (
235+
<div key={key} className="space-y-3">
236+
<h3 className="text-md font-semibold px-4 py-2 text-gray-700">
237+
{key}
238+
</h3>
239+
<div className="px-4 py-2 text-gray-700">
240+
{drug.precaution[key]?.length > 0 ? (
241+
<ul className="list-none pl-0 space-y-1">
242+
{drug.precaution[key].map((item, i) => (
243+
<li key={i}>{item}</li>
244+
))}
245+
</ul>
246+
) : (
247+
<p>정보가 없습니다.</p>
248+
)}
249+
</div>
250+
</div>
251+
))}
252+
</div>
253+
)}
254+
</div>
255+
</div>
256+
</div>
257+
</main>
258+
<Footer />
259+
</div>
260+
);
261+
}

src/app/page.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
'use client';
2-
2+
import dynamic from 'next/dynamic';
33
import { useState, useEffect, useRef } from 'react';
44
import Image from "next/image";
5-
import SearchBar from '../components/SearchBar';
65
import Header from '../components/Header';
76
import Footer from '../components/Footer';
87

8+
const SearchBar = dynamic(() => import('../components/SearchBar'), {
9+
ssr: false,
10+
loading: () => <div>검색창 로딩 중...</div>,
11+
});
12+
13+
914
const MAX_RECENT_SEARCHES = 5; // 최대 저장할 최근 검색어 수
1015

1116
const displayTypes = {

src/app/search/name/page.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use client';
2+
3+
import { Suspense } from 'react';
4+
import SearchPage from '../../../components/SearchPage';
5+
6+
export default function Page() {
7+
return (
8+
<Suspense fallback={<div>Loading...</div>}>
9+
<SearchPage searchType="name" />
10+
</Suspense>
11+
);
12+
}

src/components/NoImage.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,56 @@
1-
const NoImage = () => {
1+
const NoImage = ({ className = "" }) => {
22
return (
3-
<div className="w-full h-full bg-gray-100 rounded-lg flex items-center justify-center">
3+
<div className={`w-full h-full bg-gray-100 rounded-lg flex items-center justify-center ${className}`}>
44
<div className="text-center">
55
<svg
6-
className="mx-auto h-8 w-8 text-gray-400"
6+
className="mx-auto h-12 w-12 text-gray-400"
77
fill="none"
88
viewBox="0 0 24 24"
99
stroke="currentColor"
10+
xmlns="http://www.w3.org/2000/svg"
1011
>
12+
{/* 약병 몸체 */}
13+
<rect
14+
x="7"
15+
y="8"
16+
width="10"
17+
height="12"
18+
rx="1"
19+
strokeWidth="1.5"
20+
stroke="#9CA3AF"
21+
fill="none"
22+
/>
23+
24+
{/* 약병 뚜껑 */}
25+
<path
26+
d="M8 8v-2c0-0.6 0.4-1 1-1h6c0.6 0 1 0.4 1 1v2"
27+
strokeWidth="1.5"
28+
stroke="#9CA3AF"
29+
fill="none"
30+
/>
31+
32+
{/* 약병 라벨 */}
33+
{/* <rect
34+
x="8"
35+
y="10"
36+
width="8"
37+
height="4"
38+
rx="0.5"
39+
strokeWidth="1"
40+
stroke="#9CA3AF"
41+
fill="none"
42+
/> */}
43+
44+
{/* X 표시 - 우측 하단으로 이동 및 크기 확대 */}
1145
<path
1246
strokeLinecap="round"
1347
strokeLinejoin="round"
14-
strokeWidth={2}
15-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
48+
strokeWidth="2"
49+
d="M19 15L13 21M13 15l6 6"
50+
stroke="#EF4444"
1651
/>
1752
</svg>
18-
<p className="mt-1 text-xs text-gray-500">No Image</p>
53+
<p className="mt-1 text-xs text-gray-500">약품 이미지 없음</p>
1954
</div>
2055
</div>
2156
);

0 commit comments

Comments
 (0)