# POP 화면 배포서버 마이그레이션 가이드

> **작성일**: 2026-03-23
> **목적**: 로컬(탑씰 COMPANY_7) POP 화면 5종을 배포서버 COMPANY_21(테스트회사)로 복사
> **대상 화면**: 4173, 4479, 4480, 4576, 4577
> **주의**: DB 작업 전 반드시 백업 후 진행

---

## 0. 개념 정리 (먼저 읽기)

### PopDeployModal이란?

POP 관리 화면 내 내장된 **화면 배포 도구**입니다.

- **접근**: POP 디자이너 > 카테고리 트리 > 그룹 우클릭 또는 배포 버튼
- **하는 일**:
  1. 선택한 화면들을 다른 회사 계정으로 복사 (`screen_definitions` 새 행 생성)
  2. POP 레이아웃 JSON 복사 (`screen_layouts_pop`)
  3. **layout_json 내 화면 ID 참조 자동 리매핑** (screenId, cartScreenId, sourceScreenId, targetScreenId)
  4. 카테고리 그룹 구조 생성 (`screen_groups`, `screen_group_screens`)
  5. numberingRuleId 자동 제거 (회사별 고유값이므로)
- **제약**: 같은 서버 안에서만 동작 (로컬 → 배포 서버 간 복사 불가)
- **권장 사용 시점**: 배포 DB COMPANY_7에 화면이 먼저 세팅된 후, 같은 배포 서버 내 테스트 계정으로 복사할 때

### 마이그레이션 전체 흐름

```
[로컬 DB / COMPANY_7] ──── SQL 직접 복사 ────→ [배포 DB / COMPANY_7]
                                                        │
                                                  PopDeployModal
                                                  (배포서버 내)
                                                        │
                                                        ↓
                                                [배포 DB / COMPANY_21]
                                                   (테스트 환경)
```

---

## 1. 현황 요약

### 1-1. 환경 정보

| 구분 | 로컬 DB | 배포 DB |
|------|---------|---------|
| Host | 39.117.244.52:11132 | 211.115.91.141:11134 |
| Database | plm | plm |
| 소스 회사 | COMPANY_7 (탑씰) | - |
| 1차 타겟 | - | COMPANY_7 (탑씰, SQL 직접 삽입) |
| 2차 타겟 | - | COMPANY_21 (테스트회사, PopDeployModal) |

### 1-2. 복사 대상 화면

| 화면 ID | screen_code | screen_name | 역할 |
|---------|-------------|-------------|------|
| 4479 | COMPANY_7_179 | 홈 | POP 메인 홈 화면 |
| 4576 | COMPANY_7_194 | 입고메뉴 | 입고 카테고리 메뉴 |
| 4173 | COMPANY_7_169 | 구매입고 담기 | 구매입고 항목 선택/담기 |
| 4577 | COMPANY_7_195 | 구매입고 장바구니 | 장바구니 확인/입고 확정 |
| 4480 | COMPANY_7_180 | MES공정 | MES 생산실적 관리 |

### 1-3. POP 카테고리 구조 (screen_groups)

```
탑씰 (id:3134, code:TOPSSEAL)
├── 홈 #4479
├── 입고관리 (id:3216, code:INBOUND_MENU)
│   ├── 입고메뉴 #4576
│   └── 구매입고 (id:3221, code:PURCHASE RECEIPT)
│       ├── 구매입고 담기 #4173
│       └── 구매입고 장바구니 #4577
└── 생산실적 (id:3220, code:PRODUCTION RESULTS)
    └── MES공정 #4480
```

### 1-4. 화면 간 상호참조 (layout_json 내부)

| 출발 화면 | 참조 방식 | 대상 화면 | JSON 키 |
|----------|----------|----------|---------|
| 4479 홈 | navigate | 4480 MES공정 | `screenId: "4480"` |
| 4479 홈 | navigate | 4576 입고메뉴 | `screenId: "4576"` |
| 4576 입고메뉴 | navigate | 4173 구매입고 | `screenId: "4173"` |
| 4173 구매입고 | cart-save | 4577 장바구니 | `cartScreenId: "4577"` |
| 4577 장바구니 | source | 4173 구매입고 | `sourceScreenId: 4173` (숫자) |
| 4577 장바구니 | navigate | 4173 구매입고 | `targetScreenId: "4173"` |

