import { useMemo, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Button, Card, Checkbox, Form, Input, Modal, Radio, Select, Space, Table, Typography, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { PlusOutlined, SearchOutlined, ShopOutlined } from '@ant-design/icons' import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' import DraggableModal from '../../components/DraggableModal' import { FormField, FormGrid, FormSection } from '../../components/OrderFormLayout' import BranchFeeFlowModals from './BranchFeeFlowModals' type BranchRow = { id: number name?: string officeName?: string userid?: string phone?: string email?: string sido?: string level?: number grant?: number branchGroupId?: number | null branchGroupName?: string | null hasBusinessLicense?: boolean isTax?: number createdAt?: string } type BranchGroup = { id: number; name: string } type BranchDetail = BranchRow & { fax?: string zipcode?: string address?: string addressDetail?: string tel?: string centerName?: string isPersonal?: boolean accountBank?: string accountNumber?: string accountName?: string contactPhone?: string contactNateon?: string acceptPhone?: number acceptNateon?: number memo?: string adminMemo?: string licenseNumber?: string licenseType?: string licenseItem?: string notifyPhones?: string[] permissionOrderDateUpdate?: boolean permissionCalculate?: boolean permissionConfig?: boolean permissionExcelDownload?: boolean permissionChart?: boolean } const PAGE_SIZE = 50 const BANKS = ['국민은행', '우리은행', '신한은행', '하나은행', '기업은행', '농협', 'SC제일', '카카오뱅크', '기타'] function levelLabel(l?: number) { if (l === 5) return 센터 if (l === 9) return 관리자 if (l === 10) return 최고관리자 return 영업점 } function EditBranchFormBody({ groups, centers, }: { groups: BranchGroup[] centers: { id: number; name: string }[] }) { const isPersonal = Form.useWatch('isPersonal') const level = Form.useWatch('level') return ( <> ({ value: g.id, label: g.name }))} /> 그룹 변경 시 수수료 유지 {level === 5 ? ( ({ value: c.name, label: c.name }))} /> ) : ( 센터 레벨에서 선택 )} {level === 9 && ( 접수일자 변경 정산 환경설정 엑셀 다운로드 통계 )} {isPersonal === false && ( <> > )} ({ value: b, label: b }))} /> 정보동일 핸드폰 문자 받음 네이트온 쪽지 받음 {(fields) => ( {fields.map((field) => ( ))} )} > ) } export default function BranchListPage() { const qc = useQueryClient() const [form] = Form.useForm() const [editForm] = Form.useForm() const [createOpen, setCreateOpen] = useState(false) const [editOpen, setEditOpen] = useState(false) const [feeOpen, setFeeOpen] = useState(false) const [feeBranch, setFeeBranch] = useState(null) const [editingId, setEditingId] = useState(null) const [selectedId, setSelectedId] = useState(null) const [page, setPage] = useState(0) const [draft, setDraft] = useState({ fGroup: '', fSido: '', fUserid: '', fOffice: '', fName: '' }) const [applied, setApplied] = useState(draft) const { data: rows = [], isLoading } = useQuery({ queryKey: ['homes-branches'], queryFn: async () => { const { data } = await client.get>(`${apiPrefix()}/homes/branches`) return unwrap(data) || [] }, }) const { data: groups = [] } = useQuery({ queryKey: ['homes-branch-groups'], queryFn: async () => { const { data } = await client.get>(`${apiPrefix()}/homes/branch-groups`) return unwrap(data) || [] }, }) const { data: centers = [] } = useQuery({ queryKey: ['homes-branch-centers'], queryFn: async () => { const { data } = await client.get>(`${apiPrefix()}/homes/branch-centers`) return unwrap(data) || [] }, }) const sidoOptions = useMemo(() => { const set = new Set() rows.forEach((b) => { if (b.sido) set.add(b.sido) }) return Array.from(set).sort() }, [rows]) const filtered = useMemo(() => { const inc = (v: string | null | undefined, q: string) => (v ?? '').toLowerCase().includes(q.toLowerCase()) return rows.filter((b) => (!applied.fGroup || String(b.branchGroupId ?? '') === applied.fGroup) && (!applied.fSido || b.sido === applied.fSido) && (!applied.fUserid || inc(b.userid, applied.fUserid)) && (!applied.fOffice || inc(b.officeName, applied.fOffice)) && (!applied.fName || inc(b.name, applied.fName)), ) }, [rows, applied]) const pageRows = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE) const grantMut = useMutation({ mutationFn: async ({ id, g }: { id: number; g: number }) => { const { data } = await client.post(`${apiPrefix()}/homes/branches/${id}/grant/${g}`) return unwrap(data as ApiResponse) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['homes-branches'] }) message.success('승인 상태를 변경했습니다.') }, onError: (e: Error) => message.error(e.message), }) const createMut = useMutation({ mutationFn: async (values: Record) => { const params = new URLSearchParams({ level: String(values.level ?? 1) }) if (values.branchGroupId) params.set('branchGroupId', String(values.branchGroupId)) const { data } = await client.post( `${apiPrefix()}/homes/branches?${params.toString()}`, values, ) return unwrap(data as ApiResponse) }, onSuccess: () => { message.success('영업점을 등록했습니다.') setCreateOpen(false) form.resetFields() qc.invalidateQueries({ queryKey: ['homes-branches'] }) }, onError: (e: Error) => message.error(e.message), }) const updateMut = useMutation({ mutationFn: async (values: Record) => { const { data } = await client.put( `${apiPrefix()}/homes/branches/${editingId}/detail`, values, ) return unwrap(data as ApiResponse) }, onSuccess: () => { message.success('수정했습니다.') setEditOpen(false) setEditingId(null) qc.invalidateQueries({ queryKey: ['homes-branches'] }) }, onError: (e: Error) => message.error(e.message), }) const deleteMut = useMutation({ mutationFn: async (id: number) => { const { data } = await client.delete(`${apiPrefix()}/homes/branches/${id}`) return unwrap(data as ApiResponse) }, onSuccess: () => { message.success('삭제했습니다.') qc.invalidateQueries({ queryKey: ['homes-branches'] }) }, onError: (e: Error) => message.error(e.message), }) const openEdit = async (row: BranchRow) => { try { const { data } = await client.get>( `${apiPrefix()}/homes/branches/${row.id}/detail`, ) const detail = unwrap(data) setEditingId(row.id) editForm.setFieldsValue({ ...detail, isPersonal: detail.isPersonal !== false, notifyPhones: [...(detail.notifyPhones || []), '', '', '', '', '', ''].slice(0, 6), keepFee: false, password: '', passwordConfirmation: '', accountNameIsName: false, acceptPhone: detail.acceptPhone === 1, acceptNateon: detail.acceptNateon === 1, }) setEditOpen(true) } catch (e) { message.error((e as Error).message) } } const confirmDelete = (row: BranchRow) => { Modal.confirm({ title: '영업점 삭제', content: ( {row.officeName} 영업점을 삭제하시겠습니까? 관련 접수·정산 자료는 삭제되지 않지만, 영업점 계정 정보는 제거됩니다. ), okText: '삭제', okButtonProps: { danger: true }, cancelText: '취소', onOk: () => deleteMut.mutateAsync(row.id), }) } const columns: ColumnsType = [ { title: '그룹', dataIndex: 'branchGroupName', width: 110, ellipsis: true }, { title: '지역', dataIndex: 'sido', width: 70, align: 'center' }, { title: '아이디', dataIndex: 'userid', width: 100, ellipsis: true }, { title: '영업점명', dataIndex: 'officeName', width: 120, ellipsis: true }, { title: '이름', dataIndex: 'name', width: 90, ellipsis: true }, { title: '레벨', dataIndex: 'level', width: 80, align: 'center', render: (v) => levelLabel(v), }, { title: '연락처', dataIndex: 'phone', width: 120 }, { title: '사업자', dataIndex: 'hasBusinessLicense', width: 70, align: 'center', render: (v) => (v ? '사업자' : '개인'), }, { title: '세금계산서', dataIndex: 'isTax', width: 110, align: 'center', render: (v) => (v === 1 ? '세금계산서발행' : '종합소득세공제'), }, { title: '회원가입일', dataIndex: 'createdAt', width: 150, align: 'center', render: (v?: string) => v?.replace('T', ' ').substring(0, 19), }, { title: '수수료', width: 80, align: 'center', render: (_, r) => ( { e.stopPropagation() setFeeBranch(r) setFeeOpen(true) }} > 수수료 ), }, { title: '승인여부', width: 80, align: 'center', render: (_, r) => ( r.grant === 1 ? ( { e.stopPropagation() grantMut.mutate({ id: r.id, g: 0 }) }} > 승인 ) : ( { e.stopPropagation() grantMut.mutate({ id: r.id, g: 1 }) }} > 거부 ) ), }, { title: '수정', width: 70, align: 'center', render: (_, r) => ( { e.stopPropagation(); void openEdit(r) }} > 수정 ), }, { title: '삭제', width: 70, align: 'center', render: (_, r) => ( { e.stopPropagation(); confirmDelete(r) }} > 삭제 ), }, ] return ( 영업점 목록 영업점을 검색·등록하고 승인·수수료·상세정보를 관리합니다. } onClick={() => { form.resetFields(); form.setFieldsValue({ level: 1, isTax: 1 }); setCreateOpen(true) }}> 등록 setDraft((d) => ({ ...d, fGroup: v ?? '' }))} options={groups.map((g) => ({ value: String(g.id), label: g.name }))} /> setDraft((d) => ({ ...d, fSido: v ?? '' }))} options={sidoOptions.map((s) => ({ value: s, label: s }))} /> setDraft((d) => ({ ...d, fUserid: e.target.value }))} onPressEnter={() => { setApplied(draft); setPage(0) }} /> setDraft((d) => ({ ...d, fOffice: e.target.value }))} onPressEnter={() => { setApplied(draft); setPage(0) }} /> setDraft((d) => ({ ...d, fName: e.target.value }))} onPressEnter={() => { setApplied(draft); setPage(0) }} /> } onClick={() => { setApplied(draft); setPage(0) }} > 검색 총 {filtered.length}건 setPage(p - 1), }} onRow={(r) => ({ onClick: () => setSelectedId(r.id), className: selectedId === r.id ? 'row-selected' : undefined, })} scroll={{ x: 1400 }} locale={{ emptyText: '영업점이 없습니다.' }} /> setCreateOpen(false)} onOk={() => form.submit()} confirmLoading={createMut.isPending} okText="등록" width={860} destroyOnClose className="order-form-modal branch-form-modal" styles={{ body: { maxHeight: 'calc(92vh - 56px)', overflowY: 'auto' } }} > createMut.mutate(v)} initialValues={{ level: 1, isTax: 1 }} > ({ value: g.id, label: g.name }))} /> setEditOpen(false)} onOk={() => editForm.submit()} confirmLoading={updateMut.isPending} okText="수정" width={920} destroyOnClose className="order-form-modal branch-form-modal" styles={{ body: { maxHeight: 'calc(92vh - 56px)', overflowY: 'auto' } }} > { const phones: string[] = values.notifyPhones || [] updateMut.mutate({ ...values, isPersonal: values.isPersonal, acceptPhone: values.acceptPhone ? 1 : 0, acceptNateon: values.acceptNateon ? 1 : 0, notifyPhones: phones, }) }} > { setFeeOpen(false); setFeeBranch(null) }} /> ) }
{row.officeName} 영업점을 삭제하시겠습니까?
관련 접수·정산 자료는 삭제되지 않지만, 영업점 계정 정보는 제거됩니다.
영업점을 검색·등록하고 승인·수수료·상세정보를 관리합니다.