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