---
name: erd-to-orm
description: |
  트리거: "ERD를 코드로", "DDL 변환", "ORM 모델 생성", "DDL을 JPA로", "SQL을 SQLAlchemy로", "테이블 정의 변환해줘"
  수행: ERD 다이어그램 또는 DDL SQL을 입력받아 JPA Entity(Java) 또는 SQLAlchemy 모델(Python)로 자동 변환한다.
  관계 매핑(@OneToMany/@ManyToOne/@ManyToMany), 컬럼 제약(nullable/unique/length), 인덱스 어노테이션까지 변환한다.
  출력: 변환된 ORM 모델 파일 코드 블록 + 변환 매핑 요약표.
---

# ERD/DDL → ORM 모델 변환기

## 목적

기존 DB 스키마(DDL SQL 또는 ERD)를 JPA Entity 또는 SQLAlchemy 모델로 자동 변환하여
DB 우선 개발(Database-First) 워크플로우를 가속화한다.

## 실행 절차

1. **입력 파악**: DDL SQL 또는 ERD 텍스트, 타겟 ORM(JPA/SQLAlchemy) 확인
2. **타입 매핑**: SQL 타입 → Java/Python 타입 변환 (VARCHAR→String, NUMERIC→BigDecimal 등)
3. **제약 조건 변환**: NOT NULL→nullable=false, UNIQUE→unique=true, CHECK→검증 어노테이션
4. **관계 분석**: FK 정의로 @ManyToOne/@OneToMany/@ManyToMany 관계 결정
5. **인덱스 변환**: CREATE INDEX → @Index(인덱스 어노테이션) 또는 Index 클래스
6. **코드 생성**: 각 테이블마다 완전한 모델 클래스 생성
7. **변환 매핑 요약**: SQL 타입 ↔ ORM 타입 대응표 출력

## 타입 매핑 기준표

### SQL → JPA (Java)
| SQL 타입 | Java 타입 | JPA 어노테이션 |
|----------|-----------|----------------|
| BIGINT / BIGSERIAL | Long | @GeneratedValue(IDENTITY) |
| INT / SERIAL | Integer | |
| VARCHAR(n) | String | @Column(length=n) |
| TEXT | String | @Column(columnDefinition="TEXT") |
| NUMERIC(p,s) | BigDecimal | @Column(precision=p, scale=s) |
| BOOLEAN | Boolean | |
| TIMESTAMP | LocalDateTime | |
| DATE | LocalDate | |
| ENUM | @Enumerated(STRING) | |
| UUID | UUID | @GeneratedValue(generator="uuid2") |
| JSONB | String | @Column(columnDefinition="jsonb") |

### SQL → SQLAlchemy (Python)
| SQL 타입 | SQLAlchemy 타입 | 비고 |
|----------|-----------------|------|
| BIGINT | BigInteger | |
| INT | Integer | |
| VARCHAR(n) | String(n) | |
| TEXT | Text | |
| NUMERIC(p,s) | Numeric(p,s) | |
| BOOLEAN | Boolean | |
| TIMESTAMP | DateTime(timezone=True) | |
| UUID | UUID(as_uuid=True) | postgresql dialect |
| JSONB | JSONB | from sqlalchemy.dialects.postgresql |

## 출력 형식

### 변환 결과 (파일별 코드 블록)
```java
// src/main/java/{package}/domain/TableName.java
@Entity
@Table(name = "table_name", indexes = { ... })
public class TableName { ... }
```

### 변환 매핑 요약표
| DDL 컬럼 | SQL 타입 | ORM 필드 | 타입 | 제약 |
|----------|----------|----------|------|------|

## 사용 예시

### 입력 (DDL)
```sql
CREATE TABLE products (
    id              BIGSERIAL       PRIMARY KEY,
    category_id     INT             NOT NULL REFERENCES categories(id),
    name            VARCHAR(200)    NOT NULL,
    description     TEXT,
    price           NUMERIC(10, 2)  NOT NULL CHECK (price >= 0),
    stock_quantity  INT             NOT NULL DEFAULT 0,
    is_active       BOOLEAN         NOT NULL DEFAULT TRUE,
    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_active   ON products(is_active, created_at DESC);
```

