생산관리 M-BOM 본 편집(PR-B1) + 폴더 컬럼 + DataGrid 서버 페이지네이션 + bigint=varchar fix

PR-B1 본 편집/저장 (운영 saveMbom.do 1:1)
  · 매퍼 7종 1:1 (insert/updateMbomHeader, insert/updateMbomDetail,
    deleteMbomDetailByObjid, insertMbomHistory, updateProjectMbomStatus)
  · 신규 CREATE: createObjId + generateMbomNo(M-{partNo}-YYMMDD-NN) +
    child_objid 재매핑 + detail 일괄 insert + history(CREATE) + project_mgmt.mbom_status='Y'
  · 수정 UPDATE: 기존 mbom_header.objid UPSERT(insert/update/delete) + history(UPDATE)
  · POST /api/production/mbom/save (BEGIN/COMMIT/ROLLBACK 트랜잭션)
  · MbomDetailDialog: '본 편집' 토글 + 13개 셀 인라인 편집 + 저장/취소 가드

M-BOM 컬럼 폴더 아이콘
  · production/mbom/page.tsx: mbom_status 컬럼 → mbom_has(0/1) renderType=folder
  · onClick → MbomDetailDialog 오픈 (행 더블클릭도 그대로 유지)
  · 운영판 wace 견적/partMng 폴더 아이콘 패턴 1:1

DataGrid 서버 페이지네이션
  · props 신설: serverPaging/serverPage/serverPageSize/serverTotalItems
    + onPageChange/onPageSizeChange
  · 5메뉴 적용: production/mbom, development/change-list/ebom-regist/part-search/part-regist
  · pageSizeOptions=[10,15,20,50,100,200,500] 통일
  · 클라이언트 모드 하위호환 유지

