Skip to content

Commit f0c665c

Browse files
authored
feat(FE): 의과 약전 컴포넌트 구현 (#142)
* chore: types/react, types/react-dom dependencies 업데이트 * feat: CategoryFilter 컴포넌트 구현 * feat: SearchFilter 컴포넌트 구현 * feat: medicine-search-option 상수 정의 * feat: ExpandableTableRow 컴포넌트 구현 * feat: PharmacopoeiaDialog 컴포넌트 구현
1 parent cbd91da commit f0c665c

File tree

11 files changed

+739
-18
lines changed

11 files changed

+739
-18
lines changed

frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"@eslint/js": "^9.15.0",
3434
"@tailwindcss/typography": "^0.5.15",
3535
"@types/js-cookie": "^3.0.6",
36-
"@types/react": "^18.3.12",
37-
"@types/react-dom": "^18.3.1",
36+
"@types/react": "^19.0.7",
37+
"@types/react-dom": "^19.0.3",
3838
"@vitejs/plugin-react": "^4.3.4",
3939
"autoprefixer": "^10.4.20",
4040
"eslint": "^9.15.0",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Button, TableCell, TableRow } from '@freemed-kit/ui';
2+
import React, { useState, useRef, useEffect } from 'react';
3+
import { ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
4+
5+
interface ExpandableTableCellProps {
6+
content: React.ReactNode;
7+
isExpanded: boolean;
8+
}
9+
10+
interface ExpandableTableRowProps {
11+
contents: string[];
12+
onRowClick: () => void;
13+
}
14+
15+
const ExpandableTableCell = React.forwardRef<
16+
HTMLDivElement,
17+
ExpandableTableCellProps
18+
>(({ content, isExpanded }, ref) => {
19+
return (
20+
<TableCell className="p-3">
21+
<div ref={ref} className={isExpanded ? '' : 'line-clamp-4'}>
22+
{content}
23+
</div>
24+
</TableCell>
25+
);
26+
});
27+
28+
ExpandableTableCell.displayName = 'ExpandableTableCell';
29+
30+
const ExpandableTableRow = ({
31+
contents,
32+
onRowClick,
33+
}: ExpandableTableRowProps) => {
34+
const [isExpanded, setIsExpanded] = useState(false);
35+
const [hasTextOverflow, setHasTextOverflow] = useState(false);
36+
const cellElementRefs = useRef<(HTMLDivElement | null)[]>([]);
37+
38+
useEffect(() => {
39+
cellElementRefs.current.forEach((element) => {
40+
if (!element) return;
41+
const { scrollHeight, clientHeight } = element;
42+
if (scrollHeight > clientHeight) {
43+
setHasTextOverflow(true);
44+
}
45+
});
46+
}, [contents]);
47+
48+
return (
49+
<TableRow className="whitespace-pre-wrap" onClick={onRowClick}>
50+
{contents.map((content, index) => (
51+
<ExpandableTableCell
52+
key={index}
53+
ref={(el) => {
54+
cellElementRefs.current[index] = el;
55+
}}
56+
isExpanded={isExpanded}
57+
content={content}
58+
/>
59+
))}
60+
<TableCell className="p-0">
61+
{hasTextOverflow && (
62+
<Button
63+
variant="ghost"
64+
size="icon"
65+
className={`size-7 text-muted-foreground transition-transform duration-300 hover:bg-transparent ${isExpanded ? 'rotate-180' : ''}`}
66+
onClick={(e) => {
67+
e.stopPropagation();
68+
setIsExpanded((prev) => !prev);
69+
}}
70+
>
71+
{isExpanded ? <ChevronsDownUp /> : <ChevronsUpDown />}
72+
</Button>
73+
)}
74+
</TableCell>
75+
</TableRow>
76+
);
77+
};
78+
79+
export default ExpandableTableRow;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export const M_MEDICINE_SEARCH_OPTION = {
2+
NAME: { label: '약품명', value: 'name' },
3+
INGREDIENT: { label: '성분명', value: 'ingredient' },
4+
} as const;
5+
6+
export const M_MEDICINE_SEARCH_OPTIONS = Object.values(
7+
M_MEDICINE_SEARCH_OPTION,
8+
);
9+
10+
export const KM_MEDICINE_SEARCH_OPTION = {
11+
NAME: { label: '약품명', value: 'name' },
12+
INDICATION: { label: '적응증', value: 'indication' },
13+
} as const;
14+
15+
export const KM_MEDICINE_SEARCH_OPTIONS = Object.values(
16+
KM_MEDICINE_SEARCH_OPTION,
17+
);

frontend/src/constants/mock-data.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,149 @@ export const patientMemo = {
7979
writer: '의사 선생님',
8080
updatedAt: '2025-01-01T12:00:00',
8181
};
82+
83+
export const mMedicineCategories = [
84+
{
85+
mainCategory: '해열, 진통, 소염제',
86+
subCategories: [
87+
{
88+
id: 1,
89+
subCategory: 'NSAIDs',
90+
},
91+
{
92+
id: 2,
93+
subCategory: 'AAP',
94+
},
95+
{
96+
id: 3,
97+
subCategory: '중추성진통제',
98+
},
99+
],
100+
},
101+
{
102+
mainCategory: '골격근 이완제',
103+
subCategories: [
104+
{
105+
id: 4,
106+
subCategory: '골격근 이완제',
107+
},
108+
],
109+
},
110+
{
111+
mainCategory: '비타민',
112+
subCategories: [
113+
{
114+
id: 5,
115+
subCategory: '비타민',
116+
},
117+
],
118+
},
119+
{
120+
mainCategory: '중추신경용약',
121+
subCategories: [
122+
{
123+
id: 6,
124+
subCategory: '신경통약',
125+
},
126+
],
127+
},
128+
{
129+
mainCategory: '항고지혈증제',
130+
subCategories: [
131+
{
132+
id: 7,
133+
subCategory: 'HMG-CoA Reductase Inhibitor',
134+
},
135+
],
136+
},
137+
{
138+
mainCategory: '혈당조절제',
139+
subCategories: [
140+
{
141+
id: 8,
142+
subCategory: '인슐린분비촉진제',
143+
},
144+
{
145+
id: 9,
146+
subCategory: '인슐린작용증강제',
147+
},
148+
{
149+
id: 10,
150+
subCategory: 'SGLT2 저해제',
151+
},
152+
{
153+
id: 11,
154+
subCategory: '복합제',
155+
},
156+
],
157+
},
158+
{
159+
mainCategory: '항고지혈증제+혈당조절제',
160+
subCategories: [
161+
{
162+
id: 12,
163+
subCategory: 'HMG-CoA Reductase Inhibitor + 인슐린작용증강제',
164+
},
165+
],
166+
},
167+
{
168+
mainCategory: '항히스타민제, 호흡기질환제제',
169+
subCategories: [
170+
{
171+
id: 13,
172+
subCategory: '항히스타민제',
173+
},
174+
{
175+
id: 14,
176+
subCategory: '복합제제',
177+
},
178+
{
179+
id: 15,
180+
subCategory: '진해제',
181+
},
182+
{
183+
id: 16,
184+
subCategory: '거담제',
185+
},
186+
],
187+
},
188+
];
189+
190+
export const mMedicines = [
191+
{
192+
id: 1,
193+
name: '에스부펜정',
194+
ingredient: 'Dexibuprofen 300mg',
195+
dosage:
196+
'성인 : 1회 300 mg을 1일 2~4회 경구투여.\r\n단, 1일 1200mg을 초과하지 않는다.',
197+
efficacy:
198+
'1. 만성 다발성 관절염, 류마티스관절염\r\n2. 관절증\r\n3. 강직척추염\r\n4. 외상 및 수술 후 통증성 부종 또는 염증\r\n5. 염증, 통증 및 발열을 수반하는 감염증의 치료보조',
199+
mainCategory: '해열, 진통, 소염제',
200+
subCategory: 'NSAIDs',
201+
isExcluded: false,
202+
},
203+
{
204+
id: 2,
205+
name: '누코미트캡슐200mg',
206+
ingredient: 'Acetylcysteine 200mg',
207+
dosage:
208+
'식전에 소량의 물과 함께 복용한다.\r\n1. 급성질환\r\n성인 : 1회 200 mg 1일 3회\r\n소아 : 6~14세 1회 200 mg 1일 1회\r\n2. 만성질환\r\n성인 : 1회 200 mg 1일 2회\r\n소아 : 6~14세 1회 100 mg 1일 3회\r\n3. 낭성섬유증\r\n소아 : 6세 이상 1회 200 mg 1일 3회',
209+
efficacy:
210+
'다음 질환에서의 객담배출곤란 : 급.만성기관지염, 기관지천식, 후두염, 부비동염, 낭성섬유증',
211+
mainCategory: '항히스타민제&호흡기질환제제',
212+
subCategory: '거담제',
213+
isExcluded: false,
214+
},
215+
{
216+
id: 3,
217+
name: '유한뎬탈케어 가글 프로 마일드',
218+
ingredient: 'Allantoin 0.2g Sodium Fluoride',
219+
dosage:
220+
'성인 : 1회 300 mg을 1일 2~4회 경구투여. 단, 1일 1200mg을 초과하지 않는다.',
221+
efficacy:
222+
'1. 만성 다발성 관절염, 류마티스관절염\n4. 외상 및 수술 후 통증성 부종 또는 염증\n5. 염증, 통증 및 발열을 수반하는 감염증의 치료보조',
223+
mainCategory: '해열, 진통, 소염제',
224+
subCategory: 'NSAIDs',
225+
isExcluded: true,
226+
},
227+
];
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
Popover,
3+
PopoverTrigger,
4+
Button,
5+
PopoverContent,
6+
Command,
7+
CommandInput,
8+
CommandList,
9+
CommandEmpty,
10+
CommandGroup,
11+
CommandItem,
12+
} from '@freemed-kit/ui';
13+
import { ChevronDown, Check } from 'lucide-react';
14+
import { cn } from '@/utils/cn';
15+
import useCategoryFilter from '@/features/m/shared/category-filter/useCategoryFilter';
16+
17+
export interface CategoryFilterProps {
18+
categoryId: number | undefined;
19+
onSelect: (categoryId: number) => void;
20+
}
21+
22+
const CategoryFilter = ({ categoryId, onSelect }: CategoryFilterProps) => {
23+
const {
24+
categories,
25+
selectedCategory,
26+
isMainCategoryOpen,
27+
isSubCategoryOpen,
28+
setIsMainCategoryOpen,
29+
setIsSubCategoryOpen,
30+
handleMainCategorySelect,
31+
handleSubCategorySelect,
32+
} = useCategoryFilter({ categoryId, onSelect });
33+
34+
return (
35+
<div className="flex space-x-2">
36+
<Popover open={isMainCategoryOpen} onOpenChange={setIsMainCategoryOpen}>
37+
<PopoverTrigger asChild>
38+
<Button
39+
variant="outline"
40+
role="combobox"
41+
aria-expanded={isMainCategoryOpen}
42+
className="w-60 justify-between"
43+
>
44+
<span className="truncate">
45+
{selectedCategory.mainCategory || '대분류'}
46+
</span>
47+
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
48+
</Button>
49+
</PopoverTrigger>
50+
<PopoverContent className="w-60 p-0">
51+
<Command>
52+
<CommandInput placeholder="대분류 검색" />
53+
<CommandList>
54+
<CommandEmpty>No category found.</CommandEmpty>
55+
<CommandGroup>
56+
{categories.map((category) => (
57+
<CommandItem
58+
key={category.mainCategory}
59+
value={category.mainCategory}
60+
onSelect={() =>
61+
handleMainCategorySelect(category.subCategories[0].id)
62+
}
63+
>
64+
<Check
65+
className={cn(
66+
'mr-2 h-4 w-4',
67+
category.mainCategory === selectedCategory.mainCategory
68+
? 'opacity-100'
69+
: 'opacity-0',
70+
)}
71+
/>
72+
{category.mainCategory}
73+
</CommandItem>
74+
))}
75+
</CommandGroup>
76+
</CommandList>
77+
</Command>
78+
</PopoverContent>
79+
</Popover>
80+
<Popover open={isSubCategoryOpen} onOpenChange={setIsSubCategoryOpen}>
81+
<PopoverTrigger asChild>
82+
<Button
83+
variant="outline"
84+
role="combobox"
85+
aria-expanded={isSubCategoryOpen}
86+
className="w-60 justify-between"
87+
>
88+
<span className="truncate">
89+
{selectedCategory.subCategory || '소분류'}
90+
</span>
91+
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
92+
</Button>
93+
</PopoverTrigger>
94+
<PopoverContent className="w-60 p-0">
95+
<Command>
96+
<CommandInput placeholder="소분류 검색" />
97+
<CommandList>
98+
<CommandEmpty>No category found.</CommandEmpty>
99+
<CommandGroup>
100+
{selectedCategory.subCategories.map((category) => (
101+
<CommandItem
102+
key={category.id}
103+
value={category.subCategory}
104+
onSelect={() => handleSubCategorySelect(category.id)}
105+
>
106+
<Check
107+
className={cn(
108+
'mr-2 h-4 w-4',
109+
selectedCategory.subCategory === category.subCategory
110+
? 'opacity-100'
111+
: 'opacity-0',
112+
)}
113+
/>
114+
{category.subCategory}
115+
</CommandItem>
116+
))}
117+
</CommandGroup>
118+
</CommandList>
119+
</Command>
120+
</PopoverContent>
121+
</Popover>
122+
</div>
123+
);
124+
};
125+
126+
export default CategoryFilter;

0 commit comments

Comments
 (0)