[React] 공지사항 게시판 만들기(2): create 공지사항 작성

 

 

 

React를 활용한 공지사항 작성

 

 

 

https://joygotohome.tistory.com/124

'공지사항 목록 조회' -> '공지사항 작성'으로 이어지는 글입니다.

 

 

1. create 훅 만들기

\features\notice\model\use-notice-create.ts

import { useNavigate } from 'react-router-dom'
import { useMutation } from '@tanstack/react-query';
import { Form } from 'antd';
import { createNotice } from '../api/notice-api';
import { getCompanyId } from '@/shared/hooks/use-company-info';
import type { NoticeCreateRequest, NoticeCreateFormValues } from './types';


export const useNoticeCreate = () => {
  const navigate = useNavigate();

/**
 * [form] : 배열로 받는 이유 → useForm이 [FormInstance, ...] 형태로 반환하기 때문
 * form 객체로 값 가져오기, 초기화, 유효성 검사 등을 제어
 * 
 * useQuery    → 데이터 가져올 때 (GET)
 * useMutation → 데이터 변경할 때 (POST, PUT, DELETE)
 * 
 * isPending : API 호출 중인지 여부를 알려주는 값
 * submitNotice() 호출 전  → isPending = false
 * submitNotice() 호출 중  → isPending = true  (서버 응답 기다리는 중)
 * API 완료 후             → isPending = false
 */


  // Form.useForm : AntD Form 상태 관리 훅
  const [form] = Form.useForm<NoticeCreateFormValues>();

  // useMutation({ 원래이름: 바꾼이름, isPending }) React Query 에서 제공하는 훅
  const { mutate: submitNotice, isPending } = useMutation({

    // mutationFn: (data: type설정값) => API함수이름(data),
    mutationFn: (data: NoticeCreateRequest) => createNotice(data), // data : 나중에 mutate() 호출할 때 넘기는 값

    // 작성 성공 시 목록 페이지로 이동
    onSuccess: () => { //onSuccess : API 호출이 성공했을 때 실행되는 콜백 함수
        navigate('/notice'); 
    }
  });

  //제출
  const handleSubmit = () => {
    form.validateFields().then((values) => { // 폼 유효성 검사
        submitNotice({  //submitNotice : mutate를 이름 바꾼것
            ...values,  // 폼에서 입력한 값들을 펼쳐서 넣음 (title, content 등)
            companyId: getCompanyId(),
            startDate: values.startDate?.format('YYYY-MM-DD') ?? '',
            endDate: values.endDate?.format('YYYY-MM-DD') ?? '',
        })
    })
  };

  //취소: 공지사항으로 이동
  const handleCancel = () => {
    navigate('/notice');
  };

  return {
    form,         // 폼 인스턴스
    isPending,    // 로딩 상태
    handleSubmit, // 제출
    handleCancel, // 취소
  };
};

 

 

2. API 연결 추가

\features\notice\api\notice-api.ts

import apiClient from '@/shared/api/api-client';
import { getCompanyId } from '@/shared/hooks/use-company-info';
import type { NoticeCreateRequest, NoticeDetail, NoticeListItem, PageResponse } from '../model/types';

const BASE_URL = '/notices';

// 목록 조회 - getData 사용 (data만 바로 반환)
export const getNoticeList = async (page = 0, size = 20): Promise<PageResponse<NoticeListItem>> => {
  const companyId = getCompanyId();
  return await apiClient.getData(`${BASE_URL}/company/${companyId}`, { params: { page, size } });
};

// 작성
export const createNotice = async (data: NoticeCreateRequest): Promise<{ id: number }> => {
  return await apiClient.postData(`${BASE_URL}`, data);
};

백엔드 컨트롤러를 참고하여 주소를 확인해서 만듭니다.

 

3. TYPE 추가

\features\notice\model\types.ts

import type { Dayjs } from 'dayjs';

export interface NoticeCreateRequest {
  companyId?: number | null;
  title: string;
  content: string;
  targetType?: string | null;
  displayType?: string | null;
  startDate: string | null;
  endDate: string | null;
}

export interface NoticeCreateFormValues {
  title: string;
  content: string;
  targetType: string;
  displayType: string;
  startDate: Dayjs;   // 폼에서는 dayjs 객체
  endDate: Dayjs;
}

