fix: [quality] 설비점검 컴포넌트 개선

This commit is contained in:
유병철
2026-03-17 18:30:49 +09:00
parent 06233387b0
commit 37f0e57b16

View File

@@ -7,7 +7,7 @@
* 셀 클릭으로 ○/X/△ 토글
*/
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useSearchParams } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { toast } from 'sonner';
@@ -20,7 +20,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { DatePicker } from '@/components/ui/date-picker';
import { Label } from '@/components/ui/label';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import {
@@ -67,13 +66,15 @@ interface GridData {
nonWorkingDays: string[];
}
const YEAR_OPTIONS = Array.from({ length: 10 }, (_, i) => 2021 + i);
const MONTH_OPTIONS = Array.from({ length: 12 }, (_, i) => i + 1);
export function EquipmentInspectionGrid() {
const searchParams = useSearchParams();
const [cycle, setCycle] = useState<InspectionCycle>('daily');
const [period, setPeriod] = useState<string>(() => {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
});
const [year, setYear] = useState(() => new Date().getFullYear());
const [month, setMonth] = useState(() => new Date().getMonth() + 1);
const period = `${year}-${String(month).padStart(2, '0')}`;
const [lineFilter, setLineFilter] = useState<string>('all');
const [equipmentFilter, setEquipmentFilter] = useState<string>(() => {
if (typeof window === 'undefined') return 'all';
@@ -85,6 +86,40 @@ export function EquipmentInspectionGrid() {
const [isLoading, setIsLoading] = useState(true);
const [showReset, setShowReset] = useState(false);
// 드래그 스크롤
const scrollRef = useRef<HTMLDivElement>(null);
const isDragging = useRef(false);
const startX = useRef(0);
const scrollLeft = useRef(0);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
const el = scrollRef.current;
if (!el) return;
isDragging.current = true;
startX.current = e.pageX - el.offsetLeft;
scrollLeft.current = el.scrollLeft;
el.style.cursor = 'grabbing';
el.style.userSelect = 'none';
}, []);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!isDragging.current) return;
const el = scrollRef.current;
if (!el) return;
e.preventDefault();
const x = e.pageX - el.offsetLeft;
el.scrollLeft = scrollLeft.current - (x - startX.current);
}, []);
const handleMouseUp = useCallback(() => {
isDragging.current = false;
const el = scrollRef.current;
if (el) {
el.style.cursor = 'grab';
el.style.userSelect = '';
}
}, []);
// 옵션 로드
useEffect(() => {
getEquipmentOptions().then((r) => {
@@ -139,13 +174,19 @@ export function EquipmentInspectionGrid() {
: Array.isArray(rawLabels) ? rawLabels : [];
// 주말(토/일) 계산
const [y, m] = period.split('-').map(Number);
const weekends: string[] = [];
const weekends = new Set<string>();
const daysInMonth = new Date(y, m, 0).getDate();
for (let d = 1; d <= daysInMonth; d++) {
const dow = new Date(y, m - 1, d).getDay();
if (dow === 0 || dow === 6) weekends.push(String(d));
if (dow === 0 || dow === 6) weekends.add(String(d));
}
setGridData({ rows, labels, nonWorkingDays: weekends });
// API에서 받은 임시휴일 추가 (형식: "2026-03-17" → 일자 "17"으로 변환)
const apiHolidays: string[] = apiItems.length > 0 ? (apiItems[0].non_working_days ?? []) : [];
for (const dateStr of apiHolidays) {
const day = String(Number(dateStr.split('-')[2]));
weekends.add(day);
}
setGridData({ rows, labels, nonWorkingDays: Array.from(weekends) });
} else {
setGridData({ rows: [], labels: [], nonWorkingDays: [] });
}
@@ -187,7 +228,12 @@ export function EquipmentInspectionGrid() {
return { ...prev, rows: newRows };
});
} else {
toast.error(result.error || '점검 결과 변경에 실패했습니다.');
const errorMsg = result.error?.includes('non_working_day')
? '비근무일에는 점검을 입력할 수 없습니다.'
: result.error?.includes('no_inspect_permission')
? '담당자만 점검을 입력할 수 있습니다.'
: result.error || '점검 결과 변경에 실패했습니다.';
toast.error(errorMsg);
}
}, [cycle, period]);
@@ -243,12 +289,28 @@ export function EquipmentInspectionGrid() {
<div className="flex flex-wrap items-end gap-6">
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground"></Label>
<DatePicker
value={period ? `${period}-01` : ''}
onChange={(v) => {
if (v) setPeriod(v.substring(0, 7));
}}
/>
<div className="flex items-center gap-2">
<Select value={String(year)} onValueChange={(v) => setYear(Number(v))}>
<SelectTrigger className="w-[100px] h-10">
<SelectValue />
</SelectTrigger>
<SelectContent>
{YEAR_OPTIONS.map(y => (
<SelectItem key={y} value={String(y)}>{y}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={String(month)} onValueChange={(v) => setMonth(Number(v))}>
<SelectTrigger className="w-[80px] h-10">
<SelectValue />
</SelectTrigger>
<SelectContent>
{MONTH_OPTIONS.map(m => (
<SelectItem key={m} value={String(m)}>{m}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground"></Label>
@@ -353,7 +415,14 @@ export function EquipmentInspectionGrid() {
boxShadow: '4px 0 6px -2px rgba(0,0,0,0.08)',
});
return (
<div className="overflow-x-auto">
<div
ref={scrollRef}
className="overflow-x-auto cursor-grab"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<table className="w-full text-xs" style={{ borderCollapse: 'separate', borderSpacing: 0 }}>
<thead>
<tr>
@@ -419,10 +488,13 @@ export function EquipmentInspectionGrid() {
borderRight: `1px solid ${bc}`, borderBottom: `1px solid ${bc}`,
...(isNonWorking ? { background: '#fef2f2' } : {}),
}}
onClick={() =>
row.canInspect &&
handleCellClick(row.equipment.id, template.id, label)
}
onClick={() => {
if (!row.canInspect) {
toast.error('담당자만 점검을 입력할 수 있습니다.');
return;
}
handleCellClick(row.equipment.id, template.id, label);
}}
>
<span className="text-sm font-bold">{getResultSymbol(result)}</span>
</td>