함수형 컴포넌트로 setState 기능을 하는 useState를 사용하기 위해 import.
import React, { useState } from "react";
해당 컴포넌트는 routes.js에서 Router의 자식으로 존재하지 않기때문에 props로 history를 받을 수 없었고,
history를 사용하기 위해 withRouter(HOC)로 컴포넌트를 감싸 props로 history를 받았습니다.
import { withRouter, RouteComponentProps } from "react-router";
import { Link } from "react-router-dom";
해당 컴포넌트에서 받는 data의 타입을 models 폴더에 정의(models 폴더에서 interface 타입 정의, 관리)
타입 정의를 models 폴더에 분리한 이유는 타입 정의한 것에 대해 한번에 파악하기 쉬워 관리하기 쉽고,
유지보수하기에도 편리하기 때문입니다.
import { Props } from "models/Md_EnrollmentProductInfoNotice";
서버와 통신하는 로직을 util/service에 정의하고 해당 컴포넌트에서 import 했습니다.
많은 컴포넌트에서 서버와 통신하는 로직을 필요로하는 데 컴포넌트마다 로직을 작성하면 중복되고,
유지보수 하기 힘들기 때문에 util/service에 정의하여
재사용성과 유지보수를 해결했습니다.
import {
enrollmentTemplate,
getProductInfoNotiFormList,
getToken
} from "util/service";
사용자가 Form에 입력한 값에 대한 유효성 검사하는 함수 import.
import { validateProductInfoTemplateFormInput } from "util/templateValidation";
import config from "../../env-config";
해당 컴포넌트에 필요한 모든 form 요소를 컴포넌트화 했습니다.
import AllForm from "components/AllForm";
import CancleButton from "components/CancleButton";
import ConfirmButton from "components/ConfirmButton";
import "styles/product_info_notice.scss";
import notice2 from "images/ic-notice-red.png";
해당 컴포넌트에서 data를 받기 때문에 data의 타입을 불러와 React.FC<Props>로 타입을 정의했습니다.
React.FC<RouteComponentProps>는 해당 컴포넌트에서 withRouter 그리고 history를 사용하기 위해 타입을 정의했습니다.
해당 컴포넌트에서 props 대신에 {history, data}로 받은 이유는 어떤 매개변수를 받는지 한 눈에 파악할 수 있게 하기 위함입니다.
const EnrollmentProductInfoNoticeTemplate: React.FC<
Props & RouteComponentProps
> = ({ history, data }) => {
템플릿 네임, 변수명 그대로 템플릿 이름을 값으로 받는 변수입니다.
초기 값을 빈 문자열로 준 이유는 해당 변수를 가지고 유효성 체크를 하는데, 이때 값의 길이가 존재해야하고,
타입이 string이기 때문입니다.
빈 문자열을 주지 않으면 undefined로 문제가 발생합니다.
const [templateName, setTemplateName] = useState("");
기본 템플릿, 해당 템플릿을 기본 템플릿으로 설정 하는지 안하는지를 값으로 받는 변수입니다.
기본 값을 false로 줬습니다. 사용자가 체크해야 기본 템플릿으로 설정할 수 있게 하기 위해서 입니다.
const [isDefault, setIsDefault] = useState(false);
상품군 목록이 존재합니다. 상품군 목록을 선택해야만 해당 상품군에 맞는 form들이 사용자에게 표시되기 때문에
상품군을 선택하지 않으면 선택하라는 경고 텍스트를 보여주기 위한 변수입니다.
const [pleaseSelectProduct, setPleaseSelectProduct] = useState(false);
상품군을 선택하면 상품군에 맞는 form data들을 받는 변수입니다.
해당 변수는 AllFrom 컴포넌트에 prop으로 넘겨줍니다.
const [productFormList, setProductFormList] = useState();
변수명을 dataJson으로 한 이유는 data의 타입이 object이고 이 데이터는 서버로부터 받은 상품군 리스트를 map을 돌리고
Object.assign을 사용해 키와 값을 초기화합니다. Object.assign을 사용하기 위해 기본 값으로 빈 객체를 줬습니다.
const [dataJson, setDataJson] = useState({});
AllForm 컴포넌트에서 사용자의 입력 값을 받아 최신화 된 데이터를 받는 변수입니다.
가장 최신화 된 데이터를 받기 때문에 변수명을 getLastDataJson으로 했습니다.
const [getLastDataJson, lastDataJson] = useState();
테플릿 이름의 최대 길이수를 30자로 정한 변수입니다.
고정 값이고 재할당을 하지 않기 때문에 상수로 했습니다.
const TEMPLATENAMEMAXLENGTH = 30;
사용자가 템플릿 이름을 작성할 때 발생하는 이벤트 핸들러입니다.
onChange로부터 event를 받기 때문에 React.ChangeEvent 그리고 <HTMLInputElement> 타입을 정의했습니다.
템플릿 이름의 길이가 30보다 작을 때는 입력한 값이 계속 반영되어야하고, 30와 같아지면 30까지 입력한 값이
반영되어야 하기 때문에 if, else if로 분기처리 했습니다.
const templateNameContent = (event: React.ChangeEvent<HTMLInputElement>) => {
사용자가 입력한 값의 길이가 30자 보다 작을 때 templateName 변수에 입력한 값을 계속 업데이트 해줍니다.
if (event.target.value.length < TEMPLATENAMEMAXLENGTH) {
setTemplateName(event.target.value);
}
반면 사용자가 입력한 값의 길이가 30자와 같아지면 templateName 변수에 최대 30자까지의 값만을 업데이트 해줍니다.
else if (event.target.value.length === TEMPLATENAMEMAXLENGTH) {
setTemplateName(event.target.value.slice(0, TEMPLATENAMEMAXLENGTH));
}
};
const toggleDefaultTemplate = () => {
setIsDefault(!isDefault);
};
상품군 리스트를 클릭하면 발생하는 이벤트 핸들러입니다.
핸들러 내부에 비동기 함수가 있어, 예상치 못한 오작동을 방지하기 위해 async await로 동기화 해줬습니다.
const selectProduct = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const productItemNumber = event.target.value;
이 이벤트 핸들러 함수 안에서 서버와 통신하는 이유는 사용자가 상품군을 클릭했을 때 Form들을 보여줘야하기 때문에
해당 이벤트 핸들러가 실행되면 서버와 통신하게끔 작성했습니다.
await getProductInfoNotiFormList(
`${config.partnerAPI}/product-information-notices/form/${productItemNumber}`,
setProductFormList,
getToken().jwt_token
).then(response => {
if (response.status === 200) {
response.data.map((ele: any) => {
return setDataJson(
서버로 보내야 하는 데이터의 타입이 객체이고
Object.assign을 사용한 이유는 불변성을 유지하기 위해서 그리고 기존 객체에 새로운 키와 값을
추가 하기 위해서 입니다.
Object.assign(dataJson, {
productInformationNoticeNumber:
ele.productInformationNoticeNumber,
[ele.fieldName]: null
})
);
});
}
});
};
사용자가 입력한 값에 대한 이벤트 핸들러는 AllForm 컴포넌트에 존재합니다.
AllForm 컴포넌트에서 dataJson가 업데이트 될 때마다 최신의 dataJson을 가지고 옵니다.
const getLastDataJsonForAllForm = (data: any) => {
lastDataJson(data);
};
const confirmButtonClick = () => {
서버에 보낼 data를 Json 문자열로 변환합니다.
const data = JSON.stringify({
name: templateName,
isDefault: isDefault,
...getLastDataJson
});
if 조건문을 사용하여 사용자가 form에 입력을 제대로 했는지 확인하는 유효성 검사입니다.
필수 입력 항목을 모두 입력했을 때 템플릿이 등록되어야 하기 때문에 이렇게 작성했습니다.
if (validateProductInfoTemplateFormInput(data)) {
const result = enrollmentTemplate(
`${config.partnerAPI}/product-information-notice-templates`,
data,
getToken().jwt_token
);
result.then(response => {
if (response.status === 200) {
정상적으로 등록이 완료되면 상품정보제공고시 템플릿을 관리하면 페이지로 이동합니다.
실패했을 때 이동되면 안되기에 서버에서 주는 response의 상태 값을 체크하여 200일 때 이동되게 작성했습니다.
history.push("partner-templatemanagements-product-info-notice");
}
});
}
};
return (
<>
<div className="product-info-notice-template">
<title>상품정보제공고시 템플릿</title>
<div className="container">
<div className="flex">
<label htmlFor="template-name" className="template-name">
템플릿 이름
<span className="required">(필수)</span>
</label>
<div className="table">
<input
type="text"
id="template-name"
value={templateName}
onChange={templateNameContent}
/>
{templateName.length ? null : (
<div className="notice2">
<img src={notice2} alt="필수 입력 정보 경고 이미지" />
<span>필수 입력 정보입니다.</span>
</div>
)}
<div className="name-length-max">{`${templateName.length}/${TEMPLATENAMEMAXLENGTH}`}</div>
<div className="flex">
<input type="checkbox" id="default-template-select" />
<label
htmlFor="default-template-select"
onClick={toggleDefaultTemplate}
>
<div className={isDefault ? "checked" : "unchecked"}>
기본 템플릿으로 저장
</div>
</label>
</div>
</div>
</div>
<br />
<div className="flex">
<label htmlFor="product-list" className="product-list">
상품군
<span className="required">(필수)</span>
</label>
<div className="table">
<select id="product-list" onChange={selectProduct}>
<option>상품군을 선택해주세요.</option>
{data
? data.map(ele => {
return (
<option
key={ele.number}
data-id={ele.number}
value={ele.number}
>
{ele.title}
</option>
);
})
: null}
</select>
{pleaseSelectProduct ? (
<div className="notice2">
<img src={notice2} alt="필수 입력 정보 경고 이미지" />
<span>상품군을 꼭 선택해주세요.</span>
</div>
) : (
""
)}
</div>
</div>
<br />
특정 상품군에 맞는 Form들을 화면에 보여주기 위해 필요한 컴포넌트입니다.
컴포넌트에 필요한 state들을 prop으로 넘겨줍니다.
이렇게 작성한 이유는 state들을 해당 컴포넌트에서 유지 및 관리하는데 AllForm 컴포넌트에서도 필요하기 때문입니다.
<AllForm
templateName={templateName}
isDefault={isDefault}
productFormList={productFormList}
dataJson={dataJson}
getLastDataJson={getLastDataJsonForAllForm}
/>
<div className="button-box">
<Link to="/partner-templatemanagements-product-info-notice">
<CancleButton />
</Link>
<ConfirmButton confirmClick={confirmButtonClick} />
</div>
</div>
</div>
</>
);
};
EnrollmentProductInfoNoticeTemplate 컴포넌트에 props로 history를 넘겨주기 위해 withRouter를 사용했습니다.
다른 컴포넌트로부터 porps로 history를 받을 수 있다고해도, withRouter로 직관적으로 사용하는게 유지보수 측면이나
가독성 측면에서 좋다고 생각합니다.
export default withRouter(EnrollmentProductInfoNoticeTemplate);