POSCOxCODINGON 웹 풀스택 13기

[ 코딩온 ] KDT 웹 풀스택 13기 / 3차 React 개인 프로젝트 회고

write3027 2024. 9. 23. 00:17

"백일몽"
온라인 숙박 공유 플랫폼
서버 주소 : http://43.203.247.141:3000/
사용 기술 : React, Next.js, Typescript
개발 기간 : 2024.09.02.월 ~ 2024.09.12.목
참여 인원 : 1인. 개인 프로젝트

주요 기능 :
1. 검색 : 여행지, 날짜, 인원 선택  
2. 필터링 : 해시태그 기반 검색
3. 지도 : React google maps api. 위치 표시 및 길찾기


이번 React 개인 프로젝트에서는 사용자 중심의 UI/UX 구현을 목표로 하였습니다.
또한, 프로젝트를 통해 프론트엔드의 다양한 기술을 학습하고, 실제 서비스에 가까운 환경을 구축함으로써 웹 개발의 실질적인 기술력을 향상하는 데 목적을 두었습니다.

 

 

페이지 설명

메인페이지
헤더 검색 : 여행지, 날짜, 인원

1. 메인페이지 : 헤더

로고 : 로고클릭하면 전체 페이지를 다시 로드하여 초기화된 사용자 경험을 제공하기위해 라우팅이 아닌 리디렉션 처리를 했다.
/*로고 : 리디렉션*/
<div className="logoBox" 
	onClick={() => {window.location.href = "/";}}>
...
</div>​

 

여행지 검색 팝업창 : 사용자가 검색어를 입력하지 않고도 선택할 수 있는 옵션을 제공해 검색 과정을 편리하게 만든다. 
//팝업창
const [isPopupOpen, setIsPopupOpen] = useState(false);
//Input 클릭 시 팝업 열기
const handleInputClick = () => {
	setIsPopupOpen(true);
    setAutoComplete(placeData);
};
...

return (
...
{/* 자동 완성 창 */}
<div className="placeRefBox">
  {isPopupOpen && (
    <ul>
      {autoComplete.map((item: any, index: number) => (
        <li
          key={index}
          onClick={() => {
            setPlace(item);
            setIsPopupOpen(false);
          }}
        >
          {item}
        </li>
      ))}
    </ul>
  )}
</div>
);​


여행지 검색 팝업 초성 자동완성

  const getChosung = (str: string) => {
    const cho = [
      "ㄱ", "ㄲ","ㄴ","ㄷ","ㄸ","ㄹ","ㅁ","ㅂ","ㅃ","ㅅ","ㅆ","ㅇ","ㅈ","ㅉ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ",
    ];
    let result = "";
    for (let i = 0; i < str.length; i++) {
      const code = str.charCodeAt(i) - 44032;
      if (code > -1 && code < 11172) {
        result += cho[Math.floor(code / 588)];
      } else {
        result += str.charAt(i);
      }
    }
    return result;
  };
  const includesChosung = (target: string, search: string) => {
    return getChosung(target).includes(search);
  };
  
  //onChange시 실행하는 findPlace함수
  const findPlace = (e: ChangeEvent<HTMLInputElement>) => {
    const newPlace = e.target.value;
    setPlace(newPlace);
    if (newPlace.length > 0) {
      const newData = placeData.filter(
        (x) => x.includes(newPlace) || includesChosung(x, newPlace)
      );
      setAutoComplete(newData);
    } else {
      setAutoComplete(placeData);
    }
  };​


여행지 검색 팝업창 close.

event.target as Node : 이벤트가 발생한 클릭 대상 요소를 가리킨다. 여기서 클릭한 요소inputRef.current에 포함되지 않았는지 여부 확인

