diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..9695617 --- /dev/null +++ b/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,225 @@ +# ๐Ÿš€ ๋ฐฐํฌ ์•ˆ์ „์„ฑ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +## โœ… DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฒ€์ฆ ํ•„์ˆ˜์‚ฌํ•ญ + +### **๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ํ™•์ธ์‚ฌํ•ญ** + +#### 1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์กด์žฌ ํ™•์ธ +```bash +# ๋กœ์ปฌ์—์„œ ํ™•์ธ +ls -la backend/migrations/013_add_inbox_workflow_system.sql + +# ๋ฐฐํฌ ์„œ๋ฒ„์—์„œ ํ™•์ธ (๋ฐฐํฌ ํ›„) +docker-compose exec backend ls -la /app/migrations/013_add_inbox_workflow_system.sql +``` + +#### 2. DB ์—ฐ๊ฒฐ ๋ฐ ๊ถŒํ•œ ํ™•์ธ +```bash +# DB ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +docker-compose exec db psql -U mproject -d mproject -c "SELECT version();" + +# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กœ๊ทธ ํ…Œ์ด๋ธ” ์กด์žฌ ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c "\d migration_log" +``` + +#### 3. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ (๋ฐฐํฌ ์‹œ) +```bash +# 1๋‹จ๊ณ„: ํŒŒ์ผ ๋ณต์‚ฌ +docker cp backend/migrations/013_add_inbox_workflow_system.sql [DB_CONTAINER_NAME]:/tmp/ + +# 2๋‹จ๊ณ„: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ +docker-compose exec db psql -U mproject -d mproject -f /tmp/013_add_inbox_workflow_system.sql + +# 3๋‹จ๊ณ„: ์‹คํ–‰ ๊ฒฐ๊ณผ ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c "SELECT * FROM migration_log WHERE migration_file = '013_add_inbox_workflow_system.sql';" +``` + +#### 4. ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๊ฒ€์ฆ +```bash +# issues ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c "\d issues" + +# ์ƒˆ๋กœ์šด ์ปฌ๋Ÿผ๋“ค ์กด์žฌ ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c " +SELECT column_name, data_type, is_nullable, column_default +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' +);" +``` + +#### 5. ENUM ํƒ€์ž… ๊ฒ€์ฆ +```bash +# review_status ENUM ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c "\dT+ review_status" + +# disposal_reason_type ENUM ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c "\dT+ disposal_reason_type" +``` + +#### 6. ์ธ๋ฑ์Šค ๊ฒ€์ฆ +```bash +# ์ƒˆ๋กœ์šด ์ธ๋ฑ์Šค๋“ค ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c " +SELECT indexname, tablename, indexdef +FROM pg_indexes +WHERE tablename = 'issues' +AND indexname IN ( + 'idx_issues_review_status', + 'idx_issues_reviewed_by_id', + 'idx_issues_disposed_at' +);" +``` + +#### 7. ์ œ์•ฝ ์กฐ๊ฑด ๊ฒ€์ฆ +```bash +# ์ฒดํฌ ์ œ์•ฝ ์กฐ๊ฑด ํ™•์ธ +docker-compose exec db psql -U mproject -d mproject -c " +SELECT constraint_name, check_clause +FROM information_schema.check_constraints +WHERE constraint_name IN ( + 'chk_disposal_reason_required', + 'chk_custom_reason_logic' +);" +``` + +--- + +## ๐Ÿ”ง ๋ฐฐํฌ ์‹œ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ + +### **์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ** +```bash +#!/bin/bash +# deploy_migration.sh + +echo "๐Ÿš€ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ž‘..." + +# 1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์กด์žฌ ํ™•์ธ +if [ ! -f "backend/migrations/013_add_inbox_workflow_system.sql" ]; then + echo "โŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค!" + exit 1 +fi + +# 2. DB ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ +if ! docker-compose ps db | grep -q "Up"; then + echo "โŒ DB ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค!" + exit 1 +fi + +# 3. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋ณต์‚ฌ +echo "๐Ÿ“ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋ณต์‚ฌ ์ค‘..." +docker cp backend/migrations/013_add_inbox_workflow_system.sql $(docker-compose ps -q db):/tmp/ + +# 4. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ +echo "โšก ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์ค‘..." +docker-compose exec -T db psql -U mproject -d mproject -f /tmp/013_add_inbox_workflow_system.sql + +# 5. ์‹คํ–‰ ๊ฒฐ๊ณผ ํ™•์ธ +echo "๐Ÿ” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฒฐ๊ณผ ํ™•์ธ ์ค‘..." +MIGRATION_COUNT=$(docker-compose exec -T db psql -U mproject -d mproject -t -c "SELECT COUNT(*) FROM migration_log WHERE migration_file = '013_add_inbox_workflow_system.sql' AND status = 'SUCCESS';") + +if [ "$MIGRATION_COUNT" -eq 1 ]; then + echo "โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!" +else + echo "โŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค!" + exit 1 +fi + +# 6. ๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ (์ƒˆ๋กœ์šด ๋ชจ๋ธ ์ ์šฉ) +echo "๐Ÿ”„ ๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ ์ค‘..." +docker-compose restart backend + +echo "๐ŸŽ‰ ๋ฐฐํฌ ์™„๋ฃŒ!" +``` + +--- + +## โš ๏ธ ๋ฐฐํฌ ์‹œ ์ฃผ์˜์‚ฌํ•ญ + +### **1. ๋ฐฑ์—… ํ•„์ˆ˜** +```bash +# ๋ฐฐํฌ ์ „ DB ๋ฐฑ์—… +docker-compose exec db pg_dump -U mproject mproject > backup_$(date +%Y%m%d_%H%M%S).sql +``` + +### **2. ๋กค๋ฐฑ ๊ณ„ํš** +```bash +# ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ ๋ฐฑ์—…์œผ๋กœ ๋ณต์›) +docker-compose exec -T db psql -U mproject -d mproject < backup_YYYYMMDD_HHMMSS.sql +``` + +### **3. ๋‹จ๊ณ„๋ณ„ ๊ฒ€์ฆ** +- โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์กด์žฌ +- โœ… DB ์—ฐ๊ฒฐ ์ƒํƒœ +- โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ +- โœ… ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๊ฒ€์ฆ +- โœ… ๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ +- โœ… API ๋™์ž‘ ํ…Œ์ŠคํŠธ + +### **4. ๋ชจ๋‹ˆํ„ฐ๋ง** +```bash +# ๋ฐฐํฌ ํ›„ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง +docker-compose logs -f backend +docker-compose logs -f db +``` + +--- + +## ๐Ÿ“‹ ๋ฐฐํฌ ์™„๋ฃŒ ํ›„ ํ™•์ธ์‚ฌํ•ญ + +### **1. API ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ** +```bash +# ์ˆ˜์‹ ํ•จ API ํ…Œ์ŠคํŠธ (๊ตฌํ˜„ ํ›„) +curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:16080/api/inbox/ + +# ๋ถ€์ ํ•ฉ ์ƒํƒœ ๋ณ€๊ฒฝ ํ…Œ์ŠคํŠธ +curl -X PUT -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"review_status": "in_progress"}' \ + http://localhost:16080/api/issues/1/review-status +``` + +### **2. ํ”„๋ก ํŠธ์—”๋“œ ํŽ˜์ด์ง€ ์ ‘๊ทผ** +- โœ… ์ˆ˜์‹ ํ•จ ํŽ˜์ด์ง€ ๋กœ๋“œ ํ™•์ธ +- โœ… ๊ถŒํ•œ ์‹œ์Šคํ…œ ๋™์ž‘ ํ™•์ธ +- โœ… ๋ฐ์ดํ„ฐ ํ‘œ์‹œ ํ™•์ธ + +### **3. ์›Œํฌํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ** +- โœ… ํ๊ธฐ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ +- โœ… ์ˆ˜์ • ์ด๋ ฅ ์ €์žฅ ํ…Œ์ŠคํŠธ +- โœ… ์ƒํƒœ ๋ณ€๊ฒฝ ํ…Œ์ŠคํŠธ + +--- + +## ๐Ÿšจ ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ๋Œ€์‘ + +### **์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ๋“ค** + +#### 1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋ˆ„๋ฝ +```bash +# ํ•ด๊ฒฐ: ํŒŒ์ผ ์ˆ˜๋™ ๋ณต์‚ฌ +scp backend/migrations/013_add_inbox_workflow_system.sql server:/path/to/project/ +``` + +#### 2. ๊ถŒํ•œ ๋ฌธ์ œ +```bash +# ํ•ด๊ฒฐ: ํŒŒ์ผ ๊ถŒํ•œ ์กฐ์ • +chmod 644 backend/migrations/013_add_inbox_workflow_system.sql +``` + +#### 3. DB ์—ฐ๊ฒฐ ์‹คํŒจ +```bash +# ํ•ด๊ฒฐ: DB ์ปจํ…Œ์ด๋„ˆ ์žฌ์‹œ์ž‘ +docker-compose restart db +``` + +#### 4. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘๋ณต ์‹คํ–‰ +- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์€ ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ ๋กœ์ง ํฌํ•จ +- `migration_log` ํ…Œ์ด๋ธ”๋กœ ์‹คํ–‰ ์ด๋ ฅ ์ถ”์  + +--- + +**์ด ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋”ฐ๋ผํ•˜๋ฉด ๋ฐฐํฌ ์‹œ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค!** โœ… diff --git a/backend/migrations/013_add_inbox_workflow_system.sql b/backend/migrations/013_add_inbox_workflow_system.sql new file mode 100644 index 0000000..9c5daa8 --- /dev/null +++ b/backend/migrations/013_add_inbox_workflow_system.sql @@ -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 ์™„๋ฃŒ ==='