영수증 분석후 수정 화면까지의 흐름도
receipt data의 구조
react-hook-form을사용해야 했던 이유
구현 필요사항과 실제 구현
1. useForm의 기본값 설정
2. 다이내믹한 form 추가 하기
3. validation 설정하기
4. validation에 통과하지 못하면 해당 input의 style을 danger로 변경 (빨간색)
5. 사용자가 금액을 입력할 때마다 총 가격을 계산해서 보여주기 (+품목갯수 업데이트)
영수증 분석후 수정 화면까지의 흐름도
영수증 사진을 올리면
해당 사진을 CLOVA 서비스로 인식 요청을 보내어
품목과 금액, 구매처, 수량 등의 정보를 데이터로 응답을 받는다
응답 받은 값을 query Cache로 저장하고 edit페이지로 path를 이동한다.
query cache에 저장된 값이 있다면 불러와서 form의 defaultValue로 보여주게 된다.
실제 구현화면.
가져온 데이터들은 완벽하지 않고 중량은 영수증에 나오지 않았기 때문에
영수증과 검색api를 활용해 가공한 데이터를 모두 form의 defaultValue로 넣어주어
사용자가 직접 자신의 요구사항에 맞게 수정할 수 있게 해야 했다.
receipt data의 구조
ReceiptInfoType는 최종 영수증의 type이다.
export interface PurchaseReceiptInfoType {
receipt_id: number;
purchase_location: string;
purchase_date: string;
receiptItems: ReceiptDetailType[];
}
export interface ReceiptDetailType {
food_id?: number;
food_image?: string;
food_category?: string;
food_name?: string;
food_weight?: string;
purchase_price?: number;
price_per_amount?: number;
quantity?: number;
registered?: boolean;
}
purchase_location과 purchase_date는 객체 안에,
receiptItems의 키값으로는 ReceiptDetailType의 어레이들이 들어있다.
react-hook-form을사용해야 했던 이유
1) receiptItems의 갯수만큼 기하급수적으로 늘어나는 state
receiptItems의 갯수는
사용자가 구매한 영수증의 품목만큼 늘어나게 된다.
이 많은 것들을 모두 state로 관리해야 한다.
해당 값이 변경될 때마다 화면은 리렌더링되게 된다.
2) 등록 아래의 플러스 버튼을 클릭하면 다이내믹하게 form을 추가해야 한다.
3) 해당 값의 validation을 적용해야 한다.
숫자는 최솟값, string은 최소길이.
모든 값은 빈값으로 들어가지 못하게 막아야 하기 때문이다.
4) validation에 통과하지 못하면 해당 input의 style을 danger로 변경한다. (빨간색)
5) 사용자가 금액을 입력할 때마다 총 가격을 계산해서 보여줘야 한다.
구현 필요사항과 실제 구현
1. useForm의 기본값 설정
영수증 분석에서 온 데이터가 존재하지 않더라도 최소 object의 길이는 1로 설정
const {
control,
handleSubmit,
register,
watch,
formState: { errors },
} = useForm<PurchaseReceiptInfoType>({
defaultValues: {
purchase_location:
(foundReceiptData && foundReceiptData.purchase_location) ||
'구매처 입력란',
purchase_date:
dayjs(foundReceiptData && foundReceiptData.purchase_date).format(
'YY.MM.DD',
) + ' 구매',
receiptItems: foundReceiptData
? foundReceiptData.receiptItems.map((data) => {
return {
food_category: data?.food_category || '',
food_name: data?.food_name || '',
purchase_price: data?.purchase_price || 0,
food_weight: '',
food_id: Math.random() * 4,
quantity: data?.quantity,
registered: false,
};
})
: [
{
food_id: Math.random() * 4,
food_category: undefined,
food_name: undefined,
food_weight: undefined,
purchase_price: undefined,
quantity: undefined,
registered: false,
},
],
},
});
1) 분석된 데이터 존재할 때 :
만일 receiptItems가 영수증 분석에서 온 데이터가 존재한다면 object는 최소 1개 이상이다.
2) 분석된 데이터가 존재하지 않을 때 :
아래와 같은 화면이 나오게 되었다. (/add/receipt/edit)
영수증 분석된 데이터가 없다면,
defaultValue로 아래와 같은 object를 하나 넣어준 결과.
2. 다이내믹한 form 추가 하기
useFieldArray를 활용
const { fields, append, remove } = useFieldArray({
control,
name: 'receiptItems',
});
useForm의 defaultValues에있는 필드 receiptItems에 array를 설정하는 방법이다 .
그리고 실제 element에 아래와같이 register를 하면 된다.
{fields.map((_, index) => (
<div
key={`receipt-${index}`}>
<Input
{...register(`receiptItems.${index}.food_category`)}
/>
input 컴포넌트의 food_category이고 아래는 다른 field들이 등록되어있다.
form 추가는 append로,
form 삭제는 remove로
<MinusSvg onClick={() => remove(index)} />
(생략)
<PlusSvg
className="flex basis-1/12"
onClick={() =>
append({
food_id: Math.random() * 4,
food_category: undefined,
food_name: undefined,
food_weight: undefined,
purchase_price: undefined,
quantity: undefined,
registered: false,
})
}
/>
3. validation 설정하기
{fields.map((_, index) => (
<div
key={`receipt-${index}`}>
<Input
{...register(`receiptItems.${index}.food_category`, {
required: {
value: true,
message: '빈 칸이 없게 작성해주세요',
},
minLength: 1,
})}
/>
4. validation에 통과하지 못하면 해당 input의 style을 danger로 변경 (빨간색)
variant={
errors?.receiptItems?.[index]?.food_weight
? 'danger'
: 'underline'
}
선택적으로 프롭 설정해주기
errors.receiptItems.[index].(속성명)
으로 하면
해당 속성의 validation을 통과하지 못한다면
error가 존재한다.
만일 존재한다면 경고 UI를 보여주고
아니라면 평범한 UI를 보여주도록 구현하였다.
5. 사용자가 금액을 입력할 때마다 총 가격을 계산해서 보여주기 (+품목갯수 업데이트)
만일 이걸 state로 했다면 useState를 몇개를 사용해야 했을까?
객체안의 array로 중첩구조로 사용했다면
사용자가 입력할 때마다 리렌더링이 발생하였을 것이다 (생각만해도... )
react-hook-form은 watch로
form이 변경된 것을 보여준다.
watch는 receiptItems가 변경될 때만 콜백함수를 호출하게 된다.
컴포넌트가 언마운트 될 때
useEffect의 클린업에서 unsubscribe를 호출하여
구독을 해제하게 되고, 이를 통해 메모리 누수를 방지하고
컴포넌트가 언마운트 된 후 상태 업데이트가 시도되는 것을 방지한다.
useEffect(() => {
const { unsubscribe } = watch((value) => {
setLength(value.receiptItems && value.receiptItems.length);
setTotalPrice(
value.receiptItems &&
value.receiptItems.reduce(
(acc: number, cur) => acc + (Number(cur?.purchase_price) || 0),
0,
),
);
});
return () => unsubscribe();
}, [watch]);
'프로젝트' 카테고리의 다른 글
식재료 등록 방법 두가지와 고민한 점 (2) (1) | 2024.03.28 |
---|---|
식재료 등록 방법 두가지와 고민한 점 (1) (0) | 2024.03.28 |
nextjs에서 proxy설정해주기 (feat: next auth 예외처리) (0) | 2024.03.21 |
CLOVA OCR custom api request body 필드 정리 (0) | 2024.03.20 |
react-hook-form과 zod 선택한 이유 (0) | 2024.03.14 |