Files
TK-BOM-Project/PIPE_DEVELOPMENT_GUIDE.md
Hyungi Ahn 8f42a1054e
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
🔧 완전한 스키마 자동화 시스템 구축
 주요 기능:
- 완전한 데이터베이스 스키마 분석 및 자동 마이그레이션 시스템
- 44개 테이블 완전 지원 (운영 서버 43개 + 1개 추가)
- 누락된 테이블/컬럼 자동 감지 및 생성

🔧 해결된 스키마 문제:
- users.status 컬럼 누락 → 자동 추가
- files 테이블 4개 컬럼 누락 → 자동 추가
- materials 테이블 22개 컬럼 누락 → 자동 추가
- support_details, purchase_requests, purchase_request_items 테이블 누락 → 자동 생성
- material_purchase_tracking.description, purchase_status 컬럼 누락 → 자동 추가

🚀 자동화 도구:
- schema_analyzer.py: 코드와 DB 스키마 비교 분석
- auto_migrator.py: 자동 마이그레이션 실행
- docker_migrator.py: Docker 환경용 간편 마이그레이션
- schema_monitor.py: 실시간 스키마 모니터링

📋 리비전 관리 시스템:
- 8개 카테고리별 리비전 페이지 구현
- PIPE Cutting Plan 관리 시스템
- PIPE Issue Management 시스템
- 완전한 리비전 비교 및 추적 기능

🎯 사용법:
docker exec tk-mp-backend python3 scripts/docker_migrator.py

앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
2025-10-21 10:34:45 +09:00