> PopDeployModal을 사용하면 이 참조들이 모두 자동 리매핑됩니다.

---

## 2. 배포 DB 현황 점검 결과

### 2-1. 테이블 누락 상태

| 테이블 | 로컬 | 배포 | MES/기능 의존도 |
|--------|------|------|----------------|
| work_order_process | O (37컬럼) | **없음** | MES공정 화면 전체 동작 불가 |
| process_work_result | O (35컬럼) | **없음** | MES 체크리스트 기능 불가 |
| work_order_process_log | O (12컬럼+트리거) | **없음** | 공정 변경 이력 로깅 불가 |
| cart_items | O (16컬럼) | **없음** | 구매입고 장바구니 전체 불가 |

### 2-2. 컬럼 누락 상태

| 테이블 | 누락 컬럼 | 영향 |
|--------|----------|------|
| work_instruction | `reason`, `completed_qty` | MES 완료수량 업데이트 실패 |

### 2-3. COMPANY_21 (테스트회사) 현황

| 항목 | 상태 |
|------|------|
| 회사 존재 | O (active) |
| 기존 POP 레이아웃 | 없음 (깨끗한 상태) |
| 로그인 가능 계정 | **0개 (계정 없음!)** |
| 기존 화면 수 | 23개 (일반 ERP 화면들) |

> **중요**: COMPANY_21에는 현재 등록된 사용자가 없습니다.
> 테스트 전에 사용자 계정을 먼저 생성해야 로그인 및 POP 테스트가 가능합니다.

### 2-4. COMPANY_7 (탑씰) 배포 서버 현황

- 기존 POP 레이아웃: `screen_id 4114` (테스트용 1개만 존재)
- 로그인 계정: `topseal_admin`, `topseal_admin2`, `topseal_user`, `test1`, `test2`
- 5개 화면(4173, 4479, 4480, 4576, 4577) 모두 배포 DB에 없음 → 안전하게 삽입 가능

---

## 3. 누락 테이블/컬럼 추가 방법

### 3-1. Vexplor DDL 시스템으로 가능한 작업

Vexplor에는 **관리자 DDL 실행 시스템**이 내장되어 있습니다.

- **접근**: 관리자 > 시스템관리 > 테이블관리 (`/admin/systemMng/tableMngList`)
- **가능한 작업**:
  - 테이블 생성 (`POST /api/ddl/tables`)
  - 컬럼 추가 (`POST /api/ddl/tables/:tableName/columns`)
  - 생성 시 `table_type_columns` 메타데이터 자동 등록
- **권한**: 슈퍼 어드민 계정 (`company_code = '*'`)만 사용 가능
- **불가능한 것**: 트리거 함수 생성 (이건 psql 직접 실행 필요)

### 3-2. 작업 분류

| 작업 | 방법 | 비고 |
|------|------|------|
| work_order_process 생성 | **psql 직접 실행** | 컬럼 37개 + 인덱스 7개, UI 입력보다 SQL이 효율적 |
| process_work_result 생성 | **psql 직접 실행** | 컬럼 35개 |
| cart_items 생성 | **psql 직접 실행** | 컬럼 16개 + 인덱스 5개 |
| work_order_process_log 생성 | **psql 직접 실행** | 트리거 함수 포함 필수 |
| work_instruction 컬럼 추가 | DDL UI 또는 psql | 컬럼 2개, 어느 방법이든 가능 |
| table_type_columns 메타데이터 | psql COPY 명령 | 로컬에서 추출 후 배포에 삽입 |

> **결론**: DDL UI는 컬럼 추가(`work_instruction`) 정도에 활용하고,
> 테이블 생성은 모두 psql SQL 직접 실행이 현실적입니다.

---

## 4. 실행 절차 (단계별)

