개발관리>PART 등록·조회 메뉴 신설 (PR-A) — wace partMng 1:1 이식
backend (M1+M2):
- devPartService: listTemp/listRelease/getByObjid/create/update/deploy/removeMany
- partMngBaseSimple SELECT + 추가 15컬럼(acctfg/odrfg/unit_dc/unitmang_dc/lot_fg 등) 라벨/CASE
- deploy 트랜잭션 3단계 (isLastInit → part_mng_history INSERT → partMngDeploy + EO_NO 채번)
- EO_NO 분기: is_longd='1'→EOB{yy}-{seq} / else EO{yy}-{seq}
- objidUtil: wace CommonUtils.createObjId() 1:1 (bigint objid 채번)
- DDL: 9 신규 테이블 + part_mng 15컬럼 ALTER (운영판 1:1 추출)
frontend (M1+M2):
- part-regist (M1) / part-search (M2): 23셀 그리드 + 검색폼 + 액션
- PartFormDialog: 등록/수정 통합 (mode prop, 4 섹션)
- PartDetailDialog: 읽기 전용 + "수정" dispatch
- AdminPageRenderer dynamic 임포트 2건 + menu_info URL spec 정렬
본 PR 제외 (별 PR): 도면 다중 업로드, ERP 업로드, Excel Import, BOM_PART_QTY R/W
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -177,6 +177,7 @@ import salesSaleRoutes from "./routes/salesSaleRoutes"; // 영업관리>판매+
|
||||
import salesCommonRoutes from "./routes/salesCommonRoutes"; // 영업관리 4개 메뉴 공통 옵션 (codes/parts/customers)
|
||||
import projectMgmtRoutes from "./routes/projectMgmtRoutes"; // 프로젝트관리>진행관리 (wace_plm 도메인 이식)
|
||||
import wbsTemplateRoutes from "./routes/wbsTemplateRoutes"; // 프로젝트관리>제품구분_WBS관리 (wace_plm 도메인 이식)
|
||||
import devPartRoutes from "./routes/devPartRoutes"; // 개발관리>PART 등록/조회 (wace_plm 도메인 이식)
|
||||
import erpSyncRoutes from "./routes/erpSyncRoutes"; // ERP 마스터 동기화 (사원/부서/창고/거래처/계정과목)
|
||||
import ecrMngRoutes from "./routes/ecrMngRoutes"; // ECR(Engineering Change Request) 관리
|
||||
import customerCsRoutes from "./routes/customerCsRoutes"; // 고객 CS 관리
|
||||
@@ -423,6 +424,7 @@ app.use("/api/sales", salesSaleRoutes); // 영업관리>판매+매출 (wace_plm
|
||||
app.use("/api/sales", salesCommonRoutes); // 영업관리 공통 옵션 (codes/parts/customers)
|
||||
app.use("/api/project/progress", projectMgmtRoutes); // 프로젝트관리>진행관리 (wace_plm 도메인)
|
||||
app.use("/api/project/wbs-template", wbsTemplateRoutes); // 프로젝트관리>제품구분_WBS관리 (wace_plm 도메인)
|
||||
app.use("/api/development", devPartRoutes); // 개발관리>PART 등록/조회 (wace_plm 도메인)
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// ============================================================
|
||||
// 개발관리 PART (M1 등록 / M2 조회) 컨트롤러.
|
||||
// 라우트:
|
||||
// GET /api/development/part-temp/list (M1 그리드)
|
||||
// POST /api/development/part-temp/deploy (M1 → M2 확정)
|
||||
// GET /api/development/part/list (M2 그리드)
|
||||
// GET /api/development/part/:objid (단건 상세)
|
||||
// POST /api/development/part (신규 등록)
|
||||
// PUT /api/development/part/:objid (상세 수정)
|
||||
// DELETE /api/development/part (다중 삭제, body: { objids })
|
||||
// ============================================================
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as svc from "../services/devPartService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
function parseListFilter(q: Record<string, any>): svc.PartListFilter {
|
||||
const filter: svc.PartListFilter = { ...q };
|
||||
if (q.page) filter.page = Number(q.page);
|
||||
if (q.page_size) filter.page_size = Number(q.page_size);
|
||||
// status_arr 는 ?status_arr=a&status_arr=b 또는 콤마 직렬화 둘 다 수용
|
||||
if (q.status_arr) {
|
||||
if (Array.isArray(q.status_arr)) filter.status_arr = q.status_arr.map(String);
|
||||
else filter.status_arr = String(q.status_arr).split(",").map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
// ─── M1 그리드 ──────────────────────────────────────────────
|
||||
|
||||
export async function getTempList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.listTemp(parseListFilter(req.query as Record<string, any>));
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("개발관리 PART(M1) 목록 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── M2 그리드 ──────────────────────────────────────────────
|
||||
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.listRelease(parseListFilter(req.query as Record<string, any>));
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("개발관리 PART(M2) 목록 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 단건 상세 ──────────────────────────────────────────────
|
||||
|
||||
export async function getByObjid(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { objid } = req.params;
|
||||
const row = await svc.getByObjid(objid);
|
||||
if (!row) return res.status(404).json({ success: false, message: "not_found" });
|
||||
return res.json({ success: true, data: row });
|
||||
} catch (e: any) {
|
||||
logger.error("개발관리 PART 상세 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 신규 등록 ──────────────────────────────────────────────
|
||||
|
||||
export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const objid = await svc.create(userId, req.body);
|
||||
return res.status(201).json({ success: true, data: { objid }, message: "PART가 등록되었습니다." });
|
||||
} catch (e: any) {
|
||||
logger.error("개발관리 PART 등록 실패", { error: e.message });
|
||||
return res.status(400).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 상세 수정 ──────────────────────────────────────────────
|
||||
|
||||
export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { objid } = req.params;
|
||||
const rowCount = await svc.update(objid, req.body);
|
||||
if (rowCount === 0) return res.status(404).json({ success: false, message: "not_found" });
|
||||
return res.json({ success: true, message: "PART가 수정되었습니다." });
|
||||
} catch (e: any) {
|
||||
logger.error("개발관리 PART 수정 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 확정 (M1 → M2) ─────────────────────────────────────────
|
||||
|
||||
export async function deploy(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const objids = Array.isArray(req.body?.objids) ? (req.body.objids as any[]).map(String) : [];
|
||||
if (objids.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "objids가 비어있습니다." });
|
||||
}
|
||||
const result = await svc.deploy(userId, objids);
|
||||
return res.json({ success: true, data: result, message: `${result.deployed}건이 확정되었습니다.` });
|
||||
} catch (e: any) {
|
||||
logger.error("개발관리 PART 확정 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 다중 삭제 ──────────────────────────────────────────────
|
||||
|
||||
export async function removeMany(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const objids = Array.isArray(req.body?.objids) ? (req.body.objids as any[]).map(String) : [];
|
||||
if (objids.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "objids가 비어있습니다." });
|
||||
}
|
||||
const removed = await svc.removeMany(objids);
|
||||
return res.json({ success: true, data: { removed }, message: `${removed}건이 삭제되었습니다.` });
|
||||
} catch (e: any) {
|
||||
logger.error("개발관리 PART 삭제 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// ============================================================
|
||||
// 개발관리 PART (M1 등록 / M2 조회) 라우트.
|
||||
// app.ts: app.use("/api/development", devPartRoutes)
|
||||
// ============================================================
|
||||
|
||||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import * as ctrl from "../controllers/devPartController";
|
||||
|
||||
const router = Router();
|
||||
router.use(authenticateToken);
|
||||
|
||||
// M1 — 임시(등록) 그리드
|
||||
router.get("/part-temp/list", ctrl.getTempList);
|
||||
router.post("/part-temp/deploy", ctrl.deploy);
|
||||
|
||||
// M2 — 릴리즈 그리드
|
||||
router.get("/part/list", ctrl.getList);
|
||||
|
||||
// 다중 삭제 (body: { objids: string[] }) — /:objid 보다 위
|
||||
router.delete("/part", ctrl.removeMany);
|
||||
|
||||
// 단건 + CRUD
|
||||
router.get("/part/:objid", ctrl.getByObjid);
|
||||
router.post("/part", ctrl.create);
|
||||
router.put("/part/:objid", ctrl.update);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,513 @@
|
||||
// ============================================================
|
||||
// 개발관리 PART (M1 등록 / M2 조회) — wace_plm partMng.xml 1:1 이식.
|
||||
//
|
||||
// 매퍼 매핑 (원본: wace_plm/src/com/pms/mapper/partMng.xml):
|
||||
// partMngTempGridList → listTemp() (M1, status != 'release')
|
||||
// partMngGridList → listRelease() (M2, status = 'release')
|
||||
// partMngInfo → getByObjid()
|
||||
// insertpartInfo → create() (38 컬럼 INSERT, wace 24 + 추가 15)
|
||||
// updatePartDetail → update() (21 컬럼 UPDATE)
|
||||
// partMngDeploy → deploy() (3-step 트랜잭션: isLastInit + history + deploy)
|
||||
// partMngIsLastInit → (deploy 내부)
|
||||
// insertPartMngHistory → (deploy 내부)
|
||||
// partMngDelete → removeMany()
|
||||
//
|
||||
// 채번 정책: part_mng.objid 는 bigint. 클라이언트가 part_objid 안 보내면
|
||||
// wace CommonUtils.createObjId() 1:1 구현(objidUtil.createObjId) 사용.
|
||||
//
|
||||
// EO_NO 채번: IS_LONGD='1' 이면 EOB{yy}-{seq} / 아니면 EO{yy}-{seq}. wace 그대로.
|
||||
// ============================================================
|
||||
|
||||
import { PoolClient } from "pg";
|
||||
import { getPool, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { createObjId } from "../utils/objidUtil";
|
||||
import { PART_BASE_SIMPLE } from "./devPartSqlFragments";
|
||||
|
||||
// ─── 필터/바디 타입 ──────────────────────────────────────────
|
||||
|
||||
export interface PartTempListFilter {
|
||||
search_part_no?: string;
|
||||
search_part_name?: string;
|
||||
search_material?: string;
|
||||
search_spec?: string;
|
||||
search_part_type?: string;
|
||||
writer?: string;
|
||||
status?: string;
|
||||
status_arr?: string[];
|
||||
product_code?: string;
|
||||
upg_no?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export interface PartListFilter extends PartTempListFilter {
|
||||
search_year?: string;
|
||||
search_hardness?: string;
|
||||
search_method?: string;
|
||||
search_surface?: string;
|
||||
customer_objid?: string;
|
||||
customer_cd?: string;
|
||||
project_name?: string;
|
||||
unit_code?: string;
|
||||
search_design_date_from?: string;
|
||||
search_design_date_to?: string;
|
||||
is_last?: string;
|
||||
eo?: string;
|
||||
}
|
||||
|
||||
export interface PartCreateBody {
|
||||
part_objid?: string;
|
||||
part_no: string;
|
||||
part_name: string;
|
||||
unit?: string;
|
||||
qty?: string;
|
||||
spec?: string;
|
||||
material?: string;
|
||||
thickness?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
out_diameter?: string;
|
||||
in_diameter?: string;
|
||||
length?: string;
|
||||
remark?: string;
|
||||
part_type: string;
|
||||
product_mgmt_objid?: string;
|
||||
supply_code?: string;
|
||||
maker?: string;
|
||||
contract_objid?: string;
|
||||
post_processing?: string;
|
||||
heat_treatment_hardness?: string;
|
||||
heat_treatment_method?: string;
|
||||
surface_treatment?: string;
|
||||
acctfg?: string;
|
||||
odrfg?: string;
|
||||
unit_dc?: string;
|
||||
unitmang_dc?: string;
|
||||
unitchng_nb?: string | number;
|
||||
lot_fg?: string;
|
||||
use_yn?: string;
|
||||
qc_fg?: string;
|
||||
setitem_fg?: string;
|
||||
req_fg?: string;
|
||||
unit_length?: string;
|
||||
unit_qty?: string;
|
||||
}
|
||||
|
||||
export interface PartUpdateBody {
|
||||
part_name?: string;
|
||||
material?: string;
|
||||
heat_treatment_hardness?: string;
|
||||
heat_treatment_method?: string;
|
||||
surface_treatment?: string;
|
||||
maker?: string;
|
||||
part_type?: string;
|
||||
acctfg?: string;
|
||||
odrfg?: string;
|
||||
spec?: string;
|
||||
unit_dc?: string;
|
||||
unitmang_dc?: string;
|
||||
unitchng_nb?: string | number;
|
||||
lot_fg?: string;
|
||||
use_yn?: string;
|
||||
qc_fg?: string;
|
||||
setitem_fg?: string;
|
||||
req_fg?: string;
|
||||
unit_length?: string;
|
||||
unit_qty?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// ─── 공용 검색 절 생성 ──────────────────────────────────────
|
||||
|
||||
function buildCommonWhere(
|
||||
filter: PartTempListFilter & Partial<PartListFilter>,
|
||||
startIdx: number
|
||||
): { sql: string; params: any[] } {
|
||||
const params: any[] = [];
|
||||
const conds: string[] = [];
|
||||
let idx = startIdx;
|
||||
|
||||
if (filter.product_code) {
|
||||
conds.push(`T.PRODUCT_MGMT_OBJID = $${idx++}`);
|
||||
params.push(filter.product_code);
|
||||
}
|
||||
if (filter.upg_no) {
|
||||
conds.push(`T.UPG_NO = $${idx++}`);
|
||||
params.push(filter.upg_no);
|
||||
}
|
||||
if (filter.search_part_no) {
|
||||
conds.push(`UPPER(T.PART_NO) LIKE UPPER($${idx++})`);
|
||||
params.push(`%${filter.search_part_no}%`);
|
||||
}
|
||||
if (filter.search_part_name) {
|
||||
conds.push(`UPPER(T.PART_NAME) LIKE UPPER($${idx++})`);
|
||||
params.push(`%${filter.search_part_name}%`);
|
||||
}
|
||||
if (filter.search_material) {
|
||||
conds.push(`UPPER(T.MATERIAL) LIKE UPPER($${idx++})`);
|
||||
params.push(`%${filter.search_material}%`);
|
||||
}
|
||||
if (filter.search_spec) {
|
||||
conds.push(`UPPER(T.SPEC) LIKE UPPER($${idx++})`);
|
||||
params.push(`%${filter.search_spec}%`);
|
||||
}
|
||||
if (filter.search_part_type) {
|
||||
conds.push(`T.PART_TYPE = $${idx++}`);
|
||||
params.push(filter.search_part_type);
|
||||
}
|
||||
if (filter.writer) {
|
||||
conds.push(`T.WRITER = $${idx++}`);
|
||||
params.push(filter.writer);
|
||||
}
|
||||
if (filter.status) {
|
||||
conds.push(`T.STATUS = $${idx++}`);
|
||||
params.push(filter.status);
|
||||
}
|
||||
if (filter.status_arr && filter.status_arr.length > 0) {
|
||||
const placeholders = filter.status_arr.map(() => `$${idx++}`).join(",");
|
||||
conds.push(`T.STATUS IN (${placeholders})`);
|
||||
params.push(...filter.status_arr);
|
||||
}
|
||||
|
||||
// M2 전용 추가 필터
|
||||
if (filter.search_design_date_from) {
|
||||
conds.push(`TO_DATE(T.DESIGN_DATE, 'YYYY-MM-DD') >= $${idx++}::timestamp`);
|
||||
params.push(filter.search_design_date_from);
|
||||
}
|
||||
if (filter.search_design_date_to) {
|
||||
conds.push(`TO_DATE(T.DESIGN_DATE, 'YYYY-MM-DD') <= $${idx++}::timestamp`);
|
||||
params.push(filter.search_design_date_to);
|
||||
}
|
||||
if (filter.is_last) {
|
||||
conds.push(`T.IS_LAST = $${idx++}`);
|
||||
params.push(filter.is_last);
|
||||
}
|
||||
if (filter.eo) {
|
||||
// wace 원본: AND T.EO_TEMP IS NULL OR EO_TEMP = '' — eo=1일 때만 적용
|
||||
conds.push(`(T.EO_TEMP IS NULL OR T.EO_TEMP = '')`);
|
||||
}
|
||||
|
||||
return { sql: conds.length ? conds.join(" AND ") : "1=1", params };
|
||||
}
|
||||
|
||||
function paginate(filter: { page?: number; page_size?: number }): { limit: number; offset: number; page: number; pageSize: number } {
|
||||
const page = Math.max(1, Number(filter.page) || 1);
|
||||
const pageSize = Math.min(500, Math.max(1, Number(filter.page_size) || 20));
|
||||
return { limit: pageSize, offset: (page - 1) * pageSize, page, pageSize };
|
||||
}
|
||||
|
||||
// ─── M1 그리드 (status != 'release') ────────────────────────
|
||||
|
||||
export async function listTemp(filter: PartTempListFilter) {
|
||||
const { limit, offset, page, pageSize } = paginate(filter);
|
||||
const where = buildCommonWhere(filter, 1);
|
||||
|
||||
// M1 추가 컬럼: PARTNER_TITLE, BOM_REPORT_OBJID/CHILD_OBJID/QTY/QTY_TEMP, Q_QTY, PARENT_PART_INFO, SORT
|
||||
const baseSql = `
|
||||
SELECT T.*,
|
||||
CASE WHEN T.REVISION IS NULL THEN '0' ELSE T.REVISION END AS SORT,
|
||||
O.PARTNER_TITLE,
|
||||
(SELECT PART_NO FROM PART_MNG SP WHERE SP.OBJID::varchar = Q.PARENT_PART_NO) AS PARENT_PART_INFO,
|
||||
Q.BOM_REPORT_OBJID,
|
||||
Q.OBJID AS OBJID_QTY,
|
||||
Q.CHILD_OBJID,
|
||||
Q.QTY AS Q_QTY_RAW,
|
||||
Q.QTY_TEMP,
|
||||
(CASE
|
||||
WHEN Q.STATUS = 'deploy' THEN Q.QTY
|
||||
WHEN (Q.QTY_TEMP IS NULL OR Q.QTY_TEMP = '') THEN Q.QTY
|
||||
ELSE Q.QTY_TEMP
|
||||
END) AS Q_QTY
|
||||
FROM (${PART_BASE_SIMPLE}) T
|
||||
LEFT JOIN (
|
||||
SELECT PART_OBJID,
|
||||
ARRAY_TO_STRING(ARRAY_AGG(PARTNER_TITLE ORDER BY SEQ), ',') AS PARTNER_TITLE
|
||||
FROM (
|
||||
SELECT OSM.PART_OBJID, OSM.SEQ,
|
||||
OSM.SEQ || '. ' || (SELECT SUPPLY_NAME FROM ADMIN_SUPPLY_MNG
|
||||
WHERE OBJID::varchar = OSM.PARTNER_OBJID::varchar) AS PARTNER_TITLE
|
||||
FROM ORDER_SPEC_MNG OSM
|
||||
) OSMO
|
||||
GROUP BY PART_OBJID
|
||||
) O ON T.OBJID::varchar = O.PART_OBJID::varchar
|
||||
LEFT JOIN BOM_PART_QTY Q ON (
|
||||
T.OBJID::varchar IN (
|
||||
SELECT DISTINCT PM1.OBJID::varchar
|
||||
FROM PART_MNG PM1, PART_MNG PM2
|
||||
WHERE PM1.STATUS = 'changing'
|
||||
AND PM2.OBJID::varchar = Q.PART_NO
|
||||
AND PM1.PART_NO = PM2.PART_NO
|
||||
)
|
||||
AND Q.STATUS = 'beforeEdit'
|
||||
)
|
||||
WHERE ${where.sql}
|
||||
AND COALESCE(T.STATUS, '') <> 'release'
|
||||
ORDER BY PARENT_PART_INFO NULLS LAST, T.PART_NO
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
const dataSql = `${baseSql} LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}`;
|
||||
const countSql = `SELECT COUNT(*)::int AS total FROM (${baseSql}) X`;
|
||||
|
||||
const [dataRes, countRes] = await Promise.all([
|
||||
pool.query(dataSql, [...where.params, limit, offset]),
|
||||
pool.query(countSql, where.params),
|
||||
]);
|
||||
|
||||
return {
|
||||
rows: dataRes.rows,
|
||||
total: countRes.rows[0]?.total ?? 0,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── M2 그리드 (status = 'release') ─────────────────────────
|
||||
|
||||
export async function listRelease(filter: PartListFilter) {
|
||||
const { limit, offset, page, pageSize } = paginate(filter);
|
||||
const where = buildCommonWhere(filter, 1);
|
||||
|
||||
const baseSql = `
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY T.PART_NO,
|
||||
CASE WHEN T.REVISION LIKE 'RE%' THEN 0 ELSE 1 END,
|
||||
T.REVISION DESC
|
||||
) AS NUM,
|
||||
T.*,
|
||||
/* wace 1:1 — PART_TYPE='0000063'(SET) 면 '1', 그 외엔 BOM_QTY 합 */
|
||||
CASE WHEN T.PART_TYPE = '0000063' THEN '1'
|
||||
ELSE (SELECT SUM(CASE WHEN COALESCE(Q.QTY,'') = '' THEN 0 ELSE Q.QTY::numeric END)::varchar
|
||||
FROM BOM_PART_QTY Q
|
||||
WHERE Q.LAST_PART_OBJID = T.OBJID::varchar)
|
||||
END AS BOM_QTY
|
||||
FROM (${PART_BASE_SIMPLE}) T
|
||||
WHERE ${where.sql}
|
||||
AND T.STATUS = 'release'
|
||||
ORDER BY T.PART_NO,
|
||||
CASE WHEN T.REVISION LIKE 'RE%' THEN 0 ELSE 1 END,
|
||||
T.REVISION DESC
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
const dataSql = `${baseSql} LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}`;
|
||||
const countSql = `SELECT COUNT(*)::int AS total FROM (${baseSql}) X`;
|
||||
|
||||
const [dataRes, countRes] = await Promise.all([
|
||||
pool.query(dataSql, [...where.params, limit, offset]),
|
||||
pool.query(countSql, where.params),
|
||||
]);
|
||||
|
||||
return {
|
||||
rows: dataRes.rows,
|
||||
total: countRes.rows[0]?.total ?? 0,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 단건 상세 ──────────────────────────────────────────────
|
||||
|
||||
export async function getByObjid(objid: string) {
|
||||
const sql = `SELECT T.* FROM (${PART_BASE_SIMPLE}) T WHERE T.OBJID = $1`;
|
||||
const r = await getPool().query(sql, [objid]);
|
||||
return r.rows[0] ?? null;
|
||||
}
|
||||
|
||||
// ─── 신규 등록 (insertpartInfo + 15 추가컬럼) ───────────────
|
||||
|
||||
export async function create(userId: string, body: PartCreateBody): Promise<string> {
|
||||
if (!body.part_no || !body.part_name) {
|
||||
throw new Error("필수값 누락: part_no, part_name");
|
||||
}
|
||||
const partObjid = body.part_objid && String(body.part_objid).trim() !== ""
|
||||
? String(body.part_objid)
|
||||
: createObjId();
|
||||
|
||||
const sql = `
|
||||
INSERT INTO PART_MNG (
|
||||
OBJID, PART_NO, PART_NAME, UNIT, QTY, SPEC, MATERIAL,
|
||||
THICKNESS, WIDTH, HEIGHT, OUT_DIAMETER, IN_DIAMETER, LENGTH,
|
||||
REMARK, STATUS, REG_DATE, WRITER, IS_LAST,
|
||||
PART_TYPE, PRODUCT_MGMT_OBJID, SUPPLY_CODE, MAKER, CONTRACT_OBJID, POST_PROCESSING,
|
||||
HEAT_TREATMENT_HARDNESS, HEAT_TREATMENT_METHOD, SURFACE_TREATMENT,
|
||||
ACCTFG, ODRFG, UNIT_DC, UNITMANG_DC, UNITCHNG_NB,
|
||||
LOT_FG, USE_YN, QC_FG, SETITEM_FG, REQ_FG, UNIT_LENGTH, UNIT_QTY
|
||||
) VALUES (
|
||||
$1::numeric, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12, $13,
|
||||
$14, 'create', NOW(), $15, '1',
|
||||
$16, $17, $18, $19, $20, $21,
|
||||
$22, $23, $24,
|
||||
$25, $26, $27, $28, $29,
|
||||
$30, $31, $32, $33, $34, $35, $36
|
||||
)
|
||||
`;
|
||||
|
||||
const params = [
|
||||
partObjid,
|
||||
body.part_no, body.part_name, body.unit ?? null, body.qty ?? null, body.spec ?? null, body.material ?? null,
|
||||
body.thickness ?? null, body.width ?? null, body.height ?? null, body.out_diameter ?? null, body.in_diameter ?? null, body.length ?? null,
|
||||
body.remark ?? null, userId,
|
||||
body.part_type, body.product_mgmt_objid ?? null, body.supply_code ?? null, body.maker ?? null, body.contract_objid ?? null, body.post_processing ?? null,
|
||||
body.heat_treatment_hardness ?? null, body.heat_treatment_method ?? null, body.surface_treatment ?? null,
|
||||
body.acctfg ?? null, body.odrfg ?? null, body.unit_dc ?? null, body.unitmang_dc ?? null, body.unitchng_nb ?? null,
|
||||
body.lot_fg ?? null, body.use_yn ?? null, body.qc_fg ?? null, body.setitem_fg ?? null, body.req_fg ?? null, body.unit_length ?? null, body.unit_qty ?? null,
|
||||
];
|
||||
|
||||
await getPool().query(sql, params);
|
||||
return partObjid;
|
||||
}
|
||||
|
||||
// ─── 상세 수정 (updatePartDetail, 21 컬럼) ──────────────────
|
||||
|
||||
export async function update(objid: string, body: PartUpdateBody): Promise<number> {
|
||||
const sql = `
|
||||
UPDATE PART_MNG SET
|
||||
PART_NAME = $1,
|
||||
MATERIAL = $2,
|
||||
HEAT_TREATMENT_HARDNESS = $3,
|
||||
HEAT_TREATMENT_METHOD = $4,
|
||||
SURFACE_TREATMENT = $5,
|
||||
MAKER = $6,
|
||||
PART_TYPE = $7,
|
||||
ACCTFG = $8,
|
||||
ODRFG = $9,
|
||||
SPEC = $10,
|
||||
UNIT_DC = $11,
|
||||
UNITMANG_DC = $12,
|
||||
UNITCHNG_NB = $13,
|
||||
LOT_FG = $14,
|
||||
USE_YN = $15,
|
||||
QC_FG = $16,
|
||||
SETITEM_FG = $17,
|
||||
REQ_FG = $18,
|
||||
UNIT_LENGTH = $19,
|
||||
UNIT_QTY = $20,
|
||||
REMARK = $21,
|
||||
EDIT_DATE = NOW()
|
||||
WHERE OBJID = $22
|
||||
`;
|
||||
const r = await getPool().query(sql, [
|
||||
body.part_name ?? null, body.material ?? null,
|
||||
body.heat_treatment_hardness ?? null, body.heat_treatment_method ?? null, body.surface_treatment ?? null,
|
||||
body.maker ?? null, body.part_type ?? null,
|
||||
body.acctfg ?? null, body.odrfg ?? null, body.spec ?? null,
|
||||
body.unit_dc ?? null, body.unitmang_dc ?? null,
|
||||
body.unitchng_nb ?? null,
|
||||
body.lot_fg ?? null, body.use_yn ?? null, body.qc_fg ?? null,
|
||||
body.setitem_fg ?? null, body.req_fg ?? null,
|
||||
body.unit_length ?? null, body.unit_qty ?? null,
|
||||
body.remark ?? null,
|
||||
objid,
|
||||
]);
|
||||
return r.rowCount ?? 0;
|
||||
}
|
||||
|
||||
// ─── 확정 (M1→M2): 다중 objids 순차 트랜잭션 ────────────────
|
||||
|
||||
export async function deploy(userId: string, objids: string[]): Promise<{ deployed: number; eo_nos: Record<string, string> }> {
|
||||
if (!objids || objids.length === 0) return { deployed: 0, eo_nos: {} };
|
||||
|
||||
const eoNos: Record<string, string> = {};
|
||||
let deployed = 0;
|
||||
|
||||
await transaction(async (client: PoolClient) => {
|
||||
for (const objid of objids) {
|
||||
// 1) 동일 PART_NO 모두 IS_LAST='0' (wace partMngIsLastInit 1:1)
|
||||
await client.query(
|
||||
`UPDATE PART_MNG SET IS_LAST = '0', EDIT_DATE = NOW()
|
||||
WHERE PART_NO = (SELECT PART_NO FROM PART_MNG WHERE OBJID = $1)`,
|
||||
[objid]
|
||||
);
|
||||
|
||||
// 2) PART_MNG_HISTORY 이력 INSERT (wace insertPartMngHistory 1:1, BOM_PART_QTY 미연계)
|
||||
// BOM 컨텍스트(CHILD_OBJID/CHANGE_OPTION 등)는 deploy 단계에선 NULL.
|
||||
await client.query(
|
||||
`INSERT INTO PART_MNG_HISTORY (
|
||||
objid, product_mgmt_objid, upg_no, part_no, part_name, unit,
|
||||
qty, spec, material, weight, part_type, remark,
|
||||
es_spec, ms_spec, change_option, design_apply_point, management_flag,
|
||||
revision, status, reg_date, edit_date, writer, is_last,
|
||||
eo_no, eo_temp, excel_upload_seq, sourcing_code, sub_material,
|
||||
parent_part_no, design_date, eo_date, deploy_date,
|
||||
thickness, width, height, out_diameter, in_diameter, length,
|
||||
supply_code, change_type, contract_objid, maker,
|
||||
his_reg_date, his_writer, his_status,
|
||||
heat_treatment_hardness, heat_treatment_method, surface_treatment
|
||||
)
|
||||
SELECT
|
||||
P.OBJID::numeric, P.PRODUCT_MGMT_OBJID, P.UPG_NO, P.PART_NO, P.PART_NAME, P.UNIT,
|
||||
P.QTY, P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REMARK,
|
||||
P.ES_SPEC, P.MS_SPEC, P.CHANGE_OPTION, P.DESIGN_APPLY_POINT, P.MANAGEMENT_FLAG,
|
||||
P.REVISION, P.STATUS, P.REG_DATE, NOW(), $2, P.IS_LAST,
|
||||
P.EO_NO, P.EO_TEMP, P.EXCEL_UPLOAD_SEQ::varchar, P.SOURCING_CODE, P.SUB_MATERIAL,
|
||||
P.PARENT_PART_NO, P.DESIGN_DATE, P.EO_DATE, P.DEPLOY_DATE,
|
||||
P.THICKNESS, P.WIDTH, P.HEIGHT, P.OUT_DIAMETER, P.IN_DIAMETER, P.LENGTH,
|
||||
P.SUPPLY_CODE, P.CHANGE_TYPE, P.CONTRACT_OBJID, P.MAKER,
|
||||
NOW(), $2, 'DEPLOY',
|
||||
P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT
|
||||
FROM PART_MNG P
|
||||
WHERE P.OBJID = $1`,
|
||||
[objid, userId]
|
||||
);
|
||||
|
||||
// 3) 본 행 deploy: IS_LAST='1', STATUS='release', DEPLOY_DATE=NOW(),
|
||||
// REVISION=COALESCE→'RE', EO_DATE=오늘, EO_NO=신규 채번 (IS_LONGD에 따라 EOB/EO 분기).
|
||||
const deployRes = await client.query<{ eo_no: string }>(
|
||||
`UPDATE PART_MNG P SET
|
||||
IS_LAST = '1',
|
||||
EDIT_DATE = NOW(),
|
||||
DEPLOY_DATE = NOW(),
|
||||
STATUS = 'release',
|
||||
REVISION = (CASE WHEN COALESCE(P.REVISION,'') = '' THEN 'RE' ELSE P.REVISION END),
|
||||
EO_DATE = TO_CHAR(NOW(),'YYYY-MM-DD'),
|
||||
EO_NO = (
|
||||
CASE WHEN P.IS_LONGD = '1' THEN
|
||||
'EOB' || TO_CHAR(NOW(),'yy') || '-' || LPAD(
|
||||
(SELECT COALESCE(SUBSTR(MAX(SP.EO_NO), 7, 8)::integer + 1, 1)::varchar
|
||||
FROM PART_MNG SP
|
||||
WHERE SP.EO_NO LIKE 'EOB' || TO_CHAR(NOW(),'yy') || '-%'
|
||||
AND SP.PART_NO <> P.PART_NO
|
||||
AND COALESCE(SP.REVISION, '') <> COALESCE(P.REVISION, '')
|
||||
), 4, '0')
|
||||
ELSE
|
||||
'EO' || TO_CHAR(NOW(),'yy') || '-' || LPAD(
|
||||
(SELECT COALESCE(SUBSTR(MAX(SP.EO_NO), 6, 8)::integer + 1, 1)::varchar
|
||||
FROM PART_MNG SP
|
||||
WHERE SP.EO_NO IS NOT NULL
|
||||
AND SP.EO_NO NOT LIKE 'EOB%'
|
||||
AND SP.PART_NO <> P.PART_NO
|
||||
AND COALESCE(SP.REVISION, '') <> COALESCE(P.REVISION, '')
|
||||
), 4, '0')
|
||||
END
|
||||
)
|
||||
WHERE OBJID = $1
|
||||
RETURNING EO_NO AS eo_no`,
|
||||
[objid]
|
||||
);
|
||||
|
||||
if (deployRes.rowCount && deployRes.rowCount > 0) {
|
||||
deployed += deployRes.rowCount;
|
||||
eoNos[objid] = deployRes.rows[0]?.eo_no ?? "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("PART deploy 완료", { count: deployed, userId });
|
||||
return { deployed, eo_nos: eoNos };
|
||||
}
|
||||
|
||||
// ─── 다중 삭제 (wace partMngDelete — POSITION 트릭 → ANY 배열) ─
|
||||
|
||||
export async function removeMany(objids: string[]): Promise<number> {
|
||||
if (!objids || objids.length === 0) return 0;
|
||||
// bigint 컬럼이므로 numeric[] 캐스팅
|
||||
const r = await getPool().query(
|
||||
`DELETE FROM PART_MNG WHERE OBJID = ANY($1::numeric[])`,
|
||||
[objids]
|
||||
);
|
||||
return r.rowCount ?? 0;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// ============================================================
|
||||
// 개발관리 PART (M1·M2·상세) 공용 SELECT fragment.
|
||||
// wace_plm/src/com/pms/mapper/partMng.xml#partMngBaseSimple 1:1 이식 +
|
||||
// vexplor_rps part_mng 의 15 추가컬럼(acctfg/odrfg/unit_dc/unitmang_dc/
|
||||
// lot_fg/use_yn/qc_fg/setitem_fg/req_fg/unit_length/unit_qty/
|
||||
// heat_treatment_hardness/heat_treatment_method/surface_treatment/unitchng_nb)
|
||||
// 의 *_NM(comm_code 라벨) / Y/N CASE 변환 추가.
|
||||
//
|
||||
// 검색/페이징은 호출 측에서 WHERE 절·LIMIT/OFFSET 만 덧붙여 사용.
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* partMngBaseSimple — wace 운영판 1:1.
|
||||
* SELECT 컬럼만 정의. 호출 측에서 `${PART_BASE_SIMPLE} WHERE … ORDER BY …` 형태로 조합.
|
||||
*/
|
||||
export const PART_BASE_SIMPLE = `
|
||||
SELECT
|
||||
-- 기본 컬럼 (wace partMngBaseSimple 1:1)
|
||||
P.OBJID,
|
||||
P.PART_NO,
|
||||
P.PART_NAME,
|
||||
P.PRODUCT_MGMT_OBJID,
|
||||
P.UPG_NO,
|
||||
P.UNIT,
|
||||
CC_UNIT.code_name AS UNIT_TITLE,
|
||||
COALESCE(
|
||||
(SELECT QTY FROM BOM_PART_QTY Q
|
||||
WHERE Q.LAST_PART_OBJID = P.OBJID::varchar
|
||||
AND Q.STATUS = 'deploy'
|
||||
ORDER BY Q.DEPLOY_DATE DESC LIMIT 1),
|
||||
P.QTY
|
||||
) AS QTY,
|
||||
P.SPEC,
|
||||
P.POST_PROCESSING,
|
||||
P.MATERIAL,
|
||||
P.WEIGHT,
|
||||
P.PART_TYPE,
|
||||
CC_PART.code_name AS PART_TYPE_TITLE,
|
||||
P.REMARK,
|
||||
P.ES_SPEC,
|
||||
P.MS_SPEC,
|
||||
P.CHANGE_TYPE,
|
||||
P.DESIGN_APPLY_POINT,
|
||||
P.CHANGE_OPTION,
|
||||
-- CHANGE_OPTION 다중 라벨 ARRAY_AGG (wace 1:1)
|
||||
(SELECT ARRAY_TO_STRING(ARRAY_AGG(CC.code_name), ',')
|
||||
FROM COMM_CODE CC
|
||||
WHERE CC.code_id IN (
|
||||
SELECT UNNEST(STRING_TO_ARRAY(P.CHANGE_OPTION, ','))
|
||||
)) AS CHANGE_OPTION_NAME,
|
||||
P.MANAGEMENT_FLAG,
|
||||
P.REVISION,
|
||||
P.STATUS,
|
||||
P.REG_DATE,
|
||||
TO_CHAR(P.REG_DATE, 'YYYY-MM-DD') AS PART_REGDATE_TITLE,
|
||||
P.EDIT_DATE,
|
||||
P.WRITER,
|
||||
P.IS_LAST,
|
||||
P.IS_LONGD,
|
||||
P.EO_DATE,
|
||||
P.EO_NO,
|
||||
P.EO_TEMP,
|
||||
P.MAKER,
|
||||
P.CONTRACT_OBJID,
|
||||
P.THICKNESS,
|
||||
P.WIDTH,
|
||||
P.HEIGHT,
|
||||
P.OUT_DIAMETER,
|
||||
P.IN_DIAMETER,
|
||||
P.LENGTH,
|
||||
P.SOURCING_CODE,
|
||||
P.SUPPLY_CODE,
|
||||
SUP.SUPPLY_NAME AS SUPPLY_NAME,
|
||||
P.SUB_MATERIAL,
|
||||
P.PARENT_PART_NO,
|
||||
P.DESIGN_DATE,
|
||||
P.DEPLOY_DATE,
|
||||
P.EXCEL_UPLOAD_SEQ,
|
||||
|
||||
-- 첨부 파일 카운트
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F
|
||||
WHERE F.TARGET_OBJID = P.OBJID::varchar AND F.DOC_TYPE = '3D_CAD') AS CU01_CNT,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F
|
||||
WHERE F.TARGET_OBJID = P.OBJID::varchar AND F.DOC_TYPE = '2D_DRAWING_CAD') AS CU02_CNT,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F
|
||||
WHERE F.TARGET_OBJID = P.OBJID::varchar AND F.DOC_TYPE = '2D_PDF_CAD') AS CU03_CNT,
|
||||
(SELECT COUNT(1) FROM ATTACH_FILE_INFO F
|
||||
WHERE F.TARGET_OBJID = P.OBJID::varchar
|
||||
AND F.DOC_TYPE IN ('2D_PDF_CAD','2D_DRAWING_CAD')) AS CU_TOTAL_CNT,
|
||||
|
||||
-- 추가 15컬럼(301_alter_part_mng.sql) + 라벨
|
||||
P.HEAT_TREATMENT_HARDNESS,
|
||||
P.HEAT_TREATMENT_METHOD,
|
||||
P.SURFACE_TREATMENT,
|
||||
P.ACCTFG,
|
||||
CC_ACCT.code_name AS ACCTFG_NM,
|
||||
P.ODRFG,
|
||||
CC_ODR.code_name AS ODRFG_NM,
|
||||
P.UNIT_DC,
|
||||
CC_UDC.code_name AS UNIT_DC_NM,
|
||||
P.UNITMANG_DC,
|
||||
CC_UMDC.code_name AS UNITMANG_DC_NM,
|
||||
P.UNITCHNG_NB,
|
||||
P.LOT_FG,
|
||||
CASE WHEN P.LOT_FG = '1' THEN '예' WHEN P.LOT_FG = '0' THEN '아니오' ELSE '' END AS LOT_FG_NM,
|
||||
P.USE_YN,
|
||||
CASE WHEN P.USE_YN = '1' THEN '예' WHEN P.USE_YN = '0' THEN '아니오' ELSE '' END AS USE_YN_NM,
|
||||
P.QC_FG,
|
||||
CASE WHEN P.QC_FG = '1' THEN '예' WHEN P.QC_FG = '0' THEN '아니오' ELSE '' END AS QC_FG_NM,
|
||||
P.SETITEM_FG,
|
||||
CASE WHEN P.SETITEM_FG = '1' THEN '예' WHEN P.SETITEM_FG = '0' THEN '아니오' ELSE '' END AS SETITEM_FG_NM,
|
||||
P.REQ_FG,
|
||||
CASE WHEN P.REQ_FG = '1' THEN '예' WHEN P.REQ_FG = '0' THEN '아니오' ELSE '' END AS REQ_FG_NM,
|
||||
P.UNIT_LENGTH,
|
||||
P.UNIT_QTY
|
||||
|
||||
FROM PART_MNG P
|
||||
LEFT JOIN COMM_CODE CC_UNIT ON CC_UNIT.code_id = P.UNIT AND CC_UNIT.status = 'active'
|
||||
LEFT JOIN COMM_CODE CC_PART ON CC_PART.code_id = P.PART_TYPE AND CC_PART.status = 'active'
|
||||
LEFT JOIN COMM_CODE CC_ACCT ON CC_ACCT.code_id = P.ACCTFG AND CC_ACCT.status = 'active'
|
||||
LEFT JOIN COMM_CODE CC_ODR ON CC_ODR.code_id = P.ODRFG AND CC_ODR.status = 'active'
|
||||
LEFT JOIN COMM_CODE CC_UDC ON CC_UDC.code_id = P.UNIT_DC AND CC_UDC.status = 'active'
|
||||
LEFT JOIN COMM_CODE CC_UMDC ON CC_UMDC.code_id = P.UNITMANG_DC AND CC_UMDC.status = 'active'
|
||||
LEFT JOIN admin_supply_mng SUP ON SUP.OBJID::varchar = P.SUPPLY_CODE
|
||||
`;
|
||||
@@ -0,0 +1,24 @@
|
||||
// ============================================================
|
||||
// part_mng / part_mng_history / attach_file_info 등 wace 운영판
|
||||
// `objid bigint`(또는 numeric) 컬럼 채번 유틸.
|
||||
//
|
||||
// wace java `com.pms.common.CommonUtils.createObjId()` 1:1 이식:
|
||||
// 1) UUID v4 생성
|
||||
// 2) 하이픈 제거 → 32 hex 문자열
|
||||
// 3) Java String.hashCode() (int32) 적용
|
||||
// 4) 결과 정수를 문자열로 반환
|
||||
// 결과 범위: -2,147,483,648 ~ 2,147,483,647 (Java int 범위).
|
||||
// ============================================================
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
function javaStringHashCode(s: string): number {
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
export function createObjId(): string {
|
||||
return String(javaStringHashCode(randomUUID().replace(/-/g, "")));
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
# 개발관리 이식 GAP 분석 (원본 wace_plm 대비)
|
||||
|
||||
> 작성: 2026-05-12 / 작성자: hjjeong
|
||||
> 대상 메뉴 5종 (1 도메인 `development/`):
|
||||
> - PART 등록 / PART 조회 / E-BOM 등록 / E-BOM 조회 / 설계변경 리스트
|
||||
> 원본 위치: `wace_plm/WebContent/WEB-INF/view/partMng/` (단일 디렉토리) + `mapper/partMng.xml` 단일 매퍼.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한 문장 요약
|
||||
|
||||
5개 메뉴 모두 wace `partMng/` 단일 디렉토리 + `partMng.xml` 매퍼에 1:1 매핑됨. 의존 테이블 15개 중 **6개 보유(`part_mng`/`comm_code`/`pms_wbs_task`/`project_mgmt`/`user_info`/`product_mgmt`)** · **9개 신규 추가 완료(`300_part_bom.sql`)** · **`part_mng`에 누락 15컬럼 ALTER 완료(`301_alter_part_mng.sql`)**. 5개 메뉴 모두 P1에서 실데이터 표시 가능.
|
||||
|
||||
## 0.1 이식 원칙
|
||||
|
||||
- JSP/매퍼XML 안의 `/* */`, `<!-- -->`, `//` 주석 블록은 비활성. 활성 코드만 이식.
|
||||
- `company_code` 멀티테넌시 분기는 vexplor_rps 측에 만들지 않음 (COMPANY_16 단독).
|
||||
- `CODE_NAME()`은 영업/프로젝트와 동일하게 `LEFT JOIN comm_code CC_X ON CC_X.code_id=...` 패턴 통일.
|
||||
- `client_mng`/`supply_mng` → vexplor는 `customer_mng`로 통합되어 있으나, 개발관리 5개 메뉴는 `customer_mng`를 직접 참조하지 않음(`project_mgmt.customer_objid` 경유). 분기 변환 불필요.
|
||||
- 금액 1,234.00 / 수량 1,234 / 모든 숫자 right-align (memory `feedback_number_format.md`).
|
||||
- wace JSP 컬럼 정의 끝의 주석 블록은 비활성 항목 — grep만으로 카운트하지 말 것 (memory `feedback_wace_jsp_columns.md`).
|
||||
|
||||
---
|
||||
|
||||
## 1. 메뉴 ↔ JSP ↔ 매퍼 1:1 매핑
|
||||
|
||||
| # | 메뉴 | wace JSP | 매퍼 쿼리 (partMng.xml) | LOC |
|
||||
|---|---|---|---|---:|
|
||||
| M1 | **PART 등록** | `partMngTempList.jsp` | `partMngTempGridList` (S), `partMngDeploy` (U), `partMngDelete` (D) | 649 |
|
||||
| M2 | **PART 조회** | `partMngList.jsp` | `partMngGridList` (S), `partMngDelete` (D), `partMngFormPopUp` (S) | 834 |
|
||||
| M3 | **E-BOM 등록** | `structureList.jsp` | `getBOMStandardStructureGridList` (S), `deleteStructure` (D), `structureStatusChange` (U) | 782 |
|
||||
| M4 | **E-BOM 조회** | `structureAscendingList.jsp` | `structureAscendingList`/`structureAscendingListExcel`/`structureDescendingExcelList` (S) | 1,064 |
|
||||
| M5 | **설계변경 리스트** | `partMngHisList.jsp` | `partMngHistList` (S, read-only) | 198 |
|
||||
|
||||
vexplor_rps 측 라우트(예정):
|
||||
|
||||
```
|
||||
GET /api/development/part-temp/list (M1 그리드)
|
||||
POST /api/development/part-temp/deploy (M1 확정)
|
||||
DEL /api/development/part-temp (M1·M2 삭제 공용)
|
||||
GET /api/development/part/list (M2 그리드)
|
||||
GET /api/development/part/:objid (M2 상세 팝업)
|
||||
GET /api/development/ebom/list (M3 그리드)
|
||||
PUT /api/development/ebom/status (M3 상태변경)
|
||||
DEL /api/development/ebom/:objid (M3 삭제)
|
||||
GET /api/development/ebom/ascending (M4 정전개)
|
||||
GET /api/development/ebom/descending (M4 역전개)
|
||||
GET /api/development/eo/history/list (M5 그리드)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 메뉴별 검색 필드 & 그리드 컬럼 (활성만)
|
||||
|
||||
### M1 PART 등록 (`partMngTempList.jsp`)
|
||||
|
||||
**검색**: SEARCH_PART_NO, SEARCH_PART_NAME (둘 다 autocomplete)
|
||||
**그리드 23셀**: PART_NO · PART_NAME · CU01_CNT(3D) · CU02_CNT(2D) · CU03_CNT(PDF) · MATERIAL · HEAT_TREATMENT_HARDNESS · HEAT_TREATMENT_METHOD · SURFACE_TREATMENT · MAKER · PART_TYPE_TITLE · SPEC · ACCTFG_NM · ODRFG_NM · UNIT_DC_NM · UNITMANG_DC_NM · UNITCHNG_NB · LOT_FG_NM · USE_YN_NM · QC_FG_NM · SETITEM_FG_NM · REQ_FG_NM · UNIT_LENGTH/QTY
|
||||
**액션**: 확정(Deploy) · 삭제 · 등록 · Excel Upload · 도면다중업로드 · 조회
|
||||
**팝업**: partMngFormPopUp(신규) · partMngDetailPopUp(편집) · openPartExcelImportPopUp
|
||||
**핵심 의존 테이블**: `part_mng` (메인) · `order_spec_mng` · `admin_supply_mng` · `bom_part_qty`
|
||||
|
||||
### M2 PART 조회 (`partMngList.jsp`)
|
||||
|
||||
**검색**: 없음(메인 조회 화면). 그리드 컬럼 동일하게 23셀(M1과 동일).
|
||||
**액션**: 등록 · 삭제 · 도면연동 · ERP업로드(전체/단일/모두) · Excel Upload · 조회
|
||||
**팝업**: partMngFormPopUp · partMngDetailPopUp · FileRegistPopup · openPartExcelImportPopUp
|
||||
**핵심 의존 테이블**: `part_mng` · `bom_part_qty`
|
||||
|
||||
### M3 E-BOM 등록 (`structureList.jsp`)
|
||||
|
||||
**검색 9 필드**: customer_cd · project_name · unit_code · SEARCH_UNIT_NAME · SEARCH_WRITER · product_cd · SEARCH_PART_NO · SEARCH_PART_NAME · search_fromDate~toDate · status
|
||||
**그리드 9셀**: PRODUCT_NAME · PART_NO · PART_NAME · BOM_CNT · DEPT_USER_NAME · REG_DATE · DEPLOY_DATE · REVISION · STATUS
|
||||
**액션**: 조회 · 삭제 · E-BOM등록 · 상태변경
|
||||
**팝업**: setStructureStandardFormPopup · setBomCopyFormPopup · setStructurePopupMainFS · changeDesignNotePopUp · structureStatusChangePopup · openBomReportExcelImportPopUp
|
||||
**핵심 의존 테이블**: `part_bom_report` · `supply_mng` · `project_mgmt` · `pms_wbs_task` · `user_info` · `bom_part_qty` · `part_mng` · `comm_code`
|
||||
|
||||
### M4 E-BOM 조회 (`structureAscendingList.jsp`)
|
||||
|
||||
**검색 4 필드**: project_name · unit_code · search_partNo · search_partName
|
||||
**그리드**: 동적 — MAX_LEVEL 레벨 컬럼 + 품번 · 품명 · 3D/2D/PDF · 수량 · 변경일 · 변경항목 · 규격 · 재질 · 중량 · 비고
|
||||
**액션**: 정전개조회 · 역전개조회 · 엑셀다운로드(정/역전개)
|
||||
**팝업**: partMngDetailPopUp(클릭) · FileRegistPopup(도면)
|
||||
**핵심 의존 테이블**: `bom_part_qty` · `sales_bom_report` · `part_bom_report` · `product_mgmt_upg_detail`/`_master` · `part_mng` · `project_mgmt` · `pms_wbs_task` · `user_info` · `product_mgmt`
|
||||
|
||||
### M5 설계변경 리스트 (`partMngHisList.jsp`)
|
||||
|
||||
**검색 10 필드**: Year · contract_objid · unit_code · part_no · part_name · change_option · eo_start_date~end_date · change_type · part_type · writer_id
|
||||
**그리드 16셀**: EO_NO · PROJECT_NO · PROJECT_NAME · UNIT_NAME · PARENT_PART_INFO · PART_NO · PART_NAME · QTY · QTY_TEMP · CHANGE_TYPE_NAME · CHANGE_OPTION_NAME · REVISION · EO_DATE · PART_TYPE_NAME · WRITER_NAME · HIS_REG_DATE_TITLE
|
||||
**액션**: 조회만(Read-Only)
|
||||
**팝업**: partMngHisDetailPopUp(행 클릭)
|
||||
**핵심 의존 테이블**: `part_mng_history` · `project_mgmt` · `part_bom_report` · `pms_wbs_task` · `user_info` · `comm_code`
|
||||
|
||||
---
|
||||
|
||||
## 3. RPS DB 보유 매트릭스 (적용 완료)
|
||||
|
||||
| 테이블 | M1 | M2 | M3 | M4 | M5 | 종류 | RPS 상태 |
|
||||
|---|:-:|:-:|:-:|:-:|:-:|---|---|
|
||||
| `part_mng` | R/W | R/W | R | R | R | 메인 | ✅ +15컬럼 ALTER(`301_alter_part_mng.sql`) |
|
||||
| `bom_part_qty` | R | R | R/W | R | R | BOM 수량 | ✅ 신규(`300`) |
|
||||
| `part_bom_report` | R | R | R/W | R | R | BOM 리포트 헤더 | ✅ 신규(`300`) |
|
||||
| `part_mng_history` | – | – | – | – | R | 변경이력 | ✅ 신규(`300`) |
|
||||
| `order_spec_mng` | R | – | – | – | – | 발주 스펙 | ✅ 신규(`300`) |
|
||||
| `admin_supply_mng` | R | – | – | – | – | 공급사(관리자) | ✅ 신규(`300`) |
|
||||
| `supply_mng` | – | – | R | – | – | 공급사 | ✅ 신규(`300`) |
|
||||
| `sales_bom_report` | – | – | – | R | – | 영업 BOM 단가 | ✅ 신규(`300`) |
|
||||
| `product_mgmt_upg_master` | – | – | – | R | – | 제품 업그레이드 마스터 | ✅ 신규(`300`) |
|
||||
| `product_mgmt_upg_detail` | – | – | – | R | – | 제품 업그레이드 디테일 | ✅ 신규(`300`) |
|
||||
| `project_mgmt` | – | – | R | R | R | 프로젝트 | ✅ 기존 |
|
||||
| `pms_wbs_task` | – | – | R | R | R | 작업/유닛 | ✅ 기존 |
|
||||
| `user_info` | – | – | R | R | R | 사용자 | ✅ 기존(컬럼명 매핑 필요) |
|
||||
| `comm_code` | – | – | R | R | R | 공통코드 | ✅ 기존 |
|
||||
| `product_mgmt` | – | – | R | R | – | 제품 | ✅ 기존 |
|
||||
|
||||
**→ 5개 메뉴 모두 P1에서 실데이터 표시 가능.**
|
||||
|
||||
---
|
||||
|
||||
## 4. GAP 매트릭스
|
||||
|
||||
| # | 우선 | 항목 | 권장 작업 |
|
||||
|---|---|---|---|
|
||||
| **DEV-1** | 🔴 | 개발관리 메뉴 자체 부재 → 5개 메뉴 운영판 1:1 이식 | **본 PR 시리즈 (3 묶음)** |
|
||||
| **DEV-2** | 🔴 | `part_mng` 15컬럼 누락 (열처리/표면처리/단위/Y-N flag) | ✅ **완료** — `301_alter_part_mng.sql` |
|
||||
| **DEV-3** | 🔴 | 9개 테이블 부재 | ✅ **완료** — `300_part_bom.sql` (BEGIN/COMMIT 트랜잭션, IDEMPOTENT) |
|
||||
| **DEV-4** | 🟠 | `user_info` 컬럼명 매핑 (wace `empseq`/`rank` ↔ vexplor `emp_seq`/`rank_code`+`rank_name`) | 코드 측 alias로 처리 |
|
||||
| **DEV-5** | 🟠 | M3 상태값(작성중/적용완료 등) — wace는 `comm_code` 0000099 자식 사용 | comm_code 그대로 사용. RPS DB에 이미 존재 여부 확인 후 부재 시 INSERT |
|
||||
| **DEV-6** | 🟠 | M1·M2 팝업(등록/상세) 다이얼로그 — wace `partMngFormPopUp.jsp` 별도 LOC 큼 | M1·M2 묶음 PR에 포함 (한 번에 가는 게 효율) |
|
||||
| **DEV-7** | 🟡 | M1 도면 다중 업로드 / M2 ERP 업로드 | 본 PR 시리즈 제외 (별 PR) |
|
||||
| **DEV-8** | 🟡 | M3 BOM Excel Import / M4 엑셀 다운로드 | 본 PR 시리즈 제외 (별 PR) |
|
||||
| **DEV-9** | 🟢 | M4 동적 MAX_LEVEL 컬럼 — BOM 트리 깊이에 따라 컬럼 추가 | DataGrid 동적 컬럼 모드. 본 PR(E-BOM 묶음)에 포함 |
|
||||
| **DEV-10** | 🟢 | `admin_supply_mng.employee_email` 운영 타입 버그(`xid`) | ✅ **완료** — `300` 추출 시 `character varying`으로 정정 |
|
||||
|
||||
---
|
||||
|
||||
## 5. PR 묶음 스코프 (3 PR 시리즈)
|
||||
|
||||
### 5.1 PR-A : PART 등록·조회 묶음 (M1+M2)
|
||||
|
||||
**범위**:
|
||||
- backend: `routes/devPartRoutes.ts` + `services/devPartService.ts` + `controllers/devPartController.ts`
|
||||
- 엔드포인트: `/api/development/part-temp/list`·`/deploy`, `/api/development/part/list`·`/:objid`, `/api/development/part` (DELETE)
|
||||
- frontend:
|
||||
- `app/(main)/COMPANY_16/development/part-regist/page.tsx` (M1)
|
||||
- `app/(main)/COMPANY_16/development/part-search/page.tsx` (M2)
|
||||
- `components/development/PartFormDialog.tsx` (등록/수정 공용)
|
||||
- `components/development/PartDetailDialog.tsx` (상세)
|
||||
- `lib/api/devPart.ts`
|
||||
- 매퍼 1:1: `partMngTempGridList` · `partMngGridList` · `partMngFormPopUp` · `partMngDeploy` · `partMngDelete`
|
||||
|
||||
**제외**: 도면 다중 업로드 · ERP 업로드 · Excel Import → 별 PR
|
||||
|
||||
### 5.2 PR-B : E-BOM 등록·조회 묶음 (M3+M4)
|
||||
|
||||
**범위**:
|
||||
- backend: `routes/devBomRoutes.ts` + `services/devBomService.ts` + `controllers/devBomController.ts`
|
||||
- 엔드포인트: `/api/development/ebom/list`·`/status`·`/:objid` (DELETE), `/api/development/ebom/ascending`·`/descending`
|
||||
- frontend:
|
||||
- `app/(main)/COMPANY_16/development/ebom-regist/page.tsx` (M3)
|
||||
- `app/(main)/COMPANY_16/development/ebom-search/page.tsx` (M4)
|
||||
- `components/development/BomStandardFormDialog.tsx` (M3 등록)
|
||||
- `components/development/BomStatusChangeDialog.tsx` (M3 상태변경)
|
||||
- `lib/api/devBom.ts`
|
||||
- M4 동적 MAX_LEVEL 컬럼 처리 (DataGrid 동적 컬럼)
|
||||
|
||||
**제외**: BOM Excel Import · 정/역전개 엑셀 다운로드 → 별 PR
|
||||
|
||||
### 5.3 PR-C : 설계변경 리스트 (M5)
|
||||
|
||||
**범위**:
|
||||
- backend: `routes/devEoHistoryRoutes.ts` + `services/devEoHistoryService.ts` + `controllers/devEoHistoryController.ts`
|
||||
- 엔드포인트: `/api/development/eo/history/list` (read-only)
|
||||
- frontend:
|
||||
- `app/(main)/COMPANY_16/development/change-list/page.tsx`
|
||||
- `components/development/PartHisDetailDialog.tsx` (행 클릭 상세)
|
||||
- `lib/api/devEoHistory.ts`
|
||||
- read-only — INSERT/UPDATE/DELETE 없음
|
||||
|
||||
---
|
||||
|
||||
## 6. 사용자 결정 사항 (2026-05-12)
|
||||
|
||||
| # | 항목 | 결정 |
|
||||
|---|---|---|
|
||||
| 1 | 도메인 폴더 | 단일 `development/` |
|
||||
| 2 | 메뉴 진행 순서 | PART 묶음(M1+M2) → E-BOM 묶음(M3+M4) → 설계변경(M5) |
|
||||
| 3 | 문서 구조 | 단일 00-gap.md (본 문서) + 묶음별 *.md (총 3개) |
|
||||
| 4 | DDL 적용 | 운영DB → vexplor_rps 직접 적용 완료 (9 신규 + 1 ALTER) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 다음 단계
|
||||
|
||||
1. **PR-A** : `01-part.md` 작성 → backend route → frontend page 2개 → verify
|
||||
2. **PR-B** : `02-ebom.md` 작성 → backend route → frontend page 2개 → verify
|
||||
3. **PR-C** : `03-eo-history.md` 작성 → backend route → frontend page → verify
|
||||
@@ -0,0 +1,313 @@
|
||||
# PR-A : PART 등록·조회 묶음 구현 명세
|
||||
|
||||
> 작성: 2026-05-12 / 범위: 개발관리 M1(PART 등록) + M2(PART 조회) — 같은 `part_mng` 테이블 R/W, 매퍼 공유.
|
||||
|
||||
---
|
||||
|
||||
## 1. 매퍼 쿼리 1:1 매핑
|
||||
|
||||
원본 `wace_plm/src/com/pms/mapper/partMng.xml`:
|
||||
|
||||
| Query id | Line | 본 PR 매핑 | 용도 |
|
||||
|---|---:|---|---|
|
||||
| `partMngBaseSimple` (sql) | 78 | (서비스 측 공통 SELECT fragment) | PART_MNG 메인 88+ 컬럼 SELECT |
|
||||
| `partMngTempGridList` | 2,354 | `GET /api/development/part-temp/list` | M1 그리드 (status != 'release') + ORDER_SPEC_MNG·ADMIN_SUPPLY_MNG JOIN |
|
||||
| `partMngGridList` | 1,903 | `GET /api/development/part/list` | M2 그리드 (status = 'release' 고정) |
|
||||
| `partMngInfo` | 2,699 | `GET /api/development/part/:objid` | 상세 단건 (편집 팝업 진입) |
|
||||
| `insertpartInfo` | 7,625 | `POST /api/development/part` | 신규 등록 (38 컬럼 INSERT) |
|
||||
| `updatePartDetail` | 2,711 | `PUT /api/development/part/:objid` | 상세 수정 (21 컬럼 UPDATE) |
|
||||
| `partMngDeploy` | 4,190 | `POST /api/development/part-temp/deploy` | 확정 (M1→M2) STATUS='release', EO_NO 채번 |
|
||||
| `partMngIsLastInit` | 4,230 | (deploy 트랜잭션 내부) | 동일 PART_NO 이전 IS_LAST='0' |
|
||||
| `insertPartMngHistory` | 4,244 | (deploy 트랜잭션 내부) | PART_MNG_HISTORY 이력 INSERT |
|
||||
| `partMngDelete` | 4,486 | `DELETE /api/development/part` (body: `objids: string[]`) | 다중 삭제 |
|
||||
|
||||
`partMngBaseSimple` SELECT 핵심: `PART_MNG P` + `COMM_CODE CC_UNIT`(UNIT) + `COMM_CODE CC_PART`(PART_TYPE) + `admin_supply_mng SUP`(SUPPLY_CODE) + LATERAL `BOM_PART_QTY`(LAST_PART_OBJID·status='deploy'·최신 1행) + LATERAL `COMM_CODE`(CHANGE_OPTION 다중 라벨) + `ATTACH_FILE_INFO`(3D/2D/PDF 파일 카운트). 23개 그리드 컬럼 + CODE_NAME 라벨 + Y/N flag CASE 변환 자체 처리.
|
||||
|
||||
---
|
||||
|
||||
## 2. API 엔드포인트 명세
|
||||
|
||||
### 2.1 M1 그리드 — `GET /api/development/part-temp/list`
|
||||
|
||||
**Query**:
|
||||
```
|
||||
search_part_no?: string
|
||||
search_part_name?: string
|
||||
search_material?: string
|
||||
search_spec?: string
|
||||
search_part_type?: string (PART_TYPE_CODE comm_code id)
|
||||
writer?: string
|
||||
status?: string // 단일: 'create'/'changing'/'editing'
|
||||
status_arr?: string[] // 다중 (둘 중 하나만 사용)
|
||||
product_code?: string
|
||||
upg_no?: string
|
||||
page?: number // 기본 1
|
||||
page_size?: number // 기본 20
|
||||
```
|
||||
|
||||
**SQL** (요약):
|
||||
```sql
|
||||
SELECT T.*, SORT (REVISION), O.PARTNER_TITLE, Q.OBJID/CHILD_OBJID/QTY/QTY_TEMP, Q_QTY (CASE),
|
||||
(SELECT PART_NO FROM PART_MNG SP WHERE SP.OBJID = Q.PARENT_PART_NO) PARENT_PART_INFO
|
||||
FROM <partMngBaseSimple> T
|
||||
LEFT JOIN (ORDER_SPEC_MNG OSM JOIN ADMIN_SUPPLY_MNG SUP) O ON T.OBJID::VARCHAR = O.PART_OBJID::VARCHAR
|
||||
LEFT JOIN BOM_PART_QTY Q ON (
|
||||
T.OBJID IN (SELECT DISTINCT PM1.OBJID FROM PART_MNG PM1, PART_MNG PM2
|
||||
WHERE PM1.STATUS='changing' AND PM2.STATUS!='changing'
|
||||
AND PM2.OBJID = Q.PART_NO AND PM1.PART_NO = PM2.PART_NO)
|
||||
AND Q.STATUS = 'beforeEdit'
|
||||
)
|
||||
WHERE 1=1 + 동적 (SEARCH_PART_NO/NAME/MATERIAL/SPEC/PART_TYPE, WRITER, STATUS, STATUS_ARR)
|
||||
ORDER BY PARENT_PART_INFO, T.PART_NO
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```ts
|
||||
{
|
||||
rows: PartTempRow[]; // 그리드 23셀 + 추가 필드
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 M2 그리드 — `GET /api/development/part/list`
|
||||
|
||||
**Query**: 위 + `search_year?` `search_hardness?` `search_method?` `search_surface?` `customer_objid?` `customer_cd?` `project_name?` `unit_code?` `search_design_date_from?` `search_design_date_to?` `is_last?` `eo?`
|
||||
|
||||
**SQL** (요약):
|
||||
```sql
|
||||
SELECT NUM (ROW_NUMBER), T.*,
|
||||
DECODE(PART_TYPE, '0000063', '1',
|
||||
(SELECT SUM(...) FROM BOM_PART_QTY Q WHERE Q.LAST_PART_OBJID=T.OBJID)::CHARACTER) BOM_QTY
|
||||
FROM <partMngBaseSimple> T
|
||||
WHERE 1=1 AND T.status='release' -- M1 vs M2 핵심 차이
|
||||
+ 동적 (M1 검색 필드 + 추가 5종)
|
||||
```
|
||||
|
||||
**Response**: `{ rows: PartRow[]; total, page, pageSize }`
|
||||
|
||||
### 2.3 단건 상세 — `GET /api/development/part/:objid`
|
||||
|
||||
```sql
|
||||
SELECT T.* FROM <partMngBaseSimple> T WHERE T.OBJID = #{OBJID}
|
||||
```
|
||||
|
||||
→ `PartRow` 단일 반환. 404 시 `{ error: 'not_found' }`.
|
||||
|
||||
### 2.4 신규 등록 — `POST /api/development/part`
|
||||
|
||||
**Body** (38 컬럼, 핵심):
|
||||
```ts
|
||||
{
|
||||
part_objid: string; // numeric, 클라이언트 채번 (nanoid-based) 또는 서버 채번
|
||||
part_no: string;
|
||||
part_name: string;
|
||||
unit?: string; // comm_code
|
||||
qty?: string;
|
||||
spec?: string;
|
||||
material?: string;
|
||||
thickness?: string; width?: string; height?: string;
|
||||
out_diameter?: string; in_diameter?: string; length?: string;
|
||||
remark?: string;
|
||||
part_type: string; // comm_code (PART_TYPE_CODE)
|
||||
product_mgmt_objid?: string;
|
||||
supply_code?: string;
|
||||
maker?: string;
|
||||
contract_objid?: string;
|
||||
post_processing?: string;
|
||||
heat_treatment_hardness?: string;
|
||||
heat_treatment_method?: string;
|
||||
surface_treatment?: string;
|
||||
acctfg?: string; // comm_code 계정구분
|
||||
odrfg?: string; // 0=구매/1=생산/8=Phantom
|
||||
unit_dc?: string; unitmang_dc?: string;
|
||||
unitchng_nb?: string;
|
||||
lot_fg?: '0'|'1';
|
||||
use_yn?: '0'|'1';
|
||||
qc_fg?: '0'|'1';
|
||||
setitem_fg?: '0'|'1';
|
||||
req_fg?: '0'|'1';
|
||||
unit_length?: string;
|
||||
unit_qty?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**SQL**: `insertpartInfo` (7,625) 그대로. `STATUS='create'`, `REG_DATE=now()`, `IS_LAST='1'`, `WRITER=#{CONNECTUSERID}` (서버에서 `req.user.user_id` 주입).
|
||||
|
||||
**채번 정책**: `part_mng.objid` 는 **`bigint`** 타입(다른 영업관리 테이블 `contract_mgmt.objid` 등은 varchar — `genObjid("CM")` 패턴 사용). bigint 컬럼은 prefix-string 못 쓰므로 **wace `CommonUtils.createObjId()` 1:1 구현** 사용:
|
||||
|
||||
```typescript
|
||||
// backend-node/src/utils/objidUtil.ts (신규)
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
function javaStringHashCode(s: string): number {
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
||||
return h;
|
||||
}
|
||||
|
||||
/** wace CommonUtils.createObjId() 1:1 — UUID v4 → 하이픈 제거(32 hex) → Java String.hashCode (int32) → String. 결과: -2,147,483,648 ~ 2,147,483,647. */
|
||||
export function createObjId(): string {
|
||||
return String(javaStringHashCode(randomUUID().replaceAll('-', '')));
|
||||
}
|
||||
```
|
||||
|
||||
INSERT 시 `body.part_objid` 가 비어 있으면 서버에서 `createObjId()` 호출(클라이언트 채번도 허용하되 권장 X).
|
||||
|
||||
### 2.5 상세 수정 — `PUT /api/development/part/:objid`
|
||||
|
||||
**Body** (21 컬럼, `updatePartDetail` 1:1):
|
||||
`part_name, material, heat_treatment_hardness, heat_treatment_method, surface_treatment, maker, part_type, acctfg, odrfg, spec, unit_dc, unitmang_dc, unitchng_nb, lot_fg, use_yn, qc_fg, setitem_fg, req_fg, unit_length, unit_qty, remark`
|
||||
|
||||
→ `EDIT_DATE = NOW()` 자동.
|
||||
|
||||
### 2.6 확정 — `POST /api/development/part-temp/deploy`
|
||||
|
||||
**Body**: `{ objids: string[] }` — 다중 선택 확정.
|
||||
|
||||
**트랜잭션 (각 objid에 대해 순차 처리)**:
|
||||
1. `partMngIsLastInit`: 같은 PART_NO 모든 행 `IS_LAST='0'`
|
||||
2. `insertPartMngHistory`: 현재 행을 `PART_MNG_HISTORY`로 복사 (이력 보존)
|
||||
3. `partMngDeploy`: 본 행 `IS_LAST='1'`, `STATUS='release'`, `DEPLOY_DATE=NOW()`, `REVISION=COALESCE(REVISION,'RE')`, `EO_DATE=...`, `EO_NO=` 채번 (IS_LONGD에 따라 `EOB{yy}-{seq}` or `EO{yy}-{seq}`)
|
||||
|
||||
**EO_NO 채번 SQL** (wace 운영판 그대로):
|
||||
```sql
|
||||
CASE WHEN P.IS_LONGD = '1' THEN
|
||||
'EOB' || TO_CHAR(NOW(),'yy') || '-' || LPAD(
|
||||
(SELECT COALESCE(SUBSTR(MAX(EO_NO),7,8)::INTEGER+1, 1)
|
||||
FROM PART_MNG SP
|
||||
WHERE SP.EO_NO LIKE 'EOB' || TO_CHAR(NOW(),'yy') || '-%'
|
||||
AND SP.PART_NO != P.PART_NO
|
||||
AND SP.REVISION != P.REVISION
|
||||
)||'', 4, '0')
|
||||
ELSE
|
||||
'EO' || TO_CHAR(NOW(),'yy') || '-' || LPAD(... 'EO{yy}-{seq}' ...)
|
||||
END
|
||||
```
|
||||
|
||||
**Response**: `{ deployed: number, eo_nos: Record<objid, eo_no> }`
|
||||
|
||||
### 2.7 다중 삭제 — `DELETE /api/development/part`
|
||||
|
||||
**Body**: `{ objids: string[] }`
|
||||
|
||||
**SQL** (wace 그대로 POSITION 트릭):
|
||||
```sql
|
||||
DELETE FROM PART_MNG WHERE POSITION(OBJID||',' IN #{checkArr}||',') > 0
|
||||
```
|
||||
|
||||
→ backend-node에서는 PostgreSQL 표준인 `WHERE OBJID = ANY($1::numeric[])` 로 정리(동일 효과 + 인덱스 활용 가능).
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend 파일 구조
|
||||
|
||||
```
|
||||
backend-node/src/
|
||||
routes/
|
||||
devPartRoutes.ts // Express Router — 7 endpoint
|
||||
controllers/
|
||||
devPartController.ts // req/res 처리, validation
|
||||
services/
|
||||
devPartService.ts // SQL 실행 (pg 트랜잭션 처리 포함)
|
||||
devPartSqlFragments.ts // partMngBaseSimple SELECT fragment 재사용
|
||||
```
|
||||
|
||||
`app.ts`에 `app.use('/api/development', devPartRoutes)` 추가 (또는 메뉴 묶음 라우터 도입 시 그쪽).
|
||||
|
||||
---
|
||||
|
||||
## 4. Frontend 파일 구조
|
||||
|
||||
```
|
||||
frontend/
|
||||
app/(main)/COMPANY_16/development/
|
||||
part-regist/
|
||||
page.tsx // M1 그리드 + 상단 액션 + 페이징
|
||||
part-search/
|
||||
page.tsx // M2 그리드 + 상단 액션 + 페이징
|
||||
components/development/
|
||||
PartFormDialog.tsx // 신규/수정 통합 (mode prop)
|
||||
PartDetailDialog.tsx // 읽기 전용 상세
|
||||
lib/api/
|
||||
devPart.ts // 7 endpoint 호출 함수 + 타입
|
||||
```
|
||||
|
||||
### 4.1 그리드 23셀 (M1·M2 공통)
|
||||
|
||||
| key | 라벨 | 정렬 | 너비 |
|
||||
|---|---|---|---:|
|
||||
| part_no | 품번 | left | 140 |
|
||||
| part_name | 품명 | left | 220 |
|
||||
| cu01_cnt | 3D | right | 60 |
|
||||
| cu02_cnt | 2D | right | 60 |
|
||||
| cu03_cnt | PDF | right | 60 |
|
||||
| material | 재료 | left | 100 |
|
||||
| heat_treatment_hardness | 열처리경도 | left | 110 |
|
||||
| heat_treatment_method | 열처리방법 | left | 110 |
|
||||
| surface_treatment | 표면처리 | left | 100 |
|
||||
| maker | 메이커 | left | 100 |
|
||||
| part_type_title | 범주이름 | left | 100 |
|
||||
| spec | 규격 | left | 140 |
|
||||
| acctfg_nm | 계정구분 | center | 80 |
|
||||
| odrfg_nm | 조달구분 | center | 80 |
|
||||
| unit_dc_nm | 재고단위 | center | 80 |
|
||||
| unitmang_dc_nm | 관리단위 | center | 80 |
|
||||
| unitchng_nb | 환산수량 | right | 90 |
|
||||
| lot_fg_nm | LOT구분 | center | 80 |
|
||||
| use_yn_nm | 사용여부 | center | 80 |
|
||||
| qc_fg_nm | 검사여부 | center | 80 |
|
||||
| setitem_fg_nm | SET품여부 | center | 90 |
|
||||
| req_fg_nm | 의뢰여부 | center | 80 |
|
||||
| unit_length / unit_qty | 개당길이/수량 | right | 100 |
|
||||
|
||||
추가 (M1만): `partner_title`, `q_qty`, `parent_part_info`
|
||||
추가 (M2만): `bom_qty`
|
||||
|
||||
### 4.2 검색 폼
|
||||
|
||||
**M1 (PART 등록)** — 2 필드: SEARCH_PART_NO · SEARCH_PART_NAME (둘 다 PartSelect autocomplete)
|
||||
**M2 (PART 조회)** — 메인 조회 화면 (별도 검색 폼 없음, 그리드 헤더 inline 필터로 처리하거나 상단 간소화 검색바 1줄로 통합 — 본 PR 우선 `<Input>` 2개로 시작, 추후 보강)
|
||||
|
||||
### 4.3 액션 버튼 (각 page 상단)
|
||||
|
||||
**M1**: 등록 · 수정 · 삭제 · 확정 · 조회
|
||||
**M2**: 등록 · 수정 · 삭제 · 조회 (도면연동/ERP업로드/Excel은 본 PR 제외)
|
||||
|
||||
### 4.4 PartFormDialog (신규/수정 통합)
|
||||
|
||||
- mode: `'create' | 'edit'`
|
||||
- 38 필드 — `<Input>` + `<CommCodeSelect>` 조합
|
||||
- 검증: part_no/part_name 필수, comm_code 필드는 SmartSelect
|
||||
- 신규: POST → 신규 행 추가
|
||||
- 수정: PUT → 21 필드만 전송 (insertpartInfo는 38, updatePartDetail는 21 — wace 그대로)
|
||||
|
||||
### 4.5 PartDetailDialog (읽기 전용)
|
||||
|
||||
행 더블클릭 시 진입. 모든 필드 disabled. "수정" 버튼 → PartFormDialog(mode='edit') 전환.
|
||||
|
||||
---
|
||||
|
||||
## 5. 본 PR 제외 항목
|
||||
|
||||
| 항목 | 사유 / 후속 |
|
||||
|---|---|
|
||||
| 도면 다중 업로드 (M1) | `ATTACH_FILE_INFO` 다파일 업로드 — 별 PR |
|
||||
| ERP 업로드 (M2) | wace 외부 시스템 연동 — 별 PR |
|
||||
| Excel Upload (M1·M2) | `openPartExcelImportPopUp.jsp` 별도 — 별 PR |
|
||||
| BOM_PART_QTY R/W (M3 영역) | PR-B 에서 다룸 |
|
||||
| EO_NO 채번 분기 일부 (`IS_LONGD` flag) | 본 PR 포함 — 운영판 그대로 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 시나리오 (verify.md 기준)
|
||||
|
||||
1. M1 페이지 진입 → 그리드 표시(status != 'release') 확인
|
||||
2. "등록" → PartFormDialog 신규 → POST → M1 그리드에 새 행
|
||||
3. M1 행 선택 → "확정" → POST deploy → STATUS='release', EO_NO 채번 확인
|
||||
4. M2 페이지 진입 → deploy된 행이 M2 그리드에 표시
|
||||
5. M2 행 선택 → "수정" → PartFormDialog 수정 → PUT
|
||||
6. M2 행 다중 선택 → "삭제" → DELETE → 그리드에서 제거
|
||||
7. 검색 (SEARCH_PART_NO/NAME) → 필터 적용 확인
|
||||
8. 운영DB 11133/waceplm 의 동일 SQL 결과와 vexplor_rps 결과 행 수 비교 (sanity)
|
||||
@@ -0,0 +1,428 @@
|
||||
-- ============================================================
|
||||
-- 개발관리(PART/E-BOM/설계변경) 운영 DDL — wace_plm 운영DB(211.115.91.141:11133/waceplm) 추출
|
||||
-- 추출일: 2026-05-12
|
||||
-- 추출 방법: information_schema + pg_indexes + pg_description 쿼리
|
||||
-- (pg_dump 14.19 ↔ PG 16.8 mismatch로 pg_dump 사용 불가)
|
||||
--
|
||||
-- 대상 테이블 9개 (운영 카운트):
|
||||
-- admin_supply_mng 28 cols / 7건 ← 공급업체 마스터(관리자)
|
||||
-- bom_part_qty 19 cols / 835건 ← BOM 수량 트리
|
||||
-- order_spec_mng 12 cols / 12,522건 ← 발주 스펙 이력
|
||||
-- part_bom_report 23 cols / 40건 ← BOM 리포트 헤더
|
||||
-- part_mng_history 59 cols / 263건 ← 파트 이력(설계변경)
|
||||
-- product_mgmt_upg_detail 7 cols / 87건 ← 제품 업그레이드 디테일
|
||||
-- product_mgmt_upg_master 5 cols / 6건 ← 제품 업그레이드 마스터
|
||||
-- sales_bom_report 16 cols / 1,529건 ← 영업 BOM 단가
|
||||
-- supply_mng 29 cols / 1건 ← 공급업체(고객)
|
||||
--
|
||||
-- 비고:
|
||||
-- · 운영 스키마 1:1 보존 — 컬럼 순서/타입/길이/default 모두 운영과 동일.
|
||||
-- · objid 컬럼은 wace Java 측에서 UUID/임의 채번(연관 시퀀스 없음 — 확인 완료).
|
||||
-- · admin_supply_mng.objid·supply_mng.objid는 numeric, default 0(운영 그대로).
|
||||
-- · admin_supply_mng.employee_email 운영 타입이 'xid'(PostgreSQL 시스템 타입, transaction id)
|
||||
-- → 데이터 의미(이메일)와 무관한 운영 측 추정 실수. character varying 으로 정정 적용.
|
||||
-- · part_mng_history.objid는 numeric NOT NULL (PK)이지만 default 미지정.
|
||||
-- · product_mgmt_upg_master PK는 (objid, target_objid) 복합키.
|
||||
-- · sales_bom_report 의 supply_objid*/price* 컬럼 length 가 들쭉날쭉한 부분은 운영 그대로.
|
||||
-- · sales_bom_report_parent_objid_idx 는 UNIQUE 인덱스(운영 정의 그대로 — 1 BOM당 1 단가).
|
||||
-- · company_code 분기 없음(vexplor_rps는 COMPANY_16 단독).
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1) admin_supply_mng (공급업체 마스터 — 관리자 측 입력)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS admin_supply_mng CASCADE;
|
||||
CREATE TABLE admin_supply_mng (
|
||||
objid numeric NOT NULL DEFAULT '0'::numeric,
|
||||
supply_code character varying(100) DEFAULT 'NULL::character varying'::character varying,
|
||||
supply_name character varying(100) DEFAULT 'NULL::character varying'::character varying,
|
||||
reg_no character varying(100) DEFAULT 'NULL::character varying'::character varying,
|
||||
supply_address character varying(500) DEFAULT 'NULL::character varying'::character varying,
|
||||
supply_busname character varying(100) DEFAULT 'NULL::character varying'::character varying,
|
||||
supply_stockname character varying(100) DEFAULT 'NULL::character varying'::character varying,
|
||||
supply_tel_no character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
supply_fax_no character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
charge_user_name character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
payment_method character varying,
|
||||
reg_id character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
reg_date timestamp,
|
||||
status character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
area_cd character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
bus_reg_no character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
office_no character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
email character varying DEFAULT 'NULL::character varying'::character varying,
|
||||
account_code character varying,
|
||||
remark character varying,
|
||||
account_bank character varying,
|
||||
account_number character varying,
|
||||
account_user_name character varying,
|
||||
employee_name character varying,
|
||||
employee_position character varying,
|
||||
employee_number character varying,
|
||||
-- 운영은 'xid' 시스템 타입으로 잘못 정의됨. 의미상 이메일 → character varying 으로 정정.
|
||||
employee_email character varying,
|
||||
david character varying(50),
|
||||
CONSTRAINT admin_supply_mng_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2) bom_part_qty (BOM 수량 트리 — 835건)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS bom_part_qty CASCADE;
|
||||
CREATE TABLE bom_part_qty (
|
||||
bom_report_objid character varying(64) NOT NULL,
|
||||
objid character varying(64) NOT NULL,
|
||||
parent_objid character varying(64) DEFAULT 'NULL::character varying'::character varying,
|
||||
child_objid character varying(64) DEFAULT 'NULL::character varying'::character varying,
|
||||
parent_part_no character varying(64) DEFAULT 'NULL::character varying'::character varying,
|
||||
part_no character varying(64) DEFAULT 'NULL::character varying'::character varying,
|
||||
qty character varying,
|
||||
regdate timestamp,
|
||||
seq integer,
|
||||
status character varying,
|
||||
deploy_date character varying,
|
||||
deploy_user_id character varying,
|
||||
edit_date character varying,
|
||||
writer character varying,
|
||||
qty_temp character varying,
|
||||
last_part_objid character varying,
|
||||
editer character varying,
|
||||
item_qty character varying,
|
||||
supplier character varying,
|
||||
CONSTRAINT bom_part_qty_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
CREATE INDEX bom_part_qty_bom_report_objid2_idx ON bom_part_qty USING btree (bom_report_objid, last_part_objid, part_no);
|
||||
CREATE INDEX bom_part_qty_bom_report_objid_idx ON bom_part_qty USING btree (bom_report_objid);
|
||||
CREATE INDEX bom_part_qty_last_part_objid_idx ON bom_part_qty USING btree (last_part_objid);
|
||||
CREATE INDEX bom_part_qty_parent_objid_idx ON bom_part_qty USING btree (parent_objid);
|
||||
CREATE INDEX idx_bom_part_qty_part_no ON bom_part_qty USING btree (part_no) WHERE ((part_no IS NOT NULL) AND ((part_no)::text <> ''::text));
|
||||
|
||||
COMMENT ON COLUMN bom_part_qty.status IS '상태';
|
||||
COMMENT ON COLUMN bom_part_qty.deploy_date IS '배포일';
|
||||
COMMENT ON COLUMN bom_part_qty.deploy_user_id IS '배포자';
|
||||
COMMENT ON COLUMN bom_part_qty.edit_date IS '수정일';
|
||||
COMMENT ON COLUMN bom_part_qty.writer IS '등록자';
|
||||
COMMENT ON COLUMN bom_part_qty.qty_temp IS '수량(설변중)';
|
||||
COMMENT ON COLUMN bom_part_qty.last_part_objid IS '마지막 품번키';
|
||||
COMMENT ON COLUMN bom_part_qty.editer IS '수정자';
|
||||
COMMENT ON COLUMN bom_part_qty.item_qty IS '항목수량';
|
||||
COMMENT ON COLUMN bom_part_qty.supplier IS '공급업체';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3) order_spec_mng (발주 스펙 이력 — 12,522건, 운영 최다)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS order_spec_mng CASCADE;
|
||||
CREATE TABLE order_spec_mng (
|
||||
objid character varying NOT NULL,
|
||||
seq character varying NOT NULL,
|
||||
part_objid character varying NOT NULL,
|
||||
partner_rank character varying,
|
||||
partner_objid character varying,
|
||||
partner_price character varying,
|
||||
partner_qty character varying,
|
||||
apply_date character varying,
|
||||
remark character varying,
|
||||
regdate timestamp,
|
||||
is_last character varying,
|
||||
writer character varying,
|
||||
CONSTRAINT order_spec_mng_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4) part_bom_report (BOM 리포트 헤더 — 40건)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS part_bom_report CASCADE;
|
||||
CREATE TABLE part_bom_report (
|
||||
objid character varying NOT NULL DEFAULT ''::character varying,
|
||||
customer_objid character varying,
|
||||
contract_objid character varying,
|
||||
unit_code character varying,
|
||||
revision character varying,
|
||||
writer character varying(64),
|
||||
regdate timestamp,
|
||||
status character varying(64),
|
||||
deploy_date character varying(64),
|
||||
eo_no character varying(100),
|
||||
eo_date character varying(100),
|
||||
note character varying(2000),
|
||||
edit_date timestamp,
|
||||
editer character varying,
|
||||
unit_code_old character varying,
|
||||
multi_break_yn character varying,
|
||||
multi_yn character varying,
|
||||
multi_master_yn character varying,
|
||||
multi_master_objid character varying,
|
||||
product_cd character varying,
|
||||
part_no character varying,
|
||||
part_name character varying,
|
||||
version character varying,
|
||||
CONSTRAINT part_bom_report_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
CREATE INDEX idx_part_bom_report_customer ON part_bom_report USING btree (customer_objid) WHERE ((customer_objid IS NOT NULL) AND ((customer_objid)::text <> ''::text));
|
||||
CREATE INDEX idx_part_bom_report_regdate ON part_bom_report USING btree (regdate DESC);
|
||||
CREATE INDEX idx_part_bom_report_writer ON part_bom_report USING btree (writer);
|
||||
CREATE INDEX part_bom_report_contract_objid_idx ON part_bom_report USING btree (contract_objid);
|
||||
CREATE INDEX part_bom_report_unit_code_idx ON part_bom_report USING btree (unit_code, contract_objid);
|
||||
|
||||
COMMENT ON COLUMN part_bom_report.objid IS 'OBJECT ID';
|
||||
COMMENT ON COLUMN part_bom_report.customer_objid IS '고객사 OBJID';
|
||||
COMMENT ON COLUMN part_bom_report.contract_objid IS '계약objid';
|
||||
COMMENT ON COLUMN part_bom_report.unit_code IS 'unit';
|
||||
COMMENT ON COLUMN part_bom_report.revision IS 'rev';
|
||||
COMMENT ON COLUMN part_bom_report.writer IS '작성자';
|
||||
COMMENT ON COLUMN part_bom_report.regdate IS '등록일';
|
||||
COMMENT ON COLUMN part_bom_report.status IS '상태';
|
||||
COMMENT ON COLUMN part_bom_report.deploy_date IS '배포일';
|
||||
COMMENT ON COLUMN part_bom_report.edit_date IS '수정일';
|
||||
COMMENT ON COLUMN part_bom_report.editer IS '수정자';
|
||||
COMMENT ON COLUMN part_bom_report.unit_code_old IS 'UNIT_CODE';
|
||||
COMMENT ON COLUMN part_bom_report.multi_break_yn IS '동시적용프로젝트 깨짐 여부';
|
||||
COMMENT ON COLUMN part_bom_report.multi_yn IS '동시적용프로젝트 여부';
|
||||
COMMENT ON COLUMN part_bom_report.multi_master_yn IS '동시적용프로젝트 마스터 여부';
|
||||
COMMENT ON COLUMN part_bom_report.multi_master_objid IS '동시적용프로젝트 마스터 키';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5) part_mng_history (파트 이력 — 설계변경 핵심, 59 cols / 263건)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS part_mng_history CASCADE;
|
||||
CREATE TABLE part_mng_history (
|
||||
objid numeric NOT NULL,
|
||||
product_mgmt_objid character varying(100) DEFAULT NULL::character varying,
|
||||
upg_no character varying(100) DEFAULT NULL::character varying,
|
||||
part_no character varying(100) DEFAULT NULL::character varying,
|
||||
part_name character varying(100) DEFAULT NULL::character varying,
|
||||
unit character varying(50) DEFAULT NULL::character varying,
|
||||
qty character varying(50) DEFAULT NULL::character varying,
|
||||
spec character varying(100) DEFAULT 'NULL::character varying'::character varying,
|
||||
material character varying(100) DEFAULT NULL::character varying,
|
||||
weight character varying(50) DEFAULT NULL::character varying,
|
||||
part_type character varying(100) DEFAULT NULL::character varying,
|
||||
remark character varying(1000) DEFAULT NULL::character varying,
|
||||
es_spec character varying(100) DEFAULT NULL::character varying,
|
||||
ms_spec character varying(100) DEFAULT NULL::character varying,
|
||||
change_option character varying(50) DEFAULT NULL::character varying,
|
||||
design_apply_point character varying(50) DEFAULT NULL::character varying,
|
||||
management_flag character varying(50) DEFAULT NULL::character varying,
|
||||
revision character varying(50) DEFAULT NULL::character varying,
|
||||
status character varying(30) DEFAULT NULL::character varying,
|
||||
reg_date timestamp,
|
||||
edit_date timestamp,
|
||||
writer character varying(30) DEFAULT NULL::character varying,
|
||||
is_last character varying(5) DEFAULT NULL::character varying,
|
||||
eo_no character varying,
|
||||
eo_temp character varying,
|
||||
excel_upload_seq character varying,
|
||||
sourcing_code character varying,
|
||||
sub_material character varying(100) DEFAULT 'NULL::character varying'::character varying,
|
||||
parent_part_no character varying,
|
||||
design_date character varying,
|
||||
eo_date character varying,
|
||||
deploy_date timestamp,
|
||||
thickness character varying,
|
||||
width character varying,
|
||||
height character varying,
|
||||
out_diameter character varying,
|
||||
in_diameter character varying,
|
||||
length character varying,
|
||||
supply_code character varying,
|
||||
change_type character varying,
|
||||
contract_objid character varying,
|
||||
maker character varying,
|
||||
qty_temp character varying,
|
||||
bom_report_objid character varying,
|
||||
parent_part_objid character varying,
|
||||
parent_qty_child_objid character varying,
|
||||
bom_qty_status character varying,
|
||||
his_reg_date timestamp,
|
||||
his_writer character varying,
|
||||
his_status character varying,
|
||||
qty_child_objid character varying,
|
||||
bom_status character varying,
|
||||
bom_deploy_date timestamp,
|
||||
chg_part_objid character varying,
|
||||
chg_part_no character varying,
|
||||
chg_part_rev character varying,
|
||||
heat_treatment_hardness character varying,
|
||||
heat_treatment_method character varying,
|
||||
surface_treatment character varying,
|
||||
CONSTRAINT part_mng_history_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN part_mng_history.qty_temp IS '수량(설변중)';
|
||||
COMMENT ON COLUMN part_mng_history.bom_report_objid IS 'BOM 키';
|
||||
COMMENT ON COLUMN part_mng_history.parent_part_objid IS '부모 파트 키';
|
||||
COMMENT ON COLUMN part_mng_history.parent_qty_child_objid IS '부모 구조 키';
|
||||
COMMENT ON COLUMN part_mng_history.bom_qty_status IS 'BOM 상태';
|
||||
COMMENT ON COLUMN part_mng_history.his_reg_date IS '등록일';
|
||||
COMMENT ON COLUMN part_mng_history.his_writer IS '등록자';
|
||||
COMMENT ON COLUMN part_mng_history.his_status IS '상태';
|
||||
COMMENT ON COLUMN part_mng_history.qty_child_objid IS '구조 키';
|
||||
COMMENT ON COLUMN part_mng_history.bom_status IS 'BOM 상태';
|
||||
COMMENT ON COLUMN part_mng_history.bom_deploy_date IS '배포일';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 6) product_mgmt_upg_detail (제품 업그레이드 디테일 — 87건)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS product_mgmt_upg_detail CASCADE;
|
||||
CREATE TABLE product_mgmt_upg_detail (
|
||||
objid integer NOT NULL,
|
||||
target_objid integer,
|
||||
upg_name character varying(100),
|
||||
upg_code character varying(100),
|
||||
vc character varying(100),
|
||||
note character varying(1000),
|
||||
product_objid integer,
|
||||
CONSTRAINT product_mgmt_upg_detail_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN product_mgmt_upg_detail.objid IS 'objid';
|
||||
COMMENT ON COLUMN product_mgmt_upg_detail.target_objid IS 'upg_masterobjid';
|
||||
COMMENT ON COLUMN product_mgmt_upg_detail.upg_name IS 'upg명';
|
||||
COMMENT ON COLUMN product_mgmt_upg_detail.upg_code IS 'upg코드';
|
||||
COMMENT ON COLUMN product_mgmt_upg_detail.vc IS 'vc';
|
||||
COMMENT ON COLUMN product_mgmt_upg_detail.note IS '비고';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 7) product_mgmt_upg_master (제품 업그레이드 마스터 — 6건, 복합 PK)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS product_mgmt_upg_master CASCADE;
|
||||
CREATE TABLE product_mgmt_upg_master (
|
||||
objid integer NOT NULL,
|
||||
target_objid integer NOT NULL,
|
||||
spec_name character varying NOT NULL,
|
||||
writer character varying,
|
||||
regdate timestamp,
|
||||
CONSTRAINT product_mgmt_upg_master_pkey PRIMARY KEY (objid, target_objid)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN product_mgmt_upg_master.objid IS '제품사양마스터코드';
|
||||
COMMENT ON COLUMN product_mgmt_upg_master.target_objid IS '양산마스터코드';
|
||||
COMMENT ON COLUMN product_mgmt_upg_master.spec_name IS '사양명';
|
||||
COMMENT ON COLUMN product_mgmt_upg_master.writer IS '작성자';
|
||||
COMMENT ON COLUMN product_mgmt_upg_master.regdate IS '동록일';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 8) sales_bom_report (영업 BOM 단가 — 1,529건)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS sales_bom_report CASCADE;
|
||||
CREATE TABLE sales_bom_report (
|
||||
objid character varying NOT NULL DEFAULT ''::character varying,
|
||||
parent_objid character varying,
|
||||
supply_objid character varying,
|
||||
price character varying,
|
||||
supply_objid1 character varying,
|
||||
price1 character varying(64),
|
||||
supply_objid2 character varying(100),
|
||||
price2 character varying(64),
|
||||
supply_objid3 character varying(64),
|
||||
price3 character varying(100),
|
||||
supply_objid4 character varying(100),
|
||||
price4 character varying(2000),
|
||||
writer character varying,
|
||||
regdate timestamp,
|
||||
update_date timestamp,
|
||||
modifier character varying,
|
||||
CONSTRAINT sales_bom_report_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
-- 운영 인덱스는 UNIQUE — 1 BOM(parent_objid) 당 1 단가행
|
||||
CREATE UNIQUE INDEX sales_bom_report_parent_objid_idx ON sales_bom_report USING btree (parent_objid);
|
||||
|
||||
COMMENT ON COLUMN sales_bom_report.objid IS '키';
|
||||
COMMENT ON COLUMN sales_bom_report.parent_objid IS 'bom_report_objid';
|
||||
COMMENT ON COLUMN sales_bom_report.supply_objid IS '공급업체key';
|
||||
COMMENT ON COLUMN sales_bom_report.price IS '단가';
|
||||
COMMENT ON COLUMN sales_bom_report.supply_objid1 IS '레이져업체명';
|
||||
COMMENT ON COLUMN sales_bom_report.price1 IS '단가';
|
||||
COMMENT ON COLUMN sales_bom_report.supply_objid2 IS '용접업체명';
|
||||
COMMENT ON COLUMN sales_bom_report.price2 IS '단가';
|
||||
COMMENT ON COLUMN sales_bom_report.supply_objid3 IS '가공업체명';
|
||||
COMMENT ON COLUMN sales_bom_report.price3 IS '단가';
|
||||
COMMENT ON COLUMN sales_bom_report.supply_objid4 IS '후처리';
|
||||
COMMENT ON COLUMN sales_bom_report.price4 IS '단가';
|
||||
COMMENT ON COLUMN sales_bom_report.writer IS '담당자';
|
||||
COMMENT ON COLUMN sales_bom_report.regdate IS '작성일';
|
||||
COMMENT ON COLUMN sales_bom_report.update_date IS '수정일';
|
||||
COMMENT ON COLUMN sales_bom_report.modifier IS '수정자';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 9) supply_mng (공급업체/고객 — 1건)
|
||||
-- ------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS supply_mng CASCADE;
|
||||
CREATE TABLE supply_mng (
|
||||
objid numeric NOT NULL DEFAULT 0,
|
||||
supply_code character varying(100) DEFAULT NULL::character varying,
|
||||
supply_name character varying(100) DEFAULT NULL::character varying,
|
||||
reg_no character varying(100) DEFAULT NULL::character varying,
|
||||
supply_address character varying(500) DEFAULT NULL::character varying,
|
||||
supply_busname character varying(100) DEFAULT NULL::character varying,
|
||||
supply_stockname character varying(100) DEFAULT NULL::character varying,
|
||||
supply_tel_no character varying(30) DEFAULT NULL::character varying,
|
||||
supply_fax_no character varying(30) DEFAULT NULL::character varying,
|
||||
charge_user_name character varying(100) DEFAULT NULL::character varying,
|
||||
payment_method character varying(100),
|
||||
reg_id character varying(100) DEFAULT NULL::character varying,
|
||||
reg_date timestamp,
|
||||
status character varying(32) DEFAULT NULL::character varying,
|
||||
area_cd character varying(32) DEFAULT NULL::character varying,
|
||||
bus_reg_no character varying(100) DEFAULT NULL::character varying,
|
||||
office_no character varying(32) DEFAULT 'NULL::character varying'::character varying,
|
||||
email character varying(32) DEFAULT 'NULL::character varying'::character varying,
|
||||
cus_no character varying,
|
||||
manager1_name character varying(100),
|
||||
manager1_email character varying(100),
|
||||
manager2_name character varying(100),
|
||||
manager2_email character varying(100),
|
||||
manager3_name character varying(100),
|
||||
manager3_email character varying(100),
|
||||
manager4_name character varying(100),
|
||||
manager4_email character varying(100),
|
||||
manager5_name character varying(100),
|
||||
manager5_email character varying(100),
|
||||
CONSTRAINT supply_mng_pkey PRIMARY KEY (objid)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN supply_mng.supply_code IS '구분';
|
||||
COMMENT ON COLUMN supply_mng.supply_name IS '고객명';
|
||||
COMMENT ON COLUMN supply_mng.reg_no IS '법인/주민번호';
|
||||
COMMENT ON COLUMN supply_mng.supply_address IS '주소';
|
||||
COMMENT ON COLUMN supply_mng.supply_busname IS '업태';
|
||||
COMMENT ON COLUMN supply_mng.supply_stockname IS '업종';
|
||||
COMMENT ON COLUMN supply_mng.supply_tel_no IS '핸드폰';
|
||||
COMMENT ON COLUMN supply_mng.supply_fax_no IS '팩스번호';
|
||||
COMMENT ON COLUMN supply_mng.charge_user_name IS '대표자명';
|
||||
COMMENT ON COLUMN supply_mng.reg_id IS '실사용자명';
|
||||
COMMENT ON COLUMN supply_mng.reg_date IS '등록일';
|
||||
COMMENT ON COLUMN supply_mng.status IS '상태';
|
||||
COMMENT ON COLUMN supply_mng.area_cd IS '지역';
|
||||
COMMENT ON COLUMN supply_mng.bus_reg_no IS '사업자등록번호';
|
||||
COMMENT ON COLUMN supply_mng.office_no IS '오피스no';
|
||||
COMMENT ON COLUMN supply_mng.email IS '이메일';
|
||||
COMMENT ON COLUMN supply_mng.cus_no IS '고객번호';
|
||||
COMMENT ON COLUMN supply_mng.manager1_name IS '담당자1 이름';
|
||||
COMMENT ON COLUMN supply_mng.manager1_email IS '담당자1 이메일';
|
||||
COMMENT ON COLUMN supply_mng.manager2_name IS '담당자2 이름';
|
||||
COMMENT ON COLUMN supply_mng.manager2_email IS '담당자2 이메일';
|
||||
COMMENT ON COLUMN supply_mng.manager3_name IS '담당자3 이름';
|
||||
COMMENT ON COLUMN supply_mng.manager3_email IS '담당자3 이메일';
|
||||
COMMENT ON COLUMN supply_mng.manager4_name IS '담당자4 이름';
|
||||
COMMENT ON COLUMN supply_mng.manager4_email IS '담당자4 이메일';
|
||||
COMMENT ON COLUMN supply_mng.manager5_name IS '담당자5 이름';
|
||||
COMMENT ON COLUMN supply_mng.manager5_email IS '담당자5 이메일';
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ============================================================
|
||||
-- vexplor_rps 적용 방법 (메인 agent 검토 후 직접 실행):
|
||||
-- PGPASSWORD='vexplor0909!!' psql -h 211.115.91.141 -p 11134 -U postgres -d vexplor_rps \
|
||||
-- -f /Users/jhj/vexplor_rps/docs/migration/development/ddl-extracted/300_part_bom.sql
|
||||
--
|
||||
-- 적용 후 검증:
|
||||
-- PGPASSWORD='vexplor0909!!' psql -h 211.115.91.141 -p 11134 -U postgres -d vexplor_rps -c "
|
||||
-- SELECT relname, (SELECT count(*) FROM pg_attribute WHERE attrelid = c.oid AND attnum > 0 AND NOT attisdropped) AS cols
|
||||
-- FROM pg_class c WHERE relkind='r' AND relnamespace=(SELECT oid FROM pg_namespace WHERE nspname='public')
|
||||
-- AND relname IN ('admin_supply_mng','bom_part_qty','order_spec_mng','part_bom_report','part_mng_history','product_mgmt_upg_detail','product_mgmt_upg_master','sales_bom_report','supply_mng')
|
||||
-- ORDER BY relname;"
|
||||
-- 기대값: 28 / 19 / 12 / 23 / 59 / 7 / 5 / 16 / 29
|
||||
-- ============================================================
|
||||
@@ -0,0 +1,45 @@
|
||||
-- ============================================================
|
||||
-- part_mng ALTER — 개발관리 메뉴(PART 등록/조회) 누락 컬럼 추가
|
||||
-- 추출일: 2026-05-12
|
||||
-- 출처: 211.115.91.141:11133/waceplm (PG 16.8)
|
||||
-- 대상: 211.115.91.141:11134/vexplor_rps
|
||||
--
|
||||
-- 사유: wace PART 등록/조회 그리드 23개 컬럼 중 15개가 vexplor part_mng 에 부재.
|
||||
-- 전부 ADD COLUMN IF NOT EXISTS 로 안전하게 추가 (IDEMPOTENT).
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS heat_treatment_hardness character varying;
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS heat_treatment_method character varying;
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS surface_treatment character varying;
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS acctfg character varying;
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS odrfg character varying;
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS unit_dc character varying(20);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS unitmang_dc character varying(20);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS unitchng_nb numeric(11,6);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS lot_fg character(1);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS use_yn character(1);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS qc_fg character(1);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS setitem_fg character(1);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS req_fg character(1);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS unit_length character varying(20);
|
||||
ALTER TABLE part_mng ADD COLUMN IF NOT EXISTS unit_qty character varying(20);
|
||||
|
||||
COMMENT ON COLUMN part_mng.heat_treatment_hardness IS '열처리경도';
|
||||
COMMENT ON COLUMN part_mng.heat_treatment_method IS '열처리방법';
|
||||
COMMENT ON COLUMN part_mng.surface_treatment IS '표면처리';
|
||||
COMMENT ON COLUMN part_mng.acctfg IS '계정구분 (comm_code)';
|
||||
COMMENT ON COLUMN part_mng.odrfg IS '조달구분 (comm_code)';
|
||||
COMMENT ON COLUMN part_mng.unit_dc IS '재고단위 (comm_code)';
|
||||
COMMENT ON COLUMN part_mng.unitmang_dc IS '관리단위 (comm_code)';
|
||||
COMMENT ON COLUMN part_mng.unitchng_nb IS '환산수량';
|
||||
COMMENT ON COLUMN part_mng.lot_fg IS 'LOT구분 (Y/N)';
|
||||
COMMENT ON COLUMN part_mng.use_yn IS '사용여부 (Y/N)';
|
||||
COMMENT ON COLUMN part_mng.qc_fg IS '검사여부 (Y/N)';
|
||||
COMMENT ON COLUMN part_mng.setitem_fg IS 'SET품여부 (Y/N)';
|
||||
COMMENT ON COLUMN part_mng.req_fg IS '의뢰여부 (Y/N)';
|
||||
COMMENT ON COLUMN part_mng.unit_length IS '개당길이';
|
||||
COMMENT ON COLUMN part_mng.unit_qty IS '개당수량';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
// 개발관리 > PART 등록 (M1) — wace partMngTempList.jsp 1:1
|
||||
// 그리드: status != 'release' 인 PART 23셀
|
||||
// 액션: 등록 / 수정 / 삭제 / 확정 / 조회
|
||||
// 참조: docs/migration/development/01-part.md
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Search, Loader2, RotateCcw, Plus, Pencil, Trash2, CheckSquare,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
||||
import { devPartApi, PartListFilter, PartRow } from "@/lib/api/devPart";
|
||||
import { PartFormDialog } from "@/components/development/PartFormDialog";
|
||||
import { PartDetailDialog } from "@/components/development/PartDetailDialog";
|
||||
|
||||
// wace 23셀 + 부속 (PARENT_PART_INFO/PARTNER_TITLE/Q_QTY)
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]", frozen: true },
|
||||
{ key: "part_name", label: "품명", minWidth: "min-w-[220px]" },
|
||||
{ key: "cu01_cnt", label: "3D", width: "w-[60px]", align: "right", formatNumber: true },
|
||||
{ key: "cu02_cnt", label: "2D", width: "w-[60px]", align: "right", formatNumber: true },
|
||||
{ key: "cu03_cnt", label: "PDF", width: "w-[60px]", align: "right", formatNumber: true },
|
||||
{ key: "material", label: "재료", width: "w-[100px]" },
|
||||
{ key: "heat_treatment_hardness", label: "열처리경도", width: "w-[110px]" },
|
||||
{ key: "heat_treatment_method", label: "열처리방법", width: "w-[110px]" },
|
||||
{ key: "surface_treatment", label: "표면처리", width: "w-[100px]" },
|
||||
{ key: "maker", label: "메이커", width: "w-[100px]" },
|
||||
{ key: "part_type_title", label: "범주", width: "w-[100px]" },
|
||||
{ key: "spec", label: "규격", width: "w-[140px]" },
|
||||
{ key: "acctfg_nm", label: "계정구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "odrfg_nm", label: "조달구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_dc_nm", label: "재고단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitmang_dc_nm", label: "관리단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitchng_nb", label: "환산수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "lot_fg_nm", label: "LOT구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "use_yn_nm", label: "사용여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "qc_fg_nm", label: "검사여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "setitem_fg_nm", label: "SET품여부", width: "w-[90px]", align: "center" },
|
||||
{ key: "req_fg_nm", label: "의뢰여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_length", label: "개당길이", width: "w-[90px]", align: "right" },
|
||||
{ key: "unit_qty", label: "개당수량", width: "w-[90px]", align: "right" },
|
||||
// M1 부속
|
||||
{ key: "partner_title", label: "공급업체(시퀀스)", minWidth: "min-w-[180px]" },
|
||||
{ key: "parent_part_info", label: "상위 품번", width: "w-[120px]" },
|
||||
{ key: "q_qty", label: "수량(BOM)", width: "w-[90px]", align: "right" },
|
||||
{ key: "revision", label: "REV", width: "w-[60px]", align: "center" },
|
||||
{ key: "status", label: "상태", width: "w-[80px]", align: "center" },
|
||||
];
|
||||
|
||||
const EMPTY_FILTER: PartListFilter = {
|
||||
search_part_no: "", search_part_name: "",
|
||||
page: 1, page_size: 50,
|
||||
};
|
||||
|
||||
export default function PartRegistPage() {
|
||||
const [rows, setRows] = useState<PartRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filter, setFilter] = useState<PartListFilter>(EMPTY_FILTER);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
||||
const [formObjid, setFormObjid] = useState<string | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailObjid, setDetailObjid] = useState<string | null>(null);
|
||||
|
||||
const fetchList = useCallback(async (override?: Partial<PartListFilter>) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const f = { ...filter, ...override };
|
||||
const res = await devPartApi.listTemp(f);
|
||||
setRows(res.rows ?? []);
|
||||
setTotal(res.total ?? 0);
|
||||
setCheckedIds([]);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => { fetchList(); /* eslint-disable-next-line */ }, []);
|
||||
|
||||
// 행 더블클릭 → 상세 다이얼로그
|
||||
const columns = useMemo(
|
||||
() => GRID_COLUMNS.map((c) =>
|
||||
c.key === "part_no"
|
||||
? { ...c, onClick: (row: any) => { setDetailObjid(row.objid); setDetailOpen(true); } }
|
||||
: c,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// 등록
|
||||
const handleCreate = () => {
|
||||
setFormMode("create");
|
||||
setFormObjid(null);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
// 수정 (단일 선택 필요)
|
||||
const handleEdit = () => {
|
||||
if (checkedIds.length !== 1) return toast.error("수정할 행 1개를 선택하세요.");
|
||||
setFormMode("edit");
|
||||
setFormObjid(checkedIds[0]);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
// 삭제 (다중)
|
||||
const handleDelete = async () => {
|
||||
if (checkedIds.length === 0) return toast.error("선택된 행이 없습니다.");
|
||||
if (!confirm(`${checkedIds.length}건을 삭제하시겠습니까?`)) return;
|
||||
try {
|
||||
const res = await devPartApi.remove(checkedIds);
|
||||
toast.success(res?.message ?? "삭제되었습니다.");
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "삭제 실패");
|
||||
}
|
||||
};
|
||||
|
||||
// 확정 (M1 → M2): EO_NO 채번 + part_mng_history 이력
|
||||
const handleDeploy = async () => {
|
||||
if (checkedIds.length === 0) return toast.error("확정할 행을 선택하세요.");
|
||||
if (!confirm(`${checkedIds.length}건을 확정하시겠습니까? (M1 → M2)`)) return;
|
||||
try {
|
||||
const res = await devPartApi.deploy(checkedIds);
|
||||
toast.success(`${res.deployed}건이 확정되었습니다.`);
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "확정 실패");
|
||||
}
|
||||
};
|
||||
|
||||
// 상세 → 수정 전환
|
||||
const handleEditFromDetail = (objid: string) => {
|
||||
setDetailOpen(false);
|
||||
setFormMode("edit");
|
||||
setFormObjid(objid);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 검색폼 — wace partMngTempList.jsp 활성 2필드 */}
|
||||
<div className="border-b bg-card px-4 py-3">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="min-w-[200px]">
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">품번</Label>
|
||||
<Input
|
||||
value={filter.search_part_no ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, search_part_no: e.target.value })}
|
||||
placeholder="품번 LIKE"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-[200px]">
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">품명</Label>
|
||||
<Input
|
||||
value={filter.search_part_name ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, search_part_name: e.target.value })}
|
||||
placeholder="품명 LIKE"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex items-end gap-2">
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}>
|
||||
<RotateCcw className="h-4 w-4" /><span className="ml-1">초기화</span>
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => fetchList()} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
|
||||
<span className="ml-1">조회</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="default" onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4" /><span className="ml-1">등록</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleEdit}
|
||||
disabled={checkedIds.length !== 1}>
|
||||
<Pencil className="h-4 w-4" /><span className="ml-1">수정</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" onClick={handleDelete}
|
||||
disabled={checkedIds.length === 0}>
|
||||
<Trash2 className="h-4 w-4" /><span className="ml-1">삭제</span>
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleDeploy}
|
||||
disabled={checkedIds.length === 0}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
||||
<CheckSquare className="h-4 w-4" /><span className="ml-1">확정</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
총 {total.toLocaleString()}건 (M1: status ≠ 'release')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 p-2">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="조건에 맞는 PART가 없습니다."
|
||||
gridId="development-part-regist"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PartFormDialog
|
||||
open={formOpen}
|
||||
onOpenChange={setFormOpen}
|
||||
mode={formMode}
|
||||
editObjid={formObjid}
|
||||
onSaved={fetchList}
|
||||
/>
|
||||
<PartDetailDialog
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
objid={detailObjid}
|
||||
onEdit={handleEditFromDetail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
// 개발관리 > PART 조회 (M2) — wace partMngList.jsp 1:1
|
||||
// 그리드: status = 'release' 인 PART 23셀 + BOM_QTY
|
||||
// 액션: 등록 / 수정 / 삭제 / 조회 (도면연동/ERP/Excel은 별 PR)
|
||||
// 참조: docs/migration/development/01-part.md
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Search, Loader2, RotateCcw, Plus, Pencil, Trash2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
||||
import { devPartApi, PartListFilter, PartRow } from "@/lib/api/devPart";
|
||||
import { PartFormDialog } from "@/components/development/PartFormDialog";
|
||||
import { PartDetailDialog } from "@/components/development/PartDetailDialog";
|
||||
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "part_no", label: "품번", width: "w-[140px]", frozen: true },
|
||||
{ key: "part_name", label: "품명", minWidth: "min-w-[220px]" },
|
||||
{ key: "cu01_cnt", label: "3D", width: "w-[60px]", align: "right", formatNumber: true },
|
||||
{ key: "cu02_cnt", label: "2D", width: "w-[60px]", align: "right", formatNumber: true },
|
||||
{ key: "cu03_cnt", label: "PDF", width: "w-[60px]", align: "right", formatNumber: true },
|
||||
{ key: "material", label: "재료", width: "w-[100px]" },
|
||||
{ key: "heat_treatment_hardness", label: "열처리경도", width: "w-[110px]" },
|
||||
{ key: "heat_treatment_method", label: "열처리방법", width: "w-[110px]" },
|
||||
{ key: "surface_treatment", label: "표면처리", width: "w-[100px]" },
|
||||
{ key: "maker", label: "메이커", width: "w-[100px]" },
|
||||
{ key: "part_type_title", label: "범주", width: "w-[100px]" },
|
||||
{ key: "spec", label: "규격", width: "w-[140px]" },
|
||||
{ key: "acctfg_nm", label: "계정구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "odrfg_nm", label: "조달구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_dc_nm", label: "재고단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitmang_dc_nm", label: "관리단위", width: "w-[80px]", align: "center" },
|
||||
{ key: "unitchng_nb", label: "환산수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "lot_fg_nm", label: "LOT구분", width: "w-[80px]", align: "center" },
|
||||
{ key: "use_yn_nm", label: "사용여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "qc_fg_nm", label: "검사여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "setitem_fg_nm", label: "SET품여부", width: "w-[90px]", align: "center" },
|
||||
{ key: "req_fg_nm", label: "의뢰여부", width: "w-[80px]", align: "center" },
|
||||
{ key: "unit_length", label: "개당길이", width: "w-[90px]", align: "right" },
|
||||
{ key: "unit_qty", label: "개당수량", width: "w-[90px]", align: "right" },
|
||||
// M2 추가
|
||||
{ key: "bom_qty", label: "BOM 수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "revision", label: "REV", width: "w-[60px]", align: "center" },
|
||||
{ key: "eo_no", label: "EO_NO", width: "w-[120px]" },
|
||||
];
|
||||
|
||||
const EMPTY_FILTER: PartListFilter = {
|
||||
search_part_no: "", search_part_name: "",
|
||||
page: 1, page_size: 50,
|
||||
};
|
||||
|
||||
export default function PartSearchPage() {
|
||||
const [rows, setRows] = useState<PartRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filter, setFilter] = useState<PartListFilter>(EMPTY_FILTER);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
||||
const [formObjid, setFormObjid] = useState<string | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailObjid, setDetailObjid] = useState<string | null>(null);
|
||||
|
||||
const fetchList = useCallback(async (override?: Partial<PartListFilter>) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const f = { ...filter, ...override };
|
||||
const res = await devPartApi.list(f);
|
||||
setRows(res.rows ?? []);
|
||||
setTotal(res.total ?? 0);
|
||||
setCheckedIds([]);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => { fetchList(); /* eslint-disable-next-line */ }, []);
|
||||
|
||||
const columns = useMemo(
|
||||
() => GRID_COLUMNS.map((c) =>
|
||||
c.key === "part_no"
|
||||
? { ...c, onClick: (row: any) => { setDetailObjid(row.objid); setDetailOpen(true); } }
|
||||
: c,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormMode("create"); setFormObjid(null); setFormOpen(true);
|
||||
};
|
||||
const handleEdit = () => {
|
||||
if (checkedIds.length !== 1) return toast.error("수정할 행 1개를 선택하세요.");
|
||||
setFormMode("edit"); setFormObjid(checkedIds[0]); setFormOpen(true);
|
||||
};
|
||||
const handleDelete = async () => {
|
||||
if (checkedIds.length === 0) return toast.error("선택된 행이 없습니다.");
|
||||
if (!confirm(`${checkedIds.length}건을 삭제하시겠습니까?`)) return;
|
||||
try {
|
||||
const res = await devPartApi.remove(checkedIds);
|
||||
toast.success(res?.message ?? "삭제되었습니다.");
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "삭제 실패");
|
||||
}
|
||||
};
|
||||
const handleEditFromDetail = (objid: string) => {
|
||||
setDetailOpen(false);
|
||||
setFormMode("edit"); setFormObjid(objid); setFormOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b bg-card px-4 py-3">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="min-w-[200px]">
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">품번</Label>
|
||||
<Input
|
||||
value={filter.search_part_no ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, search_part_no: e.target.value })}
|
||||
placeholder="품번 LIKE"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-[200px]">
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">품명</Label>
|
||||
<Input
|
||||
value={filter.search_part_name ?? ""}
|
||||
onChange={(e) => setFilter({ ...filter, search_part_name: e.target.value })}
|
||||
placeholder="품명 LIKE"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex items-end gap-2">
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}>
|
||||
<RotateCcw className="h-4 w-4" /><span className="ml-1">초기화</span>
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => fetchList()} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
|
||||
<span className="ml-1">조회</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="default" onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4" /><span className="ml-1">등록</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleEdit}
|
||||
disabled={checkedIds.length !== 1}>
|
||||
<Pencil className="h-4 w-4" /><span className="ml-1">수정</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" onClick={handleDelete}
|
||||
disabled={checkedIds.length === 0}>
|
||||
<Trash2 className="h-4 w-4" /><span className="ml-1">삭제</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
총 {total.toLocaleString()}건 (M2: status = 'release')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 p-2">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="조건에 맞는 PART가 없습니다."
|
||||
gridId="development-part-search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PartFormDialog
|
||||
open={formOpen}
|
||||
onOpenChange={setFormOpen}
|
||||
mode={formMode}
|
||||
editObjid={formObjid}
|
||||
onSaved={fetchList}
|
||||
/>
|
||||
<PartDetailDialog
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
objid={detailObjid}
|
||||
onEdit={handleEditFromDetail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
"use client";
|
||||
|
||||
// 개발관리 > PART 상세 조회 다이얼로그 (read-only).
|
||||
// 행 더블클릭 진입. "수정" 버튼 클릭 시 PartFormDialog(mode='edit')로 전환은
|
||||
// 호출 페이지가 dispatch (open=false → 부모가 form dialog 오픈).
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Loader2, Pencil } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { devPartApi, PartRow } from "@/lib/api/devPart";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
objid: string | null;
|
||||
/** "수정" 버튼 클릭 시 호출 — 부모는 본 다이얼로그 닫고 PartFormDialog(mode='edit')를 띄움 */
|
||||
onEdit?: (objid: string) => void;
|
||||
}
|
||||
|
||||
export function PartDetailDialog({ open, onOpenChange, objid, onEdit }: Props) {
|
||||
const [row, setRow] = useState<PartRow | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !objid) return;
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
devPartApi.detail(objid)
|
||||
.then((data) => { if (alive) setRow(data); })
|
||||
.catch((e: any) => {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
onOpenChange(false);
|
||||
})
|
||||
.finally(() => { if (alive) setLoading(false); });
|
||||
return () => { alive = false; };
|
||||
}, [open, objid, onOpenChange]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>PART 상세 정보</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{loading || !row ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[70vh] space-y-4 overflow-y-auto px-1 py-2 text-sm">
|
||||
<Section title="기본정보">
|
||||
<Row>
|
||||
<V label="품번" value={row.part_no} />
|
||||
<V label="품명" value={row.part_name} />
|
||||
<V label="범주" value={row.part_type_title} />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="단위" value={row.unit_title} />
|
||||
<V label="수량" value={row.qty} align="right" />
|
||||
<V label="규격" value={row.spec} />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="재료" value={row.material} />
|
||||
<V label="메이커" value={row.maker} />
|
||||
<V label="비고" value={row.remark} />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="공급업체" value={row.supply_name} />
|
||||
<V label="등록자" value={row.writer} />
|
||||
<V label="등록일" value={row.part_regdate_title} />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="REVISION" value={row.revision} />
|
||||
<V label="EO_NO" value={row.eo_no} />
|
||||
<V label="STATUS" value={row.status} />
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Section title="크기 / 형상">
|
||||
<Row>
|
||||
<V label="두께" value={row.thickness} align="right" />
|
||||
<V label="너비(W)" value={row.width} align="right" />
|
||||
<V label="높이(H)" value={row.height} align="right" />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="외경" value={row.out_diameter} align="right" />
|
||||
<V label="내경" value={row.in_diameter} align="right" />
|
||||
<V label="길이(L)" value={row.length} align="right" />
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Section title="분류 / 단위">
|
||||
<Row>
|
||||
<V label="계정구분" value={row.acctfg_nm} />
|
||||
<V label="조달구분" value={row.odrfg_nm} />
|
||||
<V label="환산수량" value={row.unitchng_nb != null ? String(row.unitchng_nb) : ""} align="right" />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="재고단위" value={row.unit_dc_nm} />
|
||||
<V label="관리단위" value={row.unitmang_dc_nm} />
|
||||
<V label="개당길이 / 개당수량"
|
||||
value={[row.unit_length, row.unit_qty].filter(Boolean).join(" / ")} align="right" />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="열처리경도" value={row.heat_treatment_hardness} />
|
||||
<V label="열처리방법" value={row.heat_treatment_method} />
|
||||
<V label="표면처리" value={row.surface_treatment} />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="후가공" value={row.post_processing} />
|
||||
<V label="첨부 (3D/2D/PDF)"
|
||||
value={`${row.cu01_cnt ?? 0} / ${row.cu02_cnt ?? 0} / ${row.cu03_cnt ?? 0}`} align="center" />
|
||||
<V label="" value="" />
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Section title="Y/N 플래그">
|
||||
<Row>
|
||||
<V label="LOT구분" value={row.lot_fg_nm} align="center" />
|
||||
<V label="사용여부" value={row.use_yn_nm} align="center" />
|
||||
<V label="검사여부" value={row.qc_fg_nm} align="center" />
|
||||
</Row>
|
||||
<Row>
|
||||
<V label="SET품여부" value={row.setitem_fg_nm} align="center" />
|
||||
<V label="의뢰여부" value={row.req_fg_nm} align="center" />
|
||||
<V label="" value="" />
|
||||
</Row>
|
||||
</Section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{row && onEdit && (
|
||||
<Button onClick={() => onEdit(row.objid)}>
|
||||
<Pencil className="h-4 w-4" /><span className="ml-1">수정</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>닫기</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 보조 ─────────────────────────────────────────────────
|
||||
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-md border bg-card p-3">
|
||||
<div className="mb-2 text-sm font-semibold text-foreground">{title}</div>
|
||||
<div className="space-y-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ children }: { children: React.ReactNode }) {
|
||||
return <div className="grid grid-cols-3 gap-3">{children}</div>;
|
||||
}
|
||||
|
||||
function V({ label, value, align }: { label: string; value: any; align?: "left" | "center" | "right" }) {
|
||||
const cls = align === "right" ? "text-right" : align === "center" ? "text-center" : "";
|
||||
return (
|
||||
<div>
|
||||
{label && <Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>}
|
||||
<div className={`min-h-9 rounded-md border bg-muted/30 px-2 py-2 ${cls}`}>
|
||||
{value != null && value !== "" ? value : <span className="text-muted-foreground">—</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
"use client";
|
||||
|
||||
// 개발관리 > PART 등록/수정 통합 다이얼로그.
|
||||
// wace partMngFormPopUp.jsp + partMngDetailPopUp.jsp 1:1 (mode 분기).
|
||||
//
|
||||
// 신규: POST /api/development/part (38 컬럼)
|
||||
// 수정: PUT /api/development/part/:objid (21 컬럼만 — wace updatePartDetail 1:1)
|
||||
//
|
||||
// 그룹:
|
||||
// ① 기본정보 (필수 ★: part_no, part_name, part_type)
|
||||
// ② 크기/형상
|
||||
// ③ 분류/단위 (comm_code SmartSelect)
|
||||
// ④ Y/N 플래그 (radio '1'/'0')
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Loader2, Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
|
||||
import { devPartApi, PartCreateBody, PartUpdateBody, PartRow } from "@/lib/api/devPart";
|
||||
|
||||
// comm_code 그룹 ID (vexplor_rps DB 실재 그룹)
|
||||
const GROUP_PART_TYPE = "0000062"; // PART TYPE (조립품/부품/구매품)
|
||||
const GROUP_UNIT = "0001399"; // 단위 (m/Set/EA/식/BAG/kg/...)
|
||||
const GROUP_ACCTFG = "0900213"; // 파트_계정구분 (원자재/제품/...)
|
||||
|
||||
// ODRFG: spec '0=구매/1=생산/8=Phantom' — 하드코딩
|
||||
const ODRFG_OPTIONS = [
|
||||
{ code: "0", label: "구매" },
|
||||
{ code: "1", label: "생산" },
|
||||
{ code: "8", label: "Phantom" },
|
||||
];
|
||||
|
||||
interface FormState {
|
||||
// 기본
|
||||
part_no: string;
|
||||
part_name: string;
|
||||
part_type: string;
|
||||
unit: string;
|
||||
qty: string;
|
||||
spec: string;
|
||||
material: string;
|
||||
remark: string;
|
||||
maker: string;
|
||||
// 크기/형상
|
||||
thickness: string;
|
||||
width: string;
|
||||
height: string;
|
||||
out_diameter: string;
|
||||
in_diameter: string;
|
||||
length: string;
|
||||
// 분류/단위
|
||||
acctfg: string;
|
||||
odrfg: string;
|
||||
unit_dc: string;
|
||||
unitmang_dc: string;
|
||||
unitchng_nb: string;
|
||||
unit_length: string;
|
||||
unit_qty: string;
|
||||
// 열처리/표면처리/후가공
|
||||
heat_treatment_hardness: string;
|
||||
heat_treatment_method: string;
|
||||
surface_treatment: string;
|
||||
post_processing: string;
|
||||
// 부속 (신규 시만 표시)
|
||||
product_mgmt_objid: string;
|
||||
supply_code: string;
|
||||
contract_objid: string;
|
||||
// Y/N
|
||||
lot_fg: string;
|
||||
use_yn: string;
|
||||
qc_fg: string;
|
||||
setitem_fg: string;
|
||||
req_fg: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM: FormState = {
|
||||
part_no: "", part_name: "", part_type: "", unit: "", qty: "", spec: "", material: "", remark: "", maker: "",
|
||||
thickness: "", width: "", height: "", out_diameter: "", in_diameter: "", length: "",
|
||||
acctfg: "", odrfg: "", unit_dc: "", unitmang_dc: "", unitchng_nb: "", unit_length: "", unit_qty: "",
|
||||
heat_treatment_hardness: "", heat_treatment_method: "", surface_treatment: "", post_processing: "",
|
||||
product_mgmt_objid: "", supply_code: "", contract_objid: "",
|
||||
lot_fg: "1", use_yn: "1", qc_fg: "0", setitem_fg: "0", req_fg: "0",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
mode: "create" | "edit";
|
||||
editObjid?: string | null;
|
||||
onSaved: () => void;
|
||||
}
|
||||
|
||||
export function PartFormDialog({ open, onOpenChange, mode, editObjid, onSaved }: Props) {
|
||||
const isEdit = mode === "edit";
|
||||
const [form, setForm] = useState<FormState>(EMPTY_FORM);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const setField = useCallback(
|
||||
<K extends keyof FormState>(key: K, value: FormState[K]) =>
|
||||
setForm((prev) => ({ ...prev, [key]: value })),
|
||||
[]
|
||||
);
|
||||
|
||||
// 초기화/로드
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (isEdit && editObjid) {
|
||||
loadDetail(editObjid);
|
||||
} else {
|
||||
setForm(EMPTY_FORM);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const loadDetail = async (objid: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const row = await devPartApi.detail(objid);
|
||||
if (!row) {
|
||||
toast.error("PART를 찾을 수 없습니다.");
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
setForm(rowToForm(row));
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.part_no.trim()) return toast.error("품번은 필수입니다.");
|
||||
if (!form.part_name.trim()) return toast.error("품명은 필수입니다.");
|
||||
if (!isEdit && !form.part_type.trim()) return toast.error("범주(PART TYPE)는 필수입니다.");
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEdit && editObjid) {
|
||||
const body: PartUpdateBody = {
|
||||
part_name: form.part_name,
|
||||
material: form.material,
|
||||
heat_treatment_hardness: form.heat_treatment_hardness,
|
||||
heat_treatment_method: form.heat_treatment_method,
|
||||
surface_treatment: form.surface_treatment,
|
||||
maker: form.maker,
|
||||
part_type: form.part_type,
|
||||
acctfg: form.acctfg,
|
||||
odrfg: form.odrfg,
|
||||
spec: form.spec,
|
||||
unit_dc: form.unit_dc,
|
||||
unitmang_dc: form.unitmang_dc,
|
||||
unitchng_nb: form.unitchng_nb,
|
||||
lot_fg: form.lot_fg,
|
||||
use_yn: form.use_yn,
|
||||
qc_fg: form.qc_fg,
|
||||
setitem_fg: form.setitem_fg,
|
||||
req_fg: form.req_fg,
|
||||
unit_length: form.unit_length,
|
||||
unit_qty: form.unit_qty,
|
||||
remark: form.remark,
|
||||
};
|
||||
await devPartApi.update(editObjid, body);
|
||||
toast.success("PART가 수정되었습니다.");
|
||||
} else {
|
||||
const body: PartCreateBody = {
|
||||
part_no: form.part_no,
|
||||
part_name: form.part_name,
|
||||
part_type: form.part_type,
|
||||
unit: form.unit,
|
||||
qty: form.qty,
|
||||
spec: form.spec,
|
||||
material: form.material,
|
||||
thickness: form.thickness,
|
||||
width: form.width,
|
||||
height: form.height,
|
||||
out_diameter: form.out_diameter,
|
||||
in_diameter: form.in_diameter,
|
||||
length: form.length,
|
||||
remark: form.remark,
|
||||
product_mgmt_objid: form.product_mgmt_objid,
|
||||
supply_code: form.supply_code,
|
||||
maker: form.maker,
|
||||
contract_objid: form.contract_objid,
|
||||
post_processing: form.post_processing,
|
||||
heat_treatment_hardness: form.heat_treatment_hardness,
|
||||
heat_treatment_method: form.heat_treatment_method,
|
||||
surface_treatment: form.surface_treatment,
|
||||
acctfg: form.acctfg,
|
||||
odrfg: form.odrfg,
|
||||
unit_dc: form.unit_dc,
|
||||
unitmang_dc: form.unitmang_dc,
|
||||
unitchng_nb: form.unitchng_nb,
|
||||
lot_fg: form.lot_fg,
|
||||
use_yn: form.use_yn,
|
||||
qc_fg: form.qc_fg,
|
||||
setitem_fg: form.setitem_fg,
|
||||
req_fg: form.req_fg,
|
||||
unit_length: form.unit_length,
|
||||
unit_qty: form.unit_qty,
|
||||
};
|
||||
await devPartApi.create(body);
|
||||
toast.success("PART가 등록되었습니다.");
|
||||
}
|
||||
onSaved();
|
||||
onOpenChange(false);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const titleText = isEdit ? "PART 수정" : "PART 신규 등록";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{titleText}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[70vh] space-y-4 overflow-y-auto px-1 py-2">
|
||||
{/* ① 기본정보 */}
|
||||
<Section title="기본정보">
|
||||
<Row>
|
||||
<Field label="품번" required>
|
||||
<Input value={form.part_no} disabled={isEdit}
|
||||
onChange={(e) => setField("part_no", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="품명" required>
|
||||
<Input value={form.part_name}
|
||||
onChange={(e) => setField("part_name", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="범주(PART TYPE)" required>
|
||||
<CommCodeSelect groupId={GROUP_PART_TYPE} withAll={false}
|
||||
value={form.part_type}
|
||||
onValueChange={(v) => setField("part_type", v)} />
|
||||
</Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field label="단위">
|
||||
<CommCodeSelect groupId={GROUP_UNIT} withAll={false}
|
||||
value={form.unit}
|
||||
onValueChange={(v) => setField("unit", v)} />
|
||||
</Field>
|
||||
<Field label="수량">
|
||||
<Input value={form.qty} className="text-right"
|
||||
onChange={(e) => setField("qty", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="규격">
|
||||
<Input value={form.spec}
|
||||
onChange={(e) => setField("spec", e.target.value)} />
|
||||
</Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field label="재료">
|
||||
<Input value={form.material}
|
||||
onChange={(e) => setField("material", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="메이커">
|
||||
<Input value={form.maker}
|
||||
onChange={(e) => setField("maker", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="비고">
|
||||
<Input value={form.remark}
|
||||
onChange={(e) => setField("remark", e.target.value)} />
|
||||
</Field>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* ② 크기/형상 */}
|
||||
<Section title="크기 / 형상">
|
||||
<Row>
|
||||
<Field label="두께"><Input value={form.thickness} className="text-right"
|
||||
onChange={(e) => setField("thickness", e.target.value)} /></Field>
|
||||
<Field label="너비(W)"><Input value={form.width} className="text-right"
|
||||
onChange={(e) => setField("width", e.target.value)} /></Field>
|
||||
<Field label="높이(H)"><Input value={form.height} className="text-right"
|
||||
onChange={(e) => setField("height", e.target.value)} /></Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field label="외경"><Input value={form.out_diameter} className="text-right"
|
||||
onChange={(e) => setField("out_diameter", e.target.value)} /></Field>
|
||||
<Field label="내경"><Input value={form.in_diameter} className="text-right"
|
||||
onChange={(e) => setField("in_diameter", e.target.value)} /></Field>
|
||||
<Field label="길이(L)"><Input value={form.length} className="text-right"
|
||||
onChange={(e) => setField("length", e.target.value)} /></Field>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* ③ 분류 / 단위 */}
|
||||
<Section title="분류 / 단위">
|
||||
<Row>
|
||||
<Field label="계정구분">
|
||||
<CommCodeSelect groupId={GROUP_ACCTFG} withAll={false}
|
||||
value={form.acctfg}
|
||||
onValueChange={(v) => setField("acctfg", v)} />
|
||||
</Field>
|
||||
<Field label="조달구분">
|
||||
<select className="h-9 w-full rounded-md border bg-background px-2 text-sm"
|
||||
value={form.odrfg}
|
||||
onChange={(e) => setField("odrfg", e.target.value)}>
|
||||
<option value="">선택</option>
|
||||
{ODRFG_OPTIONS.map((o) =>
|
||||
<option key={o.code} value={o.code}>{o.label}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="환산수량(UNITCHNG_NB)">
|
||||
<Input value={form.unitchng_nb} className="text-right"
|
||||
onChange={(e) => setField("unitchng_nb", e.target.value)} />
|
||||
</Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field label="재고단위">
|
||||
<CommCodeSelect groupId={GROUP_UNIT} withAll={false}
|
||||
value={form.unit_dc}
|
||||
onValueChange={(v) => setField("unit_dc", v)} />
|
||||
</Field>
|
||||
<Field label="관리단위">
|
||||
<CommCodeSelect groupId={GROUP_UNIT} withAll={false}
|
||||
value={form.unitmang_dc}
|
||||
onValueChange={(v) => setField("unitmang_dc", v)} />
|
||||
</Field>
|
||||
<Field label="개당길이 / 개당수량">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input value={form.unit_length} className="text-right"
|
||||
placeholder="개당길이"
|
||||
onChange={(e) => setField("unit_length", e.target.value)} />
|
||||
<span className="text-xs text-muted-foreground">/</span>
|
||||
<Input value={form.unit_qty} className="text-right"
|
||||
placeholder="개당수량"
|
||||
onChange={(e) => setField("unit_qty", e.target.value)} />
|
||||
</div>
|
||||
</Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field label="열처리경도">
|
||||
<Input value={form.heat_treatment_hardness}
|
||||
onChange={(e) => setField("heat_treatment_hardness", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="열처리방법">
|
||||
<Input value={form.heat_treatment_method}
|
||||
onChange={(e) => setField("heat_treatment_method", e.target.value)} />
|
||||
</Field>
|
||||
<Field label="표면처리">
|
||||
<Input value={form.surface_treatment}
|
||||
onChange={(e) => setField("surface_treatment", e.target.value)} />
|
||||
</Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field label="후가공">
|
||||
<Input value={form.post_processing}
|
||||
onChange={(e) => setField("post_processing", e.target.value)} />
|
||||
</Field>
|
||||
{!isEdit && (
|
||||
<Field label="공급업체 코드">
|
||||
<Input value={form.supply_code}
|
||||
onChange={(e) => setField("supply_code", e.target.value)}
|
||||
placeholder="admin_supply_mng.objid" />
|
||||
</Field>
|
||||
)}
|
||||
{!isEdit && (
|
||||
<Field label="제품 OBJID">
|
||||
<Input value={form.product_mgmt_objid}
|
||||
onChange={(e) => setField("product_mgmt_objid", e.target.value)}
|
||||
placeholder="product_mgmt.objid" />
|
||||
</Field>
|
||||
)}
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* ④ Y/N 플래그 */}
|
||||
<Section title="Y/N 플래그">
|
||||
<Row>
|
||||
<Field label="LOT구분"><YNRadio value={form.lot_fg} onChange={(v) => setField("lot_fg", v)} /></Field>
|
||||
<Field label="사용여부"><YNRadio value={form.use_yn} onChange={(v) => setField("use_yn", v)} /></Field>
|
||||
<Field label="검사여부"><YNRadio value={form.qc_fg} onChange={(v) => setField("qc_fg", v)} /></Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field label="SET품여부"><YNRadio value={form.setitem_fg} onChange={(v) => setField("setitem_fg", v)} /></Field>
|
||||
<Field label="의뢰여부"><YNRadio value={form.req_fg} onChange={(v) => setField("req_fg", v)} /></Field>
|
||||
<Field label="" />
|
||||
</Row>
|
||||
</Section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={saving || loading}>
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
<span className="ml-1">{isEdit ? "수정" : "등록"}</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 보조 컴포넌트 ──────────────────────────────────────────
|
||||
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-md border bg-card p-3">
|
||||
<div className="mb-2 text-sm font-semibold text-foreground">{title}</div>
|
||||
<div className="space-y-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ children }: { children: React.ReactNode }) {
|
||||
return <div className="grid grid-cols-3 gap-3">{children}</div>;
|
||||
}
|
||||
|
||||
function Field({ label, required, children }: { label: string; required?: boolean; children?: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-red-500">*</span>}
|
||||
</Label>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function YNRadio({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<div className="flex h-9 items-center gap-3 rounded-md border bg-background px-3 text-sm">
|
||||
<label className="flex items-center gap-1">
|
||||
<input type="radio" checked={value === "1"} onChange={() => onChange("1")} />
|
||||
<span>예</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1">
|
||||
<input type="radio" checked={value === "0"} onChange={() => onChange("0")} />
|
||||
<span>아니오</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── PartRow → FormState 매핑 ──────────────────────────────
|
||||
|
||||
function rowToForm(r: PartRow): FormState {
|
||||
return {
|
||||
part_no: r.part_no ?? "",
|
||||
part_name: r.part_name ?? "",
|
||||
part_type: r.part_type ?? "",
|
||||
unit: r.unit ?? "",
|
||||
qty: r.qty ?? "",
|
||||
spec: r.spec ?? "",
|
||||
material: r.material ?? "",
|
||||
remark: r.remark ?? "",
|
||||
maker: r.maker ?? "",
|
||||
thickness: r.thickness ?? "",
|
||||
width: r.width ?? "",
|
||||
height: r.height ?? "",
|
||||
out_diameter: r.out_diameter ?? "",
|
||||
in_diameter: r.in_diameter ?? "",
|
||||
length: r.length ?? "",
|
||||
acctfg: r.acctfg ?? "",
|
||||
odrfg: r.odrfg ?? "",
|
||||
unit_dc: r.unit_dc ?? "",
|
||||
unitmang_dc: r.unitmang_dc ?? "",
|
||||
unitchng_nb: r.unitchng_nb != null ? String(r.unitchng_nb) : "",
|
||||
unit_length: r.unit_length ?? "",
|
||||
unit_qty: r.unit_qty ?? "",
|
||||
heat_treatment_hardness: r.heat_treatment_hardness ?? "",
|
||||
heat_treatment_method: r.heat_treatment_method ?? "",
|
||||
surface_treatment: r.surface_treatment ?? "",
|
||||
post_processing: r.post_processing ?? "",
|
||||
product_mgmt_objid: r.product_mgmt_objid ?? "",
|
||||
supply_code: r.supply_code ?? "",
|
||||
contract_objid: r.contract_objid ?? "",
|
||||
lot_fg: r.lot_fg ?? "1",
|
||||
use_yn: r.use_yn ?? "1",
|
||||
qc_fg: r.qc_fg ?? "0",
|
||||
setitem_fg: r.setitem_fg ?? "0",
|
||||
req_fg: r.req_fg ?? "0",
|
||||
};
|
||||
}
|
||||
@@ -107,6 +107,8 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/COMPANY_16/sales/quote": dynamic(() => import("@/app/(main)/COMPANY_16/sales/quote/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/project/progress": dynamic(() => import("@/app/(main)/COMPANY_16/project/progress/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/project/wbs-template": dynamic(() => import("@/app/(main)/COMPANY_16/project/wbs-template/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/development/part-regist": dynamic(() => import("@/app/(main)/COMPANY_16/development/part-regist/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/development/part-search": dynamic(() => import("@/app/(main)/COMPANY_16/development/part-search/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/process-info": dynamic(() => import("@/app/(main)/COMPANY_16/production/process-info/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/result": dynamic(() => import("@/app/(main)/COMPANY_16/production/result/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/work-instruction": dynamic(() => import("@/app/(main)/COMPANY_16/production/work-instruction/page"), { ssr: false, loading: LoadingFallback }),
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { apiClient } from "./client";
|
||||
|
||||
// ============================================================
|
||||
// 개발관리 PART (M1 등록 / M2 조회) — wace partMng.xml 1:1
|
||||
// 라우트: /api/development/part-temp/*, /api/development/part/*
|
||||
// ============================================================
|
||||
|
||||
export interface PartListFilter {
|
||||
search_part_no?: string;
|
||||
search_part_name?: string;
|
||||
search_material?: string;
|
||||
search_spec?: string;
|
||||
search_part_type?: string;
|
||||
writer?: string;
|
||||
status?: string;
|
||||
status_arr?: string[];
|
||||
product_code?: string;
|
||||
upg_no?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
|
||||
// M2 추가 필터
|
||||
search_year?: string;
|
||||
search_design_date_from?: string;
|
||||
search_design_date_to?: string;
|
||||
customer_objid?: string;
|
||||
customer_cd?: string;
|
||||
project_name?: string;
|
||||
unit_code?: string;
|
||||
is_last?: string;
|
||||
eo?: string;
|
||||
}
|
||||
|
||||
/** partMngBaseSimple + M1/M2 추가 컬럼 평탄화 — Postgres는 컬럼명을 소문자로 반환 */
|
||||
export interface PartRow {
|
||||
objid: string;
|
||||
part_no: string | null;
|
||||
part_name: string | null;
|
||||
product_mgmt_objid: string | null;
|
||||
upg_no: string | null;
|
||||
unit: string | null;
|
||||
unit_title: string | null;
|
||||
qty: string | null;
|
||||
spec: string | null;
|
||||
post_processing: string | null;
|
||||
material: string | null;
|
||||
weight: string | null;
|
||||
part_type: string | null;
|
||||
part_type_title: string | null;
|
||||
remark: string | null;
|
||||
es_spec: string | null;
|
||||
ms_spec: string | null;
|
||||
change_type: string | null;
|
||||
design_apply_point: string | null;
|
||||
change_option: string | null;
|
||||
change_option_name: string | null;
|
||||
management_flag: string | null;
|
||||
revision: string | null;
|
||||
status: string | null;
|
||||
reg_date: string | null;
|
||||
part_regdate_title: string | null;
|
||||
edit_date: string | null;
|
||||
writer: string | null;
|
||||
is_last: string | null;
|
||||
is_longd: string | null;
|
||||
eo_date: string | null;
|
||||
eo_no: string | null;
|
||||
eo_temp: string | null;
|
||||
maker: string | null;
|
||||
contract_objid: string | null;
|
||||
thickness: string | null;
|
||||
width: string | null;
|
||||
height: string | null;
|
||||
out_diameter: string | null;
|
||||
in_diameter: string | null;
|
||||
length: string | null;
|
||||
sourcing_code: string | null;
|
||||
supply_code: string | null;
|
||||
supply_name: string | null;
|
||||
sub_material: string | null;
|
||||
parent_part_no: string | null;
|
||||
design_date: string | null;
|
||||
deploy_date: string | null;
|
||||
excel_upload_seq: string | number | null;
|
||||
cu01_cnt: number | string | null;
|
||||
cu02_cnt: number | string | null;
|
||||
cu03_cnt: number | string | null;
|
||||
cu_total_cnt: number | string | null;
|
||||
|
||||
// 추가 15컬럼 + 라벨
|
||||
heat_treatment_hardness: string | null;
|
||||
heat_treatment_method: string | null;
|
||||
surface_treatment: string | null;
|
||||
acctfg: string | null;
|
||||
acctfg_nm: string | null;
|
||||
odrfg: string | null;
|
||||
odrfg_nm: string | null;
|
||||
unit_dc: string | null;
|
||||
unit_dc_nm: string | null;
|
||||
unitmang_dc: string | null;
|
||||
unitmang_dc_nm: string | null;
|
||||
unitchng_nb: string | number | null;
|
||||
lot_fg: string | null;
|
||||
lot_fg_nm: string | null;
|
||||
use_yn: string | null;
|
||||
use_yn_nm: string | null;
|
||||
qc_fg: string | null;
|
||||
qc_fg_nm: string | null;
|
||||
setitem_fg: string | null;
|
||||
setitem_fg_nm: string | null;
|
||||
req_fg: string | null;
|
||||
req_fg_nm: string | null;
|
||||
unit_length: string | null;
|
||||
unit_qty: string | null;
|
||||
|
||||
// M1 전용 부속
|
||||
partner_title?: string | null;
|
||||
parent_part_info?: string | null;
|
||||
bom_report_objid?: string | null;
|
||||
objid_qty?: string | null;
|
||||
child_objid?: string | null;
|
||||
q_qty?: string | null;
|
||||
q_qty_raw?: string | null;
|
||||
qty_temp?: string | null;
|
||||
sort?: string | null;
|
||||
|
||||
// M2 전용 부속
|
||||
num?: number | null;
|
||||
bom_qty?: string | null;
|
||||
}
|
||||
|
||||
export interface PartListResponse {
|
||||
rows: PartRow[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface PartCreateBody {
|
||||
part_objid?: string;
|
||||
part_no: string;
|
||||
part_name: string;
|
||||
unit?: string;
|
||||
qty?: string;
|
||||
spec?: string;
|
||||
material?: string;
|
||||
thickness?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
out_diameter?: string;
|
||||
in_diameter?: string;
|
||||
length?: string;
|
||||
remark?: string;
|
||||
part_type: string;
|
||||
product_mgmt_objid?: string;
|
||||
supply_code?: string;
|
||||
maker?: string;
|
||||
contract_objid?: string;
|
||||
post_processing?: string;
|
||||
heat_treatment_hardness?: string;
|
||||
heat_treatment_method?: string;
|
||||
surface_treatment?: string;
|
||||
acctfg?: string;
|
||||
odrfg?: string;
|
||||
unit_dc?: string;
|
||||
unitmang_dc?: string;
|
||||
unitchng_nb?: string;
|
||||
lot_fg?: string;
|
||||
use_yn?: string;
|
||||
qc_fg?: string;
|
||||
setitem_fg?: string;
|
||||
req_fg?: string;
|
||||
unit_length?: string;
|
||||
unit_qty?: string;
|
||||
}
|
||||
|
||||
export interface PartUpdateBody {
|
||||
part_name?: string;
|
||||
material?: string;
|
||||
heat_treatment_hardness?: string;
|
||||
heat_treatment_method?: string;
|
||||
surface_treatment?: string;
|
||||
maker?: string;
|
||||
part_type?: string;
|
||||
acctfg?: string;
|
||||
odrfg?: string;
|
||||
spec?: string;
|
||||
unit_dc?: string;
|
||||
unitmang_dc?: string;
|
||||
unitchng_nb?: string;
|
||||
lot_fg?: string;
|
||||
use_yn?: string;
|
||||
qc_fg?: string;
|
||||
setitem_fg?: string;
|
||||
req_fg?: string;
|
||||
unit_length?: string;
|
||||
unit_qty?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface DeployResult {
|
||||
deployed: number;
|
||||
eo_nos: Record<string, string>;
|
||||
}
|
||||
|
||||
// ─── API ────────────────────────────────────────────────────
|
||||
|
||||
export const devPartApi = {
|
||||
// M1 그리드
|
||||
async listTemp(filter: PartListFilter = {}): Promise<PartListResponse> {
|
||||
const res = await apiClient.get("/development/part-temp/list", { params: filter });
|
||||
return res.data?.data as PartListResponse;
|
||||
},
|
||||
|
||||
// M2 그리드
|
||||
async list(filter: PartListFilter = {}): Promise<PartListResponse> {
|
||||
const res = await apiClient.get("/development/part/list", { params: filter });
|
||||
return res.data?.data as PartListResponse;
|
||||
},
|
||||
|
||||
// 단건 상세
|
||||
async detail(objid: string): Promise<PartRow | null> {
|
||||
const res = await apiClient.get(`/development/part/${objid}`);
|
||||
return res.data?.data ?? null;
|
||||
},
|
||||
|
||||
// 신규 등록 (38 컬럼)
|
||||
async create(body: PartCreateBody): Promise<{ objid: string }> {
|
||||
const res = await apiClient.post("/development/part", body);
|
||||
return res.data?.data;
|
||||
},
|
||||
|
||||
// 상세 수정 (21 컬럼)
|
||||
async update(objid: string, body: PartUpdateBody) {
|
||||
return (await apiClient.put(`/development/part/${objid}`, body)).data;
|
||||
},
|
||||
|
||||
// 확정 (M1→M2): EO_NO 채번 + part_mng_history 이력
|
||||
async deploy(objids: string[]): Promise<DeployResult> {
|
||||
const res = await apiClient.post("/development/part-temp/deploy", { objids });
|
||||
return res.data?.data as DeployResult;
|
||||
},
|
||||
|
||||
// 다중 삭제
|
||||
async remove(objids: string[]) {
|
||||
const res = await apiClient.delete("/development/part", { data: { objids } });
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user