fix(WEB): 작업일지 담당자 정보 및 슬랫 데이터 파이프라인 연동

- 작업일지(스크린/슬랫/절곡): 담당자→수주담당자, 연락처→담당자연락처, 생산담당자 분리 표시
- SlatWorkLogContent: 방화유리 수량을 slatInfo.glassQty에서 표시
- SlatInfo 타입에 glassQty 추가 (WorkOrders/types, WorkerScreen/types)
- WorkerScreen: salesManager/managerPhone API 연동
- slat_info 변환 로직에 glass_qty 매핑 추가
This commit is contained in:
2026-02-19 20:59:02 +09:00
parent 5b987d057b
commit f695977cbc
9 changed files with 87 additions and 48 deletions

View File

@@ -30,6 +30,8 @@ export interface WorkOrder {
delayDays?: number; // 지연 일수
instruction?: string; // 지시사항
salesOrderNo?: string; // 수주번호
salesManager?: string; // 수주 담당자 (orders.options.manager_name)
managerPhone?: string; // 담당자 연락처 (orders.client_contact)
teamId?: number | null; // 배정 부서 ID (work_orders.team_id)
teamName?: string; // 배정 부서명
processDepartment?: string; // 공정 담당부서명 (processes.department)

View File

@@ -88,13 +88,13 @@ export function BendingWorkLogContent({ data: order }: BendingWorkLogContentProp
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
<td className="border border-gray-400 px-3 py-2">{order.salesOrderWriter || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">-</td>
<td className="border border-gray-400 px-3 py-2">{order.clientContact || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
</tr>

View File

@@ -223,13 +223,13 @@ export function ScreenWorkLogContent({ data: order, materialLots = [] }: ScreenW
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
<td className="border border-gray-400 px-3 py-2">{order.salesOrderWriter || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">-</td>
<td className="border border-gray-400 px-3 py-2">{order.clientContact || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
</tr>

View File

@@ -69,6 +69,26 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
const lotNoList = materialLots.map(lot => lot.lot_no).filter(Boolean);
const lotNoDisplay = lotNoList.length > 0 ? lotNoList.join(', ') : '';
// 슬랫 계산: 매수(세로) = floor(height / 72) + 1
const calcSlatCount = (height?: number) => height ? Math.floor(height / 72) + 1 : 0;
// 코일 사용량 = ((가로 + 4) × 매수) / 1000 + (304 × 3 × 조인트바) / 1000
const calcCoilUsage = (width?: number, height?: number, jointBar?: number) => {
const slatCount = calcSlatCount(height);
const w = width || 0;
const jb = jointBar || 0;
return Math.round(((w + 4) * slatCount / 1000 + (304 * 3 * jb) / 1000) * 10) / 10;
};
// 합계 계산
let totalCoilUsage = 0;
let totalJointBar = 0;
items.forEach(item => {
const jb = item.slatInfo?.jointBar || 0;
totalJointBar += jb;
totalCoilUsage += calcCoilUsage(item.width, item.height, jb);
});
return (
<div className="p-6 bg-white">
{/* ===== 헤더 영역 ===== */}
@@ -130,13 +150,13 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
<td className="border border-gray-400 px-3 py-2">{order.salesOrderWriter || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">-</td>
<td className="border border-gray-400 px-3 py-2">{order.clientContact || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
</tr>
@@ -153,40 +173,46 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
<table className="w-full border-collapse text-xs mb-6">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-7" rowSpan={2}>No.</th>
<th className="border border-gray-400 p-1 w-16" rowSpan={2}> LOT<br/>NO</th>
<th className="border border-gray-400 p-1 w-12" rowSpan={2}><br/></th>
<th className="border border-gray-400 p-1" rowSpan={2}></th>
<th className="border border-gray-400 p-1" colSpan={3}>(mm) - </th>
<th className="border border-gray-400 p-1 w-12" rowSpan={2}><br/></th>
<th className="border border-gray-400 p-1 w-12" rowSpan={2}><br/></th>
<th className="border border-gray-400 p-1 w-14" rowSpan={2}>/<br/></th>
<th className="border border-gray-400 px-2 py-1 w-7" rowSpan={2}>No.</th>
<th className="border border-gray-400 px-2 py-1" rowSpan={2}> LOT<br/>NO</th>
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}><br/></th>
<th className="border border-gray-400 px-2 py-1" rowSpan={2}></th>
<th className="border border-gray-400 px-2 py-1" colSpan={3}>(mm) - </th>
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}><br/></th>
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}><br/></th>
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}>/<br/></th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-14"></th>
<th className="border border-gray-400 p-1 w-14"></th>
<th className="border border-gray-400 p-1 w-12"><br/>()</th>
<th className="border border-gray-400 px-2 py-1"></th>
<th className="border border-gray-400 px-2 py-1"></th>
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap"><br/>()</th>
</tr>
</thead>
<tbody>
{items.length > 0 ? (
items.map((item, idx) => (
<tr key={item.id}>
<td className="border border-gray-400 p-1 text-center">{idx + 1}</td>
<td className="border border-gray-400 p-1 text-center text-[10px]">{lotNoDisplay}</td>
<td className="border border-gray-400 p-1 text-center">-</td>
<td className="border border-gray-400 p-1 text-[10px]">{item.productName}</td>
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{fmt(item.width)}</td>
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{fmt(item.height)}</td>
<td className="border border-gray-400 p-1 text-center">-</td>
<td className="border border-gray-400 p-1 text-center">-</td>
<td className="border border-gray-400 p-1 text-center">-</td>
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{getSymbolCode(item.floorCode)}</td>
</tr>
))
items.map((item, idx) => {
const slatCount = calcSlatCount(item.height);
const jointBar = item.slatInfo?.jointBar || 0;
const glassQty = item.slatInfo?.glassQty || 0;
const coilUsage = calcCoilUsage(item.width, item.height, jointBar);
return (
<tr key={item.id}>
<td className="border border-gray-400 px-2 py-1 text-center">{idx + 1}</td>
<td className="border border-gray-400 px-2 py-1 text-center">{lotNoDisplay}</td>
<td className="border border-gray-400 px-2 py-1 text-center">{glassQty > 0 ? fmt(glassQty) : '-'}</td>
<td className="border border-gray-400 px-2 py-1">{item.productName}</td>
<td className="border border-gray-400 px-2 py-1 text-center whitespace-nowrap">{fmt(item.width)}</td>
<td className="border border-gray-400 px-2 py-1 text-center whitespace-nowrap">{fmt(item.height)}</td>
<td className="border border-gray-400 px-2 py-1 text-center">{slatCount > 0 ? fmt(slatCount) : '-'}</td>
<td className="border border-gray-400 px-2 py-1 text-center">{jointBar > 0 ? fmt(jointBar) : '-'}</td>
<td className="border border-gray-400 px-2 py-1 text-center">{coilUsage > 0 ? coilUsage.toFixed(1) : '-'}</td>
<td className="border border-gray-400 px-2 py-1 text-center whitespace-nowrap">{getSymbolCode(item.floorCode)}</td>
</tr>
);
})
) : (
<tr>
<td colSpan={10} className="border border-gray-400 p-4 text-center text-gray-400">
<td colSpan={10} className="border border-gray-400 px-2 py-4 text-center text-gray-400">
.
</td>
</tr>
@@ -198,10 +224,10 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
<table className="w-full border-collapse text-xs mb-6">
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-36"> [m²]</td>
<td className="border border-gray-400 px-3 py-2"></td>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-36"> </td>
<td className="border border-gray-400 px-3 py-2"></td>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium whitespace-nowrap"> [m²]</td>
<td className="border border-gray-400 px-3 py-2">{totalCoilUsage > 0 ? totalCoilUsage.toFixed(1) : ''}</td>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium whitespace-nowrap"> </td>
<td className="border border-gray-400 px-3 py-2">{totalJointBar > 0 ? fmt(totalJointBar) : ''}</td>
</tr>
</tbody>
</table>

View File

@@ -118,6 +118,7 @@ export interface WorkOrderItem {
unit: string; // 단위
orderNodeId: number | null; // 개소 ID
orderNodeName: string; // 개소명
slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number }; // 슬랫 공정 정보
}
// 전개도 상세 (절곡용)
@@ -347,6 +348,7 @@ export interface WorkOrderApi {
created_at?: string;
quantity?: number;
root_nodes_count?: number;
options?: { manager_name?: string; [key: string]: unknown };
client?: { id: number; name: string };
writer?: { id: number; name: string };
};
@@ -467,6 +469,10 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
unit: item.unit || '-',
orderNodeId: item.source_order_item?.order_node_id ?? null,
orderNodeName: item.source_order_item?.node?.name || '-',
slatInfo: item.options?.slat_info ? (() => {
const si = item.options.slat_info as { length?: number; slat_count?: number; joint_bar?: number; glass_qty?: number };
return { length: si.length || 0, slatCount: si.slat_count || 0, jointBar: si.joint_bar || 0, glassQty: si.glass_qty || 0 };
})() : undefined,
})),
bendingDetails: api.bending_detail ? transformBendingDetail(api.bending_detail) : undefined,
issues: (api.issues || []).map(issue => ({

View File

@@ -39,6 +39,8 @@ interface WorkOrderApiItem {
id: number;
order_no: string;
client?: { id: number; name: string };
client_contact?: string;
options?: { manager_name?: string; [key: string]: unknown };
root_nodes_count?: number;
};
team_id?: number | null;
@@ -189,6 +191,8 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
delayDays,
instruction: api.memo || undefined,
salesOrderNo: api.sales_order?.order_no || undefined,
salesManager: api.sales_order?.options?.manager_name as string || undefined,
managerPhone: api.sales_order?.client_contact || undefined,
teamId: api.team_id ?? null,
teamName: api.team?.name || undefined,
processDepartment: api.process?.department || undefined,
@@ -635,8 +639,8 @@ export async function getWorkOrderDetail(
workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets };
}
if (opts.slat_info) {
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number };
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar };
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number; glass_qty: number };
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar, glassQty: si.glass_qty || 0 };
}
if (opts.bending_info) {
const bi = opts.bending_info as {

View File

@@ -118,7 +118,7 @@ const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
{
id: 'mock-l1', itemNo: 1, itemCode: 'KQTS01', itemName: '슬랫코일', floor: '1층', code: 'FSS-01',
width: 8260, height: 8350, quantity: 2, processType: 'slat',
slatInfo: { length: 3910, slatCount: 40, jointBar: 4 },
slatInfo: { length: 3910, slatCount: 40, jointBar: 4, glassQty: 2 },
steps: [
{ id: 'l1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
{ id: 'l1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
@@ -132,7 +132,7 @@ const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
{
id: 'mock-l2', itemNo: 2, itemCode: 'KQTS03', itemName: '슬랫코일(광폭)', floor: '2층', code: 'FSS-02',
width: 10500, height: 6200, quantity: 3, processType: 'slat',
slatInfo: { length: 5200, slatCount: 55, jointBar: 6 },
slatInfo: { length: 5200, slatCount: 55, jointBar: 6, glassQty: 3 },
steps: [
{ id: 'l2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
{ id: 'l2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
@@ -709,8 +709,8 @@ export default function WorkerScreen() {
workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets };
}
if (opts.slat_info) {
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number };
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar };
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number; glass_qty: number };
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar, glassQty: si.glass_qty || 0 };
}
if (opts.bending_info) {
const bi = opts.bending_info as {
@@ -864,8 +864,8 @@ export default function WorkerScreen() {
salesOrderNo: apiOrder.salesOrderNo || '-',
siteName: apiOrder.projectName || '-',
client: apiOrder.client || '-',
salesManager: apiOrder.assignees?.[0] || '-',
managerPhone: '-',
salesManager: apiOrder.salesManager || '-',
managerPhone: apiOrder.managerPhone || '-',
shippingDate: apiOrder.dueDate ? new Date(apiOrder.dueDate).toLocaleDateString('ko-KR') : '-',
};
}
@@ -892,8 +892,8 @@ export default function WorkerScreen() {
salesOrderNo: first.salesOrderNo || '-',
siteName: first.projectName || '-',
client: first.client || '-',
salesManager: first.assignees?.[0] || '-',
managerPhone: '-',
salesManager: first.salesManager || '-',
managerPhone: first.managerPhone || '-',
shippingDate: first.dueDate ? new Date(first.dueDate).toLocaleDateString('ko-KR') : '-',
};
}, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey]);

View File

@@ -60,7 +60,7 @@ export const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
{
id: 'mock-l1', itemNo: 1, itemCode: 'KQTS01', itemName: '슬랫코일', floor: '1층', code: 'FSS-01',
width: 8260, height: 8350, quantity: 2, processType: 'slat',
slatInfo: { length: 3910, slatCount: 40, jointBar: 4 },
slatInfo: { length: 3910, slatCount: 40, jointBar: 4, glassQty: 2 },
steps: [
{ id: 'l1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
{ id: 'l1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
@@ -73,7 +73,7 @@ export const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
{
id: 'mock-l2', itemNo: 2, itemCode: 'KQTS03', itemName: '슬랫코일(광폭)', floor: '2층', code: 'FSS-02',
width: 10500, height: 6200, quantity: 3, processType: 'slat',
slatInfo: { length: 5200, slatCount: 55, jointBar: 6 },
slatInfo: { length: 5200, slatCount: 55, jointBar: 6, glassQty: 3 },
steps: [
{ id: 'l2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
{ id: 'l2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },

View File

@@ -77,6 +77,7 @@ export interface SlatInfo {
length: number; // 길이 (mm)
slatCount: number; // 슬랫 매수
jointBar: number; // 조인트바 개수
glassQty: number; // 방화유리 수량
}
// ===== 슬랫 조인트바 전용 정보 =====