Merge branch 'main' of https://g.wace.me/jskim/vexplor_dev
Some checks failed
Build and Push Images / build-and-push (push) Failing after 48s

This commit is contained in:
SeongHyun Kim
2026-04-09 14:30:32 +09:00
186 changed files with 100844 additions and 4881 deletions

View File

@@ -4108,6 +4108,7 @@ interface UserWithDeptRequest {
dept_name?: string;
position_code?: string;
position_name?: string;
end_date?: string | null;
};
mainDept?: {
dept_code: string;
@@ -4199,6 +4200,7 @@ export const saveUserWithDept = async (
dept_name: deptName,
position_code: userInfo.position_code,
position_name: positionName,
end_date: userInfo.end_date !== undefined ? (userInfo.end_date ? `${userInfo.end_date.substring(0, 10)}T00:00:00+09:00` : null) : undefined,
company_code: companyCode !== "*" ? companyCode : undefined,
};
@@ -4230,8 +4232,8 @@ export const saveUserWithDept = async (
email, tel, cell_phone, sabun,
user_type, user_type_name, status, locale,
dept_code, dept_name, position_code, position_name,
company_code, regdate
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW())`,
company_code, end_date, regdate
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, NOW())`,
[
userInfo.user_id,
userInfo.user_name,
@@ -4250,6 +4252,7 @@ export const saveUserWithDept = async (
userInfo.position_code || null,
positionName,
companyCode !== "*" ? companyCode : null,
userInfo.end_date ? `${userInfo.end_date.substring(0, 10)}T00:00:00+09:00` : null,
]
);
}

View File

@@ -256,11 +256,11 @@ export async function getPurchaseReportData(req: any, res: Response): Promise<vo
COALESCE(po.manager, '미지정') as manager,
COALESCE(po.status, '') as status,
CAST(COALESCE(NULLIF(pd.order_qty, ''), '0') AS numeric) as "orderQty",
CAST(COALESCE(NULLIF(po.received_qty, ''), '0') AS numeric) as "receiveQty",
CAST(COALESCE(NULLIF(pd.received_qty, ''), NULLIF(po.received_qty, ''), '0') AS numeric) as "receiveQty",
CAST(COALESCE(NULLIF(pd.unit_price, ''), '0') AS numeric) as "unitPrice",
CAST(COALESCE(NULLIF(pd.order_qty, ''), '0') AS numeric)
* CAST(COALESCE(NULLIF(pd.unit_price, ''), '0') AS numeric) as "orderAmt",
CAST(COALESCE(NULLIF(po.received_qty, ''), '0') AS numeric)
CAST(COALESCE(NULLIF(pd.received_qty, ''), NULLIF(po.received_qty, ''), '0') AS numeric)
* CAST(COALESCE(NULLIF(pd.unit_price, ''), '0') AS numeric) as "receiveAmt",
1 as "orderCnt",
pd.company_code

View File

