Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map
This commit is contained in:
@@ -9,6 +9,7 @@ import { AdminService } from "../services/adminService";
|
||||
import { EncryptUtil } from "../utils/encryptUtil";
|
||||
import { FileSystemManager } from "../utils/fileSystemManager";
|
||||
import { validateBusinessNumber } from "../utils/businessNumberValidator";
|
||||
import { MenuCopyService } from "../services/menuCopyService";
|
||||
|
||||
/**
|
||||
* 관리자 메뉴 목록 조회
|
||||
@@ -3253,3 +3254,93 @@ export async function getTableSchema(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 복사
|
||||
* POST /api/admin/menus/:menuObjid/copy
|
||||
*/
|
||||
export async function copyMenu(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { menuObjid } = req.params;
|
||||
const { targetCompanyCode } = req.body;
|
||||
const userId = req.user!.userId;
|
||||
const userCompanyCode = req.user!.companyCode;
|
||||
const userType = req.user!.userType;
|
||||
const isSuperAdmin = req.user!.isSuperAdmin;
|
||||
|
||||
logger.info(`
|
||||
=== 메뉴 복사 API 호출 ===
|
||||
menuObjid: ${menuObjid}
|
||||
targetCompanyCode: ${targetCompanyCode}
|
||||
userId: ${userId}
|
||||
userCompanyCode: ${userCompanyCode}
|
||||
userType: ${userType}
|
||||
isSuperAdmin: ${isSuperAdmin}
|
||||
`);
|
||||
|
||||
// 권한 체크: 최고 관리자만 가능
|
||||
if (!isSuperAdmin && userType !== "SUPER_ADMIN") {
|
||||
logger.warn(`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`);
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다",
|
||||
error: {
|
||||
code: "FORBIDDEN",
|
||||
details: "Only super admin can copy menus",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 필수 파라미터 검증
|
||||
if (!menuObjid || !targetCompanyCode) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 파라미터가 누락되었습니다",
|
||||
error: {
|
||||
code: "MISSING_PARAMETERS",
|
||||
details: "menuObjid and targetCompanyCode are required",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 화면명 변환 설정 (선택사항)
|
||||
const screenNameConfig = req.body.screenNameConfig
|
||||
? {
|
||||
removeText: req.body.screenNameConfig.removeText,
|
||||
addPrefix: req.body.screenNameConfig.addPrefix,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// 메뉴 복사 실행
|
||||
const menuCopyService = new MenuCopyService();
|
||||
const result = await menuCopyService.copyMenu(
|
||||
parseInt(menuObjid, 10),
|
||||
targetCompanyCode,
|
||||
userId,
|
||||
screenNameConfig
|
||||
);
|
||||
|
||||
logger.info("✅ 메뉴 복사 API 성공");
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "메뉴 복사 완료",
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("❌ 메뉴 복사 API 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "메뉴 복사 중 오류가 발생했습니다",
|
||||
error: {
|
||||
code: "MENU_COPY_ERROR",
|
||||
details: error.message || "Unknown error",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export async function createOrder(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 목록 조회 API
|
||||
* 수주 목록 조회 API (마스터 + 품목 JOIN)
|
||||
* GET /api/orders
|
||||
*/
|
||||
export async function getOrders(req: AuthenticatedRequest, res: Response) {
|
||||
@@ -184,14 +184,14 @@ export async function getOrders(req: AuthenticatedRequest, res: Response) {
|
||||
|
||||
// 멀티테넌시 (writer 필드에 company_code 포함)
|
||||
if (companyCode !== "*") {
|
||||
whereConditions.push(`writer LIKE $${paramIndex}`);
|
||||
whereConditions.push(`m.writer LIKE $${paramIndex}`);
|
||||
params.push(`%${companyCode}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 검색
|
||||
if (searchText) {
|
||||
whereConditions.push(`objid LIKE $${paramIndex}`);
|
||||
whereConditions.push(`m.objid LIKE $${paramIndex}`);
|
||||
params.push(`%${searchText}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
@@ -201,16 +201,47 @@ export async function getOrders(req: AuthenticatedRequest, res: Response) {
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
// 카운트 쿼리
|
||||
const countQuery = `SELECT COUNT(*) as count FROM order_mng_master ${whereClause}`;
|
||||
// 카운트 쿼리 (고유한 수주 개수)
|
||||
const countQuery = `
|
||||
SELECT COUNT(DISTINCT m.objid) as count
|
||||
FROM order_mng_master m
|
||||
${whereClause}
|
||||
`;
|
||||
const countResult = await pool.query(countQuery, params);
|
||||
const total = parseInt(countResult.rows[0]?.count || "0");
|
||||
|
||||
// 데이터 쿼리
|
||||
// 데이터 쿼리 (마스터 + 품목 JOIN)
|
||||
const dataQuery = `
|
||||
SELECT * FROM order_mng_master
|
||||
SELECT
|
||||
m.objid as order_no,
|
||||
m.partner_objid,
|
||||
m.final_delivery_date,
|
||||
m.reason,
|
||||
m.status,
|
||||
m.reg_date,
|
||||
m.writer,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
CASE WHEN s.objid IS NOT NULL THEN
|
||||
json_build_object(
|
||||
'sub_objid', s.objid,
|
||||
'part_objid', s.part_objid,
|
||||
'partner_price', s.partner_price,
|
||||
'partner_qty', s.partner_qty,
|
||||
'delivery_date', s.delivery_date,
|
||||
'status', s.status,
|
||||
'regdate', s.regdate
|
||||
)
|
||||
END
|
||||
ORDER BY s.regdate
|
||||
) FILTER (WHERE s.objid IS NOT NULL),
|
||||
'[]'::json
|
||||
) as items
|
||||
FROM order_mng_master m
|
||||
LEFT JOIN order_mng_sub s ON m.objid = s.order_mng_master_objid
|
||||
${whereClause}
|
||||
ORDER BY reg_date DESC
|
||||
GROUP BY m.objid, m.partner_objid, m.final_delivery_date, m.reason, m.status, m.reg_date, m.writer
|
||||
ORDER BY m.reg_date DESC
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`;
|
||||
|
||||
@@ -219,6 +250,13 @@ export async function getOrders(req: AuthenticatedRequest, res: Response) {
|
||||
|
||||
const dataResult = await pool.query(dataQuery, params);
|
||||
|
||||
logger.info("수주 목록 조회 성공", {
|
||||
companyCode,
|
||||
total,
|
||||
page: parseInt(page as string),
|
||||
itemCount: dataResult.rows.length,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: dataResult.rows,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
deleteMenu, // 메뉴 삭제
|
||||
deleteMenusBatch, // 메뉴 일괄 삭제
|
||||
toggleMenuStatus, // 메뉴 상태 토글
|
||||
copyMenu, // 메뉴 복사
|
||||
getUserList,
|
||||
getUserInfo, // 사용자 상세 조회
|
||||
getUserHistory, // 사용자 변경이력 조회
|
||||
@@ -39,6 +40,7 @@ router.get("/menus", getAdminMenus);
|
||||
router.get("/user-menus", getUserMenus);
|
||||
router.get("/menus/:menuId", getMenuInfo);
|
||||
router.post("/menus", saveMenu); // 메뉴 추가
|
||||
router.post("/menus/:menuObjid/copy", copyMenu); // 메뉴 복사 (NEW!)
|
||||
router.put("/menus/:menuId", updateMenu); // 메뉴 수정
|
||||
router.put("/menus/:menuId/toggle", toggleMenuStatus); // 메뉴 상태 토글
|
||||
router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!)
|
||||
|
||||
@@ -320,19 +320,34 @@ export class DynamicFormService {
|
||||
Object.keys(dataToInsert).forEach((key) => {
|
||||
const value = dataToInsert[key];
|
||||
|
||||
// RepeaterInput 데이터인지 확인 (JSON 배열 문자열)
|
||||
if (
|
||||
// 🔥 RepeaterInput 데이터인지 확인 (배열 객체 또는 JSON 문자열)
|
||||
let parsedArray: any[] | null = null;
|
||||
|
||||
// 1️⃣ 이미 배열 객체인 경우 (ModalRepeaterTable, SelectedItemsDetailInput 등)
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
parsedArray = value;
|
||||
console.log(
|
||||
`🔄 배열 객체 Repeater 데이터 감지: ${key}, ${parsedArray.length}개 항목`
|
||||
);
|
||||
}
|
||||
// 2️⃣ JSON 문자열인 경우 (레거시 RepeaterInput)
|
||||
else if (
|
||||
typeof value === "string" &&
|
||||
value.trim().startsWith("[") &&
|
||||
value.trim().endsWith("]")
|
||||
) {
|
||||
try {
|
||||
const parsedArray = JSON.parse(value);
|
||||
if (Array.isArray(parsedArray) && parsedArray.length > 0) {
|
||||
parsedArray = JSON.parse(value);
|
||||
console.log(
|
||||
`🔄 RepeaterInput 데이터 감지: ${key}, ${parsedArray.length}개 항목`
|
||||
`🔄 JSON 문자열 Repeater 데이터 감지: ${key}, ${parsedArray?.length || 0}개 항목`
|
||||
);
|
||||
} catch (parseError) {
|
||||
console.log(`⚠️ JSON 파싱 실패: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 파싱된 배열이 있으면 처리
|
||||
if (parsedArray && Array.isArray(parsedArray) && parsedArray.length > 0) {
|
||||
// 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해)
|
||||
// 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음
|
||||
let targetTable: string | undefined;
|
||||
@@ -352,13 +367,34 @@ export class DynamicFormService {
|
||||
componentId: key,
|
||||
});
|
||||
delete dataToInsert[key]; // 원본 배열 데이터는 제거
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.log(`⚠️ JSON 파싱 실패: ${key}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Repeater 데이터 추가: ${key}`, {
|
||||
targetTable: targetTable || "없음 (화면 설계에서 설정 필요)",
|
||||
itemCount: actualData.length,
|
||||
firstItem: actualData[0],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 🔥 Repeater targetTable이 메인 테이블과 같으면 분리해서 저장
|
||||
const separateRepeaterData: typeof repeaterData = [];
|
||||
const mergedRepeaterData: typeof repeaterData = [];
|
||||
|
||||
repeaterData.forEach(repeater => {
|
||||
if (repeater.targetTable && repeater.targetTable !== tableName) {
|
||||
// 다른 테이블: 나중에 별도 저장
|
||||
separateRepeaterData.push(repeater);
|
||||
} else {
|
||||
// 같은 테이블: 메인 INSERT와 병합 (헤더+품목을 한 번에)
|
||||
mergedRepeaterData.push(repeater);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🔄 Repeater 데이터 분류:`, {
|
||||
separate: separateRepeaterData.length, // 별도 테이블
|
||||
merged: mergedRepeaterData.length, // 메인 테이블과 병합
|
||||
});
|
||||
|
||||
// 존재하지 않는 컬럼 제거
|
||||
Object.keys(dataToInsert).forEach((key) => {
|
||||
if (!tableColumns.includes(key)) {
|
||||
@@ -369,9 +405,6 @@ export class DynamicFormService {
|
||||
}
|
||||
});
|
||||
|
||||
// RepeaterInput 데이터 처리 로직은 메인 저장 후에 처리
|
||||
// (각 Repeater가 다른 테이블에 저장될 수 있으므로)
|
||||
|
||||
console.log("🎯 실제 테이블에 삽입할 데이터:", {
|
||||
tableName,
|
||||
dataToInsert,
|
||||
@@ -452,28 +485,106 @@ export class DynamicFormService {
|
||||
const userId = data.updated_by || data.created_by || "system";
|
||||
const clientIp = ipAddress || "unknown";
|
||||
|
||||
const result = await transaction(async (client) => {
|
||||
// 세션 변수 설정
|
||||
await client.query(`SET LOCAL app.user_id = '${userId}'`);
|
||||
await client.query(`SET LOCAL app.ip_address = '${clientIp}'`);
|
||||
|
||||
// UPSERT 실행
|
||||
const res = await client.query(upsertQuery, values);
|
||||
return res.rows;
|
||||
});
|
||||
|
||||
console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
|
||||
let result: any[];
|
||||
|
||||
// 🔥 메인 테이블과 병합할 Repeater가 있으면 각 품목별로 INSERT
|
||||
if (mergedRepeaterData.length > 0) {
|
||||
console.log(`🔄 메인 테이블 병합 모드: ${mergedRepeaterData.length}개 Repeater를 개별 레코드로 저장`);
|
||||
|
||||
result = [];
|
||||
|
||||
for (const repeater of mergedRepeaterData) {
|
||||
for (const item of repeater.data) {
|
||||
// 헤더 + 품목을 병합
|
||||
const rawMergedData = { ...dataToInsert, ...item };
|
||||
|
||||
// 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외)
|
||||
const validColumnNames = columnInfo.map((col) => col.column_name);
|
||||
const mergedData: Record<string, any> = {};
|
||||
|
||||
Object.keys(rawMergedData).forEach((columnName) => {
|
||||
// 실제 테이블 컬럼인지 확인
|
||||
if (validColumnNames.includes(columnName)) {
|
||||
const column = columnInfo.find((col) => col.column_name === columnName);
|
||||
if (column) {
|
||||
// 타입 변환
|
||||
mergedData[columnName] = this.convertValueForPostgreSQL(
|
||||
rawMergedData[columnName],
|
||||
column.data_type
|
||||
);
|
||||
} else {
|
||||
mergedData[columnName] = rawMergedData[columnName];
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`);
|
||||
}
|
||||
});
|
||||
|
||||
const mergedColumns = Object.keys(mergedData);
|
||||
const mergedValues: any[] = Object.values(mergedData);
|
||||
const mergedPlaceholders = mergedValues.map((_, index) => `$${index + 1}`).join(", ");
|
||||
|
||||
let mergedUpsertQuery: string;
|
||||
if (primaryKeys.length > 0) {
|
||||
const conflictColumns = primaryKeys.join(", ");
|
||||
const updateSet = mergedColumns
|
||||
.filter((col) => !primaryKeys.includes(col))
|
||||
.map((col) => `${col} = EXCLUDED.${col}`)
|
||||
.join(", ");
|
||||
|
||||
mergedUpsertQuery = updateSet
|
||||
? `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
|
||||
VALUES (${mergedPlaceholders})
|
||||
ON CONFLICT (${conflictColumns})
|
||||
DO UPDATE SET ${updateSet}
|
||||
RETURNING *`
|
||||
: `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
|
||||
VALUES (${mergedPlaceholders})
|
||||
ON CONFLICT (${conflictColumns})
|
||||
DO NOTHING
|
||||
RETURNING *`;
|
||||
} else {
|
||||
mergedUpsertQuery = `INSERT INTO ${tableName} (${mergedColumns.join(", ")})
|
||||
VALUES (${mergedPlaceholders})
|
||||
RETURNING *`;
|
||||
}
|
||||
|
||||
console.log(`📝 병합 INSERT:`, { mergedData });
|
||||
|
||||
const itemResult = await transaction(async (client) => {
|
||||
await client.query(`SET LOCAL app.user_id = '${userId}'`);
|
||||
await client.query(`SET LOCAL app.ip_address = '${clientIp}'`);
|
||||
const res = await client.query(mergedUpsertQuery, mergedValues);
|
||||
return res.rows[0];
|
||||
});
|
||||
|
||||
result.push(itemResult);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 병합 저장 완료: ${result.length}개 레코드`);
|
||||
} else {
|
||||
// 일반 모드: 헤더만 저장
|
||||
result = await transaction(async (client) => {
|
||||
await client.query(`SET LOCAL app.user_id = '${userId}'`);
|
||||
await client.query(`SET LOCAL app.ip_address = '${clientIp}'`);
|
||||
const res = await client.query(upsertQuery, values);
|
||||
return res.rows;
|
||||
});
|
||||
|
||||
console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
|
||||
}
|
||||
|
||||
// 결과를 표준 형식으로 변환
|
||||
const insertedRecord = Array.isArray(result) ? result[0] : result;
|
||||
|
||||
// 📝 RepeaterInput 데이터 저장 (각 Repeater를 해당 테이블에 저장)
|
||||
if (repeaterData.length > 0) {
|
||||
// 📝 별도 테이블 Repeater 데이터 저장
|
||||
if (separateRepeaterData.length > 0) {
|
||||
console.log(
|
||||
`🔄 RepeaterInput 데이터 저장 시작: ${repeaterData.length}개 Repeater`
|
||||
`🔄 별도 테이블 Repeater 저장 시작: ${separateRepeaterData.length}개`
|
||||
);
|
||||
|
||||
for (const repeater of repeaterData) {
|
||||
for (const repeater of separateRepeaterData) {
|
||||
const targetTableName = repeater.targetTable || tableName;
|
||||
console.log(
|
||||
`📝 Repeater "${repeater.componentId}" → 테이블 "${targetTableName}"에 ${repeater.data.length}개 항목 저장`
|
||||
@@ -497,8 +608,13 @@ export class DynamicFormService {
|
||||
created_by,
|
||||
updated_by,
|
||||
regdate: new Date(),
|
||||
// 🔥 멀티테넌시: company_code 필수 추가
|
||||
company_code: data.company_code || company_code,
|
||||
};
|
||||
|
||||
// 🔥 별도 테이블인 경우에만 외래키 추가
|
||||
// (같은 테이블이면 이미 병합 모드에서 처리됨)
|
||||
|
||||
// 대상 테이블에 존재하는 컬럼만 필터링
|
||||
Object.keys(itemData).forEach((key) => {
|
||||
if (!targetColumnNames.includes(key)) {
|
||||
|
||||
1861
backend-node/src/services/menuCopyService.ts
Normal file
1861
backend-node/src/services/menuCopyService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -300,10 +300,9 @@ class NumberingRuleService {
|
||||
FROM numbering_rules
|
||||
WHERE
|
||||
scope_type = 'global'
|
||||
OR scope_type = 'table'
|
||||
OR (scope_type = 'menu' AND menu_objid = ANY($1))
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($1)) -- ⚠️ 임시: table 스코프도 menu_objid로 필터링
|
||||
OR (scope_type = 'table' AND menu_objid IS NULL) -- ⚠️ 임시: 기존 규칙(menu_objid NULL) 포함
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($1)) -- ✅ 메뉴별로 필터링
|
||||
OR (scope_type = 'table' AND menu_objid IS NULL) -- ✅ 기존 규칙(menu_objid NULL) 포함 (하위 호환성)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($1)) THEN 1
|
||||
@@ -313,9 +312,9 @@ class NumberingRuleService {
|
||||
created_at DESC
|
||||
`;
|
||||
params = [siblingObjids];
|
||||
logger.info("최고 관리자: 형제 메뉴 포함 채번 규칙 조회 (기존 규칙 포함)", { siblingObjids });
|
||||
logger.info("최고 관리자: 형제 메뉴 기반 채번 규칙 조회 (메뉴별 필터링)", { siblingObjids });
|
||||
} else {
|
||||
// 일반 회사: 자신의 규칙만 조회 (형제 메뉴 포함)
|
||||
// 일반 회사: 자신의 규칙만 조회 (형제 메뉴 포함, 메뉴별 필터링)
|
||||
query = `
|
||||
SELECT
|
||||
rule_id AS "ruleId",
|
||||
@@ -336,10 +335,9 @@ class NumberingRuleService {
|
||||
WHERE company_code = $1
|
||||
AND (
|
||||
scope_type = 'global'
|
||||
OR scope_type = 'table'
|
||||
OR (scope_type = 'menu' AND menu_objid = ANY($2))
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($2)) -- ⚠️ 임시: table 스코프도 menu_objid로 필터링
|
||||
OR (scope_type = 'table' AND menu_objid IS NULL) -- ⚠️ 임시: 기존 규칙(menu_objid NULL) 포함
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($2)) -- ✅ 메뉴별로 필터링
|
||||
OR (scope_type = 'table' AND menu_objid IS NULL) -- ✅ 기존 규칙(menu_objid NULL) 포함 (하위 호환성)
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
@@ -350,7 +348,7 @@ class NumberingRuleService {
|
||||
created_at DESC
|
||||
`;
|
||||
params = [companyCode, siblingObjids];
|
||||
logger.info("회사별: 형제 메뉴 포함 채번 규칙 조회 (기존 규칙 포함)", { companyCode, siblingObjids });
|
||||
logger.info("회사별: 형제 메뉴 기반 채번 규칙 조회 (메뉴별 필터링)", { companyCode, siblingObjids });
|
||||
}
|
||||
|
||||
logger.info("🔍 채번 규칙 쿼리 실행", {
|
||||
|
||||
80
backend-node/src/types/order.ts
Normal file
80
backend-node/src/types/order.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 수주 관리 타입 정의
|
||||
*/
|
||||
|
||||
/**
|
||||
* 수주 품목 (order_mng_sub)
|
||||
*/
|
||||
export interface OrderItem {
|
||||
sub_objid: string; // 품목 고유 ID (예: ORD-20251121-051_1)
|
||||
part_objid: string; // 품목 코드
|
||||
partner_price: number; // 단가
|
||||
partner_qty: number; // 수량
|
||||
delivery_date: string | null; // 납기일
|
||||
status: string; // 상태
|
||||
regdate: string; // 등록일
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 마스터 (order_mng_master)
|
||||
*/
|
||||
export interface OrderMaster {
|
||||
order_no: string; // 수주 번호 (예: ORD-20251121-051)
|
||||
partner_objid: string; // 거래처 코드
|
||||
final_delivery_date: string | null; // 최종 납품일
|
||||
reason: string | null; // 메모/사유
|
||||
status: string; // 상태
|
||||
reg_date: string; // 등록일
|
||||
writer: string; // 작성자 (userId|companyCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 + 품목 (API 응답)
|
||||
*/
|
||||
export interface OrderWithItems extends OrderMaster {
|
||||
items: OrderItem[]; // 품목 목록
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 등록 요청
|
||||
*/
|
||||
export interface CreateOrderRequest {
|
||||
inputMode: string; // 입력 방식
|
||||
salesType?: string; // 판매 유형 (국내/해외)
|
||||
priceType?: string; // 단가 방식
|
||||
customerCode: string; // 거래처 코드
|
||||
contactPerson?: string; // 담당자
|
||||
deliveryDestination?: string; // 납품처
|
||||
deliveryAddress?: string; // 납품장소
|
||||
deliveryDate?: string; // 납품일
|
||||
items: Array<{
|
||||
item_code?: string; // 품목 코드
|
||||
id?: string; // 품목 ID (item_code 대체)
|
||||
quantity?: number; // 수량
|
||||
unit_price?: number; // 단가
|
||||
selling_price?: number; // 판매가
|
||||
amount?: number; // 금액
|
||||
delivery_date?: string; // 품목별 납기일
|
||||
}>;
|
||||
memo?: string; // 메모
|
||||
tradeInfo?: {
|
||||
// 해외 판매 시
|
||||
incoterms?: string;
|
||||
paymentTerms?: string;
|
||||
currency?: string;
|
||||
portOfLoading?: string;
|
||||
portOfDischarge?: string;
|
||||
hsCode?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 등록 응답
|
||||
*/
|
||||
export interface CreateOrderResponse {
|
||||
orderNo: string; // 생성된 수주 번호
|
||||
masterObjid: string; // 마스터 ID
|
||||
itemCount: number; // 품목 개수
|
||||
totalAmount: number; // 전체 금액
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user