[React] 공지사항 게시판 만들기(1): list 공지사항 목록 조회

 

 

 

React를 활용한 공지사항 제작

 

 

 

 

 공지사항 기본 파일구조

 

 

 

공지사항 파일 플로어 다이어그램

 

 

1. 기본 파일 생성 하기

App.tsx (라우터)

import { NoticeListPage } from '@/pages/notice';
import { NoticeWritePage } from '@/pages/notice';
import { NoticeDetailPage } from '@/pages/notice';

<Route path="/notice" element={<NoticeListPage />} />
<Route path="/notice/write" element={<NoticeWritePage />} />
<Route path="/notice/detail/:id" element={<NoticeDetailPage />} />

 


pages/notice/ui/notice-list-page.tsx

import React from 'react'

export const NoticeListPage = () => {
  return (
    <div>
      <h1>공지사항 목록</h1>
    </div>
  );
};

 

pages/notice/ui/notice-detail-page.tsx

import { useParams } from 'react-router-dom';

export const NoticeDetailPage = () => {
  const { id } = useParams(); // URL에서 id 꺼냄 (/notice/detail/1 → id = "1")

  return (
    <div>
      <h1>{id}번 공지사항 상세</h1>
    </div>
  );
};

 

pages/notice/ui/notice-write-page.tsx

export const NoticeWritePage = () => {
  return (
    <div>
      <h1>공지사항 작성</h1>
    </div>
  );
};

 

pages/notice/index.ts 인덱스 연결

export { NoticeListPage } from './ui/notice-list-page';
export { NoticeWritePage } from './ui/notice-write-page';
export { NoticeDetailPage } from './ui/notice-detail-page';

 

 


 

2. 공지사항 API 연결

 

features/notice/index.ts

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

 

features/notice/model/types.ts

// 백엔드 응답/요청 타입 정의
export interface NoticeListItem { id: number; title: string; ... }
export interface NoticeDetail { id: number; title: string; content: string; ... }
export interface NoticeCreateRequest { title: string; content: string; ... }
export interface PageResponse<T> { items: T[]; totalElements: number; ... }

 

features/notice/api/notice-api.ts

import apiClient from '@/shared/api/api-client';

const BASE_URL = '/notices';

// apiClient.getData → 응답에서 data만 바로 반환
export const getNoticeList = async (page = 0, size = 20) =>
  apiClient.getData(`${BASE_URL}/company/${companyId}`, { params: { page, size } });

export const getNoticeDetail = async (id: number) =>
  apiClient.getData(`${BASE_URL}/${id}`);

export const createNotice = async (data: NoticeCreateRequest) =>
  apiClient.postData(`${BASE_URL}`, data);

 

 

 

features/notice/model/use-notice-list.ts

import { useState } from 'react';
// useState : 컴포넌트 안에서 변하는 값(상태)을 관리하는 리액트 내장 훅
// → page 번호가 바뀔 때마다 화면이 다시 그려져야 하므로 필요

import { useNavigate } from 'react-router-dom';
// useNavigate : 코드로 페이지를 이동시키는 훅
// → 행 클릭 시 /notice/detail/1 로 이동할 때 필요

import { useQuery } from '@tanstack/react-query';
// useQuery : API 호출 + 로딩상태 + 캐싱을 자동으로 처리해주는 훅
// → getNoticeList 호출하고 결과를 data에 담아줌

import { getNoticeList } from '../api/notice-api';
// getNoticeList : 우리가 만든 API 호출 함수
// → useQuery 안에서 실제로 서버에 요청을 보내는 역할

export const useNoticeList = () => {
  const navigate = useNavigate();
  // navigate : 페이지 이동 함수 꺼내기
    
  const [page, setPage] = useState(0);
  // page : 현재 페이지 번호 (0부터 시작)
  // setPage : page 값을 바꾸는 함수 → 페이지 클릭 시 사용
  // useState(0) : 초기값 0

  const [size] = useState(20);
  // size : 한 페이지에 보여줄 행 수 (20으로 고정)
  // setSize 없음 → 바꿀 일이 없으니 꺼내지 않음

  //API 호출
  const { data, isLoading } = useQuery({
  	//이 데이터의 고유 이름 (캐싱 키)
    queryKey: ['notice', 'list', page, size], // page, size가 바뀌면 자동으로 API 재호출됨
	//실제로 실행할 API 함수
    queryFn: () => getNoticeList(page, size), // page, size를 넘겨서 해당 페이지 데이터를 가져옴
  });

  // id를 받아서 해당 공지사항 상세 페이지로 이동
  const handleRowClick = (id: number) => {
    navigate(`/notice/detail/${id}`);   // 예: id=1 → /notice/detail/1
  };

  return {
    navigate,            // 페이지 이동 함수
    isLoading,           // API 로딩 중 여부 (true/false)
    data: data?.items ?? [],                  // data?.items : API 응답에서 목록 배열만 꺼냄
    totalElements: data?.totalElements ?? 0,  // 전체 데이터 수 (페이지네이션에 필요)
    page,                // 현재 페이지 번호
    size,                // 페이지당 행 수
    setPage,             // 페이지 변경 함수 → ag-Grid 페이지 클릭 시 사용
    handleRowClick,      // 행 클릭 이벤트 핸들러
  };
};

 


 

