feat: CONAI Phase 1 MVP 초기 구현
소형 건설업체(100억 미만)를 위한 AI 기반 토목공사 통합관리 플랫폼 Backend (FastAPI): - SQLAlchemy 모델 13개 (users, projects, wbs, tasks, daily_reports, reports, inspections, quality, weather, permits, rag, settings) - API 라우터 11개 (auth, projects, tasks, daily_reports, reports, inspections, weather, rag, kakao, permits, settings) - Services: Claude AI 래퍼, CPM Gantt 계산, 기상청 API, RAG(pgvector), 카카오 Skill API - Alembic 마이그레이션 (pgvector 포함) - pytest 테스트 (CPM, 날씨 경보) Frontend (Next.js 15): - 11개 페이지 (대시보드, 프로젝트, Gantt, 일보, 검측, 품질, 날씨, 인허가, RAG, 설정) - TanStack Query + Zustand + Tailwind CSS 인프라: - Docker Compose (PostgreSQL pgvector + backend + frontend) - 한국어 README 및 설치 가이드 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
from .user import User
|
||||
from .project import Project, WBSItem
|
||||
from .task import Task, TaskDependency
|
||||
from .daily_report import DailyReport, DailyReportPhoto
|
||||
from .report import Report
|
||||
from .inspection import InspectionRequest
|
||||
from .quality import QualityTest
|
||||
from .weather import WeatherData, WeatherAlert
|
||||
from .permit import PermitItem
|
||||
from .rag import RagSource, RagChunk
|
||||
from .settings import ClientProfile, AlertRule, WorkTypeLibrary
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"Project", "WBSItem",
|
||||
"Task", "TaskDependency",
|
||||
"DailyReport", "DailyReportPhoto",
|
||||
"Report",
|
||||
"InspectionRequest",
|
||||
"QualityTest",
|
||||
"WeatherData", "WeatherAlert",
|
||||
"PermitItem",
|
||||
"RagSource", "RagChunk",
|
||||
"ClientProfile", "AlertRule", "WorkTypeLibrary",
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import DateTime, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
class UUIDMixin:
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=uuid.uuid4,
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Integer, Boolean, Date, Text, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
from datetime import datetime
|
||||
import enum
|
||||
|
||||
|
||||
class ReportStatus(str, enum.Enum):
|
||||
DRAFT = "draft"
|
||||
CONFIRMED = "confirmed"
|
||||
SUBMITTED = "submitted"
|
||||
|
||||
|
||||
class InputSource(str, enum.Enum):
|
||||
KAKAO = "kakao"
|
||||
WEB = "web"
|
||||
API = "api"
|
||||
|
||||
|
||||
class DailyReport(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "daily_reports"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
report_date: Mapped[str] = mapped_column(Date, nullable=False, index=True)
|
||||
weather_summary: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
temperature_high: Mapped[float | None] = mapped_column(nullable=True)
|
||||
temperature_low: Mapped[float | None] = mapped_column(nullable=True)
|
||||
workers_count: Mapped[dict | None] = mapped_column(JSONB, nullable=True) # {"concrete": 5, ...}
|
||||
equipment_list: Mapped[list | None] = mapped_column(JSONB, nullable=True) # [{"type": "backhoe", ...}]
|
||||
work_content: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
issues: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
input_source: Mapped[InputSource] = mapped_column(
|
||||
SAEnum(InputSource, name="input_source"), default=InputSource.WEB, nullable=False
|
||||
)
|
||||
raw_kakao_input: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ai_generated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
status: Mapped[ReportStatus] = mapped_column(
|
||||
SAEnum(ReportStatus, name="daily_report_status"), default=ReportStatus.DRAFT, nullable=False
|
||||
)
|
||||
confirmed_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
confirmed_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||||
pdf_s3_key: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="daily_reports")
|
||||
photos: Mapped[list["DailyReportPhoto"]] = relationship(
|
||||
"DailyReportPhoto", back_populates="daily_report", cascade="all, delete-orphan"
|
||||
)
|
||||
confirmed_user: Mapped["User | None"] = relationship("User", foreign_keys=[confirmed_by])
|
||||
|
||||
|
||||
class DailyReportPhoto(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "daily_report_photos"
|
||||
|
||||
daily_report_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("daily_reports.id"), nullable=False)
|
||||
s3_key: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
caption: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
|
||||
# relationships
|
||||
daily_report: Mapped["DailyReport"] = relationship("DailyReport", back_populates="photos")
|
||||
@@ -0,0 +1,44 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Boolean, Date, Text, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class InspectionResult(str, enum.Enum):
|
||||
PASS = "pass"
|
||||
FAIL = "fail"
|
||||
CONDITIONAL_PASS = "conditional_pass"
|
||||
|
||||
|
||||
class InspectionStatus(str, enum.Enum):
|
||||
DRAFT = "draft"
|
||||
SENT = "sent"
|
||||
COMPLETED = "completed"
|
||||
|
||||
|
||||
class InspectionRequest(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "inspection_requests"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
wbs_item_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("wbs_items.id"), nullable=True)
|
||||
inspection_type: Mapped[str] = mapped_column(String(50), nullable=False) # rebar, formwork, pipe_burial, etc.
|
||||
requested_date: Mapped[str] = mapped_column(Date, nullable=False)
|
||||
location_detail: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
checklist_items: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
||||
result: Mapped[InspectionResult | None] = mapped_column(
|
||||
SAEnum(InspectionResult, name="inspection_result"), nullable=True
|
||||
)
|
||||
inspector_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ai_generated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
status: Mapped[InspectionStatus] = mapped_column(
|
||||
SAEnum(InspectionStatus, name="inspection_status"), default=InspectionStatus.DRAFT, nullable=False
|
||||
)
|
||||
pdf_s3_key: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="inspection_requests")
|
||||
wbs_item: Mapped["WBSItem | None"] = relationship("WBSItem")
|
||||
@@ -0,0 +1,36 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Boolean, Date, Text, Integer, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class PermitStatus(str, enum.Enum):
|
||||
NOT_STARTED = "not_started"
|
||||
SUBMITTED = "submitted"
|
||||
IN_REVIEW = "in_review"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
|
||||
|
||||
class PermitItem(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "permit_items"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
permit_type: Mapped[str] = mapped_column(String(100), nullable=False) # 도로점용허가, 하천점용허가, etc.
|
||||
authority: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
required: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
deadline: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
status: Mapped[PermitStatus] = mapped_column(
|
||||
SAEnum(PermitStatus, name="permit_status"), default=PermitStatus.NOT_STARTED, nullable=False
|
||||
)
|
||||
submitted_date: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
approved_date: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
document_s3_key: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="permit_items")
|
||||
@@ -0,0 +1,79 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Integer, BigInteger, Date, Float, ForeignKey, Enum as SAEnum, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class ProjectStatus(str, enum.Enum):
|
||||
PLANNING = "planning"
|
||||
ACTIVE = "active"
|
||||
SUSPENDED = "suspended"
|
||||
COMPLETED = "completed"
|
||||
|
||||
|
||||
class ConstructionType(str, enum.Enum):
|
||||
ROAD = "road"
|
||||
SEWER = "sewer"
|
||||
WATER = "water"
|
||||
BRIDGE = "bridge"
|
||||
SITE_WORK = "site_work"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class Project(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "projects"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
client_profile_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("client_profiles.id"), nullable=True)
|
||||
construction_type: Mapped[ConstructionType] = mapped_column(
|
||||
SAEnum(ConstructionType, name="construction_type"), default=ConstructionType.OTHER, nullable=False
|
||||
)
|
||||
contract_amount: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
start_date: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
end_date: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
location_address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
location_lat: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
location_lng: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
weather_grid_x: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
weather_grid_y: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
status: Mapped[ProjectStatus] = mapped_column(
|
||||
SAEnum(ProjectStatus, name="project_status"), default=ProjectStatus.PLANNING, nullable=False
|
||||
)
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# relationships
|
||||
owner: Mapped["User"] = relationship("User", back_populates="owned_projects", foreign_keys=[owner_id])
|
||||
wbs_items: Mapped[list["WBSItem"]] = relationship("WBSItem", back_populates="project", cascade="all, delete-orphan")
|
||||
tasks: Mapped[list["Task"]] = relationship("Task", back_populates="project", cascade="all, delete-orphan")
|
||||
daily_reports: Mapped[list["DailyReport"]] = relationship("DailyReport", back_populates="project", cascade="all, delete-orphan")
|
||||
inspection_requests: Mapped[list["InspectionRequest"]] = relationship("InspectionRequest", back_populates="project", cascade="all, delete-orphan")
|
||||
quality_tests: Mapped[list["QualityTest"]] = relationship("QualityTest", back_populates="project", cascade="all, delete-orphan")
|
||||
weather_data: Mapped[list["WeatherData"]] = relationship("WeatherData", back_populates="project", cascade="all, delete-orphan")
|
||||
weather_alerts: Mapped[list["WeatherAlert"]] = relationship("WeatherAlert", back_populates="project", cascade="all, delete-orphan")
|
||||
permit_items: Mapped[list["PermitItem"]] = relationship("PermitItem", back_populates="project", cascade="all, delete-orphan")
|
||||
reports: Mapped[list["Report"]] = relationship("Report", back_populates="project", cascade="all, delete-orphan")
|
||||
client_profile: Mapped["ClientProfile | None"] = relationship("ClientProfile", back_populates="projects")
|
||||
|
||||
|
||||
class WBSItem(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "wbs_items"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
parent_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("wbs_items.id"), nullable=True)
|
||||
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
level: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||||
unit: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
design_qty: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
unit_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="wbs_items")
|
||||
parent: Mapped["WBSItem | None"] = relationship("WBSItem", remote_side="WBSItem.id", back_populates="children")
|
||||
children: Mapped[list["WBSItem"]] = relationship("WBSItem", back_populates="parent")
|
||||
tasks: Mapped[list["Task"]] = relationship("Task", back_populates="wbs_item")
|
||||
@@ -0,0 +1,35 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Date, Text, Float, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class QualityResult(str, enum.Enum):
|
||||
PASS = "pass"
|
||||
FAIL = "fail"
|
||||
|
||||
|
||||
class QualityTest(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "quality_tests"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
wbs_item_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("wbs_items.id"), nullable=True)
|
||||
test_type: Mapped[str] = mapped_column(String(50), nullable=False) # compression_strength, slump, compaction, etc.
|
||||
test_date: Mapped[str] = mapped_column(Date, nullable=False)
|
||||
location_detail: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
design_value: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
measured_value: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
unit: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
result: Mapped[QualityResult] = mapped_column(
|
||||
SAEnum(QualityResult, name="quality_result"), nullable=False
|
||||
)
|
||||
lab_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
report_number: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="quality_tests")
|
||||
wbs_item: Mapped["WBSItem | None"] = relationship("WBSItem")
|
||||
@@ -0,0 +1,41 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Integer, Text, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class RagSourceType(str, enum.Enum):
|
||||
KCS = "kcs"
|
||||
LAW = "law"
|
||||
REGULATION = "regulation"
|
||||
GUIDELINE = "guideline"
|
||||
|
||||
|
||||
class RagSource(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "rag_sources"
|
||||
|
||||
title: Mapped[str] = mapped_column(String(300), nullable=False)
|
||||
source_type: Mapped[RagSourceType] = mapped_column(
|
||||
SAEnum(RagSourceType, name="rag_source_type"), nullable=False
|
||||
)
|
||||
source_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
file_s3_key: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# relationships
|
||||
chunks: Mapped[list["RagChunk"]] = relationship("RagChunk", back_populates="source", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class RagChunk(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "rag_chunks"
|
||||
|
||||
source_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("rag_sources.id"), nullable=False)
|
||||
chunk_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
# Note: embedding column (VECTOR) added via Alembic migration with pgvector extension
|
||||
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB, nullable=True)
|
||||
|
||||
# relationships
|
||||
source: Mapped["RagSource"] = relationship("RagSource", back_populates="chunks")
|
||||
@@ -0,0 +1,38 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Date, Text, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class ReportType(str, enum.Enum):
|
||||
WEEKLY = "weekly"
|
||||
MONTHLY = "monthly"
|
||||
|
||||
|
||||
class ReportStatus(str, enum.Enum):
|
||||
DRAFT = "draft"
|
||||
REVIEWED = "reviewed"
|
||||
SUBMITTED = "submitted"
|
||||
|
||||
|
||||
class Report(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "reports"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
report_type: Mapped[ReportType] = mapped_column(
|
||||
SAEnum(ReportType, name="report_type"), nullable=False
|
||||
)
|
||||
period_start: Mapped[str] = mapped_column(Date, nullable=False)
|
||||
period_end: Mapped[str] = mapped_column(Date, nullable=False)
|
||||
content_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
ai_draft_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
status: Mapped[ReportStatus] = mapped_column(
|
||||
SAEnum(ReportStatus, name="report_status"), default=ReportStatus.DRAFT, nullable=False
|
||||
)
|
||||
pdf_s3_key: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="reports")
|
||||
@@ -0,0 +1,41 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Boolean, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
|
||||
|
||||
class ClientProfile(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "client_profiles"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
report_frequency: Mapped[str] = mapped_column(String(20), default="weekly", nullable=False)
|
||||
template_config: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
contact_info: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# relationships
|
||||
projects: Mapped[list["Project"]] = relationship("Project", back_populates="client_profile")
|
||||
|
||||
|
||||
class AlertRule(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "alert_rules"
|
||||
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=True)
|
||||
rule_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
condition: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
channels: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
||||
recipients: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
|
||||
class WorkTypeLibrary(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "work_type_library"
|
||||
|
||||
code: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
category: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
weather_constraints: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
default_checklist: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
@@ -0,0 +1,61 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Integer, Date, Boolean, Float, ForeignKey, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class DependencyType(str, enum.Enum):
|
||||
FS = "FS" # Finish-to-Start
|
||||
SS = "SS" # Start-to-Start
|
||||
FF = "FF" # Finish-to-Finish
|
||||
SF = "SF" # Start-to-Finish
|
||||
|
||||
|
||||
class Task(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
wbs_item_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("wbs_items.id"), nullable=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
planned_start: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
planned_end: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
actual_start: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
actual_end: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
progress_pct: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
|
||||
is_milestone: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
is_critical: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
early_start: Mapped[str | None] = mapped_column(Date, nullable=True) # CPM
|
||||
early_finish: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
late_start: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
late_finish: Mapped[str | None] = mapped_column(Date, nullable=True)
|
||||
total_float: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="tasks")
|
||||
wbs_item: Mapped["WBSItem | None"] = relationship("WBSItem", back_populates="tasks")
|
||||
predecessors: Mapped[list["TaskDependency"]] = relationship(
|
||||
"TaskDependency", foreign_keys="TaskDependency.successor_id", back_populates="successor"
|
||||
)
|
||||
successors: Mapped[list["TaskDependency"]] = relationship(
|
||||
"TaskDependency", foreign_keys="TaskDependency.predecessor_id", back_populates="predecessor"
|
||||
)
|
||||
weather_alerts: Mapped[list["WeatherAlert"]] = relationship("WeatherAlert", back_populates="task")
|
||||
|
||||
|
||||
class TaskDependency(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "task_dependencies"
|
||||
|
||||
predecessor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tasks.id"), nullable=False)
|
||||
successor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tasks.id"), nullable=False)
|
||||
dependency_type: Mapped[DependencyType] = mapped_column(
|
||||
SAEnum(DependencyType, name="dependency_type"), default=DependencyType.FS, nullable=False
|
||||
)
|
||||
lag_days: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
|
||||
# relationships
|
||||
predecessor: Mapped["Task"] = relationship("Task", foreign_keys=[predecessor_id], back_populates="successors")
|
||||
successor: Mapped["Task"] = relationship("Task", foreign_keys=[successor_id], back_populates="predecessors")
|
||||
@@ -0,0 +1,31 @@
|
||||
import uuid
|
||||
from sqlalchemy import String, Boolean, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "admin"
|
||||
SITE_MANAGER = "site_manager"
|
||||
SUPERVISOR = "supervisor"
|
||||
WORKER = "worker"
|
||||
|
||||
|
||||
class User(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "users"
|
||||
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
role: Mapped[UserRole] = mapped_column(
|
||||
SAEnum(UserRole, name="user_role"), default=UserRole.SITE_MANAGER, nullable=False
|
||||
)
|
||||
phone: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
kakao_user_key: Mapped[str | None] = mapped_column(String(100), nullable=True, unique=True, index=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
# relationships
|
||||
owned_projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner", foreign_keys="Project.owner_id")
|
||||
@@ -0,0 +1,57 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, Boolean, Date, Float, Integer, ForeignKey, Enum as SAEnum, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin, UUIDMixin
|
||||
import enum
|
||||
|
||||
|
||||
class ForecastType(str, enum.Enum):
|
||||
SHORT_TERM = "short_term"
|
||||
MEDIUM_TERM = "medium_term"
|
||||
OBSERVED = "observed"
|
||||
|
||||
|
||||
class AlertSeverity(str, enum.Enum):
|
||||
WARNING = "warning"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class WeatherData(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "weather_data"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
forecast_date: Mapped[str] = mapped_column(Date, nullable=False)
|
||||
forecast_type: Mapped[ForecastType] = mapped_column(
|
||||
SAEnum(ForecastType, name="forecast_type"), nullable=False
|
||||
)
|
||||
temperature_high: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
temperature_low: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
precipitation_mm: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
wind_speed_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
weather_code: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
raw_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
fetched_at: Mapped[datetime] = mapped_column(nullable=False)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="weather_data")
|
||||
|
||||
|
||||
class WeatherAlert(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "weather_alerts"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
|
||||
task_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("tasks.id"), nullable=True)
|
||||
alert_date: Mapped[str] = mapped_column(Date, nullable=False)
|
||||
alert_type: Mapped[str] = mapped_column(String(50), nullable=False) # rain_concrete, wind_highwork, etc.
|
||||
severity: Mapped[AlertSeverity] = mapped_column(
|
||||
SAEnum(AlertSeverity, name="alert_severity"), nullable=False
|
||||
)
|
||||
message: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
is_acknowledged: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# relationships
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="weather_alerts")
|
||||
task: Mapped["Task | None"] = relationship("Task", back_populates="weather_alerts")
|
||||
Reference in New Issue
Block a user