기능수정

This commit is contained in:
Macbook
2026-07-12 19:05:29 +09:00
parent 07a8114927
commit dca36c7b19
19 changed files with 714 additions and 253 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>비즈케어</title>
<title>BizCare</title>
</head>
<body>
<div id="root"></div>
+2 -2
View File
@@ -4,7 +4,7 @@ import {
AppstoreOutlined, CalendarOutlined, ShoppingCartOutlined,
} from '@ant-design/icons'
import SchedulePanel from './SchedulePanel'
import BillCareLogo from './BillCareLogo'
import BizCareLogo from './BizCareLogo'
import AccountPopover from './AccountPopover'
import { careMenuSections, filterMenuSections, resolveSelectedKey, sectionContainsPath, shopMenuSections } from '../menu'
import { fetchMyMenus } from '../api/auth'
@@ -143,7 +143,7 @@ export default function AgentShell({ shop = false }: Props) {
<div className={`topbar-brand${collapsed ? ' is-collapsed' : ''}`} style={collapsed ? undefined : { width: panelWidth }}>
{!collapsed && (
<div className="brand-inline">
<BillCareLogo title="BillCare" size="header" />
<BizCareLogo title="BizCare" size="header" />
</div>
)}
</div>
@@ -3,7 +3,7 @@ import logoMark from '../assets/logo_mark_light.png'
import { useTheme } from '../themeContext'
import type { Theme } from '../theme'
interface BillCareLogoProps {
interface BizCareLogoProps {
className?: string
title?: string
size?: 'header' | 'login'
@@ -18,25 +18,25 @@ function isDarkTheme(theme: Theme) {
* logo_image.png — 라이트 헤더 가로 로고
* logo_mark_light.png / logo_image_theme.png — 마크 (로그인·다크 헤더)
*/
export default function BillCareLogo({
export default function BizCareLogo({
className = '',
title = 'BillCare',
title = 'BizCare',
size = 'header',
forceTheme,
}: BillCareLogoProps) {
}: BizCareLogoProps) {
const { theme } = useTheme()
const active = forceTheme ?? theme
const dark = isDarkTheme(active)
const root = ['billcare-logo', `billcare-logo--${size}`, `billcare-logo--${active}`, className]
const root = ['bizcare-logo', `bizcare-logo--${size}`, `bizcare-logo--${active}`, className]
.filter(Boolean)
.join(' ')
// 로그인: 항상 마크 + BillCare (색상은 CSS로 고정)
// 로그인: 항상 마크 + BizCare (색상은 CSS로 고정)
if (size === 'login') {
return (
<div className={root} title={title}>
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
<span className="billcare-logo-title">{title}</span>
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
<span className="bizcare-logo-title">{title}</span>
</div>
)
}
@@ -54,11 +54,11 @@ export default function BillCareLogo({
)
}
// 다크/블루 헤더: 마크 + BillCare
// 다크/블루 헤더: 마크 + BizCare
return (
<div className={root} title={title}>
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
<span className="billcare-logo-title">{title}</span>
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
<span className="bizcare-logo-title">{title}</span>
</div>
)
}
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
export type MultiSelectOption = [string, string]
type Props = {
options: MultiSelectOption[]
value: string[]
onChange: (v: string[]) => void
placeholder?: string
className?: string
}
/** 클릭 시 펼쳐지는 체크박스 다중선택 (billing_react MultiSelect 이식) */
export default function MultiSelect({ options, value, onChange, placeholder, className }: Props) {
const [open, setOpen] = useState(false)
const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; maxHeight: number } | null>(null)
const rootRef = useRef<HTMLDivElement>(null)
const toggleRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const updatePanelPos = () => {
const el = toggleRef.current
if (!el) return
const r = el.getBoundingClientRect()
setPanelPos({
top: r.bottom + 2,
left: r.left,
width: Math.max(r.width, 160),
maxHeight: Math.min(300, Math.max(120, window.innerHeight - r.bottom - 12)),
})
}
useEffect(() => {
if (!open) return
updatePanelPos()
const onScrollOrResize = () => updatePanelPos()
window.addEventListener('resize', onScrollOrResize)
window.addEventListener('scroll', onScrollOrResize, true)
return () => {
window.removeEventListener('resize', onScrollOrResize)
window.removeEventListener('scroll', onScrollOrResize, true)
}
}, [open])
useEffect(() => {
const h = (e: MouseEvent) => {
const t = e.target as Node
if (rootRef.current?.contains(t) || panelRef.current?.contains(t)) return
setOpen(false)
}
document.addEventListener('mousedown', h)
return () => document.removeEventListener('mousedown', h)
}, [])
const toggle = (k: string) => onChange(value.includes(k) ? value.filter((x) => x !== k) : [...value, k])
const selectAll = () => onChange(options.map(([k]) => k))
const selectNone = () => onChange([])
const text = value.length === 0
? (placeholder || '전체')
: options.filter(([k]) => value.includes(k)).map(([, v]) => v).join(', ')
return (
<div className={`ms${open ? ' is-open' : ''}${className ? ` ${className}` : ''}`} ref={rootRef}>
<button
ref={toggleRef}
type="button"
className="ms-toggle"
onClick={() => setOpen((o) => !o)}
title={text}
>
<span className="ms-text" style={value.length ? undefined : { color: 'var(--muted)' }}>{text}</span>
<span className="ms-caret">▾</span>
</button>
{open && panelPos && createPortal(
<div
ref={panelRef}
className="ms-panel ms-panel-portal"
style={{ top: panelPos.top, left: panelPos.left, width: panelPos.width, maxHeight: panelPos.maxHeight }}
>
<div className="ms-actions">
<button type="button" className="ms-action" onClick={selectAll}>전체 선택</button>
<button type="button" className="ms-action ms-action-muted" onClick={selectNone}>전체 해제</button>
</div>
{options.map(([k, v]) => (
<label key={k} className="ms-item">
<input type="checkbox" checked={value.includes(k)} onChange={() => toggle(k)} /> {v}
</label>
))}
</div>,
document.body,
)}
</div>
)
}
export function mapOptions(items: { id: number; name: string }[]): MultiSelectOption[] {
return items.map((x) => [String(x.id), x.name])
}
export function entriesOptions(map: Record<string, string> | undefined): MultiSelectOption[] {
return map ? Object.entries(map) : []
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { type FormEvent, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { login } from '../api/auth'
import BillCareLogo from '../components/BillCareLogo'
import BizCareLogo from '../components/BizCareLogo'
export default function LoginPage() {
const navigate = useNavigate()
@@ -34,7 +34,7 @@ export default function LoginPage() {
<div className="login-wrap">
<form className="login-box" onSubmit={submit}>
<div className="login-brand">
<BillCareLogo title="BillCare" size="login" />
<BizCareLogo title="BizCare" size="login" />
<p>통합 영업·정산 관리 시스템</p>
</div>
<div className="field">
+222 -111
View File
@@ -15,6 +15,7 @@ import {
import ReceptionApplyModal from '../../components/ReceptionApplyModal'
import ReceptionBulkModal from '../../components/ReceptionBulkModal'
import ReceptionProgressModal from '../../components/ReceptionProgressModal'
import MultiSelect, { entriesOptions, mapOptions, type MultiSelectOption } from '../../components/MultiSelect'
import { DownOutlined } from '@ant-design/icons'
import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client'
import { options } from './homesShared'
@@ -29,6 +30,7 @@ import {
PROGRESS_STATUS,
GIFT_STATUS,
CENTER_STATUS,
GIFT_PLACE,
} from './receptionStatuses'
type ReceptionRow = {
@@ -96,11 +98,20 @@ type ReceptionResult = {
productCategories?: string[]
}
type IspProduct = { id: number; name: string; type?: string }
const SIDO_LIST = [
'서울', '경기', '인천', '강원', '충북', '충남', '대전', '전북', '전남', '광주',
'경북', '대구', '경남', '울산', '부산', '제주',
]
const PRODUCT_TYPE_OPTS: MultiSelectOption[] = [
['e01', '인터넷'],
['e02', '전화'],
['e03', 'TV'],
['e04', '렌탈'],
]
const initialFilters = () => ({
page: 0,
size: 50,
@@ -118,19 +129,69 @@ const initialFilters = () => ({
ispName: '',
branchEmployee: '',
branchName: '',
giftType: undefined as string | undefined,
productType: undefined as string | undefined,
progressStatus: undefined as string | undefined,
giftStatus: undefined as string | undefined,
giftPlace: undefined as string | undefined,
customerSido: undefined as string | undefined,
carrier: undefined as string | undefined,
branch: undefined as string | undefined,
settleStatus: undefined as string | undefined,
centerName: '',
paymentTransferSsid: '',
paymentTransferNumber: '',
giftType: [] as string[],
productType: [] as string[],
progressStatus: [] as string[],
giftStatus: [] as string[],
centerStatus: [] as string[],
customerSido: [] as string[],
ispId: [] as string[],
productNames: [] as string[],
productIds: [] as string[],
branches: [] as string[],
branchIds: [] as string[],
processors: [] as string[],
centerNames: [] as string[],
branchGroupIds: [] as string[],
giftPlace: '',
opended: undefined as string | undefined,
giftSended: undefined as string | undefined,
})
type Filters = ReturnType<typeof initialFilters>
const csv = (arr: string[]) => (arr.length ? arr.join(',') : undefined)
/** API 쿼리 파라미터 — 배열은 comma join */
const toSearchParams = (f: Filters, opts?: { omitPaging?: boolean }) => ({
...(opts?.omitPaging ? {} : { page: f.page, size: f.size }),
orderDateStart: f.orderDateStart || undefined,
orderDateEnd: f.orderDateEnd || undefined,
sorting: f.sorting,
postIt: f.postIt || undefined,
agreementName: f.agreementName || undefined,
customerSsid: f.customerSsid || undefined,
customerEmail: f.customerEmail || undefined,
customerName: f.customerName || undefined,
customerPhone: f.customerPhone || undefined,
contractNumber: f.contractNumber || undefined,
productName: f.productName || undefined,
ispName: f.ispName || undefined,
branchEmployee: f.branchEmployee || undefined,
branchName: f.branchName || undefined,
centerName: f.centerName || undefined,
paymentTransferSsid: f.paymentTransferSsid || undefined,
paymentTransferNumber: f.paymentTransferNumber || undefined,
giftType: csv(f.giftType),
productType: csv(f.productType),
progressStatus: csv(f.progressStatus),
giftStatus: csv(f.giftStatus),
centerStatus: csv(f.centerStatus),
customerSido: csv(f.customerSido),
ispId: csv(f.ispId),
productNames: csv(f.productNames),
ispProductId: csv(f.productNames.length ? f.productNames : f.productIds),
branch: csv(f.branches.length ? f.branches : f.branchIds),
processor: csv(f.processors.length ? f.processors : f.centerNames),
branchGroupId: csv(f.branchGroupIds),
giftPlace: f.giftPlace || undefined,
opended: f.opended || undefined,
giftSended: f.giftSended || undefined,
})
const num = (n?: number | null) => (n == null ? '' : Number(n).toLocaleString())
const giftPlaceLabel = (v?: string) => placeLabel(v)
@@ -164,39 +225,57 @@ export default function ReceptionListPage() {
},
})
const branchGroupsQuery = useQuery({
queryKey: ['homes-branch-groups'],
queryFn: async () => {
const { data } = await client.get<ApiResponse<{ id: number; name: string }[]>>(
`${apiPrefix()}/homes/branch-groups`,
)
return unwrap(data) || []
},
})
const inputCentersQuery = useQuery({
queryKey: ['homes-input-centers'],
queryFn: async () => {
const { data } = await client.get<ApiResponse<{ id: number; name?: string }[]>>(
`${apiPrefix()}/homes/input-centers`,
)
return unwrap(data) || []
},
})
const branchesQuery = useQuery({
queryKey: ['homes-branches-office'],
queryFn: async () => {
const { data } = await client.get<ApiResponse<{ id: number; name?: string; officeName?: string }[]>>(
`${apiPrefix()}/homes/branches`,
)
return unwrap(data) || []
},
})
const productsQuery = useQuery({
queryKey: ['homes-reception-products-multi', filters.ispId],
queryFn: async () => {
const results = await Promise.all(
filters.ispId.map(async (id) => {
const { data } = await client.get<ApiResponse<IspProduct[]>>(
`${apiPrefix()}/homes/reception-isps/${id}/products`,
)
return unwrap(data) || []
}),
)
return results.flat()
},
enabled: filters.ispId.length > 0,
})
const query = useQuery({
queryKey: ['homes-receptions', filters],
queryFn: async () => {
const { data } = await client.get<ApiResponse<ReceptionResult>>(`${apiPrefix()}/homes/receptions`, {
params: {
page: filters.page,
size: filters.size,
orderDateStart: filters.orderDateStart || undefined,
orderDateEnd: filters.orderDateEnd || undefined,
sorting: filters.sorting,
postIt: filters.postIt || undefined,
agreementName: filters.agreementName || undefined,
customerSsid: filters.customerSsid || undefined,
customerEmail: filters.customerEmail || undefined,
customerName: filters.customerName || undefined,
customerPhone: filters.customerPhone || undefined,
contractNumber: filters.contractNumber || undefined,
productName: filters.productName || undefined,
ispName: filters.ispName || undefined,
branchEmployee: filters.branchEmployee || undefined,
branchName: filters.branchName || undefined,
giftType: filters.giftType || undefined,
productType: filters.productType || undefined,
progressStatus: filters.progressStatus || undefined,
giftStatus: filters.giftStatus || undefined,
giftPlace: filters.giftPlace || undefined,
customerSido: filters.customerSido || undefined,
carrier: filters.carrier || undefined,
branch: filters.branch || undefined,
settleStatus: filters.settleStatus || undefined,
opended: filters.opended || undefined,
giftSended: filters.giftSended || undefined,
},
params: toSearchParams(filters),
})
return unwrap(data)
},
@@ -218,8 +297,52 @@ export default function ReceptionListPage() {
const giftSelectOpts = statusOptions(giftLabels as Record<string, string>)
const settleSelectOpts = statusOptions(settleLabels as Record<string, string>)
const setD = (patch: Partial<ReturnType<typeof initialFilters>>) => setDraft((p) => ({ ...p, ...patch }))
const applyF = (patch: Partial<ReturnType<typeof initialFilters>>) => {
const giftTypeOpts: MultiSelectOption[] = (
query.data?.giftCategories || ['상품권', '가전', '캐시백', '없음']
).map((x) => [x, x])
const progressMsOpts = entriesOptions(progressLabels as Record<string, string>)
const giftMsOpts = entriesOptions(giftLabels as Record<string, string>)
const centerStatusMsOpts = entriesOptions(settleLabels as Record<string, string>)
const sidoMsOpts: MultiSelectOption[] = SIDO_LIST.map((s) => [s, s])
const ispMsOpts = mapOptions(ispQuery.data || [])
const branchGroupMsOpts = mapOptions(branchGroupsQuery.data || [])
const processorMsOpts: MultiSelectOption[] = useMemo(() => {
const names = new Set<string>()
for (const p of query.data?.processors || []) if (p) names.add(p)
for (const c of inputCentersQuery.data || []) if (c.name) names.add(c.name)
names.add('본사')
return Array.from(names).sort().map((n) => [n, n] as MultiSelectOption)
}, [query.data?.processors, inputCentersQuery.data])
const branchMsOpts: MultiSelectOption[] = useMemo(() => {
const names = new Set<string>()
for (const b of query.data?.branches || []) if (b) names.add(b)
for (const b of branchesQuery.data || []) {
const n = b.officeName || b.name
if (n) names.add(n)
}
return Array.from(names).sort().map((n) => [n, n] as MultiSelectOption)
}, [query.data?.branches, branchesQuery.data])
const productMsOpts: MultiSelectOption[] = useMemo(() => {
let items = productsQuery.data || []
if (filters.productType.length) {
items = items.filter((p) => filters.productType.includes(p.type || ''))
}
const seen = new Set<string>()
const opts: MultiSelectOption[] = []
for (const p of items) {
if (!p.name || seen.has(p.name)) continue
seen.add(p.name)
opts.push([p.name, p.name])
}
return opts
}, [productsQuery.data, filters.productType])
const setD = (patch: Partial<Filters>) => setDraft((p) => ({ ...p, ...patch }))
const applyF = (patch: Partial<Filters>) => {
const next = { ...draft, ...patch, page: 0 }
setDraft(next)
setFilters(next)
@@ -325,7 +448,7 @@ export default function ReceptionListPage() {
const downloadExcel = async () => {
try {
const { data } = await client.get(`${apiPrefix()}/homes/receptions/excel`, {
params: { ...filters, page: undefined, size: undefined },
params: toSearchParams(filters, { omitPaging: true }),
responseType: 'blob',
})
const url = URL.createObjectURL(data)
@@ -405,11 +528,6 @@ export default function ReceptionListPage() {
</div>
</div>
<div className="toolbar order-top-buttons">
<Button type="primary" onClick={search}>검색</Button>
<Button onClick={reset}>검색 초기화</Button>
</div>
<div className="order-filter">
<div className="order-date-row">
<DatePicker
@@ -447,28 +565,27 @@ export default function ReceptionListPage() {
/>
</label>
<div className="order-attach-buttons">
<Button type="primary" className="order-search-btn" onClick={search}>검색</Button>
<Button onClick={reset}>검색 초기화</Button>
<Button onClick={downloadExcel}>Excel 다운로드</Button>
<Button onClick={downloadExcel}>사은품 Excel 다운로드</Button>
</div>
</div>
<div className="order-result-row">
<span className="order-result muted">검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건</span>
</div>
<div className="order-search-fields">
<Select
allowClear
<MultiSelect
className="ff"
placeholder="사은품"
options={giftTypeOpts}
value={draft.giftType}
onChange={(v) => setD({ giftType: v })}
options={options(query.data?.giftCategories || ['상품권', '가전', '캐시백', '없음'])}
/>
<input className="ff-i" placeholder="포스트잇" value={draft.postIt} onChange={(e) => setD({ postIt: e.target.value })} />
<input className="ff-i" placeholder="약정" value={draft.agreementName} onChange={(e) => setD({ agreementName: e.target.value })} />
<input className="ff-i" placeholder="주민번호" value={draft.customerSsid} onChange={(e) => setD({ customerSsid: e.target.value })} />
<input className="ff-i" placeholder="이메일" value={draft.customerEmail} onChange={(e) => setD({ customerEmail: e.target.value })} />
<input className="ff-i" placeholder="납입자주민" value={draft.paymentTransferSsid} onChange={(e) => setD({ paymentTransferSsid: e.target.value })} />
<input className="ff-i" placeholder="납입계좌번호" value={draft.paymentTransferNumber} onChange={(e) => setD({ paymentTransferNumber: e.target.value })} />
<label className="ff-d">
개통일자
<DatePicker
@@ -484,9 +601,18 @@ export default function ReceptionListPage() {
onChange={(d) => setD({ giftSended: d ? d.format('YYYY-MM-DD') : undefined })}
/>
</label>
<input className="ff-i" placeholder="입력처명" value={draft.centerName} onChange={(e) => setD({ centerName: e.target.value })} />
<input className="ff-i" placeholder="영업점명" value={draft.branchName} onChange={(e) => setD({ branchName: e.target.value })} />
<MultiSelect
className="ff"
placeholder="영업점 그룹"
options={branchGroupMsOpts}
value={draft.branchGroupIds}
onChange={(v) => setD({ branchGroupIds: v })}
/>
<input className="ff-i" placeholder="통신사명" value={draft.ispName} onChange={(e) => setD({ ispName: e.target.value })} />
<input className="ff-i" placeholder="상품명" value={draft.productName} onChange={(e) => setD({ productName: e.target.value })} />
<span className="order-result muted">검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건</span>
</div>
</div>
@@ -523,50 +649,45 @@ export default function ReceptionListPage() {
</select>
</th>
<th className="col-center">
<Select
allowClear
size="small"
<MultiSelect
placeholder="입력처"
value={draft.settleStatus}
onChange={(v) => applyF({ settleStatus: v })}
options={settleSelectOpts}
style={{ width: '100%' }}
options={processorMsOpts}
value={draft.processors}
onChange={(v) => applyF({ processors: v, centerNames: v })}
/>
</th>
<th className="col-branch">
<Select
allowClear
size="small"
<MultiSelect
placeholder="영업점"
value={draft.branch}
onChange={(v) => applyF({ branch: v })}
options={options(query.data?.branches || [])}
style={{ width: '100%' }}
options={branchMsOpts}
value={draft.branches}
onChange={(v) => applyF({ branches: v, branchIds: v })}
/>
</th>
<th className="col-isp">
<Select
allowClear
size="small"
<MultiSelect
placeholder="통신사"
value={draft.carrier}
onChange={(v) => applyF({ carrier: v })}
options={options(query.data?.carriers || [])}
style={{ width: '100%' }}
options={ispMsOpts}
value={draft.ispId}
onChange={(v) => applyF({ ispId: v, productNames: [], productIds: [] })}
/>
</th>
<th className="col-ptype">
<Select
allowClear
size="small"
<MultiSelect
placeholder="상품종류"
options={PRODUCT_TYPE_OPTS}
value={draft.productType}
onChange={(v) => applyF({ productType: v })}
options={options(query.data?.productCategories || ['인터넷', '전화', 'TV', '렌탈'])}
style={{ width: '100%' }}
/>
</th>
<th className="col-product">&nbsp;</th>
<th className="col-product">
<MultiSelect
placeholder="상품"
options={productMsOpts}
value={draft.productNames}
onChange={(v) => applyF({ productNames: v, productIds: v })}
/>
</th>
<th className="col-customer">
<input
placeholder="고객명"
@@ -576,14 +697,11 @@ export default function ReceptionListPage() {
/>
</th>
<th className="col-region">
<Select
allowClear
size="small"
<MultiSelect
placeholder="지역"
options={sidoMsOpts}
value={draft.customerSido}
onChange={(v) => applyF({ customerSido: v })}
options={options(SIDO_LIST)}
style={{ width: '100%' }}
/>
</th>
<th className="col-idnum">
@@ -595,14 +713,11 @@ export default function ReceptionListPage() {
/>
</th>
<th className="col-status">
<Select
allowClear
size="small"
<MultiSelect
placeholder="진행현황"
options={progressMsOpts}
value={draft.progressStatus}
onChange={(v) => applyF({ progressStatus: v })}
options={progressSelectOpts}
style={{ width: '100%' }}
/>
</th>
<th className="col-contract">
@@ -614,33 +729,29 @@ export default function ReceptionListPage() {
/>
</th>
<th className="col-giftplace">
<Select
allowClear
size="small"
placeholder="지급처"
value={draft.giftPlace}
onChange={(v) => applyF({ giftPlace: v })}
options={[
{ value: 'b01', label: '본사요청' },
{ value: 'b02', label: '자체발송' },
{ value: '본사', label: '본사' },
{ value: '영업점', label: '영업점' },
]}
style={{ width: '100%' }}
/>
<select value={draft.giftPlace} onChange={(e) => applyF({ giftPlace: e.target.value })}>
<option value="">모두</option>
{Object.entries(GIFT_PLACE).map(([code, label]) => (
<option key={code} value={code}>{label}</option>
))}
</select>
</th>
<th className="col-gift">
<Select
allowClear
size="small"
placeholder="사은품진행"
<MultiSelect
placeholder="사은품진행현황"
options={giftMsOpts}
value={draft.giftStatus}
onChange={(v) => applyF({ giftStatus: v })}
options={giftSelectOpts}
style={{ width: '100%' }}
/>
</th>
<th className="col-cstatus">&nbsp;</th>
<th className="col-cstatus">
<MultiSelect
placeholder="입력처진행현황"
options={centerStatusMsOpts}
value={draft.centerStatus}
onChange={(v) => applyF({ centerStatus: v })}
/>
</th>
</tr>
</thead>
<tbody>
+182 -31
View File
@@ -43,14 +43,14 @@ body { margin: 0; background: var(--bg); color: var(--text); }
text-align: center;
margin-bottom: 22px;
}
.login-box .login-brand .billcare-logo {
.login-box .login-brand .bizcare-logo {
margin: 0 auto;
}
.login-box .login-brand .billcare-logo-mark {
.login-box .login-brand .bizcare-logo-mark {
height: 64px;
width: auto;
}
.login-box .login-brand .billcare-logo-title {
.login-box .login-brand .bizcare-logo-title {
font-size: 28px;
color: var(--login-text) !important;
}
@@ -108,7 +108,7 @@ body { margin: 0; background: var(--bg); color: var(--text); }
font-size: 12px;
}
.billcare-logo {
.bizcare-logo {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -116,39 +116,39 @@ body { margin: 0; background: var(--bg); color: var(--text); }
max-width: 100%;
background: transparent !important;
}
.billcare-logo--login {
.bizcare-logo--login {
flex-direction: column;
gap: 10px;
}
.billcare-logo--header.billcare-logo--dark,
.billcare-logo--header.billcare-logo--blue {
.bizcare-logo--header.bizcare-logo--dark,
.bizcare-logo--header.bizcare-logo--blue {
flex-direction: row;
align-items: center;
justify-content: center;
gap: 10px;
}
img.billcare-logo,
.billcare-logo img,
.billcare-logo-mark {
img.bizcare-logo,
.bizcare-logo img,
.bizcare-logo-mark {
display: block;
background: transparent !important;
object-fit: contain;
}
img.billcare-logo--header {
img.bizcare-logo--header {
height: 28px;
width: auto;
max-width: min(150px, 100%);
object-position: center;
}
.billcare-logo--header .billcare-logo-mark {
.bizcare-logo--header .bizcare-logo-mark {
height: 30px;
width: auto;
}
.billcare-logo--login .billcare-logo-mark {
.bizcare-logo--login .bizcare-logo-mark {
height: 64px;
width: auto;
}
.billcare-logo-title {
.bizcare-logo-title {
font-family: 'Pretendard', -apple-system, 'Malgun Gothic', sans-serif;
font-weight: 800;
font-size: 15px;
@@ -157,14 +157,14 @@ img.billcare-logo--header {
color: var(--text);
white-space: nowrap;
}
.billcare-logo--header .billcare-logo-title {
.bizcare-logo--header .bizcare-logo-title {
font-size: 19px;
transform: translateY(2px);
}
.billcare-logo--login .billcare-logo-title {
.bizcare-logo--login .bizcare-logo-title {
font-size: 28px;
}
.billcare-logo--blue .billcare-logo-title {
.bizcare-logo--blue .bizcare-logo-title {
color: #e8f4ff;
}
.brand-inline {
@@ -177,7 +177,7 @@ img.billcare-logo--header {
overflow: hidden;
padding: 0 8px;
}
.brand-inline .billcare-logo {
.brand-inline .bizcare-logo {
max-width: 100%;
justify-content: center;
align-items: center;
@@ -4251,7 +4251,7 @@ html.theme-light .reception-order-page {
outline: none;
border-color: var(--primary);
}
.order-result { margin-left: auto; font-size: 12px; }
.order-result { margin-left: auto; font-size: 12px; white-space: nowrap; }
.order-attach-buttons {
margin-left: auto;
display: inline-flex;
@@ -4259,15 +4259,10 @@ html.theme-light .reception-order-page {
gap: 6px;
flex-wrap: wrap;
}
.order-result-row {
display: flex;
align-items: center;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.order-result-row .order-result {
margin-left: 0;
.order-attach-buttons .order-search-btn.ant-btn-primary,
.order-attach-buttons .order-search-btn.ant-btn-primary:hover,
.order-attach-buttons .order-search-btn.ant-btn-primary:focus {
color: #000 !important;
}
.order-search-fields {
display: flex;
@@ -4352,12 +4347,168 @@ table.order-list tr.order-col-filter select {
width: 100%;
font-size: 10px;
padding: 2px 4px;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
background: var(--surface) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 3px;
box-sizing: border-box;
}
table.order-list tr.order-col-filter select option {
background: var(--surface);
color: var(--text);
}
table.order-list tr.order-col-filter .ms-toggle {
padding: 2px 4px;
font-size: 10px;
min-width: 0;
width: 100%;
}
table.order-list tr.order-col-filter .ms-panel,
table.order-list tr.order-col-filter .ms-panel-portal {
min-width: 140px;
}
.order-search-fields .ff .ms-toggle {
width: 100%;
font-size: 11px;
padding: 6px 8px;
}
/* MultiSelect — 테마 토큰 기반 */
.ms { position: relative; display: inline-block; width: 100%; min-width: 0; }
.ms-toggle {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
text-align: left;
padding: 8px 10px;
cursor: pointer;
background: var(--surface) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 4px;
box-shadow: none !important;
}
.ms-toggle:hover {
border-color: color-mix(in srgb, var(--primary) 45%, var(--border)) !important;
color: var(--text) !important;
}
.ms.is-open .ms-toggle,
.ms-toggle:focus-visible {
border-color: var(--primary) !important;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent) !important;
outline: none;
}
.ms-toggle .ms-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
color: inherit;
}
.ms-toggle .ms-caret { color: var(--muted); font-size: 10px; flex-shrink: 0; }
.ms-panel,
.ms-panel-portal {
background: var(--elevated, var(--surface)) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 6px;
box-shadow: var(--shadow-lg, var(--shadow)) !important;
padding: 4px;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--primary) 45%, var(--muted)) var(--surface-2);
}
.ms-panel {
position: absolute;
z-index: 60;
top: calc(100% + 2px);
left: 0;
right: 0;
min-width: 160px;
max-height: 240px;
overflow-y: auto;
}
.ms-panel-portal {
position: fixed;
z-index: 5000;
right: auto;
min-width: 160px;
overflow-y: auto;
}
.ms-panel::-webkit-scrollbar,
.ms-panel-portal::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.ms-panel::-webkit-scrollbar-track,
.ms-panel-portal::-webkit-scrollbar-track {
background: var(--surface-2);
border-radius: 4px;
}
.ms-panel::-webkit-scrollbar-thumb,
.ms-panel-portal::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--primary) 40%, var(--muted));
border-radius: 4px;
border: 2px solid var(--surface-2);
}
.ms-panel::-webkit-scrollbar-thumb:hover,
.ms-panel-portal::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--primary) 60%, var(--muted));
}
.ms-actions {
display: flex;
gap: 4px;
padding: 2px 2px 6px;
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
position: sticky;
top: 0;
background: var(--elevated, var(--surface));
z-index: 1;
}
.ms-action {
padding: 2px 6px;
font-size: 11px;
background: transparent !important;
border: 1px solid var(--border) !important;
border-radius: 3px;
color: var(--text) !important;
cursor: pointer;
}
.ms-action:hover {
background: color-mix(in srgb, var(--primary) 14%, transparent) !important;
border-color: color-mix(in srgb, var(--primary) 40%, var(--border)) !important;
color: var(--text) !important;
}
.ms-action-muted { color: var(--muted) !important; }
.ms-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
border-radius: 4px;
color: var(--text) !important;
}
.ms-item:hover {
background: color-mix(in srgb, var(--primary) 14%, transparent) !important;
}
.ms-item:has(input:checked) {
background: color-mix(in srgb, var(--primary) 18%, transparent);
}
.ms-item input[type='checkbox'] {
width: auto;
margin: 0;
accent-color: var(--primary);
flex-shrink: 0;
}
table.order-list tbody tr { cursor: pointer; }
table.order-list tbody tr:hover td { background: color-mix(in srgb, var(--primary) 8%, var(--surface)); }
table.order-list .c-name { color: var(--c-name); }