feat: COMPANY_9 수주관리 페이지 추가 및 생산계획/공정 개선

- COMPANY_9 수주관리(sales/order) 하드코딩 페이지 추가
- 생산계획 서비스 로직 정리 및 공정 컨트롤러 수정
- COMPANY_7 생산계획관리 페이지 개선
- AdminPageRenderer 기능 확장
This commit is contained in:
kjs
2026-03-27 22:32:18 +09:00
parent f32861df8b
commit 2e9b67a509
5 changed files with 1228 additions and 40 deletions

View File

@@ -214,7 +214,7 @@ export async function getEquipmentList(req: AuthenticatedRequest, res: Response)
const params = companyCode === "*" ? [] : [companyCode];
const result = await pool.query(
`SELECT objid AS id, equipment_code, equipment_name FROM equipment_mng ${condition} ORDER BY equipment_code`,
`SELECT id, equipment_code, equipment_name FROM equipment_mng ${condition} ORDER BY equipment_code`,
params
);

View File

@@ -401,22 +401,9 @@ export async function previewSchedule(
const dailyCapacity = item.daily_capacity || 800;
const itemLeadTime = item.lead_time || 0;
let requiredQty = item.required_qty;
// recalculate_unstarted 시, 삭제된 수량을 비율로 분배
if (options.recalculate_unstarted) {
const deletedQtyForItem = deletedSchedules
.filter((d: any) => d.item_code === item.item_code)
.reduce((sum: number, d: any) => sum + (parseFloat(d.plan_qty) || 0), 0);
if (deletedQtyForItem > 0) {
const totalRequestedForItem = items
.filter((i) => i.item_code === item.item_code)
.reduce((sum, i) => sum + i.required_qty, 0);
if (totalRequestedForItem > 0) {
requiredQty += Math.round(deletedQtyForItem * (item.required_qty / totalRequestedForItem));
}
}
}
// 프론트에서 이미 전체 잔량 기준으로 계산하여 보내므로 그대로 사용
// (recalculate_unstarted 시 기존 planned는 위에서 이미 삭제됨)
const requiredQty = item.required_qty;
if (requiredQty <= 0) continue;
@@ -543,19 +530,9 @@ export async function generateSchedule(
// 필요 수량 계산 (삭제된 planned 수량을 비율로 분배)
const dailyCapacity = item.daily_capacity || 800;
const itemLeadTime = item.lead_time || 0;
let requiredQty = item.required_qty;
if (options.recalculate_unstarted) {
const deletedQty = deletedQtyByItem.get(item.item_code) || 0;
if (deletedQty > 0) {
const totalRequestedForItem = items
.filter((i) => i.item_code === item.item_code)
.reduce((sum, i) => sum + i.required_qty, 0);
if (totalRequestedForItem > 0) {
requiredQty += Math.round(deletedQty * (item.required_qty / totalRequestedForItem));
}
}
}
// 프론트에서 이미 전체 잔량 기준으로 계산하여 보내므로 그대로 사용
// (recalculate_unstarted 시 기존 planned는 위에서 이미 삭제됨)
const requiredQty = item.required_qty;
if (requiredQty <= 0) continue;
// 리드타임 기반 날짜 계산: 납기일 기준으로 리드타임만큼 역산

View File

@@ -409,7 +409,10 @@ export default function ProductionPlanManagementPage() {
.filter((item) => selectedItemGroups.has(item.item_code))
.forEach((item) => {
const leadTime = Number(item.lead_time) || 0;
const totalRequired = Number(item.required_plan_qty);
// 재계산 모드: 기존 planned를 삭제 후 재생성 → 수주 잔량에서 진행중만 빼기
const totalRequired = recalculateUnstarted
? Number(item.total_balance_qty || 0) - Number(item.in_progress_qty || 0)
: Number(item.required_plan_qty);
if (totalRequired <= 0) return;
// 수주가 여러 건이고 납기일이 다르면 각각 분리
@@ -460,6 +463,14 @@ export default function ProductionPlanManagementPage() {
}
});
// items가 비어있으면 사용자에게 알림
if (items.length === 0) {
toast.error("계획 수량이 있는 품목이 없습니다. 수주 잔량을 확인해주세요.");
return;
}
setGenerating(true);
try {
const req: GenerateScheduleRequest = {
@@ -491,7 +502,9 @@ export default function ProductionPlanManagementPage() {
.filter((item) => selectedItemGroups.has(item.item_code))
.forEach((item) => {
const leadTime = Number(item.lead_time) || 0;
const totalRequired = Number(item.required_plan_qty);
const totalRequired = recalculateUnstarted
? Number(item.required_plan_qty) + Number(item.existing_plan_qty || 0)
: Number(item.required_plan_qty);
if (totalRequired <= 0) return;
if (item.orders && item.orders.length > 1) {
@@ -768,9 +781,12 @@ export default function ProductionPlanManagementPage() {
.map((item) => ({
item_code: item.item_code,
item_name: item.item_name,
required_qty: Number(item.required_plan_qty),
required_qty: (importMode !== "new" && recalculateUnstarted)
? Number(item.total_balance_qty || 0) - Number(item.in_progress_qty || 0)
: Number(item.required_plan_qty),
earliest_due_date: item.earliest_due_date || new Date().toISOString().split("T")[0],
}));
}))
.filter((item) => item.required_qty > 0);
setGenerating(true);
try {

File diff suppressed because it is too large Load Diff

View File

@@ -92,8 +92,32 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
"/admin/automaticMng/batchmngList": dynamic(() => import("@/app/(main)/admin/automaticMng/batchmngList/page"), { ssr: false, loading: LoadingFallback }),
"/admin/automaticMng/crawlingList": dynamic(() => import("@/app/(main)/admin/automaticMng/crawlingList/page"), { ssr: false, loading: LoadingFallback }),
// 회사별 커스텀 페이지는 레지스트리에서 제거 — 회사코드 prefix로 자동 import 처리
// (design, sales, production, logistics, equipment, outsourcing, master-data)
// === COMPANY_7 (탑씰) ===
"/COMPANY_7/master-data/item-info": dynamic(() => import("@/app/(main)/COMPANY_7/master-data/item-info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/master-data/department": dynamic(() => import("@/app/(main)/COMPANY_7/master-data/department/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/sales/order": dynamic(() => import("@/app/(main)/COMPANY_7/sales/order/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/sales/customer": dynamic(() => import("@/app/(main)/COMPANY_7/sales/customer/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/sales/sales-item": dynamic(() => import("@/app/(main)/COMPANY_7/sales/sales-item/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/sales/shipping-order": dynamic(() => import("@/app/(main)/COMPANY_7/sales/shipping-order/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/sales/shipping-plan": dynamic(() => import("@/app/(main)/COMPANY_7/sales/shipping-plan/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/sales/claim": dynamic(() => import("@/app/(main)/COMPANY_7/sales/claim/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/production/process-info": dynamic(() => import("@/app/(main)/COMPANY_7/production/process-info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/production/work-instruction": dynamic(() => import("@/app/(main)/COMPANY_7/production/work-instruction/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_7/production/plan-management/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_7/equipment/info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/outsourcing/subcontractor": dynamic(() => import("@/app/(main)/COMPANY_7/outsourcing/subcontractor/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/outsourcing/subcontractor-item": dynamic(() => import("@/app/(main)/COMPANY_7/outsourcing/subcontractor-item/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/design/project": dynamic(() => import("@/app/(main)/COMPANY_7/design/project/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/design/change-management": dynamic(() => import("@/app/(main)/COMPANY_7/design/change-management/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/design/my-work": dynamic(() => import("@/app/(main)/COMPANY_7/design/my-work/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/design/design-request": dynamic(() => import("@/app/(main)/COMPANY_7/design/design-request/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_7/design/task-management": dynamic(() => import("@/app/(main)/COMPANY_7/design/task-management/page"), { ssr: false, loading: LoadingFallback }),
// === COMPANY_9 (제일그라스) ===
"/COMPANY_9/sales/order": dynamic(() => import("@/app/(main)/COMPANY_9/sales/order/page"), { ssr: false, loading: LoadingFallback }),
"/admin/automaticMng/exconList": dynamic(() => import("@/app/(main)/admin/automaticMng/exconList/page"), { ssr: false, loading: LoadingFallback }),
"/admin/automaticMng/exCallConfList": dynamic(() => import("@/app/(main)/admin/automaticMng/exCallConfList/page"), { ssr: false, loading: LoadingFallback }),
@@ -145,6 +169,34 @@ const DYNAMIC_ADMIN_IMPORTS: Record<string, () => Promise<any>> = {
"/admin/automaticMng/batchmngList/create": () => import("@/app/(main)/admin/automaticMng/batchmngList/create/page"),
"/admin/systemMng/dataflow/node-editorList": () => import("@/app/(main)/admin/systemMng/dataflow/page"),
"/admin/standards/new": () => import("@/app/(main)/admin/standards/new/page"),
// === 회사별 커스텀 페이지 (resolvedUrl로 매칭) ===
// COMPANY_7 (탑씰)
"/COMPANY_7/master-data/item-info": () => import("@/app/(main)/COMPANY_7/master-data/item-info/page"),
"/COMPANY_7/master-data/department": () => import("@/app/(main)/COMPANY_7/master-data/department/page"),
"/COMPANY_7/sales/order": () => import("@/app/(main)/COMPANY_7/sales/order/page"),
"/COMPANY_7/sales/customer": () => import("@/app/(main)/COMPANY_7/sales/customer/page"),
"/COMPANY_7/sales/sales-item": () => import("@/app/(main)/COMPANY_7/sales/sales-item/page"),
"/COMPANY_7/sales/shipping-order": () => import("@/app/(main)/COMPANY_7/sales/shipping-order/page"),
"/COMPANY_7/sales/shipping-plan": () => import("@/app/(main)/COMPANY_7/sales/shipping-plan/page"),
"/COMPANY_7/sales/claim": () => import("@/app/(main)/COMPANY_7/sales/claim/page"),
"/COMPANY_7/production/process-info": () => import("@/app/(main)/COMPANY_7/production/process-info/page"),
"/COMPANY_7/production/work-instruction": () => import("@/app/(main)/COMPANY_7/production/work-instruction/page"),
"/COMPANY_7/production/plan-management": () => import("@/app/(main)/COMPANY_7/production/plan-management/page"),
"/COMPANY_7/equipment/info": () => import("@/app/(main)/COMPANY_7/equipment/info/page"),
"/COMPANY_7/logistics/material-status": () => import("@/app/(main)/COMPANY_7/logistics/material-status/page"),
"/COMPANY_7/logistics/outbound": () => import("@/app/(main)/COMPANY_7/logistics/outbound/page"),
"/COMPANY_7/logistics/receiving": () => import("@/app/(main)/COMPANY_7/logistics/receiving/page"),
"/COMPANY_7/logistics/packaging": () => import("@/app/(main)/COMPANY_7/logistics/packaging/page"),
"/COMPANY_7/outsourcing/subcontractor": () => import("@/app/(main)/COMPANY_7/outsourcing/subcontractor/page"),
"/COMPANY_7/outsourcing/subcontractor-item": () => import("@/app/(main)/COMPANY_7/outsourcing/subcontractor-item/page"),
"/COMPANY_7/design/project": () => import("@/app/(main)/COMPANY_7/design/project/page"),
"/COMPANY_7/design/change-management": () => import("@/app/(main)/COMPANY_7/design/change-management/page"),
"/COMPANY_7/design/my-work": () => import("@/app/(main)/COMPANY_7/design/my-work/page"),
"/COMPANY_7/design/design-request": () => import("@/app/(main)/COMPANY_7/design/design-request/page"),
"/COMPANY_7/design/task-management": () => import("@/app/(main)/COMPANY_7/design/task-management/page"),
// COMPANY_9 (제일그라스)
"/COMPANY_9/sales/order": () => import("@/app/(main)/COMPANY_9/sales/order/page"),
};
const DYNAMIC_ADMIN_PATTERNS: Array<{
@@ -329,13 +381,13 @@ export function AdminPageRenderer({ url }: AdminPageRendererProps) {
return <DashboardViewPage params={Promise.resolve({ dashboardId: dashboardMatch[1] })} />;
}
// URL 직접 입력: 레지스트리 매칭 (admin 페이지 등 공통 페이지)
// URL 직접 입력: 레지스트리 매칭 (resolvedUrl 우선, cleanUrl 폴백)
const PageComponent = useMemo(() => {
return ADMIN_PAGE_REGISTRY[cleanUrl] || null;
}, [cleanUrl]);
return ADMIN_PAGE_REGISTRY[resolvedUrl] || ADMIN_PAGE_REGISTRY[cleanUrl] || null;
}, [resolvedUrl, cleanUrl]);
if (PageComponent) {
console.log("[AdminPageRenderer] → 레지스트리 매칭:", cleanUrl);
console.log("[AdminPageRenderer] → 레지스트리 매칭:", resolvedUrl || cleanUrl);
return <PageComponent />;
}