# ๐ 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: ๋ฆฌ๋น์ ๋ณ ๊ตฌ๋งค
#### **๐จ ์ ๊ท ๋ชจ๋ UI ํ์ด์ง๋ค (2025.10.16 ์ถ๊ฐ)**
##### **DashboardPage.jsx** - ํ๋ก์ ํธ ์ค์ฌ ๋์๋ณด๋
```jsx
// ์์น: frontend/src/pages/DashboardPage.jsx
// ํน์ง: ๋ฐ๋ณธ์ฝํฌ ์คํ์ผ์ ๋ชจ๋ํ ๋์์ธ
// ๊ธฐ๋ฅ:
// - ํ๋ก์ ํธ ์ ํ ๋ฐ ๊ด๋ฆฌ (์นด๋ ํํ)
// - ๊ถํ๋ณ ๊ธฐ๋ฅ ์นด๋ (BOM ๊ด๋ฆฌ, ์์ฌ ๊ด๋ฆฌ, ๊ตฌ๋งค ๊ด๋ฆฌ)
// - ๊ด๋ฆฌ์ ์ ์ฉ ๊ธฐ๋ฅ (์ฌ์ฉ์ ๊ด๋ฆฌ, ์์คํ
์ค์ )
// - ์์คํ
ํํฉ ๋์๋ณด๋
// - ํ๋ก์ ํธ ์์ฑ ๋ชจ๋ฌ
// ๋์์ธ ํน์ง:
// - ๊ธ๋์ค๋ชจํผ์ฆ ํจ๊ณผ (backdrop-filter: blur(10px))
// - ๊ทธ๋ผ๋ฐ์ด์
๋ฐฐ๊ฒฝ ๋ฐ ๋ฒํผ
// - ์นด๋ ํธ๋ฒ ์ ๋๋ฉ์ด์
// - ํ์ดํฌ๊ทธ๋ํผ ์ค์ฌ ๋์์ธ (์ด๋ชจ์ง ์ ๊ฑฐ)
// - ๋ฐ์ํ ๊ทธ๋ฆฌ๋ ๋ ์ด์์
// ์ฃผ์ ๊ธฐ๋ฅ:
// 1. ํ๋ก์ ํธ ์ ํ ์์คํ
// - ํ๋ก์ ํธ ๋ชฉ๋ก์ ์นด๋ ํํ๋ก ํ์
// - ์ ํ๋ ํ๋ก์ ํธ ํ์ด๋ผ์ดํธ
// - ํ๋ก์ ํธ ์ ๋ณด (์ฝ๋, ์ด๋ฆ, ๊ณ ๊ฐ์ฌ) ํ์
// 2. ๊ถํ ๊ธฐ๋ฐ ๊ธฐ๋ฅ ์ ๊ทผ
// - ํ๋ก์ ํธ ์ ํ ํ์๋ง BOM/์์ฌ ๊ด๋ฆฌ ์ ๊ทผ ๊ฐ๋ฅ
// - ๊ด๋ฆฌ์ ์ ์ฉ ๋ฉ๋ด ๋ถ๋ฆฌ ํ์
// 3. ํ๋ก์ ํธ ์์ฑ ๊ธฐ๋ฅ
// - ๋ชจ๋ฌ ํํ์ ํ๋ก์ ํธ ์์ฑ ํผ
// - ํ๋ก์ ํธ ์ฝ๋, ์ด๋ฆ, ๊ณ ๊ฐ์ฌ ์
๋ ฅ
```
##### **UserMenu.jsx** - ์ฌ์ฉ์ ๋ฉ๋ด ์ปดํฌ๋ํธ
```jsx
// ์์น: frontend/src/components/UserMenu.jsx
// ํน์ง: ๋๋กญ๋ค์ด ํํ์ ์ฌ์ฉ์ ๋ฉ๋ด
// ๊ธฐ๋ฅ:
// - ์ฌ์ฉ์ ํ๋กํ ํ์ (์๋ฐํ, ์ด๋ฆ, ์ญํ )
// - ๊ณ์ ์ค์ ๋งํฌ
// - ๊ด๋ฆฌ์ ์ ์ฉ ๋ฉ๋ด (๊ถํ๋ณ ํ์)
// - ๋ก๊ทธ์์ ๊ธฐ๋ฅ
// ๋์์ธ ํน์ง:
// - ์ํ ์๋ฐํ (๊ทธ๋ผ๋ฐ์ด์
๋ฐฐ๊ฒฝ)
// - ๋๋กญ๋ค์ด ์ ๋๋ฉ์ด์
// - ํธ๋ฒ ํจ๊ณผ
// - ์ญํ ๋ณ ์์ ๊ตฌ๋ถ
// ์ฃผ์ ๊ธฐ๋ฅ:
// 1. ์ฌ์ฉ์ ์ ๋ณด ํ์
// - ์ด๋ฆ ์ฒซ ๊ธ์๋ก ์๋ฐํ ์์ฑ
// - ์ญํ ํ์ (์์คํ
๊ด๋ฆฌ์, ๊ด๋ฆฌ์, ์ฌ์ฉ์)
// 2. ๊ถํ๋ณ ๋ฉ๋ด
// - ๊ด๋ฆฌ์: ์ฌ์ฉ์ ๊ด๋ฆฌ, ์์คํ
์ค์ , ์์คํ
๋ก๊ทธ
// - ์ผ๋ฐ ์ฌ์ฉ์: ๊ณ์ ์ค์ ๋ง
// 3. ๋ค๋น๊ฒ์ด์
์ฐ๋
// - onNavigate ์ฝ๋ฐฑ์ ํตํ ํ์ด์ง ์ด๋
// - onLogout ์ฝ๋ฐฑ์ ํตํ ๋ก๊ทธ์์ ์ฒ๋ฆฌ
```
#### **๐จ UI/UX ๋์์ธ ์์คํ
**
##### **์์ ํ๋ ํธ**
```css
/* ์ฃผ์ ์์ */
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
--background-gradient: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
--glass-background: rgba(255, 255, 255, 0.9);
--text-primary: #0f172a;
--text-secondary: #64748b;
--border-color: rgba(255, 255, 255, 0.2);
/* ๊ทธ๋ฆผ์ */
--shadow-card: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-button: 0 4px 14px 0 rgba(59, 130, 246, 0.39);
--shadow-hover: 0 8px 25px 0 rgba(59, 130, 246, 0.5);
```
##### **ํ์ดํฌ๊ทธ๋ํผ**
```css
/* ํฐํธ ์์คํ
*/
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
/* ์ ๋ชฉ */
--heading-1: 36px, weight: 800, letter-spacing: -0.025em;
--heading-2: 24px, weight: 700, letter-spacing: -0.025em;
--heading-3: 18px, weight: 600;
/* ๋ณธ๋ฌธ */
--body-large: 18px, weight: 400;
--body-medium: 16px, weight: 400;
--body-small: 14px, weight: 400;
--caption: 12px, weight: 400;
```
##### **์ปดํฌ๋ํธ ์คํ์ผ**
```css
/* ์นด๋ */
.modern-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 32px;
box-shadow: var(--shadow-card);
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.modern-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
/* ๋ฒํผ */
.modern-button {
background: var(--primary-gradient);
color: white;
border: none;
border-radius: 12px;
padding: 12px 20px;
font-weight: 600;
box-shadow: var(--shadow-button);
transition: all 0.2s ease;
letter-spacing: 0.025em;
}
.modern-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
```
#### **๐ง ์ปดํฌ๋ํธ ์ฌ์ฉ ๊ฐ์ด๋**
##### **DashboardPage ์ฌ์ฉ๋ฒ**
```jsx
import DashboardPage from './pages/DashboardPage';
// App.jsx์์ ์ฌ์ฉ
case 'dashboard':
return (
);
```
##### **UserMenu ์ฌ์ฉ๋ฒ**
```jsx
import UserMenu from './components/UserMenu';
// ํค๋์์ ์ฌ์ฉ
```
#### **๐ฑ ๋ฐ์ํ ๋์์ธ**
- **๋ฐ์คํฌํฑ**: 1200px ์ด์ - 3-4์ด ๊ทธ๋ฆฌ๋
- **ํ๋ธ๋ฆฟ**: 768px-1199px - 2์ด ๊ทธ๋ฆฌ๋
- **๋ชจ๋ฐ์ผ**: 767px ์ดํ - 1์ด ์คํ
#### **โฟ ์ ๊ทผ์ฑ ๊ณ ๋ ค์ฌํญ**
- ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
์ง์
- ์ถฉ๋ถํ ์์ ๋๋น (WCAG 2.1 AA ์ค์)
- ์คํฌ๋ฆฐ ๋ฆฌ๋ ํธํ์ฑ
- ํฌ์ปค์ค ํ์ ๋ช
ํํ
---
## ๐ ์๋๋ก์ง 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.09.24)
### ๐ **๊ฐ์ ์๊ตฌ์ฌํญ ๋ชฉ๋ก**
#### **1. ์ฌ์ฉ์ ์๊ตฌ์ฌํญ ์์
๋ฐ์** โก ์ฐ์ ์์: ๋์
- **๋ฌธ์ **: ์์ฌ ๋ชฉ๋ก ํ์ด์ง์์ ์์ฑํ ์ฌ์ฉ์ ์๊ตฌ์ฌํญ์ด ์์
๋ค์ด๋ก๋ ์ ๋ฏธ๋ฐ์
- **ํด๊ฒฐ๋ฐฉ์**:
- ์ฌ์ฉ์ ์๊ตฌ์ฌํญ ์ ์ฅ API ๊ตฌํ
- ์์
๋ด๋ณด๋ด๊ธฐ ์ ์ฌ์ฉ์ ์๊ตฌ์ฌํญ ์ปฌ๋ผ ์ถ๊ฐ
- ๋ฐฑ์๋-ํ๋ก ํธ์๋ ์ฐ๋ ๊ฐํ
#### **2. ์ฌ์ง GRADE ์ ์ฒด ํ๊ธฐ** โก ์ฐ์ ์์: ๋์
- **๋ฌธ์ **: ํ์ฌ `ASTM A312 WP304` โ ์
๋ ฅ๋ ์ ์ฒด ์ฌ์ง๋ช
ํ๊ธฐ ํ์
- **์ ์ฉ ๋ฒ์**: ๋ชจ๋ ์์ฌ (ํ์ดํ, ์๋ณด, ํ๋์ง ๋ฑ)
- **์์น**: ์๋ต์ด๋ ์ถ์ฝ ๊ธ์ง, ์๋ณธ ์ฌ์ง๋ช
๊ทธ๋๋ก ํ์
#### **3. U-Bolt & Urethane Block ์นดํ
๊ณ ๋ฆฌ** โก ์ฐ์ ์์: ์ค๊ฐ
- **์ ๊ท ์นดํ
๊ณ ๋ฆฌ**: U-BOLT, URETHANE_BLOCK
- **๋ถ๋ฅ ๊ธฐ์ค**: ํฌ๊ธฐ๋ณ, ์ฌ์ง๋ณ, ๊ธฐํ ์ฌ์๋ณ
- **๋ถ๋ฅ๊ธฐ**: ํ์์ ๊ตฌํ, ์ฐ์ ์ ์๋ ๋ถ๋ฅ
#### **4. Special Flange ๋น๊ธฐ์ฑํ ์ ๋ฆฌ** โก ์ฐ์ ์์: ์ค๊ฐ
- **์์น**: ๊ฐ ์นดํ
๊ณ ๋ฆฌ ๋งจ ํ๋จ์ ๋ฐฐ์น
- **์ ๋ณด**: ์ฌ์ง, ์ฌ์ด์ฆ, ํน์ ์ฌ์ ์์ธ ํ๊ธฐ
- **๊ตฌ๋ถ**: ๊ธฐ์ฑํ๊ณผ ๋ช
ํํ ๊ตฌ๋ถ๋๋๋ก ํ์
#### **5. ํ๋์ง ํ์
์ ๋ณด ํ์ฅ** โก ์ฐ์ ์์: ์ค๊ฐ
- **ํ์ฌ**: WN, BW ๋ฑ ๊ธฐ๋ณธ ์ ๋ณด๋ง ํ๊ธฐ
- **๊ฐ์ **: pipe์ธก ํ์
๋ ํ๊ธฐ (WN RF, SW RF, SO RF)
- **์ ์ฉ**: ํ๋์ง ์์ธ ์ ๋ณด ํ์ฅ
#### **6. Nipple ๋๋จ ์ ๋ณด ํ๊ธฐ** โก ์ฐ์ ์์: ์ค๊ฐ
- **ํ์ฌ**: ๋๋จ ์ ๋ณด ์์งํ์ง๋ง ํ๊ธฐ ์ํจ
- **๊ฐ์ **: ํ์
/์์ธ ๋ถ๋ถ์ ๋๋จ ์ ๋ณด ํ๊ธฐ
- **์ฐ๋**: ๊ธฐ์กด ๋๋จ ๊ฐ๊ณต ์ฝ๋ ํ์ฉ
#### **7. Reducing ๋ฐฐ๊ด Schedule ๋ถ๋ฆฌ** โก ์ฐ์ ์์: ์ค๊ฐ
- **๋ฌธ์ **: Main pipe์ Sub pipe์ Schedule์ด ๋ค๋ฅผ ์ ์์
- **ํด๊ฒฐ**: Schedule ํ๊ธฐ ์ 2๊ฐ๋ก ๋ถ๋ฆฌ ํํ
- **ํ์**: `Main Sch.40 / Sub Sch.80` ํํ
#### **8. ์น ํ๋ฉด ๋ด์ฉ ์๋ฆผ ํด๊ฒฐ** โก ์ฐ์ ์์: ๋์
- **๋ฌธ์ **: ๊ธด ๋ด์ฉ์ด ์น ํ๋ฉด์์ ์๋ฆฌ๋ ํ์
- **ํด๊ฒฐ**: ์ปฌ๋ผ ๋๋น ํ์ฅ, ํ
์คํธ ๋ํ ๊ฐ์
- **์ ์ฉ**: ๋ชจ๋ ํ
์ด๋ธ ๋ฐ ๋ชฉ๋ก ํ๋ฉด
#### **9. ์์ฌ ์ ์ฒด ๋ชฉ๋ก ์นดํ
๊ณ ๋ฆฌ ์ถ๊ฐ** โก ์ฐ์ ์์: ๋ฎ์
- **์ถ๊ฐ**: ์์ฌ๋ชฉ๋ก ์นดํ
๊ณ ๋ฆฌ์ "์์ฌ ์ ์ฒด ๋ชฉ๋ก" ์ต์
- **๊ธฐ๋ฅ**: ๋ชจ๋ ์นดํ
๊ณ ๋ฆฌ ํตํฉ ์กฐํ
- **์ ๋ ฌ**: ์นดํ
๊ณ ๋ฆฌ๋ณ ๊ทธ๋ฃนํ ๋๋ ํตํฉ ์ ๋ ฌ
#### **10. ์์ฌ ๋ชฉ๋ก ๋ถ๋ฅ ํํฐ ๊ธฐ๋ฅ** โก ์ฐ์ ์์: ์ค๊ฐ
- **์์น**: ์์ฌ ๋ชฉ๋ก ํ์ด์ง ๋ถ๋ฅ ์น์
- **๊ธฐ๋ฅ**: ์นดํ
๊ณ ๋ฆฌ๋ณ, ์ฌ์ง๋ณ, ์ฌ์ด์ฆ๋ณ ํํฐ๋ง
- **UI**: ๋๋กญ๋ค์ด ๋๋ ์ฒดํฌ๋ฐ์ค ํํ
#### **11. ์์ฌ ๋ฆฌ๋น์ ๋น๊ต ๊ฐ์ ** โก ์ฐ์ ์์: ๋์
- **ํ์ฌ**: ๊ณผ๊ฑฐ ๊ธฐ์ค ์๋ ๊ฒ๋ง ํ์
- **๊ฐ์ **: ๋จ๋ ๊ฒ(๊ธฐ์กด) / ํ์ํ ๊ฒ(์ ๊ท) ๋ถ๋ฆฌ ํํ
- **UI**: ํญ ๋๋ ์น์
์ผ๋ก ๊ตฌ๋ถํ์ฌ ํ์
### ๐ **๊ตฌํ ์ฐ์ ์์**
#### **Phase 1: ํต์ฌ ๊ธฐ๋ฅ ๊ฐ์ ** (1-2์ฃผ)
1. ์ฌ์ฉ์ ์๊ตฌ์ฌํญ ์์
๋ฐ์ (#1)
2. ์ฌ์ง GRADE ์ ์ฒด ํ๊ธฐ (#2)
3. ์น ํ๋ฉด ๋ด์ฉ ์๋ฆผ ํด๊ฒฐ (#8)
4. ์์ฌ ๋ฆฌ๋น์ ๋น๊ต ๊ฐ์ (#11)
#### **Phase 2: ๋ถ๋ฅ ๋ฐ ํ๊ธฐ ๊ฐ์ ** (2-3์ฃผ)
5. ํ๋์ง ํ์
์ ๋ณด ํ์ฅ (#5)
6. Nipple ๋๋จ ์ ๋ณด ํ๊ธฐ (#6)
7. Reducing ๋ฐฐ๊ด Schedule ๋ถ๋ฆฌ (#7)
8. ์์ฌ ๋ชฉ๋ก ๋ถ๋ฅ ํํฐ ๊ธฐ๋ฅ (#10)
#### **Phase 3: ์ ๊ท ์นดํ
๊ณ ๋ฆฌ ๋ฐ ๊ธฐ๋ฅ** (3-4์ฃผ)
9. U-Bolt & Urethane Block ์นดํ
๊ณ ๋ฆฌ (#3)
10. Special Flange ๋น๊ธฐ์ฑํ ์ ๋ฆฌ (#4)
11. ์์ฌ ์ ์ฒด ๋ชฉ๋ก ์นดํ
๊ณ ๋ฆฌ ์ถ๊ฐ (#9)
### ๐ **๊ฐ๋ฐ ๊ฐ์ด๋๋ผ์ธ**
#### **์ฝ๋ ์์ ์์น**
- **ํ์ ํธํ์ฑ**: ๊ธฐ์กด ๋ฐ์ดํฐ ๊ตฌ์กฐ ์ ์ง
- **์ ์ง์ ๊ฐ์ **: ๋จ๊ณ๋ณ ๊ตฌํ์ผ๋ก ์์ ์ฑ ํ๋ณด
- **ํ
์คํธ**: ๊ฐ ๊ฐ์ ์ฌํญ๋ณ ์ถฉ๋ถํ ํ
์คํธ
- **๋ฌธ์ํ**: ๋ณ๊ฒฝ์ฌํญ ์ฆ์ ๋ฌธ์ ๋ฐ์
#### **๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณ๊ฒฝ**
- **์คํค๋ง ํ์ฅ**: ๊ธฐ์กด ํ
์ด๋ธ์ ์ปฌ๋ผ ์ถ๊ฐ ๋ฐฉ์ ์ฐ์
- **๋ง์ด๊ทธ๋ ์ด์
**: ๋จ๊ณ๋ณ ์คํฌ๋ฆฝํธ ์์ฑ
- **๋ฐฑ์
**: ๋ณ๊ฒฝ ์ ๋ฐ์ดํฐ ๋ฐฑ์
ํ์
#### **UI/UX ๊ฐ์ **
- **๋ฐ์ํ**: ๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ ํธํ์ฑ ์ ์ง
- **์ ๊ทผ์ฑ**: ์ฌ์ฉ์ ์นํ์ ์ธํฐํ์ด์ค
- **์ฑ๋ฅ**: ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ต์ ํ
---
## ๐ ๋ฉ์ธ ์๋ฒ ๋ฐฐํฌ ๊ฐ์ด๋
### ๐ **๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
**
๋ฉ์ธ ์๋ฒ์ ๋ฐฐํฌํ ๋ ๋ฐ๋์ ์คํํด์ผ ํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
:
#### **ํ์ ์ถ๊ฐ ์ปฌ๋ผ๋ค** (materials ํ
์ด๋ธ)
```sql
-- ํ์ดํ ์ฌ์ด์ฆ ์ ๋ณด
ALTER TABLE materials ADD COLUMN IF NOT EXISTS main_nom VARCHAR(50);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS red_nom VARCHAR(50);
-- ์ ์ฒด ์ฌ์ง๋ช
ALTER TABLE materials ADD COLUMN IF NOT EXISTS full_material_grade TEXT;
-- ์
๋ก๋ ํ ๋ฒํธ
ALTER TABLE materials ADD COLUMN IF NOT EXISTS row_number INTEGER;
```
#### **์ฑ๋ฅ ์ต์ ํ ์ธ๋ฑ์ค**
```sql
CREATE INDEX IF NOT EXISTS idx_materials_main_nom ON materials(main_nom);
CREATE INDEX IF NOT EXISTS idx_materials_red_nom ON materials(red_nom);
CREATE INDEX IF NOT EXISTS idx_materials_full_material_grade ON materials(full_material_grade);
```
#### **์๋ ๋ง์ด๊ทธ๋ ์ด์
์คํฌ๋ฆฝํธ**
```bash
# ๋ฉ์ธ ์๋ฒ์์ ์คํ
psql -U tkmp_user -d tk_mp_bom -f backend/scripts/PRODUCTION_MIGRATION.sql
```
### โ ๏ธ **์ค์ ์ฌํญ**
- ์ด ์ปฌ๋ผ๋ค์ด ์์ผ๋ฉด ํ์ผ ์
๋ก๋ ์ 500 ์๋ฌ ๋ฐ์
- ์์ ์ด๊ธฐํ ์: `database/init/99_complete_schema.sql` ์ฌ์ฉ
- ๊ธฐ์กด ์๋ฒ ์
๋ฐ์ดํธ ์: `backend/scripts/PRODUCTION_MIGRATION.sql` ์ฌ์ฉ
### ๐ง **๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ**
1. [ ] ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์
2. [ ] ๋ง์ด๊ทธ๋ ์ด์
์คํฌ๋ฆฝํธ ์คํ
3. [ ] ์ปฌ๋ผ ์กด์ฌ ํ์ธ
4. [ ] ํ์ผ ์
๋ก๋ ํ
์คํธ
5. [ ] ์์ฌ ๋ถ๋ฅ ๊ธฐ๋ฅ ํ
์คํธ
---
**๋ง์ง๋ง ์
๋ฐ์ดํธ**: 2025๋
9์ 28์ผ (๋ฉ์ธ ์๋ฒ ๋ฐฐํฌ ๊ฐ์ด๋ ์ถ๊ฐ)