fix: [inspection] 완료된 검사 모달 readonly 처리
- ProductInspectionInputModal에 readonly prop 추가 - 완료 상태: 적합/부적합 버튼, input, textarea 모두 disabled - 일괄합격/초기화 버튼, 저장 버튼, 사진 업로드/삭제 숨김 - 이전/다음 네비게이션 시 저장 방지 - InspectionDetail에서 status=완료 시 readonly 전달
This commit is contained in:
@@ -1414,6 +1414,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
|
||||
changeReason={selectedOrderItem?.changeReason}
|
||||
orderItems={isEditMode ? formData.orderItems : (inspection?.orderItems || [])}
|
||||
onNavigate={(item) => setSelectedOrderItem(item)}
|
||||
readonly={inspection?.status === '완료'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -53,6 +53,8 @@ interface ProductInspectionInputModalProps {
|
||||
orderItems?: OrderSettingItem[];
|
||||
/** 이전/다음 이동 시 호출 (저장 후 해당 아이템으로 전환) */
|
||||
onNavigate?: (item: OrderSettingItem) => void;
|
||||
/** 읽기전용 모드 (완료된 검사) */
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export function ProductInspectionInputModal({
|
||||
@@ -69,6 +71,7 @@ export function ProductInspectionInputModal({
|
||||
changeReason: initialChangeReason = '',
|
||||
orderItems = [],
|
||||
onNavigate,
|
||||
readonly = false,
|
||||
}: ProductInspectionInputModalProps) {
|
||||
// FQC 모드 상태
|
||||
const [fqcTemplate, setFqcTemplate] = useState<FqcTemplate | null>(null);
|
||||
@@ -302,13 +305,16 @@ export function ProductInspectionInputModal({
|
||||
|
||||
const saveAndNavigate = useCallback(async (targetItem: OrderSettingItem) => {
|
||||
if (!onNavigate) return;
|
||||
// 변경된 내용이 있을 때만 저장
|
||||
if (useFqcMode) {
|
||||
// FQC: judgments 변경 확인
|
||||
const hasJudgmentChanges = Object.keys(judgments).length > 0;
|
||||
if (hasJudgmentChanges) await handleFqcComplete(false);
|
||||
} else if (legacyFormData && hasLegacyChanges()) {
|
||||
onComplete(legacyFormData, { width: conWidth, height: conHeight, changeReason });
|
||||
// readonly 모드에서는 저장 없이 이동만
|
||||
if (!readonly) {
|
||||
// 변경된 내용이 있을 때만 저장
|
||||
if (useFqcMode) {
|
||||
// FQC: judgments 변경 확인
|
||||
const hasJudgmentChanges = Object.keys(judgments).length > 0;
|
||||
if (hasJudgmentChanges) await handleFqcComplete(false);
|
||||
} else if (legacyFormData && hasLegacyChanges()) {
|
||||
onComplete(legacyFormData, { width: conWidth, height: conHeight, changeReason });
|
||||
}
|
||||
}
|
||||
// 다음 아이템으로 이동
|
||||
onNavigate(targetItem);
|
||||
@@ -397,8 +403,9 @@ export function ProductInspectionInputModal({
|
||||
type="number"
|
||||
value={conWidth ?? ''}
|
||||
onChange={(e) => setConWidth(e.target.value ? Number(e.target.value) : null)}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
className={cn("w-full h-9 px-3 rounded-md border border-input bg-background text-sm", readonly && "bg-muted cursor-not-allowed")}
|
||||
placeholder="가로"
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -407,8 +414,9 @@ export function ProductInspectionInputModal({
|
||||
type="number"
|
||||
value={conHeight ?? ''}
|
||||
onChange={(e) => setConHeight(e.target.value ? Number(e.target.value) : null)}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
className={cn("w-full h-9 px-3 rounded-md border border-input bg-background text-sm", readonly && "bg-muted cursor-not-allowed")}
|
||||
placeholder="세로"
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-3">
|
||||
@@ -417,8 +425,9 @@ export function ProductInspectionInputModal({
|
||||
type="text"
|
||||
value={changeReason}
|
||||
onChange={(e) => setChangeReason(e.target.value)}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
className={cn("w-full h-9 px-3 rounded-md border border-input bg-background text-sm", readonly && "bg-muted cursor-not-allowed")}
|
||||
placeholder="변경사유 입력"
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -441,7 +450,7 @@ export function ProductInspectionInputModal({
|
||||
({sortedItems.length}항목)
|
||||
</span>
|
||||
</div>
|
||||
{(() => {
|
||||
{!readonly && (() => {
|
||||
const allPassed = sortedItems.length > 0 && sortedItems.every((_, idx) => judgments[idx] === '적합');
|
||||
return allPassed ? (
|
||||
<button
|
||||
@@ -475,6 +484,7 @@ export function ProductInspectionInputModal({
|
||||
item={item}
|
||||
judgment={judgments[idx] ?? null}
|
||||
onToggle={toggleJudgment}
|
||||
readonly={readonly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -504,6 +514,7 @@ export function ProductInspectionInputModal({
|
||||
<LegacyInspectionForm
|
||||
data={legacyFormData}
|
||||
onChange={setLegacyFormData}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -512,22 +523,24 @@ export function ProductInspectionInputModal({
|
||||
{/* 버튼 영역 */}
|
||||
<div className="flex gap-3 mt-6 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} className="flex-1">
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={useFqcMode ? () => handleFqcComplete(true) : handleLegacyComplete}
|
||||
disabled={isSaving || isLoadingFqc}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
'검사 완료'
|
||||
)}
|
||||
{readonly ? '닫기' : '취소'}
|
||||
</Button>
|
||||
{!readonly && (
|
||||
<Button
|
||||
onClick={useFqcMode ? () => handleFqcComplete(true) : handleLegacyComplete}
|
||||
disabled={isSaving || isLoadingFqc}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
'검사 완료'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -541,11 +554,13 @@ function FqcInspectionRow({
|
||||
item,
|
||||
judgment,
|
||||
onToggle,
|
||||
readonly = false,
|
||||
}: {
|
||||
index: number;
|
||||
item: FqcTemplateItem;
|
||||
judgment: JudgmentValue;
|
||||
onToggle: (rowIndex: number, value: JudgmentValue) => void;
|
||||
readonly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-gray-50">
|
||||
@@ -556,24 +571,32 @@ function FqcInspectionRow({
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggle(index, '적합')}
|
||||
onClick={() => !readonly && onToggle(index, '적합')}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded text-xs font-medium transition-colors',
|
||||
judgment === '적합'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-blue-50 hover:text-blue-600'
|
||||
: 'bg-gray-100 text-gray-600',
|
||||
readonly
|
||||
? 'cursor-not-allowed opacity-70'
|
||||
: judgment !== '적합' && 'hover:bg-blue-50 hover:text-blue-600'
|
||||
)}
|
||||
>
|
||||
적합
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggle(index, '부적합')}
|
||||
onClick={() => !readonly && onToggle(index, '부적합')}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded text-xs font-medium transition-colors',
|
||||
judgment === '부적합'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-red-50 hover:text-red-600'
|
||||
: 'bg-gray-100 text-gray-600',
|
||||
readonly
|
||||
? 'cursor-not-allowed opacity-70'
|
||||
: judgment !== '부적합' && 'hover:bg-red-50 hover:text-red-600'
|
||||
)}
|
||||
>
|
||||
부적합
|
||||
@@ -588,9 +611,11 @@ function FqcInspectionRow({
|
||||
function LegacyInspectionForm({
|
||||
data,
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
data: ProductInspectionData;
|
||||
onChange: (data: ProductInspectionData) => void;
|
||||
readonly?: boolean;
|
||||
}) {
|
||||
const update = <K extends keyof ProductInspectionData>(key: K, value: ProductInspectionData[K]) => {
|
||||
onChange({ ...data, [key]: value });
|
||||
@@ -619,54 +644,56 @@ function LegacyInspectionForm({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-end">
|
||||
{allPassed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetAll}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={setAllPass}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-orange-500 text-white hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
일괄합격
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className="flex justify-end">
|
||||
{allPassed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetAll}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={setAllPass}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-orange-500 text-white hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
일괄합격
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 1. 겉모양 검사 */}
|
||||
<LegacyGroup title="1. 겉모양">
|
||||
<LegacyRow label="가공상태" criteria="사용상 해로운 결함이 없을 것" value={data.appearanceProcessing} onChange={v => update('appearanceProcessing', v)} />
|
||||
<LegacyRow label="재봉상태" criteria="내화실에 의해 견고하게 접합되어야 함" value={data.appearanceSewing} onChange={v => update('appearanceSewing', v)} />
|
||||
<LegacyRow label="조립상태" criteria="핸드링이 견고하게 조립되어야 함" value={data.appearanceAssembly} onChange={v => update('appearanceAssembly', v)} />
|
||||
<LegacyRow label="연기차단재" criteria="케이스 W80, 가이드레일 W50(양쪽 설치)" value={data.appearanceSmokeBarrier} onChange={v => update('appearanceSmokeBarrier', v)} />
|
||||
<LegacyRow label="하단마감재" criteria="내부 무겁방절 설치 유무" value={data.appearanceBottomFinish} onChange={v => update('appearanceBottomFinish', v)} />
|
||||
<LegacyRow label="가공상태" criteria="사용상 해로운 결함이 없을 것" value={data.appearanceProcessing} onChange={v => update('appearanceProcessing', v)} readonly={readonly} />
|
||||
<LegacyRow label="재봉상태" criteria="내화실에 의해 견고하게 접합되어야 함" value={data.appearanceSewing} onChange={v => update('appearanceSewing', v)} readonly={readonly} />
|
||||
<LegacyRow label="조립상태" criteria="핸드링이 견고하게 조립되어야 함" value={data.appearanceAssembly} onChange={v => update('appearanceAssembly', v)} readonly={readonly} />
|
||||
<LegacyRow label="연기차단재" criteria="케이스 W80, 가이드레일 W50(양쪽 설치)" value={data.appearanceSmokeBarrier} onChange={v => update('appearanceSmokeBarrier', v)} readonly={readonly} />
|
||||
<LegacyRow label="하단마감재" criteria="내부 무겁방절 설치 유무" value={data.appearanceBottomFinish} onChange={v => update('appearanceBottomFinish', v)} readonly={readonly} />
|
||||
</LegacyGroup>
|
||||
{/* 2. 모터 */}
|
||||
<LegacyGroup title="2. 모터">
|
||||
<LegacyRow label="모터" criteria="인정제품과 동일사양" value={data.motor} onChange={v => update('motor', v)} />
|
||||
<LegacyRow label="모터" criteria="인정제품과 동일사양" value={data.motor} onChange={v => update('motor', v)} readonly={readonly} />
|
||||
</LegacyGroup>
|
||||
{/* 3. 재질 */}
|
||||
<LegacyGroup title="3. 재질">
|
||||
<LegacyRow label="재질" criteria="WY-SC780 인쇄상태 확인" value={data.material} onChange={v => update('material', v)} />
|
||||
<LegacyRow label="재질" criteria="WY-SC780 인쇄상태 확인" value={data.material} onChange={v => update('material', v)} readonly={readonly} />
|
||||
</LegacyGroup>
|
||||
{/* 4. 치수(오픈사이즈) */}
|
||||
<LegacyGroup title="4. 치수(오픈사이즈)">
|
||||
<LegacyRow label="길이" criteria="수주 치수 ± 30mm" value={data.lengthJudgment} onChange={v => update('lengthJudgment', v)} />
|
||||
<LegacyRow label="높이" criteria="수주 치수 ± 30mm" value={data.heightJudgment} onChange={v => update('heightJudgment', v)} />
|
||||
<LegacyRow label="가이드레일 간격" criteria="10 ± 5mm (측정부위 높이 100 이하)" value={data.guideRailGap} onChange={v => update('guideRailGap', v)} />
|
||||
<LegacyRow label="하단막대 간격" criteria="가이드레일과 하단마감재 측 사이 25mm 이내" value={data.bottomFinishGap} onChange={v => update('bottomFinishGap', v)} />
|
||||
<LegacyRow label="길이" criteria="수주 치수 ± 30mm" value={data.lengthJudgment} onChange={v => update('lengthJudgment', v)} readonly={readonly} />
|
||||
<LegacyRow label="높이" criteria="수주 치수 ± 30mm" value={data.heightJudgment} onChange={v => update('heightJudgment', v)} readonly={readonly} />
|
||||
<LegacyRow label="가이드레일 간격" criteria="10 ± 5mm (측정부위 높이 100 이하)" value={data.guideRailGap} onChange={v => update('guideRailGap', v)} readonly={readonly} />
|
||||
<LegacyRow label="하단막대 간격" criteria="가이드레일과 하단마감재 측 사이 25mm 이내" value={data.bottomFinishGap} onChange={v => update('bottomFinishGap', v)} readonly={readonly} />
|
||||
</LegacyGroup>
|
||||
{/* 5~9. 시험 검사 */}
|
||||
<LegacyGroup title="5~9. 시험 검사">
|
||||
<LegacyRow label="내화시험" criteria="비차열/차열성 - 공인시험기관 시험성적서" value={data.fireResistanceTest} onChange={v => update('fireResistanceTest', v)} />
|
||||
<LegacyRow label="차연시험" criteria="25Pa 시 공기누설량 0.9m³/min·m² 이하" value={data.smokeLeakageTest} onChange={v => update('smokeLeakageTest', v)} />
|
||||
<LegacyRow label="개폐시험" criteria="전동개폐 2.5~6.5m/min, 자중강하 3~7m/min" value={data.openCloseTest} onChange={v => update('openCloseTest', v)} />
|
||||
<LegacyRow label="내충격시험" criteria="방화상 유해한 파괴, 박리 탈락 유무" value={data.impactTest} onChange={v => update('impactTest', v)} />
|
||||
<LegacyRow label="내화시험" criteria="비차열/차열성 - 공인시험기관 시험성적서" value={data.fireResistanceTest} onChange={v => update('fireResistanceTest', v)} readonly={readonly} />
|
||||
<LegacyRow label="차연시험" criteria="25Pa 시 공기누설량 0.9m³/min·m² 이하" value={data.smokeLeakageTest} onChange={v => update('smokeLeakageTest', v)} readonly={readonly} />
|
||||
<LegacyRow label="개폐시험" criteria="전동개폐 2.5~6.5m/min, 자중강하 3~7m/min" value={data.openCloseTest} onChange={v => update('openCloseTest', v)} readonly={readonly} />
|
||||
<LegacyRow label="내충격시험" criteria="방화상 유해한 파괴, 박리 탈락 유무" value={data.impactTest} onChange={v => update('impactTest', v)} readonly={readonly} />
|
||||
</LegacyGroup>
|
||||
{/* 사진 첨부 */}
|
||||
<LegacyGroup title="제품 사진">
|
||||
@@ -674,6 +701,7 @@ function LegacyInspectionForm({
|
||||
images={data.productImages}
|
||||
onChange={(images) => update('productImages', images)}
|
||||
maxCount={2}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</LegacyGroup>
|
||||
{/* 특이사항 */}
|
||||
@@ -681,8 +709,9 @@ function LegacyInspectionForm({
|
||||
<textarea
|
||||
value={data.specialNotes ?? ''}
|
||||
onChange={(e) => update('specialNotes', e.target.value)}
|
||||
className="w-full min-h-[60px] px-3 py-2 rounded-md border border-input bg-background text-sm resize-none"
|
||||
className={cn("w-full min-h-[60px] px-3 py-2 rounded-md border border-input bg-background text-sm resize-none", readonly && "bg-muted cursor-not-allowed")}
|
||||
placeholder="특이사항 입력"
|
||||
disabled={readonly}
|
||||
/>
|
||||
</LegacyGroup>
|
||||
</>
|
||||
@@ -703,11 +732,13 @@ function LegacyRow({
|
||||
criteria,
|
||||
value,
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
label: string;
|
||||
criteria?: string;
|
||||
value: 'pass' | 'fail' | null;
|
||||
onChange: (v: 'pass' | 'fail') => void;
|
||||
readonly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -718,20 +749,28 @@ function LegacyRow({
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange('pass')}
|
||||
onClick={() => !readonly && onChange('pass')}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
value === 'pass' ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
value === 'pass' ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-700',
|
||||
readonly
|
||||
? 'cursor-not-allowed opacity-70'
|
||||
: value !== 'pass' && 'hover:bg-gray-300'
|
||||
)}
|
||||
>
|
||||
적합
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange('fail')}
|
||||
onClick={() => !readonly && onChange('fail')}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
value === 'fail' ? 'bg-gray-700 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
value === 'fail' ? 'bg-gray-700 text-white' : 'bg-gray-200 text-gray-700',
|
||||
readonly
|
||||
? 'cursor-not-allowed opacity-70'
|
||||
: value !== 'fail' && 'hover:bg-gray-300'
|
||||
)}
|
||||
>
|
||||
부적합
|
||||
@@ -745,10 +784,12 @@ function LegacyPhotoUpload({
|
||||
images = [],
|
||||
onChange,
|
||||
maxCount,
|
||||
readonly = false,
|
||||
}: {
|
||||
images: string[];
|
||||
onChange: (images: string[]) => void;
|
||||
maxCount: number;
|
||||
readonly?: boolean;
|
||||
}) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -777,16 +818,18 @@ function LegacyPhotoUpload({
|
||||
{images.map((src, idx) => (
|
||||
<div key={idx} className="relative w-24 h-24 rounded-lg border overflow-hidden group">
|
||||
<img src={src} alt={`사진 ${idx + 1}`} className="w-full h-full object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeImage(idx)}
|
||||
className="absolute top-1 right-1 w-5 h-5 rounded-full bg-black/60 text-white text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
{!readonly && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeImage(idx)}
|
||||
className="absolute top-1 right-1 w-5 h-5 rounded-full bg-black/60 text-white text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{images.length < maxCount && (
|
||||
{!readonly && images.length < maxCount && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
@@ -796,13 +839,15 @@ function LegacyPhotoUpload({
|
||||
<span className="text-xs mt-1">{images.length}/{maxCount}</span>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{!readonly && (
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user