Remove 'source_type' column from GRID_COLUMNS in receiving pages for multiple companies to streamline data display.
This commit is contained in:
@@ -83,7 +83,6 @@ const GRID_COLUMNS = [
|
||||
{ key: "inbound_type", label: "입고유형" },
|
||||
{ key: "inbound_date", label: "입고일" },
|
||||
{ key: "reference_number", label: "참조번호" },
|
||||
{ key: "source_type", label: "데이터출처" },
|
||||
{ key: "supplier_name", label: "공급처" },
|
||||
{ key: "item_number", label: "품목코드" },
|
||||
{ key: "item_name", label: "품목명" },
|
||||
|
||||
@@ -88,25 +88,65 @@ export default function QualityMonitoringPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("all");
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 서버 페이징 + KPI summary
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(50);
|
||||
const [total, setTotal] = useState(0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const [serverSummary, setServerSummary] = useState<{ total: number; passed: number; failed: number; pending: number; passRate: number }>({ total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
|
||||
// 기간 필터 (기본: 오늘)
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
const [dateFrom, setDateFrom] = useState<string>(todayStr);
|
||||
const [dateTo, setDateTo] = useState<string>(todayStr);
|
||||
const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); };
|
||||
const setRangeThisWeek = () => {
|
||||
const now = new Date();
|
||||
const day = now.getDay() || 7;
|
||||
const mon = new Date(now); mon.setDate(now.getDate() - (day - 1));
|
||||
const sun = new Date(mon); sun.setDate(mon.getDate() + 6);
|
||||
setDateFrom(mon.toISOString().slice(0, 10));
|
||||
setDateTo(sun.toISOString().slice(0, 10));
|
||||
};
|
||||
const setRangeThisMonth = () => {
|
||||
const now = new Date();
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
setDateFrom(first.toISOString().slice(0, 10));
|
||||
setDateTo(last.toISOString().slice(0, 10));
|
||||
};
|
||||
|
||||
/* ───── 시계 ───── */
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
/* ───── 데이터 조회 ───── */
|
||||
/* ───── 데이터 조회 (서버 페이징 + summary) ───── */
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/work_order_process/data", { autoFilter: true });
|
||||
const rows: ProcessRow[] = res.data?.data?.rows ?? res.data?.rows ?? [];
|
||||
setProcessData(rows);
|
||||
const qp = new URLSearchParams({
|
||||
page: String(page),
|
||||
size: String(pageSize),
|
||||
from: dateFrom,
|
||||
to: dateTo,
|
||||
});
|
||||
const res = await apiClient.get(`/quality-monitoring/data?${qp.toString()}`);
|
||||
if (res.data?.success) {
|
||||
setProcessData((res.data.rows || []) as ProcessRow[]);
|
||||
setTotal(res.data.total || 0);
|
||||
setServerSummary(res.data.summary || { total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("품질점검현황 데이터 조회 실패:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [dateFrom, dateTo, page, pageSize]);
|
||||
|
||||
// 기간 변경 시 1페이지로 리셋
|
||||
useEffect(() => { setPage(1); }, [dateFrom, dateTo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -123,15 +163,9 @@ export default function QualityMonitoringPage() {
|
||||
}, [autoRefresh, fetchData, settings.refreshInterval]);
|
||||
|
||||
/* ───── 검사 행 변환 ───── */
|
||||
// 기간 필터는 fetchData에서 이미 적용 — 여기서는 추가 필터 없이 모두 변환
|
||||
const inspectionRows: InspectionRow[] = useMemo(() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return processData
|
||||
.filter((r) => {
|
||||
// 금일 데이터만
|
||||
const dt = r.completed_at || r.started_at || "";
|
||||
return dt.slice(0, 10) === today;
|
||||
})
|
||||
.map((r, idx) => {
|
||||
const inspQty = r.input_qty || r.plan_qty || 0;
|
||||
const goodQty = r.good_qty ?? 0;
|
||||
@@ -164,20 +198,19 @@ export default function QualityMonitoringPage() {
|
||||
return [];
|
||||
}, [activeTab, inspectionRows]);
|
||||
|
||||
/* ───── 요약 통계 ───── */
|
||||
const summary = useMemo(() => {
|
||||
const total = inspectionRows.length;
|
||||
const passed = inspectionRows.filter((r) => r.result === "합격").length;
|
||||
const failed = inspectionRows.filter((r) => r.result === "불합격").length;
|
||||
const pending = inspectionRows.filter((r) => r.result === "대기").length;
|
||||
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
||||
return { total, passed, failed, pending, passRate };
|
||||
}, [inspectionRows]);
|
||||
/* ───── 요약 통계 (서버 합산 사용) ───── */
|
||||
const summary = useMemo(() => ({
|
||||
total: serverSummary.total,
|
||||
passed: serverSummary.passed,
|
||||
failed: serverSummary.failed,
|
||||
pending: serverSummary.pending,
|
||||
passRate: serverSummary.passRate,
|
||||
}), [serverSummary]);
|
||||
|
||||
/* ───── 요약 카드 정의 ───── */
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "금일 검사건수",
|
||||
label: "검사건수",
|
||||
value: fmt(summary.total),
|
||||
sub: "건",
|
||||
color: "from-slate-500 to-slate-600",
|
||||
@@ -265,9 +298,22 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* ── 본문 ── */}
|
||||
<div className="flex-1 space-y-6 overflow-auto p-6">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-6 overflow-hidden p-6">
|
||||
{/* 기간 필터 */}
|
||||
<div className={cn("flex shrink-0 items-center gap-2 rounded-lg border p-3", theme.card, theme.cardBorder)}>
|
||||
<span className={cn("text-sm font-semibold", theme.headerText)}>조회 기간</span>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<span className={cn("text-sm", theme.mutedText)}>~</span>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<Button variant="outline" size="sm" onClick={setRangeToday}>오늘</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisWeek}>이번주</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisMonth}>이번달</Button>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div className="grid shrink-0 grid-cols-5 gap-4">
|
||||
{summaryCards.map((card) => (
|
||||
<div key={card.label} className={cn("rounded-xl bg-gradient-to-br p-5 shadow-md", card.color)}>
|
||||
<p className="text-sm font-medium text-white/80">{card.label}</p>
|
||||
@@ -280,7 +326,7 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* 검사유형 탭 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -297,8 +343,8 @@ export default function QualityMonitoringPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div className={cn("overflow-hidden rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 테이블 영역 (스크롤) */}
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col overflow-auto rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 입고/출하 준비중 */}
|
||||
{activeTab === "incoming" || activeTab === "shipping" ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
@@ -316,7 +362,7 @@ export default function QualityMonitoringPage() {
|
||||
) : filteredRows.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
<Inbox className="mb-4 h-12 w-12 opacity-40" />
|
||||
<p className="text-lg font-medium">금일 검사 데이터가 없습니다</p>
|
||||
<p className="text-lg font-medium">선택 기간에 검사 데이터가 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -419,6 +465,23 @@ export default function QualityMonitoringPage() {
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 페이지네이션 ── */}
|
||||
{total > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 mt-3 px-2 text-xs">
|
||||
<div className={cn(theme.mutedText)}>
|
||||
전체 <span className="font-semibold text-foreground">{total.toLocaleString()}</span>건 ·
|
||||
{" "}{((page - 1) * pageSize + 1).toLocaleString()}~{Math.min(page * pageSize, total).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage(1)}>«</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage((p) => Math.max(1, p - 1))}>‹</Button>
|
||||
<span className="px-2 tabular-nums">{page} / {totalPages}</span>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>›</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage(totalPages)}>»</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,6 @@ const GRID_COLUMNS = [
|
||||
{ key: "inbound_type", label: "입고유형" },
|
||||
{ key: "inbound_date", label: "입고일" },
|
||||
{ key: "reference_number", label: "참조번호" },
|
||||
{ key: "source_type", label: "데이터출처" },
|
||||
{ key: "supplier_name", label: "공급처" },
|
||||
{ key: "item_number", label: "품목코드" },
|
||||
{ key: "item_name", label: "품목명" },
|
||||
|
||||
@@ -88,25 +88,65 @@ export default function QualityMonitoringPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("all");
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 서버 페이징 + KPI summary
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(50);
|
||||
const [total, setTotal] = useState(0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const [serverSummary, setServerSummary] = useState<{ total: number; passed: number; failed: number; pending: number; passRate: number }>({ total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
|
||||
// 기간 필터 (기본: 오늘)
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
const [dateFrom, setDateFrom] = useState<string>(todayStr);
|
||||
const [dateTo, setDateTo] = useState<string>(todayStr);
|
||||
const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); };
|
||||
const setRangeThisWeek = () => {
|
||||
const now = new Date();
|
||||
const day = now.getDay() || 7;
|
||||
const mon = new Date(now); mon.setDate(now.getDate() - (day - 1));
|
||||
const sun = new Date(mon); sun.setDate(mon.getDate() + 6);
|
||||
setDateFrom(mon.toISOString().slice(0, 10));
|
||||
setDateTo(sun.toISOString().slice(0, 10));
|
||||
};
|
||||
const setRangeThisMonth = () => {
|
||||
const now = new Date();
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
setDateFrom(first.toISOString().slice(0, 10));
|
||||
setDateTo(last.toISOString().slice(0, 10));
|
||||
};
|
||||
|
||||
/* ───── 시계 ───── */
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
/* ───── 데이터 조회 ───── */
|
||||
/* ───── 데이터 조회 (서버 페이징 + summary) ───── */
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/work_order_process/data", { autoFilter: true });
|
||||
const rows: ProcessRow[] = res.data?.data?.rows ?? res.data?.rows ?? [];
|
||||
setProcessData(rows);
|
||||
const qp = new URLSearchParams({
|
||||
page: String(page),
|
||||
size: String(pageSize),
|
||||
from: dateFrom,
|
||||
to: dateTo,
|
||||
});
|
||||
const res = await apiClient.get(`/quality-monitoring/data?${qp.toString()}`);
|
||||
if (res.data?.success) {
|
||||
setProcessData((res.data.rows || []) as ProcessRow[]);
|
||||
setTotal(res.data.total || 0);
|
||||
setServerSummary(res.data.summary || { total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("품질점검현황 데이터 조회 실패:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [dateFrom, dateTo, page, pageSize]);
|
||||
|
||||
// 기간 변경 시 1페이지로 리셋
|
||||
useEffect(() => { setPage(1); }, [dateFrom, dateTo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -123,15 +163,9 @@ export default function QualityMonitoringPage() {
|
||||
}, [autoRefresh, fetchData, settings.refreshInterval]);
|
||||
|
||||
/* ───── 검사 행 변환 ───── */
|
||||
// 기간 필터는 fetchData에서 이미 적용 — 여기서는 추가 필터 없이 모두 변환
|
||||
const inspectionRows: InspectionRow[] = useMemo(() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return processData
|
||||
.filter((r) => {
|
||||
// 금일 데이터만
|
||||
const dt = r.completed_at || r.started_at || "";
|
||||
return dt.slice(0, 10) === today;
|
||||
})
|
||||
.map((r, idx) => {
|
||||
const inspQty = r.input_qty || r.plan_qty || 0;
|
||||
const goodQty = r.good_qty ?? 0;
|
||||
@@ -164,20 +198,19 @@ export default function QualityMonitoringPage() {
|
||||
return [];
|
||||
}, [activeTab, inspectionRows]);
|
||||
|
||||
/* ───── 요약 통계 ───── */
|
||||
const summary = useMemo(() => {
|
||||
const total = inspectionRows.length;
|
||||
const passed = inspectionRows.filter((r) => r.result === "합격").length;
|
||||
const failed = inspectionRows.filter((r) => r.result === "불합격").length;
|
||||
const pending = inspectionRows.filter((r) => r.result === "대기").length;
|
||||
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
||||
return { total, passed, failed, pending, passRate };
|
||||
}, [inspectionRows]);
|
||||
/* ───── 요약 통계 (서버 합산 사용) ───── */
|
||||
const summary = useMemo(() => ({
|
||||
total: serverSummary.total,
|
||||
passed: serverSummary.passed,
|
||||
failed: serverSummary.failed,
|
||||
pending: serverSummary.pending,
|
||||
passRate: serverSummary.passRate,
|
||||
}), [serverSummary]);
|
||||
|
||||
/* ───── 요약 카드 정의 ───── */
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "금일 검사건수",
|
||||
label: "검사건수",
|
||||
value: fmt(summary.total),
|
||||
sub: "건",
|
||||
color: "from-slate-500 to-slate-600",
|
||||
@@ -265,9 +298,22 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* ── 본문 ── */}
|
||||
<div className="flex-1 space-y-6 overflow-auto p-6">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-6 overflow-hidden p-6">
|
||||
{/* 기간 필터 */}
|
||||
<div className={cn("flex shrink-0 items-center gap-2 rounded-lg border p-3", theme.card, theme.cardBorder)}>
|
||||
<span className={cn("text-sm font-semibold", theme.headerText)}>조회 기간</span>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<span className={cn("text-sm", theme.mutedText)}>~</span>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<Button variant="outline" size="sm" onClick={setRangeToday}>오늘</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisWeek}>이번주</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisMonth}>이번달</Button>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div className="grid shrink-0 grid-cols-5 gap-4">
|
||||
{summaryCards.map((card) => (
|
||||
<div key={card.label} className={cn("rounded-xl bg-gradient-to-br p-5 shadow-md", card.color)}>
|
||||
<p className="text-sm font-medium text-white/80">{card.label}</p>
|
||||
@@ -280,7 +326,7 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* 검사유형 탭 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -297,8 +343,8 @@ export default function QualityMonitoringPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div className={cn("overflow-hidden rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 테이블 영역 (스크롤) */}
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col overflow-auto rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 입고/출하 준비중 */}
|
||||
{activeTab === "incoming" || activeTab === "shipping" ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
@@ -316,7 +362,7 @@ export default function QualityMonitoringPage() {
|
||||
) : filteredRows.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
<Inbox className="mb-4 h-12 w-12 opacity-40" />
|
||||
<p className="text-lg font-medium">금일 검사 데이터가 없습니다</p>
|
||||
<p className="text-lg font-medium">선택 기간에 검사 데이터가 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -419,6 +465,23 @@ export default function QualityMonitoringPage() {
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 페이지네이션 ── */}
|
||||
{total > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 mt-3 px-2 text-xs">
|
||||
<div className={cn(theme.mutedText)}>
|
||||
전체 <span className="font-semibold text-foreground">{total.toLocaleString()}</span>건 ·
|
||||
{" "}{((page - 1) * pageSize + 1).toLocaleString()}~{Math.min(page * pageSize, total).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage(1)}>«</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage((p) => Math.max(1, p - 1))}>‹</Button>
|
||||
<span className="px-2 tabular-nums">{page} / {totalPages}</span>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>›</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage(totalPages)}>»</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,6 @@ const GRID_COLUMNS = [
|
||||
{ key: "inbound_type", label: "입고유형" },
|
||||
{ key: "inbound_date", label: "입고일" },
|
||||
{ key: "reference_number", label: "참조번호" },
|
||||
{ key: "source_type", label: "데이터출처" },
|
||||
{ key: "supplier_name", label: "공급처" },
|
||||
{ key: "item_number", label: "품목코드" },
|
||||
{ key: "item_name", label: "품목명" },
|
||||
|
||||
@@ -88,25 +88,65 @@ export default function QualityMonitoringPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("all");
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 서버 페이징 + KPI summary
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(50);
|
||||
const [total, setTotal] = useState(0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const [serverSummary, setServerSummary] = useState<{ total: number; passed: number; failed: number; pending: number; passRate: number }>({ total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
|
||||
// 기간 필터 (기본: 오늘)
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
const [dateFrom, setDateFrom] = useState<string>(todayStr);
|
||||
const [dateTo, setDateTo] = useState<string>(todayStr);
|
||||
const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); };
|
||||
const setRangeThisWeek = () => {
|
||||
const now = new Date();
|
||||
const day = now.getDay() || 7;
|
||||
const mon = new Date(now); mon.setDate(now.getDate() - (day - 1));
|
||||
const sun = new Date(mon); sun.setDate(mon.getDate() + 6);
|
||||
setDateFrom(mon.toISOString().slice(0, 10));
|
||||
setDateTo(sun.toISOString().slice(0, 10));
|
||||
};
|
||||
const setRangeThisMonth = () => {
|
||||
const now = new Date();
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
setDateFrom(first.toISOString().slice(0, 10));
|
||||
setDateTo(last.toISOString().slice(0, 10));
|
||||
};
|
||||
|
||||
/* ───── 시계 ───── */
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
/* ───── 데이터 조회 ───── */
|
||||
/* ───── 데이터 조회 (서버 페이징 + summary) ───── */
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/work_order_process/data", { autoFilter: true });
|
||||
const rows: ProcessRow[] = res.data?.data?.rows ?? res.data?.rows ?? [];
|
||||
setProcessData(rows);
|
||||
const qp = new URLSearchParams({
|
||||
page: String(page),
|
||||
size: String(pageSize),
|
||||
from: dateFrom,
|
||||
to: dateTo,
|
||||
});
|
||||
const res = await apiClient.get(`/quality-monitoring/data?${qp.toString()}`);
|
||||
if (res.data?.success) {
|
||||
setProcessData((res.data.rows || []) as ProcessRow[]);
|
||||
setTotal(res.data.total || 0);
|
||||
setServerSummary(res.data.summary || { total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("품질점검현황 데이터 조회 실패:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [dateFrom, dateTo, page, pageSize]);
|
||||
|
||||
// 기간 변경 시 1페이지로 리셋
|
||||
useEffect(() => { setPage(1); }, [dateFrom, dateTo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -123,15 +163,9 @@ export default function QualityMonitoringPage() {
|
||||
}, [autoRefresh, fetchData, settings.refreshInterval]);
|
||||
|
||||
/* ───── 검사 행 변환 ───── */
|
||||
// 기간 필터는 fetchData에서 이미 적용 — 여기서는 추가 필터 없이 모두 변환
|
||||
const inspectionRows: InspectionRow[] = useMemo(() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return processData
|
||||
.filter((r) => {
|
||||
// 금일 데이터만
|
||||
const dt = r.completed_at || r.started_at || "";
|
||||
return dt.slice(0, 10) === today;
|
||||
})
|
||||
.map((r, idx) => {
|
||||
const inspQty = r.input_qty || r.plan_qty || 0;
|
||||
const goodQty = r.good_qty ?? 0;
|
||||
@@ -164,20 +198,19 @@ export default function QualityMonitoringPage() {
|
||||
return [];
|
||||
}, [activeTab, inspectionRows]);
|
||||
|
||||
/* ───── 요약 통계 ───── */
|
||||
const summary = useMemo(() => {
|
||||
const total = inspectionRows.length;
|
||||
const passed = inspectionRows.filter((r) => r.result === "합격").length;
|
||||
const failed = inspectionRows.filter((r) => r.result === "불합격").length;
|
||||
const pending = inspectionRows.filter((r) => r.result === "대기").length;
|
||||
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
||||
return { total, passed, failed, pending, passRate };
|
||||
}, [inspectionRows]);
|
||||
/* ───── 요약 통계 (서버 합산 사용) ───── */
|
||||
const summary = useMemo(() => ({
|
||||
total: serverSummary.total,
|
||||
passed: serverSummary.passed,
|
||||
failed: serverSummary.failed,
|
||||
pending: serverSummary.pending,
|
||||
passRate: serverSummary.passRate,
|
||||
}), [serverSummary]);
|
||||
|
||||
/* ───── 요약 카드 정의 ───── */
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "금일 검사건수",
|
||||
label: "검사건수",
|
||||
value: fmt(summary.total),
|
||||
sub: "건",
|
||||
color: "from-slate-500 to-slate-600",
|
||||
@@ -265,9 +298,22 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* ── 본문 ── */}
|
||||
<div className="flex-1 space-y-6 overflow-auto p-6">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-6 overflow-hidden p-6">
|
||||
{/* 기간 필터 */}
|
||||
<div className={cn("flex shrink-0 items-center gap-2 rounded-lg border p-3", theme.card, theme.cardBorder)}>
|
||||
<span className={cn("text-sm font-semibold", theme.headerText)}>조회 기간</span>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<span className={cn("text-sm", theme.mutedText)}>~</span>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<Button variant="outline" size="sm" onClick={setRangeToday}>오늘</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisWeek}>이번주</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisMonth}>이번달</Button>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div className="grid shrink-0 grid-cols-5 gap-4">
|
||||
{summaryCards.map((card) => (
|
||||
<div key={card.label} className={cn("rounded-xl bg-gradient-to-br p-5 shadow-md", card.color)}>
|
||||
<p className="text-sm font-medium text-white/80">{card.label}</p>
|
||||
@@ -280,7 +326,7 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* 검사유형 탭 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -297,8 +343,8 @@ export default function QualityMonitoringPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div className={cn("overflow-hidden rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 테이블 영역 (스크롤) */}
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col overflow-auto rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 입고/출하 준비중 */}
|
||||
{activeTab === "incoming" || activeTab === "shipping" ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
@@ -316,7 +362,7 @@ export default function QualityMonitoringPage() {
|
||||
) : filteredRows.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
<Inbox className="mb-4 h-12 w-12 opacity-40" />
|
||||
<p className="text-lg font-medium">금일 검사 데이터가 없습니다</p>
|
||||
<p className="text-lg font-medium">선택 기간에 검사 데이터가 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -419,6 +465,23 @@ export default function QualityMonitoringPage() {
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 페이지네이션 ── */}
|
||||
{total > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 mt-3 px-2 text-xs">
|
||||
<div className={cn(theme.mutedText)}>
|
||||
전체 <span className="font-semibold text-foreground">{total.toLocaleString()}</span>건 ·
|
||||
{" "}{((page - 1) * pageSize + 1).toLocaleString()}~{Math.min(page * pageSize, total).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage(1)}>«</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage((p) => Math.max(1, p - 1))}>‹</Button>
|
||||
<span className="px-2 tabular-nums">{page} / {totalPages}</span>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>›</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage(totalPages)}>»</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,6 @@ const GRID_COLUMNS = [
|
||||
{ key: "inbound_type", label: "입고유형" },
|
||||
{ key: "inbound_date", label: "입고일" },
|
||||
{ key: "reference_number", label: "참조번호" },
|
||||
{ key: "source_type", label: "데이터출처" },
|
||||
{ key: "supplier_name", label: "공급처" },
|
||||
{ key: "item_number", label: "품목코드" },
|
||||
{ key: "item_name", label: "품목명" },
|
||||
|
||||
@@ -83,7 +83,6 @@ const GRID_COLUMNS = [
|
||||
{ key: "inbound_type", label: "입고유형" },
|
||||
{ key: "inbound_date", label: "입고일" },
|
||||
{ key: "reference_number", label: "참조번호" },
|
||||
{ key: "source_type", label: "데이터출처" },
|
||||
{ key: "supplier_name", label: "공급처" },
|
||||
{ key: "item_number", label: "품목코드" },
|
||||
{ key: "item_name", label: "품목명" },
|
||||
|
||||
@@ -88,25 +88,65 @@ export default function QualityMonitoringPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("all");
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 서버 페이징 + KPI summary
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(50);
|
||||
const [total, setTotal] = useState(0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const [serverSummary, setServerSummary] = useState<{ total: number; passed: number; failed: number; pending: number; passRate: number }>({ total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
|
||||
// 기간 필터 (기본: 오늘)
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
const [dateFrom, setDateFrom] = useState<string>(todayStr);
|
||||
const [dateTo, setDateTo] = useState<string>(todayStr);
|
||||
const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); };
|
||||
const setRangeThisWeek = () => {
|
||||
const now = new Date();
|
||||
const day = now.getDay() || 7;
|
||||
const mon = new Date(now); mon.setDate(now.getDate() - (day - 1));
|
||||
const sun = new Date(mon); sun.setDate(mon.getDate() + 6);
|
||||
setDateFrom(mon.toISOString().slice(0, 10));
|
||||
setDateTo(sun.toISOString().slice(0, 10));
|
||||
};
|
||||
const setRangeThisMonth = () => {
|
||||
const now = new Date();
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
setDateFrom(first.toISOString().slice(0, 10));
|
||||
setDateTo(last.toISOString().slice(0, 10));
|
||||
};
|
||||
|
||||
/* ───── 시계 ───── */
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
/* ───── 데이터 조회 ───── */
|
||||
/* ───── 데이터 조회 (서버 페이징 + summary) ───── */
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/work_order_process/data", { autoFilter: true });
|
||||
const rows: ProcessRow[] = res.data?.data?.rows ?? res.data?.rows ?? [];
|
||||
setProcessData(rows);
|
||||
const qp = new URLSearchParams({
|
||||
page: String(page),
|
||||
size: String(pageSize),
|
||||
from: dateFrom,
|
||||
to: dateTo,
|
||||
});
|
||||
const res = await apiClient.get(`/quality-monitoring/data?${qp.toString()}`);
|
||||
if (res.data?.success) {
|
||||
setProcessData((res.data.rows || []) as ProcessRow[]);
|
||||
setTotal(res.data.total || 0);
|
||||
setServerSummary(res.data.summary || { total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("품질점검현황 데이터 조회 실패:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [dateFrom, dateTo, page, pageSize]);
|
||||
|
||||
// 기간 변경 시 1페이지로 리셋
|
||||
useEffect(() => { setPage(1); }, [dateFrom, dateTo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -123,15 +163,9 @@ export default function QualityMonitoringPage() {
|
||||
}, [autoRefresh, fetchData, settings.refreshInterval]);
|
||||
|
||||
/* ───── 검사 행 변환 ───── */
|
||||
// 기간 필터는 fetchData에서 이미 적용 — 여기서는 추가 필터 없이 모두 변환
|
||||
const inspectionRows: InspectionRow[] = useMemo(() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return processData
|
||||
.filter((r) => {
|
||||
// 금일 데이터만
|
||||
const dt = r.completed_at || r.started_at || "";
|
||||
return dt.slice(0, 10) === today;
|
||||
})
|
||||
.map((r, idx) => {
|
||||
const inspQty = r.input_qty || r.plan_qty || 0;
|
||||
const goodQty = r.good_qty ?? 0;
|
||||
@@ -164,20 +198,19 @@ export default function QualityMonitoringPage() {
|
||||
return [];
|
||||
}, [activeTab, inspectionRows]);
|
||||
|
||||
/* ───── 요약 통계 ───── */
|
||||
const summary = useMemo(() => {
|
||||
const total = inspectionRows.length;
|
||||
const passed = inspectionRows.filter((r) => r.result === "합격").length;
|
||||
const failed = inspectionRows.filter((r) => r.result === "불합격").length;
|
||||
const pending = inspectionRows.filter((r) => r.result === "대기").length;
|
||||
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
||||
return { total, passed, failed, pending, passRate };
|
||||
}, [inspectionRows]);
|
||||
/* ───── 요약 통계 (서버 합산 사용) ───── */
|
||||
const summary = useMemo(() => ({
|
||||
total: serverSummary.total,
|
||||
passed: serverSummary.passed,
|
||||
failed: serverSummary.failed,
|
||||
pending: serverSummary.pending,
|
||||
passRate: serverSummary.passRate,
|
||||
}), [serverSummary]);
|
||||
|
||||
/* ───── 요약 카드 정의 ───── */
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "금일 검사건수",
|
||||
label: "검사건수",
|
||||
value: fmt(summary.total),
|
||||
sub: "건",
|
||||
color: "from-slate-500 to-slate-600",
|
||||
@@ -265,9 +298,22 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* ── 본문 ── */}
|
||||
<div className="flex-1 space-y-6 overflow-auto p-6">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-6 overflow-hidden p-6">
|
||||
{/* 기간 필터 */}
|
||||
<div className={cn("flex shrink-0 items-center gap-2 rounded-lg border p-3", theme.card, theme.cardBorder)}>
|
||||
<span className={cn("text-sm font-semibold", theme.headerText)}>조회 기간</span>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<span className={cn("text-sm", theme.mutedText)}>~</span>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<Button variant="outline" size="sm" onClick={setRangeToday}>오늘</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisWeek}>이번주</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisMonth}>이번달</Button>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div className="grid shrink-0 grid-cols-5 gap-4">
|
||||
{summaryCards.map((card) => (
|
||||
<div key={card.label} className={cn("rounded-xl bg-gradient-to-br p-5 shadow-md", card.color)}>
|
||||
<p className="text-sm font-medium text-white/80">{card.label}</p>
|
||||
@@ -280,7 +326,7 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* 검사유형 탭 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -297,8 +343,8 @@ export default function QualityMonitoringPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div className={cn("overflow-hidden rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 테이블 영역 (스크롤) */}
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col overflow-auto rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 입고/출하 준비중 */}
|
||||
{activeTab === "incoming" || activeTab === "shipping" ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
@@ -316,7 +362,7 @@ export default function QualityMonitoringPage() {
|
||||
) : filteredRows.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
<Inbox className="mb-4 h-12 w-12 opacity-40" />
|
||||
<p className="text-lg font-medium">금일 검사 데이터가 없습니다</p>
|
||||
<p className="text-lg font-medium">선택 기간에 검사 데이터가 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -419,6 +465,23 @@ export default function QualityMonitoringPage() {
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 페이지네이션 ── */}
|
||||
{total > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 mt-3 px-2 text-xs">
|
||||
<div className={cn(theme.mutedText)}>
|
||||
전체 <span className="font-semibold text-foreground">{total.toLocaleString()}</span>건 ·
|
||||
{" "}{((page - 1) * pageSize + 1).toLocaleString()}~{Math.min(page * pageSize, total).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage(1)}>«</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage((p) => Math.max(1, p - 1))}>‹</Button>
|
||||
<span className="px-2 tabular-nums">{page} / {totalPages}</span>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>›</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage(totalPages)}>»</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,6 @@ const GRID_COLUMNS = [
|
||||
{ key: "inbound_type", label: "입고유형" },
|
||||
{ key: "inbound_date", label: "입고일" },
|
||||
{ key: "reference_number", label: "참조번호" },
|
||||
{ key: "source_type", label: "데이터출처" },
|
||||
{ key: "supplier_name", label: "공급처" },
|
||||
{ key: "item_number", label: "품목코드" },
|
||||
{ key: "item_name", label: "품목명" },
|
||||
|
||||
@@ -88,25 +88,65 @@ export default function QualityMonitoringPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("all");
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 서버 페이징 + KPI summary
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(50);
|
||||
const [total, setTotal] = useState(0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const [serverSummary, setServerSummary] = useState<{ total: number; passed: number; failed: number; pending: number; passRate: number }>({ total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
|
||||
// 기간 필터 (기본: 오늘)
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
const [dateFrom, setDateFrom] = useState<string>(todayStr);
|
||||
const [dateTo, setDateTo] = useState<string>(todayStr);
|
||||
const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); };
|
||||
const setRangeThisWeek = () => {
|
||||
const now = new Date();
|
||||
const day = now.getDay() || 7;
|
||||
const mon = new Date(now); mon.setDate(now.getDate() - (day - 1));
|
||||
const sun = new Date(mon); sun.setDate(mon.getDate() + 6);
|
||||
setDateFrom(mon.toISOString().slice(0, 10));
|
||||
setDateTo(sun.toISOString().slice(0, 10));
|
||||
};
|
||||
const setRangeThisMonth = () => {
|
||||
const now = new Date();
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
setDateFrom(first.toISOString().slice(0, 10));
|
||||
setDateTo(last.toISOString().slice(0, 10));
|
||||
};
|
||||
|
||||
/* ───── 시계 ───── */
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
/* ───── 데이터 조회 ───── */
|
||||
/* ───── 데이터 조회 (서버 페이징 + summary) ───── */
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/work_order_process/data", { autoFilter: true });
|
||||
const rows: ProcessRow[] = res.data?.data?.rows ?? res.data?.rows ?? [];
|
||||
setProcessData(rows);
|
||||
const qp = new URLSearchParams({
|
||||
page: String(page),
|
||||
size: String(pageSize),
|
||||
from: dateFrom,
|
||||
to: dateTo,
|
||||
});
|
||||
const res = await apiClient.get(`/quality-monitoring/data?${qp.toString()}`);
|
||||
if (res.data?.success) {
|
||||
setProcessData((res.data.rows || []) as ProcessRow[]);
|
||||
setTotal(res.data.total || 0);
|
||||
setServerSummary(res.data.summary || { total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("품질점검현황 데이터 조회 실패:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [dateFrom, dateTo, page, pageSize]);
|
||||
|
||||
// 기간 변경 시 1페이지로 리셋
|
||||
useEffect(() => { setPage(1); }, [dateFrom, dateTo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -123,15 +163,9 @@ export default function QualityMonitoringPage() {
|
||||
}, [autoRefresh, fetchData, settings.refreshInterval]);
|
||||
|
||||
/* ───── 검사 행 변환 ───── */
|
||||
// 기간 필터는 fetchData에서 이미 적용 — 여기서는 추가 필터 없이 모두 변환
|
||||
const inspectionRows: InspectionRow[] = useMemo(() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return processData
|
||||
.filter((r) => {
|
||||
// 금일 데이터만
|
||||
const dt = r.completed_at || r.started_at || "";
|
||||
return dt.slice(0, 10) === today;
|
||||
})
|
||||
.map((r, idx) => {
|
||||
const inspQty = r.input_qty || r.plan_qty || 0;
|
||||
const goodQty = r.good_qty ?? 0;
|
||||
@@ -164,20 +198,19 @@ export default function QualityMonitoringPage() {
|
||||
return [];
|
||||
}, [activeTab, inspectionRows]);
|
||||
|
||||
/* ───── 요약 통계 ───── */
|
||||
const summary = useMemo(() => {
|
||||
const total = inspectionRows.length;
|
||||
const passed = inspectionRows.filter((r) => r.result === "합격").length;
|
||||
const failed = inspectionRows.filter((r) => r.result === "불합격").length;
|
||||
const pending = inspectionRows.filter((r) => r.result === "대기").length;
|
||||
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
||||
return { total, passed, failed, pending, passRate };
|
||||
}, [inspectionRows]);
|
||||
/* ───── 요약 통계 (서버 합산 사용) ───── */
|
||||
const summary = useMemo(() => ({
|
||||
total: serverSummary.total,
|
||||
passed: serverSummary.passed,
|
||||
failed: serverSummary.failed,
|
||||
pending: serverSummary.pending,
|
||||
passRate: serverSummary.passRate,
|
||||
}), [serverSummary]);
|
||||
|
||||
/* ───── 요약 카드 정의 ───── */
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "금일 검사건수",
|
||||
label: "검사건수",
|
||||
value: fmt(summary.total),
|
||||
sub: "건",
|
||||
color: "from-slate-500 to-slate-600",
|
||||
@@ -265,9 +298,22 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* ── 본문 ── */}
|
||||
<div className="flex-1 space-y-6 overflow-auto p-6">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-6 overflow-hidden p-6">
|
||||
{/* 기간 필터 */}
|
||||
<div className={cn("flex shrink-0 items-center gap-2 rounded-lg border p-3", theme.card, theme.cardBorder)}>
|
||||
<span className={cn("text-sm font-semibold", theme.headerText)}>조회 기간</span>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<span className={cn("text-sm", theme.mutedText)}>~</span>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<Button variant="outline" size="sm" onClick={setRangeToday}>오늘</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisWeek}>이번주</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisMonth}>이번달</Button>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div className="grid shrink-0 grid-cols-5 gap-4">
|
||||
{summaryCards.map((card) => (
|
||||
<div key={card.label} className={cn("rounded-xl bg-gradient-to-br p-5 shadow-md", card.color)}>
|
||||
<p className="text-sm font-medium text-white/80">{card.label}</p>
|
||||
@@ -280,7 +326,7 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* 검사유형 탭 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -297,8 +343,8 @@ export default function QualityMonitoringPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div className={cn("overflow-hidden rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 테이블 영역 (스크롤) */}
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col overflow-auto rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 입고/출하 준비중 */}
|
||||
{activeTab === "incoming" || activeTab === "shipping" ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
@@ -316,7 +362,7 @@ export default function QualityMonitoringPage() {
|
||||
) : filteredRows.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
<Inbox className="mb-4 h-12 w-12 opacity-40" />
|
||||
<p className="text-lg font-medium">금일 검사 데이터가 없습니다</p>
|
||||
<p className="text-lg font-medium">선택 기간에 검사 데이터가 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -419,6 +465,23 @@ export default function QualityMonitoringPage() {
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 페이지네이션 ── */}
|
||||
{total > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 mt-3 px-2 text-xs">
|
||||
<div className={cn(theme.mutedText)}>
|
||||
전체 <span className="font-semibold text-foreground">{total.toLocaleString()}</span>건 ·
|
||||
{" "}{((page - 1) * pageSize + 1).toLocaleString()}~{Math.min(page * pageSize, total).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage(1)}>«</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage((p) => Math.max(1, p - 1))}>‹</Button>
|
||||
<span className="px-2 tabular-nums">{page} / {totalPages}</span>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>›</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage(totalPages)}>»</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,6 @@ const GRID_COLUMNS = [
|
||||
{ key: "inbound_type", label: "입고유형" },
|
||||
{ key: "inbound_date", label: "입고일" },
|
||||
{ key: "reference_number", label: "참조번호" },
|
||||
{ key: "source_type", label: "데이터출처" },
|
||||
{ key: "supplier_name", label: "공급처" },
|
||||
{ key: "item_number", label: "품목코드" },
|
||||
{ key: "item_name", label: "품목명" },
|
||||
|
||||
@@ -88,25 +88,65 @@ export default function QualityMonitoringPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("all");
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 서버 페이징 + KPI summary
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(50);
|
||||
const [total, setTotal] = useState(0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const [serverSummary, setServerSummary] = useState<{ total: number; passed: number; failed: number; pending: number; passRate: number }>({ total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
|
||||
// 기간 필터 (기본: 오늘)
|
||||
const todayStr = new Date().toISOString().slice(0, 10);
|
||||
const [dateFrom, setDateFrom] = useState<string>(todayStr);
|
||||
const [dateTo, setDateTo] = useState<string>(todayStr);
|
||||
const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); };
|
||||
const setRangeThisWeek = () => {
|
||||
const now = new Date();
|
||||
const day = now.getDay() || 7;
|
||||
const mon = new Date(now); mon.setDate(now.getDate() - (day - 1));
|
||||
const sun = new Date(mon); sun.setDate(mon.getDate() + 6);
|
||||
setDateFrom(mon.toISOString().slice(0, 10));
|
||||
setDateTo(sun.toISOString().slice(0, 10));
|
||||
};
|
||||
const setRangeThisMonth = () => {
|
||||
const now = new Date();
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
setDateFrom(first.toISOString().slice(0, 10));
|
||||
setDateTo(last.toISOString().slice(0, 10));
|
||||
};
|
||||
|
||||
/* ───── 시계 ───── */
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
/* ───── 데이터 조회 ───── */
|
||||
/* ───── 데이터 조회 (서버 페이징 + summary) ───── */
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post("/table-management/tables/work_order_process/data", { autoFilter: true });
|
||||
const rows: ProcessRow[] = res.data?.data?.rows ?? res.data?.rows ?? [];
|
||||
setProcessData(rows);
|
||||
const qp = new URLSearchParams({
|
||||
page: String(page),
|
||||
size: String(pageSize),
|
||||
from: dateFrom,
|
||||
to: dateTo,
|
||||
});
|
||||
const res = await apiClient.get(`/quality-monitoring/data?${qp.toString()}`);
|
||||
if (res.data?.success) {
|
||||
setProcessData((res.data.rows || []) as ProcessRow[]);
|
||||
setTotal(res.data.total || 0);
|
||||
setServerSummary(res.data.summary || { total: 0, passed: 0, failed: 0, pending: 0, passRate: 0 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("품질점검현황 데이터 조회 실패:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [dateFrom, dateTo, page, pageSize]);
|
||||
|
||||
// 기간 변경 시 1페이지로 리셋
|
||||
useEffect(() => { setPage(1); }, [dateFrom, dateTo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -123,15 +163,9 @@ export default function QualityMonitoringPage() {
|
||||
}, [autoRefresh, fetchData, settings.refreshInterval]);
|
||||
|
||||
/* ───── 검사 행 변환 ───── */
|
||||
// 기간 필터는 fetchData에서 이미 적용 — 여기서는 추가 필터 없이 모두 변환
|
||||
const inspectionRows: InspectionRow[] = useMemo(() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return processData
|
||||
.filter((r) => {
|
||||
// 금일 데이터만
|
||||
const dt = r.completed_at || r.started_at || "";
|
||||
return dt.slice(0, 10) === today;
|
||||
})
|
||||
.map((r, idx) => {
|
||||
const inspQty = r.input_qty || r.plan_qty || 0;
|
||||
const goodQty = r.good_qty ?? 0;
|
||||
@@ -164,20 +198,19 @@ export default function QualityMonitoringPage() {
|
||||
return [];
|
||||
}, [activeTab, inspectionRows]);
|
||||
|
||||
/* ───── 요약 통계 ───── */
|
||||
const summary = useMemo(() => {
|
||||
const total = inspectionRows.length;
|
||||
const passed = inspectionRows.filter((r) => r.result === "합격").length;
|
||||
const failed = inspectionRows.filter((r) => r.result === "불합격").length;
|
||||
const pending = inspectionRows.filter((r) => r.result === "대기").length;
|
||||
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
||||
return { total, passed, failed, pending, passRate };
|
||||
}, [inspectionRows]);
|
||||
/* ───── 요약 통계 (서버 합산 사용) ───── */
|
||||
const summary = useMemo(() => ({
|
||||
total: serverSummary.total,
|
||||
passed: serverSummary.passed,
|
||||
failed: serverSummary.failed,
|
||||
pending: serverSummary.pending,
|
||||
passRate: serverSummary.passRate,
|
||||
}), [serverSummary]);
|
||||
|
||||
/* ───── 요약 카드 정의 ───── */
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "금일 검사건수",
|
||||
label: "검사건수",
|
||||
value: fmt(summary.total),
|
||||
sub: "건",
|
||||
color: "from-slate-500 to-slate-600",
|
||||
@@ -265,9 +298,22 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* ── 본문 ── */}
|
||||
<div className="flex-1 space-y-6 overflow-auto p-6">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-6 overflow-hidden p-6">
|
||||
{/* 기간 필터 */}
|
||||
<div className={cn("flex shrink-0 items-center gap-2 rounded-lg border p-3", theme.card, theme.cardBorder)}>
|
||||
<span className={cn("text-sm font-semibold", theme.headerText)}>조회 기간</span>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<span className={cn("text-sm", theme.mutedText)}>~</span>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} />
|
||||
<Button variant="outline" size="sm" onClick={setRangeToday}>오늘</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisWeek}>이번주</Button>
|
||||
<Button variant="outline" size="sm" onClick={setRangeThisMonth}>이번달</Button>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div className="grid shrink-0 grid-cols-5 gap-4">
|
||||
{summaryCards.map((card) => (
|
||||
<div key={card.label} className={cn("rounded-xl bg-gradient-to-br p-5 shadow-md", card.color)}>
|
||||
<p className="text-sm font-medium text-white/80">{card.label}</p>
|
||||
@@ -280,7 +326,7 @@ export default function QualityMonitoringPage() {
|
||||
</div>
|
||||
|
||||
{/* 검사유형 탭 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -297,8 +343,8 @@ export default function QualityMonitoringPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div className={cn("overflow-hidden rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 테이블 영역 (스크롤) */}
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col overflow-auto rounded-xl border shadow", theme.card, theme.cardBorder)}>
|
||||
{/* 입고/출하 준비중 */}
|
||||
{activeTab === "incoming" || activeTab === "shipping" ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
@@ -316,7 +362,7 @@ export default function QualityMonitoringPage() {
|
||||
) : filteredRows.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-400">
|
||||
<Inbox className="mb-4 h-12 w-12 opacity-40" />
|
||||
<p className="text-lg font-medium">금일 검사 데이터가 없습니다</p>
|
||||
<p className="text-lg font-medium">선택 기간에 검사 데이터가 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -419,6 +465,23 @@ export default function QualityMonitoringPage() {
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 페이지네이션 ── */}
|
||||
{total > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 mt-3 px-2 text-xs">
|
||||
<div className={cn(theme.mutedText)}>
|
||||
전체 <span className="font-semibold text-foreground">{total.toLocaleString()}</span>건 ·
|
||||
{" "}{((page - 1) * pageSize + 1).toLocaleString()}~{Math.min(page * pageSize, total).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage(1)}>«</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page <= 1 || loading} onClick={() => setPage((p) => Math.max(1, p - 1))}>‹</Button>
|
||||
<span className="px-2 tabular-nums">{page} / {totalPages}</span>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>›</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 px-2" disabled={page >= totalPages || loading} onClick={() => setPage(totalPages)}>»</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user