FRONTENDSCHOOL6 / ready-act

멋쟁이사자처럼 파이널프로젝트 16조
MIT License
0 stars 4 forks source link

[R09M App] CreateRoom 페이지 - PocketHost 양식에 Date와 Time을 동시에 입력 이슈 #50

Closed jellyjoji closed 1 year ago

jellyjoji commented 1 year ago

내용

pocketHost 에 시간날짜 form 을 Create 하는 중입니다. DatePicker 에 대한 양식은 <input type="date"> 로 구현하여 form 에 맞게 전송하는데 성공했으나 포켓호스트 양식에 맞는 시간 형식을 동시에 입력할수 없는 상황입니다.

<input type="datetime-local"> 형식으로 구현할 경우 2018-06-12T19:30 형식으로 입력되어 포켓호스트 양식과 일치하지 않아 form에 올라가지 않는 문제가 있습니다.

DatePicker.jsx 현재 구현한 날짜 구현 코드입니다.

import { forwardRef, useId, useState } from 'react';
// import Input from "@/components/Input";

function DatePicker({ title, className, labelClassName, ...restProps }, ref) {
  const [, setDate] = useState(null);
  const id = useId();

  return (
    <div>
      <label htmlFor={id} className={labelClassName}>
        {title}
      </label>
      <input
        id={id}
        ref={ref}
        className={className}
        type="date"
        onChange={(e) => setDate(e.target.value)}
        {...restProps}
      />
    </div>
  );
}

export default forwardRef(DatePicker);

시간 데이터를 "00:00:00" 형식으로 다음과 같은 방법 구현하여 사용하려해도 한 입력폼에 date 와 time 이 동시에 들어가야하는 문제점이 있습니다.

TimePicker.jsx

import { useState } from 'react';

function TimePicker() {
  const [time, setTime] = useState('00:00:00');

  const handleTimeChange = (e) => {
    const inputTime = e.target.value;
    // 정규식을 사용하여 입력된 값이 유효한 형식인지 확인
    const timeRegex = /^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;
    if (timeRegex.test(inputTime)) {
      setTime(inputTime);
    }
  };

  return (
    <div>
      <label htmlFor="timeInput">Time:</label>
      <input
        type="text"
        id="timeInput"
        value={time}
        onChange={handleTimeChange}
      />
    </div>
  );
}

export default TimePicker;

참고 이미지 (선택)

문제 상황

Untitled

아래는 데이터가 입력되어 생성되는 포켓호스트 폼 형식입니다.

※ 댓글에 이슈 해결 완료 후 결과 또는 해결 과정 이미지 첨부

jellyjoji commented 1 year ago

문제 원인

요소에서 입력받은 날짜 포멧과 PocketBase의 날짜(date) 포멧이 다르기 때문입니다. 🤔

가이드

날짜 및 시간 정보를 받을 수 있도록 DatePicker 컴포넌트 코드를 아래와 같이 수정합니다.

DatePicker.jsx

import { forwardRef, useId, useState } from 'react';

function DatePicker({ title, className, labelClassName, ...restProps }, ref) {
  const id = useId();

  return (
    <>
      <div>
        <label htmlFor={id} className={labelClassName}>
          {title}
        </label>
        <input
                    **type="datetime-local"**
          id={id}
          ref={ref}
          className={className}
          {...restProps}
        />
      </div>
    </>
  );
}

export default forwardRef(DatePicker);

사용자로부터 입력 받은 날짜, 시간 정보를 PocketBase 날짜 포멧을 변환하여 폼 데이터에 추가합니다.

CreateRoom.jsx

const handleCreate = async (e) => {
  e.preventDefault();

  // ...

    // Input (날짜, 시간 정보) -> PocketBase (날짜, 시간 정보) 변환
  const dateValue = **new Date(dateRef.current.value).toISOString()**;

  const data = new FormData();

    // ...

  **data.append('pickup', dateValue);**

    // ...
};

