관리페이지 데이터 전송 구현
Deploy EG-BIM QA Gateway / deploy (push) Successful in 2m4s

This commit is contained in:
root
2026-09-21 14:46:36 +09:00
parent 42516b4f8b
commit 3c10478482
54 changed files with 16055 additions and 45 deletions
@@ -0,0 +1,686 @@
# 로컬 `userfeedback` DB ERD
- 추출 기준: 2026-09-17
- 대상: `abc-user-feedback-mysql-1 / userfeedback`
- 테이블: 33개
- UUID 저장 형식: `BINARY(16)` (애플리케이션 경계에서는 UUID v7 문자열)
- 관계 기준: MySQL `information_schema.KEY_COLUMN_USAGE`에 등록된 물리 FK
## 1. 테넌트·사용자·프로젝트·채널
```mermaid
%%{init: {"themeVariables": {"fontSize": "20px"}, "er": {"fontSize": 20, "minEntityWidth": 220, "minEntityHeight": 120, "entityPadding": 18, "diagramPadding": 24}}}%%
erDiagram
TENANT o|--o{ PROJECTS : "tenant_id"
PROJECTS o|--o{ CHANNELS : "project_id"
PROJECTS o|--o{ ROLES : "project_id"
USERS o|--o{ MEMBERS : "user_id"
ROLES o|--o{ MEMBERS : "role_id"
PROJECTS ||--o{ CATEGORIES : "project_id"
CHANNELS o|--o{ FIELDS : "channel_id"
FIELDS o|--o{ OPTIONS : "field_id"
PROJECTS o|--o{ API_KEYS : "project_id"
PROJECTS o|--o| ISSUE_TRACKERS : "project_id"
PROJECTS o|--o{ WEBHOOKS : "project_id"
WEBHOOKS o|--o{ EVENTS : "webhook_id"
EVENTS ||--o{ EVENTS_CHANNELS_CHANNELS : "events_id"
CHANNELS ||--o{ EVENTS_CHANNELS_CHANNELS : "channels_id"
TENANT {
uuid id PK
string site_name
string description
bool use_email
text allow_domains
bool use_o_auth
json oauth_config
datetime created_at
datetime updated_at
datetime deleted_at
}
USERS {
uuid id PK
string email "UK with sign_up_method"
string name
string department
enum state
string hash_password
enum type
enum sign_up_method
string oauth_subject "UK with tenant and method"
string oauth_tenant_id
json secondary_emails
string phone_number
datetime created_at
datetime updated_at
datetime deleted_at
}
PROJECTS {
uuid id PK
uuid tenant_id FK
string name UK
string description
string timezone
json issue_admin_user_ids
bool show_feedback_comment_author_name
datetime created_at
datetime updated_at
datetime deleted_at
}
CHANNELS {
uuid id PK
uuid project_id FK
string name "UK with project_id"
string description
json image_config
int feedback_search_max_days
datetime created_at
datetime updated_at
datetime deleted_at
}
ROLES {
uuid id PK
uuid project_id FK
string name
text permissions
datetime created_at
datetime updated_at
datetime deleted_at
}
MEMBERS {
uuid id PK
uuid role_id FK "UK with user_id"
uuid user_id FK "UK with role_id"
datetime created_at
datetime updated_at
datetime deleted_at
}
CATEGORIES {
uuid id PK
uuid project_id FK
string name "UK with project_id"
datetime created_at
datetime updated_at
datetime deleted_at
}
FIELDS {
uuid id PK
uuid channel_id FK
uuid ai_field_template_id FK
string name "UK with channel_id"
string key "UK with channel_id"
string description
enum format
enum status
enum property
int order
json ai_field_target_keys
bool ai_field_auto_processing
datetime created_at
datetime updated_at
datetime deleted_at
}
OPTIONS {
uuid id PK
uuid field_id FK
string name
string key
datetime created_at
datetime updated_at
datetime deleted_at
}
API_KEYS {
uuid id PK
uuid project_id FK
string value UK
datetime created_at
datetime updated_at
datetime deleted_at
}
ISSUE_TRACKERS {
uuid id PK
uuid project_id FK,UK
json data
datetime created_at
datetime updated_at
datetime deleted_at
}
WEBHOOKS {
uuid id PK
uuid project_id FK
string name
string url
string token
enum status
datetime created_at
datetime updated_at
datetime deleted_at
}
EVENTS {
uuid id PK
uuid webhook_id FK
enum status
enum type
datetime created_at
datetime updated_at
datetime deleted_at
}
EVENTS_CHANNELS_CHANNELS {
uuid events_id PK,FK
uuid channels_id PK,FK
}
```
## 2. 피드백·댓글·이슈
```mermaid
%%{init: {"themeVariables": {"fontSize": "20px"}, "er": {"fontSize": 20, "minEntityWidth": 220, "minEntityHeight": 120, "entityPadding": 18, "diagramPadding": 24}}}%%
erDiagram
CHANNELS o|--o{ FEEDBACKS : "channel_id"
FEEDBACKS ||--o{ FEEDBACK_COMMENTS : "feedback_id"
FEEDBACK_COMMENTS ||--o{ FEEDBACK_COMMENT_ATTACHMENTS : "comment_id"
PROJECTS o|--o{ ISSUES : "project_id"
CATEGORIES o|--o{ ISSUES : "category_id"
FEEDBACKS ||--o{ FEEDBACKS_ISSUES_ISSUES : "feedbacks_id"
ISSUES ||--o{ FEEDBACKS_ISSUES_ISSUES : "issues_id"
ISSUES ||--o{ ISSUE_INTERNAL_MEMOS : "issue_id"
CHANNELS o|--o{ FEEDBACK_STATISTICS : "channel_id"
PROJECTS o|--o{ ISSUE_STATISTICS : "project_id"
ISSUES o|--o{ FEEDBACK_ISSUE_STATISTICS : "issue_id"
FEEDBACKS {
uuid id PK
uuid channel_id FK
json data
datetime admin_first_read_at
uuid admin_first_read_by "logical users.id"
string source_namespace "UK with source_record_id"
string source_record_id
datetime created_at
datetime updated_at
datetime deleted_at
}
FEEDBACK_COMMENTS {
uuid id PK
uuid feedback_id FK
string author_id
string author_tenant_id
string author_name
string author_type
text content
bool is_internal
string comment_type
string idempotency_key "UK with feedback_id"
string source_namespace "UK with feedback_id and record"
string source_record_id
datetime created_at
datetime updated_at
datetime deleted_at
}
FEEDBACK_COMMENT_ATTACHMENTS {
uuid id PK
uuid comment_id FK
string original_file_name
string storage_key "UK with storage_bucket"
string storage_bucket
string mime_type
bigint file_size
string source_namespace "UK with source_record_id"
string source_record_id
datetime created_at
datetime updated_at
datetime deleted_at
}
ISSUES {
uuid id PK
uuid project_id FK
uuid category_id FK
uuid issue_admin_user_id "logical users.id"
uuid admin_first_read_by "logical users.id"
uuid hold_set_by "logical users.id"
uuid hold_released_by "logical users.id"
string name "UK with project_id"
string description
enum status
string external_issue_id "UK with provider and project"
string external_issue_provider
string external_issue_url
string external_issue_status
string external_issue_sync_status
text external_issue_sync_error
datetime external_issue_synced_at
int feedback_count
text internal_memo
datetime admin_first_read_at
text hold_reason
datetime hold_set_at
datetime hold_released_at
string hold_resume_status
datetime completed_at
datetime created_at
datetime updated_at
datetime deleted_at
}
FEEDBACKS_ISSUES_ISSUES {
uuid feedbacks_id PK,FK
uuid issues_id PK,FK
}
ISSUE_INTERNAL_MEMOS {
uuid id PK
uuid issue_id FK
uuid author_id "logical users.id"
uuid deleted_by_user_id "logical users.id"
string author_name
text content
string deleted_by_name
datetime created_at
datetime updated_at
datetime deleted_at
}
FEEDBACK_STATISTICS {
uuid id PK
uuid channel_id FK
date date "UK with channel_id"
int count
}
ISSUE_STATISTICS {
uuid id PK
uuid project_id FK
date date "UK with project_id"
int count
}
FEEDBACK_ISSUE_STATISTICS {
uuid id PK
uuid issue_id FK
date date "UK with issue_id"
int feedback_count
}
CHANNELS {
uuid id PK
}
PROJECTS {
uuid id PK
}
CATEGORIES {
uuid id PK
}
```
## 3. AI·이력·멱등성·운영
```mermaid
%%{init: {"themeVariables": {"fontSize": "20px"}, "er": {"fontSize": 20, "minEntityWidth": 220, "minEntityHeight": 120, "entityPadding": 18, "diagramPadding": 24}}}%%
erDiagram
PROJECTS ||--o{ AI_FIELD_TEMPLATES : "project_id"
PROJECTS ||--o| AI_INTEGRATIONS : "project_id"
PROJECTS ||--o{ AI_USAGES : "project_id"
CHANNELS ||--o{ AI_ISSUE_TEMPLATES : "channel_id"
AI_FIELD_TEMPLATES o|--o{ FIELDS : "ai_field_template_id"
AI_FIELD_TEMPLATES {
uuid id PK
uuid project_id FK
string title
text prompt
string model
float temperature
datetime created_at
datetime updated_at
datetime deleted_at
}
AI_INTEGRATIONS {
uuid id PK
uuid project_id FK,UK
enum provider
string api_key
string endpoint_url
text system_prompt
int token_threshold
float notification_threshold
datetime created_at
datetime updated_at
datetime deleted_at
}
AI_ISSUE_TEMPLATES {
uuid id PK
uuid channel_id FK
json target_field_keys
text prompt
bool is_enabled
string model
float temperature
int data_reference_amount
datetime created_at
datetime updated_at
datetime deleted_at
}
AI_USAGES {
uuid id PK
uuid project_id FK
int year
int month
int day
enum category
enum provider
int used_tokens
datetime created_at
datetime updated_at
datetime deleted_at
}
HISTORIES {
uuid id PK
uuid user_id "logical users.id"
enum entity_name
string entity_id
enum action
json entity
datetime created_at
datetime updated_at
datetime deleted_at
}
IDEMPOTENCY_RECORDS {
uuid id PK
string consumer "UK with idempotency_key"
string idempotency_key
string request_hash
string resource_type
uuid resource_id "logical polymorphic id"
datetime expires_at
datetime created_at
datetime updated_at
datetime deleted_at
}
NOTIFICATION_DELIVERIES {
uuid id PK
string idempotency_key UK
string event_type
string event_id
enum status
int attempts
text last_error
datetime sent_at
string target_type
string target_id
uuid project_id "logical projects.id"
uuid channel_id "logical channels.id"
uuid feedback_id "logical feedbacks.id"
datetime created_at
datetime updated_at
datetime deleted_at
}
CODES {
uuid id PK
enum type
string key
string code
string data
bool is_verified
datetime expired_at
int try_count
datetime created_at
datetime updated_at
datetime deleted_at
}
SCHEDULER_LOCKS {
enum lock_type PK
string server_id
datetime timestamp
}
MIGRATIONS {
int id PK
bigint timestamp
string name
}
PROJECTS {
uuid id PK
}
CHANNELS {
uuid id PK
}
FIELDS {
uuid id PK
}
```
## 4. 주요 물리 Unique Key
| 테이블 | 컬럼 조합 |
| ------------------------------ | --------------------------------------------------------------------------------- |
| `projects` | `name` |
| `channels` | `name, project_id` |
| `categories` | `project_id, name` |
| `fields` | `key, channel_id`; `name, channel_id` |
| `members` | `role_id, user_id` |
| `users` | `email, sign_up_method`; `oauth_subject, oauth_tenant_id, sign_up_method` |
| `feedbacks` | `source_namespace, source_record_id` |
| `feedback_comments` | `feedback_id, idempotency_key`; `feedback_id, source_namespace, source_record_id` |
| `feedback_comment_attachments` | `storage_bucket, storage_key`; `source_namespace, source_record_id` |
| `issues` | `name, project_id`; `external_issue_provider, project_id, external_issue_id` |
| `idempotency_records` | `consumer, idempotency_key` |
| `ai_integrations` | `project_id` |
| `issue_trackers` | `project_id` |
## 5. 물리 FK가 없는 논리 참조
다음 컬럼은 UUID 또는 식별자를 저장하지만 현재 MySQL FK 제약은 없다.
- `feedbacks.admin_first_read_by → users.id`
- `issues.issue_admin_user_id → users.id`
- `issues.admin_first_read_by → users.id`
- `issues.hold_set_by → users.id`
- `issues.hold_released_by → users.id`
- `issue_internal_memos.author_id → users.id`
- `issue_internal_memos.deleted_by_user_id → users.id`
- `histories.user_id → users.id`
- `idempotency_records.resource_id`는 다형성 리소스 참조
- `notification_deliveries.project_id/channel_id/feedback_id`는 이벤트 스냅샷 참조
## 6. DB 설계 이유
### 6.1 현재 규모에서는 단일 MySQL이 가장 단순하고 안정적이다
예상 데이터가 댓글과 피드백을 합쳐 연간 약 5만 건이라면, 단일 MySQL 인스턴스가 처리하기에 충분히 작은 규모다. 이 단계에서 물리적 샤딩이나 여러 DB로 데이터를 나누면 성능 이득보다 분산 트랜잭션, 데이터 정합성, 장애 복구 및 운영 복잡성이 더 커진다.
따라서 현재 설계는 다음 원칙을 사용한다.
- 업무 데이터의 최종 권위는 MySQL 하나로 유지한다.
- Redis는 캐시·멱등성 선점·일시적 잠금에 사용하되 최종 정합성의 기준으로 사용하지 않는다.
- 실제 CPU, 디스크 I/O, Buffer Pool, Lock wait 또는 목록 조회 p95가 허용 범위를 지속해서 넘을 때 읽기 복제본이나 기능별 분리를 검토한다.
- UUID를 사용해 향후 여러 서버에서 동시에 데이터를 생성하더라도 PK 발급을 중앙 DB의 `AUTO_INCREMENT`에 의존하지 않는다.
즉, 분산 환경에서 사용할 수 있는 식별자와 중복 방지 구조는 미리 갖추되, 데이터 저장소 자체는 현재 규모에 맞게 단순하게 유지한 설계다.
### 6.2 UUID v7을 모든 주요 엔티티의 PK로 사용한다
`tenant`, `projects`, `channels`, `feedbacks`, `issues`, 댓글과 첨부파일 등 주요 엔티티의 PK는 UUID v7이며 MySQL에는 `BINARY(16)`으로 저장한다.
이 방식을 선택한 이유는 다음과 같다.
- 여러 API 인스턴스가 DB 왕복 없이 충돌 가능성이 매우 낮은 ID를 직접 생성할 수 있다.
- UUID v7은 시간 순서 특성이 있어 UUID v4보다 B-Tree 인덱스의 무작위 페이지 분할과 단편화를 줄일 수 있다.
- 외부 URL과 API에서 동일 UUID를 사용하므로 내부 ID와 외부 ID를 변환하는 조회 로직이 필요 없다.
- 프로젝트나 채널을 다른 DB 또는 서비스로 이동하더라도 ID 충돌 없이 그대로 유지할 수 있다.
- MySQL에서는 문자열 `CHAR(36)` 대신 `BINARY(16)`을 사용해 PK 및 FK 인덱스 크기를 줄인다.
UUID는 DB 관리 도구에서 깨진 문자처럼 보일 수 있지만 데이터 손상이 아니라 16바이트 바이너리를 문자로 표시해서 발생하는 현상이다. 조회할 때는 `BIN_TO_UUID(id)`를 사용해야 한다.
### 6.3 `tenant → project → channel` 계층으로 데이터 소유 범위를 구분한다
데이터의 기본 소유 계층은 다음과 같다.
```text
Tenant
└── Project
├── Channel
│ ├── Field / Option
│ └── Feedback
├── Role / Member
├── Category / Issue
├── API Key
└── Webhook / Integration
```
- `tenant`는 고객 또는 조직의 최상위 경계다.
- `projects`는 권한, API Key, 이슈 및 외부 연동의 관리 단위다.
- `channels`는 프로젝트 안에서 피드백 입력 양식과 피드백 데이터를 분리하는 단위다.
- 프로젝트별 역할은 `roles`, 사용자 할당은 `members`로 분리해 한 사용자가 여러 프로젝트에서 서로 다른 권한을 가질 수 있다.
이 계층은 현재 단일 DB에서도 데이터 격리를 명확하게 하고, 향후 필요하면 `tenant_id` 또는 `project_id`를 기준으로 데이터 배치와 분할 범위를 결정할 수 있게 한다.
### 6.4 피드백 본문은 JSON, 검색·관계 기준은 정규 컬럼으로 분리한다
채널마다 입력 필드가 다르므로 `feedbacks.data`는 JSON으로 저장한다. 필드 정의는 `fields`, 선택지는 `options`에서 관리한다.
모든 입력값을 고정 컬럼으로 만들지 않은 이유는 다음과 같다.
- 채널별 양식 변경 시 매번 DB migration을 만들 필요가 없다.
- 사용자 정의 필드와 AI 생성 필드를 동일한 구조로 처리할 수 있다.
- 기존 피드백 데이터를 유지하면서 새 필드를 추가하거나 비활성화할 수 있다.
반면 검색과 무결성에 자주 사용하는 값은 JSON에 넣지 않고 정규 컬럼으로 둔다.
- `id`, `channel_id`, `created_at`, `deleted_at`
- 외부 중복 방지용 `source_namespace`, `source_record_id`
- 관리자 최초 열람 정보
특정 JSON 필드 검색이 반복적으로 느려질 때만 Generated Column과 인덱스를 추가한다. 연간 5만 건 수준에서 사용 여부가 확인되지 않은 모든 JSON 키를 미리 인덱싱하면 쓰기 비용과 운영 복잡성만 증가하기 때문이다.
### 6.5 댓글과 첨부파일을 별도 엔티티로 분리한다
댓글은 `feedback_comments`, 댓글 첨부파일은 `feedback_comment_attachments`로 분리했다.
- 하나의 피드백에 여러 댓글을 시간순으로 저장할 수 있다.
- 공개 댓글과 내부 댓글을 `is_internal`로 구분할 수 있다.
- 첨부파일 메타데이터만 DB에 두고 실제 파일은 로컬 볼륨 또는 Object Storage에 저장할 수 있다.
- 댓글 삭제와 첨부파일 정리 정책을 피드백 본문과 독립적으로 적용할 수 있다.
- 댓글 및 첨부파일도 각각 외부 원본 식별자를 가져 재전송 중복을 막을 수 있다.
파일 자체를 DB BLOB으로 저장하지 않는 것은 DB 백업 크기와 I/O 부하를 줄이고, 향후 R2/S3 같은 Object Storage로 전환하기 쉽게 하기 위한 선택이다.
### 6.6 피드백과 이슈는 N:M 관계로 설계한다
`feedbacks_issues_issues` 연결 테이블을 두어 하나의 피드백을 여러 이슈와 연결하고, 하나의 이슈가 여러 피드백을 묶을 수 있게 했다.
예를 들어 여러 사용자가 같은 장애를 각각 제보하면 피드백은 개별 기록으로 유지하면서 하나의 이슈로 묶어 처리할 수 있다. 연결 테이블의 복합 PK `(feedbacks_id, issues_id)`는 동일 연결이 중복 저장되는 것을 DB 차원에서 방지한다.
이슈의 외부 연동 식별자는 다음 조합으로 유일성을 보장한다.
```text
external_issue_provider + project_id + external_issue_id
```
따라서 Gitea, Jira, GitHub가 같은 숫자 이슈 ID를 사용해도 서로 충돌하지 않으며 프로젝트가 다르면 독립적으로 관리된다.
### 6.7 중복 요청 방지는 업무 Unique Key와 멱등성 레코드를 함께 사용한다
네트워크 재시도나 외부 시스템의 중복 전송으로 같은 데이터가 여러 번 생성되지 않도록 두 계층으로 방어한다.
1. 업무 원본 중복 방지
- `source_namespace + source_record_id`
- 원본 시스템 안에서 같은 레코드를 다시 보내면 기존 데이터를 식별한다.
2. HTTP 요청 재시도 방지
- `consumer + idempotency_key`
- 같은 소비자의 동일 요청 키는 `idempotency_records`에서 한 번만 처리한다.
Redis가 사용 가능한 경우 짧은 선점 잠금으로 동시 요청을 빠르게 차단하지만, 최종 중복 방지는 MySQL Unique Key가 담당한다. 따라서 Redis가 재시작되거나 일시적으로 중단되어도 중복 데이터가 확정 저장되지 않는다.
### 6.8 Soft Delete와 이력 테이블을 사용한다
대부분의 업무 테이블에는 `created_at`, `updated_at`, `deleted_at`이 있다.
- 일반 삭제는 `deleted_at`을 기록하는 Soft Delete로 처리해 실수로 삭제한 데이터를 복구할 수 있다.
- FK의 `ON DELETE CASCADE`는 프로젝트나 피드백을 실제로 물리 삭제하는 명시적 정리 작업에서 하위 데이터를 함께 제거한다.
- `histories`는 엔티티 종류, 동작, 당시 데이터를 JSON으로 기록해 관리자 변경 이력을 추적한다.
Soft Delete만 적용하면 Unique Key의 재사용 정책이 복잡해질 수 있으므로 프로젝트명, 채널명처럼 재사용 여부가 중요한 값은 운영 정책과 함께 관리해야 한다.
### 6.9 집계 통계는 원본 테이블과 분리한다
대시보드가 매번 전체 피드백과 이슈를 집계하지 않도록 다음 통계 테이블을 둔다.
- `feedback_statistics`: 채널·일자별 피드백 수
- `issue_statistics`: 프로젝트·일자별 이슈 수
- `feedback_issue_statistics`: 이슈·일자별 연결 피드백 수
원본 데이터는 `feedbacks``issues`이며 통계 테이블은 재생성 가능한 파생 데이터다. `scheduler_locks`는 여러 API 인스턴스가 같은 통계 작업을 동시에 실행하는 것을 막는다.
연간 5만 건에서는 원본 직접 집계도 가능하지만, 대시보드 요청이 늘어날 때의 반복 스캔을 줄이고 응답시간을 안정적으로 유지하기 위해 집계 구조를 분리했다.
### 6.10 AI와 외부 연동 설정은 업무 데이터와 분리한다
AI 기능은 다음과 같이 역할별로 분리했다.
- `ai_integrations`: 프로젝트별 AI 공급자와 연결 설정
- `ai_field_templates`: AI 필드 생성 프롬프트
- `ai_issue_templates`: 채널별 이슈 추천 설정
- `ai_usages`: 프로젝트별 사용량
웹훅도 `webhooks`, `events`, `events_channels_channels`로 분리해 하나의 웹훅에 여러 이벤트와 채널을 연결할 수 있다. 설정과 실행 데이터를 분리하면 공급자 변경, 기능 비활성화, 사용량 집계가 피드백 원본 구조에 영향을 주지 않는다.
### 6.11 일부 사용자·이벤트 참조에 물리 FK를 두지 않은 이유
관리자 사용자, 이력 작성자, 알림 대상처럼 삭제 이후에도 당시 값을 보존해야 하거나 여러 엔티티를 참조할 수 있는 컬럼에는 물리 FK가 없다.
- 사용자가 삭제되어도 과거 이력과 처리 담당자 기록은 남아야 한다.
- `idempotency_records.resource_id`는 피드백, 댓글 등 여러 리소스를 가리킬 수 있어 단일 FK를 만들 수 없다.
- `notification_deliveries`는 발송 당시의 이벤트 스냅샷이므로 원본 삭제가 알림 이력을 삭제하게 만들지 않는다.
이 선택은 보존성과 결합도 측면에서는 유리하지만 고아 참조가 생길 수 있다. 따라서 애플리케이션 검증과 정기 무결성 점검이 필요하다. 반드시 강한 정합성이 필요한 소유 관계에는 계속 물리 FK를 사용한다.
### 6.12 인덱스는 조회 패턴과 유일성 중심으로 구성한다
현재 인덱스는 다음 용도에 집중한다.
- FK 조인: `project_id`, `channel_id`, `feedback_id`, `issue_id`
- 목록 조회: 생성일, 삭제 여부, 상태
- 이름 중복 방지: 프로젝트명, 프로젝트 내 채널·카테고리·이슈명
- 외부 원본 중복 방지: source identity
- 외부 이슈 중복 방지: provider/project/external ID
- 요청 재시도 방지: idempotency key
UUID v7과 `(created_at, id)` 조합은 같은 시각에 생성된 데이터도 안정적으로 정렬하고 cursor pagination의 마지막 위치를 정확하게 표현한다. 데이터가 증가한 뒤에는 추측으로 인덱스를 늘리기보다 Slow Query Log와 실제 실행 계획을 기준으로 추가한다.
## 7. 재추출 필요 조건
아래 작업 후에는 이 문서를 다시 생성해야 한다.
1. TypeORM UUID migration 적용 또는 변경
2. Secretary Alembic migration 적용으로 `support_*` 테이블 생성
3. FK·Unique Key·인덱스 변경
4. 스테이징/운영 스키마와 로컬 스키마 비교