feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가

자재관리:
- 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장
- 재고현황 컴포넌트 리팩토링

출고관리:
- 출하관리 생성/수정/목록/상세 개선
- 차량배차관리 상세/수정/목록 기능 보강

생산관리:
- 작업지시서 WIP 생산 모달 신규 추가
- 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가
- 작업자화면 기능 대폭 확장 (카드/목록 개선)
- 검사성적서 모달 개선

품질관리:
- 실적보고서 관리 페이지 신규 추가
- 검사관리 문서/타입/목데이터 개선

단가관리:
- 단가배포 페이지 및 컴포넌트 신규 추가
- 단가표 관리 페이지 및 컴포넌트 신규 추가

공통:
- 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard)
- 메뉴 폴링 훅 개선, 레이아웃 수정
- 모바일 줌/패닝 CSS 수정
- locale 유틸 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-04 12:46:19 +09:00
parent 17c16028b1
commit c1b63b850a
70 changed files with 6832 additions and 384 deletions

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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