Date.prototype.toISOString() 매서드를 사용하여 시간데이터를 커스텀하여 해결하였습니다. toISOString() 메서드는 단순화한 확장 ISO 형식([ISO 8601](https://ko.wikipedia.org/wiki/ISO_8601))의 문자열을 반환합니다. 반환값은 언제나 24글자 또는 27글자(각각 YYYY-MM-DDTHH:mm:ss.sssZ 또는 ±YYYYYY-MM-DDTHH:mm:ss.sssZ)입니다.시간대는 언제나 UTC이며 접미어 Z로 표현합니다.

해결된 코드 첨부 const dateValue = new Date(createRoomForm.pickUp).toISOString()

import { AppContext } from '@/App';
import FormInput from '@/components/FormInput';
import { useState, useContext, useEffect } from 'react';

function PickUp({ value = null, label, title, className, labelClassName, ...restProps }) {

  const { updateCreateRoomForm } = useContext(AppContext);
  const [data, setData] = useState(value);

  useEffect(() => {
    updateCreateRoomForm('pickUp', data)
  }, [data])

  const handleInputChange = (e) => {
    setData(e.target.value);
  }
  return (
    <div>
      <FormInput
        title={title}
        value={data || ''}
        onChange={handleInputChange}
        type="datetime-local"
        placeholder="픽업 날짜와 시간을 선택헤주세요"
        labelClassName="product name"
        inputClassName="defaultInput w-full"
        label={label}
        {...restProps}
      />
    </div>
  )
}

export default PickUp;
import { AppContext } from '@/App';
import { pb } from '@/api/pocketbase';
import { ClientResponseError } from 'pocketbase';
import { useContext, useRef } from 'react';
import { Helmet } from 'react-helmet-async';
import { useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast';

import Button from '@/components/Button';
import CreateHeader from '@/layout/CreateHeader';
import Category from '@/parts/create/Category';
import Content from '@/parts/create/Content';
import Creator from '@/parts/create/Creator';
import PickUp from '@/parts/create/PickUp';
import UploadImage from '@/parts/create/UploadImage';
import MeetingPoint from '@/parts/create/MeetingPoint';
import ParticipateCounter from '@/parts/create/ParticipateCounter';
import PaymentToggleButton from '@/parts/create/PaymentToggleButton';
import Status from '@/parts/create/Status';
import Price from '@/parts/create/Price';
import Title from '@/parts/create/title';

function CreateRoom() {
  const { createRoomForm } = useContext(AppContext);
  const navigate = useNavigate();

  const formRef = useRef(null);

  const handleCreate = async (e) => {
    e.preventDefault();

    const categoryValue = createRoomForm.category;
    const titleValue = createRoomForm.title;
    const contentValue = createRoomForm.content;
    const priceValue = createRoomForm.price;
    // createRoomForm.pickUp
    const dateValue = new Date(createRoomForm.pickUp).toISOString();
    // const paymentValue = paymentRef.current.dataset.payment;

    const paymentValue = createRoomForm.payment;

    const ParticipateCounterValue = Number(
      // createRoomForm.current.textContent
      createRoomForm.participateNumber
    );

    const meetingPointValue = createRoomForm.meetingPoint;
    const creatorValue = createRoomForm.creator.id;

    // const uploadImageValue = uploadImageRef.current.files[0];
    const uploadImageValue = createRoomForm.uploadImage;
    const statusValue = createRoomForm.status;

    const data = new FormData();

    data.append('category', categoryValue);
    data.append('title', titleValue);
    data.append('content', contentValue);
    data.append('price', priceValue);
    data.append('pickUp', dateValue);
    data.append('payment', paymentValue);
    data.append('participateNumber', ParticipateCounterValue);
    data.append('meetingPoint', meetingPointValue);
    data.append('creator', creatorValue);
    data.append('participate', creatorValue);
    if (uploadImageValue) {
      data.append('uploadImage', uploadImageValue);
    }
    data.append('status', statusValue);

    try {
      await pb.collection('products').create(data);
      toast.success('등록되었습니다.', {
        position: 'top-center',
        ariaProps: {
          role: 'status',
          'aria-live': 'polite',
        },
      });
      navigate(`/products`);
    } catch (error) {
      if (!(error instanceof ClientResponseError)) {
        toast.error('등록에 실패하였습니다. 다시 시도해 주세요.', {
          position: 'top-center',
          ariaProps: {
            role: 'status',
            'aria-live': 'polite',
          },
        });
      }
    }
  };

  return (
    <>
      <Helmet>
        <title>방만들기</title>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta
          property="og:title"
          content="합리적인 소비를 위한 공동구매 서비스 R09M 공동구매 방 만들기 페이지"
        />
        <meta
          property="twitter:title"
          content="합리적인 소비를 위한 공동구매 서비스 R09M 공동구매 방 만들기 페이지"
        />
        <meta property="og:type" content="web application" />
        <meta property="og:url" content="https://r09m.vercel.app/createRoom" />
        <meta
          property="og:description"
          content="공동구매 채소 상품을 확인할 수 있는 페이지입니다. 카테고리, 상품명, 상품 이미지, 상품 가격, 내용, 픽업 날짜, 상태, 생성자, 지불 방법, 픽업 위치 등을 입력하면 방이 생성됩니다."
        />
        <meta
          name="description"
          content="공동구매 채소 상품을 확인할 수 있는 페이지입니다. 카테고리, 상품명, 상품 이미지, 상품 가격, 내용, 픽업 날짜, 상태, 생성자, 지불 방법, 픽업 위치 등을 입력하면 방이 생성됩니다."
        ></meta>
        <meta property="og:image" content="favicon.png" />
        <meta property="og:article:author" content="Ready! Act" />
      </Helmet>

      <h1 className="sr-only">R09M</h1>

      <div className="py-2">
        <div className="px-4">
          <CreateHeader />
          <h2 className="pageTitle">방만들기</h2>
        </div>
      </div>

      <div>
        <form
          encType="multipart/form-data"
          ref={formRef}
          onSubmit={handleCreate}
        >
          <div className="flex flex-col gap-4 p-4 relative"
          >

            <Category
              title="카테고리"
              className="w-full defaultInput"
              label="카테고리"
              value={createRoomForm.category}
            />

            <Title
              value={createRoomForm.title}
            />

            <Price
              value={createRoomForm.price}
            />

            <Content
              title="내용"
              placeholder="공구 모임 주요내용을 알려주세요."
              className="w-full defaultInput"
              labelClassName="product content"
              label="내용"
              value={createRoomForm.content}
            />

            <PickUp
              title="픽업 날짜"
              label="픽업 날짜"
              className="w-full defaultInput"
              labelClassName="date Picker"
              value={createRoomForm.pickUp}

            />

            <Status
              title="상태"
              label="상태"
              className="w-full defaultInput "
              labelClassName="status"
            />

            <Creator />

            <PaymentToggleButton
              title="정산 방법"
              label="정산 방법"
              labelClassName="payment"
              value={createRoomForm.payment}

            />

            <ParticipateCounter value={createRoomForm.participateCounter} labelClassName="participateCounter" label="참여자 인원" title="참여자 인원" />

            <MeetingPoint
              value={createRoomForm.meetingPoint}
              title="만날 장소" labelClassName="meetingPoint" />

            <UploadImage
              // value={createRoomForm.uploadImage}
              title="파일 업로드"
              label="파일 업로드"
              className="bg-[#EBF8E8] p-4 rounded-lg text-primary-500"
            />
          </div >
          <Button
            type="submit"
            className="fixed bottom-3 py-4 activeButton lgFontButton mx-3 w-[93vw] max-w-[544px]"
          >
            방 만들기
          </Button>
        </form >
      </div >
    </>
  );
}

export default CreateRoom;