'use client'; /** * 제품검사 요청서 - 양식 기반 동적 렌더링 * * template (ID 66) 구조를 기반으로 렌더링: * - approvalLines: 결재라인 (작성/승인) * - basicFields: 기본 정보 필드 (수주처, 업체명 등) * - sections[0-3]: 입력사항 (건축공사장, 자재유통업자, 공사시공자, 공사감리자) * - sections[4]: 검사대상 사전 고지 정보 (description + columns 테이블) * - columns: 사전 고지 테이블 컬럼 (8개, group_name으로 병합 헤더) */ import { ConstructionApprovalTable, DocumentWrapper, DocumentTable, DOC_STYLES, } from '@/components/document-system'; import type { FqcTemplate, FqcDocumentData } from '../fqcActions'; interface FqcRequestDocumentContentProps { template: FqcTemplate; documentData?: FqcDocumentData[]; documentNo?: string; createdDate?: string; readonly?: boolean; } /** 라벨 셀 */ const lbl = `${DOC_STYLES.label} w-28`; /** 서브 라벨 셀 */ const subLbl = 'bg-gray-50 px-2 py-1 font-medium border-r border-gray-300 w-28'; /** 값 셀 */ const val = DOC_STYLES.value; /** EAV 데이터에서 field_key로 값 조회 */ function getFieldValue( data: FqcDocumentData[] | undefined, fieldKey: string, ): string { if (!data) return ''; const found = data.find(d => d.fieldKey === fieldKey && d.sectionId === null); return found?.fieldValue || ''; } /** EAV 데이터에서 섹션 아이템 값 조회 */ function getSectionItemValue( data: FqcDocumentData[] | undefined, sectionId: number, fieldKey: string, ): string { if (!data) return ''; const found = data.find( d => d.sectionId === sectionId && d.fieldKey === fieldKey ); return found?.fieldValue || ''; } /** EAV 데이터에서 테이블 행 데이터 조회 */ function getTableRows( data: FqcDocumentData[] | undefined, _columns: FqcTemplate['columns'], ): Array> { if (!data) return []; // column_id가 있는 데이터만 필터 → row_index로 그룹핑 const columnData = data.filter(d => d.columnId !== null); if (columnData.length === 0) return []; const rowMap = new Map>(); for (const d of columnData) { if (!rowMap.has(d.rowIndex)) rowMap.set(d.rowIndex, {}); const row = rowMap.get(d.rowIndex)!; row[d.fieldKey] = d.fieldValue || ''; } return Array.from(rowMap.entries()) .sort(([a], [b]) => a - b) .map(([, row]) => row); } export function FqcRequestDocumentContent({ template, documentData, documentNo, createdDate, }: FqcRequestDocumentContentProps) { const { approvalLines, basicFields, sections, columns } = template; // 섹션 분리: 입력사항 섹션 (items 있는 것) vs 사전 고지 섹션 (items 없는 것) const inputSections = sections.filter(s => s.items.length > 0); const noticeSections = sections.filter(s => s.items.length === 0); const noticeSection = noticeSections[0]; // 검사대상 사전 고지 정보 // 기본필드를 2열로 배치하기 위한 페어링 const sortedFields = [...basicFields].sort((a, b) => a.sortOrder - b.sortOrder); const fieldPairs: Array<[typeof sortedFields[0], typeof sortedFields[0] | undefined]> = []; for (let i = 0; i < sortedFields.length; i += 2) { fieldPairs.push([sortedFields[i], sortedFields[i + 1]]); } // 테이블 행 데이터 const tableRows = getTableRows(documentData, columns); const sortedColumns = [...columns].sort((a, b) => a.sortOrder - b.sortOrder); // group_name으로 컬럼 그룹 분석 (병합 헤더용) const groupInfo = buildGroupInfo(sortedColumns); return ( {/* 헤더: 제목 + 결재란 */}

{template.title || template.name}

{documentNo && 문서번호: {documentNo}} {createdDate && 작성일자: {createdDate}}
{/* 기본 정보 */} {fieldPairs.map(([left, right], idx) => ( {left.label} {getFieldValue(documentData, left.fieldKey) || '-'} {right && ( <> {right.label} {getFieldValue(documentData, right.fieldKey) || '-'} )} ))} {/* 입력사항: 동적 섹션 */} {inputSections.length > 0 && (
입력사항
{inputSections.map((section, sIdx) => (
{section.title || section.name}
{buildSectionRows(section, documentData).map((row, rIdx) => ( {row.map((cell, cIdx) => ( ))} ))}
{cell.value || '-'}
))}
)} {/* 검사 요청 시 필독 (사전 고지 섹션의 description) */} {noticeSection?.description && (

{noticeSection.description}

)} {/* 검사대상 사전 고지 정보 테이블 */} {sortedColumns.length > 0 && ( {/* 그룹 헤더가 있으면 3단 헤더 */} {groupInfo.hasGroups ? ( <> {groupInfo.topRow.map((cell, i) => ( {cell.label} ))} {groupInfo.midRow.map((cell, i) => ( {cell.label} ))} {groupInfo.botRow.map((cell, i) => ( {cell.label} ))} ) : ( {sortedColumns.map((col, i) => ( {col.label} ))} )} {tableRows.length > 0 ? ( tableRows.map((row, rIdx) => ( {sortedColumns.map((col, cIdx) => ( {col.label === 'No.' ? rIdx + 1 : (row[col.label] || '-')} ))} )) ) : ( 검사대상 사전 고지 정보가 없습니다. )} )} {/* 서명 영역 */}

위 내용과 같이 제품검사를 요청합니다.

{createdDate || ''}

); } // ===== 유틸 함수 ===== interface CellInfo { isLabel: boolean; value: string; width?: string; } /** 섹션 아이템을 2열 레이아웃의 행으로 변환 */ function buildSectionRows( section: FqcTemplate['sections'][0], data?: FqcDocumentData[], ): CellInfo[][] { const items = [...section.items].sort((a, b) => a.sortOrder - b.sortOrder); const rows: CellInfo[][] = []; // 3개 아이템이면 한 행에 3개, 그 외 2개씩 if (items.length === 3) { rows.push( items.map((item, i) => [ { isLabel: true, value: item.itemName, width: i === 2 ? 'w-20' : undefined }, { isLabel: false, value: getSectionItemValue(data, section.id, item.itemName) }, ]).flat() ); } else { for (let i = 0; i < items.length; i += 2) { const left = items[i]; const right = items[i + 1]; const row: CellInfo[] = [ { isLabel: true, value: left.itemName }, { isLabel: false, value: getSectionItemValue(data, section.id, left.itemName) }, ]; if (right) { row.push( { isLabel: true, value: right.itemName }, { isLabel: false, value: getSectionItemValue(data, section.id, right.itemName) }, ); } rows.push(row); } } return rows; } interface HeaderCell { label: string; colSpan?: number; rowSpan?: number; width?: string; } interface GroupInfo { hasGroups: boolean; topRow: HeaderCell[]; midRow: HeaderCell[]; botRow: HeaderCell[]; } /** 컬럼 group_name을 분석하여 3단 헤더 구조 생성 */ function buildGroupInfo(columns: FqcTemplate['columns']): GroupInfo { const groups = columns.filter(c => c.groupName); if (groups.length === 0) return { hasGroups: false, topRow: [], midRow: [], botRow: [] }; // group_name별로 그룹핑 const groupMap = new Map(); for (const col of columns) { if (col.groupName) { if (!groupMap.has(col.groupName)) groupMap.set(col.groupName, []); groupMap.get(col.groupName)!.push(col); } } // 오픈사이즈(발주규격), 오픈사이즈(시공후규격) 패턴 감지 // group_name 패턴: "오픈사이즈(발주규격)", "오픈사이즈(시공후규격)" const parentGroups = new Map(); for (const gName of groupMap.keys()) { const match = gName.match(/^(.+?)\((.+?)\)$/); if (match) { const parent = match[1]; if (!parentGroups.has(parent)) parentGroups.set(parent, []); parentGroups.get(parent)!.push(gName); } } const topRow: HeaderCell[] = []; const midRow: HeaderCell[] = []; const botRow: HeaderCell[] = []; let colIdx = 0; while (colIdx < columns.length) { const col = columns[colIdx]; if (!col.groupName) { // 그룹이 없는 독립 컬럼 → rowSpan=3 topRow.push({ label: col.label, rowSpan: 3, width: col.width || undefined }); colIdx++; } else { // 그룹 컬럼 → 상위 그룹 확인 const match = col.groupName.match(/^(.+?)\((.+?)\)$/); if (match) { const parentName = match[1]; const subGroups = parentGroups.get(parentName) || []; // 상위 그룹의 모든 하위 컬럼 수 let totalCols = 0; for (const sg of subGroups) { totalCols += groupMap.get(sg)!.length; } topRow.push({ label: parentName, colSpan: totalCols }); // 중간행: 각 하위 그룹 for (const sg of subGroups) { const subMatch = sg.match(/\((.+?)\)$/); const subLabel = subMatch ? subMatch[1] : sg; const subCols = groupMap.get(sg)!; midRow.push({ label: subLabel, colSpan: subCols.length }); // 하단행: 실제 컬럼 라벨 for (const sc of subCols) { // 라벨에서 그룹 프리픽스 제거 (발주 가로 → 가로) const cleanLabel = sc.label.replace(/^(발주|시공)\s*/, ''); botRow.push({ label: cleanLabel, width: sc.width || undefined }); } } // 이 그룹에 속한 컬럼 수만큼 건너뛰기 colIdx += totalCols; } else { // 단순 그룹 (parentGroup 없이) const gCols = groupMap.get(col.groupName)!; topRow.push({ label: col.groupName, colSpan: gCols.length, rowSpan: 2 }); for (const gc of gCols) { botRow.push({ label: gc.label, width: gc.width || undefined }); } colIdx += gCols.length; } } } return { hasGroups: true, topRow, midRow, botRow }; }