feat: 3-System 분리 프로젝트 초기 코드 작성
TK-FB(공장관리+신고)와 M-Project(부적합관리)를 3개 독립 시스템으로 분리하기 위한 전체 코드 구조 작성. - SSO 인증 서비스 (bcrypt + pbkdf2 이중 해시 지원) - System 1: 공장관리 (TK-FB 기반, 신고 코드 제거) - System 2: 신고 (TK-FB에서 workIssue 코드 추출) - System 3: 부적합관리 (M-Project 기반) - Gateway 포털 (path-based 라우팅) - 통합 docker-compose.yml 및 배포 스크립트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
58
system3-nonconformance/api/migrations/001_init.sql
Normal file
58
system3-nonconformance/api/migrations/001_init.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- 초기 데이터베이스 설정
|
||||
|
||||
-- Enum 타입 생성 (4개 카테고리)
|
||||
CREATE TYPE userRole AS ENUM ('admin', 'user');
|
||||
CREATE TYPE issueStatus AS ENUM ('new', 'progress', 'complete');
|
||||
CREATE TYPE issueCategory AS ENUM (
|
||||
'material_missing', -- 자재누락
|
||||
'design_error', -- 설계미스
|
||||
'incoming_defect', -- 입고자재 불량
|
||||
'inspection_miss' -- 검사미스
|
||||
);
|
||||
|
||||
-- 사용자 테이블
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
hashed_password VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(100),
|
||||
role userRole DEFAULT 'user',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 이슈 테이블
|
||||
CREATE TABLE IF NOT EXISTS issues (
|
||||
id SERIAL PRIMARY KEY,
|
||||
photo_path VARCHAR(500),
|
||||
photo_path2 VARCHAR(500), -- 두 번째 사진
|
||||
category issueCategory NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status issueStatus DEFAULT 'new',
|
||||
reporter_id INTEGER REFERENCES users(id),
|
||||
report_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
work_hours FLOAT DEFAULT 0,
|
||||
detail_notes TEXT
|
||||
);
|
||||
|
||||
-- 일일 작업 테이블
|
||||
CREATE TABLE IF NOT EXISTS daily_works (
|
||||
id SERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
worker_count INTEGER NOT NULL,
|
||||
regular_hours FLOAT NOT NULL,
|
||||
overtime_workers INTEGER DEFAULT 0,
|
||||
overtime_hours FLOAT DEFAULT 0,
|
||||
overtime_total FLOAT DEFAULT 0,
|
||||
total_hours FLOAT NOT NULL,
|
||||
created_by_id INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(date)
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX idx_issues_reporter_id ON issues(reporter_id);
|
||||
CREATE INDEX idx_issues_status ON issues(status);
|
||||
CREATE INDEX idx_issues_category ON issues(category);
|
||||
CREATE INDEX idx_daily_works_date ON daily_works(date);
|
||||
CREATE INDEX idx_daily_works_created_by_id ON daily_works(created_by_id);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 두 번째 사진 경로 추가
|
||||
ALTER TABLE issues ADD COLUMN IF NOT EXISTS photo_path2 VARCHAR(500);
|
||||
|
||||
-- 인덱스 추가 (선택사항)
|
||||
CREATE INDEX IF NOT EXISTS idx_issues_photo_path2 ON issues(photo_path2);
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 카테고리 업데이트 마이그레이션
|
||||
-- dimension_defect를 design_error로 변경
|
||||
UPDATE issues
|
||||
SET category = 'design_error'
|
||||
WHERE category = 'dimension_defect';
|
||||
|
||||
-- PostgreSQL enum 타입 업데이트 (필요한 경우)
|
||||
-- 기존 enum 타입 확인 후 필요시 재생성
|
||||
@@ -0,0 +1,9 @@
|
||||
-- 카테고리 값 정규화 (대문자를 소문자로 변경)
|
||||
-- 기존 DIMENSION_DEFECT를 design_error로 변경
|
||||
UPDATE issues SET category = 'design_error' WHERE category IN ('DIMENSION_DEFECT', 'dimension_defect');
|
||||
UPDATE issues SET category = 'material_missing' WHERE category = 'MATERIAL_MISSING';
|
||||
UPDATE issues SET category = 'incoming_defect' WHERE category = 'INCOMING_DEFECT';
|
||||
UPDATE issues SET category = 'inspection_miss' WHERE category = 'INSPECTION_MISS';
|
||||
|
||||
-- 카테고리 값 확인
|
||||
SELECT category, COUNT(*) FROM issues GROUP BY category;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- PostgreSQL enum 타입 재생성
|
||||
-- 카테고리 컬럼을 임시로 텍스트로 변경
|
||||
ALTER TABLE issues ALTER COLUMN category TYPE VARCHAR(50);
|
||||
|
||||
-- 기존 enum 타입 삭제
|
||||
DROP TYPE IF EXISTS issuecategory CASCADE;
|
||||
|
||||
-- 새로운 enum 타입 생성 (4개 카테고리)
|
||||
CREATE TYPE issuecategory AS ENUM (
|
||||
'material_missing', -- 자재누락
|
||||
'design_error', -- 설계미스 (기존 dimension_defect 대체)
|
||||
'incoming_defect', -- 입고자재 불량
|
||||
'inspection_miss' -- 검사미스 (신규)
|
||||
);
|
||||
|
||||
-- 카테고리 컬럼을 새 enum 타입으로 변경
|
||||
ALTER TABLE issues ALTER COLUMN category TYPE issuecategory USING category::issuecategory;
|
||||
|
||||
-- 확인
|
||||
SELECT enumlabel FROM pg_enum WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'issuecategory');
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 프로젝트 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_no VARCHAR(50) UNIQUE NOT NULL,
|
||||
project_name VARCHAR(200) NOT NULL,
|
||||
created_by_id INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_job_no ON projects(job_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_created_by_id ON projects(created_by_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_is_active ON projects(is_active);
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 부적합 사항 테이블에 프로젝트 ID 컬럼 추가
|
||||
ALTER TABLE issues ADD COLUMN project_id INTEGER;
|
||||
|
||||
-- 외래키 제약조건 추가
|
||||
ALTER TABLE issues ADD CONSTRAINT fk_issues_project_id
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_issues_project_id ON issues(project_id);
|
||||
|
||||
-- 기존 부적합 사항들을 첫 번째 프로젝트로 할당 (있는 경우)
|
||||
UPDATE issues
|
||||
SET project_id = (SELECT id FROM projects ORDER BY created_at LIMIT 1)
|
||||
WHERE project_id IS NULL
|
||||
AND EXISTS (SELECT 1 FROM projects LIMIT 1);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- project_id 컬럼을 BIGINT로 변경
|
||||
ALTER TABLE issues ALTER COLUMN project_id TYPE BIGINT;
|
||||
|
||||
-- projects 테이블의 id도 BIGINT로 변경 (일관성을 위해)
|
||||
ALTER TABLE projects ALTER COLUMN id TYPE BIGINT;
|
||||
|
||||
-- 외래키 제약조건 재생성 (타입 변경으로 인해 필요)
|
||||
ALTER TABLE issues DROP CONSTRAINT IF EXISTS fk_issues_project_id;
|
||||
ALTER TABLE issues ADD CONSTRAINT fk_issues_project_id
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id);
|
||||
|
||||
-- 다른 테이블들도 확인하여 project_id 참조하는 곳이 있으면 수정
|
||||
-- (현재는 issues 테이블만 project_id를 가지고 있음)
|
||||
@@ -0,0 +1,26 @@
|
||||
-- 프로젝트별 일일공수 테이블 생성
|
||||
CREATE TABLE project_daily_works (
|
||||
id SERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
hours FLOAT NOT NULL,
|
||||
created_by_id INTEGER NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX idx_project_daily_works_date ON project_daily_works(date);
|
||||
CREATE INDEX idx_project_daily_works_project_id ON project_daily_works(project_id);
|
||||
CREATE INDEX idx_project_daily_works_date_project ON project_daily_works(date, project_id);
|
||||
|
||||
-- 기존 일일공수 데이터를 프로젝트별로 마이그레이션 (M Project로)
|
||||
INSERT INTO project_daily_works (date, project_id, hours, created_by_id, created_at)
|
||||
SELECT
|
||||
date::date,
|
||||
1, -- M Project ID
|
||||
total_hours,
|
||||
created_by_id,
|
||||
created_at
|
||||
FROM daily_works
|
||||
WHERE total_hours > 0;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 부적합 카테고리에 'etc' (기타) 값 추가
|
||||
-- 백엔드 코드와 데이터베이스 enum 타입 불일치 해결
|
||||
|
||||
-- issuecategory enum 타입에 'etc' 값 추가
|
||||
ALTER TYPE issuecategory ADD VALUE 'etc';
|
||||
|
||||
-- 확인 쿼리 (주석)
|
||||
-- SELECT enumlabel FROM pg_enum WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'issuecategory') ORDER BY enumsortorder;
|
||||
|
||||
-- 이제 사용 가능한 카테고리:
|
||||
-- 1. material_missing (자재누락)
|
||||
-- 2. design_error (설계미스)
|
||||
-- 3. incoming_defect (입고자재 불량)
|
||||
-- 4. inspection_miss (검사미스)
|
||||
-- 5. etc (기타) ✅ 새로 추가됨
|
||||
@@ -0,0 +1,137 @@
|
||||
-- 권한 시스템 개선 마이그레이션
|
||||
-- 새로운 사용자 역할 추가 및 개별 권한 테이블 생성
|
||||
|
||||
-- 1. 새로운 사용자 역할 추가
|
||||
ALTER TYPE userrole ADD VALUE 'super_admin';
|
||||
ALTER TYPE userrole ADD VALUE 'manager';
|
||||
|
||||
-- 2. 사용자별 개별 권한 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
permission VARCHAR(50) NOT NULL,
|
||||
granted BOOLEAN DEFAULT TRUE,
|
||||
granted_by_id INTEGER REFERENCES users(id),
|
||||
granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
revoked_at TIMESTAMP WITH TIME ZONE,
|
||||
notes TEXT,
|
||||
UNIQUE(user_id, permission)
|
||||
);
|
||||
|
||||
-- 3. 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_user_permissions_user_id ON user_permissions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_permissions_permission ON user_permissions(permission);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_permissions_granted ON user_permissions(granted);
|
||||
|
||||
-- 4. 기본 권한 설정 (기존 관리자에게 super_admin 권한 부여)
|
||||
UPDATE users SET role = 'super_admin' WHERE username = 'hyungi';
|
||||
|
||||
-- 5. 권한 확인 함수 생성
|
||||
CREATE OR REPLACE FUNCTION check_user_permission(p_user_id INTEGER, p_permission VARCHAR)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role userrole;
|
||||
has_permission BOOLEAN := FALSE;
|
||||
BEGIN
|
||||
-- 사용자 역할 가져오기
|
||||
SELECT role INTO user_role FROM users WHERE id = p_user_id AND is_active = TRUE;
|
||||
|
||||
IF user_role IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
-- super_admin은 모든 권한 보유
|
||||
IF user_role = 'super_admin' THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
-- 개별 권한 확인
|
||||
SELECT granted INTO has_permission
|
||||
FROM user_permissions
|
||||
WHERE user_id = p_user_id
|
||||
AND permission = p_permission
|
||||
AND granted = TRUE
|
||||
AND revoked_at IS NULL;
|
||||
|
||||
-- 개별 권한이 없으면 역할 기반 기본 권한 확인
|
||||
IF has_permission IS NULL THEN
|
||||
-- 기본 권한 매트릭스
|
||||
CASE
|
||||
WHEN p_permission IN ('issues.create', 'issues.view') THEN
|
||||
has_permission := TRUE; -- 모든 사용자
|
||||
WHEN p_permission IN ('issues.edit', 'issues.review', 'daily_work.create', 'daily_work.view', 'daily_work.edit') THEN
|
||||
has_permission := user_role IN ('admin', 'manager'); -- 관리자, 매니저
|
||||
WHEN p_permission IN ('projects.create', 'projects.edit', 'issues.delete', 'daily_work.delete') THEN
|
||||
has_permission := user_role = 'admin'; -- 관리자만
|
||||
WHEN p_permission IN ('projects.delete', 'users.create', 'users.edit', 'users.delete', 'users.change_role') THEN
|
||||
has_permission := user_role = 'super_admin'; -- 최고 관리자만
|
||||
ELSE
|
||||
has_permission := FALSE;
|
||||
END CASE;
|
||||
END IF;
|
||||
|
||||
RETURN COALESCE(has_permission, FALSE);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 6. 권한 부여 함수 생성
|
||||
CREATE OR REPLACE FUNCTION grant_user_permission(
|
||||
p_user_id INTEGER,
|
||||
p_permission VARCHAR,
|
||||
p_granted_by_id INTEGER,
|
||||
p_notes TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
INSERT INTO user_permissions (user_id, permission, granted, granted_by_id, notes)
|
||||
VALUES (p_user_id, p_permission, TRUE, p_granted_by_id, p_notes)
|
||||
ON CONFLICT (user_id, permission)
|
||||
DO UPDATE SET
|
||||
granted = TRUE,
|
||||
granted_by_id = p_granted_by_id,
|
||||
granted_at = NOW(),
|
||||
revoked_at = NULL,
|
||||
notes = p_notes;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 7. 권한 취소 함수 생성
|
||||
CREATE OR REPLACE FUNCTION revoke_user_permission(
|
||||
p_user_id INTEGER,
|
||||
p_permission VARCHAR,
|
||||
p_revoked_by_id INTEGER,
|
||||
p_notes TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
UPDATE user_permissions
|
||||
SET granted = FALSE,
|
||||
revoked_at = NOW(),
|
||||
notes = p_notes
|
||||
WHERE user_id = p_user_id
|
||||
AND permission = p_permission;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 8. 사용자 권한 목록 조회 뷰 생성
|
||||
CREATE OR REPLACE VIEW user_permissions_view AS
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
u.username,
|
||||
u.full_name,
|
||||
u.role,
|
||||
up.permission,
|
||||
up.granted,
|
||||
up.granted_at,
|
||||
up.revoked_at,
|
||||
granted_by.username as granted_by_username,
|
||||
up.notes
|
||||
FROM users u
|
||||
LEFT JOIN user_permissions up ON u.id = up.user_id
|
||||
LEFT JOIN users granted_by ON up.granted_by_id = granted_by.id
|
||||
WHERE u.is_active = TRUE
|
||||
ORDER BY u.username, up.permission;
|
||||
@@ -0,0 +1,111 @@
|
||||
-- 권한 시스템 단순화
|
||||
-- admin/user 구조로 변경하고 페이지별 접근 권한으로 변경
|
||||
|
||||
-- 1. 기존 복잡한 권한 테이블 삭제하고 단순한 페이지 권한 테이블로 변경
|
||||
DROP TABLE IF EXISTS user_permissions CASCADE;
|
||||
|
||||
-- 2. 페이지별 접근 권한 테이블 생성
|
||||
CREATE TABLE user_page_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
page_name VARCHAR(50) NOT NULL,
|
||||
can_access BOOLEAN DEFAULT FALSE,
|
||||
granted_by_id INTEGER REFERENCES users(id),
|
||||
granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
notes TEXT,
|
||||
UNIQUE(user_id, page_name)
|
||||
);
|
||||
|
||||
-- 3. 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_user_page_permissions_user_id ON user_page_permissions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_page_permissions_page_name ON user_page_permissions(page_name);
|
||||
|
||||
-- 4. 기존 복잡한 함수들 삭제
|
||||
DROP FUNCTION IF EXISTS check_user_permission(INTEGER, VARCHAR);
|
||||
DROP FUNCTION IF EXISTS grant_user_permission(INTEGER, VARCHAR, INTEGER, TEXT);
|
||||
DROP FUNCTION IF EXISTS revoke_user_permission(INTEGER, VARCHAR, INTEGER, TEXT);
|
||||
|
||||
-- 5. 단순한 페이지 접근 권한 체크 함수
|
||||
CREATE OR REPLACE FUNCTION check_page_access(p_user_id INTEGER, p_page_name VARCHAR)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role userrole;
|
||||
has_access BOOLEAN := FALSE;
|
||||
BEGIN
|
||||
-- 사용자 역할 가져오기
|
||||
SELECT role INTO user_role FROM users WHERE id = p_user_id AND is_active = TRUE;
|
||||
|
||||
IF user_role IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
-- admin은 모든 페이지 접근 가능
|
||||
IF user_role = 'admin' THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
-- 일반 사용자는 개별 페이지 권한 확인
|
||||
SELECT can_access INTO has_access
|
||||
FROM user_page_permissions
|
||||
WHERE user_id = p_user_id
|
||||
AND page_name = p_page_name;
|
||||
|
||||
-- 권한이 설정되지 않은 경우 기본값 (부적합 등록/조회만 허용)
|
||||
IF has_access IS NULL THEN
|
||||
CASE p_page_name
|
||||
WHEN 'issues_create' THEN has_access := TRUE;
|
||||
WHEN 'issues_view' THEN has_access := TRUE;
|
||||
ELSE has_access := FALSE;
|
||||
END CASE;
|
||||
END IF;
|
||||
|
||||
RETURN COALESCE(has_access, FALSE);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 6. 페이지 권한 부여 함수
|
||||
CREATE OR REPLACE FUNCTION grant_page_access(
|
||||
p_user_id INTEGER,
|
||||
p_page_name VARCHAR,
|
||||
p_can_access BOOLEAN,
|
||||
p_granted_by_id INTEGER,
|
||||
p_notes TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
INSERT INTO user_page_permissions (user_id, page_name, can_access, granted_by_id, notes)
|
||||
VALUES (p_user_id, p_page_name, p_can_access, p_granted_by_id, p_notes)
|
||||
ON CONFLICT (user_id, page_name)
|
||||
DO UPDATE SET
|
||||
can_access = p_can_access,
|
||||
granted_by_id = p_granted_by_id,
|
||||
granted_at = NOW(),
|
||||
notes = p_notes;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 7. 사용자 페이지 권한 조회 뷰
|
||||
CREATE OR REPLACE VIEW user_page_access_view AS
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
u.username,
|
||||
u.full_name,
|
||||
u.role,
|
||||
upp.page_name,
|
||||
upp.can_access,
|
||||
upp.granted_at,
|
||||
granted_by.username as granted_by_username,
|
||||
upp.notes
|
||||
FROM users u
|
||||
LEFT JOIN user_page_permissions upp ON u.id = upp.user_id
|
||||
LEFT JOIN users granted_by ON upp.granted_by_id = granted_by.id
|
||||
WHERE u.is_active = TRUE
|
||||
ORDER BY u.username, upp.page_name;
|
||||
|
||||
-- 8. 기존 super_admin, manager 역할을 admin으로 변경
|
||||
UPDATE users SET role = 'admin' WHERE role IN ('super_admin', 'manager');
|
||||
|
||||
-- 9. 기존 뷰 삭제
|
||||
DROP VIEW IF EXISTS user_permissions_view;
|
||||
@@ -0,0 +1,316 @@
|
||||
-- ================================================
|
||||
-- 마이그레이션: 013_add_inbox_workflow_system.sql
|
||||
-- 목적: 수신함 워크플로우를 위한 DB 스키마 추가
|
||||
-- 작성일: 2025-10-25
|
||||
-- 주의: 배포 시 반드시 이 파일이 실행되는지 확인 필요!
|
||||
-- ================================================
|
||||
|
||||
-- 트랜잭션 시작 (실패 시 롤백)
|
||||
BEGIN;
|
||||
|
||||
-- 1. 새로운 ENUM 타입 생성
|
||||
-- 검토 상태 (수신함 워크플로우용)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'review_status') THEN
|
||||
CREATE TYPE review_status AS ENUM (
|
||||
'pending_review', -- 수신함 (검토 대기)
|
||||
'in_progress', -- 관리함 (진행 중)
|
||||
'completed', -- 관리함 (완료됨)
|
||||
'disposed' -- 폐기함 (폐기됨)
|
||||
);
|
||||
RAISE NOTICE '✅ review_status ENUM 타입이 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ review_status ENUM 타입이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 폐기 사유 타입
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'disposal_reason_type') THEN
|
||||
CREATE TYPE disposal_reason_type AS ENUM (
|
||||
'duplicate', -- 중복 (기본값)
|
||||
'invalid_report', -- 잘못된 신고
|
||||
'not_applicable', -- 해당 없음
|
||||
'spam', -- 스팸/오류
|
||||
'custom' -- 직접 입력
|
||||
);
|
||||
RAISE NOTICE '✅ disposal_reason_type ENUM 타입이 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ disposal_reason_type ENUM 타입이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. issues 테이블에 새로운 컬럼 추가 (안전하게)
|
||||
-- 검토 상태 컬럼
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'review_status'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN review_status review_status DEFAULT 'pending_review';
|
||||
RAISE NOTICE '✅ issues.review_status 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.review_status 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 폐기 사유 컬럼
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'disposal_reason'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN disposal_reason disposal_reason_type;
|
||||
RAISE NOTICE '✅ issues.disposal_reason 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.disposal_reason 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 사용자 정의 폐기 사유 컬럼
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'custom_disposal_reason'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN custom_disposal_reason TEXT;
|
||||
RAISE NOTICE '✅ issues.custom_disposal_reason 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.custom_disposal_reason 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 폐기 날짜 컬럼
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'disposed_at'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN disposed_at TIMESTAMP WITH TIME ZONE;
|
||||
RAISE NOTICE '✅ issues.disposed_at 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.disposed_at 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 검토자 ID 컬럼
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'reviewed_by_id'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN reviewed_by_id INTEGER REFERENCES users(id);
|
||||
RAISE NOTICE '✅ issues.reviewed_by_id 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.reviewed_by_id 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 검토 날짜 컬럼
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'reviewed_at'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN reviewed_at TIMESTAMP WITH TIME ZONE;
|
||||
RAISE NOTICE '✅ issues.reviewed_at 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.reviewed_at 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 원본 데이터 보존 컬럼 (JSONB)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'original_data'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN original_data JSONB;
|
||||
RAISE NOTICE '✅ issues.original_data 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.original_data 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 수정 이력 컬럼 (JSONB)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'modification_log'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN modification_log JSONB DEFAULT '[]'::jsonb;
|
||||
RAISE NOTICE '✅ issues.modification_log 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ issues.modification_log 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. 인덱스 추가 (성능 최적화)
|
||||
-- review_status 인덱스
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'issues' AND indexname = 'idx_issues_review_status'
|
||||
) THEN
|
||||
CREATE INDEX idx_issues_review_status ON issues(review_status);
|
||||
RAISE NOTICE '✅ idx_issues_review_status 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ idx_issues_review_status 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- reviewed_by_id 인덱스
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'issues' AND indexname = 'idx_issues_reviewed_by_id'
|
||||
) THEN
|
||||
CREATE INDEX idx_issues_reviewed_by_id ON issues(reviewed_by_id);
|
||||
RAISE NOTICE '✅ idx_issues_reviewed_by_id 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ idx_issues_reviewed_by_id 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- disposed_at 인덱스
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'issues' AND indexname = 'idx_issues_disposed_at'
|
||||
) THEN
|
||||
CREATE INDEX idx_issues_disposed_at ON issues(disposed_at) WHERE disposed_at IS NOT NULL;
|
||||
RAISE NOTICE '✅ idx_issues_disposed_at 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ idx_issues_disposed_at 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 4. 기존 데이터 마이그레이션 (안전하게)
|
||||
-- 기존 issues의 review_status를 기존 status에 따라 설정
|
||||
UPDATE issues
|
||||
SET review_status = CASE
|
||||
WHEN status = 'new' THEN 'pending_review'::review_status
|
||||
WHEN status = 'progress' THEN 'in_progress'::review_status
|
||||
WHEN status = 'complete' THEN 'completed'::review_status
|
||||
ELSE 'pending_review'::review_status
|
||||
END
|
||||
WHERE review_status = 'pending_review'; -- 기본값인 경우만 업데이트
|
||||
|
||||
-- 5. 제약 조건 추가
|
||||
-- 폐기된 경우 폐기 사유 필수
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.check_constraints
|
||||
WHERE constraint_name = 'chk_disposal_reason_required'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD CONSTRAINT chk_disposal_reason_required
|
||||
CHECK (
|
||||
(review_status = 'disposed' AND disposal_reason IS NOT NULL) OR
|
||||
(review_status != 'disposed')
|
||||
);
|
||||
RAISE NOTICE '✅ chk_disposal_reason_required 제약 조건이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ chk_disposal_reason_required 제약 조건이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 사용자 정의 사유는 disposal_reason이 'custom'일 때만
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.check_constraints
|
||||
WHERE constraint_name = 'chk_custom_reason_logic'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD CONSTRAINT chk_custom_reason_logic
|
||||
CHECK (
|
||||
(disposal_reason = 'custom' AND custom_disposal_reason IS NOT NULL AND LENGTH(TRIM(custom_disposal_reason)) > 0) OR
|
||||
(disposal_reason != 'custom' OR disposal_reason IS NULL)
|
||||
);
|
||||
RAISE NOTICE '✅ chk_custom_reason_logic 제약 조건이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ chk_custom_reason_logic 제약 조건이 이미 존재합니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 6. 마이그레이션 완료 로그
|
||||
INSERT INTO migration_log (migration_file, executed_at, status, notes)
|
||||
VALUES (
|
||||
'013_add_inbox_workflow_system.sql',
|
||||
NOW(),
|
||||
'SUCCESS',
|
||||
'수신함 워크플로우 시스템 추가: review_status, disposal_reason, 원본데이터 보존, 수정이력 등'
|
||||
) ON CONFLICT (migration_file) DO UPDATE SET
|
||||
executed_at = NOW(),
|
||||
status = 'SUCCESS',
|
||||
notes = EXCLUDED.notes;
|
||||
|
||||
-- 트랜잭션 커밋
|
||||
COMMIT;
|
||||
|
||||
-- 7. 마이그레이션 검증
|
||||
DO $$
|
||||
DECLARE
|
||||
column_count INTEGER;
|
||||
enum_count INTEGER;
|
||||
index_count INTEGER;
|
||||
BEGIN
|
||||
-- 컬럼 개수 확인
|
||||
SELECT COUNT(*) INTO column_count
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'issues'
|
||||
AND column_name IN (
|
||||
'review_status', 'disposal_reason', 'custom_disposal_reason',
|
||||
'disposed_at', 'reviewed_by_id', 'reviewed_at',
|
||||
'original_data', 'modification_log'
|
||||
);
|
||||
|
||||
-- ENUM 타입 확인
|
||||
SELECT COUNT(*) INTO enum_count
|
||||
FROM pg_type
|
||||
WHERE typname IN ('review_status', 'disposal_reason_type');
|
||||
|
||||
-- 인덱스 확인
|
||||
SELECT COUNT(*) INTO index_count
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'issues'
|
||||
AND indexname IN (
|
||||
'idx_issues_review_status',
|
||||
'idx_issues_reviewed_by_id',
|
||||
'idx_issues_disposed_at'
|
||||
);
|
||||
|
||||
RAISE NOTICE '=== 마이그레이션 검증 결과 ===';
|
||||
RAISE NOTICE '추가된 컬럼: %/8개', column_count;
|
||||
RAISE NOTICE '생성된 ENUM: %/2개', enum_count;
|
||||
RAISE NOTICE '생성된 인덱스: %/3개', index_count;
|
||||
|
||||
IF column_count = 8 AND enum_count = 2 AND index_count = 3 THEN
|
||||
RAISE NOTICE '✅ 마이그레이션이 성공적으로 완료되었습니다!';
|
||||
ELSE
|
||||
RAISE EXCEPTION '❌ 마이그레이션 검증 실패! 일부 구조가 누락되었습니다.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 8. 최종 테이블 구조 출력
|
||||
\echo '=== 최종 issues 테이블 구조 ==='
|
||||
\d issues;
|
||||
|
||||
\echo '=== 새로운 ENUM 타입들 ==='
|
||||
\dT+ review_status;
|
||||
\dT+ disposal_reason_type;
|
||||
|
||||
\echo '=== 마이그레이션 013 완료 ==='
|
||||
@@ -0,0 +1,92 @@
|
||||
-- 014_add_user_department.sql
|
||||
-- 사용자 부서 정보 추가
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- migration_log 테이블 생성 (멱등성)
|
||||
CREATE TABLE IF NOT EXISTS migration_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
migration_file VARCHAR(255) NOT NULL UNIQUE,
|
||||
executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status VARCHAR(50),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 마이그레이션 파일 이름
|
||||
DO $$
|
||||
DECLARE
|
||||
migration_name VARCHAR(255) := '014_add_user_department.sql';
|
||||
migration_notes TEXT := '사용자 부서 정보 추가: department ENUM 타입 및 users 테이블에 department 컬럼 추가';
|
||||
current_status VARCHAR(50);
|
||||
BEGIN
|
||||
SELECT status INTO current_status FROM migration_log WHERE migration_file = migration_name;
|
||||
|
||||
IF current_status IS NULL THEN
|
||||
RAISE NOTICE '--- 마이그레이션 % 시작 ---', migration_name;
|
||||
|
||||
-- department ENUM 타입 생성
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'department_type') THEN
|
||||
CREATE TYPE department_type AS ENUM (
|
||||
'production', -- 생산
|
||||
'quality', -- 품질
|
||||
'purchasing', -- 구매
|
||||
'design', -- 설계
|
||||
'sales' -- 영업
|
||||
);
|
||||
RAISE NOTICE '✅ department_type ENUM 타입이 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ department_type ENUM 타입이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- users 테이블에 department 컬럼 추가
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'department') THEN
|
||||
ALTER TABLE users ADD COLUMN department department_type;
|
||||
RAISE NOTICE '✅ users.department 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ users.department 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 인덱스 추가 (부서별 조회 성능 향상)
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'users' AND indexname = 'idx_users_department') THEN
|
||||
CREATE INDEX idx_users_department ON users (department) WHERE department IS NOT NULL;
|
||||
RAISE NOTICE '✅ idx_users_department 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ idx_users_department 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 마이그레이션 검증
|
||||
DECLARE
|
||||
col_count INTEGER;
|
||||
enum_count INTEGER;
|
||||
idx_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO col_count FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'department';
|
||||
SELECT COUNT(*) INTO enum_count FROM pg_type WHERE typname = 'department_type';
|
||||
SELECT COUNT(*) INTO idx_count FROM pg_indexes WHERE tablename = 'users' AND indexname = 'idx_users_department';
|
||||
|
||||
RAISE NOTICE '=== 마이그레이션 검증 결과 ===';
|
||||
RAISE NOTICE '추가된 컬럼: %/1개', col_count;
|
||||
RAISE NOTICE '생성된 ENUM: %/1개', enum_count;
|
||||
RAISE NOTICE '생성된 인덱스: %/1개', idx_count;
|
||||
|
||||
IF col_count = 1 AND enum_count = 1 AND idx_count = 1 THEN
|
||||
RAISE NOTICE '✅ 마이그레이션이 성공적으로 완료되었습니다!';
|
||||
INSERT INTO migration_log (migration_file, status, notes) VALUES (migration_name, 'SUCCESS', migration_notes);
|
||||
ELSE
|
||||
RAISE EXCEPTION '❌ 마이그레이션 검증 실패!';
|
||||
END IF;
|
||||
END;
|
||||
|
||||
-- 부서 ENUM 값 확인
|
||||
RAISE NOTICE '=== 부서 ENUM 값 ===';
|
||||
PERFORM dblink_exec('dbname=' || current_database(), 'SELECT enumlabel FROM pg_enum WHERE enumtypid = ''department_type''::regtype ORDER BY enumsortorder');
|
||||
|
||||
ELSIF current_status = 'SUCCESS' THEN
|
||||
RAISE NOTICE 'ℹ️ 마이그레이션 %는 이미 성공적으로 실행되었습니다. 스킵합니다.', migration_name;
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ 마이그레이션 %는 이전에 실패했습니다. 수동 확인이 필요합니다.', migration_name;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,100 @@
|
||||
-- 015_add_duplicate_tracking.sql
|
||||
-- 중복 신고 추적 시스템 추가
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- migration_log 테이블 생성 (멱등성)
|
||||
CREATE TABLE IF NOT EXISTS migration_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
migration_file VARCHAR(255) NOT NULL UNIQUE,
|
||||
executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status VARCHAR(50),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 마이그레이션 파일 이름
|
||||
DO $$
|
||||
DECLARE
|
||||
migration_name VARCHAR(255) := '015_add_duplicate_tracking.sql';
|
||||
migration_notes TEXT := '중복 신고 추적 시스템: duplicate_of_issue_id, duplicate_reporters 컬럼 추가';
|
||||
current_status VARCHAR(50);
|
||||
BEGIN
|
||||
SELECT status INTO current_status FROM migration_log WHERE migration_file = migration_name;
|
||||
|
||||
IF current_status IS NULL THEN
|
||||
RAISE NOTICE '--- 마이그레이션 % 시작 ---', migration_name;
|
||||
|
||||
-- issues 테이블에 중복 추적 컬럼 추가
|
||||
-- 중복 대상 이슈 ID
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'duplicate_of_issue_id') THEN
|
||||
ALTER TABLE issues ADD COLUMN duplicate_of_issue_id INTEGER;
|
||||
RAISE NOTICE '✅ issues.duplicate_of_issue_id 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.duplicate_of_issue_id 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 중복 신고자 목록 (JSONB 배열)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'duplicate_reporters') THEN
|
||||
ALTER TABLE issues ADD COLUMN duplicate_reporters JSONB DEFAULT '[]'::jsonb;
|
||||
RAISE NOTICE '✅ issues.duplicate_reporters 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.duplicate_reporters 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 외래 키 제약 조건 추가 (duplicate_of_issue_id)
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'issues_duplicate_of_issue_id_fkey') THEN
|
||||
ALTER TABLE issues ADD CONSTRAINT issues_duplicate_of_issue_id_fkey
|
||||
FOREIGN KEY (duplicate_of_issue_id) REFERENCES issues(id);
|
||||
RAISE NOTICE '✅ issues_duplicate_of_issue_id_fkey 외래 키 제약 조건이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues_duplicate_of_issue_id_fkey 외래 키 제약 조건이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 인덱스 추가
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_duplicate_of') THEN
|
||||
CREATE INDEX idx_issues_duplicate_of ON issues (duplicate_of_issue_id) WHERE duplicate_of_issue_id IS NOT NULL;
|
||||
RAISE NOTICE '✅ idx_issues_duplicate_of 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ idx_issues_duplicate_of 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- JSONB 인덱스 추가 (중복 신고자 검색 성능 향상)
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_duplicate_reporters_gin') THEN
|
||||
CREATE INDEX idx_issues_duplicate_reporters_gin ON issues USING GIN (duplicate_reporters);
|
||||
RAISE NOTICE '✅ idx_issues_duplicate_reporters_gin 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ idx_issues_duplicate_reporters_gin 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 마이그레이션 검증
|
||||
DECLARE
|
||||
col_count INTEGER;
|
||||
idx_count INTEGER;
|
||||
fk_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO col_count FROM information_schema.columns WHERE table_name = 'issues' AND column_name IN ('duplicate_of_issue_id', 'duplicate_reporters');
|
||||
SELECT COUNT(*) INTO idx_count FROM pg_indexes WHERE tablename = 'issues' AND indexname IN ('idx_issues_duplicate_of', 'idx_issues_duplicate_reporters_gin');
|
||||
SELECT COUNT(*) INTO fk_count FROM pg_constraint WHERE conname = 'issues_duplicate_of_issue_id_fkey';
|
||||
|
||||
RAISE NOTICE '=== 마이그레이션 검증 결과 ===';
|
||||
RAISE NOTICE '추가된 컬럼: %/2개', col_count;
|
||||
RAISE NOTICE '생성된 인덱스: %/2개', idx_count;
|
||||
RAISE NOTICE '생성된 FK: %/1개', fk_count;
|
||||
|
||||
IF col_count = 2 AND idx_count = 2 AND fk_count = 1 THEN
|
||||
RAISE NOTICE '✅ 마이그레이션이 성공적으로 완료되었습니다!';
|
||||
INSERT INTO migration_log (migration_file, status, notes) VALUES (migration_name, 'SUCCESS', migration_notes);
|
||||
ELSE
|
||||
RAISE EXCEPTION '❌ 마이그레이션 검증 실패!';
|
||||
END IF;
|
||||
END;
|
||||
|
||||
ELSIF current_status = 'SUCCESS' THEN
|
||||
RAISE NOTICE 'ℹ️ 마이그레이션 %는 이미 성공적으로 실행되었습니다. 스킵합니다.', migration_name;
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ 마이그레이션 %는 이전에 실패했습니다. 수동 확인이 필요합니다.', migration_name;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,223 @@
|
||||
-- 016_add_management_fields.sql
|
||||
-- 관리함에서 사용할 추가 필드들과 완료 사진 업로드 기능 추가
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
migration_name VARCHAR(255) := '016_add_management_fields.sql';
|
||||
migration_notes TEXT := '관리함 필드 추가: 원인/해결방안, 담당부서/담당자, 조치예상일, 완료확인일, 원인부서, 의견, 완료사진, 프로젝트별 No 등';
|
||||
current_status VARCHAR(50);
|
||||
BEGIN
|
||||
-- migration_log 테이블이 없으면 생성 (멱등성)
|
||||
CREATE TABLE IF NOT EXISTS migration_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
migration_file VARCHAR(255) NOT NULL UNIQUE,
|
||||
executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status VARCHAR(50),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
SELECT status INTO current_status FROM migration_log WHERE migration_file = migration_name;
|
||||
|
||||
IF current_status IS NULL THEN
|
||||
RAISE NOTICE '--- 마이그레이션 % 시작 ---', migration_name;
|
||||
|
||||
-- issues 테이블에 관리함 관련 컬럼들 추가
|
||||
|
||||
-- 완료 사진 경로
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'completion_photo_path') THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_photo_path VARCHAR(255);
|
||||
RAISE NOTICE '✅ issues.completion_photo_path 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.completion_photo_path 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 해결방안 (관리함에서 입력)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'solution') THEN
|
||||
ALTER TABLE issues ADD COLUMN solution TEXT;
|
||||
RAISE NOTICE '✅ issues.solution 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.solution 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 담당부서 (관리함에서 선택)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'responsible_department') THEN
|
||||
ALTER TABLE issues ADD COLUMN responsible_department department_type;
|
||||
RAISE NOTICE '✅ issues.responsible_department 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.responsible_department 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 담당자 (관리함에서 입력)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'responsible_person') THEN
|
||||
ALTER TABLE issues ADD COLUMN responsible_person VARCHAR(100);
|
||||
RAISE NOTICE '✅ issues.responsible_person 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.responsible_person 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 조치 예상일 (관리함에서 입력)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'expected_completion_date') THEN
|
||||
ALTER TABLE issues ADD COLUMN expected_completion_date DATE;
|
||||
RAISE NOTICE '✅ issues.expected_completion_date 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.expected_completion_date 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 완료 확인일 (완료 상태로 변경 시 자동 입력)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'actual_completion_date') THEN
|
||||
ALTER TABLE issues ADD COLUMN actual_completion_date DATE;
|
||||
RAISE NOTICE '✅ issues.actual_completion_date 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.actual_completion_date 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 원인부서 (관리함에서 입력)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'cause_department') THEN
|
||||
ALTER TABLE issues ADD COLUMN cause_department department_type;
|
||||
RAISE NOTICE '✅ issues.cause_department 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.cause_department 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- ISSUE에 대한 의견 (관리함에서 입력)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'management_comment') THEN
|
||||
ALTER TABLE issues ADD COLUMN management_comment TEXT;
|
||||
RAISE NOTICE '✅ issues.management_comment 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.management_comment 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 프로젝트별 순번 (No) - 프로젝트 내에서 1, 2, 3... 순으로 증가
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'project_sequence_no') THEN
|
||||
ALTER TABLE issues ADD COLUMN project_sequence_no INTEGER;
|
||||
RAISE NOTICE '✅ issues.project_sequence_no 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.project_sequence_no 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 최종 내용 (수정된 내용이 있으면 수정본, 없으면 원본)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'final_description') THEN
|
||||
ALTER TABLE issues ADD COLUMN final_description TEXT;
|
||||
RAISE NOTICE '✅ issues.final_description 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.final_description 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 최종 카테고리 (수정된 카테고리가 있으면 수정본, 없으면 원본)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'final_category') THEN
|
||||
ALTER TABLE issues ADD COLUMN final_category issuecategory;
|
||||
RAISE NOTICE '✅ issues.final_category 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.final_category 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 인덱스 추가
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_project_sequence') THEN
|
||||
CREATE INDEX idx_issues_project_sequence ON issues (project_id, project_sequence_no);
|
||||
RAISE NOTICE '✅ idx_issues_project_sequence 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ idx_issues_project_sequence 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_responsible_department') THEN
|
||||
CREATE INDEX idx_issues_responsible_department ON issues (responsible_department) WHERE responsible_department IS NOT NULL;
|
||||
RAISE NOTICE '✅ idx_issues_responsible_department 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ idx_issues_responsible_department 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_expected_completion') THEN
|
||||
CREATE INDEX idx_issues_expected_completion ON issues (expected_completion_date) WHERE expected_completion_date IS NOT NULL;
|
||||
RAISE NOTICE '✅ idx_issues_expected_completion 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ idx_issues_expected_completion 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 프로젝트별 순번 자동 생성 함수
|
||||
CREATE OR REPLACE FUNCTION generate_project_sequence_no(p_project_id BIGINT) RETURNS INTEGER AS $func$
|
||||
DECLARE
|
||||
next_no INTEGER;
|
||||
BEGIN
|
||||
-- 해당 프로젝트의 최대 순번 + 1
|
||||
SELECT COALESCE(MAX(project_sequence_no), 0) + 1
|
||||
INTO next_no
|
||||
FROM issues
|
||||
WHERE project_id = p_project_id;
|
||||
|
||||
RETURN next_no;
|
||||
END;
|
||||
$func$ LANGUAGE plpgsql;
|
||||
RAISE NOTICE '✅ generate_project_sequence_no 함수가 생성되었습니다.';
|
||||
|
||||
-- 기존 이슈들에 대해 프로젝트별 순번 설정
|
||||
DO $update_sequence$
|
||||
DECLARE
|
||||
issue_record RECORD;
|
||||
seq_no INTEGER;
|
||||
BEGIN
|
||||
FOR issue_record IN
|
||||
SELECT id, project_id
|
||||
FROM issues
|
||||
WHERE project_sequence_no IS NULL
|
||||
ORDER BY project_id, report_date
|
||||
LOOP
|
||||
SELECT generate_project_sequence_no(issue_record.project_id) INTO seq_no;
|
||||
UPDATE issues
|
||||
SET project_sequence_no = seq_no
|
||||
WHERE id = issue_record.id;
|
||||
END LOOP;
|
||||
END $update_sequence$;
|
||||
RAISE NOTICE '✅ 기존 이슈들의 프로젝트별 순번이 설정되었습니다.';
|
||||
|
||||
-- 기존 이슈들의 final_description과 final_category 초기화
|
||||
UPDATE issues
|
||||
SET
|
||||
final_description = description,
|
||||
final_category = category
|
||||
WHERE final_description IS NULL OR final_category IS NULL;
|
||||
RAISE NOTICE '✅ 기존 이슈들의 final_description과 final_category가 초기화되었습니다.';
|
||||
|
||||
-- 마이그레이션 검증
|
||||
DECLARE
|
||||
col_count INTEGER;
|
||||
idx_count INTEGER;
|
||||
func_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO col_count FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name IN (
|
||||
'completion_photo_path', 'solution', 'responsible_department', 'responsible_person',
|
||||
'expected_completion_date', 'actual_completion_date', 'cause_department',
|
||||
'management_comment', 'project_sequence_no', 'final_description', 'final_category'
|
||||
);
|
||||
|
||||
SELECT COUNT(*) INTO idx_count FROM pg_indexes
|
||||
WHERE tablename = 'issues' AND indexname IN (
|
||||
'idx_issues_project_sequence', 'idx_issues_responsible_department', 'idx_issues_expected_completion'
|
||||
);
|
||||
|
||||
SELECT COUNT(*) INTO func_count FROM pg_proc WHERE proname = 'generate_project_sequence_no';
|
||||
|
||||
RAISE NOTICE '=== 마이그레이션 검증 결과 ===';
|
||||
RAISE NOTICE '추가된 컬럼: %/11개', col_count;
|
||||
RAISE NOTICE '생성된 인덱스: %/3개', idx_count;
|
||||
RAISE NOTICE '생성된 함수: %/1개', func_count;
|
||||
|
||||
IF col_count = 11 AND idx_count = 3 AND func_count = 1 THEN
|
||||
RAISE NOTICE '✅ 마이그레이션이 성공적으로 완료되었습니다!';
|
||||
INSERT INTO migration_log (migration_file, status, notes) VALUES (migration_name, 'SUCCESS', migration_notes);
|
||||
ELSE
|
||||
RAISE EXCEPTION '❌ 마이그레이션 검증 실패!';
|
||||
END IF;
|
||||
END;
|
||||
|
||||
ELSIF current_status = 'SUCCESS' THEN
|
||||
RAISE NOTICE 'ℹ️ 마이그레이션 %는 이미 성공적으로 실행되었습니다. 스킵합니다.', migration_name;
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ 마이그레이션 %는 이전에 실패했습니다. 수동 확인이 필요합니다.', migration_name;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,87 @@
|
||||
-- 프로젝트별 순번(project_sequence_no) 자동 할당 개선
|
||||
-- 수신함에서 진행 중/완료로 상태 변경 시 프로젝트별 순번이 자동 할당되도록 개선
|
||||
|
||||
DO $migration$
|
||||
DECLARE
|
||||
issue_record RECORD;
|
||||
seq_no INTEGER;
|
||||
updated_count INTEGER := 0;
|
||||
BEGIN
|
||||
RAISE NOTICE '=== 프로젝트별 순번 자동 할당 개선 마이그레이션 시작 ===';
|
||||
|
||||
-- 1. generate_project_sequence_no 함수가 존재하는지 확인
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'generate_project_sequence_no') THEN
|
||||
RAISE EXCEPTION '❌ generate_project_sequence_no 함수가 존재하지 않습니다. 016_add_management_fields.sql을 먼저 실행하세요.';
|
||||
END IF;
|
||||
|
||||
-- 2. 진행 중 또는 완료 상태인데 project_sequence_no가 NULL인 이슈들 찾기
|
||||
RAISE NOTICE '🔍 project_sequence_no가 누락된 이슈들을 찾는 중...';
|
||||
|
||||
FOR issue_record IN
|
||||
SELECT id, project_id, review_status
|
||||
FROM issues
|
||||
WHERE review_status IN ('in_progress', 'completed')
|
||||
AND project_sequence_no IS NULL
|
||||
ORDER BY project_id, reviewed_at NULLS LAST, report_date
|
||||
LOOP
|
||||
-- 프로젝트별 순번 생성
|
||||
SELECT generate_project_sequence_no(issue_record.project_id) INTO seq_no;
|
||||
|
||||
-- 순번 할당
|
||||
UPDATE issues
|
||||
SET project_sequence_no = seq_no
|
||||
WHERE id = issue_record.id;
|
||||
|
||||
updated_count := updated_count + 1;
|
||||
|
||||
RAISE NOTICE '✅ 이슈 ID: %, 프로젝트 ID: %, 할당된 순번: %',
|
||||
issue_record.id, issue_record.project_id, seq_no;
|
||||
END LOOP;
|
||||
|
||||
-- 3. 결과 요약
|
||||
RAISE NOTICE '=== 마이그레이션 완료 ===';
|
||||
RAISE NOTICE '📊 총 %개의 이슈에 프로젝트별 순번이 할당되었습니다.', updated_count;
|
||||
|
||||
-- 4. 검증
|
||||
DECLARE
|
||||
missing_count INTEGER;
|
||||
total_managed_count INTEGER;
|
||||
BEGIN
|
||||
-- 관리 중인 이슈 중 순번이 없는 것들 확인
|
||||
SELECT COUNT(*) INTO missing_count
|
||||
FROM issues
|
||||
WHERE review_status IN ('in_progress', 'completed')
|
||||
AND project_sequence_no IS NULL;
|
||||
|
||||
-- 전체 관리 중인 이슈 수
|
||||
SELECT COUNT(*) INTO total_managed_count
|
||||
FROM issues
|
||||
WHERE review_status IN ('in_progress', 'completed');
|
||||
|
||||
RAISE NOTICE '=== 검증 결과 ===';
|
||||
RAISE NOTICE '전체 관리 중인 이슈: %개', total_managed_count;
|
||||
RAISE NOTICE '순번이 누락된 이슈: %개', missing_count;
|
||||
|
||||
IF missing_count > 0 THEN
|
||||
RAISE WARNING '⚠️ 여전히 %개의 이슈에 순번이 누락되어 있습니다.', missing_count;
|
||||
ELSE
|
||||
RAISE NOTICE '✅ 모든 관리 중인 이슈에 순번이 정상적으로 할당되었습니다.';
|
||||
END IF;
|
||||
END;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE EXCEPTION '❌ 마이그레이션 실행 중 오류 발생: %', SQLERRM;
|
||||
END $migration$;
|
||||
|
||||
-- 마이그레이션 로그 기록
|
||||
INSERT INTO migration_log (migration_file, executed_at, status, notes)
|
||||
VALUES (
|
||||
'017_fix_project_sequence_no.sql',
|
||||
NOW(),
|
||||
'SUCCESS',
|
||||
'프로젝트별 순번 자동 할당 개선: 수신함에서 진행중/완료로 상태 변경시 project_sequence_no 자동 할당되도록 백엔드 로직 개선 및 기존 데이터 보정'
|
||||
) ON CONFLICT (migration_file) DO UPDATE SET
|
||||
executed_at = NOW(),
|
||||
status = EXCLUDED.status,
|
||||
notes = EXCLUDED.notes;
|
||||
@@ -0,0 +1,85 @@
|
||||
-- 수신함 추가 정보 필드 추가
|
||||
-- 원인부서, 해당자, 원인 상세 정보를 기록하기 위한 필드들
|
||||
|
||||
DO $migration$
|
||||
DECLARE
|
||||
col_count INTEGER := 0;
|
||||
idx_count INTEGER := 0;
|
||||
BEGIN
|
||||
RAISE NOTICE '=== 수신함 추가 정보 필드 추가 마이그레이션 시작 ===';
|
||||
|
||||
-- 1. 해당자 상세 정보 (responsible_person과 별도)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'responsible_person_detail') THEN
|
||||
ALTER TABLE issues ADD COLUMN responsible_person_detail VARCHAR(200);
|
||||
RAISE NOTICE '✅ issues.responsible_person_detail 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.responsible_person_detail 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 2. 원인 상세 정보 (기록용)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'cause_detail') THEN
|
||||
ALTER TABLE issues ADD COLUMN cause_detail TEXT;
|
||||
RAISE NOTICE '✅ issues.cause_detail 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.cause_detail 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 3. 추가 정보 입력 시간 (언제 입력되었는지 기록)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'additional_info_updated_at') THEN
|
||||
ALTER TABLE issues ADD COLUMN additional_info_updated_at TIMESTAMP WITH TIME ZONE;
|
||||
RAISE NOTICE '✅ issues.additional_info_updated_at 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.additional_info_updated_at 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 4. 추가 정보 입력자 ID (누가 입력했는지 기록)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'additional_info_updated_by_id') THEN
|
||||
ALTER TABLE issues ADD COLUMN additional_info_updated_by_id INTEGER REFERENCES users(id);
|
||||
RAISE NOTICE '✅ issues.additional_info_updated_by_id 컬럼이 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ issues.additional_info_updated_by_id 컬럼이 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 인덱스 추가 (검색 성능 향상)
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_additional_info_updated_at') THEN
|
||||
CREATE INDEX idx_issues_additional_info_updated_at ON issues (additional_info_updated_at);
|
||||
RAISE NOTICE '✅ idx_issues_additional_info_updated_at 인덱스가 생성되었습니다.';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ idx_issues_additional_info_updated_at 인덱스가 이미 존재합니다.';
|
||||
END IF;
|
||||
|
||||
-- 검증
|
||||
SELECT COUNT(*) INTO col_count FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name IN (
|
||||
'responsible_person_detail', 'cause_detail', 'additional_info_updated_at', 'additional_info_updated_by_id'
|
||||
);
|
||||
|
||||
SELECT COUNT(*) INTO idx_count FROM pg_indexes
|
||||
WHERE tablename = 'issues' AND indexname = 'idx_issues_additional_info_updated_at';
|
||||
|
||||
RAISE NOTICE '=== 마이그레이션 검증 결과 ===';
|
||||
RAISE NOTICE '추가된 컬럼: %/4개', col_count;
|
||||
RAISE NOTICE '생성된 인덱스: %/1개', idx_count;
|
||||
|
||||
IF col_count = 4 AND idx_count = 1 THEN
|
||||
RAISE NOTICE '✅ 모든 추가 정보 필드가 성공적으로 추가되었습니다.';
|
||||
ELSE
|
||||
RAISE WARNING '⚠️ 일부 필드나 인덱스가 누락되었습니다.';
|
||||
END IF;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE EXCEPTION '❌ 마이그레이션 실행 중 오류 발생: %', SQLERRM;
|
||||
END $migration$;
|
||||
|
||||
-- 마이그레이션 로그 기록
|
||||
INSERT INTO migration_log (migration_file, executed_at, status, notes)
|
||||
VALUES (
|
||||
'018_add_additional_info_fields.sql',
|
||||
NOW(),
|
||||
'SUCCESS',
|
||||
'수신함 추가 정보 필드 추가: 해당자 상세(responsible_person_detail), 원인 상세(cause_detail), 입력 시간/입력자 추적 필드'
|
||||
) ON CONFLICT (migration_file) DO UPDATE SET
|
||||
executed_at = NOW(),
|
||||
status = EXCLUDED.status,
|
||||
notes = EXCLUDED.notes;
|
||||
@@ -0,0 +1,28 @@
|
||||
-- 삭제 로그 테이블 추가
|
||||
-- 생성일: 2025-11-08
|
||||
-- 설명: 부적합 등 엔티티 삭제 시 로그를 보관하기 위한 테이블
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deletion_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id INTEGER NOT NULL,
|
||||
entity_data JSONB NOT NULL,
|
||||
deleted_by_id INTEGER NOT NULL REFERENCES users(id),
|
||||
deleted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (NOW() AT TIME ZONE 'Asia/Seoul'),
|
||||
reason TEXT
|
||||
);
|
||||
|
||||
-- 인덱스 추가
|
||||
CREATE INDEX IF NOT EXISTS idx_deletion_logs_entity_type ON deletion_logs(entity_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_deletion_logs_entity_id ON deletion_logs(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deletion_logs_deleted_by ON deletion_logs(deleted_by_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deletion_logs_deleted_at ON deletion_logs(deleted_at);
|
||||
|
||||
-- 테이블 코멘트
|
||||
COMMENT ON TABLE deletion_logs IS '엔티티 삭제 로그 - 삭제된 데이터의 백업 및 추적';
|
||||
COMMENT ON COLUMN deletion_logs.entity_type IS '삭제된 엔티티 타입 (issue, project, daily_work 등)';
|
||||
COMMENT ON COLUMN deletion_logs.entity_id IS '삭제된 엔티티의 ID';
|
||||
COMMENT ON COLUMN deletion_logs.entity_data IS '삭제 시점의 엔티티 전체 데이터 (JSON)';
|
||||
COMMENT ON COLUMN deletion_logs.deleted_by_id IS '삭제 실행자 ID';
|
||||
COMMENT ON COLUMN deletion_logs.deleted_at IS '삭제 시각 (KST)';
|
||||
COMMENT ON COLUMN deletion_logs.reason IS '삭제 사유 (선택사항)';
|
||||
@@ -0,0 +1,37 @@
|
||||
-- 완료 신청 관련 필드 추가
|
||||
-- 마이그레이션: 019_add_completion_request_fields.sql
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 완료 신청 관련 필드들 추가
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'completion_requested_at') THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_requested_at TIMESTAMP WITH TIME ZONE;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'completion_requested_by_id') THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_requested_by_id INTEGER REFERENCES users(id);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'completion_photo_path') THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_photo_path VARCHAR(500);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'completion_comment') THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_comment TEXT;
|
||||
END IF;
|
||||
|
||||
-- 마이그레이션 로그 기록
|
||||
INSERT INTO migration_log (migration_file, executed_at, status, notes)
|
||||
VALUES ('019_add_completion_request_fields.sql', NOW(), 'SUCCESS', 'Added completion request fields: completion_requested_at, completion_requested_by_id, completion_photo_path, completion_comment');
|
||||
|
||||
RAISE NOTICE '✅ 완료 신청 관련 필드 추가 완료';
|
||||
RAISE NOTICE '📝 완료 신청 필드 마이그레이션 완료 - 019_add_completion_request_fields.sql';
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- 오류 발생 시 로그 기록
|
||||
INSERT INTO migration_log (migration_file, executed_at, status, notes)
|
||||
VALUES ('019_add_completion_request_fields.sql', NOW(), 'FAILED', 'Error: ' || SQLERRM);
|
||||
|
||||
RAISE EXCEPTION '❌ 마이그레이션 실패: %', SQLERRM;
|
||||
END $$;
|
||||
@@ -0,0 +1,91 @@
|
||||
-- 020_add_management_completion_fields.sql
|
||||
-- 관리함 완료 신청 정보 필드 추가
|
||||
-- 작성일: 2025-10-26
|
||||
-- 목적: 완료 사진 및 코멘트 수정 기능 지원
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 마이그레이션 로그 확인
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 이미 실행된 마이그레이션인지 확인
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM migration_log
|
||||
WHERE migration_file = '020_add_management_completion_fields.sql'
|
||||
AND status = 'completed'
|
||||
) THEN
|
||||
RAISE NOTICE '마이그레이션이 이미 실행되었습니다: 020_add_management_completion_fields.sql';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- 마이그레이션 시작 로그
|
||||
INSERT INTO migration_log (migration_file, status, started_at, notes)
|
||||
VALUES ('020_add_management_completion_fields.sql', 'running', NOW(),
|
||||
'관리함 완료 신청 정보 필드 추가 - completion_photo, completion_comment 수정 기능');
|
||||
|
||||
-- completion_photo_path 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_photo_path'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_photo_path VARCHAR(500);
|
||||
RAISE NOTICE '✅ completion_photo_path 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_photo_path 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- completion_comment 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_comment'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_comment TEXT;
|
||||
RAISE NOTICE '✅ completion_comment 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_comment 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- completion_requested_at 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_requested_at'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_requested_at TIMESTAMP WITH TIME ZONE;
|
||||
RAISE NOTICE '✅ completion_requested_at 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_requested_at 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- completion_requested_by_id 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_requested_by_id'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_requested_by_id INTEGER REFERENCES users(id);
|
||||
RAISE NOTICE '✅ completion_requested_by_id 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_requested_by_id 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- 마이그레이션 완료 로그
|
||||
UPDATE migration_log
|
||||
SET status = 'completed', completed_at = NOW(),
|
||||
notes = notes || ' - 완료: 모든 필요한 컬럼이 추가되었습니다.'
|
||||
WHERE migration_file = '020_add_management_completion_fields.sql'
|
||||
AND status = 'running';
|
||||
|
||||
RAISE NOTICE '🎉 마이그레이션 완료: 020_add_management_completion_fields.sql';
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- 에러 발생 시 로그 업데이트
|
||||
UPDATE migration_log
|
||||
SET status = 'failed', completed_at = NOW(),
|
||||
notes = notes || ' - 실패: ' || SQLERRM
|
||||
WHERE migration_file = '020_add_management_completion_fields.sql'
|
||||
AND status = 'running';
|
||||
|
||||
RAISE EXCEPTION '❌ 마이그레이션 실패: %', SQLERRM;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user