브라우저에서 데이터베이스 ERD를 설계하는 사내 전용 도구입니다. 캔버스에서 테이블·컬럼·관계선을 직접 편집하고, SQL DDL을 주고받고, 팀원과 실시간으로 같은 다이어그램을 편집하는 것을 목표로 합니다. 데이터는 전부 자체 호스팅 PostgreSQL에만 저장됩니다.
현재 상태: Phase 4 (인증/팀/권한) 완료. 실시간 협업은 Phase 5에서 구현합니다.
| 영역 | 선택 |
|---|---|
| 웹 앱 | Next.js 16 (App Router, Turbopack) + React 19 + TypeScript strict |
| 스타일 | Tailwind CSS v4 + shadcn/ui (Radix) |
| DB / ORM | PostgreSQL 16 (Docker) + Prisma 7 (@prisma/adapter-pg) |
| 캔버스 | @xyflow/react (React Flow v12) — Phase 1 |
| 실시간 협업 | Yjs CRDT + Hocuspocus 별도 Node 프로세스 — Phase 5 |
| SQL 파싱 | node-sql-parser + Oracle 전용 수제 DDL 파서 — Phase 3 |
| 모노레포 | pnpm workspaces + Turborepo |
erd/
├── apps/
│ ├── web/ # Next.js 앱 (UI, Server Actions, CRUD, import/export)
│ └── collab/ # 실시간 협업 서버 (Hocuspocus 예정, 현재 placeholder)
├── packages/
│ ├── db/ # Prisma 스키마 + 생성된 클라이언트 (@erd/db)
│ ├── schema-ir/ # 방언 중립 SchemaIR 타입 (@erd/schema-ir)
│ ├── diagram-sync/ # Yjs 문서 키 / awareness 계약 (@erd/diagram-sync)
│ └── ui/ # 공유 shadcn 컴포넌트 자리 (@erd/ui, 현재 비어있음)
├── docker-compose.yml # postgres:16
├── pnpm-workspace.yaml
└── turbo.json
패키지들은 빌드 산출물 대신 TypeScript 소스를 그대로 export 합니다. apps/web은
next.config.ts의 transpilePackages로, apps/collab은 esbuild 번들링으로 이를 컴파일합니다.
- Node.js >= 20.9 (개발/검증은 22.23에서 진행)
- pnpm 10 (
corepack enable pnpm) - Docker Desktop
# 1) 환경변수 준비 — 모노레포 전체가 루트 .env 하나를 공유합니다
cp .env.example .env
# 2) PostgreSQL 기동
docker compose up -d postgres
# 3) 의존성 설치
pnpm install
# 4) 스키마를 DB에 적용 (Prisma 클라이언트도 함께 생성됨)
pnpm db:migrate
# 5) 최초 관리자 계정 + 기본 팀/프로젝트 생성
pnpm db:seed
# 6) 개발 서버 — http://localhost:3000
pnpm --filter @erd/web dev.env의 AUTH_SECRET은 반드시 채워야 합니다(비어 있으면 로그인이 동작하지 않습니다):
openssl rand -base64 32 # 결과를 .env의 AUTH_SECRET에 넣으세요pnpm dev로 모든 앱(web + collab)을 한 번에 띄울 수도 있습니다.
호스트 포트 5433: 컨테이너의 5432를 호스트 5433에 매핑합니다. 로컬에 이미 설치된 PostgreSQL이 5432를 점유하고 있는 경우와 충돌하지 않도록 한 것으로,
.env의POSTGRES_PORT와DATABASE_URL을 함께 바꾸면 다른 포트로 옮길 수 있습니다.
동작 확인용 헬스 엔드포인트가 있습니다 — curl http://localhost:3000/api/health는
web → @erd/db → PostgreSQL 경로가 살아있으면 {"status":"ok","database":"up",...}를 반환합니다.
| 명령 | 설명 |
|---|---|
pnpm dev |
모든 앱 개발 모드 (turbo) |
pnpm build |
전체 프로덕션 빌드 |
pnpm typecheck |
전체 워크스페이스 타입 검사 |
pnpm lint |
ESLint |
pnpm test |
전체 워크스페이스 유닛 테스트 (vitest) |
pnpm db:generate |
Prisma 클라이언트 생성 |
pnpm db:migrate |
마이그레이션 생성 + 적용 (개발용) |
pnpm db:seed |
최초 관리자 계정 + 기본 팀/프로젝트 생성 (관리자 비밀번호 재설정 겸용) |
pnpm db:studio |
Prisma Studio |
pnpm --filter @erd/web dev |
웹 앱만 기동 |
pnpm --filter @erd/collab dev |
협업 서버만 기동 |
packages/db/prisma/schema.prisma가 단일 소스입니다.
- 워크스페이스:
Team→TeamMember(OWNER/ADMIN/MEMBER),Folder(트리),Project→ProjectMember(EDITOR/COMMENTER/VIEWER). 팀 역할과 프로젝트 역할의 2단계 권한 모델입니다. - 다이어그램:
Diagram→Table→Column, 그리고Relationship. 관계는sourceColumnIds/targetColumnIds배열로 복합키를 지원하고,cardinality는 사용자가 오버라이드할 수 있도록 비정규화 저장합니다. - 협업:
Comment(요소 핀 + 스레드),DiagramVersion(전체 상태 JSON 스냅샷 → O(1) 복원),ShareLink(token / VIEW·COMMENT / 만료 / 회수). - 인증: Auth.js(NextAuth) Prisma adapter 표준 스키마 —
User/Account/Session/VerificationToken/Authenticator.User.passwordHash는 사내 Credentials 로그인용입니다.
- 생성기는
prisma-client(구prisma-client-js)이고 출력 경로가 필수입니다 →packages/db/src/generated/prisma(gitignore 대상,pnpm db:generate로 재생성). - PostgreSQL 접속은 드라이버 어댑터
@prisma/adapter-pg를 경유합니다 (packages/db/src/client.ts). - CLI가
.env를 자동으로 읽지 않으므로packages/db/prisma.config.ts가 루트.env를 명시적으로 로드합니다.
Auth.js(NextAuth v5) Credentials + 데이터베이스 세션입니다. 사내 전용 도구이므로 공개 회원가입, 초대 메일, OAuth는 없습니다 — 관리자가 계정을 직접 만들고 비밀번호를 전달합니다.
pnpm db:seed가 .env의 값으로 관리자 한 명과 기본 팀/프로젝트를 만듭니다.
| 변수 | 기본값 | 비고 |
|---|---|---|
ADMIN_EMAIL |
admin@erd.local |
로그인 이메일 |
ADMIN_PASSWORD |
admin-change-me |
배포 전 반드시 변경 (8자 이상) |
ADMIN_NAME |
관리자 |
표시 이름 |
시드를 다시 돌리면 이 계정의 비밀번호가 ADMIN_PASSWORD로 재설정됩니다 — 관리자 비밀번호를
잃어버렸을 때의 복구 경로이기도 합니다.
2단계입니다. 두 역할은 하나의 유효 프로젝트 역할로 합쳐집니다.
| 역할 | 의미 | |
|---|---|---|
| 팀 | TeamMember.role = OWNER / ADMIN / MEMBER |
팀을 관리하는 사람 |
| 프로젝트 | ProjectMember.role = EDITOR / COMMENTER / VIEWER |
내용을 만질 수 있는 사람 |
- 팀 OWNER/ADMIN → 팀 안 모든 프로젝트에서 EDITOR이며, 프로젝트 자체(이름 변경·삭제·멤버 관리)도 관리합니다.
- 그 외 → 자신의
ProjectMember행이 있으면 그 역할, 없으면 접근 불가. 팀 MEMBER라는 사실만으로는 모든 프로젝트가 보이지 않습니다(팀에 속하면 프로젝트에 추가될 자격이 생길 뿐입니다). - 프로젝트를 만든 사람은 자동으로 그 프로젝트의 EDITOR가 됩니다.
- 프로젝트 삭제·이름 변경·멤버 관리는 팀 OWNER/ADMIN만 가능합니다.
ProjectRole에는 ADMIN 단계가 없어서, 계획서의 "프로젝트 ADMIN 이상"을 이렇게 해석했습니다.
apps/web/lib/auth/permissions.ts 한 곳에 권한 판단이 모여 있고, 모든 Server Action이
requireDiagramAccess / requireProjectAccess / requireTeamAccess로 DB에서 역할을 다시 계산합니다.
클라이언트에서 버튼을 숨기는 것은 표시용일 뿐입니다.
proxy.ts(Next.js 16에서 middleware.ts가 개명된 것)는 세션 쿠키의 존재만 확인해 /login으로
보내는 낙관적 체크입니다. 인가 경계가 아닙니다 — Next 공식 문서대로 Server Function은 자신이 속한
라우트로 가는 평범한 POST라서, matcher를 조정하거나 액션을 옮기면 보호가 조용히 사라질 수 있습니다.
위조한 쿠키는 proxy를 통과하지만 액션에서 로그인이 필요합니다로 거부됩니다.
세션 쿠키에는 JWT가 아니라 Session 행을 가리키는 불투명 토큰(UUID) 만 들어갑니다. 행을 지우면 그
사용자는 다음 요청에서 바로 로그아웃됩니다 — 팀 관리 화면의 비활성화 와 비밀번호 재설정 이 그
사용자의 모든 세션을 삭제하는 것도 이 때문입니다.
Auth.js는 원래 "Credentials + session.strategy: "database"" 조합을 거부합니다. session.strategy를
명시하지 않고(어댑터가 있으면 Auth.js가 이미 "database"를 기본값으로 씁니다) jwt 콜백에서 세션 행을
직접 만든 뒤 jwt.encode가 그 토큰을 그대로 쿠키에 넣는 방식으로 해결했습니다. 근거와 검증 내용은
apps/web/lib/auth/config.ts의 모듈 주석에 적어 두었습니다.
packages/schema-ir가 방언 중립 SchemaIR를 사이에 두고 양방향을 담당합니다.
DDL 텍스트 ──parse──▶ SchemaIR ──▶ 다이어그램 모델
DDL 텍스트 ◀─generate─ SchemaIR ◀── 다이어그램 모델
- 파싱: PostgreSQL / MySQL / MSSQL은
node-sql-parser를 먼저 시도하고, 이 라이브러리가 거부하는 문장은 자체 재귀 하강 파서로 폴백합니다(T-SQL의ALTER TABLE ADD CONSTRAINT ... FOREIGN KEY, PostgreSQL의GENERATED ALWAYS AS IDENTITY등 실제 미지원 구문이 있습니다). Oracle은node-sql-parser에 문법 자체가 없어 자체 파서만 사용합니다. - 자체 파서가 다루는 문장 — 이 다섯 가지뿐이고 나머지는 경고로 표시한 뒤 건너뜁니다:
CREATE TABLE,ALTER TABLE ... ADD [CONSTRAINT] PRIMARY KEY|UNIQUE|FOREIGN KEY,COMMENT ON TABLE,COMMENT ON COLUMN,CREATE [UNIQUE] INDEX. - 생성: 네 방언 모두 템플릿 기반입니다. FK는 테이블을 모두 만든 뒤
ALTER TABLE로 붙이므로 테이블 간 순환 참조가 문제되지 않습니다. - 타입 매핑은
src/type-map.ts한 곳에 모여 있습니다(varchar→VARCHAR2/NVARCHAR, auto increment →BIGSERIAL/AUTO_INCREMENT/IDENTITY(1,1)/GENERATED ALWAYS AS IDENTITY). - Import는 항상 diff/preview 후 확정입니다. 파싱은 Server Action에서 실행되고(브라우저 번들에 파서·elkjs가 들어가지 않도록), 확정 시 에디터 스토어에 반영된 뒤 기존 저장 경로로 영속화됩니다. 좌표가 없는 신규 테이블은 elkjs layered 레이아웃으로 배치합니다.
알려진 한계:
- MSSQL에는 표준 주석 DDL이 없어(
sp_addextendedproperty는 DDL이 아님) 주석을--줄 주석으로 내보내며, 다시 가져올 때 복원되지 않습니다. - 방언이 구분하지 못하는 타입은 왕복에서 합쳐집니다 — 예: Oracle/MSSQL의
json은text와 같은 저장 타입(CLOB/NVARCHAR(MAX))이고, Oracle에는time타입이 없습니다. CHECK제약, 트리거, 시퀀스, 파티션 등 다이어그램 모델에 대응이 없는 요소는 무시합니다.
| 단계 | 범위 |
|---|---|
| 0 ✅ | 저장소 스캐폴드, Prisma 스키마 v1, Postgres(Docker), Tailwind+shadcn |
| 1 ✅ | 캔버스 편집기 코어(단일 사용자): React Flow, 인라인 편집, 관계선, zoom/pan/minimap |
| 2 ✅ | 디바운스 자동저장, DiagramVersion 수동 저장/복원 UI |
| 3 ✅ | SQL import/export(@erd/schema-ir), elkjs 오토 레이아웃 |
| 4 ✅ | Auth.js 인증(Credentials + DB 세션), 팀/프로젝트 2단계 권한, 라우트 보호, 팀·멤버 관리 UI |
| 5 | 실시간 협업: apps/collab(Hocuspocus), 캔버스를 Yjs 기반으로 전환, 커서 프레즌스 |
| 6 | 댓글/스레드 |
| 7 | 버전 diff/preview, 공유 링크 및 읽기 전용 뷰어 |
| 8 | 성능 튜닝, 감사 로그, 백업 전략 |