[React] 공지사항 게시판 만들기(4): update 공지사항 수정

 

 

 

 

React 공지사항 수정

 

 

 

https://joygotohome.tistory.com/126

공지사항 상세에서 이어지는 글입니다.

 

 

 

 

1. update 훅 만들기

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

import { useNavigate, useParams } from 'react-router-dom';
// useNavigate : 코드로 페이지를 이동시키는 훅
// useParams : URL에서 파라미터를 꺼내는 훅 (/notice/update/:id → id 꺼냄)

import { useQuery, useMutation } from '@tanstack/react-query';
// useQuery    → 데이터 가져올 때 (GET)
// useMutation → 데이터 변경할 때 (POST, PUT, DELETE)

import { Form } from 'antd';
// Form.useForm : AntD Form 상태 관리 훅
// form 객체로 값 가져오기, 초기화, 유효성 검사 등을 제어

import dayjs from 'dayjs';
// dayjs : 날짜 처리 라이브러리
// DatePicker 가 dayjs 객체를 사용하기 때문에 string → dayjs 변환에 필요

import { getNoticeDetailRequest, updateNotice } from '../api/notice-api';
// getNoticeDetailRequest : 기존 공지사항 데이터 불러오기 (폼에 채워넣기 위해)
// updateNotice : 수정된 데이터를 서버로 보내는 API 함수

import type { NoticeDetailRequest, NoticeUpdateFormValues } from './types';
// NoticeDetailRequest : 서버로 보낼 수정 요청 타입
// NoticeUpdateFormValues : 폼에서 사용하는 타입 (startDate, endDate 가 dayjs 객체)

