03cd93e1f2
비즈케어 홈즈 홈상품 운용관리(BillCare) 프론트/백엔드/Docker 구성을 exdev git에 등록합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
226 lines
6.7 KiB
TypeScript
226 lines
6.7 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import Modal from '../../components/DraggableModal'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import {
|
|
Button,
|
|
Card,
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Space,
|
|
Typography,
|
|
message } from 'antd'
|
|
import CareTable from '../../components/CareTable'
|
|
import {
|
|
QuestionCircleOutlined,
|
|
ReloadOutlined,
|
|
SearchOutlined } from '@ant-design/icons'
|
|
import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client'
|
|
|
|
type NoticeRow = {
|
|
id: number
|
|
no?: number
|
|
title?: string
|
|
views?: number
|
|
createdDate?: string
|
|
authorName?: string
|
|
contents?: string
|
|
}
|
|
|
|
type NoticeResult = {
|
|
total: number
|
|
list: NoticeRow[]
|
|
}
|
|
|
|
const initial = { page: 0, size: 10, searchType: '제목', sort: 'id,desc' } as Record<string, unknown>
|
|
|
|
export default function CsNoticePage() {
|
|
const qc = useQueryClient()
|
|
const [form] = Form.useForm()
|
|
const [filters, setFilters] = useState<Record<string, unknown>>(initial)
|
|
const [detail, setDetail] = useState<NoticeRow | null>(null)
|
|
|
|
const query = useQuery({
|
|
queryKey: ['homes-notices', filters],
|
|
queryFn: async () => {
|
|
const { data } = await client.get<ApiResponse<NoticeResult>>(`${apiPrefix()}/homes/notices`, {
|
|
params: {
|
|
...filters,
|
|
q: filters.keyword || undefined } })
|
|
return unwrap(data)
|
|
} })
|
|
|
|
const openDetail = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
const { data } = await client.get<ApiResponse<NoticeRow>>(`${apiPrefix()}/homes/notices/${id}`)
|
|
return unwrap(data)
|
|
},
|
|
onSuccess: (row) => {
|
|
setDetail(row)
|
|
qc.invalidateQueries({ queryKey: ['homes-notices'] })
|
|
},
|
|
onError: (e: Error) => message.error(e.message) })
|
|
|
|
const onSearch = (values: Record<string, unknown>) => {
|
|
setFilters({
|
|
...initial,
|
|
searchType: values.searchType || '제목',
|
|
keyword: values.keyword || undefined,
|
|
page: 0,
|
|
size: Number(values.size ?? filters.size ?? 10),
|
|
sort: filters.sort })
|
|
}
|
|
|
|
const onReset = () => {
|
|
form.resetFields()
|
|
form.setFieldsValue({ searchType: '제목', size: 10 })
|
|
setFilters(initial)
|
|
}
|
|
|
|
const columns = useMemo(
|
|
() => [
|
|
{
|
|
title: '번호',
|
|
dataIndex: 'no',
|
|
width: 90,
|
|
align: 'center' as const,
|
|
sorter: true },
|
|
{
|
|
title: '제목',
|
|
dataIndex: 'title',
|
|
ellipsis: true,
|
|
render: (v: string, row: NoticeRow) => (
|
|
<Button type="link" className="cs-notice-title" onClick={() => openDetail.mutate(row.id)}>
|
|
{v}
|
|
</Button>
|
|
) },
|
|
{
|
|
title: '조회',
|
|
dataIndex: 'views',
|
|
width: 90,
|
|
align: 'center' as const,
|
|
sorter: true,
|
|
render: (v?: number) => (v == null ? 0 : Number(v).toLocaleString()) },
|
|
{
|
|
title: '등록일',
|
|
dataIndex: 'createdDate',
|
|
width: 120,
|
|
align: 'center' as const,
|
|
sorter: true },
|
|
],
|
|
[],
|
|
)
|
|
|
|
return (
|
|
<div className="stock-page cs-notice-page homes-cs-notice-page">
|
|
<div className="stock-title">
|
|
<div>
|
|
<h2>
|
|
<QuestionCircleOutlined className="product-title-icon" />
|
|
공지사항
|
|
</h2>
|
|
<p>프로그램 사용에 대한 공지사항이나 업데이트를 알려드립니다.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="stock-filter-card" size="small">
|
|
<Form
|
|
form={form}
|
|
layout="inline"
|
|
className="farebox-filter"
|
|
initialValues={{ searchType: '제목', size: 10 }}
|
|
onFinish={onSearch}
|
|
>
|
|
<div className="farebox-filter-row">
|
|
<Form.Item name="searchType">
|
|
<Select className="w-110" options={['제목', '내용', '작성자'].map((v) => ({ value: v, label: v }))} />
|
|
</Form.Item>
|
|
<Form.Item name="keyword">
|
|
<Input className="w-230" allowClear />
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Space>
|
|
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
|
|
검색
|
|
</Button>
|
|
<Button icon={<ReloadOutlined />} onClick={onReset}>
|
|
초기화
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
<Form.Item name="size" className="farebox-size">
|
|
<Select
|
|
className="w-150"
|
|
options={[10, 15, 30, 50, 100].map((n) => ({ value: n, label: `목록 ${n}개씩 보기` }))}
|
|
onChange={(size) => {
|
|
form.setFieldValue('size', size)
|
|
setFilters((prev) => ({ ...prev, size, page: 0 }))
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
</div>
|
|
</Form>
|
|
</Card>
|
|
|
|
<Card className="stock-table-card" size="small">
|
|
<div className="farebox-table-toolbar">
|
|
<Typography.Text>
|
|
목록 <b>{query.data?.total ?? 0}</b>
|
|
</Typography.Text>
|
|
</div>
|
|
<CareTable
|
|
size="small"
|
|
loading={query.isLoading}
|
|
rowKey="id"
|
|
dataSource={query.data?.list}
|
|
columns={columns}
|
|
bordered
|
|
pagination={{
|
|
current: Number(filters.page ?? 0) + 1,
|
|
pageSize: Number(filters.size ?? 10),
|
|
total: query.data?.total ?? 0,
|
|
showSizeChanger: false,
|
|
onChange: (page) => setFilters((prev) => ({ ...prev, page: page - 1 })) }}
|
|
onChange={(_p, _f, sorter) => {
|
|
const s = Array.isArray(sorter) ? sorter[0] : sorter
|
|
if (!s?.field || !s.order) return
|
|
setFilters((prev) => ({
|
|
...prev,
|
|
sort: `${String(s.field)},${s.order === 'ascend' ? 'asc' : 'desc'}` }))
|
|
}}
|
|
locale={{ emptyText: '등록된 공지사항이 없습니다.' }}
|
|
/>
|
|
</Card>
|
|
|
|
<Modal
|
|
title="공지사항"
|
|
open={Boolean(detail)}
|
|
onCancel={() => setDetail(null)}
|
|
footer={<Button onClick={() => setDetail(null)}>닫기</Button>}
|
|
width={640}
|
|
destroyOnHidden
|
|
centered
|
|
>
|
|
{detail ? (
|
|
<div>
|
|
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
|
{detail.title}
|
|
</Typography.Title>
|
|
<div className="cs-notice-meta">
|
|
<span>조회 {detail.views ?? 0}</span>
|
|
<span>등록일 {detail.createdDate || '-'}</span>
|
|
</div>
|
|
<div className="cs-notice-body">
|
|
{(detail.contents || '')
|
|
.split('\n')
|
|
.map((line, idx) => (
|
|
<p key={idx}>{line || '\u00A0'}</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|