[Project] 직원상세정보(EmpDetail) 컴포넌트 리팩토링 하기

이슬기·2024년 3월 6일
0

project

목록 보기
40/42

구현을 하다보니 직원상세정보 코드가 너무 길고 중복되는 부분이 많았다.
크게 수정해야 할 부분을 살펴보면,

  1. 컴포넌트 분할: 코드를 더 작은 컴포넌트로 분할하여 각 컴포넌트가 한 가지 기능을 수행하도록 만듭니다. 이것은 가독성을 높이고 재사용성을 증가시키는 데 도움이 됩니다.
  1. 코드 중복 제거: 반복되는 코드를 함수로 추출하여 중복을 제거하고 코드를 간결하게 만듭니다.

위 사항들을 수정하면 코드를 좀 더 간결하게 만들 수 있다고 분석했다.

기존 코드

import React, { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styles from './empDetailInfo.module.css';
import { getEmpList, saveEmpDetails, setDetail } from '../../redux/empInfosSlice';
import { Col, Row } from 'antd';
import { DeptNameDB } from '../../services/api/empCreateApi';
import styled from 'styled-components';
import EmpUploadImg from './EmpUploadImg';
import { JobListDB } from '../../services/api/deptApi';

const EmpDetail = () => {
  const dispatch = useDispatch();
  const selectedEmployee = useSelector(state => state.empInfos.selectedEmployee);
  const memoSelectedEmployee = useMemo(() => selectedEmployee || {}, [selectedEmployee]);
  const [editing, setEditing] = useState(false); // 수정 모드 여부를 관리하는 state
  const [updatedEmployee, setUpdatedEmployee] = useState(memoSelectedEmployee); // 수정된 직원 정보를 관리하는 state
  const [originalEmployee, setOriginalEmployee] = useState(memoSelectedEmployee); // 원래의 직원 정보를 관리하는 state
  const [dept, setDept] = useState([]);
  const [job, setJob] = useState([]);
  const [e_password, setPassword] = useState("");
  
  const deptCd = dept.find(item => item.CD_VALUE === updatedEmployee.DEPT_NAME)?.CD;
  const empData = useSelector((state) => state.userInfoSlice);

  useEffect(() => {
    // 선택된 직원 정보가 변경되면 해당 정보로 state 업데이트
    setUpdatedEmployee(prevEmployee => {
      if (prevEmployee !== memoSelectedEmployee) {
        return memoSelectedEmployee;
      }
      return prevEmployee;
    });
    setOriginalEmployee(memoSelectedEmployee);
  }, [memoSelectedEmployee]);

  useEffect(() => {
    // 컴포넌트가 마운트될 때 한 번 부서 정보를 가져오도록 설정
    deptName();
    if (updatedEmployee.DEPT_NAME) {
      deptJob(); // 부서가 선택되면 직종 데이터 가져오기
    }
  }, [deptCd]);

    const deptName = () => {
    console.log("deptName");
    DeptNameDB()
      .then((response) => {
        setDept(response);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  // deptName에 있는 CD와 같은 CD 가져오기
  const deptJob = () => {
    console.log("deptJob");
    const data = {
      CD : deptCd,
      MOD_ID: empData.e_no
    }
    console.log(data);
    JobListDB(data)
      .then((response) => {
        setJob(response.data);
      })
      .catch((error) => {
        console.log(error);
      })
  } 

  const passwordGenerate = () => {
    const chars =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*";
    let randomStr = "";
    for (let i = 0; i < 10; i++) {
      let randomIndex = Math.floor(Math.random() * chars.length);
      randomStr += chars[randomIndex];
    }
    console.log(randomStr);
    setPassword(randomStr);
    setUpdatedEmployee(prevState => ({
      ...prevState,
      E_PASSWORD: randomStr // E_PASSWORD 항목만 업데이트
    }))
  };

  const handleEdit = () => {
    setEditing(true); // 수정 버튼을 누를 때 수정 모드 활성화
  };

  const handleInputChange = (e) => { // 입력 필드 값 변경 시 상태 업데이트
    const { name, value } = e.target;
    setUpdatedEmployee(prevState => ({
      ...prevState, [name]: value
    }));
  };

  const handleCancel = () => {
    setEditing(false); // 수정 취소 시 수정 모드 비활성화
    setUpdatedEmployee(originalEmployee); // 수정 취소 시 이전 상태로 되돌림
  };

  const handleImageUrlChange = (imageUrl) => {
    // 이미지 URL을 updatedEmployee에 추가
    setUpdatedEmployee(prevState => ({
      ...prevState,
      E_PROFILE: imageUrl
    }));
  }

  // 수정된 직원 정보 저장 후, 전체 직원 목록을 다시 가져옴
  const handleSaveChanges = () => {
    dispatch(saveEmpDetails(updatedEmployee)) // 수정된 직원 정보 저장
      .then(() => {
        dispatch(setDetail(updatedEmployee)); // 리덕스 스토어에서 선택된 직원 정보 업데이트
        setEditing(false); // 저장 후 수정 모드 비활성화
        dispatch(getEmpList()); // 저장 후 전체 목록 갱신

        // 수정된 직원 정보를 UI에 반영하기 위해 상태 업데이트
        setOriginalEmployee(updatedEmployee);
      })
      .catch(error => {
        console.error('Error saving employee details: ', error);
      });
  }

  const renderInputField = ({ label, name, type, options }, index) => {
    if (name === 'E_PASSWORD') {
      return (
        <div className={styles.empInfoItem} key={name}>
          <div className={styles.label}>{label}</div>
          <div className={styles.selectContainer}>
            <input
              className={styles.inputFields}
              type="password"
              value={updatedEmployee[name] || ''} // updatedEmployee의 비밀번호 값으로 설정
              onChange={handleInputChange} // 입력 필드가 변경되면 상태를 업데이트
              readOnly={!editing}
              name={name}
            />
            <MyButton type="button" onClick={passwordGenerate}>
              임시비밀번호재발급
            </MyButton>
          </div>
          {index !== inputFields.length - 1 && <div className={styles.divider} />}
        </div>
      );
    } else {
      return (
        <div className={styles.empInfoItem} key={name}>
          <div className={styles.label}>{label}</div>
          <div className={styles.selectContainer}>
            {type === 'select' ? (
              <select
                className={styles.selectBox}
                value={updatedEmployee[name] || ''}
                onChange={handleInputChange}
                disabled={!editing}
                name={name}
              >
                {name === 'DEPT_NAME' ? (
                  // 부서 선택 옵션을 동적으로 가져오기
                  dept.map((item, index) => (
                    <option key={index} value={item.CD_VALUE}>{item.CD_VALUE}</option>
                  ))
                ) : name === 'E_OCCUP' ? (
                  // 직종 선택 옵션을 동적으로 가져오기
                  job.map((item, index) => (
                    <option key={index} value={item.CD_VALUE}>{item.CD_VALUE}</option>
                  ))
                ) : (
                  // 기존의 옵션들은 그대로 사용
                  options.map((option, index) => (
                    <option key={index} value={option}>{option}</option>
                  ))
                )}
              </select>
            ) : (
              <input
                className={styles.inputFields}
                type={type}
                value={updatedEmployee[name] || ''}
                onChange={handleInputChange}
                readOnly={!editing}
                name={name}
              />
            )}
          </div>
          {index !== inputFields.length - 1 && <div className={styles.divider} />}
        </div>
      );
    }
  };
  
  const inputFields = [
    { label: '사원명', name: 'E_NAME', type: 'text' },
    { label: '성별', name: 'E_GENDER', type: 'select', options: ['남', '여'] },
    { label: '생년월일', name: 'E_BIRTH', type: 'date' },
    { label: '사원번호', name: 'E_NO', type: 'text' },
    { label: '입사일', name: 'E_HIREDATE', type: 'date' },
    { label: '퇴사일', name: 'E_ENDDATE', type: 'date' },
    { label: '연락처', name: 'E_PHONE', type: 'text' },
    { label: '이메일', name: 'E_EMAIL', type: 'text' },
    { label: '주소', name: 'E_ADDRESS', type: 'text' },
    { label: '부서', name: 'DEPT_NAME', type: 'select', options: dept.map(item => item.CD_VALUE) },
    { label: '비밀번호', name: 'E_PASSWORD', type: 'password' },
    { label: '권한', name: 'E_AUTH', type: 'select', options: ['ADMIN', 'USERA', 'USERB'] },
    { label: '현황', name: 'E_STATUS', type: 'select', options: ['재직', '휴직', '퇴직'] },
    { label: '직종', name: 'E_OCCUP', type: 'select', options: job.map(item => item.CD_VALUE) },
    { label: '직급', name: 'E_RANK', type: 'select', options: ['시설장', '팀장', '사원'] },
  ];
  
  return (
    <div className={styles.empDetailInfo}>
      <h5>직원 상세 정보</h5>     
      <div className={styles.empInfoWrap}>
        <div className={styles.empPicture}>
          <div className={styles.imgSquare}>
            {updatedEmployee.E_PROFILE ? (
              <img src={updatedEmployee.E_PROFILE} alt="프로필 사진" />
            ) : (
              <span>프로필 사진이 없습니다.</span>
            )}
          </div>
          <div className={styles.imgSaveButton}>
            { editing && <EmpUploadImg imageUrlChange={handleImageUrlChange}/> }
          </div>
        </div>
        {inputFields.map(renderInputField)}
      </div>
    <Row style={{ marginBottom: "10px" }}>
      <Col md={15}></Col>
      <Col md={9}>
      <div className="col-11" style={{ display: "flex", justifyContent: "flex-end" }}>
        <div style={{ marginRight: "10px" }}>
          {editing ? (
            <button className={styles.empSaveButton2} onClick={handleSaveChanges}>저장</button>
          ) : (
            <button className={styles.empSaveButton1} onClick={handleEdit}>수정</button>
          )}
        </div>
        {editing && (
          <button className={styles.empSaveButton3} onClick={handleCancel}>취소</button>
        )}
      </div>
      </Col>
    </Row>
  </div>
  );
};

export default EmpDetail;

const MyButton = styled.button`
  margin-top: 35px;
  border-radius: 5px;
  background-color: grey;
  color: white;
`;

1차 리팩토링 코드

크게 수정한 부분은 컴포넌트 분리이다.

  1. InputField 컴포넌트를 도입하여 입력 필드 관련 코드를 단순화하고 중복을 제거
  2. 불필요한 상태 관리 및 중복 코드를 제거하여 코드 간결화

두 가지 방법을 사용하여 리팩토링 했는데 그 중 inputField 부분이 많은 자리를 차지하기 때문에 따로 컴포넌트를 분리하였다.

EmpDetail.jsx

import React, { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styles from './empDetailInfo.module.css';
import { getEmpList, saveEmpDetails, setDetail } from '../../redux/empInfosSlice';
import { Col, Row } from 'antd';
import { DeptNameDB } from '../../services/api/empCreateApi';
import EmpUploadImg from './EmpUploadImg';
import { JobListDB } from '../../services/api/deptApi';
import EmpDetailInputField from './EmpDetailInputField';

const EmpDetail = () => {
  const dispatch = useDispatch();
  const selectedEmployee = useSelector(state => state.empInfos.selectedEmployee);
  const memoSelectedEmployee = useMemo(() => selectedEmployee || {}, [selectedEmployee]);
  const [editing, setEditing] = useState(false); // 수정 모드 여부를 관리하는 state
  const [updatedEmployee, setUpdatedEmployee] = useState(memoSelectedEmployee); // 수정된 직원 정보를 관리하는 state
  const [originalEmployee, setOriginalEmployee] = useState(memoSelectedEmployee); // 원래의 직원 정보를 관리하는 state
  const [dept, setDept] = useState([]);
  const [job, setJob] = useState([]);
  const [e_password, setPassword] = useState("");
  
  const deptCd = dept.find(item => item.CD_VALUE === updatedEmployee.DEPT_NAME)?.CD;
  const empData = useSelector((state) => state.userInfoSlice);

  useEffect(() => {
    // 선택된 직원 정보가 변경되면 해당 정보로 state 업데이트
    setUpdatedEmployee(prevEmployee => {
      if (prevEmployee !== memoSelectedEmployee) {
        return memoSelectedEmployee;
      }
      return prevEmployee;
    });
    setOriginalEmployee(memoSelectedEmployee);
  }, [memoSelectedEmployee]);

  useEffect(() => {
    // 컴포넌트가 마운트될 때 한 번 부서 정보를 가져오도록 설정
    deptName();
    if (updatedEmployee.DEPT_NAME) {
      deptJob(); // 부서가 선택되면 직종 데이터 가져오기
    }
  }, [deptCd]);

  const deptName = () => {
    DeptNameDB()
      .then((response) => {
        setDept(response);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const deptJob = () => {
    const data = {
      CD : deptCd,
      MOD_ID: empData.e_no
    }
    JobListDB(data)
      .then((response) => {
        setJob(response.data);
      })
      .catch((error) => {
        console.log(error);
      })
  } 

  const passwordGenerate = () => {
    const chars =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*";
    let randomStr = "";
    for (let i = 0; i < 10; i++) {
      let randomIndex = Math.floor(Math.random() * chars.length);
      randomStr += chars[randomIndex];
    }
    setPassword(randomStr);
    setUpdatedEmployee(prevState => ({
      ...prevState,
      E_PASSWORD: randomStr // E_PASSWORD 항목만 업데이트
    }))
  };

  const handleEdit = () => {
    setEditing(true); // 수정 버튼을 누를 때 수정 모드 활성화
  };

  const handleInputChange = (e) => { // 입력 필드 값 변경 시 상태 업데이트
    const { name, value } = e.target;
    setUpdatedEmployee(prevState => ({
      ...prevState, [name]: value
    }));
  };

  const handleCancel = () => {
    setEditing(false); // 수정 취소 시 수정 모드 비활성화
    setUpdatedEmployee(originalEmployee); // 수정 취소 시 이전 상태로 되돌림
  };

  const handleImageUrlChange = (imageUrl) => {
    // 이미지 URL을 updatedEmployee에 추가
    setUpdatedEmployee(prevState => ({
      ...prevState,
      E_PROFILE: imageUrl
    }));
  }

  // 수정된 직원 정보 저장 후, 전체 직원 목록을 다시 가져옴
  const handleSaveChanges = () => {
    dispatch(saveEmpDetails(updatedEmployee)) // 수정된 직원 정보 저장
      .then(() => {
        dispatch(setDetail(updatedEmployee)); // 리덕스 스토어에서 선택된 직원 정보 업데이트
        setEditing(false); // 저장 후 수정 모드 비활성화
        dispatch(getEmpList()); // 저장 후 전체 목록 갱신
        // 수정된 직원 정보를 UI에 반영하기 위해 상태 업데이트
        setOriginalEmployee(updatedEmployee);
      })
      .catch(error => {
        console.error('Error saving employee details: ', error);
      });
  }
  
  const inputFields = [
    { label: '사원명', name: 'E_NAME', type: 'text' },
    { label: '성별', name: 'E_GENDER', type: 'select', options: ['남', '여'] },
    { label: '생년월일', name: 'E_BIRTH', type: 'date' },
    { label: '사원번호', name: 'E_NO', type: 'text' },
    { label: '입사일', name: 'E_HIREDATE', type: 'date' },
    { label: '퇴사일', name: 'E_ENDDATE', type: 'date' },
    { label: '연락처', name: 'E_PHONE', type: 'text' },
    { label: '이메일', name: 'E_EMAIL', type: 'text' },
    { label: '주소', name: 'E_ADDRESS', type: 'text' },
    { label: '부서', name: 'DEPT_NAME', type: 'select', options: dept.map(item => item.CD_VALUE) },
    { label: '비밀번호', name: 'E_PASSWORD', type: 'text' },
    { label: '권한', name: 'E_AUTH', type: 'select', options: ['ADMIN', 'USERA', 'USERB'] },
    { label: '현황', name: 'E_STATUS', type: 'select', options: ['재직', '휴직', '퇴직'] },
    { label: '직종', name: 'E_OCCUP', type: 'select', options: job.map(item => item.CD_VALUE) },
    { label: '직급', name: 'E_RANK', type: 'select', options: ['시설장', '팀장', '사원'] },
  ];
  
  return (
    <div className={styles.empDetailInfo}>
      <h5>직원 상세 정보</h5>     
      <div className={styles.empInfoWrap}>
        <div className={styles.empPicture}>
          <div className={styles.imgSquare}>
            {updatedEmployee.E_PROFILE ? (
              <img src={updatedEmployee.E_PROFILE} alt="프로필 사진" />
            ) : (
              <span>프로필 사진이 없습니다.</span>
            )}
          </div>
          <div className={styles.imgSaveButton}>
            { editing && <EmpUploadImg imageUrlChange={handleImageUrlChange}/> }
          </div>
        </div>
        {inputFields.map((field, index) => (
          <EmpDetailInputField
            key={index}
            field={field}
            updatedEmployee={updatedEmployee}
            handleInputChange={handleInputChange}
            editing={editing}
            dept={dept}
            job={job}
            passwordGenerate={passwordGenerate}
          />
        ))}
      </div>
    <Row style={{ marginBottom: "10px" }}>
      <Col md={15}></Col>
      <Col md={9}>
      <div className="col-11" style={{ display: "flex", justifyContent: "flex-end" }}>
        <div style={{ marginRight: "10px" }}>
          {editing ? (
            <button className={styles.empSaveButton2} onClick={handleSaveChanges}>저장</button>
          ) : (
            <button className={styles.empSaveButton1} onClick={handleEdit}>수정</button>
          )}
        </div>
        {editing && (
          <button className={styles.empSaveButton3} onClick={handleCancel}>취소</button>
        )}
      </div>
      </Col>
    </Row>
  </div>
  );
};

export default EmpDetail;

EmpDetailInputField.jsx

import React from 'react';
import styles from './empDetailInfo.module.css';
import styled from 'styled-components';

const EmpDetailInputField = ({
    field,
    updatedEmployee,
    handleInputChange,
    editing,
    dept,
    job,
    passwordGenerate
}) => {
    const { label, name, type, options } = field;
    return (
        <div className={styles.empInfoItem} key={name}>
            <div className={styles.label}>{label}</div>
            <div className={styles.selectContainer}>
                {type === 'select' ? (
                    <select
                        className={styles.selectBox}
                        value={updatedEmployee[name] || ''}
                        onChange={handleInputChange}
                        disabled={!editing}
                        name={name}
                    >
                    {name === 'DEPT_NAME' ? (
                        dept.map((item, index) => (
                        <option key={index} value={item.CD_VALUE}>{item.CD_VALUE}</option>
                        ))
                    ) : name === 'E_OCCUP' ? (
                        job.map((item, index) => (
                        <option key={index} value={item.CD_VALUE}>{item.CD_VALUE}</option>
                        ))
                    ) : (
                        options.map((option, index) => (
                        <option key={index} value={option}>{option}</option>
                        ))
                    )}
                    </select>
                    ) : (
                    <input
                        className={styles.inputFields}
                        type={type}
                        value={updatedEmployee[name] || ''}
                        onChange={handleInputChange}
                        readOnly={!editing}
                        name={name}
                    />
                )}
                {name === 'E_PASSWORD' && (
                    <MyButton type="button" onClick={passwordGenerate}>
                    임시비밀번호재발급
                    </MyButton>
                )}
            </div>
        </div>
    );
};

export default EmpDetailInputField;

const MyButton = styled.button`
    margin-top: 35px;
    border-radius: 5px;
    background-color: grey;
    color: white;
`;

이렇게 EmpDetail의 inputField에 넣는 값을 따로 컴포넌트로 분리하여 중복 코드를 제거하고 좀 더 코드 가독성이 높아지도록 수정하였다.

2차 리팩토링 코드

임시비밀번호 재발급하는 코드가 EmpDetail에 남아있어 EmpDetailInputField로 이식한 뒤, EmpDetail에서는 재발급한 비밀번호를 처리하는 함수만 남겼다.

EmpDetail.jsx

  // 비밀번호 재발급 처리
  const handlePasswordGenerate = (newPassword) => {
    setPassword(newPassword);
    setUpdatedEmployee(prevState => ({
      ...prevState,
      E_PASSWORD: newPassword // E_PASSWORD 항목만 업데이트
    }))
  };
{inputFields.map((field, index) => (
          <EmpDetailInputField
            key={index}
            field={field}
            updatedEmployee={updatedEmployee}
            handleInputChange={handleInputChange}
            editing={editing}
            dept={dept}
            job={job}
            onPasswordGenerate={handlePasswordGenerate} // 비밀번호 생성 함수 전달
          />
        ))}

EmpDetailInputField.jsx

const EmpDetailInputField = ({
    field,
    updatedEmployee,
    handleInputChange,
    editing,
    dept,
    job,
    onPasswordGenerate // 임시비밀번호 재발급 함수 전달
}) => {
    const { label, name, type, options } = field;

    const passwordGenerate = () => {
        const chars =
          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*";
        let randomStr = "";
        for (let i = 0; i < 10; i++) {
          let randomIndex = Math.floor(Math.random() * chars.length);
          randomStr += chars[randomIndex];
        }
        onPasswordGenerate(randomStr); // 부모 컴포넌트로 재발급 임시비밀번호 전달
      };

0개의 댓글

관련 채용 정보