-- ============================================================================= -- MariaDB 데이터베이스 스키마 생성 SQL (오류 수정 완) -- Aislo (아이슬로) — AI + Slotti 산림 도로 설계 자동화 시스템 -- -- DB 명: aislo_db -- DBMS: MariaDB 10.6+ -- 실행 순서: -- 1. DATABASE 생성 (aislo_db) -- 2. CREATE TABLE (각 테이블) -- 3. CREATE INDEX (인덱싱) -- 4. ALTER TABLE ADD CONSTRAINT (외래키) -- ============================================================================= -- 0. 데이터베이스 생성 -- ============================================================================= CREATE DATABASE IF NOT EXISTS aislo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE aislo_db; -- 1. 테이블 생성 (순서 중요: FK 참조 테이블이 먼저 생성되어야 함) -- ============================================================================= /* --------- 1-1. 사용자 & 인증 그룹 --------- */ -- users: 시스템 사용자 CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, position VARCHAR(100), -- 직급 (과장, 대리, 사원) department VARCHAR(100), -- 부서명 (설계팀, 영업팀) phone VARCHAR(20), -- 연락처 (010-1234-5678) company_id INT, -- 소속 회사 (FK는 later) role ENUM('SYSTEM_ADMIN', 'MASTER', 'MEMBER') NOT NULL DEFAULT 'MEMBER', is_master BOOLEAN NOT NULL DEFAULT FALSE, status ENUM('PENDING_EMAIL', 'PENDING', 'ACTIVE', 'INACTIVE', 'REJECTED') NOT NULL DEFAULT 'PENDING_EMAIL', last_email_verified_at TIMESTAMP NULL, login_failures INT NOT NULL DEFAULT 0, last_failed_at TIMESTAMP NULL, account_locked_until TIMESTAMP NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL DEFAULT NULL -- 소프트 삭제 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- companies: 회사 정보 CREATE TABLE IF NOT EXISTS companies ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, business_registration_number VARCHAR(20) UNIQUE, -- 사업자등록번호 business_address VARCHAR(255), -- 사업장 주소 business_owner VARCHAR(100), -- 사업주 이름 business_status VARCHAR(50), -- 기업 상태 (활동중, 폐업, 휴업) created_by INT, -- 생성자 (FK는 later) company_code VARCHAR(20) NOT NULL UNIQUE, phone_number VARCHAR(20), representative_name VARCHAR(100), master_user_id INT NULL, status ENUM('ACTIVE', 'INACTIVE', 'SUSPENDED') NOT NULL DEFAULT 'ACTIVE', subscription_status ENUM('FREE', 'TRIAL', 'PAID', 'EXPIRED') NOT NULL DEFAULT 'FREE', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- sessions: Secure HttpOnly 쿠키의 서버 측 세션 저장소 CREATE TABLE IF NOT EXISTS sessions ( id VARCHAR(255) PRIMARY KEY, user_id INT NOT NULL, user_agent_hash CHAR(64) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, last_activity_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_sessions_user_id (user_id), INDEX idx_sessions_expires_at (expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- email_otps: 목적별 일회용 이메일 인증 코드 CREATE TABLE IF NOT EXISTS email_otps ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, purpose ENUM('REGISTER', 'LOGIN', 'REVERIFY', 'PASSWORD_CHANGE') NOT NULL, otp_hash VARCHAR(255) NOT NULL, expires_at TIMESTAMP NOT NULL, consumed_at TIMESTAMP NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_email_otps_user_purpose (user_id, purpose, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS trusted_devices ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, user_agent_hash CHAR(64) NOT NULL, verified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_trusted_devices_user_agent (user_id, user_agent_hash) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS join_requests ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, company_id INT NOT NULL, status ENUM('PENDING', 'APPROVED', 'REJECTED') NOT NULL DEFAULT 'PENDING', requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, reviewed_at TIMESTAMP NULL, reviewed_by INT NULL, review_comment TEXT, INDEX idx_join_requests_company_status (company_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS login_logs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NULL, email VARCHAR(255) NOT NULL, status ENUM('SUCCESS', 'FAILURE') NOT NULL, failure_reason VARCHAR(100), user_agent TEXT, login_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_login_logs_user_id (user_id), INDEX idx_login_logs_login_at (login_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS activity_logs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, action_type VARCHAR(100) NOT NULL, action_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, resource_id VARCHAR(255), status ENUM('SUCCESS', 'FAILURE') NOT NULL DEFAULT 'SUCCESS', details JSON, INDEX idx_activity_logs_user_at (user_id, action_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS system_admin_logs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, admin_user_id INT NOT NULL, action_type VARCHAR(100) NOT NULL, target_id VARCHAR(255), action_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, details JSON, INDEX idx_system_admin_logs_admin_at (admin_user_id, action_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS support_requests ( id INT AUTO_INCREMENT PRIMARY KEY, company_id INT NULL, user_email VARCHAR(255) NOT NULL, subject VARCHAR(255) NOT NULL, message TEXT NOT NULL, status ENUM('NEW', 'REVIEWED', 'RESOLVED', 'DELETED') NOT NULL DEFAULT 'NEW', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, reviewed_at TIMESTAMP NULL, reviewed_by INT NULL, review_notes TEXT, INDEX idx_support_requests_status_at (status, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS user_consents ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, terms_version VARCHAR(20) NOT NULL, terms_agreed BOOLEAN NOT NULL, privacy_agreed BOOLEAN NOT NULL, marketing_agreed BOOLEAN NOT NULL DEFAULT FALSE, agreed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_user_consents_version (user_id, terms_version) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-2. 프로젝트 그룹 --------- */ -- projects: 설계 프로젝트 CREATE TABLE IF NOT EXISTS projects ( id CHAR(36) PRIMARY KEY COMMENT 'UUID', user_id INT NOT NULL, -- 프로젝트 소유자 (FK는 later) company_id INT NOT NULL, -- 소속 회사 (FK는 later) name VARCHAR(255) NOT NULL, region VARCHAR(100), -- 지역명 (울진군 금강송면) road_type VARCHAR(100), -- 임도 종류 (간선임도, 지선임도, 산불진화임도, 계류보전) project_year INT, -- 사업 연도 estimated_length_m FLOAT, -- 추정 연장 (m) memo TEXT, status VARCHAR(50) DEFAULT 'NEW', -- NEW, FILE_UPLOADED, WF1_ANALYZING, ..., DONE crs_epsg INT DEFAULT 5178, -- 좌표계 (한국 표준) created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL DEFAULT NULL, storage_path VARCHAR(500) -- storage/회사/사용자/project_uuid ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- project_versions: 프로젝트 버전 이력 CREATE TABLE IF NOT EXISTS project_versions ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later version_num INT NOT NULL, snapshot_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, status VARCHAR(50), data JSON, -- 설계 데이터 스냅샷 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-3. 파일 & 입력 데이터 그룹 --------- */ -- input_files: 사용자 업로드 원본 파일 CREATE TABLE IF NOT EXISTS input_files ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later file_type VARCHAR(50), -- las, tif, tfw, prj, dxf, dwg, other original_filename VARCHAR(255) NOT NULL, raw_file_path VARCHAR(500) NOT NULL, -- storage/.../B03_FileInput/input/... file_size_mb FLOAT, upload_by INT, -- FK는 later upload_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, crs_epsg INT, metadata JSON, -- resolution_m, data_range, ... status VARCHAR(50) DEFAULT 'UPLOADED' -- UPLOADED, PROCESSED, ARCHIVED ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- processed_point_cloud: 변환된 포인트클라우드 데이터 CREATE TABLE IF NOT EXISTS processed_point_cloud ( id INT AUTO_INCREMENT PRIMARY KEY, input_file_id INT NOT NULL, -- FK는 later project_id CHAR(36) NOT NULL, -- FK는 later process_type VARCHAR(50), -- filtered, sampled, classified processed_file_path VARCHAR(500), -- storage/.../B04_wf1_Surface/processed/... converted_format VARCHAR(50), -- ply, laz, las 등 converted_file_path VARCHAR(500), -- storage/.../B04_wf1_Surface/processed/... point_count INT, min_z FLOAT, max_z FLOAT, mean_z FLOAT, x_min FLOAT, x_max FLOAT, y_min FLOAT, y_max FLOAT, density_per_sqm FLOAT, classification_summary JSON, -- ground, vegetation, building, ... processing_params JSON, processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, status VARCHAR(50) DEFAULT 'PROCESSING' -- PROCESSING, COMPLETE, FAILED ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-4. 지표면 & 분석 결과 그룹 --------- */ -- surface_models: WF1 지표면 분석 결과 CREATE TABLE IF NOT EXISTS surface_models ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later model_type VARCHAR(50), -- dem_grid, tin, mesh_triangulated, contour_lines source_file_id INT, -- FK는 later processed_cloud_id INT, -- FK는 later status VARCHAR(50) DEFAULT 'PROCESSING', -- PROCESSING, COMPLETE, FAILED crs_epsg INT, resolution_m FLOAT, model_file_path VARCHAR(500), -- storage/.../B04_wf1_Surface/models/... generation_params JSON, -- filter_type, algorithm, params created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMP NULL DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- terrain_layers: WF1 지형 레이어 CREATE TABLE IF NOT EXISTS terrain_layers ( id INT AUTO_INCREMENT PRIMARY KEY, surface_model_id INT NOT NULL, -- FK는 later layer_name VARCHAR(100), -- 지표, 제1층, 제2층 geometry_type VARCHAR(50), -- POINTCLOUD, GRID, MESH, CONTOUR layer_file_path VARCHAR(500), -- storage/.../B04_wf1_Surface/models/... file_format VARCHAR(50), -- geojson, geotiff, ply, obj file_size_mb FLOAT, statistics JSON, -- min_z, max_z, mean_slope, point_count created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-5. 경로 & 설계 그룹 --------- */ -- routes: WF2 경로 설계 결과 CREATE TABLE IF NOT EXISTS routes ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later surface_model_id INT, -- FK는 later status VARCHAR(50) DEFAULT 'DRAFT', -- DRAFT, CONFIRMED, ARCHIVED start_chainage_m FLOAT, end_chainage_m FLOAT, total_length_m FLOAT, grade_percent JSON, -- [2.5, 1.8, 3.2, ...] 각 구간 경사도 constraints JSON, -- max_grade, min_radius, avoidance_zones algorithm_params JSON, -- cost_weights, ... route_data_path VARCHAR(500), -- storage/.../B05_wf2_Route/route/... computed_at TIMESTAMP NULL DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- route_points: 웹 렌더링용 경로 포인트 샘플 CREATE TABLE IF NOT EXISTS route_points ( id INT AUTO_INCREMENT PRIMARY KEY, route_id INT NOT NULL, -- FK는 later chainage_m FLOAT, elevation_m FLOAT, slope_percent FLOAT, sequence_num INT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- route_statistics: 경로 통계 CREATE TABLE IF NOT EXISTS route_statistics ( id INT AUTO_INCREMENT PRIMARY KEY, route_id INT NOT NULL, -- FK는 later min_slope FLOAT, max_slope FLOAT, mean_slope FLOAT, cut_volume_m3 FLOAT, fill_volume_m3 FLOAT, tree_cutting_volume FLOAT, cost_score FLOAT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-6. 종단면 & 횡단면 그룹 --------- */ -- longitudinal_sections: WF3 종단면 CREATE TABLE IF NOT EXISTS longitudinal_sections ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later route_id INT NOT NULL, -- FK는 later computed_at TIMESTAMP NULL DEFAULT NULL, data JSON, -- chainages, elevations, grades, design_elevations longitudinal_file_path VARCHAR(500), -- storage/.../B06_wf3_ProfileCross/longitudinal/... status VARCHAR(50) DEFAULT 'DRAFT' -- DRAFT, CONFIRMED ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- cross_sections: WF3 횡단면 CREATE TABLE IF NOT EXISTS cross_sections ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later route_id INT NOT NULL, -- FK는 later chainage_m FLOAT, sequence_num INT, data JSON, -- left_slope, right_slope, width_m, cut_volume_m3, fill_volume_m3, structures, notes cross_section_file_path VARCHAR(500), -- storage/.../B06_wf3_ProfileCross/cross_sections/... status VARCHAR(50) DEFAULT 'DRAFT' -- DRAFT, CONFIRMED ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-7. 설계 & 구조물 그룹 --------- */ -- structures: WF4 상세 설계 - 구조물 CREATE TABLE IF NOT EXISTS structures ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later cross_section_id INT, -- FK는 later structure_type VARCHAR(100), -- 낙석방지책, 돌붙임, 계간수로, 낙차공 chainage_m FLOAT, location VARCHAR(50), -- LEFT, CENTER, RIGHT length_m FLOAT, width_m FLOAT, height_m FLOAT, material VARCHAR(100), -- 강재, 콘크리트, 목재, 돌 quantity INT, unit_price FLOAT, design_notes JSON, structure_data_path VARCHAR(500), -- storage/.../B07_wf4_DesignDetail/structures/... last_modified_by INT, -- FK는 later created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-8. 수량 산출 그룹 --------- */ -- quantity_items: WF5 수량 산출 CREATE TABLE IF NOT EXISTS quantity_items ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later category VARCHAR(100), -- 토공, 구조물, 포장, 배수, 녹화, 안전시설 item_name VARCHAR(255), unit VARCHAR(50), -- m3, 개, m, m2 quantity_design FLOAT, quantity_actual FLOAT, unit_price FLOAT, total_price FLOAT, -- quantity_actual × unit_price standard_reference VARCHAR(255), computed_at TIMESTAMP NULL DEFAULT NULL, quantity_data_path VARCHAR(500), -- storage/.../B08_wf5_Quantity/quantities/... data JSON -- formula, source, ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-9. 산출물 & 문서 그룹 --------- */ -- outputs: WF6 최종 산출물 세트 CREATE TABLE IF NOT EXISTS outputs ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later output_type VARCHAR(100), -- estimation_excel, drawing_dxf, report_pdf, all_bundle status VARCHAR(50) DEFAULT 'GENERATING', -- GENERATING, COMPLETE, FAILED generated_by INT, -- FK는 later generated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, version INT DEFAULT 1, outputs_directory_path VARCHAR(500), -- storage/.../B09_wf6_Estimation/v1/ metadata JSON -- template_used, company_name, project_name, total_cost, generation_time_sec ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- output_files: 최종 산출 파일 CREATE TABLE IF NOT EXISTS output_files ( id INT AUTO_INCREMENT PRIMARY KEY, output_id INT NOT NULL, -- FK는 later file_type VARCHAR(50), -- xlsx, pdf, dxf, dwg, json, zip original_filename VARCHAR(255), output_file_path VARCHAR(500), -- storage/.../B09_wf6_Estimation/v1/... file_size_mb FLOAT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, download_count INT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* --------- 1-10. 변경 이력 & 감시 그룹 --------- */ -- audit_logs: 보안 감시 로그 CREATE TABLE IF NOT EXISTS audit_logs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, -- FK는 later action VARCHAR(100), -- CREATE, READ, UPDATE, DELETE, EXPORT, DOWNLOAD entity_type VARCHAR(100), -- projects, routes, structures, outputs entity_id VARCHAR(36), timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, ip_address VARCHAR(45), details JSON -- old_value, new_value, reason ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- change_logs: 설계 변경 이력 -- [★오류 수정]: 외래키 ON DELETE SET NULL 조건을 위해 changed_by 컬럼을 NULL 가능으로 변경 CREATE TABLE IF NOT EXISTS change_logs ( id INT AUTO_INCREMENT PRIMARY KEY, project_id CHAR(36) NOT NULL, -- FK는 later changed_by INT NULL, -- NOT NULL 에서 NULL로 변경 완료 changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, entity_type VARCHAR(100), -- routes, cross_sections, structures, quantity_items entity_id INT, old_value JSON, new_value JSON, reason VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- ============================================================================= -- 2. 외래키(FK) 제약조건 추가 -- ============================================================================= ALTER TABLE users ADD CONSTRAINT fk_users_company_id FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE SET NULL; ALTER TABLE companies ADD CONSTRAINT fk_companies_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, ADD CONSTRAINT fk_companies_master_user_id FOREIGN KEY (master_user_id) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE sessions ADD CONSTRAINT fk_sessions_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ALTER TABLE email_otps ADD CONSTRAINT fk_email_otps_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ALTER TABLE trusted_devices ADD CONSTRAINT fk_trusted_devices_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ALTER TABLE join_requests ADD CONSTRAINT fk_join_requests_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, ADD CONSTRAINT fk_join_requests_company_id FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, ADD CONSTRAINT fk_join_requests_reviewed_by FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE login_logs ADD CONSTRAINT fk_login_logs_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE activity_logs ADD CONSTRAINT fk_activity_logs_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ALTER TABLE system_admin_logs ADD CONSTRAINT fk_system_admin_logs_admin_user_id FOREIGN KEY (admin_user_id) REFERENCES users(id) ON DELETE CASCADE; ALTER TABLE support_requests ADD CONSTRAINT fk_support_requests_company_id FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE SET NULL, ADD CONSTRAINT fk_support_requests_reviewed_by FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE user_consents ADD CONSTRAINT fk_user_consents_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ALTER TABLE projects ADD CONSTRAINT fk_projects_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT, ADD CONSTRAINT fk_projects_company_id FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE RESTRICT; ALTER TABLE project_versions ADD CONSTRAINT fk_project_versions_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ALTER TABLE input_files ADD CONSTRAINT fk_input_files_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_input_files_upload_by FOREIGN KEY (upload_by) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE processed_point_cloud ADD CONSTRAINT fk_processed_point_cloud_input_file_id FOREIGN KEY (input_file_id) REFERENCES input_files(id) ON DELETE CASCADE, ADD CONSTRAINT fk_processed_point_cloud_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ALTER TABLE surface_models ADD CONSTRAINT fk_surface_models_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_surface_models_source_file_id FOREIGN KEY (source_file_id) REFERENCES input_files(id) ON DELETE SET NULL, ADD CONSTRAINT fk_surface_models_processed_cloud_id FOREIGN KEY (processed_cloud_id) REFERENCES processed_point_cloud(id) ON DELETE SET NULL; ALTER TABLE terrain_layers ADD CONSTRAINT fk_terrain_layers_surface_model_id FOREIGN KEY (surface_model_id) REFERENCES surface_models(id) ON DELETE CASCADE; ALTER TABLE routes ADD CONSTRAINT fk_routes_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_routes_surface_model_id FOREIGN KEY (surface_model_id) REFERENCES surface_models(id) ON DELETE SET NULL; ALTER TABLE route_points ADD CONSTRAINT fk_route_points_route_id FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE; ALTER TABLE route_statistics ADD CONSTRAINT fk_route_statistics_route_id FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE; ALTER TABLE longitudinal_sections ADD CONSTRAINT fk_longitudinal_sections_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_longitudinal_sections_route_id FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE; ALTER TABLE cross_sections ADD CONSTRAINT fk_cross_sections_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_cross_sections_route_id FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE; ALTER TABLE structures ADD CONSTRAINT fk_structures_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_structures_cross_section_id FOREIGN KEY (cross_section_id) REFERENCES cross_sections(id) ON DELETE SET NULL, ADD CONSTRAINT fk_structures_last_modified_by FOREIGN KEY (last_modified_by) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE quantity_items ADD CONSTRAINT fk_quantity_items_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ALTER TABLE outputs ADD CONSTRAINT fk_outputs_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_outputs_generated_by FOREIGN KEY (generated_by) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE output_files ADD CONSTRAINT fk_output_files_output_id FOREIGN KEY (output_id) REFERENCES outputs(id) ON DELETE CASCADE; ALTER TABLE audit_logs ADD CONSTRAINT fk_audit_logs_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; ALTER TABLE change_logs ADD CONSTRAINT fk_change_logs_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, ADD CONSTRAINT fk_change_logs_changed_by FOREIGN KEY (changed_by) REFERENCES users(id) ON DELETE SET NULL; -- ============================================================================= -- 3. 인덱스 생성 (성능 최적화) -- ============================================================================= -- 사용자/회사 인덱싱 CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_company_id ON users(company_id); CREATE INDEX idx_companies_name ON companies(name); -- 프로젝트 인덱싱 CREATE INDEX idx_projects_user_id ON projects(user_id); CREATE INDEX idx_projects_company_id ON projects(company_id); CREATE INDEX idx_projects_status ON projects(status); CREATE INDEX idx_projects_created_at ON projects(created_at DESC); CREATE INDEX idx_projects_storage_path ON projects(storage_path); -- 파일 인덱싱 CREATE INDEX idx_input_files_project_id ON input_files(project_id); CREATE INDEX idx_input_files_file_type ON input_files(file_type); CREATE INDEX idx_input_files_raw_file_path ON input_files(raw_file_path); CREATE INDEX idx_processed_point_cloud_input_file_id ON processed_point_cloud(input_file_id); CREATE INDEX idx_processed_point_cloud_project_id ON processed_point_cloud(project_id); CREATE INDEX idx_processed_point_cloud_converted_file_path ON processed_point_cloud(converted_file_path); -- 지표면 모델 인덱싱 CREATE INDEX idx_surface_models_project_id ON surface_models(project_id); CREATE INDEX idx_surface_models_status ON surface_models(status); CREATE INDEX idx_surface_models_model_file_path ON surface_models(model_file_path); CREATE INDEX idx_terrain_layers_surface_model_id ON terrain_layers(surface_model_id); CREATE INDEX idx_terrain_layers_layer_file_path ON terrain_layers(layer_file_path); -- 경로 인덱싱 CREATE INDEX idx_routes_project_id ON routes(project_id); CREATE INDEX idx_routes_status ON routes(status); CREATE INDEX idx_routes_route_data_path ON routes(route_data_path); CREATE INDEX idx_route_points_route_id ON route_points(route_id); CREATE INDEX idx_route_statistics_route_id ON route_statistics(route_id); -- 단면 인덱싱 CREATE INDEX idx_longitudinal_sections_project_id ON longitudinal_sections(project_id); CREATE INDEX idx_longitudinal_sections_route_id ON longitudinal_sections(route_id); CREATE INDEX idx_longitudinal_sections_file_path ON longitudinal_sections(longitudinal_file_path); CREATE INDEX idx_cross_sections_project_id ON cross_sections(project_id); CREATE INDEX idx_cross_sections_route_id ON cross_sections(route_id); CREATE INDEX idx_cross_sections_chainage_m ON cross_sections(chainage_m); CREATE INDEX idx_cross_sections_file_path ON cross_sections(cross_section_file_path); -- 구조물/수량 인덱싱 CREATE INDEX idx_structures_project_id ON structures(project_id); CREATE INDEX idx_structures_cross_section_id ON structures(cross_section_id); CREATE INDEX idx_structures_file_path ON structures(structure_data_path); CREATE INDEX idx_quantity_items_project_id ON quantity_items(project_id); CREATE INDEX idx_quantity_items_category ON quantity_items(category); CREATE INDEX idx_quantity_items_file_path ON quantity_items(quantity_data_path); -- 산출물 인덱싱 CREATE INDEX idx_outputs_project_id ON outputs(project_id); CREATE INDEX INDEX idx_outputs_version ON outputs(version); CREATE INDEX idx_outputs_directory_path ON outputs(outputs_directory_path); CREATE INDEX idx_output_files_output_id ON output_files(output_id); CREATE INDEX idx_output_files_output_file_path ON output_files(output_file_path); -- 감시/이력 인덱싱 CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id); CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp DESC); CREATE INDEX idx_audit_logs_entity_type ON audit_logs(entity_type); CREATE INDEX idx_change_logs_project_id ON change_logs(project_id); CREATE INDEX idx_change_logs_changed_by ON change_logs(changed_by); CREATE INDEX idx_change_logs_changed_at ON change_logs(changed_at DESC); -- ============================================================================= -- 5. 생성 완료 메시지 -- ============================================================================= -- 모든 테이블과 인덱스가 정상적으로 생성되었으면 아래의 SELECT를 실행하여 확인할 수 있습니다: -- SELECT table_name FROM information_schema.tables WHERE table_schema='public'; -- SELECT indexname FROM pg_indexes WHERE schemaname='public';