Customer 페이지 추가.
This commit is contained in:
parent
6a86f05121
commit
7d7e6607f8
|
|
@ -8,8 +8,13 @@ import Login from '../auth/pages/Login'
|
||||||
import Home from '../auth/pages/Home'
|
import Home from '../auth/pages/Home'
|
||||||
import Admin from '../auth/pages/Admin'
|
import Admin from '../auth/pages/Admin'
|
||||||
import ProductsPage from '../features/products/ProductsPage'
|
import ProductsPage from '../features/products/ProductsPage'
|
||||||
|
|
||||||
|
// CS
|
||||||
|
import CsHomePage from '../features/crm/CsHome/CsHomePage'
|
||||||
import DriverDailyStatPage from '../features/crm/DriverDailyStat/DriverDailyStatPage'
|
import DriverDailyStatPage from '../features/crm/DriverDailyStat/DriverDailyStatPage'
|
||||||
import CsHomePage from '../features/crm/CsHome/CsHomePage'
|
import CustomerListPage from '../features/crm/Customer/CustomerListPage'
|
||||||
|
|
||||||
|
//
|
||||||
import { RequireAuth, RequireRole } from '../auth/authGuard'
|
import { RequireAuth, RequireRole } from '../auth/authGuard'
|
||||||
import { NAV } from '../lib/navConfig'
|
import { NAV } from '../lib/navConfig'
|
||||||
|
|
||||||
|
|
@ -18,6 +23,7 @@ import { NAV } from '../lib/navConfig'
|
||||||
const REAL_PAGES = {
|
const REAL_PAGES = {
|
||||||
'/crm/home': <CsHomePage />,
|
'/crm/home': <CsHomePage />,
|
||||||
'/crm/driver-daily-stat': <DriverDailyStatPage />,
|
'/crm/driver-daily-stat': <DriverDailyStatPage />,
|
||||||
|
'/crm/customers': <CustomerListPage />,
|
||||||
'/opr/products': <ProductsPage />,
|
'/opr/products': <ProductsPage />,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
/* Customer list — co-located styles.
|
||||||
|
Mirrors the app's plain class-based CSS and green brand (#4CAF50). */
|
||||||
|
|
||||||
|
.cust-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-title { margin: 0; font-size: 20px; }
|
||||||
|
|
||||||
|
.cust-sub {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #2e7d32;
|
||||||
|
background: #eef5ef;
|
||||||
|
border: 1px solid #d5e6d7;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #eceef0;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 11px 14px;
|
||||||
|
background: #f7f8f9;
|
||||||
|
border-bottom: 1px solid #eceef0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4b5563;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-table td {
|
||||||
|
padding: 11px 14px;
|
||||||
|
border-bottom: 1px solid #f1f2f4;
|
||||||
|
color: #1f2937;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-table tbody tr:last-child td { border-bottom: none; }
|
||||||
|
.cust-table tbody tr:hover { background: #fafbfc; }
|
||||||
|
|
||||||
|
.cust-status {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f1f2f4;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-status.on {
|
||||||
|
color: #2e7d32;
|
||||||
|
background: #eef5ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-state {
|
||||||
|
padding: 32px 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-msg {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-msg.error {
|
||||||
|
color: #b3261e;
|
||||||
|
background: #fdecea;
|
||||||
|
border: 1px solid #f6cdc8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-pager {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-pageinfo { font-size: 14px; color: #4b5563; }
|
||||||
|
|
||||||
|
.cust-btn {
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
border: 1px solid #4CAF50;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-btn.ghost {
|
||||||
|
background: #fff;
|
||||||
|
color: #2e7d32;
|
||||||
|
border-color: #dcdfe3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-link {
|
||||||
|
color: #1f2937;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-link:hover {
|
||||||
|
color: #2e7d32;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-ext {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
vertical-align: super;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-link:hover .cust-ext { color: #2e7d32; }
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
// src/features/crm/Customer/CustomerListPage.jsx
|
||||||
|
// 고객 목록 — 조회 전용. 데이터는 MIS에서 CDC로 sync, 쓰기는 MIS 전담.
|
||||||
|
// 백엔드: GET /customer?page=&size= -> Spring Page<CustomerResponseDto>
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { fetchWithRefresh } from '../../../lib/api'
|
||||||
|
import { crmUrl } from '../../../lib/endpoints'
|
||||||
|
import './Customer.css'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
// 'A'=Active(활성)만 확인됨. 나머지 cus_status 코드는 추후 채우기.
|
||||||
|
const STATUS_LABELS = { A: '활성' }
|
||||||
|
const statusLabel = (code) => (code ? (STATUS_LABELS[code] ?? code) : '-')
|
||||||
|
|
||||||
|
const formatDate = (v) => (v ? String(v).slice(0, 10) : '-')
|
||||||
|
|
||||||
|
const regionText = (c) => {
|
||||||
|
const parts = [c.cusCity, c.cusProvince].filter(Boolean)
|
||||||
|
return parts.length ? parts.join(', ') : (c.cusRegionId ?? '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneText = (c) => {
|
||||||
|
if (!c.cusPhone) return '-'
|
||||||
|
return c.cusPhoneExt ? `${c.cusPhone} (${c.cusPhoneExt})` : c.cusPhone
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIS 고객 상세 딥링크. 포털은 c_uid를 모르므로 cus_no(=MIS c_accountno)로 진입.
|
||||||
|
// (MIS customer_detail 핸들러가 c_accountno -> c_uid 해석하도록 수정되어 있어야 함)
|
||||||
|
// TODO: 운영/개발 분리하려면 import.meta.env.VITE_MIS_BASE_URL 로 빼기
|
||||||
|
const MIS_BASE = 'https://mis.greenoilinc.com/index_intranet.php'
|
||||||
|
const misCustomerUrl = (cusNo) =>
|
||||||
|
`${MIS_BASE}?view=customer_detail&mode=update&c_accountno=${encodeURIComponent(cusNo)}`
|
||||||
|
|
||||||
|
export default function CustomerListPage() {
|
||||||
|
const [rows, setRows] = useState([])
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const [totalPages, setTotalPages] = useState(0)
|
||||||
|
const [totalElements, setTotalElements] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
const load = useCallback(async (signal) => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const res = await fetchWithRefresh(crmUrl(`/customer?page=${page}&size=${PAGE_SIZE}`))
|
||||||
|
if (!res.ok) throw new Error(`고객 목록 요청 실패 (${res.status})`)
|
||||||
|
const json = await res.json() // Spring Page: { content, totalElements, totalPages, number }
|
||||||
|
if (signal?.aborted) return
|
||||||
|
setRows(json.content ?? [])
|
||||||
|
setTotalPages(json.totalPages ?? 0)
|
||||||
|
setTotalElements(json.totalElements ?? 0)
|
||||||
|
setError(null)
|
||||||
|
} catch (e) {
|
||||||
|
if (!signal?.aborted) { setError(e.message); setRows([]) }
|
||||||
|
} finally {
|
||||||
|
if (!signal?.aborted) setLoading(false)
|
||||||
|
}
|
||||||
|
}, [page])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
load(ctrl.signal)
|
||||||
|
return () => ctrl.abort()
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="cust-head">
|
||||||
|
<h2 className="cust-title">고객 관리</h2>
|
||||||
|
<span className="cust-sub">
|
||||||
|
총 {totalElements.toLocaleString()}건
|
||||||
|
<span className="cust-badge">MIS 연동 · 조회 전용</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="cust-msg error">불러오기 오류: {error}</div>}
|
||||||
|
|
||||||
|
<div className="cust-card">
|
||||||
|
<table className="cust-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>고객번호</th>
|
||||||
|
<th>고객명</th>
|
||||||
|
<th>상태</th>
|
||||||
|
<th>지역</th>
|
||||||
|
<th>연락처</th>
|
||||||
|
<th>계약일</th>
|
||||||
|
<th>영업담당</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{loading ? (
|
||||||
|
<tr><td colSpan={7} className="cust-state">불러오는 중…</td></tr>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<tr><td colSpan={7} className="cust-state">등록된 고객이 없습니다.</td></tr>
|
||||||
|
) : (
|
||||||
|
rows.map((c) => (
|
||||||
|
<tr key={c.cusUuid}>
|
||||||
|
<td>{c.cusNo ?? '-'}</td>
|
||||||
|
<td>
|
||||||
|
{c.cusNo ? (
|
||||||
|
<a className="cust-link" href={misCustomerUrl(c.cusNo)}
|
||||||
|
target="_blank" rel="noopener noreferrer"
|
||||||
|
title="MIS에서 열기">
|
||||||
|
{c.cusName ?? '-'}<span className="cust-ext">↗</span>
|
||||||
|
</a>
|
||||||
|
) : (c.cusName ?? '-')}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`cust-status${c.cusStatus === 'A' ? ' on' : ''}`}>
|
||||||
|
{statusLabel(c.cusStatus)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{regionText(c)}</td>
|
||||||
|
<td>{phoneText(c)}</td>
|
||||||
|
<td>{formatDate(c.cusContractDate)}</td>
|
||||||
|
<td>{c.cusSalesperson ?? '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="cust-pager">
|
||||||
|
<button className="cust-btn ghost"
|
||||||
|
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||||
|
disabled={page === 0 || loading}>이전</button>
|
||||||
|
<span className="cust-pageinfo">{page + 1} / {totalPages}</span>
|
||||||
|
<button className="cust-btn ghost"
|
||||||
|
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||||
|
disabled={page >= totalPages - 1 || loading}>다음</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue