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:
Hyungi Ahn
2026-02-09 14:40:11 +09:00
commit 550633b89d
824 changed files with 1071683 additions and 0 deletions

View 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);

View File

@@ -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);

View File

@@ -0,0 +1,8 @@
-- 카테고리 업데이트 마이그레이션
-- dimension_defect를 design_error로 변경
UPDATE issues
SET category = 'design_error'
WHERE category = 'dimension_defect';
-- PostgreSQL enum 타입 업데이트 (필요한 경우)
-- 기존 enum 타입 확인 후 필요시 재생성

View File

@@ -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;

View File

@@ -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');

View File

@@ -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);

View File

@@ -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);

View File

@@ -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를 가지고 있음)

View File

@@ -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;

View File

@@ -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 (기타) ✅ 새로 추가됨

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 완료 ==='

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 '삭제 사유 (선택사항)';

View File

@@ -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 $$;

View File

@@ -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;