feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가
자재관리: - 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장 - 재고현황 컴포넌트 리팩토링 출고관리: - 출하관리 생성/수정/목록/상세 개선 - 차량배차관리 상세/수정/목록 기능 보강 생산관리: - 작업지시서 WIP 생산 모달 신규 추가 - 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가 - 작업자화면 기능 대폭 확장 (카드/목록 개선) - 검사성적서 모달 개선 품질관리: - 실적보고서 관리 페이지 신규 추가 - 검사관리 문서/타입/목데이터 개선 단가관리: - 단가배포 페이지 및 컴포넌트 신규 추가 - 단가표 관리 페이지 및 컴포넌트 신규 추가 공통: - 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard) - 메뉴 폴링 훅 개선, 레이아웃 수정 - 모바일 줌/패닝 CSS 수정 - locale 유틸 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,19 +70,23 @@ export function WorkItemCard({
|
||||
{item.itemCode} ({item.itemName})
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
{item.floor} / {item.code}
|
||||
</span>
|
||||
{!item.isWip && (
|
||||
<span className="text-sm text-gray-500">
|
||||
{item.floor} / {item.code}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 제작 사이즈 */}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-700">
|
||||
<span className="text-gray-500">제작 사이즈</span>
|
||||
<span className="font-medium">
|
||||
{item.width.toLocaleString()} X {item.height.toLocaleString()} mm
|
||||
</span>
|
||||
<span className="font-medium">{item.quantity}개</span>
|
||||
</div>
|
||||
{/* 제작 사이즈 (재공품은 숨김) */}
|
||||
{!item.isWip && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-700">
|
||||
<span className="text-gray-500">제작 사이즈</span>
|
||||
<span className="font-medium">
|
||||
{item.width.toLocaleString()} X {item.height.toLocaleString()} mm
|
||||
</span>
|
||||
<span className="font-medium">{item.quantity}개</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 공정별 추가 정보 */}
|
||||
{item.processType === 'screen' && item.cuttingInfo && (
|
||||
@@ -100,10 +104,14 @@ export function WorkItemCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{item.processType === 'bending' && item.bendingInfo && (
|
||||
{item.processType === 'bending' && !item.isWip && item.bendingInfo && (
|
||||
<BendingExtraInfo info={item.bendingInfo} />
|
||||
)}
|
||||
|
||||
{item.isWip && item.wipInfo && (
|
||||
<WipExtraInfo info={item.wipInfo} />
|
||||
)}
|
||||
|
||||
{/* 진척률 프로그래스 바 */}
|
||||
<div className="space-y-1">
|
||||
<Progress value={progressPercent} className="h-2" />
|
||||
@@ -240,7 +248,7 @@ function SlatExtraInfo({
|
||||
}
|
||||
|
||||
// ===== 절곡 전용: 도면 + 공통사항 + 세부부품 =====
|
||||
import type { BendingInfo } from './types';
|
||||
import type { BendingInfo, WipInfo } from './types';
|
||||
|
||||
function BendingExtraInfo({ info }: { info: BendingInfo }) {
|
||||
return (
|
||||
@@ -311,3 +319,41 @@ function BendingExtraInfo({ info }: { info: BendingInfo }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 재공품 전용: 도면 + 공통사항 (규격, 길이별 수량) =====
|
||||
function WipExtraInfo({ info }: { info: WipInfo }) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{/* 도면 이미지 (큰 영역) */}
|
||||
<div className="flex-1 min-h-[160px] border rounded-lg bg-gray-50 flex items-center justify-center overflow-hidden">
|
||||
{info.drawingUrl ? (
|
||||
<img
|
||||
src={info.drawingUrl}
|
||||
alt="도면"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-1 text-gray-400">
|
||||
<ImageIcon className="h-8 w-8" />
|
||||
<span className="text-xs">IMG</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 공통사항 */}
|
||||
<div className="flex-1 space-y-0">
|
||||
<p className="text-xs font-medium text-gray-500 mb-2">공통사항</p>
|
||||
<div className="border rounded-lg divide-y">
|
||||
<div className="flex">
|
||||
<span className="px-3 py-2.5 text-xs text-gray-500 bg-gray-50 w-20 flex-shrink-0 border-r">규격</span>
|
||||
<span className="px-3 py-2.5 text-xs font-semibold text-gray-900 flex-1 text-right">{info.specification}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="px-3 py-2.5 text-xs text-gray-500 bg-gray-50 w-20 flex-shrink-0 border-r">길이별 수량</span>
|
||||
<span className="px-3 py-2.5 text-xs font-semibold text-gray-900 flex-1 text-right">{info.lengthQuantity}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,8 +166,71 @@ const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
|
||||
],
|
||||
};
|
||||
|
||||
// 절곡 재공품 전용 목업 데이터 (토글로 전환)
|
||||
const MOCK_ITEMS_BENDING_WIP: WorkItemData[] = [
|
||||
{
|
||||
id: 'mock-bw1', itemNo: 1, itemCode: 'KWWS03', itemName: '케이스 - 전면부', floor: '-', code: '-',
|
||||
width: 0, height: 0, quantity: 6, processType: 'bending',
|
||||
isWip: true,
|
||||
wipInfo: { specification: 'EGI 1.55T (W576)', lengthQuantity: '4,000mm X 6개' },
|
||||
steps: [
|
||||
{ id: 'bw1-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'bw1-2', name: '절단', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-bw2', itemNo: 2, itemCode: 'KWWS04', itemName: '케이스 - 후면부', floor: '-', code: '-',
|
||||
width: 0, height: 0, quantity: 4, processType: 'bending',
|
||||
isWip: true,
|
||||
wipInfo: { specification: 'EGI 1.55T (W576)', lengthQuantity: '3,500mm X 4개' },
|
||||
steps: [
|
||||
{ id: 'bw2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'bw2-2', name: '절단', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-bw3', itemNo: 3, itemCode: 'KWWS05', itemName: '하단마감재', floor: '-', code: '-',
|
||||
width: 0, height: 0, quantity: 10, processType: 'bending',
|
||||
isWip: true,
|
||||
wipInfo: { specification: 'EGI 1.2T (W400)', lengthQuantity: '2,800mm X 10개' },
|
||||
steps: [
|
||||
{ id: 'bw3-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'bw3-2', name: '절단', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 슬랫 조인트바 전용 목업 데이터 (토글로 전환)
|
||||
const MOCK_ITEMS_SLAT_JOINTBAR: WorkItemData[] = [
|
||||
{
|
||||
id: 'mock-jb1', itemNo: 1, itemCode: 'KQJB01', itemName: '조인트바 A', floor: '1층', code: 'FSS-01',
|
||||
width: 0, height: 0, quantity: 8, processType: 'slat',
|
||||
isJointBar: true,
|
||||
slatJointBarInfo: { specification: 'EGI 1.6T', length: 3910, quantity: 8 },
|
||||
steps: [
|
||||
{ id: 'jb1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 'jb1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 'jb1-3', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
materialInputs: [
|
||||
{ id: 'mjb1', lotNo: 'LOT-2026-020', itemName: '조인트바 코일', quantity: 100, unit: 'kg' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-jb2', itemNo: 2, itemCode: 'KQJB02', itemName: '조인트바 B', floor: '2층', code: 'FSS-02',
|
||||
width: 0, height: 0, quantity: 12, processType: 'slat',
|
||||
isJointBar: true,
|
||||
slatJointBarInfo: { specification: 'EGI 1.6T', length: 5200, quantity: 12 },
|
||||
steps: [
|
||||
{ id: 'jb2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'jb2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 'jb2-3', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 하드코딩된 공정별 단계 폴백
|
||||
const PROCESS_STEPS: Record<ProcessTab, { name: string; isMaterialInput: boolean }[]> = {
|
||||
const PROCESS_STEPS: Record<string, { name: string; isMaterialInput: boolean }[]> = {
|
||||
screen: [
|
||||
{ name: '자재투입', isMaterialInput: true },
|
||||
{ name: '절단', isMaterialInput: false },
|
||||
@@ -185,6 +248,10 @@ const PROCESS_STEPS: Record<ProcessTab, { name: string; isMaterialInput: boolean
|
||||
{ name: '절곡', isMaterialInput: false },
|
||||
{ name: '포장완료', isMaterialInput: false },
|
||||
],
|
||||
bending_wip: [
|
||||
{ name: '자재투입', isMaterialInput: true },
|
||||
{ name: '절단', isMaterialInput: false },
|
||||
],
|
||||
};
|
||||
|
||||
export default function WorkerScreen() {
|
||||
@@ -193,6 +260,8 @@ export default function WorkerScreen() {
|
||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<ProcessTab>('screen');
|
||||
const [bendingSubMode, setBendingSubMode] = useState<'normal' | 'wip'>('normal');
|
||||
const [slatSubMode, setSlatSubMode] = useState<'normal' | 'jointbar'>('normal');
|
||||
|
||||
// 작업 정보
|
||||
const [productionManagerId, setProductionManagerId] = useState('');
|
||||
@@ -305,13 +374,27 @@ export default function WorkerScreen() {
|
||||
});
|
||||
|
||||
// 목업 데이터 합치기 (API 데이터 뒤에 번호 이어서)
|
||||
const mockItems = MOCK_ITEMS[activeTab].map((item, i) => ({
|
||||
// 절곡 탭에서 재공품 서브모드면 WIP 전용 목업 사용
|
||||
// 슬랫 탭에서 조인트바 서브모드면 조인트바 전용 목업 사용
|
||||
const baseMockItems = (activeTab === 'bending' && bendingSubMode === 'wip')
|
||||
? MOCK_ITEMS_BENDING_WIP
|
||||
: (activeTab === 'slat' && slatSubMode === 'jointbar')
|
||||
? MOCK_ITEMS_SLAT_JOINTBAR
|
||||
: MOCK_ITEMS[activeTab];
|
||||
const mockItems = baseMockItems.map((item, i) => ({
|
||||
...item,
|
||||
itemNo: apiItems.length + i + 1,
|
||||
steps: item.steps.map((step) => {
|
||||
const stepKey = `${item.id}-${step.name}`;
|
||||
return {
|
||||
...step,
|
||||
isCompleted: stepCompletionMap[stepKey] ?? step.isCompleted,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
return [...apiItems, ...mockItems];
|
||||
}, [filteredWorkOrders, activeTab, stepCompletionMap]);
|
||||
}, [filteredWorkOrders, activeTab, stepCompletionMap, bendingSubMode, slatSubMode]);
|
||||
|
||||
// ===== 수주 정보 (첫 번째 작업 기반 표시) =====
|
||||
const orderInfo = useMemo(() => {
|
||||
@@ -509,6 +592,27 @@ export default function WorkerScreen() {
|
||||
}
|
||||
}, [getTargetOrder]);
|
||||
|
||||
// ===== 재공품 감지 =====
|
||||
const hasWipItems = useMemo(() => {
|
||||
return activeTab === 'bending' && workItems.some(item => item.isWip);
|
||||
}, [activeTab, workItems]);
|
||||
|
||||
// ===== 조인트바 감지 =====
|
||||
const hasJointBarItems = useMemo(() => {
|
||||
return activeTab === 'slat' && slatSubMode === 'jointbar';
|
||||
}, [activeTab, slatSubMode]);
|
||||
|
||||
// 재공품 통합 문서 (작업일지 + 중간검사) 핸들러
|
||||
const handleWipInspection = useCallback(() => {
|
||||
const target = getTargetOrder();
|
||||
if (target) {
|
||||
setSelectedOrder(target);
|
||||
setIsInspectionModalOpen(true);
|
||||
} else {
|
||||
toast.error('표시할 작업이 없습니다.');
|
||||
}
|
||||
}, [getTargetOrder]);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-6 pb-20">
|
||||
@@ -521,7 +625,9 @@ export default function WorkerScreen() {
|
||||
<ClipboardList className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">작업자 화면</h1>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{hasJointBarItems ? '슬랫 조인트바 공정' : hasWipItems ? '절곡 재공품 공정' : '작업자 화면'}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">작업을 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -571,6 +677,68 @@ export default function WorkerScreen() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 슬랫 탭: 슬랫/조인트바 전환 토글 */}
|
||||
{tab === 'slat' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 p-1 bg-gray-100 rounded-lg w-fit">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSlatSubMode('normal')}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
slatSubMode === 'normal'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
슬랫
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSlatSubMode('jointbar')}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
slatSubMode === 'jointbar'
|
||||
? 'bg-blue-500 text-white shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
조인트바
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">* 샘플 데이터 전환용</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 절곡 탭: 절곡/재공품 전환 토글 */}
|
||||
{tab === 'bending' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 p-1 bg-gray-100 rounded-lg w-fit">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBendingSubMode('normal')}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
bendingSubMode === 'normal'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
절곡
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBendingSubMode('wip')}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
bendingSubMode === 'wip'
|
||||
? 'bg-orange-500 text-white shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
재공품
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">* 샘플 데이터 전환용</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 수주 정보 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
@@ -661,19 +829,32 @@ export default function WorkerScreen() {
|
||||
{/* 하단 고정 버튼 - DetailActions 패턴 적용 */}
|
||||
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'}`}>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleWorkLog}
|
||||
className="flex-1 py-6 text-base font-medium"
|
||||
>
|
||||
작업일지 보기
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleInspection}
|
||||
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
중간검사하기
|
||||
</Button>
|
||||
{hasWipItems ? (
|
||||
// 재공품: 통합 버튼 1개
|
||||
<Button
|
||||
onClick={handleWipInspection}
|
||||
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
작업일지 및 중간검사하기
|
||||
</Button>
|
||||
) : (
|
||||
// 일반/조인트바: 버튼 2개
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleWorkLog}
|
||||
className="flex-1 py-6 text-base font-medium"
|
||||
>
|
||||
작업일지 보기
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleInspection}
|
||||
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
중간검사하기
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -706,8 +887,9 @@ export default function WorkerScreen() {
|
||||
open={isInspectionModalOpen}
|
||||
onOpenChange={setIsInspectionModalOpen}
|
||||
workOrderId={selectedOrder?.id || null}
|
||||
processType={activeTab}
|
||||
processType={hasWipItems ? 'bending_wip' : activeTab}
|
||||
readOnly={false}
|
||||
isJointBar={hasJointBarItems}
|
||||
/>
|
||||
|
||||
<IssueReportModal
|
||||
|
||||
@@ -41,16 +41,29 @@ export interface WorkItemData {
|
||||
quantity: number; // 수량
|
||||
processType: ProcessTab; // 공정 타입
|
||||
steps: WorkStepData[]; // 공정 단계들
|
||||
isWip?: boolean; // 재공품 여부
|
||||
isJointBar?: boolean; // 조인트바 여부
|
||||
// 스크린 전용
|
||||
cuttingInfo?: CuttingInfo;
|
||||
// 슬랫 전용
|
||||
slatInfo?: SlatInfo;
|
||||
// 슬랫 조인트바 전용
|
||||
slatJointBarInfo?: SlatJointBarInfo;
|
||||
// 절곡 전용
|
||||
bendingInfo?: BendingInfo;
|
||||
// 재공품 전용
|
||||
wipInfo?: WipInfo;
|
||||
// 자재 투입 목록
|
||||
materialInputs?: MaterialListItem[];
|
||||
}
|
||||
|
||||
// ===== 재공품 전용 정보 =====
|
||||
export interface WipInfo {
|
||||
drawingUrl?: string; // 도면 이미지 URL
|
||||
specification: string; // 규격 (EGI 1.55T (W576))
|
||||
lengthQuantity: string; // 길이별 수량 (4,000mm X 6개)
|
||||
}
|
||||
|
||||
// ===== 절단 정보 (스크린 전용) =====
|
||||
export interface CuttingInfo {
|
||||
width: number; // 절단 폭 (mm)
|
||||
@@ -64,6 +77,13 @@ export interface SlatInfo {
|
||||
jointBar: number; // 조인트바 개수
|
||||
}
|
||||
|
||||
// ===== 슬랫 조인트바 전용 정보 =====
|
||||
export interface SlatJointBarInfo {
|
||||
specification: string; // 규격 (예: EGI 1.6T)
|
||||
length: number; // 길이 (mm)
|
||||
quantity: number; // 수량
|
||||
}
|
||||
|
||||
// ===== 절곡 전용 정보 =====
|
||||
export interface BendingInfo {
|
||||
drawingUrl?: string; // 도면 이미지 URL
|
||||
|
||||
Reference in New Issue
Block a user