From 823f9034a81a1a21210b27ab7773986b20539292 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 14 May 2026 12:03:04 +0900 Subject: [PATCH] Enhance Production Plan Date Calculation Logic - Introduced helper functions to calculate working days while skipping weekends, improving the accuracy of date calculations in production planning. - Updated the logic for determining start and end dates based on lead time and production capacity, ensuring that weekend days are excluded from the calculations. - Refactored existing date calculation code in the `previewSchedule` and `generateSchedule` functions to utilize the new helper methods for better maintainability and clarity. (TASK: ERP-XXX) --- .../src/services/productionPlanService.ts | 86 +++++---- .../app/(main)/COMPANY_7/sales/order/page.tsx | 170 ++++++++++-------- .../admin/userMng/rolesList/[id]/page.tsx | 86 ++++----- .../components/admin/MenuPermissionsTable.tsx | 4 +- .../components/common/DynamicSearchFilter.tsx | 9 + .../components/common/TableSettingsModal.tsx | 47 ++++- .../components/layout/AdminPageRenderer.tsx | 7 + frontend/hooks/useTableSettings.ts | 1 + scripts/prod/deploy.sh | 32 ++-- 9 files changed, 256 insertions(+), 186 deletions(-) diff --git a/backend-node/src/services/productionPlanService.ts b/backend-node/src/services/productionPlanService.ts index bd56a01b..97a7e2bb 100644 --- a/backend-node/src/services/productionPlanService.ts +++ b/backend-node/src/services/productionPlanService.ts @@ -11,6 +11,35 @@ import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +// ─── 근무일 계산 헬퍼 (토/일 제외) ─── + +function isWeekend(date: Date): boolean { + const day = date.getDay(); + return day === 0 || day === 6; +} + +// 주말이면 평일로 이동 (forward: 다음 월요일, backward: 이전 금요일) +function skipWeekend(date: Date, direction: "forward" | "backward"): Date { + const d = new Date(date); + while (isWeekend(d)) { + d.setDate(d.getDate() + (direction === "forward" ? 1 : -1)); + } + return d; +} + +// 기준일에서 N 근무일 이동 (음수=과거, 양수=미래). 기준일은 카운트에 포함하지 않음. +function addWorkingDays(date: Date, workingDays: number): Date { + const d = new Date(date); + if (workingDays === 0) return d; + const step = workingDays > 0 ? 1 : -1; + let remaining = Math.abs(workingDays); + while (remaining > 0) { + d.setDate(d.getDate() + step); + if (!isWeekend(d)) remaining--; + } + return d; +} + // ─── 수주 데이터 조회 (품목별 그룹핑) ─── export async function getOrderSummary( @@ -579,32 +608,28 @@ export async function previewSchedule( if (requiredQty <= 0) continue; - // 리드타임 기반 날짜 계산: 납기일 기준으로 리드타임만큼 역산 + // 리드타임 기반 날짜 계산 (근무일 기준, 토/일 제외, 시작·종료 포함 N일) const dueDate = new Date(item.earliest_due_date); - let startDate: Date; + const productionDays = itemLeadTime > 0 ? itemLeadTime : Math.ceil(requiredQty / dailyCapacity); let endDate: Date; + let startDate: Date; if (itemLeadTime > 0) { - // 리드타임이 있으면: 종료일 = 납기일, 시작일 = 납기일 - 리드타임 - endDate = new Date(dueDate); - startDate = new Date(dueDate); - startDate.setDate(startDate.getDate() - itemLeadTime); + // 종료일 = 납기일(주말이면 이전 평일), 시작일 = 종료일에서 (리드타임-1) 근무일 전 + endDate = skipWeekend(new Date(dueDate), "backward"); + startDate = addWorkingDays(endDate, -(productionDays - 1)); } else { - // 리드타임이 없으면 기존 로직 (생산능력 기반) - const productionDays = Math.ceil(requiredQty / dailyCapacity); - endDate = new Date(dueDate); - endDate.setDate(endDate.getDate() - safetyLeadTime); - startDate = new Date(endDate); - startDate.setDate(startDate.getDate() - productionDays); + // 리드타임이 없으면 생산능력 기반: 종료일 = 납기일 - 안전여유(근무일) + endDate = addWorkingDays(skipWeekend(new Date(dueDate), "backward"), -safetyLeadTime); + startDate = addWorkingDays(endDate, -(productionDays - 1)); } const today = new Date(); today.setHours(0, 0, 0, 0); if (startDate < today) { - const duration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); - startDate.setTime(today.getTime()); - endDate.setTime(startDate.getTime()); - endDate.setDate(endDate.getDate() + duration); + // 시작일이 과거면 오늘(주말이면 다음 평일)부터 재배치, 작업 근무일 수 유지 + startDate = skipWeekend(today, "forward"); + endDate = addWorkingDays(startDate, productionDays - 1); } // 해당 품목의 수주 건수 확인 @@ -621,7 +646,7 @@ export async function previewSchedule( required_qty: requiredQty, daily_capacity: dailyCapacity, hourly_capacity: item.hourly_capacity || 100, - production_days: itemLeadTime > 0 ? itemLeadTime : Math.ceil(requiredQty / dailyCapacity), + production_days: productionDays, start_date: startDate.toISOString().split("T")[0], end_date: endDate.toISOString().split("T")[0], due_date: item.earliest_due_date, @@ -707,33 +732,26 @@ export async function generateSchedule( const requiredQty = item.required_qty; if (requiredQty <= 0) continue; - // 리드타임 기반 날짜 계산: 납기일 기준으로 리드타임만큼 역산 + // 리드타임 기반 날짜 계산 (근무일 기준, 토/일 제외, 시작·종료 포함 N일) const dueDate = new Date(item.earliest_due_date); - let startDate: Date; + const productionDays = itemLeadTime > 0 ? itemLeadTime : Math.ceil(requiredQty / dailyCapacity); let endDate: Date; + let startDate: Date; if (itemLeadTime > 0) { - // 리드타임이 있으면: 종료일 = 납기일, 시작일 = 납기일 - 리드타임 - endDate = new Date(dueDate); - startDate = new Date(dueDate); - startDate.setDate(startDate.getDate() - itemLeadTime); + endDate = skipWeekend(new Date(dueDate), "backward"); + startDate = addWorkingDays(endDate, -(productionDays - 1)); } else { - // 리드타임이 없으면 기존 로직 (생산능력 기반) - const productionDays = Math.ceil(requiredQty / dailyCapacity); - endDate = new Date(dueDate); - endDate.setDate(endDate.getDate() - safetyLeadTime); - startDate = new Date(endDate); - startDate.setDate(startDate.getDate() - productionDays); + endDate = addWorkingDays(skipWeekend(new Date(dueDate), "backward"), -safetyLeadTime); + startDate = addWorkingDays(endDate, -(productionDays - 1)); } - // 시작일이 오늘보다 이전이면 오늘로 조정 + // 시작일이 오늘보다 이전이면 오늘(주말이면 다음 평일)로 조정 const today = new Date(); today.setHours(0, 0, 0, 0); if (startDate < today) { - const duration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); - startDate.setTime(today.getTime()); - endDate.setTime(startDate.getTime()); - endDate.setDate(endDate.getDate() + duration); + startDate = skipWeekend(today, "forward"); + endDate = addWorkingDays(startDate, productionDays - 1); } // 계획번호 생성 (YYYYMMDD-NNNN 형식) diff --git a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx index e56d3c50..d6d83895 100644 --- a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx @@ -47,21 +47,21 @@ const parseNumber = (val: string) => val.replace(/,/g, ""); // 플랫 테이블 컬럼 정의 (마스터+디테일 통합) const FLAT_COLUMNS = [ - { key: "order_no", label: "수주번호", source: "master" }, - { key: "partner_id", label: "거래처", source: "master" }, - { key: "order_date", label: "수주일", source: "master" }, - { key: "part_code", label: "품번", source: "detail" }, - { key: "part_name", label: "품명", source: "detail" }, - { key: "spec", label: "규격", source: "detail" }, - { key: "unit", label: "단위", source: "detail" }, - { key: "qty", label: "수량", source: "detail" }, - { key: "ship_qty", label: "출하수량", source: "detail" }, - { key: "balance_qty", label: "잔량", source: "detail" }, - { key: "unit_price", label: "단가", source: "detail" }, - { key: "amount", label: "금액", source: "detail" }, - { key: "due_date", label: "납기일", source: "detail" }, - { key: "approval_status", label: "결재상태", source: "master" }, - { key: "memo", label: "메모", source: "master" }, + { key: "order_no", label: "수주번호", source: "master", width: 140, align: "left" as const }, + { key: "partner_id", label: "거래처", source: "master", width: 140, align: "left" as const }, + { key: "order_date", label: "수주일", source: "master", width: 100, align: "left" as const }, + { key: "part_code", label: "품번", source: "detail", width: 120, align: "left" as const }, + { key: "part_name", label: "품명", source: "detail", width: 140, align: "left" as const }, + { key: "spec", label: "규격", source: "detail", width: 80, align: "left" as const }, + { key: "unit", label: "단위", source: "detail", width: 70, align: "left" as const }, + { key: "qty", label: "수량", source: "detail", width: 80, align: "right" as const }, + { key: "ship_qty", label: "출하수량", source: "detail", width: 80, align: "right" as const }, + { key: "balance_qty", label: "잔량", source: "detail", width: 80, align: "right" as const }, + { key: "unit_price", label: "단가", source: "detail", width: 90, align: "right" as const }, + { key: "amount", label: "금액", source: "detail", width: 110, align: "right" as const }, + { key: "due_date", label: "납기일", source: "detail", width: 100, align: "left" as const }, + { key: "approval_status", label: "결재상태", source: "master", width: 90, align: "center" as const }, + { key: "memo", label: "메모", source: "master", width: 120, align: "left" as const }, ]; const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); @@ -69,9 +69,6 @@ const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); // 필터용 전체 키 const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label })); -// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(15) = 16 -const TOTAL_COLS = 16; - // 결재상태 라벨/색상 const APPROVAL_STATUS_LABEL: Record = { requested: "요청", @@ -1147,6 +1144,9 @@ export default function SalesOrderPage() { onFilterChange={setSearchFilters} dataCount={orders.length} externalFilterConfig={ts.filterConfig} + externalSelectOptions={{ + approval_status: Object.entries(APPROVAL_STATUS_LABEL).map(([value, label]) => ({ value, label })), + }} /> {/* 액션 바 */} @@ -1235,24 +1235,20 @@ export default function SalesOrderPage() { {/* 데이터 테이블 (플랫 리스트) */}
- + {(() => { + // 컬럼 설정에서 활성화된 컬럼만 (FLAT_COLUMNS 메타 보존) + const visibleCols = ts.visibleColumns + .map((vc) => FLAT_COLUMNS.find((fc) => fc.key === vc.key)) + .filter((c): c is typeof FLAT_COLUMNS[number] => !!c); + const totalCols = visibleCols.length + 1; // +1 = 체크박스 + const minWidth = 40 + visibleCols.reduce((sum, c) => sum + c.width, 0); + return ( +
- - - - - - - - - - - - - - - + {visibleCols.map((c) => ( + + ))} @@ -1272,11 +1268,12 @@ export default function SalesOrderPage() { onCheckedChange={() => {}} /> - {FLAT_COLUMNS.map((col) => { - const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); + {visibleCols.map((col) => { + const isRight = col.align === "right"; + const isCenter = col.align === "center"; return ( - -
+ +
handleSort(col.key)}> {col.label} {sortState?.key === col.key && ( @@ -1302,13 +1299,13 @@ export default function SalesOrderPage() { {loading ? ( - + ) : filteredFlatRows.length === 0 ? ( - +
등록된 수주가 없어요 @@ -1321,7 +1318,7 @@ export default function SalesOrderPage() { if (row._isGroupHeader) { return ( - + 📂 {row._groupValue} ({row._groupCount}건) @@ -1331,7 +1328,7 @@ export default function SalesOrderPage() { if (row._isGroupSummary) { return ( - + {row._groupValue || "합계"} {row.qty ? ` · 수량 ${Number(row.qty).toLocaleString()}` : ""} {row.amount ? ` · 금액 ${Number(row.amount).toLocaleString()}` : ""} @@ -1365,44 +1362,65 @@ export default function SalesOrderPage() { > {}} /> - {row.order_no} - {row.partner_id || ""} - {row.order_date || ""} - {row.part_code} - {row.part_name} - {row.spec} - {row.unit} - {row.qty ? Number(row.qty).toLocaleString() : ""} - {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} - {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {row.amount ? Number(row.amount).toLocaleString() : ""} - {row.due_date || ""} - - {row.approval_status && row.approval_request_id ? ( - - ) : ( - - - )} - - {row.memo || ""} + {visibleCols.map((col) => { + const align = col.align; + const cellClass = cn( + "text-[13px]", + align === "right" && "text-right font-mono", + align === "center" && "text-center", + col.key === "order_no" && "font-mono whitespace-nowrap", + col.key === "part_code" && "font-mono", + col.key === "order_date" && "whitespace-nowrap", + col.key === "spec" && "text-muted-foreground", + col.key === "ship_qty" && "text-muted-foreground", + col.key === "amount" && "font-semibold", + col.key === "memo" && "text-muted-foreground", + ); + let content: React.ReactNode = ""; + switch (col.key) { + case "order_no": content = row.order_no; break; + case "partner_id": content = {row.partner_id || ""}; break; + case "order_date": content = row.order_date || ""; break; + case "part_code": content = row.part_code; break; + case "part_name": content = {row.part_name}; break; + case "spec": content = row.spec; break; + case "unit": content = row.unit; break; + case "qty": content = row.qty ? Number(row.qty).toLocaleString() : ""; break; + case "ship_qty": content = row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""; break; + case "balance_qty": content = row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""; break; + case "unit_price": content = row.unit_price ? Number(row.unit_price).toLocaleString() : ""; break; + case "amount": content = row.amount ? Number(row.amount).toLocaleString() : ""; break; + case "due_date": content = row.due_date || ""; break; + case "approval_status": + content = row.approval_status && row.approval_request_id ? ( + + ) : ( + - + ); + break; + case "memo": content = {row.memo || ""}; break; + } + return {content}; + })} ); }) )}
+ ); + })()}
{/* 페이지네이션 */} @@ -2080,6 +2098,10 @@ export default function SalesOrderPage() { tableName={ts.tableName} settingsId={ts.settingsId} defaultVisibleKeys={ts.defaultVisibleKeys} + extraTableNames={[MASTER_TABLE]} + extraColumns={[ + { columnName: "approval_status", displayName: "결재상태", inputType: "select" }, + ]} onSave={ts.applySettings} /> diff --git a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx index 62d9b3e0..03db6c15 100644 --- a/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx +++ b/frontend/app/(main)/admin/userMng/rolesList/[id]/page.tsx @@ -110,9 +110,6 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // 탭 상태 - const [activeTab, setActiveTab] = useState<"members" | "permissions">("members"); - // 멤버 관리 상태 const [memberMode, setMemberMode] = useState<"user" | "dept">("user"); const [availableUsers, setAvailableUsers] = useState>([]); @@ -257,12 +254,11 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin }, [loadRoleGroup]); useEffect(() => { - if (roleGroup && activeTab === "members") { + if (roleGroup) { loadMembers(); - } else if (roleGroup && activeTab === "permissions") { loadMenuPermissions(); } - }, [roleGroup, activeTab, loadMembers, loadMenuPermissions]); + }, [roleGroup, loadMembers, loadMenuPermissions]); // 부서 선택 변경 시 우측에 부서 표시 + 해당 사용자를 멤버에 추가 const handleDeptSelectionChange = useCallback( @@ -402,35 +398,14 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
- {/* 탭 네비게이션 */} -
- - -
- - {/* 탭 컨텐츠 */} -
- {activeTab === "members" && ( + {/* 통합 컨텐츠: 위 멤버 관리 + 아래 메뉴 권한 */} +
+ {/* === 멤버 관리 섹션 === */} +
+
+ + {t("detail.tab.members")} ({selectedUsers.length}) +
<>
@@ -516,28 +491,31 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin )} - )} +
- {activeTab === "permissions" && ( - <> -
-
-

{t("detail.permissions.title")}

-

{t("detail.permissions.description")}

-
- + {/* === 메뉴 권한 섹션 === */} +
+
+ + {t("detail.tab.permissions")} ({menuPermissions.length}) +
+
+
+

{t("detail.permissions.title")}

+

{t("detail.permissions.description")}

+ +
- - - )} + +
diff --git a/frontend/components/admin/MenuPermissionsTable.tsx b/frontend/components/admin/MenuPermissionsTable.tsx index 61a7d577..92db50ba 100644 --- a/frontend/components/admin/MenuPermissionsTable.tsx +++ b/frontend/components/admin/MenuPermissionsTable.tsx @@ -132,7 +132,9 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro }; loadAllMenus(); - }, [currentUser, isSuperAdmin, roleGroup.companyCode, companyFilter]); + // currentUser 객체 전체를 의존성에 넣으면 useAuth 재렌더링마다 새 reference로 인식되어 무한 호출 발생. + // primitive(userId/companyCode/userType)만 의존성에 사용. + }, [currentUser?.userId, currentUser?.companyCode, currentUser?.userType, isSuperAdmin, roleGroup.companyCode, companyFilter]); // 메뉴 권한 상태 (로컬 상태 관리) const [menuPermissions, setMenuPermissions] = useState>(new Map()); diff --git a/frontend/components/common/DynamicSearchFilter.tsx b/frontend/components/common/DynamicSearchFilter.tsx index 28a57567..df05d6b5 100644 --- a/frontend/components/common/DynamicSearchFilter.tsx +++ b/frontend/components/common/DynamicSearchFilter.tsx @@ -71,6 +71,8 @@ export interface DynamicSearchFilterProps { extraActions?: React.ReactNode; /** TableSettingsModal에서 전달된 외부 필터 설정 (제공 시 자체 설정 모달 숨김) */ externalFilterConfig?: ExternalFilterConfig[]; + /** select 필터의 외부 주입 옵션 (가상 컬럼처럼 DB 조회로 잡히지 않는 컬럼용) */ + externalSelectOptions?: Record; } const FILTER_TYPE_OPTIONS: { value: FilterType; label: string }[] = [ @@ -97,6 +99,7 @@ export function DynamicSearchFilter({ dataCount, extraActions, externalFilterConfig, + externalSelectOptions, }: DynamicSearchFilterProps) { const [allColumns, setAllColumns] = useState([]); const [activeFilters, setActiveFilters] = useState([]); @@ -187,6 +190,12 @@ export function DynamicSearchFilter({ ); }, [externalFilterConfig]); + // 외부 주입 select 옵션 병합 (가상 컬럼 등) + useEffect(() => { + if (!externalSelectOptions) return; + setSelectOptions((prev) => ({ ...prev, ...externalSelectOptions })); + }, [externalSelectOptions]); + // select 타입 필터의 옵션 로드 (카테고리 → 없으면 실제 데이터 distinct) useEffect(() => { const loadOptions = async () => { diff --git a/frontend/components/common/TableSettingsModal.tsx b/frontend/components/common/TableSettingsModal.tsx index 481f9e4c..3ccc1990 100644 --- a/frontend/components/common/TableSettingsModal.tsx +++ b/frontend/components/common/TableSettingsModal.tsx @@ -88,6 +88,14 @@ export interface TableSettingsModalProps { defaultVisibleKeys?: string[]; /** AUTO_COLS에서 제외하지 않을 컬럼 키 목록 (예: ["created_date", "updated_date", "writer"]) */ includeAutoColumns?: string[]; + /** 보조 테이블명 (마스터/디테일 결합 그리드용). 메인 테이블 web-types에 없는 컬럼을 보강 */ + extraTableNames?: string[]; + /** 가상 컬럼 명세 (DB 실컬럼이 아닌 조인/계산 필드용). web-types 응답에 없을 때만 추가됨 */ + extraColumns?: Array<{ + columnName: string; + displayName: string; + inputType?: "text" | "number" | "date" | "datetime" | "category" | "select"; + }>; } // ===== 상수 ===== @@ -219,6 +227,8 @@ export function TableSettingsModal({ initialTab = "columns", defaultVisibleKeys, includeAutoColumns, + extraTableNames, + extraColumns, }: TableSettingsModalProps) { const [activeTab, setActiveTab] = useState(initialTab); const [loading, setLoading] = useState(false); @@ -249,8 +259,41 @@ export function TableSettingsModal({ const loadData = async () => { setLoading(true); try { - const res = await apiClient.get(`/table-management/tables/${tableName}/web-types`); - const types: any[] = res.data?.data || []; + // 메인 + 보조 테이블 web-types 병합 (먼저 등장한 테이블 컬럼 우선) + // 보조 테이블 호출이 실패해도 메인 결과는 유지하기 위해 allSettled 사용 + const tableNamesToLoad = [tableName, ...(extraTableNames || [])]; + const settled = await Promise.allSettled( + tableNamesToLoad.map((tn) => apiClient.get(`/table-management/tables/${tn}/web-types`)) + ); + const seenCols = new Set(); + const types: any[] = []; + settled.forEach((s, idx) => { + if (s.status === "fulfilled") { + const arr: any[] = s.value.data?.data || []; + for (const t of arr) { + if (!seenCols.has(t.columnName)) { + seenCols.add(t.columnName); + types.push(t); + } + } + } else { + console.warn(`web-types 로드 실패 (${tableNamesToLoad[idx]}):`, s.reason); + } + }); + + // 가상 컬럼 보강 (web-types에 없는 조인/계산 필드) + if (extraColumns) { + for (const ec of extraColumns) { + if (!seenCols.has(ec.columnName)) { + seenCols.add(ec.columnName); + types.push({ + columnName: ec.columnName, + displayName: ec.displayName, + inputType: ec.inputType || "text", + }); + } + } + } // 기본 컬럼 설정 생성 (defaultVisibleKeys가 있으면 해당 컬럼만 표시) const unsortedColumns: ColumnSetting[] = types diff --git a/frontend/components/layout/AdminPageRenderer.tsx b/frontend/components/layout/AdminPageRenderer.tsx index b3bf24ac..f037526d 100644 --- a/frontend/components/layout/AdminPageRenderer.tsx +++ b/frontend/components/layout/AdminPageRenderer.tsx @@ -514,6 +514,13 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_31/outsourcing/purchase-order": dynamic(() => import("@/app/(main)/COMPANY_31/outsourcing/purchase-order/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_31/outsourcing/purchase-status": dynamic(() => import("@/app/(main)/COMPANY_31/outsourcing/purchase-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_31/outsourcing/vendor-stock": dynamic(() => import("@/app/(main)/COMPANY_31/outsourcing/vendor-stock/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_31/purchase/order": dynamic(() => import("@/app/(main)/COMPANY_31/purchase/order/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_31/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_31/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_31/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_31/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_31/mold/info": dynamic(() => import("@/app/(main)/COMPANY_31/mold/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_31/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_31/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_31/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_31/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_31/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_31/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_31/design/project": dynamic(() => import("@/app/(main)/COMPANY_31/design/project/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_31/design/change-management": dynamic(() => import("@/app/(main)/COMPANY_31/design/change-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_31/design/my-work": dynamic(() => import("@/app/(main)/COMPANY_31/design/my-work/page"), { ssr: false, loading: LoadingFallback }), diff --git a/frontend/hooks/useTableSettings.ts b/frontend/hooks/useTableSettings.ts index d29068ac..c17b5bf0 100644 --- a/frontend/hooks/useTableSettings.ts +++ b/frontend/hooks/useTableSettings.ts @@ -91,6 +91,7 @@ export function useTableSettings( setColumnWidths(widths); setOrderedKeys(order); + // 화면에 표시된 컬럼만 필터 가능하도록 제한 // 화면에 표시된 컬럼만 필터 가능하도록 제한 setFilterConfig( settings.filters?.filter((f) => visible.has(f.columnName)), diff --git a/scripts/prod/deploy.sh b/scripts/prod/deploy.sh index d8430e3e..c243c802 100755 --- a/scripts/prod/deploy.sh +++ b/scripts/prod/deploy.sh @@ -17,34 +17,24 @@ echo "======================================" # Git 최신 코드 가져오기 echo "" -echo "[1/6] Git 최신 코드 가져오기..." +echo "[1/4] Git 최신 코드 가져오기..." git pull origin main -# Docker 볼륨 사용으로 호스트 디렉토리 준비 불필요 +# 새로운 이미지 빌드 (구 컨테이너는 계속 서빙 중) echo "" -echo "[2/6] Docker 볼륨 확인..." -echo "Docker named volumes 사용 (권한 문제 없음)" +echo "[2/4] Docker 이미지 빌드 (서비스 무중단)..." +docker-compose -f "$COMPOSE_FILE" build -# 기존 컨테이너 중지 및 제거 +# 변경된 컨테이너만 graceful 재생성 (이미지 해시 동일하면 건드리지 않음) echo "" -echo "[3/6] 기존 컨테이너 중지..." -docker-compose -f "$COMPOSE_FILE" down - -# 오래된 이미지 정리 -echo "" -echo "[4/6] Docker 이미지 정리..." -docker image prune -f - -# 새로운 이미지 빌드 -echo "" -echo "[5/6] Docker 이미지 빌드..." -docker-compose -f "$COMPOSE_FILE" build --no-cache - -# 컨테이너 실행 -echo "" -echo "[6/6] 컨테이너 실행..." +echo "[3/4] 컨테이너 교체..." docker-compose -f "$COMPOSE_FILE" up -d +# 오래된 이미지 정리 (서비스 복귀 후 후처리) +echo "" +echo "[4/4] Docker 이미지 정리..." +docker image prune -f + # 배포 완료 echo "" echo "======================================"