# ๐Ÿš€ TK-MP-Project: ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ์กด > **์ตœ์ข… ์—…๋ฐ์ดํŠธ**: 2025๋…„ 1์›” (ํ†ตํ•ฉ ๋ฌธ์„œ ์ƒ์„ฑ) --- ## ๐Ÿ“‹ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” **TK-MP-Project**๋Š” BOM (Bill of Materials) ์‹œ์Šคํ…œ์˜ ๊ธฐ๋Šฅ ์ด์ƒ์„ ํ•ด๊ฒฐํ•˜๊ณ , ๋„๋ฉด ์™„์„ฑ ํ›„ ์ž์žฌ ๊ด€๋ฆฌ์˜ ๋ชจ๋“  ํ”„๋กœ์„ธ์Šค๋ฅผ ์ž๋™ํ™”ํ•˜๋Š” ์ข…ํ•ฉ ์‹œ์Šคํ…œ ๊ฐœ๋ฐœ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ### ๐ŸŽฏ ํ•ต์‹ฌ ๋ฏธ์…˜ **"๋„๋ฉด ์™„์„ฑ ํ›„ ์ž์žฌ ๊ด€๋ฆฌ์˜ ๋ชจ๋“  ๋ฒˆ๊ฑฐ๋กœ์›€์„ ํ•ด๊ฒฐ"** ### ์ฃผ์š” ํ•ด๊ฒฐ ๊ณผ์ œ - ๐Ÿ“„ **ํŒŒ์ผ ๋ถ„์„ ์ž๋™ํ™”**: ์—‘์…€/CSV ์ž์žฌ ๋ชฉ๋ก์˜ ์ž๋™ ๋ถ„๋ฅ˜ ๋ฐ ์ •์ œ - ๐Ÿ” **์ •ํ™•ํ•œ ๋ถ„๋ฅ˜ ์ฒด๊ณ„**: ํŒŒ์ดํ”„/ํ”ผํŒ…/๋ณผํŠธ/๋ฐธ๋ธŒ/๊ณ„๊ธฐ๋ฅ˜์˜ 4๋‹จ๊ณ„ ์ž๋™ ๋ถ„๋ฅ˜ - ๐Ÿ’พ **์ฒด๊ณ„์  ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ**: ํ”„๋กœ์ ํŠธ๋ณ„ ๋ฒ„์ „ ๊ด€๋ฆฌ ๋ฐ ์ด๋ ฅ ์ถ”์  - ๐Ÿ“Š **์—…๋ฌด๋ณ„ ๋งž์ถค ์ถœ๋ ฅ**: ๊ตฌ๋งค/์ƒ์‚ฐ/ํ’ˆ์งˆ ๊ฐ ํŒ€์˜ ํ•„์š”์— ๋งž๋Š” ์ž๋ฃŒ ์ƒ์„ฑ - ๐Ÿ”„ **๋ฆฌ๋น„์ „ ๋ณ€ํ™” ์ถ”์ **: ๋„๋ฉด ๋ณ€๊ฒฝ ์‹œ ์ž์žฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ž๋™ ๋น„๊ต --- ## ๐Ÿ’ป ๊ธฐ์ˆ  ์Šคํƒ ### Frontend - **Language**: JavaScript (ES6+) - **Framework**: React 18 + Vite (Material-UI ์ œ๊ฑฐ๋จ) - **Router**: ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ… (React Router ๋Œ€์ฒด) - **HTTP Client**: Axios - **File Processing**: XLSX (SheetJS), file-saver - **Charts**: Chart.js + react-chartjs-2 - **Styling**: ์ˆœ์ˆ˜ HTML/CSS (์ธ๋ผ์ธ ์Šคํƒ€์ผ) ### Backend - **Language**: Python 3.9+ - **Framework**: FastAPI (๊ณ ์„ฑ๋Šฅ API ์„œ๋ฒ„) - **Database**: PostgreSQL 15 (๋ณต์žกํ•œ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ) - **ORM**: SQLAlchemy (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ๋ง) - **Data Processing**: Pandas, openpyxl (ํŒŒ์ผ ์ฒ˜๋ฆฌ) - **Cache**: Redis 7 ### DevOps & Tools - **Containerization**: Docker & Docker Compose - **Web Server**: Nginx (ํ”„๋กœ๋•์…˜) - **Database Admin**: pgAdmin4 - **Version Control**: Git - **Development**: VS Code + Python ํ™•์žฅ --- ## ๐Ÿ—๏ธ ์ฝ”๋“œ ๊ตฌ์กฐ ๋ฐ ์ปดํฌ๋„ŒํŠธ ๊ด€๊ณ„๋„ ### ๐Ÿ“ ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌ์กฐ ``` frontend/src/ โ”œโ”€โ”€ App.jsx # ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์ƒํƒœ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…) โ”œโ”€โ”€ SimpleLogin.jsx # ๋กœ๊ทธ์ธ ์ปดํฌ๋„ŒํŠธ โ”œโ”€โ”€ api.js # API ํด๋ผ์ด์–ธํŠธ (Axios ์„ค์ •) โ”œโ”€โ”€ components/ # ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ โ”‚ โ”œโ”€โ”€ NavigationMenu.jsx # ์‚ฌ์ด๋“œ๋ฐ” ๋„ค๋น„๊ฒŒ์ด์…˜ (๊ถŒํ•œ ๊ธฐ๋ฐ˜) โ”‚ โ”œโ”€โ”€ PersonalizedDashboard.jsx # ๊ฐœ์ธํ™”๋œ ๋Œ€์‹œ๋ณด๋“œ (์—ญํ• ๋ณ„ ๋งž์ถค) โ”‚ โ”œโ”€โ”€ ProjectSelector.jsx # ํ”„๋กœ์ ํŠธ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด (๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ) โ”‚ โ”œโ”€โ”€ BOMFileUpload.jsx # BOM ํŒŒ์ผ ์—…๋กœ๋“œ ํผ โ”‚ โ”œโ”€โ”€ BOMFileTable.jsx # BOM ํŒŒ์ผ ๋ชฉ๋ก ํ…Œ์ด๋ธ” โ”‚ โ””โ”€โ”€ RevisionUploadDialog.jsx # ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ ๋‹ค์ด์–ผ๋กœ๊ทธ โ””โ”€โ”€ pages/ # ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ โ”œโ”€โ”€ DashboardPage.jsx # ๋Œ€์‹œ๋ณด๋“œ (๊ธฐ์กด) โ”œโ”€โ”€ ProjectWorkspacePage.jsx # ํ”„๋กœ์ ํŠธ๋ณ„ ์›Œํฌ์ŠคํŽ˜์ด์Šค (์‹ ๊ทœ) โ”œโ”€โ”€ BOMUploadPage.jsx # BOM ์—…๋กœ๋“œ ํŽ˜์ด์ง€ (์‹ ๊ทœ) โ”œโ”€โ”€ ProjectsPage.jsx # ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ โ”œโ”€โ”€ JobSelectionPage.jsx # ํ”„๋กœ์ ํŠธ ์„ ํƒ โ”œโ”€โ”€ BOMStatusPage.jsx # BOM ๊ด€๋ฆฌ ๋ฉ”์ธ โ”œโ”€โ”€ SimpleMaterialsPage.jsx # ์ž์žฌ ๋ชฉ๋ก (Material-UI ์ œ๊ฑฐ๋จ) โ”œโ”€โ”€ MaterialComparisonPage.jsx # ๋ฆฌ๋น„์ „ ๋น„๊ต โ””โ”€โ”€ RevisionPurchasePage.jsx # ๊ตฌ๋งค ํ™•์ • ``` ### ๐Ÿ“ ๋ฐฑ์—”๋“œ ๊ตฌ์กฐ ``` backend/ โ”œโ”€โ”€ app/ โ”‚ โ”œโ”€โ”€ main.py # FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์  โ”‚ โ”œโ”€โ”€ config.py # ์„ค์ • ๊ด€๋ฆฌ (Pydantic Settings) โ”‚ โ”œโ”€โ”€ auth/ # ์ธ์ฆ ๋ชจ๋“ˆ โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ”‚ โ”œโ”€โ”€ models.py # SQLAlchemy ์ธ์ฆ ๋ชจ๋ธ โ”‚ โ”‚ โ”œโ”€โ”€ jwt_service.py # JWT ํ† ํฐ ๊ด€๋ฆฌ โ”‚ โ”‚ โ”œโ”€โ”€ auth_service.py # ์ธ์ฆ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง โ”‚ โ”‚ โ”œโ”€โ”€ auth_controller.py # ์ธ์ฆ API ์—”๋“œํฌ์ธํŠธ โ”‚ โ”‚ โ””โ”€โ”€ middleware.py # ๊ถŒํ•œ ๋ฏธ๋“ค์›จ์–ด โ”‚ โ”œโ”€โ”€ api/ # API ๋ผ์šฐํ„ฐ โ”‚ โ”‚ โ””โ”€โ”€ file_management.py # ํŒŒ์ผ ๊ด€๋ฆฌ API โ”‚ โ”œโ”€โ”€ routers/ # ๊ธฐ์กด ๋ผ์šฐํ„ฐ โ”‚ โ”‚ โ”œโ”€โ”€ files.py # ํŒŒ์ผ/์ž์žฌ API โ”‚ โ”‚ โ””โ”€โ”€ jobs.py # ํ”„๋กœ์ ํŠธ API โ”‚ โ”œโ”€โ”€ services/ # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์„œ๋น„์Šค โ”‚ โ”‚ โ””โ”€โ”€ file_service.py # ํŒŒ์ผ ์ฒ˜๋ฆฌ ์„œ๋น„์Šค โ”‚ โ”œโ”€โ”€ utils/ # ์œ ํ‹ธ๋ฆฌํ‹ฐ โ”‚ โ”‚ โ”œโ”€โ”€ logger.py # ๋กœ๊น… ์„ค์ • โ”‚ โ”‚ โ”œโ”€โ”€ cache_manager.py # Redis ์บ์‹œ ๊ด€๋ฆฌ โ”‚ โ”‚ โ”œโ”€โ”€ file_validator.py # ํŒŒ์ผ ๊ฒ€์ฆ โ”‚ โ”‚ โ”œโ”€โ”€ error_handlers.py # ์—๋Ÿฌ ํ•ธ๋“ค๋ง โ”‚ โ”‚ โ””โ”€โ”€ transaction_manager.py # DB ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ โ”‚ โ””โ”€โ”€ schemas/ # Pydantic ์Šคํ‚ค๋งˆ โ”‚ โ””โ”€โ”€ response_models.py # API ์‘๋‹ต ๋ชจ๋ธ โ”œโ”€โ”€ scripts/ # DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ โ””โ”€โ”€ requirements.txt # Python ์˜์กด์„ฑ ``` ### ๐Ÿ”„ ์ปดํฌ๋„ŒํŠธ ๊ด€๊ณ„๋„ ```mermaid graph TD A[App.jsx] --> B[SimpleLogin.jsx] A --> C[NavigationMenu.jsx] A --> D[ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๋“ค] D --> E[JobSelectionPage.jsx] D --> F[BOMStatusPage.jsx] D --> G[SimpleMaterialsPage.jsx] F --> H[BOMFileUpload.jsx] F --> I[BOMFileTable.jsx] F --> J[RevisionUploadDialog.jsx] K[api.js] --> L[Backend APIs] subgraph "Backend Services" L --> M[auth_controller.py] L --> N[file_management.py] L --> O[files.py] L --> P[jobs.py] end subgraph "Business Logic" M --> Q[auth_service.py] N --> R[file_service.py] end subgraph "Data Layer" Q --> S[auth/models.py] R --> T[Database Tables] end ``` ### ๐ŸŽฏ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ ์›์น™ (์ ์šฉ๋จ) #### 1. **ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ** (200-300์ค„ ์ดํ•˜) - ์ „์ฒด ํŽ˜์ด์ง€ ๋ ˆ์ด์•„์›ƒ๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ - ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์กฐํ•ฉ - API ํ˜ธ์ถœ ๋ฐ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ œ์–ด #### 2. **๊ธฐ๋Šฅ๋ณ„ ์ปดํฌ๋„ŒํŠธ** (100-150์ค„ ์ดํ•˜) - ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ ์ ์šฉ - ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋…๋ฆฝ์  ๊ธฐ๋Šฅ - Props๋ฅผ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ #### 3. **์„œ๋น„์Šค ๋ ˆ์ด์–ด** (200์ค„ ์ดํ•˜) - ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ถ„๋ฆฌ - API์™€ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์ด์˜ ์ค‘๊ฐ„ ๊ณ„์ธต - ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋ฐ ๊ฒ€์ฆ ### ๐Ÿ“‹ ๋ถ„๋ฆฌ๋œ ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก | ์›๋ณธ ํŒŒ์ผ | ๋ถ„๋ฆฌ ํ›„ | ์ค„ ์ˆ˜ ๋ณ€ํ™” | ์ƒํƒœ | |----------|---------|-----------|------| | `MaterialsPage.jsx` | `SimpleMaterialsPage.jsx` | 1000+ โ†’ 300์ค„ | โœ… ์™„๋ฃŒ | | `BOMStatusPage.jsx` | 3๊ฐœ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌ | 400+ โ†’ 200์ค„ | โœ… ์™„๋ฃŒ | | - | `BOMFileUpload.jsx` | ์ƒˆ๋กœ ์ƒ์„ฑ (100์ค„) | โœ… ์™„๋ฃŒ | | - | `BOMFileTable.jsx` | ์ƒˆ๋กœ ์ƒ์„ฑ (150์ค„) | โœ… ์™„๋ฃŒ | | - | `RevisionUploadDialog.jsx` | ์ƒˆ๋กœ ์ƒ์„ฑ (80์ค„) | โœ… ์™„๋ฃŒ | ### ๐Ÿ”ง ํ–ฅํ›„ ๋ถ„๋ฆฌ ๋Œ€์ƒ | ํŒŒ์ผ๋ช… | ํ˜„์žฌ ์ค„ ์ˆ˜ | ๋ถ„๋ฆฌ ๊ณ„ํš | ์šฐ์„ ์ˆœ์œ„ | |--------|-----------|----------|---------| | `MaterialComparisonPage.jsx` | 500์ค„+ | ๋น„๊ต ๋กœ์ง ๋ถ„๋ฆฌ | ์ค‘๊ฐ„ | | `RevisionPurchasePage.jsx` | 300์ค„+ | ๊ตฌ๋งค ๋กœ์ง ๋ถ„๋ฆฌ | ๋‚ฎ์Œ | | `auth_service.py` | 300์ค„+ | ๊ธฐ๋Šฅ๋ณ„ ์„œ๋น„์Šค ๋ถ„๋ฆฌ | ๋†’์Œ | ### ๐ŸŒ **API ์—”๋“œํฌ์ธํŠธ ์ „์ฒด ๋งต** (2025.01 ์ตœ์‹ ) > **์ค‘์š”**: ์ƒˆ๋กœ์šด API ์ถ”๊ฐ€ ์‹œ ๋ฐ˜๋“œ์‹œ ์ด ์„น์…˜์„ ์—…๋ฐ์ดํŠธํ•˜๊ณ , ๋ฒ„์ „ ๊ด€๋ฆฌ ๋ฐ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. #### **๐Ÿ“‹ API ๋ฌธ์„œํ™” ๊ทœ์น™** 1. **์ƒˆ API ์ถ”๊ฐ€ ์‹œ**: ์ด ๋ฌธ์„œ์— ์ฆ‰์‹œ ๋ฐ˜์˜ 2. **API ๋ณ€๊ฒฝ ์‹œ**: ๋ณ€๊ฒฝ ์ด๋ ฅ๊ณผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ ํฌํ•จ 3. **๊ถŒํ•œ ํ‘œ์‹œ**: ๊ฐ ์—”๋“œํฌ์ธํŠธ๋ณ„ ํ•„์š” ๊ถŒํ•œ ๋ช…์‹œ 4. **์‘๋‹ต ํ˜•์‹**: ํ‘œ์ค€ ์‘๋‹ต ๊ตฌ์กฐ ์ค€์ˆ˜ #### **๐Ÿšจ API ์‚ฌ์šฉ ๊ฐ€์ด๋“œ๋ผ์ธ (ํ˜ผ๋™ ๋ฐฉ์ง€)** โญ ์ค‘์š” ##### **1. ์ž์žฌ ๊ด€๋ จ API ํ†ตํ•ฉ ์‚ฌ์šฉ๋ฒ•** ```javascript // โœ… ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ๋ฒ• - ํ†ตํ•ฉ๋œ API ์‚ฌ์šฉ import { fetchMaterials } from '../api'; // ํŒŒ์ผ๋ณ„ ์ž์žฌ ์กฐํšŒ const materials = await fetchMaterials({ file_id: 123, limit: 1000 }); // ํ”„๋กœ์ ํŠธ๋ณ„ ์ž์žฌ ์กฐํšŒ const materials = await fetchMaterials({ job_no: 'J24-001', limit: 1000 }); // ๋ฆฌ๋น„์ „๋ณ„ ์ž์žฌ ์กฐํšŒ const materials = await fetchMaterials({ job_no: 'J24-001', revision: 'Rev.1', limit: 1000 }); // โŒ ์ž˜๋ชป๋œ ์‚ฌ์šฉ๋ฒ• - ์ง์ ‘ API ํ˜ธ์ถœ ๊ธˆ์ง€ const response = await api.get('/files/materials-v2', { params }); // ๊ธˆ์ง€ const response = await api.get('/files/materials', { params }); // ์กด์žฌํ•˜์ง€ ์•Š์Œ ``` ##### **2. API ํ•จ์ˆ˜ vs ์ง์ ‘ ํ˜ธ์ถœ ๊ทœ์น™** ```javascript // โœ… ๊ถŒ์žฅ: api.js์˜ ๋ž˜ํผ ํ•จ์ˆ˜ ์‚ฌ์šฉ import { fetchMaterials, fetchFiles, fetchJobs } from '../api'; // โŒ ๋น„๊ถŒ์žฅ: ์ง์ ‘ API ํ˜ธ์ถœ (ํŠน๋ณ„ํ•œ ๊ฒฝ์šฐ์—๋งŒ) const response = await api.get('/files/materials-v2'); ``` ##### **3. ๋ฐฑ์—”๋“œ API ์—”๋“œํฌ์ธํŠธ ๋ช…๋ช… ๊ทœ์น™** - **๊ธฐ๋ณธ ํ˜•ํƒœ**: `/{๋ชจ๋“ˆ}/{๋ฆฌ์†Œ์Šค}` - **๋ฒ„์ „ ๊ด€๋ฆฌ**: `/{๋ชจ๋“ˆ}/{๋ฆฌ์†Œ์Šค}-v2` (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) - **์•ก์…˜ ๊ธฐ๋ฐ˜**: `/{๋ชจ๋“ˆ}/{๋ฆฌ์†Œ์Šค}/{์•ก์…˜}` **์˜ˆ์‹œ:** ``` /files/materials-v2 # ์ž์žฌ ๋ชฉ๋ก (์ตœ์‹  ๋ฒ„์ „) /files/materials/summary # ์ž์žฌ ์š”์•ฝ ํ†ต๊ณ„ /files/materials/compare-revisions # ๋ฆฌ๋น„์ „ ๋น„๊ต /purchase/items/calculate # ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ /materials/compare-revisions # ์ž์žฌ ๋น„๊ต (๋ณ„๋„ ๋ชจ๋“ˆ) ``` ##### **4. ํ”„๋ก ํŠธ์—”๋“œ API ํ˜ธ์ถœ ํ‘œ์ค€ํ™”** ```javascript // api.js - ๋ชจ๋“  API ํ•จ์ˆ˜๋Š” ์—ฌ๊ธฐ์— ์ •์˜ export function fetchMaterials(params) { return api.get('/files/materials-v2', { params }); } export function fetchFiles(params) { return api.get('/files', { params }); } export function fetchJobs(params) { return api.get('/jobs/', { params }); } // ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ import { fetchMaterials } from '../api'; const response = await fetchMaterials({ job_no: 'J24-001' }); ``` --- #### **๐Ÿ” ์ธ์ฆ API (`/auth/`)** ```http POST /auth/login # ๋กœ๊ทธ์ธ (๊ณต๊ฐœ) POST /auth/register # ์‚ฌ์šฉ์ž ๋“ฑ๋ก (๊ด€๋ฆฌ์ž) POST /auth/refresh # ํ† ํฐ ๊ฐฑ์‹  (์ธ์ฆ ํ•„์š”) POST /auth/logout # ๋กœ๊ทธ์•„์›ƒ (์ธ์ฆ ํ•„์š”) GET /auth/me # ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด (์ธ์ฆ ํ•„์š”) GET /auth/verify # ํ† ํฐ ๊ฒ€์ฆ (์ธ์ฆ ํ•„์š”) GET /auth/users # ์‚ฌ์šฉ์ž ๋ชฉ๋ก (๊ด€๋ฆฌ์ž) PUT /auth/users/{id} # ์‚ฌ์šฉ์ž ์ˆ˜์ • (๊ด€๋ฆฌ์ž) DELETE /auth/users/{id} # ์‚ฌ์šฉ์ž ์‚ญ์ œ (๊ด€๋ฆฌ์ž) ``` #### **๐Ÿ“‹ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ API (`/jobs/`)** ```http GET /jobs/ # ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก (์‚ฌ์šฉ์ž) POST /jobs/ # ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ (๋งค๋‹ˆ์ €+) GET /jobs/{id} # ํ”„๋กœ์ ํŠธ ์ƒ์„ธ (์‚ฌ์šฉ์ž) PUT /jobs/{id} # ํ”„๋กœ์ ํŠธ ์ˆ˜์ • (๋งค๋‹ˆ์ €+) DELETE /jobs/{id} # ํ”„๋กœ์ ํŠธ ์‚ญ์ œ (๊ด€๋ฆฌ์ž) GET /jobs/stats # ํ”„๋กœ์ ํŠธ ํ†ต๊ณ„ (๋งค๋‹ˆ์ €+) POST /jobs/{id}/assign # ๋‹ด๋‹น์ž ํ• ๋‹น (๋งค๋‹ˆ์ €+) ``` **์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ:** ```json // GET /jobs/ - ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก { "success": true, "total_count": 2, "jobs": [ { "job_no": "J24-001", "job_name": "์šธ์‚ฐ SK์—๋„ˆ์ง€ ์ •์œ ์‹œ์„ค ์ฆ์„ค ๋ฐฐ๊ด€๊ณต์‚ฌ", "project_name": "์šธ์‚ฐ SK์—๋„ˆ์ง€ ์ •์œ ์‹œ์„ค ์ฆ์„ค ๋ฐฐ๊ด€๊ณต์‚ฌ", "client_name": "์‚ผ์„ฑ์—”์ง€๋‹ˆ์–ด๋ง", "end_user": "SK์—๋„ˆ์ง€", "epc_company": "์‚ผ์„ฑ์—”์ง€๋‹ˆ์–ด๋ง", "project_site": "์šธ์‚ฐ๊ด‘์—ญ์‹œ ์˜จ์‚ฐ๊ณต๋‹จ", "contract_date": "2024-03-15", "delivery_date": "2024-08-30", "delivery_terms": "FOB ์šธ์‚ฐํ•ญ", "project_type": "๋ƒ‰๋™๊ธฐ", "status": "์ง„ํ–‰์ค‘", "description": "์ •์œ ์‹œ์„ค ์ฆ์„ค์„ ์œ„ํ•œ ๋ฐฐ๊ด€ ์ž์žฌ ๊ณต๊ธ‰", "created_at": "2025-07-15T03:44:46.035325" } ] } ``` #### **๐Ÿ“„ ํŒŒ์ผ/์ž์žฌ ๊ด€๋ฆฌ API (`/files/`)** ```http GET /files # ํŒŒ์ผ ๋ชฉ๋ก (์‚ฌ์šฉ์ž) POST /files/upload # ํŒŒ์ผ ์—…๋กœ๋“œ (์„ค๊ณ„์ž+) โญ ์‚ฌ์šฉ์ž ์ถ”์  DELETE /files/delete/{file_id} # ํŒŒ์ผ ์‚ญ์ œ (์„ค๊ณ„์ž+) GET /files/stats # ํŒŒ์ผ/์ž์žฌ ํ†ต๊ณ„ (์‚ฌ์šฉ์ž) GET /files/materials-v2 # ์ž์žฌ ๋ชฉ๋ก (์‚ฌ์šฉ์ž) โญ ์ตœ์‹  ๋ฒ„์ „ GET /files/materials/summary # ์ž์žฌ ์š”์•ฝ ํ†ต๊ณ„ (์‚ฌ์šฉ์ž) GET /files/materials/compare-revisions # ๋ฆฌ๋น„์ „ ๋น„๊ต (์‚ฌ์šฉ์ž) GET /files/pipe-details # ํŒŒ์ดํ”„ ์ƒ์„ธ ์ •๋ณด (์‚ฌ์šฉ์ž) GET /files/fitting-details # ํ”ผํŒ… ์ƒ์„ธ ์ •๋ณด (์‚ฌ์šฉ์ž) GET /files/valve-details # ๋ฐธ๋ธŒ ์ƒ์„ธ ์ •๋ณด (์‚ฌ์šฉ์ž) POST /files/user-requirements # ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ƒ์„ฑ (์‚ฌ์šฉ์ž) GET /files/user-requirements # ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์กฐํšŒ (์‚ฌ์šฉ์ž) POST /files/materials/{id}/verify # ์ž์žฌ ๋ถ„๋ฅ˜ ๊ฒ€์ฆ (์„ค๊ณ„์ž+) PUT /files/materials/{id}/update-classification # ์ž์žฌ ๋ถ„๋ฅ˜ ์ˆ˜์ • (์„ค๊ณ„์ž+) POST /files/materials/confirm-purchase # ์ž์žฌ ๊ตฌ๋งค ํ™•์ • (๊ตฌ๋งค์ž+) ``` **โš ๏ธ ์ค‘์š”: ์ž์žฌ API ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ** - โœ… **์‚ฌ์šฉ**: `/files/materials-v2` (์ตœ์‹  ๋ฒ„์ „, ๋ชจ๋“  ๊ธฐ๋Šฅ ์ง€์›) - โŒ **์‚ฌ์šฉ ๊ธˆ์ง€**: `/files/materials` (์กด์žฌํ•˜์ง€ ์•Š์Œ, 404 ์˜ค๋ฅ˜ ๋ฐœ์ƒ) - ๐Ÿ”„ **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜**: ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ `fetchMaterials()` ํ•จ์ˆ˜ ์‚ฌ์šฉ ๊ถŒ์žฅ #### **๐Ÿ”ง ์ž์žฌ ๋ถ„๋ฅ˜/๋น„๊ต API (`/materials/`)** ```http POST /materials/compare-revisions # ๋ฆฌ๋น„์ „ ๋น„๊ต (์„ค๊ณ„์ž+) โญ ์‚ฌ์šฉ์ž ์ถ”์  GET /materials/comparison-history # ๋น„๊ต ์ด๋ ฅ ์กฐํšŒ (์‚ฌ์šฉ์ž) GET /materials/inventory-status # ์žฌ๊ณ  ํ˜„ํ™ฉ (๊ตฌ๋งค์ž+) POST /materials/confirm-purchase # ๊ตฌ๋งค ํ™•์ • (๊ตฌ๋งค์ž+) โญ ์‚ฌ์šฉ์ž ์ถ”์  GET /materials/purchase-status # ๊ตฌ๋งค ์ƒํƒœ (๊ตฌ๋งค์ž+) ``` #### **๐Ÿ›’ ๊ตฌ๋งค ๊ด€๋ฆฌ API (`/purchase/`)** ```http GET /purchase/items/calculate # ๊ตฌ๋งค ํ’ˆ๋ชฉ ๊ณ„์‚ฐ (๊ตฌ๋งค์ž+) POST /purchase/confirm # ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ํ™•์ • (๊ตฌ๋งค์ž+) โญ ์‚ฌ์šฉ์ž ์ถ”์  POST /purchase/items/save # ๊ตฌ๋งค ํ’ˆ๋ชฉ ์ €์žฅ (๊ตฌ๋งค์ž+) GET /purchase/items # ๊ตฌ๋งค ํ’ˆ๋ชฉ ๋ชฉ๋ก (๊ตฌ๋งค์ž+) GET /purchase/revision-diff # ๋ฆฌ๋น„์ „ ์ฐจ์ด (๊ตฌ๋งค์ž+) POST /purchase/orders/create # ๊ตฌ๋งค ์ฃผ๋ฌธ ์ƒ์„ฑ (๊ตฌ๋งค์ž+) โญ ์‚ฌ์šฉ์ž ์ถ”์  GET /purchase/orders # ๊ตฌ๋งค ์ฃผ๋ฌธ ๋ชฉ๋ก (๊ตฌ๋งค์ž+) ``` #### **๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ API (`/dashboard/`)** โญ ์‹ ๊ทœ (2025.01) ```http GET /dashboard/stats # ์‚ฌ์šฉ์ž๋ณ„ ๋งž์ถค ํ†ต๊ณ„ (์ธ์ฆ ํ•„์š”) GET /dashboard/activities # ์‚ฌ์šฉ์ž ํ™œ๋™ ์ด๋ ฅ (์ธ์ฆ ํ•„์š”) GET /dashboard/recent-activities # ์ „์ฒด ์ตœ๊ทผ ํ™œ๋™ (๋งค๋‹ˆ์ €+) GET /dashboard/quick-actions # ์—ญํ• ๋ณ„ ๋น ๋ฅธ ์ž‘์—… (์ธ์ฆ ํ•„์š”) ``` **์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ:** ```json // GET /dashboard/stats - ์‚ฌ์šฉ์ž๋ณ„ ๋งž์ถค ํ†ต๊ณ„ { "success": true, "user_role": "admin", "stats": { "total_projects": 45, "active_users": 12, "system_status": "์ •์ƒ", "today_uploads": 8 // ์ฃผ์˜: quickActions, metrics ๋“ฑ์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ชฉ ๋ฐ์ดํ„ฐ๋กœ ๋ณด์™„๋จ } } // GET /dashboard/activities - ์‚ฌ์šฉ์ž ํ™œ๋™ ์ด๋ ฅ { "success": true, "activities": [ { "id": 1, "activity_type": "FILE_UPLOAD", "activity_description": "ํŒŒ์ผ ์—…๋กœ๋“œ: ProjectX_Rev0.xlsx", "created_at": "2025-08-30T08:30:00Z", "target_id": 123, "target_type": "FILE" } ] } ``` #### **๐Ÿ”ง ํŠœ๋น™ ์‹œ์Šคํ…œ API (`/tubing/`)** ```http GET /tubing/categories # ํŠœ๋น™ ์นดํ…Œ๊ณ ๋ฆฌ (์‚ฌ์šฉ์ž) GET /tubing/manufacturers # ์ œ์กฐ์‚ฌ ๋ชฉ๋ก (์‚ฌ์šฉ์ž) GET /tubing/specifications # ์‚ฌ์–‘ ๋ชฉ๋ก (์‚ฌ์šฉ์ž) GET /tubing/products # ํŠœ๋น™ ์ œํ’ˆ ๋ชฉ๋ก (์‚ฌ์šฉ์ž) POST /tubing/products # ํŠœ๋น™ ์ œํ’ˆ ์ƒ์„ฑ (์„ค๊ณ„์ž+) POST /tubing/material-mapping # ์ž์žฌ-ํŠœ๋น™ ๋งคํ•‘ ์ƒ์„ฑ (์„ค๊ณ„์ž+) GET /tubing/material-mappings/{material_id} # ์ž์žฌ๋ณ„ ํŠœ๋น™ ๋งคํ•‘ ์กฐํšŒ (์‚ฌ์šฉ์ž) GET /tubing/search # ํŠœ๋น™ ์ œํ’ˆ ๊ฒ€์ƒ‰ (์‚ฌ์šฉ์ž) ``` --- ### ๐Ÿ“ **API ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ๋ผ์ธ** (2025.01 ์‹ ๊ทœ) #### **1. ์ƒˆ API ๋ชจ๋“ˆ ์ถ”๊ฐ€ ์ ˆ์ฐจ** ```python # 1. ๋ผ์šฐํ„ฐ ํŒŒ์ผ ์ƒ์„ฑ # backend/app/routers/new_module.py from fastapi import APIRouter, Depends, HTTPException from ..auth.middleware import get_current_user from ..services.activity_logger import log_activity_from_request router = APIRouter(prefix="/new-module", tags=["new-module"]) @router.post("/action") async def new_action( request: Request, current_user: dict = Depends(get_current_user), db: Session = Depends(get_db) ): # ์‚ฌ์šฉ์ž ์ถ”์  ํ•„์ˆ˜ log_activity_from_request( db, request, current_user['username'], "NEW_ACTION", "์ƒˆ ์•ก์…˜ ์‹คํ–‰" ) # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง... ``` ```python # 2. main.py์— ๋ผ์šฐํ„ฐ ๋“ฑ๋ก try: from .routers import new_module app.include_router(new_module.router, tags=["new-module"]) except ImportError: logger.warning("new_module ๋ผ์šฐํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") ``` ```markdown # 3. RULES.md ์—…๋ฐ์ดํŠธ (์ด ๋ฌธ์„œ) #### **๐Ÿ†• ์ƒˆ ๋ชจ๋“ˆ API (`/new-module/`)** GET /new-module/list # ๋ชฉ๋ก ์กฐํšŒ (์‚ฌ์šฉ์ž) POST /new-module/action # ์•ก์…˜ ์‹คํ–‰ (๊ถŒํ•œ) โญ ์‚ฌ์šฉ์ž ์ถ”์  ``` #### **2. API ์‘๋‹ต ํ‘œ์ค€ ํ˜•์‹** ```json // ์„ฑ๊ณต ์‘๋‹ต { "success": true, "message": "์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", "data": { ... }, "timestamp": "2025-01-XX 12:00:00" } // ์—๋Ÿฌ ์‘๋‹ต { "success": false, "error": "์—๋Ÿฌ ๋ฉ”์‹œ์ง€", "error_code": "ERROR_CODE", "detail": "์ƒ์„ธ ์—๋Ÿฌ ์ •๋ณด" } ``` #### **2-1. ์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ ๋ฌธ์„œํ™” ๊ทœ์น™** โญ ์ค‘์š” - **๋ชจ๋“  API๋Š” ์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ RULES.md์— ๋ช…์‹œ ํ•„์ˆ˜** - **ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ ์‹œ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋„๋ก JSON ์˜ˆ์‹œ ํฌํ•จ** - **ํ•„๋“œ๋ช…, ๋ฐ์ดํ„ฐ ํƒ€์ž…, ์ค‘์ฒฉ ๊ตฌ์กฐ ๋ชจ๋‘ ์ •ํ™•ํžˆ ๊ธฐ๋ก** - **๋ชฉ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ ์‹œ ์ฃผ์„์œผ๋กœ ๋ช…์‹œ** - **API ๋ณ€๊ฒฝ ์‹œ ๋ฌธ์„œ๋„ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ** **๋ฌธ์„œํ™” ์˜ˆ์‹œ:** ```json // GET /jobs/ - ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก (์‹ค์ œ ์‘๋‹ต) { "success": true, "total_count": 2, "jobs": [ { "job_no": "J24-001", "project_name": "ํ”„๋กœ์ ํŠธ๋ช…", "status": "์ง„ํ–‰์ค‘", "client_name": "๊ณ ๊ฐ์‚ฌ๋ช…" // ... ๋ชจ๋“  ํ•„๋“œ ๋ช…์‹œ } ] } ``` #### **3. ๊ถŒํ•œ ๋ ˆ๋ฒจ ์ •์˜** - **๊ณต๊ฐœ**: ์ธ์ฆ ๋ถˆํ•„์š” - **์‚ฌ์šฉ์ž**: ๋กœ๊ทธ์ธํ•œ ๋ชจ๋“  ์‚ฌ์šฉ์ž - **์„ค๊ณ„์ž+**: designer, manager, admin - **๊ตฌ๋งค์ž+**: purchaser, manager, admin - **๋งค๋‹ˆ์ €+**: manager, admin - **๊ด€๋ฆฌ์ž**: admin๋งŒ #### **4. ์‚ฌ์šฉ์ž ์ถ”์  ํ•„์ˆ˜ API** โญ ๋‹ค์Œ ์ž‘์—…์€ ๋ฐ˜๋“œ์‹œ ํ™œ๋™ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•ด์•ผ ํ•จ: - ํŒŒ์ผ ์—…๋กœ๋“œ/์‚ญ์ œ - ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ - ๊ตฌ๋งค ํ™•์ •/์ฃผ๋ฌธ ์ƒ์„ฑ - ์ž์žฌ ๋ถ„๋ฅ˜/๊ฒ€์ฆ - ์‹œ์Šคํ…œ ์„ค์ • ๋ณ€๊ฒฝ #### **5. API ๋ฒ„์ „ ๊ด€๋ฆฌ** ```http # ํ˜„์žฌ ๋ฒ„์ „ (๊ธฐ๋ณธ) GET /files/upload # ์ƒˆ ๋ฒ„์ „ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) GET /v2/files/upload # ํ—ค๋” ๊ธฐ๋ฐ˜ ๋ฒ„์ „ ๊ด€๋ฆฌ GET /files/upload Accept: application/vnd.tkmp.v2+json ``` #### **6. ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ** - **ํŽ˜์ด์ง€๋„ค์ด์…˜**: ๋ชฉ๋ก API๋Š” limit/offset ์ง€์› - **ํ•„ํ„ฐ๋ง**: ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•„ํ„ฐ ์กฐ๊ฑด ์ œ๊ณต - **์บ์‹ฑ**: ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” Redis ์บ์‹ฑ - **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ฒ˜๋ฆฌ๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… --- ### ๐Ÿ”„ **API ๋ณ€๊ฒฝ ์ด๋ ฅ** (2025.01) #### **v2.2.0 (2025.09.05)** โญ ์ตœ์‹  - โœ… **์ •๋ฆฌ**: API ์—”๋“œํฌ์ธํŠธ ํ‘œ์ค€ํ™” ๋ฐ ํ†ตํ•ฉ - โœ… **๋ฌธ์„œํ™”**: ์ „์ฒด API ๋งต ์—…๋ฐ์ดํŠธ (์‹ค์ œ ๊ตฌํ˜„ ๊ธฐ์ค€) - โœ… **๊ฐœ์„ **: ํ”„๋ก ํŠธ์—”๋“œ API ํ˜ธ์ถœ ํ‘œ์ค€ํ™” (`fetchMaterials` ํ•จ์ˆ˜ ์‚ฌ์šฉ) - โœ… **์ˆ˜์ •**: `/files/materials` โ†’ `/files/materials-v2` ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ - โœ… **์ถ”๊ฐ€**: API ์‚ฌ์šฉ ๊ฐ€์ด๋“œ๋ผ์ธ ๋ฐ ํ˜ผ๋™ ๋ฐฉ์ง€ ๊ทœ์น™ - โœ… **์ถ”๊ฐ€**: ํŠœ๋น™ ์‹œ์Šคํ…œ API ๋ฌธ์„œํ™” #### **v2.1.1 (2025.08.30)** - โœ… **๋ฌธ์„œํ™”**: `/jobs/` API ์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ ๋ช…์‹œ - โœ… **๋ฌธ์„œํ™”**: `/dashboard/` API ์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ ๋ช…์‹œ - โœ… **๊ฐœ์„ **: ํ”„๋ก ํŠธ์—”๋“œ-๋ฐฑ์—”๋“œ API ์‘๋‹ต ๊ตฌ์กฐ ๋ถˆ์ผ์น˜ ํ•ด๊ฒฐ - โœ… **์ถ”๊ฐ€**: ์‹ค์ œ ์‘๋‹ต ๊ตฌ์กฐ ๋ฌธ์„œํ™” ๊ทœ์น™ ๋ฐ ๊ฐ€์ด๋“œ๋ผ์ธ #### **v2.1.0 (2025.01.XX)** - โœ… **์ถ”๊ฐ€**: `/dashboard/` API ๋ชจ๋“ˆ (์‚ฌ์šฉ์ž๋ณ„ ๋งž์ถค ๋Œ€์‹œ๋ณด๋“œ) - โœ… **๊ฐœ์„ **: ๋ชจ๋“  ์—…๋กœ๋“œ/์ˆ˜์ • API์— ์‚ฌ์šฉ์ž ์ถ”์  ์ถ”๊ฐ€ - โœ… **๋ณ€๊ฒฝ**: `/files/upload` - `uploaded_by` ํ•„๋“œ ํ•„์ˆ˜ํ™” - โš ๏ธ **์ค‘๋‹จ ์˜ˆ์ •**: `/old-endpoint` (v3.0์—์„œ ์ œ๊ฑฐ ์˜ˆ์ •) #### **v2.0.0 (2025.01.XX)** - โœ… **์ถ”๊ฐ€**: ์ธ์ฆ ์‹œ์Šคํ…œ (`/auth/`) ์™„์ „ ๊ตฌํ˜„ - โœ… **์ถ”๊ฐ€**: ์‚ฌ์šฉ์ž ํ™œ๋™ ๋กœ๊ทธ ์‹œ์Šคํ…œ - โœ… **๋ณ€๊ฒฝ**: ๋ชจ๋“  API์— JWT ํ† ํฐ ์ธ์ฆ ์ ์šฉ - ๐Ÿ”„ **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜**: ๊ธฐ์กด API ํ˜ธ์ถœ ์‹œ Authorization ํ—ค๋” ํ•„์ˆ˜ --- ### ๐Ÿ“‹ **๊ฐœ๋ฐœ์ž ์ฒดํฌ๋ฆฌ์ŠคํŠธ** ์ƒˆ API ๊ฐœ๋ฐœ ์‹œ ๋‹ค์Œ ์‚ฌํ•ญ์„ ํ™•์ธ: - [ ] **๋ฌธ์„œํ™”**: RULES.md API ๋งต์— ์ถ”๊ฐ€ - [ ] **์ธ์ฆ**: ์ ์ ˆํ•œ ๊ถŒํ•œ ๋ ˆ๋ฒจ ์„ค์ • - [ ] **์ถ”์ **: ์ค‘์š” ์ž‘์—…์€ ํ™œ๋™ ๋กœ๊ทธ ๊ธฐ๋ก - [ ] **๊ฒ€์ฆ**: ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ (Pydantic) - [ ] **์—๋Ÿฌ**: ํ‘œ์ค€ ์—๋Ÿฌ ์‘๋‹ต ํ˜•์‹ ์ค€์ˆ˜ - [ ] **ํ…Œ์ŠคํŠธ**: ๋‹จ์œ„/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ - [ ] **๋กœ๊น…**: ์ ์ ˆํ•œ ๋กœ๊ทธ ๋ ˆ๋ฒจ๋กœ ๊ธฐ๋ก - [ ] **์„ฑ๋Šฅ**: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๊ณ ๋ ค ### ๐Ÿ“Š ๋ฐ์ดํ„ฐ ํ๋ฆ„๋„ ```mermaid sequenceDiagram participant U as User participant F as Frontend participant A as Auth API participant B as BOM API participant D as Database participant R as Redis Cache U->>F: ๋กœ๊ทธ์ธ ์š”์ฒญ F->>A: POST /auth/login A->>D: ์‚ฌ์šฉ์ž ๊ฒ€์ฆ A->>F: JWT ํ† ํฐ ๋ฐ˜ํ™˜ F->>F: ํ† ํฐ ์ €์žฅ (localStorage) U->>F: BOM ํŽ˜์ด์ง€ ์ ‘๊ทผ F->>B: GET /jobs/ (ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก) B->>D: ํ”„๋กœ์ ํŠธ ์กฐํšŒ B->>F: ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ U->>F: ํ”„๋กœ์ ํŠธ ์„ ํƒ F->>B: GET /files?job_no=xxx B->>R: ์บ์‹œ ํ™•์ธ alt ์บ์‹œ ํžˆํŠธ R->>B: ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ else ์บ์‹œ ๋ฏธ์Šค B->>D: ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ B->>R: ์บ์‹œ ์ €์žฅ end B->>F: ํŒŒ์ผ ๋ชฉ๋ก ๋ฐ˜ํ™˜ U->>F: ์ž์žฌ ํ™•์ธ ํด๋ฆญ F->>B: GET /files/materials?file_id=xxx B->>D: ์ž์žฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ B->>F: ์ž์žฌ ๋ชฉ๋ก ๋ฐ˜ํ™˜ ``` ### ๐Ÿ” ๊ถŒํ•œ ์ฒด๊ณ„ ```mermaid graph TD A[์‚ฌ์šฉ์ž] --> B{์—ญํ• } B -->|admin| C[์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž] B -->|user| D[์ผ๋ฐ˜ ์‚ฌ์šฉ์ž] B -->|viewer| E[์กฐํšŒ ์ „์šฉ] C --> F[๋ชจ๋“  ๊ถŒํ•œ] D --> G[BOM ๊ด€๋ฆฌ] D --> H[ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ] E --> I[์กฐํšŒ๋งŒ ๊ฐ€๋Šฅ] F --> J[์‚ฌ์šฉ์ž ๊ด€๋ฆฌ] F --> K[์‹œ์Šคํ…œ ์„ค์ •] G --> L[ํŒŒ์ผ ์—…๋กœ๋“œ] G --> M[์ž์žฌ ๊ด€๋ฆฌ] H --> N[ํ”„๋กœ์ ํŠธ CRUD] ``` --- ## ๐Ÿณ Docker ์‹คํ–‰ ํ™˜๊ฒฝ ### ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์„ฑ - **tk-mp-frontend**: React + Nginx (ํฌํŠธ: 3000/13000) - **tk-mp-backend**: FastAPI + Uvicorn (ํฌํŠธ: 8000/18000) - **tk-mp-postgres**: PostgreSQL (ํฌํŠธ: 5432) - **tk-mp-redis**: Redis (ํฌํŠธ: 6379) - **tk-mp-pgadmin**: pgAdmin4 (ํฌํŠธ: 5050) ### ์‹คํ–‰ ๋ฐฉ๋ฒ• ```bash # ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ./scripts/dev.sh # ๋˜๋Š” docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d # ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ./scripts/prod.sh # ๋˜๋Š” docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d # ์‹œ๋†€๋กœ์ง€ NAS ํ™˜๊ฒฝ docker-compose -f docker-compose.synology.yml up -d # ๊ธฐ๋ณธ ํ™˜๊ฒฝ docker-compose up -d ``` ### ์ค‘์š”ํ•œ ์„ค์ • ์‚ฌํ•ญ - **API URL ์„ค์ •**: - ๊ฐœ๋ฐœํ™˜๊ฒฝ: `http://localhost:8000` (์ง์ ‘ ์ ‘๊ทผ) - ํ”„๋กœ๋•์…˜: `/api` (nginx ํ”„๋ก์‹œ๋ฅผ ํ†ตํ•œ ์ƒ๋Œ€๊ฒฝ๋กœ) - **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ**: `postgres:5432` (์ปจํ…Œ์ด๋„ˆ๋ช… ์‚ฌ์šฉ, localhost ์•„๋‹˜!) - **ํ™˜๊ฒฝ๋ณ€์ˆ˜**: `VITE_API_URL`๋กœ API URL ์˜ค๋ฒ„๋ผ์ด๋“œ ๊ฐ€๋Šฅ --- ## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ``` TK-MP-Project/ โ”œโ”€โ”€ README.md โ”œโ”€โ”€ docker-compose.yml โ”œโ”€โ”€ docker-compose.dev.yml โ”œโ”€โ”€ docker-compose.prod.yml โ”œโ”€โ”€ docker-compose.synology.yml โ”œโ”€โ”€ frontend/ # React ํ”„๋ก ํŠธ์—”๋“œ โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”‚ โ”œโ”€โ”€ pages/ # ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # ์žฌ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ โ”‚ โ”‚ โ”œโ”€โ”€ utils/ # ์œ ํ‹ธ๋ฆฌํ‹ฐ (์—‘์…€ ๋“ฑ) โ”‚ โ”‚ โ””โ”€โ”€ api.js # API ํ†ต์‹  โ”‚ โ”œโ”€โ”€ Dockerfile โ”‚ โ””โ”€โ”€ nginx.conf โ”œโ”€โ”€ backend/ # FastAPI ๋ฐฑ์—”๋“œ โ”‚ โ”œโ”€โ”€ app/ โ”‚ โ”‚ โ”œโ”€โ”€ routers/ # API ๋ผ์šฐํ„ฐ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง (๋ถ„๋ฅ˜๊ธฐ ๋“ฑ) โ”‚ โ”‚ โ”œโ”€โ”€ models.py # DB ๋ชจ๋ธ โ”‚ โ”‚ โ”œโ”€โ”€ main.py # FastAPI ์•ฑ โ”‚ โ”‚ โ””โ”€โ”€ database.py # DB ์—ฐ๊ฒฐ ์„ค์ • โ”‚ โ”œโ”€โ”€ scripts/ # DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ โ”‚ โ”œโ”€โ”€ uploads/ # ์—…๋กœ๋“œ๋œ ํŒŒ์ผ โ”‚ โ””โ”€โ”€ requirements.txt โ”œโ”€โ”€ database/ # DB ์Šคํ‚ค๋งˆ ๋ฐ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ โ”‚ โ””โ”€โ”€ init/ โ”‚ โ”œโ”€โ”€ 01_schema.sql โ”‚ โ””โ”€โ”€ 02_seed_data.sql โ””โ”€โ”€ scripts/ # ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ โ”œโ”€โ”€ dev.sh โ”œโ”€โ”€ prod.sh โ””โ”€โ”€ deploy-synology.sh ``` --- ## ๐Ÿ—„๏ธ ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ### ํ•ต์‹ฌ ํ…Œ์ด๋ธ”๋“ค ```sql -- ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ jobs (job_no, job_name, client_name, end_user, epc_company, status, ...) -- ํŒŒ์ผ ๊ด€๋ฆฌ files (id, job_no, revision, original_filename, bom_name, parsed_count, ...) -- ์ž์žฌ ์ •๋ณด materials (id, file_id, original_description, classified_category, quantity, ...) -- ์ž์žฌ๋ณ„ ์ƒ์„ธ ์ •๋ณด pipe_details (material_id, length_mm, ...) fitting_details, flange_details, bolt_details, gasket_details, instrument_details ``` ### ์ฃผ์š” ๊ธฐ๋Šฅ - ํ”„๋กœ์ ํŠธ๋ณ„ ํŒŒ์ผ ๋ฒ„์ „ ๊ด€๋ฆฌ (Rev.0, Rev.1, Rev.2) - ์ž์žฌ ์ž๋™ ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ (์นดํ…Œ๊ณ ๋ฆฌ, ์žฌ์งˆ, ์‚ฌ์ด์ฆˆ) - ๋ถ„๋ฅ˜ ์‹ ๋ขฐ๋„ ๋ฐ ์‚ฌ์šฉ์ž ๊ฒ€์ฆ ์‹œ์Šคํ…œ --- ## ๐Ÿ”ง ์ค‘์š”ํ•œ ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜ & ํŒจํ„ด ### 1. ์ž์žฌ ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ ```python # ํ•ญ์ƒ ์ด ์ˆœ์„œ๋กœ ๋ถ„๋ฅ˜๊ธฐ ํ˜ธ์ถœ classification_result = classify_pipe("", description, main_nom, length_value) # ๊ฒฐ๊ณผ: {"category": "PIPE", "confidence": 0.95, ...} ``` ### 2. ํŒŒ์ดํ”„ ๊ธธ์ด ์ฒ˜๋ฆฌ ๊ทœ์น™ ```javascript // โŒ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ: ํ‰๊ท  ๊ธธ์ด ๊ณ„์‚ฐ/ํ‘œ์‹œ // โœ… ํ•ญ์ƒ ํ•  ๊ฒƒ: ์ด ๊ธธ์ด ๊ธฐ์ค€ ๊ณ„์‚ฐ const totalLength = quantity * unitLength; // ์ด ๊ธธ์ด = ์ˆ˜๋Ÿ‰ ร— ๋‹จ์œ„๊ธธ์ด ``` ### 3. ์ž์žฌ ํ•ด์‹ฑ ๊ทœ์น™ ```python # ์ž์žฌ ๊ณ ์œ ์„ฑ ํŒ๋‹จ: description + size + material_grade material_hash = hashlib.md5(f"{description}|{size_spec}|{material_grade}".encode()).hexdigest() ``` ### 4. ๋ฆฌ๋น„์ „ ๋น„๊ต ๋กœ์ง (2025.01 ์‹ ๊ทœ โญ) ```python # ์ด์ „ ๋ฆฌ๋น„์ „ ์ž๋™ ํƒ์ง€: ์ˆซ์ž ๊ธฐ๋ฐ˜ ๋น„๊ต current_rev_num = int(current_revision.replace("Rev.", "")) # Rev.0 โ†’ Rev.1 โ†’ Rev.2 ์ˆœ์„œ # ์ž์žฌ ํ•ด์‹ฑ ๊ทœ์น™ (RULES ์ค€์ˆ˜) material_hash = hashlib.md5(f"{description}|{size}|{material}".encode()).hexdigest() # ๋ฆฌ๋น„์ „ ๋น„๊ต ์›Œํฌํ”Œ๋กœ์šฐ if revision != "Rev.0": # ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ์ธ ๊ฒฝ์šฐ๋งŒ revision_comparison = get_revision_comparison(db, job_no, revision, materials_data) if revision_comparison.get("has_previous_confirmation"): # ๋ณ€๊ฒฝ์—†์Œ: ๊ธฐ์กด ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ ์žฌ์‚ฌ์šฉ (confidence = 1.0) # ๋ณ€๊ฒฝ๋จ + ์‹ ๊ทœ: ์žฌ๋ถ„๋ฅ˜ ํ•„์š” materials_to_classify = changed_materials + new_materials else: # ์ด์ „ ํ™•์ • ์ž๋ฃŒ ์—†์Œ: ์ „์ฒด ๋ถ„๋ฅ˜ materials_to_classify = all_materials ``` ### 5. ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ํ™•์ • ์›Œํฌํ”Œ๋กœ์šฐ (2025.01 ์‹ ๊ทœ โญ) ```python # ํ™•์ • ๋ฐ์ดํ„ฐ ์ €์žฅ ๊ตฌ์กฐ purchase_confirmations (๋งˆ์Šคํ„ฐ) โ†’ confirmed_purchase_items (์ƒ์„ธ) # ํ™•์ • ์‹œ ํŒŒ์ผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ files.purchase_confirmed = TRUE files.confirmed_at = timestamp files.confirmed_by = username # ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ ์‹œ ์ตœ์ ํ™” - ํ™•์ •๋œ ์ž๋ฃŒ ์žˆ์Œ: ๋ณ€๊ฒฝ๋œ ์ž์žฌ๋งŒ ๋ถ„๋ฅ˜ (์„ฑ๋Šฅ ํ–ฅ์ƒ) - ํ™•์ •๋œ ์ž๋ฃŒ ์—†์Œ: ์ „์ฒด ์ž์žฌ ๋ถ„๋ฅ˜ (๊ธฐ์กด ๋ฐฉ์‹) ``` ### 6. ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๊ทœ์น™ (2025.01 ์‹ ๊ทœ โญ) ```python # 413 ์˜ค๋ฅ˜ ๋ฐฉ์ง€: ์š”์ฒญ ๋ฐ์ดํ„ฐ ์ตœ์ ํ™” # โŒ ์ „์ฒด ๋ฐ์ดํ„ฐ ์ „์†ก (์šฉ๋Ÿ‰ ์ดˆ๊ณผ) purchase_items: List[dict] # ๋ชจ๋“  ํ•„๋“œ ํฌํ•จ # โœ… ํ•„์ˆ˜ ํ•„๋“œ๋งŒ ์ „์†ก (์šฉ๋Ÿ‰ ์ตœ์ ํ™”) class PurchaseItemMinimal(BaseModel): item_code: str category: str specification: str size: str = "" material: str = "" bom_quantity: float calculated_qty: float unit: str = "EA" safety_factor: float = 1.0 # ์„œ๋ฒ„ ์š”์ฒญ ํฌ๊ธฐ ์ œํ•œ ์„ค์ • app.add_middleware(RequestSizeLimitMiddleware, max_request_size=100 * 1024 * 1024) # 100MB # Nginx ํ”„๋ก์‹œ ์„ค์ • (์ค‘์š”!) server { client_max_body_size 100M; # ์ „์—ญ ์„ค์ • location /api/ { proxy_pass http://backend:8000/; client_max_body_size 100M; # API ๊ฒฝ๋กœ๋ณ„ ์„ค์ • proxy_request_buffering off; # ๋Œ€์šฉ๋Ÿ‰ ์š”์ฒญ ์ตœ์ ํ™” } } ``` --- ## ๐Ÿ“ **์ฝ”๋“œ ๋ถ„๋ฆฌ ๊ธฐ์ค€ ๋ฐ ํ’ˆ์งˆ ๊ฐ€์ด๋“œ๋ผ์ธ** ### ๐ŸŽฏ **์ฝ”๋“œ ๊ธธ์ด ๊ธฐ์ค€ (2025.01 ์ˆ˜๋ฆฝ)** #### **ํ•จ์ˆ˜/๋ฉ”์„œ๋“œ** - **์ด์ƒ์ **: 10-20์ค„ - **ํ—ˆ์šฉ ๊ฐ€๋Šฅ**: 30์ค„ ์ดํ•˜ - **๋ฆฌํŒฉํ† ๋ง ํ•„์š”**: 50์ค„ ์ด์ƒ #### **ํŒŒ์ผ** - **์ด์ƒ์ **: 200-300์ค„ - **ํ—ˆ์šฉ ๊ฐ€๋Šฅ**: 500์ค„ ์ดํ•˜ - **๋ถ„๋ฆฌ ํ•„์š”**: 800์ค„ ์ด์ƒ #### **ํด๋ž˜์Šค** - **์ด์ƒ์ **: 100-200์ค„ - **ํ—ˆ์šฉ ๊ฐ€๋Šฅ**: 300์ค„ ์ดํ•˜ - **๋ถ„๋ฆฌ ํ•„์š”**: 500์ค„ ์ด์ƒ ### ๐Ÿ”ง **๋ฆฌํŒฉํ† ๋ง ์›์น™** #### **1. ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ (SRP)** ```python # โŒ ๋‚˜์œ ์˜ˆ: ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๊ฐ€ ์—ฌ๋Ÿฌ ์ผ์„ ํ•จ def process_file_and_save_and_classify(file): # ํŒŒ์ผ ์ฒ˜๋ฆฌ + ์ €์žฅ + ๋ถ„๋ฅ˜ (3๊ฐ€์ง€ ์ฑ…์ž„) pass # โœ… ์ข‹์€ ์˜ˆ: ๊ฐ๊ฐ ๋ถ„๋ฆฌ def process_file(file): pass def save_file(file): pass def classify_materials(materials): pass ``` #### **2. ํ•จ์ˆ˜ ๋ถ„๋ฆฌ ๊ธฐ์ค€** - ์ค‘๋ณต ์ฝ”๋“œ 3ํšŒ ์ด์ƒ โ†’ ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌ - ์กฐ๊ฑด๋ฌธ์ด 3๋‹จ๊ณ„ ์ด์ƒ ์ค‘์ฒฉ โ†’ ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌ - ํ•œ ํ•จ์ˆ˜์—์„œ 5๊ฐœ ์ด์ƒ ๋ณ€์ˆ˜ ์‚ฌ์šฉ โ†’ ํด๋ž˜์Šค ๊ณ ๋ ค #### **3. ํŒŒ์ผ ๋ถ„๋ฆฌ ๊ธฐ์ค€** ```python # ๊ธฐ๋Šฅ๋ณ„ ๋ถ„๋ฆฌ ์˜ˆ์‹œ routers/ # API ์—”๋“œํฌ์ธํŠธ โ”œโ”€โ”€ files.py # ํŒŒ์ผ ๊ด€๋ จ API โ”œโ”€โ”€ jobs.py # ์ž‘์—… ๊ด€๋ จ API โ””โ”€โ”€ materials.py # ์ž์žฌ ๊ด€๋ จ API services/ # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง โ”œโ”€โ”€ classifiers/ # ๋ถ„๋ฅ˜๊ธฐ๋“ค โ”œโ”€โ”€ validators/ # ๊ฒ€์ฆ ๋กœ์ง โ””โ”€โ”€ processors/ # ์ฒ˜๋ฆฌ ๋กœ์ง utils/ # ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ โ”œโ”€โ”€ logger.py # ๋กœ๊น… โ”œโ”€โ”€ validators.py # ๊ฒ€์ฆ โ””โ”€โ”€ helpers.py # ํ—ฌํผ ํ•จ์ˆ˜ ``` ### ๐Ÿ›ก๏ธ **๋ณด์•ˆ ์ฝ”๋”ฉ ๊ฐ€์ด๋“œ๋ผ์ธ** #### **1. ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ** ```python # โœ… ํ•„์ˆ˜ ๊ฒ€์ฆ ํ•ญ๋ชฉ - ํŒŒ์ผ ํ™•์žฅ์ž ๊ฒ€์ฆ - ํŒŒ์ผ ํฌ๊ธฐ ์ œํ•œ (50MB) - MIME ํƒ€์ž… ๊ฒ€์ฆ (์‹ค์ œ ๋‚ด์šฉ ํ™•์ธ) - ํŒŒ์ผ๋ช… ๋ณด์•ˆ ๊ฒ€์ฆ (์œ„ํ—˜ ๋ฌธ์ž ์ฐจ๋‹จ) - ์—…๋กœ๋“œ ๊ฒฝ๋กœ ์ œํ•œ ``` #### **2. CORS ์„ค์ •** ```python # โŒ ์ ˆ๋Œ€ ๊ธˆ์ง€: ์šด์˜ ํ™˜๊ฒฝ์—์„œ ๋ชจ๋“  ๋„๋ฉ”์ธ ํ—ˆ์šฉ allow_origins=["*"] # โœ… ํ™˜๊ฒฝ๋ณ„ ์ œํ•œ๋œ ๋„๋ฉ”์ธ๋งŒ ํ—ˆ์šฉ CORS_ORIGINS = { "development": ["http://localhost:3000", "http://localhost:5173"], "production": ["https://your-domain.com"], "synology": ["http://192.168.0.3:10173"] } ``` #### **3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ณด์•ˆ** ```python # โŒ ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋…ธ์ถœ ๊ธˆ์ง€ return {"error": f"Database connection failed: {db_password}"} # โœ… ์•ˆ์ „ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ return {"error": "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."} # ์ƒ์„ธ ์—๋Ÿฌ๋Š” ๋กœ๊ทธ์—๋งŒ ๊ธฐ๋ก logger.error(f"DB connection failed: {detailed_error}") ``` ### ๐Ÿ“Š **๋กœ๊น… ๊ฐ€์ด๋“œ๋ผ์ธ** #### **1. ๋กœ๊ทธ ๋ ˆ๋ฒจ ์‚ฌ์šฉ๋ฒ•** ```python logger.debug("๋””๋ฒ„๊น… ์ •๋ณด (๊ฐœ๋ฐœ ์‹œ์—๋งŒ)") logger.info("์ผ๋ฐ˜์ ์ธ ์ •๋ณด (์ •์ƒ ๋™์ž‘)") logger.warning("์ฃผ์˜๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ") logger.error("์—๋Ÿฌ ๋ฐœ์ƒ (๋ณต๊ตฌ ๊ฐ€๋Šฅ)") logger.critical("์‹ฌ๊ฐํ•œ ์—๋Ÿฌ (์‹œ์Šคํ…œ ์ค‘๋‹จ)") ``` #### **2. ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ํ˜•์‹** ```python # โœ… ์ข‹์€ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ logger.info(f"ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ - ํŒŒ์ผ๋ช…: {filename}, ํฌ๊ธฐ: {file_size} bytes, ์‚ฌ์šฉ์ž: {user_id}") logger.error(f"์ž์žฌ ๋ถ„๋ฅ˜ ์‹คํŒจ - ํŒŒ์ผID: {file_id}, ์—๋Ÿฌ: {error_msg}", exc_info=True) # โŒ ๋‚˜์œ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ logger.info("ํŒŒ์ผ ์—…๋กœ๋“œ๋จ") logger.error("์—๋Ÿฌ ๋ฐœ์ƒ") ``` #### **3. ๋ฏผ๊ฐ ์ •๋ณด ๋กœ๊น… ๊ธˆ์ง€** ```python # โŒ ์ ˆ๋Œ€ ๋กœ๊น…ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ์ •๋ณด - ๋น„๋ฐ€๋ฒˆํ˜ธ, API ํ‚ค - ๊ฐœ์ธ์ •๋ณด (์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ) - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด # โœ… ๋กœ๊น…ํ•ด๋„ ๋˜๋Š” ์ •๋ณด - ํŒŒ์ผ๋ช…, ํŒŒ์ผ ํฌ๊ธฐ - ์ž‘์—… ID, ์‚ฌ์šฉ์ž ID (ํ•ด์‹œ๋œ ๊ฐ’) - ์ฒ˜๋ฆฌ ์‹œ๊ฐ„, ์ƒํƒœ ์ •๋ณด ``` --- ## ๐Ÿ› ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ & ํ•ด๊ฒฐ๋ฒ• ### 1. ํŒŒ์ดํ”„ ๊ธธ์ด ํ•ฉ์‚ฐ ๋ฌธ์ œ ```python # โŒ ์ž˜๋ชป๋œ SQL: GROUP BY์— pd.length_mm ํฌํ•จ # โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: Python์—์„œ ๊ฐ™์€ ํŒŒ์ดํ”„๋“ค ํ•ฉ์น˜๊ธฐ if material_hash in materials_dict: existing["quantity"] += float(new_quantity) existing["total_length"] += new_quantity * unit_length ``` ### 2. ํ”„๋ก ํŠธ์—”๋“œ ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™” ```javascript // โŒ ์‚ฌ์šฉ ์ „์— ์„ ์–ธํ•˜์ง€ ์•Š์Œ const summaryData = [..., consolidatedMaterials.length, ...]; const consolidatedMaterials = consolidateMaterials(materials); // ๋’ค์— ์„ ์–ธ // โœ… ์‚ฌ์šฉ ์ „์— ๋จผ์ € ์„ ์–ธ const consolidatedMaterials = consolidateMaterials(materials); const summaryData = [..., consolidatedMaterials.length, ...]; ``` ### 3. API ์‘๋‹ต ์ฒ˜๋ฆฌ ```javascript // โœ… ํ•ญ์ƒ Axios ์‘๋‹ต ๊ตฌ์กฐ ํ™•์ธ setComparisonResult(result.data || result); // response.data ์šฐ์„  ``` --- ## ๐ŸŽฏ UI/UX ๊ฐ€์ด๋“œ๋ผ์ธ ### 1. ์ž์žฌ ํ‘œ์‹œ ๊ทœ์น™ - **ํŒŒ์ดํ”„**: "์ด ๊ธธ์ด: 4,561mm" (ํ‰๊ท ๋‹จ์œ„ ํ‘œ์‹œ ๊ธˆ์ง€) - **๊ธฐํƒ€ ์ž์žฌ**: "์ˆ˜๋Ÿ‰: 24 EA" - **๋ณ€๊ฒฝ์‚ฌํ•ญ**: "์ด์ „: 2,781mm โ†’ ํ˜„์žฌ: 4,561mm / ๋ณ€ํ™”: +1,780mm" ### 2. ๋ฒ„ํŠผ ๋„ค์ด๋ฐ - "BOM ๋ชฉ๋ก์œผ๋กœ" (๋’ค๋กœ๊ฐ€๊ธฐ) - "์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ" - "์ƒ์„ธ ๋น„๊ต ๋ณด๊ธฐ" ### 3. ํŽ˜์ด์ง€ ๋„ค๋น„๊ฒŒ์ด์…˜ ```javascript // BOM ๊ด€๋ จ ํŽ˜์ด์ง€๋“ค์€ job_no ๊ธฐ์ค€์œผ๋กœ ์ด๋™ navigate(`/bom-status?job_no=${jobNo}`); navigate(`/material-comparison?job_no=${jobNo}&revision=${revision}`); ``` ### 4. ํ”„๋กœ์ ํŠธ ์ค‘์‹ฌ ์›Œํฌํ”Œ๋กœ์šฐ (2025.01 ์‹ ๊ทœ) โญ #### **๊ธฐ๋ณธ ์›์น™** - **ํ”„๋กœ์ ํŠธ ์šฐ์„ **: ์‚ฌ์šฉ์ž๋Š” ๋จผ์ € ํ”„๋กœ์ ํŠธ๋ฅผ ์„ ํƒํ•˜๊ณ , ๊ทธ ๋‹ค์Œ์— ์—…๋ฌด๋ฅผ ์„ ํƒ - **์ปจํ…์ŠคํŠธ ์œ ์ง€**: ์„ ํƒ๋œ ํ”„๋กœ์ ํŠธ ์ •๋ณด๋Š” ๋ชจ๋“  ํ•˜์œ„ ํŽ˜์ด์ง€์—์„œ ์œ ์ง€ - **๊ถŒํ•œ ๊ธฐ๋ฐ˜ ๋ฉ”๋‰ด**: ์‚ฌ์šฉ์ž ์—ญํ• ์— ๋”ฐ๋ผ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์—…๋ฌด๋งŒ ํ‘œ์‹œ #### **์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌ์กฐ** ``` ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ โ†’ ํ”„๋กœ์ ํŠธ ์„ ํƒ โ†’ ํ”„๋กœ์ ํŠธ ์›Œํฌ์ŠคํŽ˜์ด์Šค โ†’ ์—…๋ฌด ์ง„ํ–‰ ``` #### **์ฃผ์š” ์ปดํฌ๋„ŒํŠธ** **1. ProjectSelector (ํ”„๋กœ์ ํŠธ ์„ ํƒ๊ธฐ)** - ๋“œ๋กญ๋‹ค์šด ํ˜•ํƒœ์˜ ํ”„๋กœ์ ํŠธ ์„ ํƒ UI - ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ง€์› (ํ”„๋กœ์ ํŠธ๋ช…, Job ๋ฒˆํ˜ธ) - ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ ๋ฐ ์ƒํƒœ ํ‘œ์‹œ - ์„ ํƒ๋œ ํ”„๋กœ์ ํŠธ ์ •๋ณด ํ•˜์ด๋ผ์ดํŠธ **2. ProjectWorkspacePage (ํ”„๋กœ์ ํŠธ ์›Œํฌ์ŠคํŽ˜์ด์Šค)** - ํ”„๋กœ์ ํŠธ๋ณ„ ๋งž์ถค ๋Œ€์‹œ๋ณด๋“œ - ๊ถŒํ•œ ๊ธฐ๋ฐ˜ ์—…๋ฌด ๋ฉ”๋‰ด (์นด๋“œ ํ˜•ํƒœ) - ํ”„๋กœ์ ํŠธ ํ†ต๊ณ„ ๋ฐ ์ตœ๊ทผ ํ™œ๋™ ํ‘œ์‹œ - ๋น ๋ฅธ ์ž‘์—… ๋ฒ„ํŠผ **3. ๊ถŒํ•œ๋ณ„ ์—…๋ฌด ๋ฉ”๋‰ด** ```javascript // ์„ค๊ณ„์ž ์—…๋ฌด - BOM ํŒŒ์ผ ์—…๋กœ๋“œ - BOM ๊ด€๋ฆฌ - ์ž์žฌ ๋ถ„๋ฅ˜ ๊ฒ€์ฆ // ๊ตฌ๋งค์ž ์—…๋ฌด - ๊ตฌ๋งค ๊ด€๋ฆฌ - ๋ฆฌ๋น„์ „ ๋น„๊ต - ๊ตฌ๋งค ํ™•์ • // ๊ณตํ†ต ์—…๋ฌด - ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ - ๋ฆฌํฌํŠธ ์ƒ์„ฑ ``` #### **์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ ์‚ฌํ•ญ** - **์ง๊ด€์  ํ๋ฆ„**: ์‹ค์ œ ์—…๋ฌด ํ๋ฆ„๊ณผ ์ผ์น˜ํ•˜๋Š” ๋„ค๋น„๊ฒŒ์ด์…˜ - **์ปจํ…์ŠคํŠธ ์ธ์‹**: ํ”„๋กœ์ ํŠธ ์ •๋ณด๊ฐ€ ์ž๋™์œผ๋กœ ์ „๋‹ฌ๋จ - **ํšจ์œจ์„ฑ ์ฆ๋Œ€**: ๋ถˆํ•„์š”ํ•œ ํ”„๋กœ์ ํŠธ ์„ ํƒ ๋‹จ๊ณ„ ์ œ๊ฑฐ - **์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ**: ์„ ํƒ๋œ ํ”„๋กœ์ ํŠธ์™€ ์ง„ํ–‰๋ฅ  ์‹œ๊ฐํ™” --- ## ๐Ÿ”„ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ### โญ 1. ๋„์ปค ์‹คํ–‰ (๊ถŒ์žฅ - ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ๊ณผ ๋™์ผ) ```bash # TK-MP-Project ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ์‹คํ–‰ docker-compose up -d # ๋กœ๊ทธ ํ™•์ธ docker-compose logs -f # ์„œ๋น„์Šค ์žฌ์‹œ์ž‘ (์ฝ”๋“œ ๋ณ€๊ฒฝ ์‹œ) docker-compose restart # ์™„์ „ ์žฌ๋นŒ๋“œ (Dockerfile ๋ณ€๊ฒฝ ์‹œ) docker-compose down docker-compose up --build -d ``` **๋„์ปค ์ ‘์† ์ฃผ์†Œ:** - ํ”„๋ก ํŠธ์—”๋“œ: http://localhost:13000 - ๋ฐฑ์—”๋“œ API: http://localhost:18000 - API ๋ฌธ์„œ: http://localhost:18000/docs - PostgreSQL: localhost:5432 - Redis: localhost:6379 - pgAdmin: http://localhost:5050 ### 2. ๋กœ์ปฌ ๊ฐœ๋ฐœ ์‹คํ–‰ (๊ฐœ๋ฐœ/๋””๋ฒ„๊น… ์ „์šฉ) ```bash # ๋ฐฑ์—”๋“œ ์‹คํ–‰ (ํ„ฐ๋ฏธ๋„ 1๋ฒˆ) - TK-MP-Project ๋ฃจํŠธ์—์„œ source venv/bin/activate # ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” (venv๋Š” ๋ฃจํŠธ์— ์žˆ์Œ) cd backend python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 18000 # ํ”„๋ก ํŠธ์—”๋“œ ์‹คํ–‰ (ํ„ฐ๋ฏธ๋„ 2๋ฒˆ) - TK-MP-Project ๋ฃจํŠธ์—์„œ cd frontend npm run dev # npm start ์•„๋‹˜! ``` **๋กœ์ปฌ ๊ฐœ๋ฐœ ์ ‘์† ์ฃผ์†Œ:** - ๋ฐฑ์—”๋“œ API: http://localhost:18000 - API ๋ฌธ์„œ: http://localhost:18000/docs - ํ”„๋ก ํŠธ์—”๋“œ: http://localhost:13000 (ํฌํŠธ ์ถฉ๋Œ ์‹œ ์ž๋™ ๋ณ€๊ฒฝ๋จ) ### 3. ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ ์‹œ ```bash # ๋„์ปค ํ™˜๊ฒฝ (๊ถŒ์žฅ) docker-compose restart backend # ๋กœ์ปฌ ํ™˜๊ฒฝ (๋””๋ฒ„๊น…์šฉ) cd backend python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 18000 ``` ### 3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์‹œ ```sql -- scripts/ ํด๋”์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ SQL ํŒŒ์ผ ์ƒ์„ฑ -- ๋ฒˆํ˜ธ ์ˆœ์„œ: 01_, 02_, 03_... ``` ### 4. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ``` ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑ (์‚ฌ์šฉ์ž ์„ ํ˜ธ์‚ฌํ•ญ) ์˜ˆ: "ํŒŒ์ดํ”„ ๊ธธ์ด ๊ณ„์‚ฐ ๋ฐ ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„๊ทธ ์ˆ˜์ •" ``` ### โš ๏ธ ์ค‘์š”: ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ ํƒ ๊ฐ€์ด๋“œ #### ๐Ÿณ ๋„์ปค ํ™˜๊ฒฝ (๊ถŒ์žฅ) - **์‚ฌ์šฉ ์‹œ๊ธฐ**: ์ผ๋ฐ˜์ ์ธ ๊ฐœ๋ฐœ, ํ…Œ์ŠคํŠธ, ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ - **์žฅ์ **: ํ™˜๊ฒฝ ์ผ๊ด€์„ฑ, NAS ๋ฐฐํฌ์™€ ๋™์ผํ•œ ํ™˜๊ฒฝ - **๋‹จ์ **: ๋””๋ฒ„๊น…์ด ์•ฝ๊ฐ„ ๋ณต์žก #### ๐Ÿ’ป ๋กœ์ปฌ ํ™˜๊ฒฝ (์ œํ•œ์  ์‚ฌ์šฉ) - **์‚ฌ์šฉ ์‹œ๊ธฐ**: ๋ฐฑ์—”๋“œ ๋””๋ฒ„๊น…, ์ƒˆ๋กœ์šด ํŒจํ‚ค์ง€ ํ…Œ์ŠคํŠธ - **์žฅ์ **: ๋น ๋ฅธ ๋””๋ฒ„๊น…, IDE ํ†ตํ•ฉ - **๋‹จ์ **: ํ™˜๊ฒฝ ์ฐจ์ด๋กœ ์ธํ•œ ๋ฐฐํฌ ๋ฌธ์ œ ๊ฐ€๋Šฅ์„ฑ --- ## ๐Ÿ’ฐ ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ ๊ทœ์น™ ### 1. ํŒŒ์ดํ”„ (PIPE) ```javascript // 6,000mm ๋‹จ์œ„ ํŒ๋งค + ์ ˆ๋‹จ์—ฌ์œ ๋ถ„ 2mm/์กฐ๊ฐ const cutLength = originalLength + 2; // ์ ˆ๋‹จ ์—ฌ์œ ๋ถ„ const pipeCount = Math.ceil(cutLength / 6000); // ์˜ฌ๋ฆผ ์ฒ˜๋ฆฌ ``` ### 2. ํ”ผํŒ…/๊ณ„๊ธฐ/๋ฐธ๋ธŒ (FITTING/INSTRUMENT/VALVE) ```javascript // BOM ์ˆ˜๋Ÿ‰ ๊ทธ๋Œ€๋กœ const purchaseQuantity = bomQuantity; ``` ### 3. ๋ณผํŠธ/๋„ˆํŠธ (BOLT) ```javascript // +5% ํ›„ 4์˜ ๋ฐฐ์ˆ˜๋กœ ์˜ฌ๋ฆผ const withMargin = bomQuantity * 1.05; const purchaseQuantity = Math.ceil(withMargin / 4) * 4; // ์˜ˆ: 150 โ†’ 157.5 โ†’ 160 SETS ``` ### 4. ๊ฐ€์Šค์ผ“ (GASKET) ```javascript // 5์˜ ๋ฐฐ์ˆ˜๋กœ ์˜ฌ๋ฆผ const purchaseQuantity = Math.ceil(bomQuantity / 5) * 5; // ์˜ˆ: 7 โ†’ 10 EA ``` --- ## โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง์•„์•ผ ํ•  ๊ฒƒ๋“ค 1. **ํŒŒ์ดํ”„ "ํ‰๊ท ๋‹จ์œ„" ํ‘œ์‹œ** - ์‚ฌ์šฉ์ž๊ฐ€ ํ˜ผ๋ž€์Šค๋Ÿฌ์›Œํ•จ 2. **ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๊ธธ์ด ๊ฐ’** - ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ’ ์‚ฌ์šฉ 3. **์˜์–ด ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€** - ์‚ฌ์šฉ์ž๊ฐ€ ํ•œ๊ตญ์–ด ์„ ํ˜ธ 4. **SQL์—์„œ ๊ณผ๋„ํ•œ GROUP BY** - ๊ฐ™์€ ์ž์žฌ ๋ถ„๋ฆฌ๋จ 5. **๋น„์œจ ๊ธฐ๋ฐ˜ ๊ธธ์ด ๊ณ„์‚ฐ** - ์‹ค์ œ ์ด๊ธธ์ด ์‚ฌ์šฉํ•ด์•ผ ํ•จ --- ## ๐Ÿš€ ๊ฐœ๋ฐœ ๋กœ๋“œ๋งต ### Phase 1: ๊ธฐ๋ฐ˜ ์‹œ์Šคํ…œ ๊ตฌ์ถ• โœ… (์™„๋ฃŒ) - [x] Git ํ™˜๊ฒฝ ๊ตฌ์ถ• - [x] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„ - [x] Docker ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ • - [x] FastAPI ๊ธฐ๋ณธ ๊ตฌ์กฐ ๊ตฌํ˜„ - [x] ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ํŒŒ์‹ฑ ๊ธฐ๋Šฅ ### Phase 2: ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ โœ… (์™„๋ฃŒ) - [x] ์ž์žฌ ๋ถ„๋ฅ˜ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ - [x] ์›น ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์ถ• - [x] ๊ตฌ๋งค BOM ์ƒ์„ฑ ๊ธฐ๋Šฅ - [x] ๋ฆฌ๋น„์ „ ๋น„๊ต ๊ธฐ๋Šฅ - [x] ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ ### Phase 3: ๊ณ ๋„ํ™” ๐Ÿšง (์ง„ํ–‰ ์ค‘) - [x] ๋ฆฌ๋น„์ „ ๋น„๊ต ๊ธฐ๋Šฅ - [x] ํŒŒ์ดํ”„ cutting ์ž๋ฃŒ ์ƒ์„ฑ - [ ] ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ ์‹œ์Šคํ…œ ์™„์„ฑ - [ ] ํŠœ๋น™ ์‹œ์Šคํ…œ ์™„์„ฑ - [ ] ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” --- ## ๐ŸŽฏ ํ˜„์žฌ ์ง„ํ–‰ ์ƒํ™ฉ ### โœ… ์™„๋ฃŒ๋œ ๊ธฐ๋Šฅ๋“ค - ์ž์žฌ ์—…๋กœ๋“œ ๋ฐ ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ (ํŒŒ์ดํ”„, ํ”ผํŒ…, ๋ณผํŠธ, ๋ฐธ๋ธŒ, ๊ฐ€์Šค์ผ“, ๊ณ„๊ธฐ๋ฅ˜) - ๋ฆฌ๋น„์ „ ๋น„๊ต ๊ธฐ๋Šฅ - ํŒŒ์ดํ”„ ๊ธธ์ด ํ•ฉ์‚ฐ ๋กœ์ง ์ˆ˜์ • - ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ - Docker ํ™˜๊ฒฝ ๊ตฌ์„ฑ (๊ฐœ๋ฐœ/ํ”„๋กœ๋•์…˜/์‹œ๋†€๋กœ์ง€) ### ๐Ÿšง ์ง„ํ–‰ ์ค‘์ธ ๊ธฐ๋Šฅ๋“ค - ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ ์‹œ์Šคํ…œ - ํŠœ๋น™ ์‹œ์Šคํ…œ ### ๐Ÿ”’ **Phase 1: ๋ณด์•ˆ & ์•ˆ์ •์„ฑ ๊ฐœ์„  ์™„๋ฃŒ (2025.01)** #### **1. CORS ์„ค์ • ํ™˜๊ฒฝ๋ณ„ ๋ถ„๋ฆฌ** โœ… - **ํŒŒ์ผ**: `backend/app/config.py` - ์ค‘์•™ํ™”๋œ ์„ค์ • ๊ด€๋ฆฌ - **ํ™˜๊ฒฝ๋ณ€์ˆ˜**: `backend/env.example` - ์„ค์ • ๊ฐ€์ด๋“œ - **๋ณด์•ˆ ๊ฐ•ํ™”**: `allow_origins=["*"]` โ†’ ํ™˜๊ฒฝ๋ณ„ ์ œํ•œ๋œ ๋„๋ฉ”์ธ #### **2. ๋กœ๊น… ์‹œ์Šคํ…œ ๊ตฌ์ถ•** โœ… - **ํŒŒ์ผ**: `backend/app/utils/logger.py` - ๊ตฌ์กฐํ™”๋œ ๋กœ๊น… - **๊ธฐ๋Šฅ**: ํŒŒ์ผ ๋กœํ…Œ์ด์…˜, ๋ ˆ๋ฒจ๋ณ„ ๋กœ๊น…, ์ค‘์•™ํ™”๋œ ๋กœ๊ฑฐ ๊ด€๋ฆฌ - **์ ์šฉ**: print() โ†’ logger๋กœ ๊ต์ฒด (446๊ฐœ print๋ฌธ ์ค‘ ์ฃผ์š” ๋ถ€๋ถ„) #### **3. ์ฝ”๋“œ ๋ถ„๋ฆฌ ๋ฐ ๋ฆฌํŒฉํ† ๋ง** โœ… - **๋ถ„๋ฆฌ ๊ธฐ์ค€**: ํ•จ์ˆ˜ 20์ค„, ํŒŒ์ผ 300์ค„, ํด๋ž˜์Šค 200์ค„ - **ํŒŒ์ผ**: `backend/app/api/file_management.py` - ํŒŒ์ผ API ๋ถ„๋ฆฌ - **๊ตฌ์กฐ ๊ฐœ์„ **: main.py ๊ธฐ๋Šฅ๋ณ„ ๋ถ„๋ฆฌ๋กœ ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ #### **4. ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฒ€์ฆ ๊ฐ•ํ™”** โœ… - **ํŒŒ์ผ**: `backend/app/utils/file_validator.py` - ์ข…ํ•ฉ ํŒŒ์ผ ๊ฒ€์ฆ - **๋ณด์•ˆ ๊ธฐ๋Šฅ**: - ํŒŒ์ผ ํฌ๊ธฐ ์ œํ•œ (50MB) - ํ™•์žฅ์ž ๊ฒ€์ฆ (.xlsx, .xls, .csv) - MIME ํƒ€์ž… ๊ฒ€์ฆ (์‹ค์ œ ํŒŒ์ผ ๋‚ด์šฉ ํ™•์ธ) - ํŒŒ์ผ๋ช… ๋ณด์•ˆ ๊ฒ€์ฆ (์œ„ํ—˜ ๋ฌธ์ž ์ฐจ๋‹จ) #### **5. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ‘œ์ค€ํ™”** โœ… - **ํŒŒ์ผ**: `backend/app/utils/error_handlers.py` - ํ‘œ์ค€ํ™”๋œ ์—๋Ÿฌ ์‘๋‹ต - **๊ธฐ๋Šฅ**: - ์ปค์Šคํ…€ ์˜ˆ์™ธ ํด๋ž˜์Šค (TKMPException) - ํ‘œ์ค€ํ™”๋œ ์—๋Ÿฌ ์‘๋‹ต ํ˜•์‹ - ์ž๋™ ์—๋Ÿฌ ํ•ธ๋“ค๋ง (๊ฒ€์ฆ, DB, ์ผ๋ฐ˜ ์˜ˆ์™ธ) ### ๐Ÿ“Š ๊ตฌํ˜„๋œ ํŽ˜์ด์ง€๋“ค - MainPage: ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ - JobSelectionPage: ํ”„๋กœ์ ํŠธ ์„ ํƒ - JobRegistrationPage: ํ”„๋กœ์ ํŠธ ๋“ฑ๋ก - BOMStatusPage: BOM ์ƒํƒœ ๊ด€๋ฆฌ - MaterialsPage: ์ž์žฌ ๋ชฉ๋ก - MaterialComparisonPage: ๋ฆฌ๋น„์ „ ๋น„๊ต - PurchaseConfirmationPage: ๊ตฌ๋งค ํ™•์ธ - RevisionPurchasePage: ๋ฆฌ๋น„์ „๋ณ„ ๊ตฌ๋งค --- ## ๐ŸŒ ์‹œ๋†€๋กœ์ง€ NAS ๋ฐฐํฌ ๊ฐ€์ด๋“œ โญ ### ๐Ÿณ ๋„์ปค ๊ธฐ๋ฐ˜ ๋ฐฐํฌ (๊ถŒ์žฅ) #### ์„œ๋น„์Šค ๊ตฌ์„ฑ - **ํ”„๋ก ํŠธ์—”๋“œ**: React + Nginx (ํฌํŠธ 13000) - **๋ฐฑ์—”๋“œ**: FastAPI + Uvicorn (ํฌํŠธ 18000) - **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค**: PostgreSQL (ํฌํŠธ 5432) - **์บ์‹œ**: Redis (ํฌํŠธ 6379) - **๊ด€๋ฆฌ๋„๊ตฌ**: pgAdmin4 (ํฌํŠธ 5050) #### ๋ฐฐํฌ ๋ช…๋ น์–ด ```bash # 1. ํ”„๋กœ์ ํŠธ ํŒŒ์ผ์„ NAS๋กœ ๋ณต์‚ฌ scp -r TK-MP-Project/ admin@[NAS_IP]:/volume1/docker/ # 2. NAS SSH ์ ‘์† ssh admin@[NAS_IP] # 3. ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™ cd /volume1/docker/TK-MP-Project/ # 4. ๋„์ปค ์ปดํฌ์ฆˆ ์‹คํ–‰ docker-compose up -d # 5. ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ docker-compose ps ``` #### ์ ‘์† ์ฃผ์†Œ (NAS IP ๊ธฐ์ค€) - **ํ”„๋ก ํŠธ์—”๋“œ**: http://[NAS_IP]:13000 - **๋ฐฑ์—”๋“œ API**: http://[NAS_IP]:18000 - **API ๋ฌธ์„œ**: http://[NAS_IP]:18000/docs - **pgAdmin**: http://[NAS_IP]:5050 #### ์ž๋™ ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ (๊ณง ๊ตฌํ˜„ ์˜ˆ์ •) ```bash ./deploy-synology.sh [NAS_IP] ``` ### ์ฃผ์˜์‚ฌํ•ญ 1. **ํฌํŠธ ์ถฉ๋Œ**: NAS์—์„œ 13000, 18000 ํฌํŠธ๊ฐ€ ์‚ฌ์šฉ ์ค‘์ด์ง€ ์•Š์€์ง€ ํ™•์ธ 2. **๊ถŒํ•œ**: Docker ๋ช…๋ น์–ด๋Š” `sudo` ๊ถŒํ•œ ํ•„์š” 3. **๋ฐฉํ™”๋ฒฝ**: DSM ์ œ์–ดํŒ์—์„œ ํ•ด๋‹น ํฌํŠธ ํ—ˆ์šฉ ์„ค์ • 4. **๋ฆฌ์†Œ์Šค**: ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ™•์ธ --- ## ๐Ÿšจ Docker ์‹คํ–‰ ๊ด€๋ จ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ### ํ•ด๊ฒฐ๋œ ์ฃผ์š” ๋ฌธ์ œ๋“ค (2025.08.01) 1. **ํ”„๋ก ํŠธ์—”๋“œ API ์—ฐ๊ฒฐ ์˜ค๋ฅ˜** - **๋ฌธ์ œ**: ๋นŒ๋“œ๋œ ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ 10080 ํฌํŠธ๋กœ API ์š”์ฒญ - **์›์ธ**: ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • ๋ˆ„๋ฝ์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํฌํŠธ ์‚ฌ์šฉ - **ํ•ด๊ฒฐ**: ```bash # Dockerfile์—์„œ ๋นŒ๋“œ ์‹œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฃผ์ž… ARG VITE_API_URL=http://localhost:8000 ENV VITE_API_URL=$VITE_API_URL # docker-compose.yml์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • environment: - VITE_API_URL=${VITE_API_URL:-/api} ``` 2. **๋ฐฑ์—”๋“œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ** - **๋ฌธ์ œ**: `psycopg2.OperationalError` - localhost:5432 ์—ฐ๊ฒฐ ๊ฑฐ๋ถ€ - **์›์ธ**: ๋ฐฑ์—”๋“œ๊ฐ€ localhost๋กœ DB ์ ‘๊ทผ ์‹œ๋„ (Docker ์ปจํ…Œ์ด๋„ˆ ๋‚ด์—์„œ๋Š” ๋ถˆ๊ฐ€) - **ํ•ด๊ฒฐ**: ```python # backend/app/database.py ์ˆ˜์ • DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql://tkmp_user:tkmp_password_2025@postgres:5432/tk_mp_bom" # localhost โ†’ postgres ) ``` ### ์‹คํ–‰ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - [ ] Docker ๋ฐ Docker Compose ์„ค์น˜ ํ™•์ธ - [ ] ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ ์ •์ƒ ์‹คํ–‰ ํ™•์ธ: `docker-compose ps` - [ ] ๋ฐฑ์—”๋“œ API ๋ฌธ์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅ: http://localhost:8000/docs - [ ] ํ”„๋ก ํŠธ์—”๋“œ ๋กœ๋”ฉ ํ™•์ธ: http://localhost:3000 - [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ™•์ธ: pgAdmin (http://localhost:5050) --- ## ๐Ÿ“š ๋ฐฑ์—”๋“œ ๊ฐœ์„ /ํ™•์žฅ/์šด์˜ ๊ถŒ์žฅ์‚ฌํ•ญ ### 1. ์ฝ”๋“œ ๊ตฌ์กฐ/ํ’ˆ์งˆ - ResponseModel(Pydantic) ์ ์šฉ: API ๋ฐ˜ํ™˜๊ฐ’์˜ ํƒ€์ž… ์•ˆ์ •์„ฑ ๋ฐ ๋ฌธ์„œํ™” ๊ฐ•ํ™” - ๋กœ๊น…/์—๋Ÿฌ ์ฒ˜๋ฆฌ: print โ†’ logging ๋ชจ๋“ˆ, ์šด์˜ ํ™˜๊ฒฝ์— ๋งž๋Š” ์—๋Ÿฌ/์ด๋ฒคํŠธ ๊ธฐ๋ก - ํ™˜๊ฒฝ๋ณ€์ˆ˜/์„ค์ • ๋ถ„๋ฆฌ: CORS, DB, ํฌํŠธ ๋“ฑ ํ™˜๊ฒฝ๋ณ„ ๊ด€๋ฆฌ ์šฉ์ดํ•˜๊ฒŒ ๋ถ„๋ฆฌ - ๋ผ์šฐํ„ฐ ์ž๋™ ๋“ฑ๋ก/๋™์  ๊ด€๋ฆฌ: ๋ผ์šฐํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ ๊ฒฝ์šฐ ์ฝ”๋“œ ์ค‘๋ณต ์ตœ์†Œํ™” ### 2. ๋ณด์•ˆ/์šด์˜ - CORS ์ œํ•œ: ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ํ—ˆ์šฉ origin์„ ์ œํ•œ - ์—…๋กœ๋“œ ํŒŒ์ผ ๊ฒ€์ฆ ๊ฐ•ํ™”: ๊ฒฝ๋กœ, ํŒŒ์ผ๋ช…, ํฌ๊ธฐ ๋“ฑ ๋ณด์•ˆ ๊ฒ€์ฆ ์ถ”๊ฐ€ ### 3. ์„ฑ๋Šฅ/ํ™•์žฅ์„ฑ - ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ/๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ: ๋น„๋™๊ธฐ/์ฒญํฌ ์ฒ˜๋ฆฌ, ์ธ๋ฑ์Šค ํŠœ๋‹ ๋“ฑ - DB ํŠธ๋žœ์žญ์…˜ ๋ช…ํ™•ํ™”: ํŒŒ์ผ/์ž์žฌ ์ €์žฅ ๋“ฑ์—์„œ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๊ฐ•ํ™” ### 4. ํ…Œ์ŠคํŠธ/CI - ์ž๋™ํ™” ํ…Œ์ŠคํŠธ(assert ๊ธฐ๋ฐ˜): print ์œ„์ฃผ โ†’ assert ๊ธฐ๋ฐ˜ ์ž๋™ํ™”๋กœ CI/CD ์—ฐ๋™ - ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•๋Œ€: ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ/๊ฒฝ๊ณ„ ์ผ€์ด์Šค ์ถ”๊ฐ€ ### 5. ๊ธฐํƒ€ - ์ฝ”๋“œ/์œ ํ‹ธ ํ•จ์ˆ˜ ๋ถ„๋ฆฌ: ์ค‘๋ณต ์œ ํ‹ธ ํ•จ์ˆ˜๋Š” ๋ณ„๋„ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌ - ์ƒํƒœ/ํ™œ์„ฑํ™” ๊ด€๋ฆฌ enumํ™”: status ๋“ฑ์€ enum์œผ๋กœ ๊ด€๋ฆฌ - ์‚ญ์ œ/์ˆ˜์ • API ์ถ”๊ฐ€: Job ๋“ฑ ์ฃผ์š” ์—”ํ‹ฐํ‹ฐ์˜ ๋…ผ๋ฆฌ์  ์‚ญ์ œ/์ˆ˜์ • ์ง€์› --- ## ๐Ÿ“ž ๊ฐœ๋ฐœํŒ€ ์ •๋ณด - **Lead Developer**: hyungi - **Repository**: Git ๊ธฐ๋ฐ˜ ๋ฒ„์ „ ๊ด€๋ฆฌ - **Development Environment**: VS Code + Python + Node.js --- ## ๐Ÿ“ ์ถ”๊ฐ€ ์ฐธ๊ณ ์‚ฌํ•ญ - ์‚ฌ์šฉ์ž๋Š” ๊ฐ€์ƒํ™˜๊ฒฝ์—์„œ Python ์‹คํ–‰์„ ์„ ํ˜ธ - ๋ฐฑ์—”๋“œ ์„œ๋ฒ„๋Š” ์ž๋™ ์žฌ์‹œ์ž‘๋˜๋ฏ€๋กœ ์ˆ˜๋™ ์žฌ์‹œ์ž‘ ๋ถˆํ•„์š” - ์ž‘์—… ์ƒํƒœ๋Š” 'in-progress'์™€ 'complete'๋ฅผ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ - ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€๋Š” ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑ --- --- ## ๐Ÿš€ **๋‹ค์Œ ๋‹จ๊ณ„ ๊ณ„ํš** ### โšก **Phase 2: ์„ฑ๋Šฅ ์ตœ์ ํ™” ์™„๋ฃŒ (2025.01)** โœ… #### **1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™”** โœ… - **ํŒŒ์ผ**: `backend/scripts/16_performance_indexes.sql` - 17๊ฐœ ์„ฑ๋Šฅ ์ธ๋ฑ์Šค ์ถ”๊ฐ€ - **๊ธฐ๋Šฅ**: ๋ณตํ•ฉ ์ธ๋ฑ์Šค, ๊ฒ€์ƒ‰ ์ธ๋ฑ์Šค(GIN), ์กฐ๊ฑด๋ถ€ ์ธ๋ฑ์Šค, ์™ธ๋ž˜ํ‚ค ์ธ๋ฑ์Šค - **๋ชจ๋‹ˆํ„ฐ๋ง**: ์ธ๋ฑ์Šค ์‚ฌ์šฉ๋ฅ  ๋ฐ ํ…Œ์ด๋ธ” ํฌ๊ธฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ทฐ #### **2. ์บ์‹ฑ ์ „๋žต** โœ… - **ํŒŒ์ผ**: `backend/app/utils/cache_manager.py` - Redis ์บ์‹ฑ ์‹œ์Šคํ…œ - **๊ธฐ๋Šฅ**: ํŒŒ์ผ ๋ชฉ๋ก, ์ž์žฌ ๋ชฉ๋ก, ์ž‘์—… ๋ชฉ๋ก, ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ, ํ†ต๊ณ„ ์บ์‹ฑ - **TTL ์„ค์ •**: ๋ฐ์ดํ„ฐ ์œ ํ˜•๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์บ์‹œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ #### **3. ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ฒ˜๋ฆฌ** โœ… - **ํŒŒ์ผ**: `backend/app/utils/file_processor.py` - ์ฒญํฌ ๊ธฐ๋ฐ˜ ํŒŒ์ผ ์ฒ˜๋ฆฌ - **๊ธฐ๋Šฅ**: Excel/CSV ์ฒญํฌ ์ฒ˜๋ฆฌ, DataFrame ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” - **์„ฑ๋Šฅ**: ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ 50% ๊ฐ์†Œ, ์ฒ˜๋ฆฌ ์†๋„ 30% ํ–ฅ์ƒ #### **4. API ์‘๋‹ต ๋ชจ๋ธ** โœ… - **ํŒŒ์ผ**: `backend/app/schemas/response_models.py` - ํ‘œ์ค€ํ™”๋œ ์‘๋‹ต ๋ชจ๋ธ - **๊ธฐ๋Šฅ**: Pydantic ๊ธฐ๋ฐ˜ ํƒ€์ž… ์•ˆ์ „์„ฑ, ์ž๋™ ๋ฌธ์„œํ™”, ์ผ๊ด€๋œ API ์‘๋‹ต ### ๐Ÿ”ง **Phase 3: ์ฝ”๋“œ ํ’ˆ์งˆ ํ–ฅ์ƒ ์™„๋ฃŒ (2025.01)** โœ… #### **1. ํ…Œ์ŠคํŠธ ์ž๋™ํ™”** โœ… - **๋””๋ ‰ํ† ๋ฆฌ**: `backend/tests/` - ์ž๋™ํ™” ํ…Œ์ŠคํŠธ ๊ตฌ์ถ• - **ํŒŒ์ผ**: `conftest.py`, `test_file_management.py`, `test_classifiers.py` - **๊ธฐ๋Šฅ**: ๋‹จ์œ„/ํ†ตํ•ฉ/์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ, 80% ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ #### **2. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ์ฒด๊ณ„ํ™”** โœ… - **ํŒŒ์ผ**: `backend/app/config.py` - ๊ตฌ์กฐํ™”๋œ ์„ค์ • ๊ด€๋ฆฌ - **๊ธฐ๋Šฅ**: ํ™˜๊ฒฝ๋ณ„ ์ž๋™ ์„ค์ •, Pydantic ๊ฒ€์ฆ, ์„ค์ • ์ค‘์•™ํ™” #### **3. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ถ„๋ฆฌ** โœ… - **ํŒŒ์ผ**: `backend/app/services/file_service.py` - ์„œ๋น„์Šค ๋ ˆ์ด์–ด - **๊ธฐ๋Šฅ**: API-Service-Data ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ, ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ #### **4. ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๊ฐ•ํ™”** โœ… - **ํŒŒ์ผ**: `backend/app/utils/transaction_manager.py` - ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ - **๊ธฐ๋Šฅ**: ์ปจํ…์ŠคํŠธ ๋งค๋‹ˆ์ €, ์„ธ์ด๋ธŒํฌ์ธํŠธ, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ, ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ### ๐ŸŽฏ **Phase 4: ํ”„๋กœ์ ํŠธ ์ž…๋ ฅ ํผ ๊ฐœ์„  ์™„๋ฃŒ (2025.01)** โœ… #### **1. ํ”„๋กœ์ ํŠธ ์œ ํ˜• ๊ฐœ์„ ** โœ… - **๋ณ€๊ฒฝ ์ „**: ํ”Œ๋žœํŠธ, ๊ฑด์ถ•, ์ธํ”„๋ผ, ์œ ์ง€๋ณด์ˆ˜, ๊ธฐํƒ€ - **๋ณ€๊ฒฝ ํ›„**: ๋ƒ‰๋™๊ธฐ, BOG, ๋‹ค์ด์•„ํ”„๋žŒ, ๋“œ๋ผ์ด์–ด - **๊ธฐ๋Šฅ**: ๋™์  ์ถ”๊ฐ€/์‚ญ์ œ ๊ฐ€๋Šฅ, ์‚ฌ์šฉ์ž ์ •์˜ ์œ ํ˜• ์ง€์› #### **2. ๋‚ ์งœ ํ•„๋“œ ๋ช…์นญ ๋ณ€๊ฒฝ** โœ… - **์‹œ์ž‘์ผ** โ†’ **์ˆ˜์ฃผ์ผ** (contract_date) - **์ข…๋ฃŒ์ผ** โ†’ **๋‚ฉ๊ธฐ์ผ** (delivery_date) - **๊ฒ€์ฆ**: ๋‚ฉ๊ธฐ์ผ์ด ์ˆ˜์ฃผ์ผ ์ดํ›„์ธ์ง€ ํ™•์ธ #### **3. ๋‚ฉํ’ˆ ๋ฐฉ๋ฒ• ์ถ”๊ฐ€** โœ… - **์ƒˆ ํ•„๋“œ**: delivery_terms (๋‚ฉํ’ˆ ๋ฐฉ๋ฒ•) - **์˜ต์…˜**: FOB, CIF, EXW, DDP, ์ง์ ‘๋‚ฉํ’ˆ, ํƒ๋ฐฐ, ๊ธฐํƒ€ - **๊ตญ์ œ ๋ฌด์—ญ ์กฐ๊ฑด**: ํ‘œ์ค€ ์ธ์ฝ”ํ…€์ฆˆ ์ง€์› #### **4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์—…๋ฐ์ดํŠธ** โœ… - **ํŒŒ์ผ**: `backend/scripts/17_add_project_type_column.sql` - **๋ณ€๊ฒฝ**: jobs ํ…Œ์ด๋ธ”์— project_type ์ปฌ๋Ÿผ ์ถ”๊ฐ€ - **์ธ๋ฑ์Šค**: ํ”„๋กœ์ ํŠธ ์œ ํ˜•๋ณ„ ์กฐํšŒ ์„ฑ๋Šฅ ํ–ฅ์ƒ #### **5. UI/UX ๊ฐœ์„ ** โœ… - **ํ”„๋กœ์ ํŠธ ์œ ํ˜• ๊ด€๋ฆฌ**: + / - ๋ฒ„ํŠผ์œผ๋กœ ๋™์  ๊ด€๋ฆฌ - **๋ฐ˜์‘ํ˜• ๋””์ž์ธ**: ๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ ์ตœ์ ํ™” - **์‚ฌ์šฉ์ž ๊ฒฝํ—˜**: ์ง๊ด€์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค ### ๐Ÿ—๏ธ **Phase 5: ์ „์‚ฌ์  ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ํ™•์žฅ ๊ณ„ํš (2025.01 ์ˆ˜๋ฆฝ)** ๐ŸŽฏ #### **๐Ÿ“‹ ์‹œ์Šคํ…œ ํ™•์žฅ ๋ชฉํ‘œ** - **์ตœ์ข… ๋ชฉํ‘œ**: ํ”„๋กœ์ ํŠธ ๋“ฑ๋ก๋ถ€ํ„ฐ ์ถœํ•˜๊นŒ์ง€ ์ „ ๊ณต์ • ๊ด€๋ฆฌ - **์‚ฌ์šฉ์ž ๊ทœ๋ชจ**: ์ „์ฒด 50๋ช… (๋™์‹œ ์ ‘์† 10-15๋ช… ์˜ˆ์ƒ) - **์•„ํ‚คํ…์ฒ˜**: ๋ชจ๋“ˆํ™” ๊ธฐ๋ฐ˜ ํ™•์žฅํ˜• ์‹œ์Šคํ…œ #### **๐Ÿ›๏ธ ๋ชจ๋“ˆํ™” ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„** ##### **ํ•ต์‹ฌ ๋ชจ๋“ˆ ๊ตฌ์กฐ** ``` TK-ERP-System/ โ”œโ”€โ”€ ๐Ÿ” ์ธ์ฆ ๋ชจ๋“ˆ (Auth Module) โ”‚ โ”œโ”€โ”€ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ, ๊ถŒํ•œ ์ œ์–ด โ”‚ โ””โ”€โ”€ JWT ๊ธฐ๋ฐ˜ ํ† ํฐ ์ธ์ฆ โ”œโ”€โ”€ ๐Ÿ“‹ BOM ๊ด€๋ฆฌ ๋ชจ๋“ˆ (ํ˜„์žฌ TK-MP) โ”‚ โ”œโ”€โ”€ ์ž์žฌ ๋ถ„๋ฅ˜, BOM ์ƒ์„ฑ โ”‚ โ””โ”€โ”€ ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ, ํŒŒ์ผ ์ฒ˜๋ฆฌ โ”œโ”€โ”€ ๐Ÿ—๏ธ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ๋ชจ๋“ˆ (์‹ ๊ทœ) โ”‚ โ”œโ”€โ”€ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ, ์ผ์ • ๊ด€๋ฆฌ โ”‚ โ””โ”€โ”€ ์ง„ํ–‰๋ฅ  ์ถ”์ , ๋งˆ์ผ์Šคํ†ค โ”œโ”€โ”€ ๐Ÿ’ฐ ๊ฒฌ์ /๊ณ„์•ฝ ๊ด€๋ฆฌ ๋ชจ๋“ˆ (์‹ ๊ทœ) โ”‚ โ”œโ”€โ”€ ๊ฒฌ์ ์„œ ์ƒ์„ฑ, ๊ณ„์•ฝ ๊ด€๋ฆฌ โ”‚ โ””โ”€โ”€ ๊ฐ€๊ฒฉ ์ •์ฑ…, ์Šน์ธ ์›Œํฌํ”Œ๋กœ์šฐ โ”œโ”€โ”€ ๐Ÿ“ฆ ๊ตฌ๋งค/์กฐ๋‹ฌ ๊ด€๋ฆฌ ๋ชจ๋“ˆ (์‹ ๊ทœ) โ”‚ โ”œโ”€โ”€ ๋ฐœ์ฃผ, ์ž…๊ณ  ๊ด€๋ฆฌ โ”‚ โ””โ”€โ”€ ๊ณต๊ธ‰์—…์ฒด ๊ด€๋ฆฌ, ์žฌ๊ณ  ์ถ”์  โ”œโ”€โ”€ ๐Ÿญ ์ƒ์‚ฐ ๊ด€๋ฆฌ ๋ชจ๋“ˆ (์‹ ๊ทœ) โ”‚ โ”œโ”€โ”€ ์ž‘์—… ์ง€์‹œ, ๊ณต์ • ๊ด€๋ฆฌ โ”‚ โ””โ”€โ”€ ํ’ˆ์งˆ ๊ด€๋ฆฌ, ์ง„ํ–‰ ์ƒํ™ฉ โ””โ”€โ”€ ๐Ÿšš ์ถœํ•˜ ๊ด€๋ฆฌ ๋ชจ๋“ˆ (์‹ ๊ทœ) โ”œโ”€โ”€ ํฌ์žฅ, ๋ฐฐ์†ก ๊ด€๋ฆฌ โ””โ”€โ”€ ๋‚ฉํ’ˆ ํ™•์ธ, ๊ณ ๊ฐ ํ”ผ๋“œ๋ฐฑ ``` ##### **๊ธฐ์ˆ  ์•„ํ‚คํ…์ฒ˜** - **API Gateway**: ํ†ตํ•ฉ ๋ผ์šฐํŒ… ๋ฐ ์ธ์ฆ - **๋ชจ๋“ˆ ๊ฐ„ ํ†ต์‹ **: REST API + ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ - **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค**: PostgreSQL ํ†ตํ•ฉ DB - **์บ์‹ฑ**: Redis ๋ถ„์‚ฐ ์บ์‹œ - **๋ฉ”์‹œ์ง€ ํ**: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ (Redis Pub/Sub) #### **๐Ÿš€ ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„ ๊ณ„ํš** ##### **Phase 5.1: ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ•** (1๊ฐœ์›”) ๐Ÿ”„ ###### **TK-FB-Project ์ธ์ฆ ์‹œ์Šคํ…œ ๋ถ„์„ ์™„๋ฃŒ** โœ… - **์•„ํ‚คํ…์ฒ˜**: Controller โ†’ Service โ†’ Model โ†’ Database ๊ณ„์ธตํ™” ๊ตฌ์กฐ - **JWT ํ† ํฐ**: Access Token (24h) + Refresh Token (7d) - **RBAC ๊ถŒํ•œ**: admin, system, leader, support, user (5๋‹จ๊ณ„) - **๋ณด์•ˆ ๊ธฐ๋Šฅ**: ๊ณ„์ • ์ž ๊ธˆ, ๋กœ๊ทธ์ธ ์ด๋ ฅ, bcrypt ํ•ด์‹ฑ ###### **๊ตฌํ˜„ ์ˆœ์„œ ๋ฐ ์ƒ์„ธ ๊ณ„ํš** **Step 1: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์ƒ์„ฑ** (3์ผ) ```sql -- ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ” CREATE TABLE users ( user_id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(100) NOT NULL, email VARCHAR(100), role VARCHAR(20) DEFAULT 'user', access_level VARCHAR(20) DEFAULT 'worker', is_active BOOLEAN DEFAULT true, failed_login_attempts INT DEFAULT 0, locked_until TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- ๋กœ๊ทธ์ธ ์ด๋ ฅ ํ…Œ์ด๋ธ” CREATE TABLE login_logs ( log_id SERIAL PRIMARY KEY, user_id INT REFERENCES users(user_id), login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ip_address VARCHAR(45), user_agent TEXT, login_status VARCHAR(20), failure_reason VARCHAR(100) ); ``` **Step 2: ๋ฐฑ์—”๋“œ ์ธ์ฆ API ๊ตฌํ˜„** (7์ผ) - `backend/app/auth/` ๋ชจ๋“ˆ ์ƒ์„ฑ - JWT ํ† ํฐ ์ƒ์„ฑ/๊ฒ€์ฆ ์„œ๋น„์Šค - ์‚ฌ์šฉ์ž ๋ชจ๋ธ ๋ฐ ์ธ์ฆ ์ปจํŠธ๋กค๋Ÿฌ - ๊ถŒํ•œ ๋ฏธ๋“ค์›จ์–ด ๊ตฌํ˜„ **Step 3: ํ”„๋ก ํŠธ์—”๋“œ ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ** (5์ผ) - ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ - JWT ํ† ํฐ ๊ด€๋ฆฌ (localStorage/sessionStorage) - ์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ (Context API) - ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ ๊ตฌํ˜„ **Step 4: ๊ถŒํ•œ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด** (7์ผ) - ์—ญํ• ๋ณ„ ๋ฉ”๋‰ด ํ‘œ์‹œ/์ˆจ๊น€ - API ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ๊ฒ€์ฆ - ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๊ตฌํ˜„ - ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ๊ธฐ๋Šฅ **Step 5: ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ํ…Œ์ŠคํŠธ** (8์ผ) - ๊ณ„์ • ์ž ๊ธˆ ๋กœ์ง - ๋กœ๊ทธ์ธ ์ด๋ ฅ ์ถ”์  - ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ - ๋ฌธ์„œํ™” ๋ฐ ๋ฐฐํฌ ##### **Phase 5.2: ๋ชจ๋“ˆ ๋ถ„๋ฆฌ** (2๊ฐœ์›”) - ํ˜„์žฌ BOM ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๋ชจ๋“ˆํ™” - ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๋ถ„๋ฆฌ - API Gateway ๊ตฌ์ถ• ##### **Phase 5.3: ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ๋ชจ๋“ˆ** (3๊ฐœ์›”) - ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ - ์ผ์ • ๊ด€๋ฆฌ ๋ฐ ๊ฐ„ํŠธ ์ฐจํŠธ - ์ง„ํ–‰๋ฅ  ๋Œ€์‹œ๋ณด๋“œ ##### **Phase 5.4: ๊ฒฌ์ /๊ณ„์•ฝ ๊ด€๋ฆฌ** (3๊ฐœ์›”) - ๊ฒฌ์ ์„œ ์ž๋™ ์ƒ์„ฑ - ๊ณ„์•ฝ ๊ด€๋ฆฌ ์›Œํฌํ”Œ๋กœ์šฐ - ์Šน์ธ ํ”„๋กœ์„ธ์Šค ##### **Phase 5.5: ๊ตฌ๋งค/์กฐ๋‹ฌ ๊ด€๋ฆฌ** (4๊ฐœ์›”) - BOM ๊ธฐ๋ฐ˜ ์ž๋™ ๋ฐœ์ฃผ - ๊ณต๊ธ‰์—…์ฒด ๊ด€๋ฆฌ - ์žฌ๊ณ  ๊ด€๋ฆฌ ์‹œ์Šคํ…œ #### **๐ŸŽฏ ์„ฑ๋Šฅ ๋ชฉํ‘œ** - **๋™์‹œ ์‚ฌ์šฉ์ž**: 15๋ช… ์ด์ƒ ์ง€์› - **์‘๋‹ต ์‹œ๊ฐ„**: API ํ‰๊ท  200ms ์ดํ•˜ - **๊ฐ€์šฉ์„ฑ**: 99.5% ์ด์ƒ - **ํ™•์žฅ์„ฑ**: ๋ชจ๋“ˆ๋ณ„ ๋…๋ฆฝ ํ™•์žฅ ๊ฐ€๋Šฅ ### ๐Ÿณ **Docker ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ฐ€์ด๋“œ๋ผ์ธ** โš ๏ธ #### **์ค‘์š”: ๋ชจ๋“  ๊ฐœ๋ฐœ์€ Docker ํ™˜๊ฒฝ์—์„œ ์ง„ํ–‰** - **์‹œ์Šคํ…œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ์‹œ**: ๋กœ์ปฌ์ด ์•„๋‹Œ **Dockerfile์— ์ถ”๊ฐ€** ํ•„์ˆ˜ - **Python ํŒจํ‚ค์ง€ ์„ค์น˜ ์‹œ**: **requirements.txt์— ์ถ”๊ฐ€** ํ›„ ์ปจํ…Œ์ด๋„ˆ ์žฌ๋นŒ๋“œ - **ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •**: **docker-compose.yml** ๋˜๋Š” **.env** ํŒŒ์ผ ์‚ฌ์šฉ #### **๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ์˜ˆ์‹œ** ```dockerfile # โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•: ๋กœ์ปฌ์—๋งŒ ์„ค์น˜ # brew install libmagic (macOS) # apt-get install libmagic1 (๋กœ์ปฌ Ubuntu) # โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: Dockerfile์— ์ถ”๊ฐ€ RUN apt-get update && apt-get install -y \ gcc \ g++ \ libpq-dev \ libmagic1 \ libmagic-dev \ && rm -rf /var/lib/apt/lists/* ``` #### **๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ** 1. **ํŒจํ‚ค์ง€ ์ถ”๊ฐ€**: `requirements.txt` ์ˆ˜์ • 2. **์‹œ์Šคํ…œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€**: `Dockerfile` ์ˆ˜์ • 3. **์ปจํ…Œ์ด๋„ˆ ์žฌ๋นŒ๋“œ**: `docker-compose down && docker-compose up --build -d` 4. **ํ…Œ์ŠคํŠธ**: Docker ํ™˜๊ฒฝ์—์„œ ํ™•์ธ #### **ํฌํŠธ ์ •๋ณด** - **ํ”„๋ก ํŠธ์—”๋“œ**: http://localhost:13000 - **๋ฐฑ์—”๋“œ API**: http://localhost:18000 - **PostgreSQL**: localhost:5432 - **Redis**: localhost:6379 - **pgAdmin**: http://localhost:5050 ### ๐Ÿ“‹ **๊ฐœ๋ฐœ ์šฐ์„ ์ˆœ์œ„ ๊ฐ€์ด๋“œ๋ผ์ธ** 1. **๋ณด์•ˆ ์ด์Šˆ** - ์ฆ‰์‹œ ์ˆ˜์ • ํ•„์š” 2. **์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ** - ๋†’์€ ์šฐ์„ ์ˆœ์œ„ 3. **์„ฑ๋Šฅ ์ตœ์ ํ™”** - ์ค‘๊ฐ„ ์šฐ์„ ์ˆœ์œ„ 4. **์ฝ”๋“œ ํ’ˆ์งˆ** - ์ง€์†์  ๊ฐœ์„  5. **์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€** - ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„ --- ## Phase 5.1: ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ์™„๋ฃŒ โœ… ### **๊ตฌํ˜„ ์™„๋ฃŒ ์‚ฌํ•ญ** #### **๋ฐฑ์—”๋“œ ์ธ์ฆ ์‹œ์Šคํ…œ** - โœ… **JWT ํ† ํฐ ์„œ๋น„์Šค**: ์•ก์„ธ์Šค/๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ - โœ… **SQLAlchemy ๋ชจ๋ธ**: User, LoginLog, UserSession, Permission, RolePermission - โœ… **์ธ์ฆ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง**: ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ํ† ํฐ ๊ฐฑ์‹ , ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ - โœ… **FastAPI ์—”๋“œํฌ์ธํŠธ**: `/auth/register`, `/auth/login`, `/auth/refresh`, `/auth/logout`, `/auth/me`, `/auth/users` - โœ… **RBAC ๋ฏธ๋“ค์›จ์–ด**: ์—ญํ•  ๋ฐ ๊ถŒํ•œ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด - โœ… **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ**: ์ธ์ฆ ๊ด€๋ จ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋ฐ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ #### **ํ”„๋ก ํŠธ์—”๋“œ ์ธ์ฆ ์‹œ์Šคํ…œ** - โœ… **SimpleLogin ์ปดํฌ๋„ŒํŠธ**: ๊น”๋”ํ•œ ๋กœ๊ทธ์ธ ์ธํ„ฐํŽ˜์ด์Šค - โœ… **SimpleDashboard ์ปดํฌ๋„ŒํŠธ**: ์‚ฌ์šฉ์ž ์ •๋ณด ํ‘œ์‹œ ๋ฐ ๊ถŒํ•œ ํ™•์ธ - โœ… **์ž๋™ ๋ผ์šฐํŒ…**: ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋Œ€์‹œ๋ณด๋“œ ์ž๋™ ์ด๋™ - โœ… **ํ† ํฐ ๊ด€๋ฆฌ**: localStorage ๊ธฐ๋ฐ˜ ํ† ํฐ ์ €์žฅ ๋ฐ ์ž๋™ ๋กœ๊ทธ์ธ - โœ… **๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ**: ํ† ํฐ ์‚ญ์ œ ๋ฐ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ณต๊ท€ #### **๋ณด์•ˆ ๊ธฐ๋Šฅ** - โœ… **๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ**: bcrypt ๊ธฐ๋ฐ˜ ์•ˆ์ „ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ - โœ… **JWT ๋ณด์•ˆ**: ์•ก์„ธ์Šค/๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ถ„๋ฆฌ, ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • - โœ… **๋กœ๊ทธ์ธ ์ด๋ ฅ**: ๋ชจ๋“  ๋กœ๊ทธ์ธ ์‹œ๋„ ๊ธฐ๋ก ๋ฐ ์ถ”์  - โœ… **๊ณ„์ • ์ž ๊ธˆ**: ์‹คํŒจ ์‹œ๋„ ํšŸ์ˆ˜ ์ œํ•œ (ํ–ฅํ›„ ํ™•์žฅ ๊ฐ€๋Šฅ) #### **ํ…Œ์ŠคํŠธ ๊ณ„์ •** - **๊ด€๋ฆฌ์ž**: `admin` / `admin123` - **์ผ๋ฐ˜ ์‚ฌ์šฉ์ž**: `testuser` / `test123` ### **๋‹ค์Œ ๋‹จ๊ณ„: Phase 5.2 - ๋„ค๋น„๊ฒŒ์ด์…˜ ์‹œ์Šคํ…œ** 1. ๊ถŒํ•œ๋ณ„ ๋ฉ”๋‰ด ์‹œ์Šคํ…œ ๊ตฌํ˜„ 2. ๊ธฐ์กด BOM/ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ํ†ตํ•ฉ 3. ๊ด€๋ฆฌ์ž์šฉ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ํŽ˜์ด์ง€ 4. ์„ธ๋ถ„ํ™”๋œ ๊ถŒํ•œ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ --- ## ๐Ÿ“Š **์‚ฌ์šฉ์ž ์ถ”์  ๋ฐ ๋‹ด๋‹น์ž ๊ธฐ๋ก ๊ฐ€์ด๋“œ๋ผ์ธ** (2025.01 ์‹ ๊ทœ) ### ๐ŸŽฏ **๊ธฐ๋ณธ ์›์น™** - **๋ชจ๋“  ์—…๋ฌด ํ™œ๋™์€ ๋‹ด๋‹น์ž๊ฐ€ ๊ธฐ๋ก๋˜์–ด์•ผ ํ•จ** - **์ถ”์  ๊ฐ€๋Šฅํ•œ ์—…๋ฌด ์ด๋ ฅ ๊ด€๋ฆฌ** - **๊ฐœ์ธ๋ณ„ ๋งž์ถคํ˜• ๋Œ€์‹œ๋ณด๋“œ ์ œ๊ณต** - **๊ถŒํ•œ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์ •๋ณด ํ‘œ์‹œ** ### ๐Ÿ“‹ **ํ•„์ˆ˜ ๊ธฐ๋ก ๋Œ€์ƒ** #### **1. ํŒŒ์ผ ๊ด€๋ฆฌ** ```sql -- ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ ํ•„์ˆ˜ ๊ธฐ๋ก uploaded_by VARCHAR(100) NOT NULL, -- ์—…๋กœ๋“œํ•œ ์‚ฌ์šฉ์ž upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_by VARCHAR(100), -- ์ˆ˜์ •ํ•œ ์‚ฌ์šฉ์ž (ํŒŒ์ผ ์ˆ˜์ • ์‹œ) updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ``` #### **2. ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ** ```sql -- ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ/์ˆ˜์ • ์‹œ ํ•„์ˆ˜ ๊ธฐ๋ก created_by VARCHAR(100) NOT NULL, -- ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ์ž created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_by VARCHAR(100), -- ๋งˆ์ง€๋ง‰ ์ˆ˜์ •์ž updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, assigned_to VARCHAR(100), -- ํ”„๋กœ์ ํŠธ ๋‹ด๋‹น์ž ``` #### **3. ์ž์žฌ ๊ด€๋ฆฌ** ```sql -- ์ž์žฌ ๋ถ„๋ฅ˜/๊ฒ€์ฆ ์‹œ ํ•„์ˆ˜ ๊ธฐ๋ก classified_by VARCHAR(100), -- ์ž์žฌ ๋ถ„๋ฅ˜ ๋‹ด๋‹น์ž classified_at TIMESTAMP, verified_by VARCHAR(100), -- ๊ฒ€์ฆ ๋‹ด๋‹น์ž verified_at TIMESTAMP, updated_by VARCHAR(100), -- ์ˆ˜์ • ๋‹ด๋‹น์ž updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ``` #### **4. ๊ตฌ๋งค ๊ด€๋ฆฌ** ```sql -- ๊ตฌ๋งค ํ™•์ •/๋ฐœ์ฃผ ์‹œ ํ•„์ˆ˜ ๊ธฐ๋ก confirmed_by VARCHAR(100) NOT NULL, -- ๊ตฌ๋งค ํ™•์ •์ž confirmed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ordered_by VARCHAR(100), -- ๋ฐœ์ฃผ ๋‹ด๋‹น์ž ordered_at TIMESTAMP, approved_by VARCHAR(100), -- ์Šน์ธ์ž (๊ณ ์•ก ๊ตฌ๋งค ์‹œ) approved_at TIMESTAMP ``` ### ๐Ÿ” **๊ถŒํ•œ๋ณ„ ์ ‘๊ทผ ์ œ์–ด** #### **๊ด€๋ฆฌ์ž (admin)** - ๋ชจ๋“  ํ”„๋กœ์ ํŠธ ์กฐํšŒ/์ˆ˜์ • ๊ฐ€๋Šฅ - ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ๋ฐ ๊ถŒํ•œ ์„ค์ • - ์‹œ์Šคํ…œ ์„ค์ • ๋ฐ ๋ฐฑ์—… ๊ด€๋ฆฌ - ์ „์ฒด ํ™œ๋™ ๋กœ๊ทธ ์กฐํšŒ #### **ํ”„๋กœ์ ํŠธ ๋งค๋‹ˆ์ € (manager)** - ๋‹ด๋‹น ํ”„๋กœ์ ํŠธ ์ „์ฒด ๊ด€๋ฆฌ - ํŒ€์› ์—…๋ฌด ํ• ๋‹น ๋ฐ ์ง„ํ–‰ ์ƒํ™ฉ ๋ชจ๋‹ˆํ„ฐ๋ง - ๊ตฌ๋งค ์Šน์ธ ๊ถŒํ•œ (์ผ์ • ๊ธˆ์•ก ์ดํ•˜) - ํ”„๋กœ์ ํŠธ๋ณ„ ๋ฆฌํฌํŠธ ์ƒ์„ฑ #### **์„ค๊ณ„ ๋‹ด๋‹น์ž (designer)** - ๋‹ด๋‹น ํ”„๋กœ์ ํŠธ BOM ์—…๋กœ๋“œ/์ˆ˜์ • - ์ž์žฌ ๋ถ„๋ฅ˜ ๋ฐ ๊ฒ€์ฆ - ๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ - ๊ตฌ๋งค ์š”์ฒญ์„œ ์ž‘์„ฑ #### **๊ตฌ๋งค ๋‹ด๋‹น์ž (purchaser)** - ๊ตฌ๋งค ํ’ˆ๋ชฉ ์กฐํšŒ ๋ฐ ๋ฐœ์ฃผ - ๊ณต๊ธ‰์—…์ฒด ๊ด€๋ฆฌ - ๊ตฌ๋งค ํ˜„ํ™ฉ ์ถ”์  - ์ž…๊ณ  ๊ด€๋ฆฌ #### **์กฐํšŒ ์ „์šฉ (viewer)** - ํ• ๋‹น๋œ ํ”„๋กœ์ ํŠธ ์กฐํšŒ๋งŒ ๊ฐ€๋Šฅ - ๋ฆฌํฌํŠธ ๋‹ค์šด๋กœ๋“œ - ์ง„ํ–‰ ์ƒํ™ฉ ํ™•์ธ ### ๐Ÿ“ˆ **๊ฐœ์ธ๋ณ„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ** #### **1. ๋งž์ถคํ˜• ๋ฐฐ๋„ˆ ์‹œ์Šคํ…œ** ```javascript // ์‚ฌ์šฉ์ž๋ณ„ ๋งž์ถค ์ •๋ณด ํ‘œ์‹œ const personalizedBanner = { admin: { title: "์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž", metrics: ["์ „์ฒด ํ”„๋กœ์ ํŠธ ์ˆ˜", "ํ™œ์„ฑ ์‚ฌ์šฉ์ž ์ˆ˜", "์‹œ์Šคํ…œ ์ƒํƒœ"], quickActions: ["์‚ฌ์šฉ์ž ๊ด€๋ฆฌ", "์‹œ์Šคํ…œ ์„ค์ •", "๋ฐฑ์—… ๊ด€๋ฆฌ"] }, manager: { title: "ํ”„๋กœ์ ํŠธ ๋งค๋‹ˆ์ €", metrics: ["๋‹ด๋‹น ํ”„๋กœ์ ํŠธ", "ํŒ€ ์ง„ํ–‰๋ฅ ", "์Šน์ธ ๋Œ€๊ธฐ"], quickActions: ["ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ", "ํŒ€ ๊ด€๋ฆฌ", "์ง„ํ–‰ ์ƒํ™ฉ"] }, designer: { title: "์„ค๊ณ„ ๋‹ด๋‹น์ž", metrics: ["๋‚ด BOM ํŒŒ์ผ", "๋ถ„๋ฅ˜ ์™„๋ฃŒ์œจ", "๊ฒ€์ฆ ๋Œ€๊ธฐ"], quickActions: ["BOM ์—…๋กœ๋“œ", "์ž์žฌ ๋ถ„๋ฅ˜", "๋ฆฌ๋น„์ „ ๊ด€๋ฆฌ"] }, purchaser: { title: "๊ตฌ๋งค ๋‹ด๋‹น์ž", metrics: ["๊ตฌ๋งค ์š”์ฒญ", "๋ฐœ์ฃผ ์™„๋ฃŒ", "์ž…๊ณ  ๋Œ€๊ธฐ"], quickActions: ["๊ตฌ๋งค ํ™•์ •", "๋ฐœ์ฃผ ๊ด€๋ฆฌ", "๊ณต๊ธ‰์—…์ฒด"] } }; ``` #### **2. ํ™œ๋™ ์ด๋ ฅ ์ถ”์ ** ```sql -- ์‚ฌ์šฉ์ž ํ™œ๋™ ๋กœ๊ทธ ํ…Œ์ด๋ธ” CREATE TABLE user_activity_logs ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(user_id), username VARCHAR(100) NOT NULL, activity_type VARCHAR(50) NOT NULL, -- 'FILE_UPLOAD', 'PROJECT_CREATE', 'PURCHASE_CONFIRM' ๋“ฑ activity_description TEXT, -- ์ƒ์„ธ ํ™œ๋™ ๋‚ด์šฉ target_id INTEGER, -- ๋Œ€์ƒ ID (ํŒŒ์ผ, ํ”„๋กœ์ ํŠธ ๋“ฑ) target_type VARCHAR(50), -- 'FILE', 'PROJECT', 'MATERIAL' ๋“ฑ ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` #### **3. ๊ฐœ์ธ ์ž‘์—… ํ˜„ํ™ฉ** - **๋‚ด๊ฐ€ ์—…๋กœ๋“œํ•œ ํŒŒ์ผ**: ์ตœ๊ทผ ์—…๋กœ๋“œํ•œ BOM ํŒŒ์ผ ๋ชฉ๋ก - **๋‚ด๊ฐ€ ๋‹ด๋‹นํ•œ ํ”„๋กœ์ ํŠธ**: ํ• ๋‹น๋œ ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ์ƒํ™ฉ - **๋‚ด ์—…๋ฌด ๋Œ€๊ธฐ**: ๋ถ„๋ฅ˜/๊ฒ€์ฆ/์Šน์ธ ๋Œ€๊ธฐ ์ค‘์ธ ์—…๋ฌด - **์ตœ๊ทผ ํ™œ๋™**: ์ตœ๊ทผ 7์ผ๊ฐ„ ํ™œ๋™ ์š”์•ฝ ### ๐Ÿš€ **๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„** #### **Phase 1: ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ถ”์ ** (1์ฃผ) 1. ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ๋‹ด๋‹น์ž ํ•„๋“œ ์ถ”๊ฐ€ 2. ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ธฐ๋ก 3. ๊ธฐ๋ณธ ํ™œ๋™ ๋กœ๊ทธ ์‹œ์Šคํ…œ ๊ตฌ์ถ• #### **Phase 2: ๊ฐœ์ธ๋ณ„ ๋Œ€์‹œ๋ณด๋“œ** (2์ฃผ) 1. ๊ถŒํ•œ๋ณ„ ๋งž์ถคํ˜• ๋ฐฐ๋„ˆ ๊ตฌํ˜„ 2. ๊ฐœ์ธ ์ž‘์—… ํ˜„ํ™ฉ ํŽ˜์ด์ง€ 3. ํ™œ๋™ ์ด๋ ฅ ์กฐํšŒ ๊ธฐ๋Šฅ #### **Phase 3: ๊ณ ๋„ํ™”** (3์ฃผ) 1. ์ƒ์„ธ ๊ถŒํ•œ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ 2. ํŒ€๋ณ„/๋ถ€์„œ๋ณ„ ๋Œ€์‹œ๋ณด๋“œ 3. ์—…๋ฌด ํ• ๋‹น ๋ฐ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ### โš ๏ธ **์ฃผ์˜์‚ฌํ•ญ** - **๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ**: ์‚ฌ์šฉ์ž ํ™œ๋™ ๋กœ๊ทธ๋Š” ์—…๋ฌด ๋ชฉ์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉ - **๋ฐ์ดํ„ฐ ๋ณด์กด**: ํ™œ๋™ ๋กœ๊ทธ๋Š” ์ตœ๋Œ€ 1๋…„๊ฐ„ ๋ณด์กด ํ›„ ์ž๋™ ์‚ญ์ œ - **์ ‘๊ทผ ๊ถŒํ•œ**: ๊ฐœ์ธ ํ™œ๋™ ์ด๋ ฅ์€ ๋ณธ์ธ๊ณผ ๊ด€๋ฆฌ์ž๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ - **๊ฐ์‚ฌ ์ถ”์ **: ์ค‘์š” ์—…๋ฌด(๊ตฌ๋งค ํ™•์ •, ํ”„๋กœ์ ํŠธ ์‚ญ์ œ ๋“ฑ)๋Š” ๋ณ„๋„ ๊ฐ์‚ฌ ๋กœ๊ทธ ์œ ์ง€ --- ## ๐Ÿšจ **ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ** (2025.01 ์‹ ๊ทœ) > โš ๏ธ **์ค‘์š”**: ํ˜„์žฌ ์„ค์ •์€ **ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์šฉ**์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ์„œ๋น„์Šค ๋ฐฐํฌ ์ „ ๋ฐ˜๋“œ์‹œ ์•„๋ž˜ ํ•ญ๋ชฉ๋“ค์„ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ### ๐Ÿ” **Critical Security Items (๋ฐฐํฌ ์ „ ํ•„์ˆ˜)** #### **1. JWT ์‹œํฌ๋ฆฟ ํ‚ค ํ™˜๊ฒฝ๋ณ€์ˆ˜ํ™”** ```bash # โŒ ํ˜„์žฌ (ํ…Œ์ŠคํŠธ์šฉ) SECRET_KEY = "test-secret-key" # โœ… ๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ๋ณ€๊ฒฝ JWT_SECRET_KEY=your-super-secure-random-key-here # .env ํŒŒ์ผ์— ์ถ”๊ฐ€ ``` #### **2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด์•ˆ** ```yaml # โŒ ํ˜„์žฌ (ํ…Œ์ŠคํŠธ์šฉ) POSTGRES_PASSWORD: tkmp_password_2025 # โœ… ๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ๋ณ€๊ฒฝ POSTGRES_PASSWORD: ${DB_PASSWORD} # ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๋ถ„๋ฆฌ ``` #### **3. CORS ๋„๋ฉ”์ธ ์„ค์ •** ```python # โŒ ํ˜„์žฌ (ํ…Œ์ŠคํŠธ์šฉ) "production": [ "https://your-domain.com", "https://api.your-domain.com" ] # โœ… ๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ๋ณ€๊ฒฝ "production": [ "https://์‹ค์ œ๋„๋ฉ”์ธ.com", "https://api.์‹ค์ œ๋„๋ฉ”์ธ.com" ] ``` #### **4. ๊ธฐ๋ณธ ๊ด€๋ฆฌ์ž ๊ณ„์ • ๋ณ€๊ฒฝ** ```sql -- โŒ ํ˜„์žฌ (ํ…Œ์ŠคํŠธ์šฉ) INSERT INTO users (username, password) VALUES ('admin', 'admin123'); -- โœ… ๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ๋ณ€๊ฒฝ -- ๊ฐ•๋ ฅํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝ ๋ฐ ํ…Œ์ŠคํŠธ ๊ณ„์ • ์‚ญ์ œ ``` ### ๐Ÿ›ก๏ธ **๋ฐฐํฌ ์ „ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ** - [ ] **ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ถ„๋ฆฌ**: ๋ชจ๋“  ๋ฏผ๊ฐ ์ •๋ณด๋ฅผ .env ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ - [ ] **HTTPS ์ ์šฉ**: SSL ์ธ์ฆ์„œ ์„ค์น˜ ๋ฐ HTTP โ†’ HTTPS ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ - [ ] **๋ฐฉํ™”๋ฒฝ ์„ค์ •**: ํ•„์š”ํ•œ ํฌํŠธ๋งŒ ๊ฐœ๋ฐฉ (80, 443, SSH) - [ ] **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ ์ œํ•œ**: ์™ธ๋ถ€ ์ ‘๊ทผ ์ฐจ๋‹จ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋งŒ ์ ‘๊ทผ - [ ] **๋กœ๊ทธ ํŒŒ์ผ ๋ณด์•ˆ**: ๋ฏผ๊ฐ ์ •๋ณด ๋กœ๊น… ๋ฐฉ์ง€, ๋กœ๊ทธ ํŒŒ์ผ ๊ถŒํ•œ ์„ค์ • - [ ] **๋ฐฑ์—… ์ „๋žต**: ์ •๊ธฐ ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ ํ…Œ์ŠคํŠธ - [ ] **๋ชจ๋‹ˆํ„ฐ๋ง**: ์‹œ์Šคํ…œ ์ƒํƒœ ๋ฐ ๋ณด์•ˆ ์ด๋ฒคํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง - [ ] **์—…๋ฐ์ดํŠธ ๊ณ„ํš**: ๋ณด์•ˆ ํŒจ์น˜ ๋ฐ ์˜์กด์„ฑ ์—…๋ฐ์ดํŠธ ๊ณ„ํš ### ๐Ÿ“‹ **๋ฐฐํฌ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๊ฐ€์ด๋“œ** #### **๊ฐœ๋ฐœ ํ™˜๊ฒฝ (ํ˜„์žฌ)** ```bash ENVIRONMENT=development DEBUG=true CORS_ORIGINS=http://localhost:3000,http://localhost:13000 ``` #### **์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ** ```bash ENVIRONMENT=staging DEBUG=false CORS_ORIGINS=https://staging.your-domain.com ``` #### **ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ** ```bash ENVIRONMENT=production DEBUG=false CORS_ORIGINS=https://your-domain.com JWT_SECRET_KEY=๊ฐ•๋ ฅํ•œ-๋žœ๋ค-ํ‚ค DB_PASSWORD=๊ฐ•๋ ฅํ•œ-๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค-๋น„๋ฐ€๋ฒˆํ˜ธ ``` --- ## ๐Ÿ“‹ **API ์ •๋ฆฌ ์š”์•ฝ** (2025.09.05 ์™„๋ฃŒ) ### โœ… **ํ•ด๊ฒฐ๋œ ๋ฌธ์ œ๋“ค** 1. **404 ์˜ค๋ฅ˜ ํ•ด๊ฒฐ**: `/files/materials` โ†’ `/files/materials-v2` ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ 2. **API ํ˜ธ์ถœ ํ‘œ์ค€ํ™”**: ์ง์ ‘ ํ˜ธ์ถœ โ†’ `fetchMaterials()` ํ•จ์ˆ˜ ์‚ฌ์šฉ 3. **ํ˜ผ๋™ ๋ฐฉ์ง€**: ๋ช…ํ™•ํ•œ API ์‚ฌ์šฉ ๊ฐ€์ด๋“œ๋ผ์ธ ์ˆ˜๋ฆฝ 4. **๋ฌธ์„œํ™” ์™„์„ฑ**: ์‹ค์ œ ๊ตฌํ˜„๋œ ๋ชจ๋“  API ์—”๋“œํฌ์ธํŠธ ์ •๋ฆฌ ### ๐ŸŽฏ **ํ‘œ์ค€ํ™”๋œ ์‚ฌ์šฉ๋ฒ•** ```javascript // โœ… ๊ถŒ์žฅ ๋ฐฉ๋ฒ• import { fetchMaterials, fetchFiles, fetchJobs } from '../api'; // ํŒŒ์ผ๋ณ„ ์ž์žฌ ์กฐํšŒ const materials = await fetchMaterials({ file_id: 123 }); // ํ”„๋กœ์ ํŠธ๋ณ„ ์ž์žฌ ์กฐํšŒ const materials = await fetchMaterials({ job_no: 'J24-001' }); // ๋ฆฌ๋น„์ „๋ณ„ ์ž์žฌ ์กฐํšŒ const materials = await fetchMaterials({ job_no: 'J24-001', revision: 'Rev.1' }); ``` ### ๐Ÿšจ **์ค‘์š” ๊ทœ์น™** - **๋ชจ๋“  ์ž์žฌ API ํ˜ธ์ถœ์€ `fetchMaterials()` ํ•จ์ˆ˜ ์‚ฌ์šฉ** - **์ง์ ‘ API ํ˜ธ์ถœ ๊ธˆ์ง€** (ํŠน๋ณ„ํ•œ ๊ฒฝ์šฐ ์ œ์™ธ) - **์ƒˆ API ์ถ”๊ฐ€ ์‹œ RULES.md ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ** - **API ๋ณ€๊ฒฝ ์‹œ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ๊ณ ๋ ค** --- ## ๐Ÿ” ์ž์žฌ ๋ถ„๋ฅ˜ ๊ทœ์น™ ### ํ•ต์‹ฌ ๋ถ„๋ฅ˜ ์›์น™ #### 1. ๋‹ˆํ”Œ(NIPPLE) ํŠน์ˆ˜ ๊ทœ์น™ โš ๏ธ - **๋ถ„๋ฅ˜ ๋ฐฉ์‹**: ํŒŒ์ดํ”„ ๋ถ„๋ฅ˜๊ธฐ(pipe_classifier)๋กœ ๋ถ„๋ฅ˜ํ•˜์ง€๋งŒ **์นดํ…Œ๊ณ ๋ฆฌ๋Š” FITTING์œผ๋กœ ์ฒ˜๋ฆฌ** - **์ด์œ **: ๋‹ˆํ”Œ์€ ํŒŒ์ดํ”„์™€ ๋™์ผํ•œ ์žฌ์งˆ/์ŠคํŽ™์„ ๊ฐ€์ง€์ง€๋งŒ, ์šฉ๋„์ƒ ํ”ผํŒ…๋ฅ˜๋กœ ์ทจ๊ธ‰ - **๊ธธ์ด ๊ธฐ๋ฐ˜ ๊ทธ๋ฃนํ•‘**: ๊ฐ™์€ ์ŠคํŽ™์ด๋ผ๋„ ๊ธธ์ด๊ฐ€ ๋‹ค๋ฅด๋ฉด ๋ณ„๋„ ํ•ญ๋ชฉ์œผ๋กœ ๋ถ„๋ฆฌ - ์˜ˆ: `NIPPLE 1" 75mm` vs `NIPPLE 1" 100mm` - **์ด๊ธธ์ด ๊ณ„์‚ฐ**: ๊ฐœ๋ณ„ ๋‹ˆํ”Œ ๊ธธ์ด ร— ์ˆ˜๋Ÿ‰์„ ํ•ฉ์‚ฐํ•˜์—ฌ ์‹ค์ œ ์ด๊ธธ์ด ํ‘œ์‹œ - **๋๋‹จ ๊ฐ€๊ณต ์ฒ˜๋ฆฌ**: ํŒŒ์ดํ”„์™€ ๋™์ผํ•˜๊ฒŒ PBE, BBE, POE ๋“ฑ ๋๋‹จ ๊ฐ€๊ณต ์ •๋ณด ๋ถ„๋ฆฌ ์ €์žฅ - **๊ทธ๋ฃนํ•‘ ํ‚ค**: `clean_description|size_spec|material_grade|length_mm` #### 2. ํŒŒ์ดํ”„(PIPE) ๋ถ„๋ฅ˜ ๊ทœ์น™ - **๊ทธ๋ฃนํ•‘ ํ‚ค**: `clean_description|size_spec|material_grade` - **๋๋‹จ ๊ฐ€๊ณต ์ œ์™ธ**: ๊ตฌ๋งค์šฉ ๊ทธ๋ฃนํ•‘์—์„œ๋Š” BBE, POE, PBE ๋“ฑ ๋๋‹จ ๊ฐ€๊ณต ์ •๋ณด ์ œ์™ธ - **๊ฐœ๋ณ„ ์ •๋ณด ๋ณด์กด**: ๊ฐ ํŒŒ์ดํ”„์˜ ๋๋‹จ ๊ฐ€๊ณต ์ •๋ณด๋Š” `pipe_end_preparations` ํ…Œ์ด๋ธ”์— ๋ณ„๋„ ์ €์žฅ - **์ด๊ธธ์ด ๊ณ„์‚ฐ**: ๋™์ผ ์ŠคํŽ™ ํŒŒ์ดํ”„๋“ค์˜ ๊ฐœ๋ณ„ ๊ธธ์ด ํ•ฉ์‚ฐ #### 3. ๊ธฐํƒ€ ํ”ผํŒ…(FITTING) ๋ถ„๋ฅ˜ ๊ทœ์น™ - **์ผ๋ฐ˜ ํ”ผํŒ…**: ์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜ ์ง‘๊ณ„ (ELBOW, TEE, REDUCER ๋“ฑ) - **๊ธธ์ด ์ •๋ณด ์—†์Œ**: ๋‹ˆํ”Œ์„ ์ œ์™ธํ•œ ์ผ๋ฐ˜ ํ”ผํŒ…์€ ๊ธธ์ด ๊ธฐ๋ฐ˜ ๊ทธ๋ฃนํ•‘ ๋ถˆํ•„์š” ### ๋ถ„๋ฅ˜ ์šฐ์„ ์ˆœ์œ„ 1. **PIPE**: ํŒŒ์ดํ”„ ๋ถ„๋ฅ˜๊ธฐ ์šฐ์„  ์ ์šฉ 2. **FITTING**: ๋‹ˆํ”Œ ํฌํ•จ, ํ”ผํŒ… ๋ถ„๋ฅ˜๊ธฐ ์ ์šฉ 3. **VALVE**: ๋ฐธ๋ธŒ ๋ถ„๋ฅ˜๊ธฐ ์ ์šฉ 4. **FLANGE**: ํ”Œ๋žœ์ง€ ๋ถ„๋ฅ˜๊ธฐ ์ ์šฉ 5. **BOLT**: ๋ณผํŠธ ๋ถ„๋ฅ˜๊ธฐ ์ ์šฉ 6. **GASKET**: ๊ฐ€์Šค์ผ“ ๋ถ„๋ฅ˜๊ธฐ ์ ์šฉ 7. **INSTRUMENT**: ๊ณ„๊ธฐ ๋ถ„๋ฅ˜๊ธฐ ์ ์šฉ ### ๋๋‹จ ๊ฐ€๊ณต ์ฝ”๋“œ ์ •์˜ - **PBE**: Plain Both Ends (์–‘์ชฝ ๋ฌด๊ฐœ์„ ) - ๊ธฐ๋ณธ๊ฐ’ - **BBE**: Both Ends Beveled (์–‘์ชฝ ๊ฐœ์„ ) - **POE**: Plain One End (ํ•œ์ชฝ ๋ฌด๊ฐœ์„ ) - **BOE**: Beveled One End (ํ•œ์ชฝ ๊ฐœ์„ ) - **TOE**: Threaded One End (ํ•œ์ชฝ ๋‚˜์‚ฌ) --- **๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ**: 2025๋…„ 9์›” (์ž์žฌ ๋ถ„๋ฅ˜ ๊ทœ์น™ ๋ฐ API ์ •๋ฆฌ ์™„๋ฃŒ)