### STEP 0: 배포 DB 백업

```sql
-- 배포 DB에서 실행
CREATE TABLE backup_20260323_screen_definitions AS
  SELECT * FROM screen_definitions WHERE company_code = 'COMPANY_7';
CREATE TABLE backup_20260323_screen_layouts_pop AS
  SELECT * FROM screen_layouts_pop WHERE company_code = 'COMPANY_7';
CREATE TABLE backup_20260323_screen_groups AS
  SELECT * FROM screen_groups WHERE company_code = 'COMPANY_7';
CREATE TABLE backup_20260323_screen_group_screens AS
  SELECT * FROM screen_group_screens WHERE company_code = 'COMPANY_7';
```

---

### STEP 1: 누락 테이블 생성 (배포 DB에서 psql 실행)

```bash
PGPASSWORD='$DB_PASSWORD' psql -h 211.115.91.141 -p 11134 -U postgres -d plm
```

#### 1-1. work_order_process

```sql
CREATE TABLE work_order_process (
  id character varying(500) NOT NULL DEFAULT (gen_random_uuid())::text,
  created_date timestamp without time zone DEFAULT now(),
  updated_date timestamp without time zone DEFAULT now(),
  writer character varying(255),
  company_code character varying(255),
  wo_id character varying(500),
  seq_no character varying(255),
  process_code character varying(255),
  process_name character varying(255),
  is_required character varying(255),
  is_fixed_order character varying(255),
  standard_time character varying(255),
  status character varying(255),
  accepted_by character varying(255),
  accepted_at character varying(255),
  started_at character varying(255),
  completed_at character varying(255),
  plan_qty character varying(255),
  input_qty character varying(255),
  good_qty character varying(255),
  defect_qty character varying(255),
  equipment_code character varying(255),
  remark character varying(500),
  paused_at character varying(500),
  total_paused_time character varying(500) DEFAULT '0',
  routing_detail_id character varying(500),
  actual_work_time character varying(500) DEFAULT NULL::character varying,
  completed_by character varying(500) DEFAULT NULL::character varying,
  total_production_qty character varying(500),
  defect_detail character varying(500),
  result_note character varying(500),
  result_status character varying(500) DEFAULT 'draft'::character varying,
  attachments character varying(500),
  parent_process_id character varying(500) DEFAULT NULL::character varying,
  concession_qty character varying(500) DEFAULT '0'::character varying,
  is_rework character varying(500) DEFAULT 'N'::character varying,
  rework_source_id character varying(500) DEFAULT NULL::character varying,
  CONSTRAINT work_order_process_pkey PRIMARY KEY (id)
);

CREATE INDEX idx_wop_company ON work_order_process (company_code);
CREATE INDEX idx_wop_wo_id ON work_order_process (wo_id);
CREATE INDEX idx_wop_wo_id_seq_no ON work_order_process (wo_id, seq_no);
CREATE INDEX idx_wop_process ON work_order_process (company_code, process_code);
CREATE INDEX idx_wop_status ON work_order_process (company_code, process_code, status);
CREATE INDEX idx_wop_parent_process_id ON work_order_process (parent_process_id);
```

#### 1-2. process_work_result

```sql
CREATE TABLE process_work_result (
  id character varying(500) NOT NULL DEFAULT (gen_random_uuid())::text,
  created_date timestamp without time zone DEFAULT now(),
  updated_date timestamp without time zone DEFAULT now(),
  writer character varying(500) DEFAULT NULL::character varying,
  company_code character varying(500),
  work_order_process_id character varying(500),
  source_work_item_id character varying(500),
  source_detail_id character varying(500),
  work_phase character varying(500),
  item_title character varying(500),
  item_sort_order character varying(500),
  detail_content character varying(500),
  detail_type character varying(500),
  detail_sort_order character varying(500),
  is_required character varying(500),
  inspection_code character varying(500),
  inspection_method character varying(500),
  unit character varying(500),
  lower_limit character varying(500),
  upper_limit character varying(500),
  input_type character varying(500),
  lookup_target character varying(500),
  display_fields character varying(500),
  duration_minutes character varying(500),
  status character varying(500),
  result_value character varying(500),
  is_passed character varying(500),
  remark character varying(500),
  recorded_by character varying(500),
  recorded_at character varying(500),
  started_at character varying(500) DEFAULT NULL::character varying,
  group_started_at character varying(500) DEFAULT NULL::character varying,
  group_paused_at character varying(500) DEFAULT NULL::character varying,
  group_total_paused_time character varying(500) DEFAULT NULL::character varying,
  group_completed_at character varying(500) DEFAULT NULL::character varying,
  CONSTRAINT process_work_result_pkey PRIMARY KEY (id)
);
```

