feat(WEB): 중간검사 문서 템플릿 동적 연동 - 공정관리 선택기 + Worker Screen 동적 폼

- ProcessStep 타입에 documentTemplateId/documentTemplateName 추가
- 공정관리 actions.ts: document_template_id 매핑 + getDocumentTemplates 서버 액션
- StepForm: 검사여부 사용 시 문서양식 선택 드롭다운 추가
- WorkerScreen actions.ts: getInspectionTemplate, saveInspectionDocument 서버 액션 추가
- InspectionInputModal: tolerance 기반 자동 판정 + 동적 폼(DynamicInspectionForm) 추가
  - evaluateTolerance: symmetric/asymmetric/range 3가지 tolerance 판정
  - 기존 공정별 하드코딩은 템플릿 없을 때 레거시 모드로 유지
- InspectionReportModal: 템플릿 모드 동적 렌더링 (기준서/DATA/결재라인)
- WorkerScreen index: handleInspectionComplete에서 Document 저장 호출 추가
This commit is contained in:
2026-02-10 08:36:12 +09:00
parent 14b84cc08d
commit 12a423051a
7 changed files with 574 additions and 13 deletions

View File

@@ -9,7 +9,7 @@
* - 완료 정보: 유형(선택 완료 시 완료/클릭 시 완료)
*/
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
@@ -37,7 +37,8 @@ import {
STEP_CONNECTION_TARGET_OPTIONS,
DEFAULT_INSPECTION_SETTING,
} from '@/types/process';
import { createProcessStep, updateProcessStep } from './actions';
import { createProcessStep, updateProcessStep, getDocumentTemplates } from './actions';
import type { DocumentTemplateOption } from './actions';
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
import { InspectionSettingModal } from './InspectionSettingModal';
import { InspectionPreviewModal } from './InspectionPreviewModal';
@@ -104,6 +105,12 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
initialData?.completionType || '클릭 시 완료'
);
// 문서양식 선택
const [documentTemplateId, setDocumentTemplateId] = useState<number | undefined>(
initialData?.documentTemplateId
);
const [documentTemplates, setDocumentTemplates] = useState<DocumentTemplateOption[]>([]);
// 검사 설정
const [inspectionSetting, setInspectionSetting] = useState<InspectionSetting>(
initialData?.inspectionSetting || DEFAULT_INSPECTION_SETTING
@@ -118,6 +125,17 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
// 검사여부가 "사용"인지 확인
const isInspectionEnabled = needsInspection === '사용';
// 검사여부가 "사용"이면 문서양식 목록 조회
useEffect(() => {
if (isInspectionEnabled && documentTemplates.length === 0) {
getDocumentTemplates().then((result) => {
if (result.success && result.data) {
setDocumentTemplates(result.data);
}
});
}
}, [isInspectionEnabled, documentTemplates.length]);
// 제출
const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => {
if (!stepName.trim()) {
@@ -131,6 +149,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
isRequired: isRequired === '필수',
needsApproval: needsApproval === '필요',
needsInspection: needsInspection === '사용',
documentTemplateId: isInspectionEnabled ? documentTemplateId : undefined,
isActive: isActive === '사용',
order: initialData?.order || 0,
connectionType,
@@ -250,6 +269,33 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
</Select>
</div>
</div>
{/* 문서양식 선택 (검사여부가 "사용"일 때만 표시) */}
{isInspectionEnabled && (
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
<div className="space-y-2">
<Label> ( 릿)</Label>
<Select
key={`doc-template-${documentTemplateId ?? 'none'}`}
value={documentTemplateId ? String(documentTemplateId) : ''}
onValueChange={(v) => setDocumentTemplateId(v ? Number(v) : undefined)}
>
<SelectTrigger>
<SelectValue placeholder="문서양식을 선택하세요" />
</SelectTrigger>
<SelectContent>
{documentTemplates.map((tmpl) => (
<SelectItem key={tmpl.id} value={String(tmpl.id)}>
{tmpl.name} ({tmpl.category})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
. MNG에서 .
</p>
</div>
</div>
)}
</CardContent>
</Card>
@@ -338,6 +384,8 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
completionType,
initialData?.stepCode,
isInspectionEnabled,
documentTemplateId,
documentTemplates,
]
);

View File

@@ -519,6 +519,40 @@ export async function getItemTypeOptions(): Promise<Array<{ value: string; label
return result.data.map((item) => ({ value: item.code, label: item.name }));
}
// ============================================================================
// 문서 양식 (Document Template) API
// ============================================================================
export interface DocumentTemplateOption {
id: number;
name: string;
category: string;
}
/**
* 문서 양식 목록 조회 (드롭다운용)
*/
export async function getDocumentTemplates(): Promise<{
success: boolean;
data?: DocumentTemplateOption[];
error?: string;
}> {
interface ApiTemplateItem { id: number; name: string; category: string }
const result = await executeServerAction<{ data: ApiTemplateItem[] }>({
url: `${API_URL}/api/v1/document-templates?is_active=1&per_page=100`,
errorMessage: '문서 양식 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data?.data) return { success: false, error: result.error };
return {
success: true,
data: result.data.data.map((item) => ({
id: item.id,
name: item.name,
category: item.category,
})),
};
}
// ============================================================================
// 공정 단계 (Process Step) API
// ============================================================================
@@ -531,6 +565,12 @@ interface ApiProcessStep {
is_required: boolean;
needs_approval: boolean;
needs_inspection: boolean;
document_template_id: number | null;
document_template?: {
id: number;
name: string;
category: string;
} | null;
is_active: boolean;
sort_order: number;
connection_type: string | null;
@@ -548,6 +588,8 @@ function transformStepApiToFrontend(apiStep: ApiProcessStep): ProcessStep {
isRequired: apiStep.is_required,
needsApproval: apiStep.needs_approval,
needsInspection: apiStep.needs_inspection,
documentTemplateId: apiStep.document_template_id ?? undefined,
documentTemplateName: apiStep.document_template?.name ?? undefined,
isActive: apiStep.is_active,
order: apiStep.sort_order,
connectionType: (apiStep.connection_type as ProcessStep['connectionType']) || '없음',
@@ -603,6 +645,7 @@ export async function createProcessStep(
is_required: data.isRequired,
needs_approval: data.needsApproval,
needs_inspection: data.needsInspection,
document_template_id: data.documentTemplateId || null,
is_active: data.isActive,
connection_type: data.connectionType || null,
connection_target: data.connectionTarget || null,
@@ -627,6 +670,7 @@ export async function updateProcessStep(
if (data.isRequired !== undefined) apiData.is_required = data.isRequired;
if (data.needsApproval !== undefined) apiData.needs_approval = data.needsApproval;
if (data.needsInspection !== undefined) apiData.needs_inspection = data.needsInspection;
if (data.documentTemplateId !== undefined) apiData.document_template_id = data.documentTemplateId || null;
if (data.isActive !== undefined) apiData.is_active = data.isActive;
if (data.connectionType !== undefined) apiData.connection_type = data.connectionType || null;
if (data.connectionTarget !== undefined) apiData.connection_target = data.connectionTarget || null;