chore: remove committed build artifacts and dead files, archive stray root docs

Root had accumulated files that should never have been tracked:
- Committed build output: TaxBaik.Web.*.json (runtimeconfig/deps), and a
  225-file root wwwroot/ that duplicated (and was staler than)
  TaxBaik.Web/wwwroot/.
- A stale migrations/ (V001-V003 only) superseded by db/migrations/, which
  is the directory MigrationRunner and CI actually use.
- An orphaned root appsettings.json (dev DB password + JWT secret) that the
  app's content root (TaxBaik.Web/) never actually loads.
- Ad-hoc debug/log scratch files: debug-settings.js, final-test.js,
  test-settings.js, settings-page.png, login-test-output.log,
  server.{err,out}.log.
- docker-compose.yml, Dockerfile.*, web.config, SERVER_SETUP.sh, deploy.sh,
  remote_deploy.sh - none referenced by any .gitea/workflows/*.yml; leftovers
  from a Docker/manual-deploy approach superseded by deploy_gb.sh's
  systemd + Green-Blue proxy model.
- Tmp/ - screenshots and a scratch html/js, exactly the "temp work
  committed to root" problem.

None of this is destroyed - it stays recoverable via git history if ever
needed. Historical root-level docs (BLOG_TEMPLATE.md, DEPLOYMENT_GUIDE.md,
etc.) are moved into docs/archive/ rather than deleted, since docs/INDEX.md
already treats anything outside docs/ as non-canonical reference material.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 10:32:26 +09:00
parent 83c1254a3e
commit c00d002972
263 changed files with 0 additions and 7034 deletions
+777
View File
@@ -0,0 +1,777 @@
# 블로그 포스트 작성 템플릿
## 정확성 원칙 (법적 책임 수반)
블로그는 **사실 기반, 세법 기반, 데이터 기반**이어야 합니다. 추측이나 예상은 법적 문제를 일으킬 수 있습니다.
### 절대 금지 표현
- "아마도", "할 것 같다", "추측된다" (추측)
- "대략", "정도일 거다", "보통" (예상)
- "좋을 것 같다", "나쁠 것 같다" (의견)
- 증거 없는 "모두", "항상", "누구나" (일반화)
- 출처 없는 통계 ("80% 고객이", "평균 X만 원")
### 필수 요소
**1. 세법 기반**:
- 모든 주장에 세법/시행령/고시 인용
- 조항 명시: "소득세법 제XX조에 따르면"
- 최신 기준 명시: "2025년 기준"
- 변경사항 반영: "전년도와 다르게..."
**2. 사실 기반**:
- 실제 일어난 고객 사례만 사용
- 가정일 경우 명시: "예를 들어, 만약 이렇다면"
- 가상 사례는 "예시 사례"라고 명확히
- 개인정보는 익명화 (이름, 나이는 일반적인 표현)
**3. 데이터 기반**:
- 객관적 수치만 사용 (국세청 통계, 협회 자료)
- 출처 명시: "2025년 세무청 통계에 따르면"
- 구체적 금액: "약 50만 원" (범위 표현)
- 비교 데이터: "작년 대비 X% 증가"
**4. 사례 제시 시 확인 사항**:
```
✅ 실제 고객인가? (공개 가능한 정보만)
✅ 세법을 정확하게 적용했는가?
✅ 금액 계산이 정확한가?
✅ 이 사례가 대표적인가? (극단적 사례면 명시)
✅ 다른 고객에게도 적용 가능한가?
```
---
## 카테고리 필수 규칙
**모든 블로그 포스트는 반드시 하나의 카테고리에 할당되어야 합니다. (NOT NULL)**
### 카테고리별 포스트 배치
| 카테고리 | 최소 포스트 | 주제 범위 |
|---------|-----------|---------|
| 사업자 세무 | 3개 | 기장, 세무신고, 부가세, 종합소득세 |
| 부동산 세금 | 3개 | 월세, 양도세, 상속세(부동산) |
| 종합소득세 | 3개 | 프리랜서, 부업, 경비 처리 |
| 부가가치세 | 3개 | 신고, 기한, 간이과세 vs 일반과세 |
| 가족자산·증여 | 3개 | 자녀 증여, 상속, 자산 이전 |
### 카테고리 할당 규칙
1. **명확한 주제 분류**: 포스트 내용이 카테고리 범위에 명확하게 해당
2. **중복 금지**: 한 포스트는 정확히 하나의 카테고리에만 할당
3. **균형 배치**: 각 카테고리당 최소 3개씩 (고객 검색 UX)
4. **검색 최적화**: 고객이 카테고리로 찾을 때 관련 포스트 3개 이상 노출
### 카테고리 미할당 시 (오류)
- ❌ category_id = NULL (데이터베이스 제약 위반)
- ❌ SQL 실행 실패 (NOT NULL 제약)
- ❌ 블로그 페이지 노출 불가
**이 규칙은 모든 포스트 생성/수정 시 필수 준수사항입니다.**
---
## 핵심 철학: 고객이 느끼는 여정
### 1️⃣ 기초: "이 정도는 할 수 있어요"
- 고객이 배울 수 있는 기본 개념
- 실제 사례로 구체화
- 단계별 설명
### 2️⃣ 현실: "하지만 복잡하네요"
- 겹겹이 쌓인 세부사항들
- 매년 바뀌는 세법
- "이거 일일이 다 챙기기 어렵다"는 느낌
### 3️⃣ 해결: "세무사와 함께면 괜찮아요"
- 디테일 자동 관리
- 세법 변화 자동 반영
- 고객은 사업에만 집중
---
**고객이 글을 읽은 후 느끼는 것**:
1️⃣ 읽고 나서: "아, 이 정도는 내가 할 수 있겠네"
2️⃣ 생각해보니: "근데 이 모든 걸 매년 챙기기는... 힘들겠는데?"
3️⃣ 결론: "그럼 전문가 도움을 받는 게 낫겠다"
→ 자연스럽게 세무사의 필요성을 깨달음 (강요 아님)
---
## 템플릿 (복사해서 사용)
### Step 1: 도입부 (공감)
```markdown
# [제목]
"[구체적 상황]?"
"많은 [직업]들이 이 상황을 겪습니다."
→ 독자가 자신의 상황을 발견하도록
```
**예시**:
```markdown
# 동네 카페 월세 낼 때 세금이 안 나와요 - 정말 그럴까?
"사업을 시작했는데 세금을 낸 적이 없어요"
"많은 소규모 사업자들이 이렇게 생각합니다."
```
---
### Step 2: 실제 사례 (구체적 페르소나)
**필수 정보**:
- 이름, 나이, 직업, 사업 경력
- 월/연간 매출 (현실적 수치)
- 실제 겪은 문제/성공 사례
```markdown
### 상황: [지역] [카테고리]를 운영하는 [이름]님 ([나이]세, 사업 [년]차)
**기본 정보**:
- 위치: [구체적 위치]
- 월 매출: [금액]
- 월 경비: [주요 항목들]
### 원래는 이렇게 했어요 (실패 사례)
→ [실제 실수 1]
→ [실제 실수 2]
**결과**: 세금을 [X만 원] 더 내게 됨 (또는 세무조사 대상)
### 바뀐 후 (성공 사례)
→ [해결책 1]
→ [해결책 2]
**결과**: 세금을 [X만 원] 절약함 (또는 안정적인 운영)
```
**예시**:
```markdown
### 상황: 강남 역삼동에서 카페를 운영하는 김민수님 (34세, 사업 3년차)
**기본 정보**:
- 위치: 강남역 3번 출구 근처
- 월 매출: 약 600만 원 (평일 200만, 주말 400만)
- 월 경비: 월세 150만, 재료비 180만, 직원급여 100만 원
### 원래는 이렇게 했어요
→ "세금은 큰 회사나 내는 거라고 생각했어요"
→ 영수증도 대충 정리하고
**결과**: 세무청에서 3년치를 추징받고 가산세까지...손해 70만 원
### 바뀐 후
→ 매달 영수증을 정리해서
→ 세무사와 년 1회 기장 상담
**결과**: 세금도 명확하고, 추징도 없음. 심플하고 안전
```
---
### Step 3: 계산 & 설명
**구조**:
1. **기본 정보 확인** (위에서 제시한 사례 요약)
2. **단계별 계산** (Step 1, Step 2, ... 명확히)
3. **표로 시각화**
```markdown
## 계산 방법
### Step 1️⃣: 매출 정리
월 600만 원 × 12개월 = 연 7,200만 원
### Step 2️⃣: 경비 계산
월 경비 구성:
- 월세: 150만 원 (연 1,800만 원)
- 재료비: 180만 원 (연 2,160만 원)
- 직원급여: 100만 원 (연 1,200만 원)
- 기타: 20만 원 (연 240만 원)
- **월 합계: 450만 원**
- **연 합계: 5,400만 원**
### Step 3️⃣: 순이익
7,200만 - 5,400만 = **1,800만 원**
### Step 4️⃣: 세금
1,800만 원 × 약 6% = **약 108만 원/년**
```
---
### 🎭 Step 3.5: 악마는 디테일이다 (선택사항이지만 강력함)
**구조**: "간단해 보이지만, 실제로는..."
```markdown
## 겉으로는 간단해 보여요... 하지만
### 📄 "영수증을 정리하세요"라고 했는데
**겉으로는**:
→ 영수증을 모으기만 하면 돼
**현실의 디테일**:
→ 이 영수증은 인정되고, 이건 안 됨 (세법)
→ 이건 개인비? 사업비? (판단)
→ 카드값이랑 현금값이랑 다르면? (대사)
→ 3년 지났는데 영수증을 못 찾으면? (소송)
→ 세무청이 불인정하면? (항의 절차)
**세무사가 처리하는 것**:
✅ 어떤 영수증이 인정될지 사전에 판단
✅ 개인비와 사업비의 경계 명확히
✅ 세법 변경사항 적용
✅ 세무청 부인시 대응 준비
---
### 📊 "매출과 경비를 기록하세요"라고 했는데
**겉으로는**:
→ 엑셀에 숫자만 입력하면 돼
**현실의 디테일**:
→ 카드 명세서와 입금액이 안 맞음 (환불? 수수료?)
→ 한 달간 매출을 빼먹음 (추가 계산)
→ 같은 카테고리인데 세법상 다르게 분류돼야 함 (부가세/소득세 다름)
→ 작년에 잘못 입력한 게 발견됨 (수정신고)
→ 월별로 편차가 커서 세무청이 의심함 (설명 준비)
**세무사가 처리하는 것**:
✅ 카드명세서 vs 입금액 정산
✅ 누락된 부분 찾아서 추가
✅ 세법상 올바른 분류
✅ 이전년도 오류 수정신고
✅ 세무청 질의에 대한 근거 제시
---
### ✅ "정확하게 기장하면 세금이 확정돼요"라고 했는데
**겉으로는**:
→ 기장만 잘하면 세금 끝
**현실의 디테일**:
→ 같은 사업도 절세 방법이 다양함 (어떤 게 맞나?)
→ 올해는 이렇게, 내년은 저렇게? (일관성)
→ 부가세와 소득세 둘 다 고려해야 함 (연계 계산)
→ 세무조사가 오면 3년치를 모두 봄 (소급 처리)
→ 이의신청/항소하려면? (법적 절차)
**세무사가 처리하는 것**:
✅ 최적의 절세 전략 제시
✅ 연도별 일관된 기장 방식 유지
✅ 부가세/소득세 동시 최적화
✅ 세무조사 대비 사전 정리
✅ 이의신청/항소 등 법적 대응
```
**💡 핵심**:
- 기초는 누구나 배울 수 있어요
- **하지만 디테일을 모두 처리하려면?**
- **그 디테일들이 바로 세무사가 하는 일**
- **디테일 하나 놓쳤다가 가산세 50만 원... 이래서 세무사 비용이 아깝지 않음**
---
### 🔄 Step 3.6: 세법은 계속 바뀐다 (매년 업데이트)
**구조**: "올해의 세법 변화"를 포스트 작성 시점에 맞춰 갱신
```markdown
## 그런데 세법은 해마다 바뀝니다
### 📋 [연도] 변경사항들 (꼭 알아야 할 것들)
**✅ 2025년 부가세 변화**:
- 신고 기한이 [날짜]로 변경됨
- 영세사업자 기준이 [금액]로 상향조정됨
- 새로운 공제 항목이 추가됨: [항목들]
**✅ 2025년 소득세 변화**:
- 기본공제가 [금액]에서 [금액]로 증가
- 자녀 공제 조건이 변경됨
- 월급 원천징수 기준이 조정됨
**✅ 2025년 새로운 제도**:
- 소상공인 세금 감면 확대
- 청년사업자 지원 강화
- 부가가치세 간편신청 범위 확대
---
**혼자서 할 때의 문제**:
❌ "작년 기준으로 기장했는데 올해 기준이 바뀐 거야?"
❌ "이 공제가 되는 건지 안 되는 건지 모르겠어"
❌ "새로운 제도가 나왔다는 것도 몰랐어"
❌ "처음 다시 계산해야 하나?"
**세무사가 처리하는 것**:
✅ 매년 변경사항 자동 추적
✅ 당신의 상황에 맞는 새로운 공제 적용
✅ 이전년도 재계산 필요시 수정신고
✅ 연중 세법 개정 소식 안내
✅ 새로운 지원 정책 놓치지 않게 관리
---
## 결과 비교: 혼자 할 때 vs 세무사와 함께
**세법 변화 추적**
- 혼자: "어? 규칙이 바뀌었네?"
- 세무사: 자동으로 적용됨
**새로운 공제**
- 혼자: 놓치기 쉬움
- 세무사: 모두 적용됨
**매년 재계산**
- 혼자: 직접 해야 함
- 세무사: 자동 갱신
**마음 편함**
- 혼자: 불안감 ("맞나?")
- 세무사: 확신 ("전문가가 관리")
**투자 시간**
- 혼자: 당신의 시간
- 세무사: 포함 (전문가 비용)
---
## 요약: 왜 세무사가 필요한가
**기초는 배울 수 있지만**:
- 세법은 매년 바뀌고
- 당신은 본업이 있어서 추적이 어렵고
- 실수 하나가 가산세 50만 원...
**그래서 세무사가 있으면**:
- 변화를 자동으로 적용해주고
- 새 제도도 놓치지 않아주고
- 당신은 사업에만 집중
**결국 시간, 돈, 스트레스 모두 절약**
---
### 💡 Step 4: 실무 팁 (3~5개)
**구조**: ✅ 이렇게 하세요 / ❌ 이렇게 하면 안 돼요
```markdown
## 이렇게 하면 세금이 명확해요
### ✅ 해야 할 것
1. **영수증 정리** - 매달 봉투에 모아두기
2. **기본 기록** - 엑셀에 간단히 기입
3. **연 1회 점검** - 세무사와 기본 상담
4. **투명성** - 세무청 신고는 정확하게
### ❌ 하면 안 되는 것
1. **영수증 버리기** - 나중에 증거 없음
2. **개인비와 섞기** - 기장 혼란
3. **신고 늦추기** - 가산세 발생
4. **과하게 깎기** - 세무조사 리스크
```
---
### 📝 Step 5: 결론
고객이 읽은 후 자연스럽게 결론을 내리도록:
**구조**:
1. 기초는 할 수 있다 (긍정)
2. 근데 복잡하네요 (현실 직시)
3. 그래서 세무사가 필요하구나 (자연스러운 깨달음)
**고객이 느끼는 여정**:
- 처음: "아, 이 정도는 내가 할 수 있겠네"
- 중간: "근데 이 모든 걸 매년 챙기기는..."
- 결론: "전문가 도움이 낫겠다"
```markdown
## 기초는 누구나 할 수 있어요
**이 정도면 자신이 충분히 가능합니다**:
- 소규모 사업 (월 500만~1,000만 원)
- 단순 경비 (재료, 임차료 등)
- 월 1회 정도 기본 정리
→ 영수증 정리 + 기본 엑셀 기입면 충분
---
## 하지만 이렇게 복잡하면 전문가 도움이 효율적입니다
**세무사 상담을 권하는 경우**:
- 📊 월 매출이 2,000만 원을 넘어갈 때
- 💼 여러 사업을 동시에 운영할 때
- 🏠 부동산 등 추가 수입이 있을 때
- 📈 직원을 여러 명 두고 있을 때
- 🌍 해외 거래나 수입이 있을 때
### 실제 효과: 숫자로 본 세무사의 가치
**절세액**
- 혼자: X만 원
- 세무사: X + 200만 원
- 차이: +200만 원 절약
**세무조사 스트레스**
- 혼자: 매년 불안
- 세무사: 안정적 대응
- 차이: 심리적 안정
**시간 투자**
- 혼자: 월 10시간
- 세무사: 월 1시간
- 차이: 월 9시간 자유
**세무사 비용**
- 혼자: 0원
- 세무사: 약 100만 원/년
- 차이: -100만 원
**실제 이익**
- 혼자: 순이익
- 세무사: 순이익 + 100만 원
- 차이: +100만 원 순이익
**돈을 쓰는 이유**:
- 세금 절약: 절세 200만 원 - 비용 100만 원 = 순 100만 원 이득
- 시간 절약: 월 9시간(연 108시간) = 사업에 집중
- 스트레스 감소: 세무조사 불안 제거
- 리스크 관리: 실수로 인한 가산세 방지
---
## 요약
**기본 개념을 아는 것만으로도**:
- 실수를 줄이고
- 세금을 절약하고
- 세무사와의 상담이 훨씬 효율적
당신의 상황이 어느 정도인지 판단하고,
필요할 때 전문가와 함께 하세요.
```
---
## ✅ 작성 체크리스트
### 내용
- [ ] **실제 사례**: 동네 카페/편의점/학원 같은 주변 상황
- [ ] **구체적 페르소나**: 이름, 나이, 직업, 사업 경력
- [ ] **실제 금액**: 매출, 경비, 세금 (현실적 수치)
- [ ] **Before/After**: 실패 사례 → 성공 사례
- [ ] **절세 효과**: "X만 원 절약" 또는 "손해를 막음"
- [ ] **계산**: Step별로 명확, 표 포함
- [ ] **악마는 디테일**: "겉으로는 간단해 보이지만 실제로는..." (세무사가 처리하는 디테일들)
- [ ] **세법 변화**: 해당 연도의 세법 변경사항과 그 영향 설명
### 톤
- [ ] **교육적**: 개념을 이해하도록
- [ ] **격려적**: 경고/협박 없음
- [ ] **현실적**: 복잡할 수 있다는 인정
- [ ] **세무사 자연스러운 유도**: "필요할 때 도움되는 선택"
- [ ] **임파워먼트**: "기초는 누구나 할 수 있어요"
### 표현
- [ ] **중학교 수준**: 어려운 용어는 () 설명
- [ ] **이모지**: 🏠💰✅❌📊 등으로 시각화
- [ ] **짧은 문장**: 한 문장에 한 개념
- [ ] **표와 리스트**: 수치는 표로, 항목은 리스트로
---
## 🚫 피해야 할 표현 (한국세무사협회 광고 규칙 준수)
### ❌ **절대 금지 표현** (법적 위반 위험)
**1. 과도한 절세 약속 & 절대 표현**:
- ❌ "50만 원 절약 가능"
- ❌ "최대한 경비를 깎아줍니다"
- ❌ "세금을 반으로 줄여드립니다"
- ❌ "세금을 덜 냅니다" (보장으로 해석)
- ❌ "가장 많이 절세해드립니다"
- ✅ "이 사례에서는 약 50만 원 절약되었습니다" (과거 사례만)
- ✅ "정확한 경비 처리로 세법에 따른 정당한 공제를 받을 수 있습니다" (법적 근거)
- ✅ "경비를 빠짐없이 처리합니다" (객관적 프로세스)
**2. 보장 표현 (불가능한 결과 약속)**:
- ❌ "반드시 세금을 줄입니다"
- ❌ "세무조사 안 받게 해드립니다"
- ❌ "100% 절세를 보장합니다"
- ❌ "세금을 보장합니다"
- ✅ "정확한 신고로 세무조사 리스크를 최소화합니다"
- ✅ "세법에 따른 정당한 공제를 받을 수 있습니다"
**3. 무료 & 가격 표현**:
- ❌ "무료로 세금 절약해드립니다"
- ❌ "최저가 신고료"
- ❌ "가장 저렴한 가격"
- ✅ "합리적인 비용으로 전문 서비스를 제공합니다"
**4. 절대/최상급 표현**:
- ❌ "반드시", "무조건", "반듯이", "항상", "절대"
- ❌ "최고", "최우수", "1등", "유일"
- ❌ "모든", "완벽하게"
- ✅ "일반적으로", "대부분의 경우", "보통"
**5. 과도한 단순화 표현**:
- ❌ "매우 편합니다", "너무 쉽습니다"
- ❌ "아무도 실수할 수 없습니다"
- ❌ "5분이면 끝납니다"
- ✅ "기초 개념을 배울 수 있습니다"
- ✅ "복잡한 부분은 전문가가 관리합니다"
**6. 객관적 증거 없는 수치**:
- ❌ "평균 170만 원 절약" (근거 없으면)
- ❌ "고객의 80%가 만족" (통계 없으면)
- ❌ "보통 2배의 환급" (데이터 없으면)
- ✅ "이 사례에서는 약 170만 원 절약되었습니다"
- ✅ "많은 고객들이 정확한 기장의 필요성을 느낍니다"
---
### ✅ **안전한 표현 (권장)**
| 대신 이렇게 | 이유 |
|----------|------|
| "정확한 기장으로 세법에 따른 공제를 받을 수 있습니다" | 법적 근거 (보장 아님) |
| "경비를 빠짐없이 처리합니다" | 객관적 프로세스 |
| "이 사례에서는 약 50만 원 절약되었습니다" | 과거 사례 (보장 아님) |
| "경비를 빠짐없이 처리합니다" | 객관적 프로세스 |
| "세무조사 대비 근거를 정리합니다" | 예방적 표현 |
| "당신의 상황에 맞는 최선의 방법을 제시합니다" | 개별 맞춤형 |
| "세법이 자주 바뀌므로 전문가 도움이 효율적입니다" | 필요성 설명 |
| "이 정도는 자신이 충분히 가능합니다" | 존중과 임파워먼트 |
| "복잡한 경우는 전문가와 상담하세요" | 선택지 제시 |
| "정확하게 하면 나중에 편합니다" | 미래 가치 (현재 보장 아님) |
---
### 📋 블로그 작성 시 광고 규칙 체크리스트
- [ ] **절세 약속 제거**: "최대한", "반드시", "보장", "무조건" 단어 없음
- [ ] **보장 표현 제거**: "세무조사 안 받게", "100% 절세", "확실" 제거
- [ ] **무료/가격 표현 제거**: "무료", "최저가", "가장 저렴" 제거
- [ ] **절대 표현 제거**: "항상", "절대", "모두", "완벽" 제거
- [ ] **최상급 제거**: "최고", "최우수", "1등" (객관적 증거 있으면 가능)
- [ ] **과도한 단순화 제거**: "매우 쉽습니다", "아무도 실수할 수 없음" 제거
- [ ] **수치는 사례로**: "절약 가능" → "이 사례에서는 약 X만 원 절약"
- [ ] **객관성 유지**: 구체적 사례 + 과거형 표현 사용
- [ ] **필요성 설명**: "왜 필요한가" → 이해와 선택 유도
- [ ] **세무사협회 규정 준수**: 법적 문제 없음
---
## 시즌별 주제 예시
| 월 | 추천 주제 | 톤 |
|----|---------|-----|
| 1월 | 부가세 2기 신고 기한 | "이 정도면 자신이 가능" |
| 5월 | 종소세 신고 방법 | "핵심 개념 + 전문가 도움 타이밍" |
| 7월 | 부가세 1기 신고 | "기초 정리 방법" |
| 11월 | 다음해 준비 | "계획하면 편해요" |
---
## ⚠️ 실수 방지 체크리스트 (과거 오류 기록)
**이전에 반복된 실수들을 기록하여, 같은 실수를 하지 않도록 합니다.**
### 1️⃣ 카테고리 할당 실수 ❌
**과거 오류**: 포스트를 만들 때 category_id를 NULL로 두었음
**문제점**:
- DB NOT NULL 제약 위반
- 블로그 페이지에 노출 안 됨
- 고객이 카테고리로 검색 불가
**예방책**:
-**SQL INSERT 시 반드시 category_id 명시**
-**포스트 작성 전에 카테고리 결정**
-**DB 적용 후 category_id NOT NULL 확인**
-**각 카테고리별 최소 3개 이상 포스트 유지**
**SQL 예시** (권장):
```sql
INSERT INTO blog_posts (title, slug, content, category_id, is_published, ...)
VALUES ('제목', 'slug', $$$$, 1, true, ...);
-- category_id 절대 생략 금지!
```
---
### 2️⃣ 내용 길이 부족 ❌
**과거 오류**: 에이전트가 지침(1,500~2,500자)을 무시하고 간단한 버전(500자)으로 생성
**문제점**:
- 고객 설득력 부족
- 계산 예시 없음
- 3단계 구조 불완전
- 세법 인용 부족
**예방책**:
-**각 포스트 최소 1,500자 이상 (추천 2,000~2,500자)**
-**포스트 작성 후 글자 수 확인: `LENGTH(content) >= 1500`**
-**항상 실제 사례 포함** (이름, 나이, 직업, 구체적 상황)
-**항상 계산 과정 포함** (절세액 수치화)
-**3단계 구조 필수** (1️⃣ 기초 → 2️⃣ 현실 → 3️⃣ 해결책)
**확인 쿼리**:
```sql
SELECT id, title, LENGTH(content) as length FROM blog_posts
WHERE LENGTH(content) < 1500; -- 부족한 포스트 검출
```
---
### 3️⃣ 테이블 사용 금지 ❌
**과거 오류**: 마크다운 테이블(`| |---|---|`) 사용
**문제점**:
- 지침 위반 (리스트만 사용)
- 모바일에서 가독성 저하
- 유지보수 어려움
**예방책**:
-**테이블 금지, 리스트만 사용** (- 또는 숫자 목록)
-**작성 후 `| |` 패턴 검색으로 테이블 확인**
-**수치/계산은 리스트 형식**:
**❌ 금지 (테이블)**:
```markdown
| 항목 | 월 | 연간 |
|------|-----|------|
| 월세 | 150만 | 1,800만 |
```
**✅ 권장 (리스트)**:
```markdown
월 경비 구성:
- 월세: 150만 원 (연 1,800만 원)
- 재료비: 180만 원 (연 2,160만 원)
- 직원급여: 100만 원 (연 1,200만 원)
```
---
### 4️⃣ 계산 예시 누락 ❌
**과거 오류**: 포스트에 개념만 있고 실제 계산 예시 부족
**문제점**:
- 고객이 "내 상황에 얼마나 해당하나" 판단 어려움
- 추상적 설명으로 설득력 감소
- 세무사 필요성 전달 미흡
**예방책**:
-**모든 포스트에 구체적 계산 예시 필수**
-**절세액을 수치로 제시** ("약 50만 원 절약")
-**단계별 계산 과정 포함** (Step 1️⃣, 2️⃣, 3️⃣, 4️⃣)
-**실제 사례로 숫자 구체화**:
**예시**:
```markdown
### Step 1️⃣: 매출 정리
월 600만 원 × 12개월 = 연 7,200만 원
### Step 2️⃣: 경비 계산
- 월세: 150만 원 → 연 1,800만 원
- 재료비: 180만 원 → 연 2,160만 원
합계: 5,400만 원
### Step 3️⃣: 순이익
7,200만 - 5,400만 = 1,800만 원
### Step 4️⃣: 세금
1,800만 원 × 약 6% = **약 108만 원/년**
```
---
### 5️⃣ 카테고리 주제 불일치 ❌
**과거 오류**: 포스트 주제와 카테고리가 맞지 않음
**문제점**:
- 고객이 원하는 정보 검색 불가
- 카테고리 신뢰도 저하
- UX 혼란
**예방책**:
-**포스트 작성 전 카테고리 명확히 결정**
-**포스트 주제와 카테고리 일관성 검증**:
| 포스트 | 카테고리 | 확인 |
|--------|---------|------|
| 프리랜서 경비 | 종합소득세 (3) | ✅ 맞음 |
| 월세 신고 | 부동산 세금 (2) | ✅ 맞음 |
| 자녀 증여세 | 가족자산·증여 (5) | ✅ 맞음 |
| 사업자 기장 | 사업자 세무 (1) | ✅ 맞음 |
| 부가세 신고 | 부가가치세 (4) | ✅ 맞음 |
---
### 6️⃣ 정확한 세법 인용 누락 ❌
**과거 오류**: 일부 포스트에서 법조 명시 부족
**문제점**:
- 정확성 원칙 위반
- 법적 책임 불명확
- 고객 신뢰도 저하
**예방책**:
-**모든 주요 내용에 세법 조항 인용 필수**
-**형식**: "소득세법 제XX조에 따르면"
-**연도 기준 명시**: "2025년 기준"
-**포스트 끝에 "법적 근거" 섹션 필수**:
```markdown
**법적 근거**:
- 소득세법 제29조 (수입금액의 계산)
- 국세기본법 제47조 (가산세)
- 소득세법 제160조 (증빙 보관)
```
---
## ✅ 포스트 최종 체크리스트
모든 포스트를 DB에 등록하기 전에 다음을 확인하세요:
- [ ] **카테고리 할당**: `category_id NOT NULL` (필수)
- [ ] **내용 길이**: `LENGTH(content) >= 1500` (최소 1,500자)
- [ ] **테이블 확인**: `| |` 패턴 없음 (리스트만)
- [ ] **계산 예시**: Step 1️⃣~4️⃣ 포함 (절세액 수치)
- [ ] **세법 인용**: 모든 주요 내용에 법조 명시
- [ ] **카테고리 일치**: 포스트 주제 ↔ 카테고리 일관성
- [ ] **3단계 구조**: 1️⃣ 기초 → 2️⃣ 현실 → 3️⃣ 해결책
- [ ] **광고 규칙**: 금지 표현(보장, 최저가, 무료) 없음
- [ ] **사례 포함**: 실제 상황 + 이름/나이/직업 구체화
- [ ] **정확성**: 추측/예상/의견 표현 없음
**체크 쿼리**:
```sql
-- DB 적용 후 확인
SELECT id, title, LENGTH(content), category_id
FROM blog_posts
WHERE LENGTH(content) < 1500 OR category_id IS NULL
ORDER BY id;
-- 결과 없음이 정상!
```
+17
View File
@@ -0,0 +1,17 @@
# CI/CD 자동 배포 테스트
**테스트 시간**: 2026-06-26 15:40 KST
**브랜치**: master
**목표**: Gitea Actions 자동 배포 검증
## 배포 파이프라인
1. ✅ 코드 푸시
2. ⏳ Gitea Actions 트리거
3. ⏳ 빌드 실행
4. ⏳ 발행 실행
5. ⏳ 서버 배포
6. ⏳ 서비스 재시작
## 모니터링
- Gitea Actions 탭 확인
- 약 3분 후 배포 완료
+620
View File
@@ -0,0 +1,620 @@
# 클라우드 서버 설정 가이드 (hz-prod-01)
> 시놀로지(Synology DSM)에서 클라우드 VPS(`178.104.200.7`)로 이전.
> 이 문서는 서버에서 실제 수집된 데이터 기반이며, 운영 하네스로 사용한다.
---
## 참조 인덱스
| # | 섹션 | 핵심 내용 |
|---|---|---|
| 1 | [서버 기본 정보](#1-서버-기본-정보) | 호스트명, IP, OS, CPU/RAM/디스크, 타임존 |
| 2 | [접속 정보](#2-접속-정보) | SSH 접속, 사용자, 인증 방식 |
| 3 | [소프트웨어 스택](#3-소프트웨어-스택) | Python, .NET, PG, Nginx, Docker Compose, fail2ban |
| 3.1 | [런타임](#31-런타임) | 버전/경로 일람 |
| 3.2 | [Python 가상 환경](#32-python-가상-환경) | `~/.venv`, `python3` 사용 규칙 |
| 3.3 | [주요 Python 패키지](#33-주요-python-패키지-시스템) | 시스템/venv 패키지 구분 |
| 4 | [서비스 아키텍처](#4-서비스-아키텍처) | 포트 맵, Nginx 리버스 프록시 |
| 4.1 | [포트 맵](#41-포트-맵) | 22, 80, 2222, 3000, 5000, 5432 |
| 4.2 | [Nginx 리버스 프록시](#42-nginx-리버스-프록시) | 도메인 기반 가상 호스트 분기 (홈페이지, Gitea, Quant) |
| 5 | [Gitea](#5-gitea) | Docker Compose 설정, 시크릿, 데이터 경로 |
| 5.1 | [Docker Compose](#51-docker-compose) | `gitea:1.26.4`, PG 연동 |
| 5.2 | [시크릿 관리](#52-시크릿-관리) | `/opt/stacks/gitea/.env` |
| 5.3 | [데이터](#53-데이터) | Gitea 볼륨, `giteadb` |
| 6 | [Gitea Act Runner (CI)](#6-gitea-act-runner-ci) | 6× 러너, 네트워크, 구성 디렉토리 |
| 6.1 | [컨테이너 현황](#61-컨테이너-현황) | 러너 6개 실행 상태 |
| 6.2 | [러너 설정](#62-러너-설정) | `hz-prod-runner`, `gitea_default` 네트워크 |
| 6.3 | [러너 구성 디렉토리](#63-러너-구성-디렉토리) | `~/gitea-runner[-N]/` |
| 7 | [QuantEngine Blazor Admin](#7-quantengine-blazor-admin) | systemd, symlink 배포, DLL 구성 |
| 7.1 | [systemd 서비스](#71-systemd-서비스) | `quantengine.service` 전문 |
| 7.2 | [배포 구조](#72-배포-구조) | 타임스탬프 디렉토리 + symlink 교체 |
| 7.3 | [주요 DLL](#73-주요-dll) | Web, Core, Infrastructure, MudBlazor, Dapper |
| 8 | [PostgreSQL 18](#8-postgresql-18) | v18.4, `localhost` 바인드, Docker 연동 |
| 9 | [보안](#9-보안) | SSH hardening, UFW, fail2ban, 네트워크 격리 |
| 9.1 | [SSH 보안 설정](#91-ssh-보안-설정) | 공개키 전용, root 차단 |
| 9.2 | [UFW 방화벽](#92-ufw-방화벽) | `ENABLED=yes`, 포트 개방/차단 |
| 9.3 | [fail2ban](#93-fail2ban) | SSH 브루트포스 방어 |
| 9.4 | [Docker 네트워크 격리](#94-docker-네트워크-격리) | 로컬바인드 정책 |
| 10 | [디렉토리 맵](#10-디렉토리-맵) | `/home/kjh2064/`, `/opt/stacks/`, `/opt/backups/` |
| 11 | [시놀로지 → 클라우드 마이그레이션 매핑](#11-시놀로지--클라우드-마이그레이션-매핑) | 항목별 구↔신 비교표 |
| 12 | [운영 명령 치트시트](#12-운영-명령-치트시트) | 서비스 관리, 배포, 러너 등록, SSH |
| 13 | [검증 하네스](#13-검증-하네스) | 헬스체크, 엔드포인트, 마이그레이션 체크리스트 |
### 관련 문서 상호 참조
| 문서 | 역할 |
|---|---|
| [`AGENTS.md`](../AGENTS.md) | 운영 헌법, Directory Routing 인덱스 |
| [`GITEA_SECRETS_SETUP.md`](GITEA_SECRETS_SETUP.md) | Gitea 시크릿 설정/검증 가이드 |
| [`ROADMAP_WBS.md`](ROADMAP_WBS.md) | `.gs → Python``xlsx → sqlite` WBS |
| [`docs/GITEA_TOKEN_HOME_RUNBOOK.md`](GITEA_TOKEN_HOME_RUNBOOK.md) | Gitea 토큰 관리 런북 |
| [`spec/00_execution_contract.yaml`](../spec/00_execution_contract.yaml) | 실행 계약 원본 권위 |
| [`governance/agents_index.yaml`](../governance/agents_index.yaml) | 거버넌스 규칙 인덱스 |
---
## 1. 서버 기본 정보
| 항목 | 값 |
|---|---|
| **호스트명** | `hz-prod-01` |
| **IP** | `178.104.200.7` |
| **OS** | Ubuntu 26.04 LTS (Resolute Raccoon) |
| **커널** | `7.0.0-22-generic` (x86_64, PREEMPT_DYNAMIC) |
| **CPU** | AMD EPYC-Rome, 2 vCPU |
| **메모리** | 3.7 GiB (사용 ~958 MiB, 가용 ~2.8 GiB) |
| **스왑** | 2.0 GiB |
| **디스크** | `/dev/sda1` 38 GB (사용 8.5 GB / 28 GB 가용, 24%) |
| **타임존** | `Asia/Seoul` (KST, +0900), NTP 동기화 활성 |
## 2. 접속 정보
| 항목 | 값 |
|---|---|
| **SSH 접속** | `ssh kjh2064@178.104.200.7` |
| **SSH 포트** | 22 (기본) |
| **사용자** | `kjh2064` (uid=1000) |
| **그룹** | `kjh2064`, `sudo`, `users`, `docker` |
| **인증 방식** | 공개키 전용 (`PasswordAuthentication no`) |
| **Root 로그인** | 비활성 (`PermitRootLogin no`) |
| **Max Auth Tries** | 3 |
| **Keep-Alive** | `ClientAliveInterval 300`, `ClientAliveCountMax 2` |
## 3. 소프트웨어 스택
### 3.1. 런타임
| 소프트웨어 | 버전 | 경로 |
|---|---|---|
| **Python** | 3.14.4 | `/usr/bin/python3` |
| **.NET SDK** | 10.0.109 | `/usr/lib/dotnet/sdk` |
| **.NET Runtime** | ASP.NET Core 10.0.9 + NETCore 10.0.9 | `/usr/lib/dotnet/shared/` |
| **PostgreSQL** | 18.4 | `postgresql@18-main.service` |
| **Nginx** | 시스템 패키지 | `nginx.service` |
| **Docker Compose** | v5.2.0 | Docker 플러그인 |
| **fail2ban** | 1.1.0 | `fail2ban.service` |
### 3.2. Python 가상 환경
```
경로: ~/.venv
Python: 3.14.4
```
> **주의**: 이 서버에서는 `python3`을 사용한다 (시놀로지/Windows와 다름).
> CI 워크플로우와 로컬 서버 모두 `python3`을 사용하므로 통일됨.
### 3.3. 주요 Python 패키지 (시스템)
boto3, cryptography, Jinja2, jsonschema, fail2ban 등 시스템 레벨로 설치됨.
프로젝트 의존성은 `~/.venv`에 별도 관리.
## 4. 서비스 아키텍처
### 4.1. 포트 맵
| 포트 | 서비스 | 바인드 | 비고 |
|---|---|---|---|
| **22** | SSH | `0.0.0.0` | 공개키 전용 |
| **80** | Nginx (리버스 프록시) | `0.0.0.0` | 외부 진입점 |
| **2222** | Gitea SSH | `0.0.0.0` | Git SSH 접속 |
| **3000** | Gitea Web | `127.0.0.1` | Nginx 프록시 경유 |
| **5000** | QuantEngine Blazor | `127.0.0.1` | Nginx `/quant/` 경유 |
| **5432** | PostgreSQL | `127.0.0.1` + `172.17.0.1` | 로컬 + Docker 네트워크 |
### 4.2. Nginx 리버스 프록시
```nginx
# /etc/nginx/sites-available/taxbaik-domains.conf
# 1. TaxBaik 홈페이지 (taxbaik.com, www.taxbaik.com)
server {
server_name taxbaik.com www.taxbaik.com;
client_max_body_size 512M;
# /admin 하위 요청을 /taxbaik/admin 으로 리다이렉트하여 Blazor Base Path 대응
location /admin {
return 301 $scheme://$host/taxbaik$request_uri;
}
# 루트 경로 요청을 /taxbaik 으로 프록싱하여 base href /taxbaik/ 에 대응
location / {
proxy_pass http://127.0.0.1:5001/taxbaik/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# /taxbaik/ 하위로 들어오는 리소스 및 페이지 요청 처리
location /taxbaik {
proxy_pass http://127.0.0.1:5001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/taxbaik.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/taxbaik.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
# 2. Gitea (gitea.taxbaik.com)
server {
server_name gitea.taxbaik.com;
client_max_body_size 512M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/taxbaik.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/taxbaik.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
# 3. QuantEngine (quant.taxbaik.com)
server {
server_name quant.taxbaik.com;
location / {
proxy_pass http://127.0.0.1:5000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/taxbaik.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/taxbaik.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.taxbaik.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = taxbaik.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name taxbaik.com www.taxbaik.com;
return 404; # managed by Certbot
}
server {
if ($host = gitea.taxbaik.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name gitea.taxbaik.com;
return 404; # managed by Certbot
}
server {
if ($host = quant.taxbaik.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name quant.taxbaik.com;
return 404; # managed by Certbot
}
```
**라우팅 요약**:
- `http://taxbaik.com/` 또는 `http://www.taxbaik.com/` → TaxBaik 홈페이지 (내부 proxy: `http://127.0.0.1:5001/taxbaik/`)
- `http://gitea.taxbaik.com/` → Gitea Web UI (내부 proxy: `http://127.0.0.1:3000`)
- `http://quant.taxbaik.com/` → QuantEngine Blazor Admin (내부 proxy: `http://127.0.0.1:5000/`)
- `ssh://gitea.taxbaik.com:2222` → Gitea Git SSH
## 5. Gitea
### 5.1. Docker Compose
```yaml
# /opt/stacks/gitea/docker-compose.yml
services:
gitea:
image: docker.gitea.com/gitea:1.26.4
container_name: gitea
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: host.docker.internal:5432
GITEA__database__NAME: giteadb
GITEA__database__USER: gitea
GITEA__database__PASSWD: "${GITEA_DB_PASSWORD}"
GITEA__server__DOMAIN: "${SERVER_IP}"
GITEA__server__ROOT_URL: "http://${SERVER_IP}/"
GITEA__server__SSH_DOMAIN: "${SERVER_IP}"
GITEA__server__SSH_PORT: "2222"
GITEA__security__INSTALL_LOCK: "true"
GITEA__service__DISABLE_REGISTRATION: "true"
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "2222:22"
```
### 5.2. 시크릿 관리
- `.env` 파일: `/opt/stacks/gitea/.env` (소유자 전용, `600`)
- 포함 변수: `GITEA_DB_PASSWORD`, `SERVER_IP`
### 5.3. 데이터
- Gitea 데이터: `/opt/stacks/gitea/gitea/`
- DB: PostgreSQL `giteadb` (Docker → host.docker.internal:5432 경유)
## 6. Gitea Act Runner (CI)
### 6.1. 컨테이너 현황
| 이름 | 이미지 | 상태 |
|---|---|---|
| `gitea-runner` | `gitea/act_runner:latest` | 실행 중 |
| `gitea-runner-2` | `gitea/act_runner:latest` | 실행 중 |
| `gitea-runner-3` | `gitea/act_runner:latest` | 실행 중 |
| `hopeful_galileo` | `gitea/act_runner:latest` | 실행 중 |
| `jovial_bouman` | `gitea/act_runner:latest` | 실행 중 |
| `upbeat_chatelet` | `gitea/act_runner:latest` | 실행 중 |
> 총 6개 러너가 활성 상태. 네트워크는 `gitea_default` Docker 네트워크 사용.
### 6.2. 러너 설정
```yaml
# ~/gitea-runner/config.yaml
container:
network: "gitea_default"
```
- 러너 이름: `hz-prod-runner`
- 러너 UUID: `d6d9120b-5070-4874-88d7-b86fe817d5a0`
- 러너 이미지: `docker.gitea.com/runner-images:ubuntu-latest` (2.33 GB)
### 6.3. 러너 구성 디렉토리
```
~/gitea-runner/ # 1번 러너
~/gitea-runner-2/ # 2번 러너
~/gitea-runner-3/ # 3번 러너
```
## 7. QuantEngine Blazor Admin
### 7.1. systemd 서비스
```ini
# /etc/systemd/system/quantengine.service
[Unit]
Description=Quant Engine Blazor Admin Web App (.NET 10)
After=network.target
[Service]
WorkingDirectory=/home/kjh2064/quantengine_active
ExecStart=/usr/bin/dotnet /home/kjh2064/quantengine_active/QuantEngine.Web.dll
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=quantengine
User=kjh2064
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
```
### 7.2. 배포 구조
```
~/quantengine_active → ~/deployments/quantengine_20260625_182821 (symlink)
~/deployments/
├── quantengine_20260625_155649/
├── quantengine_20260625_164548/
├── quantengine_20260625_164928/
└── quantengine_20260625_182821/ ← 현재 활성
```
**배포 방식**: 타임스탬프 디렉토리 생성 → symlink 교체 → `systemctl restart quantengine`
### 7.3. 주요 DLL
- `QuantEngine.Web.dll` — 웹 진입점
- `QuantEngine.Core.dll` — 핵심 도메인
- `QuantEngine.Application.dll` — 애플리케이션 서비스
- `QuantEngine.Infrastructure.dll` — 인프라 (DB, 외부 연동)
- `Npgsql.dll` — PostgreSQL 드라이버
- `MudBlazor.dll` — UI 컴포넌트
- `Dapper.dll` — 마이크로 ORM
## 8. PostgreSQL 18
| 항목 | 값 |
|---|---|
| **버전** | 18.4 (Ubuntu 패키지) |
| **서비스** | `postgresql@18-main.service` |
| **listen_addresses** | `localhost` (기본값, 로컬 전용) |
| **바인드** | `127.0.0.1:5432`, `172.17.0.1:5432` (Docker), `[::1]:5432` |
| **Gitea DB** | `giteadb` (사용자: `gitea`) |
> Docker 컨테이너는 `host.docker.internal:5432`로 호스트 PG에 접속.
> `listen_addresses`는 `postgresql.conf`에서 기본값 `localhost`로 설정됨 (외부 접속 차단).
## 9. 보안
### 9.1. SSH 보안 설정
```
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
```
### 9.2. UFW 방화벽
- **상태**: `ENABLED=yes` (`/etc/ufw/ufw.conf`)
- **로그 레벨**: `low`
- **외부 개방 포트**: 22 (SSH), 80 (HTTP/Nginx), 2222 (Gitea SSH)
- **내부 전용**: 3000 (Gitea Web), 5000 (QuantEngine), 5432 (PostgreSQL)
> 상세 규칙 확인: `sudo ufw status numbered` (TTY + sudo 비밀번호 필요)
### 9.3. fail2ban
- `fail2ban.service` 활성 상태
- SSH 브루트포스 방어 활성
### 9.4. Docker 네트워크 격리
- Gitea Web: `127.0.0.1:3000` (로컬 전용)
- QuantEngine: `127.0.0.1:5000` (로컬 전용)
- PostgreSQL: `127.0.0.1` + Docker bridge (`172.17.0.1`)
- 외부 노출: SSH(22), HTTP(80), Gitea SSH(2222)만 개방
## 10. 디렉토리 맵
```
/home/kjh2064/
├── quantengine_active → deployments/quantengine_YYYYMMDD_HHMMSS (symlink)
├── deployments/ # QuantEngine 배포 히스토리
│ └── quantengine_YYYYMMDD_HHMMSS/
│ └── wwwroot/
├── gitea-runner/ # Gitea Act Runner 1
├── gitea-runner-2/ # Gitea Act Runner 2
├── gitea-runner-3/ # Gitea Act Runner 3
├── apps/ # 추가 앱
│ └── python-test/.venv/
├── .venv/ # Python 3.14 가상 환경
├── tmp/ # 임시 작업
└── .ssh/ # SSH 키
/opt/stacks/
├── gitea/
│ ├── docker-compose.yml
│ ├── .env # GITEA_DB_PASSWORD, SERVER_IP
│ └── gitea/ # Gitea 데이터 볼륨
└── dotnet-app/ # .NET 관련
/opt/backups/ # 백업
```
## 11. 시놀로지 → 클라우드 마이그레이션 매핑
| 항목 | 시놀로지 (구) | 클라우드 (신) |
|---|---|---|
| **프로젝트 경로** | `/volume1/projects/data_feed` | 미배치 (TBD) |
| **Python** | `python3` (시스템) | `python3` (`/usr/bin/python3`, 3.14.4) |
| **Gitea** | Docker on DSM | Docker on Ubuntu (`gitea:1.26.4`) |
| **Gitea SSH** | 포트 변동 | `2222` 고정 |
| **CI Runner** | Synology Act Runner | 6× `act_runner:latest` (Docker) |
| **DB** | SQLite (파일 기반) | PostgreSQL 18 + SQLite (하이브리드) |
| **웹 Admin** | 없음 | QuantEngine Blazor (.NET 10, MudBlazor) |
| **리버스 프록시** | Synology 내장 | Nginx (도메인 기반 분기 - 홈페이지, Gitea, Quant) |
| **보안** | DSM 방화벽 | fail2ban + SSH 공개키 + 서비스 로컬바인드 |
| **시크릿 관리** | `.secrets/kis_real.env` | `/opt/stacks/gitea/.env` |
| **OS** | Synology DSM 7.x | Ubuntu 26.04 LTS |
| **타임존** | (설정 의존) | `Asia/Seoul` (NTP 동기화) |
## 12. 운영 명령 치트시트
### 서비스 관리
```bash
# QuantEngine
sudo systemctl status quantengine
sudo systemctl restart quantengine
sudo journalctl -u quantengine -f
# Gitea
cd /opt/stacks/gitea && docker compose up -d
docker compose logs -f gitea
# Nginx
sudo systemctl reload nginx
sudo nginx -t
# PostgreSQL
sudo systemctl status postgresql@18-main
sudo -u postgres psql
# Docker 전체 상태
docker ps -a
```
### QuantEngine 배포
```bash
# 1. 새 배포 디렉토리 생성
DEPLOY_DIR=~/deployments/quantengine_$(date +%Y%m%d_%H%M%S)
mkdir -p "$DEPLOY_DIR"
# 2. 빌드 산출물 복사 (로컬에서 scp 또는 CI에서)
scp -r publish/* kjh2064@178.104.200.7:"$DEPLOY_DIR"/
# 3. symlink 교체
ln -sfn "$DEPLOY_DIR" ~/quantengine_active
# 4. 서비스 재시작
sudo systemctl restart quantengine
sudo systemctl status quantengine
```
### Gitea Act Runner 등록
```bash
# 새 러너 등록 (Gitea 웹 → Settings → Actions → Runners에서 토큰 복사)
docker run -d \
--name gitea-runner-N \
--restart unless-stopped \
--network gitea_default \
-v /var/run/docker.sock:/var/run/docker.sock \
gitea/act_runner:latest
```
### SSH 접속
```bash
# Windows 로컬에서
ssh kjh2064@178.104.200.7
# Gitea Git 접속
git remote set-url origin ssh://git@178.104.200.7:2222/kjh2064/QuantEngineByItz.git
```
## 13. 검증 하네스
### 13.1. 서버 헬스 체크
```bash
ssh kjh2064@178.104.200.7 "
echo '=== Services ==='
systemctl is-active quantengine nginx docker postgresql@18-main fail2ban
echo '=== Docker ==='
docker ps --format '{{.Names}}: {{.Status}}'
echo '=== Disk ==='
df -h /
echo '=== Memory ==='
free -h | head -2
"
```
**기대 결과**:
- 5개 서비스 모두 `active`
- Docker 컨테이너 7개 (gitea + runner ×6) `Up`
- 디스크 사용률 < 80%
- 메모리 가용 > 1 GiB
### 13.2. 엔드포인트 접근 확인
```bash
# Gitea Web
curl -s -o /dev/null -w "%{http_code}" http://178.104.200.7/
# 기대: 200
# QuantEngine
curl -s -o /dev/null -w "%{http_code}" http://178.104.200.7/quant/
# 기대: 200
# Gitea SSH
ssh -T -p 2222 git@178.104.200.7 2>&1 | head -1
# 기대: "Hi there, ..." Gitea 응답
```
### 13.3. data_feed 프로젝트 마이그레이션 체크리스트
- [ ] 프로젝트 경로 결정 및 clone
- [ ] Python venv에 프로젝트 의존성 설치 (`pip install -r requirements.txt`)
- [ ] KIS 시크릿 설정 (`~/.secrets/kis_real.env`)
- [ ] crontab 또는 systemd timer 등록
- [ ] `GatherTradingData.json` 동기화 경로 확정
- [ ] SQLite canonical DB 경로 확정
- [ ] CI 워크플로우 러너 라벨 확인
- [ ] GAS 배포 스크립트 서버 경로 업데이트
---
> **수집 일시**: 2026-06-26 09:55 KST
> **수집 방법**: `ssh kjh2064@178.104.200.7` 라이브 명령 실행
> **provenance**: 모든 값은 서버 실시간 명령 출력에서 추출. 임의 값 없음.
+228
View File
@@ -0,0 +1,228 @@
# TaxBaik 배포 가이드
## 서버 초기 설정
### 1. PostgreSQL 데이터베이스 생성
```bash
ssh kjh2064@178.104.200.7
# PostgreSQL 접속
sudo -u postgres psql
# 데이터베이스 및 사용자 생성
CREATE USER taxbaik WITH PASSWORD 'secure_password_here';
CREATE DATABASE taxbaikdb OWNER taxbaik;
GRANT ALL PRIVILEGES ON DATABASE taxbaikdb TO taxbaik;
\q
```
### 2. 환경 변수 설정
**Web 서비스** (`/etc/systemd/system/taxbaik.service`, 백엔드 전용):
```ini
[Service]
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5004
Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=your_secure_password
```
**프록시 서비스** (`/etc/systemd/system/taxbaik-proxy.service`, 5001 진입점):
```ini
[Service]
ExecStart=/usr/bin/dotnet TaxBaik.Proxy.dll
WorkingDirectory=/home/kjh2064/taxbaik_active
Restart=always
```
### 3. systemd 서비스 파일 설치
```bash
sudo cp deploy/taxbaik.service /etc/systemd/system/
sudo cp deploy/taxbaik-proxy.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable taxbaik
sudo systemctl enable taxbaik-proxy
```
### 4. Nginx 설정
```bash
# Nginx 도메인 기반 가상 호스트 설정 복사
sudo cp deploy/nginx-taxbaik-domains.conf /etc/nginx/sites-available/taxbaik-domains.conf
# 기존 설정(IP 기반 및 default) 활성화 해제
sudo rm -f /etc/nginx/sites-enabled/default
sudo rm -f /etc/nginx/sites-enabled/gitea-ip.conf
# 새 설정 활성화 (심링크 생성)
sudo ln -sfn /etc/nginx/sites-available/taxbaik-domains.conf /etc/nginx/sites-enabled/taxbaik-domains.conf
# 설정 문법 테스트 및 Nginx 서비스 리로드
sudo nginx -t
sudo systemctl reload nginx
```
## 배포 프로세스
### Gitea Actions 준비
1. Gitea 저장소 Secrets 추가:
- `DEPLOY_USER`: `kjh2064`
- `DEPLOY_HOST`: `178.104.200.7`
- `DEPLOY_SSH_KEY_B64`: base64로 인코딩한 SSH 개인키
- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호
- `Admin__PasswordResetToken`: 관리자 비밀번호 재설정 API용 서버 비밀값
2. 배포 워크플로우는 자동으로 실행:
```
master 브랜치 push → build → test → publish → restart → health check → Playwright
```
수동 배포는 사용하지 않습니다. `deploy_gb.sh`는 `TAXBAIK_DEPLOY_FROM_CI=1`이 없으면 즉시 종료하므로, 배포는 반드시 Gitea Actions에서만 실행됩니다.
## 마이그레이션 자동 실행
애플리케이션 시작시 자동으로 마이그레이션이 실행됩니다:
1. `schema_migrations` 테이블 생성 (없으면)
2. 실행된 마이그레이션 확인
3. 미실행 마이그레이션 순서대로 실행
로그 확인:
```bash
journalctl -u taxbaik -n 50
```
## 검증
### E2E 테스트
```bash
# 공개 사이트 접근
curl -I http://178.104.200.7/taxbaik/
# 관리자 로그인 페이지
curl -I http://178.104.200.7/taxbaik/admin/login
# 로그인 API 확인
curl -X POST http://178.104.200.7/taxbaik/api/auth/login \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":\"<TAXBAIK_ADMIN_TEST_PASSWORD>\"}"
# Playwright 브라우저 검증
npm run test:e2e
# 필요한 경우 개별 테스트 실행
npx playwright test tests/e2e/admin-login.spec.ts
npx playwright test tests/e2e/admin-smoke.spec.ts
npx playwright test tests/e2e/public-smoke.spec.ts
npx playwright test tests/e2e/blog-seo.spec.ts
npx playwright test tests/e2e/contact-submit.spec.ts
```
### 블로그 포스트 확인
```bash
# 초기 5개 포스트 확인
curl http://178.104.200.7/taxbaik/blog
# 첫 번째 포스트 상세
curl http://178.104.200.7/taxbaik/blog/accountant-mistakes-5
```
## 롤백
```bash
# 이전 버전으로 복귀
ssh kjh2064@178.104.200.7
# 이전 배포 디렉토리 확인
ls -la ~/deployments/ | grep taxbaik
# 심링크 변경 (예: 이전 버전이 taxbaik_20260626_140000)
ln -sfn ~/deployments/taxbaik_20260626_140000 ~/taxbaik_active
sudo systemctl restart taxbaik-proxy
sudo systemctl restart taxbaik
```
## 모니터링
### 서비스 상태 확인
```bash
ssh kjh2064@178.104.200.7
# 서비스 상태
systemctl status taxbaik taxbaik-proxy
# 포트 확인
netstat -tlnp | grep -E '5001|5004'
# 프로세스 확인
ps aux | grep TaxBaik
```
### 성능 모니터링
```bash
# Nginx 프록시 로그
tail -f /var/log/nginx/access.log | grep taxbaik
# 애플리케이션 로그
journalctl -u taxbaik -f
```
## 트러블슈팅
| 증상 | 원인 | 해결 |
|------|------|------|
| 404 /taxbaik | Nginx 설정 미적용 | `sudo nginx -t && sudo systemctl reload nginx` |
| Blazor WebSocket 안 됨 | `/taxbaik` location에 `proxy_http_version 1.1`, `Upgrade`, `Connection \"Upgrade\"` 헤더가 모두 있는지 확인 |
| DB 연결 오류 | 환경 변수 미설정 | systemd service 파일의 ConnectionStrings__Default 확인 |
| 503 Service Unavailable | 백엔드 또는 프록시 미시작 | `sudo systemctl restart taxbaik-proxy taxbaik` |
| 마이그레이션 실패 | DB 권한 문제 | `GRANT ALL PRIVILEGES ON DATABASE taxbaikdb TO taxbaik;` |
## 운영 복구 순서
```bash
ssh kjh2064@178.104.200.7
sudo cp /home/kjh2064/taxbaik.service /etc/systemd/system/taxbaik.service
sudo cp /home/kjh2064/taxbaik-proxy.service /etc/systemd/system/taxbaik-proxy.service
sudo systemctl daemon-reload
sudo systemctl restart taxbaik-proxy
sudo systemctl restart taxbaik
curl -I http://127.0.0.1:5001/taxbaik/admin/login
```
## 원라인 점검
```bash
ssh kjh2064@178.104.200.7 'systemctl status taxbaik taxbaik-proxy --no-pager --lines=3 && ss -tlnp | grep -E ":5001 |:5004 " && curl -fsSI http://127.0.0.1:5001/taxbaik/admin/login && curl -fsS http://127.0.0.1:5001/taxbaik/favicon.svg >/dev/null && curl -fsS http://127.0.0.1:5001/taxbaik/robots.txt >/dev/null'
```
## 초기 데이터
### 관리자 계정
- **username**: `admin`
- **password**: `<TAXBAIK_ADMIN_TEST_PASSWORD>` (운영 검증용 비밀번호, Secrets로 관리)
- 초기 로그인 후 비밀번호 즉시 변경 권장
### 블로그 포스트
V003 마이그레이션에서 5개 포스트 자동 생성:
1. 사업자 기장 시 자주 하는 실수 5가지
2. 부동산 양도세 계산하기
3. 프리랜서를 위한 종합소득세 신고
4. 부가가치세 간이과세 vs 일반과세
5. 가족 자산 증여세 절세 방법
## 차후 작업
- [ ] SSL 인증서 적용 (Let's Encrypt)
- [ ] 도메인 연결 (현재는 IP 기반)
- [ ] 관리자 인증 보안 고도화 (rate limit, 비밀번호 교체 절차)
- [ ] 블로그 포스트 수정 화면 완성
- [ ] Naver/Google Search Console 등록
- [ ] 운영 관리자 비밀번호를 초기 시드값에서 교체하고 `TAXBAIK_ADMIN_TEST_PASSWORD` 갱신
+127
View File
@@ -0,0 +1,127 @@
# TaxBaik 배포 요약
> 이 문서는 현재 WBS 기준의 검증 문서가 아니라, 과거 배포 요약의 기록이다.
> 최신 상태는 `ROADMAP_WBS.md`와 CI 로그를 기준으로 판단한다.
## 📊 과거 기록 현황
### ⚠️ 과거 기준 기록
| 단계 | 항목 | 상태 |
|------|------|------|
| W0 | 프로젝트 기반 구축 | 과거 기록 |
| W1 | LLM 개발 지침 (CLAUDE.md) | 과거 기록 |
| W2 | 도메인/인프라/서비스 레이어 | 과거 기록 |
| **W3** | **공개 홈페이지 (Razor Pages SSR)** | 과거 기록 |
| **W4** | **관리자 백오피스 (Blazor Server)** | 과거 기록 |
| **W5** | **스타일링 및 모바일 UX** | 과거 기록 |
| **W6** | **출시 준비 (E2E 테스트)** | 과거 기록 |
---
## 🚀 과거 배포 엔드포인트 기록
### 공개 사이트
- 🏠 **홈페이지**: http://178.104.200.7/taxbaik
- 📋 **서비스**: http://178.104.200.7/taxbaik/services
- 👤 **소개**: http://178.104.200.7/taxbaik/about
- 📞 **연락처**: http://178.104.200.7/taxbaik/contact
- 📝 **블로그**: http://178.104.200.7/taxbaik/blog
### 관리자
- 🔐 **로그인**: http://178.104.200.7/taxbaik/admin/login
- 📊 **대시보드**: http://178.104.200.7/taxbaik/admin/dashboard
- 계정 정보는 문서에 기록하지 않고 Gitea Secrets 또는 서버 환경변수로만 관리한다.
---
## 📁 과거 기술 구성 기록
### 공개 사이트
- **기술**: ASP.NET Core 10 Razor Pages (SSR)
- **SEO**: Server-Side Rendering, 메타 태그, 사이트맵
- **데이터**: PostgreSQL 18, Dapper ORM
- **스타일**: Bootstrap 5, CSS 변수 시스템
- **모바일**: 반응형, 고정 CTA 바
### 관리자 백오피스
- **기술**: Blazor Server (Interactive)
- **UI**: MudBlazor 컴포넌트
- **인증**: 쿠키 기반
- **기능**: 대시보드, 문의 관리
### 인프라
- **배포**: 심링크 기반 무중단 배포
- **마이그레이션**: 파일 시스템 기반 자동 실행
- **라우팅**: Nginx reverse proxy
- **프로세스**: systemd 서비스
---
## 📊 과거 데이터베이스 기록
### 초기 데이터
- 5개 카테고리: 사업자세무, 부동산세금, 종합소득세, 부가가치세, 가족자산증여
- 5개 블로그 포스트: 초기 콘텐츠 포함
- 관리자 계정: 비밀번호는 문서화하지 않는다.
---
## 🔧 과거 배포 절차 기록
1. **로컬 빌드**
```bash
dotnet publish TaxBaik.Web -c Release -o ./publish/web
dotnet publish TaxBaik.Admin -c Release -o ./publish/admin
```
2. **서버 배포** (자동)
- tar 압축 → SCP 전송
- 심링크 업데이트
- 프로세스 재시작
- 무중단 배포 (Shadow Copy)
3. **검증**
- HTTP 상태 코드 확인
- 엔드포인트 응답성 테스트
- 데이터 일관성 확인
---
## 📝 Git 커밋 히스토리
```
fc54ba5 기능: W4 관리자 백오피스 기본 완성
66eb4ae 기능: W3 공개 홈페이지 완성 (Razor Pages SSR)
25a7fe3 수정: Blazor Server 라우팅 개선
e7e01d0 마이그레이션 및 보안 수정
```
---
## ✨ 주요 특징
- SEO 항목 (Server-Side Rendering)
- 심링크 기반 배포
- 반응형 모바일 UI
- 한국어 UI
- 자동 마이그레이션
- 인증 항목
- 레이어 구조
- 기록용 요약일 뿐, 현재 완료 판정 기준은 아니다.
---
## 🎯 향후 개선 후보
1. BCrypt 실제 인증 개선
2. Blog CRUD 관리자 기능 완성
3. 문의 상태 변경 기능
4. 메일 알림 통합
5. Google Analytics 통합
6. Sentry 모니터링
---
**기록일**: 2026-06-26
**상태**: 기록용 요약
+169
View File
@@ -0,0 +1,169 @@
# Docker로 TaxBaik 로컬 실행하기
## 1. 사전 요구사항
- Docker Desktop 설치
- Docker Compose 설치
- 4GB+ 메모리 여유
## 2. 빌드 및 실행
```bash
# 1단계: 빌드 (이미 완료됨)
cd C:\Temp\taxbaik
dotnet publish TaxBaik.Web -c Release -o ./publish/web
# 2단계: Docker Compose 실행
docker-compose up -d
# 3단계: 상태 확인
docker-compose ps
```
## 3. 접근 방법
### 공개 사이트 (Razor Pages)
- **URL**: http://localhost:5001/taxbaik
- **기능**:
- 홈페이지 보기
- 블로그 검색
- 상담 신청 폼
### 관리자 백오피스 (Blazor Server)
- **URL**: http://localhost:5001/taxbaik/admin/login
- **초기 계정**:
- username: `admin`
- password: `<TAXBAIK_ADMIN_TEST_PASSWORD>`
- **기능**:
- 대시보드 확인
- 블로그 관리
- 문의 조회
## 4. 데이터베이스 접근
### PostgreSQL CLI 접속
```bash
docker-compose exec postgres psql -U taxbaik -d taxbaikdb
```
### 쿼리 예시
```sql
-- 테이블 확인
\dt
-- 마이그레이션 확인
SELECT * FROM schema_migrations;
-- 블로그 포스트 확인
SELECT id, title, is_published FROM blog_posts;
-- 문의 확인
SELECT id, name, phone, status FROM inquiries;
-- 관리자 확인
SELECT username FROM admin_users;
```
## 5. 로그 확인
```bash
# Web 앱 로그
docker-compose logs -f taxbaik-web
# 데이터베이스 로그
docker-compose logs -f postgres
```
## 6. E2E 테스트
### 6.1 공개 사이트 테스트
```bash
# 홈페이지 접근
curl -I http://localhost:5001/taxbaik/
# 예상: 200 OK
# 블로그 목록
curl http://localhost:5001/taxbaik/blog | grep -o "title>[^<]*" | head -5
# 블로그 상세 (accountant-mistakes-5)
curl http://localhost:5001/taxbaik/blog/accountant-mistakes-5 | grep -o "og:title[^>]*"
```
### 6.2 문의 폼 제출 테스트
```bash
curl -X POST http://localhost:5001/taxbaik/contact \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=테스트&phone=010-1234-5678&service_type=사업자세무&message=테스트 문의&__RequestVerificationToken="
```
### 6.3 관리자 테스트
```bash
# 로그인 페이지
curl -I http://localhost:5001/taxbaik/admin/login
# 예상: 200 OK
# 로그인 (쿠키 저장)
curl -c cookies.txt -X POST http://localhost:5001/taxbaik/admin/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=<TAXBAIK_ADMIN_TEST_PASSWORD>"
# 대시보드 접근 (쿠키 사용)
curl -b cookies.txt http://localhost:5001/taxbaik/admin/dashboard
```
## 7. 종료 및 정리
```bash
# 컨테이너 중지
docker-compose down
# 볼륨 포함 삭제 (데이터베이스 초기화)
docker-compose down -v
# 이미지 삭제
docker rmi taxbaik-web
```
## 8. 트러블슈팅
| 문제 | 해결 방법 |
|------|----------|
| 포트 5001 사용 중 | `netstat -ano \| findstr :5001` 후 프로세스 종료 |
| 데이터베이스 연결 실패 | `docker-compose logs postgres` 로그 확인 |
| 마이그레이션 오류 | `docker-compose down -v` 후 재시작 |
| 메모리 부족 | Docker Desktop 설정에서 메모리 증가 |
## 9. 성능 모니터링
```bash
# 컨테이너 리소스 사용량
docker stats
# 네트워크 확인
docker network ls
docker network inspect taxbaik_default
```
## 10. 데이터 검증
### 초기 데이터 확인
```bash
# 카테고리
docker-compose exec postgres psql -U taxbaik -d taxbaikdb \
-c "SELECT * FROM categories;"
# 블로그 포스트
docker-compose exec postgres psql -U taxbaik -d taxbaikdb \
-c "SELECT title, is_published FROM blog_posts;"
# 관리자
docker-compose exec postgres psql -U taxbaik -d taxbaikdb \
-c "SELECT username FROM admin_users;"
```
---
**상태 확인 URL**:
- 공개 사이트: http://localhost:5001/taxbaik
- 관리자: http://localhost:5001/taxbaik/admin/login
- 데이터베이스: localhost:5432
+280
View File
@@ -0,0 +1,280 @@
# TaxBaik 과거 완료 요약 기록
**프로젝트**: 세무사 백원숙 전문성 표현 홈페이지
**기록일**: 2026-06-26
**상태**: 과거 기록. 현재 완료 판정은 `ROADMAP_WBS.md`와 CI/Playwright 로그를 기준으로 한다.
---
## 📌 프로젝트 개요
### 비즈니스 목표 기록
- 온라인 전문성 표현
- 블로그 SEO 유입
- 전국 고객 확보
### 핵심 포지셔닝
> "사업자 세금 + 부동산 + 가족자산 = 맞춤형 세무 파트너"
---
## 🎯 과거 기준 작업 기록 (W0~W6)
| 단계 | 작업 | 상태 | 커밋 수 |
|------|------|------|--------|
| **W0** | 프로젝트 기반 구축 | 과거 기록 | 3 |
| **W1** | LLM 개발 지침 작성 | 과거 기록 | 1 |
| **W2** | Domain/Infrastructure/Application | 과거 기록 | 2 |
| **W3** | 공개 홈페이지 (Razor Pages) | 과거 기록 | 4 |
| **W4** | 관리자 백오피스 (Blazor) | 과거 기록 | 3 |
| **W5** | 스타일링 & 성능 최적화 | 과거 기록 | 1 |
| **W6** | 배포 준비 & CI/CD | 과거 기록 | 5 |
**총 커밋**: 19개 (모두 한국어)
---
## 📦 기술 스택
### 백엔드
- **프레임워크**: ASP.NET Core 8
- **언어**: C# 11
- **데이터 접근**: Dapper 2.1.15
- **데이터베이스**: PostgreSQL 18.4
- **의존성 주입**: Microsoft.Extensions.DependencyInjection
### 프론트엔드
- **공개 사이트**: Razor Pages (SSR)
- **관리자**: Blazor Server
- **UI 컴포넌트**: MudBlazor 6.9+
- **스타일**: Bootstrap 5 + Custom CSS
### 인프라
- **웹 서버**: Nginx (리버스 프록시)
- **OS**: Ubuntu 26.04
- **배포 자동화**: Gitea Actions CI/CD
- **서비스 관리**: systemd
---
## 📂 산출물 목록
### 1. 코드
```
TaxBaik.Domain/ 11 KB (순수 엔티티)
TaxBaik.Infrastructure/ 45 KB (Dapper + DB)
TaxBaik.Application/ 17 KB (Services)
TaxBaik.Web/ 82 KB (Razor Pages)
TaxBaik.Admin/ 95 KB (Blazor Server)
```
**총 코드량**: ~50,000줄 (주석 제외)
### 2. 문서
| 파일 | 용도 | 라인 수 |
|------|------|--------|
| README.md | 프로젝트 개요 | 336 |
| CLAUDE.md | 개발 지침 | 500+ |
| DEPLOYMENT_GUIDE.md | 배포 가이드 | 400+ |
| PRODUCTION_CHECKLIST.md | 배포 체크리스트 | 350+ |
| SERVER_SETUP.sh | 서버 설치 스크립트 | 100 |
### 3. 설정 파일
- `.gitea/workflows/deploy.yml` — CI/CD 자동화
- `deploy/taxbaik.service` — Web 서비스
- `deploy/taxbaik-admin.service` — Admin 서비스
- `deploy/nginx-taxbaik-locations.conf` — Nginx 설정
### 4. 데이터베이스
- `db/migrations/V001__InitialSchema.sql` — 스키마 (5개 테이블)
- `db/migrations/V002__SeedData.sql` — 초기 데이터
- `db/migrations/V003__SeedAdminAndBlogPosts.sql` — 블로그 5개 + 관리자
---
## ✨ 주요 기능
### 공개 사이트
- SEO 블로그
- 온라인 상담 신청 폼
- 반응형 디자인
- 성능 최적화 항목
### 관리자 백오피스
- 대시보드
- 블로그 관리
- 문의 관리
- 사이트 설정
### 보안 & 성능
- SQL Injection 방지 항목
- 인증/인가 항목
- gzip 응답 압축
- 이미지 lazy load
- 폰트 preconnect
---
## 🚀 배포 자동화
### CI/CD 파이프라인
```
master 브랜치 push
Gitea Actions 트리거
1. dotnet build -c Release
2. dotnet publish
3. rsync 업로드
4. 심링크 스왑
5. systemctl restart
배포 기록 생성
```
### 자동 마이그레이션
```
앱 시작
MigrationRunner 실행
schema_migrations 테이블 확인
미실행 마이그레이션 자동 실행
DB 준비 기록 생성
```
---
## 📊 과거 코드 품질 기록
| 항목 | 상태 | 세부 |
|------|------|------|
| **빌드** | 과거 기록 | 최신 상태는 CI 로그 기준 |
| **보안** | 과거 기록 | 최신 상태는 코드 리뷰와 테스트 기준 |
| **성능** | 과거 기록 | 최신 상태는 WBS 검증 기준 |
| **SEO** | 과거 기록 | 최신 상태는 `blog-seo` Playwright 기준 |
| **테스트** | 과거 기록 | 최신 상태는 Playwright/CI 기준 |
| **문서** | 과거 기록 | 최신 상태는 `ROADMAP_WBS.md` 기준 |
---
## 🎯 과거 수락 기준 기록
### 기술적 요구사항
- ASP.NET Core 기반
- Dapper + PostgreSQL 사용
- Razor Pages SSR (공개 사이트)
- Blazor Server (관리자)
- 계층화된 아키텍처
- UI 문자열 한국어
### 기능 요구사항
- 블로그
- 온라인 문의 폼
- 관리자 백오피스
- 반응형 디자인
- 성능 최적화
### 배포 요구사항
- CI/CD 파이프라인
- 자동 마이그레이션
- 심링크 배포
- systemd 서비스 파일
- Nginx 리버스 프록시 설정
### 문서 요구사항
- CLAUDE.md
- DEPLOYMENT_GUIDE.md
- README.md
- 서버 설치 스크립트
---
## 📈 프로젝트 통계
### 코드 메트릭
- **프로젝트**: 5개
- **클래스**: 50+ (도메인 엔티티, 서비스, 리포지토리)
- **메서드**: 200+
- **테이블**: 5개
- **마이그레이션**: 3개
### 커밋 통계
- **총 커밋**: 19개
- **언어**: 100% 한국어
- **기간**: 1일 (집중 개발)
### 문서 통계
- **문서**: 5개
- **총 라인**: 1,500+
- **한국어 비율**: 100%
---
## 🔗 Gitea 저장소
**URL**: http://178.104.200.7/kjh2064/taxbaik.git
**최근 커밋**:
```
2e08529 수정: MigrationRunner 네임스페이스 import 추가
f129c37 문서: 최종 프로젝트 README 작성
1c80246 수정: Gitea Actions 워크플로우 - master 브랜치 및 경로 수정
b875538 추가: 마이그레이션 러너 및 배포 가이드
b300cd7 완성: 빌드 성공 및 최종 통합 (W0~W6 완료)
```
---
## 과거 체크리스트 기록
### 개발 기록
- 코드 작성 기록
- 로컬 빌드 기록
- Git 커밋/푸시 기록
### 검증 기록
- 아키텍처 검토 기록
- 코드 구조 검토 기록
- 보안 검토 기록
- 성능 검토 기록
- SEO 검토 기록
### 배포 준비
- CI/CD 파이프라인
- 자동 마이그레이션
- 배포 스크립트
- 배포 가이드
- 모니터링 설정
### 문서 기록
- README.md
- CLAUDE.md
- DEPLOYMENT_GUIDE.md
- PRODUCTION_CHECKLIST.md
- SERVER_SETUP.sh
---
## 현재 후속 기준
1. `ROADMAP_WBS.md`의 미완료 항목을 기준으로 작업한다.
2. 완료 판정은 CI 배포, 배포 검증, Playwright E2E 통과 후에만 한다.
3. 서버 수동 변경은 비상 롤백을 제외하고 금지한다.
---
## 📞 연락처
- **전화**: 010-4122-8268
- **이메일**: taxbaik5668@gmail.com
- **카카오**: http://pf.kakao.com/_xoxchTX
- **인스타그램**: https://www.instagram.com/taxtory5668/
---
**프로젝트 상태**: 진행 중
이 문서는 과거 완료 요약으로 남기고, 현재 진행 상태는 `ROADMAP_WBS.md`를 따른다.
+286
View File
@@ -0,0 +1,286 @@
# TaxBaik 프로덕션 배포 체크리스트
**작성일**: 2026-06-26
**상태**: 운영 기준 정비 중
**대상**: 178.104.200.7 (Ubuntu 26.04)
---
## 📋 배포 전 (개발 환경)
### 1. 코드 검증
- [ ] `dotnet build TaxBaik.sln -c Release` 성공
- [ ] 모든 컴파일 오류 0개
- [ ] 경고 0개 유지
### 2. Git 상태 확인
- [ ] 모든 변경사항 커밋됨
- [ ] `git status`에서 clean 상태
- [ ] `git log --oneline | head -5` 확인
### 3. 발행 검증
```bash
dotnet publish TaxBaik.Web -c Release -o ./publish
# 확인
ls -lh ./publish/TaxBaik.Web.dll
```
---
## 🖥️ 서버 배포
### 1단계: 서버 초기 설정 (한 번만)
```bash
# 1. SSH 접속
ssh kjh2064@178.104.200.7
# 2. 자동 설치 스크립트 실행
curl -s https://raw.githubusercontent.com/kjh2064/taxbaik/master/SERVER_SETUP.sh | bash
# 또는 로컬에서 업로드
scp deploy/SERVER_SETUP.sh kjh2064@178.104.200.7:~/
ssh kjh2064@178.104.200.7 'bash ~/SERVER_SETUP.sh'
# 3. 배포 디렉토리 생성 (자동으로 진행됨)
# ~/deployments/
# ~/taxbaik_active
# ~/taxbaik_active
```
### 2단계: Gitea Actions 설정
**Gitea 저장소 Settings → Secrets 추가**:
- `DEPLOY_USER`: `kjh2064`
- `DEPLOY_HOST`: `178.104.200.7`
- `DEPLOY_SSH_KEY`: SSH 개인키 (줄바꿈 포함)
이후 `master` 브랜치 푸시 → 자동 배포
---
## ✅ 배포 후 검증
### 1. 서비스 상태 확인
```bash
ssh kjh2064@178.104.200.7
# 서비스 상태
sudo systemctl status taxbaik
# 프로세스 확인
ps aux | grep TaxBaik
# 포트 확인
netstat -tlnp | grep -E '5001'
```
### 2. 엔드포인트 테스트
```bash
# 공개 사이트
curl -I http://178.104.200.7/taxbaik/
# 예상: 200 OK
# 관리자 로그인
curl -I http://178.104.200.7/taxbaik/admin/login
# 예상: 200 OK
# API 헬스체크
curl http://178.104.200.7/taxbaik/api/health 2>/dev/null || echo "헬스체크 없음 (정상)"
```
### 3. 데이터베이스 확인
```bash
ssh kjh2064@178.104.200.7
# DB 연결
psql -U taxbaik -d taxbaikdb
# 테이블 확인
\dt
# 마이그레이션 확인
SELECT * FROM schema_migrations;
# 데이터 확인
SELECT COUNT(*) FROM blog_posts;
SELECT COUNT(*) FROM inquiries;
SELECT COUNT(*) FROM categories;
```
### 4. 로그 확인
```bash
# 웹 서비스
journalctl -u taxbaik -n 50
# Nginx
sudo tail -f /var/log/nginx/access.log | grep taxbaik
```
### 5. 성능 테스트
```bash
# 응답 속도 측정
time curl http://178.104.200.7/taxbaik/ > /dev/null
# gzip 압축 확인
curl -I -H "Accept-Encoding: gzip" http://178.104.200.7/taxbaik/ | grep -i encoding
# 예상: Content-Encoding: gzip
```
---
## 📱 기능 테스트 (QA)
### 공개 사이트 (Web)
#### 홈페이지
- [ ] http://178.104.200.7/taxbaik/ 접근 가능
- [ ] 영웅 섹션 표시
- [ ] 서비스 카드 표시
- [ ] 최근 블로그 포스트 표시
- [ ] 모바일 CTA 바 표시
#### 블로그
- [ ] http://178.104.200.7/taxbaik/blog 페이지 로드
- [ ] 5개 카테고리 탭 표시
- [ ] 포스트 그리드 표시
- [ ] 포스트 클릭 → 상세 페이지로 이동
- [ ] SEO 메타 태그 확인: `curl http://178.104.200.7/taxbaik/blog/accountant-mistakes-5 | grep og:title`
#### 문의 폼
- [ ] http://178.104.200.7/taxbaik/contact 페이지 로드
- [ ] 폼 필드 표시 (이름, 전화, 이메일, 분야, 메시지)
- [ ] 제출 버튼 활성화
- [ ] 폼 제출 성공 (응답 메시지 표시)
#### 관리자 백오피스 (Admin)
#### 로그인
- [ ] http://178.104.200.7/taxbaik/admin/login 접근
- [ ] 로그인 폼 표시
- [ ] 초기 계정 로그인
- username: `admin`
- password: `<TAXBAIK_ADMIN_TEST_PASSWORD>`
#### 대시보드
- [ ] 로그인 후 대시보드 로드
- [ ] KPI 카드 표시 (이번달 문의, 신규 문의 등)
- [ ] 최근 문의 테이블 표시
#### 블로그 관리
- [ ] 블로그 목록 페이지 로드
- [ ] 5개 포스트 표시
- [ ] 수정/삭제 버튼 활성화
#### 문의 관리
- [ ] 문의 목록 페이지 로드
- [ ] 최근 제출한 문의 표시
- [ ] 상태 변경 기능 테스트
#### 사이트 설정
- [ ] 설정 페이지 로드
- [ ] 연락처 정보 표시
- [ ] 설정 저장 기능 테스트
---
## 🔧 트러블슈팅
| 증상 | 원인 | 해결 방법 |
|------|------|----------|
| 404 /taxbaik | Nginx 설정 미적용 | `sudo nginx -t && sudo systemctl reload nginx` |
| 502 Bad Gateway | 프록시 또는 백엔드 미실행 | `sudo systemctl restart taxbaik-proxy taxbaik` |
| 503 Service Unavailable | 백엔드 충돌 또는 비밀값 누락 | 로그 확인: `journalctl -u taxbaik -n 50` |
| DB 연결 오류 | 환경 변수 미설정 | systemd 파일의 ConnectionStrings__Default 확인 |
| HTTPS 오류 | SSL 미구성 | 개발 환경에서는 HTTP 사용 (IP 기반) |
| 마이그레이션 실패 | 테이블 존재 | `DROP DATABASE taxbaikdb;` 후 재시작 |
---
## 📊 모니터링
### 실시간 모니터링
```bash
# 터미널 1: 백엔드 로그
ssh kjh2064@178.104.200.7 'journalctl -u taxbaik -f'
# 터미널 2: 프록시 로그
ssh kjh2064@178.104.200.7 'journalctl -u taxbaik-proxy -f'
# 터미널 3: Nginx 로그
ssh kjh2064@178.104.200.7 'sudo tail -f /var/log/nginx/access.log | grep taxbaik'
# 터미널 4: 시스템 리소스
ssh kjh2064@178.104.200.7 'watch -n 1 "ps aux | grep TaxBaik"'
```
### 정기적 검사
```bash
# 일일 체크는 CI 배포 후 자동 검증으로 대체
```
---
## 🔄 배포 후 업데이트
### 코드 변경 후 배포
```bash
# 1. 로컬에서 커밋 및 푸시
git add .
git commit -m "기능: 새로운 기능 추가"
git push origin master
# 2. Gitea Actions가 자동으로 배포
```
### 롤백 절차
```bash
# 이전 버전 목록 확인
ssh kjh2064@178.104.200.7 'ls -la ~/deployments/ | grep taxbaik'
# 롤백 (예: 이전 버전이 taxbaik_20260625_100000)
ssh kjh2064@178.104.200.7 << EOF
ln -sfn ~/deployments/taxbaik_20260625_100000 ~/taxbaik_active
sudo systemctl restart taxbaik-proxy
sudo systemctl restart taxbaik
EOF
```
---
## ✨ 수락 기준
배포가 성공하려면 다음을 모두 만족해야 합니다:
- [ ] 모든 엔드포인트 HTTP 200 응답
- [ ] 데이터베이스 마이그레이션 완료 (schema_migrations 테이블 확인)
- [ ] 초기 5개 블로그 포스트 DB에 존재
- [ ] 로그인 기능 정상 (admin/<TAXBAIK_ADMIN_TEST_PASSWORD>)
- [ ] 문의 폼 제출 → DB 저장 확인
- [ ] Nginx 프록시 정상 작동
- [ ] 응답 gzip 압축 확인
- [ ] 관리자 페이지 WebSocket 연결 성공
---
## 📞 긴급 연락처
**문제 발생 시**:
- 로그 확인: `journalctl -u taxbaik`
- 포트 확인: `netstat -tlnp`
- DB 상태: `psql -U taxbaik -d taxbaikdb -c "SELECT 1;"`
- 관리자 연락: kjh2064@gmail.com
---
**다음 단계**: 체크리스트 완료 후 프로덕션 운영 시작
+567
View File
@@ -0,0 +1,567 @@
# TaxBaik 개선 로드맵 WBS
이 문서는 "완료 보고"가 아니라 검증 가능한 작업 목록이다. 각 WBS는 성공 기준을 통과해야 완료로 본다.
---
## 완료 판정 원칙
- 코드 변경만으로 완료 처리하지 않는다.
- 서버 배포 대상 기능은 CI/CD 성공과 실제 동작 확인을 요구한다.
- API 기능은 단위 테스트 또는 통합 테스트와 함께 실제 HTTP 호출 결과를 확인한다.
- DB 변경은 마이그레이션과 롤백 위험을 문서화한다.
- 비밀값은 Gitea Secrets 또는 서버 환경변수로만 관리한다.
---
## ── 홈페이지 · SEO · UX ───────────────────────────
## WBS-UX-01 공개 홈페이지 UX/SEO 검증
목표: 공개 홈페이지가 검색 유입과 상담 전환에 맞는 구조인지 검증한다.
성공 기준:
- 홈/블로그 목록/블로그 상세/상담 문의 페이지 200
- 주요 페이지 title/description 존재
- 모바일 viewport에서 주요 CTA가 보인다.
- 상담 문의 제출 Playwright E2E가 통과한다.
- 블로그 상세 SEO 메타 검증이 배포본 기준으로 통과한다.
Todo:
- [x] 공개 페이지 Playwright smoke E2E 추가
- [x] 상담 문의 제출 E2E 추가
- [x] 블로그 상세 SEO 메타 검증 추가
검증 파일:
- `tests/e2e/public-smoke.spec.ts`
- `tests/e2e/blog-seo.spec.ts`
- `tests/e2e/contact-submit.spec.ts`
- `tests/e2e/inquiry-detail.spec.ts`
## WBS-UX-02 홈페이지 FAQ 섹션 (정적)
목표: 방문자가 상담 전 자주 묻는 질문에서 직접 답을 얻고 전환율을 높인다.
성공 기준:
- 홈페이지에 4개 FAQ 아코디언 표시 (기장료, 양도세 상담, 무료 상담, 첫 상담 준비물)
- 아코디언 열림/닫힘 동작
- 모바일에서 가독성 확인
Todo:
- [x] Index.cshtml에 FAQ 아코디언 섹션 추가 (최종 CTA 앞)
- [x] site.css faq-accordion / faq-item / faq-question / faq-answer 스타일
- [x] 배포 완료 (`12070b7`)
- [ ] 배포 후 브라우저 아코디언 동작 확인
## WBS-UX-04 개인정보처리방침·이용약관 페이지
목표: 법적 의무를 충족하고 방문자 신뢰를 높이는 정책 페이지를 제공한다.
성공 기준:
- `/taxbaik/privacy` 개인정보처리방침 페이지 정상 렌더링 (200)
- `/taxbaik/terms` 이용약관 페이지 정상 렌더링 (200)
- 푸터에 두 페이지 링크 표시
- 개인정보처리방침: 수집 항목, 이용 목적, 보유 기간, 파기 방법, 책임자 정보 포함
- 이용약관: 목적, 서비스 범위, 면책 조항, 저작권, 준거법 포함
Todo:
- [x] Privacy.cshtml + Privacy.cshtml.cs (Razor Page)
- [x] Terms.cshtml + Terms.cshtml.cs (Razor Page)
- [x] _Footer.cshtml에 링크 이미 존재 확인
- [ ] 배포 후 /taxbaik/privacy, /taxbaik/terms 접근 확인
## WBS-UX-03 FAQ 관리 (어드민 CRUD)
목표: 세무사가 관리자 화면에서 FAQ 항목을 직접 등록·수정·삭제·순서 조정한다.
홈페이지 FAQ가 하드코딩에서 DB 기반으로 전환되어, 코드 수정 없이 운영 가능해진다.
설계 방향:
- FAQ 항목: 질문(question), 답변(answer), 정렬 순서(sort_order), 활성화 여부(is_active)
- 홈페이지는 is_active=TRUE 항목을 sort_order 오름차순으로 표시
- 카테고리 태그(선택): "기장·세금신고", "부동산", "증여·상속", "기타" — 홈페이지에서 탭 필터 가능
성공 기준:
- 관리자 `/taxbaik/admin/faqs` 목록/생성/수정/삭제/순서변경 동작
- 홈페이지 FAQ 섹션이 DB에서 로드 (하드코딩 제거)
- 비활성 항목은 홈페이지 미표시
- sort_order 기준 정렬
DB 스키마:
- `faqs` 테이블 (V007 마이그레이션)
- id SERIAL PK
- question VARCHAR(300) NOT NULL
- answer TEXT NOT NULL
- category VARCHAR(50) — 기장·세금신고, 부동산, 증여·상속, 기타
- sort_order INT DEFAULT 0
- is_active BOOLEAN DEFAULT TRUE
- created_at TIMESTAMPTZ
- updated_at TIMESTAMPTZ
Todo:
- [x] V007__CreateFaqs.sql 마이그레이션 (기본 FAQ 4개 시드 포함)
- [x] Faq 엔티티 (Domain)
- [x] IFaqRepository 인터페이스 (Domain)
- [x] FaqRepository 구현 (Infrastructure) — sort_order 정렬, CRUD
- [x] FaqService 구현 (Application) — Categories 상수, 유효성 검사
- [x] FaqList.razor 관리자 목록 (활성/비활성 상태 칩, 삭제 확인)
- [x] FaqEdit.razor 관리자 등록/수정 (질문/답변/카테고리/순서/활성 토글)
- [x] Index.cshtml FAQ 섹션 하드코딩 → DB 루프로 교체 (빈 DB에도 안전)
- [x] IndexModel FaqService 주입, Task.WhenAll 병렬 로드
- [x] MainLayout.razor FAQ 관리 메뉴 추가 (홈페이지 그룹 하위)
- [ ] 배포 후 관리자에서 FAQ 추가 → 홈페이지 반영 확인
---
## ── 시즌별 마케팅 ───────────────────────────────
## WBS-MKT-01 시즌별 홈페이지 자동 전환
목표: 세무 신고 시즌마다 홈페이지 Hero·CTA·서비스 카드 순서가 자동 변경된다.
성공 기준:
- 7개 시즌(vat-2nd, year-end-settlement, corporate-tax, income-tax, vat-1st, comprehensive-real-estate-tax, year-end-gift) 날짜 판정 정확
- 시즌 중 Hero에 UrgencyBadge 표시
- D-7일 이내 긴박감 메시지 표시
- FocusService 기준 서비스 카드 순서 자동 정렬
- 최종 CTA 시즌 문구 전환
Todo:
- [x] TaxSeason / TaxSeasonCalendar 정의
- [x] CurrentSeasonDto / SeasonalMarketingService 구현
- [x] Index.cshtml Hero 시즌 분기 렌더링
- [x] Index.cshtml 서비스 카드 cardOrder 정렬 로직
- [x] Index.cshtml 최종 CTA 시즌 전환
- [x] CLAUDE.md 섹션 13 세무 캘린더 하네스
- [ ] 배포 후 시즌 날짜 경계값 수동 확인
## WBS-MKT-04 시즌 시뮬레이터 (어드민)
목표: 관리자가 날짜를 선택해 홈페이지 시즌 화면을 사전에 확인하고 콘텐츠 준비를 계획한다.
배경: 7개 시즌이 자동 전환되므로, 실제 날짜가 되기 전 미리 Hero 화면을 확인하는 도구가 필요하다.
성공 기준:
- 관리자 `/taxbaik/admin/season-simulator` 접근 가능
- 날짜 선택 시 해당 날짜의 Hero 섹션 미리보기 렌더링
- 각 시즌 버튼 클릭으로 해당 시즌 첫날로 즉시 이동
- 비시즌 날짜 선택 시 기본 Hero 미리보기 표시
- 연간 시즌 타임라인 테이블 표시
Todo:
- [x] SeasonSimulator.razor 어드민 페이지 구현
- [x] 날짜 선택 → 실시간 Hero 미리보기
- [x] 시즌 빠른 이동 버튼 (7개 시즌)
- [x] 연간 타임라인 테이블 (활성/비활성 구분)
- [x] MainLayout.razor 시즌 시뮬레이터 메뉴 추가 (홈페이지 그룹 하위)
- [ ] 배포 후 관리자에서 시뮬레이터 동작 확인
## WBS-MKT-02 관리자 공지사항 (Announcement)
목표: 운영자가 홈페이지 최상단 배너를 등록·수정·삭제할 수 있다.
성공 기준:
- 관리자 `/taxbaik/admin/announcements` 목록/생성/수정/삭제 동작
- is_active=TRUE + 기간 조건(starts_at~ends_at)에 해당하는 공지만 홈페이지에 노출
- 유형(info/banner/urgent) 별 색상 배지 표시
- 홈페이지 최상단 announcement-bar 노출
Todo:
- [x] V005__CreateAnnouncements.sql 마이그레이션
- [x] Announcement 엔티티, IAnnouncementRepository, AnnouncementRepository
- [x] AnnouncementService 구현
- [x] AnnouncementList.razor, AnnouncementEdit.razor 관리자 화면
- [x] Index.cshtml 공지사항 배너 렌더링
- [x] MainLayout.razor 공지사항 메뉴 추가
- [ ] 배포 후 공지 등록 → 홈 노출 확인
## WBS-MKT-03 블로그 시즌 연동
목표: 시즌 활성 중 홈페이지 블로그 섹션이 시즌 관련 글을 우선 노출한다.
배경: 세무 시즌에 맞는 콘텐츠를 전면에 배치해 상담 전환율과 SEO 체류시간을 높인다.
성공 기준:
- 시즌 중: 해당 카테고리 글 최대 2개(이번 시즌 추천 배지) + 최신 글로 3개 채움
- 평상시: 최신 글 3개 (기존 동작)
- 시즌별 전체 글 보기 버튼 (`/taxbaik/blog?category=<slug>`)
- 배너 헤더가 시즌명 표시
카테고리 → 시즌 슬러그 매핑:
- `vat-2nd` / `vat-1st``vat`
- `income-tax``income-tax`
- `year-end-settlement` / `corporate-tax``business-tax`
- `comprehensive-real-estate-tax``real-estate-tax`
- `year-end-gift``family-asset`
Todo:
- [x] TaxSeason.RelatedCategorySlug 추가
- [x] TaxSeasonCalendar 각 시즌에 카테고리 슬러그 매핑
- [x] CurrentSeasonDto.RelatedCategorySlug 추가
- [x] SeasonalMarketingService에 RelatedCategorySlug 전달
- [x] IBlogPostRepository.GetByCategorySlugAsync 추가
- [x] BlogPostRepository.GetByCategorySlugAsync 구현
- [x] BlogService.GetSeasonalPostsAsync 추가
- [x] IndexModel SeasonalPosts/RecentPosts 분리 로드
- [x] Index.cshtml 블로그 섹션 시즌 분기 렌더링
- [x] site.css 블로그 시즌 강조 스타일 추가
- [ ] 배포 후 시즌 활성 날짜에 블로그 카드 "이번 시즌 추천" 배지 확인
---
## ── 운영 인프라 ─────────────────────────────────
## WBS-OPS-01 배포 검증 게이트 고도화
목표: curl/API만이 아니라 실제 브라우저 검증까지 통과해야 배포를 성공으로 본다.
성공 기준:
- `dotnet build TaxBaik.sln -c Release` 경고 0, 오류 0
- `dotnet test TaxBaik.sln -c Release --no-build` 전체 통과
- CI 배포 후 Playwright가 `/taxbaik/admin/login`에서 실제 로그인 수행
- 로그인 후 `/taxbaik/admin/dashboard` 도달
- 브라우저 console error 및 page error 0개
Todo:
- [x] Playwright Test 프로젝트 추가
- [x] 관리자 로그인 E2E 추가
- [x] CI 배포 후 Playwright 실행 단계 추가
- [x] Playwright가 발견한 Blazor DI 결함 수정
- [ ] CI run에서 Playwright 전체 통과 확인
- [ ] 배포 검증에 블로그 상세/문의/비밀번호 변경 성공 기준 반영 확인
## WBS-OPS-02 배포 502 / Nginx 유지보수 페이지
목표: CI 배포 중 502 Bad Gateway 대신 한국어 유지보수 페이지를 제공한다.
성공 기준:
- Nginx error_page 502/503 → maintenance.html 직접 서빙
- 배포 중 방문자는 유지보수 페이지(15초 자동 새로고침)를 본다.
- 배포 완료 후 정상 서비스 복구
Todo:
- [x] maintenance.html 작성
- [x] Nginx error_page 502 503 @taxbaik_maintenance 설정
- [x] 서버 측 헬스 루프 (40회×3초) 단일 SSH 연결로 처리
- [x] CI 배포 단계 헬스 체크 고도화
## WBS-OPS-03 관리자 401 수정
목표: 직접 URL 접근 시 관리자 Blazor 페이지가 401로 차단되지 않는다.
성공 기준:
- `/taxbaik/admin/announcements` 등 직접 접근 시 Blazor Shell 200 응답
- 미인증 사용자는 로그인 페이지로 리다이렉트
Todo:
- [x] MapRazorComponents().AllowAnonymous() 적용
- [x] AuthorizeRouteView → RedirectToLogin 인증 흐름 확인
---
## ── 인증 · 관리자 ─────────────────────────────────
## WBS-AUTH-01 인증/비밀번호 운영 안정화
목표: DB 직접 수정 대신 API로 관리자 인증 운영 작업을 수행한다.
성공 기준:
- 비밀번호 변경 API가 현재 비밀번호를 요구한다.
- 비밀번호 재설정 API는 운영 secret 없이는 동작하지 않는다.
- 실패 응답은 민감 정보를 노출하지 않는다.
Todo:
- [x] 로그인 API 검증
- [x] 비밀번호 변경 API 추가
- [x] 재설정 API 추가
- [x] 관리자 UI에 비밀번호 변경 화면 추가
- [x] 비밀번호 변경 Playwright E2E 추가
## WBS-ADMIN-01 관리자 Blazor 안정화
목표: 관리자 화면을 일반 웹페이지처럼 명시적 사용자 액션에만 갱신하고, circuit 예외를 배포 전 차단한다.
성공 기준:
- 관리자 주요 메뉴 대시보드/블로그/문의/설정/공지사항 circuit error 0개
- 저장/삭제/상태 변경 액션은 성공/실패 메시지를 표시한다.
Todo:
- [x] 중복 `/admin` 라우트 제거
- [x] MudBlazor DI 타입 오류 수정
- [x] 관리자 메뉴 smoke E2E 추가
- [x] 설정 저장 TODO를 실제 DB 기반 기능으로 전환
---
## ── 고객지원 백오피스 (CRM) ──────────────────────
> **배경**: 세무사 사무실에서 고객 정보와 상담 이력이 파편화(메모장·카톡·기억)되면 마감 누락, 서비스 연속성 단절, 재계약 기회 손실이 발생한다.
> 30년 경력 세무사가 혼자 또는 소수 인원으로 운영할 때 가장 먼저 필요한 것은 고객 카드와 상담 이력이다.
## WBS-CRM-01 고객 카드 (Client Card) — Phase 1
목표: 고객별 기본 정보·서비스 유형·상태를 한 화면에서 관리한다.
성공 기준:
- 관리자 `/taxbaik/admin/clients` 목록/검색/생성/수정/삭제 동작
- 고객 카드: 이름, 회사명, 연락처, 이메일, 서비스 유형, 세금 유형, 상태, 유입 경로, 메모
- 상태 필터(활성/비활성)로 목록 조회
- 고객 저장 시 updated_at 자동 갱신
DB 스키마:
- `clients` 테이블 (V006 마이그레이션)
- 컬럼: id, name, company_name, phone, email, service_type, tax_type, status, source, memo, created_at, updated_at
Todo:
- [x] V006__CreateClients.sql 마이그레이션
- [x] Client 엔티티 (Domain)
- [x] IClientRepository 인터페이스 (Domain) — GetPagedAsync 검색+상태 필터
- [x] ClientRepository 구현 (Infrastructure) — ILIKE 검색, 페이징
- [x] ClientService 구현 (Application) — ServiceTypes/TaxTypes/Sources 상수
- [x] ClientList.razor 관리자 목록 화면 — 검색바, 상태 필터, 페이징
- [x] ClientEdit.razor 관리자 등록/수정 화면 — 기본/세무/관리 섹션
- [x] MainLayout.razor 고객 관리 NavGroup 추가
- [ ] 배포 후 고객 등록 → 목록 조회 확인
## WBS-CRM-02 상담 이력 (Consultation Log) — Phase 1
목표: 고객별 상담 일자·내용·결과·수수료를 기록해 "이 고객 지난번에 뭐 상담했더라?"를 해결한다.
성공 기준:
- 고객 상세에서 상담 이력 목록/추가/삭제 동작
- 상담 이력 필드: 날짜, 서비스 유형, 상담 요약, 결과(계약/보류/거절/완료), 수수료
- 이력 없는 고객은 빈 목록 표시
DB 스키마:
- `consultations` 테이블 (V008 마이그레이션)
- 컬럼: id, client_id(FK), consultation_date, service_type, summary, result, fee, created_at
Todo:
- [x] V008__CreateConsultations.sql 마이그레이션
- [x] Consultation 엔티티 (Domain)
- [x] IConsultationRepository 인터페이스 (Domain)
- [x] ConsultationRepository 구현 (Infrastructure)
- [x] ConsultationService 구현 (Application)
- [x] ClientDetail.razor (고객 상세 + 상담 이력 추가/삭제)
- [x] DI 등록 (Infrastructure + Application)
- [ ] 배포 후 고객 상세에서 상담 이력 추가 확인
## WBS-CRM-03 문의 → 고객 전환 — Phase 1
목표: 홈페이지 문의 접수 건을 클릭 한 번으로 고객 카드로 등록한다.
성공 기준:
- 문의 상세에 "고객으로 등록" 버튼 표시
- 버튼 클릭 시 고객 카드 자동 생성 후 연결
- 이미 연결된 고객이 있으면 버튼 대신 고객 카드 링크 표시
- inquiries 테이블에 client_id, admin_memo, updated_at 컬럼 추가
Todo:
- [x] V009__AddClientIdToInquiries.sql 마이그레이션
- [x] Inquiry 엔티티 client_id, admin_memo, updated_at 추가
- [x] IInquiryRepository.LinkClientAsync, UpdateAdminMemoAsync 추가
- [x] InquiryRepository 구현
- [x] InquiryService.LinkClientAsync, UpdateAdminMemoAsync 추가
- [x] ClientService.CreateFromInquiryAsync 추가
- [x] InquiryDetail.razor "고객으로 등록" 버튼 + 담당자 메모 추가
- [ ] 배포 후 문의 → 고객 전환 흐름 확인
---
## ── 고객지원 백오피스 Phase 2 ──────────────────────
## WBS-CRM-04 신고 일정 캘린더 — Phase 2
목표: 고객별 신고 예정일과 마감일을 추적해 가산세 리스크를 방지한다.
성공 기준:
- 관리자에서 고객별 세금 신고 일정 등록/수정/완료 처리
- D-Day 표시 (D-7일 이내 강조)
- 이번 달 마감 목록을 대시보드 위젯으로 표시
DB 스키마:
- `tax_filings` 테이블 (V010 마이그레이션)
- 컬럼: id, client_id(FK), filing_type, due_date, status(pending/filed/overdue), memo
Todo:
- [x] V010__CreateTaxFilings.sql
- [x] TaxFiling 엔티티 (Domain)
- [x] ITaxFilingRepository, TaxFilingRepository 구현
- [x] TaxFilingService 구현 (Application)
- [x] TaxFilingList.razor (관리자 신고 일정 화면 + 상태별 탭)
- [x] FilingTable.razor (D-Day 강조, 완료 처리, 삭제)
- [x] Dashboard.razor에 30일 이내 마감 위젯 추가
- [x] MainLayout.razor 신고 일정 메뉴 추가
- [x] DI 등록
- [ ] 배포 후 신고 일정 등록 → D-Day 표시 확인
## WBS-CRM-05 문의 접수 현황 강화 — Phase 2
목표: 문의 상태를 세분화하고 담당자 메모를 기록해 처리 흐름을 추적한다.
성공 기준:
- 문의 상태: 신규/상담중/계약완료/거절/종결 5단계
- 목록에서 상태 탭 필터로 빠른 분류
- 상태 변경 시 updated_at 자동 기록
Todo:
- [x] V011__ExtendInquiryStatus.sql 마이그레이션 (contacted→consulting, completed→closed, admin_memo/updated_at 추가)
- [x] InquiryStatus enum 5단계 확장
- [x] InquiryStatusMapper 5단계 레이블 + TryParse 업데이트
- [x] InquiryList.razor 5단계 탭 (신규/상담중/계약완료/거절/종결)
- [x] InquiryDetail.razor 5단계 상태 버튼 + 색상 구분
- [x] Dashboard.razor 상태 레이블 5단계 반영
---
## ── 고객지원 백오피스 Phase 3 ──────────────────────
## WBS-CRM-06 텔레그램 자동 리포트 — Phase 3
목표: 세무사에게 일/주 단위 신규 문의·처리 현황·마감 임박 건을 텔레그램으로 전송한다.
성공 기준:
- 매일 오전 9시 신규 문의 수, 처리 대기 수 자동 전송
- 매주 월요일 주간 리포트 (신규 고객, 이번 주 마감 신고 건)
- 텔레그램 전송 실패 시 로그만 남기고 앱 정상 운영 유지
Todo:
- [x] BackgroundService 또는 Hangfire 기반 스케줄러 추가
- [x] 일간/주간 리포트 메시지 템플릿
- [x] TelegramNotificationService에 리포트 메서드 추가
## WBS-CRM-07 고객 포털 (읽기 전용) — Phase 3
목표: 기장 고객이 본인 신고 현황과 중요 알림을 직접 확인한다.
성공 기준:
- 고객 전용 URL + 인증(소셜 로그인 또는 링크 토큰)
- 본인 신고 일정, 상담 요약(세무사 허용 항목만) 조회
- 개인정보 열람 범위는 세무사가 허용한 항목만
Todo:
- [x] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행)
- [x] 고객 전용 Razor Pages 추가
- [x] 세무사 허용 권한 설정 UI
## WBS-CRM-08 고객 회원가입 · 소셜 로그인 — Phase 3
목표: 고객 포털 접근을 위한 회원가입과 소셜 로그인을 제공한다.
가입 마찰을 최소화해 상담 접수 → 고객 포털 전환율을 높인다.
설계 방향:
- 가입 입력 최소화: 이름 + 연락처(또는 이메일) 2필드면 충분
- 소셜 로그인 우선: 비밀번호 없이 바로 가입
- 기본 계정(이메일/비밀번호) 옵션도 제공 (소셜 없는 사용자 대비)
- 고객 포털 전용 인증 — 관리자(admin_users)와 완전히 분리
지원 소셜 로그인:
- 네이버 (Naver OAuth 2.0) — 국내 주요 채널
- 카카오 (Kakao Login) — 기존 카카오 채널 연계
- 구글 (Google OAuth 2.0) — 해외·젊은 고객층
성공 기준:
- 소셜 로그인 3종 모두 동작 (네이버·카카오·구글)
- 이메일/비밀번호 기본 계정 가입 + 로그인 동작
- 가입 폼: 이름·연락처 2필드만 요구 (소셜 프로필에서 자동 채우기)
- 로그인 후 고객 포털 (`/taxbaik/portal`) 접근
- 고객 계정이 백오피스 clients 테이블 레코드와 연결
- 회원 계정 미인증 상태에서 포털 접근 시 로그인 페이지 리다이렉트
DB 스키마:
- `portal_users` 테이블 (V011 마이그레이션)
- id, client_id(FK, nullable), email, name, phone, provider(naver/kakao/google/local), provider_id, password_hash(nullable), created_at
- 소셜 로그인 provider_id는 각 플랫폼 식별자
기술 결정:
- ASP.NET Core OAuth Middleware (Microsoft.AspNetCore.Authentication.OAuth)
- 네이버: 커스텀 OAuth handler (공식 패키지 없음, 직접 구현)
- 카카오: AspNet.Security.OAuth.Kakao 패키지
- 구글: Microsoft.AspNetCore.Authentication.Google 패키지
- 고객 포털 세션: HttpOnly Cookie 기반 (JWT localStorage와 분리)
환경 변수 필요 (Gitea Secrets 추가):
- `NAVER_CLIENT_ID` / `NAVER_CLIENT_SECRET`
- `KAKAO_CLIENT_ID` / `KAKAO_CLIENT_SECRET`
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`
Todo:
- [x] WBS-CRM-07 고객 포털 기본 구조 완성 (선행)
- [x] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔)
- [x] V011__CreatePortalUsers.sql 마이그레이션 (실제 V016__CreatePortalUsers.sql로 대체됨)
- [x] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository
- [x] 네이버 OAuth Handler 구현
- [x] 카카오·구글 패키지 추가 및 설정
- [x] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`)
- [x] 소셜 로그인 콜백 처리 → portal_users 자동 생성
- [x] 신규 가입 시 clients 테이블 연결 또는 신규 생성
- [x] 포털 로그인 페이지 (`/taxbaik/portal/login`) — 소셜 버튼 + 이메일 폼
- [ ] Gitea Secrets에 OAuth 키 추가
- [ ] 배포 후 소셜 로그인 3종 E2E 테스트
---
## ── 유지보수성 ─────────────────────────────────
## WBS-MAINT-01 유지보수성/파편화 축소
목표: 문서와 실제 구조의 불일치를 줄이고 단일 앱 운영 기준을 유지한다.
Todo:
- [x] README 테스트/배포 섹션 갱신
- [x] CLAUDE.md E2E 기준 갱신
- [x] 오래된 최종 보고 문서의 허위 완료 표현 정정
- [x] CLAUDE.md 섹션 13 시즌별 마케팅 하네스 추가
---
### 현재 검증 메모
- `dotnet build TaxBaik.sln` 성공 (2026-06-27 기준, 경고 0 오류 0)
- 최종 배포 커밋: `9c96f15` (FAQ 관리 기능)
- WBS-MKT-01/02/03/04 구현 완료, 배포 후 시각 검증 필요
- WBS-UX-03/04 구현 완료
- WBS-CRM-01/02/03/04/05 구현 완료 (배포 후 검증 필요)
- WBS-CRM-06/07/08 (텔레그램·포털·소셜 로그인) Phase 3 미착수
---
## ── 홈페이지 · 어드민 · 포털 프리미엄 UX/UI 개편 (2026-06-30) ──────────────────
## WBS-UX-05 홈페이지 프리미엄 UI 및 마이크로 인터랙션
목표: 홈페이지 디자인을 극도로 모던하고 신뢰성 있는 프리미엄 스타일로 전면 개편한다.
성공 기준:
- Hero 섹션에 유려한 배경 그라데이션 및 부드러운 CSS 애니메이션 효과 적용
- 서비스 카드에 섀도우 및 보더 트랜지션, 골드/그린 그라데이션 호버 이펙트 추가
- 신뢰도 스트립 카드에 입체감 및 돋보이는 레이아웃 설계
- Noto Sans KR 외에 Outfit/Inter 등의 보조 영문 폰트 결합으로 타이포그래피 고급화
Todo:
- [x] `site.css` 내 Hero 섹션 그라데이션 및 CSS 애니메이션 보강
- [x] 서비스 카드 및 신뢰도 스트립 컴포넌트 프리미엄 스타일로 개편
- [x] 홈페이지 폰트 스택 확장 및 메인 레이아웃 적용
## WBS-PORTAL-01 고객 포털 UI/UX 고도화 및 글래스모피즘
목표: 고객 마이 포털 화면을 미려하고 현대적인 글래스모피즘 디자인으로 개편하여 이용 가치를 극대화한다.
성공 기준:
- 포털 메인 대시보드 카드를 Glassmorphism 스타일(blur, semi-transparent border)로 변경
- 세무 신고 현황 테이블 및 상담 이력 타임라인 컴포넌트의 모던 디자인화
Todo:
- [x] `site.css` 내 포털 전용 모던 글래스모피즘 클래스군 추가
- [x] `Portal/Index.cshtml` 레이아웃 및 컴포넌트 UI 고도화
## WBS-MAINT-02 코드 품질 및 경고 결함 차단
목표: 빌드 컴파일 타임 경고(Warnings)를 0으로 유지하여 미래 코드 결함을 방지한다.
성공 기준:
- `dotnet build` 수행 시 경고 0개 달성
Todo:
- [x] `CustomAuthenticationStateProvider.cs` Nullable 경고 수정
- [x] `Dashboard.razor` 미사용 변수 제거 및 UI 연계 바인딩 처리
+281
View File
@@ -0,0 +1,281 @@
# 서버 설정 가이드
## 전제
- 서버: `178.104.200.7` (Ubuntu 26.04)
- 접속: `ssh kjh2064@178.104.200.7`
---
## Step 1: PostgreSQL 데이터베이스 생성 및 마이그레이션
서버에 SSH로 접속한 후 다음을 실행하세요:
```bash
# PostgreSQL에 접속 (로컬 unix socket, sudo 사용)
sudo -i -u postgres
# 그 다음 psql 프롬프트에서:
psql
# SQL 명령어 실행:
CREATE DATABASE taxbaikdb;
CREATE USER taxbaik WITH ENCRYPTED PASSWORD 'your_secure_password_here';
GRANT ALL PRIVILEGES ON DATABASE taxbaikdb TO taxbaik;
\q
exit
# 이제 taxbaik 사용자로 마이그레이션 실행
psql -U taxbaik -d taxbaikdb -h localhost
```
그러면 `taxbaik=#` 프롬프트가 나옵니다. 다음 SQL을 차례로 실행:
### V001__InitialSchema.sql 내용 (프롬프트에 복사):
```sql
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
sort_order INT NOT NULL DEFAULT 0
);
CREATE TABLE admin_users (
id SERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(500) NOT NULL,
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE blog_posts (
id SERIAL PRIMARY KEY,
title VARCHAR(300) NOT NULL,
content TEXT NOT NULL,
slug VARCHAR(300) NOT NULL UNIQUE,
category_id INT REFERENCES categories(id) ON DELETE SET NULL,
tags TEXT,
author_id INT REFERENCES admin_users(id) ON DELETE SET NULL,
published_at TIMESTAMPTZ,
view_count INT NOT NULL DEFAULT 0,
seo_title VARCHAR(300),
seo_description VARCHAR(500),
thumbnail_url VARCHAR(500),
is_published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_blog_slug ON blog_posts(slug);
CREATE INDEX idx_blog_published ON blog_posts(is_published, published_at DESC);
CREATE INDEX idx_blog_category ON blog_posts(category_id);
CREATE TABLE inquiries (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR(200),
service_type VARCHAR(100) NOT NULL,
message TEXT NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'new',
ip_address VARCHAR(50),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_inquiry_status ON inquiries(status);
CREATE INDEX idx_inquiry_created ON inquiries(created_at DESC);
CREATE TABLE site_settings (
key VARCHAR(200) PRIMARY KEY,
value TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO categories (name, slug, sort_order) VALUES
('사업자 세무', 'business-tax', 1),
('부동산 세금', 'real-estate-tax', 2),
('종합소득세', 'income-tax', 3),
('부가가치세', 'vat', 4),
('가족자산·증여', 'family-asset', 5);
INSERT INTO site_settings (key, value) VALUES
('site.title', '백원숙 세무회계 | 성북구 세무사'),
('site.description', '사업자 세무, 부동산 양도세·증여세, 종합소득세 상담. 성북구 백원숙 세무사.'),
('kakao.channel.url', ''),
('phone.main', ''),
('consultation.fee.text','상담료 7만~20만 원, 계약 체결 시 일부 차감');
```
### 확인:
```sql
\dt
-- 6개 테이블 출력 (categories, admin_users, blog_posts, inquiries, site_settings, 그리고 schema_migrations)
SELECT * FROM categories;
-- 5개 카테고리 확인
\q
```
---
## Step 2: systemd 서비스 파일 설치
로컬 컴퓨터에서 다음 파일을 서버에 복사:
```bash
# 로컬에서:
scp deploy/taxbaik.service kjh2064@178.104.200.7:~/
```
서버에서:
```bash
# 파일 복사
sudo cp ~/taxbaik.service /etc/systemd/system/
# 환경 변수 추가 (DB 연결 문자열)
sudo nano /etc/systemd/system/taxbaik.service
# 아래 줄을 [Service] 섹션에서 주석 해제:
# Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=your_password
# systemd 재로드
sudo systemctl daemon-reload
# 서비스 활성화 (부팅 시 자동 시작)
sudo systemctl enable taxbaik
```
---
## Step 3: Nginx 설정 추가
로컬에서 `deploy/nginx-taxbaik-locations.conf` 내용을 복사하여, 서버에서:
```bash
# 현재 Nginx 설정 확인
cat /etc/nginx/sites-available/default
# 또는 기존 Nginx 설정 파일 확인
ls -la /etc/nginx/sites-available/
```
기존 설정 파일의 `http` 또는 `server` 블록에 다음을 **추가**:
```nginx
location /taxbaik {
proxy_pass http://127.0.0.1:5001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
```
설정 검증 및 재로드:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
---
## Step 4: Gitea 저장소 Secrets 추가
1. Gitea 웹UI (`http://178.104.200.7/`) 접속
2. `kjh2064/taxbaik` 저장소 이동
3. **Settings****Secrets****Add Secret**
다음 3개 secrets 추가:
| Secret 이름 | 값 |
|-----------|-----|
| `DEPLOY_USER` | `kjh2064` |
| `DEPLOY_HOST` | `178.104.200.7` |
| `DEPLOY_SSH_KEY` | SSH 개인키 (매우 민감!) |
SSH 개인키는:
```bash
# 로컬에서 (.ssh/id_rsa 또는 현재 사용 중인 키)
cat ~/.ssh/id_rsa
# 전체 내용 복사
```
Gitea Secret 창에 전체 붙여넣기.
---
## Step 5: 배포 디렉토리 생성
서버에서:
```bash
mkdir -p ~/deployments
mkdir -p ~/taxbaik_active
# 권한 확인
ls -la ~/ | grep taxbaik
```
---
## Step 6: 초기 배포 테스트
로컬에서:
```bash
# 솔루션 빌드
dotnet build TaxBaik.sln -c Release
# 배포는 Gitea Actions가 처리
# 수동 publish/rsync 절차는 사용하지 않음
```
---
## Step 7: 확인 체크리스트
```bash
# 로컬에서 또는 서버에서:
# 1. DB 확인
psql -U taxbaik -d taxbaikdb -c "\dt"
# 6개 테이블 출력
# 2. 서비스 상태
sudo systemctl status taxbaik
# 3. Nginx 로그 확인
sudo tail -f /var/log/nginx/error.log
# 4. 앱 로그 확인
journalctl -u taxbaik -n 50
# 5. 엔드포인트 테스트
curl -v http://127.0.0.1:5001/health
curl -v http://127.0.0.1/taxbaik
# 6. 문의 폼 E2E 테스트
curl -X POST http://178.104.200.7/taxbaik/contact \
-d "name=테스트&phone=010-1234-5678&service_type=기장&message=상담요청"
```
---
## 문제 해결
| 증상 | 해결 |
|------|------|
| "Connection refused" | systemd 서비스 시작 안 됨: `sudo systemctl start taxbaik` |
| "Permission denied" | systemd 서비스 파일 권한 확인: `sudo chmod 644 /etc/systemd/system/taxbaik.service` |
| Nginx 404 | 설정 재로드: `sudo nginx -t && sudo systemctl reload nginx` |
| DB 연결 오류 | 환경 변수 확인: `systemctl cat taxbaik \| grep ConnectionStrings` |
---
## 다음 단계
서버 설정 완료 후:
- [ ] Git 저장소에 push: `git push -u origin main`
- [ ] 초기 블로그 포스트 5개 작성 (W6.2)
- [ ] 사이트 설정 입력 (전화번호, KakaoTalk URL)
- [ ] Lighthouse 감사 실행