#### 1-3. work_order_process_log + 트리거

```sql
-- 로그 테이블
CREATE TABLE work_order_process_log (
  log_id SERIAL PRIMARY KEY,
  operation_type character varying(10) NOT NULL,
  original_id character varying(100),
  changed_column character varying(100),
  old_value text,
  new_value text,
  changed_by character varying(50),
  changed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
  ip_address character varying(50),
  user_agent text,
  full_row_before jsonb,
  full_row_after jsonb
);

-- 트리거 함수 (로컬 DB에서 정확한 정의 먼저 추출)
-- 로컬에서: SELECT pg_get_functiondef(oid) FROM pg_proc WHERE proname = 'work_order_process_log_trigger_func';
-- 추출한 함수 정의를 아래에 붙여넣기

-- 트리거 등록 (함수 생성 후 실행)
CREATE TRIGGER work_order_process_audit_trigger
  AFTER INSERT OR UPDATE OR DELETE ON work_order_process
  FOR EACH ROW EXECUTE FUNCTION work_order_process_log_trigger_func();
```

#### 1-4. cart_items

```sql
CREATE TABLE cart_items (
  id character varying(255) NOT NULL DEFAULT (gen_random_uuid())::text,
  created_date timestamp without time zone DEFAULT now(),
  updated_date timestamp without time zone DEFAULT now(),
  company_code character varying(20),
  cart_type character varying(255),
  screen_id character varying(255),
  user_id character varying(255),
  source_table character varying(255),
  row_key text,
  row_data text,
  quantity character varying(255),
  unit character varying(255),
  package_unit character varying(255),
  package_entries text,
  status character varying(255),
  memo text,
  CONSTRAINT cart_items_pkey PRIMARY KEY (id)
);

CREATE INDEX idx_cart_items_company ON cart_items (company_code);
CREATE INDEX idx_cart_items_screen_user ON cart_items (screen_id, user_id);
CREATE INDEX idx_cart_items_type ON cart_items (cart_type);
CREATE INDEX idx_cart_items_status ON cart_items (status);
```

---

### STEP 2: 기존 테이블 컬럼 추가

#### 방법 A: DDL UI (관리자 화면)

1. 슈퍼 어드민 계정으로 배포서버 접속
2. 관리자 > 시스템관리 > 테이블관리
3. `work_instruction` 테이블 선택
4. 컬럼 추가 버튼 → `reason` (varchar 500) 추가
5. 컬럼 추가 버튼 → `completed_qty` (varchar 500, 기본값: '0') 추가

#### 방법 B: SQL 직접

```sql
ALTER TABLE work_instruction
  ADD COLUMN IF NOT EXISTS reason character varying(500),
  ADD COLUMN IF NOT EXISTS completed_qty character varying(500) DEFAULT '0'::character varying;
```

---

### STEP 3: table_type_columns 메타데이터 복사 (로컬 → 배포)

```bash
# 로컬 DB에서 추출
PGPASSWORD='ph0909!!' psql -h 39.117.244.52 -p 11132 -U postgres -d plm -c "
COPY (
  SELECT * FROM table_type_columns
  WHERE table_name IN ('work_order_process', 'cart_items', 'process_work_result')
) TO STDOUT WITH CSV HEADER" > /tmp/ttc_export.csv

# 배포 DB에 삽입 (충돌 시 무시)
PGPASSWORD='$DB_PASSWORD' psql -h 211.115.91.141 -p 11134 -U postgres -d plm -c "
COPY table_type_columns FROM STDIN WITH CSV HEADER
ON CONFLICT DO NOTHING" < /tmp/ttc_export.csv
```

