Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| 디자인 시스템 import·컴포넌트 목록 | [docs/agent/design-system-llm.md](docs/agent/design-system-llm.md) |
| Warehouse 스키마 (ETL·Seed) | [docs/agent/warehouse-schema.md](docs/agent/warehouse-schema.md) |
| Rust API 서버 (`api-server`) | [packages/api-server/AGENT.md](packages/api-server/AGENT.md) |
| DB 마이그레이션 역할(SeaORM vs `supabase/migrations`) | [packages/api-server/AGENT.md — §2.4](packages/api-server/AGENT.md) |

## 반드시 지킬 것

Expand All @@ -34,4 +35,4 @@
- 디자인 시스템 토큰·UI 가이드: [docs/design-system/README.md](docs/design-system/README.md)
- 문서 인덱스: [docs/README.md](docs/README.md)

**마지막 업데이트**: 2026-04-02
**마지막 업데이트**: 2026-04-21
3 changes: 2 additions & 1 deletion docs/agent/database-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ Supabase 기반 이중 스키마 (public / warehouse) 구조의 진입점. 앱
- 업데이트 체크리스트: [`docs/database/02-update-checklist.md`](../database/02-update-checklist.md)
- Warehouse 스키마 인벤토리: [`docs/agent/warehouse-schema.md`](warehouse-schema.md)
- Supabase CLI setup: [`docs/database/04-supabase-cli-setup.md`](../database/04-supabase-cli-setup.md)
- 마이그레이션 레이아웃 (SeaORM vs `supabase/migrations`, 보관 `legacy/`): [`packages/api-server/migration/README.md`](../../packages/api-server/migration/README.md), [`supabase/migrations/README.md`](../../supabase/migrations/README.md)

## Key files / concepts

- **Public schema**: 앱 데이터 (posts, items, users, solutions, social 등)
- **Warehouse schema**: ETL·Seed 파이프라인 (seed_candidates, review_queue, seed_images 등)
- Migration 전략: SeaORM (테이블·컬럼) + Supabase CLI (RLS·함수·warehouse)
- Migration 전략: 활성 SeaORM은 `packages/api-server/migration/`; Supabase CLI는 `supabase/migrations/`; 이전 전체는 `packages/api-server/legacy/`, `supabase/legacy/` 보관. 수동 SQL은 `legacy/sql/` 등 참고 (원격 덤프와 중복 주의)
- Types: `packages/shared/supabase/types.ts` (typegen 재생성으로 drift 감지)

## Gotchas
Expand Down
6 changes: 3 additions & 3 deletions docs/database/04-supabase-cli-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ supabase link --project-ref fvxchskblyhuswzlcmql

| 변경 유형 | 관리 도구 | 위치 |
|---------|----------|------|
| 테이블/컬럼 (API 서버 관련) | SeaORM | `packages/api-server/migration/` |
| RLS, 함수, 트리거, 뷰 | Supabase CLI | `supabase/migrations/` |
| warehouse 스키마 | Supabase CLI | `supabase/migrations/` |
| 테이블/컬럼 (API 서버 관련) | SeaORM | `packages/api-server/migration/` (활성), 보관 `packages/api-server/legacy/` |
| RLS, 함수, 트리거, 뷰 | Supabase CLI | `supabase/migrations/` (활성), 보관 `supabase/legacy/` |
| warehouse 스키마 | SeaORM 또는 Supabase CLI (팀 합의) | 위 경로 중 하나 |

## Common Commands

Expand Down
22 changes: 20 additions & 2 deletions packages/api-server/AGENT.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DECODED - Coding Agent 행동 규칙

**Version:** 3.1.0
**Last Updated:** 2026.01.12
**Version:** 3.3.0
**Last Updated:** 2026.04.21
**Purpose:** Coding Agent가 개발 시 반드시 준수해야 하는 실행 가능한 규칙

---
Expand Down Expand Up @@ -43,6 +43,22 @@ Axum v0.7에서는 경로 파라미터를 `{param}` 형식으로 사용해야

**에러 메시지**: `Path segments must not start with ':'. For capture groups, use {capture}.`

### 2.4 마이그레이션 트랙 역할 분리 (SeaORM vs Supabase SQL)

앱 스키마는 **`public`** 과 **`warehouse`** 를 사용한다. 변경을 넣을 때 **아래 구분**을 따른다.

