# ๐Ÿš€ 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 # ์‚ฌ์ด๋“œ๋ฐ” ๋„ค๋น„๊ฒŒ์ด์…˜ (๊ถŒํ•œ ๊ธฐ๋ฐ˜) โ”‚ โ”œโ”€โ”€ BOMFileUpload.jsx # BOM ํŒŒ์ผ ์—…๋กœ๋“œ ํผ โ”‚ โ”œโ”€โ”€ BOMFileTable.jsx # BOM ํŒŒ์ผ ๋ชฉ๋ก ํ…Œ์ด๋ธ” โ”‚ โ””โ”€โ”€ RevisionUploadDialog.jsx # ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ ๋‹ค์ด์–ผ๋กœ๊ทธ โ””โ”€โ”€ pages/ # ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ โ”œโ”€โ”€ DashboardPage.jsx # ๋Œ€์‹œ๋ณด๋“œ โ”œโ”€โ”€ 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 ์—”๋“œํฌ์ธํŠธ ๋งต #### ์ธ์ฆ API (`/auth/`) ``` 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} # ์‚ฌ์šฉ์ž ์ˆ˜์ • (๊ด€๋ฆฌ์ž) ``` #### ํ”„๋กœ์ ํŠธ API (`/jobs/`) ``` GET /jobs/ # ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก POST /jobs/ # ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ PUT /jobs/{id} # ํ”„๋กœ์ ํŠธ ์ˆ˜์ • DELETE /jobs/{id} # ํ”„๋กœ์ ํŠธ ์‚ญ์ œ ``` #### ํŒŒ์ผ/์ž์žฌ API (`/files/`) ``` GET /files # ํŒŒ์ผ ๋ชฉ๋ก (job_no ํ•„ํ„ฐ) POST /files/upload # ํŒŒ์ผ ์—…๋กœ๋“œ DELETE /files/{id} # ํŒŒ์ผ ์‚ญ์ œ GET /files/stats # ํŒŒ์ผ/์ž์žฌ ํ†ต๊ณ„ GET /files/materials # ์ž์žฌ ๋ชฉ๋ก (file_id ํ•„ํ„ฐ) ``` ### ๐Ÿ“Š ๋ฐ์ดํ„ฐ ํ๋ฆ„๋„ ```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. ๋ฆฌ๋น„์ „ ๋น„๊ต ๋กœ์ง ```python # ์ด์ „ ๋ฆฌ๋น„์ „ ์ž๋™ ํƒ์ง€: ์ˆซ์ž ๊ธฐ๋ฐ˜ ๋น„๊ต current_rev_num = int(current_revision.replace("Rev.", "")) # Rev.0 โ†’ Rev.1 โ†’ Rev.2 ์ˆœ์„œ ``` --- ## ๐Ÿ“ **์ฝ”๋“œ ๋ถ„๋ฆฌ ๊ธฐ์ค€ ๋ฐ ํ’ˆ์งˆ ๊ฐ€์ด๋“œ๋ผ์ธ** ### ๐ŸŽฏ **์ฝ”๋“œ ๊ธธ์ด ๊ธฐ์ค€ (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}`); ``` --- ## ๐Ÿ”„ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ### 1. ์„œ๋ฒ„ ์‹คํ–‰ ๋ช…๋ น์–ด ```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 8000 # ํ”„๋ก ํŠธ์—”๋“œ ์‹คํ–‰ (ํ„ฐ๋ฏธ๋„ 2๋ฒˆ) - TK-MP-Project ๋ฃจํŠธ์—์„œ cd frontend npm run dev # npm start ์•„๋‹˜! ``` **์ ‘์† ์ฃผ์†Œ:** - ๋ฐฑ์—”๋“œ API: http://localhost:8000 - API ๋ฌธ์„œ: http://localhost:8000/docs - ํ”„๋ก ํŠธ์—”๋“œ: http://localhost:5173 ### 2. ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ ์‹œ ```bash # ํ•ญ์ƒ ๊ฐ€์ƒํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ (์‚ฌ์šฉ์ž ์„ ํ˜ธ์‚ฌํ•ญ) cd backend python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ``` ### 3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์‹œ ```sql -- scripts/ ํด๋”์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ SQL ํŒŒ์ผ ์ƒ์„ฑ -- ๋ฒˆํ˜ธ ์ˆœ์„œ: 01_, 02_, 03_... ``` ### 4. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ``` ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑ (์‚ฌ์šฉ์ž ์„ ํ˜ธ์‚ฌํ•ญ) ์˜ˆ: "ํŒŒ์ดํ”„ ๊ธธ์ด ๊ณ„์‚ฐ ๋ฐ ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„๊ทธ ์ˆ˜์ •" ``` --- ## ๐Ÿ’ฐ ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ ๊ทœ์น™ ### 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: ๋ฆฌ๋น„์ „๋ณ„ ๊ตฌ๋งค --- ## ๐ŸŒ ์‹œ๋†€๋กœ์ง€ DSM ๋ฐฐํฌ ๊ฐ€์ด๋“œ ### ์„œ๋น„์Šค ๊ตฌ์„ฑ - **ํ”„๋ก ํŠธ์—”๋“œ**: React + Vite (ํฌํŠธ 10173) - **๋ฐฑ์—”๋“œ**: FastAPI (ํฌํŠธ 10080) - **๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค**: PostgreSQL (ํฌํŠธ 15432) - **์บ์‹œ**: Redis (ํฌํŠธ 16379) ### ์ž๋™ ๋ฐฐํฌ (๊ถŒ์žฅ) ```bash ./deploy-synology.sh 192.168.0.3 ``` ### ์ ‘์† ํ™•์ธ - ํ”„๋ก ํŠธ์—”๋“œ: http://192.168.0.3:10173 - ๋ฐฑ์—”๋“œ API ๋ฌธ์„œ: http://192.168.0.3:10080/docs ### ์ฃผ์˜์‚ฌํ•ญ 1. **ํฌํŠธ ์ถฉ๋Œ**: ์‹œ๋†€๋กœ์ง€์—์„œ 10080, 10173 ํฌํŠธ๊ฐ€ ์‚ฌ์šฉ ์ค‘์ด์ง€ ์•Š์€์ง€ ํ™•์ธ 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๋…„ 1์›” (์ฝ”๋“œ ๊ตฌ์กฐ ์ •๋ฆฌ ๋ฐ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ ์™„๋ฃŒ)