Remove 'source_type' column from GRID_COLUMNS in receiving pages for multiple companies to streamline data display.

This commit is contained in:
kjs
2026-04-30 10:52:34 +09:00
parent 372b97a313
commit 0d4fcfb871
13 changed files with 546 additions and 175 deletions

View File

@@ -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: "품목명" },

View File

@@ -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>

View File

@@ -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: "품목명" },

View File

@@ -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>

View File

@@ -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: "품목명" },

View File

@@ -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>

View File

@@ -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: "품목명" },

View File

@@ -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: "품목명" },

View File

@@ -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>

View File

@@ -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: "품목명" },

View File

@@ -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>

View File

@@ -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: "품목명" },

View File

@@ -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>