@@ -843,45 +843,45 @@ export const previewFile = async (
return;
}
// 파일 경로에서 회사코드와 날짜 폴더 추출
const filePathParts = fileRecord.file_path!.split("/");
let fileCompanyCode = filePathParts[2] || "DEFAULT";
// company_* 처리 (실제 회사 코드로 변환)
if (fileCompanyCode === "company_*") {
fileCompanyCode = "company_*"; // 실제 디렉토리명 유지
}
// file_path의 /uploads/ 이후를 baseUploadDir과 직접 결합
const fileName = fileRecord.saved_file_name!;
// 파일 경로에 날짜 구조가 있는지 확인 (YYYY/MM/DD)
let dateFolder = "";
if (filePathParts.length >= 6) {
dateFolder = `${filePathParts[3]}/${filePathParts[4]}/${filePathParts[5]}`;
const dbFilePath = fileRecord.file_path || "";
const uploadsIdx = dbFilePath.indexOf("/uploads/");
let finalPath: string;
if (uploadsIdx !== -1) {
const relativePath = dbFilePath.substring(uploadsIdx + "/uploads/".length);
finalPath = path.join(baseUploadDir, relativePath);
} else {
// fallback: 기존 방식
const filePathParts = dbFilePath.split("/");
let fileCompanyCode = filePathParts[2] || "DEFAULT";
if (fileCompanyCode === "company_*") {
fileCompanyCode = "company_*";
}
let dateFolder = "";
if (filePathParts.length >= 6) {
dateFolder = `${filePathParts[3]}/${filePathParts[4]}/${filePathParts[5]}`;
}
const companyUploadDir = getCompanyUploadDir(
fileCompanyCode,
dateFolder || undefined
);
finalPath = path.join(companyUploadDir, fileName);
}
const companyUploadDir = getCompanyUploadDir(
fileCompanyCode,
dateFolder || undefined
);
const filePath = path.join(companyUploadDir, fileName);
console.log("🔍 파일 미리보기 경로 확인:", {
objid: objid,
filePathFromDB: fileRecord.file_path,
companyCode: companyCode,
dateFolder: dateFolder,
fileName: fileName,
companyUploadDir: companyUploadDir,
finalFilePath: filePath,
fileExists: fs.existsSync(filePath),
finalFilePath: finalPath,
fileExists: fs.existsSync(finalPath),
});
if (!fs.existsSync(filePath)) {
console.error("❌ 파일 없음:", filePath);
if (!fs.existsSync(finalPath)) {
console.error("❌ 파일 없음:", finalPath);
res.status(404).json({
success: false,
message: `실제 파일을 찾을 수 없습니다: ${filePath}`,
message: `실제 파일을 찾을 수 없습니다: ${finalPath}`,
});
return;
}
@@ -929,7 +929,7 @@ export const previewFile = async (
res.setHeader("Content-Type", mimeType);
// 파일 스트림으로 전송
const fileStream = fs.createReadStream(filePath);
const fileStream = fs.createReadStream(finalPath);
fileStream.pipe(res);
} catch (error) {
console.error("파일 미리보기 오류:", error);

View File

@@ -246,15 +246,33 @@ export async function create(req: AuthenticatedRequest, res: Response) {
);
}
// 판매출고인 경우 출하지시의 ship_qty 업데이트
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
if (item.outbound_type === "판매출고" && item.source_id && item.source_type === "shipment_instruction_detail") {
const outQtyNum = Number(item.outbound_qty) || 0;
await client.query(
`UPDATE shipment_instruction_detail
SET ship_qty = COALESCE(ship_qty, 0) + $1,
updated_date = NOW()
WHERE id = $2 AND company_code = $3`,
[item.outbound_qty || 0, item.source_id, companyCode]
[outQtyNum, item.source_id, companyCode]
);
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
const sidRes = await client.query(
`SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`,
[item.source_id, companyCode]
);
const detailId = sidRes.rows[0]?.detail_id;
if (detailId) {
await client.query(
`UPDATE sales_order_detail
SET ship_qty = (COALESCE(NULLIF(ship_qty,'')::numeric, 0) + $1)::text,
balance_qty = (COALESCE(NULLIF(qty,'')::numeric, 0) - COALESCE(NULLIF(ship_qty,'')::numeric, 0) - $1)::text,
updated_date = NOW()
WHERE id = $2 AND company_code = $3`,
[outQtyNum, detailId, companyCode]
);
}
}
}

View File

@@ -332,8 +332,24 @@ export async function create(req: AuthenticatedRequest, res: Response) {
[purchaseNo, companyCode]
);
const newStatus = unreceived.rows.length === 0 ? '입고완료' : '부분입고';
// 발주 헤더의 received_qty도 디테일 합계로 동기화
await client.query(
`UPDATE purchase_order_mng SET status = $1, updated_date = NOW()
`UPDATE purchase_order_mng SET
status = $1,
received_qty = (
SELECT CAST(COALESCE(SUM(CAST(NULLIF(received_qty, '') AS numeric)), 0) AS text)
FROM purchase_detail
WHERE purchase_no = $2 AND company_code = $3
),
remain_qty = (
SELECT CAST(COALESCE(SUM(
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0)
- COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0)
), 0) AS text)
FROM purchase_detail
WHERE purchase_no = $2 AND company_code = $3
),
updated_date = NOW()
WHERE purchase_no = $2 AND company_code = $3`,
[newStatus, purchaseNo, companyCode]
);

View File

@@ -9,6 +9,7 @@ import { encryptionService } from "../services/encryptionService";
import {
sendSmartFactoryLog,
getTodayPlanStatus,
planDailySends,
} from "../utils/smartFactoryLog";
/**
@@ -277,8 +278,9 @@ export const upsertSchedule = async (
]
);
// 계획은 매일 00:05에만 생성 (즉시 재생성하면 지난 시각 소급 전송 위험)
res.json({ success: true, message: "스케줄이 저장되었습니다. 내일 00:05부터 적용됩니다." });
// 스케줄 변경 후 오늘 계획 즉시 재생성 (이미 전송된 사용자는 자동 제외됨)
await planDailySends();
res.json({ success: true, message: "스케줄이 저장되었습니다." });
} catch (error) {
logger.error("스케줄 저장 실패:", error);
res.status(500).json({ success: false, message: "스케줄 저장 실패" });