3. Features UI 공지사항 리스트 연결

features\notice\ui\notice-list.tsx

import { useMemo } from 'react';
// useMemo : columns 를 매번 새로 만들지 않고 캐싱
// 리렌더링 될 때마다 columns 재생성하는 불필요한 연산 방지

import { theme } from 'antd';
// theme.useToken() : AntD 디자인 토큰 접근
// 라이트/다크 모드 전환 시 색상 자동 대응

import type { ColumnsType } from 'antd/es/table';
// ColumnsType<T> : 테이블 컬럼 배열에 타입 지정
// T 에 NoticeListItem 넣으면 dataIndex 등에서 타입 자동완성 지원

import { useNoticeList } from '../model/use-notice-list';
// API 호출, 페이지네이션 상태, 클릭 이벤트 등
// 목록에 필요한 모든 값과 함수를 가져오는 커스텀 훅

import { Container, Header, LoadingFlex, NoticeSpinner, NoticeTable } from './notice-list.styles';
// 이 파일에서 직접 스타일 작성하지 않고 스타일 파일에서만 가져옴

import type { NoticeListItem } from '../model/types';
// 컬럼 타입 지정에 필요한 공지사항 목록 아이템 타입

export const NoticeList = () => {

  // 컴포넌트 선언/훅 연결: 훅에서 필요한 값들만 꺼내서 사용
  const {
    isLoading,      // API 호출 중 여부 (true: 로딩중, false: 완료)
    data,           // 공지사항 목록 배열
    totalElements,  // 전체 데이터 수 (페이지네이션 총 개수 표시용)
    page,           // 현재 페이지 번호 (0부터 시작)
    size,           // 페이지당 행 수
    setPage,        // 페이지 번호 변경 함수 (페이지 클릭 시 호출)
    handleRowClick, // 행 클릭 시 상세 페이지로 이동하는 함수
  } = useNoticeList();

  // 컬럼정의
  // 컴포넌트 최초 렌더링 시 한 번만 계산하고 이후 캐싱된 값 재사용
  const columns = useMemo<ColumnsType<NoticeListItem>>(() => [
    { title: '제목', dataIndex: 'title', key: 'title', width: 160, },
    { title: '등록일', dataIndex: 'registrationDateTime', key: 'registrationDateTime', width: 160, },
    { title: '상태', dataIndex: 'isActive', key: 'isActive', width: 160, },
  ], []);

  if(isLoading) return (
    <FlexLoading>
        {/* 데이터 오기 전 빈 테이블 노출 방지 */}
        <NoticeSpinner />  
    </FlexLoading>
  )

    return (
        <FlexContainer>
            <FlexHeader>
                <h2>공지사항</h2>
            </FlexHeader>
            <NoticeTable
            columns={columns} // 훅에서 가져온 목록 배열을 테이블에 바인딩
            dataSource={data}
            rowKey="id"
            onRow={(record) => ({
                onClick: () => handleRowClick(record.id), // 행 클릭 시 해당 공지사항 id로 상세 페이지 이동
            })}
            pagination={{
                current: page + 1,                  // AntD 페이지는 1부터 시작, 서버는 0부터 시작 → +1 보정
                pageSize: size,
                total: totalElements,               // 전체 데이터 수 기반으로 페이지 수 자동 계산
                onChange: (p) => setPage(p - 1),    // AntD에서 받은 페이지 번호 -1 해서 서버 형식으로 변환
            }}
            />
        </FlexContainer>
    );
};

 

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

import styled from 'styled-components';
import { Flex, Table, Spin } from 'antd';
import type { NoticeListItem } from '../model/types';

// 전체 페이지를 감싸는 최상위 컨테이너
// 세로 방향으로 Header, Table 순서로 배치
export const FlexContainer = styled(Flex)`
  height: 100%;
  flex-direction: column;
  gap: 0;
`;

// 헤더 영역
// 왼쪽 제목, 오른쪽 버튼 등을 양쪽 끝으로 배치
export const FlexHeader = styled(Flex)`
  padding: 1rem;
  justify-content: space-between;
  align-items: center;
`;

// API 호출 중일 때 스피너를 화면 정중앙에 띄우는 컨테이너
export const FlexLoading = styled(Flex)`
  min-height: 100vh;
  align-items: center;
  justify-content: center;
`;

// 목록 로딩 중 표시할 스피너
// 1.5rem = Spin size="large" 와 동일한 크기
export const NoticeSpinner = styled(Spin)`
  font-size: 1.5rem;
`;

export const NoticeTable = styled(Table<NoticeListItem>)`
  padding: 0 1rem;
  // Table<NoticeListItem> : dataSource, columns 에 NoticeListItem 타입 적용
  
  .ant-table-row {
    cursor: pointer;
  }
  // 행 클릭 가능함을 포인터 커서로 표시
`;

 

 

4. Page 연결

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

import { NoticeList } from '@/features/notice';

export const NoticeListPage = () => {
  return (
    <NoticeList />
  )
}

feature에서 제작한 공지사항 리스트 page에 연결