BillCare 초기 소스 등록

비즈케어 홈즈 홈상품 운용관리(BillCare) 프론트/백엔드/Docker 구성을 exdev git에 등록합니다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-07-12 05:05:17 +09:00
commit 03cd93e1f2
160 changed files with 47915 additions and 0 deletions
@@ -0,0 +1,142 @@
import { useState } from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useNavigate, useParams } from 'react-router-dom'
import dayjs, { type Dayjs } from 'dayjs'
import { Button, Card, DatePicker, Select, Space, Typography, message } from 'antd'
import CareTable from '../../components/CareTable'
import { ArrowLeftOutlined, SearchOutlined } from '@ant-design/icons'
import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client'
const hours = Array.from({ length: 24 }, (_, i) => ({ value: i, label: `${String(i).padStart(2, '0')}` }))
const minutes = [0, 10, 20, 30, 40, 50].map((v) => ({ value: v, label: `${String(v).padStart(2, '0')}` }))
const money = (v: unknown) => (v == null || v === '' ? '-' : Number(v).toLocaleString())
type OpenRow = Record<string, unknown> & { id: number }
export default function OpenUnitCostApplyPage() {
const { id } = useParams()
const navigate = useNavigate()
const tableId = Number(id)
const [from, setFrom] = useState<Dayjs>(dayjs())
const [to, setTo] = useState<Dayjs>(dayjs())
const [fromHour, setFromHour] = useState<number | undefined>()
const [fromMinute, setFromMinute] = useState<number | undefined>()
const [toHour, setToHour] = useState<number | undefined>()
const [toMinute, setToMinute] = useState<number | undefined>()
const [searched, setSearched] = useState(false)
const [filters, setFilters] = useState<Record<string, unknown>>({})
const [selected, setSelected] = useState<number[]>([])
const enabled = searched && !!filters.dateFrom && !!filters.dateTo
const query = useQuery({
queryKey: ['unitcost-apply', tableId, filters],
queryFn: async () => {
const { data } = await client.get<ApiResponse<{ total: number; list: OpenRow[]; table: Record<string, unknown> }>>(
`${apiPrefix()}/unitcosts/${tableId}/apply-search`,
{ params: filters },
)
return unwrap(data)
},
enabled: enabled && Number.isFinite(tableId) })
const apply = useMutation({
mutationFn: async () => {
if (!selected.length) throw new Error('적용할 개통을 선택하세요.')
const { data } = await client.post(`${apiPrefix()}/unitcosts/${tableId}/apply`, { ids: selected })
return unwrap(data as ApiResponse<{ updated: number }>)
},
onSuccess: (data) => {
message.success(`${data.updated}건에 정책단가를 적용했습니다.`)
setSelected([])
query.refetch()
},
onError: (e: Error) => message.error(e.message) })
const onSearch = () => {
if (!from || !to) {
message.warning('개통일을 선택하세요.')
return
}
if (to.diff(from, 'day') > 2) {
message.warning('최대 3일까지 조회할 수 있습니다.')
return
}
setSearched(true)
setSelected([])
setFilters({
dateFrom: from.format('YYYY-MM-DD'),
dateTo: to.format('YYYY-MM-DD'),
fromHour,
fromMinute,
toHour,
toMinute })
}
const tableLabel = query.data?.table
? `${query.data.table.telecom ?? ''} (${query.data.table.incompanyName ?? ''})`
: ''
const columns = [
{ title: '개통일', dataIndex: 'opendate', width: 100 },
{ title: '통신사', dataIndex: 'telecom', width: 70 },
{ title: '출고처', dataIndex: 'outcompanyName', width: 110, ellipsis: true },
{ title: '고객명', dataIndex: 'name', width: 90 },
{ title: '종류', dataIndex: 'openhow', width: 80 },
{ title: '모델명', dataIndex: 'pmodel', width: 140, ellipsis: true },
{ title: '요금제', dataIndex: 'plan', width: 120, ellipsis: true },
{ title: '기본정책', dataIndex: 'basicprice1', width: 100, align: 'right' as const, render: money },
]
return (
<div className="stock-page unitcost-apply-page">
<div className="stock-title">
<div>
<h2></h2>
<p> .</p>
</div>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/care/open/unitcost')}></Button>
</div>
<Card className="stock-filter-card" size="small" title={`${tableLabel} 정책적용`}>
<Space wrap>
<DatePicker value={from} onChange={(v) => v && setFrom(v)} />
<Select allowClear placeholder="-시-" className="w-100" options={hours} value={fromHour} onChange={setFromHour} />
<Select allowClear placeholder="-분-" className="w-100" options={minutes} value={fromMinute} onChange={setFromMinute} />
<span>~</span>
<DatePicker value={to} onChange={(v) => v && setTo(v)} />
<Select allowClear placeholder="-시-" className="w-100" options={hours} value={toHour} onChange={setToHour} />
<Select allowClear placeholder="-분-" className="w-100" options={minutes} value={toMinute} onChange={setToMinute} />
<Button type="primary" icon={<SearchOutlined />} onClick={onSearch}></Button>
<Typography.Text type="danger">* 3</Typography.Text>
</Space>
</Card>
<Card className="stock-table-card sheet-result-card" size="small">
{!enabled ? (
<div className="sheet-empty">
<div> .</div>
<div className="unitcost-apply-sub">(/MNP// . )</div>
</div>
) : (
<>
<div className="recovery-table-head">
<Typography.Text> <b>{query.data?.total ?? 0}</b> </Typography.Text>
<Button type="primary" loading={apply.isPending} onClick={() => apply.mutate()}> </Button>
</div>
<CareTable
rowKey="id"
size="small"
loading={query.isLoading}
dataSource={query.data?.list}
columns={columns}
rowSelection={{
selectedRowKeys: selected,
onChange: (keys) => setSelected(keys.map(Number)) }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }}
/>
</>
)}
</Card>
</div>
)
}