export const useNoticeUpdate = () => {
  const navigate = useNavigate();
  const { id } = useParams();
  // URL /notice/update/1 에서 id = "1" 꺼냄

  const numericId = Number(id);
  // useParams 는 string 으로 반환하므로 number 로 변환
  // API 함수가 number 타입을 요구하기 때문

  const [form] = Form.useForm<NoticeUpdateFormValues>();
  // [form] : 배열로 받는 이유 → useForm 이 [FormInstance, ...] 형태로 반환하기 때문
  // NoticeUpdateFormValues : startDate, endDate 가 dayjs 객체인 폼 전용 타입

  const { isLoading } = useQuery({
    queryKey: ['notice', 'detail', numericId],
    // queryKey : 이 데이터의 고유 이름 (캐싱 키)
    // numericId 가 다르면 다른 캐시로 저장됨
    // use-notice-detail 과 같은 queryKey 사용 → 같은 데이터 캐시 공유

    queryFn: () => getNoticeDetailRequest(numericId),
    // 실제로 실행할 API 함수
    // numericId 로 해당 공지사항 데이터 불러옴

    enabled: !!numericId,
    // numericId 가 있을 때만 API 호출
    // !! : 값을 boolean 으로 변환 (0 이나 NaN 이면 false → API 호출 안함)

    select: (data) => {
      // select : API 응답 데이터를 가공할 때 사용
      // 여기서는 데이터를 폼에 세팅하는 용도로 사용
      form.setFieldsValue({
        ...data,
        // 나머지 필드 (title, content 등) 는 그대로 세팅
        targetType: data.targetType ?? undefined,
        displayType: data.displayType ?? undefined,
        // null → undefined 로 변환
        // AntD Form 은 null 허용 안하고 undefined 만 허용하기 때문
        startDate: data.startDate ? dayjs(data.startDate) : undefined,
        endDate: data.endDate ? dayjs(data.endDate) : undefined,
        // string → dayjs 객체로 변환
        // DatePicker 는 dayjs 객체를 값으로 받기 때문
      });
      return data;
    },
  });

  const { mutate, isPending } = useMutation({
    mutationFn: (data: NoticeDetailRequest) => updateNotice(numericId, data),
    // mutationFn : 실제로 실행할 API 함수
    // mutate() 호출 시 여기 등록한 함수가 실행됨

    onSuccess: () => {
      navigate(`/notice/detail/${numericId}`);
      // 수정 성공 시 해당 공지사항 상세 페이지로 이동
    },
  });

  const handleSubmit = () => {
    form.validateFields().then((values) => {
      // validateFields : 폼 유효성 검사
      // 필수값 비어있으면 에러 메시지 표시하고 여기서 멈춤
      mutate({
        id: numericId,
        companyId: null,
        title: values.title,
        content: values.content,
        targetType: values.targetType,
        displayType: values.displayType,
        startDate: values.startDate?.format('YYYY-MM-DD') ?? null,
        endDate: values.endDate?.format('YYYY-MM-DD') ?? null,
        // dayjs 객체 → 'YYYY-MM-DD' 문자열로 변환
        // 백엔드가 string 타입을 요구하기 때문
        useYn: 'Y',
        registrationDateTime: '',
        modificationDateTime: '',
      });
    });
  };

  const handleCancel = () => {
    navigate(`/notice/detail/${numericId}`);
    // 취소 시 해당 공지사항 상세 페이지로 이동
  };

  return {
    form,           // 폼 인스턴스 → WriteForm 에 연결
    isLoading,      // 기존 데이터 로딩 중 여부 → 로딩 중일 때 폼 숨김
    isPending,      // 수정 API 호출 중 여부 → 수정 버튼 로딩 상태에 사용
    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, NoticeDetailRequest, 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 getNoticeDetailRequest = async (id: number): Promise<NoticeDetailRequest> => {
  return await apiClient.getData(`${BASE_URL}/${id}`);
};

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

// 수정
export const updateNotice = async (id: number, data: NoticeDetailRequest): Promise<{ id: number }> => {
  return await apiClient.putData(`${BASE_URL}/${id}`, data);
};

수정  api를 백엔드 코드에 맞춰서 작성합니다.

 

 

 

3. type 추가

\features\notice\model\types.ts

export interface NoticeDetailRequest {
  id: number;
  companyId: number | null;
  title: string;
  content: string;
  targetType: string | null;
  displayType: string | null;
  startDate: string | null;
  endDate: string | null;
  useYn: string;
  registrationDateTime: string;
  modificationDateTime: string;
}

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

받아오고 전송하는건  NoticeDetailRequest 인터페이스를 그대로 사용하였습니다.

업데이트 폼만 따로 빼서 작성했다가 가려합니다.

 

 

 

4. Form그리기

\features\notice\ui\notice-update.tsx

import { Form, Input, Select, DatePicker } from 'antd';
// Form.Item : 폼 필드 래퍼 (label, rules, name 등 관리)
// Input : 텍스트 입력
// Select : 드롭다운 선택
// DatePicker : 날짜 선택

import { useNoticeUpdate } from '../model/use-notice-update';
// 수정 페이지에 필요한 모든 값과 함수를 가져오는 커스텀 훅

import {
  WriteContainer,
  WriteForm,
  ButtonFlex,
  CancelButton,
  SubmitButton,
} from './notice-update.styles';
// 모든 스타일 컴포넌트는 스타일 파일에서 가져옴

export const NoticeUpdate = () => {
  const { form, isLoading, isPending, handleSubmit, handleCancel } = useNoticeUpdate();

  if (isLoading) return null;
  // 기존 데이터 로딩 중일 때 폼 숨김
  // 데이터가 세팅되기 전에 빈 폼이 보이는 것 방지

  return (
    <WriteContainer>
      <WriteForm form={form}>
        {/* form={form} : 훅에서 만든 form 인스턴스를 AntD Form 에 연결
            연결해야 validateFields(), setFieldsValue() 등이 작동 */}

        <Form.Item name="title" label="제목" rules={[{ required: true, message: '제목을 입력해주세요' }]}>
          {/* name="title" : form.getFieldsValue() 할 때 키값
              rules : 유효성 검사 규칙 */}
          <Input />
        </Form.Item>

        <Form.Item name="content" label="내용" rules={[{ required: true, message: '내용을 입력해주세요' }]}>
          <Input.TextArea rows={4} />
          {/* TextArea : 여러 줄 입력 가능한 텍스트 입력
              rows={4} : 기본 높이 4줄 */}
        </Form.Item>

        <Form.Item name="targetType" label="공지 대상" rules={[{ required: true, message: '공지 대상을 선택해주세요' }]}>
          <Select>
            <Select.Option value="ALL">전체</Select.Option>
            <Select.Option value="SYSTEM">시스템 업데이트</Select.Option>
            {/* value : 백엔드로 보내는 실제 값
                텍스트 : 화면에 보이는 값 */}
          </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" />
          {/* format : 화면에 표시되는 날짜 형식
              내부적으로는 dayjs 객체로 관리 */}
        </Form.Item>

        <Form.Item name="endDate" label="종료일" rules={[{ required: true, message: '종료일을 선택해주세요' }]}>
          <DatePicker format="YYYY-MM-DD" />
        </Form.Item>

        <ButtonFlex>
          <CancelButton onClick={handleCancel}>취소</CancelButton>
          <SubmitButton type="primary" loading={isPending} onClick={handleSubmit}>
            수정
            {/* loading={isPending} : API 호출 중일 때 버튼 로딩 상태로 변경 */}
          </SubmitButton>
        </ButtonFlex>
      </WriteForm>
    </WriteContainer>
  );
};

 

 

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

import styled from 'styled-components';
import { Flex, Form, Button } from 'antd';
import type { NoticeUpdateFormValues } from '../model/types';

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

export const WriteForm = styled(Form<NoticeUpdateFormValues>)`
  width: 100%;
` as typeof Form;
// Form<NoticeUpdateFormValues> : 폼 타입 지정
// as typeof Form : styled 로 감쌀 때 Form 의 타입 정보 유지
// → form={form} 같은 props 타입 오류 방지

export const ButtonFlex = styled(Flex)`
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid #d9d9d9;
`;
// 버튼 영역
// justify-content: flex-end → 오른쪽 정렬
// border-top → 버튼 위에 구분선

export const CancelButton = styled(Button)`
  min-width: 5rem;
`;
// 취소 버튼 최소 너비 고정

export const SubmitButton = styled(Button)`
  min-width: 5rem;
`;
// 수정 버튼 최소 너비 고정

 

 

 

 

 

 

5. page 연결하기

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

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

export const NoticeUpdatePage = () => {
  return (
    <NoticeUpdate />
  )
}

 

 

\pages\notice\index.ts

export { NoticeListPage } from './ui/notice-list-page';
export { NoticeCreatePage } from './ui/notice-create-page';
export { NoticeDetailPage } from './ui/notice-detail-page';
export { NoticeUpdatePage } from './ui/notice-update-page';

 

\features\notice\index.ts

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