---

### STEP 4: 화면 5종 → 배포 COMPANY_7 복사 (SQL)

> 화면 ID 4173~4577은 배포 DB에 없으므로 동일 ID로 안전하게 삽입 가능

```bash
# 로컬에서 screen_definitions 추출
PGPASSWORD='ph0909!!' psql -h 39.117.244.52 -p 11132 -U postgres -d plm -c "
COPY (
  SELECT * FROM screen_definitions
  WHERE screen_id IN (4173, 4479, 4480, 4576, 4577)
) TO STDOUT WITH CSV HEADER" > /tmp/screen_def.csv

# 배포에 삽입
PGPASSWORD='$DB_PASSWORD' psql -h 211.115.91.141 -p 11134 -U postgres -d plm -c "
COPY screen_definitions FROM STDIN WITH CSV HEADER
ON CONFLICT DO NOTHING" < /tmp/screen_def.csv


# screen_layouts_pop 추출 (layout_id 제외)
PGPASSWORD='ph0909!!' psql -h 39.117.244.52 -p 11132 -U postgres -d plm -c "
COPY (
  SELECT screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by
  FROM screen_layouts_pop
  WHERE screen_id IN (4173, 4479, 4480, 4576, 4577)
) TO STDOUT WITH CSV HEADER" > /tmp/screen_layouts.csv

# 배포에 삽입
PGPASSWORD='$DB_PASSWORD' psql -h 211.115.91.141 -p 11134 -U postgres -d plm -c "
COPY screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
FROM STDIN WITH CSV HEADER
ON CONFLICT (screen_id, company_code) DO NOTHING" < /tmp/screen_layouts.csv


# screen_groups 추출 (그룹 4개: 3134, 3216, 3220, 3221)
PGPASSWORD='ph0909!!' psql -h 39.117.244.52 -p 11132 -U postgres -d plm -c "
COPY (
  SELECT * FROM screen_groups
  WHERE id IN (3134, 3216, 3220, 3221)
) TO STDOUT WITH CSV HEADER" > /tmp/screen_groups.csv

# 배포에 삽입
PGPASSWORD='$DB_PASSWORD' psql -h 211.115.91.141 -p 11134 -U postgres -d plm -c "
COPY screen_groups FROM STDIN WITH CSV HEADER
ON CONFLICT DO NOTHING" < /tmp/screen_groups.csv


# screen_group_screens 추출
PGPASSWORD='ph0909!!' psql -h 39.117.244.52 -p 11132 -U postgres -d plm -c "
COPY (
  SELECT * FROM screen_group_screens
  WHERE screen_id IN (4173, 4479, 4480, 4576, 4577)
) TO STDOUT WITH CSV HEADER" > /tmp/screen_group_screens.csv

# 배포에 삽입
PGPASSWORD='$DB_PASSWORD' psql -h 211.115.91.141 -p 11134 -U postgres -d plm -c "
COPY screen_group_screens FROM STDIN WITH CSV HEADER
ON CONFLICT DO NOTHING" < /tmp/screen_group_screens.csv


# 시퀀스 동기화 (배포 DB에서 실행)
SELECT setval('screen_definitions_screen_id_seq',
  GREATEST((SELECT MAX(screen_id) FROM screen_definitions),
           (SELECT last_value FROM screen_definitions_screen_id_seq)));

SELECT setval('screen_layouts_pop_layout_id_seq',
  GREATEST((SELECT MAX(layout_id) FROM screen_layouts_pop),
           (SELECT last_value FROM screen_layouts_pop_layout_id_seq)));

SELECT setval('screen_groups_id_seq',
  GREATEST((SELECT MAX(id) FROM screen_groups),
           (SELECT last_value FROM screen_groups_id_seq)));

SELECT setval('screen_group_screens_id_seq',
  GREATEST((SELECT MAX(id) FROM screen_group_screens),
           (SELECT last_value FROM screen_group_screens_id_seq)));
```