1124 lines
40 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔧 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 구조
<div className="area-assignment-stage">
<h3>📂 1단계: 구역별 도면 할당</h3>
{/* 구역 입력 */}
<div className="area-input">
<input
type="text"
placeholder="#01, #02 등 구역 입력"
value={currentArea}
onChange={(e) => setCurrentArea(e.target.value)}
/>
<button onClick={addArea}>구역 추가</button>
</div>
{/* 도면 선택 */}
<div className="drawing-selection">
<h4>📋 사용 가능한 도면 목록</h4>
{availableDrawings.map(drawing => (
<label key={drawing.name}>
<input
type="checkbox"
checked={selectedDrawings.includes(drawing.name)}
onChange={() => toggleDrawingSelection(drawing.name)}
/>
{drawing.name} ({drawing.pipe_count} 단관)
</label>
))}
</div>
{/* 구역별 할당 현황 */}
<div className="area-assignments">
{areas.map(area => (
<div key={area.name} className="area-card">
<h4>{area.name}</h4>
<div className="assigned-drawings">
{area.drawings.map(drawing => (
<span key={drawing} className="drawing-tag">
{drawing}
<button onClick={() => removeDrawingFromArea(area.name, drawing)}>×</button>
</span>
))}
</div>
</div>
))}
</div>
<button
className="btn-next-stage"
onClick={proceedToLineNumberInput}
disabled={!allDrawingsAssigned}
>
2단계: 라인번호 입력으로 진행
</button>
</div>
```
#### **2단계: 라인번호 입력**
```javascript
<div className="line-number-input-stage">
<h3>🔢 2단계: 라인번호 입력</h3>
{/* 구역/도면 선택 */}
<div className="area-drawing-selector">
<select value={selectedArea} onChange={(e) => setSelectedArea(e.target.value)}>
<option value="">구역 선택</option>
{areas.map(area => (
<option key={area.name} value={area.name}>{area.name}</option>
))}
</select>
<select value={selectedDrawing} onChange={(e) => setSelectedDrawing(e.target.value)}>
<option value="">도면 선택</option>
{getDrawingsForArea(selectedArea).map(drawing => (
<option key={drawing} value={drawing}>{drawing}</option>
))}
</select>
</div>
{/* 단관 정보 테이블 */}
<div className="pipe-segments-table">
<table>
<thead>
<tr>
<th>라인번호</th>
<th>재질</th>
<th>규격</th>
<th>길이(mm)</th>
<th>끝단가공</th>
<th>액션</th>
</tr>
</thead>
<tbody>
{pipeSegments.map((segment, index) => (
<tr key={index}>
<td>
<input
type="text"
value={segment.line_no}
onChange={(e) => updateSegmentLineNo(index, e.target.value)}
placeholder="LINE-A-001"
/>
</td>
<td>{segment.material_grade}</td>
<td>{segment.schedule_spec} {segment.nominal_size}</td>
<td>{segment.length_mm}</td>
<td>
<select
value={segment.end_preparation}
onChange={(e) => updateSegmentEndPrep(index, e.target.value)}
>
<option value="무개선">무개선</option>
<option value="한개선">한개선</option>
<option value="양개선">양개선</option>
</select>
</td>
<td>
<button onClick={() => removeSegment(index)}>삭제</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="stage-actions">
<button onClick={addNewSegment}> 단관 추가</button>
<button onClick={proceedToConfirmation}> 3단계: 확인으로 진행</button>
</div>
</div>
```
#### **3단계: 단관 정보 확인**
```javascript
<div className="confirmation-stage">
<h3> 3단계: 단관 정보 확인</h3>
{/* 전체 요약 */}
<div className="summary-stats">
<div className="stat-card">
<h4>📊 전체 현황</h4>
<p> 구역: {areas.length}</p>
<p> 도면: {totalDrawings}</p>
<p> 단관: {totalSegments}</p>
</div>
</div>
{/* 구역별 상세 */}
{areas.map(area => (
<div key={area.name} className="area-summary">
<h4>🏗 {area.name} 구역</h4>
{area.drawings.map(drawing => (
<div key={drawing} className="drawing-summary">
<h5>📋 {drawing}</h5>
<table className="segments-summary">
<thead>
<tr>
<th>라인번호</th>
<th>재질/규격</th>
<th>길이</th>
<th>끝단가공</th>
</tr>
</thead>
<tbody>
{getSegmentsForDrawing(area.name, drawing).map(segment => (
<tr key={segment.id}>
<td>{segment.line_no}</td>
<td>{segment.material_grade} {segment.schedule_spec} {segment.nominal_size}</td>
<td>{segment.length_mm}mm</td>
<td>{segment.end_preparation}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
))}
<div className="final-actions">
<button onClick={exportToExcel}>📊 Excel 내보내기</button>
<button onClick={saveCuttingPlan} className="btn-primary">
💾 Cutting Plan 저장
</button>
</div>
</div>
```
### **2. 리비전 비교 페이지**
**파일**: `frontend/src/pages/revision/PipeRevisionComparisonPage.jsx`
#### **4단계: 리비전 비교**
```javascript
<div className="revision-comparison-stage">
<h3>🔄 4단계: 리비전 비교</h3>
{/* 비교 요약 */}
<div className="comparison-summary">
<div className="summary-grid">
<div className="summary-card added">
<h4> 추가된 단관</h4>
<span className="count">{comparisonResult.added_segments}</span>
</div>
<div className="summary-card removed">
<h4> 삭제된 단관</h4>
<span className="count">{comparisonResult.removed_segments}</span>
</div>
<div className="summary-card modified">
<h4>🔧 수정된 단관</h4>
<span className="count">{comparisonResult.modified_segments}</span>
</div>
<div className="summary-card unchanged">
<h4> 변경없음</h4>
<span className="count">{comparisonResult.unchanged_segments}</span>
</div>
</div>
</div>
{/* 변경된 도면만 표시 */}
<div className="changed-drawings">
<h4>📋 변경된 도면 목록</h4>
{changedDrawings.map(drawing => (
<div key={drawing.name} className="drawing-comparison">
<h5>📄 {drawing.name}</h5>
<table className="comparison-table">
<thead>
<tr>
<th>상태</th>
<th>라인번호</th>
<th>재질/규격</th>
<th>길이</th>
<th>끝단가공</th>
<th>변경사항</th>
</tr>
</thead>
<tbody>
{drawing.segments.map(segment => (
<tr
key={segment.id}
className={`segment-row ${segment.change_type}`}
style={{
backgroundColor: segment.change_type === 'unchanged' ? '#e8f5e8' : '#ffffff'
}}
>
<td>
<span className={`status-badge ${segment.change_type}`}>
{getStatusLabel(segment.change_type)}
</span>
</td>
<td>{segment.line_no}</td>
<td>{segment.material_info}</td>
<td>{segment.length_mm}mm</td>
<td>{segment.end_preparation}</td>
<td>
{segment.change_type !== 'unchanged' && (
<div className="change-details">
{segment.changes.map(change => (
<div key={change.field} className="change-item">
<strong>{change.field}:</strong>
{change.old_value} {change.new_value}
</div>
))}
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
<div className="revision-actions">
<button onClick={acceptAllChanges}> 모든 변경사항 적용</button>
<button onClick={proceedToSummary}> 5단계: 변경 요약으로 진행</button>
</div>
</div>
```
#### **5단계: 변경 도면 요약**
```javascript
<div className="change-summary-stage">
<h3>📊 5단계: 변경 도면 요약</h3>
{/* 변경 도면만 집중 표시 */}
<div className="changed-drawings-only">
<h4> 검토가 필요한 도면들</h4>
{changedDrawingsOnly.map(drawing => (
<div key={drawing.name} className="drawing-change-card">
<div className="drawing-header">
<h5>📄 {drawing.name}</h5>
<div className="change-stats">
<span className="added">+{drawing.added_count}</span>
<span className="removed">-{drawing.removed_count}</span>
<span className="modified">~{drawing.modified_count}</span>
</div>
</div>
<div className="change-impact">
<h6>📈 파이프 구매량 영향</h6>
{drawing.material_impacts.map(impact => (
<div key={impact.material} className="material-impact">
<span className="material">{impact.material}</span>
<span className="length-change">
{impact.length_change > 0 ? '+' : ''}{impact.length_change}m
</span>
<span className="percentage">
({impact.percentage_change > 0 ? '+' : ''}{impact.percentage_change}%)
</span>
</div>
))}
</div>
</div>
))}
</div>
{/* 전체 구매량 재계산 */}
<div className="purchase-recalculation">
<h4>💰 파이프 구매량 재계산</h4>
<div className="purchase-comparison">
<div className="before-after">
<div className="before">
<h5>이전 구매 계획</h5>
{previousPurchasePlan.map(item => (
<div key={item.material} className="purchase-item">
<span>{item.material}</span>
<span>{item.total_length}m</span>
</div>
))}
</div>
<div className="after">
<h5>신규 구매 계획</h5>
{newPurchasePlan.map(item => (
<div key={item.material} className="purchase-item">
<span>{item.material}</span>
<span>{item.total_length}m</span>
{item.additional_needed > 0 && (
<span className="additional">
(+{item.additional_needed}m 추가 필요)
</span>
)}
</div>
))}
</div>
</div>
</div>
</div>
<div className="final-actions">
<button onClick={exportRevisionReport}>📊 리비전 리포트 내보내기</button>
<button onClick={sendToPipeRevisionPage}>
PIPE 리비전 페이지로 추가 구매량 전송
</button>
</div>
</div>
```
### **3. 단관 이슈 관리 페이지**
**파일**: `frontend/src/pages/PipeIssueManagementPage.jsx`
```javascript
<div className="pipe-issue-management">
<div className="page-header">
<h1>🛠 단관 이슈 관리</h1>
<p>현장에서 발생하는 파이프 관련 이슈를 체계적으로 관리합니다</p>
</div>
{/* 검색/필터 섹션 */}
<div className="search-section">
<div className="search-controls">
<select value={selectedArea} onChange={(e) => setSelectedArea(e.target.value)}>
<option value="">구역 선택</option>
{areas.map(area => (
<option key={area} value={area}>{area}</option>
))}
</select>
<select value={selectedDrawing} onChange={(e) => setSelectedDrawing(e.target.value)}>
<option value="">도면 선택</option>
{getDrawingsForArea(selectedArea).map(drawing => (
<option key={drawing} value={drawing}>{drawing}</option>
))}
</select>
<button onClick={loadSegments}>🔍 단관 정보 로드</button>
</div>
</div>
{/* 도면 전반 이슈 섹션 */}
<div className="drawing-issues-section">
<h3>📋 도면 전반 이슈</h3>
{/* 기존 이슈 목록 */}
<div className="existing-issues">
{drawingIssues.map(issue => (
<div key={issue.id} className="issue-card">
<div className="issue-header">
<span className={`severity-badge ${issue.severity}`}>
{issue.severity.toUpperCase()}
</span>
<span className={`status-badge ${issue.status}`}>
{issue.status.toUpperCase()}
</span>
</div>
<div className="issue-content">
<p>{issue.issue_description}</p>
<div className="issue-meta">
<small>보고자: {issue.reported_by} | {issue.reported_at}</small>
</div>
</div>
<div className="issue-actions">
<button onClick={() => editDrawingIssue(issue.id)}>수정</button>
<button onClick={() => deleteDrawingIssue(issue.id)}>삭제</button>
{issue.status === 'open' && (
<button onClick={() => resolveDrawingIssue(issue.id)}>해결</button>
)}
</div>
</div>
))}
</div>
{/* 새 이슈 추가 */}
<div className="add-drawing-issue">
<h4> 도면 이슈 추가</h4>
<textarea
placeholder="도면 전반적인 문제점을 입력하세요..."
value={newDrawingIssue}
onChange={(e) => setNewDrawingIssue(e.target.value)}
/>
<div className="issue-controls">
<select value={newIssueSeverity} onChange={(e) => setNewIssueSeverity(e.target.value)}>
<option value="low">낮음</option>
<option value="medium">보통</option>
<option value="high">높음</option>
<option value="critical">심각</option>
</select>
<button onClick={addDrawingIssue}>이슈 추가</button>
</div>
</div>
</div>
{/* 단관별 상세 정보 섹션 */}
<div className="segments-section">
<h3>🔧 단관별 상세 정보</h3>
{segments.length > 0 && (
<table className="segments-table">
<thead>
<tr>
<th>라인번호</th>
<th>재질/규격</th>
<th>길이</th>
<th>끝단가공</th>
<th>이슈 상태</th>
<th>액션</th>
</tr>
</thead>
<tbody>
{segments.map(segment => (
<tr key={segment.id}>
<td>{segment.line_no}</td>
<td>{segment.material_grade} {segment.schedule_spec} {segment.nominal_size}</td>
<td>{segment.length_mm}mm</td>
<td>{segment.end_preparation}</td>
<td>
{segment.has_issues ? (
<span className="has-issues"> 이슈있음</span>
) : (
<span className="no-issues"> 정상</span>
)}
</td>
<td>
<button onClick={() => openSegmentIssueDialog(segment)}>
{segment.has_issues ? '이슈 수정' : '이슈 입력'}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* 단관 이슈 입력 다이얼로그 */}
{showSegmentIssueDialog && (
<div className="modal-overlay">
<div className="issue-dialog">
<h4>🔧 {selectedSegment.line_no} 이슈 관리</h4>
<div className="segment-info">
<p><strong>구역:</strong> {selectedArea}</p>
<p><strong>도면:</strong> {selectedDrawing}</p>
<p><strong>재질:</strong> {selectedSegment.material_info}</p>
<p><strong>원래 길이:</strong> {selectedSegment.length_mm}mm</p>
</div>
<div className="issue-input">
<label>이슈 설명:</label>
<textarea
placeholder="예: 설치가 힘들어 30mm 절단함"
value={segmentIssueDescription}
onChange={(e) => setSegmentIssueDescription(e.target.value)}
/>
<label>이슈 유형:</label>
<select value={segmentIssueType} onChange={(e) => setSegmentIssueType(e.target.value)}>
<option value="cutting">절단</option>
<option value="installation">설치</option>
<option value="material">재질</option>
<option value="routing">라우팅</option>
<option value="other">기타</option>
</select>
<label>길이 변경 (mm):</label>
<input
type="number"
placeholder="예: -30 (30mm 절단)"
value={lengthChange}
onChange={(e) => setLengthChange(e.target.value)}
/>
<label>심각도:</label>
<select value={segmentIssueSeverity} onChange={(e) => setSegmentIssueSeverity(e.target.value)}>
<option value="low">낮음</option>
<option value="medium">보통</option>
<option value="high">높음</option>
<option value="critical">심각</option>
</select>
</div>
<div className="dialog-actions">
<button onClick={saveSegmentIssue}>저장</button>
<button onClick={closeSegmentIssueDialog}>취소</button>
</div>
</div>
</div>
)}
{/* 이슈 리포트 섹션 */}
<div className="issue-report-section">
<h3>📊 이슈 리포트</h3>
<div className="report-actions">
<button onClick={generateIssueReport}>📋 이슈 리포트 생성</button>
<button onClick={exportIssuesToExcel}>📊 Excel 내보내기</button>
</div>
</div>
</div>
```
---
## 🔌 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 관리 시스템을 구축할 수 있습니다! 🚀