🔄 전반적인 시스템 리팩토링 완료
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

 백엔드 구조 개선:
- DatabaseService: 공통 DB 쿼리 로직 통합
- FileUploadService: 파일 업로드 로직 모듈화 및 트랜잭션 관리 개선
- 서비스 레이어 패턴 도입으로 코드 재사용성 향상

 프론트엔드 컴포넌트 개선:
- LoadingSpinner, ErrorMessage, ConfirmDialog 공통 컴포넌트 생성
- 재사용 가능한 컴포넌트 라이브러리 구축
- deprecated/backup 파일들 완전 제거

 성능 최적화:
- optimize_database.py: 핵심 DB 인덱스 자동 생성
- 쿼리 최적화 및 통계 업데이트 자동화
- VACUUM ANALYZE 자동 실행

 코드 정리:
- 개별 SQL 마이그레이션 파일들을 legacy/ 폴더로 정리
- 중복된 마이그레이션 스크립트 정리
- 깔끔하고 체계적인 프로젝트 구조 완성

 자동 마이그레이션 시스템 강화:
- complete_migrate.py: SQLAlchemy 기반 완전한 마이그레이션
- analyze_and_fix_schema.py: 백엔드 코드 분석 기반 스키마 수정
- fix_missing_tables.py: 누락된 테이블/컬럼 자동 생성
- start.sh: 배포 시 자동 실행 순서 최적화
This commit is contained in:
Hyungi Ahn
2025-10-20 08:41:06 +09:00
parent 0c99697a6f
commit 3398f71b80
61 changed files with 3370 additions and 4512 deletions

View File

