feat: POP 작업상세 체크리스트 UI를 judgment_criteria 기반으로 매핑
- 백엔드: /api/pop/production/checklist-items/:processId 신규 추가
process_work_result ⟕ inspection_standard(judgment_criteria) LEFT JOIN으로
한 번에 반환 (inspection_code 미설정 시 null로 안전)
- 프론트: resolveInputType + normalizeByJudgmentCriteria + normalizeByDetailType
1순위: judgment_criteria (CAT_JC_01~04) → inspect/numeric_range/ox/select/text
2순위: detail_type 폴백
- checklist/equip_inspection → check(토글)
- inspection/equip_condition/production_result → inspect(숫자 키패드)
- lookup → input(텍스트)
- material_input → material(BOM 자재)
기존 canonical 값은 건드리지 않아 회귀 없음
- fetchData는 전용 API 사용 + 실패 시 기존 dataApi.getTableData로 폴백
This commit is contained in:
@@ -2969,3 +2969,84 @@ export const getMaterialInputs = async (req: AuthenticatedRequest, res: Response
|
|||||||
return res.status(500).json({ success: false, message: error.message });
|
return res.status(500).json({ success: false, message: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 체크리스트 조회 (judgment_criteria 조인 포함)
|
||||||
|
*
|
||||||
|
* process_work_result를 조회하면서 inspection_standard.judgment_criteria를
|
||||||
|
* LEFT JOIN으로 같이 반환한다.
|
||||||
|
*
|
||||||
|
* UI는 프론트의 resolveInputType()에서
|
||||||
|
* 1순위: judgment_criteria (CAT_JC_01~04)
|
||||||
|
* 2순위: detail_type 폴백
|
||||||
|
* 으로 입력 UI를 결정한다.
|
||||||
|
*/
|
||||||
|
export const getChecklistItems = async (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const pool = getPool();
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { processId } = req.params;
|
||||||
|
if (!processId) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, message: "processId 필수" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT
|
||||||
|
pwr.id,
|
||||||
|
pwr.company_code,
|
||||||
|
pwr.work_order_process_id,
|
||||||
|
pwr.source_work_item_id,
|
||||||
|
pwr.source_detail_id,
|
||||||
|
pwr.work_phase,
|
||||||
|
pwr.item_title,
|
||||||
|
pwr.item_sort_order,
|
||||||
|
pwr.detail_content,
|
||||||
|
pwr.detail_type,
|
||||||
|
pwr.detail_sort_order,
|
||||||
|
pwr.is_required,
|
||||||
|
pwr.inspection_code,
|
||||||
|
pwr.inspection_method,
|
||||||
|
pwr.unit,
|
||||||
|
pwr.lower_limit,
|
||||||
|
pwr.upper_limit,
|
||||||
|
pwr.input_type,
|
||||||
|
pwr.lookup_target,
|
||||||
|
pwr.display_fields,
|
||||||
|
pwr.duration_minutes,
|
||||||
|
pwr.status,
|
||||||
|
pwr.result_value,
|
||||||
|
pwr.is_passed,
|
||||||
|
pwr.remark,
|
||||||
|
pwr.recorded_by,
|
||||||
|
pwr.recorded_at,
|
||||||
|
pwr.started_at,
|
||||||
|
pwr.group_started_at,
|
||||||
|
pwr.group_paused_at,
|
||||||
|
pwr.group_total_paused_time,
|
||||||
|
pwr.group_completed_at,
|
||||||
|
ist.judgment_criteria
|
||||||
|
FROM process_work_result pwr
|
||||||
|
LEFT JOIN inspection_standard ist
|
||||||
|
ON pwr.inspection_code = ist.inspection_code
|
||||||
|
AND pwr.company_code = ist.company_code
|
||||||
|
WHERE pwr.work_order_process_id = $1
|
||||||
|
AND pwr.company_code = $2
|
||||||
|
ORDER BY
|
||||||
|
COALESCE(NULLIF(pwr.item_sort_order, '')::int, 0),
|
||||||
|
COALESCE(NULLIF(pwr.detail_sort_order, '')::int, 0)`,
|
||||||
|
[processId, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({ success: true, data: result.rows });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("[pop/production] checklist-items 조회 오류:", error);
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
getBomMaterials,
|
getBomMaterials,
|
||||||
saveMaterialInput,
|
saveMaterialInput,
|
||||||
getMaterialInputs,
|
getMaterialInputs,
|
||||||
|
getChecklistItems,
|
||||||
} from "../controllers/popProductionController";
|
} from "../controllers/popProductionController";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -49,5 +50,6 @@ router.get("/rework-history/:woId", getReworkHistory);
|
|||||||
router.get("/bom-materials/:processId", getBomMaterials);
|
router.get("/bom-materials/:processId", getBomMaterials);
|
||||||
router.post("/material-input", saveMaterialInput);
|
router.post("/material-input", saveMaterialInput);
|
||||||
router.get("/material-inputs/:processId", getMaterialInputs);
|
router.get("/material-inputs/:processId", getMaterialInputs);
|
||||||
|
router.get("/checklist-items/:processId", getChecklistItems);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -56,6 +56,141 @@ interface WorkResultRow {
|
|||||||
group_completed_at: string | null;
|
group_completed_at: string | null;
|
||||||
inspection_method: string | null;
|
inspection_method: string | null;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
|
inspection_code?: string | null;
|
||||||
|
judgment_criteria?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 입력 UI 타입 해석
|
||||||
|
// ========================================
|
||||||
|
//
|
||||||
|
// 1순위: inspection_standard.judgment_criteria (CAT_JC_01~04)
|
||||||
|
// - CAT_JC_01: 수치(범위) → "number" (숫자 키패드)
|
||||||
|
// - CAT_JC_02: 텍스트입력 → "text" (텍스트 입력)
|
||||||
|
// - CAT_JC_03: O/X → "checkbox" (큰 토글 버튼)
|
||||||
|
// - CAT_JC_04: 선택형 → "select" (선택 버튼)
|
||||||
|
//
|
||||||
|
// 2순위: detail_type 폴백 (inspection_code가 없거나 judgment_criteria가 비어있을 때)
|
||||||
|
type ResolvedInputType = "number" | "text" | "checkbox" | "select";
|
||||||
|
|
||||||
|
function resolveInputType(item: {
|
||||||
|
judgment_criteria?: string | null;
|
||||||
|
detail_type?: string | null;
|
||||||
|
input_type?: string | null;
|
||||||
|
}): ResolvedInputType {
|
||||||
|
// 1순위: judgment_criteria
|
||||||
|
if (item.judgment_criteria) {
|
||||||
|
switch (item.judgment_criteria) {
|
||||||
|
case "CAT_JC_01":
|
||||||
|
return "number";
|
||||||
|
case "CAT_JC_02":
|
||||||
|
return "text";
|
||||||
|
case "CAT_JC_03":
|
||||||
|
return "checkbox";
|
||||||
|
case "CAT_JC_04":
|
||||||
|
return "select";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2순위: detail_type 폴백
|
||||||
|
switch (item.detail_type) {
|
||||||
|
case "checklist":
|
||||||
|
case "check":
|
||||||
|
case "procedure":
|
||||||
|
case "equip_inspection":
|
||||||
|
return "checkbox";
|
||||||
|
case "inspection":
|
||||||
|
case "equip_condition":
|
||||||
|
case "production_result":
|
||||||
|
return "number";
|
||||||
|
case "input":
|
||||||
|
return "text";
|
||||||
|
case "lookup":
|
||||||
|
return "text";
|
||||||
|
default:
|
||||||
|
// 마지막 보루: 기존 input_type 값 유지 (number/checkbox가 들어있을 수 있음)
|
||||||
|
if (item.input_type === "number") return "number";
|
||||||
|
if (item.input_type === "checkbox") return "checkbox";
|
||||||
|
if (item.input_type === "select") return "select";
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* judgment_criteria가 있는 경우 item을 "inspect" 라우터가 올바르게
|
||||||
|
* 분기하도록 detail_type/input_type을 정규화한다.
|
||||||
|
*
|
||||||
|
* CAT_JC_01(수치) → detail_type=inspect, input_type=numeric_range
|
||||||
|
* CAT_JC_02(텍스트) → detail_type=inspect, input_type=text
|
||||||
|
* CAT_JC_03(O/X) → detail_type=inspect, input_type=ox
|
||||||
|
* CAT_JC_04(선택형) → detail_type=inspect, input_type=select
|
||||||
|
*
|
||||||
|
* judgment_criteria가 없으면 원본을 그대로 반환(detail_type 폴백).
|
||||||
|
*/
|
||||||
|
function normalizeByJudgmentCriteria(item: WorkResultRow): WorkResultRow {
|
||||||
|
if (!item.judgment_criteria) return item;
|
||||||
|
let inputType: string | null = null;
|
||||||
|
switch (item.judgment_criteria) {
|
||||||
|
case "CAT_JC_01":
|
||||||
|
inputType = "numeric_range";
|
||||||
|
break;
|
||||||
|
case "CAT_JC_02":
|
||||||
|
inputType = "text";
|
||||||
|
break;
|
||||||
|
case "CAT_JC_03":
|
||||||
|
inputType = "ox";
|
||||||
|
break;
|
||||||
|
case "CAT_JC_04":
|
||||||
|
inputType = "select";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return { ...item, detail_type: "inspect", input_type: inputType };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* detail_type 폴백 정규화.
|
||||||
|
* DB에 실제로 들어있는 원본 detail_type 값(`checklist`, `equip_inspection`,
|
||||||
|
* `inspection`, `production_result`, `lookup`, `material_input` 등)을
|
||||||
|
* 기존 라우터가 알고 있는 canonical 값(`check`, `inspect`, `input`, `material`)으로
|
||||||
|
* 매핑한다.
|
||||||
|
*
|
||||||
|
* 이 단계는 judgment_criteria 정규화가 일어나지 않은 경우에만 영향을 준다.
|
||||||
|
*/
|
||||||
|
function normalizeByDetailType(item: WorkResultRow): WorkResultRow {
|
||||||
|
const dt = item.detail_type ?? "";
|
||||||
|
// 이미 canonical 타입 or inspect_* 레거시이면 그대로 둔다
|
||||||
|
if (
|
||||||
|
dt === "check" ||
|
||||||
|
dt === "input" ||
|
||||||
|
dt === "procedure" ||
|
||||||
|
dt === "material" ||
|
||||||
|
dt === "result" ||
|
||||||
|
dt === "info" ||
|
||||||
|
dt === "" ||
|
||||||
|
dt.startsWith("inspect")
|
||||||
|
) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
switch (dt) {
|
||||||
|
// 토글(확인/미확인) — checklist/장비점검/절차류
|
||||||
|
case "checklist":
|
||||||
|
case "equip_inspection":
|
||||||
|
return { ...item, detail_type: "check" };
|
||||||
|
// 숫자 키패드 — 검사/장비상태/생산실적
|
||||||
|
case "inspection":
|
||||||
|
case "equip_condition":
|
||||||
|
case "production_result":
|
||||||
|
return { ...item, detail_type: "inspect", input_type: item.input_type || "numeric_range" };
|
||||||
|
// 텍스트 — 자유입력/조회
|
||||||
|
case "lookup":
|
||||||
|
return { ...item, detail_type: "input", input_type: item.input_type || "text" };
|
||||||
|
// 자재 투입 — BOM 자재 목록
|
||||||
|
case "material_input":
|
||||||
|
return { ...item, detail_type: "material" };
|
||||||
|
default:
|
||||||
|
return item;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupTimerState {
|
interface GroupTimerState {
|
||||||
@@ -537,16 +672,24 @@ export function PopWorkDetailComponent({
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [resultRes, processRes] = await Promise.all([
|
const [resultRes, processRes] = await Promise.all([
|
||||||
dataApi.getTableData("process_work_result", {
|
// POP 전용 API: inspection_standard.judgment_criteria 조인 포함
|
||||||
size: 500,
|
apiClient
|
||||||
filters: { work_order_process_id: workOrderProcessId },
|
.get(`/pop/production/checklist-items/${workOrderProcessId}`)
|
||||||
}),
|
.then((res) => ({ data: (res.data?.data ?? []) as WorkResultRow[] }))
|
||||||
|
.catch(async () => {
|
||||||
|
// 폴백: 전용 API가 없는 환경에서는 기존 일반 조회 사용
|
||||||
|
const fb = await dataApi.getTableData("process_work_result", {
|
||||||
|
size: 500,
|
||||||
|
filters: { work_order_process_id: workOrderProcessId },
|
||||||
|
});
|
||||||
|
return { data: (fb.data ?? []) as unknown as WorkResultRow[] };
|
||||||
|
}),
|
||||||
dataApi.getTableData("work_order_process", {
|
dataApi.getTableData("work_order_process", {
|
||||||
size: 1,
|
size: 1,
|
||||||
filters: { id: workOrderProcessId },
|
filters: { id: workOrderProcessId },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
setAllResults((resultRes.data ?? []) as unknown as WorkResultRow[]);
|
setAllResults(resultRes.data);
|
||||||
const proc = (processRes.data?.[0] ?? null) as ProcessTimerData | null;
|
const proc = (processRes.data?.[0] ?? null) as ProcessTimerData | null;
|
||||||
setProcessData(proc);
|
setProcessData(proc);
|
||||||
if (proc) {
|
if (proc) {
|
||||||
@@ -3074,8 +3217,11 @@ interface ChecklistItemProps {
|
|||||||
onSave: (rowId: string, resultValue: string, isPassed: string | null, newStatus: string) => void;
|
onSave: (rowId: string, resultValue: string, isPassed: string | null, newStatus: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChecklistItem({ item, saving, disabled, onSave }: ChecklistItemProps) {
|
function ChecklistItem({ item: rawItem, saving, disabled, onSave }: ChecklistItemProps) {
|
||||||
const isDisabled = disabled || saving;
|
const isDisabled = disabled || saving;
|
||||||
|
// 1순위: judgment_criteria 기반 정규화 (inspection_standard 조인 결과)
|
||||||
|
// 2순위: detail_type 폴백 정규화 (원본 DB 값 → canonical 라우터 값)
|
||||||
|
const item = normalizeByDetailType(normalizeByJudgmentCriteria(rawItem));
|
||||||
const dt = item.detail_type ?? "";
|
const dt = item.detail_type ?? "";
|
||||||
|
|
||||||
// "inspect_numeric" 등 레거시 형식 → "inspect"로 정규화, 접미사를 input_type 폴백으로 사용
|
// "inspect_numeric" 등 레거시 형식 → "inspect"로 정규화, 접미사를 input_type 폴백으로 사용
|
||||||
@@ -3119,8 +3265,10 @@ function ChecklistItem({ item, saving, disabled, onSave }: ChecklistItemProps) {
|
|||||||
// 체크리스트 입력부 (우측 영역 전용 - 분할 레이아웃)
|
// 체크리스트 입력부 (우측 영역 전용 - 분할 레이아웃)
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
function ChecklistItemInput({ item, saving, disabled, onSave }: ChecklistItemProps) {
|
function ChecklistItemInput({ item: rawItem, saving, disabled, onSave }: ChecklistItemProps) {
|
||||||
const isDisabled = disabled || saving;
|
const isDisabled = disabled || saving;
|
||||||
|
// 1순위: judgment_criteria, 2순위: detail_type 폴백
|
||||||
|
const item = normalizeByDetailType(normalizeByJudgmentCriteria(rawItem));
|
||||||
const dt = item.detail_type ?? "";
|
const dt = item.detail_type ?? "";
|
||||||
|
|
||||||
if (dt.startsWith("inspect")) {
|
if (dt.startsWith("inspect")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user