---

### STEP 5: COMPANY_21 테스트 계정 사용자 생성

> COMPANY_21에 현재 등록된 사용자가 없습니다. 로그인하려면 계정이 필요합니다.

배포서버 관리자 화면(슈퍼 어드민)에서 COMPANY_21 소속 사용자를 추가합니다:
- 관리자 > 회사관리 > COMPANY_21 > 사용자 추가
- 또는 SQL:

```sql
-- user_info 테이블에 테스트 사용자 추가 (기존 패턴 참고)
-- 실제 password 해시는 기존 계정 방식과 동일하게 처리 필요
INSERT INTO user_info (user_id, user_name, company_code, user_type, status, password)
VALUES ('test21', '테스트계정', 'COMPANY_21', 'COMPANY_ADMIN', 'active', '-- 해시된 비밀번호 --');
```

---

### STEP 6: PopDeployModal로 COMPANY_7 → COMPANY_21 복사

1. 배포서버에서 `topseal_admin` 계정으로 로그인
2. POP 디자이너 > POP 관리 화면 > 카테고리 트리 접속
3. "탑씰" 그룹에서 배포 버튼 클릭
4. 대상 회사: **COMPANY_21 (테스트회사)** 선택
5. 5개 화면 포함 여부 확인 후 배포 실행
6. PopDeployModal이 COMPANY_21 전용 새 화면 ID 자동 부여 + 참조 자동 리매핑

---

### STEP 7: 검증

```sql
-- 배포 DB에서 실행 --

-- 7-1. 누락 테이블 생성 확인
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('work_order_process', 'cart_items', 'process_work_result', 'work_order_process_log');
-- 예상: 4건

-- 7-2. work_instruction 컬럼 확인
SELECT column_name FROM information_schema.columns
WHERE table_name = 'work_instruction' AND column_name IN ('reason', 'completed_qty');
-- 예상: 2건

-- 7-3. COMPANY_7 화면 삽입 확인
SELECT screen_id, screen_name, company_code FROM screen_definitions
WHERE screen_id IN (4173, 4479, 4480, 4576, 4577);
-- 예상: 5건 (COMPANY_7)

-- 7-4. COMPANY_21 화면 복사 확인 (PopDeployModal 후)
SELECT sd.screen_id, sd.screen_name, sd.company_code,
  CASE WHEN slp.layout_id IS NOT NULL THEN 'Y' ELSE 'N' END as has_layout
FROM screen_definitions sd
LEFT JOIN screen_layouts_pop slp ON sd.screen_id = slp.screen_id
WHERE sd.company_code = 'COMPANY_21'
AND sd.screen_name IN ('홈', 'MES공정', '입고메뉴', '구매입고 담기', '구매입고 장바구니');
-- 예상: 5건 + has_layout = Y

-- 7-5. 화면 간 참조 무결성 (COMPANY_21 기준 새 ID로 리매핑됐는지 확인)
SELECT slp.screen_id,
  layout_data::text LIKE '%screenId%' as has_nav_ref,
  layout_data::text LIKE '%cartScreenId%' as has_cart_ref
FROM screen_layouts_pop slp
JOIN screen_definitions sd ON slp.screen_id = sd.screen_id
WHERE sd.company_code = 'COMPANY_21'
AND sd.screen_name IN ('홈', '입고메뉴', '구매입고 담기', '구매입고 장바구니');

-- 7-6. 시퀀스 정합성
SELECT 'screen_definitions' as tbl,
  (SELECT MAX(screen_id) FROM screen_definitions) as max_id,
  (SELECT last_value FROM screen_definitions_screen_id_seq) as seq_val,
  CASE WHEN (SELECT last_value FROM screen_definitions_screen_id_seq)
            >= (SELECT MAX(screen_id) FROM screen_definitions)
    THEN 'OK' ELSE 'MISMATCH' END as status;
```

---

