From ab607dfa9a1d42eec3d60c2412a83bc35ccf7548 Mon Sep 17 00:00:00 2001 From: hyungi Date: Fri, 17 Oct 2025 14:44:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=86=B5=ED=95=A9=20BOM=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐ŸŽฏ ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: - ํ†ตํ•ฉ BOM ํŽ˜์ด์ง€ (UnifiedBOMPage) ์‹ ๊ทœ ๊ฐœ๋ฐœ - ํƒญ ๊ตฌ์กฐ๋กœ ์—…๋กœ๋“œ โ†’ ํŒŒ์ผ ๊ด€๋ฆฌ โ†’ ์ž์žฌ ๊ด€๋ฆฌ ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐœ์„  - ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ๋กœ ์ŠคํŒŒ๊ฒŒํ‹ฐ ์ฝ”๋“œ ๋ฐฉ์ง€ ๐Ÿ“ค ์—…๋กœ๋“œ ํƒญ (BOMUploadTab): - ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŒŒ์ผ ์—…๋กœ๋“œ - ํŒŒ์ผ ๊ฒ€์ฆ ๋ฐ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ - ์—…๋กœ๋“œ ์™„๋ฃŒ ํ›„ ์ž๋™ Files ํƒญ ์ด๋™ ๐Ÿ“Š ํŒŒ์ผ ๊ด€๋ฆฌ ํƒญ (BOMFilesTab): - ํ”„๋กœ์ ํŠธ๋ณ„ BOM ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ - ๋ฆฌ๋น„์ „ ํžˆ์Šคํ† ๋ฆฌ ํ‘œ์‹œ - BOM ์„ ํƒ ํ›„ ์ž๋™ Materials ํƒญ ์ด๋™ ๐Ÿ“‹ ์ž์žฌ ๊ด€๋ฆฌ ํƒญ (BOMMaterialsTab): - ๊ธฐ์กด BOMManagementPage ๋ž˜ํ•‘ - ์„ ํƒ๋œ BOM์˜ ์ž์žฌ ๋ถ„๋ฅ˜ ๋ฐ ๊ด€๋ฆฌ ๐Ÿ”ง ๋ฐฑ์—”๋“œ API ๊ฐœ์„ : - /files/project/{project_code} ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ - ํ•œ๊ธ€ ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ URL ์ธ์ฝ”๋”ฉ ์ง€์› - ํ”„๋กœ์ ํŠธ๋ณ„ ํŒŒ์ผ ์กฐํšŒ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๐ŸŽจ ๋Œ€์‹œ๋ณด๋“œ ๊ฐœ์„ : - 3๊ฐœ BOM ์นด๋“œ๋ฅผ 1๊ฐœ ํ†ตํ•ฉ ์นด๋“œ๋กœ ๋ณ€๊ฒฝ - ๊ธฐ๋Šฅ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํƒœ๊ทธ ์ถ”๊ฐ€ - ๋” ์ง๊ด€์ ์ธ ๋„ค๋น„๊ฒŒ์ด์…˜ ๐Ÿ“ ์ฝ”๋“œ ๊ตฌ์กฐ ๊ฐœ์„ : - ๊ธฐ์กด ํŽ˜์ด์ง€๋“ค์„ _deprecated ํด๋”๋กœ ์ด๋™ - ํƒญ๋ณ„ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ (components/bom/tabs/) - PAGES_GUIDE.md ์—…๋ฐ์ดํŠธ โœจ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ : - ์ž์—ฐ์Šค๋Ÿฌ์šด ์›Œํฌํ”Œ๋กœ์šฐ (์—…๋กœ๋“œ โ†’ ์„ ํƒ โ†’ ๊ด€๋ฆฌ) - ํƒญ ๊ฐ„ ์ƒํƒœ ๊ณต์œ  ๋ฐ ์ž๋™ ๋„ค๋น„๊ฒŒ์ด์…˜ - ํ†ตํ•ฉ๋œ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ๋ชจ๋“  BOM ์ž‘์—… ์ฒ˜๋ฆฌ --- backend/app/routers/files.py | 39 ++ frontend/PAGES_GUIDE.md | 65 +- frontend/src/App.jsx | 10 +- .../src/components/bom/tabs/BOMFilesTab.jsx | 429 +++++++++++++ .../components/bom/tabs/BOMMaterialsTab.jsx | 105 +++ .../src/components/bom/tabs/BOMUploadTab.jsx | 496 +++++++++++++++ frontend/src/pages/BOMManagementPage.jsx | 23 +- frontend/src/pages/DashboardPage.jsx | 45 +- frontend/src/pages/UnifiedBOMPage.jsx | 266 ++++++++ .../src/pages/_deprecated/BOMRevisionPage.jsx | 350 ++++++++++ .../src/pages/_deprecated/BOMUploadPage.jsx | 600 ++++++++++++++++++ .../{ => _deprecated}/BOMWorkspacePage.jsx | 0 12 files changed, 2405 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/bom/tabs/BOMFilesTab.jsx create mode 100644 frontend/src/components/bom/tabs/BOMMaterialsTab.jsx create mode 100644 frontend/src/components/bom/tabs/BOMUploadTab.jsx create mode 100644 frontend/src/pages/UnifiedBOMPage.jsx create mode 100644 frontend/src/pages/_deprecated/BOMRevisionPage.jsx create mode 100644 frontend/src/pages/_deprecated/BOMUploadPage.jsx rename frontend/src/pages/{ => _deprecated}/BOMWorkspacePage.jsx (100%) diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index d0682e5..d21a92b 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -1779,6 +1779,45 @@ async def get_files( except Exception as e: raise HTTPException(status_code=500, detail=f"ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: {str(e)}") +@router.get("/project/{project_code}") +async def get_files_by_project( + project_code: str, + db: Session = Depends(get_db) +): + """ํ”„๋กœ์ ํŠธ๋ณ„ ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ""" + try: + query = """ + SELECT id, filename, original_filename, bom_name, job_no, revision, + description, file_size, parsed_count, upload_date, is_active + FROM files + WHERE is_active = TRUE AND job_no = :job_no + ORDER BY upload_date DESC + """ + + result = db.execute(text(query), {"job_no": project_code}) + files = result.fetchall() + + return [ + { + "id": file.id, + "filename": file.filename, + "original_filename": file.original_filename, + "bom_name": file.bom_name, + "job_no": file.job_no, + "revision": file.revision, + "description": file.description, + "file_size": file.file_size, + "parsed_count": file.parsed_count, + "upload_date": file.upload_date, + "created_at": file.upload_date, + "is_active": file.is_active + } + for file in files + ] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"ํ”„๋กœ์ ํŠธ ํŒŒ์ผ ์กฐํšŒ ์‹คํŒจ: {str(e)}") + @router.get("/stats") async def get_files_stats(db: Session = Depends(get_db)): """ํŒŒ์ผ ๋ฐ ์ž์žฌ ํ†ต๊ณ„ ์กฐํšŒ""" diff --git a/frontend/PAGES_GUIDE.md b/frontend/PAGES_GUIDE.md index d292939..28032db 100644 --- a/frontend/PAGES_GUIDE.md +++ b/frontend/PAGES_GUIDE.md @@ -33,12 +33,14 @@ - **์—ญํ• **: ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ ํŽ˜์ด์ง€ - **๊ธฐ๋Šฅ**: - ํ”„๋กœ์ ํŠธ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด - - ํ”„๋กœ์ ํŠธ๋ณ„ ๊ธฐ๋Šฅ ์นด๋“œ (BOM ๊ด€๋ฆฌ, ๊ตฌ๋งค์‹ ์ฒญ ๊ด€๋ฆฌ) + - **์ƒˆ๋กœ์šด 3๊ฐœ BOM ์นด๋“œ**: ๐Ÿ“ค BOM Upload, ๐Ÿ“Š Revision Management, ๐Ÿ“‹ BOM Management + - ๊ตฌ๋งค์‹ ์ฒญ ๊ด€๋ฆฌ ์นด๋“œ - ๊ด€๋ฆฌ์ž ์ „์šฉ ๊ธฐ๋Šฅ (์‚ฌ์šฉ์ž ๊ด€๋ฆฌ, ๋กœ๊ทธ ๊ด€๋ฆฌ) - ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ/ํŽธ์ง‘/์‚ญ์ œ/๋น„ํ™œ์„ฑํ™” - **๋ผ์šฐํŒ…**: `/dashboard` - **์ ‘๊ทผ ๊ถŒํ•œ**: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž - **๋””์ž์ธ**: ๋ฐ๋ณธ์”ฝํฌ ์Šคํƒ€์ผ, ๊ธ€๋ž˜์Šค๋ชจํ”ผ์ฆ˜ ํšจ๊ณผ +- **์—…๋ฐ์ดํŠธ**: BOM ๊ธฐ๋Šฅ์„ 3๊ฐœ ์ „์šฉ ํŽ˜์ด์ง€๋กœ ๋ถ„๋ฆฌ ### `MainPage.jsx` - **์—ญํ• **: ์ดˆ๊ธฐ ๋žœ๋”ฉ ํŽ˜์ด์ง€ @@ -88,17 +90,40 @@ ## BOM ๊ด€๋ฆฌ ํŽ˜์ด์ง€ -### `BOMManagementPage.jsx` -- **์—ญํ• **: BOM(Bill of Materials) ํ†ตํ•ฉ ๊ด€๋ฆฌ ํŽ˜์ด์ง€ +### `BOMUploadPage.jsx` โญ ์‹ ๊ทœ +- **์—ญํ• **: BOM ํŒŒ์ผ ์—…๋กœ๋“œ ์ „์šฉ ํŽ˜์ด์ง€ - **๊ธฐ๋Šฅ**: - - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ž์žฌ ์กฐํšŒ (PIPE, FITTING, FLANGE, VALVE, GASKET, BOLT, SUPPORT, SPECIAL) + - ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŒŒ์ผ ์—…๋กœ๋“œ + - ํŒŒ์ผ ๊ฒ€์ฆ (ํ˜•์‹: .xlsx, .xls, .csv / ์ตœ๋Œ€ 50MB) + - ์‹ค์‹œ๊ฐ„ ์—…๋กœ๋“œ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ + - ์ž๋™ BOM ์ด๋ฆ„ ์„ค์ • + - ์—…๋กœ๋“œ ์™„๋ฃŒ ํ›„ BOM ๊ด€๋ฆฌ ํŽ˜์ด์ง€๋กœ ์ž๋™ ์ด๋™ +- **๋ผ์šฐํŒ…**: `/bom-upload` +- **์ ‘๊ทผ ๊ถŒํ•œ**: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž +- **๋””์ž์ธ**: ๋ชจ๋˜ UI, ๊ธ€๋ž˜์Šค๋ชจํ”ผ์ฆ˜ ํšจ๊ณผ + +### `BOMRevisionPage.jsx` โญ ์‹ ๊ทœ +- **์—ญํ• **: BOM ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ ์ „์šฉ ํŽ˜์ด์ง€ +- **ํ˜„์žฌ ์ƒํƒœ**: ๊ธฐ๋ณธ ๊ตฌ์กฐ ์™„์„ฑ, ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์˜ˆ์ • +- **๊ธฐ๋Šฅ**: + - BOM ํŒŒ์ผ ๋ชฉ๋ก ํ‘œ์‹œ + - ๋ฆฌ๋น„์ „ ํžˆ์Šคํ† ๋ฆฌ ๊ฐœ์š” + - ๊ฐœ๋ฐœ ์˜ˆ์ • ๊ธฐ๋Šฅ ์•ˆ๋‚ด (ํƒ€์ž„๋ผ์ธ, ๋น„๊ต, ๋กค๋ฐฑ) +- **๋ผ์šฐํŒ…**: `/bom-revision` +- **์ ‘๊ทผ ๊ถŒํ•œ**: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž +- **ํ–ฅํ›„ ๊ณ„ํš**: ๐Ÿ“Š ๋ฆฌ๋น„์ „ ํƒ€์ž„๋ผ์ธ, ๐Ÿ” ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋น„๊ต, โช ๋กค๋ฐฑ ์‹œ์Šคํ…œ + +### `BOMManagementPage.jsx` +- **์—ญํ• **: BOM(Bill of Materials) ์ž์žฌ ๊ด€๋ฆฌ ํŽ˜์ด์ง€ +- **๊ธฐ๋Šฅ**: + - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ž์žฌ ์กฐํšŒ (PIPE, FITTING, FLANGE, VALVE, GASKET, BOLT, SUPPORT, SPECIAL, UNCLASSIFIED) - ์ž์žฌ ์„ ํƒ ๋ฐ ๊ตฌ๋งค์‹ ์ฒญ (์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ) - ๊ตฌ๋งค์‹ ์ฒญ๋œ ์ž์žฌ ๋น„ํ™œ์„ฑํ™” ํ‘œ์‹œ - - ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ ๋ฐ ์ €์žฅ - - ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ + - ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ ๋ฐ ์ €์žฅ (Brand, Additional Request) + - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ „์šฉ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ - **๋ผ์šฐํŒ…**: `/bom-management` - **์ ‘๊ทผ ๊ถŒํ•œ**: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž -- **ํŠน์ง•**: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌ๋œ ๊ตฌ์กฐ +- **ํŠน์ง•**: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ปดํฌ๋„ŒํŠธ๋กœ ์™„์ „ ๋ถ„๋ฆฌ๋œ ๊ตฌ์กฐ ### `NewMaterialsPage.jsx` (๋ ˆ๊ฑฐ์‹œ) - **์—ญํ• **: ๊ธฐ์กด ์ž์žฌ ๊ด€๋ฆฌ ํŽ˜์ด์ง€ (ํ˜„์žฌ ๋ฐฑ์—…์šฉ) @@ -113,13 +138,10 @@ - **๋ผ์šฐํŒ…**: `/bom-status` - **์ ‘๊ทผ ๊ถŒํ•œ**: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž -### `BOMWorkspacePage.jsx` -- **์—ญํ• **: BOM ์ž‘์—… ๊ณต๊ฐ„ -- **๊ธฐ๋Šฅ**: - - BOM ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ - - ์ž์žฌ ๋ถ„๋ฅ˜ ์ž‘์—… -- **๋ผ์šฐํŒ…**: `/bom-workspace` -- **์ ‘๊ทผ ๊ถŒํ•œ**: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž +### `_deprecated/BOMWorkspacePage.jsx` (์‚ฌ์šฉ ์ค‘๋‹จ) +- **์—ญํ• **: ๊ธฐ์กด BOM ์ž‘์—… ๊ณต๊ฐ„ (์‚ฌ์šฉ ์ค‘๋‹จ) +- **์ƒํƒœ**: `BOMUploadPage`์™€ `BOMRevisionPage`๋กœ ๋ถ„๋ฆฌ๋จ +- **์ด์œ **: ์—…๋กœ๋“œ์™€ ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์„ ๋ณ„๋„ ํŽ˜์ด์ง€๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉ์„ฑ ๊ฐœ์„  --- @@ -303,6 +325,21 @@ ## ์ตœ๊ทผ ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ +### 2024-10-17: BOM ํŽ˜์ด์ง€ ๊ตฌ์กฐ ๊ฐœํŽธ โญ ์ฃผ์š” ์—…๋ฐ์ดํŠธ +- **์ƒˆ๋กœ์šด ํŽ˜์ด์ง€ ์ถ”๊ฐ€**: + - `BOMUploadPage.jsx`: ์ „์šฉ ์—…๋กœ๋“œ ํŽ˜์ด์ง€ (๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ, ํŒŒ์ผ ๊ฒ€์ฆ) + - `BOMRevisionPage.jsx`: ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ ํŽ˜์ด์ง€ (๊ธฐ๋ณธ ๊ตฌ์กฐ, ํ–ฅํ›„ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์˜ˆ์ •) +- **๊ธฐ์กด ํŽ˜์ด์ง€ ์ •๋ฆฌ**: + - `BOMWorkspacePage.jsx` โ†’ `_deprecated/` ํด๋”๋กœ ์ด๋™ (์‚ฌ์šฉ ์ค‘๋‹จ) + - ์—…๋กœ๋“œ์™€ ๋ฆฌ๋น„์ „ ๊ธฐ๋Šฅ์„ ๋ณ„๋„ ํŽ˜์ด์ง€๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉ์„ฑ ๊ฐœ์„  +- **๋Œ€์‹œ๋ณด๋“œ ๊ฐœํŽธ**: + - BOM ๊ด€๋ฆฌ๋ฅผ 3๊ฐœ ์นด๋“œ๋กœ ๋ถ„๋ฆฌ: ๐Ÿ“ค Upload, ๐Ÿ“Š Revision, ๐Ÿ“‹ Management + - ๊ฐ ๊ธฐ๋Šฅ๋ณ„ ์ „์šฉ ํŽ˜์ด์ง€๋กœ ๋ช…ํ™•ํ•œ ์—ญํ•  ๋ถ„๋‹ด +- **๋ผ์šฐํŒ… ์—…๋ฐ์ดํŠธ**: + - `/bom-upload`: ์ƒˆ ํŒŒ์ผ ์—…๋กœ๋“œ + - `/bom-revision`: ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ + - `/bom-management`: ์ž์žฌ ๊ด€๋ฆฌ + ### 2024-10-17: SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ถ”๊ฐ€ - `SpecialMaterialsView.jsx` ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ - ํŠน์ˆ˜ ์ œ์ž‘ ์ž์žฌ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a99bf71..361bb6a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'; import SimpleLogin from './SimpleLogin'; import DashboardPage from './pages/DashboardPage'; import { UserMenu, ErrorBoundary } from './components/common'; -import BOMWorkspacePage from './pages/BOMWorkspacePage'; import NewMaterialsPage from './pages/NewMaterialsPage'; import BOMManagementPage from './pages/BOMManagementPage'; +import UnifiedBOMPage from './pages/UnifiedBOMPage'; import SystemSettingsPage from './pages/SystemSettingsPage'; import AccountSettingsPage from './pages/AccountSettingsPage'; import UserManagementPage from './pages/UserManagementPage'; @@ -240,12 +240,12 @@ function App() { /> ); - case 'bom': + case 'unified-bom': return ( - navigateToPage('dashboard')} + selectedProject={pageParams.selectedProject} + user={user} /> ); case 'materials': diff --git a/frontend/src/components/bom/tabs/BOMFilesTab.jsx b/frontend/src/components/bom/tabs/BOMFilesTab.jsx new file mode 100644 index 0000000..9ed7742 --- /dev/null +++ b/frontend/src/components/bom/tabs/BOMFilesTab.jsx @@ -0,0 +1,429 @@ +import React, { useState, useEffect } from 'react'; +import api from '../../../api'; + +const BOMFilesTab = ({ + selectedProject, + user, + bomFiles, + setBomFiles, + selectedBOM, + onBOMSelect, + refreshTrigger +}) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [groupedFiles, setGroupedFiles] = useState({}); + + // BOM ํŒŒ์ผ ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + const loadBOMFiles = async () => { + if (!selectedProject) return; + + try { + setLoading(true); + setError(''); + + const projectCode = selectedProject.official_project_code || selectedProject.job_no; + const encodedProjectCode = encodeURIComponent(projectCode); + const response = await api.get(`/files/project/${encodedProjectCode}`); + const files = response.data || []; + + setBomFiles(files); + + // BOM ์ด๋ฆ„๋ณ„๋กœ ๊ทธ๋ฃนํ™” + const groups = groupFilesByBOM(files); + setGroupedFiles(groups); + + } catch (err) { + console.error('BOM ํŒŒ์ผ ๋กœ๋“œ ์‹คํŒจ:', err); + setError('BOM ํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setLoading(false); + } + }; + + loadBOMFiles(); + }, [selectedProject, refreshTrigger, setBomFiles]); + + // ํŒŒ์ผ์„ BOM ์ด๋ฆ„๋ณ„๋กœ ๊ทธ๋ฃนํ™” + const groupFilesByBOM = (fileList) => { + const groups = {}; + + fileList.forEach(file => { + const bomName = file.bom_name || file.original_filename; + if (!groups[bomName]) { + groups[bomName] = []; + } + groups[bomName].push(file); + }); + + // ๊ฐ ๊ทธ๋ฃน ๋‚ด์—์„œ ๋ฆฌ๋น„์ „ ๋ฒˆํ˜ธ๋กœ ์ •๋ ฌ + Object.keys(groups).forEach(bomName => { + groups[bomName].sort((a, b) => { + const revA = parseInt(a.revision?.replace('Rev.', '') || '0'); + const revB = parseInt(b.revision?.replace('Rev.', '') || '0'); + return revB - revA; // ์ตœ์‹  ๋ฆฌ๋น„์ „์ด ์œ„๋กœ + }); + }); + + return groups; + }; + + // BOM ์„ ํƒ ์ฒ˜๋ฆฌ + const handleBOMClick = (bomFile) => { + if (onBOMSelect) { + onBOMSelect(bomFile); + } + }; + + // ํŒŒ์ผ ์‚ญ์ œ + const handleDeleteFile = async (fileId, bomName) => { + if (!window.confirm(`์ด ํŒŒ์ผ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)) { + return; + } + + try { + await api.delete(`/files/delete/${fileId}`); + + // ํŒŒ์ผ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ + const projectCode = selectedProject.official_project_code || selectedProject.job_no; + const encodedProjectCode = encodeURIComponent(projectCode); + const response = await api.get(`/files/project/${encodedProjectCode}`); + const files = response.data || []; + setBomFiles(files); + setGroupedFiles(groupFilesByBOM(files)); + + } catch (err) { + console.error('ํŒŒ์ผ ์‚ญ์ œ ์‹คํŒจ:', err); + setError('ํŒŒ์ผ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + }; + + // ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ (ํ–ฅํ›„ ๊ตฌํ˜„) + const handleRevisionUpload = (parentFile) => { + // TODO: ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ + alert('๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์€ ํ–ฅํ›„ ๊ตฌํ˜„ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.'); + }; + + // ๋‚ ์งœ ํฌ๋งทํŒ… + const formatDate = (dateString) => { + if (!dateString) return 'N/A'; + try { + return new Date(dateString).toLocaleDateString('ko-KR'); + } catch { + return dateString; + } + }; + + if (loading) { + return ( +
+
โณ
+
Loading BOM files...
+
+ ); + } + + if (error) { + return ( +
+
+
โš ๏ธ
+ {error} +
+
+ ); + } + + if (bomFiles.length === 0) { + return ( +
+
๐Ÿ“„
+

+ No BOM Files Found +

+

+ Upload your first BOM file using the Upload tab +

+
+ ); + } + + return ( +
+
+
+

+ BOM Files & Revisions +

+

+ Select a BOM file to manage its materials +

+
+ +
+ {Object.keys(groupedFiles).length} BOM Groups โ€ข {bomFiles.length} Total Files +
+
+ + {/* BOM ํŒŒ์ผ ๊ทธ๋ฃน ๋ชฉ๋ก */} +
+ {Object.entries(groupedFiles).map(([bomName, files]) => { + const latestFile = files[0]; // ์ตœ์‹  ๋ฆฌ๋น„์ „ + const isSelected = selectedBOM?.id === latestFile.id; + + return ( +
handleBOMClick(latestFile)} + > +
+
+

+ ๐Ÿ“‹ + {bomName} + {isSelected && ( + + SELECTED + + )} +

+ +
+
+ Latest: {latestFile.revision || 'Rev.0'} +
+
+ Revisions: {files.length} +
+
+ Updated: {formatDate(latestFile.upload_date)} +
+
+ Size: {latestFile.file_size ? `${Math.round(latestFile.file_size / 1024)} KB` : 'N/A'} +
+
+
+ +
+ + + +
+
+ + {/* ๋ฆฌ๋น„์ „ ํžˆ์Šคํ† ๋ฆฌ */} + {files.length > 1 && ( +
+

+ Revision History +

+
+ {files.map((file, index) => ( +
+ {file.revision || 'Rev.0'} + {index === 0 && ' (Latest)'} +
+ ))} +
+
+ )} + + {/* ์„ ํƒ ์•ˆ๋‚ด */} + {!isSelected && ( +
+ Click to select this BOM for material management +
+ )} +
+ ); + })} +
+ + {/* ํ–ฅํ›„ ๊ธฐ๋Šฅ ์•ˆ๋‚ด */} +
+

