diff --git a/db/migrations/V006__CreateClients.sql b/db/migrations/V006__CreateClients.sql
index c33b88b..1c3b7e8 100644
--- a/db/migrations/V006__CreateClients.sql
+++ b/db/migrations/V006__CreateClients.sql
@@ -5,10 +5,10 @@ CREATE TABLE IF NOT EXISTS clients (
company_name VARCHAR(200),
phone VARCHAR(30),
email VARCHAR(200),
- service_type VARCHAR(50), -- 기장, 부동산, 증여·상속, 종합소득세, 기타
- tax_type VARCHAR(30), -- 개인, 법인, 면세사업자
+ service_type VARCHAR(50), -- 기장, 부동산, 증여상속, 종합소득세, 기타
+ tax_type VARCHAR(30), -- 개인사업자, 법인사업자, 면세사업자
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, inactive
- source VARCHAR(50), -- 홈페이지문의, 소개, 직접방문, 기타
+ source VARCHAR(50), -- 홈페이지문의, 소개, 직접방문, 카카오채널, 블로그, 기타
memo TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
diff --git a/db/migrations/V007__CreateFaqs.sql b/db/migrations/V007__CreateFaqs.sql
index 76a008d..14831a2 100644
--- a/db/migrations/V007__CreateFaqs.sql
+++ b/db/migrations/V007__CreateFaqs.sql
@@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS faqs (
id SERIAL PRIMARY KEY,
question VARCHAR(300) NOT NULL,
answer TEXT NOT NULL,
- category VARCHAR(50), -- 기장·세금신고, 부동산, 증여·상속, 기타
+ category VARCHAR(50), -- 기장세금신고, 부동산, 증여상속, 기타
sort_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -17,20 +17,20 @@ INSERT INTO faqs (question, answer, category, sort_order, is_active) VALUES
(
'기장료가 얼마인지 미리 알 수 있나요?',
'업종과 매출 규모에 따라 다르지만, 무료 상담 후 정확한 견적을 안내드립니다. 일반적으로 소규모 사업자는 월 10만 원대부터 시작하며, 부가가치세·소득세 신고 시기에는 별도 수수료 없이 포함 처리합니다. 먼저 상담해 보시면 구체적인 금액을 바로 말씀드릴 수 있습니다.',
- '기장·세금신고', 10, TRUE
+ '기장세금신고', 10, TRUE
),
(
'양도세 상담은 어떻게 진행되나요?',
- '등기부등본, 취득·양도 계약서, 보유 기간 확인 서류 등을 카카오 채널 또는 문의폼으로 전달해 주시면 예상 세액과 절세 방법을 검토해 드립니다. 매도 전에 상담하시면 취득세·비과세 요건 등을 사전에 확인할 수 있어 훨씬 유리합니다.',
+ '등기부등본, 취득·양도 계약서, 보유 기간 확인 서류 등을 카카오채널 또는 문의폼으로 전달해 주시면 예상 세액과 절세 방법을 검토해 드립니다. 매도 전에 상담하시면 취득세·비과세 요건 등을 사전에 확인할 수 있어 훨씬 유리합니다.',
'부동산', 20, TRUE
),
(
'무료 상담도 가능한가요?',
- '네, 초기 현황 파악과 방향성 검토까지는 무료로 진행합니다. 카카오 채널 또는 문의폼으로 연락 주시면 빠르게 확인해 드립니다. 실질적인 세무 처리·신고 대행이 시작되는 시점부터 수수료가 발생합니다.',
+ '네, 초기 현황 파악과 방향성 검토까지는 무료로 진행합니다. 카카오채널 또는 문의폼으로 연락 주시면 빠르게 확인해 드립니다. 실질적인 세무 처리·신고 대행이 시작되는 시점부터 수수료가 발생합니다.',
'기타', 30, TRUE
),
(
'처음 상담 시 어떤 자료를 준비해야 하나요?',
- '상담 목적에 따라 다르지만 아래 자료가 있으면 더 정확한 안내가 가능합니다. 사업자 세무: 사업자등록증, 최근 3개월 매출·매입 자료 / 부동산: 등기부등본, 취득·매도 계약서, 보유 기간 확인 자료 / 증여·상속: 재산 목록, 증여 예정 자산 내역. 자료가 없어도 상담은 가능합니다. 먼저 연락 주세요.',
- '기타', 40, TRUE
+ '상담 목적에 따라 다르지만 아래 자료가 있으면 더 정확한 안내가 가능합니다. 사업자 세무: 사업자등록증, 최근 3개월 매출·매입 자료 / 부동산: 등기부등본, 취득·매도 계약서, 보유 기간 확인 자료 / 증여상속: 재산 목록, 증여 예정 자산 내역. 자료가 없어도 상담은 가능합니다. 먼저 연락 주세요.',
+ '증여상속', 40, TRUE
);
diff --git a/db/migrations/V017__CreateCommonCodes.sql b/db/migrations/V017__CreateCommonCodes.sql
index af0cd96..be04dc0 100644
--- a/db/migrations/V017__CreateCommonCodes.sql
+++ b/db/migrations/V017__CreateCommonCodes.sql
@@ -35,13 +35,13 @@ INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
('FILING_TYPE', '법인세', '법인세', 30),
('FILING_TYPE', '원천세', '원천세', 40),
('FILING_TYPE', '양도소득세', '양도소득세', 50),
-('FILING_TYPE', '상속/증여세', '상속/증여세', 60)
+('FILING_TYPE', '상속증여세', '상속·증여세', 60)
ON CONFLICT (code_group, code_value) DO NOTHING;
-- Seed data for SERVICE_TYPE
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
-('SERVICE_TYPE', '개인 기장대리', '개인 기장대리', 10),
-('SERVICE_TYPE', '법인 기장대리', '법인 기장대리', 20),
+('SERVICE_TYPE', '개인기장대리', '개인 기장대리', 10),
+('SERVICE_TYPE', '법인기장대리', '법인 기장대리', 20),
('SERVICE_TYPE', '세무조정', '세무조정', 30),
('SERVICE_TYPE', '세무컨설팅', '세무컨설팅', 40),
('SERVICE_TYPE', '불복청구', '불복청구', 50)
diff --git a/db/migrations/V019__UpdateBlogPostsCleanup.sql b/db/migrations/V019__UpdateBlogPostsCleanup.sql
index 3d8aefb..792220d 100644
--- a/db/migrations/V019__UpdateBlogPostsCleanup.sql
+++ b/db/migrations/V019__UpdateBlogPostsCleanup.sql
@@ -1,9 +1,6 @@
-- V019: Fix blog posts migration (V018 had quote escaping issues)
-- Complete rewrite using $$ quote style to avoid escaping problems
--- Delete posts 6-12 added in V018 (if they exist)
-DELETE FROM blog_posts WHERE id >= 6;
-
-- Re-insert all 12 posts with proper formatting
-- 6. 스마트스토어 판매자를 위한 첫 세무 기장
diff --git a/db/migrations/V020__ImproveBlogs3LayerTemplate.sql b/db/migrations/V020__ImproveBlogs3LayerTemplate.sql
index a5a5763..d5107a3 100644
--- a/db/migrations/V020__ImproveBlogs3LayerTemplate.sql
+++ b/db/migrations/V020__ImproveBlogs3LayerTemplate.sql
@@ -3,8 +3,6 @@
-- Layer 2: Details + Tax law changes (impossible to track alone)
-- Layer 3: Professional value (tax accountants needed)
-DELETE FROM blog_posts WHERE id >= 1;
-
-- 1. 사업자 기장 시 자주 하는 실수 5가지
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
VALUES (
diff --git a/db/migrations/V021__FixBlogPostsAdvertisingCompliance.sql b/db/migrations/V021__FixBlogPostsAdvertisingCompliance.sql
index 03d5c4b..351e051 100644
--- a/db/migrations/V021__FixBlogPostsAdvertisingCompliance.sql
+++ b/db/migrations/V021__FixBlogPostsAdvertisingCompliance.sql
@@ -2,8 +2,6 @@
-- Remove absolute claims, replace with past-tense examples
-- Replace guarantee language with possibility statements
-DELETE FROM blog_posts WHERE id >= 1;
-
-- 1. 사업자 기장 시 자주 하는 실수 5가지
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
VALUES (
diff --git a/db/migrations/V022__ApplyAccuracyPrincipleToBlogs.sql b/db/migrations/V022__ApplyAccuracyPrincipleToBlogs.sql
index 1104f6d..be209d1 100644
--- a/db/migrations/V022__ApplyAccuracyPrincipleToBlogs.sql
+++ b/db/migrations/V022__ApplyAccuracyPrincipleToBlogs.sql
@@ -2,8 +2,6 @@
-- Add tax law citations, 2025 standards, data sources
-- Remove speculation, assumptions, opinions
-DELETE FROM blog_posts WHERE id >= 1;
-
-- 1. 사업자 기장 시 자주 하는 실수 5가지
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
VALUES (
diff --git a/db/migrations/V023__CustomerFriendlyLanguageUpdate.sql b/db/migrations/V023__CustomerFriendlyLanguageUpdate.sql
index 4312057..0b601fd 100644
--- a/db/migrations/V023__CustomerFriendlyLanguageUpdate.sql
+++ b/db/migrations/V023__CustomerFriendlyLanguageUpdate.sql
@@ -2,8 +2,6 @@
-- Remove internal jargon (Layer 1-3, "3층 구조", etc.)
-- Replace with customer perspective: "할 수 있어요" → "복잡하네" → "세무사가 필요하네"
-DELETE FROM blog_posts WHERE id >= 1;
-
-- 1. 사업자 기장 시 자주 하는 실수 5가지
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
VALUES (
diff --git a/db/migrations/V024__UpdateBlogsWithLatestTemplate.sql b/db/migrations/V024__UpdateBlogsWithLatestTemplate.sql
index 10c0164..781f5d6 100644
--- a/db/migrations/V024__UpdateBlogsWithLatestTemplate.sql
+++ b/db/migrations/V024__UpdateBlogsWithLatestTemplate.sql
@@ -3,8 +3,6 @@
-- Simplify emojis (remove section headers like 📊, 🧮)
-- Keep customer-friendly language (1️⃣ 2️⃣ 3️⃣)
-DELETE FROM blog_posts WHERE id >= 1;
-
-- 1. 사업자 기장 시 자주 하는 실수 5가지
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
VALUES (
diff --git a/db/migrations/V025__AddNineBlogPosts.sql b/db/migrations/V025__AddNineBlogPosts.sql
index 967c2bf..b67e970 100644
--- a/db/migrations/V025__AddNineBlogPosts.sql
+++ b/db/migrations/V025__AddNineBlogPosts.sql
@@ -1,8 +1,6 @@
-- V025: Add 9 new blog posts with correct SQL structure
-- All posts follow BLOG_TEMPLATE.md guidelines: 3-step structure, accuracy principle, list format
-DELETE FROM blog_posts WHERE id >= 4;
-
INSERT INTO blog_posts (title, content, slug, category_id, is_published, seo_title, seo_description, tags, created_at, updated_at) VALUES
-- 1. 프리랜서가 놓친 경비 5가지
diff --git a/db/migrations/V025__AddNineBlogPostsProper.sql b/db/migrations/V025__AddNineBlogPostsProper.sql
index 44005f7..a98de7c 100644
--- a/db/migrations/V025__AddNineBlogPostsProper.sql
+++ b/db/migrations/V025__AddNineBlogPostsProper.sql
@@ -2,8 +2,6 @@
-- Each post: 1,500-2,500 words, law citations, 3-step structure
-- 2025 tax year basis, accuracy principle
-DELETE FROM blog_posts WHERE id >= 1;
-
-- 1. 프리랜서가 놓친 경비 5가지
INSERT INTO blog_posts (title, slug, content, category_id, is_published, seo_title, seo_description, tags, created_at, updated_at)
VALUES (
diff --git a/db/migrations/V026__AddBasePostsAndAssignCategories.sql b/db/migrations/V026__AddBasePostsAndAssignCategories.sql
index e750455..c439fa2 100644
--- a/db/migrations/V026__AddBasePostsAndAssignCategories.sql
+++ b/db/migrations/V026__AddBasePostsAndAssignCategories.sql
@@ -6,8 +6,6 @@
-- cat 4 (부가가치세): 부가세 신고, 부가세 기한, 사업자 등록
-- cat 5 (가족자산): 연말정산 환급
-DELETE FROM blog_posts WHERE id >= 1;
-
INSERT INTO blog_posts (title, slug, content, category_id, is_published, seo_title, seo_description, tags, created_at, updated_at) VALUES
-- 기초 3개 포스트 (V022, V024)
diff --git a/db/migrations/V027__BlogSoftDeleteAndSlugIndex.sql b/db/migrations/V027__BlogSoftDeleteAndSlugIndex.sql
new file mode 100644
index 0000000..74700fd
--- /dev/null
+++ b/db/migrations/V027__BlogSoftDeleteAndSlugIndex.sql
@@ -0,0 +1,21 @@
+ALTER TABLE blog_posts
+ ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
+
+DROP INDEX IF EXISTS idx_blog_slug;
+DROP INDEX IF EXISTS blog_posts_slug_key;
+
+CREATE UNIQUE INDEX IF NOT EXISTS ux_blog_posts_slug_active
+ ON blog_posts (slug)
+ WHERE deleted_at IS NULL;
+
+CREATE INDEX IF NOT EXISTS idx_blog_slug_active
+ ON blog_posts (slug)
+ WHERE deleted_at IS NULL;
+
+CREATE INDEX IF NOT EXISTS idx_blog_published_active
+ ON blog_posts (is_published, published_at DESC)
+ WHERE deleted_at IS NULL;
+
+CREATE INDEX IF NOT EXISTS idx_blog_category_active
+ ON blog_posts (category_id)
+ WHERE deleted_at IS NULL;
diff --git a/db/migrations/V028__SeedAdminComboCommonCodes.sql b/db/migrations/V028__SeedAdminComboCommonCodes.sql
new file mode 100644
index 0000000..2ac052f
--- /dev/null
+++ b/db/migrations/V028__SeedAdminComboCommonCodes.sql
@@ -0,0 +1,131 @@
+-- Seed and normalize admin common codes.
+INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
+('INQUIRY_SERVICE_TYPE', '사업자세무', '사업자세무', 10),
+('INQUIRY_SERVICE_TYPE', '부동산세금', '부동산세금', 20),
+('INQUIRY_SERVICE_TYPE', '가족자산', '가족자산', 30),
+('INQUIRY_SERVICE_TYPE', '기타', '기타', 40),
+
+('INQUIRY_STATUS', 'new', '신규', 10),
+('INQUIRY_STATUS', 'consulting', '상담중', 20),
+('INQUIRY_STATUS', 'contracted', '계약완료', 30),
+('INQUIRY_STATUS', 'rejected', '거절', 40),
+('INQUIRY_STATUS', 'closed', '종결', 50),
+
+('CLIENT_STATUS', 'active', '활성', 10),
+('CLIENT_STATUS', 'inactive', '비활성', 20),
+
+('CLIENT_SERVICE_TYPE', '기장', '기장', 10),
+('CLIENT_SERVICE_TYPE', '부동산', '부동산', 20),
+('CLIENT_SERVICE_TYPE', '증여상속', '증여·상속', 30),
+('CLIENT_SERVICE_TYPE', '종합소득세', '종합소득세', 40),
+('CLIENT_SERVICE_TYPE', '법인세', '법인세', 50),
+('CLIENT_SERVICE_TYPE', '부가가치세', '부가가치세', 60),
+('CLIENT_SERVICE_TYPE', '기타', '기타', 70),
+
+('CLIENT_TAX_TYPE', '개인사업자', '개인사업자', 10),
+('CLIENT_TAX_TYPE', '법인사업자', '법인사업자', 20),
+('CLIENT_TAX_TYPE', '면세사업자', '면세사업자', 30),
+('CLIENT_TAX_TYPE', '근로소득자', '근로소득자', 40),
+('CLIENT_TAX_TYPE', '기타', '기타', 50),
+
+('CLIENT_SOURCE', '홈페이지문의', '홈페이지 문의', 10),
+('CLIENT_SOURCE', '소개', '소개', 20),
+('CLIENT_SOURCE', '직접방문', '직접 방문', 30),
+('CLIENT_SOURCE', '카카오채널', '카카오 채널', 40),
+('CLIENT_SOURCE', '블로그', '블로그', 50),
+('CLIENT_SOURCE', '기타', '기타', 60),
+
+('CONTRACT_SERVICE_TYPE', '개인기장대리', '개인 기장대리', 10),
+('CONTRACT_SERVICE_TYPE', '법인기장대리', '법인 기장대리', 20),
+('CONTRACT_SERVICE_TYPE', '세무조정', '세무조정', 30),
+('CONTRACT_SERVICE_TYPE', '세무컨설팅', '세무컨설팅', 40),
+('CONTRACT_SERVICE_TYPE', '불복청구', '불복청구', 50),
+
+('REVENUE_SERVICE_TYPE', '기장수수료', '기장 수수료', 10),
+('REVENUE_SERVICE_TYPE', '세무조정료', '세무조정료', 20),
+('REVENUE_SERVICE_TYPE', '세무상담료', '세무상담료', 30),
+('REVENUE_SERVICE_TYPE', '신고대행료', '신고 대행료', 40),
+('REVENUE_SERVICE_TYPE', '자문수수료', '자문 수수료', 50),
+
+('FILING_TYPE', '종합소득세', '종합소득세', 10),
+('FILING_TYPE', '부가가치세', '부가가치세', 20),
+('FILING_TYPE', '법인세', '법인세', 30),
+('FILING_TYPE', '원천세', '원천세', 40),
+('FILING_TYPE', '양도소득세', '양도소득세', 50),
+('FILING_TYPE', '상속증여세', '상속·증여세', 60),
+('FILING_TYPE', '세무조정', '세무조정', 70),
+
+('TAX_RISK_LEVEL', 'low', '낮음', 10),
+('TAX_RISK_LEVEL', 'normal', '보통', 20),
+('TAX_RISK_LEVEL', 'high', '높음', 30)
+ON CONFLICT (code_group, code_value) DO UPDATE
+SET code_name = EXCLUDED.code_name,
+ sort_order = EXCLUDED.sort_order,
+ is_active = TRUE;
+
+-- Normalize storage keys and migrate existing rows.
+UPDATE common_codes
+SET code_value = CASE
+ WHEN code_group = 'CLIENT_SERVICE_TYPE' AND code_value = '증여·상속' THEN '증여상속'
+ WHEN code_group = 'CLIENT_SOURCE' AND code_value = '홈페이지 문의' THEN '홈페이지문의'
+ WHEN code_group = 'CLIENT_SOURCE' AND code_value = '직접 방문' THEN '직접방문'
+ WHEN code_group = 'CLIENT_SOURCE' AND code_value = '카카오 채널' THEN '카카오채널'
+ WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '개인 기장대리' THEN '개인기장대리'
+ WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '법인 기장대리' THEN '법인기장대리'
+ WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '기장 수수료' THEN '기장수수료'
+ WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '신고 대행료' THEN '신고대행료'
+ WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '자문 수수료' THEN '자문수수료'
+ WHEN code_group = 'FILING_TYPE' AND code_value = '상속·증여세' THEN '상속증여세'
+ ELSE code_value
+ END,
+ code_name = CASE
+ WHEN code_group = 'CLIENT_SERVICE_TYPE' AND code_value = '증여·상속' THEN '증여·상속'
+ WHEN code_group = 'CLIENT_SOURCE' AND code_value = '홈페이지 문의' THEN '홈페이지 문의'
+ WHEN code_group = 'CLIENT_SOURCE' AND code_value = '직접 방문' THEN '직접 방문'
+ WHEN code_group = 'CLIENT_SOURCE' AND code_value = '카카오 채널' THEN '카카오 채널'
+ WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '개인 기장대리' THEN '개인 기장대리'
+ WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '법인 기장대리' THEN '법인 기장대리'
+ WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '기장 수수료' THEN '기장 수수료'
+ WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '신고 대행료' THEN '신고 대행료'
+ WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '자문 수수료' THEN '자문 수수료'
+ WHEN code_group = 'FILING_TYPE' AND code_value = '상속·증여세' THEN '상속·증여세'
+ ELSE code_name
+ END
+WHERE (code_group, code_value) IN (
+ ('CLIENT_SERVICE_TYPE', '증여·상속'),
+ ('CLIENT_SOURCE', '홈페이지 문의'),
+ ('CLIENT_SOURCE', '직접 방문'),
+ ('CLIENT_SOURCE', '카카오 채널'),
+ ('CONTRACT_SERVICE_TYPE', '개인 기장대리'),
+ ('CONTRACT_SERVICE_TYPE', '법인 기장대리'),
+ ('REVENUE_SERVICE_TYPE', '기장 수수료'),
+ ('REVENUE_SERVICE_TYPE', '신고 대행료'),
+ ('REVENUE_SERVICE_TYPE', '자문 수수료'),
+ ('FILING_TYPE', '상속·증여세')
+);
+
+UPDATE clients
+SET
+ service_type = CASE WHEN service_type = '증여·상속' THEN '증여상속' ELSE service_type END,
+ source = CASE
+ WHEN source = '홈페이지 문의' THEN '홈페이지문의'
+ WHEN source = '직접 방문' THEN '직접방문'
+ WHEN source = '카카오 채널' THEN '카카오채널'
+ ELSE source
+ END;
+
+UPDATE contracts
+SET service_type = REPLACE(REPLACE(service_type, ' ', ''), '·', '')
+WHERE service_type IS NOT NULL;
+
+UPDATE revenue_tracking
+SET service_type = REPLACE(REPLACE(service_type, ' ', ''), '·', '')
+WHERE service_type IS NOT NULL;
+
+UPDATE tax_filings
+SET filing_type = '상속증여세'
+WHERE filing_type = '상속·증여세';
+
+UPDATE tax_filing_schedules
+SET filing_type = '상속증여세'
+WHERE filing_type = '상속·증여세';
diff --git a/db/migrations/V029__WidenCommonCodeAndComboColumns.sql b/db/migrations/V029__WidenCommonCodeAndComboColumns.sql
new file mode 100644
index 0000000..79e65cd
--- /dev/null
+++ b/db/migrations/V029__WidenCommonCodeAndComboColumns.sql
@@ -0,0 +1,22 @@
+-- Allow Korean code values and future growth without truncation risk.
+ALTER TABLE common_codes
+ ALTER COLUMN code_group TYPE VARCHAR(80),
+ ALTER COLUMN code_value TYPE VARCHAR(120),
+ ALTER COLUMN code_name TYPE VARCHAR(200);
+
+ALTER TABLE clients
+ ALTER COLUMN service_type TYPE VARCHAR(100),
+ ALTER COLUMN tax_type TYPE VARCHAR(60),
+ ALTER COLUMN source TYPE VARCHAR(100);
+
+ALTER TABLE contracts
+ ALTER COLUMN service_type TYPE VARCHAR(120);
+
+ALTER TABLE revenue_tracking
+ ALTER COLUMN service_type TYPE VARCHAR(120);
+
+ALTER TABLE tax_filings
+ ALTER COLUMN filing_type TYPE VARCHAR(120);
+
+ALTER TABLE tax_filing_schedules
+ ALTER COLUMN filing_type TYPE VARCHAR(120);
diff --git a/docs/ADMIN_PATTERN_CRITIQUE_WBS.md b/docs/ADMIN_PATTERN_CRITIQUE_WBS.md
new file mode 100644
index 0000000..7d52cc7
--- /dev/null
+++ b/docs/ADMIN_PATTERN_CRITIQUE_WBS.md
@@ -0,0 +1,97 @@
+# Admin Pattern Critique And WBS
+
+대상은 어드민 Blog, 문의사항, 등록/수정 페이지 전반이다. 이 문서는 비판, 개선 방향, 정량 완료 기준을 한 곳에 둔다.
+
+## Brutal Critique
+
+| 영역 | 현재 문제 | 왜 위험한가 | 개선 기준 |
+| --- | --- | --- | --- |
+| API-first 위반 | 어드민 Razor 컴포넌트가 `BlogService`, `InquiryService`, repository를 직접 주입 | 어드민을 클라이언트 사이드 Blazor WebAssembly로 운용할 때 구조가 깨지고 API 계약 테스트가 우회된다 | 모든 어드민 화면은 BrowserClient를 통해 `/api/*` 호출 |
+| Blog 등록/수정 중복 | `BlogCreate.razor`와 `BlogEdit.razor`가 필드, JS 편집기, 저장 로직을 반복 | 한쪽만 수정되는 파편화가 생긴다 | `BlogForm.razor` + `BlogEditorJsModule` 패턴 |
+| JS 과다/전역 상태 | `window.easyMDEInstance` 단일 전역 인스턴스 사용 | 페이지 이동/다중 편집/재렌더에서 내용 섞임 위험, Blazor 책임 경계가 흐려진다 | JS 제거 우선 검토, 불가피하면 JS module + element별 instance map + dispose |
+| 문의 수정 착시 | `InquiryEdit`가 이름/전화/이메일/내용 수정 UI를 보여주지만 실제 저장은 상태/메모 중심 | 운영자가 저장 성공을 믿어도 핵심 데이터가 DB에 반영되지 않을 수 있다 | 전체 수정 API를 만들거나 해당 필드를 read-only 처리 |
+| 문자열 상태 난립 | 문의 상태, 서비스 유형이 UI 문자열/API 문자열/DB 값으로 분산 | 오타 하나가 통계와 필터를 깨뜨린다 | enum/공통코드/상태 mapper 단일화 |
+| 삭제 위험 | Blog/Inquiry 삭제가 즉시 hard delete | 운영 감사, 상담 이력, SEO URL 보존에 취약 | soft delete 또는 archive 정책 |
+| 정합성 부족 | Blog slug 생성이 전체 목록 조회 기반 | 동시 생성 충돌에 약하고 데이터가 늘면 느려진다 | DB unique index + 충돌 재시도 |
+| 템플릿 부재 | CRUD 페이지마다 버튼, 오류, 로딩, 페이징 패턴이 다름 | 바이브코딩식 흔들림이 반복된다 | List/Form/Detail/PageState 템플릿화 |
+| 배포 완료 착시 | 문서상 완료 항목과 운영 검증 항목이 섞임 | 체크박스가 실제 성공을 대체한다 | WBS는 수치, 로그, CI URL로만 완료 |
+
+## Target Admin Pattern
+
+```text
+Razor Page/Form
+ -> BrowserClient with JWT
+ -> Controller DTO
+ -> Application Service
+ -> Repository
+ -> DB constraints/indexes
+```
+
+어드민은 클라이언트 사이드 Blazor WebAssembly 기준이다. 예외는 명시해야 한다. 서버 전용 컴포넌트가 Application Service를 직접 호출해야 한다면 `ENGINEERING_HARNESS.md`의 API-first 기준에 대한 사유와 제거 예정 WBS를 남긴다.
+
+## Quantitative Success Metrics
+
+| 지표 | 기준값 | 측정 방법 |
+| --- | --- | --- |
+| Admin direct service injection | 0건 | `rg "@inject .*Service|@inject I.*Repository" TaxBaik.Web/Components/Admin` |
+| Blog create/edit duplicate fields | 0개 중복 폼 | `BlogForm.razor` 단일 사용 여부 |
+| Admin JavaScript surface | 필수 module만 허용 | `window.*` 전역 admin JS 0건, JS interop 사유 문서화 |
+| Inquiry visible-but-unsaved fields | 0개 | E2E로 수정 후 API 재조회 |
+| Protected admin API anonymous access | 0개 | API smoke에서 401/403 확인 |
+| CI required gates | 6/6 통과 | build, unit, publish, deploy, browser e2e, api smoke |
+| Playwright admin flows | 8개 이상 통과 | login, blog CRUD, inquiry CRUD/status, responsive, password, smoke |
+| DB integrity constraints | 핵심 테이블 100% | PK, FK, unique/check/index 리뷰 |
+| WBS evidence coverage | 100% | 각 완료 항목에 command/log/test 파일 기재 |
+
+## Roadmap
+
+| Phase | 목적 | 종료 조건 |
+| --- | --- | --- |
+| P0 Harness | 기준 고정과 문서 최소화 | 이 문서와 Engineering Harness가 README에서 참조됨 |
+| P1 Stabilize | Blog/Inquiry 착시와 중복 제거 | API-first 전환, 공통 폼, 정합성 테스트 통과 |
+| P2 Harden | DB 제약, 충돌 방지, 삭제 정책 | migration + 회귀 테스트 + E2E 통과 |
+| P3 Standardize | CRUD 템플릿화와 반복 패턴 제거 | 신규 CRUD 생성 시 템플릿만 사용 |
+| P4 Integrate | 더존 UX 정신 내재화 | 고밀도 화면, 표준 동선, 빠른 입력, 상태 가시성, 회귀 최소화 검증 |
+| P5 Operate | CI/CD와 운영 지표 고도화 | 배포본 기준 smoke/E2E/로그 알림 안정화 |
+
+## Detailed WBS
+
+| ID | 작업 | 산출물 | 정량 완료 기준 |
+| --- | --- | --- | --- |
+| P0-01 | 문서 기준점 정리 | `docs/INDEX.md`, `ENGINEERING_HARNESS.md` | canonical 문서 3개 이하, README 링크 1곳 |
+| P0-02 | 기존 장문 문서 역할 축소 | README 문서 섹션 정리 | `CLAUDE.md`를 보조자료로 표시 |
+| P1-01 | Blog API client 도입 | `IBlogBrowserClient`, `BlogBrowserClient` | Blog admin page direct service/repository injection 0건 |
+| P1-02 | Blog 공통 폼 도입 | `BlogForm.razor` | create/edit 필드 중복 0건, 저장 E2E 2개 통과 |
+| P1-03 | Markdown editor JS 최소화/격리 | Blazor 대체 또는 JS module | 전역 `window.easyMDEInstance` 사용 0건, JS interop 사유 명시 |
+| P1-04 | Inquiry 수정 계약 확정 | `UpdateInquiryRequest` 또는 read-only UI | 화면 표시 editable 필드와 저장 필드 불일치 0건 |
+| P1-05 | Inquiry API client 도입 | `IInquiryBrowserClient` 정비 | Inquiry admin direct service injection 0건 |
+| P1-06 | 상태/서비스 유형 단일화 | enum/common code/mapper | 상태 문자열 하드코딩 UI 위치 0건 또는 공통 상수 참조 |
+| P2-01 | Blog slug 충돌 방지 | unique index + retry | 동시 생성 테스트 1개 통과 |
+| P2-02 | 삭제 정책 정리 | soft delete migration 또는 archive 정책 | hard delete 운영 엔티티 0건 또는 예외 문서화 |
+| P2-03 | DB index 점검 | migration | 목록/검색/상태 필터 explain 기준 seq scan 위험 제거 |
+| P2-04 | 낙관적 충돌 방지 | `updatedAt` 조건부 update | stale update API 테스트 1개 이상 통과 |
+| P3-01 | CRUD 템플릿 작성 | page/form/client/test skeleton | 신규 admin CRUD 생성 시간 30% 감소 |
+| P3-02 | 공통 PageState/Error 처리 | reusable component/service | admin page 중복 try/catch/snackbar 패턴 50% 감소 |
+| P3-03 | 메뉴/라우팅 표준화 | route registry 또는 constants | admin route 문자열 중복 50% 감소 |
+| P4-01 | 더존 UX 패턴 캡슐화 | 고밀도 grid/form/template 규칙 | 신규 어드민 화면이 템플릿을 따르지 않는 경우 0건 |
+| P4-02 | UX 회귀 검증 | responsive, keyboard flow, density, state visibility test | 핵심 CRUD 화면 E2E 100% 통과 |
+| P5-01 | CI gate 명문화 | workflow 체크 목록 | 6개 gate 모두 required |
+| P5-02 | 배포본 API smoke 확장 | workflow curl 추가 | Blog/Inquiry create-read-update test 2xx |
+| P5-03 | 운영 회귀 대시보드 | test report/version endpoint | 배포 커밋과 E2E 결과 추적 가능 |
+
+## Immediate Refactor Order
+
+1. `InquiryEdit` 착시 제거: 전체 수정 API를 추가하거나 저장 안 되는 필드를 read-only로 바꾼다.
+2. `BlogForm.razor`를 만들고 create/edit 중복을 제거한다.
+3. Blog/Inquiry 어드민 페이지를 BrowserClient 경유로 바꾼다.
+4. 상태/서비스 유형 문자열을 단일 source로 모은다.
+5. DB 제약과 삭제 정책을 migration으로 고정한다.
+
+## Completion Rule
+
+WBS 항목은 다음 네 가지가 모두 있어야 완료다.
+
+- 관련 코드 또는 문서 diff
+- 로컬 검증 명령과 결과
+- CI/CD workflow 성공
+- 배포본 기준 API 또는 Browser E2E 증거
diff --git a/docs/COMBO_POLICY.md b/docs/COMBO_POLICY.md
new file mode 100644
index 0000000..9f8dedc
--- /dev/null
+++ b/docs/COMBO_POLICY.md
@@ -0,0 +1,72 @@
+# Combo Policy
+
+이 문서는 TaxBaik 어드민의 콤보 정책을 정한다. 여기서 콤보는 `MudSelect`, `MudAutocomplete`, `MudChip`, 상태 필터, 코드 선택 입력을 포함한다.
+
+## Policy
+
+- 닫힌 집합은 `MudSelect`를 쓴다.
+- 열린 집합 또는 검색이 필요한 집합은 `MudAutocomplete`를 쓴다.
+- 상태/유형/등급처럼 값이 고정된 항목은 문자열 직접 입력을 금지한다.
+- 선택한 값은 저장 값과 표시 값을 분리한다.
+- 표시 값은 사람이 읽는 라벨, 저장 값은 코드값이어야 한다.
+- `null` 허용 여부는 UI에서 명시한다.
+- `전체`, `선택 안 함`, `기타`는 서로 다른 의미로 취급한다.
+- 다중 선택이 필요하면 단일 선택 콤보를 억지로 재사용하지 않는다.
+
+## Closed Set
+
+다음 경우 `MudSelect`를 기본으로 사용한다.
+
+- 상태
+- 세금 유형
+- 신고 유형
+- 위험도
+- 고정 서비스 유형
+- 공지 유형
+
+규칙:
+
+- 값은 상수, enum, 공통코드 중 하나에서만 가져온다.
+- `MudSelectItem`의 라벨과 값은 일치하는 쌍으로 관리한다.
+- 운영자가 값의 의미를 추측해야 하는 항목은 콤보로 두지 않는다.
+
+## Search Set
+
+다음 경우 `MudAutocomplete`를 기본으로 사용한다.
+
+- 고객 선택
+- 회사 선택
+- 데이터가 많아 스크롤 선택이 비효율적인 경우
+
+규칙:
+
+- 검색어 입력 후 서버 또는 클라이언트 필터 결과를 보여준다.
+- 결과가 적을 때는 `MudSelect`보다 `MudAutocomplete`를 우선하지 않는다.
+- 선택 후 보여주는 텍스트와 저장되는 id를 분리한다.
+
+## Display Rules
+
+- 목록에서는 상태를 칩으로 보여준다.
+- 폼에서는 텍스트보다 구조화된 값으로 저장한다.
+- 필터에서는 현재 선택값이 명확히 보이게 한다.
+- `Clearable`은 의미가 명확한 경우에만 켠다.
+
+## Standard Sources
+
+- 상태 값은 `InquiryStatusMapper` 또는 전용 enum을 사용한다.
+- 공지/신고/세무 정보는 각 도메인별 공통코드 소스를 둔다.
+- 고객/회사 선택은 검색형 콤보로 통일한다.
+
+## Anti-Patterns
+
+- 같은 화면에 `MudSelect`와 자유 텍스트 입력을 섞어 같은 의미를 표현
+- 코드값과 표시값을 뒤섞어서 저장
+- 콤보 옵션을 화면마다 하드코딩
+- `기타`를 예외 처리처럼 쓰고 실제 저장 값은 제각각 두는 것
+- `전체`를 저장 값으로 사용
+
+## Acceptance Criteria
+
+- 신규 어드민 화면은 이 문서의 `Closed Set`/`Search Set` 중 하나를 명시해야 한다.
+- 상태/유형/등급 입력이 있는 화면은 콤보 정책 위반이 없어야 한다.
+- 고객/회사처럼 데이터가 많은 항목은 검색형 선택으로 통일한다.
diff --git a/docs/COMMON_CODE_POLICY.md b/docs/COMMON_CODE_POLICY.md
new file mode 100644
index 0000000..22619f3
--- /dev/null
+++ b/docs/COMMON_CODE_POLICY.md
@@ -0,0 +1,55 @@
+# Common Code Policy
+
+이 문서는 어드민 콤보, 상태, 유형, 출처 값의 단일 기준이다. 값은 DB `common_codes`를 우선 사용하고, 화면은 표시명만 바꾼다.
+
+## Canonical Rules
+
+- `code_value`는 저장 키다.
+- `code_name`은 화면 표시값이다.
+- `code_value`는 공백을 넣지 않는다.
+- 새 콤보를 추가할 때는 먼저 `common_codes`에 그룹을 추가한다.
+- 화면 하드코딩 배열은 금지한다. 불가피하면 임시 폴백으로만 두고 제거 계획을 함께 적는다.
+- 같은 의미의 값이 테이블마다 다르면 저장값을 먼저 통일하고 마이그레이션으로 이관한다.
+
+## Grouping Rules
+
+- 상태값: `*_STATUS`
+- 유형값: `*_TYPE`
+- 출처값: `*_SOURCE`
+- 위험도/스코어: `*_LEVEL`
+
+## Standard Groups
+
+- `INQUIRY_SERVICE_TYPE`
+- `INQUIRY_STATUS`
+- `CLIENT_STATUS`
+- `CLIENT_SERVICE_TYPE`
+- `CLIENT_TAX_TYPE`
+- `CLIENT_SOURCE`
+- `CONTRACT_SERVICE_TYPE`
+- `REVENUE_SERVICE_TYPE`
+- `FILING_TYPE`
+- `TAX_RISK_LEVEL`
+- `BUSINESS_TYPE`
+
+## Data Rules
+
+- DB seed와 운영 데이터의 저장값이 다르면 UI를 먼저 맞추지 말고 저장값을 먼저 정규화한다.
+- 한글 코드값을 사용하더라도 컬럼 길이를 먼저 검토하고, 업무 테이블과 마스터 테이블을 함께 조정한다.
+- 표시용 문구가 길면 `code_name`에 둔다.
+
+## UI Rules
+
+- `MudSelect`는 `code_value`를 바인딩하고 `code_name`을 보여준다.
+- 검색형이면 `MudAutocomplete`를 쓰고, 선택형이면 `MudSelect`를 쓴다.
+- 자유 입력을 허용하지 않을 값은 텍스트 필드로 만들지 않는다.
+
+## Acceptance Criteria
+
+- 신규 콤보 추가 시 DB 마이그레이션이 먼저 존재해야 한다.
+- 화면에 하드코딩된 선택값이 없어야 한다.
+- 기존 저장값과 신규 저장값의 불일치가 없어야 한다.
+
+## Audit
+
+- 점검 SQL은 [docs/ops/COMMON_CODE_AUDIT.sql](./ops/COMMON_CODE_AUDIT.sql)를 사용한다.
diff --git a/docs/DOUZONE_UX_GUIDE.md b/docs/DOUZONE_UX_GUIDE.md
new file mode 100644
index 0000000..c957363
--- /dev/null
+++ b/docs/DOUZONE_UX_GUIDE.md
@@ -0,0 +1,104 @@
+# DOUZONE UX Guide
+
+이 문서는 TaxBaik 어드민 UX의 기준선이다. 목표는 더존 세무회계프로그램류의 고밀도 운영 화면을 구현하되, TaxBaik의 도메인과 검증 규칙을 유지하는 것이다.
+
+## UX Principles
+
+- 고밀도 우선: 한 화면에서 상태, 입력, 결과, 작업을 함께 본다.
+- 표준 동선 우선: 목록 -> 상세 -> 수정 -> 저장 흐름을 기본으로 둔다.
+- 빠른 입력 우선: 마우스 최소, 키보드/단축 동선 최대, 기본값 명확화.
+- 상태 가시성 우선: 진행중/성공/실패/비활성/삭제됨을 즉시 구분 가능하게 한다.
+- 회귀 최소화 우선: 같은 화면 패턴은 같은 컴포넌트와 같은 구조를 사용한다.
+- 추측 금지: 의미가 불명확한 텍스트, 상태, 버튼, 색상은 새로 만들지 않는다.
+
+## Layout Template
+
+어드민 화면은 기본적으로 아래 구조를 따른다.
+
+```text
+PageHeader
+FilterBar or ActionBar
+ContentSurface
+ -> DenseGrid or DetailPanel
+ -> EmptyState when empty
+ -> Paging/Footer when needed
+```
+
+권장 규칙:
+
+- 페이지 제목은 1개만 둔다.
+- 보조 설명은 1줄만 둔다.
+- 주요 액션은 우측 상단 또는 헤더 우측에 둔다.
+- 목록은 `Dense`를 기본으로 한다.
+- 상세/수정은 좌우 2열 또는 상단 요약 + 하단 폼 패턴을 우선한다.
+
+## Component Template
+
+### Page Header
+
+- 구성: `Eyebrow`, `Title`, `Subtitle`, `Primary Action`
+- 역할: 화면 맥락 고정, 다음 행동 제시
+- 금지: 동일 화면에 헤더가 2개 이상 존재
+
+### Dense Grid
+
+- 행 간격은 좁게 유지한다.
+- 컬럼은 우선순위 순으로 배치한다.
+- 상태는 텍스트 대신 칩/색상/아이콘으로 함께 보여준다.
+- 작업 버튼은 `보기`, `수정`, `삭제`처럼 짧고 일관되게 둔다.
+
+### Form
+
+- 기본값은 채워진 상태로 시작한다.
+- 저장 전 필수 검증은 화면에서 즉시 보인다.
+- 저장되지 않는 필드는 read-only로 바꾼다.
+- 입력이 많은 폼은 섹션으로 나누되, 섹션 수는 최소화한다.
+
+### Empty State
+
+- 데이터 없음, 필터 결과 없음, 로드 실패를 구분한다.
+- 단순 문구보다 다음 행동 버튼을 함께 둔다.
+
+### Status Chip
+
+- 상태는 문자열 그대로 노출하지 말고 칩으로 시각화한다.
+- 색상은 의미를 유지한다.
+- 동일 상태는 동일 색을 사용한다.
+
+## Text And Labels
+
+- 라벨은 짧게 쓴다.
+- 같은 개념은 같은 단어를 쓴다.
+- 약어는 화면 전체에서 통일한다.
+- 운영자가 오해할 수 있는 추상적인 표현은 금지한다.
+
+## Serving Rules
+
+- 공개 사이트는 SSR, 어드민은 Blazor WebAssembly 기준으로 본다.
+- 어드민 화면은 API-first 경유를 기본으로 한다.
+- JS는 불가피할 때만 사용하고, 모듈로 격리한다.
+- 상태/메뉴/라우트/버튼은 문자열 흩뿌리기를 금지하고 공통 상수 또는 템플릿으로 묶는다.
+
+## Reference Rules
+
+- 이 문서를 어드민 UX의 1차 기준으로 사용한다.
+- 세부 코드 규칙은 [ENGINEERING_HARNESS.md](./ENGINEERING_HARNESS.md)를 따른다.
+- 콤보/선택/검색 규칙은 [COMBO_POLICY.md](./COMBO_POLICY.md)를 따른다.
+- 공통코드/저장값 규칙은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)를 따른다.
+- 패턴 비판과 WBS는 [ADMIN_PATTERN_CRITIQUE_WBS.md](./ADMIN_PATTERN_CRITIQUE_WBS.md)를 따른다.
+- 문서 인덱스는 [INDEX.md](./INDEX.md)를 따른다.
+
+## Prohibited Patterns
+
+- 목록마다 서로 다른 헤더 구조
+- 버튼 색과 의미의 중복/충돌
+- 저장 안 되는 필드를 편집 가능한 척 보여주기
+- 전역 JS 상태에 의존하는 편집기
+- 같은 CRUD 화면의 개별 구현체마다 다른 DOM/행 높이/행동 패턴
+- 불필요한 중첩 컴포넌트와 과한 추상화
+
+## Acceptance Criteria
+
+- 신규 어드민 화면은 이 문서의 레이아웃/컴포넌트 규칙 중 최소 80%를 따른다.
+- 기존 화면은 새로 건드릴 때 이 문서로 수렴한다.
+- 화면 추가 시 `PageHeader`, `EmptyState`, `DenseGrid`, `Form` 패턴 중 하나 이상을 재사용한다.
diff --git a/docs/ENGINEERING_HARNESS.md b/docs/ENGINEERING_HARNESS.md
new file mode 100644
index 0000000..c4ffff2
--- /dev/null
+++ b/docs/ENGINEERING_HARNESS.md
@@ -0,0 +1,96 @@
+# Engineering Harness
+
+이 문서는 TaxBaik 코드가 매번 흔들리지 않도록 막는 최소 하네스다. 여기에 없는 내용은 추측하지 않고 코드, 테스트, 운영 로그, DB 스키마 중 하나로 확인한다.
+
+## Non-Negotiables
+
+| 항목 | 기준 | 실패 판정 |
+| --- | --- | --- |
+| Runtime | ASP.NET Core `net10.0` 기준 유지 | 프로젝트별 TargetFramework 불일치 |
+| Public UI | 홈페이지/공개 페이지는 서버 사이드 렌더링 기준 | 공개 페이지가 불필요하게 WASM 번들에 의존 |
+| Admin UI | 어드민은 클라이언트 사이드 Blazor WebAssembly + MudBlazor + API-first | 어드민 컴포넌트가 Application/Repository를 직접 주입 |
+| API | 모든 운영 기능은 `/api/*` DTO 경유 | UI 전용 서비스 호출만 존재 |
+| Auth | JWT 인증, 관리자 API는 `[Authorize]` | 익명으로 관리자 데이터 접근 가능 |
+| Deploy | Gitea Actions CI/CD만 배포 경로 | 수동 SSH/복사로 운영 반영 |
+| Evidence | 빌드, 테스트, E2E, API smoke 로그 | "확인함", "될 것" 같은 진술 |
+
+## Architecture Guardrails
+
+- Domain은 엔티티, enum, repository interface만 가진다.
+- Application은 use case와 검증 규칙을 가진다. HTTP, JS, MudBlazor, DB 연결 세부를 모른다.
+- Infrastructure는 Dapper SQL과 외부 시스템 구현을 가진다.
+- Web은 Controller, 공개 Razor Pages SSR, Blazor host, 인증/서빙 설정을 가진다.
+- Web.Client/Admin UI는 클라이언트 사이드 Blazor WebAssembly로 본다. 서버 DI 서비스에 의존하지 않고 API client만 호출한다.
+- JavaScript는 최소화한다. 브라우저 API, 인증 토큰 저장, 서드파티 편집기처럼 Blazor/MudBlazor만으로 해결하기 부적절한 경우에만 JS module로 격리한다.
+- 상속은 프레임워크 요구 또는 명확한 다형성 모델에만 사용한다. 폼/테이블/CRUD 재사용은 기본적으로 컴포넌트 합성과 작은 service/client로 처리한다.
+
+## Code Quality Harness
+
+| 원칙 | 적용 방식 |
+| --- | --- |
+| SOLID | 페이지는 orchestration만, 검증은 Application, 저장은 Repository, HTTP 계약은 DTO |
+| 유지보수 | Blog/Inquiry 같은 CRUD는 `List`, `Form`, `Client`, `Dto`, `Validator` 패턴으로 고정 |
+| 리팩토링 | 동작 보존 테스트를 먼저 추가하고 작은 단위로 이동 |
+| 일관성 | 오류 응답은 ProblemDetails, 페이징은 `{ data, total, page, pageSize }` |
+| 파편화 방지 | 같은 필드/상태/서비스유형 문자열은 enum/상수/공통 코드 중 하나로 단일화 |
+| 과유불급 | 추상화는 2개 이상 실제 사용처가 생긴 뒤 도입 |
+| 정규화 | 고객, 문의, 상담, 계약, 세금신고는 원천 테이블을 분리 |
+| 역정규화 | 대시보드/검색/운영 요약용 스냅샷만 허용하고 원천 id와 갱신 시점을 저장 |
+| 충돌방지 | 수정 API는 가능하면 `updatedAt` 또는 row version 기반 충돌 감지를 둔다 |
+| 더존 UX 정신 | 더존 세무회계프로그램처럼 고밀도, 표준 동선, 빠른 입력, 상태 가시성, 회귀 최소화를 기본 UX 원칙으로 삼는다 |
+| 추측금지 | 세법, 세율, 더존 필드, 운영 계정, 배포 결과는 공식 자료/코드/DB/로그 없이는 단정하지 않는다 |
+| JS 최소화 | Blazor/MudBlazor 우선, 불가피한 JS는 module + dispose + 테스트 가능한 얇은 wrapper |
+| 공통코드 | 상태/유형/출처/위험도는 `common_codes`를 우선 소스로 사용하고 화면 하드코딩을 금지 |
+
+## Data Integrity Harness
+
+- DB 제약 조건이 1차 방어선이다: NOT NULL, UNIQUE, FK, CHECK, index.
+- Application validation은 사용자 메시지와 use case 규칙을 담당한다.
+- UI validation은 빠른 피드백일 뿐이며 유일한 검증으로 보지 않는다.
+- 관리자 수정 화면에 노출한 필드는 실제 저장되어야 한다. 저장하지 않는 필드는 read-only로 표시한다.
+- 상태 전이는 허용 목록을 둔다. 임의 문자열 저장을 금지한다.
+- 삭제는 운영 데이터 손실 위험이 있으면 soft delete 또는 archive를 우선 검토한다.
+- 콤보 값은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)를 1차 기준으로 삼는다.
+
+## API-First Admin Pattern
+
+새 어드민 기능은 클라이언트 사이드 Blazor WebAssembly를 기준으로 아래 구조를 기본 템플릿으로 따른다.
+
+| Layer | Naming | 책임 |
+| --- | --- | --- |
+| DTO | `CreateXRequest`, `UpdateXRequest`, `XResponse` | HTTP 계약 |
+| Controller | `XController` | 인증, 라우팅, status code, ProblemDetails |
+| Client | `IXBrowserClient`, `XBrowserClient` | JWT 포함 HTTP 호출 |
+| Page | `XList.razor`, `XCreate.razor`, `XEdit.razor` | 화면 상태와 navigation |
+| Form | `XForm.razor` | 입력 컴포넌트와 UI validation |
+| Tests | unit + Playwright/API smoke | 회귀 방지 |
+
+## Rendering Boundary
+
+| 영역 | 렌더링 | 데이터 접근 |
+| --- | --- | --- |
+| Public Home/Blog/Contact | 서버 사이드 렌더링 | 서버 Application Service 직접 사용 가능 |
+| Admin | 클라이언트 사이드 Blazor WebAssembly | JWT 포함 HTTP API만 사용 |
+| Shared DTO | 서버/클라이언트 공유 가능 | UI 전용 상태와 DB 엔티티를 섞지 않음 |
+
+공개 페이지의 SEO와 초기 로딩은 SSR로 최적화한다. 어드민은 앱처럼 동작해야 하므로 WebAssembly와 API 계약을 기준으로 설계한다.
+
+## CI Harness
+
+완료는 로컬 성공이 아니라 CI와 배포본 성공이다.
+
+| Gate | Command/Check | Target |
+| --- | --- | --- |
+| Build | `dotnet build TaxBaik.sln -c Release --no-restore` | error 0 |
+| Unit | `dotnet test TaxBaik.sln -c Release --no-build` | failed 0 |
+| Browser | `npx playwright test --project="Desktop Chrome"` | failed 0 |
+| API Smoke | login + protected admin API curl | HTTP 2xx |
+| Deploy | `.gitea/workflows/deploy.yml` | success |
+| Post Deploy | `.gitea/workflows/browser-e2e.yml` | success |
+
+## Stop Conditions
+
+- 동일 개념이 3곳 이상 다른 이름/계약으로 구현되면 기능 추가를 중단하고 정리한다.
+- UI가 저장한다고 보이는 필드를 API/Application이 저장하지 않으면 릴리스하지 않는다.
+- 운영 배포 검증이 CI 밖에서만 가능하면 완료로 보지 않는다.
+- 데이터 모델을 추측해서 세무 규칙이나 더존 UX 관습을 왜곡해 구현하지 않는다.
diff --git a/docs/INDEX.md b/docs/INDEX.md
new file mode 100644
index 0000000..2fc89b6
--- /dev/null
+++ b/docs/INDEX.md
@@ -0,0 +1,31 @@
+# TaxBaik Engineering Index
+
+이 디렉터리의 문서만 현재 개발 기준의 기준점으로 사용한다. 기존 장문 문서는 이 문서에서 참조하지 않으면 보조 자료로만 본다.
+
+## Canonical Documents
+
+| 문서 | 용도 | 변경 조건 |
+| --- | --- | --- |
+| [ENGINEERING_HARNESS.md](./ENGINEERING_HARNESS.md) | 아키텍처, 코드 품질, 배포, 데이터 정합성 하네스 | 방향성 변경 또는 반복 위반 발견 |
+| [DOUZONE_UX_GUIDE.md](./DOUZONE_UX_GUIDE.md) | 더존식 어드민 UX 원칙, 템플릿, 컴포넌트, 서빙 규칙 | 화면 패턴 변경 또는 신규 템플릿 추가 |
+| [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md) | 공통코드, 저장값, 컬럼 길이, 하드코딩 금지 규칙 | 공통코드 또는 콤보 추가/수정 |
+| [COMBO_POLICY.md](./COMBO_POLICY.md) | 콤보/선택/검색 입력 정책과 저장값 규칙 | 상태/유형/선택 입력 정책 변경 |
+| [ADMIN_PATTERN_CRITIQUE_WBS.md](./ADMIN_PATTERN_CRITIQUE_WBS.md) | 어드민 Blog/문의 등록 패턴 비판, 개선 로드맵, 정량 WBS | WBS 상태 또는 성공 지표 변경 |
+
+## Route And Serving Map
+
+| 영역 | 라우트/파일 | 기준 |
+| --- | --- | --- |
+| Public Home/Blog/Contact | `/taxbaik/`, `/taxbaik/blog`, `/taxbaik/contact` | 서버 사이드 렌더링, SEO 우선, WASM 의존 금지 |
+| Admin Blog | `/taxbaik/admin/blog`, `/taxbaik/admin/blog/create`, `/taxbaik/admin/blog/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, API-first 클라이언트 경유, JS 최소화 |
+| Admin Inquiry | `/taxbaik/admin/inquiries`, `/taxbaik/admin/inquiries/create`, `/taxbaik/admin/inquiries/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, 공개 접수/관리자 등록/상태 변경 분리 |
+| Public API | `/taxbaik/api/*` | JWT 인증, ProblemDetails 오류, DTO 입출력 |
+| CI/CD | `.gitea/workflows/deploy.yml`, `.gitea/workflows/browser-e2e.yml` | 수동 배포 금지, 배포본 E2E 통과 후 완료 |
+
+## Document Rules
+
+- 문서는 짧게 유지한다. 새 문서를 만들기 전에 이 인덱스에 추가할 가치가 있는지 판단한다.
+- 동일한 기준을 여러 문서에 중복 작성하지 않는다.
+- WBS 완료 여부는 체크박스가 아니라 수치와 실행 로그로 판단한다.
+- 코드 변경 시 관련 WBS ID를 커밋/PR 설명 또는 작업 메모에 남긴다.
+- 공통코드 관련 규칙은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)만 1차 기준으로 사용한다.
diff --git a/docs/ops/COMMON_CODE_AUDIT.sql b/docs/ops/COMMON_CODE_AUDIT.sql
new file mode 100644
index 0000000..19bdb39
--- /dev/null
+++ b/docs/ops/COMMON_CODE_AUDIT.sql
@@ -0,0 +1,17 @@
+-- Common code audit checks
+SELECT code_group, code_value
+FROM common_codes
+WHERE code_value LIKE '% %';
+
+SELECT code_group, COUNT(*)
+FROM common_codes
+GROUP BY code_group
+ORDER BY code_group;
+
+SELECT DISTINCT c.service_type
+FROM clients c
+LEFT JOIN common_codes cc
+ ON cc.code_group = 'CLIENT_SERVICE_TYPE'
+ AND cc.code_value = c.service_type
+WHERE c.service_type IS NOT NULL
+ AND cc.code_value IS NULL;