### 출력 - JPA Entity (Java)
```java
// src/main/java/com/example/domain/Product.java
package com.example.domain;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(
    name = "products",
    indexes = {
        @Index(name = "idx_products_category", columnList = "category_id"),
        @Index(name = "idx_products_active",   columnList = "is_active, created_at DESC")
    }
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", nullable = false)
    private Category category;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(columnDefinition = "TEXT")
    private String description;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    @Builder.Default
    private Integer stockQuantity = 0;

    @Column(nullable = false)
    @Builder.Default
    private Boolean isActive = true;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt;
}
```

### 출력 - SQLAlchemy 모델 (Python)
```python
# app/models/product.py
from sqlalchemy import (
    Column, BigInteger, Integer, String, Text,
    Numeric, Boolean, DateTime, ForeignKey, Index, func
)
from sqlalchemy.orm import relationship, DeclarativeBase


class Base(DeclarativeBase):
    pass


class Product(Base):
    __tablename__ = "products"
    __table_args__ = (
        Index("idx_products_category", "category_id"),
        Index("idx_products_active", "is_active", "created_at"),
    )

    id              = Column(BigInteger, primary_key=True, autoincrement=True)
    category_id     = Column(Integer, ForeignKey("categories.id", ondelete="RESTRICT"), nullable=False)
    name            = Column(String(200), nullable=False)
    description     = Column(Text)
    price           = Column(Numeric(10, 2), nullable=False)
    stock_quantity  = Column(Integer, nullable=False, default=0)
    is_active       = Column(Boolean, nullable=False, default=True)
    created_at      = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
    updated_at      = Column(DateTime(timezone=True), nullable=False,
                             server_default=func.now(), onupdate=func.now())

    # 관계
    category = relationship("Category", back_populates="products", lazy="select")
```

### 출력 - N:M 관계 변환 예시 (JPA)
```java
// DDL: post_tags(post_id FK, tag_id FK) → JPA @ManyToMany

// Post.java
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
    name = "post_tags",
    joinColumns = @JoinColumn(name = "post_id"),
    inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags = new ArrayList<>();

// Tag.java
@ManyToMany(mappedBy = "tags", fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();
```

### 출력 - 변환 매핑 요약표
| DDL 컬럼 | SQL 타입 | JPA 필드 | Java 타입 | 제약 |
|----------|----------|----------|-----------|------|
| id | BIGSERIAL | id | Long | PK, IDENTITY |
| category_id | INT NOT NULL FK | category | Category | ManyToOne, LAZY |
| name | VARCHAR(200) NOT NULL | name | String | length=200, nullable=false |
| price | NUMERIC(10,2) NOT NULL | price | BigDecimal | precision=10, scale=2 |
| is_active | BOOLEAN DEFAULT TRUE | isActive | Boolean | default=true |
| created_at | TIMESTAMP NOT NULL | createdAt | LocalDateTime | @CreatedDate, updatable=false |

## 주의사항

- **양방향 관계 무한루프**: `@ToString`, `@EqualsAndHashCode`에서 양방향 관계 필드 제외 필요. Lombok `@ToString(exclude={"orders"})` 적용.
- **FetchType 기본값**: `@ManyToOne`, `@OneToOne`은 기본 EAGER. 반드시 `LAZY`로 명시 변경.
- **N:M 중간 테이블에 추가 컬럼**: 추가 컬럼이 있으면 `@ManyToMany` 대신 중간 엔티티 클래스로 분리.
- **camelCase 변환**: DDL의 `snake_case` 컬럼명은 JPA에서 자동 변환되지만, 명시적으로 `@Column(name="snake_case")` 권장.
- **SQLAlchemy 2.0**: `Session.execute(select(Model))` 방식 사용. `Session.query()` 방식은 레거시.
- **CHECK 제약**: JPA에서 `@Column(columnDefinition="... CHECK ...")`로 DDL 직접 지정 가능하나, Bean Validation(`@Min`, `@DecimalMin`)으로 대체 권장.
- **DEFAULT 값**: DB의 DEFAULT는 `@Builder.Default`(Lombok) 또는 `@Column(insertable=false)`로 처리.
