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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
1124 lines
40 KiB
Markdown
1124 lines
40 KiB
Markdown
# 🔧 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 관리 시스템을 구축할 수 있습니다! 🚀
|