+
{/* ๋ถ๊ฐ ๋ด์ญ ํค๋ */}
diff --git a/src/components/accounting/GeneralJournalEntry/actions.ts b/src/components/accounting/GeneralJournalEntry/actions.ts
index 5e486062..92ad2353 100644
--- a/src/components/accounting/GeneralJournalEntry/actions.ts
+++ b/src/components/accounting/GeneralJournalEntry/actions.ts
@@ -62,6 +62,7 @@ export async function getJournalSummary(params: {
export async function createManualJournal(data: {
journalDate: string;
description: string;
+ receiptNo?: string;
rows: JournalEntryRow[];
}): Promise
{
return executeServerAction({
@@ -70,6 +71,7 @@ export async function createManualJournal(data: {
body: {
journal_date: data.journalDate,
description: data.description,
+ receipt_no: data.receiptNo || null,
rows: data.rows.map((r) => ({
side: r.side,
account_subject_id: r.accountSubjectId,
diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx
index 3556c4db..7efc9bdd 100644
--- a/src/components/approval/ApprovalBox/index.tsx
+++ b/src/components/approval/ApprovalBox/index.tsx
@@ -73,7 +73,7 @@ import {
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { usePermission } from '@/hooks/usePermission';
-import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal';
+import { InspectionReportModal } from '@/components/document-system/modals';
export function ApprovalBox() {
const router = useRouter();
diff --git a/src/components/auth/ModuleGuard.tsx b/src/components/auth/ModuleGuard.tsx
new file mode 100644
index 00000000..079c59a9
--- /dev/null
+++ b/src/components/auth/ModuleGuard.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+/**
+ * ๋ชจ๋ ๋ผ์ฐํธ ๊ฐ๋
+ *
+ * ํ์ฌ ํ
๋ํธ๊ฐ ๋ณด์ ํ์ง ์์ ๋ชจ๋์ ํ์ด์ง์ ์ ๊ทผ ์ ์ฐจ๋จ.
+ * (protected)/layout.tsx์์ PermissionGate ๋ด๋ถ์ ๋ํ.
+ *
+ * Phase 1: ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๊ฐ๋ (๋ฐฑ์๋ ๋ณ๊ฒฝ ๋ถํ์)
+ * Phase 2: middleware.ts๋ก ์ด๋ (์๋ฒ ์ฌ์ด๋ ๊ฐ๋)
+ */
+
+import { usePathname, useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { toast } from 'sonner';
+import { useModules } from '@/hooks/useModules';
+import { ShieldAlert } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+export function ModuleGuard({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+ const router = useRouter();
+ const { isRouteAllowed, tenantIndustry } = useModules();
+
+ // locale ์ ๋์ฌ ์ ๊ฑฐ (์: /ko/production โ /production)
+ const cleanPath = pathname.replace(/^\/[a-z]{2}(?=\/)/, '');
+
+ const allowed = isRouteAllowed(cleanPath);
+
+ useEffect(() => {
+ // industry๊ฐ ์์ง ์ค์ ๋์ง ์์ ํ
๋ํธ๋ ๊ฐ๋ ๋นํ์ฑ (์ ๋ถ ํ์ฉ)
+ if (!tenantIndustry) return;
+
+ if (!allowed) {
+ toast.error('์ ๊ทผ ๊ถํ์ด ์๋ ๋ชจ๋์
๋๋ค.');
+ }
+ }, [allowed, tenantIndustry]);
+
+ // industry ๋ฏธ์ค์ ์ ๊ฐ๋ ๋นํ์ฑ (ํ์ ํธํ)
+ if (!tenantIndustry) {
+ return <>{children}>;
+ }
+
+ if (!allowed) {
+ return (
+
+
+
์ ๊ทผ ๊ถํ ์์
+
+ ํ์ฌ ๊ณ์ฝ์ ํฌํจ๋์ง ์์ ๋ชจ๋์
๋๋ค.
+
+
+
+ );
+ }
+
+ return <>{children}>;
+}
diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx
index 05940b6e..f2ec3373 100644
--- a/src/components/business/CEODashboard/CEODashboard.tsx
+++ b/src/components/business/CEODashboard/CEODashboard.tsx
@@ -41,17 +41,23 @@ import { getCardManagementModalConfigWithData } from './modalConfigs';
import { transformEntertainmentDetailResponse, transformWelfareDetailResponse, transformVatDetailResponse } from '@/lib/api/dashboard/transformers';
import { toast } from 'sonner';
import { consumeStaleSections, DASHBOARD_INVALIDATE_EVENT, type DashboardSectionKey } from '@/lib/dashboard-invalidation';
+import { useModules } from '@/hooks/useModules';
+import { sectionRequiresModule } from './types';
export function CEODashboard() {
const router = useRouter();
- // API ๋ฐ์ดํฐ Hook
+ // ๋ชจ๋ ํ์ฑํ ์ ๋ณด (tenantIndustry ๋ฏธ์ค์ ์ ๋ชจ๋ ๋ชจ๋ ํ์)
+ const { isEnabled, tenantIndustry } = useModules();
+ const moduleAware = !!tenantIndustry; // industry ์ค์ ์์๋ง ๋ชจ๋ ํํฐ๋ง ์ ์ฉ
+
+ // API ๋ฐ์ดํฐ Hook (๋ชจ๋ ๋นํ์ฑ ์ API ํธ์ถ ์คํต)
const apiData = useCEODashboard({
salesStatus: true,
purchaseStatus: true,
- dailyProduction: true,
- unshipped: true,
- construction: true,
+ dailyProduction: !moduleAware || isEnabled('production'),
+ unshipped: true, // ๊ณตํต (outbound/logistics)
+ construction: !moduleAware || isEnabled('construction'),
dailyAttendance: true,
});
@@ -548,8 +554,16 @@ export function CEODashboard() {
}
}, [calendarData]);
- // ์น์
์์
- const sectionOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
+ // ์น์
์์ (๋ชจ๋ ๋นํ์ฑ ์น์
ํํฐ๋ง)
+ const sectionOrder = useMemo(() => {
+ const rawOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
+ if (!moduleAware) return rawOrder; // industry ๋ฏธ์ค์ ์ ์ ๋ถ ํ์
+ return rawOrder.filter((key) => {
+ const requiredModule = sectionRequiresModule(key);
+ if (!requiredModule) return true; // ๊ณตํต ์น์
+ return isEnabled(requiredModule);
+ });
+ }, [dashboardSettings.sectionOrder, moduleAware, isEnabled]);
// ์์ฝ ๋ค๋น๊ฒ์ด์
๋ฐ ํ
const { summaries, activeSectionKey, sectionRefs, scrollToSection } = useSectionSummary({
diff --git a/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx b/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx
index cccd950f..34614f8a 100644
--- a/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx
+++ b/src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState, useCallback, useEffect } from 'react';
+import { useState, useCallback, useEffect, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -19,7 +19,8 @@ import type {
WelfareCalculationType,
SectionKey,
} from '../types';
-import { DEFAULT_SECTION_ORDER, SECTION_LABELS } from '../types';
+import { DEFAULT_SECTION_ORDER, SECTION_LABELS, sectionRequiresModule } from '../types';
+import { useModules } from '@/hooks/useModules';
import {
SectionRow,
StatusBoardItemsList,
@@ -40,6 +41,10 @@ export function DashboardSettingsDialog({
settings,
onSave,
}: DashboardSettingsDialogProps) {
+ // ๋ชจ๋ ํ์ฑํ ์ ๋ณด (industry ๋ฏธ์ค์ ์ ์ ๋ถ ํ์)
+ const { isEnabled, tenantIndustry } = useModules();
+ const moduleAware = !!tenantIndustry;
+
const [localSettings, setLocalSettings] = useState(settings);
const [expandedSections, setExpandedSections] = useState>({
todayIssueList: false,
@@ -53,8 +58,16 @@ export function DashboardSettingsDialog({
const [draggedSection, setDraggedSection] = useState(null);
const [dragOverSection, setDragOverSection] = useState(null);
- // ์น์
์์
- const sectionOrder = localSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
+ // ์น์
์์ (๋ชจ๋ ๋นํ์ฑ ์น์
์จ๊น)
+ const sectionOrder = useMemo(() => {
+ const rawOrder = localSettings.sectionOrder ?? DEFAULT_SECTION_ORDER;
+ if (!moduleAware) return rawOrder;
+ return rawOrder.filter((key: SectionKey) => {
+ const requiredModule = sectionRequiresModule(key);
+ if (!requiredModule) return true;
+ return isEnabled(requiredModule);
+ });
+ }, [localSettings.sectionOrder, moduleAware, isEnabled]);
// settings๊ฐ ๋ณ๊ฒฝ๋ ๋ ๋ก์ปฌ ์ํ ์
๋ฐ์ดํธ
useEffect(() => {
diff --git a/src/components/business/CEODashboard/sections/CalendarSection.tsx b/src/components/business/CEODashboard/sections/CalendarSection.tsx
index ba010adb..8b6f003e 100644
--- a/src/components/business/CEODashboard/sections/CalendarSection.tsx
+++ b/src/components/business/CEODashboard/sections/CalendarSection.tsx
@@ -16,6 +16,7 @@ import { ScheduleCalendar } from '@/components/common/ScheduleCalendar';
import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types';
import { getCalendarEventsForYear, type CalendarEvent } from '@/constants/calendarEvents';
import { useCalendarScheduleStore } from '@/stores/useCalendarScheduleStore';
+import { useModules } from '@/hooks/useModules';
import { CollapsibleDashboardCard } from '../components';
import type {
CalendarScheduleItem,
@@ -117,6 +118,12 @@ const TASK_FILTER_OPTIONS: { value: ExtendedTaskFilterType; label: string }[] =
{ value: 'issue', label: '์ด์' },
];
+// ์ผ์ ํ์
โ ๋ชจ๋ ๋งคํ (์ด ํ์
์ ๋งํฌ/ํํฐ๊ฐ ํด๋น ๋ชจ๋์ ์๊ตฌ)
+const SCHEDULE_TYPE_MODULE: Record = {
+ order: 'production',
+ construction: 'construction',
+};
+
export function CalendarSection({
schedules,
issues = [],
@@ -124,12 +131,24 @@ export function CalendarSection({
onScheduleEdit,
}: CalendarSectionProps) {
const router = useRouter();
+ const { isEnabled, tenantIndustry } = useModules();
+ const moduleAware = !!tenantIndustry;
const [selectedDate, setSelectedDate] = useState(new Date());
const [currentDate, setCurrentDate] = useState(new Date());
const [, _setViewType] = useState('month');
const [deptFilter, setDeptFilter] = useState('all');
const [taskFilter, setTaskFilter] = useState('all');
+ // ๋ชจ๋ ๊ธฐ๋ฐ ์
๋ฌด ํํฐ ์ต์
(๋นํ์ฑ ๋ชจ๋ ํํฐ ์จ๊น)
+ const filteredTaskFilterOptions = useMemo(() => {
+ if (!moduleAware) return TASK_FILTER_OPTIONS;
+ return TASK_FILTER_OPTIONS.filter((option) => {
+ const requiredModule = SCHEDULE_TYPE_MODULE[option.value];
+ if (!requiredModule) return true;
+ return isEnabled(requiredModule as 'production' | 'construction');
+ });
+ }, [moduleAware, isEnabled]);
+
// ์คํ ์ด์์ ๊ณตํด์ผ/์ธ๋ฌด์ผ์ ๊ฐ์ ธ์ค๊ธฐ (API ์ฐ๋)
const schedulesByYear = useCalendarScheduleStore((s) => s.schedulesByYear);
const fetchSchedules = useCalendarScheduleStore((s) => s.fetchSchedules);
@@ -272,7 +291,13 @@ export function CalendarSection({
};
// ์ผ์ ํ์
๋ณ ์์ธ ํ์ด์ง ๋งํฌ ์์ฑ (bill_123 โ /ko/accounting/bills/123)
+ // ๋ชจ๋ ๋นํ์ฑ ์ ํด๋น ํ์
์ ๋งํฌ ์จ๊น
const getScheduleLink = (schedule: CalendarScheduleItem): string | null => {
+ // ๋ชจ๋ ์์กด ํ์
์ธ๋ฐ ํด๋น ๋ชจ๋์ด ๋นํ์ฑ์ด๋ฉด ๋งํฌ ์์
+ const requiredModule = SCHEDULE_TYPE_MODULE[schedule.type];
+ if (moduleAware && requiredModule && !isEnabled(requiredModule as 'production' | 'construction')) {
+ return null;
+ }
const basePath = SCHEDULE_TYPE_ROUTES[schedule.type];
if (!basePath) return null;
// expected_expense๋ ๋ชฉ๋ก ํ์ด์ง๋ง ์กด์ฌ (์์ธ ํ์ด์ง ์์)
@@ -383,7 +408,7 @@ export function CalendarSection({
- {TASK_FILTER_OPTIONS.map((option) => (
+ {filteredTaskFilterOptions.map((option) => (
{option.label}
@@ -432,7 +457,7 @@ export function CalendarSection({
- {TASK_FILTER_OPTIONS.map((option) => (
+ {filteredTaskFilterOptions.map((option) => (
{option.label}
diff --git a/src/components/business/CEODashboard/types.ts b/src/components/business/CEODashboard/types.ts
index 44b34bd9..790d072c 100644
--- a/src/components/business/CEODashboard/types.ts
+++ b/src/components/business/CEODashboard/types.ts
@@ -3,6 +3,7 @@
*/
import type React from 'react';
+import type { ModuleId } from '@/modules/types';
// ์ฒดํฌํฌ์ธํธ ํ์
(๊ฒฝ๊ณ /์ฑ๊ณต/์๋ฌ/์ ๋ณด)
export type CheckPointType = 'success' | 'warning' | 'error' | 'info';
@@ -716,6 +717,21 @@ export interface DetailModalConfig {
table?: TableConfig;
}
+// ===== ๋ชจ๋๋ณ ์น์
๋งคํ (Phase 2: Dashboard Decoupling) =====
+
+/** ํน์ ๋ชจ๋์ด ํ์ํ ์น์
๋งคํ (์ฌ๊ธฐ ์๋ ์น์
์ ๊ณตํต = ํญ์ ํ์) */
+export const MODULE_DEPENDENT_SECTIONS: Partial> = {
+ production: 'production',
+ shipment: 'production',
+ construction: 'construction',
+ // unshipped๋ ๊ณตํต(outbound/logistics) โ ๋ชจ๋ ์์กด์ฑ ์์
+};
+
+/** ์น์
์ด ์๊ตฌํ๋ ๋ชจ๋ ID ๋ฐํ. ๊ณตํต ์น์
์ด๋ฉด null */
+export function sectionRequiresModule(sectionKey: SectionKey): ModuleId | null {
+ return MODULE_DEPENDENT_SECTIONS[sectionKey] ?? null;
+}
+
// ๊ธฐ๋ณธ ์ค์ ๊ฐ
export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = {
// ์ ์ค๋์ ์ด์ (๋ฆฌ์คํธ ํํ)
diff --git a/src/components/business/CEODashboard/useSectionSummary.ts b/src/components/business/CEODashboard/useSectionSummary.ts
index bfe8f1a2..dd0f20e8 100644
--- a/src/components/business/CEODashboard/useSectionSummary.ts
+++ b/src/components/business/CEODashboard/useSectionSummary.ts
@@ -2,7 +2,8 @@
import { useMemo, useEffect, useState, useRef, useCallback } from 'react';
import type { CEODashboardData, DashboardSettings, SectionKey } from './types';
-import { SECTION_LABELS } from './types';
+import { SECTION_LABELS, sectionRequiresModule } from './types';
+import { useModules } from '@/hooks/useModules';
export type SummaryStatus = 'normal' | 'warning' | 'danger';
@@ -220,10 +221,21 @@ export function useSectionSummary({
// ์นฉ ํด๋ฆญ์ผ๋ก ์ ํ๋ ํค โ ํด๋น ์น์
์ด ํ๋ฉด์ ๋ณด์ด๋ ํ ์ ์ง
const pinnedKey = useRef(null);
- // ํ์ฑํ๋ ์น์
๋ง ํํฐ
+ // ๋ชจ๋ ํ์ฑํ ์ ๋ณด (tenantIndustry ๋ฏธ์ค์ ์ ์ ๋ถ ํ์)
+ const { isEnabled, tenantIndustry } = useModules();
+ const moduleAware = !!tenantIndustry;
+
+ // ํ์ฑํ๋ ์น์
๋ง ํํฐ (์ค์ + ๋ชจ๋)
const enabledSections = useMemo(
- () => sectionOrder.filter((key) => isSectionEnabled(key, dashboardSettings)),
- [sectionOrder, dashboardSettings],
+ () => sectionOrder.filter((key) => {
+ if (!isSectionEnabled(key, dashboardSettings)) return false;
+ if (moduleAware) {
+ const requiredModule = sectionRequiresModule(key);
+ if (requiredModule && !isEnabled(requiredModule)) return false;
+ }
+ return true;
+ }),
+ [sectionOrder, dashboardSettings, moduleAware, isEnabled],
);
// ์์ฝ ๋ฐ์ดํฐ ๊ณ์ฐ
diff --git a/src/components/business/construction/MODULE.md b/src/components/business/construction/MODULE.md
new file mode 100644
index 00000000..b1498417
--- /dev/null
+++ b/src/components/business/construction/MODULE.md
@@ -0,0 +1,32 @@
+# Construction Module (๊ฑด์ค๊ด๋ฆฌ)
+
+**Module ID**: `construction`
+**Tenant**: Juil (์ฃผ์ผ๊ฑด์ค)
+**Route Prefixes**: `/construction`
+**Component Count**: 161 files
+
+## Dependencies on Common ERP
+- `@/lib/api/*` โ Server actions, API client
+- `@/components/ui/*` โ UI primitives (shadcn/ui)
+- `@/components/templates/*` โ IntegratedListTemplateV2 ๋ฑ
+- `@/components/organisms/*` โ PageLayout, PageHeader
+- `@/hooks/*` โ usePermission, useModules ๋ฑ
+- `@/stores/authStore` โ Tenant ์ ๋ณด
+- `@/components/common/*` โ ๊ณตํต ์ปดํฌ๋ํธ
+
+## Exports to Common ERP
+**NONE** โ ๊ฑด์ค ๋ชจ๋์ ๋
๋ฆฝ์ ์ผ๋ก ์๋.
+
+## Related Dashboard Sections
+- `construction` (์๊ณต ํํฉ)
+
+## Subdirectories
+- `bidding/` โ ์
์ฐฐ ๊ด๋ฆฌ
+- `contract/` โ ๊ณ์ฝ ๊ด๋ฆฌ
+- `estimates/` โ ๊ฒฌ์ ๊ด๋ฆฌ
+- `progress-billing/` โ ๊ธฐ์ฑ ๊ด๋ฆฌ
+- `site-management/` โ ํ์ฅ ๊ด๋ฆฌ
+- `labor-management/` โ ๋
ธ๋ฌด ๊ด๋ฆฌ
+- `item-management/` โ ์์ฌ ๊ด๋ฆฌ
+- `partners/` โ ํ๋ ฅ์
์ฒด ๊ด๋ฆฌ
+- ๊ธฐํ 20๊ฐ ํ์ ๋๋ฉ์ธ
diff --git a/src/components/business/construction/item-management/ItemDetailClient.tsx b/src/components/business/construction/item-management/ItemDetailClient.tsx
index c54c6a60..cca78995 100644
--- a/src/components/business/construction/item-management/ItemDetailClient.tsx
+++ b/src/components/business/construction/item-management/ItemDetailClient.tsx
@@ -30,6 +30,7 @@ import {
UNIT_OPTIONS,
} from './constants';
import { getItem, createItem, updateItem, deleteItem, getCategoryOptions } from './actions';
+import { BomTreeViewer } from '@/components/items/BomTreeViewer';
interface ItemDetailClientProps {
itemId?: string;
@@ -460,6 +461,9 @@ export default function ItemDetailClient({
)}
+
+ {/* BOM ํธ๋ฆฌ */}
+ {itemId && }
), [
formData,
@@ -469,6 +473,7 @@ export default function ItemDetailClient({
handleAddOrderItem,
handleRemoveOrderItem,
handleOrderItemChange,
+ itemId,
]);
return (
diff --git a/src/components/document-system/modals/AssigneeSelectModal.tsx b/src/components/document-system/modals/AssigneeSelectModal.tsx
new file mode 100644
index 00000000..0df27bea
--- /dev/null
+++ b/src/components/document-system/modals/AssigneeSelectModal.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+/**
+ * AssigneeSelectModal โ ๊ณต์ ๋ํผ
+ *
+ * ์๋ณธ: @/components/production/WorkOrders/AssigneeSelectModal
+ * ๋ชฉ์ : ๊ณตํต ERP(์์
)์์ ์์ฐ ๋ชจ๋์ ์ง์ importํ์ง ์๋๋ก
+ * dynamic import๋ก ์ ์ ์์กด์ฑ ์ฒด์ธ์ ๋์
+ */
+
+import dynamic from 'next/dynamic';
+import { Loader2 } from 'lucide-react';
+
+const AssigneeSelectModalImpl = dynamic(
+ () =>
+ import('@/components/production/WorkOrders/AssigneeSelectModal').then(
+ (mod) => mod.AssigneeSelectModal,
+ ),
+ {
+ loading: () => (
+
+
+
+ ),
+ ssr: false,
+ },
+);
+
+export { AssigneeSelectModalImpl as AssigneeSelectModal };
diff --git a/src/components/document-system/modals/InspectionReportModal.tsx b/src/components/document-system/modals/InspectionReportModal.tsx
new file mode 100644
index 00000000..bad3100f
--- /dev/null
+++ b/src/components/document-system/modals/InspectionReportModal.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+/**
+ * InspectionReportModal โ ๊ณต์ ๋ํผ
+ *
+ * ์๋ณธ: @/components/production/WorkOrders/documents/InspectionReportModal
+ * ๋ชฉ์ : ๊ณตํต ERP(๊ฒฐ์ฌ, ํ์ง)์์ ์์ฐ ๋ชจ๋์ ์ง์ importํ์ง ์๋๋ก
+ * dynamic import๋ก ์ ์ ์์กด์ฑ ์ฒด์ธ์ ๋์
+ */
+
+import dynamic from 'next/dynamic';
+import { Loader2 } from 'lucide-react';
+
+const InspectionReportModalImpl = dynamic(
+ () =>
+ import('@/components/production/WorkOrders/documents/InspectionReportModal').then(
+ (mod) => mod.InspectionReportModal,
+ ),
+ {
+ loading: () => (
+
+
+
+ ),
+ ssr: false,
+ },
+);
+
+export { InspectionReportModalImpl as InspectionReportModal };
diff --git a/src/components/document-system/modals/WorkLogModal.tsx b/src/components/document-system/modals/WorkLogModal.tsx
new file mode 100644
index 00000000..961e750a
--- /dev/null
+++ b/src/components/document-system/modals/WorkLogModal.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+/**
+ * WorkLogModal โ ๊ณต์ ๋ํผ
+ *
+ * ์๋ณธ: @/components/production/WorkOrders/documents/WorkLogModal
+ * ๋ชฉ์ : ๊ณตํต ERP(ํ์ง QMS)์์ ์์ฐ ๋ชจ๋์ ์ง์ importํ์ง ์๋๋ก
+ * dynamic import๋ก ์ ์ ์์กด์ฑ ์ฒด์ธ์ ๋์
+ */
+
+import dynamic from 'next/dynamic';
+import { Loader2 } from 'lucide-react';
+
+const WorkLogModalImpl = dynamic(
+ () =>
+ import('@/components/production/WorkOrders/documents/WorkLogModal').then(
+ (mod) => mod.WorkLogModal,
+ ),
+ {
+ loading: () => (
+
+
+
+ ),
+ ssr: false,
+ },
+);
+
+export { WorkLogModalImpl as WorkLogModal };
diff --git a/src/components/document-system/modals/index.ts b/src/components/document-system/modals/index.ts
new file mode 100644
index 00000000..a67412a4
--- /dev/null
+++ b/src/components/document-system/modals/index.ts
@@ -0,0 +1,9 @@
+/**
+ * document-system/modals โ ๋ชจ๋ ๊ฒฝ๊ณ๋ฅผ ๋๋ ๊ณต์ ๋ชจ๋ฌ ๋ํผ
+ *
+ * ๊ณตํต ERP ์ฝ๋์์ ํ
๋ํธ ์ ์ฉ ๋ชจ๋ฌ์ ์ฌ์ฉํ ๋
+ * ์ง์ import ๋์ ์ด ๋ํผ๋ฅผ ํตํด dynamic import๋ก ์ ๊ทผ
+ */
+export { InspectionReportModal } from './InspectionReportModal';
+export { WorkLogModal } from './WorkLogModal';
+export { AssigneeSelectModal } from './AssigneeSelectModal';
diff --git a/src/components/items/BomTreeViewer.tsx b/src/components/items/BomTreeViewer.tsx
new file mode 100644
index 00000000..533b9f29
--- /dev/null
+++ b/src/components/items/BomTreeViewer.tsx
@@ -0,0 +1,332 @@
+'use client';
+
+/**
+ * BOM Tree ์๊ฐํ ์ปดํฌ๋ํธ
+ *
+ * API: GET /api/proxy/items/{id}/bom/tree
+ * 3๋จ๊ณ ํธ๋ฆฌ: FG(๋ฃจํธ) โ CAT(์นดํ
๊ณ ๋ฆฌ ๊ทธ๋ฃน) โ PT(๋ถํ)
+ * CAT ๋
ธ๋: ์นดํ
๊ณ ๋ฆฌ ํค๋๋ก ๋ ๋๋ง (์ ํ/ํผ์นจ, count ํ์)
+ * PT ๋
ธ๋: ํ๋ชฉ ํ์ผ๋ก ๋ ๋๋ง (์ฝ๋, ํ๋ชฉ๋ช
, ์๋, ๋จ์)
+ */
+
+import { useState, useEffect, useCallback } from 'react';
+import { ChevronDown, ChevronRight, ChevronsUpDown, Loader2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Package } from 'lucide-react';
+
+// BOM ํธ๋ฆฌ ๋
ธ๋ ํ์
+interface BomTreeNode {
+ id: number;
+ code: string;
+ name: string;
+ item_type: string;
+ specification?: string;
+ unit?: string;
+ quantity?: number;
+ count?: number; // CAT ๋
ธ๋ โ ํ์ ํ๋ชฉ ๊ฑด์
+ depth: number;
+ children: BomTreeNode[];
+}
+
+// ์ ํ๋ณ ๋ฑ์ง ์คํ์ผ
+const ITEM_TYPE_COLORS: Record
= {
+ FG: 'bg-blue-100 text-blue-800',
+ PT: 'bg-green-100 text-green-800',
+ RM: 'bg-orange-100 text-orange-800',
+ SM: 'bg-purple-100 text-purple-800',
+ CS: 'bg-gray-100 text-gray-800',
+ BN: 'bg-pink-100 text-pink-800',
+ SF: 'bg-cyan-100 text-cyan-800',
+};
+
+// ๋
ธ๋๋ณ ๊ณ ์ ํค ์์ฑ (CAT ๋
ธ๋๋ id=0์ด๋ฏ๋ก ์ด๋ฆ ๊ธฐ๋ฐ)
+function getNodeKey(node: BomTreeNode, index: number): string {
+ if (node.item_type === 'CAT') return `cat-${index}-${node.name}`;
+ return `item-${node.id}`;
+}
+
+// ๋ชจ๋ ๋
ธ๋์ ํค๋ฅผ ์ฌ๊ท์ ์ผ๋ก ์์ง
+function collectAllKeys(nodes: BomTreeNode[]): Set {
+ const keys = new Set();
+ function walk(node: BomTreeNode, index: number) {
+ keys.add(getNodeKey(node, index));
+ node.children?.forEach((child, i) => walk(child, i));
+ }
+ nodes.forEach((node, i) => walk(node, i));
+ return keys;
+}
+
+// ์นดํ
๊ณ ๋ฆฌ ๋
ธ๋ ์ปดํฌ๋ํธ
+function CategoryNode({
+ node,
+ nodeKey,
+ isOpen,
+ onToggle,
+}: {
+ node: BomTreeNode;
+ nodeKey: string;
+ isOpen: boolean;
+ onToggle: (key: string) => void;
+}) {
+ const hasChildren = node.children && node.children.length > 0;
+
+ return (
+
+ {/* ์นดํ
๊ณ ๋ฆฌ ํค๋ */}
+
hasChildren && onToggle(nodeKey)}
+ >
+ {hasChildren && (
+ isOpen
+ ?
+ :
+ )}
+ {node.name}
+ {node.count ?? node.children?.length ?? 0}๊ฑด
+
+
+ {/* ํ์ ํ๋ชฉ (์ ํ/ํผ์นจ) */}
+ {isOpen && hasChildren && (
+
+ {node.children.map((child, i) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+// ํ๋ชฉ(PT) ๋
ธ๋ ์ปดํฌ๋ํธ
+function ItemNode({ node }: { node: BomTreeNode }) {
+ return (
+
+
+ {/* ์ ํ ๋ฑ์ง */}
+
+ {node.item_type}
+
+
+ {/* ์ฝ๋ โ PC๋ง ์ธ๋ผ์ธ ํ์ */}
+
+ {node.code}
+
+
+ {/* ํ๋ชฉ๋ช
*/}
+ {node.name}
+
+ {/* ์๋ + ๋จ์ */}
+
+ {node.quantity != null && (
+ x{node.quantity}
+ )}
+ {node.unit && (
+ {node.unit}
+ )}
+
+
+ {/* ์ฝ๋ 2์ค โ ๋ชจ๋ฐ์ผ๋ง */}
+
+ {node.code}
+
+
+ );
+}
+
+// ๋ฒ์ฉ ๋
ธ๋ ๋ ๋๋ฌ (CAT ๋ถ๊ธฐ)
+function BomNodeRenderer({
+ node,
+ index,
+ expandedNodes,
+ onToggle,
+}: {
+ node: BomTreeNode;
+ index: number;
+ expandedNodes: Set;
+ onToggle: (key: string) => void;
+}) {
+ const nodeKey = getNodeKey(node, index);
+ const isCategory = node.item_type === 'CAT';
+
+ if (isCategory) {
+ return (
+
+ );
+ }
+
+ return ;
+}
+
+interface BomTreeViewerProps {
+ itemId: string;
+ itemType: string;
+}
+
+export function BomTreeViewer({ itemId, itemType }: BomTreeViewerProps) {
+ const [treeData, setTreeData] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [expandedNodes, setExpandedNodes] = useState>(new Set());
+ const [allExpanded, setAllExpanded] = useState(true);
+
+ // ํธ๋ฆฌ ๋ฐ์ดํฐ ๋ก๋
+ const loadTree = useCallback(async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const response = await fetch(`/api/proxy/items/${itemId}/bom/tree`);
+ const result = await response.json();
+
+ if (result.success && result.data) {
+ const root = result.data as BomTreeNode;
+ setTreeData(root);
+ // ๊ธฐ๋ณธ: ๋ชจ๋ ์นดํ
๊ณ ๋ฆฌ ํผ์นจ
+ if (root.children) {
+ setExpandedNodes(collectAllKeys(root.children));
+ }
+ } else {
+ setTreeData(null);
+ }
+ } catch {
+ setError('BOM ํธ๋ฆฌ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [itemId]);
+
+ useEffect(() => {
+ const isBomTarget = ['FG', 'PT', '์ ํ', '๋ถํ'].includes(itemType);
+ if (isBomTarget) {
+ loadTree();
+ } else {
+ setIsLoading(false);
+ }
+ }, [loadTree, itemType]);
+
+ // BOM ๋์์ด ์๋๋ฉด ๋ ๋๋งํ์ง ์์
+ if (!['FG', 'PT', '์ ํ', '๋ถํ'].includes(itemType)) return null;
+
+ // ๋ก๋ฉ
+ if (isLoading) {
+ return (
+
+
+
+ BOM ํธ๋ฆฌ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
+
+ );
+ }
+
+ // ์๋ฌ
+ if (error) {
+ return (
+
+
+ {error}
+
+
+
+ );
+ }
+
+ // ๋ฐ์ดํฐ ์์
+ if (!treeData || !treeData.children || treeData.children.length === 0) {
+ return (
+
+
+
+
+ ๋ถํ ๊ตฌ์ฑ (BOM)
+
+
+
+
+ ๋ฑ๋ก๋ BOM ์ ๋ณด๊ฐ ์์ต๋๋ค.
+
+
+
+ );
+ }
+
+ const toggleNode = (key: string) => {
+ setExpandedNodes((prev) => {
+ const next = new Set(prev);
+ if (next.has(key)) next.delete(key);
+ else next.add(key);
+ return next;
+ });
+ };
+
+ const expandAll = () => { setExpandedNodes(collectAllKeys(treeData.children)); setAllExpanded(true); };
+ const collapseAll = () => { setExpandedNodes(new Set()); setAllExpanded(false); };
+ const toggleAll = () => { if (allExpanded) collapseAll(); else expandAll(); };
+
+ // ์นดํ
๊ณ ๋ฆฌ ๊ทธ๋ฃน ์ & ์ด ํ๋ชฉ ์
+ const categories = treeData.children.filter(n => n.item_type === 'CAT');
+ const totalItems = categories.reduce((sum, cat) => sum + (cat.count ?? cat.children?.length ?? 0), 0);
+ const groupCount = categories.length;
+
+ return (
+
+
+ {/* PC: ํ ์ค ๋ ์ด์์ */}
+
+
+
+ ๋ถํ ๊ตฌ์ฑ (BOM)
+
+
+
+ ์ด {totalItems}๊ฐ ํ๋ชฉ ยท {groupCount}๊ฐ ๊ทธ๋ฃน
+
+
+
+
+ {/* ๋ชจ๋ฐ์ผ: ์ค๋ฐ๊ฟ ๋ ์ด์์ */}
+
+
+
+ ๋ถํ ๊ตฌ์ฑ (BOM)
+
+
+ ์ด {totalItems}๊ฐ ํ๋ชฉ ยท {groupCount}๊ฐ ๊ทธ๋ฃน
+
+
+
+
+
+
+ {treeData.children.map((node, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/items/ItemDetailClient.tsx b/src/components/items/ItemDetailClient.tsx
index 8fb254e9..b1dd133d 100644
--- a/src/components/items/ItemDetailClient.tsx
+++ b/src/components/items/ItemDetailClient.tsx
@@ -28,6 +28,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { ArrowLeft, Edit, Package, FileImage, Download, FileText, Check, Calendar } from 'lucide-react';
+import { BomTreeViewer } from './BomTreeViewer';
import { downloadFileById } from '@/lib/utils/fileDownload';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { useMenuStore } from '@/stores/menuStore';
@@ -554,60 +555,9 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
)}
- {/* BOM ์ ๋ณด - ์ ๊ณก ๋ถํ์ ์ ์ธ */}
- {(item.itemType === 'FG' || (item.itemType === 'PT' && item.partType !== 'BENDING')) && item.bom && item.bom.length > 0 && (
-
-
-
-
-
- ๋ถํ ๊ตฌ์ฑ (BOM)
-
-
- ์ด {item.bom.length}๊ฐ ํ๋ชฉ
-
-
-
-
-
-
-
-
- ๋ฒํธ
- ํ๋ชฉ์ฝ๋
- ํ๋ชฉ๋ช
- ์๋
- ๋จ์
-
-
-
- {item.bom.map((line, index) => (
-
- {index + 1}
-
-
- {line.childItemCode}
-
-
-
-
- {line.childItemName}
- {line.isBending && (
-
- ์ ๊ณกํ
-
- )}
-
-
- {line.quantity}
- {line.unit}
-
- ))}
-
-
-
-
-
+ {/* BOM ํธ๋ฆฌ - FG/PT๋ง ํ์ (์ ๊ณก ๋ถํ ์ ์ธ) */}
+ {(item.itemType === 'FG' || (item.itemType === 'PT' && item.partType !== 'BENDING')) && (
+
)}
{/* ํ๋จ ์ก์
๋ฒํผ (sticky) */}
diff --git a/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx b/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx
index a4740820..63d0e7d2 100644
--- a/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx
+++ b/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx
@@ -18,8 +18,8 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
- VisuallyHidden,
DialogDescription,
+ VisuallyHidden,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -670,12 +670,10 @@ export function ImportInspectionInputModal({
// ์บก์ฒ ์คํจ ์ ๋ฌด์ โ rendered_html ์์ด ์ ์ฅ ์งํ
}
- // 5. ์ ์ฅ API ํธ์ถ (rendered_html์ด ๋๋ฌด ํฌ๋ฉด ์ ์ธ โ 413 ๋ฐฉ์ง)
- const MAX_HTML_SIZE = 500 * 1024; // 500KB ์ ํ
- const safeHtml = renderedHtml && renderedHtml.length <= MAX_HTML_SIZE ? renderedHtml : undefined;
- if (renderedHtml && renderedHtml.length > MAX_HTML_SIZE) {
- console.warn(`[ImportInspection] rendered_html ํฌ๊ธฐ ์ด๊ณผ (${(renderedHtml.length / 1024).toFixed(0)}KB), ์ ์ธํ๊ณ ์ ์ฅํฉ๋๋ค.`);
- }
+ // 5. ์ ์ฅ API ํธ์ถ (rendered_html 500KB ์ด๊ณผ ์ ์ ์ธ โ 413 ์๋ฌ ๋ฐฉ์ง)
+ const MAX_HTML_SIZE = 500 * 1024;
+ const safeHtml = renderedHtml && renderedHtml.length <= MAX_HTML_SIZE
+ ? renderedHtml : undefined;
const result = await saveInspectionData({
templateId: parseInt(template.templateId),
diff --git a/src/components/material/ReceivingManagement/InventoryAdjustmentDialog.tsx b/src/components/material/ReceivingManagement/InventoryAdjustmentDialog.tsx
index 867403c5..12b398b1 100644
--- a/src/components/material/ReceivingManagement/InventoryAdjustmentDialog.tsx
+++ b/src/components/material/ReceivingManagement/InventoryAdjustmentDialog.tsx
@@ -126,7 +126,7 @@ export function InventoryAdjustmentDialog({ open, onOpenChange, onSave }: Props)
return (
{/* ํ
์ด๋ธ */}
-
+
diff --git a/src/components/material/ReceivingManagement/ReceivingDetail.tsx b/src/components/material/ReceivingManagement/ReceivingDetail.tsx
index 5c291ef3..e8f51812 100644
--- a/src/components/material/ReceivingManagement/ReceivingDetail.tsx
+++ b/src/components/material/ReceivingManagement/ReceivingDetail.tsx
@@ -57,7 +57,6 @@ import {
RECEIVING_STATUS_OPTIONS,
type ReceivingDetail as ReceivingDetailType,
type ReceivingStatus,
- type InventoryAdjustmentRecord,
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { toast } from 'sonner';
@@ -68,7 +67,7 @@ interface Props {
mode?: 'view' | 'edit' | 'new';
}
-// ์ด๊ธฐ ํผ ๋ฐ์ดํฐ ์์ฑ (์ธ์
์ฌ์ฉ์, ์ค๋ ๋ ์ง ๊ธฐ๋ณธ๊ฐ)
+// ์ด๊ธฐ ํผ ๋ฐ์ดํฐ (๋์ ํจ์ โ ์ธ์
์ฌ์ฉ์ ์ด๋ฆ + ์ค๋ ๋ ์ง ๊ธฐ๋ณธ๊ฐ)
function createInitialFormData(): Partial {
return {
materialNo: '',
@@ -93,16 +92,6 @@ function createInitialFormData(): Partial {
};
}
-// ๋กํธ๋ฒํธ ์์ฑ (YYMMDD-NN)
-function generateLotNo(): string {
- const now = new Date();
- const yy = String(now.getFullYear()).slice(-2);
- const mm = String(now.getMonth() + 1).padStart(2, '0');
- const dd = String(now.getDate()).padStart(2, '0');
- const seq = String(Math.floor(Math.random() * 100)).padStart(2, '0');
- return `${yy}${mm}${dd}-${seq}`;
-}
-
// localStorage์์ ๋ก๊ทธ์ธ ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
function getLoggedInUser(): { name: string; department: string } {
if (typeof window === 'undefined') return { name: '', department: '' };
@@ -160,9 +149,6 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
file?: { id: number; display_name?: string; original_name?: string; file_path: string; file_size: number; mime_type?: string };
}>>([]);
- // ์ฌ๊ณ ์กฐ์ ์ด๋ ฅ ์ํ
- const [adjustments, setAdjustments] = useState([]);
-
// Dev ๋ชจ๋ ํผ ์๋ ์ฑ์ฐ๊ธฐ
useDevFill(
'receiving',
@@ -171,7 +157,6 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
const data = generateReceivingData();
setFormData((prev) => ({
...prev,
- lotNo: generateLotNo(),
itemCode: data.itemCode,
itemName: data.itemName,
specification: data.specification,
@@ -201,10 +186,6 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
if (result.success && result.data) {
setDetail(result.data);
- // ์ฌ๊ณ ์กฐ์ ์ด๋ ฅ ์ค์
- if (result.data.inventoryAdjustments) {
- setAdjustments(result.data.inventoryAdjustments);
- }
// ๊ธฐ์กด ์ฑ์ ์ ํ์ผ ์ ๋ณด ์ค์
if (result.data.certificateFileId) {
setExistingCertFile({
@@ -243,9 +224,7 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
if (templateCheck.success) {
setHasInspectionTemplate(templateCheck.hasTemplate);
}
- if (templateCheck.attachments && templateCheck.attachments.length > 0) {
- setInspectionAttachments(templateCheck.attachments);
- }
+ setInspectionAttachments(templateCheck.attachments ?? []);
} else {
setHasInspectionTemplate(false);
}
@@ -296,14 +275,14 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
const result = await createReceiving(saveData);
if (result.success) {
toast.success('์
๊ณ ๊ฐ ๋ฑ๋ก๋์์ต๋๋ค.');
- // ๋ฑ๋ก ์๋ฃ ํ ์์ฑ๋ ์
๊ณ ์์ธ ํ์ด์ง๋ก ๋ฐ๋ก ์ด๋ (๋ชฉ๋ก ๊ฒฝ์ ๋ฐฉ์ง)
const newId = result.data?.id;
if (newId) {
router.push(`/ko/material/receiving-management/${newId}?mode=view`);
} else {
router.push('/ko/material/receiving-management');
}
- return { success: true };
+ // ์ปค์คํ
๋ค๋น๊ฒ์ด์
์ฒ๋ฆฌ: error='' โ ํ
ํ๋ฆฟ์ navigateToList() ํธ์ถ ๋ฐฉ์ง
+ return { success: false, error: '' };
} else {
toast.error(result.error || '๋ฑ๋ก์ ์คํจํ์ต๋๋ค.');
return { success: false, error: result.error };
@@ -345,30 +324,6 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
loadData();
};
- // ์ฌ๊ณ ์กฐ์ ํ ์ถ๊ฐ
- const handleAddAdjustment = () => {
- const newRecord: InventoryAdjustmentRecord = {
- id: `adj-${Date.now()}`,
- adjustmentDate: getTodayString(),
- quantity: 0,
- inspector: getLoggedInUserName() || 'ํ๊ธธ๋',
- };
- setAdjustments((prev) => [...prev, newRecord]);
- };
-
- // ์ฌ๊ณ ์กฐ์ ํ ์ญ์
- const handleRemoveAdjustment = (adjId: string) => {
- setAdjustments((prev) => prev.filter((a) => a.id !== adjId));
- };
-
- // ์ฌ๊ณ ์กฐ์ ์๋ ๋ณ๊ฒฝ
- const handleAdjustmentQtyChange = (adjId: string, value: string) => {
- const numValue = value === '' || value === '-' ? 0 : Number(value);
- setAdjustments((prev) =>
- prev.map((a) => (a.id === adjId ? { ...a, quantity: numValue } : a))
- );
- };
-
// ์ทจ์ ํธ๋ค๋ฌ - ๋ฑ๋ก ๋ชจ๋๋ฉด ๋ชฉ๋ก์ผ๋ก, ์์ ๋ชจ๋๋ฉด ์์ธ๋ก ์ด๋
const handleCancel = () => {
if (isNewMode) {
@@ -515,47 +470,9 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
- {/* ์ฌ๊ณ ์กฐ์ */}
-
-
- ์ฌ๊ณ ์กฐ์
-
-
-
-
-
-
- No
- ์กฐ์ ์ผ์
- ์ฆ๊ฐ ์๋
- ๊ฒ์์
-
-
-
- {adjustments.length > 0 ? (
- adjustments.map((adj, idx) => (
-
- {idx + 1}
- {adj.adjustmentDate}
- {adj.quantity}
- {adj.inspector}
-
- ))
- ) : (
-
-
- ์ฌ๊ณ ์กฐ์ ์ด๋ ฅ์ด ์์ต๋๋ค.
-
-
- )}
-
-
-
-
-
);
- }, [detail, adjustments, inspectionAttachments, existingCertFile]);
+ }, [detail, inspectionAttachments, existingCertFile]);
// ===== ๋ฑ๋ก/์์ ํผ ์ฝํ
์ธ =====
const renderFormContent = useCallback(() => {
@@ -582,15 +499,12 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
/>
- {/* ์์์ฌ๋กํธ - ์์ ๊ฐ๋ฅ */}
+ {/* ์์์ฌ๋กํธ - ์๋์ฑ๋ฒ (readOnly) */}
-
setFormData((prev) => ({ ...prev, lotNo: e.target.value }))}
- placeholder="์์์ฌ๋กํธ๋ฅผ ์
๋ ฅํ์ธ์"
- />
+
+ {formData.lotNo || (isNewMode ? '์ ์ฅ ์ ์๋ ์์ฑ' : '-')}
+
{/* ํ๋ชฉ์ฝ๋ - ๊ฒ์ ๋ชจ๋ฌ ์ ํ */}
@@ -801,88 +715,9 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
- {/* ์ฌ๊ณ ์กฐ์ */}
-
-
- ์ฌ๊ณ ์กฐ์
-
-
-
-
-
-
);
- }, [formData, adjustments, uploadedFile, existingCertFile]);
+ }, [formData, uploadedFile, existingCertFile]);
// ===== ์ปค์คํ
ํค๋ ์ก์
(view/edit ๋ชจ๋) =====
// ์์ ๋ฒํผ์ IntegratedDetailTemplate์ DetailActions์์ ์์ด์ฝ์ผ๋ก ์ ๊ณตํ๋ฏ๋ก ์ค๋ณต ์ ๊ฑฐ
diff --git a/src/components/material/ReceivingManagement/ReceivingList.tsx b/src/components/material/ReceivingManagement/ReceivingList.tsx
index 948f1d57..242254d6 100644
--- a/src/components/material/ReceivingManagement/ReceivingList.tsx
+++ b/src/components/material/ReceivingManagement/ReceivingList.tsx
@@ -37,7 +37,6 @@ import {
type FilterFieldConfig,
} from '@/components/templates/UniversalListPage';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
-import { InventoryAdjustmentDialog } from './InventoryAdjustmentDialog';
import { getReceivings, getReceivingStats } from './actions';
import {
RECEIVING_STATUS_LABELS,
@@ -84,9 +83,6 @@ export function ReceivingList() {
const [stats, setStats] = useState(null);
const [totalItems, setTotalItems] = useState(0);
- // ===== ์ฌ๊ณ ์กฐ์ ํ์
์ํ =====
- const [isAdjustmentOpen, setIsAdjustmentOpen] = useState(false);
-
// ===== ๋ ์ง ๋ฒ์ ์ํ (์ต๊ทผ 30์ผ) =====
const today = new Date();
const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
@@ -290,17 +286,9 @@ export function ReceivingList() {
// ํต๊ณ ์นด๋
stats: statCards,
- // ํค๋ ์ก์
(์ฌ๊ณ ์กฐ์ + ์
๊ณ ๋ฑ๋ก ๋ฒํผ)
+ // ํค๋ ์ก์
(์
๊ณ ๋ฑ๋ก ๋ฒํผ)
headerActions: () => (
-
@@ -127,7 +177,7 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
| ์์ฃผ์ผ |
- {data.scheduledDate} |
+ {orderDetail?.received_at || data.scheduledDate} |
| ์์ฃผ์ฒ |
@@ -135,11 +185,11 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
| ์์ฃผ ๋ด๋น์ |
- {data.registrant || '-'} |
+ {orderDetail?.manager_name || data.registrant || '-'} |
| ๋ด๋น์ ์ฐ๋ฝ์ฒ |
- {data.driverContact || '-'} |
+ {orderDetail?.client_contact || data.driverContact || '-'} |
@@ -156,7 +206,7 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
| ๋ฉ๊ธฐ์์ฒญ์ผ |
- {data.scheduledDate} |
+ {orderDetail?.delivery_date || data.scheduledDate} |
| ์ถ๊ณ ์ผ |
@@ -164,7 +214,7 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
| ์
ํฐ์ถ์๋ |
- {data.productGroups.length}๊ฐ์ |
+ {shutterCount}๊ฐ์ |
@@ -234,404 +284,390 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
);
})()}
- {/* ========== ์์ฌ ๋ฐ ์ฒ ๊ฑฐ ๋ด์ญ ํค๋ ========== */}
-
- ์์ฌ ๋ฐ ์ฒ ๊ฑฐ ๋ด์ญ
-
+
์๋์ ๊ฐ์ด ์ฃผ๋ฌธํ์ค๋ ํ์ง ๋ฐ ๋ฉ๊ธฐ์ผ์ ์ค์ํ์ฌ ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
{/* ========== 1. ์คํฌ๋ฆฐ ========== */}
-
-
1. ์คํฌ๋ฆฐ
-
-
-
-
- | No |
- ํ๋ฅ |
- ๋ถํธ |
- ์คํ์ฌ์ด์ฆ |
- ์ ์์ฌ์ด์ฆ |
- ๊ฐ์ด๋ ๋ ์ผ |
- ์ฌํํธ (์ธ์น) |
- ์ผ์ด์ค (์ธ์น) |
- ๋ชจํฐ |
- ๋ง๊ฐ |
-
-
- | ๊ฐ๋ก |
- ์ธ๋ก |
- ๊ฐ๋ก |
- ์ธ๋ก |
- ๋ธ๋ผ์ผํธ |
- ์ฉ๋Kg |
-
-
-
- {MOCK_SCREEN_ROWS.map((row) => (
-
- | {row.no} |
- {row.type} |
- {row.code} |
- {formatNumber(row.openW)} |
- {formatNumber(row.openH)} |
- {formatNumber(row.madeW)} |
- {formatNumber(row.madeH)} |
- {row.guideRail} |
- {row.shaft} |
- {row.caseInch} |
- {row.bracket} |
- {row.capacity} |
- {row.finish} |
+ {screenProducts.length > 0 && (
+
+
1. ์คํฌ๋ฆฐ
+
+
+
+
+ | No |
+ ํ๋ฅ |
+ ๋ถํธ |
+ ์คํ์ฌ์ด์ฆ |
+ ์ ์์ฌ์ด์ฆ |
+ ๊ฐ์ด๋ ๋ ์ผ |
+ ์ฌํํธ (์ธ์น) |
+ ์ผ์ด์ค (์ธ์น) |
+ ๋ชจํฐ |
+ ๋ง๊ฐ |
- ))}
-
-
+
+ | ๊ฐ๋ก |
+ ์ธ๋ก |
+ ๊ฐ๋ก |
+ ์ธ๋ก |
+ ๋ธ๋ผ์ผํธ |
+ ์ฉ๋Kg |
+
+
+
+ {screenProducts.map((row) => (
+
+ | {row.no} |
+ {row.floor ?? '-'} |
+ {row.symbol ?? '-'} |
+ {row.open_width ? formatNumber(Number(row.open_width)) : '-'} |
+ {row.open_height ? formatNumber(Number(row.open_height)) : '-'} |
+ {row.made_width ? formatNumber(Number(row.made_width)) : '-'} |
+ {row.made_height ? formatNumber(Number(row.made_height)) : '-'} |
+ {row.guide_rail ?? '-'} |
+ {row.shaft ?? '-'} |
+ {row.case_inch ?? '-'} |
+ {row.bracket ?? '-'} |
+ {row.capacity ?? '-'} |
+ {row.finish ?? '-'} |
+
+ ))}
+
+
+
-
+ )}
- {/* ========== 2. ์ ์ฌ ========== */}
-
-
2. ์ ์ฌ
-
-
-
-
- | No. |
- ๋ถํธ |
- ์คํ์ฌ์ด์ฆ |
- ์ ์์ฌ์ด์ฆ |
- ๊ฐ์ด๋ ๋ ์ผ |
- ์ฌํํธ (์ธ์น) |
- ์กฐ์ธํธ๋ฐ (๊ท๊ฒฉ) |
- ์ผ์ด์ค (์ธ์น) |
- ๋ชจํฐ |
- ๋ง๊ฐ |
-
-
- | ๊ฐ๋ก |
- ์ธ๋ก |
- ๊ฐ๋ก |
- ์ธ๋ก |
- ๋ธ๋ผ์ผํธ |
- ์ฉ๋Kg |
-
-
-
- {MOCK_STEEL_ROWS.map((row) => (
-
- | {row.no} |
- {row.code} |
- {formatNumber(row.openW)} |
- {formatNumber(row.openH)} |
- {formatNumber(row.madeW)} |
- {formatNumber(row.madeH)} |
- {row.guideRail} |
- {row.shaft} |
- {row.jointBar} |
- {row.caseInch} |
- {row.bracket} |
- {row.capacity} |
- {row.finish} |
+ {/* ========== 2. ์ฒ ์ฌ ========== */}
+ {steelProducts.length > 0 && (
+
+
2. ์ฒ ์ฌ
+
+
+
+
+ | No. |
+ ๋ถํธ |
+ ์คํ์ฌ์ด์ฆ |
+ ์ ์์ฌ์ด์ฆ |
+ ๊ฐ์ด๋ ๋ ์ผ |
+ ์ฌํํธ (์ธ์น) |
+ ์กฐ์ธํธ๋ฐ (๊ท๊ฒฉ) |
+ ์ผ์ด์ค (์ธ์น) |
+ ๋ชจํฐ |
+ ๋ง๊ฐ |
- ))}
-
-
+
+ | ๊ฐ๋ก |
+ ์ธ๋ก |
+ ๊ฐ๋ก |
+ ์ธ๋ก |
+ ๋ธ๋ผ์ผํธ |
+ ์ฉ๋Kg |
+
+
+
+ {steelProducts.map((row) => (
+
+ | {row.no} |
+ {row.symbol ?? '-'} |
+ {row.open_width ? formatNumber(Number(row.open_width)) : '-'} |
+ {row.open_height ? formatNumber(Number(row.open_height)) : '-'} |
+ {row.made_width ? formatNumber(Number(row.made_width)) : '-'} |
+ {row.made_height ? formatNumber(Number(row.made_height)) : '-'} |
+ {row.guide_rail ?? '-'} |
+ {row.shaft ?? '-'} |
+ {row.joint_bar ?? '-'} |
+ {row.case_inch ?? '-'} |
+ {row.bracket ?? '-'} |
+ {row.capacity ?? '-'} |
+ {row.finish ?? '-'} |
+
+ ))}
+
+
+
-
+ )}
{/* ========== 3. ๋ชจํฐ ========== */}
-
-
3. ๋ชจํฐ
-
-
-
-
- | ํญ๋ชฉ |
- ๊ตฌ๋ถ |
- ๊ท๊ฒฉ |
- ์๋ |
- {showLotColumn && ์
๊ณ LOT | }
- ํญ๋ชฉ |
- ๊ตฌ๋ถ |
- ๊ท๊ฒฉ |
- ์๋ |
- {showLotColumn && ์
๊ณ LOT | }
-
-
-
- {Array.from({ length: motorRows }).map((_, i) => {
- const left = MOCK_MOTOR_LEFT[i];
- const right = MOCK_MOTOR_RIGHT[i];
- return (
-
- | {left?.item || ''} |
- {left?.type || ''} |
- {left?.spec || ''} |
- {left?.qty ?? ''} |
- {showLotColumn && {left?.lot || ''} | }
- {right?.item || ''} |
- {right?.type || ''} |
- {right?.spec || ''} |
- {right?.qty ?? ''} |
- {showLotColumn && {right?.lot || ''} | }
-
- );
- })}
-
-
+ {motorRows > 0 && (
+
+
3. ๋ชจํฐ
+
+
+
+
+ | ํญ๋ชฉ |
+ ๊ตฌ๋ถ |
+ ๊ท๊ฒฉ |
+ ์๋ |
+ {showLotColumn && ์
๊ณ LOT | }
+ ํญ๋ชฉ |
+ ๊ตฌ๋ถ |
+ ๊ท๊ฒฉ |
+ ์๋ |
+ {showLotColumn && ์
๊ณ LOT | }
+
+
+
+ {Array.from({ length: motorRows }).map((_, i) => {
+ const left = motorsLeft[i];
+ const right = motorsRight[i];
+ return (
+
+ | {left?.item || ''} |
+ {left?.type || ''} |
+ {left?.spec || ''} |
+ {left?.qty ?? ''} |
+ {showLotColumn && {left?.lot || ''} | }
+ {right?.item || ''} |
+ {right?.type || ''} |
+ {right?.spec || ''} |
+ {right?.qty ?? ''} |
+ {showLotColumn && {right?.lot || ''} | }
+
+ );
+ })}
+
+
+
-
+ )}
{/* ========== 4. ์ ๊ณก๋ฌผ ========== */}
-
-
4. ์ ๊ณก๋ฌผ
+ {bendingParts.length > 0 && (
+
+
4. ์ ๊ณก๋ฌผ
- {/* 4-1. ๊ฐ์ด๋๋ ์ผ */}
-
-
4-1. ๊ฐ์ด๋๋ ์ผ - EGI 1.5ST + ๋ง๊ฐ์ฌ EGI 1.1ST + ๋ณ๋๋ง๊ฐ์ฌ SUS 1.1ST
-
- {/* ๋ฉ์ธ ํ
์ด๋ธ */}
-
-
-
-
- | ๋ฐฑ๋ฉดํ (120X70) |
- ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ์๋ |
-
-
-
- {MOCK_GUIDE_RAIL_ITEMS.map((item, i) => (
-
- {i === 0 && (
- |
- IMG
- |
- )}
- {item.name} |
- {item.spec} |
- {item.qty} |
-
- ))}
-
-
-
-
- {/* ์ฐ๊ธฐ์ฐจ๋จ์ฌ */}
-
-
-
-
- | |
- ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ์๋ |
-
-
-
-
- |
- IMG
- |
- {MOCK_GUIDE_SMOKE.name} |
- {MOCK_GUIDE_SMOKE.spec} |
- {MOCK_GUIDE_SMOKE.qty} |
-
-
-
-
-
-
- * ๊ฐ์ด๋๋ ์ผ ๋ง๊ฐ์ฌ ์์ธก์ ์ค์น - EGI 0.8T + ํ์ด๋ฒ๊ธ๋ผ์ค์ฝํ
์ง๋ฌผ
-
-
-
- {/* 4-2. ์ผ์ด์ค(์
ํฐ๋ฐ์ค) */}
-
-
4-2. ์ผ์ด์ค(์
ํฐ๋ฐ์ค) - EGI 1.5ST
-
- {/* ๋ฉ์ธ ํ
์ด๋ธ */}
-
-
-
-
- | |
- ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ์๋ |
-
-
-
- {MOCK_CASE_ITEMS.map((item, i) => (
-
- {i === 0 && (
- |
- IMG
- |
- )}
- {item.name} |
- {item.spec} |
- {item.qty} |
-
- ))}
-
-
-
-
- {/* ์ฐ๊ธฐ์ฐจ๋จ์ฌ */}
-
-
-
-
- | |
- ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ์๋ |
-
-
-
-
- |
- IMG
- |
- {MOCK_CASE_SMOKE.name} |
- {MOCK_CASE_SMOKE.spec} |
- {MOCK_CASE_SMOKE.qty} |
-
-
-
-
-
-
- * ์ ๋ฉด๋ถ, ํ๋ฌ๋ถ ์์ธก์ ์ค์น - EGI 0.8T + ํ์ด๋ฒ๊ธ๋ผ์ค์ฝํ
์ง๋ฌผ
-
-
-
- {/* 4-3. ํ๋จ๋ง๊ฐ์ฌ (ํ ๊ธ: ์คํฌ๋ฆฐ / ์ ์ฌ) */}
-
- {/* ํ ๊ธ ๋ฒํผ */}
-
- setBottomFinishView('screen')}
- className={`px-3 py-1 text-[10px] font-bold border rounded ${
- bottomFinishView === 'screen'
- ? 'bg-gray-800 text-white border-gray-800'
- : 'bg-white text-gray-600 border-gray-400 hover:bg-gray-100'
- }`}
- >
- # ์คํฌ๋ฆฐ์ ๊ฒฝ์ฐ
-
- setBottomFinishView('steel')}
- className={`px-3 py-1 text-[10px] font-bold border rounded ${
- bottomFinishView === 'steel'
- ? 'bg-gray-800 text-white border-gray-800'
- : 'bg-white text-gray-600 border-gray-400 hover:bg-gray-100'
- }`}
- >
- # ์ ์ฌ์ ๊ฒฝ์ฐ
-
-
-
- {bottomFinishView === 'screen' ? (
- <>
-
- 4-3. ํ๋จ๋ง๊ฐ์ฌ - ํ๋จ๋ง๊ฐ์ฌ(EGI 1.5ST) + ํ๋จ๋ณด๊ฐ์๋น(EGI 1.5ST) + ํ๋จ ๋ณด๊ฐํ์ฒ (EGI 1.1ST) + ํ๋จ ๋ฌด๊ฒํ์ฒ (50X12T)
-
+ {/* 4-1. ๊ฐ์ด๋๋ ์ผ */}
+ {guideRailItems.length > 0 && (
+
+
4-1. ๊ฐ์ด๋๋ ์ผ
- | ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ๊ธธ์ด |
- ์๋ |
- ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ๊ธธ์ด |
- ์๋ |
+ |
+ ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
- {MOCK_BOTTOM_SCREEN.map((row, i) => (
+ {guideRailItems.map((item, i) => (
- | {row.name} |
- {row.spec} |
- {row.l1} |
- {row.q1} |
- {row.name2} |
- {row.spec2} |
- {row.l2} |
- {row.q2} |
+ {i === 0 && (
+
+ IMG
+ |
+ )}
+ {item.name} |
+ {item.spec} |
+ {item.qty} |
))}
- >
- ) : (
- <>
-
- 4-3. ํ๋จ๋ง๊ฐ์ฌ -EGI 1.5ST
+
+ {/* ๊ฐ์ด๋๋ ์ผ ์ฐ๊ธฐ์ฐจ๋จ์ฌ */}
+ {guideSmokeItems.length > 0 && (
+
+
+
+
+ | |
+ ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
+
+
+
+ {guideSmokeItems.map((item, i) => (
+
+ {i === 0 && (
+ |
+ IMG
+ |
+ )}
+ {item.name} |
+ {item.spec} |
+ {item.qty} |
+
+ ))}
+
+
+
+ )}
+
+
+ * ๊ฐ์ด๋๋ ์ผ ๋ง๊ฐ์ฌ ์์ธก์ ์ค์น - EGI 0.8T + ํ์ด๋ฒ๊ธ๋ผ์ค์ฝํ
์ง๋ฌผ
+
+ )}
+
+ {/* 4-2. ์ผ์ด์ค(์
ํฐ๋ฐ์ค) */}
+ {caseItems.length > 0 && (
+
+
4-2. ์ผ์ด์ค(์
ํฐ๋ฐ์ค)
- | ํ๋จ๋ง๊ฐ์ฌ |
- ๊ท๊ฒฉ |
- ๊ธธ์ด |
- ์๋ |
+ |
+ ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
-
- |
- IMG
- |
- {MOCK_BOTTOM_STEEL.spec} |
- {MOCK_BOTTOM_STEEL.length} |
- {MOCK_BOTTOM_STEEL.qty} |
-
+ {caseItems.map((item, i) => (
+
+ {i === 0 && (
+ |
+ IMG
+ |
+ )}
+ {item.name} |
+ {item.spec} |
+ {item.qty} |
+
+ ))}
- >
+
+ {/* ์ผ์ด์ค ์ฐ๊ธฐ์ฐจ๋จ์ฌ */}
+ {caseSmokeItems.length > 0 && (
+
+
+
+
+ | |
+ ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
+
+
+
+ {caseSmokeItems.map((item, i) => (
+
+ {i === 0 && (
+ |
+ IMG
+ |
+ )}
+ {item.name} |
+ {item.spec} |
+ {item.qty} |
+
+ ))}
+
+
+
+ )}
+
+
+ * ์ ๋ฉด๋ถ, ํ๋ฌ๋ถ ์์ธก์ ์ค์น - EGI 0.8T + ํ์ด๋ฒ๊ธ๋ผ์ค์ฝํ
์ง๋ฌผ
+
+
+ )}
+
+ {/* 4-3. ํ๋จ๋ง๊ฐ์ฌ */}
+ {bottomItems.length > 0 && (
+
+
4-3. ํ๋จ๋ง๊ฐ์ฌ
+
+
+
+
+ | |
+ ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
+
+
+
+ {bottomItems.map((item, i) => (
+
+ {i === 0 && (
+ |
+ IMG
+ |
+ )}
+ {item.name} |
+ {item.spec} |
+ {item.qty} |
+
+ ))}
+
+
+
+
+ )}
+
+ {/* ์ฐ๊ธฐ์ฐจ๋จ์ฌ (๊ตฌ๋ถ ๋ถ๊ฐ) */}
+ {otherSmokeItems.length > 0 && (
+
+
์ฐ๊ธฐ์ฐจ๋จ์ฌ
+
+
+
+
+ | ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
+
+
+
+ {otherSmokeItems.map((item, i) => (
+
+ | {item.name} |
+ {item.spec} |
+ {item.qty} |
+
+ ))}
+
+
+
+
)}
-
+ )}
{/* ========== 5. ๋ถ์์ฌ ========== */}
-
-
5. ๋ถ์์ฌ
-
-
-
-
- | ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ์๋ |
- ํญ๋ชฉ |
- ๊ท๊ฒฉ |
- ์๋ |
-
-
-
- {MOCK_SUBSIDIARY.map((row, i) => (
-
- | {row.leftItem} |
- {row.leftSpec} |
- {row.leftQty} |
- {row.rightItem} |
- {row.rightSpec} |
- {row.rightQty} |
+ {subsidiaryParts.length > 0 && (
+
+
5. ๋ถ์์ฌ
+
+
+
+
+ | ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
+ ํญ๋ชฉ |
+ ๊ท๊ฒฉ |
+ ์๋ |
- ))}
-
-
+
+
+ {subsidiaryRows.map((row, i) => (
+
+ | {row.left?.name ?? ''} |
+ {row.left?.spec ?? ''} |
+ {row.left?.qty ?? ''} |
+ {row.right?.name ?? ''} |
+ {row.right?.spec ?? ''} |
+ {row.right?.qty ?? ''} |
+
+ ))}
+
+
+
-
+ )}
{/* ========== ํน์ด์ฌํญ ========== */}
{data.remarks && (
@@ -644,4 +680,4 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, s
)}
);
-}
+}
\ No newline at end of file
diff --git a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx
index f76d7e80..1cf0ba74 100644
--- a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx
+++ b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx
@@ -6,12 +6,13 @@
*/
import type { ShipmentDetail } from '../types';
-import { ShipmentOrderDocument } from './ShipmentOrderDocument';
+import { ShipmentOrderDocument, type OrderDocumentDetail } from './ShipmentOrderDocument';
interface ShippingSlipProps {
data: ShipmentDetail;
+ orderDetail?: OrderDocumentDetail | null;
}
-export function ShippingSlip({ data }: ShippingSlipProps) {
- return