+ ๐Ÿšง Coming Soon: Advanced Revision Features +

+
+
+
๐Ÿ“Š
+
+ Visual Timeline +
+
+ Interactive revision history +
+
+
+
๐Ÿ”
+
+ Diff Comparison +
+
+ Compare changes between revisions +
+
+
+
โช
+
+ Rollback System +
+
+ Restore previous versions +
+
+
+
+
+ ); +}; + +export default BOMFilesTab; diff --git a/frontend/src/components/bom/tabs/BOMMaterialsTab.jsx b/frontend/src/components/bom/tabs/BOMMaterialsTab.jsx new file mode 100644 index 0000000..25da6e4 --- /dev/null +++ b/frontend/src/components/bom/tabs/BOMMaterialsTab.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import BOMManagementPage from '../../../pages/BOMManagementPage'; + +const BOMMaterialsTab = ({ + selectedProject, + user, + selectedBOM, + onNavigate +}) => { + // BOMManagementPage์— ํ•„์š”ํ•œ props ๊ตฌ์„ฑ + const bomManagementProps = { + onNavigate, + user, + selectedProject, + fileId: selectedBOM?.id, + jobNo: selectedBOM?.job_no || selectedProject?.official_project_code || selectedProject?.job_no, + bomName: selectedBOM?.bom_name || selectedBOM?.original_filename, + revision: selectedBOM?.revision || 'Rev.0', + filename: selectedBOM?.original_filename + }; + + return ( +
+ {/* ํ—ค๋” ์ •๋ณด */} +
+
+
+

+ Material Management +

+

+ BOM: {selectedBOM?.bom_name || selectedBOM?.original_filename} โ€ข {selectedBOM?.revision || 'Rev.0'} +

+
+ +
+
+
+ {selectedBOM?.id || 'N/A'} +
+
+ File ID +
+
+ +
+
+ {selectedBOM?.revision || 'Rev.0'} +
+
+ Revision +
+
+
+
+
+ + {/* BOM ๊ด€๋ฆฌ ํŽ˜์ด์ง€ ์ž„๋ฒ ๋“œ */} +
div': { + padding: '0 !important', + background: 'transparent !important', + minHeight: 'auto !important' + } + }}> + +
+
+ ); +}; + +export default BOMMaterialsTab; diff --git a/frontend/src/components/bom/tabs/BOMUploadTab.jsx b/frontend/src/components/bom/tabs/BOMUploadTab.jsx new file mode 100644 index 0000000..807b59c --- /dev/null +++ b/frontend/src/components/bom/tabs/BOMUploadTab.jsx @@ -0,0 +1,496 @@ +import React, { useState, useRef, useCallback } from 'react'; +import api from '../../../api'; + +const BOMUploadTab = ({ + selectedProject, + user, + onUploadSuccess, + onNavigate +}) => { + const [dragOver, setDragOver] = useState(false); + const [uploading, setUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [selectedFiles, setSelectedFiles] = useState([]); + const [bomName, setBomName] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const fileInputRef = useRef(null); + + // ํŒŒ์ผ ๊ฒ€์ฆ + const validateFile = (file) => { + const allowedTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv' + ]; + + const maxSize = 50 * 1024 * 1024; // 50MB + + if (!allowedTypes.includes(file.type) && !file.name.match(/\.(xlsx|xls|csv)$/i)) { + return '์ง€์›๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. Excel ๋˜๋Š” CSV ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'; + } + + if (file.size > maxSize) { + return 'ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค. 50MB ์ดํ•˜์˜ ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'; + } + + return null; + }; + + // ํŒŒ์ผ ์„ ํƒ ์ฒ˜๋ฆฌ + const handleFileSelect = useCallback((files) => { + const fileList = Array.from(files); + const validFiles = []; + const errors = []; + + fileList.forEach(file => { + const error = validateFile(file); + if (error) { + errors.push(`${file.name}: ${error}`); + } else { + validFiles.push(file); + } + }); + + if (errors.length > 0) { + setError(errors.join('\n')); + return; + } + + setSelectedFiles(validFiles); + setError(''); + + // ์ฒซ ๋ฒˆ์งธ ํŒŒ์ผ๋ช…์„ ๊ธฐ๋ณธ BOM ์ด๋ฆ„์œผ๋กœ ์„ค์ • (ํ™•์žฅ์ž ์ œ๊ฑฐ) + if (validFiles.length > 0 && !bomName) { + const fileName = validFiles[0].name; + const nameWithoutExt = fileName.replace(/\.[^/.]+$/, ''); + setBomName(nameWithoutExt); + } + }, [bomName]); + + // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ฒ˜๋ฆฌ + const handleDragOver = useCallback((e) => { + e.preventDefault(); + setDragOver(true); + }, []); + + const handleDragLeave = useCallback((e) => { + e.preventDefault(); + setDragOver(false); + }, []); + + const handleDrop = useCallback((e) => { + e.preventDefault(); + setDragOver(false); + handleFileSelect(e.dataTransfer.files); + }, [handleFileSelect]); + + // ํŒŒ์ผ ์„ ํƒ ๋ฒ„ํŠผ ํด๋ฆญ + const handleFileButtonClick = () => { + fileInputRef.current?.click(); + }; + + // ํŒŒ์ผ ์—…๋กœ๋“œ + const handleUpload = async () => { + if (selectedFiles.length === 0) { + setError('์—…๋กœ๋“œํ•  ํŒŒ์ผ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.'); + return; + } + + if (!bomName.trim()) { + setError('BOM ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + return; + } + + if (!selectedProject) { + setError('ํ”„๋กœ์ ํŠธ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.'); + return; + } + + try { + setUploading(true); + setUploadProgress(0); + setError(''); + setSuccess(''); + + let uploadedFile = null; + + for (let i = 0; i < selectedFiles.length; i++) { + const file = selectedFiles[i]; + const formData = new FormData(); + + formData.append('file', file); + formData.append('job_no', selectedProject.official_project_code || selectedProject.job_no); + formData.append('bom_name', bomName.trim()); + formData.append('revision', 'Rev.0'); // ์ƒˆ ์—…๋กœ๋“œ๋Š” ํ•ญ์ƒ Rev.0 + + const response = await api.post('/files/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: (progressEvent) => { + const progress = Math.round( + ((i * 100) + (progressEvent.loaded * 100) / progressEvent.total) / selectedFiles.length + ); + setUploadProgress(progress); + } + }); + + if (!response.data?.success) { + throw new Error(response.data?.message || '์—…๋กœ๋“œ ์‹คํŒจ'); + } + + // ์ฒซ ๋ฒˆ์งธ ํŒŒ์ผ์˜ ์ •๋ณด๋ฅผ ์ €์žฅ + if (i === 0) { + uploadedFile = { + id: response.data.file_id, + bom_name: bomName.trim(), + revision: 'Rev.0', + job_no: selectedProject.official_project_code || selectedProject.job_no, + original_filename: file.name + }; + } + } + + setSuccess(`${selectedFiles.length}๊ฐœ ํŒŒ์ผ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!`); + + // ํŒŒ์ผ ์ดˆ๊ธฐํ™” + setSelectedFiles([]); + setBomName(''); + + // 2์ดˆ ํ›„ Files ํƒญ์œผ๋กœ ์ด๋™ + setTimeout(() => { + if (onUploadSuccess) { + onUploadSuccess(uploadedFile); + } + }, 2000); + + } catch (err) { + console.error('์—…๋กœ๋“œ ์‹คํŒจ:', err); + setError(`์—…๋กœ๋“œ ์‹คํŒจ: ${err.response?.data?.detail || err.message}`); + } finally { + setUploading(false); + setUploadProgress(0); + } + }; + + // ํŒŒ์ผ ์ œ๊ฑฐ + const removeFile = (index) => { + const newFiles = selectedFiles.filter((_, i) => i !== index); + setSelectedFiles(newFiles); + + if (newFiles.length === 0) { + setBomName(''); + } + }; + + // ํŒŒ์ผ ํฌ๊ธฐ ํฌ๋งทํŒ… + const formatFileSize = (bytes) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + return ( +
+ {/* BOM ์ด๋ฆ„ ์ž…๋ ฅ */} +
+ + setBomName(e.target.value)} + placeholder="Enter BOM name..." + style={{ + width: '100%', + padding: '12px 16px', + border: '2px solid #e5e7eb', + borderRadius: '12px', + fontSize: '16px', + transition: 'border-color 0.2s ease', + outline: 'none' + }} + onFocus={(e) => e.target.style.borderColor = '#3b82f6'} + onBlur={(e) => e.target.style.borderColor = '#e5e7eb'} + /> +
+ + {/* ํŒŒ์ผ ๋“œ๋กญ ์˜์—ญ */} +
+
+ {dragOver ? '๐Ÿ“' : '๐Ÿ“„'} +
+

+ {dragOver ? 'Drop files here' : 'Upload BOM Files'} +

+

+ Drag and drop your Excel or CSV files here, or click to browse +

+
+ ๐Ÿ“‹ + Supported: .xlsx, .xls, .csv (Max 50MB) +
+
+ + {/* ์ˆจ๊ฒจ์ง„ ํŒŒ์ผ ์ž…๋ ฅ */} + handleFileSelect(e.target.files)} + style={{ display: 'none' }} + /> + + {/* ์„ ํƒ๋œ ํŒŒ์ผ ๋ชฉ๋ก */} + {selectedFiles.length > 0 && ( +
+

+ Selected Files ({selectedFiles.length}) +

+
+ {selectedFiles.map((file, index) => ( +
+
+ ๐Ÿ“„ +
+
+ {file.name} +
+
+ {formatFileSize(file.size)} +
+
+
+ +
+ ))} +
+
+ )} + + {/* ์—…๋กœ๋“œ ์ง„ํ–‰๋ฅ  */} + {uploading && ( +
+
+ + Uploading... + + + {uploadProgress}% + +
+
+
+
+
+ )} + + {/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */} + {error && ( +
+
+ โš ๏ธ +
+ {error} +
+
+
+ )} + + {/* ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ */} + {success && ( +
+
+ โœ… +
+ {success} +
+
+
+ )} + + {/* ์—…๋กœ๋“œ ๋ฒ„ํŠผ */} +
+ +
+ + {/* ๊ฐ€์ด๋“œ ์ •๋ณด */} +
+

