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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
40 KiB
40 KiB
🔧 PIPE Cutting Plan & 이슈 관리 시스템 개발 가이드
📋 목차
🎯 시스템 개요
핵심 목적
- PIPE 자재의 체계적인 Cutting Plan 관리
- 구역별/도면별 단관 정보 관리
- 리비전 시 변경사항 추적 및 비교
- 현장 이슈 및 문제점 체계적 기록
주요 특징
- 2단계 워크플로우: 구역 할당 → 라인번호 입력
- 스마트 리비전: 기존 데이터와 신규 BOM 자동 비교
- 현장 이슈 관리: 도면별/단관별 문제점 추적
- Excel 연동: 완전한 데이터 내보내기 지원
🔄 전체 워크플로우
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 (단관 정보)
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 (리비전 비교)
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 (리비전 변경 상세)
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 (도면 전반 이슈)
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 (단관별 이슈)
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단계: 구역별 도면 할당
// 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단계: 라인번호 입력
<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단계: 단관 정보 확인
<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단계: 리비전 비교
<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단계: 변경 도면 요약
<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
<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
# 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
# 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
# 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 작성 전 리비전
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 작성 후 리비전
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. 단관 비교 로직
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. 구매량 재계산
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주)
- ✅
PipeCuttingPlanPage.jsx기본 구조 생성 - 🔄 2단계 워크플로우 구현
- 1단계: 구역별 도면 할당
- 2단계: 라인번호 입력
- 3단계: 확인 및 저장
- 📊 Excel 내보내기 기능
- 🗄️ 기본 DB 테이블 생성 및 마이그레이션
Phase 2: 리비전 비교 시스템 (3주)
- 🔄 리비전 감지 및 비교 로직
- 📋 4단계: 리비전 비교 페이지
- 📊 5단계: 변경 도면 요약 페이지
- 💰 구매량 재계산 기능
- 🔗 PIPE 리비전 페이지 연동
Phase 3: 이슈 관리 시스템 (2주)
- 🛠️ 단관 이슈 관리 페이지
- 📋 도면별/단관별 이슈 입력
- 📊 이슈 리포트 및 Excel 내보내기
- 🔍 이슈 검색 및 필터링
Phase 4: 고도화 및 최적화 (1주)
- 🎨 UI/UX 개선
- ⚡ 성능 최적화
- 🧪 테스트 코드 작성
- 📚 사용자 매뉴얼 작성
🛠️ 기술 스택
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.jsx2단계 워크플로우PipeRevisionComparisonPage.jsx구현PipeIssueManagementPage.jsx구현- 공통 컴포넌트 개발
- CSS 스타일링
통합 테스트
- 전체 워크플로우 테스트
- 리비전 시나리오 테스트
- Excel 내보내기 테스트
- 이슈 관리 기능 테스트
🎯 성공 지표
- 사용성: 현장 작업자가 직관적으로 사용 가능
- 정확성: 리비전 비교 결과의 100% 정확성
- 효율성: 기존 수작업 대비 80% 시간 단축
- 추적성: 모든 변경사항의 완전한 이력 관리
- 확장성: 향후 추가 기능 개발 용이성
이 가이드를 바탕으로 체계적이고 실용적인 PIPE 관리 시스템을 구축할 수 있습니다! 🚀