20a429eecb
사용자 검증으로 발견된 5가지 함정 일괄 정정.
(1) ebom-search 검색폼 운영판 1:1 — wace structureAscendingList.jsp 노출 필드만:
- 제거: 프로젝트 OBJID (raw input), UNIT_CODE (raw input)
운영판도 고객사/프로젝트번호/유닛명 모두 주석 처리되어 노출 안 됨
- 유지: 품번 / 품명 / 표시 레벨 (1~5 select)
- BomTreeFilter.search_level 추가 + ascending/descending CTE 에 T.lev <= $search_level::int
(2) 품번/품명 자동완성 (wace select2-part 1:1):
- 영업관리 PartSelect 는 item_info 마스터 기반 → 개발관리(part_mng)용 별도 컴포넌트 신설
- backend GET /api/development/part/options : IS_LAST='1' part_mng 전체 (영업관리 sales/parts 패턴)
- frontend DevPartSelect.tsx : SmartSelect 캐시 + mode partNo/partName 분리
- ebom-search 페이지 단순 Input → DevPartSelect 교체
- 품번 선택 시 품명 자동 채움 / 품명 선택 시 품번 자동 채움 (운영판 select2-part 1:1)
(3) BomReportStatusDialog 운영판 1:1 재작성 — wace structureStatusChangePopup.jsp:
- 잘못된 점: read-only 박스 + 상태 select(create/changeDesign/deploy 3옵션)
- 정정: 5필드 모두 편집 가능 (CommCodeSelect 제품구분 / 품번 input / 품명 input /
Version input / 상태 Y/N 라디오) — 운영 매퍼 updateStructureStatus 5컬럼 UPDATE 1:1
- 헤더 파란 바 + 4컬럼 테이블(25%/75%) + 저장/닫기 중앙 배치 (운영판 스타일 1:1)
(4) DataGrid id 매핑 — 체크박스 ID 키 불일치 함정:
- DataGrid 는 row.id 로 체크박스 ID 관리, 백엔드 응답은 row.objid (postgres lowercase)
- 결과: checkedIds[0] 가 undefined → 상태변경/수정/삭제 다이얼로그가 objid=undefined 로 열려
detail 호출 안 됨 → 빈 폼 표시 (사용자 지적 "기본 정보 표시 안됨")
- 일괄 수정 (3 페이지) : ebom-regist / part-regist / part-search
gridRows = useMemo(() => rows.map(r => ({ ...r, id: r.objid })), [rows])
영업관리 페이지 동일 패턴 1:1
(5) STATUS_TITLE 매핑 운영판 1:1 — 운영 그리드는 'Y'/'N' 글자 그대로 표시:
CASE UPPER(T.STATUS)
WHEN 'CREATE' THEN '등록중'
WHEN 'CHANGEDESIGN' THEN '설계변경미배포'
WHEN 'DEPLOY' THEN '배포완료'
ELSE COALESCE(T.STATUS, '') END AS STATUS_TITLE
- 운영 매퍼는 ELSE '' 이지만 RPS 는 raw fallback (사용자 화면에서 식별 가능)
- 'Y'/'N' 매핑 라벨 추가 → 운영 스크린샷 확인 후 제거 (운영판은 raw)
미해결 (별 작업):
- 확정일 (DEPLOY_DATE) 표시 — 운영판은 별도 "배포" 액션 (deployBomReport 매퍼) 으로 채움.
RPS ebom-regist 에 배포 버튼 미구현 → 신규 BOM 확정일 빈값. 별 PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
// 개발관리 PART 자동완성 셀렉트 — wace select2-part 1:1 (영업관리 PartSelect 패턴 동일).
|
|
//
|
|
// 영업관리 PartSelect 는 item_info(영업 마스터) 기반.
|
|
// 개발관리는 part_mng 기반이므로 별도 컴포넌트.
|
|
//
|
|
// - part_mng IS_LAST='1' 전체를 한 번 캐시 (objid 기준 단일 소스)
|
|
// - mode='partNo' : 라벨로 part_no 표시
|
|
// - mode='partName': 라벨로 part_name 표시
|
|
// - 선택 시 onValueChange(part_no/part_name 텍스트, 원본 row) — ebom-search 가 LIKE 가 아닌
|
|
// 완전일치 검색을 하므로 value 는 텍스트 그대로 전달
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface DevPartRow {
|
|
objid: string;
|
|
part_no: string;
|
|
part_name: string;
|
|
}
|
|
|
|
interface DevPartSelectProps {
|
|
mode: "partNo" | "partName";
|
|
/** 현재 선택값 — part_no 또는 part_name 텍스트 */
|
|
value: string;
|
|
/** 선택 시 텍스트 + 원본 row */
|
|
onValueChange: (value: string, row?: DevPartRow) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
let cachedRows: DevPartRow[] | null = null;
|
|
let inflight: Promise<DevPartRow[]> | null = null;
|
|
|
|
const fetchParts = async (): Promise<DevPartRow[]> => {
|
|
if (cachedRows) return cachedRows;
|
|
if (inflight) return inflight;
|
|
inflight = (async () => {
|
|
const res = await apiClient.get("/development/part/options");
|
|
const rows = (res.data?.data?.rows ?? []) as any[];
|
|
cachedRows = rows
|
|
.filter((r) => r.objid != null)
|
|
.map((r) => ({
|
|
objid: String(r.objid),
|
|
part_no: r.part_no ?? "",
|
|
part_name: r.part_name ?? "",
|
|
}));
|
|
return cachedRows!;
|
|
})();
|
|
try {
|
|
return await inflight;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
};
|
|
|
|
const toOptions = (rows: DevPartRow[], mode: DevPartSelectProps["mode"]): SmartSelectOption[] => {
|
|
const seen = new Set<string>();
|
|
const result: SmartSelectOption[] = [];
|
|
for (const r of rows) {
|
|
const label = mode === "partNo" ? r.part_no : r.part_name;
|
|
if (!label || seen.has(label)) continue;
|
|
seen.add(label);
|
|
result.push({ code: label, label });
|
|
}
|
|
return result;
|
|
};
|
|
|
|
export function DevPartSelect({
|
|
mode, value, onValueChange,
|
|
placeholder = mode === "partNo" ? "품번 입력하여 검색..." : "품명 입력하여 검색...",
|
|
disabled, className,
|
|
}: DevPartSelectProps) {
|
|
const [options, setOptions] = useState<SmartSelectOption[]>(
|
|
cachedRows ? toOptions(cachedRows, mode) : [],
|
|
);
|
|
|
|
useEffect(() => {
|
|
let alive = true;
|
|
fetchParts()
|
|
.then((rows) => { if (alive) setOptions(toOptions(rows, mode)); })
|
|
.catch(() => {});
|
|
return () => { alive = false; };
|
|
}, [mode]);
|
|
|
|
return (
|
|
<SmartSelect
|
|
options={options}
|
|
value={value}
|
|
onValueChange={(v) => {
|
|
const row = cachedRows?.find((r) => (mode === "partNo" ? r.part_no : r.part_name) === v);
|
|
onValueChange(v, row);
|
|
}}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|