+ ๐Ÿ“‹ Upload Guidelines +

+
+
+

+ โœ… Supported Formats +

+
    +
  • Excel files (.xlsx, .xls)
  • +
  • CSV files (.csv)
  • +
  • Maximum file size: 50MB
  • +
+
+
+

+ ๐Ÿ“Š Required Columns +

+
    +
  • Description (์ž์žฌ๋ช…/ํ’ˆ๋ช…)
  • +
  • Quantity (์ˆ˜๋Ÿ‰)
  • +
  • Size information (์‚ฌ์ด์ฆˆ)
  • +
+
+
+

+ โšก Auto Processing +

+
    +
  • Automatic material classification
  • +
  • WELD GAP items excluded
  • +
  • Ready for material management
  • +
+
+
+
+
+ ); +}; + +export default BOMUploadTab; diff --git a/frontend/src/pages/BOMManagementPage.jsx b/frontend/src/pages/BOMManagementPage.jsx index 2aed5d5..fb8de56 100644 --- a/frontend/src/pages/BOMManagementPage.jsx +++ b/frontend/src/pages/BOMManagementPage.jsx @@ -164,6 +164,22 @@ const BOMManagementPage = ({ } }, [fileId]); + // ์ž์žฌ ๋กœ๋“œ ํ›„ ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธ + useEffect(() => { + if (materials.length > 0) { + const availableCategories = categories.filter(category => { + const count = getCategoryMaterials(category.key).length; + return count > 0; + }); + + // ํ˜„์žฌ ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ์— ์ž์žฌ๊ฐ€ ์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์œ ํšจํ•œ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ์ „ํ™˜ + const currentCategoryHasMaterials = getCategoryMaterials(selectedCategory).length > 0; + if (!currentCategoryHasMaterials && availableCategories.length > 0) { + setSelectedCategory(availableCategories[0].key); + } + } + }, [materials, selectedCategory]); + // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ž์žฌ ํ•„ํ„ฐ๋ง const getCategoryMaterials = (category) => { return materials.filter(material => @@ -391,7 +407,12 @@ const BOMManagementPage = ({ gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '16px' }}> - {categories.map((category) => { + {categories + .filter((category) => { + const count = getCategoryMaterials(category.key).length; + return count > 0; // 0๊ฐœ์ธ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ์ˆจ๊น€ + }) + .map((category) => { const isActive = selectedCategory === category.key; const count = getCategoryMaterials(category.key).length; diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index 019c78e..5ed50d3 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -479,9 +479,9 @@ const DashboardPage = ({ gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '24px' }}> - {/* BOM ๊ด€๋ฆฌ */} + {/* ํ†ตํ•ฉ BOM ๊ด€๋ฆฌ */}
navigateToPage('bom', { selectedProject })} + onClick={() => navigateToPage('unified-bom', { selectedProject })} style={{ background: 'white', borderRadius: '16px', @@ -530,8 +530,47 @@ const DashboardPage = ({ margin: 0, lineHeight: '1.5' }}> - Upload and manage Bill of Materials files. Classify materials and generate reports. + Upload, manage revisions, and classify materials in one unified workspace

+ + {/* ๊ธฐ๋Šฅ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} +
+
+ ๐Ÿ“ค Upload +
+
+ ๐Ÿ“Š Revisions +
+
+ ๐Ÿ“‹ Materials +
+
{/* ๊ตฌ๋งค์‹ ์ฒญ ๊ด€๋ฆฌ */} diff --git a/frontend/src/pages/UnifiedBOMPage.jsx b/frontend/src/pages/UnifiedBOMPage.jsx new file mode 100644 index 0000000..0018b62 --- /dev/null +++ b/frontend/src/pages/UnifiedBOMPage.jsx @@ -0,0 +1,266 @@ +import React, { useState, useEffect } from 'react'; +import BOMUploadTab from '../components/bom/tabs/BOMUploadTab'; +import BOMFilesTab from '../components/bom/tabs/BOMFilesTab'; +import BOMMaterialsTab from '../components/bom/tabs/BOMMaterialsTab'; + +const UnifiedBOMPage = ({ + onNavigate, + selectedProject, + user +}) => { + const [activeTab, setActiveTab] = useState('upload'); + const [selectedBOM, setSelectedBOM] = useState(null); + const [bomFiles, setBomFiles] = useState([]); + const [refreshTrigger, setRefreshTrigger] = useState(0); + + // ์—…๋กœ๋“œ ์„ฑ๊ณต ์‹œ Files ํƒญ์œผ๋กœ ์ด๋™ + const handleUploadSuccess = (uploadedFile) => { + setRefreshTrigger(prev => prev + 1); + setActiveTab('files'); + // ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ์ž๋™ ์„ ํƒ + if (uploadedFile) { + setSelectedBOM(uploadedFile); + } + }; + + // BOM ํŒŒ์ผ ์„ ํƒ ์‹œ Materials ํƒญ์œผ๋กœ ์ด๋™ + const handleBOMSelect = (bomFile) => { + setSelectedBOM(bomFile); + setActiveTab('materials'); + }; + + // ํƒญ ์ •์˜ + const tabs = [ + { + id: 'upload', + label: 'Upload', + icon: '๐Ÿ“ค', + description: 'Upload new BOM files' + }, + { + id: 'files', + label: 'Files & Revisions', + icon: '๐Ÿ“Š', + description: 'Manage BOM files and revisions' + }, + { + id: 'materials', + label: 'Materials', + icon: '๐Ÿ“‹', + description: 'Manage classified materials', + disabled: !selectedBOM + } + ]; + + return ( +
+ {/* ํ—ค๋” */} +
+
+
+

+ BOM Management System +

+

+ Project: {selectedProject?.job_name || 'No Project Selected'} + {selectedBOM && ( + + โ†’ {selectedBOM.bom_name || selectedBOM.original_filename} + + )} +

+
+ +
+ + {/* ํ”„๋กœ์ ํŠธ ์ •๋ณด */} +
+
+
+ {selectedProject?.official_project_code || selectedProject?.job_no || 'N/A'} +
+
+ Project Code +
+
+ +
+
+ {user?.username || 'Unknown'} +
+
+ Current User +
+
+ +
+
+ {bomFiles.length} +
+
+ BOM Files +
+
+
+
+ + {/* ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ */} +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ + {/* ํƒญ ์ปจํ…์ธ  */} +
+ {activeTab === 'upload' && ( + + )} + + {activeTab === 'files' && ( + + )} + + {activeTab === 'materials' && selectedBOM && ( + + )} +
+
+ ); +}; + +export default UnifiedBOMPage; diff --git a/frontend/src/pages/_deprecated/BOMRevisionPage.jsx b/frontend/src/pages/_deprecated/BOMRevisionPage.jsx new file mode 100644 index 0000000..181fded --- /dev/null +++ b/frontend/src/pages/_deprecated/BOMRevisionPage.jsx @@ -0,0 +1,350 @@ +import React, { useState, useEffect } from 'react'; +import api from '../api'; + +const BOMRevisionPage = ({ + onNavigate, + selectedProject, + user +}) => { + const [bomFiles, setBomFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + // BOM ํŒŒ์ผ ๋ชฉ๋ก ๋กœ๋“œ (๊ธฐ๋ณธ ๊ตฌ์กฐ๋งŒ) + useEffect(() => { + const loadBOMFiles = async () => { + if (!selectedProject) return; + + try { + setLoading(true); + // TODO: ์‹ค์ œ API ๊ตฌํ˜„ ํ•„์š” + // const response = await api.get(`/files/project/${selectedProject.job_no}`); + // setBomFiles(response.data); + + // ์ž„์‹œ ๋ฐ์ดํ„ฐ + setBomFiles([ + { + id: 1, + bom_name: 'Main Process BOM', + revisions: ['Rev.0', 'Rev.1', 'Rev.2'], + latest_revision: 'Rev.2', + upload_date: '2024-10-17', + status: 'Active' + }, + { + id: 2, + bom_name: 'Utility BOM', + revisions: ['Rev.0', 'Rev.1'], + latest_revision: 'Rev.1', + upload_date: '2024-10-16', + status: 'Active' + } + ]); + } catch (err) { + console.error('BOM ํŒŒ์ผ ๋กœ๋“œ ์‹คํŒจ:', err); + setError('BOM ํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setLoading(false); + } + }; + + loadBOMFiles(); + }, [selectedProject]); + + return ( +
+ {/* ํ—ค๋” */} +
+
+
+

+ BOM Revision Management +

+

+ Project: {selectedProject?.job_name || 'No Project Selected'} +

+
+
+ + +
+
+ + {/* ํ”„๋กœ์ ํŠธ ์ •๋ณด */} +
+
+
+ {selectedProject?.official_project_code || selectedProject?.job_no || 'N/A'} +
+
+ Project Code +
+
+ +
+
+ {bomFiles.length} +
+
+ BOM Files +
+
+ +
+
+ {bomFiles.reduce((total, bom) => total + bom.revisions.length, 0)} +
+
+ Total Revisions +
+
+
+
+ + {/* ๊ฐœ๋ฐœ ์˜ˆ์ • ๋ฐฐ๋„ˆ */} +
+
๐Ÿšง
+

+ Advanced Revision Management +

+

+ ๊ณ ๊ธ‰ ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์ด ๊ฐœ๋ฐœ ์ค‘์ž…๋‹ˆ๋‹ค. ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ์™„๋ฃŒ ํ›„ ๋ณธ๊ฒฉ์ ์ธ ๊ฐœ๋ฐœ์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค. +

+ + {/* ์˜ˆ์ • ๊ธฐ๋Šฅ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} +
+
+
๐Ÿ“Š
+

+ Revision Timeline +

+

+ ์‹œ๊ฐ์  ๋ฆฌ๋น„์ „ ํžˆ์Šคํ† ๋ฆฌ +

+
+ +
+
๐Ÿ”
+

+ Diff Comparison +

+

+ ๋ฆฌ๋น„์ „ ๊ฐ„ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋น„๊ต +

+
+ +
+
โช
+

+ Rollback System +

+

+ ์ด์ „ ๋ฆฌ๋น„์ „์œผ๋กœ ๋กค๋ฐฑ +

+
+
+
+ + {/* ์ž„์‹œ BOM ํŒŒ์ผ ๋ชฉ๋ก (๊ธฐ๋ณธ ๊ตฌ์กฐ) */} + {bomFiles.length > 0 && ( +
+

+ Current BOM Files (Preview) +

+ +
+ {bomFiles.map((bom) => ( +
+
+

+ {bom.bom_name} +

+
+ Latest: {bom.latest_revision} + Revisions: {bom.revisions.length} + Uploaded: {bom.upload_date} +
+
+ +
+ + +
+
+ ))} +
+
+ )} +
+ ); +}; + +export default BOMRevisionPage; diff --git a/frontend/src/pages/_deprecated/BOMUploadPage.jsx b/frontend/src/pages/_deprecated/BOMUploadPage.jsx new file mode 100644 index 0000000..16ab320 --- /dev/null +++ b/frontend/src/pages/_deprecated/BOMUploadPage.jsx @@ -0,0 +1,600 @@ +import React, { useState, useRef, useCallback } from 'react'; +import api from '../api'; + +const BOMUploadPage = ({ + onNavigate, + selectedProject, + user +}) => { + const [dragOver, setDragOver] = useState(false); + const [uploading, setUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [selectedFiles, setSelectedFiles] = useState([]); + const [bomName, setBomName] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const fileInputRef = useRef(null); + + // ํŒŒ์ผ ๊ฒ€์ฆ + const validateFile = (file) => { + const allowedTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv' + ]; + + const maxSize = 50 * 1024 * 1024; // 50MB + + if (!allowedTypes.includes(file.type) && !file.name.match(/\.(xlsx|xls|csv)$/i)) { + return '์ง€์›๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. Excel ๋˜๋Š” CSV ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'; + } + + if (file.size > maxSize) { + return 'ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค. 50MB ์ดํ•˜์˜ ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'; + } + + return null; + }; + + // ํŒŒ์ผ ์„ ํƒ ์ฒ˜๋ฆฌ + const handleFileSelect = useCallback((files) => { + const fileList = Array.from(files); + const validFiles = []; + const errors = []; + + fileList.forEach(file => { + const error = validateFile(file); + if (error) { + errors.push(`${file.name}: ${error}`); + } else { + validFiles.push(file); + } + }); + + if (errors.length > 0) { + setError(errors.join('\n')); + return; + } + + setSelectedFiles(validFiles); + setError(''); + + // ์ฒซ ๋ฒˆ์งธ ํŒŒ์ผ๋ช…์„ ๊ธฐ๋ณธ BOM ์ด๋ฆ„์œผ๋กœ ์„ค์ • (ํ™•์žฅ์ž ์ œ๊ฑฐ) + if (validFiles.length > 0 && !bomName) { + const fileName = validFiles[0].name; + const nameWithoutExt = fileName.replace(/\.[^/.]+$/, ''); + setBomName(nameWithoutExt); + } + }, [bomName]); + + // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ฒ˜๋ฆฌ + const handleDragOver = useCallback((e) => { + e.preventDefault(); + setDragOver(true); + }, []); + + const handleDragLeave = useCallback((e) => { + e.preventDefault(); + setDragOver(false); + }, []); + + const handleDrop = useCallback((e) => { + e.preventDefault(); + setDragOver(false); + handleFileSelect(e.dataTransfer.files); + }, [handleFileSelect]); + + // ํŒŒ์ผ ์„ ํƒ ๋ฒ„ํŠผ ํด๋ฆญ + const handleFileButtonClick = () => { + fileInputRef.current?.click(); + }; + + // ํŒŒ์ผ ์—…๋กœ๋“œ + const handleUpload = async () => { + if (selectedFiles.length === 0) { + setError('์—…๋กœ๋“œํ•  ํŒŒ์ผ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.'); + return; + } + + if (!bomName.trim()) { + setError('BOM ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + return; + } + + if (!selectedProject) { + setError('ํ”„๋กœ์ ํŠธ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.'); + return; + } + + try { + setUploading(true); + setUploadProgress(0); + setError(''); + setSuccess(''); + + for (let i = 0; i < selectedFiles.length; i++) { + const file = selectedFiles[i]; + const formData = new FormData(); + + formData.append('file', file); + formData.append('job_no', selectedProject.official_project_code || selectedProject.job_no); + formData.append('bom_name', bomName.trim()); + formData.append('revision', 'Rev.0'); // ์ƒˆ ์—…๋กœ๋“œ๋Š” ํ•ญ์ƒ Rev.0 + + const response = await api.post('/files/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: (progressEvent) => { + const progress = Math.round( + ((i * 100) + (progressEvent.loaded * 100) / progressEvent.total) / selectedFiles.length + ); + setUploadProgress(progress); + } + }); + + if (!response.data?.success) { + throw new Error(response.data?.message || '์—…๋กœ๋“œ ์‹คํŒจ'); + } + } + + setSuccess(`${selectedFiles.length}๊ฐœ ํŒŒ์ผ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!`); + + // 3์ดˆ ํ›„ BOM ๊ด€๋ฆฌ ํŽ˜์ด์ง€๋กœ ์ด๋™ + setTimeout(() => { + if (onNavigate) { + onNavigate('bom-management', { + file_id: response.data.file_id, + jobNo: selectedProject.official_project_code || selectedProject.job_no, + bomName: bomName.trim(), + revision: 'Rev.0' + }); + } + }, 3000); + + } catch (err) { + console.error('์—…๋กœ๋“œ ์‹คํŒจ:', err); + setError(`์—…๋กœ๋“œ ์‹คํŒจ: ${err.response?.data?.detail || err.message}`); + } finally { + setUploading(false); + setUploadProgress(0); + } + }; + + // ํŒŒ์ผ ์ œ๊ฑฐ + const removeFile = (index) => { + const newFiles = selectedFiles.filter((_, i) => i !== index); + setSelectedFiles(newFiles); + + if (newFiles.length === 0) { + setBomName(''); + } + }; + + // ํŒŒ์ผ ํฌ๊ธฐ ํฌ๋งทํŒ… + const formatFileSize = (bytes) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + return ( +
+ {/* ํ—ค๋” */} +
+
+
+

+ BOM File Upload +

+

+ Project: {selectedProject?.job_name || 'No Project Selected'} +

+
+ +
+ + {/* ํ”„๋กœ์ ํŠธ ์ •๋ณด */} +
+
+
+ {selectedProject?.official_project_code || selectedProject?.job_no || 'N/A'} +
+
+ Project Code +
+
+ +
+
+ {user?.username || 'Unknown'} +
+
+ Uploaded by +
+
+
+
+ + {/* ์—…๋กœ๋“œ ์˜์—ญ */} +
+ {/* BOM ์ด๋ฆ„ ์ž…๋ ฅ */} +
+ + setBomName(e.target.value)} + placeholder="Enter BOM name..." + style={{ + width: '100%', + padding: '12px 16px', + border: '2px solid #e5e7eb', + borderRadius: '12px', + fontSize: '16px', + transition: 'border-color 0.2s ease', + outline: 'none' + }} + onFocus={(e) => e.target.style.borderColor = '#3b82f6'} + onBlur={(e) => e.target.style.borderColor = '#e5e7eb'} + /> +
+ + {/* ํŒŒ์ผ ๋“œ๋กญ ์˜์—ญ */} +
+
+ {dragOver ? '๐Ÿ“' : '๐Ÿ“„'} +
+

+ {dragOver ? 'Drop files here' : 'Upload BOM Files'} +

+

+ Drag and drop your Excel or CSV files here, or click to browse +

+
+ ๐Ÿ“‹ + Supported: .xlsx, .xls, .csv (Max 50MB) +
+
+ + {/* ์ˆจ๊ฒจ์ง„ ํŒŒ์ผ ์ž…๋ ฅ */} + handleFileSelect(e.target.files)} + style={{ display: 'none' }} + /> + + {/* ์„ ํƒ๋œ ํŒŒ์ผ ๋ชฉ๋ก */} + {selectedFiles.length > 0 && ( +
+

+ Selected Files ({selectedFiles.length}) +

+
+ {selectedFiles.map((file, index) => ( +
+
+ ๐Ÿ“„ +
+
+ {file.name} +
+
+ {formatFileSize(file.size)} +
+
+
+ +
+ ))} +
+
+ )} + + {/* ์—…๋กœ๋“œ ์ง„ํ–‰๋ฅ  */} + {uploading && ( +
+
+ + Uploading... + + + {uploadProgress}% + +
+
+
+
+
+ )} + + {/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */} + {error && ( +
+
+ โš ๏ธ +
+ {error} +
+
+
+ )} + + {/* ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ */} + {success && ( +
+
+ โœ… +
+ {success} +
+
+
+ )} + + {/* ์—…๋กœ๋“œ ๋ฒ„ํŠผ */} +
+ + +
+
+ + {/* ๊ฐ€์ด๋“œ ์ •๋ณด */} +
+

+ ๐Ÿ“‹ Upload Guidelines +

+
+
+

+ โœ… Supported Formats +

+
    +
  • Excel files (.xlsx, .xls)
  • +
  • CSV files (.csv)
  • +
  • Maximum file size: 50MB
  • +
+
+
+

+ ๐Ÿ“Š Required Columns +

+
    +
  • Description (์ž์žฌ๋ช…/ํ’ˆ๋ช…)
  • +
  • Quantity (์ˆ˜๋Ÿ‰)
  • +
  • Size information (์‚ฌ์ด์ฆˆ)
  • +
+
+
+

+ โšก Auto Processing +

+
    +
  • Automatic material classification
  • +
  • WELD GAP items excluded
  • +
  • Ready for BOM management
  • +
+
+
+
+
+ ); +}; + +export default BOMUploadPage; diff --git a/frontend/src/pages/BOMWorkspacePage.jsx b/frontend/src/pages/_deprecated/BOMWorkspacePage.jsx similarity index 100% rename from frontend/src/pages/BOMWorkspacePage.jsx rename to frontend/src/pages/_deprecated/BOMWorkspacePage.jsx