feat: BLOCK DETAIL Phase 4 + 안정화 - 그룹별 타이머, 터치 최적화 UI, DB 저장 버그 수정
pop-work-detail 컴포넌트에 그룹별 타이머 시스템과 터치 최적화 UI를 추가하고, 체크리스트 결과가 DB에 저장되지 않던 버그를 수정하여 안정화를 완료한다. [그룹별 타이머] - group-timer API 신규: start/pause/resume/complete 액션 (popProductionController) - process_work_result에 group_started_at/paused_at/total_paused_time/completed_at 활용 - GroupTimerHeader UI: 순수 작업시간 + 경과시간 이중 표시 - 첫 그룹 "시작" 시 work_order_process.started_at 자동 기록 (공정 시작 자동 감지) - 공정 완료 시 actual_work_time을 그룹 타이머 합산으로 백엔드 자동 계산 [터치 최적화 UI] - 12개 영역 전면 스케일업: 버튼 h-11~h-12, 입력 h-11, 체크박스 h-6 w-6 - 사이드바 w-[180px], InfoBar text-sm, 최소 터치 영역 40~44px 확보 - 산업 현장 태블릿 터치 사용 최적화 [DB 저장 버그 수정] - saveResultValue/handleQuantityRegister: execute-action task 형식 수정 (fixedValue + lookupMode:"manual" + manualItemField/manualPkColumn:"id") - 원인: 백엔드가 __cart_row_key를 찾는데 프론트에서 id만 전송하여 lookup 실패 [디자이너 설정 확장] - displayMode: list/step 전환 설정 추가 - PopWorkDetailConfig: 표시 모드 Select 드롭다운 - types.ts: PopWorkDetailConfig 인터페이스 displayMode 추가 - PopCardListV2Component: parentRow.__processFlow__ 전달 보강
This commit is contained in:
@@ -206,10 +206,11 @@ export const controlTimer = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (!["start", "pause", "resume"].includes(action)) {
|
||||
if (!["start", "pause", "resume", "complete"].includes(action)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "action은 start, pause, resume 중 하나여야 합니다.",
|
||||
message:
|
||||
"action은 start, pause, resume, complete 중 하나여야 합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,6 +263,47 @@ export const controlTimer = async (
|
||||
[work_order_process_id, companyCode]
|
||||
);
|
||||
break;
|
||||
|
||||
case "complete": {
|
||||
const { good_qty, defect_qty } = req.body;
|
||||
|
||||
const groupSumResult = await pool.query(
|
||||
`SELECT COALESCE(SUM(
|
||||
CASE WHEN group_started_at IS NOT NULL AND group_completed_at IS NOT NULL THEN
|
||||
EXTRACT(EPOCH FROM group_completed_at::timestamp - group_started_at::timestamp)::int
|
||||
- COALESCE(group_total_paused_time::int, 0)
|
||||
ELSE 0 END
|
||||
), 0)::text AS total_work_seconds
|
||||
FROM process_work_result
|
||||
WHERE work_order_process_id = $1 AND company_code = $2`,
|
||||
[work_order_process_id, companyCode]
|
||||
);
|
||||
const calculatedWorkTime = groupSumResult.rows[0]?.total_work_seconds || "0";
|
||||
|
||||
result = await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET status = 'completed',
|
||||
completed_at = NOW()::text,
|
||||
completed_by = $3,
|
||||
actual_work_time = $4,
|
||||
good_qty = COALESCE($5, good_qty),
|
||||
defect_qty = COALESCE($6, defect_qty),
|
||||
paused_at = NULL,
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2
|
||||
AND status != 'completed'
|
||||
RETURNING id, status, completed_at, completed_by, actual_work_time, good_qty, defect_qty`,
|
||||
[
|
||||
work_order_process_id,
|
||||
companyCode,
|
||||
userId,
|
||||
calculatedWorkTime,
|
||||
good_qty || null,
|
||||
defect_qty || null,
|
||||
]
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result || result.rowCount === 0) {
|
||||
@@ -289,3 +331,137 @@ export const controlTimer = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 그룹(작업항목)별 타이머 제어
|
||||
* 좌측 사이드바의 각 작업 그룹마다 독립적인 시작/정지/재개/완료 타이머
|
||||
*/
|
||||
export const controlGroupTimer = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { work_order_process_id, source_work_item_id, action } = req.body;
|
||||
|
||||
if (!work_order_process_id || !source_work_item_id || !action) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
"work_order_process_id, source_work_item_id, action은 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!["start", "pause", "resume", "complete"].includes(action)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
"action은 start, pause, resume, complete 중 하나여야 합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("[pop/production] group-timer 요청", {
|
||||
companyCode,
|
||||
work_order_process_id,
|
||||
source_work_item_id,
|
||||
action,
|
||||
});
|
||||
|
||||
const whereClause = `work_order_process_id = $1 AND source_work_item_id = $2 AND company_code = $3`;
|
||||
const baseParams = [work_order_process_id, source_work_item_id, companyCode];
|
||||
|
||||
let result;
|
||||
|
||||
switch (action) {
|
||||
case "start":
|
||||
result = await pool.query(
|
||||
`UPDATE process_work_result
|
||||
SET group_started_at = CASE WHEN group_started_at IS NULL THEN NOW()::text ELSE group_started_at END,
|
||||
updated_date = NOW()
|
||||
WHERE ${whereClause}
|
||||
RETURNING id, group_started_at`,
|
||||
baseParams
|
||||
);
|
||||
await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET started_at = NOW()::text, updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2 AND started_at IS NULL`,
|
||||
[work_order_process_id, companyCode]
|
||||
);
|
||||
break;
|
||||
|
||||
case "pause":
|
||||
result = await pool.query(
|
||||
`UPDATE process_work_result
|
||||
SET group_paused_at = NOW()::text,
|
||||
updated_date = NOW()
|
||||
WHERE ${whereClause} AND group_paused_at IS NULL
|
||||
RETURNING id, group_paused_at`,
|
||||
baseParams
|
||||
);
|
||||
break;
|
||||
|
||||
case "resume":
|
||||
result = await pool.query(
|
||||
`UPDATE process_work_result
|
||||
SET group_total_paused_time = (
|
||||
COALESCE(group_total_paused_time::int, 0)
|
||||
+ EXTRACT(EPOCH FROM NOW() - group_paused_at::timestamp)::int
|
||||
)::text,
|
||||
group_paused_at = NULL,
|
||||
updated_date = NOW()
|
||||
WHERE ${whereClause} AND group_paused_at IS NOT NULL
|
||||
RETURNING id, group_total_paused_time`,
|
||||
baseParams
|
||||
);
|
||||
break;
|
||||
|
||||
case "complete": {
|
||||
result = await pool.query(
|
||||
`UPDATE process_work_result
|
||||
SET group_completed_at = NOW()::text,
|
||||
group_total_paused_time = CASE
|
||||
WHEN group_paused_at IS NOT NULL THEN (
|
||||
COALESCE(group_total_paused_time::int, 0)
|
||||
+ EXTRACT(EPOCH FROM NOW() - group_paused_at::timestamp)::int
|
||||
)::text
|
||||
ELSE group_total_paused_time
|
||||
END,
|
||||
group_paused_at = NULL,
|
||||
updated_date = NOW()
|
||||
WHERE ${whereClause}
|
||||
RETURNING id, group_started_at, group_completed_at, group_total_paused_time`,
|
||||
baseParams
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result || result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "대상 그룹을 찾을 수 없거나 현재 상태에서 수행할 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("[pop/production] group-timer 완료", {
|
||||
action,
|
||||
source_work_item_id,
|
||||
affectedRows: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
affectedRows: result.rowCount,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("[pop/production] group-timer 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "그룹 타이머 처리 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import {
|
||||
createWorkProcesses,
|
||||
controlTimer,
|
||||
controlGroupTimer,
|
||||
} from "../controllers/popProductionController";
|
||||
|
||||
const router = Router();
|
||||
@@ -11,5 +12,6 @@ router.use(authenticateToken);
|
||||
|
||||
router.post("/create-work-processes", createWorkProcesses);
|
||||
router.post("/timer", controlTimer);
|
||||
router.post("/group-timer", controlGroupTimer);
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user