- README, 예제 설명, 코드 주석, CSS 머리말, 시험 임시 폴더 접두어의 예전 이름 정리 - 브라우저 저장 키·세션 DB 이름은 저장된 설정 보존을 위해 유지
214 lines
13 KiB
TypeScript
214 lines
13 KiB
TypeScript
import type { NetworkModel } from './inpModel';
|
|
|
|
export interface ColumnDef {
|
|
key: string;
|
|
label: string;
|
|
readOnly?: boolean;
|
|
isNumeric?: boolean;
|
|
width?: string;
|
|
align?: 'left' | 'right' | 'center';
|
|
description?: string;
|
|
}
|
|
|
|
/**
|
|
* Which physical-unit conversion (if any) a junction/pipe column's raw model
|
|
* value needs for Korean/metric display. The single source of truth for
|
|
* "elevation is a head-length quantity, diameter is a pipe-diameter
|
|
* quantity, etc" -- shared by every editor (bottom attribute table, full
|
|
* attribute table) so they never independently decide the wrong conversion
|
|
* for the same column key.
|
|
*/
|
|
export const UNIT_FIELD_KINDS: Record<string, 'head' | 'demand' | 'length' | 'diameter'> = {
|
|
elevation: 'head',
|
|
invertElev: 'head',
|
|
demand: 'demand',
|
|
length: 'length',
|
|
diameter: 'diameter',
|
|
};
|
|
|
|
export const JUNCTION_COLUMNS: ColumnDef[] = [
|
|
{ key: 'id', label: '절점 ID', width: '85px', align: 'left', description: 'EPANET 절점 고유 식별자' },
|
|
{ key: 'type', label: '시설 유형', readOnly: true, width: '85px', align: 'center', description: 'EPANET 시설 유형 (JUNCTION)' },
|
|
{ key: 'elevation', label: '지반고 (m)', isNumeric: true, width: '95px', align: 'right', description: '화면 표시 단위는 m이며 원본 모델 값은 변경하지 않습니다.' },
|
|
{ key: 'demand', label: '기본수요량 (m³/day)', isNumeric: true, width: '120px', align: 'right', description: '화면 표시 단위는 m³/day이며 원본 EPANET 유량 단위로 저장됩니다.' },
|
|
{ key: 'pattern', label: '수요 패턴', width: '85px', align: 'left', description: '시간변화 수요 패턴 ID' },
|
|
{ key: 'x', label: 'X 좌표', isNumeric: true, width: '100px', align: 'right', description: '모델 X 좌표' },
|
|
{ key: 'y', label: 'Y 좌표', isNumeric: true, width: '100px', align: 'right', description: '모델 Y 좌표' },
|
|
{ key: 'tag', label: '태그', width: '80px', align: 'left', description: '사용자 태그' },
|
|
{ key: 'invertElev', label: '관저고 (m)', isNumeric: true, width: '95px', align: 'right', description: '지자체 설계 보조 필드' },
|
|
];
|
|
|
|
export const PIPE_COLUMNS: ColumnDef[] = [
|
|
{ key: 'id', label: '관로 ID', width: '85px', align: 'left', description: 'EPANET 관로 고유 식별자' },
|
|
{ key: 'node1', label: '시작절점', width: '100px', align: 'left', description: 'EPANET Node1. 실제 흐름 방향은 해석 유량 부호에 따라 달라질 수 있습니다.' },
|
|
{ key: 'node2', label: '끝절점', width: '100px', align: 'left', description: 'EPANET Node2. 실제 흐름 방향은 해석 유량 부호에 따라 달라질 수 있습니다.' },
|
|
{ key: 'length', label: '관로길이 (m)', isNumeric: true, width: '100px', align: 'right', description: '화면 표시 단위는 m이며 원본 모델 단위로 저장됩니다.' },
|
|
{ key: 'diameter', label: '관경 (mm)', isNumeric: true, width: '95px', align: 'right', description: '화면 표시 단위는 mm이며 원본 모델 단위로 저장됩니다.' },
|
|
{ key: 'roughness', label: '조도계수', isNumeric: true, width: '90px', align: 'right', description: '마찰손실식에 따른 조도계수' },
|
|
{ key: 'minorLoss', label: '미소손실계수', isNumeric: true, width: '100px', align: 'right', description: '국부손실계수 K' },
|
|
{ key: 'status', label: '상태', width: '80px', align: 'center', description: '개폐 상태 (Open / Closed / CV)' },
|
|
{ key: 'bulkCoeff', label: '수체반응계수', isNumeric: true, width: '105px', align: 'right', description: 'EPANET [REACTIONS] BULK 관로별 재정의 값. 반응 차수에 따라 단위가 달라질 수 있으며 빈 값은 전역값을 따릅니다.' },
|
|
{ key: 'wallCoeff', label: '관벽반응계수', isNumeric: true, width: '105px', align: 'right', description: 'EPANET [REACTIONS] WALL 관로별 재정의 값. 반응 차수에 따라 단위가 달라질 수 있으며 빈 값은 전역값을 따릅니다.' },
|
|
{ key: 'tag', label: '태그', width: '80px', align: 'left', description: 'EPANET [TAGS] LINK 태그' },
|
|
{ key: 'description', label: '설명', width: '140px', align: 'left', description: 'PIPES 행의 세미콜론 뒤 설명' },
|
|
];
|
|
|
|
export const RESERVOIR_COLUMNS: ColumnDef[] = [
|
|
{ key: 'id', label: 'ID', width: '90px', align: 'left', description: '저수지/배수지 고유 식별자' },
|
|
{ key: 'head', label: '총수두 (Head)', isNumeric: true, width: '100px', align: 'right', description: '상수원 총수두 (Elevation + Pressure Head)' },
|
|
{ key: 'pattern', label: 'Head Pattern', width: '100px', align: 'left', description: '수두 변화 시간 패턴 ID' },
|
|
{ key: 'initQuality', label: '초기수질', isNumeric: true, readOnly: true, width: '90px', align: 'right', description: '초기 수질 농도' },
|
|
{ key: 'sourceQuality', label: 'Source Quality', readOnly: true, width: '105px', align: 'left', description: '상수원 수질 조건' },
|
|
{ key: 'tag', label: 'Tag', width: '80px', align: 'left', description: '사용자 태그' },
|
|
{ key: 'x', label: 'X', isNumeric: true, width: '105px', align: 'right', description: 'X 좌표' },
|
|
{ key: 'y', label: 'Y', isNumeric: true, width: '105px', align: 'right', description: 'Y 좌표' },
|
|
];
|
|
|
|
export const TANK_COLUMNS: ColumnDef[] = [
|
|
{ key: 'id', label: 'ID', width: '85px', align: 'left', description: '탱크 고유 식별자' },
|
|
{ key: 'elevation', label: 'Elevation', isNumeric: true, width: '95px', align: 'right', description: '바닥 지반 표고' },
|
|
{ key: 'initLevel', label: 'Initial Level', isNumeric: true, width: '95px', align: 'right', description: '초기 수위' },
|
|
{ key: 'minLevel', label: 'Minimum Level', isNumeric: true, width: '95px', align: 'right', description: '최저 허용 수위' },
|
|
{ key: 'maxLevel', label: 'Maximum Level', isNumeric: true, width: '95px', align: 'right', description: '최고 허용 수위' },
|
|
{ key: 'diameter', label: 'Diameter', isNumeric: true, width: '90px', align: 'right', description: '탱크 직경' },
|
|
{ key: 'minVol', label: 'Min Volume', isNumeric: true, readOnly: true, width: '90px', align: 'right', description: '최저 저수용량' },
|
|
{ key: 'volCurve', label: 'Volume Curve', readOnly: true, width: '95px', align: 'left', description: '수위-용량 곡선 ID' },
|
|
{ key: 'overflow', label: 'Overflow', readOnly: true, width: '80px', align: 'center', description: '월류 허용 여부' },
|
|
{ key: 'initQuality', label: '초기수질', isNumeric: true, readOnly: true, width: '85px', align: 'right', description: '탱크 초기 수질' },
|
|
{ key: 'tag', label: 'Tag', width: '80px', align: 'left', description: '사용자 태그' },
|
|
{ key: 'x', label: 'X', isNumeric: true, width: '100px', align: 'right', description: 'X 좌표' },
|
|
{ key: 'y', label: 'Y', isNumeric: true, width: '100px', align: 'right', description: 'Y 좌표' },
|
|
];
|
|
|
|
export const PUMP_COLUMNS: ColumnDef[] = [
|
|
{ key: 'id', label: 'ID', width: '85px', align: 'left', description: '펌프 고유 식별자' },
|
|
{ key: 'node1', label: '상류절점 (Node1)', width: '110px', align: 'left', description: '흡입측 상류 절점' },
|
|
{ key: 'node2', label: '하류절점 (Node2)', width: '110px', align: 'left', description: '토출측 하류 절점' },
|
|
{ key: 'parameters', label: 'Pump Curve', width: '120px', align: 'left', description: '펌프 특성 곡선 (HEAD Curve ID 또는 POWER/SPEED)' },
|
|
// pattern/status/tag/description are not yet parsed into any structured
|
|
// pump field (status lives only in the raw-preserved [STATUS] section, and
|
|
// SPipe never parses a per-pump pattern/tag/description at all), so
|
|
// editing them here would silently do nothing -- read-only until that
|
|
// parsing exists.
|
|
{ key: 'pattern', label: 'Pattern', width: '85px', align: 'left', readOnly: true, description: '가동 스케줄 패턴 ID (읽기 전용: 현재 미지원)' },
|
|
{ key: 'status', label: 'Status', width: '80px', align: 'center', readOnly: true, description: '상태 (읽기 전용: [STATUS] 섹션에서 직접 편집)' },
|
|
{ key: 'tag', label: 'Tag', width: '80px', align: 'left', readOnly: true, description: '사용자 태그 (읽기 전용: 현재 미지원)' },
|
|
{ key: 'description', label: '설명', width: '120px', align: 'left', readOnly: true, description: '펌프 상세 설명 (읽기 전용: 현재 미지원)' },
|
|
];
|
|
|
|
export const VALVE_COLUMNS: ColumnDef[] = [
|
|
{ key: 'id', label: 'ID', readOnly: true, width: '85px', align: 'left', description: '밸브 고유 식별자' },
|
|
{ key: 'node1', label: '상류절점 (Node1)', width: '110px', align: 'left', description: '유입측 상류 절점' },
|
|
{ key: 'node2', label: '하류절점 (Node2)', width: '110px', align: 'left', description: '유출측 하류 절점' },
|
|
{ key: 'valveType', label: 'Type', width: '80px', align: 'center', description: '밸브 유형 (PRV, PSV, PBV, FCV, TCV, GPV, PCV)' },
|
|
{ key: 'setting', label: 'Setting', isNumeric: true, width: '95px', align: 'right', description: '설정값 (압력, 유량, 손실계수)' },
|
|
{ key: 'diameter', label: 'Diameter', isNumeric: true, width: '90px', align: 'right', description: '밸브 구경 (mm / in)' },
|
|
{ key: 'minorLoss', label: 'MinorLoss', isNumeric: true, width: '90px', align: 'right', description: '미소손실계수' },
|
|
{ key: 'status', label: 'Status', width: '80px', align: 'center', description: '상태 (None / Open / Closed)' },
|
|
{ key: 'tag', label: 'Tag', width: '80px', align: 'left', description: '사용자 태그' },
|
|
{ key: 'description', label: '설명', width: '120px', align: 'left', description: '밸브 상세 설명' },
|
|
];
|
|
export interface PasteDiffItem {
|
|
rowId: string;
|
|
field: string;
|
|
oldValue: any;
|
|
newValue: any;
|
|
parsedValue?: any;
|
|
isValid: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export function validateSingleField(
|
|
targetType: 'junction' | 'pipe',
|
|
field: string,
|
|
rawVal: any,
|
|
rowId: string,
|
|
model: NetworkModel
|
|
): { valid: boolean; parsedValue?: any; error?: string } {
|
|
const strVal = String(rawVal ?? '').trim();
|
|
|
|
if (field === 'type') {
|
|
return { valid: true, parsedValue: 'JUNCTION' };
|
|
}
|
|
|
|
if (field === 'id') {
|
|
if (!strVal) return { valid: false, error: 'ID는 필수입니다.' };
|
|
if (strVal !== rowId) {
|
|
if (targetType === 'junction' && model.nodes[strVal]) {
|
|
return { valid: false, error: `이미 존재하는 절점 ID입니다: ${strVal}` };
|
|
}
|
|
if (
|
|
targetType === 'pipe' &&
|
|
(model.pipes[strVal] || model.pumps[strVal] || model.valves?.[strVal])
|
|
) {
|
|
return { valid: false, error: `이미 존재하는 관로 ID입니다: ${strVal}` };
|
|
}
|
|
}
|
|
return { valid: true, parsedValue: strVal };
|
|
}
|
|
|
|
if (targetType === 'junction') {
|
|
if (field === 'elevation' || field === 'invertElev' || field === 'demand' || field === 'x' || field === 'y') {
|
|
const n = parseFloat(strVal);
|
|
if (isNaN(n)) return { valid: false, error: '유효한 숫자를 입력하세요.' };
|
|
return { valid: true, parsedValue: n };
|
|
}
|
|
if (field === 'pattern') {
|
|
return { valid: true, parsedValue: strVal || undefined };
|
|
}
|
|
}
|
|
|
|
if (targetType === 'pipe') {
|
|
if (field === 'node1' || field === 'node2') {
|
|
if (!strVal) return { valid: false, error: '연결 절점 ID는 필수입니다.' };
|
|
if (!model.nodes[strVal]) {
|
|
return { valid: false, error: `존재하지 않는 절점 ID입니다: ${strVal}` };
|
|
}
|
|
const pipe = model.pipes[rowId];
|
|
if (pipe) {
|
|
const otherNode = field === 'node1' ? pipe.node2 : pipe.node1;
|
|
if (strVal === otherNode) {
|
|
return { valid: false, error: '시작과 끝 절점은 달라야 합니다 (자기순환 불가).' };
|
|
}
|
|
}
|
|
return { valid: true, parsedValue: strVal };
|
|
}
|
|
if (field === 'length') {
|
|
const n = parseFloat(strVal);
|
|
if (isNaN(n) || n <= 0) return { valid: false, error: '연장은 0보다 큰 숫자여야 합니다.' };
|
|
return { valid: true, parsedValue: n };
|
|
}
|
|
if (field === 'diameter') {
|
|
const n = parseFloat(strVal);
|
|
if (isNaN(n) || n <= 0) return { valid: false, error: '관경은 0보다 큰 숫자여야 합니다.' };
|
|
return { valid: true, parsedValue: n };
|
|
}
|
|
if (field === 'roughness') {
|
|
const n = parseFloat(strVal);
|
|
if (isNaN(n) || n <= 0) return { valid: false, error: '조도계수는 0보다 큰 숫자여야 합니다.' };
|
|
return { valid: true, parsedValue: n };
|
|
}
|
|
if (field === 'minorLoss') {
|
|
const n = parseFloat(strVal);
|
|
if (isNaN(n) || n < 0) return { valid: false, error: '미소손실은 0 이상의 숫자여야 합니다.' };
|
|
return { valid: true, parsedValue: n };
|
|
}
|
|
if (field === 'bulkCoeff' || field === 'wallCoeff') {
|
|
if (!strVal) return { valid: true, parsedValue: undefined };
|
|
const n = Number(strVal);
|
|
if (!Number.isFinite(n)) return { valid: false, error: '반응계수는 숫자 또는 빈 값이어야 합니다.' };
|
|
return { valid: true, parsedValue: n };
|
|
}
|
|
if (field === 'status') {
|
|
const upper = strVal.toUpperCase();
|
|
if (!['OPEN', 'CLOSED', 'CV'].includes(upper)) {
|
|
return { valid: false, error: '상태는 Open, Closed, CV 중 하나여야 합니다.' };
|
|
}
|
|
const formatted = upper === 'OPEN' ? 'Open' : upper === 'CLOSED' ? 'Closed' : 'CV';
|
|
return { valid: true, parsedValue: formatted };
|
|
}
|
|
}
|
|
|
|
return { valid: true, parsedValue: strVal };
|
|
}
|