--- src/App.tsx
+++ src/App.tsx
... | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 |
import { UserListPage } from './user/UserListPage';
|
| 4 | 4 |
import {AdminLayout} from "./admin/layout/AdminLayout.tsx";
|
| 5 | 5 |
import {AdminRoute} from "./admin/route/AdminRoute.tsx";
|
| 6 |
+import {ToastContainer} from "react-toastify";
|
|
| 6 | 7 |
|
| 7 | 8 |
type Skin = 'admin' | 'user'; |
| 8 | 9 |
|
... | ... | @@ -62,6 +63,7 @@ |
| 62 | 63 |
<UserListPage /> |
| 63 | 64 |
</UserLayout> |
| 64 | 65 |
)} |
| 66 |
+ <ToastContainer position="bottom-right" autoClose={3000} />
|
|
| 65 | 67 |
</> |
| 66 | 68 |
); |
| 67 | 69 |
} |
+++ src/admin/component/EmptyRow.tsx
... | ... | @@ -0,0 +1,15 @@ |
| 1 | +export function EmptyRow({colSpan}: any) { | |
| 2 | + 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> | |
| 14 | + ); | |
| 15 | +}(No newline at end of file) |
+++ src/admin/component/ImageTable.tsx
... | ... | @@ -0,0 +1,46 @@ |
| 1 | +interface ImageTableProps { | |
| 2 | + list: any[]; | |
| 3 | +} | |
| 4 | + | |
| 5 | +export const ImageTable = ({list = []}: ImageTableProps) => { | |
| 6 | + if (!list || list.length === 0) return ( | |
| 7 | + <ul className="gallery_list"> | |
| 8 | + <div className="board1_btn w100per" style={{textAlign: "center"}}> | |
| 9 | + 데이터가 없습니다. | |
| 10 | + </div> | |
| 11 | + </ul> | |
| 12 | + ); | |
| 13 | + | |
| 14 | + return ( | |
| 15 | + <ul className="gallery_list"> | |
| 16 | + {list.map((item) => ( | |
| 17 | + <li key={item.mazId}> | |
| 18 | + <a href="#" data-mainzone-id={item.mazId}> | |
| 19 | + | |
| 20 | + {item.useYn === "Y" ? ( | |
| 21 | + <span className="status primary">사용</span> | |
| 22 | + ) : ( | |
| 23 | + <span className="status gray">미사용</span> | |
| 24 | + )} | |
| 25 | + | |
| 26 | + <div className="images_area"> | |
| 27 | + <img | |
| 28 | + src={`/uss/ion/pwm/getImage.do?atchFileId=${item.mainzoneImageFile}`} | |
| 29 | + alt="" | |
| 30 | + /> | |
| 31 | + </div> | |
| 32 | + | |
| 33 | + <div className="list_content"> | |
| 34 | + <b className="list_title">{item.mazNm}</b> | |
| 35 | + <ul className="list_info"> | |
| 36 | + <li>작성자 {item.registerId}</li> | |
| 37 | + <li>노출순서 {item.sort}</li> | |
| 38 | + <li>{item.regdt}</li> | |
| 39 | + </ul> | |
| 40 | + </div> | |
| 41 | + </a> | |
| 42 | + </li> | |
| 43 | + ))} | |
| 44 | + </ul> | |
| 45 | + ); | |
| 46 | +}(No newline at end of file) |
+++ src/admin/component/PageHeader.tsx
... | ... | @@ -0,0 +1,39 @@ |
| 1 | +interface PageHeaderProps { | |
| 2 | + title: string; | |
| 3 | + breadcrumb: any[]; | |
| 4 | + homeUrl?: string; | |
| 5 | +} | |
| 6 | + | |
| 7 | +export const PageHeader = ({title, breadcrumb, homeUrl}: PageHeaderProps) => { | |
| 8 | + return ( | |
| 9 | + <div className="content_title"> | |
| 10 | + <div className="left"> | |
| 11 | + <h3>{title}</h3> | |
| 12 | + </div> | |
| 13 | + <div className="right"> | |
| 14 | + <ol className="breadcrumb"> | |
| 15 | + <li> | |
| 16 | + <a href={homeUrl || "/cmm/main/mainPage.do"} className="home"> | |
| 17 | + <i></i> | |
| 18 | + </a> | |
| 19 | + </li> | |
| 20 | + {breadcrumb.map((item, idx) => { | |
| 21 | + const isLast = idx === breadcrumb.length - 1; | |
| 22 | + | |
| 23 | + return ( | |
| 24 | + <li key={idx}> | |
| 25 | + {isLast ? ( | |
| 26 | + <strong className="current_location"> | |
| 27 | + {item.label} | |
| 28 | + </strong> | |
| 29 | + ) : ( | |
| 30 | + <a href={item.url || "#"}>{item.label}</a> | |
| 31 | + )} | |
| 32 | + </li> | |
| 33 | + ); | |
| 34 | + })} | |
| 35 | + </ol> | |
| 36 | + </div> | |
| 37 | + </div> | |
| 38 | + ); | |
| 39 | +} |
+++ src/admin/component/SearchBar.tsx
... | ... | @@ -0,0 +1,65 @@ |
| 1 | +import type {ChangeEvent} from 'react' | |
| 2 | + | |
| 3 | +interface SearchBarProps { | |
| 4 | + searchSortCnd: string | |
| 5 | + searchKeyword: string | |
| 6 | + options: { value: string; label: string }[] | |
| 7 | + onChange: ( | |
| 8 | + name: string, | |
| 9 | + value: string | |
| 10 | + ) => void | |
| 11 | + onSearch: () => void | |
| 12 | +} | |
| 13 | + | |
| 14 | +export const SearchBar = ({ | |
| 15 | + searchSortCnd, | |
| 16 | + searchKeyword, | |
| 17 | + options, | |
| 18 | + onChange, | |
| 19 | + onSearch | |
| 20 | + }: SearchBarProps) => { | |
| 21 | + const handleChange = (event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { | |
| 22 | + onChange(event.target.name, event.target.value); | |
| 23 | + } | |
| 24 | + | |
| 25 | + return ( | |
| 26 | + <> | |
| 27 | + <select | |
| 28 | + id="searchCnd" | |
| 29 | + name="searchSortCnd" | |
| 30 | + className="search_select" | |
| 31 | + value={searchSortCnd} | |
| 32 | + onChange={handleChange} | |
| 33 | + > | |
| 34 | + <option value="">전체</option> | |
| 35 | + {options.map((option) => ( | |
| 36 | + <option key={option.value} value={option.value}> | |
| 37 | + {option.label} | |
| 38 | + </option> | |
| 39 | + ))} | |
| 40 | + </select> | |
| 41 | + | |
| 42 | + <div className="search_type input_type"> | |
| 43 | + <input | |
| 44 | + type="text" | |
| 45 | + id="searchWrd" | |
| 46 | + name="searchKeyword" | |
| 47 | + className="input search_input" | |
| 48 | + value={searchKeyword} | |
| 49 | + onChange={handleChange} | |
| 50 | + maxLength={20} | |
| 51 | + placeholder="검색어를 입력하세요" | |
| 52 | + /> | |
| 53 | + </div> | |
| 54 | + | |
| 55 | + <button | |
| 56 | + type="button" | |
| 57 | + className="btn btn_search" | |
| 58 | + onClick={onSearch} | |
| 59 | + > | |
| 60 | + 검색 | |
| 61 | + </button> | |
| 62 | + </> | |
| 63 | + ) | |
| 64 | +} | |
| 65 | + |
+++ src/admin/component/pagination/PageSizeSelector.tsx
... | ... | @@ -0,0 +1,27 @@ |
| 1 | +import type {ChangeEvent} from "react"; | |
| 2 | + | |
| 3 | +interface PageSizeSelectorProps { | |
| 4 | + value: number; | |
| 5 | + options: { value: string; label: string; }[]; | |
| 6 | + onChange: (e: ChangeEvent<HTMLSelectElement>) => void; | |
| 7 | +} | |
| 8 | + | |
| 9 | +export const PageSizeSelector = ({value, options, onChange}: PageSizeSelectorProps) => { | |
| 10 | + return ( | |
| 11 | + <> | |
| 12 | + <select | |
| 13 | + id="pageUnit" | |
| 14 | + name="pageUnit" | |
| 15 | + className="search_select" | |
| 16 | + value={String(value)} | |
| 17 | + onChange={onChange} | |
| 18 | + > | |
| 19 | + {options.map((option) => ( | |
| 20 | + <option key={option.value} value={option.value}> | |
| 21 | + {option.label} | |
| 22 | + </option> | |
| 23 | + ))} | |
| 24 | + </select> | |
| 25 | + </> | |
| 26 | + ) | |
| 27 | +}; |
--- src/admin/feature/board/api/boardApi.ts
+++ src/admin/feature/board/api/boardApi.ts
... | ... | @@ -1,7 +1,8 @@ |
| 1 |
-import type {BoardSearchParams, BoardListItem} from "../model/board.types.ts";
|
|
| 2 | 1 |
import {apiClient} from "../../../../api/apiClient.ts";
|
| 3 | 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";
|
|
| 4 | 5 |
|
| 5 |
-export async function fetchBoardList(params: BoardSearchParams) {
|
|
| 6 |
+export async function fetchBoardList(params: SearchParams) {
|
|
| 6 | 7 |
return apiClient.get<PageResponse<BoardListItem>>('/cop/bbs/list.do', params);
|
| 7 | 8 |
} |
+++ src/admin/feature/board/components/BoardListSearchForm.tsx
... | ... | @@ -0,0 +1,84 @@ |
| 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
... | ... | @@ -0,0 +1,41 @@ |
| 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/BoardListTableHeader.tsx
... | ... | @@ -0,0 +1,111 @@ |
| 1 | +import type {SearchParams} from "../../../../type/searchParams.ts"; | |
| 2 | + | |
| 3 | +interface BoardListTableHeaderProps { | |
| 4 | + params: SearchParams; | |
| 5 | + onChange: (params: SearchParams) => void | |
| 6 | +} | |
| 7 | + | |
| 8 | +export function BoardListTableHeader({params, onChange}: BoardListTableHeaderProps) { | |
| 9 | + const handleSort = (field: string) => { | |
| 10 | + | |
| 11 | + const nextOrder = | |
| 12 | + params.searchSortCnd === field && | |
| 13 | + params.searchSortOrd === 'ASC' | |
| 14 | + ? 'DESC' | |
| 15 | + : 'ASC'; | |
| 16 | + | |
| 17 | + onChange({ | |
| 18 | + ...params, | |
| 19 | + searchSortCnd: field, | |
| 20 | + searchSortOrd: nextOrder, | |
| 21 | + pageIndex: 1, | |
| 22 | + }); | |
| 23 | + }; | |
| 24 | + | |
| 25 | + const getSortIcon = (field: string) => { | |
| 26 | + | |
| 27 | + if (params.searchSortCnd !== field) { | |
| 28 | + return '-'; | |
| 29 | + } | |
| 30 | + | |
| 31 | + return params.searchSortOrd === 'ASC' | |
| 32 | + ? '▲' | |
| 33 | + : '▼'; | |
| 34 | + }; | |
| 35 | + | |
| 36 | + return ( | |
| 37 | + <> | |
| 38 | + <colgroup> | |
| 39 | + <col style={{width: '6%'}}/> | |
| 40 | + <col style={{width: '18%'}}/> | |
| 41 | + <col style={{width: '18%'}}/> | |
| 42 | + <col style={{width: '10%'}}/> | |
| 43 | + <col style={{width: '10%'}}/> | |
| 44 | + <col style={{width: '10%'}}/> | |
| 45 | + <col style={{width: '6%'}}/> | |
| 46 | + <col style={{width: '20%'}}/> | |
| 47 | + </colgroup> | |
| 48 | + | |
| 49 | + <thead> | |
| 50 | + <tr> | |
| 51 | + <th>번호</th> | |
| 52 | + <th> | |
| 53 | + 게시판명 | |
| 54 | + <button | |
| 55 | + className={`sort sortBtn ${params.searchSortCnd === 'BBS_NM' ? 'active' : ''}`} | |
| 56 | + onClick={() => handleSort('BBS_NM')} | |
| 57 | + > | |
| 58 | + {getSortIcon('BBS_NM')} | |
| 59 | + </button> | |
| 60 | + </th> | |
| 61 | + <th> | |
| 62 | + 연결 메뉴 | |
| 63 | + <button | |
| 64 | + className={`sort sortBtn ${params.searchSortCnd === 'MENU_NM' ? 'active' : ''}`} | |
| 65 | + onClick={() => handleSort('MENU_NM')} | |
| 66 | + > | |
| 67 | + {getSortIcon('MENU_NM')} | |
| 68 | + </button> | |
| 69 | + </th> | |
| 70 | + <th> | |
| 71 | + 댓글 / 글수 | |
| 72 | + <button | |
| 73 | + className={`sort sortBtn ${params.searchSortCnd === 'TOTCNT' ? 'active' : ''}`} | |
| 74 | + onClick={() => handleSort('TOTCNT')} | |
| 75 | + > | |
| 76 | + {getSortIcon('TOTCNT')} | |
| 77 | + </button> | |
| 78 | + </th> | |
| 79 | + <th> | |
| 80 | + 게시판유형 | |
| 81 | + <button | |
| 82 | + className={`sort sortBtn ${params.searchSortCnd === 'BBS_TY_CODE_NM' ? 'active' : ''}`} | |
| 83 | + onClick={() => handleSort('BBS_TY_CODE_NM')} | |
| 84 | + > | |
| 85 | + {getSortIcon('BBS_TY_CODE_NM')} | |
| 86 | + </button> | |
| 87 | + </th> | |
| 88 | + <th> | |
| 89 | + 생성일 | |
| 90 | + <button | |
| 91 | + className={`sort sortBtn ${params.searchSortCnd === 'FRST_REGIST_PNTTM' ? 'active' : ''}`} | |
| 92 | + onClick={() => handleSort('FRST_REGIST_PNTTM')} | |
| 93 | + > | |
| 94 | + {getSortIcon('FRST_REGIST_PNTTM')} | |
| 95 | + </button> | |
| 96 | + </th> | |
| 97 | + <th> | |
| 98 | + 사용여부 | |
| 99 | + <button | |
| 100 | + className={`sort sortBtn ${params.searchSortCnd === 'USE_AT' ? 'active' : ''}`} | |
| 101 | + onClick={() => handleSort('USE_AT')} | |
| 102 | + > | |
| 103 | + {getSortIcon('USE_AT')} | |
| 104 | + </button> | |
| 105 | + </th> | |
| 106 | + <th>게시판 관리</th> | |
| 107 | + </tr> | |
| 108 | + </thead> | |
| 109 | + </> | |
| 110 | + ) | |
| 111 | +} |
+++ src/admin/feature/board/components/BoardListTableRow.tsx
... | ... | @@ -0,0 +1,44 @@ |
| 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/hook/useBoardList.ts
+++ src/admin/feature/board/hook/useBoardList.ts
... | ... | @@ -1,13 +1,21 @@ |
| 1 |
-import type {BoardSearchParams} from "../model/board.types.ts";
|
|
| 2 |
- |
|
| 3 | 1 |
import {keepPreviousData, useQuery} from "@tanstack/react-query";
|
| 4 | 2 |
import {fetchBoardList} from "../api/boardApi.ts";
|
| 3 |
+import type {SearchParams} from "../../../../type/searchParams.ts";
|
|
| 5 | 4 |
|
| 6 |
-export function useBoardListQuery(searchParams: BoardSearchParams) {
|
|
| 5 |
+export function useBoardListQuery(searchParams: SearchParams) {
|
|
| 7 | 6 |
|
| 8 |
- return useQuery({
|
|
| 7 |
+ const query = useQuery({
|
|
| 9 | 8 |
queryKey: ['boardList', searchParams], |
| 10 | 9 |
queryFn: () => fetchBoardList(searchParams), |
| 11 | 10 |
placeholderData: keepPreviousData, |
| 12 | 11 |
}); |
| 12 |
+ |
|
| 13 |
+ return {
|
|
| 14 |
+ list: query.data?.list ?? [], |
|
| 15 |
+ totalCount: query.data?.totalCount ?? 0, |
|
| 16 |
+ currentPage: query.data?.currentPage ?? 0, |
|
| 17 |
+ recordPerPage: query.data?.recordPerPage ?? 0, |
|
| 18 |
+ isLoading: query.isLoading, |
|
| 19 |
+ error: query.error, |
|
| 20 |
+ } |
|
| 13 | 21 |
} |
--- src/admin/feature/board/model/board.types.ts
... | ... | @@ -1,18 +0,0 @@ |
| 1 | -export interface BoardSearchParams { | |
| 2 | - searchCondition: string | |
| 3 | - searchSortOrder: string | |
| 4 | - searchKeyword: string | |
| 5 | - pageUnit: number, | |
| 6 | - pageIndex: number | |
| 7 | -} | |
| 8 | - | |
| 9 | -export interface BoardListItem { | |
| 10 | - bbsId: string | |
| 11 | - bbsNm: string | |
| 12 | - menuNm: string | |
| 13 | - newCnt: number | |
| 14 | - totCnt: number | |
| 15 | - bbsTyCodeNm: string | |
| 16 | - frstRegisterPnttm: string | |
| 17 | - useAt: 'Y' | 'N' | |
| 18 | -}(No newline at end of file) |
--- src/admin/feature/board/page/BoardListPage.tsx
+++ src/admin/feature/board/page/BoardListPage.tsx
... | ... | @@ -1,7 +1,73 @@ |
| 1 |
+import {useBoardListQuery} from "../hook/useBoardList.ts";
|
|
| 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";
|
|
| 8 |
+ |
|
| 9 |
+const initSearchParam: SearchParams = {
|
|
| 10 |
+ pageIndex: 1, |
|
| 11 |
+ pageUnit: 10, |
|
| 12 |
+ searchKeyword: "", |
|
| 13 |
+ searchSortCnd: "BBS_NM", |
|
| 14 |
+ searchSortOrd: "ASC" |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 1 | 17 |
export const BoardListPage = () => {
|
| 18 |
+ const [searchParams, setSearchParams] = useState<SearchParams>(initSearchParam); |
|
| 19 |
+ const {
|
|
| 20 |
+ list, |
|
| 21 |
+ totalCount, |
|
| 22 |
+ currentPage, |
|
| 23 |
+ recordPerPage, |
|
| 24 |
+ isLoading, |
|
| 25 |
+ error |
|
| 26 |
+ } = useBoardListQuery(searchParams); |
|
| 27 |
+ |
|
| 28 |
+ const toastId = useRef<string | number | null>(null); |
|
| 29 |
+ |
|
| 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]); |
|
| 45 |
+ |
|
| 46 |
+ const title = '게시판 관리' |
|
| 47 |
+ const breadcrumb = [{label: '게시판 관리'}]
|
|
| 48 |
+ const homeUrl = '#' |
|
| 49 |
+ |
|
| 2 | 50 |
return ( |
| 3 |
- <div> |
|
| 4 |
- <h2>게시판 관리</h2> |
|
| 5 |
- </div> |
|
| 6 |
- ); |
|
| 51 |
+ <> |
|
| 52 |
+ <PageHeader |
|
| 53 |
+ title={title}
|
|
| 54 |
+ breadcrumb={breadcrumb}
|
|
| 55 |
+ homeUrl={homeUrl}
|
|
| 56 |
+ /> |
|
| 57 |
+ <BoardListSearchForm |
|
| 58 |
+ totalCount={totalCount}
|
|
| 59 |
+ searchParams={searchParams}
|
|
| 60 |
+ onChange={setSearchParams}
|
|
| 61 |
+ /> |
|
| 62 |
+ {isLoading && <p>Loading...</p>}
|
|
| 63 |
+ <BoardListTable |
|
| 64 |
+ items={list}
|
|
| 65 |
+ params={searchParams}
|
|
| 66 |
+ onChange={setSearchParams}
|
|
| 67 |
+ totalCount={totalCount}
|
|
| 68 |
+ currentPage={currentPage}
|
|
| 69 |
+ recordPerPage={recordPerPage}
|
|
| 70 |
+ /> |
|
| 71 |
+ </> |
|
| 72 |
+ ) |
|
| 7 | 73 |
}; |
+++ src/admin/feature/board/type/board.types.ts
... | ... | @@ -0,0 +1,10 @@ |
| 1 | +export interface BoardListItem { | |
| 2 | + bbsId: string | |
| 3 | + bbsNm: string | |
| 4 | + menuNm: string | |
| 5 | + newCnt: number | |
| 6 | + totCnt: number | |
| 7 | + bbsTyCodeNm: string | |
| 8 | + frstRegisterPnttm: string | |
| 9 | + useAt: 'Y' | 'N' | |
| 10 | +}(No newline at end of file) |
--- src/admin/hook/useMenuList.ts
+++ src/admin/hook/useMenuList.ts
... | ... | @@ -16,7 +16,6 @@ |
| 16 | 16 |
queryKey: ['menuList'], |
| 17 | 17 |
queryFn: fetchMenuList, |
| 18 | 18 |
select: (data) => {
|
| 19 |
- console.log(data); |
|
| 20 | 19 |
const headMenuList = |
| 21 | 20 |
(data.head ?? []) |
| 22 | 21 |
.map(toMenuItem) |
... | ... | @@ -35,8 +34,6 @@ |
| 35 | 34 |
|
| 36 | 35 |
staleTime: 1000 * 60 * 10, |
| 37 | 36 |
}); |
| 38 |
- |
|
| 39 |
- console.log(query); |
|
| 40 | 37 |
|
| 41 | 38 |
return {
|
| 42 | 39 |
headMenuList: query.data?.headMenuList ?? [], |
--- src/admin/layout/AdminLayout.tsx
+++ src/admin/layout/AdminLayout.tsx
... | ... | @@ -1,16 +1,18 @@ |
| 1 | 1 |
import type {ReactNode} from "react";
|
| 2 | 2 |
import {AdminSideBar} from "./AdminSideBar.tsx";
|
| 3 |
+import {AdminTopBar} from "./AdminTopBar.tsx";
|
|
| 3 | 4 |
|
| 4 | 5 |
type AdminLayoutProps = {
|
| 5 | 6 |
children: ReactNode; |
| 6 | 7 |
} |
| 7 | 8 |
|
| 8 |
- |
|
| 9 | 9 |
export function AdminLayout({children}: AdminLayoutProps) {
|
| 10 | 10 |
return ( |
| 11 | 11 |
<div className="wrap"> |
| 12 | 12 |
<AdminSideBar/> |
| 13 |
+ |
|
| 13 | 14 |
<div className="container sub"> |
| 15 |
+ <AdminTopBar/> |
|
| 14 | 16 |
<div className="content_wrap"> |
| 15 | 17 |
{children}
|
| 16 | 18 |
</div> |
--- src/admin/layout/AdminSideBar.tsx
+++ src/admin/layout/AdminSideBar.tsx
... | ... | @@ -1,8 +1,16 @@ |
| 1 |
-import { MenuList } from '../component/menu/MenuList.tsx';
|
|
| 2 |
-import { useMenuList } from '../hook/useMenuList.ts';
|
|
| 1 |
+import {useEffect} from 'react';
|
|
| 2 |
+import {toast} from 'react-toastify';
|
|
| 3 |
+import {MenuList} from '../component/menu/MenuList.tsx';
|
|
| 4 |
+import {useMenuList} from '../hook/useMenuList.ts';
|
|
| 3 | 5 |
|
| 4 | 6 |
export const AdminSideBar = () => {
|
| 5 |
- const { headMenuList, menuList, isLoading, errorMessage } = useMenuList();
|
|
| 7 |
+ const {headMenuList, menuList, isLoading, errorMessage} = useMenuList();
|
|
| 8 |
+ |
|
| 9 |
+ useEffect(() => {
|
|
| 10 |
+ if (errorMessage) {
|
|
| 11 |
+ toast.error(errorMessage); |
|
| 12 |
+ } |
|
| 13 |
+ }, [errorMessage]); |
|
| 6 | 14 |
|
| 7 | 15 |
return ( |
| 8 | 16 |
<div className="menu_wrap"> |
... | ... | @@ -10,8 +18,7 @@ |
| 10 | 18 |
<a href="/">DashBoard</a> |
| 11 | 19 |
</h1> |
| 12 | 20 |
<nav className="menu"> |
| 13 |
- {isLoading && <p style={{ padding: '0 20px', color: '#fff' }}>메뉴 로딩중...</p>}
|
|
| 14 |
- {errorMessage && <p style={{ padding: '0 20px', color: '#ffd1d1' }}>{errorMessage}</p>}
|
|
| 21 |
+ {isLoading && <p style={{padding: '0 20px', color: '#fff'}}>메뉴 로딩중...</p>}
|
|
| 15 | 22 |
<MenuList headMenuList={headMenuList} menuList={menuList} />
|
| 16 | 23 |
</nav> |
| 17 | 24 |
</div> |
+++ src/admin/layout/AdminTopBar.tsx
... | ... | @@ -0,0 +1,55 @@ |
| 1 | +export const AdminTopBar = () => { | |
| 2 | + return ( | |
| 3 | + <div className="top_util"> | |
| 4 | + <ul className="user_util"> | |
| 5 | + <li className="final_date"> | |
| 6 | + <i></i> | |
| 7 | + <p>최종접속일시 :</p> | |
| 8 | + <span></span> | |
| 9 | + </li> | |
| 10 | + | |
| 11 | + <li className="ip"> | |
| 12 | + <i></i> | |
| 13 | + <p>IP</p> | |
| 14 | + </li> | |
| 15 | + | |
| 16 | + <li className="time_out"> | |
| 17 | + <i></i> | |
| 18 | + | |
| 19 | + <p> | |
| 20 | + 로그인 타임아웃 :{" "} | |
| 21 | + <span className="view_timer" id="ViewTimer"> | |
| 22 | + {/*<font color="red"></font>*/} | |
| 23 | + </span> | |
| 24 | + </p> | |
| 25 | + | |
| 26 | + <button | |
| 27 | + type="button" | |
| 28 | + className="btn_extend" | |
| 29 | + onClick={() => {}} | |
| 30 | + > | |
| 31 | + 연장 | |
| 32 | + </button> | |
| 33 | + </li> | |
| 34 | + </ul> | |
| 35 | + | |
| 36 | + <div className="user_info"> | |
| 37 | + <div className="area_right"> | |
| 38 | + <ul className="user_info_ul"> | |
| 39 | + <li> | |
| 40 | + {}({}) | |
| 41 | + </li> | |
| 42 | + </ul> | |
| 43 | + | |
| 44 | + <button | |
| 45 | + type="button" | |
| 46 | + className="btn btn_logout" | |
| 47 | + onClick={() => {}} | |
| 48 | + > | |
| 49 | + 로그아웃 | |
| 50 | + </button> | |
| 51 | + </div> | |
| 52 | + </div> | |
| 53 | + </div> | |
| 54 | + ); | |
| 55 | +}(No newline at end of file) |
--- src/main.tsx
+++ src/main.tsx
... | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 |
import {BrowserRouter} from "react-router-dom";
|
| 4 | 4 |
import App from './App'; |
| 5 | 5 |
import './styles/app.css'; |
| 6 |
+import 'react-toastify/dist/ReactToastify.css'; |
|
| 6 | 7 |
|
| 7 | 8 |
const queryClient = new QueryClient(); |
| 8 | 9 |
|
--- src/styles/app.css
+++ src/styles/app.css
... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 |
.skin_switcher {
|
| 2 | 2 |
position: fixed; |
| 3 | 3 |
z-index: 99999; |
| 4 |
- right: 16px; |
|
| 4 |
+ left: 16px; |
|
| 5 | 5 |
bottom: 16px; |
| 6 | 6 |
display: flex; |
| 7 | 7 |
gap: 6px; |
+++ src/type/searchParams.ts
... | ... | @@ -0,0 +1,8 @@ |
| 1 | +export interface SearchParams { | |
| 2 | + searchSortCnd: string | |
| 3 | + searchSortCnd: string | |
| 4 | + searchSortOrd: string | |
| 5 | + searchKeyword: string | |
| 6 | + pageUnit: number | |
| 7 | + pageIndex: number | |
| 8 | +} |
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?