백엔드 DTO를 참고하여 타입을 지정합니다.

만든 값을 넘길때 날짜 타입을 정확히 하여 날리기 위해서 별로도 FormValues를 제작하였습니다.

 


 

4. form그리기

\features\notice\ui\notice-create.tsx

import { Form, Input, Select, DatePicker } from 'antd';
import { useNoticeCreate } from '../../model/use-notice-create';
import { WriteContainer, WriteForm, ButtonFlex, CancelButton, SubmitButton,} from './notice-create.styles';

export const NoticeCreate = () => {
  const { form, isPending, handleSubmit, handleCancel } = useNoticeCreate();

  return (
    <WriteContainer>
      <WriteForm form={form}>
        <Form.Item name="targetType" label="공지 대상" rules={[{ required: true, message: '공지 대상을 선택해주세요' }]}>
          <Select>
            <Select.Option value="ALL">전체</Select.Option>
            <Select.Option value="SYSTEM">시스템 업데이트</Select.Option>
          </Select>
        </Form.Item>
        <Form.Item name="displayType" label="노출 방식" rules={[{ required: true, message: '노출 방식을 선택해주세요' }]}>
          <Select>
            <Select.Option value="BANNER">배너</Select.Option>
            <Select.Option value="POPUP">팝업</Select.Option>
          </Select>
        </Form.Item>
        <Form.Item name="startDate" label="시작일" rules={[{ required: true, message: '시작일을 선택해주세요' }]}>
          <DatePicker format="YYYY-MM-DD" />
        </Form.Item>
        <Form.Item name="endDate" label="종료일" rules={[{ required: true, message: '종료일을 선택해주세요' }]}>
          <DatePicker format="YYYY-MM-DD" />
        </Form.Item>
        <Form.Item name="title" label="제목" rules={[{ required: true, message: '제목을 입력해주세요' }]}>
          <Input />
        </Form.Item>
        <Form.Item name="content" label="내용" rules={[{ required: true, message: '내용을 입력해주세요' }]}>
          <Input.TextArea rows={4} />
        </Form.Item>
        <ButtonFlex>
          <CancelButton onClick={handleCancel}>취소</CancelButton>
          <SubmitButton type="primary" loading={isPending} onClick={handleSubmit}>작성</SubmitButton>
        </ButtonFlex>
      </WriteForm>
    </WriteContainer>
  );
};

기본 폼을 지정합니다.

 

\features\notice\ui\notice-create.styles.ts

import styled from 'styled-components';
import { Form, Button, Flex } from 'antd';

// 전체 폼을 감싸는 컨테이너
// 세로 방향으로 필드들 배치
export const WriteContainer = styled(Flex)`
  flex-direction: column;
  padding: 1.5rem;
  background-color: white;
  border-radius: 0.5rem;
`;

// Form 을 styled 로 감쌀 때 as typeof Form 필요
// → Form 의 타입 정보가 유지되어야 form={form} 같은 props 오류 안남
export const WriteForm = styled(Form)`
  width: 100%;
` as typeof Form;


// 제목, 내용 입력 필드 너비 100%
export const WriteInput = styled.input`
  width: 100%;
`;

// 버튼 영역
export const ButtonFlex = styled(Flex)`
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid #d9d9d9;
`;

export const CancelButton = styled(Button)`
  min-width: 5rem;
`;

export const SubmitButton = styled(Button)`
  min-width: 5rem;
`;

스타일은 나중에 다시 잡기로 하고, 일단 틀만 만들었습니다.

 

 


 

 

5. page 연결하기

\features\notice\index.ts

export type { NoticeCreateRequest, NoticeDetail, NoticeListItem, PageResponse } from './model/types';
export { NoticeList } from './ui/notice-list';
export { NoticeCreate } from './ui/notice-create';

index를 잡아줍니다.

 

 

\pages\notice\ui\notice-create-page.tsx

import { NoticeCreate } from '@/features/notice'

export const NoticeCreatePage = () => {
  return (
    <NoticeCreate />
  )
}

페이지를 연결해줍니다.

 

 

공지사항 작성 화면 기본 틀이 잡혔습니다.