Merge pull request 'jskim-node' (#35) from jskim-node into main
Some checks failed
Build and Push Images / build-and-push (push) Failing after 49s
Some checks failed
Build and Push Images / build-and-push (push) Failing after 49s
Reviewed-on: jskim/vexplor_dev#35
This commit is contained in:
@@ -163,24 +163,29 @@ export async function getMaterialStatus(
|
||||
bomParams.push(companyCode);
|
||||
}
|
||||
|
||||
// inventory_unit은 카테고리 코드(CAT_xxx)로 저장됨 → category_values 조인으로 라벨 해상
|
||||
const bomQuery = `
|
||||
SELECT
|
||||
b.item_code AS parent_item_code,
|
||||
b.base_qty AS bom_base_qty,
|
||||
bd.child_item_id,
|
||||
bd.quantity AS bom_qty,
|
||||
bd.unit AS bom_unit,
|
||||
bd.loss_rate,
|
||||
ii.item_name AS material_name,
|
||||
ii.item_number AS material_code,
|
||||
ii.unit AS material_unit,
|
||||
ii.inventory_unit AS material_inventory_unit,
|
||||
COALESCE(cv_inv.value_label, ii.inventory_unit) AS material_inventory_unit,
|
||||
COALESCE(ii.width::text, '') AS material_width,
|
||||
COALESCE(ii.height::text, '') AS material_height,
|
||||
COALESCE(ii.thickness::text, '') AS material_thickness
|
||||
FROM bom b
|
||||
JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code
|
||||
LEFT JOIN item_info ii ON bd.child_item_id = ii.id AND b.company_code = ii.company_code
|
||||
LEFT JOIN category_values cv_inv
|
||||
ON cv_inv.table_name = 'item_info'
|
||||
AND cv_inv.column_name = 'inventory_unit'
|
||||
AND cv_inv.value_code = ii.inventory_unit
|
||||
AND cv_inv.company_code = ii.company_code
|
||||
AND cv_inv.is_active = true
|
||||
WHERE b.item_code IN (${itemPlaceholders})
|
||||
${bomCompanyCondition}
|
||||
ORDER BY b.item_code, bd.seq_no
|
||||
@@ -221,11 +226,7 @@ export async function getMaterialStatus(
|
||||
materialCode:
|
||||
bomRow.material_code || bomRow.child_item_id,
|
||||
materialName: bomRow.material_name || "알 수 없음",
|
||||
unit:
|
||||
bomRow.material_inventory_unit ||
|
||||
bomRow.bom_unit ||
|
||||
bomRow.material_unit ||
|
||||
"EA",
|
||||
unit: bomRow.material_inventory_unit || "",
|
||||
requiredQty,
|
||||
width: bomRow.material_width || "",
|
||||
height: bomRow.material_height || "",
|
||||
|
||||
@@ -463,7 +463,10 @@ export async function getWorkItemDetails(req: AuthenticatedRequest, res: Respons
|
||||
SELECT id, work_item_id, detail_type, content, is_required, sort_order, remark,
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
duration_minutes, input_type, lookup_target, display_fields,
|
||||
selected_bom_items, process_inspection_apply, equip_inspection_apply, created_date
|
||||
selected_bom_items, process_inspection_apply, equip_inspection_apply,
|
||||
condition_unit, condition_base_value, condition_tolerance,
|
||||
condition_auto_collect, condition_plc_data,
|
||||
created_date
|
||||
FROM process_work_item_detail
|
||||
WHERE work_item_id = $1 AND company_code = $2
|
||||
ORDER BY sort_order, created_date
|
||||
@@ -493,6 +496,9 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
duration_minutes, input_type, lookup_target, display_fields,
|
||||
selected_bom_items, process_inspection_apply, equip_inspection_apply,
|
||||
// 설비조건(equip_condition) 전용 5개 필드 — TASK:ERP-015
|
||||
condition_unit, condition_base_value, condition_tolerance,
|
||||
condition_auto_collect, condition_plc_data,
|
||||
} = req.body;
|
||||
|
||||
if (!work_item_id || !content) {
|
||||
@@ -516,8 +522,11 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo
|
||||
(company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer,
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
duration_minutes, input_type, lookup_target, display_fields, selected_bom_items,
|
||||
process_inspection_apply, equip_inspection_apply)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
||||
process_inspection_apply, equip_inspection_apply,
|
||||
condition_unit, condition_base_value, condition_tolerance,
|
||||
condition_auto_collect, condition_plc_data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
|
||||
$21, $22, $23, $24, $25)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
@@ -545,6 +554,11 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo
|
||||
bomItemsJson,
|
||||
process_inspection_apply || null,
|
||||
equip_inspection_apply || null,
|
||||
condition_unit || null,
|
||||
condition_base_value || null,
|
||||
condition_tolerance || null,
|
||||
condition_auto_collect || null,
|
||||
condition_plc_data || null,
|
||||
]);
|
||||
|
||||
logger.info("작업 항목 상세 생성", { companyCode, id: result.rows[0].id });
|
||||
@@ -571,6 +585,9 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
duration_minutes, input_type, lookup_target, display_fields,
|
||||
selected_bom_items, process_inspection_apply, equip_inspection_apply,
|
||||
// 설비조건(equip_condition) 전용 5개 필드 — TASK:ERP-015
|
||||
condition_unit, condition_base_value, condition_tolerance,
|
||||
condition_auto_collect, condition_plc_data,
|
||||
} = req.body;
|
||||
|
||||
const bomItemsJson = Array.isArray(selected_bom_items) ? JSON.stringify(selected_bom_items) : selected_bom_items ?? null;
|
||||
@@ -594,6 +611,11 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo
|
||||
selected_bom_items = $17,
|
||||
process_inspection_apply = $18,
|
||||
equip_inspection_apply = $19,
|
||||
condition_unit = $20,
|
||||
condition_base_value = $21,
|
||||
condition_tolerance = $22,
|
||||
condition_auto_collect = $23,
|
||||
condition_plc_data = $24,
|
||||
updated_date = NOW()
|
||||
WHERE id = $6 AND company_code = $7
|
||||
RETURNING *
|
||||
@@ -619,6 +641,11 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo
|
||||
bomItemsJson,
|
||||
process_inspection_apply || null,
|
||||
equip_inspection_apply || null,
|
||||
condition_unit ?? null,
|
||||
condition_base_value ?? null,
|
||||
condition_tolerance ?? null,
|
||||
condition_auto_collect ?? null,
|
||||
condition_plc_data ?? null,
|
||||
]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
@@ -733,8 +760,11 @@ export async function saveAll(req: AuthenticatedRequest, res: Response) {
|
||||
`INSERT INTO process_work_item_detail
|
||||
(company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer,
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
duration_minutes, input_type, lookup_target, display_fields)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`,
|
||||
duration_minutes, input_type, lookup_target, display_fields,
|
||||
condition_unit, condition_base_value, condition_tolerance,
|
||||
condition_auto_collect, condition_plc_data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17,
|
||||
$18, $19, $20, $21, $22)`,
|
||||
[
|
||||
companyCode,
|
||||
workItemId,
|
||||
@@ -753,6 +783,12 @@ export async function saveAll(req: AuthenticatedRequest, res: Response) {
|
||||
detail.input_type || null,
|
||||
detail.lookup_target || null,
|
||||
detail.display_fields || null,
|
||||
// 설비조건(equip_condition) 전용 5개 — TASK:ERP-015
|
||||
detail.condition_unit || null,
|
||||
detail.condition_base_value || null,
|
||||
detail.condition_tolerance || null,
|
||||
detail.condition_auto_collect || null,
|
||||
detail.condition_plc_data || null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -665,7 +665,9 @@ export async function getWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
`SELECT id, wi_work_item_id AS work_item_id, detail_type, content, is_required, sort_order, remark,
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
duration_minutes, input_type, lookup_target, display_fields,
|
||||
process_inspection_apply, equip_inspection_apply
|
||||
process_inspection_apply, equip_inspection_apply,
|
||||
condition_unit, condition_base_value, condition_tolerance,
|
||||
condition_auto_collect, condition_plc_data
|
||||
FROM wi_process_work_item_detail
|
||||
WHERE wi_work_item_id = $1 AND company_code = $2
|
||||
ORDER BY sort_order`,
|
||||
@@ -690,7 +692,9 @@ export async function getWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
`SELECT id, work_item_id, detail_type, content, is_required, sort_order, remark,
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
duration_minutes, input_type, lookup_target, display_fields,
|
||||
process_inspection_apply, equip_inspection_apply
|
||||
process_inspection_apply, equip_inspection_apply,
|
||||
condition_unit, condition_base_value, condition_tolerance,
|
||||
condition_auto_collect, condition_plc_data
|
||||
FROM process_work_item_detail
|
||||
WHERE work_item_id = $1 AND company_code = $2
|
||||
ORDER BY sort_order`,
|
||||
@@ -771,9 +775,9 @@ export async function copyWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
|
||||
for (const origDetail of origDetails.rows) {
|
||||
await client.query(
|
||||
`INSERT INTO wi_process_work_item_detail (id, company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer)
|
||||
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
|
||||
[companyCode, newItemId, origDetail.detail_type, origDetail.content, origDetail.is_required, origDetail.sort_order, origDetail.remark, origDetail.inspection_code, origDetail.inspection_method, origDetail.unit, origDetail.lower_limit, origDetail.upper_limit, origDetail.duration_minutes, origDetail.input_type, origDetail.lookup_target, origDetail.display_fields, origDetail.process_inspection_apply || null, origDetail.equip_inspection_apply || null, userId]
|
||||
`INSERT INTO wi_process_work_item_detail (id, company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, condition_unit, condition_base_value, condition_tolerance, condition_auto_collect, condition_plc_data, writer)
|
||||
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)`,
|
||||
[companyCode, newItemId, origDetail.detail_type, origDetail.content, origDetail.is_required, origDetail.sort_order, origDetail.remark, origDetail.inspection_code, origDetail.inspection_method, origDetail.unit, origDetail.lower_limit, origDetail.upper_limit, origDetail.duration_minutes, origDetail.input_type, origDetail.lookup_target, origDetail.display_fields, origDetail.process_inspection_apply || null, origDetail.equip_inspection_apply || null, origDetail.condition_unit || null, origDetail.condition_base_value || null, origDetail.condition_tolerance || null, origDetail.condition_auto_collect || null, origDetail.condition_plc_data || null, userId]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -838,9 +842,9 @@ export async function saveWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
if (wi.details && Array.isArray(wi.details)) {
|
||||
for (const d of wi.details) {
|
||||
await client.query(
|
||||
`INSERT INTO wi_process_work_item_detail (id, company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer)
|
||||
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
|
||||
[companyCode, newId, d.detail_type, d.content, d.is_required, d.sort_order, d.remark || null, d.inspection_code || null, d.inspection_method || null, d.unit || null, d.lower_limit || null, d.upper_limit || null, d.duration_minutes || null, d.input_type || null, d.lookup_target || null, d.display_fields || null, d.process_inspection_apply || null, d.equip_inspection_apply || null, userId]
|
||||
`INSERT INTO wi_process_work_item_detail (id, company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, condition_unit, condition_base_value, condition_tolerance, condition_auto_collect, condition_plc_data, writer)
|
||||
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)`,
|
||||
[companyCode, newId, d.detail_type, d.content, d.is_required, d.sort_order, d.remark || null, d.inspection_code || null, d.inspection_method || null, d.unit || null, d.lower_limit || null, d.upper_limit || null, d.duration_minutes || null, d.input_type || null, d.lookup_target || null, d.display_fields || null, d.process_inspection_apply || null, d.equip_inspection_apply || null, d.condition_unit || null, d.condition_base_value || null, d.condition_tolerance || null, d.condition_auto_collect || null, d.condition_plc_data || null, userId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
Inbox,
|
||||
Settings2,
|
||||
Upload,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
|
||||
const [eqForm, setEqForm] = useState<Record<string, any>>({});
|
||||
const [eqSaving, setEqSaving] = useState(false);
|
||||
const [eqKeyword, setEqKeyword] = useState("");
|
||||
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
|
||||
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
/* ───── 채번 ───── */
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
|
||||
)
|
||||
: defects;
|
||||
|
||||
const filteredEquipments = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
const filteredEquipments = useMemo(() => {
|
||||
const base = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
if (!eqSortKey) return base;
|
||||
const key = eqSortKey;
|
||||
const dir = eqSortDir;
|
||||
return [...base].sort((a, b) => {
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
const aEmpty = av === null || av === undefined || av === "";
|
||||
const bEmpty = bv === null || bv === undefined || bv === "";
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
const na = Number(av);
|
||||
const nb = Number(bv);
|
||||
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
|
||||
const sa = String(av).toLowerCase();
|
||||
const sb = String(bv).toLowerCase();
|
||||
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
|
||||
|
||||
const handleEqSort = useCallback((key: string) => {
|
||||
setEqSortKey((prev) => {
|
||||
if (prev === key) {
|
||||
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
return prev;
|
||||
}
|
||||
setEqSortDir("asc");
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderEqSortIcon = (key: string) => {
|
||||
if (eqSortKey !== key)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
|
||||
return eqSortDir === "asc" ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
};
|
||||
|
||||
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
|
||||
const openInspCreate = async () => {
|
||||
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
|
||||
이미지
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비코드
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
|
||||
장비코드{renderEqSortIcon("equipment_code")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
|
||||
장비명{renderEqSortIcon("equipment_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비유형
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
|
||||
장비유형{renderEqSortIcon("equipment_type")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
모델명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
|
||||
모델명{renderEqSortIcon("model_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
제조사
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
|
||||
제조사{renderEqSortIcon("manufacturer")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
설치장소
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
|
||||
설치장소{renderEqSortIcon("installation_location")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
최근교정일
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
|
||||
최근교정일{renderEqSortIcon("last_calibration_date")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
교정주기(개월)
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
|
||||
교정주기(개월){renderEqSortIcon("calibration_period")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비상태
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
|
||||
장비상태{renderEqSortIcon("equipment_status")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
담당자
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
|
||||
담당자{renderEqSortIcon("manager_id")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
Inbox,
|
||||
Settings2,
|
||||
Upload,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
|
||||
const [eqForm, setEqForm] = useState<Record<string, any>>({});
|
||||
const [eqSaving, setEqSaving] = useState(false);
|
||||
const [eqKeyword, setEqKeyword] = useState("");
|
||||
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
|
||||
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
/* ───── 채번 ───── */
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
|
||||
)
|
||||
: defects;
|
||||
|
||||
const filteredEquipments = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
const filteredEquipments = useMemo(() => {
|
||||
const base = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
if (!eqSortKey) return base;
|
||||
const key = eqSortKey;
|
||||
const dir = eqSortDir;
|
||||
return [...base].sort((a, b) => {
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
const aEmpty = av === null || av === undefined || av === "";
|
||||
const bEmpty = bv === null || bv === undefined || bv === "";
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
const na = Number(av);
|
||||
const nb = Number(bv);
|
||||
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
|
||||
const sa = String(av).toLowerCase();
|
||||
const sb = String(bv).toLowerCase();
|
||||
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
|
||||
|
||||
const handleEqSort = useCallback((key: string) => {
|
||||
setEqSortKey((prev) => {
|
||||
if (prev === key) {
|
||||
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
return prev;
|
||||
}
|
||||
setEqSortDir("asc");
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderEqSortIcon = (key: string) => {
|
||||
if (eqSortKey !== key)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
|
||||
return eqSortDir === "asc" ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
};
|
||||
|
||||
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
|
||||
const openInspCreate = async () => {
|
||||
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
|
||||
이미지
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비코드
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
|
||||
장비코드{renderEqSortIcon("equipment_code")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
|
||||
장비명{renderEqSortIcon("equipment_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비유형
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
|
||||
장비유형{renderEqSortIcon("equipment_type")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
모델명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
|
||||
모델명{renderEqSortIcon("model_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
제조사
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
|
||||
제조사{renderEqSortIcon("manufacturer")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
설치장소
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
|
||||
설치장소{renderEqSortIcon("installation_location")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
최근교정일
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
|
||||
최근교정일{renderEqSortIcon("last_calibration_date")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
교정주기(개월)
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
|
||||
교정주기(개월){renderEqSortIcon("calibration_period")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비상태
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
|
||||
장비상태{renderEqSortIcon("equipment_status")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
담당자
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
|
||||
담당자{renderEqSortIcon("manager_id")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function ItemInspectionInfoPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// FK 옵션
|
||||
const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]);
|
||||
const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string; size: string }[]>([]);
|
||||
const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; judgment_criteria: string; selection_options: string; unit: string; types: string[] }[]>([]);
|
||||
const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]);
|
||||
|
||||
@@ -136,6 +136,7 @@ export default function ItemInspectionInfoPage() {
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.inventory_unit || "",
|
||||
size: r.size || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -244,7 +245,7 @@ export default function ItemInspectionInfoPage() {
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
const cm = itemCatMapRef.current;
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" })));
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "", size: r.size || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -296,6 +297,7 @@ export default function ItemInspectionInfoPage() {
|
||||
name: r.item_name,
|
||||
item_type: cm["type"]?.[r.type] || r.type || "",
|
||||
unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "",
|
||||
size: r.size || "",
|
||||
}));
|
||||
setCopyFilteredItems(list);
|
||||
setCopyTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
@@ -1244,17 +1246,19 @@ export default function ItemInspectionInfoPage() {
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[120px]">규격</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={5} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm truncate max-w-[120px]" title={item.size}>{item.size}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
Inbox,
|
||||
Settings2,
|
||||
Upload,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
|
||||
const [eqForm, setEqForm] = useState<Record<string, any>>({});
|
||||
const [eqSaving, setEqSaving] = useState(false);
|
||||
const [eqKeyword, setEqKeyword] = useState("");
|
||||
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
|
||||
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
/* ───── 채번 ───── */
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
|
||||
)
|
||||
: defects;
|
||||
|
||||
const filteredEquipments = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
const filteredEquipments = useMemo(() => {
|
||||
const base = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
if (!eqSortKey) return base;
|
||||
const key = eqSortKey;
|
||||
const dir = eqSortDir;
|
||||
return [...base].sort((a, b) => {
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
const aEmpty = av === null || av === undefined || av === "";
|
||||
const bEmpty = bv === null || bv === undefined || bv === "";
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
const na = Number(av);
|
||||
const nb = Number(bv);
|
||||
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
|
||||
const sa = String(av).toLowerCase();
|
||||
const sb = String(bv).toLowerCase();
|
||||
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
|
||||
|
||||
const handleEqSort = useCallback((key: string) => {
|
||||
setEqSortKey((prev) => {
|
||||
if (prev === key) {
|
||||
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
return prev;
|
||||
}
|
||||
setEqSortDir("asc");
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderEqSortIcon = (key: string) => {
|
||||
if (eqSortKey !== key)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
|
||||
return eqSortDir === "asc" ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
};
|
||||
|
||||
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
|
||||
const openInspCreate = async () => {
|
||||
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
|
||||
이미지
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비코드
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
|
||||
장비코드{renderEqSortIcon("equipment_code")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
|
||||
장비명{renderEqSortIcon("equipment_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비유형
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
|
||||
장비유형{renderEqSortIcon("equipment_type")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
모델명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
|
||||
모델명{renderEqSortIcon("model_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
제조사
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
|
||||
제조사{renderEqSortIcon("manufacturer")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
설치장소
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
|
||||
설치장소{renderEqSortIcon("installation_location")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
최근교정일
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
|
||||
최근교정일{renderEqSortIcon("last_calibration_date")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
교정주기(개월)
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
|
||||
교정주기(개월){renderEqSortIcon("calibration_period")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비상태
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
|
||||
장비상태{renderEqSortIcon("equipment_status")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
담당자
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
|
||||
담당자{renderEqSortIcon("manager_id")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
Inbox,
|
||||
Settings2,
|
||||
Upload,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
|
||||
const [eqForm, setEqForm] = useState<Record<string, any>>({});
|
||||
const [eqSaving, setEqSaving] = useState(false);
|
||||
const [eqKeyword, setEqKeyword] = useState("");
|
||||
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
|
||||
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
/* ───── 채번 ───── */
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
|
||||
)
|
||||
: defects;
|
||||
|
||||
const filteredEquipments = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
const filteredEquipments = useMemo(() => {
|
||||
const base = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
if (!eqSortKey) return base;
|
||||
const key = eqSortKey;
|
||||
const dir = eqSortDir;
|
||||
return [...base].sort((a, b) => {
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
const aEmpty = av === null || av === undefined || av === "";
|
||||
const bEmpty = bv === null || bv === undefined || bv === "";
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
const na = Number(av);
|
||||
const nb = Number(bv);
|
||||
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
|
||||
const sa = String(av).toLowerCase();
|
||||
const sb = String(bv).toLowerCase();
|
||||
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
|
||||
|
||||
const handleEqSort = useCallback((key: string) => {
|
||||
setEqSortKey((prev) => {
|
||||
if (prev === key) {
|
||||
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
return prev;
|
||||
}
|
||||
setEqSortDir("asc");
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderEqSortIcon = (key: string) => {
|
||||
if (eqSortKey !== key)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
|
||||
return eqSortDir === "asc" ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
};
|
||||
|
||||
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
|
||||
const openInspCreate = async () => {
|
||||
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
|
||||
이미지
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비코드
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
|
||||
장비코드{renderEqSortIcon("equipment_code")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
|
||||
장비명{renderEqSortIcon("equipment_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비유형
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
|
||||
장비유형{renderEqSortIcon("equipment_type")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
모델명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
|
||||
모델명{renderEqSortIcon("model_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
제조사
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
|
||||
제조사{renderEqSortIcon("manufacturer")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
설치장소
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
|
||||
설치장소{renderEqSortIcon("installation_location")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
최근교정일
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
|
||||
최근교정일{renderEqSortIcon("last_calibration_date")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
교정주기(개월)
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
|
||||
교정주기(개월){renderEqSortIcon("calibration_period")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비상태
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
|
||||
장비상태{renderEqSortIcon("equipment_status")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
담당자
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
|
||||
담당자{renderEqSortIcon("manager_id")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
Inbox,
|
||||
Settings2,
|
||||
Upload,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
|
||||
const [eqForm, setEqForm] = useState<Record<string, any>>({});
|
||||
const [eqSaving, setEqSaving] = useState(false);
|
||||
const [eqKeyword, setEqKeyword] = useState("");
|
||||
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
|
||||
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
/* ───── 채번 ───── */
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
|
||||
)
|
||||
: defects;
|
||||
|
||||
const filteredEquipments = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
const filteredEquipments = useMemo(() => {
|
||||
const base = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
if (!eqSortKey) return base;
|
||||
const key = eqSortKey;
|
||||
const dir = eqSortDir;
|
||||
return [...base].sort((a, b) => {
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
const aEmpty = av === null || av === undefined || av === "";
|
||||
const bEmpty = bv === null || bv === undefined || bv === "";
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
const na = Number(av);
|
||||
const nb = Number(bv);
|
||||
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
|
||||
const sa = String(av).toLowerCase();
|
||||
const sb = String(bv).toLowerCase();
|
||||
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
|
||||
|
||||
const handleEqSort = useCallback((key: string) => {
|
||||
setEqSortKey((prev) => {
|
||||
if (prev === key) {
|
||||
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
return prev;
|
||||
}
|
||||
setEqSortDir("asc");
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderEqSortIcon = (key: string) => {
|
||||
if (eqSortKey !== key)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
|
||||
return eqSortDir === "asc" ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
};
|
||||
|
||||
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
|
||||
const openInspCreate = async () => {
|
||||
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
|
||||
이미지
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비코드
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
|
||||
장비코드{renderEqSortIcon("equipment_code")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
|
||||
장비명{renderEqSortIcon("equipment_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비유형
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
|
||||
장비유형{renderEqSortIcon("equipment_type")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
모델명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
|
||||
모델명{renderEqSortIcon("model_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
제조사
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
|
||||
제조사{renderEqSortIcon("manufacturer")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
설치장소
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
|
||||
설치장소{renderEqSortIcon("installation_location")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
최근교정일
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
|
||||
최근교정일{renderEqSortIcon("last_calibration_date")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
교정주기(개월)
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
|
||||
교정주기(개월){renderEqSortIcon("calibration_period")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비상태
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
|
||||
장비상태{renderEqSortIcon("equipment_status")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
담당자
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
|
||||
담당자{renderEqSortIcon("manager_id")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
Inbox,
|
||||
Settings2,
|
||||
Upload,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
|
||||
const [eqForm, setEqForm] = useState<Record<string, any>>({});
|
||||
const [eqSaving, setEqSaving] = useState(false);
|
||||
const [eqKeyword, setEqKeyword] = useState("");
|
||||
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
|
||||
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
/* ───── 채번 ───── */
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
|
||||
)
|
||||
: defects;
|
||||
|
||||
const filteredEquipments = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
const filteredEquipments = useMemo(() => {
|
||||
const base = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
if (!eqSortKey) return base;
|
||||
const key = eqSortKey;
|
||||
const dir = eqSortDir;
|
||||
return [...base].sort((a, b) => {
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
const aEmpty = av === null || av === undefined || av === "";
|
||||
const bEmpty = bv === null || bv === undefined || bv === "";
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
const na = Number(av);
|
||||
const nb = Number(bv);
|
||||
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
|
||||
const sa = String(av).toLowerCase();
|
||||
const sb = String(bv).toLowerCase();
|
||||
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
|
||||
|
||||
const handleEqSort = useCallback((key: string) => {
|
||||
setEqSortKey((prev) => {
|
||||
if (prev === key) {
|
||||
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
return prev;
|
||||
}
|
||||
setEqSortDir("asc");
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderEqSortIcon = (key: string) => {
|
||||
if (eqSortKey !== key)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
|
||||
return eqSortDir === "asc" ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
};
|
||||
|
||||
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
|
||||
const openInspCreate = async () => {
|
||||
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
|
||||
이미지
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비코드
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
|
||||
장비코드{renderEqSortIcon("equipment_code")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
|
||||
장비명{renderEqSortIcon("equipment_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비유형
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
|
||||
장비유형{renderEqSortIcon("equipment_type")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
모델명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
|
||||
모델명{renderEqSortIcon("model_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
제조사
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
|
||||
제조사{renderEqSortIcon("manufacturer")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
설치장소
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
|
||||
설치장소{renderEqSortIcon("installation_location")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
최근교정일
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
|
||||
최근교정일{renderEqSortIcon("last_calibration_date")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
교정주기(개월)
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
|
||||
교정주기(개월){renderEqSortIcon("calibration_period")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비상태
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
|
||||
장비상태{renderEqSortIcon("equipment_status")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
담당자
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
|
||||
담당자{renderEqSortIcon("manager_id")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 절단계획 → 작업지시 적용 모달
|
||||
* jskim-node의 작업지시 모달 구조와 호환 (품목별 일정/설비/작업조/작업자 지정).
|
||||
* 저장 시 마스터에 batch_no(=plan_no) / cutting_plan_id 를 함께 전달.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { CheckCircle2, ChevronsUpDown, Loader2, X } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Popover, PopoverContent, PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Table, TableHeader, TableRow, TableHead, TableBody, TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import {
|
||||
previewWorkInstructionNo, saveWorkInstruction,
|
||||
getEquipmentList, getEmployeeList,
|
||||
} from "@/lib/api/workInstruction";
|
||||
|
||||
// ─── 공용 다중선택 Popover (설비/작업조/작업자) ────────────────────
|
||||
interface MultiSelectOption { value: string; label: string; sub?: string; }
|
||||
interface MultiSelectPopoverProps {
|
||||
options: MultiSelectOption[];
|
||||
value: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
placeholder?: string;
|
||||
searchable?: boolean;
|
||||
triggerClassName?: string;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
function MultiSelectPopover({
|
||||
options, value, onChange, placeholder = "선택", searchable = false, triggerClassName, emptyMessage = "항목이 없어요",
|
||||
}: MultiSelectPopoverProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
const selectedSet = useMemo(() => new Set(value), [value]);
|
||||
const toggle = (val: string) => {
|
||||
if (selectedSet.has(val)) onChange(value.filter((v) => v !== val));
|
||||
else onChange([...value, val]);
|
||||
};
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!searchable || !keyword.trim()) return options;
|
||||
const k = keyword.trim().toLowerCase();
|
||||
return options.filter((o) => o.label.toLowerCase().includes(k) || (o.sub || "").toLowerCase().includes(k));
|
||||
}, [options, keyword, searchable]);
|
||||
|
||||
const display = useMemo(() => {
|
||||
if (value.length === 0) return placeholder;
|
||||
if (value.length === 1) return options.find((o) => o.value === value[0])?.label || value[0];
|
||||
if (value.length === 2) {
|
||||
return value.map((v) => options.find((o) => o.value === v)?.label || v).join(", ");
|
||||
}
|
||||
return `${value.length}개 선택`;
|
||||
}, [value, options, placeholder]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" aria-expanded={open}
|
||||
className={cn("w-full justify-between font-normal", triggerClassName || "h-7 text-xs")}>
|
||||
<span className={cn("truncate", value.length === 0 && "text-muted-foreground")}>{display}</span>
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)", minWidth: 200 }} align="start">
|
||||
{searchable && (
|
||||
<div className="p-2 border-b">
|
||||
<Input placeholder="검색..." value={keyword} onChange={(e) => setKeyword(e.target.value)} className="h-7 text-xs" />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-56 overflow-y-auto py-1">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="py-4 text-center text-xs text-muted-foreground">{emptyMessage}</div>
|
||||
) : filtered.map((opt) => (
|
||||
<label
|
||||
key={opt.value}
|
||||
className="flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-muted/50 text-xs"
|
||||
onClick={(e) => { e.preventDefault(); toggle(opt.value); }}
|
||||
>
|
||||
<Checkbox checked={selectedSet.has(opt.value)} onCheckedChange={() => toggle(opt.value)} className="h-3.5 w-3.5" />
|
||||
<span className="flex-1 truncate">
|
||||
{opt.label}{opt.sub ? <span className="text-muted-foreground ml-1">({opt.sub})</span> : null}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{value.length > 0 && (
|
||||
<div className="p-1.5 border-t flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">{value.length}개 선택됨</span>
|
||||
<Button variant="ghost" size="sm" className="h-6 text-[11px] px-2" onClick={() => onChange([])}>초기화</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 모달 인터페이스 ────────────────────────────────────────────────
|
||||
export interface WorkInstructionApplyItem {
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
spec?: string;
|
||||
qty: number;
|
||||
remark?: string;
|
||||
sourceTable?: string;
|
||||
sourceId?: string;
|
||||
// 품목별 일정/설비/작업조/작업자
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
equipmentIds?: string[];
|
||||
workTeams?: string[];
|
||||
workers?: string[];
|
||||
}
|
||||
|
||||
export interface WorkInstructionApplyModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
initialItems: WorkInstructionApplyItem[];
|
||||
batchNo?: string | null;
|
||||
cuttingPlanId?: number | null;
|
||||
onSaved?: (result: { id: string; workInstructionNo: string }) => void;
|
||||
}
|
||||
|
||||
export default function WorkInstructionApplyModal({
|
||||
open, onOpenChange, initialItems, batchNo, cuttingPlanId, onSaved,
|
||||
}: WorkInstructionApplyModalProps) {
|
||||
const [wiNo, setWiNo] = useState("");
|
||||
const [status, setStatus] = useState("일반");
|
||||
const [remark, setRemark] = useState("");
|
||||
const [items, setItems] = useState<WorkInstructionApplyItem[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [equipmentOptions, setEquipmentOptions] = useState<{ id: string; equipment_code: string; equipment_name: string }[]>([]);
|
||||
const [workerOptions, setWorkerOptions] = useState<{ user_id: string; user_name: string; dept_name: string | null }[]>([]);
|
||||
|
||||
// 모달 오픈 시 초기화 + 옵션 로드
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
setItems(initialItems.map((x) => ({
|
||||
...x,
|
||||
startDate: x.startDate || today,
|
||||
endDate: x.endDate || "",
|
||||
equipmentIds: x.equipmentIds || [],
|
||||
workTeams: x.workTeams || [],
|
||||
workers: x.workers || [],
|
||||
})));
|
||||
setStatus("일반");
|
||||
setRemark("");
|
||||
|
||||
previewWorkInstructionNo().then((r) => { if (r.success) setWiNo(r.instructionNo); }).catch(() => {});
|
||||
getEquipmentList().then((r) => { if (r.success) setEquipmentOptions(r.data || []); }).catch(() => {});
|
||||
getEmployeeList().then((r) => { if (r.success) setWorkerOptions(r.data || []); }).catch(() => {});
|
||||
}, [open, initialItems]);
|
||||
|
||||
const canSave = useMemo(() => items.length > 0 && items.every((i) => i.qty > 0), [items]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!canSave) { toast.error("품목/수량을 확인해주세요"); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
// 하위호환: 마스터에는 첫 품목의 대표값을 실음 (jskim 스타일)
|
||||
const first = items[0];
|
||||
const payload = {
|
||||
status,
|
||||
startDate: first?.startDate || "",
|
||||
endDate: first?.endDate || "",
|
||||
equipmentId: first?.equipmentIds?.[0] || "",
|
||||
workTeam: first?.workTeams?.[0] || "",
|
||||
worker: first?.workers?.[0] || "",
|
||||
remark,
|
||||
routing: null,
|
||||
batchNo: batchNo || null,
|
||||
cuttingPlanId: cuttingPlanId ?? null,
|
||||
items: items.map((i) => ({
|
||||
itemNumber: i.itemCode, itemCode: i.itemCode, partCode: i.itemCode,
|
||||
qty: String(i.qty), remark: i.remark || "",
|
||||
sourceTable: i.sourceTable || "cutting_plan",
|
||||
sourceId: i.sourceId || (cuttingPlanId != null ? String(cuttingPlanId) : ""),
|
||||
routing: null,
|
||||
startDate: i.startDate || "",
|
||||
endDate: i.endDate || "",
|
||||
equipmentIds: (i.equipmentIds || []).join(","),
|
||||
workTeams: (i.workTeams || []).join(","),
|
||||
workers: (i.workers || []).join(","),
|
||||
})),
|
||||
};
|
||||
const r = await saveWorkInstruction(payload);
|
||||
if (!r.success) { toast.error(r.message || "저장 실패"); return; }
|
||||
toast.success(`작업지시 ${r.data?.workInstructionNo || wiNo} 등록 완료`);
|
||||
onOpenChange(false);
|
||||
onSaved?.(r.data);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || "저장 실패");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const equipmentSelectOptions = useMemo(
|
||||
() => equipmentOptions.map((eq) => ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code })),
|
||||
[equipmentOptions]
|
||||
);
|
||||
const workerSelectOptions = useMemo(
|
||||
() => workerOptions.map((w) => ({ value: w.user_id, label: w.user_name, sub: w.dept_name || undefined })),
|
||||
[workerOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[1500px] w-[95vw] max-h-[90vh] flex flex-col overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>작업지시 적용 확인</DialogTitle>
|
||||
<DialogDescription>
|
||||
기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요.
|
||||
{batchNo ? <span className="ml-2 text-primary font-medium">배치번호 {batchNo}</span> : null}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="space-y-5">
|
||||
<div className="bg-muted/30 border rounded-lg p-5">
|
||||
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground">작업지시 기본 정보</h4>
|
||||
<p className="text-[11px] text-muted-foreground mt-2">시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground">작업지시번호</Label>
|
||||
<Input value={wiNo} readOnly className="h-9 bg-muted cursor-not-allowed font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground">상태</Label>
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="일반">일반</SelectItem>
|
||||
<SelectItem value="긴급">긴급</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground">비고</Label>
|
||||
<Input className="h-9" placeholder="비고를 입력해주세요" value={remark} onChange={(e) => setRemark(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-5">
|
||||
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground mb-3">품목 목록</h4>
|
||||
<div className="overflow-auto">
|
||||
<Table className="min-w-[1700px]">
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">순번</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">배치번호</TableHead>
|
||||
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료예정일</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>
|
||||
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업조</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업자</TableHead>
|
||||
<TableHead className="w-[240px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="text-[13px] text-center">{idx + 1}</TableCell>
|
||||
<TableCell className="text-[13px] font-mono text-primary">{batchNo || "-"}</TableCell>
|
||||
<TableCell className="text-[13px] font-medium">{item.itemCode || "-"}</TableCell>
|
||||
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName || item.itemCode}>
|
||||
{item.itemName || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">{item.spec || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Input type="number" className="h-7 text-[13px] w-20"
|
||||
value={item.qty}
|
||||
onChange={(e) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="date" className="h-7 text-[13px]"
|
||||
value={item.startDate || ""}
|
||||
onChange={(e) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="date" className="h-7 text-[13px]"
|
||||
value={item.endDate || ""}
|
||||
onChange={(e) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MultiSelectPopover
|
||||
options={equipmentSelectOptions}
|
||||
value={item.equipmentIds || []}
|
||||
onChange={(next) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))}
|
||||
placeholder="설비 선택"
|
||||
searchable
|
||||
emptyMessage="설비가 없어요"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MultiSelectPopover
|
||||
options={[{ value: "주간", label: "주간" }, { value: "야간", label: "야간" }]}
|
||||
value={item.workTeams || []}
|
||||
onChange={(next) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))}
|
||||
placeholder="작업조 선택"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MultiSelectPopover
|
||||
options={workerSelectOptions}
|
||||
value={item.workers || []}
|
||||
onChange={(next) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))}
|
||||
placeholder="작업자 선택"
|
||||
searchable
|
||||
emptyMessage="사원을 찾을 수 없어요"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input className="h-7 text-[13px]" placeholder="비고"
|
||||
value={item.remark || ""}
|
||||
onChange={(e) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6"
|
||||
onClick={() => setItems((prev) => prev.filter((_, i) => i !== idx))}>
|
||||
<X className="w-3 h-3 text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={13} className="text-center text-muted-foreground text-[12px] py-6">
|
||||
품목이 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>취소</Button>
|
||||
<Button onClick={handleSave} disabled={!canSave || saving}>
|
||||
{saving ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : <CheckCircle2 className="w-4 h-4 mr-1.5" />}
|
||||
최종 적용
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
2785
frontend/app/(main)/COMPANY_9/production/cutting-plan/page.tsx
Normal file
2785
frontend/app/(main)/COMPANY_9/production/cutting-plan/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -647,6 +647,7 @@ export default function WorkInstructionPage() {
|
||||
{ key: "worker", label: "작업자", width: "w-[100px]", render: (v, row) => Number(row.detail_seq) === 1 ? getWorkerName(v) : "" },
|
||||
{ key: "start_date", label: "시작일", width: "w-[100px]", align: "center", render: (v, row) => Number(row.detail_seq) === 1 ? (v || "-") : "" },
|
||||
{ key: "end_date", label: "완료일", width: "w-[100px]", align: "center", render: (v, row) => Number(row.detail_seq) === 1 ? (v || "-") : "" },
|
||||
{ key: "batch_no", label: "배치번호", width: "w-[130px]", align: "center", render: (v, row) => Number(row.detail_seq) === 1 ? (v ? <span className="font-mono text-primary">{v}</span> : <span className="text-muted-foreground">-</span>) : "" },
|
||||
{ key: "actions", label: "작업", width: "w-[150px]", align: "center", sortable: false, filterable: false, render: (_v, row) => {
|
||||
const isFirstOfGroup = Number(row.detail_seq) === 1;
|
||||
if (!isFirstOfGroup) return null;
|
||||
@@ -911,6 +912,9 @@ export default function WorkInstructionPage() {
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">순번</TableHead>
|
||||
{editOrder?.batch_no ? (
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">배치번호</TableHead>
|
||||
) : null}
|
||||
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>
|
||||
@@ -928,10 +932,13 @@ export default function WorkInstructionPage() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{editItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={14} className="text-center py-8 text-sm text-muted-foreground">등록된 품목이 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={editOrder?.batch_no ? 15 : 14} className="text-center py-8 text-sm text-muted-foreground">등록된 품목이 없어요</TableCell></TableRow>
|
||||
) : editItems.map((item, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="text-[13px] text-center">{idx + 1}</TableCell>
|
||||
{editOrder?.batch_no ? (
|
||||
<TableCell className="text-[13px] font-mono text-primary">{editOrder.batch_no}</TableCell>
|
||||
) : null}
|
||||
<TableCell className="text-[13px] font-medium">{item.itemCode}</TableCell>
|
||||
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName}>{item.itemName || "-"}</TableCell>
|
||||
<TableCell className="text-[13px] truncate" title={item.spec}>{item.spec || "-"}</TableCell>
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
Inbox,
|
||||
Settings2,
|
||||
Upload,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
|
||||
const [eqForm, setEqForm] = useState<Record<string, any>>({});
|
||||
const [eqSaving, setEqSaving] = useState(false);
|
||||
const [eqKeyword, setEqKeyword] = useState("");
|
||||
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
|
||||
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
/* ───── 채번 ───── */
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
|
||||
)
|
||||
: defects;
|
||||
|
||||
const filteredEquipments = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
const filteredEquipments = useMemo(() => {
|
||||
const base = eqKeyword.trim()
|
||||
? equipments.filter(
|
||||
(r) =>
|
||||
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
|
||||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
|
||||
)
|
||||
: equipments;
|
||||
if (!eqSortKey) return base;
|
||||
const key = eqSortKey;
|
||||
const dir = eqSortDir;
|
||||
return [...base].sort((a, b) => {
|
||||
const av = a[key];
|
||||
const bv = b[key];
|
||||
const aEmpty = av === null || av === undefined || av === "";
|
||||
const bEmpty = bv === null || bv === undefined || bv === "";
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
const na = Number(av);
|
||||
const nb = Number(bv);
|
||||
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
|
||||
const sa = String(av).toLowerCase();
|
||||
const sb = String(bv).toLowerCase();
|
||||
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
||||
});
|
||||
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
|
||||
|
||||
const handleEqSort = useCallback((key: string) => {
|
||||
setEqSortKey((prev) => {
|
||||
if (prev === key) {
|
||||
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
return prev;
|
||||
}
|
||||
setEqSortDir("asc");
|
||||
return key;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderEqSortIcon = (key: string) => {
|
||||
if (eqSortKey !== key)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
|
||||
return eqSortDir === "asc" ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
};
|
||||
|
||||
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
|
||||
const openInspCreate = async () => {
|
||||
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
|
||||
이미지
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비코드
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
|
||||
장비코드{renderEqSortIcon("equipment_code")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
|
||||
장비명{renderEqSortIcon("equipment_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비유형
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
|
||||
장비유형{renderEqSortIcon("equipment_type")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
모델명
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
|
||||
모델명{renderEqSortIcon("model_name")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
제조사
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
|
||||
제조사{renderEqSortIcon("manufacturer")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
설치장소
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
|
||||
설치장소{renderEqSortIcon("installation_location")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
최근교정일
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
|
||||
최근교정일{renderEqSortIcon("last_calibration_date")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
교정주기(개월)
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
|
||||
교정주기(개월){renderEqSortIcon("calibration_period")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
장비상태
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
|
||||
장비상태{renderEqSortIcon("equipment_status")}
|
||||
</TableHead>
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
|
||||
담당자
|
||||
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
|
||||
담당자{renderEqSortIcon("manager_id")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -340,6 +340,7 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/COMPANY_9/production/work-instruction": dynamic(() => import("@/app/(main)/COMPANY_9/production/work-instruction/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_9/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_9/production/plan-management/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_9/production/bom": dynamic(() => import("@/app/(main)/COMPANY_9/production/bom/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_9/production/cutting-plan": dynamic(() => import("@/app/(main)/COMPANY_9/production/cutting-plan/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_9/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_9/equipment/info/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_9/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_9/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_9/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_9/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }),
|
||||
@@ -572,6 +573,7 @@ const DYNAMIC_ADMIN_IMPORTS: Record<string, () => Promise<any>> = {
|
||||
"/COMPANY_9/production/work-instruction": () => import("@/app/(main)/COMPANY_9/production/work-instruction/page"),
|
||||
"/COMPANY_9/production/plan-management": () => import("@/app/(main)/COMPANY_9/production/plan-management/page"),
|
||||
"/COMPANY_9/production/bom": () => import("@/app/(main)/COMPANY_9/production/bom/page"),
|
||||
"/COMPANY_9/production/cutting-plan": () => import("@/app/(main)/COMPANY_9/production/cutting-plan/page"),
|
||||
"/COMPANY_9/equipment/info": () => import("@/app/(main)/COMPANY_9/equipment/info/page"),
|
||||
"/COMPANY_9/equipment/plc-settings": () => import("@/app/(main)/COMPANY_9/equipment/plc-settings/page"),
|
||||
"/COMPANY_9/monitoring/production": () => import("@/app/(main)/COMPANY_9/monitoring/production/page"),
|
||||
|
||||
Reference in New Issue
Block a user