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 });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 체크리스트 조회 (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,
|
||||
saveMaterialInput,
|
||||
getMaterialInputs,
|
||||
getChecklistItems,
|
||||
} from "../controllers/popProductionController";
|
||||
|
||||
const router = Router();
|
||||
@@ -49,5 +50,6 @@ router.get("/rework-history/:woId", getReworkHistory);
|
||||
router.get("/bom-materials/:processId", getBomMaterials);
|
||||
router.post("/material-input", saveMaterialInput);
|
||||
router.get("/material-inputs/:processId", getMaterialInputs);
|
||||
router.get("/checklist-items/:processId", getChecklistItems);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -56,6 +56,141 @@ interface WorkResultRow {
|
||||
group_completed_at: string | null;
|
||||
inspection_method: 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 {
|
||||
@@ -537,16 +672,24 @@ export function PopWorkDetailComponent({
|
||||
try {
|
||||
setLoading(true);
|
||||
const [resultRes, processRes] = await Promise.all([
|
||||
dataApi.getTableData("process_work_result", {
|
||||
size: 500,
|
||||
filters: { work_order_process_id: workOrderProcessId },
|
||||
}),
|
||||
// POP 전용 API: inspection_standard.judgment_criteria 조인 포함
|
||||
apiClient
|
||||
.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", {
|
||||
size: 1,
|
||||
filters: { id: workOrderProcessId },
|
||||
}),
|
||||
]);
|
||||
setAllResults((resultRes.data ?? []) as unknown as WorkResultRow[]);
|
||||
setAllResults(resultRes.data);
|
||||
const proc = (processRes.data?.[0] ?? null) as ProcessTimerData | null;
|
||||
setProcessData(proc);
|
||||
if (proc) {
|
||||
@@ -3074,8 +3217,11 @@ interface ChecklistItemProps {
|
||||
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;
|
||||
// 1순위: judgment_criteria 기반 정규화 (inspection_standard 조인 결과)
|
||||
// 2순위: detail_type 폴백 정규화 (원본 DB 값 → canonical 라우터 값)
|
||||
const item = normalizeByDetailType(normalizeByJudgmentCriteria(rawItem));
|
||||
const dt = item.detail_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;
|
||||
// 1순위: judgment_criteria, 2순위: detail_type 폴백
|
||||
const item = normalizeByDetailType(normalizeByJudgmentCriteria(rawItem));
|
||||
const dt = item.detail_type ?? "";
|
||||
|
||||
if (dt.startsWith("inspect")) {
|
||||
|
||||
Reference in New Issue
Block a user