미새문지

24.09.13 day66 작업 일지 본문

개발 TIL

24.09.13 day66 작업 일지

문미새 2024. 9. 13. 23:32
728x90

오늘 백엔드를 맡은 재희님과 api연동 및 기획 변경이나 추가할 점에 대해 회의했다.

본인은 이번 회의로 각자 테스팅 방식이나 백엔드에서 서버를 띄워서 프론트도 테스팅이 가능하게 할 수 있냐 물어보려 했었는데, 전날 밤을 꼴딱 새서 디비랑 클라우드로 띄워 서버가 켜있는 동안 프론트도 테스팅이 가능하게 구현해놓았더라 역시 킹갓재희

덕분에 오늘 회의가 짧을까 싶었는데 의외의 에러들 때문에 회의시간을 꽉꽉 채워버렸다 

일단 백과 프론트가 처음에 기획을 할 때 전달받을 데이터가 어떤 값이 있는지, 어떻게 전달되는지 미리 결정하고 그대로 했었어야 했는데 기획할 때 이 부분이 진짜 어려운 것 같다.

백은 백대로 erd를 구성하고 디비 설계를 하려면 초반 기획이 확실하게 정해져야 했고 프론트는 정해진 데이터 값을 토대로 코드를 짰어야 했는데, 그 부분이 부실해 각자 어려움을 느꼈던 날이였다.

 

기존의 로그인에서 회원가입으로 들어가는 부분은 카카오 로그인 페이지에서만 구현되어 있어, 이메일 로그인 페이지에서도 회원가입 페이지로 접근 가능하게 수정했다.

같은 컴포넌트를 사용하려면 각자 경로가 다르기 때문에 해당 위치에 따라 다른 경로로 갈 수 있게 코드를 수정했다.

import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import styled from "styled-components";

const OtherBtn = () => {
  const navigate = useNavigate();
  const location = useLocation();

  const handleMoveRegister = () => {
    navigate("/register");
  };

  const handleMoveEmailLogin = () => {
    if (location.pathname === "/login") {
      navigate("/login/email");
    } else if (location.pathname === "/login/email") {
      navigate("/login");
    }
  };

  const btnText =
    location.pathname === "/login" ? "이메일 로그인" : "카카오 로그인";

  return (
    <>
      <>
        <OtherBtnWrapper>
          <Btn onClick={handleMoveRegister}>이메일 회원가입</Btn>
          <Slash> | </Slash>
          <Btn onClick={handleMoveEmailLogin}>{btnText}</Btn>
        </OtherBtnWrapper>
      </>
    </>
  );
};
export default OtherBtn;

const OtherBtnWrapper = styled.div`
  width: 100%;
  text-align: center;
  margin-top: 20px;
  font-size: 12px;
  color: #848484;
`;

const Slash = styled.span`
  margin: 0 5px;
`;

const Btn = styled.span`
  cursor: pointer;
`;

useLocation 기능을 이용해 현재 어떤 경로에 있는지 확인하고 소셜 로그인 경로일 땐 이메일 로그인 경로를 이메일 로그인 경로일 땐 소셜 로그인 경로를 연결해줬다.

 

루틴 추가 부분은 회의를 거쳐 많은 수정이 이루어졌다.

import styled from "styled-components";
import PageCount from "../common/pageCount";
import PageGuide from "../common/pageGuide";
import CommonInput from "../common/commonInput";
import { useState } from "react";
import NextBtn from "../signup/nextBtn";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../redux/store";
import {
  setTitle,
  setGender,
  setSkin,
  setAge,
  setProblem,
  setGrade,
} from "../../redux/slice/addRoutineSlice";
import CommonCheckBox from "../common/commonCheckbox";
import CommonRadioBox from "../common/commonRadiobox";

interface NextProps {
  onNext: () => void;
}