## 5. COMPANY_21 테스트 환경 수정 가능 여부

| 항목 | 수정 가능? | 방법 |
|------|----------|------|
| 회사명/정보 | O | 관리자 > 회사관리 |
| 사용자 추가/수정 | O | 관리자 > 사용자관리 |
| POP 화면 수정 | O | POP 디자이너에서 직접 편집 |
| 화면 삭제 후 재배포 | O | PopDeployModal 재실행 |
| 기존 ERP 화면 영향 없음 | O | POP 레이아웃 별도 테이블 관리 |

> COMPANY_21은 테스트 전용 계정이므로 자유롭게 수정/삭제 가능합니다.
> 기존 23개 ERP 화면(구매관리, 영업관리 등)은 POP과 무관하므로 건드리지 않아도 됩니다.

---

## 6. 요약: 실행 순서

| 순서 | 작업 | 방법 | 담당 |
|------|------|------|------|
| STEP 0 | 배포 DB 백업 | psql | DB 담당자 |
| STEP 1 | 누락 테이블 4개 생성 | psql SQL | DB 담당자 |
| STEP 2 | work_instruction 컬럼 추가 | DDL UI 또는 psql | DB 담당자 |
| STEP 3 | table_type_columns 메타데이터 복사 | psql COPY | DB 담당자 |
| STEP 4 | 화면 5종 COMPANY_7에 삽입 | psql COPY | DB 담당자 |
| STEP 5 | COMPANY_21 테스트 사용자 생성 | 관리자 UI 또는 SQL | 어드민 계정 |
| STEP 6 | PopDeployModal로 COMPANY_21 복사 | 배포서버 UI | 어드민 계정 |
| STEP 7 | 검증 쿼리 실행 | psql | DB 담당자 |
| STEP 8 | 브라우저 POP 화면 테스트 | 브라우저 | 테스터 |

---

## 7. 롤백 방법

```sql
-- COMPANY_21 POP 화면 삭제 (PopDeployModal로 생성된 것)
DELETE FROM screen_group_screens
WHERE screen_id IN (
  SELECT screen_id FROM screen_definitions WHERE company_code = 'COMPANY_21'
  AND screen_name IN ('홈', 'MES공정', '입고메뉴', '구매입고 담기', '구매입고 장바구니')
);
DELETE FROM screen_layouts_pop
WHERE screen_id IN (
  SELECT screen_id FROM screen_definitions WHERE company_code = 'COMPANY_21'
  AND screen_name IN ('홈', 'MES공정', '입고메뉴', '구매입고 담기', '구매입고 장바구니')
);
DELETE FROM screen_definitions
WHERE company_code = 'COMPANY_21'
AND screen_name IN ('홈', 'MES공정', '입고메뉴', '구매입고 담기', '구매입고 장바구니');

-- COMPANY_7 화면 삭제 (SQL로 삽입한 것)
DELETE FROM screen_group_screens WHERE screen_id IN (4173, 4479, 4480, 4576, 4577);
DELETE FROM screen_layouts_pop WHERE screen_id IN (4173, 4479, 4480, 4576, 4577);
DELETE FROM screen_definitions WHERE screen_id IN (4173, 4479, 4480, 4576, 4577);
DELETE FROM screen_groups WHERE id IN (3134, 3216, 3220, 3221)
  AND id NOT IN (SELECT id FROM backup_20260323_screen_groups);

-- 생성한 테이블 제거
DROP TABLE IF EXISTS work_order_process_log;
DROP TABLE IF EXISTS process_work_result;
DROP TABLE IF EXISTS work_order_process;
DROP TABLE IF EXISTS cart_items;

-- 추가 컬럼 제거
ALTER TABLE work_instruction DROP COLUMN IF EXISTS reason;
ALTER TABLE work_instruction DROP COLUMN IF EXISTS completed_qty;
```

---

*이 문서는 로컬 DB와 배포 DB를 읽기 전용으로 점검한 결과를 바탕으로 작성되었습니다.*
*실제 실행 전 반드시 배포 DB 백업을 완료하세요.*
