# ๐ง PIPE Cutting Plan & ์ด์ ๊ด๋ฆฌ ์์คํ
๊ฐ๋ฐ ๊ฐ์ด๋
## ๐ ๋ชฉ์ฐจ
1. [์์คํ
๊ฐ์](#์์คํ
-๊ฐ์)
2. [์ ์ฒด ์ํฌํ๋ก์ฐ](#์ ์ฒด-์ํฌํ๋ก์ฐ)
3. [๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ](#๋ฐ์ดํฐ๋ฒ ์ด์ค-์ค๊ณ)
4. [ํ์ด์ง๋ณ ์์ธ ์ค๊ณ](#ํ์ด์ง๋ณ-์์ธ-์ค๊ณ)
5. [API ์๋ํฌ์ธํธ](#api-์๋ํฌ์ธํธ)
6. [๋ฆฌ๋น์ ๊ด๋ฆฌ ๋ก์ง](#๋ฆฌ๋น์ -๊ด๋ฆฌ-๋ก์ง)
7. [๊ฐ๋ฐ ์ฐ์ ์์](#๊ฐ๋ฐ-์ฐ์ ์์)
8. [๊ธฐ์ ์คํ](#๊ธฐ์ -์คํ)
---
## ๐ฏ ์์คํ
๊ฐ์
### **ํต์ฌ ๋ชฉ์ **
- PIPE ์์ฌ์ ์ฒด๊ณ์ ์ธ Cutting Plan ๊ด๋ฆฌ
- ๊ตฌ์ญ๋ณ/๋๋ฉด๋ณ ๋จ๊ด ์ ๋ณด ๊ด๋ฆฌ
- ๋ฆฌ๋น์ ์ ๋ณ๊ฒฝ์ฌํญ ์ถ์ ๋ฐ ๋น๊ต
- ํ์ฅ ์ด์ ๋ฐ ๋ฌธ์ ์ ์ฒด๊ณ์ ๊ธฐ๋ก
### **์ฃผ์ ํน์ง**
- **2๋จ๊ณ ์ํฌํ๋ก์ฐ**: ๊ตฌ์ญ ํ ๋น โ ๋ผ์ธ๋ฒํธ ์
๋ ฅ
- **์ค๋งํธ ๋ฆฌ๋น์ **: ๊ธฐ์กด ๋ฐ์ดํฐ์ ์ ๊ท BOM ์๋ ๋น๊ต
- **ํ์ฅ ์ด์ ๊ด๋ฆฌ**: ๋๋ฉด๋ณ/๋จ๊ด๋ณ ๋ฌธ์ ์ ์ถ์
- **Excel ์ฐ๋**: ์์ ํ ๋ฐ์ดํฐ ๋ด๋ณด๋ด๊ธฐ ์ง์
---
## ๐ ์ ์ฒด ์ํฌํ๋ก์ฐ
```mermaid
graph TD
A[BOM ์
๋ก๋] --> B{PIPE ๋ฐ์ดํฐ ์ถ์ถ}
B --> C[1๋จ๊ณ: ๊ตฌ์ญ๋ณ ๋๋ฉด ํ ๋น]
C --> D[2๋จ๊ณ: ๋ผ์ธ๋ฒํธ ์
๋ ฅ]
D --> E[3๋จ๊ณ: ๋จ๊ด ์ ๋ณด ํ์ธ]
E --> F[Cutting Plan ์ ์ฅ]
F --> G{์๋ก์ด ๋ฆฌ๋น์ ?}
G -->|Yes| H[4๋จ๊ณ: ๋ฆฌ๋น์ ๋น๊ต]
G -->|No| I[์ด์ ๊ด๋ฆฌ ํ์ด์ง]
H --> J[๋ณ๊ฒฝ์ฌํญ ํ์ธ/์์ ]
J --> K[5๋จ๊ณ: ๋ณ๊ฒฝ ๋๋ฉด ์์ฝ]
K --> L[ํ์ดํ ๊ตฌ๋งค๋ ์ฌ๊ณ์ฐ]
L --> I
I --> M[ํ์ฅ ์ด์ ์
๋ ฅ]
M --> N[Excel ๋ด๋ณด๋ด๊ธฐ]
```
---
## ๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ
### **1. pipe_cutting_plans** (๋จ๊ด ์ ๋ณด)
```sql
CREATE TABLE pipe_cutting_plans (
id SERIAL PRIMARY KEY,
job_no VARCHAR(50) NOT NULL,
file_id INTEGER REFERENCES files(id),
-- ๊ธฐ๋ณธ ์ ๋ณด
area VARCHAR(10), -- #01, #02 ๋ฑ
drawing_name VARCHAR(100) NOT NULL, -- P&ID-001
line_no VARCHAR(50) NOT NULL, -- LINE-A-001
-- ํ์ดํ ์ ๋ณด
material_grade VARCHAR(50), -- A106 GR.B
schedule_spec VARCHAR(20), -- SCH40, SCH80
nominal_size VARCHAR(50), -- 4", 6", 8"
length_mm DECIMAL(10,3) NOT NULL, -- 1500.0
-- ๋๋จ ์ ๋ณด
end_preparation VARCHAR(20), -- ๋ฌด๊ฐ์ , ํ๊ฐ์ , ์๊ฐ์
-- ๋ฉํ๋ฐ์ดํฐ
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
-- ์ธ๋ฑ์ค
INDEX idx_cutting_plans_job (job_no),
INDEX idx_cutting_plans_area_drawing (area, drawing_name),
INDEX idx_cutting_plans_material (material_grade, nominal_size)
);
```
### **2. pipe_revision_comparisons** (๋ฆฌ๋น์ ๋น๊ต)
```sql
CREATE TABLE pipe_revision_comparisons (
id SERIAL PRIMARY KEY,
job_no VARCHAR(50) NOT NULL,
current_file_id INTEGER REFERENCES files(id),
previous_cutting_plan_id INTEGER REFERENCES pipe_cutting_plans(id),
-- ๋น๊ต ๊ฒฐ๊ณผ
comparison_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_drawings INTEGER, -- ์ ์ฒด ๋๋ฉด ์
changed_drawings INTEGER, -- ๋ณ๊ฒฝ๋ ๋๋ฉด ์
unchanged_drawings INTEGER, -- ๋ณ๊ฒฝ๋์ง ์์ ๋๋ฉด ์
-- ๋จ๊ด ๋ณ๊ฒฝ ํต๊ณ
total_segments INTEGER, -- ์ ์ฒด ๋จ๊ด ์
added_segments INTEGER, -- ์ถ๊ฐ๋ ๋จ๊ด
removed_segments INTEGER, -- ์ญ์ ๋ ๋จ๊ด
modified_segments INTEGER, -- ์์ ๋ ๋จ๊ด
unchanged_segments INTEGER, -- ๋ณ๊ฒฝ๋์ง ์์ ๋จ๊ด
-- ๋ฉํ๋ฐ์ดํฐ
created_by VARCHAR(100),
is_applied BOOLEAN DEFAULT FALSE,
applied_at TIMESTAMP,
applied_by VARCHAR(100)
);
```
### **3. pipe_revision_changes** (๋ฆฌ๋น์ ๋ณ๊ฒฝ ์์ธ)
```sql
CREATE TABLE pipe_revision_changes (
id SERIAL PRIMARY KEY,
comparison_id INTEGER REFERENCES pipe_revision_comparisons(id),
-- ๋ณ๊ฒฝ ์ ๋ณด
drawing_name VARCHAR(100) NOT NULL,
change_type VARCHAR(20) NOT NULL, -- 'added', 'removed', 'modified', 'unchanged'
-- ์ด์ ๋ฐ์ดํฐ (์์ /์ญ์ ์ ๊ฒฝ์ฐ)
old_line_no VARCHAR(50),
old_material_grade VARCHAR(50),
old_schedule_spec VARCHAR(20),
old_nominal_size VARCHAR(50),
old_length_mm DECIMAL(10,3),
old_end_preparation VARCHAR(20),
-- ์๋ก์ด ๋ฐ์ดํฐ (์ถ๊ฐ/์์ ์ ๊ฒฝ์ฐ)
new_line_no VARCHAR(50),
new_material_grade VARCHAR(50),
new_schedule_spec VARCHAR(20),
new_nominal_size VARCHAR(50),
new_length_mm DECIMAL(10,3),
new_end_preparation VARCHAR(20),
-- ๋ณ๊ฒฝ ์ฌ์
change_reason TEXT,
-- ๋ฉํ๋ฐ์ดํฐ
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### **4. pipe_drawing_issues** (๋๋ฉด ์ ๋ฐ ์ด์)
```sql
CREATE TABLE pipe_drawing_issues (
id SERIAL PRIMARY KEY,
job_no VARCHAR(50) NOT NULL,
area VARCHAR(10) NOT NULL,
drawing_name VARCHAR(100) NOT NULL,
-- ์ด์ ์ ๋ณด
issue_description TEXT NOT NULL,
severity VARCHAR(20) DEFAULT 'medium', -- 'low', 'medium', 'high', 'critical'
status VARCHAR(20) DEFAULT 'open', -- 'open', 'in_progress', 'resolved'
-- ํด๊ฒฐ ์ ๋ณด
resolution_notes TEXT,
resolved_by VARCHAR(100),
resolved_at TIMESTAMP,
-- ๋ฉํ๋ฐ์ดํฐ
reported_by VARCHAR(100),
reported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### **5. pipe_segment_issues** (๋จ๊ด๋ณ ์ด์)
```sql
CREATE TABLE pipe_segment_issues (
id SERIAL PRIMARY KEY,
job_no VARCHAR(50) NOT NULL,
area VARCHAR(10) NOT NULL,
drawing_name VARCHAR(100) NOT NULL,
line_no VARCHAR(50) NOT NULL,
-- ์๋ณธ ์ ๋ณด
original_material_info VARCHAR(100),
original_length DECIMAL(10,3),
-- ์ด์ ์ ๋ณด
issue_description TEXT NOT NULL,
issue_type VARCHAR(50), -- 'cutting', 'installation', 'material', 'routing', 'other'
-- ๋ณ๊ฒฝ์ฌํญ (์๋ ๊ฒฝ์ฐ)
length_change DECIMAL(10,3), -- +/- ๋ณ๊ฒฝ๋
new_length DECIMAL(10,3), -- ์ต์ข
๊ธธ์ด
material_change VARCHAR(100), -- ์ฌ์ง ๋ณ๊ฒฝ ์ ๋ณด
-- ์ด์ ๊ด๋ฆฌ
severity VARCHAR(20) DEFAULT 'medium',
status VARCHAR(20) DEFAULT 'open',
-- ํด๊ฒฐ ์ ๋ณด
resolution_notes TEXT,
resolved_by VARCHAR(100),
resolved_at TIMESTAMP,
-- ๋ฉํ๋ฐ์ดํฐ
cutting_plan_id INTEGER REFERENCES pipe_cutting_plans(id),
reported_by VARCHAR(100),
reported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
---
## ๐ ํ์ด์ง๋ณ ์์ธ ์ค๊ณ
### **1. PIPE Cutting Plan ๋ฉ์ธ ํ์ด์ง**
**ํ์ผ**: `frontend/src/pages/revision/PipeCuttingPlanPage.jsx`
#### **1๋จ๊ณ: ๊ตฌ์ญ๋ณ ๋๋ฉด ํ ๋น**
```javascript
// UI ๊ตฌ์กฐ
๐ 1๋จ๊ณ: ๊ตฌ์ญ๋ณ ๋๋ฉด ํ ๋น
{/* ๊ตฌ์ญ ์
๋ ฅ */}
setCurrentArea(e.target.value)}
/>
{/* ๋๋ฉด ์ ํ */}
๐ ์ฌ์ฉ ๊ฐ๋ฅํ ๋๋ฉด ๋ชฉ๋ก
{availableDrawings.map(drawing => (
))}
{/* ๊ตฌ์ญ๋ณ ํ ๋น ํํฉ */}
{areas.map(area => (
{area.name}
{area.drawings.map(drawing => (
{drawing}
))}
))}
```
#### **2๋จ๊ณ: ๋ผ์ธ๋ฒํธ ์
๋ ฅ**
```javascript
๐ข 2๋จ๊ณ: ๋ผ์ธ๋ฒํธ ์
๋ ฅ
{/* ๊ตฌ์ญ/๋๋ฉด ์ ํ */}
{/* ๋จ๊ด ์ ๋ณด ํ
์ด๋ธ */}
```
#### **3๋จ๊ณ: ๋จ๊ด ์ ๋ณด ํ์ธ**
```javascript
โ
3๋จ๊ณ: ๋จ๊ด ์ ๋ณด ํ์ธ
{/* ์ ์ฒด ์์ฝ */}
๐ ์ ์ฒด ํํฉ
์ด ๊ตฌ์ญ: {areas.length}๊ฐ
์ด ๋๋ฉด: {totalDrawings}๊ฐ
์ด ๋จ๊ด: {totalSegments}๊ฐ
{/* ๊ตฌ์ญ๋ณ ์์ธ */}
{areas.map(area => (
๐๏ธ {area.name} ๊ตฌ์ญ
{area.drawings.map(drawing => (
๐ {drawing}
| ๋ผ์ธ๋ฒํธ |
์ฌ์ง/๊ท๊ฒฉ |
๊ธธ์ด |
๋๋จ๊ฐ๊ณต |
{getSegmentsForDrawing(area.name, drawing).map(segment => (
| {segment.line_no} |
{segment.material_grade} {segment.schedule_spec} {segment.nominal_size} |
{segment.length_mm}mm |
{segment.end_preparation} |
))}
))}
))}
```
### **2. ๋ฆฌ๋น์ ๋น๊ต ํ์ด์ง**
**ํ์ผ**: `frontend/src/pages/revision/PipeRevisionComparisonPage.jsx`
#### **4๋จ๊ณ: ๋ฆฌ๋น์ ๋น๊ต**
```javascript
๐ 4๋จ๊ณ: ๋ฆฌ๋น์ ๋น๊ต
{/* ๋น๊ต ์์ฝ */}
โ ์ถ๊ฐ๋ ๋จ๊ด
{comparisonResult.added_segments}
โ ์ญ์ ๋ ๋จ๊ด
{comparisonResult.removed_segments}
๐ง ์์ ๋ ๋จ๊ด
{comparisonResult.modified_segments}
โ
๋ณ๊ฒฝ์์
{comparisonResult.unchanged_segments}
{/* ๋ณ๊ฒฝ๋ ๋๋ฉด๋ง ํ์ */}
๐ ๋ณ๊ฒฝ๋ ๋๋ฉด ๋ชฉ๋ก
{changedDrawings.map(drawing => (
๐ {drawing.name}
| ์ํ |
๋ผ์ธ๋ฒํธ |
์ฌ์ง/๊ท๊ฒฉ |
๊ธธ์ด |
๋๋จ๊ฐ๊ณต |
๋ณ๊ฒฝ์ฌํญ |
{drawing.segments.map(segment => (
|
{getStatusLabel(segment.change_type)}
|
{segment.line_no} |
{segment.material_info} |
{segment.length_mm}mm |
{segment.end_preparation} |
{segment.change_type !== 'unchanged' && (
{segment.changes.map(change => (
{change.field}:
{change.old_value} โ {change.new_value}
))}
)}
|
))}
))}
```
#### **5๋จ๊ณ: ๋ณ๊ฒฝ ๋๋ฉด ์์ฝ**
```javascript
๐ 5๋จ๊ณ: ๋ณ๊ฒฝ ๋๋ฉด ์์ฝ
{/* ๋ณ๊ฒฝ ๋๋ฉด๋ง ์ง์ค ํ์ */}
โ ๏ธ ๊ฒํ ๊ฐ ํ์ํ ๋๋ฉด๋ค
{changedDrawingsOnly.map(drawing => (
๐ {drawing.name}
+{drawing.added_count}
-{drawing.removed_count}
~{drawing.modified_count}
๐ ํ์ดํ ๊ตฌ๋งค๋ ์ํฅ
{drawing.material_impacts.map(impact => (
{impact.material}
{impact.length_change > 0 ? '+' : ''}{impact.length_change}m
({impact.percentage_change > 0 ? '+' : ''}{impact.percentage_change}%)
))}
))}
{/* ์ ์ฒด ๊ตฌ๋งค๋ ์ฌ๊ณ์ฐ */}
๐ฐ ํ์ดํ ๊ตฌ๋งค๋ ์ฌ๊ณ์ฐ
์ด์ ๊ตฌ๋งค ๊ณํ
{previousPurchasePlan.map(item => (
{item.material}
{item.total_length}m
))}
์ ๊ท ๊ตฌ๋งค ๊ณํ
{newPurchasePlan.map(item => (
{item.material}
{item.total_length}m
{item.additional_needed > 0 && (
(+{item.additional_needed}m ์ถ๊ฐ ํ์)
)}
))}
```
### **3. ๋จ๊ด ์ด์ ๊ด๋ฆฌ ํ์ด์ง**
**ํ์ผ**: `frontend/src/pages/PipeIssueManagementPage.jsx`
```javascript
๐ ๏ธ ๋จ๊ด ์ด์ ๊ด๋ฆฌ
ํ์ฅ์์ ๋ฐ์ํ๋ ํ์ดํ ๊ด๋ จ ์ด์๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค
{/* ๊ฒ์/ํํฐ ์น์
*/}
{/* ๋๋ฉด ์ ๋ฐ ์ด์ ์น์
*/}
๐ ๋๋ฉด ์ ๋ฐ ์ด์
{/* ๊ธฐ์กด ์ด์ ๋ชฉ๋ก */}
{drawingIssues.map(issue => (
{issue.severity.toUpperCase()}
{issue.status.toUpperCase()}
{issue.issue_description}
๋ณด๊ณ ์: {issue.reported_by} | {issue.reported_at}
{issue.status === 'open' && (
)}
))}
{/* ์ ์ด์ ์ถ๊ฐ */}
โ ์ ๋๋ฉด ์ด์ ์ถ๊ฐ
{/* ๋จ๊ด๋ณ ์์ธ ์ ๋ณด ์น์
*/}
๐ง ๋จ๊ด๋ณ ์์ธ ์ ๋ณด
{segments.length > 0 && (
| ๋ผ์ธ๋ฒํธ |
์ฌ์ง/๊ท๊ฒฉ |
๊ธธ์ด |
๋๋จ๊ฐ๊ณต |
์ด์ ์ํ |
์ก์
|
{segments.map(segment => (
| {segment.line_no} |
{segment.material_grade} {segment.schedule_spec} {segment.nominal_size} |
{segment.length_mm}mm |
{segment.end_preparation} |
{segment.has_issues ? (
โ ๏ธ ์ด์์์
) : (
โ
์ ์
)}
|
|
))}
)}
{/* ๋จ๊ด ์ด์ ์
๋ ฅ ๋ค์ด์ผ๋ก๊ทธ */}
{showSegmentIssueDialog && (
๐ง {selectedSegment.line_no} ์ด์ ๊ด๋ฆฌ
๊ตฌ์ญ: {selectedArea}
๋๋ฉด: {selectedDrawing}
์ฌ์ง: {selectedSegment.material_info}
์๋ ๊ธธ์ด: {selectedSegment.length_mm}mm
)}
{/* ์ด์ ๋ฆฌํฌํธ ์น์
*/}
๐ ์ด์ ๋ฆฌํฌํธ
```
---
## ๐ API ์๋ํฌ์ธํธ
### **Backend ๋ผ์ฐํฐ ๊ตฌ์กฐ**
```
backend/app/routers/
โโโ pipe_cutting_plan.py # Cutting Plan ๊ด๋ฆฌ
โโโ pipe_revision.py # ๋ฆฌ๋น์ ๋น๊ต ๋ฐ ๊ด๋ฆฌ
โโโ pipe_issue_management.py # ์ด์ ๊ด๋ฆฌ
```
### **1. Cutting Plan API**
```python
# backend/app/routers/pipe_cutting_plan.py
@router.get("/pipe-cutting-plan/drawings/{job_no}")
async def get_available_drawings(job_no: str):
"""์ฌ์ฉ ๊ฐ๋ฅํ ๋๋ฉด ๋ชฉ๋ก ์กฐํ"""
pass
@router.post("/pipe-cutting-plan/area-assignment")
async def save_area_assignment(assignment_data: AreaAssignmentRequest):
"""๊ตฌ์ญ๋ณ ๋๋ฉด ํ ๋น ์ ์ฅ"""
pass
@router.get("/pipe-cutting-plan/segments/{job_no}/{area}/{drawing}")
async def get_pipe_segments(job_no: str, area: str, drawing: str):
"""ํน์ ๊ตฌ์ญ/๋๋ฉด์ ๋จ๊ด ์ ๋ณด ์กฐํ"""
pass
@router.post("/pipe-cutting-plan/segments")
async def save_pipe_segments(segments_data: PipeSegmentsRequest):
"""๋จ๊ด ์ ๋ณด ์ ์ฅ"""
pass
@router.get("/pipe-cutting-plan/export/{job_no}")
async def export_cutting_plan_excel(job_no: str):
"""Cutting Plan Excel ๋ด๋ณด๋ด๊ธฐ"""
pass
```
### **2. Revision API**
```python
# backend/app/routers/pipe_revision.py
@router.post("/pipe-revision/compare")
async def compare_pipe_revisions(comparison_request: PipeRevisionComparisonRequest):
"""ํ์ดํ ๋ฆฌ๋น์ ๋น๊ต"""
pass
@router.get("/pipe-revision/comparison/{comparison_id}")
async def get_revision_comparison(comparison_id: int):
"""๋ฆฌ๋น์ ๋น๊ต ๊ฒฐ๊ณผ ์กฐํ"""
pass
@router.post("/pipe-revision/apply-changes")
async def apply_revision_changes(changes_data: RevisionChangesRequest):
"""๋ฆฌ๋น์ ๋ณ๊ฒฝ์ฌํญ ์ ์ฉ"""
pass
@router.get("/pipe-revision/purchase-impact/{comparison_id}")
async def get_purchase_impact(comparison_id: int):
"""๊ตฌ๋งค๋ ์ํฅ ๋ถ์"""
pass
```
### **3. Issue Management API**
```python
# backend/app/routers/pipe_issue_management.py
@router.get("/pipe-issues/drawings/{job_no}/{area}")
async def get_drawing_issues(job_no: str, area: str):
"""๋๋ฉด ์ด์ ๋ชฉ๋ก ์กฐํ"""
pass
@router.post("/pipe-issues/drawing")
async def create_drawing_issue(issue_data: DrawingIssueRequest):
"""๋๋ฉด ์ด์ ์์ฑ"""
pass
@router.get("/pipe-issues/segments/{job_no}/{area}/{drawing}")
async def get_segment_issues(job_no: str, area: str, drawing: str):
"""๋จ๊ด ์ด์ ๋ชฉ๋ก ์กฐํ"""
pass
@router.post("/pipe-issues/segment")
async def create_segment_issue(issue_data: SegmentIssueRequest):
"""๋จ๊ด ์ด์ ์์ฑ"""
pass
@router.get("/pipe-issues/report/{job_no}")
async def generate_issue_report(job_no: str):
"""์ด์ ๋ฆฌํฌํธ ์์ฑ"""
pass
```
---
## โ๏ธ ๋ฆฌ๋น์ ๊ด๋ฆฌ ๋ก์ง
### **๋ฆฌ๋น์ ๊ท์น**
#### **1. Cutting Plan ์์ฑ ์ ๋ฆฌ๋น์ **
```python
def handle_pre_cutting_plan_revision(job_no: str, new_file_id: int):
"""
Cutting Plan ์์ฑ ๋ฒํผ์ ๋๋ฅด๊ธฐ ์ ๋ฆฌ๋น์ ์ฒ๋ฆฌ
- ๊ธฐ์กด ๋จ๊ด ์ ๋ณด ์ ์ฒด ์ญ์
- ์ BOM ํ์ผ ์
๋ก๋ ํ์
"""
# ๊ธฐ์กด ๋จ๊ด ์ ๋ณด ์ญ์
delete_existing_pipe_segments(job_no)
# ์ BOM ๋ฐ์ดํฐ ์ถ์ถ ๋ฐ ์ ์ฅ
extract_and_save_pipe_data(new_file_id)
return {
"status": "pre_cutting_plan_revision",
"message": "๊ธฐ์กด ๋จ๊ด ์ ๋ณด๊ฐ ์ญ์ ๋์์ต๋๋ค. ์๋ก์ด Cutting Plan์ ์์ฑํด์ฃผ์ธ์.",
"requires_new_cutting_plan": True
}
```
#### **2. Cutting Plan ์์ฑ ํ ๋ฆฌ๋น์ **
```python
def handle_post_cutting_plan_revision(job_no: str, new_file_id: int):
"""
Cutting Plan ์ ์ฅ ํ ๋ฆฌ๋น์ ์ฒ๋ฆฌ
- ๊ธฐ์กด ์ ์ฅ๋ ๋ฐ์ดํฐ์ ์ ๊ท BOM ๋น๊ต
- ๋๋ฉด๋ณ ๋ณ๊ฒฝ์ฌํญ ๋ถ์
"""
# ๊ธฐ์กด Cutting Plan ์กฐํ
existing_plan = get_existing_cutting_plan(job_no)
# ์ ๊ท BOM์์ ํ์ดํ ๋ฐ์ดํฐ ์ถ์ถ
new_pipe_data = extract_pipe_data_from_bom(new_file_id)
# ๋๋ฉด๋ณ ๋น๊ต ์ํ
comparison_result = compare_pipe_data_by_drawing(existing_plan, new_pipe_data)
return {
"status": "post_cutting_plan_revision",
"comparison_id": save_comparison_result(comparison_result),
"changed_drawings": comparison_result.changed_drawings,
"requires_review": True
}
```
#### **3. ๋จ๊ด ๋น๊ต ๋ก์ง**
```python
def compare_pipe_segments(existing_segments, new_segments):
"""
๋จ๊ด๋ณ ์์ธ ๋น๊ต
- ๋๋ฉด๋ช
, ๊ธธ์ด, ์ฌ์ง, ๋๋จ๊ฐ๊ณต ์ ๋ณด ๋น๊ต
- ๋ณ๊ฒฝ ์ ํ ๋ถ๋ฅ: added, removed, modified, unchanged
"""
comparison_results = []
for drawing_name in set(existing_segments.keys()) | set(new_segments.keys()):
existing = existing_segments.get(drawing_name, [])
new = new_segments.get(drawing_name, [])
# ๋จ๊ด๋ณ ๋งค์นญ ๋ฐ ๋น๊ต
matched_pairs, added, removed = match_segments(existing, new)
for old_segment, new_segment in matched_pairs:
if segments_are_identical(old_segment, new_segment):
comparison_results.append({
"drawing_name": drawing_name,
"change_type": "unchanged",
"segment_data": new_segment
})
else:
comparison_results.append({
"drawing_name": drawing_name,
"change_type": "modified",
"old_data": old_segment,
"new_data": new_segment,
"changes": get_segment_changes(old_segment, new_segment)
})
# ์ถ๊ฐ๋ ๋จ๊ด
for segment in added:
comparison_results.append({
"drawing_name": drawing_name,
"change_type": "added",
"segment_data": segment
})
# ์ญ์ ๋ ๋จ๊ด
for segment in removed:
comparison_results.append({
"drawing_name": drawing_name,
"change_type": "removed",
"segment_data": segment
})
return comparison_results
def segments_are_identical(segment1, segment2):
"""๋จ๊ด ๋์ผ์ฑ ๊ฒ์ฌ"""
return (
segment1.material_grade == segment2.material_grade and
segment1.schedule_spec == segment2.schedule_spec and
segment1.nominal_size == segment2.nominal_size and
segment1.length_mm == segment2.length_mm and
segment1.end_preparation == segment2.end_preparation
)
```
#### **4. ๊ตฌ๋งค๋ ์ฌ๊ณ์ฐ**
```python
def recalculate_pipe_purchase_requirements(comparison_id: int):
"""
๋ฆฌ๋น์ ํ ํ์ดํ ๊ตฌ๋งค๋ ์ฌ๊ณ์ฐ
- ํ์ ๋ ๋จ๊ด ์ ๋ณด๋ก๋ถํฐ ์ด ๊ธธ์ด ๊ณ์ฐ
- ๊ธฐ์กด ๊ตฌ๋งค ์ ์ฒญ๋๊ณผ ๋น๊ต
- ์ถ๊ฐ ๊ตฌ๋งค ํ์๋ ์ฐ์ถ
"""
# ํ์ ๋ ๋จ๊ด ์ ๋ณด ์กฐํ
confirmed_segments = get_confirmed_segments_from_comparison(comparison_id)
# ์ฌ์ง๋ณ ์ด ๊ธธ์ด ๊ณ์ฐ
material_requirements = calculate_total_length_by_material(confirmed_segments)
# ๊ธฐ์กด PIPE BOM ํ์ด์ง์ ๊ตฌ๋งค ์ ์ฒญ ์ ๋ณด ์กฐํ
existing_purchase_requests = get_existing_pipe_purchase_requests(job_no)
# ์ถ๊ฐ ๊ตฌ๋งค ํ์๋ ๊ณ์ฐ
additional_requirements = []
for material, required_length in material_requirements.items():
existing_request = existing_purchase_requests.get(material, 0)
if required_length > existing_request:
additional_requirements.append({
"material": material,
"existing_request": existing_request,
"total_required": required_length,
"additional_needed": required_length - existing_request
})
return additional_requirements
```
---
## ๐
๊ฐ๋ฐ ์ฐ์ ์์
### **Phase 1: ๊ธฐ๋ณธ Cutting Plan ์์คํ
** (2์ฃผ)
1. โ
`PipeCuttingPlanPage.jsx` ๊ธฐ๋ณธ ๊ตฌ์กฐ ์์ฑ
2. ๐ 2๋จ๊ณ ์ํฌํ๋ก์ฐ ๊ตฌํ
- 1๋จ๊ณ: ๊ตฌ์ญ๋ณ ๋๋ฉด ํ ๋น
- 2๋จ๊ณ: ๋ผ์ธ๋ฒํธ ์
๋ ฅ
- 3๋จ๊ณ: ํ์ธ ๋ฐ ์ ์ฅ
3. ๐ Excel ๋ด๋ณด๋ด๊ธฐ ๊ธฐ๋ฅ
4. ๐๏ธ ๊ธฐ๋ณธ DB ํ
์ด๋ธ ์์ฑ ๋ฐ ๋ง์ด๊ทธ๋ ์ด์
### **Phase 2: ๋ฆฌ๋น์ ๋น๊ต ์์คํ
** (3์ฃผ)
1. ๐ ๋ฆฌ๋น์ ๊ฐ์ง ๋ฐ ๋น๊ต ๋ก์ง
2. ๐ 4๋จ๊ณ: ๋ฆฌ๋น์ ๋น๊ต ํ์ด์ง
3. ๐ 5๋จ๊ณ: ๋ณ๊ฒฝ ๋๋ฉด ์์ฝ ํ์ด์ง
4. ๐ฐ ๊ตฌ๋งค๋ ์ฌ๊ณ์ฐ ๊ธฐ๋ฅ
5. ๐ PIPE ๋ฆฌ๋น์ ํ์ด์ง ์ฐ๋
### **Phase 3: ์ด์ ๊ด๋ฆฌ ์์คํ
** (2์ฃผ)
1. ๐ ๏ธ ๋จ๊ด ์ด์ ๊ด๋ฆฌ ํ์ด์ง
2. ๐ ๋๋ฉด๋ณ/๋จ๊ด๋ณ ์ด์ ์
๋ ฅ
3. ๐ ์ด์ ๋ฆฌํฌํธ ๋ฐ Excel ๋ด๋ณด๋ด๊ธฐ
4. ๐ ์ด์ ๊ฒ์ ๋ฐ ํํฐ๋ง
### **Phase 4: ๊ณ ๋ํ ๋ฐ ์ต์ ํ** (1์ฃผ)
1. ๐จ UI/UX ๊ฐ์
2. โก ์ฑ๋ฅ ์ต์ ํ
3. ๐งช ํ
์คํธ ์ฝ๋ ์์ฑ
4. ๐ ์ฌ์ฉ์ ๋งค๋ด์ผ ์์ฑ
---
## ๐ ๏ธ ๊ธฐ์ ์คํ
### **Frontend**
- **React 18**: ์ปดํฌ๋ํธ ๊ธฐ๋ฐ UI
- **CSS Modules**: ์คํ์ผ ๊ด๋ฆฌ
- **Axios**: API ํต์
- **React Hooks**: ์ํ ๊ด๋ฆฌ
### **Backend**
- **FastAPI**: REST API ์๋ฒ
- **SQLAlchemy**: ORM
- **PostgreSQL**: ๋ฐ์ดํฐ๋ฒ ์ด์ค
- **Pandas**: Excel ์ฒ๋ฆฌ
### **Database**
- **PostgreSQL 14+**: ๋ฉ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค
- **Redis**: ์บ์ (์ ํ์ฌํญ)
### **DevOps**
- **Docker**: ์ปจํ
์ด๋ํ
- **Docker Compose**: ๋ฉํฐ ์ปจํ
์ด๋ ๊ด๋ฆฌ
---
## ๐ ๊ฐ๋ฐ ์ฒดํฌ๋ฆฌ์คํธ
### **๋ฐ์ดํฐ๋ฒ ์ด์ค**
- [ ] `pipe_cutting_plans` ํ
์ด๋ธ ์์ฑ
- [ ] `pipe_revision_comparisons` ํ
์ด๋ธ ์์ฑ
- [ ] `pipe_revision_changes` ํ
์ด๋ธ ์์ฑ
- [ ] `pipe_drawing_issues` ํ
์ด๋ธ ์์ฑ
- [ ] `pipe_segment_issues` ํ
์ด๋ธ ์์ฑ
- [ ] ์ธ๋ฑ์ค ๋ฐ ์ธ๋ํค ์ค์
- [ ] ๋ง์ด๊ทธ๋ ์ด์
์คํฌ๋ฆฝํธ ์์ฑ
### **Backend API**
- [ ] `pipe_cutting_plan.py` ๋ผ์ฐํฐ ๊ตฌํ
- [ ] `pipe_revision.py` ๋ผ์ฐํฐ ๊ตฌํ
- [ ] `pipe_issue_management.py` ๋ผ์ฐํฐ ๊ตฌํ
- [ ] ๋ฐ์ดํฐ ์ถ์ถ ์๋น์ค ๊ตฌํ
- [ ] Excel ๋ด๋ณด๋ด๊ธฐ ์๋น์ค ๊ตฌํ
- [ ] ๋ฆฌ๋น์ ๋น๊ต ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ
### **Frontend Pages**
- [ ] `PipeCuttingPlanPage.jsx` 2๋จ๊ณ ์ํฌํ๋ก์ฐ
- [ ] `PipeRevisionComparisonPage.jsx` ๊ตฌํ
- [ ] `PipeIssueManagementPage.jsx` ๊ตฌํ
- [ ] ๊ณตํต ์ปดํฌ๋ํธ ๊ฐ๋ฐ
- [ ] CSS ์คํ์ผ๋ง
### **ํตํฉ ํ
์คํธ**
- [ ] ์ ์ฒด ์ํฌํ๋ก์ฐ ํ
์คํธ
- [ ] ๋ฆฌ๋น์ ์๋๋ฆฌ์ค ํ
์คํธ
- [ ] Excel ๋ด๋ณด๋ด๊ธฐ ํ
์คํธ
- [ ] ์ด์ ๊ด๋ฆฌ ๊ธฐ๋ฅ ํ
์คํธ
---
## ๐ฏ ์ฑ๊ณต ์งํ
1. **์ฌ์ฉ์ฑ**: ํ์ฅ ์์
์๊ฐ ์ง๊ด์ ์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
2. **์ ํ์ฑ**: ๋ฆฌ๋น์ ๋น๊ต ๊ฒฐ๊ณผ์ 100% ์ ํ์ฑ
3. **ํจ์จ์ฑ**: ๊ธฐ์กด ์์์
๋๋น 80% ์๊ฐ ๋จ์ถ
4. **์ถ์ ์ฑ**: ๋ชจ๋ ๋ณ๊ฒฝ์ฌํญ์ ์์ ํ ์ด๋ ฅ ๊ด๋ฆฌ
5. **ํ์ฅ์ฑ**: ํฅํ ์ถ๊ฐ ๊ธฐ๋ฅ ๊ฐ๋ฐ ์ฉ์ด์ฑ
---
์ด ๊ฐ์ด๋๋ฅผ ๋ฐํ์ผ๋ก ์ฒด๊ณ์ ์ด๊ณ ์ค์ฉ์ ์ธ PIPE ๊ด๋ฆฌ ์์คํ
์ ๊ตฌ์ถํ ์ ์์ต๋๋ค! ๐