feat: 품목관리 기능 개선 및 문서화 업데이트

- 품목 상세/수정 페이지 파일 다운로드 기능 개선
- DynamicItemForm 파일 업로드 UI/UX 개선 (시방서, 인정서)
- BendingDiagramSection 조립/절곡 부품 전개도 통합
- API proxy route 품목 타입별 라우팅 개선
- ItemListClient 파일 다운로드 유틸리티 적용
- 품목코드 중복 체크 및 다이얼로그 추가

문서화:
- DynamicItemForm 훅 분리 계획서 추가 (2161줄 → 900줄 목표)
- 백엔드 API 마이그레이션 문서 추가
- 대용량 파일 처리 전략 가이드 추가
- 테넌트 데이터 격리 감사 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-16 11:01:25 +09:00
parent 8457dba0fc
commit b1587071f2
25 changed files with 3905 additions and 183 deletions

View File

@@ -6,10 +6,11 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { FileImage, Plus, Trash2, X } from 'lucide-react';
import { FileImage, Plus, Trash2, X, Download, Loader2 } from 'lucide-react';
import type { BendingDetail } from '@/types/item';
import type { UseFormSetValue } from 'react-hook-form';
import type { CreateItemFormData } from '@/lib/utils/validation';
import { downloadFileById } from '@/lib/utils/fileDownload';
export interface BendingDiagramSectionProps {
selectedPartType: string;
@@ -26,6 +27,16 @@ export interface BendingDiagramSectionProps {
widthSumFieldKey?: string;
setValue: UseFormSetValue<CreateItemFormData>;
isSubmitting: boolean;
/** 기존 전개도 이미지 URL (수정 모드) */
existingBendingDiagram?: string;
/** 기존 전개도 파일명 */
existingBendingDiagramFileName?: string;
/** 기존 전개도 파일 ID (삭제용) */
existingBendingDiagramFileId?: number | null;
/** 기존 파일 삭제 콜백 */
onDeleteExistingFile?: () => void;
/** 파일 삭제 중 상태 */
isDeletingFile?: boolean;
}
export default function BendingDiagramSection({
@@ -42,7 +53,27 @@ export default function BendingDiagramSection({
widthSumFieldKey,
setValue,
isSubmitting,
existingBendingDiagram,
existingBendingDiagramFileName,
existingBendingDiagramFileId,
onDeleteExistingFile,
isDeletingFile,
}: BendingDiagramSectionProps) {
// 기존 파일 다운로드 핸들러
const handleDownloadExistingFile = async () => {
if (!existingBendingDiagramFileId) {
alert('파일 ID가 없습니다.');
return;
}
try {
const fileName = existingBendingDiagramFileName || '전개도_이미지';
await downloadFileById(existingBendingDiagramFileId, fileName);
} catch (error) {
console.error('[BendingDiagramSection] 파일 다운로드 실패:', error);
alert('파일 다운로드에 실패했습니다.');
}
};
// 폭 합계 업데이트 헬퍼
const updateWidthSum = (details: BendingDetail[]) => {
const totalSum = details.reduce((acc, d) => {
@@ -108,6 +139,76 @@ export default function BendingDiagramSection({
</p>
</div>
{/* 기존 전개도 이미지 (수정 모드) - 파일 ID가 있으면 프록시로 이미지 로드 */}
{existingBendingDiagramFileId && !bendingDiagram && (
<div className="p-4 border rounded-lg bg-blue-50 border-blue-200">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<FileImage className="h-5 w-5 text-blue-600" />
<span className="text-sm font-medium text-blue-900"> </span>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleDownloadExistingFile}
className="text-blue-600 border-blue-300 hover:bg-blue-100"
>
<Download className="h-4 w-4 mr-1" />
</Button>
{onDeleteExistingFile && (
<Button
type="button"
variant="outline"
size="sm"
onClick={onDeleteExistingFile}
disabled={isDeletingFile}
className="text-red-600 border-red-300 hover:bg-red-50"
>
{isDeletingFile ? (
<>
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
...
</>
) : (
<>
<Trash2 className="h-4 w-4 mr-1" />
</>
)}
</Button>
)}
</div>
</div>
{existingBendingDiagramFileName && (
<p className="text-xs text-blue-700 mb-2">
: {existingBendingDiagramFileName}
</p>
)}
<div className="border rounded bg-white p-2">
<img
src={`/api/proxy/files/${existingBendingDiagramFileId}/download`}
alt="기존 전개도"
className="max-w-full h-auto max-h-96 mx-auto"
onError={(e) => {
// 이미지 로드 실패 시 대체 텍스트 표시
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.parentElement?.insertAdjacentHTML(
'beforeend',
'<p class="text-center text-gray-500 py-8">이미지를 불러올 수 없습니다</p>'
);
}}
/>
</div>
<p className="text-xs text-blue-700 mt-2">
*
</p>
</div>
)}
{/* 파일 선택 방식 */}
{bendingDiagramInputMethod === 'file' && (
<div>