feat: 단가관리 페이지 마이그레이션 및 HR 관리 기능 추가
## 단가관리 (Pricing Management) - 단가 목록 페이지 (IntegratedListTemplateV2 공통 템플릿 적용) - 단가 등록/수정 폼 (원가/마진 자동 계산) - 이력 조회, 수정 이력, 최종 확정 다이얼로그 - 판매관리 > 단가관리 네비게이션 메뉴 추가 ## HR 관리 (Human Resources) - 사원관리 (목록, 등록, 수정, 상세, CSV 업로드) - 부서관리 (트리 구조) - 근태관리 (기본 구조) ## 품목관리 개선 - Radix UI Select controlled mode 버그 수정 (key prop 적용) - DynamicItemForm 파일 업로드 지원 - 수정 페이지 데이터 로딩 개선 ## 문서화 - 단가관리 마이그레이션 체크리스트 - HR 관리 구현 체크리스트 - Radix UI Select 버그 수정 가이드 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { ArrowLeft, Edit, Package } from 'lucide-react';
|
||||
import { ArrowLeft, Edit, Package, FileImage, Download, FileText, Check, Calendar } from 'lucide-react';
|
||||
|
||||
interface ItemDetailClientProps {
|
||||
item: ItemMaster;
|
||||
@@ -60,6 +60,22 @@ function formatItemCodeForAssembly(item: ItemMaster): string {
|
||||
return item.itemCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage 경로를 전체 URL로 변환
|
||||
* - 이미 전체 URL인 경우 그대로 반환
|
||||
* - 상대 경로인 경우 API URL + /storage/ 붙여서 반환
|
||||
*/
|
||||
function getStorageUrl(path: string | undefined): string | null {
|
||||
if (!path) return null;
|
||||
// 이미 전체 URL인 경우
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
// 상대 경로인 경우
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
return `${apiUrl}/storage/${path}`;
|
||||
}
|
||||
|
||||
export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -339,6 +355,186 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 절곡품/조립품 전개도 정보 */}
|
||||
{item.itemType === 'PT' &&
|
||||
(item.partType === 'BENDING' || item.partType === 'ASSEMBLY') &&
|
||||
(item.bendingDiagram || (item.bendingDetails && item.bendingDetails.length > 0)) && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
||||
<FileImage className="h-4 w-4 md:h-5 md:w-5" />
|
||||
{item.partType === 'ASSEMBLY' ? '조립품 전개도 (바라시)' : '절곡품 전개도 (바라시)'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 md:space-y-6 pt-0">
|
||||
{/* 전개도 이미지 */}
|
||||
{item.bendingDiagram ? (
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs md:text-sm">전개도 이미지</Label>
|
||||
<div className="mt-2 p-2 md:p-4 border rounded-lg bg-gray-50">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getStorageUrl(item.bendingDiagram) || ''}
|
||||
alt="전개도"
|
||||
className="max-w-full h-auto max-h-64 md:max-h-96 mx-auto border rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 md:py-8 text-xs md:text-sm text-muted-foreground border rounded-lg bg-gray-50">
|
||||
등록된 전개도 이미지가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 전개도 상세 데이터 */}
|
||||
{item.bendingDetails && item.bendingDetails.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs md:text-sm">전개도 상세 데이터</Label>
|
||||
<div className="mt-2 overflow-x-auto bg-white rounded border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-100">
|
||||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">번호</TableHead>
|
||||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">입력</TableHead>
|
||||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">연신율</TableHead>
|
||||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">연신율계산후</TableHead>
|
||||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">합계</TableHead>
|
||||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">음영</TableHead>
|
||||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">A각</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{item.bendingDetails.map((detail, detailIndex) => {
|
||||
const calculated = detail.input + detail.elongation;
|
||||
let sum = 0;
|
||||
for (let i = 0; i <= detailIndex; i++) {
|
||||
const d = item.bendingDetails![i];
|
||||
sum += d.input + d.elongation;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={detail.id} className={detail.shaded ? "bg-gray-200" : ""}>
|
||||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.no}</TableCell>
|
||||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.input}</TableCell>
|
||||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.elongation}</TableCell>
|
||||
<TableCell className="px-1 md:px-2 py-1 text-center bg-blue-50">{calculated}</TableCell>
|
||||
<TableCell className="px-1 md:px-2 py-1 text-center bg-green-50 font-medium">{sum}</TableCell>
|
||||
<TableCell className="px-1 md:px-2 py-1 text-center">
|
||||
{detail.shaded ? <Check className="h-3 w-3 md:h-4 md:w-4 inline" /> : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.aAngle || "-"}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="mt-2 p-2 md:p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<p className="text-xs md:text-sm">
|
||||
<span className="font-medium">최종 전개 길이:</span>{" "}
|
||||
<span className="text-base md:text-lg font-bold text-blue-700">
|
||||
{item.bendingDetails.reduce((sum, d) => sum + d.input + d.elongation, 0)} mm
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 제품(FG) 인정 정보 및 첨부 파일 */}
|
||||
{item.itemType === 'FG' && (item.certificationNumber || item.specificationFile || item.certificationFile) && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
||||
<FileText className="h-4 w-4 md:h-5 md:w-5" />
|
||||
인정 정보 및 첨부 파일
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 md:space-y-6 pt-0">
|
||||
{/* 인정 정보 */}
|
||||
{item.certificationNumber && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg border">
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs md:text-sm">인정번호</Label>
|
||||
<p className="mt-1 font-medium">{item.certificationNumber}</p>
|
||||
</div>
|
||||
{item.certificationStartDate && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs md:text-sm flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
유효기간 시작
|
||||
</Label>
|
||||
<p className="mt-1">{new Date(item.certificationStartDate).toLocaleDateString('ko-KR')}</p>
|
||||
</div>
|
||||
)}
|
||||
{item.certificationEndDate && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs md:text-sm flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
유효기간 종료
|
||||
</Label>
|
||||
<p className="mt-1">{new Date(item.certificationEndDate).toLocaleDateString('ko-KR')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 첨부 파일 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 시방서 */}
|
||||
<div className="p-4 border rounded-lg">
|
||||
<Label className="text-muted-foreground text-xs md:text-sm">시방서</Label>
|
||||
{item.specificationFile ? (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-sm truncate flex-1">
|
||||
{item.specificationFileName || '시방서 파일'}
|
||||
</span>
|
||||
<a
|
||||
href={getStorageUrl(item.specificationFile) || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
다운로드
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-sm text-muted-foreground">등록된 시방서가 없습니다.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 인정서 */}
|
||||
<div className="p-4 border rounded-lg">
|
||||
<Label className="text-muted-foreground text-xs md:text-sm">인정서</Label>
|
||||
{item.certificationFile ? (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-green-600" />
|
||||
<span className="text-sm truncate flex-1">
|
||||
{item.certificationFileName || '인정서 파일'}
|
||||
</span>
|
||||
<a
|
||||
href={getStorageUrl(item.certificationFile) || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-sm text-green-600 hover:text-green-800"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
다운로드
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-sm text-muted-foreground">등록된 인정서가 없습니다.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* BOM 정보 - 절곡 부품은 제외 */}
|
||||
{(item.itemType === 'FG' || (item.itemType === 'PT' && item.partType !== 'BENDING')) && item.bom && item.bom.length > 0 && (
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user