From 935b222602f75d0c34eab439c449a214e44216d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 12 Feb 2026 21:28:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EA=B0=9C=EC=86=8C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D/=EC=A7=84=ED=96=89=ED=98=84=ED=99=A9=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(5.2.5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FQC 상태 데이터 기반으로 개소별 검사 진행현황을 시각화. fqcStatusItems 배열에서 documentMap/stats/itemStatus를 파생. - 진행현황 통계 바: 합격/불합격/진행중/미생성 카운트 + 컬러 프로그래스 바 - View 모드: 개소별 FQC 상태 뱃지 + Eye 아이콘 클릭으로 검사 조회 - Edit 모드: 개소별 FQC 상태 뱃지 + 검사 버튼 나란히 표시 - Legacy fallback: orderId 없으면 기존 검사완료/미검사 뱃지 유지 --- .../InspectionManagement/InspectionDetail.tsx | 211 +++++++++++++----- 1 file changed, 156 insertions(+), 55 deletions(-) diff --git a/src/components/quality/InspectionManagement/InspectionDetail.tsx b/src/components/quality/InspectionManagement/InspectionDetail.tsx index c91450a6..152bc053 100644 --- a/src/components/quality/InspectionManagement/InspectionDetail.tsx +++ b/src/components/quality/InspectionManagement/InspectionDetail.tsx @@ -21,6 +21,7 @@ import { Trash2, ChevronDown, ClipboardCheck, + Eye, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -62,7 +63,7 @@ import { updateInspection, completeInspection, } from './actions'; -import { getFqcStatus } from './fqcActions'; +import { getFqcStatus, type FqcStatusItem } from './fqcActions'; import { statusColorMap, isOrderSpecSame, @@ -139,8 +140,37 @@ export function InspectionDetail({ id }: InspectionDetailProps) { const [inspectionInputOpen, setInspectionInputOpen] = useState(false); const [selectedOrderItem, setSelectedOrderItem] = useState(null); - // FQC 문서 매핑 (orderItemId → documentId) - const [fqcDocumentMap, setFqcDocumentMap] = useState>({}); + // FQC 상태 데이터 (개소별 진행현황) + const [fqcStatusItems, setFqcStatusItems] = useState([]); + + // 파생: 문서 매핑 (orderItemId → documentId) + const fqcDocumentMap = useMemo(() => { + const map: Record = {}; + fqcStatusItems.forEach((item) => { + if (item.documentId) map[String(item.orderItemId)] = item.documentId; + }); + return map; + }, [fqcStatusItems]); + + // 파생: 진행현황 통계 + const fqcStats = useMemo(() => { + if (fqcStatusItems.length === 0) return null; + return { + total: fqcStatusItems.length, + passed: fqcStatusItems.filter((i) => i.judgement === '합격').length, + failed: fqcStatusItems.filter((i) => i.judgement === '불합격').length, + inProgress: fqcStatusItems.filter((i) => i.documentId != null && !i.judgement).length, + notCreated: fqcStatusItems.filter((i) => i.documentId == null).length, + }; + }, [fqcStatusItems]); + + // 개소별 FQC 상태 조회 헬퍼 + const getFqcItemStatus = useCallback( + (orderItemId: string): FqcStatusItem | null => { + return fqcStatusItems.find((i) => String(i.orderItemId) === orderItemId) ?? null; + }, + [fqcStatusItems] + ); // ===== API 데이터 로드 ===== const loadInspection = useCallback(async () => { @@ -179,7 +209,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) { loadInspection(); }, [loadInspection]); - // ===== FQC 문서 매핑 로드 ===== + // ===== FQC 진행현황 로드 ===== useEffect(() => { if (!inspection) return; @@ -191,25 +221,21 @@ export function InspectionDetail({ id }: InspectionDetailProps) { if (orderIds.size === 0) return; // orderId 없으면 legacy 모드 유지 - // 각 orderId별 FQC 상태 조회 후 매핑 구축 - const loadFqcMap = async () => { - const map: Record = {}; + // 각 orderId별 FQC 상태 조회 + const loadFqcStatus = async () => { + const allItems: FqcStatusItem[] = []; for (const orderId of orderIds) { const result = await getFqcStatus(orderId); if (result.success && result.data) { - result.data.items.forEach((item) => { - if (item.documentId) { - map[String(item.orderItemId)] = item.documentId; - } - }); + allItems.push(...result.data.items); } } - if (Object.keys(map).length > 0) { - setFqcDocumentMap(map); + if (allItems.length > 0) { + setFqcStatusItems(allItems); } }; - loadFqcMap(); + loadFqcStatus(); }, [inspection]); // ===== 네비게이션 ===== @@ -379,6 +405,32 @@ export function InspectionDetail({ id }: InspectionDetailProps) { })); }, []); + // ===== FQC 상태 뱃지 렌더링 ===== + const renderFqcBadge = useCallback( + (item: OrderSettingItem) => { + const fqcItem = getFqcItemStatus(item.id); + if (!fqcItem) { + // FQC 데이터 없음 → legacy 상태 + return item.inspectionData ? ( + 검사완료 + ) : ( + 미검사 + ); + } + if (fqcItem.judgement === '합격') { + return 합격; + } + if (fqcItem.judgement === '불합격') { + return 불합격; + } + if (fqcItem.documentId) { + return 진행중; + } + return 미생성; + }, + [getFqcItemStatus] + ); + // ===== 정보 필드 렌더링 헬퍼 ===== const renderInfoField = (label: string, value: React.ReactNode) => (
@@ -387,6 +439,42 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
); + // ===== FQC 진행현황 통계 바 ===== + const renderFqcProgressBar = useMemo(() => { + if (!fqcStats) return null; + const { total, passed, failed, inProgress, notCreated } = fqcStats; + return ( +
+
+ 합격 {passed} + 불합격 {failed} + 진행중 {inProgress} + 미생성 {notCreated} +
+
+ {passed > 0 && ( +
+ )} + {failed > 0 && ( +
+ )} + {inProgress > 0 && ( +
+ )} +
+
+ ); + }, [fqcStats]); + // ===== 수주 설정 아코디언 (조회 모드) ===== const renderOrderAccordion = (groups: OrderGroup[]) => { if (groups.length === 0) { @@ -423,7 +511,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) { 시공 가로 시공 세로 변경사유 - 검사 + 검사 @@ -438,15 +526,19 @@ export function InspectionDetail({ id }: InspectionDetailProps) { {item.constructionHeight} {item.changeReason || '-'} - {item.inspectionData ? ( - - 검사완료 - - ) : ( - - 미검사 - - )} +
+ {renderFqcBadge(item)} + {(getFqcItemStatus(item.id) || item.inspectionData) && ( + + )} +
))} @@ -509,7 +601,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) { 시공 가로 시공 세로 변경사유 - 검사 + 검사 @@ -551,15 +643,18 @@ export function InspectionDetail({ id }: InspectionDetailProps) { /> - +
+ {renderFqcBadge(item)} + +
))} @@ -735,13 +830,16 @@ export function InspectionDetail({ id }: InspectionDetailProps) { {/* 수주 설정 정보 */} - - 수주 설정 정보 -
- 전체: {orderSummary.total} - 일치: {orderSummary.same} - 불일치: {orderSummary.changed} + +
+ 수주 설정 정보 +
+ 전체: {orderSummary.total} + 일치: {orderSummary.same} + 불일치: {orderSummary.changed} +
+ {renderFqcProgressBar}
{renderOrderAccordion(orderGroups)} @@ -749,7 +847,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
); - }, [inspection, orderSummary, orderGroups, handleOpenInspectionInput]); + }, [inspection, orderSummary, orderGroups, handleOpenInspectionInput, renderFqcBadge, getFqcItemStatus, renderFqcProgressBar]); // ===== Edit 모드 폼 렌더링 ===== const renderFormContent = useCallback(() => { @@ -1032,19 +1130,22 @@ export function InspectionDetail({ id }: InspectionDetailProps) { {/* 수주 설정 정보 */} - -
- 수주 설정 정보 - -
-
- 전체: {orderSummary.total} - 일치: {orderSummary.same} - 불일치: {orderSummary.changed} + +
+
+ 수주 설정 정보 + +
+
+ 전체: {orderSummary.total} + 일치: {orderSummary.same} + 불일치: {orderSummary.changed} +
+ {renderFqcProgressBar}
{renderEditOrderAccordion(orderGroups)} @@ -1052,7 +1153,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
); - }, [inspection, formData, orderSummary, orderGroups, updateField, updateNested, handleRemoveOrderItem, handleOpenInspectionInput, handleUpdateOrderItemField, orderModalOpen]); + }, [inspection, formData, orderSummary, orderGroups, updateField, updateNested, handleRemoveOrderItem, handleOpenInspectionInput, handleUpdateOrderItemField, orderModalOpen, renderFqcBadge, renderFqcProgressBar]); // ===== 모드 & Config ===== const mode = isEditMode ? 'edit' : 'view';