| 담당 | 도구 | 넣는 것 |
|------|------|---------|
| DDL·구조 | SeaORM (`packages/api-server/migration/`, 활성) | `CREATE EXTENSION`, 테이블·컬럼·인덱스·**외래키(FK)** 등 ORM이 소유하는 스키마 |
| Supabase 플랫폼 | Supabase CLI (`supabase/migrations/*.sql`, 활성) | **RLS** 정책, `SECURITY DEFINER` 함수, `auth.uid()` 등과 맞물리는 트리거·RPC 등 |
| (보관) | `packages/api-server/legacy/`, `supabase/legacy/` | 이전 마이그레이션 전체 — 크레이트 `migration_legacy`, 자동 적용 대상 아님 |

**Greenfield 권장 적용 순서**: (1) 확장 및 SeaORM 마이그레이션 적용 → (2) `supabase/migrations` SQL을 파일명(타임스탬프) 순으로 적용.

SeaORM 마이그레이션 안에서 raw SQL로 RLS를 넣는 것은 가능하지만, **RLS·함수는 Supabase 트랙에 모아** 링크된 프로젝트에서 `supabase db push` 로 같이 올리기 쉽게 유지하는 것을 권장한다. 뷰 등 그 밖의 객체는 팀이 한쪽 트랙으로만 모은다.

상세: [`migration/README.md`](migration/README.md), [`supabase/migrations/README.md`](../../supabase/migrations/README.md). 보관: [`legacy/README.md`](legacy/README.md), [`supabase/legacy/README.md`](../../supabase/legacy/README.md).

---

## 3. 비동기 프로그래밍 규칙 ⚠️
Expand Down Expand Up @@ -194,6 +210,8 @@ some_async_fn().await;

| 버전 | 날짜 | 변경 내용 |
|------|------|----------|
| 3.3.0 | 2026.04.21 | 활성 `migration/`·`supabase/migrations/` vs 보관 `legacy/` 디렉터리 구조 반영 |
| 3.2.0 | 2026.04.21 | 마이그레이션 트랙 역할 분리(SeaORM vs `supabase/migrations`) 추가 |
| 3.1.0 | 2026.01.12 | 비동기 프로그래밍 규칙(Send 트레이트) 추가 |
| 3.0.0 | 2026.01.08 | 코드 예제 제거, 텍스트 설명 중심으로 재구성 (510줄 → 250줄) |
| 2.0.0 | 2026.01.08 | 문서 대폭 축소 및 구조 개편 (2,338줄 → ~510줄) |
Expand Down
2 changes: 1 addition & 1 deletion packages/api-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["migration", "entity"]
members = ["migration", "legacy", "entity"]

[workspace.dependencies]
# Web Framework
Expand Down
18 changes: 18 additions & 0 deletions packages/api-server/legacy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "migration_legacy"
version = "0.5.1"
edition = "2021"
license = "MIT"
publish = false

[lib]
name = "migration_legacy"
path = "src/lib.rs"

[dependencies]
sea-orm-migration = { workspace = true }
tokio = { workspace = true }
dotenvy = { workspace = true }

[lints.rust]
unused_imports = "deny"
71 changes: 71 additions & 0 deletions packages/api-server/legacy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Database Migrations — **legacy (archived)**

> 이 디렉터리는 **보관**용이다. 크레이트 이름은 `migration_legacy`이며 `cargo run -p migration_legacy`로 실행한다.
> **활성** SeaORM 마이그레이션은 [`../migration/`](../migration/) 을 사용한다.

SeaORM 마이그레이션 및 Supabase 설정 가이드 (과거 본문)

## 마이그레이션 실행

### 1. Rust 마이그레이션 실행

```bash
# 마이그레이션 디렉토리로 이동 (legacy 크레이트)
cd legacy

# 마이그레이션 적용
cargo run -- up

# 마이그레이션 롤백
cargo run -- down

# 마이그레이션 상태 확인
cargo run -- status

# 마이그레이션 새로고침 (down → up)
cargo run -- refresh
```

### 2. Supabase SQL 스크립트 실행

Rust 마이그레이션 후, Supabase Dashboard의 SQL Editor에서 다음 파일들을 순서대로 실행하세요:

