chore(WEB): QMS 품질관리 Day 탭 및 루트 리스트 개선
- DayTabs 컴포넌트 리팩토링 - RouteList 기능 확장 및 UI 개선 - 목업 데이터 구조 조정 - 페이지 레이아웃 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,28 +71,32 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 전체 진행률 - 한줄 표시 */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 px-4 py-2.5 flex items-center gap-4">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">전체 심사 진행률</span>
|
||||
|
||||
{/* 전체 프로그레스 바 */}
|
||||
<div className="flex-1 flex items-center gap-3">
|
||||
{/* 진행률 - 3줄 표시 */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 px-4 py-3 space-y-2">
|
||||
{/* 전체 심사 진행률 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700 w-28 shrink-0">전체 심사</span>
|
||||
<div className="flex-1 h-2.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-600 rounded-full transition-all duration-500"
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
overallPercentage === 100 ? 'bg-green-500' : 'bg-blue-600'
|
||||
)}
|
||||
style={{ width: `${overallPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-blue-600 whitespace-nowrap">{overallPercentage}%</span>
|
||||
<span className={cn(
|
||||
'text-sm font-bold w-16 text-right',
|
||||
overallPercentage === 100 ? 'text-green-600' : 'text-blue-600'
|
||||
)}>
|
||||
{totalCompleted}/{totalItems}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="w-px h-5 bg-gray-300" />
|
||||
|
||||
{/* 1일차 진행률 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">1일차</span>
|
||||
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-600 w-28 shrink-0">1일차: 기준/매뉴얼</span>
|
||||
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
@@ -102,17 +106,17 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
|
||||
/>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs font-medium whitespace-nowrap',
|
||||
'text-sm font-medium w-16 text-right',
|
||||
day1Percentage === 100 ? 'text-green-600' : 'text-gray-600'
|
||||
)}>
|
||||
{day1Percentage}%
|
||||
{day1Progress.completed}/{day1Progress.total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 2일차 진행률 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">2일차</span>
|
||||
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-600 w-28 shrink-0">2일차: 로트추적</span>
|
||||
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
@@ -122,10 +126,10 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
|
||||
/>
|
||||
</div>
|
||||
<span className={cn(
|
||||
'text-xs font-medium whitespace-nowrap',
|
||||
'text-sm font-medium w-16 text-right',
|
||||
day2Percentage === 100 ? 'text-green-600' : 'text-gray-600'
|
||||
)}>
|
||||
{day2Percentage}%
|
||||
{day2Progress.completed}/{day2Progress.total}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp, MapPin } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp, MapPin, Check } from 'lucide-react';
|
||||
import { RouteItem } from '../types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface RouteListProps {
|
||||
routes: RouteItem[];
|
||||
selectedId: string | null;
|
||||
onSelect: (route: RouteItem) => void;
|
||||
onToggleItem: (routeId: string, itemId: string, isCompleted: boolean) => void;
|
||||
reportCode: string | null;
|
||||
}
|
||||
|
||||
export const RouteList = ({ routes, selectedId, onSelect, reportCode }: RouteListProps) => {
|
||||
export const RouteList = ({ routes, selectedId, onSelect, onToggleItem, reportCode }: RouteListProps) => {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
const handleClick = (route: RouteItem) => {
|
||||
@@ -19,6 +21,11 @@ export const RouteList = ({ routes, selectedId, onSelect, reportCode }: RouteLis
|
||||
setExpandedId(expandedId === route.id ? null : route.id);
|
||||
};
|
||||
|
||||
const handleToggle = (e: React.MouseEvent, routeId: string, itemId: string, currentState: boolean) => {
|
||||
e.stopPropagation();
|
||||
onToggleItem(routeId, itemId, !currentState);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm h-full flex flex-col overflow-hidden">
|
||||
<h2 className="font-bold text-gray-800 text-sm mb-4">
|
||||
@@ -37,6 +44,8 @@ export const RouteList = ({ routes, selectedId, onSelect, reportCode }: RouteLis
|
||||
routes.map((route) => {
|
||||
const isSelected = selectedId === route.id;
|
||||
const isExpanded = expandedId === route.id;
|
||||
const completedCount = route.subItems.filter(item => item.isCompleted).length;
|
||||
const totalCount = route.subItems.length;
|
||||
|
||||
return (
|
||||
<div key={route.id} className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
@@ -52,6 +61,16 @@ export const RouteList = ({ routes, selectedId, onSelect, reportCode }: RouteLis
|
||||
<h3 className={`font-bold ${isSelected ? 'text-green-700' : 'text-gray-700'}`}>
|
||||
{route.code}
|
||||
</h3>
|
||||
{totalCount > 0 && (
|
||||
<span className={cn(
|
||||
'text-[10px] font-medium px-1.5 py-0.5 rounded',
|
||||
completedCount === totalCount
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
)}>
|
||||
{completedCount}/{totalCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mb-1">수주일: {route.date}</p>
|
||||
<p className="text-xs text-gray-500 mb-2">현장: {route.site}</p>
|
||||
@@ -75,23 +94,35 @@ export const RouteList = ({ routes, selectedId, onSelect, reportCode }: RouteLis
|
||||
{route.subItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between border border-gray-100 p-2 rounded hover:bg-gray-50"
|
||||
className={cn(
|
||||
"flex items-center justify-between border p-2 rounded transition-colors",
|
||||
item.isCompleted
|
||||
? "border-green-200 bg-green-50"
|
||||
: "border-gray-100 hover:bg-gray-50"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="text-xs font-bold text-green-700">{item.name}</div>
|
||||
<div className="flex-1">
|
||||
<div className={cn(
|
||||
"text-xs font-bold",
|
||||
item.isCompleted ? "text-green-700" : "text-gray-700"
|
||||
)}>
|
||||
{item.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{item.location}</div>
|
||||
</div>
|
||||
<span
|
||||
className={`text-[10px] font-bold px-1.5 py-0.5 rounded border ${
|
||||
item.status === '합격'
|
||||
? 'text-green-600 border-green-200 bg-green-50'
|
||||
: item.status === '불합격'
|
||||
? 'text-red-600 border-red-200 bg-red-50'
|
||||
: 'text-yellow-600 border-yellow-200 bg-yellow-50'
|
||||
}`}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleToggle(e, route.id, item.id, item.isCompleted)}
|
||||
className={cn(
|
||||
"flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded border transition-all",
|
||||
item.isCompleted
|
||||
? "text-green-600 border-green-300 bg-green-100 hover:bg-green-200"
|
||||
: "text-gray-500 border-gray-300 bg-white hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300"
|
||||
)}
|
||||
>
|
||||
{item.status}
|
||||
</span>
|
||||
<Check size={12} className={item.isCompleted ? "text-green-600" : "text-gray-400"} />
|
||||
{item.isCompleted ? '완료' : '확인'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@ export const MOCK_REPORTS: InspectionReport[] = [
|
||||
];
|
||||
|
||||
// 수주루트 목록 (reportId로 연결)
|
||||
export const MOCK_ROUTES: Record<string, RouteItem[]> = {
|
||||
export const MOCK_ROUTES_INITIAL: Record<string, RouteItem[]> = {
|
||||
'1': [
|
||||
{
|
||||
id: '1-1',
|
||||
@@ -155,13 +155,13 @@ export const MOCK_ROUTES: Record<string, RouteItem[]> = {
|
||||
site: '강남 아파트 A동',
|
||||
locationCount: 7,
|
||||
subItems: [
|
||||
{ id: '1-1-1', name: 'KD-SS-240924-19-01', location: '101동 501호', status: '합격' },
|
||||
{ id: '1-1-2', name: 'KD-SS-240924-19-02', location: '101동 502호', status: '합격' },
|
||||
{ id: '1-1-3', name: 'KD-SS-240924-19-03', location: '101동 503호', status: '합격' },
|
||||
{ id: '1-1-4', name: 'KD-SS-240924-19-04', location: '101동 601호', status: '합격' },
|
||||
{ id: '1-1-5', name: 'KD-SS-240924-19-05', location: '101동 602호', status: '합격' },
|
||||
{ id: '1-1-6', name: 'KD-SS-240924-19-06', location: '101동 603호', status: '합격' },
|
||||
{ id: '1-1-7', name: 'KD-SS-240924-19-07', location: '102동 501호', status: '합격' },
|
||||
{ id: '1-1-1', name: 'KD-SS-240924-19-01', location: '101동 501호', isCompleted: true },
|
||||
{ id: '1-1-2', name: 'KD-SS-240924-19-02', location: '101동 502호', isCompleted: true },
|
||||
{ id: '1-1-3', name: 'KD-SS-240924-19-03', location: '101동 503호', isCompleted: true },
|
||||
{ id: '1-1-4', name: 'KD-SS-240924-19-04', location: '101동 601호', isCompleted: true },
|
||||
{ id: '1-1-5', name: 'KD-SS-240924-19-05', location: '101동 602호', isCompleted: true },
|
||||
{ id: '1-1-6', name: 'KD-SS-240924-19-06', location: '101동 603호', isCompleted: false },
|
||||
{ id: '1-1-7', name: 'KD-SS-240924-19-07', location: '102동 501호', isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -171,8 +171,8 @@ export const MOCK_ROUTES: Record<string, RouteItem[]> = {
|
||||
site: '강남 아파트 B동',
|
||||
locationCount: 7,
|
||||
subItems: [
|
||||
{ id: '1-2-1', name: 'KD-SS-241024-15-01', location: '103동 501호', status: '합격' },
|
||||
{ id: '1-2-2', name: 'KD-SS-241024-15-02', location: '103동 502호', status: '대기' },
|
||||
{ id: '1-2-1', name: 'KD-SS-241024-15-01', location: '103동 501호', isCompleted: true },
|
||||
{ id: '1-2-2', name: 'KD-SS-241024-15-02', location: '103동 502호', isCompleted: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -184,8 +184,8 @@ export const MOCK_ROUTES: Record<string, RouteItem[]> = {
|
||||
site: '서초 오피스텔 본관',
|
||||
locationCount: 8,
|
||||
subItems: [
|
||||
{ id: '2-1-1', name: 'SC-AP-241101-01-01', location: '1층 로비', status: '합격' },
|
||||
{ id: '2-1-2', name: 'SC-AP-241101-01-02', location: '2층 사무실', status: '합격' },
|
||||
{ id: '2-1-1', name: 'SC-AP-241101-01-01', location: '1층 로비', isCompleted: true },
|
||||
{ id: '2-1-2', name: 'SC-AP-241101-01-02', location: '2층 사무실', isCompleted: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -197,7 +197,7 @@ export const MOCK_ROUTES: Record<string, RouteItem[]> = {
|
||||
site: '송파 주상복합 A타워',
|
||||
locationCount: 10,
|
||||
subItems: [
|
||||
{ id: '3-1-1', name: 'SP-CW-240801-01-01', location: '1층 외벽', status: '합격' },
|
||||
{ id: '3-1-1', name: 'SP-CW-240801-01-01', location: '1층 외벽', isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AuditSettingsPanel, SettingsButton, type AuditDisplaySettings } from '.
|
||||
import { InspectionReport, RouteItem, Document, DocumentItem, ChecklistCategory } from './types';
|
||||
import {
|
||||
MOCK_REPORTS,
|
||||
MOCK_ROUTES,
|
||||
MOCK_ROUTES_INITIAL,
|
||||
MOCK_DOCUMENTS,
|
||||
DEFAULT_DOCUMENTS,
|
||||
MOCK_DAY1_CATEGORIES,
|
||||
@@ -55,6 +55,9 @@ export default function QualityInspectionPage() {
|
||||
const [selectedReport, setSelectedReport] = useState<InspectionReport | null>(null);
|
||||
const [selectedRoute, setSelectedRoute] = useState<RouteItem | null>(null);
|
||||
|
||||
// 2일차 루트 데이터 상태 (완료 토글용)
|
||||
const [routesData, setRoutesData] = useState<Record<string, RouteItem[]>>(MOCK_ROUTES_INITIAL);
|
||||
|
||||
// 모달 상태
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [selectedDoc, setSelectedDoc] = useState<Document | null>(null);
|
||||
@@ -70,20 +73,20 @@ export default function QualityInspectionPage() {
|
||||
return { completed, total };
|
||||
}, [day1Categories]);
|
||||
|
||||
// ===== 2일차 진행률 계산 (로트 추적 완료 기준) =====
|
||||
// ===== 2일차 진행률 계산 (개소별 완료 기준) =====
|
||||
const day2Progress = useMemo(() => {
|
||||
let completed = 0;
|
||||
let total = 0;
|
||||
Object.values(MOCK_ROUTES).forEach(routes => {
|
||||
Object.values(routesData).forEach(routes => {
|
||||
routes.forEach(route => {
|
||||
total++;
|
||||
const allPassed = route.subItems.length > 0 &&
|
||||
route.subItems.every(item => item.status === '합격');
|
||||
if (allPassed) completed++;
|
||||
route.subItems.forEach(item => {
|
||||
total++;
|
||||
if (item.isCompleted) completed++;
|
||||
});
|
||||
});
|
||||
});
|
||||
return { completed, total };
|
||||
}, []);
|
||||
}, [routesData]);
|
||||
|
||||
// ===== 1일차 필터링된 카테고리 (완료 항목 숨기기 옵션) =====
|
||||
const filteredDay1Categories = useMemo(() => {
|
||||
@@ -165,8 +168,8 @@ export default function QualityInspectionPage() {
|
||||
|
||||
const currentRoutes = useMemo(() => {
|
||||
if (!selectedReport) return [];
|
||||
return MOCK_ROUTES[selectedReport.id] || [];
|
||||
}, [selectedReport]);
|
||||
return routesData[selectedReport.id] || [];
|
||||
}, [selectedReport, routesData]);
|
||||
|
||||
const currentDocuments = useMemo(() => {
|
||||
if (!selectedRoute) return DEFAULT_DOCUMENTS;
|
||||
@@ -204,6 +207,26 @@ export default function QualityInspectionPage() {
|
||||
setSearchTerm(term);
|
||||
};
|
||||
|
||||
// ===== 2일차 개소별 완료 토글 =====
|
||||
const handleToggleItem = useCallback((routeId: string, itemId: string, isCompleted: boolean) => {
|
||||
setRoutesData(prev => {
|
||||
const newData = { ...prev };
|
||||
for (const reportId of Object.keys(newData)) {
|
||||
newData[reportId] = newData[reportId].map(route => {
|
||||
if (route.id !== routeId) return route;
|
||||
return {
|
||||
...route,
|
||||
subItems: route.subItems.map(item => {
|
||||
if (item.id !== itemId) return item;
|
||||
return { ...item, isCompleted };
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-6 bg-slate-100 flex flex-col lg:overflow-hidden">
|
||||
{/* 헤더 (설정 버튼 포함) */}
|
||||
@@ -316,6 +339,7 @@ export default function QualityInspectionPage() {
|
||||
routes={currentRoutes}
|
||||
selectedId={selectedRoute?.id || null}
|
||||
onSelect={handleRouteSelect}
|
||||
onToggleItem={handleToggleItem}
|
||||
reportCode={selectedReport?.code || null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface UnitInspection {
|
||||
id: string;
|
||||
name: string; // e.g., KD-SS-240924-19-01
|
||||
location: string; // e.g., 101동 501호
|
||||
status: '합격' | '불합격' | '대기';
|
||||
isCompleted: boolean; // 확인 완료 여부
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
|
||||
Reference in New Issue
Block a user