fix(WEB): 작업지시 상세/생성 화면 버그 수정

- 작업지시 상세: 우선순위, 비고(메모), 수정버튼 표시 추가
- 공정 진행 상태: pending/unassigned 시 첫 단계 미완료로 표시
- 생산지시 생성: 담당자 선택 시 users.id 사용하도록 수정
  - Employee 타입에 userId 필드 추가
  - 시스템 계정이 있는 직원만 담당자로 선택 가능
This commit is contained in:
2026-01-16 15:30:59 +09:00
parent b14ea842f8
commit 34deb61632
5 changed files with 77 additions and 13 deletions

View File

@@ -27,7 +27,9 @@ import {
TableRow,
} from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { Factory, ArrowLeft, BarChart3, CheckCircle2, AlertCircle } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Factory, ArrowLeft, BarChart3, CheckCircle2, AlertCircle, User } from "lucide-react";
import { PageLayout } from "@/components/organisms/PageLayout";
import {
AlertDialog,
@@ -48,6 +50,8 @@ import {
type CreateProductionOrderData,
} from "@/components/orders/actions";
import { getProcessList } from "@/components/process-management/actions";
import { getEmployees } from "@/components/hr/EmployeeManagement/actions";
import type { Employee } from "@/components/hr/EmployeeManagement/types";
import type { Process } from "@/types/process";
import { formatAmount } from "@/utils/formatAmount";
@@ -348,25 +352,28 @@ export default function ProductionOrderCreatePage() {
const [error, setError] = useState<string | null>(null);
const [order, setOrder] = useState<Order | null>(null);
const [processes, setProcesses] = useState<Process[]>([]);
const [employees, setEmployees] = useState<Employee[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
// 우선순위 상태
const [selectedPriority, setSelectedPriority] = useState<PriorityLevel>("normal");
const [selectedAssignee, setSelectedAssignee] = useState<string>("");
const [memo, setMemo] = useState("");
// 성공 다이얼로그 상태
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
const [generatedWorkOrders, setGeneratedWorkOrders] = useState<Array<{ workOrderNo: string; processName?: string }>>([]);
// 수주 데이터 공정 목록 로드
// 수주 데이터, 공정 목록, 직원 목록 로드
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
// 수주 정보 공정 목록을 병렬로 로드
const [orderResult, processResult] = await Promise.all([
// 수주 정보, 공정 목록, 직원 목록을 병렬로 로드
const [orderResult, processResult, employeeResult] = await Promise.all([
getOrderById(orderId),
getProcessList({ status: "사용중" }),
getEmployees({ status: "active", per_page: 100 }),
]);
if (orderResult.success && orderResult.data) {
@@ -378,6 +385,10 @@ export default function ProductionOrderCreatePage() {
if (processResult.success && processResult.data) {
setProcesses(processResult.data.items);
}
if (employeeResult.data) {
setEmployees(employeeResult.data);
}
} catch {
setError("서버 오류가 발생했습니다.");
} finally {
@@ -400,6 +411,12 @@ export default function ProductionOrderCreatePage() {
const handleConfirm = async () => {
if (!order) return;
// 담당자 필수 검증
if (!selectedAssignee) {
setError("담당자를 선택해주세요.");
return;
}
setIsSubmitting(true);
setError(null);
try {
@@ -421,6 +438,7 @@ export default function ProductionOrderCreatePage() {
const productionData: CreateProductionOrderData = {
priority: selectedPriority,
assigneeId: parseInt(selectedAssignee, 10),
memo: memo || undefined,
processIds: allProcessIds.length > 0 ? allProcessIds : undefined,
};
@@ -708,6 +726,31 @@ export default function ProductionOrderCreatePage() {
)}
</div>
{/* 담당자 선택 */}
<div>
<Label className="text-sm font-medium flex items-center gap-2 mb-2">
<User className="h-4 w-4" />
<span className="text-red-500">*</span>
</Label>
<Select value={selectedAssignee} onValueChange={setSelectedAssignee}>
<SelectTrigger className={cn("w-full md:w-[300px]", !selectedAssignee && "border-red-300")}>
<SelectValue placeholder="담당자를 선택하세요" />
</SelectTrigger>
<SelectContent>
{employees
.filter((emp) => emp.userId) // 시스템 계정이 있는 직원만
.map((emp) => (
<SelectItem key={emp.id} value={String(emp.userId)}>
{emp.name}
</SelectItem>
))}
</SelectContent>
</Select>
{!selectedAssignee && (
<p className="text-xs text-red-500 mt-1"> .</p>
)}
</div>
{/* 메모 */}
<div>
<p className="text-sm font-medium mb-2"></p>

View File

@@ -98,6 +98,7 @@ export interface UserInfo {
export interface Employee {
id: string;
userId?: number; // users 테이블 ID (담당자 선택 등에 사용)
// 기본 정보 (필수)
name: string;

View File

@@ -196,6 +196,7 @@ export function transformApiToFrontend(api: EmployeeApiData): Employee {
return {
id: String(api.id),
userId: api.user_id, // users 테이블 ID
name: api.user?.name || api.display_name || '',
employeeCode: extra.employee_code,
residentNumber: extra.resident_number,

View File

@@ -7,7 +7,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2, Undo2 } from 'lucide-react';
import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2, Undo2, Pencil } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -388,6 +388,10 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
</Button>
)}
<Button variant="outline" onClick={() => router.push(`/production/work-orders/${orderId}/edit`)}>
<Pencil className="w-4 h-4 mr-1.5" />
</Button>
<Button variant="outline" onClick={() => setIsWorkLogOpen(true)}>
<FileText className="w-4 h-4 mr-1.5" />
@@ -442,6 +446,18 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
: order.assignee}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">
{order.priority === 1 ? '1 (긴급)' : order.priority === 5 ? '5 (일반)' : order.priority || '-'}
</p>
</div>
{order.note && (
<div className="col-span-4 mt-2 pt-4 border-t">
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium whitespace-pre-wrap">{order.note}</p>
</div>
)}
</div>
</div>

View File

@@ -449,20 +449,21 @@ function transformBendingDetail(api: WorkOrderBendingDetailApi): BendingDetail[]
}
// 상태 → 단계 변환
// 작업 진행 표시용: 실제 작업이 시작되기 전에는 -1 (아무것도 완료/진행 없음)
function getStatusStep(status: WorkOrderStatus): number {
const stepMap: Record<WorkOrderStatus, number> = {
unassigned: 0,
pending: 1,
waiting: 2,
in_progress: 3,
completed: 4,
shipped: 5,
unassigned: -1, // 미배정: 아무 단계도 시작 안함
pending: -1, // 대기: 배정됨, 작업 미시작
waiting: 0, // 준비중: 첫 단계 진행 중
in_progress: 0, // 작업중: 첫 단계 진행 중 (TODO: 실제 단계 추적 필요)
completed: 999, // 완료: 모든 단계 완료
shipped: 999, // 출하: 모든 단계 완료
};
return stepMap[status] || 0;
return stepMap[status] ?? -1;
}
// Frontend → API 변환 (등록/수정용)
export function transformFrontendToApi(data: Partial<WorkOrder> & { processId?: number }): Record<string, unknown> {
export function transformFrontendToApi(data: Partial<WorkOrder> & { processId?: number; assigneeIds?: number[] }): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (data.projectName !== undefined) result.project_name = data.projectName;
@@ -471,6 +472,8 @@ export function transformFrontendToApi(data: Partial<WorkOrder> & { processId?:
if (data.scheduledDate !== undefined) result.scheduled_date = data.scheduledDate;
if (data.dueDate !== undefined && data.scheduledDate === undefined) result.scheduled_date = data.dueDate;
if (data.note !== undefined) result.memo = data.note;
if (data.priority !== undefined) result.priority = data.priority;
if (data.assigneeIds !== undefined) result.assignee_ids = data.assigneeIds;
// items 변환
if (data.items !== undefined) {