const AddRoutine1: React.FC<NextProps> = ({ onNext }) => {
  const dispatch = useDispatch();
  const title = useSelector((state: RootState) => state.addRoutine.title);
  const gender = useSelector((state: RootState) => state.addRoutine.gender);
  const skin = useSelector((state: RootState) => state.addRoutine.skin);
  const age = useSelector((state: RootState) => state.addRoutine.age);
  const problem = useSelector((state: RootState) => state.addRoutine.problem);
  const grade = useSelector((state: RootState) => state.addRoutine.grade);

  const handleGenderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    let updateGender = [...gender];

    if (updateGender.includes(value)) {
      updateGender = updateGender.filter((e) => e !== value);
    } else {
      updateGender.push(value);
    }

    dispatch(setGender(updateGender));
  };

  const handleSkinTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = parseInt(e.target.value, 10);
    dispatch(setSkin(value));
  };

  const handleAgeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = parseInt(e.target.value, 10);
    dispatch(setAge(value));
  };

  const handleProblemChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = parseInt(e.target.value, 10);
    let updateProblem = [...problem];

    if (updateProblem.includes(value)) {
      updateProblem = updateProblem.filter((e) => e !== value);
    } else {
      updateProblem.push(value);
    }

    updateProblem.sort((a, b) => a - b);
    dispatch(setProblem(updateProblem));
  };

  const isButtonDisabled = () => {
    return (
      !title || gender.length === 0 || skin === 0 || age === 0 || grade <= 0
    );
  };
};
export default AddRoutine1;

먼저 기존에 있던 전체 체크박스들을 삭제하고 회원가입과 비슷한 방식의 입력박스로 수정했다.

회원가입과 다른 점이 있다면 각각의 입력값을 통합으로 하지 않고 개별로 나눠서 데이터를 전달하는 걸로 했다. 이 편이 가독성이 좋아 코드 작성에 더 도움될 것 같긴 하다. 그리고 성별 입력 칸은 한 개만 선택 가능하던 기존의 라디오 버튼이 아닌 중복 선택이 가능한 체크박스로 변경했다.

중복으로 변경한 이유는 해당 입력페이지는 어떤 성별에게 사용되는지 체크하기 때문에 남녀가 같이 사용할 수도 있는 루틴이 있어서 중복값으로 했다.

성별은 배열로 만들어 만약 남녀 둘다 체크를 했을 경우 서버에 데이터를 보낼 때 A를 보내는 걸로 지정하여, 남 : M, 여: F, 둘 다: A로 정했다.

피부 타입도 중복으로 할려다가  각각의 선택지가 중복선택하기 애매한 것 같아 라디오 버튼을 이용해 단일선택으로 할 수 있게 했다. 마찬가지로 나이도 4개로 지정해서 40대부턴 +로 통합했다.

트러블 부분은 체크 박스로 중복 선택이 가능하며 필수로 선택하게는 안했다. 

단계에 정수값을 입력했을 시 해당 값에 따라 다음 페이지에 단계입력 개수가 늘어나게 된다.

 

import styled from "styled-components";
import PageCount from "../common/pageCount";
import CommonInput from "../common/commonInput";
import { useEffect, useState } from "react";
import NextBtn from "../signup/nextBtn";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../redux/store";
import { setRoutineItem } from "../../redux/slice/addRoutineSlice";
import axios from "axios";

interface NextProps {
  onNext: () => void;
}

