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';

'React' 카테고리의 다른 글
| [React] 공지사항 게시판 만들기(3): detail 공지사항 상세페이지 조회 (0) | 2026.03.23 |
|---|---|
| [React] 공지사항 게시판 만들기(2): create 공지사항 작성 (0) | 2026.03.23 |
| [React] 공지사항 게시판 만들기(1): list 공지사항 목록 조회 (0) | 2026.03.23 |
| [React] 프로젝트 생성방법 vite, CRA (0) | 2026.01.07 |
| [React] NextJS 새로운 페이지 생성 및 연결 방법 (0) | 2026.01.07 |