bigint=varchar fix (mbom 트리 SQL 4종)
  · ATTACH_FILE_INFO 서브쿼리: P.OBJID(bigint) = F.TARGET_OBJID(varchar) → P.OBJID::varchar 캐스트
  · EBOM_WORKING_TREE_SQL INNER JOIN: P.OBJID = COALESCE(V.LAST_PART_OBJID,V.PART_NO) → ::varchar 캐스트
  · 사용자 보고: 폴더 클릭 시 'operator does not exist: bigint = character varying' 토스트

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-14 16:26:20 +09:00
parent 3a5bcb5e3a
commit 7a7f4f03b5
11 changed files with 882 additions and 88 deletions
@@ -172,7 +172,13 @@ export default function EoHistoryPage() {
gridId="development-eo-history"
showColumnSettings
paginationStyle="range"
pageSizeOptions={[10, 15, 20, 50, 100]}
pageSizeOptions={[10, 15, 20, 50, 100, 200, 500]}
serverPaging
serverPage={filter.page ?? 1}
serverPageSize={filter.page_size ?? 50}
serverTotalItems={total}
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
summaryStats={eoSummary}
systemColumnKeys={["writer_name", "his_reg_date_title"]}
onRefresh={() => fetchList()}
@@ -206,7 +206,13 @@ export default function EbomRegistPage() {
gridId="development-ebom-regist"
showColumnSettings
paginationStyle="range"
pageSizeOptions={[10, 15, 20, 50, 100]}
pageSizeOptions={[10, 15, 20, 50, 100, 200, 500]}
serverPaging
serverPage={filter.page ?? 1}
serverPageSize={filter.page_size ?? 50}
serverTotalItems={total}
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
summaryStats={bomSummary}
systemColumnKeys={["dept_user_name", "reg_date", "deploy_date", "revision", "status_title"]}
onRefresh={() => fetchList()}
@@ -239,7 +239,13 @@ export default function PartRegistPage() {
gridId="development-part-regist"
showColumnSettings
paginationStyle="range"
pageSizeOptions={[10, 15, 20, 50, 100]}
pageSizeOptions={[10, 15, 20, 50, 100, 200, 500]}
serverPaging
serverPage={filter.page ?? 1}
serverPageSize={filter.page_size ?? 50}
serverTotalItems={total}
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
summaryStats={partSummary}
systemColumnKeys={["revision", "status"]}
onRefresh={() => fetchList()}
@@ -201,7 +201,13 @@ export default function PartSearchPage() {
gridId="development-part-search"
showColumnSettings
paginationStyle="range"
pageSizeOptions={[10, 15, 20, 50, 100]}
pageSizeOptions={[10, 15, 20, 50, 100, 200, 500]}
serverPaging
serverPage={filter.page ?? 1}
serverPageSize={filter.page_size ?? 50}
serverTotalItems={total}
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
summaryStats={partSummary}
systemColumnKeys={["revision", "eo_no"]}
onRefresh={() => fetchList()}
@@ -51,27 +51,10 @@ const EMPTY_FILTER: MbomListFilter = {
search_req_del_date_from: "",
search_req_del_date_to: "",
page: 1,
page_size: 50,
page_size: 50, // DataGrid 서버 페이지네이션 — 페이지 변경/사이즈 변경 시 부모가 직접 재요청
};
const GRID_COLUMNS: DataGridColumn[] = [
{ key: "project_no", label: "프로젝트번호", width: "w-[140px]" },
{ key: "category_name", label: "주문유형", width: "w-[100px]", align: "center" },
{ key: "product_name", label: "제품구분", width: "w-[90px]", align: "center" },
{ key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" },
{ key: "receipt_date", label: "접수일", width: "w-[100px]", align: "center" },
{ key: "writer_name", label: "작성자", width: "w-[90px]", align: "center" },
{ key: "customer_name", label: "고객사", minWidth: "min-w-[160px]" },
{ key: "paid_type_name", label: "유/무상", width: "w-[80px]", align: "center" },
{ key: "part_no", label: "품번", width: "w-[150px]" },
{ key: "part_name", label: "품명", minWidth: "min-w-[180px]" },
{ key: "serial_no", label: "S/N", width: "w-[110px]", align: "center" },
{ key: "quantity", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true },
{ key: "req_del_date", label: "요청납기", width: "w-[100px]", align: "center" },
{ key: "customer_request", label: "고객사요청사항", minWidth: "min-w-[200px]" },
{ key: "mbom_status", label: "M-BOM", width: "w-[80px]", align: "center" },
{ key: "mbom_regdate", label: "최종저장일", width: "w-[100px]", align: "center" },
];
// 그리드 컬럼은 useMemo 로 컴포넌트 내부에서 생성 — onClick(openDialog) 캡처 위해.
export default function MbomMgmtPage() {
const [rows, setRows] = useState<MbomRow[]>([]);
@@ -124,11 +107,43 @@ export default function MbomMgmtPage() {
}, []);
// DataGrid 키 부여 (objid + part_no 조합 — 같은 프로젝트 다중 행 unique)
// mbom_has: folder 컬럼이 숫자 > 0 일 때 파랑. mbom_header_objid 가 있으면 저장된 M-BOM.
const gridRows = useMemo(
() => rows.map((r, i) => ({ ...r, id: `${r.objid}__${r.part_no ?? ""}__${i}` })),
() => rows.map((r, i) => ({
...r,
id: `${r.objid}__${r.part_no ?? ""}__${i}`,
mbom_has: r.mbom_header_objid ? 1 : 0,
})),
[rows]
);
const openMbomDialog = useCallback((row: any) => {
if (!row?.objid) return;
setDialogObjid(String(row.objid));
setDialogOpen(true);
}, []);
const GRID_COLUMNS: DataGridColumn[] = useMemo(() => ([
{ key: "project_no", label: "프로젝트번호", width: "w-[140px]" },
{ key: "category_name", label: "주문유형", width: "w-[100px]", align: "center" },
{ key: "product_name", label: "제품구분", width: "w-[90px]", align: "center" },
{ key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" },
{ key: "receipt_date", label: "접수일", width: "w-[100px]", align: "center" },
{ key: "writer_name", label: "작성자", width: "w-[90px]", align: "center" },
{ key: "customer_name", label: "고객사", minWidth: "min-w-[160px]" },
{ key: "paid_type_name", label: "유/무상", width: "w-[80px]", align: "center" },
{ key: "part_no", label: "품번", width: "w-[150px]" },
{ key: "part_name", label: "품명", minWidth: "min-w-[180px]" },
{ key: "serial_no", label: "S/N", width: "w-[110px]", align: "center" },
{ key: "quantity", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true },
{ key: "req_del_date", label: "요청납기", width: "w-[100px]", align: "center" },
{ key: "customer_request", label: "고객사요청사항", minWidth: "min-w-[200px]" },
// M-BOM 컬럼 — 폴더 아이콘 (저장됨=파랑, 미저장=흰색). 클릭 시 본 편집 다이얼로그.
{ key: "mbom_has", label: "M-BOM", width: "w-[80px]", align: "center",
renderType: "folder", onClick: openMbomDialog },
{ key: "mbom_regdate", label: "최종저장일", width: "w-[100px]", align: "center" },
]), [openMbomDialog]);
const handleSearch = () => {
setFilter((f) => ({ ...f, page: 1 }));
fetchList({ page: 1 });
@@ -227,6 +242,14 @@ export default function MbomMgmtPage() {
showRowNumber
emptyMessage="조건에 맞는 프로젝트가 없습니다."
gridId="production-mbom-mgmt"
pageSizeOptions={[25, 50, 100, 200, 500]}
paginationStyle="range"
serverPaging
serverPage={filter.page ?? 1}
serverPageSize={filter.page_size ?? 50}
serverTotalItems={total}
onPageChange={(p) => { setFilter(f => ({ ...f, page: p })); fetchList({ page: p }); }}
onPageSizeChange={(n) => { setFilter(f => ({ ...f, page: 1, page_size: n })); fetchList({ page: 1, page_size: n }); }}
onRowDoubleClick={(row: any) => {
if (!row?.objid) return;
setDialogObjid(String(row.objid));
@@ -239,6 +262,7 @@ export default function MbomMgmtPage() {
open={dialogOpen}
onOpenChange={setDialogOpen}
projectObjid={dialogObjid}
onSaved={fetchList}
/>
</div>
);