feat(WEB): 작업자 화면 부서/담당자/생산일자 API 연동 및 사원관리 날짜필터 개선

- 작업자 화면 작업정보 부서: 하드코딩 → 테넌트 부서 목록 동적 로드 (getDepartments API)
- 작업자 화면 작업정보 생산담당자: 선택된 부서별 사용자 목록 연동 (getDepartmentUsers API)
- 작업자 화면 작업정보 생산일자: scheduled_date 필드 조회 연동
- 작업지시 선택 시 공정 담당부서(process.department) 기반 부서 자동 세팅
- 사이드바 자동 선택 시 API 작업지시 우선 선택 (목업보다 우선)
- 사원관리 초기 진입 시 날짜 필터 기본값 제거 (전체 기간 조회)
This commit is contained in:
2026-02-13 17:14:42 +09:00
parent 7f39f3066f
commit 680fe057e7
4 changed files with 141 additions and 19 deletions

View File

@@ -77,9 +77,9 @@ export function EmployeeManagement() {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20;
// 날짜 범위 상태 (Input type="date" 용)
const [startDate, setStartDate] = useState('2025-12-01');
const [endDate, setEndDate] = useState('2025-12-31');
// 날짜 범위 상태 (Input type="date" 용) - 초기값 비움: 전체 기간 조회
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
// 필터 및 정렬 상태
const [filterOption, setFilterOption] = useState<FilterOption>('all');

View File

@@ -30,6 +30,10 @@ export interface WorkOrder {
delayDays?: number; // 지연 일수
instruction?: string; // 지시사항
salesOrderNo?: string; // 수주번호
teamId?: number | null; // 배정 부서 ID (work_orders.team_id)
teamName?: string; // 배정 부서명
processDepartment?: string; // 공정 담당부서명 (processes.department)
scheduledDate?: string; // 생산 예정일 (YYYY-MM-DD)
createdAt: string;
// 공정 설정 (작업자 화면용)
processOptions?: {

View File

@@ -9,6 +9,7 @@
import { executeServerAction } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
import type { WorkItemData, WorkStepData, ProcessTab } from './types';
@@ -40,7 +41,10 @@ interface WorkOrderApiItem {
client?: { id: number; name: string };
root_nodes_count?: number;
};
team_id?: number | null;
team?: { id: number; name: string } | null;
assignee?: { id: number; name: string };
assignees?: { id: number; user_id: number; user?: { id: number; name: string } }[];
items?: {
id: number;
item_name: string;
@@ -172,7 +176,9 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
processName: processInfo.name,
client: api.sales_order?.client?.name || '-',
projectName: api.project_name || '-',
assignees: api.assignee ? [api.assignee.name] : [],
assignees: api.assignees?.length
? api.assignees.map((a) => a.user?.name || '').filter(Boolean)
: api.assignee ? [api.assignee.name] : [],
quantity: totalQuantity,
shutterCount: nodeGroups.length || api.sales_order?.root_nodes_count || 0,
dueDate,
@@ -183,6 +189,10 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
delayDays,
instruction: api.memo || undefined,
salesOrderNo: api.sales_order?.order_no || undefined,
teamId: api.team_id ?? null,
teamName: api.team?.name || undefined,
processDepartment: api.process?.department || undefined,
scheduledDate: api.scheduled_date || undefined,
createdAt: api.created_at,
processOptions: {
needsInspection: api.process?.options?.needs_inspection ?? false,
@@ -782,4 +792,52 @@ export async function saveInspectionDocument(
errorMessage: '검사 문서 동기화에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 부서 목록 조회 (작업 정보용) =====
export interface DepartmentOption {
id: number;
name: string;
}
export async function getDepartments(): Promise<{
success: boolean;
data: DepartmentOption[];
error?: string;
}> {
interface DeptApiItem { id: number; name: string; parent_id?: number | null; [key: string]: unknown }
const result = await executeServerAction<{ data: DeptApiItem[] } | DeptApiItem[]>({
url: buildApiUrl('/api/v1/departments', { per_page: 100 }),
errorMessage: '부서 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
const list = Array.isArray(result.data) ? result.data : (result.data.data || []);
return {
success: true,
data: list.map((d) => ({ id: d.id, name: d.name })),
};
}
// ===== 부서별 사용자 목록 조회 =====
export interface DepartmentUser {
id: number;
name: string;
}
export async function getDepartmentUsers(departmentId: number): Promise<{
success: boolean;
data: DepartmentUser[];
error?: string;
}> {
interface UserApiItem { id: number; name: string; [key: string]: unknown }
const result = await executeServerAction<{ data: UserApiItem[] } | UserApiItem[]>({
url: buildApiUrl(`/api/v1/departments/${departmentId}/users`),
errorMessage: '부서 사용자 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
const list = Array.isArray(result.data) ? result.data : (result.data.data || []);
return {
success: true,
data: list.map((u) => ({ id: u.id, name: u.name })),
};
}

View File

@@ -40,8 +40,8 @@ import { Button } from '@/components/ui/button';
import { PageLayout } from '@/components/organisms/PageLayout';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate, getStepProgress, toggleStepProgress, deleteMaterialInput, updateMaterialInput } from './actions';
import type { StepProgressItem } from './actions';
import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate, getStepProgress, toggleStepProgress, deleteMaterialInput, updateMaterialInput, getDepartments, getDepartmentUsers } from './actions';
import type { StepProgressItem, DepartmentOption, DepartmentUser } from './actions';
import type { InspectionTemplateData } from './types';
import { getProcessList } from '@/components/process-management/actions';
import type { InspectionSetting, Process } from '@/types/process';
@@ -320,6 +320,8 @@ export default function WorkerScreen() {
const [departmentId, setDepartmentId] = useState('');
const [productionManagerId, setProductionManagerId] = useState('');
const [productionDate, setProductionDate] = useState('');
const [departmentList, setDepartmentList] = useState<DepartmentOption[]>([]);
const [departmentUsers, setDepartmentUsers] = useState<DepartmentUser[]>([]);
// 좌측 사이드바
const [selectedSidebarOrderId, setSelectedSidebarOrderId] = useState<string>('');
@@ -352,8 +354,29 @@ export default function WorkerScreen() {
useEffect(() => {
loadData();
// 부서 목록 로드
getDepartments().then((res) => {
if (res.success) setDepartmentList(res.data);
});
}, [loadData]);
// 부서 선택 시 해당 부서 사용자 목록 로드
useEffect(() => {
if (!departmentId) {
setDepartmentUsers([]);
setProductionManagerId('');
return;
}
getDepartmentUsers(Number(departmentId)).then((res) => {
if (res.success) {
setDepartmentUsers(res.data);
} else {
setDepartmentUsers([]);
}
setProductionManagerId('');
});
}, [departmentId]);
// PC에서 사이드바 sticky 동작을 위해 main의 overflow 임시 해제
useEffect(() => {
const mainEl = document.querySelector('main');
@@ -531,12 +554,24 @@ export default function WorkerScreen() {
return;
}
// 우선순위 순서: urgent → priority → normal
// API 작업지시 우선 선택 (부서/담당자 연동을 위해)
if (apiSidebarOrders.length > 0) {
const firstApi = apiSidebarOrders[0];
setSelectedSidebarOrderId(firstApi.id);
if (activeProcessTabKey === 'slat') {
setSlatSubMode(firstApi.subType === 'jointbar' ? 'jointbar' : 'normal');
}
if (activeProcessTabKey === 'bending') {
setBendingSubMode(firstApi.subType === 'wip' ? 'wip' : 'normal');
}
return;
}
// API 작업지시 없으면 목업에서 우선순위 순서: urgent → priority → normal
for (const group of PRIORITY_GROUPS) {
const first = allOrders.find((o) => o.priority === group.key);
if (first) {
setSelectedSidebarOrderId(first.id);
// subType에 따라 서브모드도 설정
if (activeProcessTabKey === 'slat') {
setSlatSubMode(first.subType === 'jointbar' ? 'jointbar' : 'normal');
}
@@ -796,6 +831,29 @@ export default function WorkerScreen() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSidebarOrderId, workItems.length]);
// ===== 작업지시 변경 시 작업 정보 자동 세팅 =====
useEffect(() => {
const apiOrder = filteredWorkOrders.find((wo) => wo.id === selectedSidebarOrderId);
if (apiOrder) {
// 부서 세팅: 1순위 work_orders.team_id → 2순위 process.department(부서명 매칭)
if (apiOrder.teamId) {
setDepartmentId(String(apiOrder.teamId));
} else if (apiOrder.processDepartment && departmentList.length > 0) {
const matched = departmentList.find((d) => d.name === apiOrder.processDepartment);
setDepartmentId(matched ? String(matched.id) : '');
} else {
setDepartmentId('');
}
// 생산일자 세팅
setProductionDate(apiOrder.scheduledDate || '');
} else {
setDepartmentId('');
setProductionManagerId('');
setProductionDate('');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSidebarOrderId, filteredWorkOrders, departmentList]);
// ===== 수주 정보 (사이드바 선택 항목 기반) =====
const orderInfo = useMemo(() => {
// 1. 선택된 API 작업지시에서 찾기
@@ -1427,7 +1485,7 @@ export default function WorkerScreen() {
</CardContent>
</Card>
{/* 작업 정보 - 부서 필드 추가 */}
{/* 작업 정보 - API 연동 */}
<Card>
<CardContent className="p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3"> </h3>
@@ -1435,6 +1493,7 @@ export default function WorkerScreen() {
<div className="space-y-1.5">
<Label className="text-sm text-gray-600"></Label>
<Select
key={`dept-${departmentId}`}
value={departmentId}
onValueChange={setDepartmentId}
>
@@ -1442,28 +1501,29 @@ export default function WorkerScreen() {
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="production"></SelectItem>
<SelectItem value="quality"></SelectItem>
{departmentList.map((dept) => (
<SelectItem key={dept.id} value={String(dept.id)}>
{dept.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-sm text-gray-600"> </Label>
<Select
key={`manager-${departmentId}-${productionManagerId}`}
value={productionManagerId}
onValueChange={setProductionManagerId}
disabled={!departmentId}
>
<SelectTrigger>
<SelectValue placeholder="선택" />
<SelectValue placeholder={departmentId ? '선택' : '부서를 먼저 선택'} />
</SelectTrigger>
<SelectContent>
{Array.from(
new Set(
filteredWorkOrders.flatMap((o) => o.assignees || []).filter(Boolean)
)
).map((name) => (
<SelectItem key={name} value={name}>
{name}
{departmentUsers.map((user) => (
<SelectItem key={user.id} value={String(user.id)}>
{user.name}
</SelectItem>
))}
</SelectContent>