조민수 조민수 05-07
게시판 관리 게시판 마스터 목록 + (포토형, 일반형) 게시판 목록 [detail, 홈페이지 이동 추가해야함.]
@4f668c2bcc9ba7adb2e916d8ffe86d746508aa70
 
public/publish/adm/images/component/icon_page_first.svg (added)
+++ public/publish/adm/images/component/icon_page_first.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12" fill="none">
+  <path d="M6 2L2 6L6 10" stroke="#5B606C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+  <path d="M12 2L8 6L12 10" stroke="#5B606C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
 
public/publish/adm/images/component/icon_page_last.svg (added)
+++ public/publish/adm/images/component/icon_page_last.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12" fill="none">
+  <path d="M8 2L12 6L8 10" stroke="#5B606C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+  <path d="M2 2L6 6L2 10" stroke="#5B606C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
 
public/publish/adm/images/component/icon_page_next.svg (added)
+++ public/publish/adm/images/component/icon_page_next.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="8" height="12" viewBox="0 0 8 12" fill="none">
+  <path d="M2 2L6 6L2 10" stroke="#5B606C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
 
public/publish/adm/images/component/icon_page_prev.svg (added)
+++ public/publish/adm/images/component/icon_page_prev.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="8" height="12" viewBox="0 0 8 12" fill="none">
+  <path d="M6 2L2 6L6 10" stroke="#5B606C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
src/admin/component/EmptyRow.tsx
--- src/admin/component/EmptyRow.tsx
+++ src/admin/component/EmptyRow.tsx
@@ -1,15 +1,13 @@
-export function EmptyRow({colSpan}: any) {
+interface EmptyRowProps {
+    colSpan: number;
+}
+
+export function EmptyRow({colSpan}: EmptyRowProps) {
     return (
-        <div className="table table_type_cols">
-            <table>
-                <tbody>
-                <tr>
-                    <td colSpan={colSpan} style={{textAlign: "center"}}>
-                        데이터가 없습니다.
-                    </td>
-                </tr>
-                </tbody>
-            </table>
-        </div>
+        <tr>
+            <td colSpan={colSpan} style={{textAlign: "center"}}>
+                데이터가 없습니다.
+            </td>
+        </tr>
     );
 }
(No newline at end of file)
 
src/admin/component/ListSearchForm.tsx (added)
+++ src/admin/component/ListSearchForm.tsx
@@ -0,0 +1,71 @@
+import type {ChangeEvent} from 'react'
+import {SearchBar} from "./SearchBar.tsx";
+import type {SearchParams} from "../../type/searchParams.ts";
+import {PageSizeSelector} from "./pagination/PageSizeSelector.tsx";
+
+interface ListSearchFormProps<T extends SearchParams> {
+    totalItems: number;
+    searchParams: T;
+    onChange: (params: T) => void;
+    searchOptions: { value: string; label: string; }[];
+    pageSizeOptions?: { value: string; label: string; }[];
+    totalLabel?: string;
+}
+
+export function ListSearchForm<T extends SearchParams>({
+                                                           totalItems,
+                                                           searchParams,
+                                                           onChange,
+                                                           searchOptions,
+                                                           pageSizeOptions,
+                                                           totalLabel,
+
+                                                       }: ListSearchFormProps<T>) {
+    const handleChange = (
+        name: string,
+        value: string
+    ) => {
+        onChange({
+            ...searchParams,
+            [name]: value,
+            pageIndex: 1,
+        });
+    };
+
+    const handlePageChange = (event: ChangeEvent<HTMLSelectElement>) => {
+        const {name, value} = event.target
+        onChange({
+            ...searchParams,
+            [name]: Number(value),
+            pageIndex: 1,
+        })
+    }
+
+    return (
+        <div className="search_area">
+            <div className="search_left">
+                <p className="total_number">
+                    {totalLabel} <b>{totalItems}</b>
+                </p>
+            </div>
+
+            <div className="search_right">
+                <SearchBar
+                    searchCnd={searchParams.searchCnd}
+                    searchKeyword={searchParams.searchKeyword}
+                    options={searchOptions}
+                    onChange={handleChange}
+                />
+                {
+                    pageSizeOptions && pageSizeOptions.length > 0 && (
+                        <PageSizeSelector
+                            value={searchParams.pageUnit}
+                            options={pageSizeOptions}
+                            onChange={handlePageChange}
+                        />
+                    )
+                }
+            </div>
+        </div>
+    )
+}
src/admin/component/SearchBar.tsx
--- src/admin/component/SearchBar.tsx
+++ src/admin/component/SearchBar.tsx
@@ -1,22 +1,20 @@
 import type {ChangeEvent} from 'react'
 
 interface SearchBarProps {
-    searchSortCnd: string
+    searchCnd: string
     searchKeyword: string
     options: { value: string; label: string }[]
     onChange: (
         name: string,
         value: string
     ) => void
-    onSearch: () => void
 }
 
 export const SearchBar = ({
-                              searchSortCnd,
+                              searchCnd,
                               searchKeyword,
                               options,
-                              onChange,
-                              onSearch
+                              onChange
                           }: SearchBarProps) => {
     const handleChange = (event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
         onChange(event.target.name, event.target.value);
@@ -26,12 +24,11 @@
         <>
             <select
                 id="searchCnd"
-                name="searchSortCnd"
+                name="searchCnd"
                 className="search_select"
-                value={searchSortCnd}
+                value={searchCnd}
                 onChange={handleChange}
             >
-                <option value="">전체</option>
                 {options.map((option) => (
                     <option key={option.value} value={option.value}>
                         {option.label}
@@ -51,15 +48,6 @@
                     placeholder="검색어를 입력하세요"
                 />
             </div>
-
-            <button
-                type="button"
-                className="btn btn_search"
-                onClick={onSearch}
-            >
-                검색
-            </button>
         </>
     )
 }
-
 
src/admin/component/pagination/Pagination.tsx (added)
+++ src/admin/component/pagination/Pagination.tsx
@@ -0,0 +1,96 @@
+type PaginationProps = {
+    totalItems: number;
+    totalPages: number;
+    currentPage: number;
+    size: number;
+    onPageChange: (page: number) => void;
+};
+
+export function Pagination({
+                               totalItems,
+                               totalPages,
+                               currentPage,
+                               size = 10,
+                               onPageChange,
+                           }: PaginationProps) {
+
+    if (totalItems === 0) {
+        return null;
+    }
+
+    const startPage =
+        Math.floor((currentPage - 1) / size)
+        * size + 1;
+
+    const endPage =
+        Math.min(startPage + size - 1, totalPages);
+
+    const pages = Array.from(
+        {length: endPage - startPage + 1},
+        (_, idx) => startPage + idx
+    );
+
+    return (
+        <div className="page">
+
+            <button
+                className="btn_page_first"
+                aria-label="첫 페이지"
+                onClick={() => onPageChange(1)}
+                disabled={currentPage === 1}
+            >
+                <i></i>
+            </button>
+
+            <button
+                className="btn_page_prev"
+                aria-label="이전 페이지"
+                onClick={() =>
+                    onPageChange(
+                        Math.max(startPage - size, 1)
+                    )
+                }
+                disabled={startPage === 1}
+            >
+                <i></i>
+            </button>
+
+            {pages.map(page => (
+                <button
+                    key={page}
+                    className={page === currentPage ? 'on' : ''}
+                    onClick={() => onPageChange(page)}
+                    disabled={page === currentPage}
+                >
+                    {page}
+                </button>
+            ))}
+
+            <button
+                className="btn_page_next"
+                aria-label="다음 페이지"
+                onClick={() =>
+                    onPageChange(
+                        Math.min(
+                            startPage + size,
+                            totalPages
+                        )
+                    )
+                }
+                disabled={endPage >= totalPages}
+            >
+                <i></i>
+            </button>
+
+            <button
+                className="btn_page_last"
+                aria-label="마지막 페이지"
+                onClick={() => onPageChange(totalPages)}
+                disabled={currentPage === totalPages}
+            >
+                <i></i>
+            </button>
+
+        </div>
+    );
+}
src/admin/feature/board/api/boardApi.ts
--- src/admin/feature/board/api/boardApi.ts
+++ src/admin/feature/board/api/boardApi.ts
@@ -1,8 +1,17 @@
 import {apiClient} from "../../../../api/apiClient.ts";
 import type {PageResponse} from "../../../../type/pageResponse.ts";
-import type {SearchParams} from "../../../../type/searchParams.ts";
-import type {BoardListItem} from "../type/board.types.ts";
+import type {
+    BoardArticleExtra,
+    BoardArticleListItem,
+    BoardArticleSearchParams,
+    BoardListItem,
+    BoardSearchParams
+} from "../type/board.types.ts";
 
-export async function fetchBoardList(params: SearchParams) {
+export async function fetchBoardList(params: BoardSearchParams) {
     return apiClient.get<PageResponse<BoardListItem>>('/cop/bbs/list.do', params);
 }
+
+export async function fetchBoardArticleList(params: BoardArticleSearchParams) {
+    return apiClient.get<PageResponse<BoardArticleListItem, BoardArticleExtra>>('/cop/bbs/boardList.do', params);
+}
 
src/admin/feature/board/components/BoardListSearchForm.tsx (deleted)
--- src/admin/feature/board/components/BoardListSearchForm.tsx
@@ -1,84 +0,0 @@
-import {useState, type ChangeEvent} from 'react'
-import {SearchBar} from "../../../component/SearchBar.tsx";
-import type {SearchParams} from "../../../../type/searchParams.ts";
-import {PageSizeSelector} from "../../../component/pagination/PageSizeSelector.tsx";
-
-interface BoardListSearchFormProps {
-    totalCount: number
-    searchParams: SearchParams
-    onChange: (params: SearchParams) => void
-}
-
-const searchOptions = [
-    {value: '0', label: '게시판명/연결메뉴'},
-    {value: '1', label: '게시판유형'},
-]
-
-const pageSizeOptions = [
-    {value: '10', label: '10건씩'},
-    {value: '20', label: '20건씩'},
-    {value: '30', label: '30건씩'},
-]
-
-export function BoardListSearchForm({
-                                        totalCount,
-                                        searchParams,
-                                        onChange
-                                    }: BoardListSearchFormProps) {
-    const [draftParams, setDraftParams] = useState({
-        searchSortCnd: searchParams.searchSortCnd,
-        searchKeyword: searchParams.searchKeyword,
-    });
-
-    const handleChange = (
-        name: string,
-        value: string
-    ) => {
-        setDraftParams((prev) => ({
-            ...prev,
-            [name]: value,
-        }));
-    };
-
-    const handleSearch = () => {
-        onChange({
-            ...searchParams,
-            ...draftParams,
-            pageIndex: 1,
-        });
-    };
-
-    const handlePageChange = (event: ChangeEvent<HTMLSelectElement>) => {
-        const {name, value} = event.target
-        onChange({
-            ...searchParams,
-            [name]: Number(value),
-            pageIndex: 1,
-        })
-    }
-
-    return (
-        <div className="search_area">
-            <div className="search_left">
-                <p className="total_number">
-                    게시물 <b>{totalCount}</b>
-                </p>
-            </div>
-
-            <div className="search_right">
-                <SearchBar
-                    searchSortCnd={draftParams.searchSortCnd}
-                    searchKeyword={draftParams.searchKeyword}
-                    options={searchOptions}
-                    onChange={handleChange}
-                    onSearch={handleSearch}
-                />
-                <PageSizeSelector
-                    value={searchParams.pageUnit}
-                    options={pageSizeOptions}
-                    onChange={handlePageChange}
-                />
-            </div>
-        </div>
-    )
-}
 
src/admin/feature/board/components/BoardListTable.tsx (deleted)
--- src/admin/feature/board/components/BoardListTable.tsx
@@ -1,41 +0,0 @@
-import type {BoardListItem} from "../type/board.types.ts";
-import type {SearchParams} from "../../../../type/searchParams.ts";
-import {EmptyRow} from "../../../component/EmptyRow.tsx";
-import {BoardListTableHeader} from "./BoardListTableHeader.tsx";
-import {BoardListTableRow} from "./BoardListTableRow.tsx";
-
-interface BoardListTableProps {
-    items: BoardListItem[]
-    params: SearchParams
-    onChange: (params: SearchParams) => void
-    totalCount: number
-    currentPage: number
-    recordPerPage: number
-}
-
-export function BoardListTable({items, params, onChange, totalCount, currentPage, recordPerPage}: BoardListTableProps) {
-    if (!items.length) {
-        return <EmptyRow colSpan={9}/>
-    }
-
-    return (
-        <div className="table table_type_cols">
-            <table>
-                <BoardListTableHeader params={params} onChange={onChange}/>
-                <tbody>
-                {items.map((item, index) => (
-                    <BoardListTableRow
-                        key={item.bbsId}
-                        item={item}
-                        index={index}
-                        searchParams={params}
-                        totalCount={totalCount}
-                        currentPage={currentPage}
-                        recordPerPage={recordPerPage}
-                    />
-                ))}
-                </tbody>
-            </table>
-        </div>
-    )
-}
 
src/admin/feature/board/components/BoardListTableRow.tsx (deleted)
--- src/admin/feature/board/components/BoardListTableRow.tsx
@@ -1,44 +0,0 @@
-import type {BoardListItem} from "../type/board.types.ts";
-import type {SearchParams} from "../../../../type/searchParams.ts";
-
-interface BoardListTableRowProps {
-    item: BoardListItem
-    index: number
-    searchParams: SearchParams
-    totalCount: number
-    currentPage: number
-    recordPerPage: number
-}
-
-export function BoardListTableRow({item, index, searchParams, totalCount, currentPage, recordPerPage}: BoardListTableRowProps) {
-    const rowNumber = searchParams.searchSortOrd === 'DESC'
-        ? totalCount - (currentPage - 1) * recordPerPage - index
-        : (currentPage - 1) * recordPerPage + (index + 1)
-
-    return (
-        <tr>
-            <td>{rowNumber}</td>
-            <td>{item.bbsNm}</td>
-            <td>{item.menuNm}</td>
-            <td>
-                {item.newCnt}/{item.totCnt}
-            </td>
-            <td>{item.bbsTyCodeNm}</td>
-            <td>{item.frstRegisterPnttm}</td>
-            <td>
-                {item.useAt === 'Y' ? (
-                    <span className="status text blue">사용</span>
-                ) : (
-                    <span className="status text gray">미사용</span>
-                )}
-            </td>
-            <td>
-                <div className="btn_wrap center">
-                    <button className="btn line primary small">수정</button>
-                    <button className="btn line lightgray small">게시판보기</button>
-                    <button className="btn line lightgray small">템플릿</button>
-                </div>
-            </td>
-        </tr>
-    )
-}
 
src/admin/feature/board/components/article/BoardArticleImageListTable.tsx (added)
+++ src/admin/feature/board/components/article/BoardArticleImageListTable.tsx
@@ -0,0 +1,18 @@
+import {BoardArticleImageListTableRow} from "./BoardArticleImageListTableRow.tsx";
+import type {BoardArticleListItem} from "../../type/board.types.ts";
+
+type BoardArticleImageListTableProps = {
+    items: BoardArticleListItem[];
+}
+
+export const BoardArticleImageListTable = ({items}: BoardArticleImageListTableProps) => {
+    return (
+        <ul className={"gallery_list"}>
+            {items.length > 0 ? items.map((item, index) =>
+                (<BoardArticleImageListTableRow
+                    key={index}
+                    item={item}
+                />)) : null}
+        </ul>
+    )
+}(No newline at end of file)
 
src/admin/feature/board/components/article/BoardArticleImageListTableRow.tsx (added)
+++ src/admin/feature/board/components/article/BoardArticleImageListTableRow.tsx
@@ -0,0 +1,28 @@
+import type {BoardArticleListItem} from "../../type/board.types.ts";
+
+type BoardArticleImageListTableRowProps = {
+    item: BoardArticleListItem
+
+}
+
+export const BoardArticleImageListTableRow = ({item}: BoardArticleImageListTableRowProps) => {
+    return (
+        <li>
+            <div className="images_area">
+                <a href="#" className="link-detail">
+                    <img alt={item.nttSj} src={`/uss/ion/image.do?atchFileId=${item.atchFileId}&fileSn=${item.fileSn}`}/>
+                </a>
+            </div>
+            <div className="list_content">
+                <b className="list_title">
+                    {item.nttSj}
+                </b>
+                <ul className="list_info">
+                    <li>작성자 : {item.frstRegisterNm}</li>
+                    <li>조회수 : {item.inqireCo}</li>
+                    <li>{item.frstRegisterPnttm}</li>
+                </ul>
+            </div>
+        </li>
+    )
+}(No newline at end of file)
 
src/admin/feature/board/components/article/BoardArticleListTable.tsx (added)
+++ src/admin/feature/board/components/article/BoardArticleListTable.tsx
@@ -0,0 +1,52 @@
+import type {BoardArticleListItem, BoardArticleSearchParams} from "../../type/board.types.ts";
+import {EmptyRow} from "../../../../component/EmptyRow.tsx";
+import {BoardArticleListTableHeader} from "./BoardArticleListTableHeader.tsx";
+import {BoardArticleListTableRow} from "./BoardArticleListTableRow.tsx";
+
+type BoartArticleListTableProps = {
+
+    items: BoardArticleListItem[];
+    params: BoardArticleSearchParams;
+    onChange: (params: BoardArticleSearchParams) => void;
+    totalItems: number
+    currentPage: number
+    totalPages: number
+}
+
+export const BoartArticleListTable = ({
+                                          items,
+                                          params,
+                                          onChange,
+                                          totalItems,
+                                          currentPage,
+                                          totalPages
+                                      }: BoartArticleListTableProps) => {
+
+    return (
+        <div className={"table table_type_cols"}>
+            <table>
+                <BoardArticleListTableHeader
+                    params={params}
+                    onChange={onChange}
+                />
+                <tbody>
+                {items.length > 0 ?
+                        items.map((item, index) => (
+                            <BoardArticleListTableRow
+                                key={item.nttId}
+                                item={item}
+                                index={index}
+                                searchParams={params}
+                                totalItems={totalItems}
+                                currentPage={currentPage}
+                                totalPages={totalPages}
+                            />
+                        )) :
+                    (<EmptyRow colSpan={8}/>)
+                }
+                </tbody>
+            </table>
+        </div>
+    )
+
+}(No newline at end of file)
 
src/admin/feature/board/components/article/BoardArticleListTableHeader.tsx (added)
+++ src/admin/feature/board/components/article/BoardArticleListTableHeader.tsx
@@ -0,0 +1,82 @@
+import type {BoardArticleSearchParams} from "../../type/board.types.ts";
+
+type BoardArticleListTableHeaderProps = {
+    params: BoardArticleSearchParams;
+    onChange: (params: BoardArticleSearchParams) => void;
+}
+
+export const BoardArticleListTableHeader = ({
+                                                params,
+                                                onChange
+                                            }: BoardArticleListTableHeaderProps) => {
+    const handleSort = (field: string) => {
+        const nextOrder =
+            params.searchSortCnd === field &&
+            params.searchSortOrd === 'ASC'
+                ? 'DESC'
+                : 'ASC';
+
+        onChange({
+            ...params,
+            searchSortCnd: field,
+            searchSortOrd: nextOrder,
+            pageIndex: 1,
+        });
+    };
+
+    const getSortIcon = (field: string) => {
+
+        if (params.searchSortCnd !== field) {
+            return '-';
+        }
+
+        return params.searchSortOrd === 'ASC'
+            ? '▲'
+            : '▼';
+    };
+
+
+    return (
+        <>
+            <colgroup>
+                <col style={{width: "40px"}}/>
+                <col style={{width: "6%"}}/>
+                <col style={{width: "auto"}}/>
+                <col style={{width: "15%"}}/>
+                <col style={{width: "15%"}}/>
+                <col style={{width: "10%"}}/>
+                <col style={{width: "12%"}}/>
+                <col style={{width: "8%"}}/>
+            </colgroup>
+
+            <thead>
+            <tr>
+                <th>
+                    <input type={"checkbox"} name={"checkbox"}/>
+                    <label htmlFor={"checkbox"}></label>
+                </th>
+                <th>번호</th>
+                <th scope={"col"}>
+                    제목
+                </th>
+                <th scope={"col"}>
+                    첨부파일
+                </th>
+                <th scope={"col"}>
+                    공개여부
+                </th>
+                <th scope="col">작성자</th>
+                <th scope="col">작성일
+                    <button
+                        className={`sort sortBtn ${params.searchSortCnd === 'FRST_REGIST_PNTTM' ? 'active' : ''}`}
+                        onClick={() => handleSort('FRST_REGIST_PNTTM')}
+                    >
+                        {getSortIcon('FRST_REGIST_PNTTM')}
+                    </button>
+                </th>
+                <th scope="col">조회수</th>
+            </tr>
+            </thead>
+        </>
+    )
+}(No newline at end of file)
 
src/admin/feature/board/components/article/BoardArticleListTableRow.tsx (added)
+++ src/admin/feature/board/components/article/BoardArticleListTableRow.tsx
@@ -0,0 +1,37 @@
+import type {BoardArticleListItem} from "../../type/board.types.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+
+type BoardArticleListTableRowProps = {
+    item: BoardArticleListItem
+    index: number
+    searchParams: SearchParams
+    totalItems: number
+    currentPage: number
+    totalPages: number
+}
+
+export const BoardArticleListTableRow = ({
+                                             item,
+                                             index,
+                                             searchParams,
+                                             totalItems,
+                                             currentPage,
+                                             totalPages,
+                                         }: BoardArticleListTableRowProps) => {
+
+    const rowNumber = searchParams.searchSortOrd === 'DESC'
+        ? totalItems - (currentPage - 1) * totalPages - index
+        : (currentPage - 1) * totalPages + (index + 1)
+    return (
+        <tr>
+            <td></td>
+            <td>{rowNumber}</td>
+            <td>{item.nttSj}</td>
+            <td>{item.atchFileId}</td>
+            <td>{item.secretAt}</td>
+            <td>{item.frstRegisterNm}</td>
+            <td>{item.frstRegisterPnttm}</td>
+            <td>{item.inqireCo}</td>
+        </tr>
+    )
+}(No newline at end of file)
 
src/admin/feature/board/components/master/BoardListTable.tsx (added)
+++ src/admin/feature/board/components/master/BoardListTable.tsx
@@ -0,0 +1,56 @@
+import type {BoardListItem} from "../../type/board.types.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+import {EmptyRow} from "../../../../component/EmptyRow.tsx";
+import {BoardListTableHeader} from "./BoardListTableHeader.tsx";
+import {BoardListTableRow} from "./BoardListTableRow.tsx";
+
+interface BoardListTableProps {
+    items: BoardListItem[]
+    params: SearchParams
+    onChange: (params: SearchParams) => void
+    totalItems: number
+    currentPage: number
+    totalPages: number
+    onDetail: (bbsId: string) => void
+    onArticleList: (bbsId: string) => void
+    onPreview: (bbsId: string) => void
+}
+
+export function BoardListTable({
+                                   items,
+                                   params,
+                                   onChange,
+                                   totalItems,
+                                   currentPage,
+                                   totalPages,
+                                   onDetail,
+                                   onArticleList,
+                                   onPreview
+                               }: BoardListTableProps) {
+
+    return (
+        <div className="table table_type_cols">
+            <table>
+                <BoardListTableHeader params={params} onChange={onChange}/>
+                <tbody>
+                {items.length > 0 ?
+                    items.map((item, index) => (
+                        <BoardListTableRow
+                            key={item.bbsId}
+                            item={item}
+                            index={index}
+                            searchParams={params}
+                            totalItems={totalItems}
+                            currentPage={currentPage}
+                            totalPages={totalPages}
+                            onDetail={onDetail}
+                            onArticleList={onArticleList}
+                            onPreview={onPreview}
+                        />))
+                    : (<EmptyRow colSpan={9}/>)
+                }
+                </tbody>
+            </table>
+        </div>
+    )
+}
src/admin/feature/board/components/master/BoardListTableHeader.tsx (Renamed from src/admin/feature/board/components/BoardListTableHeader.tsx)
--- src/admin/feature/board/components/BoardListTableHeader.tsx
+++ src/admin/feature/board/components/master/BoardListTableHeader.tsx
@@ -1,4 +1,4 @@
-import type {SearchParams} from "../../../../type/searchParams.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
 
 interface BoardListTableHeaderProps {
     params: SearchParams;
 
src/admin/feature/board/components/master/BoardListTableRow.tsx (added)
+++ src/admin/feature/board/components/master/BoardListTableRow.tsx
@@ -0,0 +1,58 @@
+import type {BoardListItem} from "../../type/board.types.ts";
+import type {SearchParams} from "../../../../../type/searchParams.ts";
+
+interface BoardListTableRowProps {
+    item: BoardListItem
+    index: number
+    searchParams: SearchParams
+    totalItems: number
+    currentPage: number
+    totalPages: number
+    onDetail: (bbsId: string) => void
+    onArticleList: (bbsId: string) => void
+    onPreview: (bbsId: string) => void
+}
+
+export function BoardListTableRow({
+                                      item,
+                                      index,
+                                      searchParams,
+                                      totalItems,
+                                      currentPage,
+                                      totalPages,
+                                      onDetail,
+                                      onArticleList,
+                                      onPreview
+                                  }: BoardListTableRowProps) {
+    const rowNumber = searchParams.searchSortOrd === 'DESC'
+        ? totalItems - (currentPage - 1) * totalPages - index
+        : (currentPage - 1) * totalPages + (index + 1)
+    const bbsId = item.bbsId;
+
+    return (
+        <tr>
+            <td>{rowNumber}</td>
+            <td>{item.bbsNm}</td>
+            <td>{item.menuNm}</td>
+            <td>
+                {item.newCnt}/{item.totCnt}
+            </td>
+            <td>{item.bbsTyCodeNm}</td>
+            <td>{item.frstRegisterPnttm}</td>
+            <td>
+                {item.useAt === 'Y' ? (
+                    <span className="status text blue">사용</span>
+                ) : (
+                    <span className="status text gray">미사용</span>
+                )}
+            </td>
+            <td>
+                <div className="btn_wrap center">
+                    <button className="btn line primary small" onClick={() => onDetail(bbsId)}>수정</button>
+                    <button className="btn line lightgray small" onClick={() => onArticleList(bbsId)}>게시판보기</button>
+                    <button className="btn line lightgray small" onClick={() => onPreview(bbsId)}>홈페이지</button>
+                </div>
+            </td>
+        </tr>
+    )
+}
 
src/admin/feature/board/hook/useBoardArticleList.ts (added)
+++ src/admin/feature/board/hook/useBoardArticleList.ts
@@ -0,0 +1,23 @@
+import {keepPreviousData, useQuery} from "@tanstack/react-query";
+import {fetchBoardArticleList} from "../api/boardApi.ts";
+import type {BoardArticleSearchParams} from "../type/board.types.ts";
+
+export function useBoardArticleList(searchParams: BoardArticleSearchParams) {
+    const query = useQuery({
+        queryKey: ['boardArticleList'],
+        queryFn: () => fetchBoardArticleList(searchParams),
+        placeholderData: keepPreviousData,
+    });
+    console.log("useBoardArticleList", query);
+
+    return {
+        list : query.data?.list ?? [],
+        extraData: query.data?.extraData ?? null,
+        currentPage: query.data?.currentPage ?? 0,
+        totalItems: query.data?.totalItems ?? 0,
+        totalPages: query.data?.totalPages ?? 0,
+        size: query.data?.size ?? 0,
+        isLoading: query.isLoading,
+        error: query.error,
+    }
+}(No newline at end of file)
src/admin/feature/board/hook/useBoardList.ts
--- src/admin/feature/board/hook/useBoardList.ts
+++ src/admin/feature/board/hook/useBoardList.ts
@@ -12,9 +12,9 @@
 
     return {
         list: query.data?.list ?? [],
-        totalCount: query.data?.totalCount ?? 0,
+        totalItems: query.data?.totalItems ?? 0,
         currentPage: query.data?.currentPage ?? 0,
-        recordPerPage: query.data?.recordPerPage ?? 0,
+        totalPages: query.data?.totalPages ?? 0,
         isLoading: query.isLoading,
         error: query.error,
     }
 
src/admin/feature/board/page/BoardArticleListPage.tsx (added)
+++ src/admin/feature/board/page/BoardArticleListPage.tsx
@@ -0,0 +1,90 @@
+import {useParams} from "react-router-dom";
+import {useBoardArticleList} from "../hook/useBoardArticleList.ts";
+import type {BoardArticleSearchParams} from "../type/board.types.ts";
+import {useState} from "react";
+import {PageHeader} from "../../../component/PageHeader.tsx";
+import {ListSearchForm} from "../../../component/ListSearchForm.tsx";
+import {Pagination} from "../../../component/pagination/Pagination.tsx";
+import {useLoadingToast} from "../../../hook/useLoadingToast.ts";
+import {BoartArticleListTable} from "../components/article/BoardArticleListTable.tsx";
+import {BoardArticleImageListTable} from "../components/article/BoardArticleImageListTable.tsx";
+
+const initSearchParam: BoardArticleSearchParams = {
+    pageIndex: 1,
+    pageUnit: 10,
+    searchCnd: "0",
+    searchKeyword: "",
+    searchSortCnd: "FRST_REGIST_PNTTM",
+    searchSortOrd: "ASC",
+    bbsId: ""
+};
+
+export const BoardArticleListPage = () => {
+    const {bbsId = ''} = useParams();
+    const searchOptions = [
+        {value: '0', label: '제목'},
+        {value: '1', label: '내용'},
+        {value: '2', label: '작성자'},
+    ]
+
+    const [searchParams, setSearchParams] = useState<BoardArticleSearchParams>({
+        ...initSearchParam,
+        bbsId,
+    });
+    const {
+        list,
+        extraData,
+        totalItems,
+        currentPage,
+        totalPages,
+        size,
+        isLoading,
+        error
+    } = useBoardArticleList(searchParams);
+
+    const bbsNm = extraData ? extraData.boardMaster?.bbsNm : null;
+    const bbsTyCode = extraData ? extraData.boardMaster?.bbsTyCode : null;
+    useLoadingToast({
+        isLoading,
+        error,
+        successMessage: `${bbsNm} 목록을 불러왔습니다.`
+    });
+
+    const title = `${bbsNm} 목록`
+    const breadcrumb = [{label: '게시판 관리', url: '/admin/cop/bbs/SelectBBSMasterInfs.do'}, {label: `${bbsNm} 목록`}]
+    const homeUrl = '#'
+
+    return (
+        <>
+            <PageHeader title={title} breadcrumb={breadcrumb} homeUrl={homeUrl}/>
+            <ListSearchForm
+                totalItems={totalItems}
+                searchParams={searchParams}
+                onChange={setSearchParams}
+                searchOptions={searchOptions}
+                totalLabel={"게시글"}
+            />
+            {bbsTyCode === "BBST05" ?
+                <BoardArticleImageListTable
+                    items={list}
+                /> :
+                <BoartArticleListTable
+                    items={list}
+                    params={searchParams}
+                    onChange={setSearchParams}
+                    totalPages={totalPages}
+                    currentPage={currentPage}
+                    totalItems={totalItems}
+                />
+            }
+            <Pagination
+                totalItems={totalItems}
+                totalPages={totalPages}
+                currentPage={currentPage}
+                size={size}
+                onPageChange={() => {
+                }}
+            />
+        </>
+    );
+}(No newline at end of file)
src/admin/feature/board/page/BoardListPage.tsx
--- src/admin/feature/board/page/BoardListPage.tsx
+++ src/admin/feature/board/page/BoardListPage.tsx
@@ -1,47 +1,58 @@
 import {useBoardListQuery} from "../hook/useBoardList.ts";
 import {PageHeader} from "../../../component/PageHeader.tsx";
-import {useEffect, useRef, useState} from "react";
-import type {SearchParams} from "../../../../type/searchParams.ts";
-import {BoardListSearchForm} from "../components/BoardListSearchForm.tsx";
-import {BoardListTable} from "../components/BoardListTable.tsx";
-import {toast} from "react-toastify";
+import {useState} from "react";
+import {ListSearchForm} from "../../../component/ListSearchForm.tsx";
+import {BoardListTable} from "../components/master/BoardListTable.tsx";
+import {useNavigate} from "react-router-dom";
+import type {BoardSearchParams} from "../type/board.types.ts";
+import {useLoadingToast} from "../../../hook/useLoadingToast.ts";
 
-const initSearchParam: SearchParams = {
+const initSearchParam: BoardSearchParams = {
     pageIndex: 1,
     pageUnit: 10,
+    searchCnd: "0",
     searchKeyword: "",
     searchSortCnd: "BBS_NM",
     searchSortOrd: "ASC"
 }
 
 export const BoardListPage = () => {
-    const [searchParams, setSearchParams] = useState<SearchParams>(initSearchParam);
+    const searchOptions = [
+        {value: '0', label: '게시판명/연결메뉴'},
+        {value: '1', label: '게시판유형'},
+    ]
+
+    const pageSizeOptions = [
+        {value: '10', label: '10건씩'},
+        {value: '20', label: '20건씩'},
+        {value: '30', label: '30건씩'},
+    ]
+    const [searchParams, setSearchParams] = useState<BoardSearchParams>(initSearchParam);
     const {
         list,
-        totalCount,
+        totalItems,
         currentPage,
-        recordPerPage,
+        totalPages,
         isLoading,
         error
     } = useBoardListQuery(searchParams);
+    const navigate = useNavigate();
 
-    const toastId = useRef<string | number | null>(null);
+    const handleDetail = (bbsId: string) => {
+        navigate(`/admin/cop/bbs/detail/${bbsId}`);
+    }
+    const handleArticleList = (bbsId: string) => {
+        navigate(`/detail/cop/bbs/article/${bbsId}`);
+    }
+    const handlePreview = (bbsId: string) => {
+        navigate(`/preview/${bbsId}`);
+    }
 
-    useEffect(() => {
-        if (isLoading) {
-            toastId.current = toast.info("Loading...");
-        }
-
-        if (!isLoading && toastId.current) {
-            toast.dismiss(toastId.current);
-            toast.success("로드 완료!");
-        }
-
-        if (error && toastId.current) {
-            toast.dismiss(toastId.current);
-            toast.error(error.message);
-        }
-    }, [isLoading, error]);
+    useLoadingToast({
+        isLoading,
+        error,
+        successMessage : '게시판 마스터 목록을 조회하였습니다.'
+    });
 
     const title = '게시판 관리'
     const breadcrumb = [{label: '게시판 관리'}]
@@ -54,19 +65,26 @@
                 breadcrumb={breadcrumb}
                 homeUrl={homeUrl}
             />
-            <BoardListSearchForm
-                totalCount={totalCount}
+            <ListSearchForm
+                totalItems={totalItems}
                 searchParams={searchParams}
                 onChange={setSearchParams}
+                searchOptions={searchOptions}
+                pageSizeOptions={pageSizeOptions}
+                totalLabel={"게시판"}
             />
             {isLoading && <p>Loading...</p>}
+
             <BoardListTable
                 items={list}
                 params={searchParams}
                 onChange={setSearchParams}
-                totalCount={totalCount}
+                totalItems={totalItems}
                 currentPage={currentPage}
-                recordPerPage={recordPerPage}
+                totalPages={totalPages}
+                onDetail={handleDetail}
+                onArticleList={handleArticleList}
+                onPreview={handlePreview}
             />
         </>
     )
src/admin/feature/board/type/board.types.ts
--- src/admin/feature/board/type/board.types.ts
+++ src/admin/feature/board/type/board.types.ts
@@ -1,3 +1,13 @@
+import type {SearchParams} from "../../../../type/searchParams.ts";
+
+export interface BoardSearchParams extends SearchParams {
+
+}
+
+export interface BoardArticleSearchParams extends SearchParams {
+    bbsId: string;
+}
+
 export interface BoardListItem {
     bbsId: string
     bbsNm: string
@@ -7,4 +17,24 @@
     bbsTyCodeNm: string
     frstRegisterPnttm: string
     useAt: 'Y' | 'N'
+}
+
+export interface BoardArticleListItem {
+    bbsId: string;
+    nttId: string;
+    replyLc: string;
+    nttSj: string;
+    atchFileId: string;
+    secretAt: string;
+    frstRegisterNm: string;
+    frstRegisterPnttm: string;
+    inqireCo: string;
+    fileSn: string;
+}
+
+export interface BoardArticleExtra {
+    boardMaster?: {
+        bbsNm: string;
+        bbsTyCode: string;
+    }
 }
(No newline at end of file)
 
src/admin/hook/useLoadingToast.ts (added)
+++ src/admin/hook/useLoadingToast.ts
@@ -0,0 +1,35 @@
+import {useEffect, useRef} from "react";
+import {type Id, toast} from "react-toastify";
+
+type UseLoadingToastProps = {
+    isLoading: boolean;
+    error?: Error | null;
+    loadingMessage?: string;
+    successMessage?: string;
+}
+
+export const useLoadingToast = ({
+                                    isLoading,
+                                    error,
+                                    loadingMessage = 'Loading ...',
+                                    successMessage = '완료'
+                                }: UseLoadingToastProps) => {
+    const toastId = useRef<Id | null>(null);
+
+    useEffect(() => {
+        if (isLoading && !toastId.current) {
+            toastId.current = toast.info(loadingMessage);
+            return;
+        }
+        if (!isLoading && toastId.current) {
+            toast.dismiss(toastId.current);
+            toastId.current = null;
+
+            if (error) {
+                toast.error(error.message);
+            } else {
+                toast.success(successMessage);
+            }
+        }
+    }, [isLoading, error, loadingMessage, successMessage]);
+}(No newline at end of file)
src/admin/route/AdminRoute.tsx
--- src/admin/route/AdminRoute.tsx
+++ src/admin/route/AdminRoute.tsx
@@ -1,6 +1,7 @@
 import {Navigate, Route, Routes} from "react-router-dom";
 import {BoardListPage} from "../feature/board/page/BoardListPage.tsx";
 import {ADMIN_BBS_MASTER_ROUTE} from "./adminRouteMap.ts";
+import {BoardArticleListPage} from "../feature/board/page/BoardArticleListPage.tsx";
 
 const ReadyPage = () => {
     return <div>Preparing menu.</div>;
@@ -11,6 +12,7 @@
         <Routes>
             <Route path="/" element={<Navigate to={ADMIN_BBS_MASTER_ROUTE} replace />} />
             <Route path={ADMIN_BBS_MASTER_ROUTE} element={<BoardListPage />} />
+            <Route path={`/detail/cop/bbs/article/:bbsId`} element={<BoardArticleListPage />}/>
             <Route path="*" element={<ReadyPage />} />
         </Routes>
     );
src/styles/adm/button.css
--- src/styles/adm/button.css
+++ src/styles/adm/button.css
@@ -88,9 +88,25 @@
 .page a,.page button{display:flex;width:36px;height:36px;border:1px solid transparent;border-radius:5px;font-size:16px;color:#878B96;justify-content:center;align-items:center;transition:all .3s;}
 .page a:hover, .page button:hover{background:#F5F6F7;color:#3a3c41;}
 .page .active,.page .on{border:1px solid #E2E7EF;background:#e2e7ef;font-weight:600;color:#3a3c41;}
-.page .btn_page{border:1px solid #d2d7df;border-radius:5px;overflow:hidden;}
-.page .btn_page:hover{background-color:#F5F6F7;}
-.page .btn_page i{display:inline-block;width:100%;height:100%;transition:all .3s;}
+.page .btn_page,
+.page .btn_page_first,
+.page .btn_page_prev,
+.page .btn_page_next,
+.page .btn_page_last{border:1px solid #d2d7df;border-radius:5px;overflow:hidden;}
+.page .btn_page:hover,
+.page .btn_page_first:hover,
+.page .btn_page_prev:hover,
+.page .btn_page_next:hover,
+.page .btn_page_last:hover{background-color:#F5F6F7;}
+.page .btn_page i,
+.page .btn_page_first i,
+.page .btn_page_prev i,
+.page .btn_page_next i,
+.page .btn_page_last i{display:inline-block;width:100%;height:100%;transition:all .3s;}
+.btn_page_first i{background:url(/publish/adm/images/component/icon_page_first.svg) no-repeat center;}
+.btn_page_prev i{background:url(/publish/adm/images/component/icon_page_prev.svg) no-repeat center;}
+.btn_page_next i{background:url(/publish/adm/images/component/icon_page_next.svg) no-repeat center;}
+.btn_page_last i{background:url(/publish/adm/images/component/icon_page_last.svg) no-repeat center;}
 .btn_first i{background:url(/publish/adm/images/component/icon_double_arrow_left_page.png) no-repeat center;}
 .btn_prev i{background:url(/publish/adm/images/component/icon_arrow_left_page.png) no-repeat center;}
 .btn_next i{background:url(/publish/adm/images/component/icon_arrow_right_page.png) no-repeat center;}
src/type/pageResponse.ts
--- src/type/pageResponse.ts
+++ src/type/pageResponse.ts
@@ -1,6 +1,8 @@
-export interface PageResponse<T> {
-    list: T[]
-    totalCount: number
-    currentPage: number
-    recordPerPage: number
+export interface PageResponse<T, E = Record<string, unknown>> {
+    list: T[];
+    extraData: E;
+    totalItems: number;
+    totalPages: number;
+    currentPage: number;
+    size: number;
 }
(No newline at end of file)
src/type/searchParams.ts
--- src/type/searchParams.ts
+++ src/type/searchParams.ts
@@ -1,8 +1,8 @@
 export interface SearchParams {
     searchSortCnd: string
-    searchSortCnd: string
     searchSortOrd: string
     searchKeyword: string
+    searchCnd: string
     pageUnit: number
     pageIndex: number
 }
vite.config.ts
--- vite.config.ts
+++ vite.config.ts
@@ -1,9 +1,23 @@
 import { defineConfig, loadEnv } from 'vite'
 import react from '@vitejs/plugin-react'
+import type { ProxyOptions } from 'vite'
 
 export default defineConfig(({ mode }) => {
   const env = loadEnv(mode, process.cwd(), '')
   const apiProxyTarget = env.VITE_API_PROXY_TARGET || 'http://localhost:9999'
+  const apiProxy = (): ProxyOptions => ({
+    target: apiProxyTarget,
+    changeOrigin: true,
+    configure: (proxy) => {
+      proxy.on('proxyRes', (proxyRes) => {
+        const location = proxyRes.headers.location
+
+        if (typeof location === 'string' && location.startsWith(apiProxyTarget)) {
+          proxyRes.headers.location = location.replace(apiProxyTarget, '')
+        }
+      })
+    },
+  })
 
   return {
     plugins: [react()],
@@ -11,14 +25,14 @@
       port: 5173,
       host: '0.0.0.0',
       proxy: {
-        '/uat': { target: apiProxyTarget, changeOrigin: true },
-        '/cmm': { target: apiProxyTarget, changeOrigin: true },
-        '/sym': { target: apiProxyTarget, changeOrigin: true },
-        '/cop': { target: apiProxyTarget, changeOrigin: true },
-        '/uss': { target: apiProxyTarget, changeOrigin: true },
-        '/sec': { target: apiProxyTarget, changeOrigin: true },
-        '/sts': { target: apiProxyTarget, changeOrigin: true },
-        '/react': { target: apiProxyTarget, changeOrigin: true },
+        '/uat': apiProxy(),
+        '/cmm': apiProxy(),
+        '/sym': apiProxy(),
+        '/cop': apiProxy(),
+        '/uss': apiProxy(),
+        '/sec': apiProxy(),
+        '/sts': apiProxy(),
+        '/react': apiProxy(),
       },
     },
   }
Add a comment
List