const AddRoutine2: React.FC<NextProps> = ({ onNext }) => {
  const dispatch = useDispatch();
  const grade = useSelector((state: RootState) => state.addRoutine.grade);
  const routineItems = useSelector(
    (state: RootState) => state.addRoutine.routineItem
  );
  const [allRoutineItems, setAllRoutineItems] = useState(
    new Array(grade).fill({ step_name: "", description: "", item_key: 0 })
  );
  const [searchResults, setSearchResults] = useState<string[]>([]);
  const [totalPrice, setTotalPrice] = useState<number>(0);
  const backPort = process.env.REACT_APP_BACKEND_PORT;

  useEffect(() => {
    setAllRoutineItems(
      new Array(grade).fill({ step_name: "", description: "", item_key: 0 })
    );
  }, [grade]);

  useEffect(() => {
    // if (routineItems.length > 0) {
    //   setAllRoutineItems(routineItems);
    // } else {
    //   setAllRoutineItems(
    //     new Array(grade).fill({ step_name: "", description: "", item_key: 0 })
    //   );
    // }
    if (routineItems.length < grade) {
      setAllRoutineItems([
        ...routineItems,
        ...new Array(grade - routineItems.length).fill({
          step_name: "",
          description: "",
          item_key: 0,
        }),
      ]);
    } else {
      setAllRoutineItems(routineItems.slice(0, grade));
    }
  }, [grade, routineItems]);

  // useEffect(() => {
  //   const sum = searchResults.reduce(
  //     (acc, product) => acc + parseFloat(product.price || "0"),
  //     0
  //   );
  //   setTotalPrice(sum);
  // }, [searchResults]);

  const handleRoutineItemChange = (
    index: number,
    key: keyof (typeof allRoutineItems)[0],
    value: string
  ) => {
    const updatedItem = { ...allRoutineItems[index], [key]: value };
    const updatedItems = [...allRoutineItems];
    updatedItems[index] = updatedItem;

    setAllRoutineItems(updatedItems);

    if (key === "item_key" && value.length > 2) {
      searchItemData(value);
    }
    dispatch(setRoutineItem({ index, item: updatedItem }));
  };

  const handleInputBlur = (
    index: number,
    key: keyof (typeof allRoutineItems)[0],
    value: string
  ) => {
    handleRoutineItemChange(index, key, value);
  };

  const searchItemData = async (query: string) => {
    try {
      const response = await axios.get(`${backPort}/api/item/${query}`);
      console.log(response.data);

      setSearchResults(response.data.item || []);
    } catch (error) {
      console.error("제품 검색 중 오류 발생", error);
    }
  };

  // const handleProductSelect = (
  //   index: number,
  //   productName: string,
  //   productPrice: string
  // ) => {
  //   const updatedItems = [...allRoutineItems];
  //   updatedItems[index] = { ...updatedItems[index], itemName: productName };
  //   setAllRoutineItems(updatedItems);
  //   dispatch(setRoutineItem(updatedItems[index]));
  //   setSearchResults([]);
  // };

  const isButtonDisabled = allRoutineItems.some(
    (item) =>
      !item.step_name?.trim() || !item.description?.trim() || !item.item_key
  );
};
export default AddRoutine2;

2페이지에선 단계별로 단계주제, 단계설명, 사용제품을 입력할 수 있는데 기존에 이 입력값에서 에러가 발생해 이 후 서버에 api로 데이터를 보낼 때 에러가 발생하게 됐다.

원인은 원래 제품입력칸에 제품 이름을 입력해야 해서 string값으로 받아야 했는데 루틴 추가 api를 테스트하기 위해 제품 key값을 받음으로써 number로 변경하다가 에러가 발생했던 것 같다.

정수값은 trim이 안먹히기 때문에 해당 에러도 발생했었고, onChange에 의해 한글자 입력할 때마다 계속 배열이 생성되서 배열길이가 20이 되고 하다보니 백엔드에서 받을 배열 길이와 맞지 않아 에러가 발생했다.

해당 부분을 리덕스에서 백엔드와 같은 변수값으로 지정하고 해당 단계에 맞게 배열에 객체로 담아 관리하게 작성했다.

그리고 이전 페이지로 돌아가서 단계값을 수정하면 현재 값보다 높게 수정 시 루틴 단계가 추가, 낮게 수정 시 루틴 단계가 감소하며 사라지는 입력값은 기존에 작성된 부분은 사라지게 수정했다.

 

import styled from "styled-components";
import PageCount from "../common/pageCount";
import PageGuide from "../common/pageGuide";
import CommonTextarea from "../common/commonTextarea";
import { useState } from "react";
import CompleteBtn from "../common/completeBtn";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../redux/store";
import {
  setTitle,
  setGrade,
  setRoutineItem,
  setTag,
} from "../../redux/slice/addRoutineSlice";
import axios from "axios";

