diff --git a/src/components/dev/DevFillContext.tsx b/src/components/dev/DevFillContext.tsx index d3ff2ad0..535d5a8c 100644 --- a/src/components/dev/DevFillContext.tsx +++ b/src/components/dev/DevFillContext.tsx @@ -18,7 +18,8 @@ import React, { createContext, useContext, useState, useCallback, useEffect, Rea export type DevFillPageType = | 'quote' | 'quoteV2' | 'order' | 'workOrder' | 'workOrderComplete' | 'shipment' // 판매/생산 플로우 | 'deposit' | 'withdrawal' | 'purchaseApproval' | 'cardTransaction' // 회계 플로우 - | 'client'; // 기준정보 + | 'client' // 기준정보 + | 'receiving'; // 자재 // 폼 채우기 함수 타입 type FillFormFunction = (data?: unknown) => void | Promise; diff --git a/src/components/dev/DevToolbar.tsx b/src/components/dev/DevToolbar.tsx index 83a01456..518d14f4 100644 --- a/src/components/dev/DevToolbar.tsx +++ b/src/components/dev/DevToolbar.tsx @@ -35,6 +35,8 @@ import { CreditCard, // 카드 // 기준정보 아이콘 Building2, // 거래처 + // 자재 아이콘 + PackageCheck, // 입고 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -57,6 +59,8 @@ const PAGE_PATTERNS: { pattern: RegExp; type: DevFillPageType; label: string }[] { pattern: /\/accounting\/card-transactions\/new/, type: 'cardTransaction', label: '카드' }, // 기준정보 { pattern: /\/client-management-sales-admin\/new/, type: 'client', label: '거래처' }, + // 자재 + { pattern: /\/material\/receiving-management\/new/, type: 'receiving', label: '입고' }, ]; // ?mode=new 쿼리 파라미터를 사용하는 페이지 매핑 @@ -91,6 +95,11 @@ const MASTER_DATA_STEPS: { type: DevFillPageType; label: string; icon: typeof Fi { type: 'client', label: '거래처', icon: Building2, path: '/sales/client-management-sales-admin/new', fillEnabled: true }, ]; +// 자재 단계 정의 +const MATERIAL_STEPS: { type: DevFillPageType; label: string; icon: typeof FileText; path: string; fillEnabled: boolean }[] = [ + { type: 'receiving', label: '입고', icon: PackageCheck, path: '/material/receiving-management/new', fillEnabled: true }, +]; + export function DevToolbar() { const pathname = usePathname(); const router = useRouter(); @@ -320,9 +329,10 @@ export function DevToolbar() { )} - {/* 회계 플로우 버튼 영역 */} + {/* 2행: 회계 + 기준정보 + 자재 버튼 영역 */} {isExpanded && (
+ {/* 회계 */} 회계: {ACCOUNTING_STEPS.map((step) => { const Icon = step.icon; @@ -330,7 +340,6 @@ export function DevToolbar() { const isRegistered = hasRegisteredForm(step.type); const isCurrentLoading = isLoading === step.type; - // 활성화된 페이지: 폼 채우기 (fillEnabled가 true인 경우만) if (isActive && step.fillEnabled) { return (
- )} - {/* 기준정보 버튼 영역 */} - {isExpanded && ( -
+ {/* 구분선 */} +
+ + {/* 기준정보 */} 기준: {MASTER_DATA_STEPS.map((step) => { const Icon = step.icon; @@ -380,7 +387,6 @@ export function DevToolbar() { const isRegistered = hasRegisteredForm(step.type); const isCurrentLoading = isLoading === step.type; - // 활성화된 페이지: 폼 채우기 (fillEnabled가 true인 경우만) if (isActive && step.fillEnabled) { return ( + ); + } + + return ( + + ); + })}
)} @@ -424,7 +476,7 @@ export function DevToolbar() { {isExpanded && !activePage && (

- 견적/수주/작업지시/출하/입금/출금/매입/거래처 페이지에서 자동 채우기가 활성화됩니다 + 견적/수주/작업지시/출하/입금/출금/매입/거래처/입고 페이지에서 자동 채우기가 활성화됩니다

)} diff --git a/src/components/dev/generators/receivingData.ts b/src/components/dev/generators/receivingData.ts new file mode 100644 index 00000000..5d9bd827 --- /dev/null +++ b/src/components/dev/generators/receivingData.ts @@ -0,0 +1,62 @@ +/** + * 입고 샘플 데이터 생성기 + */ + +import { + randomPick, + randomInt, + randomRemark, + today, +} from './index'; + +// ===== 상수 정의 ===== + +const SUPPLIERS = [ + '(주)대한철강', '삼성전자부품', '한국플라스틱', '글로벌전자', + '동양화학', '한국볼트', '지오TNS (KG스틸)', 'SK이노베이션', + '포스코', 'LG화학', '현대중공업', '한화솔루션', +]; + +const ITEMS = [ + { code: 'STEEL-001', name: 'SUS304 스테인리스 판재', spec: '1000x2000x3T', unit: 'EA' }, + { code: 'STEEL-002', name: '알루미늄 프로파일', spec: '40x40x2000L', unit: 'EA' }, + { code: 'ELEC-002', name: 'MCU 컨트롤러 IC', spec: 'STM32F103C8T6', unit: 'EA' }, + { code: 'ELEC-005', name: 'DC 모터 24V', spec: '24V 100RPM', unit: 'EA' }, + { code: 'ELEC-007', name: '커패시터 100uF', spec: '100uF 50V', unit: 'EA' }, + { code: 'PLAS-003', name: 'ABS 사출 케이스', spec: '150x100x50', unit: 'SET' }, + { code: 'CHEM-001', name: '에폭시 접착제', spec: '500ml', unit: 'EA' }, + { code: 'BOLT-001', name: 'SUS 볼트 M8x30', spec: 'M8x30 SUS304', unit: 'EA' }, + { code: '80008', name: '80008 egi1.55', spec: '1.55 * 1218 × 480', unit: 'EA' }, +]; + +// ===== 타입 정의 ===== + +export interface ReceivingFormData { + itemCode: string; + itemName: string; + specification: string; + unit: string; + supplier: string; + receivingQty: number; + receivingDate: string; + status: string; + remark: string; +} + +// ===== 메인 생성 함수 ===== + +export function generateReceivingData(): ReceivingFormData { + const item = randomPick(ITEMS); + + return { + itemCode: item.code, + itemName: item.name, + specification: item.spec, + unit: item.unit, + supplier: randomPick(SUPPLIERS), + receivingQty: randomInt(10, 500), + receivingDate: today(), + status: 'receiving_pending', + remark: randomRemark(), + }; +} \ No newline at end of file diff --git a/src/components/dev/index.ts b/src/components/dev/index.ts index a331b96b..727d5d67 100644 --- a/src/components/dev/index.ts +++ b/src/components/dev/index.ts @@ -14,4 +14,5 @@ export { generateQuoteData, generateQuoteItem } from './generators/quoteData'; export { generateOrderData, generateOrderDataFull } from './generators/orderData'; export { generateWorkOrderData } from './generators/workOrderData'; export { generateShipmentData } from './generators/shipmentData'; -export { generateDepositData, generateWithdrawalData, generatePurchaseApprovalData, generateCardTransactionData } from './generators/accountingData'; \ No newline at end of file +export { generateDepositData, generateWithdrawalData, generatePurchaseApprovalData, generateCardTransactionData } from './generators/accountingData'; +export { generateReceivingData } from './generators/receivingData'; \ No newline at end of file diff --git a/src/components/material/ReceivingManagement/ReceivingDetail.tsx b/src/components/material/ReceivingManagement/ReceivingDetail.tsx index d88cda44..8d8b152b 100644 --- a/src/components/material/ReceivingManagement/ReceivingDetail.tsx +++ b/src/components/material/ReceivingManagement/ReceivingDetail.tsx @@ -14,10 +14,11 @@ * 2. 수입검사 정보 - 검사일, 검사결과, 업체 제공 성적서 자료 */ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; -import { Upload, FileText } from 'lucide-react'; -import { SearchableSelect, type SearchableSelectOption } from '@/components/ui/searchable-select'; +import { Upload, FileText, Search } from 'lucide-react'; +import { ItemSearchModal } from '@/components/quotes/ItemSearchModal'; +import { SupplierSearchModal } from './SupplierSearchModal'; import { InspectionModalV2 } from '@/app/[locale]/(protected)/quality/qms/components/InspectionModalV2'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -37,9 +38,6 @@ import { getReceivingById, createReceiving, updateReceiving, - searchItems, - searchSuppliers, - type ItemOption, } from './actions'; import { RECEIVING_STATUS_OPTIONS, @@ -48,6 +46,8 @@ import { } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { toast } from 'sonner'; +import { useDevFill, generateReceivingData } from '@/components/dev'; +import { useAuth } from '@/contexts/AuthContext'; interface Props { id: string; @@ -72,8 +72,19 @@ const INITIAL_FORM_DATA: Partial = { certificateFile: undefined, }; +// 로트번호 생성 (YYMMDD-NN) +function generateLotNo(): string { + const now = new Date(); + const yy = String(now.getFullYear()).slice(-2); + const mm = String(now.getMonth() + 1).padStart(2, '0'); + const dd = String(now.getDate()).padStart(2, '0'); + const seq = String(Math.floor(Math.random() * 100)).padStart(2, '0'); + return `${yy}${mm}${dd}-${seq}`; +} + export function ReceivingDetail({ id, mode = 'view' }: Props) { const router = useRouter(); + const { currentUser } = useAuth(); const isNewMode = mode === 'new' || id === 'new'; const isEditMode = mode === 'edit'; const isViewMode = mode === 'view' && !isNewMode; @@ -87,36 +98,39 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { // 폼 데이터 (등록/수정 모드용) const [formData, setFormData] = useState>(INITIAL_FORM_DATA); - // 수입검사 성적서 모달 상태 + // 모달 상태 const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); + const [isItemSearchOpen, setIsItemSearchOpen] = useState(false); + const [isSupplierSearchOpen, setIsSupplierSearchOpen] = useState(false); - // 품목/발주처 검색 옵션 및 로딩 상태 - const [itemOptions, setItemOptions] = useState([]); - const [supplierOptions, setSupplierOptions] = useState([]); - const [isItemSearching, setIsItemSearching] = useState(false); - const [isSupplierSearching, setIsSupplierSearching] = useState(false); + // currentUser를 ref로 추적 (useCallback 내에서 항상 최신 값 참조) + const currentUserRef = useRef(currentUser); + useEffect(() => { + currentUserRef.current = currentUser; + }, [currentUser]); - // 품목 검색 핸들러 - const handleItemSearch = useCallback(async (query: string) => { - setIsItemSearching(true); - try { - const result = await searchItems(query); - if (result.success) setItemOptions(result.data); - } finally { - setIsItemSearching(false); - } - }, []); - - // 발주처 검색 핸들러 - const handleSupplierSearch = useCallback(async (query: string) => { - setIsSupplierSearching(true); - try { - const result = await searchSuppliers(query); - if (result.success) setSupplierOptions(result.data); - } finally { - setIsSupplierSearching(false); - } - }, []); + // Dev 모드 폼 자동 채우기 + useDevFill( + 'receiving', + useCallback(async () => { + if (!isNewMode) return; + const data = generateReceivingData(); + setFormData((prev) => ({ + ...prev, + lotNo: generateLotNo(), + itemCode: data.itemCode, + itemName: data.itemName, + specification: data.specification, + unit: data.unit, + supplier: data.supplier, + receivingQty: data.receivingQty, + receivingDate: data.receivingDate, + createdBy: currentUserRef.current?.name || '', + status: data.status as ReceivingStatus, + remark: data.remark, + })); + }, [isNewMode]) + ); // API 데이터 로드 const loadData = useCallback(async () => { @@ -313,31 +327,28 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { {/* 로트번호 - 읽기전용 */} {renderReadOnlyField('로트번호', formData.lotNo, true)} - {/* 품목코드 - 검색 선택 */} + {/* 품목코드 - 검색 모달 선택 */}
-
- + { - const found = itemOptions.find((o) => o.value === value) as ItemOption | undefined; - setFormData((prev) => ({ - ...prev, - itemCode: value, - itemName: found?.itemName ?? prev.itemName ?? '', - specification: found?.specification ?? prev.specification ?? '', - unit: found?.unit ?? prev.unit ?? 'EA', - })); - }} - onSearch={handleItemSearch} - isLoading={isItemSearching} - placeholder="품목코드 검색 및 선택" - searchPlaceholder="품목코드 또는 품목명 검색..." - emptyText="검색 결과가 없습니다" + readOnly + placeholder="품목코드를 검색하세요" + className="cursor-pointer bg-gray-50" + onClick={() => setIsItemSearchOpen(true)} /> +
@@ -350,22 +361,28 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { {/* 단위 - 읽기전용 */} {renderReadOnlyField('단위', formData.unit, true)} - {/* 발주처 - 검색 선택 */} + {/* 발주처 - 검색 모달 선택 */}
-
- + handleInputChange('supplier', value)} - onSearch={handleSupplierSearch} - isLoading={isSupplierSearching} - placeholder="발주처 검색 및 선택" - searchPlaceholder="발주처명 검색..." - emptyText="검색 결과가 없습니다" + readOnly + placeholder="발주처를 검색하세요" + className="cursor-pointer bg-gray-50" + onClick={() => setIsSupplierSearchOpen(true)} /> +
@@ -527,6 +544,32 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { onCancel={handleCancel} /> + {/* 품목 검색 모달 */} + { + setFormData((prev) => ({ + ...prev, + itemCode: item.code, + itemName: item.name, + specification: item.specification || '', + })); + }} + /> + + {/* 발주처 검색 모달 */} + { + setFormData((prev) => ({ + ...prev, + supplier: supplier.name, + })); + }} + /> + {/* 수입검사 성적서 모달 */} (undefined); export function AuthProvider({ children }: { children: ReactNode }) { // 상태 관리 (SSR-safe: 항상 초기값으로 시작) const [users, setUsers] = useState(initialUsers); - const [currentUser, setCurrentUser] = useState(initialUsers[2]); // TestUser3 (드미트리) + const [currentUser, setCurrentUser] = useState(null); // ✅ 추가: 이전 tenant.id 추적 (테넌트 전환 감지용) const previousTenantIdRef = useRef(null); @@ -256,7 +256,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { clearTenantCache, resetAllData: () => { setUsers(initialUsers); - setCurrentUser(initialUsers[2]); // TestUser3 + setCurrentUser(null); } };