1. **Auth 트리거**: `sql/01_auth_trigger_handle_new_user.sql`
- Supabase Auth에서 새 사용자 생성 시 자동으로 users 테이블에 레코드 생성

2. **RLS 정책**: `sql/02_rls_policy_users.sql`
- users 테이블에 Row Level Security 정책 적용
- 모든 사용자가 프로필 조회 가능
- 사용자는 자신의 프로필만 수정/삭제 가능

## 마이그레이션 목록

### m20240101_000001_create_users

- users 테이블 생성
- auth.users 외래키 참조 (CASCADE DELETE)
- updated_at 자동 업데이트 트리거
- 인덱스: email, username, rank

## Entity 생성

마이그레이션 실행 후 SeaORM Entity를 생성합니다:

```bash
# 프로젝트 루트로 이동
cd ..

# entity 디렉토리 생성 (없는 경우)
mkdir -p src/entities

# Entity 생성
sea-orm-cli generate entity \
--database-url "$DATABASE_URL" \
--output-dir src/entities \
--with-serde both
```

## 참고

- [SeaORM Migration 문서](https://www.sea-ql.org/SeaORM/docs/migration/writing-migration/)
- [Supabase RLS 문서](https://supabase.com/docs/guides/auth/row-level-security)
10 changes: 10 additions & 0 deletions packages/api-server/legacy/clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 마이그레이션 크레이트 — SeaORM 매크로/생성 코드의 unwrap 은 허용
# (부모 디렉터리 clippy.toml 대신 이 파일이 적용됨)

disallowed-methods = []

disallowed-macros = [
{ path = "std::println", reason = "tracing::info! / tracing::debug! 를 사용하세요" },
{ path = "std::dbg", reason = "tracing::debug! 를 사용하세요" },
{ path = "std::eprintln", reason = "tracing::warn! 또는 tracing::error! 를 사용하세요" },
]
145 changes: 145 additions & 0 deletions packages/api-server/legacy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
pub use sea_orm_migration::prelude::*;

mod m20230101_000000_local_auth_stub;
mod m20240101_000001_create_users;
mod m20240101_000002_create_categories;
mod m20240101_000003_create_posts;
mod m20240101_000004_create_spots;
mod m20240101_000005_create_solutions;
mod m20240101_000006_create_votes;
mod m20240101_000007_create_comments;
mod m20240101_000008_create_synonyms;
mod m20240101_000009_create_search_logs;
mod m20240101_000010_create_curations;
mod m20240101_000011_create_curation_posts;
mod m20240101_000013_create_badges;
mod m20240101_000014_create_user_badges;
mod m20240101_000015_create_click_logs;
mod m20240101_000016_create_earnings;
mod m20240101_000017_create_settlements;
mod m20240101_000018_create_view_logs;
mod m20240101_000019_seed_badges;
mod m20240101_000020_add_trending_score_to_posts;
mod m20260108_005000_create_point_logs;
mod m20260110_000001_create_subcategories;
mod m20260110_000002_update_categories_and_seed_subcategories;
mod m20260110_000003_alter_spots_subcategory_id;
mod m20260111_000001_add_ai_metadata_to_solutions;
mod m20260112_000001_add_comment_to_solutions;
mod m20260126_000001_add_qna_to_solutions;
mod m20260127_000001_update_solutions_schema;
mod m20260129_000001_remove_product_fields_from_solutions;
mod m20260129_000002_rename_solution_product_name_to_title;
mod m20260130_000001_add_link_type_to_solutions;
mod m20260205_000001_create_processed_batches;
mod m20260205_000002_create_failed_batch_items;
mod m20260205_000003_make_subcategory_nullable;
mod m20260205_000004_make_media_title_nullable;
mod m20260205_000005_rename_media_title_to_title;
mod m20260215_000001_add_created_with_solutions_to_posts;
mod m20260316_000001_create_post_magazines;
mod m20260316_000002_add_post_magazine_id_to_posts;
mod m20260317_000001_add_ai_summary_to_posts;
mod m20260318_000001_create_post_likes;
mod m20260318_000002_create_saved_posts;
mod m20260320_000001_add_system_uncategorized_subcategory;
mod m20260402_000001_add_try_fields_to_posts;
mod m20260402_000001_add_warehouse_fk_posts_solutions;
mod m20260402_000002_create_try_spot_tags;
mod m20260403_000001_backfill_created_with_solutions;
mod m20260406_000001_drop_post_magazines_thread_id;
mod m20260406_000002_add_style_tags_to_posts;
mod m20260407_000001_create_post_magazine_news_references;
mod m20260409_add_image_dimensions;
mod m20260412_000001_add_posts_performance_indexes;
mod m20260419_000001_create_raw_posts_tables;
mod m20260420_000001_add_initial_scraped_at_to_raw_post_sources;
mod m20260501_000001_decouple_auth_users_fk;
mod m20260501_000002_auth_uid_stub;
mod m20260502_000001_enable_extensions;
mod m20260502_000002_warehouse_schema_tables_and_rls;
mod m20260502_000003_public_missing_tables_and_rls;
mod m20260502_000004_embeddings_and_search_similar;
mod m20260502_000005_magazine_approval_and_rpcs;
mod m20260502_000006_backfill_public_columns;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
// Ensure auth.users stub exists before any FK references it (#267).
Box::new(m20230101_000000_local_auth_stub::Migration),
Box::new(m20240101_000001_create_users::Migration),
Box::new(m20240101_000002_create_categories::Migration),
Box::new(m20240101_000003_create_posts::Migration),
Box::new(m20240101_000004_create_spots::Migration),
Box::new(m20240101_000005_create_solutions::Migration),
Box::new(m20240101_000006_create_votes::Migration),
Box::new(m20240101_000007_create_comments::Migration),
Box::new(m20240101_000008_create_synonyms::Migration),
Box::new(m20240101_000009_create_search_logs::Migration),
Box::new(m20240101_000010_create_curations::Migration),
Box::new(m20240101_000011_create_curation_posts::Migration),
Box::new(m20260108_005000_create_point_logs::Migration),
Box::new(m20240101_000013_create_badges::Migration),
Box::new(m20240101_000014_create_user_badges::Migration),
Box::new(m20240101_000015_create_click_logs::Migration),
Box::new(m20240101_000016_create_earnings::Migration),
Box::new(m20240101_000017_create_settlements::Migration),
Box::new(m20240101_000018_create_view_logs::Migration),
Box::new(m20240101_000019_seed_badges::Migration),
Box::new(m20240101_000020_add_trending_score_to_posts::Migration),
Box::new(m20260110_000001_create_subcategories::Migration),
Box::new(m20260110_000002_update_categories_and_seed_subcategories::Migration),
Box::new(m20260110_000003_alter_spots_subcategory_id::Migration),
Box::new(m20260111_000001_add_ai_metadata_to_solutions::Migration),
Box::new(m20260112_000001_add_comment_to_solutions::Migration),
Box::new(m20260126_000001_add_qna_to_solutions::Migration),
Box::new(m20260127_000001_update_solutions_schema::Migration),
Box::new(m20260129_000001_remove_product_fields_from_solutions::Migration),
Box::new(m20260129_000002_rename_solution_product_name_to_title::Migration),
Box::new(m20260130_000001_add_link_type_to_solutions::Migration),
Box::new(m20260205_000001_create_processed_batches::Migration),
Box::new(m20260205_000002_create_failed_batch_items::Migration),
Box::new(m20260205_000003_make_subcategory_nullable::Migration),
Box::new(m20260205_000004_make_media_title_nullable::Migration),
Box::new(m20260205_000005_rename_media_title_to_title::Migration),
Box::new(m20260215_000001_add_created_with_solutions_to_posts::Migration),
Box::new(m20260316_000001_create_post_magazines::Migration),
Box::new(m20260316_000002_add_post_magazine_id_to_posts::Migration),
Box::new(m20260317_000001_add_ai_summary_to_posts::Migration),
Box::new(m20260318_000001_create_post_likes::Migration),
Box::new(m20260318_000002_create_saved_posts::Migration),
Box::new(m20260320_000001_add_system_uncategorized_subcategory::Migration),
Box::new(m20260402_000001_add_try_fields_to_posts::Migration),
Box::new(m20260402_000002_create_try_spot_tags::Migration),
// Warehouse schema must exist before the FK migration references it.
// On prod the schema already exists (Supabase CLI ran earlier); these new
// SeaORM migrations are idempotent no-ops there. On fresh local, these run
// first and create the schema so the FK migration below can succeed.
Box::new(m20260502_000001_enable_extensions::Migration),
Box::new(m20260502_000002_warehouse_schema_tables_and_rls::Migration),
Box::new(m20260402_000001_add_warehouse_fk_posts_solutions::Migration),
Box::new(m20260403_000001_backfill_created_with_solutions::Migration),
Box::new(m20260406_000001_drop_post_magazines_thread_id::Migration),
Box::new(m20260406_000002_add_style_tags_to_posts::Migration),
Box::new(m20260407_000001_create_post_magazine_news_references::Migration),
Box::new(m20260409_add_image_dimensions::Migration),
Box::new(m20260412_000001_add_posts_performance_indexes::Migration),
// #258 raw_posts pipeline tables
Box::new(m20260419_000001_create_raw_posts_tables::Migration),
// #214 Pinterest adapter — initial vs incremental scrape tracking
Box::new(m20260420_000001_add_initial_scraped_at_to_raw_post_sources::Migration),
// PR #273 — auth.uid() stub must run before RLS migrations that use it.
Box::new(m20260501_000002_auth_uid_stub::Migration),
Box::new(m20260501_000001_decouple_auth_users_fk::Migration),
// #202 remaining migrations (warehouse schema already registered above)
Box::new(m20260502_000003_public_missing_tables_and_rls::Migration),
Box::new(m20260502_000004_embeddings_and_search_similar::Migration),
Box::new(m20260502_000005_magazine_approval_and_rpcs::Migration),
Box::new(m20260502_000006_backfill_public_columns::Migration),
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use sea_orm_migration::prelude::*;

/// #258 — create warehouse.raw_post_sources + warehouse.raw_posts.
///
/// This migration mirrors `supabase/migrations/20260419120000_create_raw_posts_tables.sql`
/// This migration mirrors `supabase/legacy/20260419120000_create_raw_posts_tables.sql`
/// so local dev environments using the SeaORM runner stay in sync with
/// Supabase-managed ones.
#[derive(DeriveMigrationName)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ use sea_orm_migration::prelude::*;
/// Why: Path X-1 (#267) requires public schema to stand alone on plain Postgres.
/// `auth.users` only exists when Supabase GoTrue is running; local dev doesn't have it.
/// `public.users` is already kept in sync with `auth.users` via the handle_new_user trigger
/// (see migration/sql/01_auth_trigger_handle_new_user.sql), so `public.users.id` is the
/// (see legacy/sql/01_auth_trigger_handle_new_user.sql), so `public.users.id` is the
/// authoritative application-side identity.
///
/// Idempotent: every step uses `DROP CONSTRAINT IF EXISTS` + `ADD CONSTRAINT` wrapped in
/// `IF EXISTS` table checks. Prod (where old constraints exist) and fresh local (where
/// some tables — content_reports, user_follows, user_tryon_history — haven't been created
/// yet because they still live in migration/sql/*) both converge safely.
/// yet because they still live in legacy/sql/*) both converge safely.
#[derive(DeriveMigrationName)]
pub struct Migration;

Expand Down Expand Up @@ -57,7 +57,7 @@ impl MigrationTrait for Migration {
.await?;

// 3) public.user_follows: follower_id + following_id → public.users(id) CASCADE
// (table comes from migration/sql/04_user_follows.sql — may be absent locally)
// (table comes from legacy/sql/04_user_follows.sql — may be absent locally)
conn.execute_unprepared(
r#"
DO $$
Expand All @@ -81,7 +81,7 @@ impl MigrationTrait for Migration {
.await?;

// 4) public.user_tryon_history.user_id → public.users(id) CASCADE
// (table comes from migration/sql/05_user_tryon_history.sql)
// (table comes from legacy/sql/05_user_tryon_history.sql)
conn.execute_unprepared(
r#"
DO $$
Expand All @@ -99,7 +99,7 @@ impl MigrationTrait for Migration {
.await?;

// 5) public.content_reports.{reporter_id,reviewed_by} → public.users(id)
// (table comes from migration/sql/06_content_reports.sql)
// (table comes from legacy/sql/06_content_reports.sql)
conn.execute_unprepared(
r#"
DO $$
Expand Down
Loading