fix: [quality] 설비점검 컴포넌트 개선
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user