const AddRoutine3: React.FC = () => {
  const dispatch = useDispatch();
  const title = useSelector((state: RootState) => state.addRoutine.title);
  const gender = useSelector((state: RootState) => state.addRoutine.gender);
  const skin = useSelector((state: RootState) => state.addRoutine.skin);
  const age = useSelector((state: RootState) => state.addRoutine.age);
  const problem = useSelector((state: RootState) => state.addRoutine.problem);
  const grade = useSelector((state: RootState) => state.addRoutine.grade);
  const routineItem = useSelector(
    (state: RootState) => state.addRoutine.routineItem
  );
  const tag = useSelector((state: RootState) => state.addRoutine.tag);
  const [tagInput, setTagInput] = useState(tag.join(", "));

  const token = sessionStorage.getItem("authToken");

  const tagChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setTagInput(e.target.value);
  };
  const handleTagBlur = () => {
    const tagArr = tagInput.split(/[\s,]+/).filter((tag) => tag.trim() !== "");
    dispatch(setTag(tagArr));
  };

  const [isSubmit, setIsSubmit] = useState(false);
  const backPort = process.env.REACT_APP_BACKEND_PORT;

  const handleSubmit = async () => {
    setIsSubmit(true);
    let for_gender = "";

    if (gender.length === 2) {
      for_gender = "A";
    } else {
      for_gender = gender[0];
    }

    const requestBody = {
      main: {
        routine_name: title,
        steps: grade,
        for_gender: for_gender,
        for_skin: skin,
        for_age: age,
        for_problem: problem,
      },
      details: routineItem,
      tags: tag,
    };

    console.log("요청할 body 데이터:", requestBody);

    try {
      const response = await axios.post(
        `${backPort}/api/routine`,
        requestBody,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );
      console.log("응답:", response.data);
    } catch (error) {
      console.error("제출 중 오류 발생", error);
    } finally {
      setIsSubmit(false);
    }
  };

  const isButtonDisabled = () => {
    return !tag || tag.length === 0;
  };

  return (
    <>
      <AddRoutine3Wrapper>
        <PageCount count="3" />
        <PageGuide text="태그를 등록해주세요" />
        <CommonTextarea
          value={tagInput}
          onChange={tagChange}
          onBlur={handleTagBlur}
        />
        <span>스페이스 또는 쉼표(,)로 구분해주세요</span>
        <CompleteBtn
          text="등록"
          onClick={handleSubmit}
          disabled={isButtonDisabled() || isSubmit}
        />
      </AddRoutine3Wrapper>
    </>
  );
};
export default AddRoutine3;

const AddRoutine3Wrapper = styled.div`
  width: 70%;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  padding-bottom: 150px;

  span {
    font-size: 12px;
    color: #454545;
  }
`;

3페이지에서는 스페이스나 쉼표로 단어를 구별하여 배열에 단어들을 받는 방식으로 작성되었는데 코드를 잘못 작성하여 스페이스나 쉼표 등 아예 텍스트외에는 입력할 수 없게 되어 이 부분을 수정했다.

원인은 타입이 배열인 tag값을 그대로 textarea에 value로 적용하여 스페이스나 쉼표를 입력하면 onChange에 의해 바로 trim으로 수정되서 입력되었다 사라지는게 너무 빨라 입력이 안되는 걸로 보였던 것 같다.

해당 부분을 onBlur로 입력을 다 마치고 커서를 바깥으로 뺄 때 값이 적용되게 하여 이 후 입력 없이 바로 배열로 변경되게 했다.

위의 gender를 중복 선택 시 A를 보내게 api 요청 직전 변경되게 작성했고, 

백엔드에서 요청했던 데이터 구조에 맞게 객체로 묶어서 전송했다.

그동안 코드를 깃에 올려서 백엔드가 테스트를 하고 수정부분을 말해줬는데, 이제 디비 클라우드가 생겨서 필요할 때 백엔드가 클라우드를 띄워주면 프론트 쪽에서도 테스트가 가능하게 되어 너무 편해졌다.

이제 기존의 못했던 api부분들을 다 연결하고 보류했던 마이페이지나 장바구니 부분도 만져야 한다.

728x90

'개발 TIL' 카테고리의 다른 글

24.09.20 day68 작업 일지  (0) 2024.09.20
24.09.19 day67 작업 일지  (4) 2024.09.19
24.09.11 day65 작업 일지  (2) 2024.09.11
24.09.10 day64 작업 일지  (0) 2024.09.10
24.09.09 day63 작업 일지  (0) 2024.09.09