const inputRef = useRef<HTMLDivElement>(null); // Input과 팝업을 감싸는 ref
useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      	if ( inputRef.current &&b!inputRef.current.contains(event.target as Node) ) 
      		{setIsPopupOpen(false);}
     };
    document.addEventListener("mousedown", handleClickOutside);
    return () => { document.removeEventListener("mousedown", handleClickOutside); };
  }, []);
  
  ...
   return (
    <PlaceStyledComponent>
      <div ref={inputRef}>
        여행지
        ...


날짜 검색 

Antd “RangePicker” disabledDate속성 : 오늘 이전의 날짜 모두 비활성화
diffDays : 선택한 날짜 일수 계산

 // 날짜 차이를 계산하여 선택된 일 수를 저장
const diffDays = dates[1].diff(dates[0], "day"); // dayjs의 diff 함수를 사용해 일수 계산
setSelectedDays(diffDays);
      
<RangePicker
            className="selectDate"
            ref={rangePickerRef} // RangePicker의 ref 추가
            onChange={onRangeChange} // 날짜 변경 시 호출
            value={dates}
            disabledDate={(current) =>
              current.isBefore(moment().subtract(1, "day"))
            }
          />


인원 검색 : 팝업에서 선택한 값 useState에 저장. 성인 최소 1인 이상.

  //people 컴포넌트.
  const [peopleNum, setPeopleNum] = useState(1);
  const [adultCount, setAdultCount] = useState(1);
  const [childCount, setChildCount] = useState(0);
  const [infantCount, setInfantCount] = useState(0);
  const [petCount, setPetCount] = useState(0);
  // 인원 조절 핸들러
  const decrementCount = (type: string) => {
    if (type === "adult" && adultCount > 1) {
      setAdultCount(adultCount - 1);
      setPeopleNum(peopleNum - 1);
    } else if (type === "child" && childCount > 0) {
      setChildCount(childCount - 1);
      setPeopleNum(peopleNum - 1);
    } else if (type === "infant" && infantCount > 0)
      setInfantCount(infantCount - 1);
    else if (type === "pet" && petCount > 0) setPetCount(petCount - 1);
  };

 

1. 메인페이지 : 필터링

allData 배열을 필터링. hash 배열 안에 tag가 title 변수와 일치하는 항목이 있는 경우만 남김.
const [clickFilter, setClickFilter] = useState("최고의 전망");
  const [newFilterData, setNewFilterData] = useState<any>(initialFilterData);

  const clickFilterItem = (title: string) => {
    setClickFilter(title);
    const newData = allData.filter((item) =>
      item.hash.some((hashItem) => hashItem.tag === title)
    );
    setNewFilterData(newData);
  };​


이미지
: react Swiper 사용.

{/* 이미지 swiper로 보여주기 */}
  <Swiper
    onSwiper={(swiper) => {
      swiperRef.current = swiper;
    }}
    onSlideChange={(swiper) => {
      setIsBeginning(swiper.isBeginning); // 첫 슬라이드 여부 업데이트
      setIsEnd(swiper.isEnd); // 마지막 슬라이드 여부 업데이트
    }}
    modules={[Navigation, Pagination, Scrollbar, A11y]} // 필요한 모듈 추가
    spaceBetween={50} // 슬라이드 간의 간격
    slidesPerView={1} // 한 번에 보여줄 슬라이드 수
    pagination={{
      clickable: true, // 페이지네이션 점을 클릭할 수 있음
      dynamicBullets: true, // 동적 페이지네이션
    }}
    scrollbar={{ draggable: true }} // 스크롤바 추가
    navigation
  >
    {data.src.map((image: any, index: number) => (
      <SwiperSlide key={index}>
        <img src={image.src} alt={`image${index + 1}`} />
      </SwiperSlide>
    ))}
  </Swiper>


제목
: 한줄만 나오게. 나머지는 ...

//css
.contentBox {
      .title {
        white-space: nowrap; /* 텍스트를 한 줄로 */
        overflow: hidden; /* 넘치는 텍스트를 숨김 */
        text-overflow: ellipsis; /* 넘치는 텍스트에 '...' 표시 */
      }
    }​


가격 : 세자리수마다 컴마표시. toLocaleString()

 


검색페이지

2. 검색페이지

- 헤더에서 검색결과 라우팅.
- query로 받아온 정보로 filter.
- 지도 : react-google-maps/api사용.
        MarkerF :검색 지역 지도에서 마커
        item hover : 우측 이미지 컴포넌트에서 지도 컴포넌트로 props로 hover상태 전달. MarkerF 속성 변경
        position: sticky 를 사용해 스크롤 해도 map위치 고정 

 

마커 클릭

3. 상세페이지

- Antd “Image” : click해서 사진 크게 띄우도록
- 날짜 : 숙박 일수로 가격 계산하는 함수 만듦
- 인원수 : 여행자 수 선택 최대인원보다 적게 제한
- 숙소 설명 : 더보기, 간략히보기. “스크롤 함수” -> 부드럽게 열고 닫히도록
- 가격표시 position:sticky로 고정.
- 마커 클릭시 구글맵 길찾기 검색
       MarkerF onClick : findRoad함수. lat, lng url에 넣는다. &hl=ko 한국어버전

개발중 페이지

로그인, 예약하기
: 버튼이 동작하지 않는 상황에서 사용자가 혼란을 겪지 않도록 antd “Modal” 띄워줌.

반응형 Header

반응형 Header

@media(max-width:768px)
Input창에 숨겼다가 위에서 아래로 내려오도록. 팝업창
translateY, transition 사용.

반응형 Main Page

반응형 Main Page

Swiper 화살표 삭제
-> 모바일에서는 터치로 밀어서 Swiper 사용하므로

반응형 Search Page

모바일 hover 동작 안하므로 지도 고정X

반응형 Detail Page

각 사진 디폴트로 크게 볼 수 있도록 Swiper로 변경.
더보기 : 화면 팝업 대신 모달

 

 

개발 과정
1. Dummy Data 생성
2. 주요 기능 개발 : 검색 및 필터, 지도 API 연동
3. 반응형 UI/UX 구현: 모바일 최적화 작업

 

향후 계획
- 로그인 및 회원가입 기능 추가
- 숙소 예약 기능 개발
- Socket 채팅: 게스트-호스트 간 실시간 소통 기능 개발
느낀점
프로젝트를 시작하고 약 1주일간은 헤더를 고정하는것도 생각하지 못할만큼 UI/UX를 몰랐다.
하지만 실제 서비스중인 웹사이트를 약 2주간 분석하면서 UI/UX측면에서 필요한 기능을 많이 배울 수 있었다.
평소 웹사이트를 사용하면서 기능을 디테일하게 관찰하고 사용하는 습관을 가지면 역량높은 프론트엔드 개발자로 성장하는데 도움 될 것같다.

이번 플젝은 처음으로 기획부터 서버배포까지 혼자헸다.
혼자 어려움을 다 헤쳐나간만큼 공부가 많이 되었고, 많은 성장을 한 것 같다고 느낀다.
역량을 더 갖추기 위해 앞으로도 많이 굴러보자..@_@