티스토리 뷰
딥러닝 모델을 통해 사용자의 증상 기록에서 자주 등장하는 키워드 5개를 추출하여 “많이 기록된 키워드”로 보여주는 기능을 만들었다.
처음에는 키워드를 텍스트로 보여주기만 하려고 했는데 해당 키워드로 증상 기록을 필터링할 수 있도록 하면
조금 더 의미있는 기능이 될 것 같아서 시도해보았다.
구현 시 고려해야할 점을 정리해보자면,
1) record.type이 user인지 hospital인지에 따라 보이는 기록의 형태가 다르다.
키워드가 추출되는 부분은
record.type === user 일때 : 증상(description) 부분
record.type === hospital 일때 : 진단결과(diagnosis), 처방내용(prescription), 상세소견(description) 부분
2) 증상 기록 목록은 이미 record.type으로 한번 필터링되도록 구현되어 있다.
아래와 같이 이미 record type에 따라 필터링 된 기록을 한번 더 필터링하도록 만들어야 한다.
3) description 의 경우 사용자의 입력을 그대로 표현해주기 위해 줄바꿈(엔터)이 있는 경우 줄바꿈 처리가 되어야 한다.
구현하기
우선, 줄바꿈 처리를 먼저 진행해주었다.
record.description에 줄바꿈(\n)이 포함되어 있을 때만 줄바꿈(\n)을 기준으로 split하여 p태그로 감싸주어 줄바꿈이 되도록 하였다.
// ChartBox.tsx
// ...
{record.description.includes("\n") ? (
record.description
.split("\n")
.map((ele, idx) => (
<p>
{ele}
</p>
))
) :(
record.description
)}
// ...
현재 모아보기에 따른 필터링은 구현이 완료된 상태이기 때문에 위 ChartBox.tsx에서의 record는
ChartTimeline.tsx에서 모아보기로 필터링된 증상 기록을 map 돌린 record값들이다.
// ChartTimeline.tsx
// ...
const { isLoading } = useQuery<RecordWithImageAndHospital[] | undefined>([RECORDS_READ, position], getApi, {
onSuccess(data) {
setRecords(data);
},
enabled: !!position,
});
useEffect(() => {
queryClient.invalidateQueries([RECORDS_READ, position]);
setFilterItem("all");
setClickedKeyword(null);
}, [position]);
// 서버에서 받아온 신체부위에 따른 증상 기록 리스트
const [records, setRecords] = useState<RecordWithImageAndHospital[] | undefined>();
// 모아보기에 따른 필터링 (전체/증상기록/병원기록)
const [filterItem, setFilterItem] = useState<string>("all");
const [filtredRecord, setFiltredRecord] = useState<RecordWithImageAndHospital[] | undefined>();
const handleRadioChange = (event: any) => {
setFilterItem(event.target.value);
};
useEffect(() => {
setFiltredRecord(records);
}, [records]);
useEffect(() => {
if (filterItem === "all") {
setFiltredRecord(records);
} else {
setFiltredRecord(records?.filter(record => record.type === filterItem));
}
}, [filterItem]);
위처럼 모아보기로 필터링된 `filtredRecord`를 키워드에 따른 필터링을 위해 한번 더 map을 돌려야한다.
5개의 키워드 중 클릭한 키워드 `clickedKeyword`와 그 키워드로 필터링한 record 목록 `filtredRecordByKeyword`을 위한 state를 만들고
const [clickedKeyword, setClickedKeyword] = useRecoilState(selectedKeyword);
const [filtredRecordByKeyword, setFiltredRecordByKeyword] = useState<RecordWithImageAndHospital[] | undefined>();
클릭한 키워드가 없을 때는 `filtredRecordByKeyword`에 모아보기로만 필터링한 `filtredRecord`를,
클릭한 키워드가 있을 때는 해당 키워드로 `description`, `diagnosis`, `prescription`를 필터링해서
`filtredRecordByKeyword`에 담아준다.
useEffect(() => {
if (clickedKeyword) {
setFiltredRecordByKeyword(
filtredRecord?.filter(
record =>
record.description.includes(clickedKeyword) ||
record.diagnosis?.includes(clickedKeyword) ||
record.prescription?.includes(clickedKeyword),
),
);
} else {
setFiltredRecordByKeyword(filtredRecord);
}
}, [clickedKeyword, filtredRecord]);
그리고 filtredRecordByKeyword로 map을 돌려 ChartBox로 record값을 내려주는데 이 때 기록이 없는 경우에는 기록이 없다는 텍스트를 보여준다.
//...
{filtredRecordByKeyword?.length === 0 ? (
<NoRecord>
<img src={ToriQuestion.src} />
<p>
<strong>{KoreanPosition[position!]}</strong>에 대한 기록이 없습니다
</p>
</NoRecord>
) : !query.position ? (
<NoRecord>
<img src={ToriQuestion.src} />
<p>자세한 기록을 확인하고 싶은 부위를 선택해주세요</p>
</NoRecord>
) : isLoading ? (
<RecordSkeleton />
) : (
filtredRecordByKeyword?.map((record, index) => (
<ChartBox
index={index}
record={record} // 모아보기, 키워드로 필터링된 record
clickedKeyword={clickedKeyword} // 클릭한 키워드(string)
patientId={patientId}
position={position}
setShowModal={setShowModal}
/>
))
)}
그럼 여기까지 키워드로 필터링된 record가 줄바꿈처리되어 보여지는 것까지 구현하였다.
이제 키워드를 클릭했을 때 해당 키워드에 하이라이팅 효과를 넣어야한다.
해당 키워드만 span태그로 감싸서 따로 class를 통해 style을 부여하는 방법이 가장 간단할 것 같아 그렇게 진행해보기로 했다.
줄바꿈(\n)으로 split된 문장에 클릭한 키워드clickedKeyword가 포함되었다면 clickedKeyword로 한번 더 split하여 map을 돌린다.
이미 줄바꿈(\n)으로 map을 돌리는 코드가 있어서 그 안에 또 다시 map을 사용하니 코드가 너무 복잡하고 읽기 힘들어
키워드를 하이라이팅하는 코드는SplitTextByKeyword.tsx로 따로 빼내 작성하였다.
// ChartBox.tsx
// ...
<div>
{record.description.includes("\n") ? (
record.description
.split("\n")
.map((ele, idx) => (
<p key={`${ele} + ${idx} + ${Date.now()}`}>
{clickedKeyword && ele.includes(clickedKeyword) ? (
<SplitTextByKeyword text={ele} clickedKeyword={clickedKeyword} />
) : (
ele
)}
</p>
))
) : clickedKeyword && record.description.includes(clickedKeyword) ? (
<SplitTextByKeyword text={record.description} clickedKeyword={clickedKeyword} />
) : (
record.description
)}
</div>
clickedKeyword로 split한 문자열의 경우 span으로 감싸고 해당 키워드가 있는 부분만 class를 준 span으로 한번 더 감싸주었다.
// SplitTextByKeyword.tsx
// ...
<>
{text.split(clickedKeyword).map((splitText, idx, arr) =>
idx === arr.length - 1 ? (
<span>{splitText}</span>
) : (
<span>
{splitText}
<span className="keyword-mark">{clickedKeyword}</span>
</span>
),
)}
</>
description, diagnosis, prescription 모두 동일하게 작성하였고 정상적으로 작동하였다.
구현이 완료된 동작
리팩토링하고 싶은 부분
증상 기록(record)에 대한 필터링도 이중으로 들어가고, 증상 설명(record.description)에 대해 줄바꿈으로도 split하고
키워드로도 split을 해야하다보니 map을 많이 사용하게 되어 코드가 알아보기도 힘들고 지저분해졌다.
split이나 filter말고 더 깔끔한 다른 방법이 있는지 찾아봐야겠다.
그리고 줄바꿈 처리와 키워드 하이라이팅 로직이 description, diagnosis, prescription에 동일하게 반복되는데
지금은 이부분을 반복적으로 나열했지만 로직을 따로 빼내서 불필요한 반복이 줄어들도록 수정이 필요하다.
프로젝트를 진행하면서는 우선 기능의 완성을 목적이고, 중간중간 기능의 디테일한 부분이 바뀌는 경우가 있어서
코드가 남이 알아보기 힘들게 되는 경우가 많은데 팀원들과 시간이 된다면 깔끔하게 리팩토링해보고 싶다.
'사이드 프로젝트 > 🩺 바디토리 : 건강기록 및 병원추천 웹서비스' 카테고리의 다른 글
바디토리 서비스 기획 과정이 궁금하다면? (0) | 2023.03.31 |
---|---|
토리놈... (0) | 2022.12.14 |
svgr과 lottie를 함께 사용하니 발생했던 이슈 (0) | 2022.12.13 |
대표 캐릭터 만들고 lottie로 구현하기 (0) | 2022.12.09 |
hydration error는 뭐길래 자꾸 발생하나 (0) | 2022.12.08 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그