@@ -0,0 +1,36 @@
-- jobs 테이블 생성
CREATE TABLE IF NOT EXISTS jobs (
-- 기본 정보
job_no VARCHAR(50) PRIMARY KEY,
job_name VARCHAR(200) NOT NULL,
-- 계약 관계 (핵심)
client_name VARCHAR(100) NOT NULL,
-- 프로젝트 정보
end_user VARCHAR(100),
epc_company VARCHAR(100),
project_site VARCHAR(200),
-- 상업 정보
contract_date DATE,
delivery_date DATE,
delivery_terms VARCHAR(100),
-- 상태 관리 (핵심)
status VARCHAR(20) DEFAULT '진행중',
delivery_completed_date DATE,
project_closed_date DATE,
-- 관리 정보
description TEXT,
created_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
CREATE INDEX IF NOT EXISTS idx_jobs_client ON jobs(client_name);
CREATE INDEX IF NOT EXISTS idx_jobs_created_at ON jobs(created_at);

View File

@@ -0,0 +1,19 @@
-- files 테이블에 job_no 컬럼 추가 및 project_id 대체
-- 새 컬럼 추가
ALTER TABLE files ADD COLUMN IF NOT EXISTS job_no VARCHAR(50);
-- 외래키 제약조건 추가 (MySQL/PostgreSQL 문법)
-- MySQL의 경우:
-- ALTER TABLE files ADD CONSTRAINT fk_files_job_no FOREIGN KEY (job_no) REFERENCES jobs(job_no);
-- PostgreSQL의 경우:
-- ALTER TABLE files ADD CONSTRAINT fk_files_job_no FOREIGN KEY (job_no) REFERENCES jobs(job_no);
-- SQLite의 경우는 외래키 제약조건을 나중에 추가하기 어려우므로 생략
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_files_job_no ON files(job_no);
-- 기존 project_id 컬럼은 일단 유지 (호환성을 위해)
-- 나중에 완전 이전 후 DROP 할 예정

View File

@@ -0,0 +1,18 @@
-- files 테이블에 job_no 컬럼 추가
-- 실행일: 2025.07.16
-- job_no 컬럼 추가
ALTER TABLE files ADD COLUMN job_no VARCHAR(50);
-- job_no에 인덱스 추가
CREATE INDEX idx_files_job_no ON files(job_no);
-- 기존 데이터가 있다면 project_id를 기반으로 job_no 설정
-- (이 부분은 실제 데이터가 있을 때만 실행)
-- UPDATE files SET job_no = (SELECT official_project_code FROM projects WHERE projects.id = files.project_id);
-- job_no를 NOT NULL로 설정 (데이터 마이그레이션 후)
-- ALTER TABLE files ALTER COLUMN job_no SET NOT NULL;
-- project_id 컬럼 제거 (선택사항 - 기존 데이터 백업 후)
-- ALTER TABLE files DROP COLUMN project_id;

View File

@@ -0,0 +1,80 @@
-- 분류 결과 저장을 위한 컬럼 추가
-- 2024년 BOM 분류 시스템 개선
-- materials 테이블에 분류 관련 컬럼 추가
ALTER TABLE materials ADD COLUMN IF NOT EXISTS subcategory VARCHAR(100);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS standard VARCHAR(200);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS grade VARCHAR(200);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS classification_details JSONB;
-- files 테이블에 분류 통계 컬럼 추가
ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_stats JSONB;
ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_completed BOOLEAN DEFAULT FALSE;
ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_timestamp TIMESTAMP;
-- 인덱스 추가 (성능 향상)
CREATE INDEX IF NOT EXISTS idx_materials_classified_category ON materials(classified_category);
CREATE INDEX IF NOT EXISTS idx_materials_subcategory ON materials(subcategory);
CREATE INDEX IF NOT EXISTS idx_materials_standard ON materials(standard);
CREATE INDEX IF NOT EXISTS idx_materials_grade ON materials(grade);
CREATE INDEX IF NOT EXISTS idx_materials_classification_confidence ON materials(classification_confidence);
-- 기존 데이터에 대한 기본값 설정
UPDATE materials SET
subcategory = COALESCE(subcategory, ''),
standard = COALESCE(standard, ''),
grade = COALESCE(grade, ''),
classification_details = COALESCE(classification_details, '{}'::jsonb)
WHERE subcategory IS NULL OR standard IS NULL OR grade IS NULL OR classification_details IS NULL;
-- 분류 완료된 파일들 업데이트
UPDATE files SET
classification_completed = TRUE,
classification_timestamp = created_at
WHERE parsed_count > 0;
-- 통계 뷰 생성 (분류 결과 통계 조회용)
CREATE OR REPLACE VIEW classification_summary AS
SELECT
f.job_no,
f.original_filename,
f.parsed_count,
f.classification_completed,
f.classification_timestamp,
COUNT(*) as total_materials,
COUNT(CASE WHEN m.classified_category = 'BOLT' THEN 1 END) as bolt_count,
COUNT(CASE WHEN m.classified_category = 'FLANGE' THEN 1 END) as flange_count,
COUNT(CASE WHEN m.classified_category = 'FITTING' THEN 1 END) as fitting_count,
COUNT(CASE WHEN m.classified_category = 'GASKET' THEN 1 END) as gasket_count,
COUNT(CASE WHEN m.classified_category = 'INSTRUMENT' THEN 1 END) as instrument_count,
COUNT(CASE WHEN m.classified_category = 'PIPE' THEN 1 END) as pipe_count,
COUNT(CASE WHEN m.classified_category = 'VALVE' THEN 1 END) as valve_count,
COUNT(CASE WHEN m.classified_category = 'MATERIAL' THEN 1 END) as material_count,
COUNT(CASE WHEN m.classified_category = 'OTHER' THEN 1 END) as other_count,
AVG(m.classification_confidence) as avg_confidence,
COUNT(CASE WHEN m.is_verified = TRUE THEN 1 END) as verified_count
FROM files f
LEFT JOIN materials m ON f.id = m.file_id
WHERE f.is_active = TRUE
GROUP BY f.id, f.job_no, f.original_filename, f.parsed_count, f.classification_completed, f.classification_timestamp;
-- 분류 성능 통계 뷰
CREATE OR REPLACE VIEW classification_performance AS
SELECT
classified_category,
subcategory,
standard,
COUNT(*) as total_count,
AVG(classification_confidence) as avg_confidence,
COUNT(CASE WHEN is_verified = TRUE THEN 1 END) as verified_count,
COUNT(CASE WHEN is_verified = FALSE THEN 1 END) as unverified_count,
ROUND(
(COUNT(CASE WHEN is_verified = TRUE THEN 1 END)::DECIMAL / COUNT(*) * 100), 2
) as verification_rate
FROM materials
WHERE classified_category IS NOT NULL
GROUP BY classified_category, subcategory, standard
ORDER BY total_count DESC;
-- 변경사항 확인
SELECT 'Database schema updated successfully' as status;

View File

@@ -0,0 +1,5 @@
-- materials 테이블에 length 컬럼 추가
ALTER TABLE materials ADD COLUMN length NUMERIC(10, 3);
-- 기존 데이터의 length 컬럼을 NULL로 초기화
UPDATE materials SET length = NULL;

View File

@@ -0,0 +1,137 @@
-- 자재 규격/재질 기준표 테이블 생성 스크립트
-- 실행 순서: 1) material_standards, 2) material_categories, 3) material_specifications, 4) material_grades, 5) material_patterns
-- 특수 재질: 6) special_materials, 7) special_material_grades, 8) special_material_patterns
-- 1. 자재 규격 표준 테이블
CREATE TABLE IF NOT EXISTS material_standards (
id SERIAL PRIMARY KEY,
standard_code VARCHAR(20) UNIQUE NOT NULL,
standard_name VARCHAR(100) NOT NULL,
description TEXT,
country VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 제조방식별 카테고리 테이블
CREATE TABLE IF NOT EXISTS material_categories (
id SERIAL PRIMARY KEY,
standard_id INTEGER REFERENCES material_standards(id),
category_code VARCHAR(50) NOT NULL,
category_name VARCHAR(100) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. 구체적인 규격 테이블
CREATE TABLE IF NOT EXISTS material_specifications (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES material_categories(id),
spec_code VARCHAR(20) NOT NULL,
spec_name VARCHAR(100) NOT NULL,
description TEXT,
material_type VARCHAR(50),
manufacturing VARCHAR(50),
pressure_rating VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 등급별 상세 정보 테이블
CREATE TABLE IF NOT EXISTS material_grades (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
grade_code VARCHAR(20) NOT NULL,
grade_name VARCHAR(100),
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
temp_range VARCHAR(100),
yield_strength VARCHAR(50),
tensile_strength VARCHAR(50),
corrosion_resistance VARCHAR(50),
stabilizer VARCHAR(50),
base_grade VARCHAR(20),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. 정규식 패턴 테이블
CREATE TABLE IF NOT EXISTS material_patterns (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6. 특수 재질 테이블
CREATE TABLE IF NOT EXISTS special_materials (
id SERIAL PRIMARY KEY,
material_type VARCHAR(50) NOT NULL,
material_name VARCHAR(100) NOT NULL,
description TEXT,
composition VARCHAR(200),
applications TEXT,
temp_max VARCHAR(50),
manufacturing VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 7. 특수 재질 등급 테이블
CREATE TABLE IF NOT EXISTS special_material_grades (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
grade_code VARCHAR(20) NOT NULL,
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
strength VARCHAR(50),
purity VARCHAR(100),
corrosion VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 8. 특수 재질 정규식 패턴 테이블
CREATE TABLE IF NOT EXISTS special_material_patterns (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_material_standards_code ON material_standards(standard_code);
CREATE INDEX IF NOT EXISTS idx_material_categories_standard ON material_categories(standard_id);
CREATE INDEX IF NOT EXISTS idx_material_specifications_category ON material_specifications(category_id);
CREATE INDEX IF NOT EXISTS idx_material_grades_specification ON material_grades(specification_id);
CREATE INDEX IF NOT EXISTS idx_material_patterns_specification ON material_patterns(specification_id);
CREATE INDEX IF NOT EXISTS idx_special_materials_type ON special_materials(material_type);
CREATE INDEX IF NOT EXISTS idx_special_material_grades_material ON special_material_grades(material_id);
CREATE INDEX IF NOT EXISTS idx_special_material_patterns_material ON special_material_patterns(material_id);
-- 활성 상태 인덱스
CREATE INDEX IF NOT EXISTS idx_material_standards_active ON material_standards(is_active);
CREATE INDEX IF NOT EXISTS idx_material_categories_active ON material_categories(is_active);
CREATE INDEX IF NOT EXISTS idx_material_specifications_active ON material_specifications(is_active);
CREATE INDEX IF NOT EXISTS idx_material_grades_active ON material_grades(is_active);
CREATE INDEX IF NOT EXISTS idx_material_patterns_active ON material_patterns(is_active);
CREATE INDEX IF NOT EXISTS idx_special_materials_active ON special_materials(is_active);
CREATE INDEX IF NOT EXISTS idx_special_material_grades_active ON special_material_grades(is_active);
CREATE INDEX IF NOT EXISTS idx_special_material_patterns_active ON special_material_patterns(is_active);

View File

@@ -0,0 +1,109 @@
-- 파이프 상세 정보 및 사용자 요구사항 테이블 생성
-- 2024-01-XX
-- 파이프 상세 정보 테이블
CREATE TABLE IF NOT EXISTS pipe_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL,
-- 재질 정보
material_standard TEXT, -- ASTM, KS, JIS 등
material_grade TEXT, -- A106, A53, STPG370 등
material_type TEXT, -- CARBON, STAINLESS 등
-- 파이프 특화 정보
manufacturing_method TEXT, -- SEAMLESS, WELDED, CAST
end_preparation TEXT, -- BOTH_ENDS_BEVELED, ONE_END_BEVELED, NO_BEVEL
schedule TEXT, -- SCH 10, 20, 40, 80 등
wall_thickness TEXT, -- 벽두께 정보
-- 치수 정보
nominal_size TEXT, -- MAIN_NOM (인치, 직경)
length_mm REAL, -- LENGTH (길이)
-- 신뢰도
material_confidence REAL,
manufacturing_confidence REAL,
end_prep_confidence REAL,
schedule_confidence REAL,
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 요구사항 타입 마스터 테이블
CREATE TABLE IF NOT EXISTS requirement_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type_code TEXT UNIQUE NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT' 등
type_name TEXT NOT NULL, -- '임팩테스트', '열처리' 등
category TEXT NOT NULL, -- 'TEST', 'TREATMENT', 'CERTIFICATION', 'CUSTOM' 등
description TEXT, -- 타입 설명
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 사용자 추가 요구사항 테이블
CREATE TABLE IF NOT EXISTS user_requirements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL,
-- 요구사항 타입
requirement_type TEXT NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
-- 요구사항 내용
requirement_title TEXT NOT NULL, -- '임팩테스트', '열처리', '인증서' 등
requirement_description TEXT, -- 상세 설명
requirement_spec TEXT, -- 구체적 스펙 (예: "Charpy V-notch -20°C")
-- 상태 관리
status TEXT DEFAULT 'PENDING', -- 'PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'
priority TEXT DEFAULT 'NORMAL', -- 'LOW', 'NORMAL', 'HIGH', 'URGENT'
-- 담당자 정보
assigned_to TEXT, -- 담당자명
due_date DATE, -- 완료 예정일
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_pipe_details_file_id ON pipe_details(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_file_id ON user_requirements(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_status ON user_requirements(status);
CREATE INDEX IF NOT EXISTS idx_user_requirements_type ON user_requirements(requirement_type);
-- 기본 요구사항 타입 데이터 삽입
INSERT OR IGNORE INTO requirement_types (type_code, type_name, category, description) VALUES
('IMPACT_TEST', '임팩테스트', 'TEST', 'Charpy V-notch, Izod 등의 충격 시험'),
('HEAT_TREATMENT', '열처리', 'TREATMENT', '정규화, 어닐링, 템퍼링 등의 열처리'),
('CERTIFICATION', '인증서', 'CERTIFICATION', '재질증명서, 시험성적서 등'),
('CUSTOM_SPEC', '특수사양', 'CUSTOM', '고객 특별 요구사항'),
('NDT_TEST', '비파괴검사', 'TEST', '초음파, 방사선, 자분탐상 등'),
('CHEMICAL_ANALYSIS', '화학분석', 'TEST', '화학성분 분석'),
('MECHANICAL_TEST', '기계적성질', 'TEST', '인장, 압축, 굽힘 시험'),
('SURFACE_TREATMENT', '표면처리', 'TREATMENT', '도금, 도장, 패시베이션 등'),
('DIMENSIONAL_CHECK', '치수검사', 'INSPECTION', '치수, 형상 검사'),
('WELDING_SPEC', '용접사양', 'SPEC', '용접 방법, 용접재 등');
-- 트리거 생성 (updated_at 자동 업데이트)
CREATE TRIGGER IF NOT EXISTS update_pipe_details_timestamp
AFTER UPDATE ON pipe_details
FOR EACH ROW
BEGIN
UPDATE pipe_details SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_user_requirements_timestamp
AFTER UPDATE ON user_requirements
FOR EACH ROW
BEGIN
UPDATE user_requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

View File

@@ -0,0 +1,115 @@
-- 파이프 상세 정보 및 사용자 요구사항 테이블 생성 (PostgreSQL)
-- 2024-01-XX
-- 파이프 상세 정보 테이블
CREATE TABLE IF NOT EXISTS pipe_details (
id SERIAL PRIMARY KEY,
file_id INTEGER NOT NULL,
-- 재질 정보
material_standard VARCHAR(50), -- ASTM, KS, JIS 등
material_grade VARCHAR(50), -- A106, A53, STPG370 등
material_type VARCHAR(50), -- CARBON, STAINLESS 등
-- 파이프 특화 정보
manufacturing_method VARCHAR(50), -- SEAMLESS, WELDED, CAST
end_preparation VARCHAR(50), -- BOTH_ENDS_BEVELED, ONE_END_BEVELED, NO_BEVEL
schedule VARCHAR(50), -- SCH 10, 20, 40, 80 등
wall_thickness VARCHAR(50), -- 벽두께 정보
-- 치수 정보
nominal_size VARCHAR(50), -- MAIN_NOM (인치, 직경)
length_mm DECIMAL(10, 3), -- LENGTH (길이)
-- 신뢰도
material_confidence DECIMAL(3, 2),
manufacturing_confidence DECIMAL(3, 2),
end_prep_confidence DECIMAL(3, 2),
schedule_confidence DECIMAL(3, 2),
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 요구사항 타입 마스터 테이블
CREATE TABLE IF NOT EXISTS requirement_types (
id SERIAL PRIMARY KEY,
type_code VARCHAR(50) UNIQUE NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT' 등
type_name VARCHAR(100) NOT NULL, -- '임팩테스트', '열처리' 등
category VARCHAR(50) NOT NULL, -- 'TEST', 'TREATMENT', 'CERTIFICATION', 'CUSTOM' 등
description TEXT, -- 타입 설명
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 사용자 추가 요구사항 테이블
CREATE TABLE IF NOT EXISTS user_requirements (
id SERIAL PRIMARY KEY,
file_id INTEGER NOT NULL,
-- 요구사항 타입
requirement_type VARCHAR(50) NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
-- 요구사항 내용
requirement_title VARCHAR(200) NOT NULL, -- '임팩테스트', '열처리', '인증서' 등
requirement_description TEXT, -- 상세 설명
requirement_spec TEXT, -- 구체적 스펙 (예: "Charpy V-notch -20°C")
-- 상태 관리
status VARCHAR(20) DEFAULT 'PENDING', -- 'PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'
priority VARCHAR(20) DEFAULT 'NORMAL', -- 'LOW', 'NORMAL', 'HIGH', 'URGENT'
-- 담당자 정보
assigned_to VARCHAR(100), -- 담당자명
due_date DATE, -- 완료 예정일
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_pipe_details_file_id ON pipe_details(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_file_id ON user_requirements(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_status ON user_requirements(status);
CREATE INDEX IF NOT EXISTS idx_user_requirements_type ON user_requirements(requirement_type);
-- 기본 요구사항 타입 데이터 삽입
INSERT INTO requirement_types (type_code, type_name, category, description) VALUES
('IMPACT_TEST', '임팩테스트', 'TEST', 'Charpy V-notch, Izod 등의 충격 시험'),
('HEAT_TREATMENT', '열처리', 'TREATMENT', '정규화, 어닐링, 템퍼링 등의 열처리'),
('CERTIFICATION', '인증서', 'CERTIFICATION', '재질증명서, 시험성적서 등'),
('CUSTOM_SPEC', '특수사양', 'CUSTOM', '고객 특별 요구사항'),
('NDT_TEST', '비파괴검사', 'TEST', '초음파, 방사선, 자분탐상 등'),
('CHEMICAL_ANALYSIS', '화학분석', 'TEST', '화학성분 분석'),
('MECHANICAL_TEST', '기계적성질', 'TEST', '인장, 압축, 굽힘 시험'),
('SURFACE_TREATMENT', '표면처리', 'TREATMENT', '도금, 도장, 패시베이션 등'),
('DIMENSIONAL_CHECK', '치수검사', 'INSPECTION', '치수, 형상 검사'),
('WELDING_SPEC', '용접사양', 'SPEC', '용접 방법, 용접재 등')
ON CONFLICT (type_code) DO NOTHING;
-- 트리거 함수 생성 (updated_at 자동 업데이트)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- 트리거 생성
CREATE TRIGGER update_pipe_details_timestamp
BEFORE UPDATE ON pipe_details
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_requirements_timestamp
BEFORE UPDATE ON user_requirements
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,18 @@
-- main_nom, red_nom 컬럼 추가 스크립트
-- 2025.01.17 - MAIN_NOM/RED_NOM 별도 저장을 위한 스키마 개선
-- materials 테이블에 main_nom, red_nom 컬럼 추가
ALTER TABLE materials ADD COLUMN IF NOT EXISTS main_nom VARCHAR(50);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS red_nom VARCHAR(50);
-- 인덱스 추가 (검색 성능 향상)
CREATE INDEX IF NOT EXISTS idx_materials_main_nom ON materials(main_nom);
CREATE INDEX IF NOT EXISTS idx_materials_red_nom ON materials(red_nom);
CREATE INDEX IF NOT EXISTS idx_materials_main_red_nom ON materials(main_nom, red_nom);
-- 기존 데이터에 대한 기본값 설정 (필요시)
-- UPDATE materials SET main_nom = '', red_nom = '' WHERE main_nom IS NULL OR red_nom IS NULL;
-- 코멘트 추가
COMMENT ON COLUMN materials.main_nom IS 'MAIN_NOM 필드 - 주 사이즈 (예: 4", 150A)';
COMMENT ON COLUMN materials.red_nom IS 'RED_NOM 필드 - 축소 사이즈 (Reducing 피팅/플랜지용)';

View File

@@ -0,0 +1,16 @@
-- bolt_details 테이블에 pressure_rating 컬럼 추가
-- 2025.01.16 - 볼트 압력등급 정보 저장을 위한 스키마 개선
ALTER TABLE bolt_details ADD COLUMN IF NOT EXISTS pressure_rating VARCHAR(50);
-- 기존 레코드에 기본값 설정 (선택사항)
UPDATE bolt_details SET pressure_rating = 'UNKNOWN' WHERE pressure_rating IS NULL;
-- 인덱스 추가 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_bolt_details_pressure_rating ON bolt_details(pressure_rating);
-- 확인용 쿼리
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'bolt_details'
AND column_name = 'pressure_rating';

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
자재 규격/재질 기준표 테이블 생성 및 데이터 삽입 스크립트
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine, text
from app.database import DATABASE_URL
def create_tables():
"""테이블 생성"""
engine = create_engine(DATABASE_URL)
with engine.connect() as conn:
print("자재 규격/재질 기준표 테이블 생성 시작...")
# 1. 자재 규격 표준 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_standards (
id SERIAL PRIMARY KEY,
standard_code VARCHAR(20) UNIQUE NOT NULL,
standard_name VARCHAR(100) NOT NULL,
description TEXT,
country VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_standards 테이블 생성됨")
# 2. 제조방식별 카테고리 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_categories (
id SERIAL PRIMARY KEY,
standard_id INTEGER REFERENCES material_standards(id),
category_code VARCHAR(50) NOT NULL,
category_name VARCHAR(100) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_categories 테이블 생성됨")
# 3. 구체적인 규격 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_specifications (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES material_categories(id),
spec_code VARCHAR(20) NOT NULL,
spec_name VARCHAR(100) NOT NULL,
description TEXT,
material_type VARCHAR(50),
manufacturing VARCHAR(50),
pressure_rating VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_specifications 테이블 생성됨")
# 4. 등급별 상세 정보 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_grades (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
grade_code VARCHAR(20) NOT NULL,
grade_name VARCHAR(100),
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
temp_range VARCHAR(100),
yield_strength VARCHAR(50),
tensile_strength VARCHAR(50),
corrosion_resistance VARCHAR(50),
stabilizer VARCHAR(50),
base_grade VARCHAR(20),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_grades 테이블 생성됨")
# 5. 정규식 패턴 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_patterns (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_patterns 테이블 생성됨")
# 6. 특수 재질 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS special_materials (
id SERIAL PRIMARY KEY,
material_type VARCHAR(50) NOT NULL,
material_name VARCHAR(100) NOT NULL,
description TEXT,
composition VARCHAR(200),
applications TEXT,
temp_max VARCHAR(50),
manufacturing VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - special_materials 테이블 생성됨")
# 7. 특수 재질 등급 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS special_material_grades (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
grade_code VARCHAR(20) NOT NULL,
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
strength VARCHAR(50),
purity VARCHAR(100),
corrosion VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - special_material_grades 테이블 생성됨")
# 8. 특수 재질 정규식 패턴 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS special_material_patterns (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - special_material_patterns 테이블 생성됨")
# 인덱스 생성
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_standards_code ON material_standards(standard_code);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_categories_standard ON material_categories(standard_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_specifications_category ON material_specifications(category_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_grades_specification ON material_grades(specification_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_patterns_specification ON material_patterns(specification_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_materials_type ON special_materials(material_type);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_grades_material ON special_material_grades(material_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_patterns_material ON special_material_patterns(material_id);"))
# 활성 상태 인덱스
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_standards_active ON material_standards(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_categories_active ON material_categories(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_specifications_active ON material_specifications(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_grades_active ON material_grades(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_patterns_active ON material_patterns(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_materials_active ON special_materials(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_grades_active ON special_material_grades(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_patterns_active ON special_material_patterns(is_active);"))
conn.commit()
print("모든 테이블 및 인덱스 생성 완료!")
def main():
"""메인 실행 함수"""
print("자재 규격/재질 기준표 DB 마이그레이션 시작")
print("=" * 50)
create_tables()
print("\n" + "=" * 50)
print("마이그레이션 완료!")
print("\n다음 단계: python scripts/06_insert_material_standards_data.py")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,68 @@
-- PIPE_DETAILS 테이블 간소화 스크립트
-- 2025.01.17 - 실무 중심 구조 개선
-- 기존 테이블 백업
CREATE TABLE IF NOT EXISTS pipe_details_backup AS SELECT * FROM pipe_details;
-- 새로운 간소화된 테이블 생성
DROP TABLE IF EXISTS pipe_details_new;
CREATE TABLE pipe_details_new (
id SERIAL PRIMARY KEY,
material_id INTEGER NOT NULL,
file_id INTEGER NOT NULL,
-- 핵심 PIPE 정보 (실무 필수)
outer_diameter VARCHAR(20), -- main_nom 기반 외경 정보
schedule VARCHAR(10), -- SCH 40, SCH 80 등
material_spec VARCHAR(100), -- 단일 재질 정보 (ASTM A106 GR B)
manufacturing_method VARCHAR(20), -- SEAMLESS, WELDED, CAST
end_preparation VARCHAR(20), -- POE, BOE, PEE, BEE 등
length_mm DECIMAL(10,2), -- 길이 (mm)
-- 추가 정보 (cutting plan 용)
area_number VARCHAR(10), -- 에리어 번호 (#01, #02)
spool_number VARCHAR(10), -- 스풀 번호 (A, B, C)
drawing_number VARCHAR(50), -- 도면 번호
-- 메타 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 외래키
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 인덱스 추가 (성능)
CREATE INDEX IF NOT EXISTS idx_pipe_details_material_id ON pipe_details_new(material_id);
CREATE INDEX IF NOT EXISTS idx_pipe_details_file_id ON pipe_details_new(file_id);
CREATE INDEX IF NOT EXISTS idx_pipe_details_outer_diameter ON pipe_details_new(outer_diameter);
CREATE INDEX IF NOT EXISTS idx_pipe_details_schedule ON pipe_details_new(schedule);
-- 기존 데이터 마이그레이션 (가능한 것만)
INSERT INTO pipe_details_new (
material_id, file_id, outer_diameter, schedule, material_spec,
manufacturing_method, length_mm
)
SELECT
material_id,
file_id,
nominal_size as outer_diameter, -- nominal_size를 outer_diameter로
schedule,
COALESCE(material_standard, material_grade, material_type) as material_spec, -- 중복 필드 통합
manufacturing_method,
length_mm
FROM pipe_details
WHERE material_id IS NOT NULL;
-- 백업 테이블로 기존 테이블 교체
DROP TABLE IF EXISTS pipe_details;
ALTER TABLE pipe_details_new RENAME TO pipe_details;
-- 코멘트 추가
COMMENT ON TABLE pipe_details IS '간소화된 PIPE 상세 정보 - 실무 중심 구조';
COMMENT ON COLUMN pipe_details.outer_diameter IS '외경 정보 (main_nom에서 추출, 예: 4", 150A)';
COMMENT ON COLUMN pipe_details.material_spec IS '통합 재질 정보 (예: ASTM A106 GR B)';
COMMENT ON COLUMN pipe_details.end_preparation IS '끝단 가공 (POE-BOE, PEE, BEE 등)';
COMMENT ON COLUMN pipe_details.area_number IS '에리어 번호 (cutting plan용)';
COMMENT ON COLUMN pipe_details.spool_number IS '스풀 번호 (cutting plan용)';

View File

@@ -0,0 +1,286 @@
-- 구매 관리 시스템 테이블들
-- 실행일: 2025.01.21
-- ================================
-- 1. 구매 품목 마스터 테이블
-- ================================
CREATE TABLE IF NOT EXISTS purchase_items (
id SERIAL PRIMARY KEY,
-- 품목 식별
item_code VARCHAR(100) UNIQUE NOT NULL, -- PI-PIPE-A106-4IN-001
category VARCHAR(50) NOT NULL, -- PIPE, VALVE, FITTING 등
specification TEXT NOT NULL, -- 상세 사양
-- 기본 정보
material_spec VARCHAR(200), -- ASTM A106 GR B
size_spec VARCHAR(100), -- 4", 1-1/2" x 1"
unit VARCHAR(10) NOT NULL, -- EA, mm, SET
-- BOM 수량 정보
bom_quantity DECIMAL(10,3) NOT NULL, -- BOM상 필요 수량
-- 구매 계산 정보
safety_factor DECIMAL(3,2) DEFAULT 1.10, -- 여유율 (1.1 = 10% 추가)
minimum_order_qty DECIMAL(10,3) DEFAULT 0, -- 최소 주문 수량
order_unit_qty DECIMAL(10,3) DEFAULT 1, -- 주문 단위 (박스, 롤 등)
calculated_qty DECIMAL(10,3), -- 최종 계산된 구매 수량
-- PIPE 전용 정보
cutting_loss DECIMAL(10,3) DEFAULT 0, -- 절단 손실 (mm)
standard_length DECIMAL(10,3), -- 표준 길이 (PIPE: 6000mm)
pipes_count INTEGER, -- 필요한 파이프 본수
waste_length DECIMAL(10,3), -- 여유분 길이
-- 상세 스펙 (JSON)
detailed_spec JSONB, -- 압력등급, 연결방식 등 상세 정보
-- 구매 정보
preferred_supplier VARCHAR(200), -- 선호 공급업체
last_unit_price DECIMAL(10,2), -- 최근 단가
currency VARCHAR(10) DEFAULT 'KRW', -- 통화
lead_time_days INTEGER DEFAULT 30, -- 리드타임
-- 연결 정보
job_no VARCHAR(50) NOT NULL, -- Job 번호
revision VARCHAR(20) DEFAULT 'Rev.0', -- 리비전
file_id INTEGER, -- 원본 파일 ID
-- 관리 정보
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
-- 외래키
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE SET NULL
);
-- ================================
-- 2. 개별 자재와 구매품목 연결 테이블
-- ================================
CREATE TABLE IF NOT EXISTS material_purchase_mapping (
id SERIAL PRIMARY KEY,
material_id INTEGER NOT NULL, -- materials 테이블 ID
purchase_item_id INTEGER NOT NULL, -- purchase_items 테이블 ID
quantity_ratio DECIMAL(5,2) DEFAULT 1.0, -- 변환 비율
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 외래키
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE,
FOREIGN KEY (purchase_item_id) REFERENCES purchase_items(id) ON DELETE CASCADE,
-- 유니크 제약
UNIQUE(material_id, purchase_item_id)
);
-- ================================
-- 3. 구매 주문 테이블
-- ================================
CREATE TABLE IF NOT EXISTS purchase_orders (
id SERIAL PRIMARY KEY,
-- 주문 기본 정보
order_no VARCHAR(50) UNIQUE NOT NULL, -- PO-2024-001
job_no VARCHAR(50) NOT NULL, -- Job 번호
revision VARCHAR(20) NOT NULL, -- 리비전
-- 주문 상태
status VARCHAR(20) DEFAULT 'DRAFT', -- DRAFT, APPROVED, ORDERED, RECEIVED, CANCELLED
order_date DATE, -- 주문일
required_date DATE, -- 요청일
delivery_date DATE, -- 납기일
-- 공급업체 정보
supplier_name VARCHAR(200),
supplier_contact VARCHAR(200),
-- 금액 정보
total_amount DECIMAL(12,2),
currency VARCHAR(10) DEFAULT 'KRW',
-- 관리 정보
created_by VARCHAR(100),
approved_by VARCHAR(100),
approved_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
notes TEXT
);
-- ================================
-- 4. 구매 주문 상세 테이블
-- ================================
CREATE TABLE IF NOT EXISTS purchase_order_items (
id SERIAL PRIMARY KEY,
purchase_order_id INTEGER NOT NULL, -- purchase_orders 테이블 ID
purchase_item_id INTEGER NOT NULL, -- purchase_items 테이블 ID
-- 수량 정보
ordered_quantity DECIMAL(10,3) NOT NULL, -- 주문 수량
received_quantity DECIMAL(10,3) DEFAULT 0, -- 입고 수량
-- 가격 정보
unit_price DECIMAL(10,2), -- 단가
total_price DECIMAL(12,2), -- 총액
-- 납기 정보
required_date DATE, -- 요청일
delivery_date DATE, -- 납기일
-- 상태
status VARCHAR(20) DEFAULT 'ORDERED', -- ORDERED, PARTIAL, RECEIVED, CANCELLED
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
notes TEXT,
-- 외래키
FOREIGN KEY (purchase_order_id) REFERENCES purchase_orders(id) ON DELETE CASCADE,
FOREIGN KEY (purchase_item_id) REFERENCES purchase_items(id) ON DELETE CASCADE
);
-- ================================
-- 5. 구매 이력 테이블 (리비전 추적용)
-- ================================
CREATE TABLE IF NOT EXISTS purchase_history (
id SERIAL PRIMARY KEY,
job_no VARCHAR(50) NOT NULL,
revision VARCHAR(20) NOT NULL,
-- 변경 내용
change_type VARCHAR(20) NOT NULL, -- ADDED, MODIFIED, REMOVED
purchase_item_id INTEGER,
-- 수량 변경
previous_quantity DECIMAL(10,3),
new_quantity DECIMAL(10,3),
quantity_diff DECIMAL(10,3), -- 차이 (음수면 감소, 양수면 증가)
-- 변경 사유
change_reason VARCHAR(200),
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
-- 외래키
FOREIGN KEY (purchase_item_id) REFERENCES purchase_items(id) ON DELETE SET NULL
);
-- ================================
-- 6. 인덱스 생성
-- ================================
-- purchase_items 인덱스
CREATE INDEX IF NOT EXISTS idx_purchase_items_job_revision ON purchase_items(job_no, revision);
CREATE INDEX IF NOT EXISTS idx_purchase_items_category ON purchase_items(category);
CREATE INDEX IF NOT EXISTS idx_purchase_items_item_code ON purchase_items(item_code);
CREATE INDEX IF NOT EXISTS idx_purchase_items_active ON purchase_items(is_active);
-- material_purchase_mapping 인덱스
CREATE INDEX IF NOT EXISTS idx_material_purchase_mapping_material ON material_purchase_mapping(material_id);
CREATE INDEX IF NOT EXISTS idx_material_purchase_mapping_purchase ON material_purchase_mapping(purchase_item_id);
-- purchase_orders 인덱스
CREATE INDEX IF NOT EXISTS idx_purchase_orders_job_revision ON purchase_orders(job_no, revision);
CREATE INDEX IF NOT EXISTS idx_purchase_orders_status ON purchase_orders(status);
CREATE INDEX IF NOT EXISTS idx_purchase_orders_order_date ON purchase_orders(order_date);
-- purchase_order_items 인덱스
CREATE INDEX IF NOT EXISTS idx_purchase_order_items_order ON purchase_order_items(purchase_order_id);
CREATE INDEX IF NOT EXISTS idx_purchase_order_items_item ON purchase_order_items(purchase_item_id);
-- purchase_history 인덱스
CREATE INDEX IF NOT EXISTS idx_purchase_history_job_revision ON purchase_history(job_no, revision);
CREATE INDEX IF NOT EXISTS idx_purchase_history_change_type ON purchase_history(change_type);
CREATE INDEX IF NOT EXISTS idx_purchase_history_created_at ON purchase_history(created_at);
-- ================================
-- 7. 트리거 생성 (updated_at 자동 업데이트)
-- ================================
-- purchase_items updated_at 트리거
CREATE OR REPLACE FUNCTION update_purchase_items_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_purchase_items_timestamp
BEFORE UPDATE ON purchase_items
FOR EACH ROW
EXECUTE FUNCTION update_purchase_items_timestamp();
-- purchase_orders updated_at 트리거
CREATE OR REPLACE FUNCTION update_purchase_orders_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_purchase_orders_timestamp
BEFORE UPDATE ON purchase_orders
FOR EACH ROW
EXECUTE FUNCTION update_purchase_orders_timestamp();
-- purchase_order_items updated_at 트리거
CREATE OR REPLACE FUNCTION update_purchase_order_items_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_purchase_order_items_timestamp
BEFORE UPDATE ON purchase_order_items
FOR EACH ROW
EXECUTE FUNCTION update_purchase_order_items_timestamp();
-- ================================
-- 8. 기본 뷰 생성
-- ================================
-- 구매 요약 뷰
CREATE OR REPLACE VIEW purchase_summary AS
SELECT
pi.job_no,
pi.revision,
pi.category,
COUNT(*) as item_count,
SUM(pi.bom_quantity) as total_bom_quantity,
SUM(pi.calculated_qty) as total_purchase_quantity,
SUM(poi.ordered_quantity) as total_ordered_quantity,
SUM(poi.received_quantity) as total_received_quantity,
AVG(pi.safety_factor) as avg_safety_factor
FROM purchase_items pi
LEFT JOIN purchase_order_items poi ON pi.id = poi.purchase_item_id
WHERE pi.is_active = TRUE
GROUP BY pi.job_no, pi.revision, pi.category;
-- 리비전 변경 요약 뷰
CREATE OR REPLACE VIEW revision_changes_summary AS
SELECT
ph.job_no,
ph.revision,
ph.change_type,
COUNT(*) as change_count,
SUM(CASE WHEN ph.quantity_diff > 0 THEN ph.quantity_diff ELSE 0 END) as total_increase,
SUM(CASE WHEN ph.quantity_diff < 0 THEN ABS(ph.quantity_diff) ELSE 0 END) as total_decrease,
SUM(ph.quantity_diff) as net_change
FROM purchase_history ph
GROUP BY ph.job_no, ph.revision, ph.change_type;
-- 스크립트 완료 확인
SELECT 'Purchase management tables created successfully!' as status;

View File

@@ -0,0 +1,243 @@
-- 자재 비교 및 발주 추적 시스템
-- 실행일: 2025.01.22
-- ================================
-- 1. materials 테이블에 해시 컬럼 추가
-- ================================
-- 자재 비교를 위한 해시 컬럼 추가
ALTER TABLE materials ADD COLUMN IF NOT EXISTS material_hash VARCHAR(32);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS normalized_description TEXT;
-- 해시 인덱스 추가 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_materials_hash ON materials(material_hash);
CREATE INDEX IF NOT EXISTS idx_materials_file_hash ON materials(file_id, material_hash);
-- ================================
-- 2. 자재 비교 결과 저장 테이블
-- ================================
CREATE TABLE IF NOT EXISTS material_revisions_comparison (
id SERIAL PRIMARY KEY,
-- 비교 기본 정보
job_no VARCHAR(50) NOT NULL,
current_revision VARCHAR(20) NOT NULL,
previous_revision VARCHAR(20) NOT NULL,
current_file_id INTEGER NOT NULL,
previous_file_id INTEGER NOT NULL,
-- 비교 결과 요약
total_current_items INTEGER DEFAULT 0,
total_previous_items INTEGER DEFAULT 0,
new_items_count INTEGER DEFAULT 0,
modified_items_count INTEGER DEFAULT 0,
removed_items_count INTEGER DEFAULT 0,
unchanged_items_count INTEGER DEFAULT 0,
-- 상세 결과 (JSON)
comparison_details JSONB,
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
-- 외래키
FOREIGN KEY (current_file_id) REFERENCES files(id),
FOREIGN KEY (previous_file_id) REFERENCES files(id),
-- 유니크 제약 (같은 비교는 한 번만)
UNIQUE(job_no, current_revision, previous_revision)
);
-- ================================
-- 3. 개별 자재 비교 상세 테이블
-- ================================
CREATE TABLE IF NOT EXISTS material_comparison_details (
id SERIAL PRIMARY KEY,
comparison_id INTEGER NOT NULL,
material_hash VARCHAR(32) NOT NULL,
-- 비교 타입
change_type VARCHAR(20) NOT NULL, -- 'NEW', 'MODIFIED', 'REMOVED', 'UNCHANGED'
-- 자재 정보
description TEXT NOT NULL,
size_spec VARCHAR(100),
material_grade VARCHAR(100),
-- 수량 비교
previous_quantity DECIMAL(10,3) DEFAULT 0,
current_quantity DECIMAL(10,3) DEFAULT 0,
quantity_diff DECIMAL(10,3) DEFAULT 0,
-- 추가 구매 필요량 (핵심!)
additional_purchase_needed DECIMAL(10,3) DEFAULT 0,
-- 분류 정보
classified_category VARCHAR(50),
classification_confidence DECIMAL(3,2),
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 외래키
FOREIGN KEY (comparison_id) REFERENCES material_revisions_comparison(id) ON DELETE CASCADE
);
-- ================================
-- 4. 발주 추적 테이블 (실제 발주 관리)
-- ================================
CREATE TABLE IF NOT EXISTS material_purchase_tracking (
id SERIAL PRIMARY KEY,
-- 연결 정보
job_no VARCHAR(50) NOT NULL,
material_hash VARCHAR(32) NOT NULL,
revision VARCHAR(20) NOT NULL,
-- 자재 정보
description TEXT NOT NULL,
size_spec VARCHAR(100),
unit VARCHAR(10) DEFAULT 'EA',
-- 수량 정보
bom_quantity DECIMAL(10,3) NOT NULL, -- BOM상 필요 수량
safety_margin DECIMAL(3,2) DEFAULT 1.10, -- 여유율 (10%)
calculated_quantity DECIMAL(10,3) NOT NULL, -- 계산된 구매 수량
-- 발주 상태
purchase_status VARCHAR(20) DEFAULT 'PENDING', -- 'PENDING', 'CONFIRMED', 'ORDERED', 'RECEIVED'
confirmed_quantity DECIMAL(10,3) DEFAULT 0, -- 확정된 발주 수량
ordered_quantity DECIMAL(10,3) DEFAULT 0, -- 실제 주문 수량
received_quantity DECIMAL(10,3) DEFAULT 0, -- 입고 수량
-- 발주 정보
purchase_order_no VARCHAR(100),
supplier_name VARCHAR(200),
unit_price DECIMAL(10,2),
total_price DECIMAL(12,2),
order_date DATE,
delivery_date DATE,
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
confirmed_by VARCHAR(100),
confirmed_at TIMESTAMP,
-- 외래키
FOREIGN KEY (job_no) REFERENCES jobs(job_no),
-- 유니크 제약
UNIQUE(job_no, material_hash, revision)
);
-- ================================
-- 5. 누적 재고 현황 뷰
-- ================================
CREATE OR REPLACE VIEW material_inventory_status AS
SELECT
mpt.job_no,
mpt.material_hash,
mpt.description,
mpt.size_spec,
mpt.unit,
-- 누적 수량
SUM(mpt.confirmed_quantity) as total_confirmed,
SUM(mpt.ordered_quantity) as total_ordered,
SUM(mpt.received_quantity) as total_received,
-- 현재 가용 재고
SUM(mpt.received_quantity) as available_stock,
-- 최신 리비전 정보
MAX(mpt.revision) as latest_revision,
MAX(mpt.updated_at) as last_updated
FROM material_purchase_tracking mpt
WHERE mpt.purchase_status != 'CANCELLED'
GROUP BY mpt.job_no, mpt.material_hash, mpt.description, mpt.size_spec, mpt.unit;
-- ================================
-- 6. 인덱스 생성
-- ================================
-- material_revisions_comparison 인덱스
CREATE INDEX IF NOT EXISTS idx_material_revisions_job ON material_revisions_comparison(job_no);
CREATE INDEX IF NOT EXISTS idx_material_revisions_current ON material_revisions_comparison(current_revision);
-- material_comparison_details 인덱스
CREATE INDEX IF NOT EXISTS idx_material_comparison_hash ON material_comparison_details(material_hash);
CREATE INDEX IF NOT EXISTS idx_material_comparison_type ON material_comparison_details(change_type);
-- material_purchase_tracking 인덱스
CREATE INDEX IF NOT EXISTS idx_purchase_tracking_job_hash ON material_purchase_tracking(job_no, material_hash);
CREATE INDEX IF NOT EXISTS idx_purchase_tracking_status ON material_purchase_tracking(purchase_status);
CREATE INDEX IF NOT EXISTS idx_purchase_tracking_revision ON material_purchase_tracking(revision);
-- ================================
-- 7. 해시 생성 함수 (PostgreSQL)
-- ================================
CREATE OR REPLACE FUNCTION generate_material_hash(
description TEXT,
size_spec TEXT DEFAULT '',
material_grade TEXT DEFAULT ''
) RETURNS VARCHAR(32) AS $$
BEGIN
-- 정규화: 대소문자 통일, 공백 정리
description := UPPER(TRIM(REGEXP_REPLACE(description, '\s+', ' ', 'g')));
size_spec := UPPER(TRIM(COALESCE(size_spec, '')));
material_grade := UPPER(TRIM(COALESCE(material_grade, '')));
-- MD5 해시 생성 (32자리)
RETURN MD5(description || '|' || size_spec || '|' || material_grade);
END;
$$ LANGUAGE plpgsql;
-- ================================
-- 8. 기존 데이터에 해시 생성 (배치 처리)
-- ================================
-- 기존 materials 데이터에 해시 추가
UPDATE materials
SET
material_hash = generate_material_hash(original_description, size_spec, material_grade),
normalized_description = UPPER(TRIM(REGEXP_REPLACE(original_description, '\s+', ' ', 'g')))
WHERE material_hash IS NULL;
-- ================================
-- 9. 트리거 생성 (자동 해시 생성)
-- ================================
CREATE OR REPLACE FUNCTION auto_generate_material_hash()
RETURNS TRIGGER AS $$
BEGIN
-- 새로 삽입되거나 업데이트될 때 자동으로 해시 생성
NEW.material_hash := generate_material_hash(
NEW.original_description,
NEW.size_spec,
NEW.material_grade
);
NEW.normalized_description := UPPER(TRIM(REGEXP_REPLACE(NEW.original_description, '\s+', ' ', 'g')));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_auto_material_hash
BEFORE INSERT OR UPDATE ON materials
FOR EACH ROW
EXECUTE FUNCTION auto_generate_material_hash();
-- ================================
-- 10. 완료 메시지
-- ================================
SELECT 'Material comparison and purchase tracking system created successfully!' as status;

View File

@@ -0,0 +1,184 @@
-- ================================
-- Tubing 제품 관리 시스템
-- 실행일: 2025.08.01
-- ================================
-- 1. Tubing 카테고리 테이블 (일반, VCR, 기타 등)
CREATE TABLE IF NOT EXISTS tubing_categories (
id SERIAL PRIMARY KEY,
category_code VARCHAR(20) UNIQUE NOT NULL,
category_name VARCHAR(100) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. Tubing 규격 마스터 테이블
CREATE TABLE IF NOT EXISTS tubing_specifications (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES tubing_categories(id),
spec_code VARCHAR(50) UNIQUE NOT NULL,
spec_name VARCHAR(200) NOT NULL,
-- 물리적 규격
outer_diameter_mm DECIMAL(8,3), -- 외경 (mm)
wall_thickness_mm DECIMAL(6,3), -- 두께 (mm)
inner_diameter_mm DECIMAL(8,3), -- 내경 (mm, 계산 또는 실측)
-- 재질 정보
material_grade VARCHAR(100), -- SS316, SS316L, Inconel625 등
material_standard VARCHAR(100), -- ASTM A269, JIS G3463 등
-- 압력/온도 등급
max_pressure_bar DECIMAL(8,2), -- 최대 압력 (bar)
max_temperature_c DECIMAL(6,2), -- 최대 온도 (°C)
min_temperature_c DECIMAL(6,2), -- 최소 온도 (°C)
-- 표준 규격
standard_length_m DECIMAL(8,3), -- 표준 길이 (m)
bend_radius_min_mm DECIMAL(8,2), -- 최소 벤딩 반경 (mm)
-- 기타 정보
surface_finish VARCHAR(100), -- 표면 마감 (BA, #4, 2B 등)
hardness VARCHAR(50), -- 경도
notes TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. 제조사 정보 테이블
CREATE TABLE IF NOT EXISTS tubing_manufacturers (
id SERIAL PRIMARY KEY,
manufacturer_code VARCHAR(20) UNIQUE NOT NULL,
manufacturer_name VARCHAR(200) NOT NULL,
country VARCHAR(100),
website VARCHAR(500),
contact_info JSONB, -- 연락처 정보 (JSON)
quality_certs JSONB, -- 품질 인증서 정보 (ISO, API 등)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 제조사별 제품 테이블 (품목번호 매핑)
CREATE TABLE IF NOT EXISTS tubing_products (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES tubing_specifications(id),
manufacturer_id INTEGER REFERENCES tubing_manufacturers(id),
-- 제조사 품목번호 정보
manufacturer_part_number VARCHAR(200) NOT NULL, -- 제조사 품목번호
manufacturer_product_name VARCHAR(300), -- 제조사 제품명
-- 가격/공급 정보
list_price DECIMAL(12,2), -- 정가
currency VARCHAR(10) DEFAULT 'KRW', -- 통화
lead_time_days INTEGER, -- 리드타임 (일)
minimum_order_qty DECIMAL(10,3), -- 최소 주문 수량
standard_packaging_qty DECIMAL(10,3), -- 표준 포장 수량
-- 가용성 정보
availability_status VARCHAR(50), -- 재고 상태
last_price_update DATE, -- 마지막 가격 업데이트
-- 추가 정보
datasheet_url VARCHAR(500), -- 데이터시트 URL
catalog_page VARCHAR(100), -- 카탈로그 페이지
notes TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 유니크 제약 (같은 규격의 같은 제조사 제품은 하나만)
UNIQUE(specification_id, manufacturer_id, manufacturer_part_number)
);
-- 5. BOM에서 사용되는 Tubing 매핑 테이블
CREATE TABLE IF NOT EXISTS material_tubing_mapping (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
tubing_product_id INTEGER REFERENCES tubing_products(id),
-- 매핑 정보
confidence_score DECIMAL(3,2), -- 매핑 신뢰도 (0.00-1.00)
mapping_method VARCHAR(50), -- 매핑 방법 (auto/manual)
mapped_by VARCHAR(100), -- 매핑한 사용자
mapped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 수량 정보
required_length_m DECIMAL(10,3), -- 필요 길이 (m)
calculated_quantity DECIMAL(10,3), -- 계산된 주문 수량
-- 검증 정보
is_verified BOOLEAN DEFAULT FALSE,
verified_by VARCHAR(100),
verified_at TIMESTAMP,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ================================
-- 인덱스 생성
-- ================================
-- Tubing 규격 관련 인덱스
CREATE INDEX idx_tubing_specs_category ON tubing_specifications(category_id);
CREATE INDEX idx_tubing_specs_material ON tubing_specifications(material_grade);
CREATE INDEX idx_tubing_specs_diameter ON tubing_specifications(outer_diameter_mm, wall_thickness_mm);
-- 제품 관련 인덱스
CREATE INDEX idx_tubing_products_spec ON tubing_products(specification_id);
CREATE INDEX idx_tubing_products_manufacturer ON tubing_products(manufacturer_id);
CREATE INDEX idx_tubing_products_part_number ON tubing_products(manufacturer_part_number);
-- 매핑 관련 인덱스
CREATE INDEX idx_material_tubing_mapping_material ON material_tubing_mapping(material_id);
CREATE INDEX idx_material_tubing_mapping_product ON material_tubing_mapping(tubing_product_id);
-- ================================
-- 기초 데이터 입력
-- ================================
-- Tubing 카테고리 기초 데이터
INSERT INTO tubing_categories (category_code, category_name, description) VALUES
('GENERAL', '일반 Tubing', '일반적인 스테인리스 스틸 튜빙'),
('VCR', 'VCR Tubing', 'VCR (Vacuum Coupling Radiation) 연결용 튜빙'),
('SANITARY', 'Sanitary Tubing', '위생용 튜빙 (식품, 제약 등)'),
('HVAC', 'HVAC Tubing', '공조용 튜빙'),
('HYDRAULIC', 'Hydraulic Tubing', '유압용 튜빙'),
('PNEUMATIC', 'Pneumatic Tubing', '공압용 튜빙'),
('PROCESS', 'Process Tubing', '공정용 특수 튜빙'),
('EXOTIC', 'Exotic Material', '특수 재질 튜빙 (Hastelloy, Inconel 등)')
ON CONFLICT (category_code) DO NOTHING;
-- 주요 제조사 기초 데이터
INSERT INTO tubing_manufacturers (manufacturer_code, manufacturer_name, country) VALUES
('SWAGELOK', 'Swagelok Company', 'USA'),
('PARKER', 'Parker Hannifin', 'USA'),
('HAM_LET', 'Ham-Let Group', 'Israel'),
('SUPERLOK', 'Superlok USA', 'USA'),
('FITOK', 'Fitok Group', 'China'),
('DK_LOK', 'DK-Lok Corporation', 'South Korea'),
('GYROLOK', 'Gyrolok (Oliver Valves)', 'UK'),
('AS_ONE', 'AS ONE Corporation', 'Japan')
ON CONFLICT (manufacturer_code) DO NOTHING;
-- 기본 스테인리스 스틸 튜빙 규격 예시
INSERT INTO tubing_specifications (
category_id, spec_code, spec_name,
outer_diameter_mm, wall_thickness_mm, inner_diameter_mm,
material_grade, material_standard,
max_pressure_bar, max_temperature_c, min_temperature_c,
standard_length_m
) VALUES
(1, 'SS316-6MM-1MM', '6mm OD x 1mm WT SS316 Tubing', 6.0, 1.0, 4.0, 'SS316', 'ASTM A269', 413, 815, -196, 6.0),
(1, 'SS316-8MM-1MM', '8mm OD x 1mm WT SS316 Tubing', 8.0, 1.0, 6.0, 'SS316', 'ASTM A269', 310, 815, -196, 6.0),
(1, 'SS316-10MM-1MM', '10mm OD x 1mm WT SS316 Tubing', 10.0, 1.0, 8.0, 'SS316', 'ASTM A269', 248, 815, -196, 6.0),
(1, 'SS316-12MM-1.5MM', '12mm OD x 1.5mm WT SS316 Tubing', 12.0, 1.5, 9.0, 'SS316', 'ASTM A269', 310, 815, -196, 6.0),
(1, 'SS316L-6MM-1MM', '6mm OD x 1mm WT SS316L Tubing', 6.0, 1.0, 4.0, 'SS316L', 'ASTM A269', 413, 815, -196, 6.0)
ON CONFLICT (spec_code) DO NOTHING;

View File

@@ -0,0 +1,163 @@
-- ================================
-- 성능 최적화를 위한 추가 인덱스
-- 생성일: 2025.01 (Phase 2)
-- ================================
-- 1. 복합 인덱스 (자주 함께 사용되는 컬럼들)
-- ================================
-- files 테이블: job_no + revision 조합 (리비전 비교 시 자주 사용)
CREATE INDEX IF NOT EXISTS idx_files_job_revision
ON files(job_no, revision)
WHERE is_active = true;
-- files 테이블: job_no + upload_date (최신 파일 조회)
CREATE INDEX IF NOT EXISTS idx_files_job_date
ON files(job_no, upload_date DESC)
WHERE is_active = true;
-- materials 테이블: file_id + category (자재 분류별 조회)
CREATE INDEX IF NOT EXISTS idx_materials_file_category
ON materials(file_id, classified_category);
-- materials 테이블: category + material_grade (자재 종류별 재질 검색)
CREATE INDEX IF NOT EXISTS idx_materials_category_grade
ON materials(classified_category, material_grade);
-- 2. 검색 성능 향상 인덱스
-- ================================
-- materials 테이블: description 텍스트 검색 (GIN 인덱스)
CREATE INDEX IF NOT EXISTS idx_materials_description_gin
ON materials USING gin(to_tsvector('english', original_description));
-- materials 테이블: 해시 기반 중복 검색
CREATE INDEX IF NOT EXISTS idx_materials_hash
ON materials(material_hash)
WHERE material_hash IS NOT NULL;
-- 3. 정렬 성능 향상 인덱스
-- ================================
-- jobs 테이블: 상태별 생성일 정렬
CREATE INDEX IF NOT EXISTS idx_jobs_status_created
ON jobs(status, created_at DESC)
WHERE is_active = true;
-- materials 테이블: 수량별 정렬 (대용량 자재 우선 표시)
CREATE INDEX IF NOT EXISTS idx_materials_quantity_desc
ON materials(quantity DESC);
-- 4. 조건부 인덱스 (특정 조건에서만 사용)
-- ================================
-- 검증되지 않은 자재만 (분류 검토 필요한 항목)
CREATE INDEX IF NOT EXISTS idx_materials_unverified
ON materials(classified_category, classification_confidence)
WHERE is_verified = false;
-- 신뢰도가 낮은 분류 (0.8 미만)
CREATE INDEX IF NOT EXISTS idx_materials_low_confidence
ON materials(file_id, classified_category)
WHERE classification_confidence < 0.8;
-- 5. 외래키 성능 향상
-- ================================
-- pipe_details 테이블
CREATE INDEX IF NOT EXISTS idx_pipe_details_material
ON pipe_details(material_id);
-- fitting_details 테이블
CREATE INDEX IF NOT EXISTS idx_fitting_details_material
ON fitting_details(material_id);
-- valve_details 테이블
CREATE INDEX IF NOT EXISTS idx_valve_details_material
ON valve_details(material_id);
-- flange_details 테이블
CREATE INDEX IF NOT EXISTS idx_flange_details_material
ON flange_details(material_id);
-- bolt_details 테이블
CREATE INDEX IF NOT EXISTS idx_bolt_details_material
ON bolt_details(material_id);
-- gasket_details 테이블
CREATE INDEX IF NOT EXISTS idx_gasket_details_material
ON gasket_details(material_id);
-- instrument_details 테이블
CREATE INDEX IF NOT EXISTS idx_instrument_details_material
ON instrument_details(material_id);
-- 6. 통계 및 집계 성능 향상
-- ================================
-- 프로젝트별 자재 통계 (job_no 기준)
CREATE INDEX IF NOT EXISTS idx_materials_job_stats
ON materials(
(SELECT job_no FROM files WHERE files.id = materials.file_id),
classified_category
);
-- 파이프 길이 집계용 (파이프 cutting 계산)
CREATE INDEX IF NOT EXISTS idx_pipe_length_aggregation
ON pipe_details(material_id, length_mm)
WHERE length_mm > 0;
-- 7. 성능 모니터링을 위한 뷰 생성
-- ================================
-- 인덱스 사용률 모니터링 뷰
CREATE OR REPLACE VIEW index_usage_stats AS
SELECT
schemaname,
tablename,
indexname,
idx_tup_read,
idx_tup_fetch,
idx_scan,
CASE
WHEN idx_scan = 0 THEN 'UNUSED'
WHEN idx_scan < 10 THEN 'LOW_USAGE'
WHEN idx_scan < 100 THEN 'MEDIUM_USAGE'
ELSE 'HIGH_USAGE'
END as usage_level
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;
-- 테이블 크기 및 성능 모니터링 뷰
CREATE OR REPLACE VIEW table_performance_stats AS
SELECT
schemaname,
tablename,
n_tup_ins as inserts,
n_tup_upd as updates,
n_tup_del as deletes,
seq_scan as sequential_scans,
seq_tup_read as sequential_reads,
idx_scan as index_scans,
idx_tup_fetch as index_reads,
CASE
WHEN seq_scan + idx_scan = 0 THEN 0
ELSE ROUND((idx_scan::numeric / (seq_scan + idx_scan)) * 100, 2)
END as index_usage_percentage
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY seq_scan + idx_scan DESC;
-- ================================
-- 인덱스 생성 완료 로그
-- ================================
-- 성능 최적화 인덱스 생성 완료 확인
DO $$
BEGIN
RAISE NOTICE '성능 최적화 인덱스 생성 완료 - Phase 2 (2025.01)';
RAISE NOTICE '총 생성된 인덱스: 복합 인덱스 4개, 검색 인덱스 2개, 정렬 인덱스 2개';
RAISE NOTICE '조건부 인덱스 2개, 외래키 인덱스 7개, 집계 인덱스 2개';
RAISE NOTICE '모니터링 뷰 2개 생성';
END $$;

View File

@@ -0,0 +1,29 @@
-- jobs 테이블에 project_type 컬럼 추가
-- TK-MP-Project 프로젝트 유형 관리를 위한 스키마 업데이트
-- project_type 컬럼 추가 (기존 데이터가 있을 수 있으므로 안전하게 추가)
DO $$
BEGIN
-- project_type 컬럼이 존재하지 않으면 추가
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'jobs'
AND column_name = 'project_type'
) THEN
ALTER TABLE jobs ADD COLUMN project_type VARCHAR(50) DEFAULT '냉동기';
-- 기존 데이터에 대한 기본값 설정
UPDATE jobs SET project_type = '냉동기' WHERE project_type IS NULL;
-- NOT NULL 제약 조건 추가
ALTER TABLE jobs ALTER COLUMN project_type SET NOT NULL;
-- 인덱스 추가 (프로젝트 유형별 조회 성능 향상)
CREATE INDEX IF NOT EXISTS idx_jobs_project_type ON jobs(project_type);
RAISE NOTICE 'project_type 컬럼이 성공적으로 추가되었습니다.';
ELSE
RAISE NOTICE 'project_type 컬럼이 이미 존재합니다.';
END IF;
END $$;

View File

@@ -0,0 +1,242 @@
-- TK-MP-Project 인증 시스템을 위한 사용자 및 로그인 테이블 생성
-- TK-FB-Project 인증 시스템을 참고하여 구현
-- 1. 사용자 테이블 생성
CREATE TABLE IF NOT EXISTS users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
email VARCHAR(100),
-- 권한 관리
role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('admin', 'system', 'leader', 'support', 'user')),
access_level VARCHAR(20) DEFAULT 'worker' CHECK (access_level IN ('admin', 'system', 'group_leader', 'support_team', 'worker')),
-- 계정 상태 관리
is_active BOOLEAN DEFAULT true,
failed_login_attempts INT DEFAULT 0,
locked_until TIMESTAMP NULL,
-- 추가 정보
department VARCHAR(50),
position VARCHAR(50),
phone VARCHAR(20),
-- 타임스탬프
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login_at TIMESTAMP NULL
);
-- 2. 로그인 이력 테이블 생성
CREATE TABLE IF NOT EXISTS login_logs (
log_id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id) ON DELETE CASCADE,
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
user_agent TEXT,
login_status VARCHAR(20) CHECK (login_status IN ('success', 'failed')),
failure_reason VARCHAR(100),
session_duration INT, -- 세션 지속 시간 (초)
-- 인덱스를 위한 컬럼
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. 사용자 세션 테이블 (JWT Refresh Token 관리)
CREATE TABLE IF NOT EXISTS user_sessions (
session_id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id) ON DELETE CASCADE,
refresh_token VARCHAR(500) NOT NULL,
expires_at TIMESTAMP NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 권한 테이블 (확장 가능한 권한 시스템)
CREATE TABLE IF NOT EXISTS permissions (
permission_id SERIAL PRIMARY KEY,
permission_name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
module VARCHAR(30), -- 모듈별 권한 관리 (bom, project, purchase 등)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. 역할-권한 매핑 테이블
CREATE TABLE IF NOT EXISTS role_permissions (
role_permission_id SERIAL PRIMARY KEY,
role VARCHAR(20) NOT NULL,
permission_id INT REFERENCES permissions(permission_id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(role, permission_id)
);
-- 6. 인덱스 생성 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
CREATE INDEX IF NOT EXISTS idx_login_logs_user_id ON login_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_login_logs_login_time ON login_logs(login_time);
CREATE INDEX IF NOT EXISTS idx_login_logs_ip_address ON login_logs(ip_address);
CREATE INDEX IF NOT EXISTS idx_login_logs_status ON login_logs(login_status);
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_user_sessions_refresh_token ON user_sessions(refresh_token);
CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_user_sessions_is_active ON user_sessions(is_active);
CREATE INDEX IF NOT EXISTS idx_permissions_module ON permissions(module);
CREATE INDEX IF NOT EXISTS idx_role_permissions_role ON role_permissions(role);
-- 7. 기본 권한 데이터 삽입
INSERT INTO permissions (permission_name, description, module) VALUES
-- BOM 관리 권한
('bom.view', 'BOM 조회 권한', 'bom'),
('bom.create', 'BOM 생성 권한', 'bom'),
('bom.edit', 'BOM 수정 권한', 'bom'),
('bom.delete', 'BOM 삭제 권한', 'bom'),
('bom.approve', 'BOM 승인 권한', 'bom'),
-- 프로젝트 관리 권한
('project.view', '프로젝트 조회 권한', 'project'),
('project.create', '프로젝트 생성 권한', 'project'),
('project.edit', '프로젝트 수정 권한', 'project'),
('project.delete', '프로젝트 삭제 권한', 'project'),
('project.manage', '프로젝트 관리 권한', 'project'),
-- 파일 관리 권한
('file.upload', '파일 업로드 권한', 'file'),
('file.download', '파일 다운로드 권한', 'file'),
('file.delete', '파일 삭제 권한', 'file'),
-- 사용자 관리 권한
('user.view', '사용자 조회 권한', 'user'),
('user.create', '사용자 생성 권한', 'user'),
('user.edit', '사용자 수정 권한', 'user'),
('user.delete', '사용자 삭제 권한', 'user'),
-- 시스템 관리 권한
('system.admin', '시스템 관리 권한', 'system'),
('system.logs', '로그 조회 권한', 'system'),
('system.settings', '시스템 설정 권한', 'system')
ON CONFLICT (permission_name) DO NOTHING;
-- 8. 역할별 기본 권한 할당
INSERT INTO role_permissions (role, permission_id)
SELECT 'admin', permission_id FROM permissions
ON CONFLICT (role, permission_id) DO NOTHING;
INSERT INTO role_permissions (role, permission_id)
SELECT 'system', permission_id FROM permissions
ON CONFLICT (role, permission_id) DO NOTHING;
INSERT INTO role_permissions (role, permission_id)
SELECT 'leader', permission_id FROM permissions
WHERE permission_name IN (
'bom.view', 'bom.create', 'bom.edit', 'bom.approve',
'project.view', 'project.create', 'project.edit', 'project.manage',
'file.upload', 'file.download', 'file.delete',
'user.view'
)
ON CONFLICT (role, permission_id) DO NOTHING;
INSERT INTO role_permissions (role, permission_id)
SELECT 'support', permission_id FROM permissions
WHERE permission_name IN (
'bom.view', 'bom.create', 'bom.edit',
'project.view', 'project.create', 'project.edit',
'file.upload', 'file.download'
)
ON CONFLICT (role, permission_id) DO NOTHING;
INSERT INTO role_permissions (role, permission_id)
SELECT 'user', permission_id FROM permissions
WHERE permission_name IN (
'bom.view',
'project.view',
'file.upload', 'file.download'
)
ON CONFLICT (role, permission_id) DO NOTHING;
-- 9. 기본 관리자 계정 생성 (비밀번호: admin123)
-- bcrypt 해시: $2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi
INSERT INTO users (username, password, name, email, role, access_level, department, position) VALUES
('admin', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '시스템 관리자', 'admin@tkmp.com', 'admin', 'admin', 'IT', '시스템 관리자'),
('system', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '시스템 계정', 'system@tkmp.com', 'system', 'system', 'IT', '시스템 계정')
ON CONFLICT (username) DO NOTHING;
-- 10. 트리거 함수 생성 (updated_at 자동 업데이트)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- 11. 트리거 적용
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_sessions_updated_at BEFORE UPDATE ON user_sessions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 12. 뷰 생성 (사용자 정보 조회용)
CREATE OR REPLACE VIEW user_info_view AS
SELECT
u.user_id,
u.username,
u.name,
u.email,
u.role,
u.access_level,
u.department,
u.position,
u.is_active,
u.created_at,
u.last_login_at,
COUNT(ll.log_id) as login_count,
MAX(ll.login_time) as last_successful_login
FROM users u
LEFT JOIN login_logs ll ON u.user_id = ll.user_id AND ll.login_status = 'success'
GROUP BY u.user_id, u.username, u.name, u.email, u.role, u.access_level,
u.department, u.position, u.is_active, u.created_at, u.last_login_at;
-- 완료 메시지
DO $$
BEGIN
RAISE NOTICE '✅ TK-MP-Project 인증 시스템 데이터베이스 스키마가 성공적으로 생성되었습니다!';
RAISE NOTICE '📋 생성된 테이블: users, login_logs, user_sessions, permissions, role_permissions';
RAISE NOTICE '👤 기본 계정: admin/admin123, system/admin123';
RAISE NOTICE '🔐 권한 시스템: 5단계 역할 + 모듈별 세분화된 권한';
END $$;

View File

@@ -0,0 +1,142 @@
-- 사용자 추적 및 담당자 기록 필드 추가
-- 생성일: 2025.01
-- 목적: RULES 가이드라인에 따른 사용자 추적 시스템 구축
-- ================================
-- 1. 기존 테이블에 담당자 필드 추가
-- ================================
-- files 테이블 수정 (uploaded_by는 이미 존재)
ALTER TABLE files
ADD COLUMN IF NOT EXISTS updated_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
-- jobs 테이블 수정
ALTER TABLE jobs
ADD COLUMN IF NOT EXISTS created_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS updated_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS assigned_to VARCHAR(100);
-- materials 테이블 수정
ALTER TABLE materials
ADD COLUMN IF NOT EXISTS classified_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS classified_at TIMESTAMP,
ADD COLUMN IF NOT EXISTS updated_by VARCHAR(100);
-- ================================
-- 2. 사용자 활동 로그 테이블 생성
-- ================================
CREATE TABLE IF NOT EXISTS user_activity_logs (
id SERIAL PRIMARY KEY,
user_id INTEGER, -- users 테이블 참조 (외래키 제약 없음 - 유연성)
username VARCHAR(100) NOT NULL, -- 사용자명 (필수)
-- 활동 정보
activity_type VARCHAR(50) NOT NULL, -- 'FILE_UPLOAD', 'PROJECT_CREATE', 'PURCHASE_CONFIRM' 등
activity_description TEXT, -- 상세 활동 내용
-- 대상 정보
target_id INTEGER, -- 대상 ID (파일, 프로젝트 등)
target_type VARCHAR(50), -- 'FILE', 'PROJECT', 'MATERIAL', 'PURCHASE' 등
-- 세션 정보
ip_address VARCHAR(45), -- IP 주소
user_agent TEXT, -- 브라우저 정보
-- 추가 메타데이터 (JSON)
metadata JSONB, -- 추가 정보 (파일 크기, 처리 시간 등)
-- 시간 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ================================
-- 3. 구매 관련 테이블 수정
-- ================================
-- purchase_items 테이블 수정 (이미 created_by 존재하는지 확인 후 추가)
ALTER TABLE purchase_items
ADD COLUMN IF NOT EXISTS updated_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS approved_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMP;
-- material_purchase_tracking 테이블 수정 (이미 confirmed_by 존재)
ALTER TABLE material_purchase_tracking
ADD COLUMN IF NOT EXISTS ordered_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS ordered_at TIMESTAMP,
ADD COLUMN IF NOT EXISTS approved_by VARCHAR(100),
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMP;
-- ================================
-- 4. 인덱스 생성 (성능 최적화)
-- ================================
-- 사용자 활동 로그 인덱스
CREATE INDEX IF NOT EXISTS idx_user_activity_logs_username ON user_activity_logs(username);
CREATE INDEX IF NOT EXISTS idx_user_activity_logs_activity_type ON user_activity_logs(activity_type);
CREATE INDEX IF NOT EXISTS idx_user_activity_logs_created_at ON user_activity_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_user_activity_logs_target ON user_activity_logs(target_type, target_id);
-- 담당자 필드 인덱스
CREATE INDEX IF NOT EXISTS idx_files_uploaded_by ON files(uploaded_by);
CREATE INDEX IF NOT EXISTS idx_files_updated_by ON files(updated_by);
CREATE INDEX IF NOT EXISTS idx_jobs_created_by ON jobs(created_by);
CREATE INDEX IF NOT EXISTS idx_jobs_assigned_to ON jobs(assigned_to);
CREATE INDEX IF NOT EXISTS idx_materials_classified_by ON materials(classified_by);
-- ================================
-- 5. 트리거 생성 (자동 updated_at 갱신)
-- ================================
-- files 테이블 updated_at 자동 갱신
CREATE OR REPLACE FUNCTION update_files_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS trigger_files_updated_at ON files;
CREATE TRIGGER trigger_files_updated_at
BEFORE UPDATE ON files
FOR EACH ROW
EXECUTE FUNCTION update_files_updated_at();
-- jobs 테이블 updated_at 자동 갱신
CREATE OR REPLACE FUNCTION update_jobs_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS trigger_jobs_updated_at ON jobs;
CREATE TRIGGER trigger_jobs_updated_at
BEFORE UPDATE ON jobs
FOR EACH ROW
EXECUTE FUNCTION update_jobs_updated_at();
-- ================================
-- 6. 기본 데이터 설정
-- ================================
-- 기존 데이터에 기본 담당자 설정 (시스템 마이그레이션용)
UPDATE files SET uploaded_by = 'system' WHERE uploaded_by IS NULL;
UPDATE jobs SET created_by = 'system' WHERE created_by IS NULL;
-- ================================
-- 7. 권한 및 보안 설정
-- ================================
-- 활동 로그 테이블은 INSERT만 허용 (수정/삭제 방지)
-- 실제 운영에서는 별도 권한 관리 필요
COMMENT ON TABLE user_activity_logs IS '사용자 활동 로그 - 모든 업무 활동 추적';
COMMENT ON COLUMN user_activity_logs.activity_type IS '활동 유형: FILE_UPLOAD, PROJECT_CREATE, PURCHASE_CONFIRM, MATERIAL_CLASSIFY 등';
COMMENT ON COLUMN user_activity_logs.metadata IS '추가 정보 JSON: 파일크기, 처리시간, 변경내용 등';
-- 완료 메시지
SELECT 'User tracking system tables created successfully!' as result;

View File

@@ -0,0 +1,50 @@
-- 파이프 끝단 가공 정보 테이블 생성
-- 각 파이프별로 끝단 가공 정보를 별도 저장
CREATE TABLE IF NOT EXISTS pipe_end_preparations (
id SERIAL PRIMARY KEY,
material_id INTEGER NOT NULL REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
-- 끝단 가공 정보
end_preparation_type VARCHAR(50) DEFAULT 'PBE', -- PBE(양쪽무개선), BBE(양쪽개선), POE(한쪽개선), PE(무개선)
end_preparation_code VARCHAR(20), -- 원본 코드 (BBE, POE, PBE 등)
machining_required BOOLEAN DEFAULT FALSE, -- 가공 필요 여부
cutting_note TEXT, -- 가공 메모
-- 원본 정보 보존
original_description TEXT NOT NULL, -- 끝단 가공 포함된 원본 설명
clean_description TEXT NOT NULL, -- 끝단 가공 제외한 구매용 설명
-- 메타데이터
confidence FLOAT DEFAULT 0.0, -- 분류 신뢰도
matched_pattern VARCHAR(100), -- 매칭된 패턴
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_pipe_end_preparations_material_id ON pipe_end_preparations(material_id);
CREATE INDEX IF NOT EXISTS idx_pipe_end_preparations_file_id ON pipe_end_preparations(file_id);
CREATE INDEX IF NOT EXISTS idx_pipe_end_preparations_type ON pipe_end_preparations(end_preparation_type);
-- 기본 끝단 가공 타입 정의
COMMENT ON COLUMN pipe_end_preparations.end_preparation_type IS 'PBE: 양쪽무개선(기본값), BBE: 양쪽개선, POE: 한쪽개선, PE: 무개선';
COMMENT ON COLUMN pipe_end_preparations.machining_required IS '가공이 필요한지 여부 (개선 작업 등)';
COMMENT ON COLUMN pipe_end_preparations.clean_description IS '구매 시 사용할 끝단 가공 정보가 제거된 설명';
-- 트리거: updated_at 자동 업데이트
CREATE OR REPLACE FUNCTION update_pipe_end_preparations_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_pipe_end_preparations_updated_at
BEFORE UPDATE ON pipe_end_preparations
FOR EACH ROW
EXECUTE FUNCTION update_pipe_end_preparations_updated_at();

View File

@@ -0,0 +1,19 @@
-- 사용자 요구사항 테이블에 material_id 컬럼 추가
-- 2025.09.24 - 사용자 피드백 기반 개선사항 #1
-- material_id 컬럼 추가 (nullable로 시작)
ALTER TABLE user_requirements
ADD COLUMN IF NOT EXISTS material_id INTEGER;
-- 외래키 제약조건 추가
ALTER TABLE user_requirements
ADD CONSTRAINT fk_user_requirements_material_id
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE;
-- 인덱스 추가 (성능 향상)
CREATE INDEX IF NOT EXISTS idx_user_requirements_material_id ON user_requirements(material_id);
-- 기존 데이터 정리 (필요시)
-- DELETE FROM user_requirements WHERE material_id IS NULL;
COMMENT ON COLUMN user_requirements.material_id IS '자재 ID (개별 자재별 요구사항 연결)';

View File

@@ -0,0 +1,11 @@
-- 전체 재질명 표기를 위한 컬럼 추가
-- 2025.09.24 - 사용자 피드백 기반 개선사항 #2
-- full_material_grade 컬럼 추가 (원본 설명에서 추출한 전체 재질명)
ALTER TABLE materials
ADD COLUMN IF NOT EXISTS full_material_grade TEXT;
-- 인덱스 추가 (검색 성능 향상)
CREATE INDEX IF NOT EXISTS idx_materials_full_material_grade ON materials(full_material_grade);
COMMENT ON COLUMN materials.full_material_grade IS '전체 재질명 (예: ASTM A312 TP304, ASTM A106 GR B 등)';

View File

@@ -0,0 +1,45 @@
-- SUPPORT 카테고리 상세 정보 테이블 생성
-- 2025.09.24 - 사용자 피드백 기반 개선사항 #3
-- support_details 테이블 생성
CREATE TABLE IF NOT EXISTS support_details (
id SERIAL PRIMARY KEY,
material_id INTEGER NOT NULL REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
-- 서포트 타입 정보
support_type VARCHAR(50), -- URETHANE_BLOCK, CLAMP, HANGER, SPRING_HANGER 등
support_subtype VARCHAR(100), -- 상세 타입
-- 하중 정보
load_rating VARCHAR(20), -- LIGHT, MEDIUM, HEAVY, CUSTOM
load_capacity VARCHAR(20), -- 40T, 50TON 등
-- 재질 정보
material_standard VARCHAR(50), -- 재질 표준
material_grade VARCHAR(100), -- 재질 등급
-- 사이즈 정보
pipe_size VARCHAR(20), -- 지지하는 파이프 크기
length_mm DECIMAL(10,2), -- 길이 (mm)
width_mm DECIMAL(10,2), -- 폭 (mm)
height_mm DECIMAL(10,2), -- 높이 (mm)
-- 분류 신뢰도
classification_confidence DECIMAL(3,2), -- 0.00-1.00
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_support_details_material_id ON support_details(material_id);
CREATE INDEX IF NOT EXISTS idx_support_details_file_id ON support_details(file_id);
CREATE INDEX IF NOT EXISTS idx_support_details_support_type ON support_details(support_type);
-- 코멘트 추가
COMMENT ON TABLE support_details IS '배관 지지재 상세 정보 (우레탄 블록, 클램프, 행거 등)';
COMMENT ON COLUMN support_details.support_type IS '서포트 타입 (URETHANE_BLOCK, CLAMP, HANGER 등)';
COMMENT ON COLUMN support_details.load_capacity IS '하중 용량 (40T, 50TON 등)';
COMMENT ON COLUMN support_details.pipe_size IS '지지하는 파이프 크기';

View File

@@ -0,0 +1,130 @@
-- ================================
-- SPECIAL 카테고리 지원 추가 마이그레이션
-- 생성일: 2025.09.30
-- 목적: SPECIAL 카테고리 분류 및 관련 기능 지원
-- ================================
-- 1. materials 테이블 SPECIAL 카테고리 지원 확인
-- ================================
-- classified_category 컬럼이 SPECIAL 값을 지원하는지 확인
-- (이미 VARCHAR(50)이므로 추가 작업 불필요, 하지만 명시적으로 체크)
-- SPECIAL 카테고리 관련 인덱스 추가 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_materials_special_category
ON materials(classified_category)
WHERE classified_category = 'SPECIAL';
-- 2. SPECIAL 카테고리 분류 규칙 테이블 생성
-- ================================
-- SPECIAL 키워드 패턴 테이블
CREATE TABLE IF NOT EXISTS special_classification_patterns (
id SERIAL PRIMARY KEY,
pattern_type VARCHAR(20) NOT NULL, -- 'KEYWORD', 'REGEX', 'EXACT'
pattern_value VARCHAR(200) NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1, -- 우선순위 (낮을수록 높은 우선순위)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 기본 SPECIAL 키워드 패턴 삽입
INSERT INTO special_classification_patterns (pattern_type, pattern_value, description, priority) VALUES
('KEYWORD', 'SPECIAL', '영문 SPECIAL 키워드', 1),
('KEYWORD', '스페셜', '한글 스페셜 키워드', 1),
('KEYWORD', 'SPEC', '영문 SPEC 축약어', 2),
('KEYWORD', 'SPL', '영문 SPL 축약어', 2)
ON CONFLICT DO NOTHING;
-- 3. SPECIAL 자재 추가 정보 테이블
-- ================================
-- SPECIAL 자재 상세 정보 테이블 (도면 업로드 관련)
CREATE TABLE IF NOT EXISTS special_material_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 도면 정보
drawing_number VARCHAR(100), -- 도면 번호
drawing_revision VARCHAR(20), -- 도면 리비전
drawing_uploaded BOOLEAN DEFAULT FALSE, -- 도면 업로드 여부
drawing_file_path TEXT, -- 도면 파일 경로
-- 특수 요구사항
special_requirements TEXT, -- 특수 제작 요구사항
manufacturing_notes TEXT, -- 제작 참고사항
approval_required BOOLEAN DEFAULT TRUE, -- 승인 필요 여부
approved_by VARCHAR(100), -- 승인자
approved_at TIMESTAMP, -- 승인 일시
-- 분류 정보
classification_confidence FLOAT DEFAULT 1.0,
classification_reason TEXT, -- 분류 근거
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 인덱스 생성 (성능 최적화)
-- ================================
-- special_classification_patterns 인덱스
CREATE INDEX IF NOT EXISTS idx_special_patterns_type ON special_classification_patterns(pattern_type);
CREATE INDEX IF NOT EXISTS idx_special_patterns_active ON special_classification_patterns(is_active);
CREATE INDEX IF NOT EXISTS idx_special_patterns_priority ON special_classification_patterns(priority);
-- special_material_details 인덱스
CREATE INDEX IF NOT EXISTS idx_special_details_material ON special_material_details(material_id);
CREATE INDEX IF NOT EXISTS idx_special_details_file ON special_material_details(file_id);
CREATE INDEX IF NOT EXISTS idx_special_details_drawing_uploaded ON special_material_details(drawing_uploaded);
CREATE INDEX IF NOT EXISTS idx_special_details_approval ON special_material_details(approval_required);
-- 5. 기존 자재 재분류 (선택적)
-- ================================
-- 기존 자료 중 SPECIAL 키워드가 포함된 자재를 SPECIAL 카테고리로 재분류
UPDATE materials
SET
classified_category = 'SPECIAL',
classification_confidence = 1.0,
updated_by = 'SYSTEM_MIGRATION',
classified_at = CURRENT_TIMESTAMP
WHERE
(
UPPER(original_description) LIKE '%SPECIAL%' OR
UPPER(original_description) LIKE '%스페셜%' OR
UPPER(original_description) LIKE '%SPEC%' OR
UPPER(original_description) LIKE '%SPL%'
)
AND (classified_category IS NULL OR classified_category != 'SPECIAL');
-- 6. 통계 및 검증
-- ================================
-- SPECIAL 카테고리 자재 개수 확인
DO $$
DECLARE
special_count INTEGER;
BEGIN
SELECT COUNT(*) INTO special_count FROM materials WHERE classified_category = 'SPECIAL';
RAISE NOTICE 'SPECIAL 카테고리로 분류된 자재 개수: %', special_count;
END $$;
-- 7. 권한 설정 (필요시)
-- ================================
-- SPECIAL 자재 관리 권한 (향후 확장용)
-- 현재는 기본 materials 테이블 권한을 따름
COMMIT;
-- ================================
-- 마이그레이션 완료 로그
-- ================================
INSERT INTO migration_log (script_name, executed_at, description) VALUES
('24_add_special_category_support.sql', CURRENT_TIMESTAMP, 'SPECIAL 카테고리 지원 추가 및 기존 자재 재분류')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
SPECIAL 카테고리 마이그레이션 실행 스크립트
생성일: 2025.09.30
목적: SPECIAL 카테고리 지원 추가 및 기존 자재 재분류
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine, text
from app.database import DATABASE_URL
def execute_special_migration():
"""SPECIAL 카테고리 마이그레이션 실행"""
engine = create_engine(DATABASE_URL)
with engine.connect() as conn:
print("🚀 SPECIAL 카테고리 마이그레이션 시작...")
print("=" * 60)
try:
# 1. 마이그레이션 스크립트 실행
print("📋 1단계: 마이그레이션 스크립트 실행...")
script_path = os.path.join(os.path.dirname(__file__), '24_add_special_category_support.sql')
with open(script_path, 'r', encoding='utf-8') as f:
sql_content = f.read()
# SQL 명령어들을 분리하여 실행
sql_commands = sql_content.split(';')
for i, command in enumerate(sql_commands):
command = command.strip()
if command and not command.startswith('--') and command != 'COMMIT':
try:
conn.execute(text(command))
if i % 10 == 0: # 진행상황 표시
print(f" - 명령어 {i+1}/{len(sql_commands)} 실행 중...")
except Exception as e:
print(f" ⚠️ 명령어 실행 중 오류 (무시됨): {e}")
continue
print("✅ 마이그레이션 스크립트 실행 완료")
# 2. 기존 자재 재분류 확인
print("\n📊 2단계: 기존 자재 재분류 결과 확인...")
# SPECIAL 키워드가 포함된 자재 개수 확인
result = conn.execute(text("""
SELECT COUNT(*) as count
FROM materials
WHERE classified_category = 'SPECIAL'
""")).fetchone()
special_count = result.count if result else 0
print(f" - SPECIAL 카테고리로 분류된 자재: {special_count}")
# 키워드별 분류 결과 확인
keyword_results = conn.execute(text("""
SELECT
CASE
WHEN UPPER(original_description) LIKE '%SPECIAL%' THEN 'SPECIAL'
WHEN UPPER(original_description) LIKE '%스페셜%' THEN '스페셜'
WHEN UPPER(original_description) LIKE '%SPEC%' THEN 'SPEC'
WHEN UPPER(original_description) LIKE '%SPL%' THEN 'SPL'
END as keyword_type,
COUNT(*) as count
FROM materials
WHERE classified_category = 'SPECIAL'
GROUP BY keyword_type
ORDER BY count DESC
""")).fetchall()
if keyword_results:
print(" - 키워드별 분류 결과:")
for row in keyword_results:
print(f" * {row.keyword_type}: {row.count}")
# 3. 테이블 생성 확인
print("\n🏗️ 3단계: 새 테이블 생성 확인...")
# special_classification_patterns 테이블 확인
patterns_count = conn.execute(text("""
SELECT COUNT(*) as count
FROM special_classification_patterns
""")).fetchone()
patterns_count = patterns_count.count if patterns_count else 0
print(f" - special_classification_patterns 테이블: {patterns_count}개 패턴 등록됨")
# special_material_details 테이블 확인
details_exists = conn.execute(text("""
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'special_material_details'
) as exists
""")).fetchone()
if details_exists.exists:
print(" - special_material_details 테이블: 생성 완료")
else:
print(" - ❌ special_material_details 테이블: 생성 실패")
# 4. 인덱스 생성 확인
print("\n🔍 4단계: 인덱스 생성 확인...")
special_indexes = conn.execute(text("""
SELECT indexname
FROM pg_indexes
WHERE indexname LIKE '%special%'
ORDER BY indexname
""")).fetchall()
if special_indexes:
print(" - SPECIAL 관련 인덱스:")
for idx in special_indexes:
print(f" * {idx.indexname}")
else:
print(" - ⚠️ SPECIAL 관련 인덱스가 생성되지 않았습니다.")
# 커밋
conn.commit()
print("\n" + "=" * 60)
print("🎉 SPECIAL 카테고리 마이그레이션 완료!")
print(f"📊 총 {special_count}개 자재가 SPECIAL 카테고리로 분류되었습니다.")
print("🔧 새로운 기능:")
print(" - SPECIAL 키워드 자동 감지")
print(" - 도면 업로드 관리")
print(" - 특수 제작 요구사항 추적")
print(" - 승인 프로세스 지원")
except Exception as e:
print(f"\n❌ 마이그레이션 실행 중 오류 발생: {e}")
conn.rollback()
raise
def main():
"""메인 실행 함수"""
print("SPECIAL 카테고리 마이그레이션 실행")
print("TK-MP-Project - 특수 자재 관리 시스템")
print("=" * 60)
try:
execute_special_migration()
except Exception as e:
print(f"\n💥 마이그레이션 실패: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,28 @@
-- users 테이블에 status 컬럼 추가 및 기존 데이터 마이그레이션
-- 1. status 컬럼 추가 (기본값은 'active')
ALTER TABLE users
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'active';
-- 2. status 컬럼에 CHECK 제약 조건 추가
ALTER TABLE users
ADD CONSTRAINT users_status_check
CHECK (status IN ('pending', 'active', 'suspended', 'deleted'));
-- 3. 기존 데이터 마이그레이션
-- is_active가 false인 사용자는 'pending'으로
-- is_active가 true인 사용자는 'active'로
UPDATE users
SET status = CASE
WHEN is_active = FALSE THEN 'pending'
WHEN is_active = TRUE THEN 'active'
ELSE 'active'
END;
-- 4. status 컬럼에 인덱스 추가 (조회 성능 향상)
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
-- 5. 향후 is_active 컬럼은 deprecated로 간주
-- 하지만 하위 호환성을 위해 당분간 유지
COMMENT ON COLUMN users.status IS 'User account status: pending, active, suspended, deleted';
COMMENT ON COLUMN users.is_active IS 'DEPRECATED: Use status column instead. Kept for backward compatibility.';

View File

@@ -0,0 +1,135 @@
-- 엑셀 내보내기 이력 및 구매 상태 관리 테이블
-- 1. 엑셀 내보내기 이력 테이블
CREATE TABLE IF NOT EXISTS excel_export_history (
export_id SERIAL PRIMARY KEY,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
job_no VARCHAR(50) REFERENCES jobs(job_no),
exported_by INTEGER REFERENCES users(user_id),
export_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
export_type VARCHAR(50), -- 'full', 'category', 'filtered'
category VARCHAR(50), -- PIPE, FLANGE, VALVE 등
material_count INTEGER,
file_name VARCHAR(255),
notes TEXT,
-- 메타데이터
filters_applied JSONB, -- 적용된 필터 조건들
export_options JSONB -- 내보내기 옵션들
);
-- 2. 내보낸 자재 상세 (어떤 자재들이 내보내졌는지 추적)
CREATE TABLE IF NOT EXISTS exported_materials (
id SERIAL PRIMARY KEY,
export_id INTEGER REFERENCES excel_export_history(export_id) ON DELETE CASCADE,
material_id INTEGER REFERENCES materials(id),
purchase_status VARCHAR(50) DEFAULT 'pending', -- pending, requested, ordered, received, cancelled
purchase_request_no VARCHAR(100), -- 구매요청 번호
purchase_order_no VARCHAR(100), -- 구매주문 번호
requested_date TIMESTAMP,
ordered_date TIMESTAMP,
expected_date DATE,
received_date TIMESTAMP,
quantity_exported INTEGER, -- 내보낸 수량
quantity_ordered INTEGER, -- 주문 수량
quantity_received INTEGER, -- 입고 수량
unit_price DECIMAL(15, 2),
total_price DECIMAL(15, 2),
vendor_name VARCHAR(255),
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_by INTEGER REFERENCES users(user_id)
);
-- 3. 구매 상태 이력 (상태 변경 추적)
CREATE TABLE IF NOT EXISTS purchase_status_history (
history_id SERIAL PRIMARY KEY,
exported_material_id INTEGER REFERENCES exported_materials(id) ON DELETE CASCADE,
material_id INTEGER REFERENCES materials(id),
previous_status VARCHAR(50),
new_status VARCHAR(50),
changed_by INTEGER REFERENCES users(user_id),
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reason TEXT,
metadata JSONB -- 추가 정보 (예: 문서 번호, 승인자 등)
);
-- 4. 구매 문서 관리
CREATE TABLE IF NOT EXISTS purchase_documents (
document_id SERIAL PRIMARY KEY,
export_id INTEGER REFERENCES excel_export_history(export_id),
document_type VARCHAR(50), -- 'purchase_request', 'purchase_order', 'invoice', 'receipt'
document_no VARCHAR(100),
document_date DATE,
file_path VARCHAR(500),
uploaded_by INTEGER REFERENCES users(user_id),
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
notes TEXT
);
-- 인덱스 추가
CREATE INDEX IF NOT EXISTS idx_export_history_file_id ON excel_export_history(file_id);
CREATE INDEX IF NOT EXISTS idx_export_history_job_no ON excel_export_history(job_no);
CREATE INDEX IF NOT EXISTS idx_export_history_date ON excel_export_history(export_date);
CREATE INDEX IF NOT EXISTS idx_exported_materials_export_id ON exported_materials(export_id);
CREATE INDEX IF NOT EXISTS idx_exported_materials_material_id ON exported_materials(material_id);
CREATE INDEX IF NOT EXISTS idx_exported_materials_status ON exported_materials(purchase_status);
CREATE INDEX IF NOT EXISTS idx_exported_materials_pr_no ON exported_materials(purchase_request_no);
CREATE INDEX IF NOT EXISTS idx_exported_materials_po_no ON exported_materials(purchase_order_no);
CREATE INDEX IF NOT EXISTS idx_purchase_history_material ON purchase_status_history(material_id);
CREATE INDEX IF NOT EXISTS idx_purchase_history_date ON purchase_status_history(changed_at);
-- 뷰 생성: 구매 상태별 자재 현황
CREATE OR REPLACE VIEW v_purchase_status_summary AS
SELECT
em.purchase_status,
COUNT(DISTINCT em.material_id) as material_count,
COUNT(DISTINCT em.export_id) as export_count,
SUM(em.quantity_exported) as total_quantity_exported,
SUM(em.quantity_ordered) as total_quantity_ordered,
SUM(em.quantity_received) as total_quantity_received,
SUM(em.total_price) as total_amount,
MAX(em.updated_at) as last_updated
FROM exported_materials em
GROUP BY em.purchase_status;
-- 뷰 생성: 자재별 최신 구매 상태
CREATE OR REPLACE VIEW v_material_latest_purchase_status AS
SELECT DISTINCT ON (m.id)
m.id as material_id,
m.original_description,
m.classified_category,
em.purchase_status,
em.purchase_request_no,
em.purchase_order_no,
em.vendor_name,
em.expected_date,
em.quantity_ordered,
em.quantity_received,
em.updated_at as status_updated_at,
eeh.export_date as last_exported_date
FROM materials m
LEFT JOIN exported_materials em ON m.id = em.material_id
LEFT JOIN excel_export_history eeh ON em.export_id = eeh.export_id
ORDER BY m.id, em.updated_at DESC;
-- 트리거: updated_at 자동 업데이트
CREATE OR REPLACE FUNCTION update_exported_materials_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_exported_materials_updated_at_trigger
BEFORE UPDATE ON exported_materials
FOR EACH ROW
EXECUTE FUNCTION update_exported_materials_updated_at();
-- 코멘트 추가
COMMENT ON TABLE excel_export_history IS '엑셀 내보내기 이력 관리';
COMMENT ON TABLE exported_materials IS '내보낸 자재의 구매 상태 추적';
COMMENT ON TABLE purchase_status_history IS '구매 상태 변경 이력';
COMMENT ON TABLE purchase_documents IS '구매 관련 문서 관리';
COMMENT ON COLUMN exported_materials.purchase_status IS 'pending: 구매신청 전, requested: 구매신청, ordered: 구매주문, received: 입고완료, cancelled: 취소';

View File

@@ -0,0 +1,44 @@
-- 구매신청 관리 테이블
-- 구매신청 그룹 (같이 신청한 항목들의 묶음)
CREATE TABLE IF NOT EXISTS purchase_requests (
request_id SERIAL PRIMARY KEY,
request_no VARCHAR(50) UNIQUE, -- PR-20241014-001 형식
file_id INTEGER REFERENCES files(id),
job_no VARCHAR(50) REFERENCES jobs(job_no),
category VARCHAR(50),
material_count INTEGER,
excel_file_path VARCHAR(500), -- 저장된 엑셀 파일 경로
requested_by INTEGER REFERENCES users(user_id),
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'requested', -- requested, ordered, received
notes TEXT
);
-- 구매신청 자재 상세
CREATE TABLE IF NOT EXISTS purchase_request_items (
item_id SERIAL PRIMARY KEY,
request_id INTEGER REFERENCES purchase_requests(request_id) ON DELETE CASCADE,
material_id INTEGER REFERENCES materials(id),
quantity INTEGER,
unit VARCHAR(20),
user_requirement TEXT,
is_ordered BOOLEAN DEFAULT FALSE,
is_received BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스
CREATE INDEX IF NOT EXISTS idx_purchase_requests_file_id ON purchase_requests(file_id);
CREATE INDEX IF NOT EXISTS idx_purchase_requests_job_no ON purchase_requests(job_no);
CREATE INDEX IF NOT EXISTS idx_purchase_requests_status ON purchase_requests(status);
CREATE INDEX IF NOT EXISTS idx_purchase_request_items_request_id ON purchase_request_items(request_id);
CREATE INDEX IF NOT EXISTS idx_purchase_request_items_material_id ON purchase_request_items(material_id);
-- 뷰: 구매신청된 자재 ID 목록
CREATE OR REPLACE VIEW v_requested_material_ids AS
SELECT DISTINCT material_id
FROM purchase_request_items;
COMMENT ON TABLE purchase_requests IS '구매신청 그룹 관리';
COMMENT ON TABLE purchase_request_items IS '구매신청 자재 상세';

View File

@@ -0,0 +1,20 @@
-- 리비전 관리 개선: 자재 상태 추적
-- 리비전 업로드 시 삭제된 자재의 상태를 추적
-- materials 테이블에 revision_status 컬럼 추가
ALTER TABLE materials ADD COLUMN IF NOT EXISTS revision_status VARCHAR(20) DEFAULT 'active';
-- 가능한 값: 'active', 'inventory', 'deleted_not_purchased', 'changed'
-- revision_status 설명:
-- 'active': 정상 활성 자재 (기본값)
-- 'inventory': 재고품 (구매신청 후 리비전에서 삭제됨 - 연노랑색 표시)
-- 'deleted_not_purchased': 구매신청 전 삭제됨 (숨김 처리)
-- 'changed': 변경된 자재 (추가 구매 필요)
-- 인덱스 추가 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_materials_revision_status ON materials(revision_status);
CREATE INDEX IF NOT EXISTS idx_materials_drawing_name ON materials(drawing_name);
CREATE INDEX IF NOT EXISTS idx_materials_line_no ON materials(line_no);
COMMENT ON COLUMN materials.revision_status IS '리비전 자재 상태: active(활성), inventory(재고품), deleted_not_purchased(삭제됨), changed(변경됨)';

View File

@@ -0,0 +1,237 @@
-- ================================
-- TK-MP-Project 메인 서버 배포용 마이그레이션
-- 생성일: 2025.09.28
-- 목적: 개발 중 추가된 필수 컬럼들을 메인 서버에 적용
-- ================================
-- 1. materials 테이블 필수 컬럼 추가
-- ================================
-- 파이프 사이즈 정보
ALTER TABLE materials ADD COLUMN IF NOT EXISTS main_nom VARCHAR(50);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS red_nom VARCHAR(50);
-- 전체 재질명
ALTER TABLE materials ADD COLUMN IF NOT EXISTS full_material_grade TEXT;
-- 업로드 시 행 번호 추적
ALTER TABLE materials ADD COLUMN IF NOT EXISTS row_number INTEGER;
-- 해시값 (구매 추적용)
ALTER TABLE materials ADD COLUMN IF NOT EXISTS material_hash VARCHAR(64);
-- 검증 정보
ALTER TABLE materials ADD COLUMN IF NOT EXISTS verified_by VARCHAR(100);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS verified_at TIMESTAMP;
-- 분류 상세 정보 (이미 있을 수 있지만 확인)
ALTER TABLE materials ADD COLUMN IF NOT EXISTS classified_subcategory VARCHAR(100);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS schedule VARCHAR(20);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS drawing_name VARCHAR(100);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS area_code VARCHAR(20);
ALTER TABLE materials ADD COLUMN IF NOT EXISTS line_no VARCHAR(50);
-- 2. files 테이블 필수 컬럼 추가
-- ================================
-- 프로젝트 연결 정보
ALTER TABLE files ADD COLUMN IF NOT EXISTS job_no VARCHAR(50);
ALTER TABLE files ADD COLUMN IF NOT EXISTS bom_name VARCHAR(255);
ALTER TABLE files ADD COLUMN IF NOT EXISTS description TEXT;
ALTER TABLE files ADD COLUMN IF NOT EXISTS parsed_count INTEGER DEFAULT 0;
-- 3. material_purchase_tracking 테이블 컬럼 추가
-- ================================
-- 구매 상태 및 설명
ALTER TABLE material_purchase_tracking
ADD COLUMN IF NOT EXISTS purchase_status VARCHAR(20) DEFAULT 'pending',
ADD COLUMN IF NOT EXISTS description TEXT;
-- 4. user_requirements 테이블 컬럼 추가
-- ================================
-- 자재별 요구사항 연결
ALTER TABLE user_requirements ADD COLUMN IF NOT EXISTS material_id INTEGER;
-- 5. 성능 최적화 인덱스 추가
-- ================================
-- materials 테이블 인덱스
CREATE INDEX IF NOT EXISTS idx_materials_main_nom ON materials(main_nom);
CREATE INDEX IF NOT EXISTS idx_materials_red_nom ON materials(red_nom);
CREATE INDEX IF NOT EXISTS idx_materials_main_red_nom ON materials(main_nom, red_nom);
CREATE INDEX IF NOT EXISTS idx_materials_full_material_grade ON materials(full_material_grade);
CREATE INDEX IF NOT EXISTS idx_materials_material_hash ON materials(material_hash);
CREATE INDEX IF NOT EXISTS idx_materials_verified_by ON materials(verified_by);
CREATE INDEX IF NOT EXISTS idx_materials_classified_subcategory ON materials(classified_subcategory);
CREATE INDEX IF NOT EXISTS idx_materials_schedule ON materials(schedule);
-- files 테이블 인덱스
CREATE INDEX IF NOT EXISTS idx_files_job_no ON files(job_no);
-- user_requirements 테이블 인덱스
CREATE INDEX IF NOT EXISTS idx_user_requirements_material_id ON user_requirements(material_id);
-- fitting_details 테이블 분리 스케줄 컬럼 추가
ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS main_schedule VARCHAR(20);
ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS red_schedule VARCHAR(20);
ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS has_different_schedules BOOLEAN DEFAULT FALSE;
-- fitting_details 분리 스케줄 인덱스
CREATE INDEX IF NOT EXISTS idx_fitting_details_main_schedule ON fitting_details(main_schedule);
CREATE INDEX IF NOT EXISTS idx_fitting_details_red_schedule ON fitting_details(red_schedule);
-- 3. 컬럼 설명 추가
-- ================================
COMMENT ON COLUMN materials.main_nom IS 'MAIN_NOM 필드 - 주 사이즈 (예: 4", 150A)';
COMMENT ON COLUMN materials.red_nom IS 'RED_NOM 필드 - 축소 사이즈 (Reducing 피팅/플랜지용)';
COMMENT ON COLUMN materials.full_material_grade IS '전체 재질명 (예: ASTM A312 TP304, ASTM A106 GR B 등)';
COMMENT ON COLUMN materials.row_number IS '업로드 파일에서의 행 번호 (디버깅용)';
-- 6. support_details 테이블 생성 (SUPPORT 카테고리용)
-- ================================
CREATE TABLE IF NOT EXISTS support_details (
id SERIAL PRIMARY KEY,
material_id INTEGER NOT NULL REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
-- 서포트 타입 정보
support_type VARCHAR(50), -- URETHANE_BLOCK, CLAMP, HANGER, SPRING_HANGER 등
support_subtype VARCHAR(100), -- 상세 타입
-- 하중 정보
load_rating VARCHAR(20), -- LIGHT, MEDIUM, HEAVY, CUSTOM
load_capacity VARCHAR(20), -- 40T, 50TON 등
-- 재질 정보
material_standard VARCHAR(50), -- 재질 표준
material_grade VARCHAR(100), -- 재질 등급
-- 사이즈 정보
pipe_size VARCHAR(20), -- 지지하는 파이프 크기
length_mm DECIMAL(10,2), -- 길이 (mm)
width_mm DECIMAL(10,2), -- 폭 (mm)
height_mm DECIMAL(10,2), -- 높이 (mm)
-- 분류 신뢰도
classification_confidence DECIMAL(3,2), -- 0.00-1.00
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- support_details 인덱스
CREATE INDEX IF NOT EXISTS idx_support_details_material_id ON support_details(material_id);
CREATE INDEX IF NOT EXISTS idx_support_details_file_id ON support_details(file_id);
CREATE INDEX IF NOT EXISTS idx_support_details_support_type ON support_details(support_type);
-- 8. SPECIAL 카테고리 지원 추가
-- ================================
-- SPECIAL 카테고리 관련 인덱스 추가 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_materials_special_category
ON materials(classified_category)
WHERE classified_category = 'SPECIAL';
-- SPECIAL 키워드 패턴 테이블
CREATE TABLE IF NOT EXISTS special_classification_patterns (
id SERIAL PRIMARY KEY,
pattern_type VARCHAR(20) NOT NULL, -- 'KEYWORD', 'REGEX', 'EXACT'
pattern_value VARCHAR(200) NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1, -- 우선순위 (낮을수록 높은 우선순위)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 기본 SPECIAL 키워드 패턴 삽입
INSERT INTO special_classification_patterns (pattern_type, pattern_value, description, priority) VALUES
('KEYWORD', 'SPECIAL', '영문 SPECIAL 키워드', 1),
('KEYWORD', '스페셜', '한글 스페셜 키워드', 1),
('KEYWORD', 'SPEC', '영문 SPEC 축약어', 2),
('KEYWORD', 'SPL', '영문 SPL 축약어', 2)
ON CONFLICT DO NOTHING;
-- SPECIAL 자재 상세 정보 테이블 (도면 업로드 관련)
CREATE TABLE IF NOT EXISTS special_material_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 도면 정보
drawing_number VARCHAR(100), -- 도면 번호
drawing_revision VARCHAR(20), -- 도면 리비전
drawing_uploaded BOOLEAN DEFAULT FALSE, -- 도면 업로드 여부
drawing_file_path TEXT, -- 도면 파일 경로
-- 특수 요구사항
special_requirements TEXT, -- 특수 제작 요구사항
manufacturing_notes TEXT, -- 제작 참고사항
approval_required BOOLEAN DEFAULT TRUE, -- 승인 필요 여부
approved_by VARCHAR(100), -- 승인자
approved_at TIMESTAMP, -- 승인 일시
-- 분류 정보
classification_confidence FLOAT DEFAULT 1.0,
classification_reason TEXT, -- 분류 근거
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- SPECIAL 관련 인덱스
CREATE INDEX IF NOT EXISTS idx_special_patterns_type ON special_classification_patterns(pattern_type);
CREATE INDEX IF NOT EXISTS idx_special_patterns_active ON special_classification_patterns(is_active);
CREATE INDEX IF NOT EXISTS idx_special_details_material ON special_material_details(material_id);
CREATE INDEX IF NOT EXISTS idx_special_details_drawing_uploaded ON special_material_details(drawing_uploaded);
-- 기존 자재 중 SPECIAL 키워드가 포함된 자재를 SPECIAL 카테고리로 재분류
UPDATE materials
SET
classified_category = 'SPECIAL',
classification_confidence = 1.0,
classified_at = CURRENT_TIMESTAMP
WHERE
(
UPPER(original_description) LIKE '%SPECIAL%' OR
UPPER(original_description) LIKE '%스페셜%' OR
UPPER(original_description) LIKE '%SPEC%' OR
UPPER(original_description) LIKE '%SPL%'
)
AND (classified_category IS NULL OR classified_category != 'SPECIAL');
-- 7. 기존 데이터 정리 (선택사항)
-- ================================
-- 기존 데이터에 기본값 설정 (필요시 주석 해제)
-- UPDATE materials SET main_nom = '', red_nom = '', full_material_grade = ''
-- WHERE main_nom IS NULL OR red_nom IS NULL OR full_material_grade IS NULL;
-- ================================
-- 마이그레이션 완료 확인
-- ================================
DO $$
BEGIN
-- 컬럼 존재 확인
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'materials'
AND column_name IN ('main_nom', 'red_nom', 'full_material_grade', 'row_number')
GROUP BY table_name
HAVING COUNT(*) = 4
) THEN
RAISE NOTICE '✅ TK-MP-Project 메인 서버 마이그레이션 완료!';
RAISE NOTICE '📋 추가된 컬럼: main_nom, red_nom, full_material_grade, row_number';
RAISE NOTICE '🔍 추가된 인덱스: 4개 (성능 최적화)';
RAISE NOTICE '🚀 파일 업로드 기능 정상 작동 가능';
ELSE
RAISE NOTICE '❌ 마이그레이션 실패 - 일부 컬럼이 생성되지 않았습니다.';
END IF;
END $$;

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
TK-MP-Project 자동 DB 마이그레이션 스크립트
배포 시 백엔드 시작 전에 자동으로 실행되어 DB 스키마를 동기화
"""
import sys
import os
import time
from datetime import datetime
# 백엔드 모듈 경로 추가
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
def wait_for_db():
"""데이터베이스 연결 대기"""
max_retries = 30
retry_count = 0
while retry_count < max_retries:
try:
from app.database import engine
# 간단한 연결 테스트
with engine.connect() as conn:
conn.execute("SELECT 1")
print("✅ 데이터베이스 연결 성공")
return True
except Exception as e:
retry_count += 1
print(f"⏳ 데이터베이스 연결 대기 중... ({retry_count}/{max_retries})")
time.sleep(2)
print("❌ 데이터베이스 연결 실패")
return False
def sync_database_schema():
"""데이터베이스 스키마 동기화"""
try:
from app.models import Base
from app.database import engine
print("🔄 데이터베이스 스키마 동기화 중...")
# 모든 테이블 생성/업데이트
Base.metadata.create_all(bind=engine)
print("✅ 데이터베이스 스키마 동기화 완료")
return True
except Exception as e:
print(f"❌ 스키마 동기화 실패: {str(e)}")
return False
def ensure_required_data():
"""필수 데이터 확인 및 생성"""
try:
from app.database import get_db
from app.auth.models import User
from sqlalchemy.orm import Session
print("🔄 필수 데이터 확인 중...")
db = next(get_db())
# 관리자 계정 확인
admin_user = db.query(User).filter(User.username == 'admin').first()
if not admin_user:
print("📝 기본 관리자 계정 생성 중...")
admin_user = User(
username='admin',
password='$2b$12$ld4LDOW5mxkiRQEkXfMUIep/aIzFleQZ4yoL10ZQkUxGqnkYuhNMW', # admin123
name='시스템 관리자',
email='admin@tkmp.com',
role='admin',
access_level='admin',
department='IT',
position='시스템 관리자',
status='active'
)
db.add(admin_user)
db.commit()
print("✅ 기본 관리자 계정 생성 완료")
db.close()
print("✅ 필수 데이터 확인 완료")
return True
except Exception as e:
print(f"❌ 필수 데이터 확인 실패: {str(e)}")
return False
def main():
"""메인 실행 함수"""
print("🚀 TK-MP-Project 자동 DB 마이그레이션 시작")
print(f"⏰ 시작 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 1. 데이터베이스 연결 대기
if not wait_for_db():
sys.exit(1)
# 2. 스키마 동기화
if not sync_database_schema():
sys.exit(1)
# 3. 필수 데이터 확인
if not ensure_required_data():
sys.exit(1)
print("🎉 자동 DB 마이그레이션 완료!")
print(f"⏰ 완료 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS jobs (
job_no VARCHAR(50) PRIMARY KEY,
job_name VARCHAR(200) NOT NULL,
client_name VARCHAR(100) NOT NULL,
end_user VARCHAR(100),
epc_company VARCHAR(100),
project_site VARCHAR(200),
contract_date DATE,
delivery_date DATE,
delivery_terms VARCHAR(100),
status VARCHAR(20) DEFAULT '진행중',
delivery_completed_date DATE,
project_closed_date DATE,
description TEXT,
created_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT true
);

View File

@@ -0,0 +1,237 @@
-- 1. FITTING 상세 테이블
CREATE TABLE IF NOT EXISTS fitting_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 피팅 타입 정보
fitting_type VARCHAR(50),
fitting_subtype VARCHAR(50),
-- 연결 방식
connection_method VARCHAR(50),
connection_code VARCHAR(50),
-- 압력 등급
pressure_rating VARCHAR(50),
max_pressure VARCHAR(50),
-- 제작 방법
manufacturing_method VARCHAR(50),
-- 재질 정보
material_standard VARCHAR(100),
material_grade VARCHAR(100),
material_type VARCHAR(50),
-- 사이즈 정보
main_size VARCHAR(50),
reduced_size VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. VALVE 상세 테이블
CREATE TABLE IF NOT EXISTS valve_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 밸브 타입 정보
valve_type VARCHAR(50),
valve_subtype VARCHAR(50),
actuator_type VARCHAR(50),
-- 연결 방식
connection_method VARCHAR(50),
-- 압력 등급
pressure_rating VARCHAR(50),
pressure_class VARCHAR(50),
-- 재질 정보
body_material VARCHAR(100),
trim_material VARCHAR(100),
-- 사이즈 정보
size_inches VARCHAR(50),
-- 특수 사양
fire_safe BOOLEAN,
low_temp_service BOOLEAN,
special_features JSONB,
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. FLANGE 상세 테이블
CREATE TABLE IF NOT EXISTS flange_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 플랜지 타입
flange_type VARCHAR(50),
facing_type VARCHAR(50),
-- 압력 등급
pressure_rating VARCHAR(50),
-- 재질 정보
material_standard VARCHAR(100),
material_grade VARCHAR(100),
-- 사이즈 정보
size_inches VARCHAR(50),
bolt_hole_count INTEGER,
bolt_hole_size VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. BOLT 상세 테이블
CREATE TABLE IF NOT EXISTS bolt_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 볼트 타입
bolt_type VARCHAR(50),
thread_type VARCHAR(50),
-- 사양 정보
diameter VARCHAR(50),
length VARCHAR(50),
-- 재질 정보
material_standard VARCHAR(100),
material_grade VARCHAR(100),
coating_type VARCHAR(100),
-- 너트/와셔 정보
includes_nut BOOLEAN,
includes_washer BOOLEAN,
nut_type VARCHAR(50),
washer_type VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. GASKET 상세 테이블
CREATE TABLE IF NOT EXISTS gasket_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 가스켓 타입
gasket_type VARCHAR(50),
gasket_subtype VARCHAR(50),
-- 재질 정보
material_type VARCHAR(100),
filler_material VARCHAR(100),
-- 사이즈 및 등급
size_inches VARCHAR(50),
pressure_rating VARCHAR(50),
thickness VARCHAR(50),
-- 특수 사양
temperature_range VARCHAR(100),
fire_safe BOOLEAN,
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6. INSTRUMENT 상세 테이블
CREATE TABLE IF NOT EXISTS instrument_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 계장품 타입
instrument_type VARCHAR(50),
instrument_subtype VARCHAR(50),
-- 측정 사양
measurement_type VARCHAR(50),
measurement_range VARCHAR(100),
accuracy VARCHAR(50),
-- 연결 정보
connection_type VARCHAR(50),
connection_size VARCHAR(50),
-- 재질 정보
body_material VARCHAR(100),
wetted_parts_material VARCHAR(100),
-- 전기 사양
electrical_rating VARCHAR(100),
output_signal VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스 생성
CREATE INDEX idx_fitting_details_material_id ON fitting_details(material_id);
CREATE INDEX idx_fitting_details_file_id ON fitting_details(file_id);
CREATE INDEX idx_fitting_details_type ON fitting_details(fitting_type);
CREATE INDEX idx_valve_details_material_id ON valve_details(material_id);
CREATE INDEX idx_valve_details_file_id ON valve_details(file_id);
CREATE INDEX idx_valve_details_type ON valve_details(valve_type);
CREATE INDEX idx_flange_details_material_id ON flange_details(material_id);
CREATE INDEX idx_flange_details_file_id ON flange_details(file_id);
CREATE INDEX idx_bolt_details_material_id ON bolt_details(material_id);
CREATE INDEX idx_bolt_details_file_id ON bolt_details(file_id);
CREATE INDEX idx_gasket_details_material_id ON gasket_details(material_id);
CREATE INDEX idx_gasket_details_file_id ON gasket_details(file_id);
CREATE INDEX idx_instrument_details_material_id ON instrument_details(material_id);
CREATE INDEX idx_instrument_details_file_id ON instrument_details(file_id);

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
TK-MP-Project 완전한 DB 스키마 생성 스크립트
백엔드 SQLAlchemy 모델을 기반으로 PostgreSQL 스키마를 자동 생성
"""
import sys
import os
from datetime import datetime
# 백엔드 모듈 경로 추가
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from sqlalchemy import create_engine, MetaData
from sqlalchemy.schema import CreateTable, CreateIndex
from app.models import Base
from app.auth.models import User, LoginLog, UserSession, Permission, RolePermission
def generate_schema_sql():
"""SQLAlchemy 모델을 기반으로 완전한 PostgreSQL 스키마 생성"""
# 메모리 내 SQLite 엔진 생성 (스키마 생성용)
engine = create_engine('sqlite:///:memory:', echo=False)
# 모든 테이블 생성
Base.metadata.create_all(engine)
# PostgreSQL 방언으로 변환
from sqlalchemy.dialects import postgresql
schema_lines = []
schema_lines.append("-- ================================")
schema_lines.append("-- TK-MP-Project 완전한 데이터베이스 스키마")
schema_lines.append(f"-- 자동 생성일: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
schema_lines.append("-- SQLAlchemy 모델 기반 자동 생성")
schema_lines.append("-- ================================")
schema_lines.append("")
# 테이블 생성 SQL 생성
for table in Base.metadata.sorted_tables:
create_table_sql = str(CreateTable(table).compile(dialect=postgresql.dialect()))
# SQLite -> PostgreSQL 변환
create_table_sql = create_table_sql.replace('DATETIME', 'TIMESTAMP')
create_table_sql = create_table_sql.replace('BOOLEAN', 'BOOLEAN')
create_table_sql = create_table_sql.replace('TEXT', 'TEXT')
create_table_sql = create_table_sql.replace('JSON', 'JSONB')
schema_lines.append(f"-- {table.name} 테이블")
schema_lines.append(f"CREATE TABLE IF NOT EXISTS {create_table_sql[13:]};") # "CREATE TABLE " 제거
schema_lines.append("")
# 인덱스 생성
schema_lines.append("-- ================================")
schema_lines.append("-- 인덱스 생성")
schema_lines.append("-- ================================")
schema_lines.append("")
for table in Base.metadata.sorted_tables:
for index in table.indexes:
create_index_sql = str(CreateIndex(index).compile(dialect=postgresql.dialect()))
schema_lines.append(f"CREATE INDEX IF NOT EXISTS {create_index_sql[13:]};") # "CREATE INDEX " 제거
# 필수 데이터 삽입
schema_lines.append("")
schema_lines.append("-- ================================")
schema_lines.append("-- 필수 기본 데이터 삽입")
schema_lines.append("-- ================================")
schema_lines.append("")
# 기본 관리자 계정
schema_lines.append("-- 기본 관리자 계정 (비밀번호: admin123)")
schema_lines.append("INSERT INTO users (username, password, name, email, role, access_level, department, position, status) VALUES")
schema_lines.append("('admin', '$2b$12$ld4LDOW5mxkiRQEkXfMUIep/aIzFleQZ4yoL10ZQkUxGqnkYuhNMW', '시스템 관리자', 'admin@tkmp.com', 'admin', 'admin', 'IT', '시스템 관리자', 'active'),")
schema_lines.append("('system', '$2b$12$ld4LDOW5mxkiRQEkXfMUIep/aIzFleQZ4yoL10ZQkUxGqnkYuhNMW', '시스템 계정', 'system@tkmp.com', 'system', 'system', 'IT', '시스템 계정', 'active')")
schema_lines.append("ON CONFLICT (username) DO NOTHING;")
schema_lines.append("")
# 기본 권한 데이터
schema_lines.append("-- 기본 권한 데이터")
permissions = [
('bom.view', 'BOM 조회 권한', 'bom'),
('bom.create', 'BOM 생성 권한', 'bom'),
('bom.edit', 'BOM 수정 권한', 'bom'),
('bom.delete', 'BOM 삭제 권한', 'bom'),
('project.view', '프로젝트 조회 권한', 'project'),
('project.create', '프로젝트 생성 권한', 'project'),
('project.edit', '프로젝트 수정 권한', 'project'),
('file.upload', '파일 업로드 권한', 'file'),
('file.download', '파일 다운로드 권한', 'file'),
('user.view', '사용자 조회 권한', 'user'),
('user.create', '사용자 생성 권한', 'user'),
('system.admin', '시스템 관리 권한', 'system')
]
schema_lines.append("INSERT INTO permissions (permission_name, description, module) VALUES")
for i, (name, desc, module) in enumerate(permissions):
comma = "," if i < len(permissions) - 1 else ""
schema_lines.append(f"('{name}', '{desc}', '{module}'){comma}")
schema_lines.append("ON CONFLICT (permission_name) DO NOTHING;")
schema_lines.append("")
# 완료 메시지
schema_lines.append("-- ================================")
schema_lines.append("-- 스키마 생성 완료")
schema_lines.append("-- ================================")
schema_lines.append("")
schema_lines.append("DO $$")
schema_lines.append("BEGIN")
schema_lines.append(" RAISE NOTICE '✅ TK-MP-Project 완전한 데이터베이스 스키마가 성공적으로 생성되었습니다!';")
schema_lines.append(" RAISE NOTICE '👤 기본 계정: admin/admin123, system/admin123';")
schema_lines.append(" RAISE NOTICE '🔐 권한 시스템: 모듈별 세분화된 권한 적용';")
schema_lines.append(" RAISE NOTICE '📊 자동 생성: SQLAlchemy 모델 기반 완전 동기화';")
schema_lines.append("END $$;")
return '\n'.join(schema_lines)
def main():
"""메인 실행 함수"""
try:
print("🔄 SQLAlchemy 모델 분석 중...")
schema_sql = generate_schema_sql()
# 스키마 파일 저장
output_file = os.path.join(os.path.dirname(__file__), '..', '..', 'database', 'init', '00_auto_generated_schema.sql')
os.makedirs(os.path.dirname(output_file), exist_ok=True)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(schema_sql)
print(f"✅ 완전한 DB 스키마가 생성되었습니다: {output_file}")
print("📋 포함된 내용:")
print(" - 모든 SQLAlchemy 모델 기반 테이블")
print(" - 필수 인덱스")
print(" - 기본 관리자 계정")
print(" - 기본 권한 데이터")
print("🚀 배포 시 이 파일이 자동으로 실행됩니다.")
except Exception as e:
print(f"❌ 스키마 생성 실패: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
전체 데이터베이스 설정 및 더미 데이터 생성
"""
import sys
import os
from sqlalchemy import create_engine, text
# 프로젝트 루트를 Python path에 추가
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def setup_database():
"""데이터베이스 전체 설정"""
try:
from app.database import engine
print("✅ 기존 데이터베이스 연결 사용")
except ImportError:
# 개발용 직접 연결
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL)
print("⚠️ 개발용 SQLite 연결")
try:
with engine.connect() as conn:
print("🏗️ 1단계: jobs 테이블 생성...")
# jobs 테이블 생성 SQL 실행
with open('scripts/01_create_jobs_table.sql', 'r', encoding='utf-8') as f:
sql_commands = f.read().split(';')
for command in sql_commands:
command = command.strip()
if command:
conn.execute(text(command))
print("✅ jobs 테이블 생성 완료")
print("🔧 2단계: files 테이블 수정...")
# files 테이블 수정 (선택적)
try:
with open('scripts/02_modify_files_table.sql', 'r', encoding='utf-8') as f:
sql_commands = f.read().split(';')
for command in sql_commands:
command = command.strip()
if command and not command.startswith('--'):
conn.execute(text(command))
print("✅ files 테이블 수정 완료")
except Exception as e:
print(f"⚠️ files 테이블 수정 건너뜀: {e}")
# 커밋
conn.commit()
print("🎯 3단계: 더미 데이터 생성...")
# 더미 데이터 스크립트 실행
exec(open('scripts/03_insert_dummy_data.py').read())
print("\n🎉 전체 설정 완료!")
print("\n📋 다음 단계:")
print(" 1. API 서버 실행")
print(" 2. GET /jobs 엔드포인트 테스트")
print(" 3. Job 선택 후 BOM 파일 업로드")
return True
except Exception as e:
print(f"❌ 데이터베이스 설정 실패: {e}")
return False
if __name__ == "__main__":
setup_database()