Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da9f49c973 | |||
| 1839c2c3d1 | |||
| df4c555dd1 | |||
| e1348226c6 | |||
| 97e7cfb867 | |||
| 11772d1f46 | |||
| 84e0577e89 | |||
| 31cc5603c9 | |||
| 0d36d27631 | |||
| 60c31d7ccb | |||
| 42a0d2ae3b | |||
| e599ef9ad8 | |||
| 223d916012 | |||
| f1cc0ca35c | |||
| e1325a1688 | |||
| 29b25cb1b4 | |||
| 8d72d2a0c2 | |||
| 1cdb172b07 | |||
| 864497e56f | |||
| 19c9b9b17a | |||
| 988b166118 | |||
| 78d3990484 | |||
| b3c4ee430d | |||
| 7b27f748de | |||
| abad1630b6 | |||
| 6ffff70ece | |||
| ed8ac34542 | |||
| 6b14ce929e | |||
| e830c08263 | |||
| a1065e8233 | |||
| 7cdb0bf8e9 | |||
| 8bea85df96 | |||
| 127490906b | |||
| ada05e254d | |||
| 7602f5be59 | |||
| 777cdcd918 | |||
| 0f6ba33af3 | |||
| 6d263c20bf | |||
| c9bf4f4f6f | |||
| b12d2ae0c6 | |||
| f9cbafdb3d | |||
| 64de7d2304 | |||
| 1f628b49a8 | |||
| a4a2499c7d | |||
| 6b11b64135 | |||
| a60451b95f | |||
| 2a046d0393 | |||
| 62ce89359a | |||
| 32c5a3d042 | |||
| 68291867f9 | |||
| d24f3f58db | |||
| 71cd2c1129 | |||
| 24ecf89028 | |||
| ff6651c4f2 | |||
| f892b85b7e | |||
| 62a7b2f2ef | |||
| 184ff2259b | |||
| 163812e964 | |||
| ba158f9824 | |||
| b2477d977b | |||
| 80c97fba96 | |||
| 1fb3a3c329 | |||
| abd7bbf016 | |||
| c765db37b3 | |||
| 967a784d6e | |||
| 03809bbf26 | |||
| c626c164f8 | |||
| 15f5dcf4ea | |||
| a84f842490 | |||
| 8999e51d4e | |||
| f98405b791 | |||
| ee964457d9 |
@@ -49,12 +49,13 @@ jobs:
|
|||||||
# Suppress stderr and allow failures to handle transition/down periods cleanly
|
# Suppress stderr and allow failures to handle transition/down periods cleanly
|
||||||
VERSION_BODY="$(curl -fsS "http://${DEPLOY_HOST}/taxbaik/version.json" 2>/dev/null || true)"
|
VERSION_BODY="$(curl -fsS "http://${DEPLOY_HOST}/taxbaik/version.json" 2>/dev/null || true)"
|
||||||
BLOG_STATUS="$(curl -s -o /dev/null -w '%{http_code}' "http://${DEPLOY_HOST}/taxbaik/blog/accountant-mistakes-5" || true)"
|
BLOG_STATUS="$(curl -s -o /dev/null -w '%{http_code}' "http://${DEPLOY_HOST}/taxbaik/blog/accountant-mistakes-5" || true)"
|
||||||
if echo "$VERSION_BODY" | grep -q "\"version\": \"${SHORT_VERSION}\"" && [ "$BLOG_STATUS" = "200" ]; then
|
LOGIN_STATUS="$(curl -s -o /dev/null -w '%{http_code}' "http://${DEPLOY_HOST}/taxbaik/admin/login" || true)"
|
||||||
|
if echo "$VERSION_BODY" | grep -q "\"version\": \"${SHORT_VERSION}\"" && [ "$BLOG_STATUS" = "200" ] && [ "$LOGIN_STATUS" = "200" ]; then
|
||||||
echo "✓ Deployment ready for ${SHORT_VERSION} (attempt $i/20)"
|
echo "✓ Deployment ready for ${SHORT_VERSION} (attempt $i/20)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
if [ $i -lt 20 ]; then
|
if [ $i -lt 20 ]; then
|
||||||
echo " Attempt $i/20: waiting for deployment... (blog=${BLOG_STATUS:-?}, version=${VERSION_BODY:0:30}...)"
|
echo " Attempt $i/20: waiting for deployment... (blog=${BLOG_STATUS:-?}, login=${LOGIN_STATUS:-?}, version=${VERSION_BODY:0:30}...)"
|
||||||
sleep 3
|
sleep 3
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -72,6 +73,23 @@ jobs:
|
|||||||
echo "Running E2E tests on Desktop Chrome (production verification)"
|
echo "Running E2E tests on Desktop Chrome (production verification)"
|
||||||
npx playwright test --project="Desktop Chrome" --reporter=html --reporter=list
|
npx playwright test --project="Desktop Chrome" --reporter=html --reporter=list
|
||||||
|
|
||||||
|
- name: API smoke verification
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
E2E_ADMIN_USERNAME: test_admin
|
||||||
|
E2E_ADMIN_PASSWORD: TestAdmin@123456
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
TOKEN="$(curl -s -X POST "http://${DEPLOY_HOST}/taxbaik/api/auth/login" -H "Content-Type: application/json" -d "{\"username\":\"${E2E_ADMIN_USERNAME}\",\"password\":\"${E2E_ADMIN_PASSWORD}\"}" | python3 -c 'import sys, json; print(json.load(sys.stdin)["accessToken"])')"
|
||||||
|
test -n "$TOKEN"
|
||||||
|
curl -fsS -H "Authorization: Bearer $TOKEN" "http://${DEPLOY_HOST}/taxbaik/api/blog/admin?page=1&pageSize=1" >/dev/null
|
||||||
|
curl -fsS -H "Authorization: Bearer $TOKEN" "http://${DEPLOY_HOST}/taxbaik/api/faq" >/dev/null
|
||||||
|
curl -fsS -H "Authorization: Bearer $TOKEN" "http://${DEPLOY_HOST}/taxbaik/api/announcement" >/dev/null
|
||||||
|
curl -fsS -H "Authorization: Bearer $TOKEN" "http://${DEPLOY_HOST}/taxbaik/api/inquiry?page=1&pageSize=1" >/dev/null
|
||||||
|
curl -fsS "http://${DEPLOY_HOST}/taxbaik/favicon.svg" >/dev/null
|
||||||
|
curl -fsS "http://${DEPLOY_HOST}/taxbaik/favicon.ico" >/dev/null
|
||||||
|
curl -fsS "http://${DEPLOY_HOST}/taxbaik/robots.txt" >/dev/null
|
||||||
|
|
||||||
- name: Browser E2E summary
|
- name: Browser E2E summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ jobs:
|
|||||||
- name: Publish Web
|
- name: Publish Web
|
||||||
run: dotnet publish TaxBaik.Web/ -c Release -o ./publish --no-restore
|
run: dotnet publish TaxBaik.Web/ -c Release -o ./publish --no-restore
|
||||||
|
|
||||||
|
- name: Publish Proxy
|
||||||
|
run: dotnet publish TaxBaik.Proxy/ -c Release -o ./publish/proxy
|
||||||
|
|
||||||
- name: Write production secrets
|
- name: Write production secrets
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
@@ -67,8 +70,13 @@ jobs:
|
|||||||
)'
|
)'
|
||||||
test -s ./publish/appsettings.Production.json || { echo "appsettings.Production.json is empty" >&2; exit 1; }
|
test -s ./publish/appsettings.Production.json || { echo "appsettings.Production.json is empty" >&2; exit 1; }
|
||||||
|
|
||||||
|
- name: Verify proxy artifact
|
||||||
|
run: |
|
||||||
|
test -s ./publish/proxy/TaxBaik.Proxy.dll || { echo "TaxBaik.Proxy.dll missing" >&2; exit 1; }
|
||||||
|
test -s ./publish/proxy/TaxBaik.Proxy.runtimeconfig.json || { echo "TaxBaik.Proxy.runtimeconfig.json missing" >&2; exit 1; }
|
||||||
|
|
||||||
- name: Copy migrations
|
- name: Copy migrations
|
||||||
run: cp -r db/migrations ./publish/migrations || true
|
run: mkdir -p ./publish/db && cp -r db/migrations ./publish/db/ || true
|
||||||
|
|
||||||
- name: Generate build info
|
- name: Generate build info
|
||||||
run: |
|
run: |
|
||||||
@@ -100,12 +108,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Package artifact
|
- name: Package artifact
|
||||||
run: |
|
run: |
|
||||||
|
cp deploy_gb.sh ./publish/deploy_gb.sh
|
||||||
tar -czf taxbaik_deploy.tgz -C ./publish .
|
tar -czf taxbaik_deploy.tgz -C ./publish .
|
||||||
echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)"
|
echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)"
|
||||||
|
|
||||||
- name: Deploy & verify on server
|
- name: Deploy & verify on server
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
export TAXBAIK_DEPLOY_FROM_CI=1
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
COMMIT=$(git rev-parse --short HEAD)
|
COMMIT=$(git rev-parse --short HEAD)
|
||||||
DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
|
DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
|
||||||
@@ -148,7 +158,7 @@ jobs:
|
|||||||
# 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리, Green-Blue 지원)
|
# 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리, Green-Blue 지원)
|
||||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||||
-o ServerAliveInterval=10 \
|
-o ServerAliveInterval=10 \
|
||||||
"$DEPLOY_USER@$DEPLOY_HOST" bash << REMOTE
|
"$DEPLOY_USER@$DEPLOY_HOST" TAXBAIK_DEPLOY_FROM_CI=1 bash << REMOTE
|
||||||
set -e
|
set -e
|
||||||
DEPLOY_HOME="/home/kjh2064"
|
DEPLOY_HOME="/home/kjh2064"
|
||||||
DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
||||||
@@ -162,12 +172,12 @@ jobs:
|
|||||||
echo "--- [2/5] 운영 설정 검증 ---"
|
echo "--- [2/5] 운영 설정 검증 ---"
|
||||||
test -s "\$DEPLOY_DIR/appsettings.Production.json" \
|
test -s "\$DEPLOY_DIR/appsettings.Production.json" \
|
||||||
|| { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; }
|
|| { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; }
|
||||||
|
test -s "\$DEPLOY_DIR/proxy/TaxBaik.Proxy.dll" \
|
||||||
|
|| { echo "FATAL: TaxBaik.Proxy.dll 없음" >&2; exit 1; }
|
||||||
|
|
||||||
echo "--- [3/5] 심볼릭 링크 전환 ---"
|
echo "--- [3/4] Green-Blue 배포 실행 ---"
|
||||||
ln -sfn "\$DEPLOY_DIR" "\$DEPLOY_HOME/taxbaik_active"
|
chmod +x "\$DEPLOY_DIR/deploy_gb.sh"
|
||||||
|
"\$DEPLOY_DIR/deploy_gb.sh" "\$DEPLOY_DIR"
|
||||||
echo "--- [4/5] 서비스 재시작 ---"
|
|
||||||
sudo /usr/bin/systemctl restart taxbaik
|
|
||||||
|
|
||||||
echo "--- [5/5] 헬스 체크 (최대 60초) ---"
|
echo "--- [5/5] 헬스 체크 (최대 60초) ---"
|
||||||
ATTEMPTS=20
|
ATTEMPTS=20
|
||||||
@@ -191,13 +201,20 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "✓ [3/4] 버전 정보 확인 완료"
|
echo "✓ [3/4] 버전 정보 확인 완료"
|
||||||
|
|
||||||
# 검증 3: 관리자 로그인 페이지
|
# 검증 4: 5001 프록시 확인
|
||||||
|
if ! ss -tlnp | grep -q ':5001 '; then
|
||||||
|
echo "❌ 5001 프록시가 실행 중이 아님" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ [4/5] 5001 프록시 확인 완료"
|
||||||
|
|
||||||
|
# 검증 5: 관리자 로그인 페이지
|
||||||
LOGIN_STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login 2>/dev/null || echo "000")
|
LOGIN_STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login 2>/dev/null || echo "000")
|
||||||
if [ "\$LOGIN_STATUS" != "200" ]; then
|
if [ "\$LOGIN_STATUS" != "200" ]; then
|
||||||
echo "❌ 관리자 로그인 페이지 로드 실패 (상태: \$LOGIN_STATUS)" >&2
|
echo "❌ 관리자 로그인 페이지 로드 실패 (상태: \$LOGIN_STATUS)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✓ [4/4] 관리자 페이지 로드 완료"
|
echo "✓ [5/5] 관리자 페이지 로드 완료"
|
||||||
|
|
||||||
echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)"
|
echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)"
|
||||||
# 구 배포 디렉토리 정리 (최근 5개 보존)
|
# 구 배포 디렉토리 정리 (최근 5개 보존)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
-- 결과 없음이 정상!
|
||||||
|
```
|
||||||
@@ -564,33 +564,24 @@ ssh kjh2064@178.104.200.7
|
|||||||
|
|
||||||
배포는 수동 실행이 아니라 **Gitea Actions CI/CD**만 사용한다.
|
배포는 수동 실행이 아니라 **Gitea Actions CI/CD**만 사용한다.
|
||||||
|
|
||||||
**표준 배포 (현재)**:
|
**무중단 Green-Blue 배포 아키텍처 (2026-06-30 적용 완료)**:
|
||||||
1. `master` 브랜치에 push
|
1. **프록시 레이어**: 포트 `5001`에서 영구 가동되는 초경량 .NET TCP 프록시([TaxBaik.Proxy])가 수신 대기합니다. Nginx는 `/taxbaik` 트래픽을 기존과 같이 `5001`로 중계합니다.
|
||||||
2. Gitea Actions가 `TaxBaik.Web`을 build/publish
|
2. **동적 포트 스위칭**: 프록시는 요청이 들어올 때마다 `/home/kjh2064/taxbaik_port` 파일을 읽어 active 포트(5003 또는 5004)를 판단하고 트래픽을 포워딩합니다.
|
||||||
3. CI가 서버의 `taxbaik` 서비스와 `~/taxbaik_active`를 갱신
|
3. **배포 흐름 (`deploy_gb.sh`)**:
|
||||||
4. CI가 서비스 재시작 후 `/taxbaik/admin/login`으로 헬스 체크
|
- Gitea Actions가 코드를 build/publish 후 압축하여 서버에 업로드합니다.
|
||||||
|
- 서버의 배포 스크립트([deploy_gb.sh])가 실행되어 현재 미사용 중인 예비 포트(Target Port: 5003 또는 5004)를 파악합니다.
|
||||||
**API 클라이언트 설정 (Green-Blue 대비)**:
|
- 예비 포트에서 새 .NET 웹 앱을 실행하고 `http://127.0.0.1:$target_port/taxbaik/healthz` 헬스 체크를 통과할 때까지 폴링(최대 60초)합니다.
|
||||||
- API 클라이언트 Base URL이 이제 동적 설정됨: `appsettings.json` > `ApiClient:BaseUrl`
|
- 헬스 체크 성공 시 `/home/kjh2064/taxbaik_port` 파일에 새 포트 번호를 기입하여 **트래픽을 즉시 무중단 전환**합니다.
|
||||||
- 기본값: `http://localhost:5001/taxbaik/api/`
|
- 기존 포트에서 동작하던 구버전 .NET 프로세스를 종료(`kill -15`)합니다.
|
||||||
- 배포 시 환경변수로 오버라이드 가능:
|
- 만약 헬스 체크 실패 시 새 프로세스만 강제 종료하고 배포를 롤백하여 실서비스 다운타임을 방지합니다.
|
||||||
```bash
|
|
||||||
export ApiClient__BaseUrl="http://localhost:5002/taxbaik/api/"
|
|
||||||
systemctl start taxbaik # 새 포트에 배포
|
|
||||||
```
|
|
||||||
- Nginx가 `/taxbaik` → active 포트로 라우팅하면 자동 전환됨
|
|
||||||
|
|
||||||
**운영 규칙**:
|
**운영 규칙**:
|
||||||
- 로컬 또는 서버에서 수동 `dotnet publish`로 운영 배포하지 않는다
|
- 로컬 또는 서버에서 수동 `dotnet publish`로 운영 배포하지 않는다.
|
||||||
- `rsync`로 직접 아티팩트를 올리지 않는다
|
- 배포 실패 시 Gitea Actions CI/CD 로그 및 `~/deployments/taxbaik_timestamp/web_*.log`를 먼저 확인한다.
|
||||||
- 배포 실패 시 CI 로그를 먼저 본다
|
- 배포 후 최종 검증은 프록시 포트를 경유하는 메인 홈페이지, 관리자 로그인 페이지, 로그인 API를 모두 포함한다.
|
||||||
- 배포된 아티팩트는 CI가 만든 것만 신뢰한다
|
|
||||||
- 배포 후 검증은 홈, 관리자 로그인 페이지, 로그인 API를 모두 포함한다
|
|
||||||
|
|
||||||
**롤백**:
|
**롤백**:
|
||||||
- 이전 정상 커밋을 `master`에 revert 또는 hotfix로 되돌린다
|
- 이전 정상 커밋을 `master`에 revert 또는 hotfix로 되돌려 다시 배포를 수행하거나, 비상시 서버의 `taxbaik_port` 파일의 포트 번호를 수동 수정하여 이전 버전 포트로 즉시 원상복구한다.
|
||||||
- 서버 파일을 수동으로 복구하지 않는다
|
|
||||||
- 롤백은 커밋 단위로 추적 가능해야 한다
|
|
||||||
|
|
||||||
### 3.4 서비스 파일 위치
|
### 3.4 서비스 파일 위치
|
||||||
```
|
```
|
||||||
@@ -754,6 +745,22 @@ ssh kjh2064@178.104.200.7 crontab -l | grep backup
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 5-1. 블로그 & FAQ 콘텐츠 작성 규칙
|
||||||
|
|
||||||
|
**핵심**: 고객 임파워먼트 (당신도 할 수 있습니다!)
|
||||||
|
- ✅ 주변에서 흔히 보는 실제 사례 (이름, 나이, 직업 구체화)
|
||||||
|
- ✅ 절세 효과 수치화 ("세금을 X만 원 절약했습니다")
|
||||||
|
- ✅ 중학교 2학년도 이해 가능한 수준
|
||||||
|
- ✅ 단계별 설명 + 표로 시각화
|
||||||
|
- ✅ 결론: "정확하게 하면 이런 이점이 있습니다" (임파워먼트)
|
||||||
|
|
||||||
|
**피해야 할 톤**: "복잡하니까 맡기세요" (세무사 의존성 강화)
|
||||||
|
**세무사 언급**: "더 복잡하면 전문가와 상담하세요" (선택지)
|
||||||
|
|
||||||
|
**자세한 템플릿 및 체크리스트**: `BLOG_TEMPLATE.md` 참고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. 코드 규칙
|
## 6. 코드 규칙
|
||||||
|
|
||||||
### 6.1 C# 네이밍
|
### 6.1 C# 네이밍
|
||||||
@@ -1637,7 +1644,7 @@ curl http://127.0.0.1/taxbaik/admin/login
|
|||||||
### E2E 테스트 & 반응형 검증
|
### E2E 테스트 & 반응형 검증
|
||||||
```bash
|
```bash
|
||||||
# 문의 폼 제출
|
# 문의 폼 제출
|
||||||
curl -X POST http://178.104.200.7/taxbaik/contact \
|
curl -X POST http://taxbaik.com/taxbaik/contact \
|
||||||
-d "name=테스트&phone=010-1234-5678&service_type=사업자세무&message=테스트"
|
-d "name=테스트&phone=010-1234-5678&service_type=사업자세무&message=테스트"
|
||||||
|
|
||||||
# 관리자 DB에서 확인
|
# 관리자 DB에서 확인
|
||||||
@@ -1676,7 +1683,7 @@ npx playwright test admin-responsive.spec.ts --project="Desktop Chrome"
|
|||||||
|
|
||||||
**프로덕션 E2E 테스트**:
|
**프로덕션 E2E 테스트**:
|
||||||
```bash
|
```bash
|
||||||
export E2E_BASE_URL="http://178.104.200.7/taxbaik"
|
export E2E_BASE_URL="http://taxbaik.com/taxbaik"
|
||||||
export E2E_ADMIN_USERNAME="test_admin"
|
export E2E_ADMIN_USERNAME="test_admin"
|
||||||
export E2E_ADMIN_PASSWORD="TestAdmin@123456"
|
export E2E_ADMIN_PASSWORD="TestAdmin@123456"
|
||||||
|
|
||||||
@@ -1944,7 +1951,7 @@ else
|
|||||||
2. **Actions run 생성 확인**
|
2. **Actions run 생성 확인**
|
||||||
```powershell
|
```powershell
|
||||||
$headers = @{ Authorization = "token $env:GITEA_TOKEN_TAXBAIK" }
|
$headers = @{ Authorization = "token $env:GITEA_TOKEN_TAXBAIK" }
|
||||||
$runs = Invoke-RestMethod -Headers $headers -Uri "http://178.104.200.7/api/v1/repos/kjh2064/taxbaik/actions/runs?limit=10"
|
$runs = Invoke-RestMethod -Headers $headers -Uri "http://gitea.taxbaik.com/api/v1/repos/kjh2064/taxbaik/actions/runs?limit=10"
|
||||||
$runs.workflow_runs | Select-Object id,path,event,head_sha,display_title,status,conclusion
|
$runs.workflow_runs | Select-Object id,path,event,head_sha,display_title,status,conclusion
|
||||||
```
|
```
|
||||||
`deploy.yml@refs/heads/master`, `event=push`, 최신 `head_sha`가 있어야 배포가 실제로 시작된 것이다.
|
`deploy.yml@refs/heads/master`, `event=push`, 최신 `head_sha`가 있어야 배포가 실제로 시작된 것이다.
|
||||||
|
|||||||
+120
-13
@@ -17,7 +17,7 @@
|
|||||||
| 3.3 | [주요 Python 패키지](#33-주요-python-패키지-시스템) | 시스템/venv 패키지 구분 |
|
| 3.3 | [주요 Python 패키지](#33-주요-python-패키지-시스템) | 시스템/venv 패키지 구분 |
|
||||||
| 4 | [서비스 아키텍처](#4-서비스-아키텍처) | 포트 맵, Nginx 리버스 프록시 |
|
| 4 | [서비스 아키텍처](#4-서비스-아키텍처) | 포트 맵, Nginx 리버스 프록시 |
|
||||||
| 4.1 | [포트 맵](#41-포트-맵) | 22, 80, 2222, 3000, 5000, 5432 |
|
| 4.1 | [포트 맵](#41-포트-맵) | 22, 80, 2222, 3000, 5000, 5432 |
|
||||||
| 4.2 | [Nginx 리버스 프록시](#42-nginx-리버스-프록시) | `/` → Gitea, `/quant/` → Blazor |
|
| 4.2 | [Nginx 리버스 프록시](#42-nginx-리버스-프록시) | 도메인 기반 가상 호스트 분기 (홈페이지, Gitea, Quant) |
|
||||||
| 5 | [Gitea](#5-gitea) | Docker Compose 설정, 시크릿, 데이터 경로 |
|
| 5 | [Gitea](#5-gitea) | Docker Compose 설정, 시크릿, 데이터 경로 |
|
||||||
| 5.1 | [Docker Compose](#51-docker-compose) | `gitea:1.26.4`, PG 연동 |
|
| 5.1 | [Docker Compose](#51-docker-compose) | `gitea:1.26.4`, PG 연동 |
|
||||||
| 5.2 | [시크릿 관리](#52-시크릿-관리) | `/opt/stacks/gitea/.env` |
|
| 5.2 | [시크릿 관리](#52-시크릿-관리) | `/opt/stacks/gitea/.env` |
|
||||||
@@ -126,17 +126,22 @@ boto3, cryptography, Jinja2, jsonschema, fail2ban 등 시스템 레벨로 설치
|
|||||||
### 4.2. Nginx 리버스 프록시
|
### 4.2. Nginx 리버스 프록시
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
# /etc/nginx/sites-enabled/gitea-ip.conf
|
# /etc/nginx/sites-available/taxbaik-domains.conf
|
||||||
|
|
||||||
|
# 1. TaxBaik 홈페이지 (taxbaik.com, www.taxbaik.com)
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
server_name taxbaik.com www.taxbaik.com;
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
client_max_body_size 512M;
|
client_max_body_size 512M;
|
||||||
|
|
||||||
# QuantEngine Blazor Web App
|
|
||||||
location /quant/ {
|
# /admin 하위 요청을 /taxbaik/admin 으로 리다이렉트하여 Blazor Base Path 대응
|
||||||
proxy_pass http://127.0.0.1:5000/;
|
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_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "Upgrade";
|
proxy_set_header Connection "Upgrade";
|
||||||
@@ -147,7 +152,33 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Gitea (기본)
|
# /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 / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -159,13 +190,89 @@ server {
|
|||||||
proxy_connect_timeout 300;
|
proxy_connect_timeout 300;
|
||||||
proxy_send_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://178.104.200.7/` → Gitea Web UI
|
- `http://taxbaik.com/` 또는 `http://www.taxbaik.com/` → TaxBaik 홈페이지 (내부 proxy: `http://127.0.0.1:5001/taxbaik/`)
|
||||||
- `http://178.104.200.7/quant/` → QuantEngine Blazor Admin
|
- `http://gitea.taxbaik.com/` → Gitea Web UI (내부 proxy: `http://127.0.0.1:3000`)
|
||||||
- `ssh://178.104.200.7:2222` → Gitea Git SSH
|
- `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. Gitea
|
||||||
|
|
||||||
@@ -384,7 +491,7 @@ ClientAliveCountMax 2
|
|||||||
| **CI Runner** | Synology Act Runner | 6× `act_runner:latest` (Docker) |
|
| **CI Runner** | Synology Act Runner | 6× `act_runner:latest` (Docker) |
|
||||||
| **DB** | SQLite (파일 기반) | PostgreSQL 18 + SQLite (하이브리드) |
|
| **DB** | SQLite (파일 기반) | PostgreSQL 18 + SQLite (하이브리드) |
|
||||||
| **웹 Admin** | 없음 | QuantEngine Blazor (.NET 10, MudBlazor) |
|
| **웹 Admin** | 없음 | QuantEngine Blazor (.NET 10, MudBlazor) |
|
||||||
| **리버스 프록시** | Synology 내장 | Nginx (`/` → Gitea, `/quant/` → Blazor) |
|
| **리버스 프록시** | Synology 내장 | Nginx (도메인 기반 분기 - 홈페이지, Gitea, Quant) |
|
||||||
| **보안** | DSM 방화벽 | fail2ban + SSH 공개키 + 서비스 로컬바인드 |
|
| **보안** | DSM 방화벽 | fail2ban + SSH 공개키 + 서비스 로컬바인드 |
|
||||||
| **시크릿 관리** | `.secrets/kis_real.env` | `/opt/stacks/gitea/.env` |
|
| **시크릿 관리** | `.secrets/kis_real.env` | `/opt/stacks/gitea/.env` |
|
||||||
| **OS** | Synology DSM 7.x | Ubuntu 26.04 LTS |
|
| **OS** | Synology DSM 7.x | Ubuntu 26.04 LTS |
|
||||||
|
|||||||
+44
-11
@@ -19,32 +19,46 @@ GRANT ALL PRIVILEGES ON DATABASE taxbaikdb TO taxbaik;
|
|||||||
|
|
||||||
### 2. 환경 변수 설정
|
### 2. 환경 변수 설정
|
||||||
|
|
||||||
**Web 서비스** (`/etc/systemd/system/taxbaik.service`):
|
**Web 서비스** (`/etc/systemd/system/taxbaik.service`, 백엔드 전용):
|
||||||
```ini
|
```ini
|
||||||
[Service]
|
[Service]
|
||||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||||
Environment=ASPNETCORE_URLS=http://127.0.0.1:5001
|
Environment=ASPNETCORE_URLS=http://127.0.0.1:5004
|
||||||
Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=your_secure_password
|
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 서비스 파일 설치
|
### 3. systemd 서비스 파일 설치
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp deploy/taxbaik.service /etc/systemd/system/
|
sudo cp deploy/taxbaik.service /etc/systemd/system/
|
||||||
|
sudo cp deploy/taxbaik-proxy.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable taxbaik
|
sudo systemctl enable taxbaik
|
||||||
|
sudo systemctl enable taxbaik-proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Nginx 설정
|
### 4. Nginx 설정
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 현재 Nginx 설정 확인
|
# Nginx 도메인 기반 가상 호스트 설정 복사
|
||||||
sudo cat /etc/nginx/sites-available/default | head -30
|
sudo cp deploy/nginx-taxbaik-domains.conf /etc/nginx/sites-available/taxbaik-domains.conf
|
||||||
|
|
||||||
# location 블록 추가 (또는 기존 설정에 병합)
|
# 기존 설정(IP 기반 및 default) 활성화 해제
|
||||||
sudo cp deploy/nginx-taxbaik-locations.conf /etc/nginx/conf.d/taxbaik.conf
|
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 nginx -t
|
||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
@@ -65,7 +79,7 @@ sudo systemctl reload nginx
|
|||||||
master 브랜치 push → build → test → publish → restart → health check → Playwright
|
master 브랜치 push → build → test → publish → restart → health check → Playwright
|
||||||
```
|
```
|
||||||
|
|
||||||
수동 배포는 비상 롤백 외에는 사용하지 않습니다. 배포 이슈는 Gitea Actions 로그로 해결합니다.
|
수동 배포는 사용하지 않습니다. `deploy_gb.sh`는 `TAXBAIK_DEPLOY_FROM_CI=1`이 없으면 즉시 종료하므로, 배포는 반드시 Gitea Actions에서만 실행됩니다.
|
||||||
|
|
||||||
## 마이그레이션 자동 실행
|
## 마이그레이션 자동 실행
|
||||||
|
|
||||||
@@ -128,6 +142,7 @@ ls -la ~/deployments/ | grep taxbaik
|
|||||||
|
|
||||||
# 심링크 변경 (예: 이전 버전이 taxbaik_20260626_140000)
|
# 심링크 변경 (예: 이전 버전이 taxbaik_20260626_140000)
|
||||||
ln -sfn ~/deployments/taxbaik_20260626_140000 ~/taxbaik_active
|
ln -sfn ~/deployments/taxbaik_20260626_140000 ~/taxbaik_active
|
||||||
|
sudo systemctl restart taxbaik-proxy
|
||||||
sudo systemctl restart taxbaik
|
sudo systemctl restart taxbaik
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -139,10 +154,10 @@ sudo systemctl restart taxbaik
|
|||||||
ssh kjh2064@178.104.200.7
|
ssh kjh2064@178.104.200.7
|
||||||
|
|
||||||
# 서비스 상태
|
# 서비스 상태
|
||||||
systemctl status taxbaik
|
systemctl status taxbaik taxbaik-proxy
|
||||||
|
|
||||||
# 포트 확인
|
# 포트 확인
|
||||||
netstat -tlnp | grep -E '5001'
|
netstat -tlnp | grep -E '5001|5004'
|
||||||
|
|
||||||
# 프로세스 확인
|
# 프로세스 확인
|
||||||
ps aux | grep TaxBaik
|
ps aux | grep TaxBaik
|
||||||
@@ -165,9 +180,27 @@ journalctl -u taxbaik -f
|
|||||||
| 404 /taxbaik | Nginx 설정 미적용 | `sudo nginx -t && sudo systemctl reload nginx` |
|
| 404 /taxbaik | Nginx 설정 미적용 | `sudo nginx -t && sudo systemctl reload nginx` |
|
||||||
| Blazor WebSocket 안 됨 | `/taxbaik` location에 `proxy_http_version 1.1`, `Upgrade`, `Connection \"Upgrade\"` 헤더가 모두 있는지 확인 |
|
| Blazor WebSocket 안 됨 | `/taxbaik` location에 `proxy_http_version 1.1`, `Upgrade`, `Connection \"Upgrade\"` 헤더가 모두 있는지 확인 |
|
||||||
| DB 연결 오류 | 환경 변수 미설정 | systemd service 파일의 ConnectionStrings__Default 확인 |
|
| DB 연결 오류 | 환경 변수 미설정 | systemd service 파일의 ConnectionStrings__Default 확인 |
|
||||||
| 503 Service Unavailable | 앱 미시작 | `sudo systemctl restart taxbaik` |
|
| 503 Service Unavailable | 백엔드 또는 프록시 미시작 | `sudo systemctl restart taxbaik-proxy taxbaik` |
|
||||||
| 마이그레이션 실패 | DB 권한 문제 | `GRANT ALL PRIVILEGES ON DATABASE taxbaikdb TO 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'
|
||||||
|
```
|
||||||
|
|
||||||
## 초기 데이터
|
## 초기 데이터
|
||||||
|
|
||||||
### 관리자 계정
|
### 관리자 계정
|
||||||
|
|||||||
+8
-40
@@ -48,29 +48,7 @@ ssh kjh2064@178.104.200.7 'bash ~/SERVER_SETUP.sh'
|
|||||||
# ~/taxbaik_active
|
# ~/taxbaik_active
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2단계: 첫 배포 (수동)
|
### 2단계: Gitea Actions 설정
|
||||||
|
|
||||||
```bash
|
|
||||||
# 로컬에서 실행
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
|
|
||||||
# SSH 키 설정 (필요시)
|
|
||||||
export DEPLOY_USER="kjh2064"
|
|
||||||
export DEPLOY_HOST="178.104.200.7"
|
|
||||||
|
|
||||||
# 배포
|
|
||||||
rsync -avz --delete ./publish/ \
|
|
||||||
$DEPLOY_USER@$DEPLOY_HOST:~/deployments/taxbaik_${TIMESTAMP}/
|
|
||||||
|
|
||||||
# 심링크 변경 및 시작
|
|
||||||
ssh $DEPLOY_USER@$DEPLOY_HOST << EOF
|
|
||||||
ln -sfn ~/deployments/taxbaik_${TIMESTAMP} ~/taxbaik_active
|
|
||||||
sudo systemctl start taxbaik
|
|
||||||
sudo systemctl status taxbaik
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3단계: Gitea Actions 설정 (선택)
|
|
||||||
|
|
||||||
**Gitea 저장소 Settings → Secrets 추가**:
|
**Gitea 저장소 Settings → Secrets 추가**:
|
||||||
- `DEPLOY_USER`: `kjh2064`
|
- `DEPLOY_USER`: `kjh2064`
|
||||||
@@ -217,8 +195,8 @@ curl -I -H "Accept-Encoding: gzip" http://178.104.200.7/taxbaik/ | grep -i encod
|
|||||||
| 증상 | 원인 | 해결 방법 |
|
| 증상 | 원인 | 해결 방법 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| 404 /taxbaik | Nginx 설정 미적용 | `sudo nginx -t && sudo systemctl reload nginx` |
|
| 404 /taxbaik | Nginx 설정 미적용 | `sudo nginx -t && sudo systemctl reload nginx` |
|
||||||
| 502 Bad Gateway | 앱 미실행 | `sudo systemctl restart taxbaik` |
|
| 502 Bad Gateway | 프록시 또는 백엔드 미실행 | `sudo systemctl restart taxbaik-proxy taxbaik` |
|
||||||
| 503 Service Unavailable | 앱 충돌 | 로그 확인: `journalctl -u taxbaik -n 50` |
|
| 503 Service Unavailable | 백엔드 충돌 또는 비밀값 누락 | 로그 확인: `journalctl -u taxbaik -n 50` |
|
||||||
| DB 연결 오류 | 환경 변수 미설정 | systemd 파일의 ConnectionStrings__Default 확인 |
|
| DB 연결 오류 | 환경 변수 미설정 | systemd 파일의 ConnectionStrings__Default 확인 |
|
||||||
| HTTPS 오류 | SSL 미구성 | 개발 환경에서는 HTTP 사용 (IP 기반) |
|
| HTTPS 오류 | SSL 미구성 | 개발 환경에서는 HTTP 사용 (IP 기반) |
|
||||||
| 마이그레이션 실패 | 테이블 존재 | `DROP DATABASE taxbaikdb;` 후 재시작 |
|
| 마이그레이션 실패 | 테이블 존재 | `DROP DATABASE taxbaikdb;` 후 재시작 |
|
||||||
@@ -230,11 +208,11 @@ curl -I -H "Accept-Encoding: gzip" http://178.104.200.7/taxbaik/ | grep -i encod
|
|||||||
### 실시간 모니터링
|
### 실시간 모니터링
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 터미널 1: 웹 서비스 로그
|
# 터미널 1: 백엔드 로그
|
||||||
ssh kjh2064@178.104.200.7 'journalctl -u taxbaik -f'
|
ssh kjh2064@178.104.200.7 'journalctl -u taxbaik -f'
|
||||||
|
|
||||||
# 터미널 2: 통합 서비스 로그
|
# 터미널 2: 프록시 로그
|
||||||
ssh kjh2064@178.104.200.7 'journalctl -u taxbaik -f'
|
ssh kjh2064@178.104.200.7 'journalctl -u taxbaik-proxy -f'
|
||||||
|
|
||||||
# 터미널 3: Nginx 로그
|
# 터미널 3: Nginx 로그
|
||||||
ssh kjh2064@178.104.200.7 'sudo tail -f /var/log/nginx/access.log | grep taxbaik'
|
ssh kjh2064@178.104.200.7 'sudo tail -f /var/log/nginx/access.log | grep taxbaik'
|
||||||
@@ -246,13 +224,7 @@ ssh kjh2064@178.104.200.7 'watch -n 1 "ps aux | grep TaxBaik"'
|
|||||||
### 정기적 검사
|
### 정기적 검사
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 일일 체크 (cron job)
|
# 일일 체크는 CI 배포 후 자동 검증으로 대체
|
||||||
0 9 * * * /home/kjh2064/health-check.sh
|
|
||||||
|
|
||||||
# 내용:
|
|
||||||
#!/bin/bash
|
|
||||||
curl -f http://127.0.0.1:5001/taxbaik || systemctl restart taxbaik
|
|
||||||
curl -f http://127.0.0.1:5001/taxbaik/admin/login || systemctl restart taxbaik
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -268,11 +240,6 @@ git commit -m "기능: 새로운 기능 추가"
|
|||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
# 2. Gitea Actions가 자동으로 배포
|
# 2. Gitea Actions가 자동으로 배포
|
||||||
# 또는 수동 배포:
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
dotnet publish TaxBaik.Web -c Release -o ./publish
|
|
||||||
rsync -avz ./publish/ kjh2064@178.104.200.7:~/deployments/taxbaik_${TIMESTAMP}/
|
|
||||||
ssh kjh2064@178.104.200.7 "ln -sfn ~/deployments/taxbaik_${TIMESTAMP} ~/taxbaik_active && sudo systemctl restart taxbaik"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 롤백 절차
|
### 롤백 절차
|
||||||
@@ -284,6 +251,7 @@ ssh kjh2064@178.104.200.7 'ls -la ~/deployments/ | grep taxbaik'
|
|||||||
# 롤백 (예: 이전 버전이 taxbaik_20260625_100000)
|
# 롤백 (예: 이전 버전이 taxbaik_20260625_100000)
|
||||||
ssh kjh2064@178.104.200.7 << EOF
|
ssh kjh2064@178.104.200.7 << EOF
|
||||||
ln -sfn ~/deployments/taxbaik_20260625_100000 ~/taxbaik_active
|
ln -sfn ~/deployments/taxbaik_20260625_100000 ~/taxbaik_active
|
||||||
|
sudo systemctl restart taxbaik-proxy
|
||||||
sudo systemctl restart taxbaik
|
sudo systemctl restart taxbaik
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ master 브랜치에 푸시하면 파이프라인이 다음 단계를 수행합
|
|||||||
- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호
|
- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호
|
||||||
- `Admin__PasswordResetToken`: 관리자 비밀번호 재설정 API용 서버 비밀값
|
- `Admin__PasswordResetToken`: 관리자 비밀번호 재설정 API용 서버 비밀값
|
||||||
|
|
||||||
수동 배포는 비상 롤백 절차 외에는 사용하지 않습니다. 실패 시 [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)의 CI 점검 절차를 따릅니다.
|
배포는 Gitea Actions CI/CD로만 수행합니다. 수동 배포 경로는 CI 하네스로 차단되어 있으며, 실패 시 [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)의 CI 점검 절차를 따릅니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -522,3 +522,46 @@ Todo:
|
|||||||
- WBS-UX-03/04 구현 완료
|
- WBS-UX-03/04 구현 완료
|
||||||
- WBS-CRM-01/02/03/04/05 구현 완료 (배포 후 검증 필요)
|
- WBS-CRM-01/02/03/04/05 구현 완료 (배포 후 검증 필요)
|
||||||
- WBS-CRM-06/07/08 (텔레그램·포털·소셜 로그인) Phase 3 미착수
|
- 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 연계 바인딩 처리
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace TaxBaik.Application.Tests;
|
||||||
|
|
||||||
|
using TaxBaik.Web.Components.Admin.Shared;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class BusinessDayCalculatorTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2026, 2, 14, 2026, 2, 19)]
|
||||||
|
[InlineData(2026, 8, 15, 2026, 8, 20)]
|
||||||
|
[InlineData(2026, 9, 24, 2026, 9, 29)]
|
||||||
|
[InlineData(2026, 10, 3, 2026, 10, 8)]
|
||||||
|
public void GetEffectiveDueDate_SkipsWeekendHolidayAndSubstituteHoliday(
|
||||||
|
int dueYear, int dueMonth, int dueDay,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay)
|
||||||
|
{
|
||||||
|
var effective = BusinessDayCalculator.GetEffectiveDueDate(new DateOnly(dueYear, dueMonth, dueDay));
|
||||||
|
|
||||||
|
Assert.Equal(new DateOnly(expectedYear, expectedMonth, expectedDay), effective);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2026, 2, 19, 0)]
|
||||||
|
[InlineData(2026, 2, 20, -1)]
|
||||||
|
[InlineData(2026, 2, 18, 1)]
|
||||||
|
public void GetDday_UsesEffectiveDueDate(
|
||||||
|
int refYear, int refMonth, int refDay,
|
||||||
|
int expectedDays)
|
||||||
|
{
|
||||||
|
var dday = BusinessDayCalculator.GetDday(new DateOnly(2026, 2, 14), new DateOnly(refYear, refMonth, refDay));
|
||||||
|
|
||||||
|
Assert.Equal(expectedDays, dday);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,5 +18,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\TaxBaik.Web\TaxBaik.Web.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<RevenueTrackingService>();
|
services.AddScoped<RevenueTrackingService>();
|
||||||
services.AddScoped<TelegramReportService>();
|
services.AddScoped<TelegramReportService>();
|
||||||
services.AddScoped<PortalUserService>();
|
services.AddScoped<PortalUserService>();
|
||||||
|
services.AddScoped<CommonCodeService>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
using TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace TaxBaik.Application.Services;
|
||||||
|
|
||||||
|
public class CommonCodeService(ICommonCodeRepository commonCodeRepository)
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<string>> GetAllGroupsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetAllGroupsAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetByGroupAsync(codeGroup, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetAllActiveAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CommonCode?> GetAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetAsync(codeGroup, codeValue, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(CommonCode code, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Normalize(code);
|
||||||
|
await commonCodeRepository.UpsertAsync(code, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await commonCodeRepository.DeleteAsync(codeGroup.Trim(), codeValue.Trim(), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Normalize(CommonCode code)
|
||||||
|
{
|
||||||
|
code.CodeGroup = code.CodeGroup.Trim();
|
||||||
|
code.CodeValue = code.CodeValue.Trim();
|
||||||
|
code.CodeName = code.CodeName.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,10 @@ public class TaxProfileService(ITaxProfileRepository repository)
|
|||||||
public async Task UpdateAsync(int profileId, string? businessType, string? accountingMethod,
|
public async Task UpdateAsync(int profileId, string? businessType, string? accountingMethod,
|
||||||
DateTime? nextFilingDueDate, string taxRiskLevel = "normal", CancellationToken ct = default)
|
DateTime? nextFilingDueDate, string taxRiskLevel = "normal", CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var profile = new TaxProfile { Id = profileId };
|
var profile = await repository.GetByIdAsync(profileId, ct);
|
||||||
|
if (profile == null)
|
||||||
|
throw new ValidationException("세무 프로필을 찾을 수 없습니다.");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(businessType))
|
if (!string.IsNullOrWhiteSpace(businessType))
|
||||||
profile.BusinessType = businessType.Trim();
|
profile.BusinessType = businessType.Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(accountingMethod))
|
if (!string.IsNullOrWhiteSpace(accountingMethod))
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
public class CommonCode
|
||||||
|
{
|
||||||
|
public string CodeGroup { get; set; } = string.Empty;
|
||||||
|
public string CodeValue { get; set; } = string.Empty;
|
||||||
|
public string CodeName { get; set; } = string.Empty;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
namespace TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
public interface ICommonCodeRepository
|
||||||
|
{
|
||||||
|
Task<IEnumerable<string>> GetAllGroupsAsync(CancellationToken ct = default);
|
||||||
|
Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default);
|
||||||
|
Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
||||||
|
Task<CommonCode?> GetAsync(string codeGroup, string codeValue, CancellationToken ct = default);
|
||||||
|
Task UpsertAsync(CommonCode code, CancellationToken ct = default);
|
||||||
|
Task DeleteAsync(string codeGroup, string codeValue, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using TaxBaik.Domain.Entities;
|
|||||||
public interface ITaxProfileRepository
|
public interface ITaxProfileRepository
|
||||||
{
|
{
|
||||||
Task<int> CreateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
Task<int> CreateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
||||||
|
Task<TaxProfile?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<TaxProfile>> GetAllAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<TaxProfile>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||||
Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
||||||
Task UpdateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
Task UpdateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IConsultingActivityRepository, ConsultingActivityRepository>();
|
services.AddScoped<IConsultingActivityRepository, ConsultingActivityRepository>();
|
||||||
services.AddScoped<IContractRepository, ContractRepository>();
|
services.AddScoped<IContractRepository, ContractRepository>();
|
||||||
services.AddScoped<IRevenueTrackingRepository, RevenueTrackingRepository>();
|
services.AddScoped<IRevenueTrackingRepository, RevenueTrackingRepository>();
|
||||||
|
services.AddScoped<ICommonCodeRepository, CommonCodeRepository>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
using TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace TaxBaik.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class CommonCodeRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ICommonCodeRepository
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<string>> GetAllGroupsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<string>(
|
||||||
|
"SELECT DISTINCT code_group FROM common_codes WHERE is_active = TRUE ORDER BY code_group");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<CommonCode>(
|
||||||
|
@"SELECT code_group as CodeGroup, code_value as CodeValue, code_name as CodeName, sort_order as SortOrder, is_active as IsActive
|
||||||
|
FROM common_codes
|
||||||
|
WHERE code_group = @CodeGroup AND is_active = TRUE
|
||||||
|
ORDER BY sort_order",
|
||||||
|
new { CodeGroup = codeGroup });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<CommonCode>(
|
||||||
|
@"SELECT code_group as CodeGroup, code_value as CodeValue, code_name as CodeName, sort_order as SortOrder, is_active as IsActive
|
||||||
|
FROM common_codes
|
||||||
|
WHERE is_active = TRUE
|
||||||
|
ORDER BY code_group, sort_order");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CommonCode?> GetAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QuerySingleOrDefaultAsync<CommonCode>(
|
||||||
|
@"SELECT code_group as CodeGroup, code_value as CodeValue, code_name as CodeName, sort_order as SortOrder, is_active as IsActive
|
||||||
|
FROM common_codes
|
||||||
|
WHERE code_group = @CodeGroup AND code_value = @CodeValue",
|
||||||
|
new { CodeGroup = codeGroup, CodeValue = codeValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(CommonCode code, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"INSERT INTO common_codes (code_group, code_value, code_name, sort_order, is_active)
|
||||||
|
VALUES (@CodeGroup, @CodeValue, @CodeName, @SortOrder, @IsActive)
|
||||||
|
ON CONFLICT (code_group, code_value) DO UPDATE
|
||||||
|
SET code_name = EXCLUDED.code_name,
|
||||||
|
sort_order = EXCLUDED.sort_order,
|
||||||
|
is_active = EXCLUDED.is_active",
|
||||||
|
code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"DELETE FROM common_codes
|
||||||
|
WHERE code_group = @CodeGroup AND code_value = @CodeValue",
|
||||||
|
new { CodeGroup = codeGroup, CodeValue = codeValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,17 @@ public class TaxProfileRepository(IDbConnectionFactory connectionFactory) : Base
|
|||||||
profile);
|
profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<TaxProfile?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryFirstOrDefaultAsync<TaxProfile>(
|
||||||
|
@"SELECT id, client_id, business_registration, business_type, establishment_date,
|
||||||
|
annual_revenue_range, employee_count, accounting_method, fiscal_year_end, last_filing_date,
|
||||||
|
next_filing_due_date, tax_risk_level, previous_audit_history, special_notes, created_at, updated_at
|
||||||
|
FROM tax_profiles WHERE id = @Id",
|
||||||
|
new { Id = id });
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TaxProfile>> GetAllAsync(CancellationToken cancellationToken = default)
|
public async Task<IEnumerable<TaxProfile>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
using var conn = Conn();
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
private const string PortFile = "/home/kjh2064/taxbaik_port";
|
||||||
|
private static int _fallbackPort = 5003;
|
||||||
|
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
// Allow setting fallback port via args
|
||||||
|
if (args.Length > 0 && int.TryParse(args[0], out var port))
|
||||||
|
{
|
||||||
|
_fallbackPort = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener = new TcpListener(IPAddress.Loopback, 5001);
|
||||||
|
listener.Start();
|
||||||
|
Console.WriteLine($"[TaxBaik Proxy] Listening on 127.0.0.1:5001 (Forwarding to target in {PortFile})");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = await listener.AcceptTcpClientAsync();
|
||||||
|
_ = HandleClientAsync(client);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[TaxBaik Proxy] Accept error: {ex.Message}");
|
||||||
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetTargetPort()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(PortFile))
|
||||||
|
{
|
||||||
|
var content = File.ReadAllText(PortFile).Trim();
|
||||||
|
if (int.TryParse(content, out var port) && port > 1024 && port < 65535)
|
||||||
|
{
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return _fallbackPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleClientAsync(TcpClient client)
|
||||||
|
{
|
||||||
|
client.NoDelay = true;
|
||||||
|
int targetPort = GetTargetPort();
|
||||||
|
using var backend = new TcpClient();
|
||||||
|
backend.NoDelay = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
await backend.ConnectAsync(IPAddress.Loopback, targetPort, cts.Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[TaxBaik Proxy] Failed to connect to backend on port {targetPort}: {ex.Message}");
|
||||||
|
client.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var clientStream = client.GetStream();
|
||||||
|
using var backendStream = backend.GetStream();
|
||||||
|
|
||||||
|
var toBackend = clientStream.CopyToAsync(backendStream);
|
||||||
|
var toClient = backendStream.CopyToAsync(clientStream);
|
||||||
|
|
||||||
|
await Task.WhenAny(toBackend, toClient);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
client.Close();
|
||||||
|
backend.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net10.0",
|
||||||
|
"includedFrameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "10.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"wasmHostProperties": {
|
||||||
|
"perHostConfig": [
|
||||||
|
{
|
||||||
|
"name": "browser",
|
||||||
|
"host": "browser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configProperties": {
|
||||||
|
"Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport": false,
|
||||||
|
"Microsoft.Extensions.DependencyInjection.VerifyOpenGenericServiceTrimmability": true,
|
||||||
|
"System.ComponentModel.DefaultValueAttribute.IsSupported": false,
|
||||||
|
"System.ComponentModel.Design.IDesignerHost.IsSupported": false,
|
||||||
|
"System.ComponentModel.TypeConverter.EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization": false,
|
||||||
|
"System.ComponentModel.TypeDescriptor.IsComObjectDescriptorSupported": false,
|
||||||
|
"System.Data.DataSet.XmlSerializationIsSupported": false,
|
||||||
|
"System.Diagnostics.Debugger.IsSupported": false,
|
||||||
|
"System.Diagnostics.Metrics.Meter.IsSupported": false,
|
||||||
|
"System.Diagnostics.Tracing.EventSource.IsSupported": false,
|
||||||
|
"System.GC.Server": true,
|
||||||
|
"System.Globalization.Invariant": false,
|
||||||
|
"System.TimeZoneInfo.Invariant": false,
|
||||||
|
"System.Linq.Enumerable.IsSizeOptimized": true,
|
||||||
|
"System.Net.Http.EnableActivityPropagation": false,
|
||||||
|
"System.Net.Http.WasmEnableStreamingResponse": true,
|
||||||
|
"System.Net.SocketsHttpHandler.Http3Support": false,
|
||||||
|
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||||
|
"System.Resources.ResourceManager.AllowCustomResourceTypes": false,
|
||||||
|
"System.Resources.UseSystemResourceKeys": true,
|
||||||
|
"System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported": true,
|
||||||
|
"System.Runtime.InteropServices.BuiltInComInterop.IsSupported": false,
|
||||||
|
"System.Runtime.InteropServices.EnableConsumingManagedCodeFromNativeHosting": false,
|
||||||
|
"System.Runtime.InteropServices.EnableCppCLIHostActivation": false,
|
||||||
|
"System.Runtime.InteropServices.Marshalling.EnableGeneratedComInterfaceComImportInterop": false,
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false,
|
||||||
|
"System.StartupHookProvider.IsSupported": false,
|
||||||
|
"System.Text.Encoding.EnableUnsafeUTF7Encoding": false,
|
||||||
|
"System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault": true,
|
||||||
|
"System.Threading.Thread.EnableAutoreleasePool": false,
|
||||||
|
"Microsoft.AspNetCore.Components.Endpoints.NavigationManager.DisableThrowNavigationException": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
global using System.Net.Http;
|
||||||
|
global using System.Net.Http.Json;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@* WASM 기반(M3) 검증용 컴포넌트. 라우팅/렌더모드 전면 적용은 M4에서 처리한다. *@
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
|
||||||
|
<MudPaper Class="pa-6 ma-4" Elevation="2">
|
||||||
|
<MudText Typo="Typo.h5" GutterBottom="true">WebAssembly 렌더 모드 점검</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="mb-4">이 컴포넌트가 클릭에 반응하면 Interactive WebAssembly 기반이 정상 동작하는 것입니다.</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Increment">카운트: @count</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int count;
|
||||||
|
private void Increment() => count++;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using MudBlazor.Services;
|
||||||
|
using TaxBaik.Application.Services;
|
||||||
|
using TaxBaik.Web.Services;
|
||||||
|
using TaxBaik.Web.Services.AdminClients;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
|
// MudBlazor (WASM 측 인터랙티브 컴포넌트용)
|
||||||
|
builder.Services.AddMudServices(config =>
|
||||||
|
{
|
||||||
|
config.SnackbarConfiguration.HideTransitionDuration = 400;
|
||||||
|
config.SnackbarConfiguration.ShowTransitionDuration = 300;
|
||||||
|
config.PopoverOptions.ThrowOnDuplicateProvider = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// API Base Url 동적 구성 (호스트 기준 /taxbaik/api/)
|
||||||
|
var apiBaseUrl = builder.HostEnvironment.BaseAddress.TrimEnd('/') + "/taxbaik/api/";
|
||||||
|
|
||||||
|
// HTTP Client for API (with automatic token refresh)
|
||||||
|
builder.Services.AddScoped<ITokenStore, TokenStore>();
|
||||||
|
builder.Services.AddScoped<TokenRefreshHandler>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
|
// 각 Browser API Client 등록
|
||||||
|
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<ITaxProfileBrowserClient, TaxProfileBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<ITaxFilingScheduleBrowserClient, TaxFilingScheduleBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<IConsultingActivityBrowserClient, ConsultingActivityBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<IContractBrowserClient, ContractBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
builder.Services.AddHttpClient<ICommonCodeBrowserClient, CommonCodeBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
|
// Blazor 인증 (WASM 측 클라이언트)
|
||||||
|
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
|
||||||
|
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CustomAuthenticationStateProvider>());
|
||||||
|
builder.Services.AddScoped<ILocalStorageService, LocalStorageService>();
|
||||||
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
builder.Services.AddAuthorizationCore();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
namespace TaxBaik.Web.Services.AdminClients;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
public interface ICommonCodeBrowserClient
|
||||||
|
{
|
||||||
|
Task<List<string>> GetGroupsAsync(CancellationToken ct = default);
|
||||||
|
Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
||||||
|
Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default);
|
||||||
|
Task<CommonCode?> GetAsync(string group, string value, CancellationToken ct = default);
|
||||||
|
Task<bool> UpsertAsync(CommonCode code, CancellationToken ct = default);
|
||||||
|
Task<bool> DeleteAsync(string group, string value, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CommonCodeBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<CommonCodeBrowserClient> logger) : ICommonCodeBrowserClient
|
||||||
|
{
|
||||||
|
private const string BaseUrl = "/api/commoncode";
|
||||||
|
|
||||||
|
private void EnsureAuthHeader()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
|
else
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<List<CommonCode>>($"{BaseUrl}", ct) ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get all active common codes");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<List<CommonCode>>($"{BaseUrl}/group/{group}", ct) ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get common codes for group {Group}", group);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetGroupsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<List<string>>($"{BaseUrl}/groups", ct) ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get common code groups");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CommonCode?> GetAsync(string group, string value, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<CommonCode>($"{BaseUrl}/{group}/{value}", ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get common code {Group}/{Value}", group, value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpsertAsync(CommonCode code, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
var response = await httpClient.PostAsJsonAsync(BaseUrl, code, ct);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to upsert common code {Group}/{Value}", code.CodeGroup, code.CodeValue);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(string group, string value, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
var response = await httpClient.DeleteAsync($"{BaseUrl}/{group}/{value}", ct);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to delete common code {Group}/{Value}", group, value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-8
@@ -1,6 +1,7 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using TaxBaik.Application.Services;
|
||||||
|
|
||||||
namespace TaxBaik.Web.Services;
|
namespace TaxBaik.Web.Services;
|
||||||
|
|
||||||
@@ -8,18 +9,18 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
{
|
{
|
||||||
private readonly ILocalStorageService _localStorage;
|
private readonly ILocalStorageService _localStorage;
|
||||||
private readonly ITokenStore _tokenStore;
|
private readonly ITokenStore _tokenStore;
|
||||||
private readonly AuthService _authService;
|
private readonly IApiClient _apiClient;
|
||||||
private readonly ILogger<CustomAuthenticationStateProvider> _logger;
|
private readonly ILogger<CustomAuthenticationStateProvider> _logger;
|
||||||
|
|
||||||
public CustomAuthenticationStateProvider(
|
public CustomAuthenticationStateProvider(
|
||||||
ILocalStorageService localStorage,
|
ILocalStorageService localStorage,
|
||||||
ITokenStore tokenStore,
|
ITokenStore tokenStore,
|
||||||
AuthService authService,
|
IApiClient apiClient,
|
||||||
ILogger<CustomAuthenticationStateProvider> logger)
|
ILogger<CustomAuthenticationStateProvider> logger)
|
||||||
{
|
{
|
||||||
_localStorage = localStorage;
|
_localStorage = localStorage;
|
||||||
_tokenStore = tokenStore;
|
_tokenStore = tokenStore;
|
||||||
_authService = authService;
|
_apiClient = apiClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +65,9 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
if (!string.IsNullOrEmpty(_tokenStore.RefreshToken) && ShouldRefreshToken())
|
if (!string.IsNullOrEmpty(_tokenStore.RefreshToken) && ShouldRefreshToken())
|
||||||
{
|
{
|
||||||
_logger.LogInformation("토큰 만료 5분 전 - 자동 갱신 시작");
|
_logger.LogInformation("토큰 만료 5분 전 - 자동 갱신 시작");
|
||||||
var newTokenPair = await _authService.RefreshAccessTokenAsync(_tokenStore.RefreshToken);
|
var request = new { RefreshToken = _tokenStore.RefreshToken };
|
||||||
if (newTokenPair != null)
|
var newTokenPair = await _apiClient.PostAsync<WasmAuthTokenPair>("auth/refresh", request);
|
||||||
|
if (newTokenPair != null && !string.IsNullOrEmpty(newTokenPair.AccessToken))
|
||||||
{
|
{
|
||||||
await LoginAsync(newTokenPair.AccessToken, newTokenPair.RefreshToken, newTokenPair.ExpiresIn);
|
await LoginAsync(newTokenPair.AccessToken, newTokenPair.RefreshToken, newTokenPair.ExpiresIn);
|
||||||
_logger.LogInformation("토큰 자동 갱신 성공");
|
_logger.LogInformation("토큰 자동 갱신 성공");
|
||||||
@@ -79,7 +81,7 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var principal = _authService.ValidateToken(accessToken);
|
var principal = ValidateTokenWithoutDb(accessToken ?? string.Empty);
|
||||||
if (principal == null)
|
if (principal == null)
|
||||||
{
|
{
|
||||||
await LogoutAsync();
|
await LogoutAsync();
|
||||||
@@ -95,6 +97,22 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ClaimsPrincipal? ValidateTokenWithoutDb(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
var jwtToken = handler.ReadJwtToken(token);
|
||||||
|
var identity = new ClaimsIdentity(jwtToken.Claims, "jwt");
|
||||||
|
return new ClaimsPrincipal(identity);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task LoginAsync(string accessToken, string refreshToken, int expiresIn)
|
public async Task LoginAsync(string accessToken, string refreshToken, int expiresIn)
|
||||||
{
|
{
|
||||||
var tokenExpiryTicks = DateTime.UtcNow.AddSeconds(expiresIn).Ticks;
|
var tokenExpiryTicks = DateTime.UtcNow.AddSeconds(expiresIn).Ticks;
|
||||||
@@ -115,13 +133,13 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
private bool ShouldRefreshToken()
|
private bool ShouldRefreshToken()
|
||||||
{
|
{
|
||||||
// 토큰이 5분 이내로 만료되면 갱신 (300초 = 5분)
|
// 토큰이 5분 이내로 만료되면 갱신 (300초 = 5분)
|
||||||
if (_tokenStore.TokenExpiryTicks <= 0)
|
if (!_tokenStore.TokenExpiryTicks.HasValue || _tokenStore.TokenExpiryTicks.Value <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const int refreshThresholdSeconds = 300;
|
const int refreshThresholdSeconds = 300;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var expiryTime = new DateTime((long)_tokenStore.TokenExpiryTicks, DateTimeKind.Utc);
|
var expiryTime = new DateTime(_tokenStore.TokenExpiryTicks.Value, DateTimeKind.Utc);
|
||||||
var timeUntilExpiry = expiryTime - DateTime.UtcNow;
|
var timeUntilExpiry = expiryTime - DateTime.UtcNow;
|
||||||
return timeUntilExpiry.TotalSeconds <= refreshThresholdSeconds && timeUntilExpiry.TotalSeconds > 0;
|
return timeUntilExpiry.TotalSeconds <= refreshThresholdSeconds && timeUntilExpiry.TotalSeconds > 0;
|
||||||
}
|
}
|
||||||
@@ -158,3 +176,17 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class WasmAuthTokenPair
|
||||||
|
{
|
||||||
|
public WasmAuthTokenPair() { }
|
||||||
|
public WasmAuthTokenPair(string accessToken, string refreshToken, int expiresIn)
|
||||||
|
{
|
||||||
|
AccessToken = accessToken;
|
||||||
|
RefreshToken = refreshToken;
|
||||||
|
ExpiresIn = expiresIn;
|
||||||
|
}
|
||||||
|
public string AccessToken { get; set; } = "";
|
||||||
|
public string RefreshToken { get; set; } = "";
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
}
|
||||||
+2
-2
@@ -62,7 +62,7 @@ public class TokenRefreshHandler : DelegatingHandler
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<AuthTokenPair?> RefreshTokenAsync(string refreshToken, HttpRequestMessage originalRequest, CancellationToken ct)
|
private async Task<WasmAuthTokenPair?> RefreshTokenAsync(string refreshToken, HttpRequestMessage originalRequest, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@ public class TokenRefreshHandler : DelegatingHandler
|
|||||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
|
||||||
return result != null
|
return result != null
|
||||||
? new AuthTokenPair(result.AccessToken, result.RefreshToken, result.ExpiresIn)
|
? new WasmAuthTokenPair(result.AccessToken, result.RefreshToken, result.ExpiresIn)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>TaxBaik.WasmClient</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
|
||||||
|
<PackageReference Include="MudBlazor" Version="6.10.0" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using MudBlazor
|
||||||
|
@using TaxBaik.WasmClient
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v10.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v10.0": {
|
||||||
|
"TaxBaik.Web/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"BCrypt.Net-Next": "4.0.3",
|
||||||
|
"Microsoft.AspNetCore.Authentication.Google": "10.0.9",
|
||||||
|
"Microsoft.AspNetCore.Authentication.JwtBearer": "10.0.9",
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly.Server": "10.0.9",
|
||||||
|
"Microsoft.IdentityModel.Tokens": "8.19.1",
|
||||||
|
"MudBlazor": "6.10.0",
|
||||||
|
"Serilog.AspNetCore": "8.0.1",
|
||||||
|
"Serilog.Sinks.Console": "6.0.0",
|
||||||
|
"Serilog.Sinks.File": "5.0.0",
|
||||||
|
"System.IdentityModel.Tokens.Jwt": "8.19.1",
|
||||||
|
"TaxBaik.Application": "1.0.0",
|
||||||
|
"TaxBaik.Infrastructure": "1.0.0",
|
||||||
|
"TaxBaik.Web.Client": "1.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"TaxBaik.Web.dll": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BCrypt.Net-Next/4.0.3": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/BCrypt.Net-Next.dll": {
|
||||||
|
"assemblyVersion": "4.0.3.0",
|
||||||
|
"fileVersion": "4.0.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Dapper/2.1.15": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net5.0/Dapper.dll": {
|
||||||
|
"assemblyVersion": "2.0.0.0",
|
||||||
|
"fileVersion": "2.1.15.52653"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Authentication.Google/10.0.9": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.AspNetCore.Authentication.Google.dll": {
|
||||||
|
"assemblyVersion": "10.0.9.0",
|
||||||
|
"fileVersion": "10.0.926.27113"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Authentication.JwtBearer/10.0.9": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll": {
|
||||||
|
"assemblyVersion": "10.0.9.0",
|
||||||
|
"fileVersion": "10.0.926.27113"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly/10.0.9": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.JSInterop.WebAssembly": "10.0.9"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.AspNetCore.Components.WebAssembly.dll": {
|
||||||
|
"assemblyVersion": "10.0.9.0",
|
||||||
|
"fileVersion": "10.0.926.27113"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly.Server/10.0.9": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly": "10.0.9"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.AspNetCore.Components.WebAssembly.Server.dll": {
|
||||||
|
"assemblyVersion": "10.0.9.0",
|
||||||
|
"fileVersion": "10.0.926.27113"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Bcl.Cryptography/10.0.2": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.Bcl.Cryptography.dll": {
|
||||||
|
"assemblyVersion": "10.0.0.2",
|
||||||
|
"fileVersion": "10.0.225.61305"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyModel/8.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.23.53103"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Abstractions/8.19.1": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.IdentityModel.Abstractions.dll": {
|
||||||
|
"assemblyVersion": "8.19.1.0",
|
||||||
|
"fileVersion": "8.19.1.26153"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.JsonWebTokens/8.19.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.IdentityModel.Tokens": "8.19.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.IdentityModel.JsonWebTokens.dll": {
|
||||||
|
"assemblyVersion": "8.19.1.0",
|
||||||
|
"fileVersion": "8.19.1.26153"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Logging/8.19.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.IdentityModel.Abstractions": "8.19.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.IdentityModel.Logging.dll": {
|
||||||
|
"assemblyVersion": "8.19.1.0",
|
||||||
|
"fileVersion": "8.19.1.26153"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Protocols/8.0.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.IdentityModel.Tokens": "8.19.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net9.0/Microsoft.IdentityModel.Protocols.dll": {
|
||||||
|
"assemblyVersion": "8.0.1.0",
|
||||||
|
"fileVersion": "8.0.1.50722"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Protocols.OpenIdConnect/8.0.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.IdentityModel.Protocols": "8.0.1",
|
||||||
|
"System.IdentityModel.Tokens.Jwt": "8.19.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net9.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": {
|
||||||
|
"assemblyVersion": "8.0.1.0",
|
||||||
|
"fileVersion": "8.0.1.50722"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Tokens/8.19.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Bcl.Cryptography": "10.0.2",
|
||||||
|
"Microsoft.IdentityModel.Logging": "8.19.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.IdentityModel.Tokens.dll": {
|
||||||
|
"assemblyVersion": "8.19.1.0",
|
||||||
|
"fileVersion": "8.19.1.26153"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.JSInterop.WebAssembly/10.0.9": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Microsoft.JSInterop.WebAssembly.dll": {
|
||||||
|
"assemblyVersion": "10.0.9.0",
|
||||||
|
"fileVersion": "10.0.926.27113"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MudBlazor/6.10.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net7.0/MudBlazor.dll": {
|
||||||
|
"assemblyVersion": "6.10.0.0",
|
||||||
|
"fileVersion": "6.10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Npgsql/10.0.3": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/Npgsql.dll": {
|
||||||
|
"assemblyVersion": "10.0.3.0",
|
||||||
|
"fileVersion": "10.0.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog/4.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Serilog.dll": {
|
||||||
|
"assemblyVersion": "4.0.0.0",
|
||||||
|
"fileVersion": "4.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.AspNetCore/8.0.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Serilog": "4.0.0",
|
||||||
|
"Serilog.Extensions.Hosting": "8.0.0",
|
||||||
|
"Serilog.Extensions.Logging": "8.0.0",
|
||||||
|
"Serilog.Formatting.Compact": "2.0.0",
|
||||||
|
"Serilog.Settings.Configuration": "8.0.0",
|
||||||
|
"Serilog.Sinks.Console": "6.0.0",
|
||||||
|
"Serilog.Sinks.Debug": "2.0.0",
|
||||||
|
"Serilog.Sinks.File": "5.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Serilog.AspNetCore.dll": {
|
||||||
|
"assemblyVersion": "8.0.1.0",
|
||||||
|
"fileVersion": "8.0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.Extensions.Hosting/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Serilog": "4.0.0",
|
||||||
|
"Serilog.Extensions.Logging": "8.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Serilog.Extensions.Hosting.dll": {
|
||||||
|
"assemblyVersion": "7.0.0.0",
|
||||||
|
"fileVersion": "8.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.Extensions.Logging/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Serilog": "4.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Serilog.Extensions.Logging.dll": {
|
||||||
|
"assemblyVersion": "7.0.0.0",
|
||||||
|
"fileVersion": "8.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.Formatting.Compact/2.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Serilog": "4.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net7.0/Serilog.Formatting.Compact.dll": {
|
||||||
|
"assemblyVersion": "2.0.0.0",
|
||||||
|
"fileVersion": "2.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.Settings.Configuration/8.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyModel": "8.0.0",
|
||||||
|
"Serilog": "4.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Serilog.Settings.Configuration.dll": {
|
||||||
|
"assemblyVersion": "8.0.0.0",
|
||||||
|
"fileVersion": "8.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.Sinks.Console/6.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Serilog": "4.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net8.0/Serilog.Sinks.Console.dll": {
|
||||||
|
"assemblyVersion": "6.0.0.0",
|
||||||
|
"fileVersion": "6.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.Sinks.Debug/2.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Serilog": "4.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard2.1/Serilog.Sinks.Debug.dll": {
|
||||||
|
"assemblyVersion": "2.0.0.0",
|
||||||
|
"fileVersion": "2.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog.Sinks.File/5.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Serilog": "4.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net5.0/Serilog.Sinks.File.dll": {
|
||||||
|
"assemblyVersion": "5.0.0.0",
|
||||||
|
"fileVersion": "5.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"System.IdentityModel.Tokens.Jwt/8.19.1": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.IdentityModel.JsonWebTokens": "8.19.1",
|
||||||
|
"Microsoft.IdentityModel.Tokens": "8.19.1"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net10.0/System.IdentityModel.Tokens.Jwt.dll": {
|
||||||
|
"assemblyVersion": "8.19.1.0",
|
||||||
|
"fileVersion": "8.19.1.26153"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TaxBaik.Application/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"TaxBaik.Domain": "1.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"TaxBaik.Application.dll": {
|
||||||
|
"assemblyVersion": "1.0.0.0",
|
||||||
|
"fileVersion": "1.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TaxBaik.Domain/1.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"TaxBaik.Domain.dll": {
|
||||||
|
"assemblyVersion": "1.0.0.0",
|
||||||
|
"fileVersion": "1.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TaxBaik.Infrastructure/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Dapper": "2.1.15",
|
||||||
|
"Npgsql": "10.0.3",
|
||||||
|
"TaxBaik.Domain": "1.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"TaxBaik.Infrastructure.dll": {
|
||||||
|
"assemblyVersion": "1.0.0.0",
|
||||||
|
"fileVersion": "1.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TaxBaik.Web.Client/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly": "10.0.9",
|
||||||
|
"Microsoft.IdentityModel.Tokens": "8.19.1",
|
||||||
|
"MudBlazor": "6.10.0",
|
||||||
|
"System.IdentityModel.Tokens.Jwt": "8.19.1",
|
||||||
|
"TaxBaik.Application": "1.0.0"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"TaxBaik.Web.Client.dll": {
|
||||||
|
"assemblyVersion": "1.0.0.0",
|
||||||
|
"fileVersion": "1.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"TaxBaik.Web/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"BCrypt.Net-Next/4.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-W+U9WvmZQgi5cX6FS5GDtDoPzUCV4LkBLkywq/kRZhuDwcbavOzcDAr3LXJFqHUi952Yj3LEYoWW0jbEUQChsA==",
|
||||||
|
"path": "bcrypt.net-next/4.0.3",
|
||||||
|
"hashPath": "bcrypt.net-next.4.0.3.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Dapper/2.1.15": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-1aWSAosZymEM+mRwfrXteRIN74/JTUjqj9B/KqEbanH6vfUKy9D9cemRN0q1ZOEfSB7d1PpFTpVOCbf2Uv70Og==",
|
||||||
|
"path": "dapper/2.1.15",
|
||||||
|
"hashPath": "dapper.2.1.15.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Authentication.Google/10.0.9": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-xqjTc8/ap0dwKmdaqSlV8RxjXb02uQ8rynDtTuHRU2gmOYaNm6O+uUjobp4Ararzq0ndKNXiWnQErxjWEGFGiA==",
|
||||||
|
"path": "microsoft.aspnetcore.authentication.google/10.0.9",
|
||||||
|
"hashPath": "microsoft.aspnetcore.authentication.google.10.0.9.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Authentication.JwtBearer/10.0.9": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Hs5NDsGm8YicDDNx5RoBIT+H2AB9R27MvZ2gHoupTiHr+nnH3VxzY7DcmlbJ3b5DvvOhK35lWt/9Odtrq9sjtA==",
|
||||||
|
"path": "microsoft.aspnetcore.authentication.jwtbearer/10.0.9",
|
||||||
|
"hashPath": "microsoft.aspnetcore.authentication.jwtbearer.10.0.9.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly/10.0.9": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-tBv68AsZ3r6z2QdV2m3cSSKUCbvEscN8REpHxcUs22vlR6UjTz6IKdInKNREkJ/3G1AQrBKrRTdrfrHVffE8Iw==",
|
||||||
|
"path": "microsoft.aspnetcore.components.webassembly/10.0.9",
|
||||||
|
"hashPath": "microsoft.aspnetcore.components.webassembly.10.0.9.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly.Server/10.0.9": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-ZTtYvBILwGxhIiXi1L03ETBBOgMmizStu7dO/YblK6rPTa27wpEgYKp5Z9bUfr+wsFvHIDWd/ZMGb9on41f6yw==",
|
||||||
|
"path": "microsoft.aspnetcore.components.webassembly.server/10.0.9",
|
||||||
|
"hashPath": "microsoft.aspnetcore.components.webassembly.server.10.0.9.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Bcl.Cryptography/10.0.2": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw==",
|
||||||
|
"path": "microsoft.bcl.cryptography/10.0.2",
|
||||||
|
"hashPath": "microsoft.bcl.cryptography.10.0.2.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyModel/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-NSmDw3K0ozNDgShSIpsZcbFIzBX4w28nDag+TfaQujkXGazBm+lid5onlWoCBy4VsLxqnnKjEBbGSJVWJMf43g==",
|
||||||
|
"path": "microsoft.extensions.dependencymodel/8.0.0",
|
||||||
|
"hashPath": "microsoft.extensions.dependencymodel.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Abstractions/8.19.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-gFA8THIk23uNF/vMdOHnjIdXD1LyA2g12cHzMJ+Xag6WpgWLw6E/6uCXxvA0gp9d2yAvkRt3xzFzMUiO/hofnQ==",
|
||||||
|
"path": "microsoft.identitymodel.abstractions/8.19.1",
|
||||||
|
"hashPath": "microsoft.identitymodel.abstractions.8.19.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.JsonWebTokens/8.19.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-6eeY+y2QFyjj3XnCz/8gJdoP5smYHTS9ow1bw2nsZzDIPjPhBZlackYTIduSMipVpxnoT/B62LkrXX2jPggOXg==",
|
||||||
|
"path": "microsoft.identitymodel.jsonwebtokens/8.19.1",
|
||||||
|
"hashPath": "microsoft.identitymodel.jsonwebtokens.8.19.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Logging/8.19.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-H+sMrMpdbWnwkQnpb/ESkQovtOgdefmj0ecGCcP40mDKzE5i4dUYkH6599M9mWYFNGNJnTp92l/9wLubYXWimw==",
|
||||||
|
"path": "microsoft.identitymodel.logging/8.19.1",
|
||||||
|
"hashPath": "microsoft.identitymodel.logging.8.19.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Protocols/8.0.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==",
|
||||||
|
"path": "microsoft.identitymodel.protocols/8.0.1",
|
||||||
|
"hashPath": "microsoft.identitymodel.protocols.8.0.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Protocols.OpenIdConnect/8.0.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==",
|
||||||
|
"path": "microsoft.identitymodel.protocols.openidconnect/8.0.1",
|
||||||
|
"hashPath": "microsoft.identitymodel.protocols.openidconnect.8.0.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.IdentityModel.Tokens/8.19.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-KDiuSLXud2AFVNAOottd8ztVysfPeHyr4r8gofU3/VKUXlI7oytzGTnPsNJ/B3nui17rgz8wAdWNJOtzPjkUxw==",
|
||||||
|
"path": "microsoft.identitymodel.tokens/8.19.1",
|
||||||
|
"hashPath": "microsoft.identitymodel.tokens.8.19.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.JSInterop.WebAssembly/10.0.9": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-4G0A7GuQrtCAes8PuJPTDUcy+lCrxHWjr8ZlkDOa4h8a2Txj1XdhbXKLnld2vMY5EyZNC5jZXxa1xTD/AOCUlw==",
|
||||||
|
"path": "microsoft.jsinterop.webassembly/10.0.9",
|
||||||
|
"hashPath": "microsoft.jsinterop.webassembly.10.0.9.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MudBlazor/6.10.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Dpjouo3MVva4p8Nh2VCzHzvzReWhnzmCBNlrhymeXjn6oBEtT3Oi9z/R2sHOg/jYrW/hIPKMhfZHnptilHScsw==",
|
||||||
|
"path": "mudblazor/6.10.0",
|
||||||
|
"hashPath": "mudblazor.6.10.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Npgsql/10.0.3": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==",
|
||||||
|
"path": "npgsql/10.0.3",
|
||||||
|
"hashPath": "npgsql.10.0.3.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog/4.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-2jDkUrSh5EofOp7Lx5Zgy0EB+7hXjjxE2ktTb1WVQmU00lDACR2TdROGKU0K1pDTBSJBN1PqgYpgOZF8mL7NJw==",
|
||||||
|
"path": "serilog/4.0.0",
|
||||||
|
"hashPath": "serilog.4.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.AspNetCore/8.0.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-B/X+wAfS7yWLVOTD83B+Ip9yl4MkhioaXj90JSoWi1Ayi8XHepEnsBdrkojg08eodCnmOKmShFUN2GgEc6c0CQ==",
|
||||||
|
"path": "serilog.aspnetcore/8.0.1",
|
||||||
|
"hashPath": "serilog.aspnetcore.8.0.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.Extensions.Hosting/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-db0OcbWeSCvYQkHWu6n0v40N4kKaTAXNjlM3BKvcbwvNzYphQFcBR+36eQ/7hMMwOkJvAyLC2a9/jNdUL5NjtQ==",
|
||||||
|
"path": "serilog.extensions.hosting/8.0.0",
|
||||||
|
"hashPath": "serilog.extensions.hosting.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.Extensions.Logging/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==",
|
||||||
|
"path": "serilog.extensions.logging/8.0.0",
|
||||||
|
"hashPath": "serilog.extensions.logging.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.Formatting.Compact/2.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-ob6z3ikzFM3D1xalhFuBIK1IOWf+XrQq+H4KeH4VqBcPpNcmUgZlRQ2h3Q7wvthpdZBBoY86qZOI2LCXNaLlNA==",
|
||||||
|
"path": "serilog.formatting.compact/2.0.0",
|
||||||
|
"hashPath": "serilog.formatting.compact.2.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.Settings.Configuration/8.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-nR0iL5HwKj5v6ULo3/zpP8NMcq9E2pxYA6XKTSWCbugVs4YqPyvaqaKOY+OMpPivKp7zMEpax2UKHnDodbRB0Q==",
|
||||||
|
"path": "serilog.settings.configuration/8.0.0",
|
||||||
|
"hashPath": "serilog.settings.configuration.8.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.Sinks.Console/6.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-fQGWqVMClCP2yEyTXPIinSr5c+CBGUvBybPxjAGcf7ctDhadFhrQw03Mv8rJ07/wR5PDfFjewf2LimvXCDzpbA==",
|
||||||
|
"path": "serilog.sinks.console/6.0.0",
|
||||||
|
"hashPath": "serilog.sinks.console.6.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.Sinks.Debug/2.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==",
|
||||||
|
"path": "serilog.sinks.debug/2.0.0",
|
||||||
|
"hashPath": "serilog.sinks.debug.2.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Serilog.Sinks.File/5.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==",
|
||||||
|
"path": "serilog.sinks.file/5.0.0",
|
||||||
|
"hashPath": "serilog.sinks.file.5.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"System.IdentityModel.Tokens.Jwt/8.19.1": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-2VHcRtT95GAcW1E3aVBLvL2rAAMxKHXKMXKXFyWzwgkdFXZPMMvP8tVOfnRydL4vTr1RirNuGC6T8VSEF2YsPQ==",
|
||||||
|
"path": "system.identitymodel.tokens.jwt/8.19.1",
|
||||||
|
"hashPath": "system.identitymodel.tokens.jwt.8.19.1.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"TaxBaik.Application/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"TaxBaik.Domain/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"TaxBaik.Infrastructure/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"TaxBaik.Web.Client/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net10.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "10.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.AspNetCore.App",
|
||||||
|
"version": "10.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.GC.Server": true,
|
||||||
|
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -6,9 +6,16 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>백원숙 세무회계 - 관리자</title>
|
<title>백원숙 세무회계 - 관리자</title>
|
||||||
<base href="/taxbaik/" />
|
<base href="/taxbaik/" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/taxbaik/favicon.svg" />
|
||||||
|
<link rel="alternate icon" href="/taxbaik/favicon.ico" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||||
|
<!-- EasyMDE 마크다운 에디터 -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde@2.18.0/dist/easymde.min.css" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/easymde@2.18.0/dist/easymde.min.js"></script>
|
||||||
|
<!-- Marked 라이브러리 (EasyMDE 미리보기용) -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.documentElement.classList.toggle(
|
document.documentElement.classList.toggle(
|
||||||
'admin-login-route',
|
'admin-login-route',
|
||||||
@@ -32,13 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MudThemeProvider @bind-IsDarkMode="isDarkMode" Theme="mudTheme" />
|
<MudThemeProvider @bind-IsDarkMode="isDarkMode" Theme="mudTheme" />
|
||||||
<MudPopoverProvider />
|
<Routes @rendermode="new InteractiveServerRenderMode(prerender: true)" />
|
||||||
<MudDialogProvider />
|
|
||||||
<MudSnackbarProvider />
|
|
||||||
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
|
||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
<script src="js/admin-session.js"></script>
|
<script src="js/admin-session.js"></script>
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
<script>window.taxbaikAdminSession?.bindLoginForm();</script>
|
||||||
<script>window.taxbaikAdminSession?.watchReconnect();</script>
|
<script>window.taxbaikAdminSession?.watchReconnect();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -80,49 +85,49 @@
|
|||||||
},
|
},
|
||||||
LayoutProperties = new LayoutProperties()
|
LayoutProperties = new LayoutProperties()
|
||||||
{
|
{
|
||||||
DefaultBorderRadius = "8px"
|
DefaultBorderRadius = "6px"
|
||||||
},
|
},
|
||||||
Typography = new Typography()
|
Typography = new Typography()
|
||||||
{
|
{
|
||||||
Default = new Default()
|
Default = new Default()
|
||||||
{
|
{
|
||||||
FontSize = ".875rem",
|
FontSize = ".8125rem",
|
||||||
FontWeight = 400,
|
FontWeight = 400,
|
||||||
LineHeight = 1.5
|
LineHeight = 1.5
|
||||||
},
|
},
|
||||||
H1 = new H1()
|
H1 = new H1()
|
||||||
{
|
{
|
||||||
FontSize = "2.5rem",
|
FontSize = "1.75rem",
|
||||||
FontWeight = 600,
|
FontWeight = 600,
|
||||||
LineHeight = 1.2
|
LineHeight = 1.2
|
||||||
},
|
},
|
||||||
H2 = new H2()
|
H2 = new H2()
|
||||||
{
|
{
|
||||||
FontSize = "2rem",
|
FontSize = "1.5rem",
|
||||||
FontWeight = 600,
|
FontWeight = 600,
|
||||||
LineHeight = 1.3
|
LineHeight = 1.3
|
||||||
},
|
},
|
||||||
H3 = new H3()
|
H3 = new H3()
|
||||||
{
|
{
|
||||||
FontSize = "1.75rem",
|
FontSize = "1.25rem",
|
||||||
FontWeight = 600,
|
FontWeight = 600,
|
||||||
LineHeight = 1.3
|
LineHeight = 1.3
|
||||||
},
|
},
|
||||||
H4 = new H4()
|
H4 = new H4()
|
||||||
{
|
{
|
||||||
FontSize = "1.5rem",
|
FontSize = "1.1rem",
|
||||||
FontWeight = 600,
|
FontWeight = 600,
|
||||||
LineHeight = 1.4
|
LineHeight = 1.4
|
||||||
},
|
},
|
||||||
H5 = new H5()
|
H5 = new H5()
|
||||||
{
|
{
|
||||||
FontSize = "1.25rem",
|
FontSize = "0.95rem",
|
||||||
FontWeight = 500,
|
FontWeight = 500,
|
||||||
LineHeight = 1.4
|
LineHeight = 1.4
|
||||||
},
|
},
|
||||||
H6 = new H6()
|
H6 = new H6()
|
||||||
{
|
{
|
||||||
FontSize = "1rem",
|
FontSize = "0.85rem",
|
||||||
FontWeight = 500,
|
FontWeight = 500,
|
||||||
LineHeight = 1.5
|
LineHeight = 1.5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@inject VersionInfo VersionInfo
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: true))
|
||||||
|
|
||||||
|
<MudPopoverProvider />
|
||||||
|
<MudDialogProvider />
|
||||||
|
<MudSnackbarProvider />
|
||||||
|
|
||||||
<MudLayout Class="admin-shell">
|
<MudLayout Class="admin-shell">
|
||||||
<MudAppBar Elevation="0" Class="admin-topbar">
|
<MudAppBar Elevation="0" Class="admin-topbar">
|
||||||
@@ -10,9 +16,9 @@
|
|||||||
Edge="Edge.Start"
|
Edge="Edge.Start"
|
||||||
Class="admin-menu-button"
|
Class="admin-menu-button"
|
||||||
OnClick="@ToggleDrawer" />
|
OnClick="@ToggleDrawer" />
|
||||||
<div class="admin-topbar-title">
|
<div class="admin-topbar-title" style="display: flex; align-items: center; gap: 8px;">
|
||||||
<MudText Typo="Typo.caption" Color="Color.Secondary">TaxBaik Admin</MudText>
|
<MudText Typo="Typo.body2" Class="font-weight-bold" Style="color: var(--primary-color);">[TaxBaik]</MudText>
|
||||||
<MudText Typo="Typo.h6">세무회계 관리 대시보드</MudText>
|
<MudText Typo="Typo.body2" Style="font-weight: bold; color: #1E293B;">세무회계 관리 대시보드</MudText>
|
||||||
</div>
|
</div>
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
|
|
||||||
@@ -82,7 +88,14 @@
|
|||||||
|
|
||||||
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
||||||
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
||||||
|
<MudNavLink Href="/taxbaik/admin/common-codes" Icon="@Icons.Material.Filled.Category">공통관리</MudNavLink>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
|
<div class="admin-drawer-version">
|
||||||
|
<div class="admin-drawer-version-label">Version</div>
|
||||||
|
<div class="admin-drawer-version-value">v@(VersionInfo.Version)</div>
|
||||||
|
<div class="admin-drawer-version-built">@VersionInfo.Built</div>
|
||||||
|
</div>
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
|
|
||||||
<MudMainContent Class="admin-main">
|
<MudMainContent Class="admin-main">
|
||||||
@@ -115,7 +128,7 @@
|
|||||||
|
|
||||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
{
|
{
|
||||||
_ = InvokeAsync(() => JS.InvokeVoidAsync("taxbaikAdminSession.showLoading"));
|
_ = InvokeAsync(() => JS.InvokeVoidAsync("taxbaikAdminSession.hideLoading"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleDrawer()
|
private void ToggleDrawer()
|
||||||
|
|||||||
@@ -22,14 +22,22 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div class="d-flex pa-4 gap-4 align-center">
|
||||||
|
<MudTextField @bind-Value="searchQuery" Placeholder="공지사항 제목 검색..." Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudPaper Class="admin-surface" Elevation="0">
|
<MudPaper Class="admin-surface" Elevation="0">
|
||||||
@if (announcements is null)
|
@if (announcements is null)
|
||||||
{
|
{
|
||||||
<MudProgressLinear Indeterminate="true" />
|
<MudProgressLinear Indeterminate="true" />
|
||||||
}
|
}
|
||||||
else if (!announcements.Any())
|
else if (!FilteredAnnouncements.Any())
|
||||||
{
|
{
|
||||||
<MudText Class="pa-4 text-muted">등록된 공지사항이 없습니다.</MudText>
|
<div class="pa-6 text-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Campaign" Style="font-size:3rem; opacity:.3;" />
|
||||||
|
<MudText Class="mt-2 text-muted">검색 조건에 맞는 공지사항이 없습니다.</MudText>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -45,7 +53,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var item in announcements)
|
@foreach (var item in FilteredAnnouncements)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@item.Title</td>
|
<td>@item.Title</td>
|
||||||
@@ -86,6 +94,9 @@
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</MudSimpleTable>
|
</MudSimpleTable>
|
||||||
|
<MudText Typo="Typo.caption" Class="pa-2 text-muted">
|
||||||
|
검색 결과 @(FilteredAnnouncements.Count())개 · 총 @(announcements.Count)개
|
||||||
|
</MudText>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@@ -94,6 +105,12 @@
|
|||||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<Announcement>? announcements;
|
private List<Announcement>? announcements;
|
||||||
|
private string searchQuery = "";
|
||||||
|
|
||||||
|
private IEnumerable<Announcement> FilteredAnnouncements => announcements?
|
||||||
|
.Where(a => string.IsNullOrEmpty(searchQuery) ||
|
||||||
|
a.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(a => a.SortOrder) ?? Enumerable.Empty<Announcement>();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/admin/blog/create"
|
@page "/admin/blog/create"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
@using TaxBaik.Domain.Interfaces
|
@using TaxBaik.Domain.Interfaces
|
||||||
@@ -21,8 +22,8 @@
|
|||||||
|
|
||||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
<MudForm @ref="form">
|
<MudForm @ref="form">
|
||||||
<MudTextField @bind-Value="model.Title" Label="제목"
|
<MudTextField @bind-Value="model.Title" Label="제목 *"
|
||||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
Variant="Variant.Outlined" Class="mb-4" Required="true" RequiredError="제목을 입력하세요." Counter="100" MaxLength="100" />
|
||||||
|
|
||||||
<MudSelect T="int?" @bind-Value="model.CategoryId" Label="카테고리"
|
<MudSelect T="int?" @bind-Value="model.CategoryId" Label="카테고리"
|
||||||
Variant="Variant.Outlined" Class="mb-4">
|
Variant="Variant.Outlined" Class="mb-4">
|
||||||
@@ -32,8 +33,11 @@
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.Content" Label="본문"
|
<div class="mb-4">
|
||||||
Variant="Variant.Outlined" Lines="10" Class="mb-4" Required="true" />
|
<label class="d-block mb-2" style="font-weight: 500;">본문 내용 (마크다운) *</label>
|
||||||
|
<textarea id="markdown-editor" @bind="model.Content" style="display: none;"></textarea>
|
||||||
|
<div id="editor-container" style="border: 1px solid #d0d0d0; border-radius: 4px; min-height: 400px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.Tags" Label="태그 (쉼표로 구분)"
|
<MudTextField @bind-Value="model.Tags" Label="태그 (쉼표로 구분)"
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
Variant="Variant.Outlined" Class="mb-4" />
|
||||||
@@ -57,12 +61,24 @@
|
|||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private List<Domain.Entities.Category> categories = [];
|
private List<Domain.Entities.Category> categories = [];
|
||||||
private CreatePostModel model = new();
|
private CreatePostModel model = new();
|
||||||
|
private EasyMDE.Editor? editor;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IJSRuntime JS { get; set; } = null!;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
categories = (await CategoryRepository.GetAllAsync()).ToList();
|
categories = (await CategoryRepository.GetAllAsync()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("window.initMarkdownEditor", "markdown-editor", model.Content ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void GoBack()
|
private void GoBack()
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo("/taxbaik/admin/blog");
|
Navigation.NavigateTo("/taxbaik/admin/blog");
|
||||||
@@ -73,6 +89,15 @@
|
|||||||
if (form == null)
|
if (form == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// 에디터에서 최신 내용 가져오기
|
||||||
|
model.Content = await JS.InvokeAsync<string>("window.getMarkdownContent");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(model.Content))
|
||||||
|
{
|
||||||
|
Snackbar.Add("본문 내용을 입력하세요.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await form.Validate();
|
await form.Validate();
|
||||||
if (!form.IsValid)
|
if (!form.IsValid)
|
||||||
return;
|
return;
|
||||||
@@ -110,3 +135,33 @@
|
|||||||
public bool IsPublished { get; set; }
|
public bool IsPublished { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- EasyMDE 초기화 스크립트 -->
|
||||||
|
<script>
|
||||||
|
window.initMarkdownEditor = function(editorId, initialContent) {
|
||||||
|
if (!window.easyMDEInstance) {
|
||||||
|
window.easyMDEInstance = new EasyMDE({
|
||||||
|
element: document.getElementById(editorId),
|
||||||
|
spellChecker: false,
|
||||||
|
autoDownloadFontAwesome: false,
|
||||||
|
initialValue: initialContent || "",
|
||||||
|
toolbar: [
|
||||||
|
"bold", "italic", "strikethrough", "|",
|
||||||
|
"heading", "code", "|",
|
||||||
|
"unordered-list", "ordered-list", "|",
|
||||||
|
"link", "image", "table", "|",
|
||||||
|
"quote", "horizontal-rule", "|",
|
||||||
|
"preview", "side-by-side", "fullscreen", "|",
|
||||||
|
"guide"
|
||||||
|
],
|
||||||
|
previewRender: function(plainText) {
|
||||||
|
return marked.parse(plainText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getMarkdownContent = function() {
|
||||||
|
return window.easyMDEInstance ? window.easyMDEInstance.value() : "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/admin/blog/{id:int}/edit"
|
@page "/admin/blog/{id:int}/edit"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||||
@using TaxBaik.Application.DTOs
|
@using TaxBaik.Application.DTOs
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
@using TaxBaik.Domain.Interfaces
|
@using TaxBaik.Domain.Interfaces
|
||||||
@@ -32,8 +33,8 @@ else
|
|||||||
{
|
{
|
||||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
<MudForm @ref="form">
|
<MudForm @ref="form">
|
||||||
<MudTextField @bind-Value="model.Title" Label="제목"
|
<MudTextField @bind-Value="model.Title" Label="제목 *"
|
||||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
Variant="Variant.Outlined" Class="mb-4" Required="true" RequiredError="제목을 입력하세요." Counter="100" MaxLength="100" />
|
||||||
|
|
||||||
<MudSelect T="int?" @bind-Value="model.CategoryId" Label="카테고리"
|
<MudSelect T="int?" @bind-Value="model.CategoryId" Label="카테고리"
|
||||||
Variant="Variant.Outlined" Class="mb-4">
|
Variant="Variant.Outlined" Class="mb-4">
|
||||||
@@ -43,8 +44,11 @@ else
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.Content" Label="본문"
|
<div class="mb-4">
|
||||||
Variant="Variant.Outlined" Lines="10" Class="mb-4" Required="true" />
|
<label class="d-block mb-2" style="font-weight: 500;">본문 내용 (마크다운) *</label>
|
||||||
|
<textarea id="markdown-editor" @bind="model.Content" style="display: none;"></textarea>
|
||||||
|
<div id="editor-container" style="border: 1px solid #d0d0d0; border-radius: 4px; min-height: 400px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.Tags" Label="태그 (쉼표로 구분)"
|
<MudTextField @bind-Value="model.Tags" Label="태그 (쉼표로 구분)"
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
Variant="Variant.Outlined" Class="mb-4" />
|
||||||
@@ -71,6 +75,9 @@ else
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IJSRuntime JS { get; set; } = null!;
|
||||||
|
|
||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private Domain.Entities.BlogPost? post;
|
private Domain.Entities.BlogPost? post;
|
||||||
private List<Domain.Entities.Category> categories = [];
|
private List<Domain.Entities.Category> categories = [];
|
||||||
@@ -98,6 +105,14 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender && post != null)
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("window.initMarkdownEditor", "markdown-editor", model.Content ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void MapPostToModel(Domain.Entities.BlogPost post)
|
private void MapPostToModel(Domain.Entities.BlogPost post)
|
||||||
{
|
{
|
||||||
model.Title = post.Title;
|
model.Title = post.Title;
|
||||||
@@ -119,6 +134,15 @@ else
|
|||||||
if (form == null || post == null)
|
if (form == null || post == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// 에디터에서 최신 내용 가져오기
|
||||||
|
model.Content = await JS.InvokeAsync<string>("window.getMarkdownContent");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(model.Content))
|
||||||
|
{
|
||||||
|
Snackbar.Add("본문 내용을 입력하세요.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await form.Validate();
|
await form.Validate();
|
||||||
if (!form.IsValid)
|
if (!form.IsValid)
|
||||||
return;
|
return;
|
||||||
@@ -185,3 +209,33 @@ else
|
|||||||
public bool IsPublished { get; set; }
|
public bool IsPublished { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- EasyMDE 초기화 스크립트 -->
|
||||||
|
<script>
|
||||||
|
window.initMarkdownEditor = function(editorId, initialContent) {
|
||||||
|
if (!window.easyMDEInstance) {
|
||||||
|
window.easyMDEInstance = new EasyMDE({
|
||||||
|
element: document.getElementById(editorId),
|
||||||
|
spellChecker: false,
|
||||||
|
autoDownloadFontAwesome: false,
|
||||||
|
initialValue: initialContent || "",
|
||||||
|
toolbar: [
|
||||||
|
"bold", "italic", "strikethrough", "|",
|
||||||
|
"heading", "code", "|",
|
||||||
|
"unordered-list", "ordered-list", "|",
|
||||||
|
"link", "image", "table", "|",
|
||||||
|
"quote", "horizontal-rule", "|",
|
||||||
|
"preview", "side-by-side", "fullscreen", "|",
|
||||||
|
"guide"
|
||||||
|
],
|
||||||
|
previewRender: function(plainText) {
|
||||||
|
return marked.parse(plainText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getMarkdownContent = function() {
|
||||||
|
return window.easyMDEInstance ? window.easyMDEInstance.value() : "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
@page "/admin/blog"
|
@page "/admin/blog"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject IApiClient ApiClient
|
@inject IBlogBrowserClient BlogClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>블로그 관리</PageTitle>
|
<PageTitle>블로그 관리</PageTitle>
|
||||||
|
|
||||||
<section class="admin-page-hero">
|
<AdminPageHeader Title="블로그 관리" Eyebrow="Content" Subtitle="검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다.">
|
||||||
<div>
|
<ChildContent>
|
||||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Content</MudText>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.EditNote"
|
||||||
<MudText Typo="Typo.h4" Class="admin-page-title">블로그 관리</MudText>
|
Href="/taxbaik/admin/blog/create">새 포스트 작성</MudButton>
|
||||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다.</MudText>
|
</ChildContent>
|
||||||
</div>
|
</AdminPageHeader>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.EditNote"
|
|
||||||
Href="/taxbaik/admin/blog/create">새 포스트 작성</MudButton>
|
<div class="d-flex pa-4 gap-4 align-center">
|
||||||
</section>
|
<MudTextField @bind-Value="searchQuery" Placeholder="블로그 제목 또는 본문 검색..." Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudPaper Class="admin-surface mb-4" Elevation="0">
|
<MudPaper Class="admin-surface mb-4" Elevation="0">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||||
<MudText Typo="Typo.subtitle1">@($"전체 포스트 {totalPosts}개")</MudText>
|
<MudText Typo="Typo.subtitle1">@($"검색 결과 {FilteredPosts.Count()}개 / 전체 포스트 {totalPosts}개")</MudText>
|
||||||
<MudText Typo="Typo.body2">페이지 @currentPage / @totalPages</MudText>
|
<MudText Typo="Typo.body2">페이지 @currentPage / @totalPages</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<MudDataGrid Items="@posts" Striped="true" Hoverable="true" Loading="@isLoading" Class="admin-grid">
|
<MudDataGrid Items="@FilteredPosts" Striped="true" Hoverable="true" Loading="@isLoading" Class="admin-grid">
|
||||||
<Columns>
|
<Columns>
|
||||||
<PropertyColumn Property="x => x.Title" Title="제목" />
|
<PropertyColumn Property="x => x.Title" Title="제목" />
|
||||||
<PropertyColumn Property="x => x.IsPublished" Title="발행">
|
<PropertyColumn Property="x => x.IsPublished" Title="발행">
|
||||||
@@ -53,25 +55,27 @@
|
|||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<TaxBaik.Domain.Entities.BlogPost> posts = [];
|
private List<TaxBaik.Application.DTOs.BlogPostResponseDto> posts = [];
|
||||||
|
private string searchQuery = "";
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private int currentPage = 1;
|
private int currentPage = 1;
|
||||||
private int totalPages = 1;
|
private int totalPages = 1;
|
||||||
private int totalPosts = 0;
|
private int totalPosts = 0;
|
||||||
private const int PageSize = 20;
|
private const int PageSize = 20;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
private IEnumerable<TaxBaik.Application.DTOs.BlogPostResponseDto> FilteredPosts => posts
|
||||||
|
.Where(p => string.IsNullOrEmpty(searchQuery) ||
|
||||||
|
p.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(p.Content != null && p.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadPosts();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
|
||||||
await LoadPosts();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,9 +85,9 @@
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await ApiClient.GetAsync<PagedBlogResponse>($"blog/admin?page={currentPage}&pageSize={PageSize}");
|
var result = await BlogClient.GetAdminPagedAsync(currentPage, PageSize);
|
||||||
posts = result?.Data ?? [];
|
posts = result.Items.ToList();
|
||||||
totalPosts = result?.Total ?? 0;
|
totalPosts = result.Total;
|
||||||
totalPages = Math.Max(1, (int)Math.Ceiling(totalPosts / (double)PageSize));
|
totalPages = Math.Max(1, (int)Math.Ceiling(totalPosts / (double)PageSize));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -113,21 +117,21 @@
|
|||||||
await LoadPosts();
|
await LoadPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TogglePublish(TaxBaik.Domain.Entities.BlogPost post, bool isPublished)
|
private async Task TogglePublish(TaxBaik.Application.DTOs.BlogPostResponseDto post, bool isPublished)
|
||||||
{
|
{
|
||||||
var previous = post.IsPublished;
|
var previous = post.IsPublished;
|
||||||
post.IsPublished = isPublished;
|
post.IsPublished = isPublished;
|
||||||
var result = await ApiClient.PutAsync<TaxBaik.Domain.Entities.BlogPost>($"blog/{post.Id}", new
|
var result = await BlogClient.UpdateAsync(post.Id, new TaxBaik.Application.DTOs.CreateBlogPostDto
|
||||||
{
|
{
|
||||||
post.Title,
|
Title = post.Title,
|
||||||
post.Content,
|
Content = post.Content,
|
||||||
post.CategoryId,
|
CategoryId = post.CategoryId,
|
||||||
post.Tags,
|
Tags = post.Tags,
|
||||||
post.SeoTitle,
|
SeoTitle = post.SeoTitle,
|
||||||
post.SeoDescription,
|
SeoDescription = post.SeoDescription,
|
||||||
post.ThumbnailUrl,
|
ThumbnailUrl = post.ThumbnailUrl,
|
||||||
IsPublished = isPublished,
|
IsPublished = isPublished,
|
||||||
post.AuthorId
|
AuthorId = post.AuthorId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -142,14 +146,13 @@
|
|||||||
|
|
||||||
private async Task DeletePost(int postId)
|
private async Task DeletePost(int postId)
|
||||||
{
|
{
|
||||||
await ApiClient.DeleteAsync($"blog/{postId}");
|
var deleted = await BlogClient.DeleteAsync(postId);
|
||||||
|
if (!deleted)
|
||||||
|
{
|
||||||
|
Snackbar.Add("포스트 삭제에 실패했습니다.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
||||||
await LoadPosts();
|
await LoadPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PagedBlogResponse
|
|
||||||
{
|
|
||||||
public List<TaxBaik.Domain.Entities.BlogPost> Data { get; set; } = [];
|
|
||||||
public int Total { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
@page "/admin/common-codes"
|
||||||
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Domain.Entities
|
||||||
|
@attribute [Authorize]
|
||||||
|
@inject ICommonCodeBrowserClient CommonCodeClient
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>공통관리</PageTitle>
|
||||||
|
|
||||||
|
<section class="admin-page-hero">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.caption" Class="admin-eyebrow">System</MudText>
|
||||||
|
<MudText Typo="Typo.h4" Class="admin-page-title">공통관리</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="admin-page-subtitle">공통코드 그룹과 항목을 일관된 기준으로 관리합니다.</MudText>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
<MudItem XS="12" MD="4">
|
||||||
|
<MudPaper Class="admin-surface pa-4" Elevation="0">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">그룹</MudText>
|
||||||
|
<MudSelect T="string" Value="@selectedGroup" ValueChanged="OnGroupChanged" Label="코드 그룹" Variant="Variant.Outlined" FullWidth="true">
|
||||||
|
@foreach (var group in groups)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@group">@group</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
<MudButton Class="mt-3" Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate">새 코드 추가</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem XS="12" MD="8">
|
||||||
|
<MudPaper Class="admin-surface pa-4" Elevation="0">
|
||||||
|
@if (isLoading)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Indeterminate="true" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudTable Items="@codes" Dense="true" Hover="true">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>그룹</MudTh>
|
||||||
|
<MudTh>값</MudTh>
|
||||||
|
<MudTh>이름</MudTh>
|
||||||
|
<MudTh>순서</MudTh>
|
||||||
|
<MudTh>상태</MudTh>
|
||||||
|
<MudTh>작업</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.CodeGroup</MudTd>
|
||||||
|
<MudTd>@context.CodeValue</MudTd>
|
||||||
|
<MudTd>@context.CodeName</MudTd>
|
||||||
|
<MudTd>@context.SortOrder</MudTd>
|
||||||
|
<MudTd>@(context.IsActive ? "활성" : "비활성")</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Text" OnClick="@(() => EditCode(context))">수정</MudButton>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Error" OnClick="@(() => DeleteCode(context))">삭제</MudButton>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
<MudDivider Class="my-4" />
|
||||||
|
|
||||||
|
<MudForm @ref="form">
|
||||||
|
<MudTextField @bind-Value="editModel.CodeGroup" Label="그룹" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!isCreateMode)" Class="mb-3" />
|
||||||
|
<MudTextField @bind-Value="editModel.CodeValue" Label="값" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!isCreateMode)" Class="mb-3" />
|
||||||
|
<MudTextField @bind-Value="editModel.CodeName" Label="이름" Variant="Variant.Outlined" FullWidth="true" Required="true" Class="mb-3" />
|
||||||
|
<MudNumericField T="int" @bind-Value="editModel.SortOrder" Label="순서" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
|
||||||
|
<MudSwitch @bind-Checked="editModel.IsActive" Color="Color.Primary">활성</MudSwitch>
|
||||||
|
<div class="d-flex gap-2 mt-4">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveCode">저장</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" OnClick="PrepareCreate">초기화</MudButton>
|
||||||
|
</div>
|
||||||
|
</MudForm>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<string> groups = [];
|
||||||
|
private List<CommonCode> codes = [];
|
||||||
|
private string selectedGroup = "";
|
||||||
|
private bool isLoading = true;
|
||||||
|
private MudForm? form;
|
||||||
|
private CommonCode editModel = new();
|
||||||
|
private bool isCreateMode = true;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
groups = await CommonCodeClient.GetGroupsAsync();
|
||||||
|
selectedGroup = groups.FirstOrDefault() ?? "";
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGroupChanged(string value)
|
||||||
|
{
|
||||||
|
selectedGroup = value;
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadCodes()
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
codes = string.IsNullOrWhiteSpace(selectedGroup)
|
||||||
|
? []
|
||||||
|
: await CommonCodeClient.GetByGroupAsync(selectedGroup);
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrepareCreate()
|
||||||
|
{
|
||||||
|
isCreateMode = true;
|
||||||
|
editModel = new CommonCode
|
||||||
|
{
|
||||||
|
CodeGroup = selectedGroup,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditCode(CommonCode code)
|
||||||
|
{
|
||||||
|
isCreateMode = false;
|
||||||
|
editModel = new CommonCode
|
||||||
|
{
|
||||||
|
CodeGroup = code.CodeGroup,
|
||||||
|
CodeValue = code.CodeValue,
|
||||||
|
CodeName = code.CodeName,
|
||||||
|
SortOrder = code.SortOrder,
|
||||||
|
IsActive = code.IsActive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveCode()
|
||||||
|
{
|
||||||
|
if (form != null)
|
||||||
|
{
|
||||||
|
await form.Validate();
|
||||||
|
if (!form.IsValid)
|
||||||
|
{
|
||||||
|
Snackbar.Add("필수 항목을 입력하세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editModel.CodeValue.Contains(' '))
|
||||||
|
{
|
||||||
|
Snackbar.Add("code_value에는 공백을 넣을 수 없습니다.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await CommonCodeClient.UpsertAsync(editModel))
|
||||||
|
{
|
||||||
|
Snackbar.Add("저장 실패", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.Add("저장되었습니다.", Severity.Success);
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteCode(CommonCode code)
|
||||||
|
{
|
||||||
|
if (!await CommonCodeClient.DeleteAsync(code.CodeGroup, code.CodeValue))
|
||||||
|
{
|
||||||
|
Snackbar.Add("삭제 실패", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.Add("삭제되었습니다.", Severity.Success);
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/admin/contracts"
|
@page "/admin/contracts"
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@inject IContractBrowserClient ContractClient
|
@inject IContractBrowserClient ContractClient
|
||||||
@inject IClientBrowserClient ClientClient
|
@inject IClientBrowserClient ClientClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@@ -21,116 +22,126 @@
|
|||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-contract">
|
||||||
새 계약 추가
|
새 계약 추가
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<MudPaper Class="admin-surface" Elevation="0">
|
@if (contracts is null)
|
||||||
@if (contracts is null)
|
{
|
||||||
{
|
<MudProgressLinear Indeterminate="true" />
|
||||||
<MudProgressLinear Indeterminate="true" />
|
}
|
||||||
}
|
else
|
||||||
else if (contracts.Count == 0)
|
{
|
||||||
{
|
<MudGrid Spacing="2" Class="mt-2">
|
||||||
<MudAlert Severity="Severity.Info" Class="mt-4">
|
<!-- Left: Dense Grid List -->
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Description" Class="me-2" />
|
<MudItem XS="12" MD="8">
|
||||||
계약이 없습니다.
|
@if (contracts.Count == 0)
|
||||||
</MudAlert>
|
{
|
||||||
}
|
<MudAlert Severity="Severity.Info">
|
||||||
else
|
<MudIcon Icon="@Icons.Material.Filled.Description" Class="me-2" />
|
||||||
{
|
계약이 없습니다.
|
||||||
<MudDataGrid T="Contract"
|
</MudAlert>
|
||||||
Items="@contracts"
|
}
|
||||||
Dense="true"
|
else
|
||||||
Hover="true"
|
{
|
||||||
Striped="true"
|
<MudDataGrid T="Contract"
|
||||||
Virtualize="true"
|
Items="@contracts"
|
||||||
RowsPerPage="30"
|
Dense="true"
|
||||||
Class="admin-grid">
|
Hover="true"
|
||||||
<Columns>
|
Striped="true"
|
||||||
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
Virtualize="true"
|
||||||
<TemplateColumn Title="고객">
|
RowsPerPage="30"
|
||||||
<CellTemplate>
|
SelectedItem="@selectedContract"
|
||||||
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
SelectedItemChanged="OnRowSelected"
|
||||||
|
Class="admin-grid">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
||||||
|
<TemplateColumn Title="고객">
|
||||||
|
<CellTemplate>
|
||||||
|
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
||||||
|
{
|
||||||
|
@clientName
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.ContractNumber" Title="계약번호" />
|
||||||
|
<PropertyColumn Property="x => x.ServiceType" Title="서비스 유형" />
|
||||||
|
<PropertyColumn Property="x => x.MonthlyFee" Title="월 수수료" Format="C" />
|
||||||
|
<TemplateColumn Title="계약기간">
|
||||||
|
<CellTemplate>
|
||||||
|
@context.Item.StartDate.ToString("yyyy-MM-dd")
|
||||||
|
@if (context.Item.EndDate.HasValue)
|
||||||
|
{
|
||||||
|
<span>~@context.Item.EndDate.Value.ToString("yyyy-MM-dd")</span>
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="상태">
|
||||||
|
<CellTemplate>
|
||||||
|
@{
|
||||||
|
var isActive = !context.Item.EndDate.HasValue || context.Item.EndDate.Value >= DateTime.Today;
|
||||||
|
}
|
||||||
|
@if (isActive)
|
||||||
|
{
|
||||||
|
<MudChip Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">활성</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChip Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">만료</MudChip>
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="작업" Sortable="false">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
|
||||||
|
OnClick="@(async () => await DeleteContract(context.Item.Id))" />
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
</MudDataGrid>
|
||||||
|
}
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Right: Detail Form Panel (Inline Editor) -->
|
||||||
|
<MudItem XS="12" MD="4">
|
||||||
|
<MudPaper Class="pa-4 admin-surface admin-editor-panel" Elevation="0" Style="border: 1px solid var(--border-color); min-height: 400px;">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-4">
|
||||||
|
<MudText Typo="Typo.h6" Class="font-weight-bold">@(isEditMode ? "계약 상세 정보" : "새 계약 추가")</MudText>
|
||||||
|
@if (isEditMode)
|
||||||
|
{
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="PrepareCreate">
|
||||||
|
새로 작성
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<MudForm @ref="form">
|
||||||
|
<MudSelect T="int?" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" RequiredError="고객을 선택하세요." Disabled="@isEditMode">
|
||||||
|
@foreach (var client in clients)
|
||||||
{
|
{
|
||||||
<MudLink Href="@($"/taxbaik/admin/clients/{context.Item.ClientId}")" Color="Color.Primary">
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
@clientName
|
|
||||||
</MudLink>
|
|
||||||
}
|
}
|
||||||
</CellTemplate>
|
</MudSelect>
|
||||||
</TemplateColumn>
|
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
||||||
<PropertyColumn Property="x => x.ContractNumber" Title="계약번호" />
|
<CommonCodeSelect @bind-Value="contractForm.ServiceType" Group="CONTRACT_SERVICE_TYPE" Label="서비스 유형" Class="mb-3" Required="true" />
|
||||||
<PropertyColumn Property="x => x.ServiceType" Title="서비스 유형" />
|
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
||||||
<PropertyColumn Property="x => x.MonthlyFee" Title="월 수수료" Format="C" />
|
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="@true" Class="mb-4" />
|
||||||
<TemplateColumn Title="계약기간">
|
|
||||||
<CellTemplate>
|
<div class="d-flex justify-end gap-2">
|
||||||
@context.Item.StartDate.ToString("yyyy-MM-dd")
|
@if (isEditMode)
|
||||||
@if (context.Item.EndDate.HasValue)
|
|
||||||
{
|
{
|
||||||
<span>~@context.Item.EndDate.Value.ToString("yyyy-MM-dd")</span>
|
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="@(async () => await DeleteContract(selectedContract?.Id ?? 0))">삭제</MudButton>
|
||||||
}
|
|
||||||
</CellTemplate>
|
|
||||||
</TemplateColumn>
|
|
||||||
<TemplateColumn Title="상태">
|
|
||||||
<CellTemplate>
|
|
||||||
@{
|
|
||||||
var isActive = !context.Item.EndDate.HasValue || context.Item.EndDate.Value >= DateTime.Today;
|
|
||||||
}
|
|
||||||
@if (isActive)
|
|
||||||
{
|
|
||||||
<MudChip Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">활성</MudChip>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudChip Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">만료</MudChip>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveContract" id="btn-save-contract">저장</MudButton>
|
||||||
}
|
}
|
||||||
</CellTemplate>
|
</div>
|
||||||
</TemplateColumn>
|
</MudForm>
|
||||||
<TemplateColumn Title="작업" Sortable="false">
|
</MudPaper>
|
||||||
<CellTemplate>
|
</MudItem>
|
||||||
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
</MudGrid>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
|
}
|
||||||
OnClick="@(async () => await DeleteContract(context.Item.Id))" />
|
|
||||||
</MudButtonGroup>
|
|
||||||
</CellTemplate>
|
|
||||||
</TemplateColumn>
|
|
||||||
</Columns>
|
|
||||||
</MudDataGrid>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
<!-- Create Dialog -->
|
|
||||||
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
|
|
||||||
<TitleContent>
|
|
||||||
<MudText Typo="Typo.h6">새 계약 추가</MudText>
|
|
||||||
</TitleContent>
|
|
||||||
<DialogContent>
|
|
||||||
<MudForm @ref="form">
|
|
||||||
<MudSelect T="int?" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
|
|
||||||
@foreach (var client in clients)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
|
||||||
<MudSelect T="string" @bind-Value="contractForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
|
||||||
<MudSelectItem Value="@("개인 기장대리")">개인 기장대리</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("법인 기장대리")">법인 기장대리</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("세무조정 대행")">세무조정 대행</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("양도세 신고대리")">양도세 신고대리</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("상속·증여 자문")">상속·증여 자문</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("세무조사 대응")">세무조사 대응</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
|
||||||
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
|
||||||
</MudForm>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<MudButton OnClick="CloseDialog">취소</MudButton>
|
|
||||||
<MudButton Color="Color.Primary" OnClick="SaveContract">저장</MudButton>
|
|
||||||
</DialogActions>
|
|
||||||
</MudDialog>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
@@ -141,21 +152,19 @@
|
|||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
private decimal mrr = 0;
|
private decimal mrr = 0;
|
||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private bool isDialogOpen;
|
private bool isEditMode;
|
||||||
|
private Contract? selectedContract;
|
||||||
private ContractForm contractForm = new();
|
private ContractForm contractForm = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadData();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
PrepareCreate();
|
||||||
{
|
|
||||||
await LoadData();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,14 +185,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenCreateDialog()
|
private void PrepareCreate()
|
||||||
{
|
{
|
||||||
|
selectedContract = null;
|
||||||
|
isEditMode = false;
|
||||||
contractForm = new ContractForm
|
contractForm = new ContractForm
|
||||||
{
|
{
|
||||||
ClientId = clients.FirstOrDefault()?.Id,
|
ClientId = clients.FirstOrDefault()?.Id,
|
||||||
StartDate = DateTime.Today
|
StartDate = DateTime.Today
|
||||||
};
|
};
|
||||||
isDialogOpen = true;
|
}
|
||||||
|
|
||||||
|
private void OnRowSelected(Contract contract)
|
||||||
|
{
|
||||||
|
if (contract == null) return;
|
||||||
|
selectedContract = contract;
|
||||||
|
isEditMode = true;
|
||||||
|
contractForm = new ContractForm
|
||||||
|
{
|
||||||
|
ClientId = contract.ClientId,
|
||||||
|
ContractNumber = contract.ContractNumber,
|
||||||
|
ServiceType = contract.ServiceType,
|
||||||
|
StartDate = contract.StartDate,
|
||||||
|
MonthlyFee = contract.MonthlyFee
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveContract()
|
private async Task SaveContract()
|
||||||
@@ -211,7 +236,7 @@
|
|||||||
if (newId > 0)
|
if (newId > 0)
|
||||||
{
|
{
|
||||||
Snackbar.Add("계약이 추가되었습니다.", Severity.Success);
|
Snackbar.Add("계약이 추가되었습니다.", Severity.Success);
|
||||||
CloseDialog();
|
PrepareCreate();
|
||||||
await LoadData();
|
await LoadData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,6 +264,10 @@
|
|||||||
{
|
{
|
||||||
await ContractClient.DeleteAsync(id);
|
await ContractClient.DeleteAsync(id);
|
||||||
Snackbar.Add("계약이 삭제되었습니다.", Severity.Success);
|
Snackbar.Add("계약이 삭제되었습니다.", Severity.Success);
|
||||||
|
if (selectedContract?.Id == id)
|
||||||
|
{
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
await LoadData();
|
await LoadData();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -247,18 +276,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseDialog()
|
|
||||||
{
|
|
||||||
isDialogOpen = false;
|
|
||||||
contractForm = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetClientDisplayName(Client client)
|
private static string GetClientDisplayName(Client client)
|
||||||
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
? client.CompanyName
|
? client.CompanyName
|
||||||
: !string.IsNullOrWhiteSpace(client.Name)
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
? client.Name
|
? client.Name
|
||||||
: $"Client #{client.Id}";
|
: $"Client #{client.Id}";
|
||||||
|
|
||||||
private class ContractForm
|
private class ContractForm
|
||||||
{
|
{
|
||||||
public int? ClientId { get; set; }
|
public int? ClientId { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/admin/dashboard"
|
@page "/admin/dashboard"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@inject IAdminDashboardClient DashboardClient
|
@inject IAdminDashboardClient DashboardClient
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
@@ -17,49 +18,58 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Metrics Grid - Pure HTML div instead of MudGrid to ensure proper layout -->
|
@if (!string.IsNullOrEmpty(errorMessage))
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Error" Class="mb-4">@errorMessage</MudAlert>
|
||||||
|
}
|
||||||
|
@if (isLoading)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-4" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Metrics Grid -->
|
||||||
<div class="admin-metric-grid">
|
<div class="admin-metric-grid">
|
||||||
<div class="admin-metric-card accent-blue cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries"))' style="cursor: pointer;">
|
<div class="admin-metric-card accent-blue cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries"))'>
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px; height: 100%;">
|
<div class="admin-metric-card-body">
|
||||||
<span style="font-size: 0.75rem; color: #999; text-transform: uppercase; font-weight: 600;">이번달 문의</span>
|
<span class="admin-metric-card-label">이번달 문의</span>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex: 1;">
|
<div class="admin-metric-card-value-row">
|
||||||
<span style="font-size: 2rem; font-weight: 700; color: #1565c0;">@summary.ThisMonthInquiries</span>
|
<span class="admin-metric-card-value" style="color: var(--primary-dark);">@summary.ThisMonthInquiries</span>
|
||||||
<span style="font-size: 2.5rem; opacity: 0.15; color: #1976d2;">💬</span>
|
<span class="admin-metric-card-icon" style="color: var(--primary-color);">💬</span>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size: 0.9rem; color: #666;">월간 상담 유입 (클릭 시 이동)</span>
|
<span class="admin-metric-card-caption">월간 상담 유입 (클릭 시 이동)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-metric-card accent-amber cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries?status=new"))' style="cursor: pointer;">
|
<div class="admin-metric-card accent-amber cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/inquiries?status=new"))'>
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px; height: 100%;">
|
<div class="admin-metric-card-body">
|
||||||
<span style="font-size: 0.75rem; color: #999; text-transform: uppercase; font-weight: 600;">신규 문의</span>
|
<span class="admin-metric-card-label">신규 문의</span>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex: 1;">
|
<div class="admin-metric-card-value-row">
|
||||||
<span style="font-size: 2rem; font-weight: 700; color: #e65100;">@summary.NewInquiries</span>
|
<span class="admin-metric-card-value" style="color: var(--tertiary-dark);">@summary.NewInquiries</span>
|
||||||
<span style="font-size: 2.5rem; opacity: 0.15; color: #f57c00;">⚠️</span>
|
<span class="admin-metric-card-icon" style="color: var(--tertiary-color);">⚠️</span>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size: 0.9rem; color: #666;">처리 대기 (클릭 시 이동)</span>
|
<span class="admin-metric-card-caption">처리 대기 (클릭 시 이동)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-metric-card accent-slate cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))' style="cursor: pointer;">
|
<div class="admin-metric-card accent-slate cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))'>
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px; height: 100%;">
|
<div class="admin-metric-card-body">
|
||||||
<span style="font-size: 0.75rem; color: #999; text-transform: uppercase; font-weight: 600;">전체 포스트</span>
|
<span class="admin-metric-card-label">전체 포스트</span>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex: 1;">
|
<div class="admin-metric-card-value-row">
|
||||||
<span style="font-size: 2rem; font-weight: 700; color: #455a64;">@summary.TotalPosts</span>
|
<span class="admin-metric-card-value" style="color: #455a64;">@summary.TotalPosts</span>
|
||||||
<span style="font-size: 2.5rem; opacity: 0.15; color: #607d8b;">📄</span>
|
<span class="admin-metric-card-icon" style="color: #607d8b;">📄</span>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size: 0.9rem; color: #666;">콘텐츠 자산 (클릭 시 이동)</span>
|
<span class="admin-metric-card-caption">콘텐츠 자산 (클릭 시 이동)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-metric-card accent-green cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))' style="cursor: pointer;">
|
<div class="admin-metric-card accent-green cursor-pointer" @onclick='(() => Nav.NavigateTo("/taxbaik/admin/blog"))'>
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px; height: 100%;">
|
<div class="admin-metric-card-body">
|
||||||
<span style="font-size: 0.75rem; color: #999; text-transform: uppercase; font-weight: 600;">발행된 포스트</span>
|
<span class="admin-metric-card-label">발행된 포스트</span>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; flex: 1;">
|
<div class="admin-metric-card-value-row">
|
||||||
<span style="font-size: 2rem; font-weight: 700; color: #2e7d32;">@summary.PublishedPosts</span>
|
<span class="admin-metric-card-value" style="color: var(--secondary-dark);">@summary.PublishedPosts</span>
|
||||||
<span style="font-size: 2.5rem; opacity: 0.15; color: #388e3c;">🌐</span>
|
<span class="admin-metric-card-icon" style="color: var(--secondary-color);">🌐</span>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size: 0.9rem; color: #666;">검색 노출 대상 (클릭 시 이동)</span>
|
<span class="admin-metric-card-caption">검색 노출 대상 (클릭 시 이동)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +96,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var f in upcomingFilings)
|
@foreach (var f in upcomingFilings)
|
||||||
{
|
{
|
||||||
var dday = (f.DueDate.Date - DateTime.Today).Days;
|
var dday = BusinessDayCalculator.GetDday(DateOnly.FromDateTime(f.DueDate));
|
||||||
|
var effectiveDueDate = BusinessDayCalculator.GetEffectiveDueDate(DateOnly.FromDateTime(f.DueDate));
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<MudLink Href="@($"/taxbaik/admin/clients/{f.ClientId}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
<MudLink Href="@($"/taxbaik/admin/clients/{f.ClientId}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
||||||
@@ -94,7 +105,7 @@
|
|||||||
</MudLink>
|
</MudLink>
|
||||||
</td>
|
</td>
|
||||||
<td>@f.FilingType</td>
|
<td>@f.FilingType</td>
|
||||||
<td>@f.DueDate.ToString("yyyy-MM-dd")</td>
|
<td>@effectiveDueDate.ToDateTime(TimeOnly.MinValue).ToString("yyyy-MM-dd")</td>
|
||||||
<td>
|
<td>
|
||||||
@if (dday < 0)
|
@if (dday < 0)
|
||||||
{
|
{
|
||||||
@@ -166,35 +177,30 @@
|
|||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
try
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
{
|
||||||
try
|
var summaryTask = DashboardClient.GetSummaryAsync();
|
||||||
{
|
var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30);
|
||||||
// API 클라이언트 사용 (서비스 직접 호출 X)
|
|
||||||
var summaryTask = DashboardClient.GetSummaryAsync();
|
|
||||||
var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30);
|
|
||||||
|
|
||||||
await Task.WhenAll(summaryTask, filingsTask);
|
await Task.WhenAll(summaryTask, filingsTask);
|
||||||
summary = await summaryTask;
|
summary = await summaryTask;
|
||||||
upcomingFilings = (await filingsTask).ToList();
|
upcomingFilings = (await filingsTask).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errorMessage = "대시보드 데이터를 불러올 수 없습니다.";
|
errorMessage = "대시보드 데이터를 불러올 수 없습니다.";
|
||||||
Console.Error.WriteLine($"Dashboard error: {ex.Message}");
|
Console.Error.WriteLine($"Dashboard error: {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,16 +22,21 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div class="d-flex pa-4 gap-4 align-center">
|
||||||
|
<MudTextField @bind-Value="searchQuery" Placeholder="질문 또는 답변 검색..." Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="flex-grow-1" Immediate="true" Clearable="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudPaper Class="admin-surface" Elevation="0">
|
<MudPaper Class="admin-surface" Elevation="0">
|
||||||
@if (faqs is null)
|
@if (faqs is null)
|
||||||
{
|
{
|
||||||
<MudProgressLinear Indeterminate="true" />
|
<MudProgressLinear Indeterminate="true" />
|
||||||
}
|
}
|
||||||
else if (!faqs.Any())
|
else if (!FilteredFaqs.Any())
|
||||||
{
|
{
|
||||||
<div class="pa-6 text-center">
|
<div class="pa-6 text-center">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.QuestionAnswer" Style="font-size:3rem; opacity:.3;" />
|
<MudIcon Icon="@Icons.Material.Filled.QuestionAnswer" Style="font-size:3rem; opacity:.3;" />
|
||||||
<MudText Class="mt-2 text-muted">등록된 FAQ가 없습니다.</MudText>
|
<MudText Class="mt-2 text-muted">검색 조건에 맞는 FAQ가 없습니다.</MudText>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -39,7 +44,7 @@
|
|||||||
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:60px;">순서</th>
|
<th style="width:110px;">순서</th>
|
||||||
<th>질문</th>
|
<th>질문</th>
|
||||||
<th style="width:130px;">카테고리</th>
|
<th style="width:130px;">카테고리</th>
|
||||||
<th style="width:90px;">상태</th>
|
<th style="width:90px;">상태</th>
|
||||||
@@ -47,11 +52,15 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var item in faqs)
|
@foreach (var item in FilteredFaqs)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<MudText Typo="Typo.body2">@item.SortOrder</MudText>
|
<div class="d-flex align-center justify-start gap-1">
|
||||||
|
<MudText Typo="Typo.body2" Class="mr-2">@item.SortOrder</MudText>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowDropUp" Size="Size.Small" OnClick="@(() => MoveUpAsync(item))" Style="padding:2px;" Dense="true" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowDropDown" Size="Size.Small" OnClick="@(() => MoveDownAsync(item))" Style="padding:2px;" Dense="true" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<MudText Typo="Typo.body2" Style="max-width:480px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
<MudText Typo="Typo.body2" Style="max-width:480px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
||||||
@@ -77,10 +86,10 @@
|
|||||||
<td>
|
<td>
|
||||||
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
||||||
<MudButton @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/faqs/{item.Id}/edit"))">
|
<MudButton @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/faqs/{item.Id}/edit"))">
|
||||||
수정
|
수정
|
||||||
</MudButton>
|
</MudButton>
|
||||||
<MudButton Color="Color.Error" @onclick="@(() => DeleteAsync(item))">
|
<MudButton Color="Color.Error" @onclick="@(() => DeleteAsync(item))">
|
||||||
삭제
|
삭제
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudButtonGroup>
|
</MudButtonGroup>
|
||||||
</td>
|
</td>
|
||||||
@@ -89,7 +98,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</MudSimpleTable>
|
</MudSimpleTable>
|
||||||
<MudText Typo="Typo.caption" Class="pa-2 text-muted">
|
<MudText Typo="Typo.caption" Class="pa-2 text-muted">
|
||||||
총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개
|
검색 결과 @(FilteredFaqs.Count())개 · 총 @(faqs.Count)개 · 노출 중 @(faqs.Count(f => f.IsActive))개
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -99,6 +108,13 @@
|
|||||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<Faq>? faqs;
|
private List<Faq>? faqs;
|
||||||
|
private string searchQuery = "";
|
||||||
|
|
||||||
|
private IEnumerable<Faq> FilteredFaqs => faqs?
|
||||||
|
.Where(f => string.IsNullOrEmpty(searchQuery) ||
|
||||||
|
f.Question.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(f.Answer != null && f.Answer.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(f => f.SortOrder) ?? Enumerable.Empty<Faq>();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -120,7 +136,7 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
faqs = (await FaqClient.GetAllAsync()).ToList();
|
faqs = (await FaqClient.GetAllAsync()).OrderBy(f => f.SortOrder).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -129,6 +145,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task MoveUpAsync(Faq item)
|
||||||
|
{
|
||||||
|
if (faqs == null) return;
|
||||||
|
var sorted = faqs.OrderBy(f => f.SortOrder).ToList();
|
||||||
|
var index = sorted.IndexOf(item);
|
||||||
|
if (index <= 0) return;
|
||||||
|
|
||||||
|
var prev = sorted[index - 1];
|
||||||
|
var temp = item.SortOrder;
|
||||||
|
item.SortOrder = prev.SortOrder;
|
||||||
|
prev.SortOrder = temp;
|
||||||
|
|
||||||
|
if (item.SortOrder == prev.SortOrder)
|
||||||
|
{
|
||||||
|
prev.SortOrder = item.SortOrder + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FaqClient.UpdateAsync(item.Id, item);
|
||||||
|
await FaqClient.UpdateAsync(prev.Id, prev);
|
||||||
|
Snackbar.Add("순서가 상향되었습니다.", Severity.Success);
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"순서 조정 실패: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MoveDownAsync(Faq item)
|
||||||
|
{
|
||||||
|
if (faqs == null) return;
|
||||||
|
var sorted = faqs.OrderBy(f => f.SortOrder).ToList();
|
||||||
|
var index = sorted.IndexOf(item);
|
||||||
|
if (index < 0 || index >= sorted.Count - 1) return;
|
||||||
|
|
||||||
|
var next = sorted[index + 1];
|
||||||
|
var temp = item.SortOrder;
|
||||||
|
item.SortOrder = next.SortOrder;
|
||||||
|
next.SortOrder = temp;
|
||||||
|
|
||||||
|
if (item.SortOrder == next.SortOrder)
|
||||||
|
{
|
||||||
|
next.SortOrder = item.SortOrder + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FaqClient.UpdateAsync(item.Id, item);
|
||||||
|
await FaqClient.UpdateAsync(next.Id, next);
|
||||||
|
Snackbar.Add("순서가 하향되었습니다.", Severity.Success);
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"순서 조정 실패: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task DeleteAsync(Faq item)
|
private async Task DeleteAsync(Faq item)
|
||||||
{
|
{
|
||||||
var confirmed = await DialogService.ShowMessageBox(
|
var confirmed = await DialogService.ShowMessageBox(
|
||||||
|
|||||||
@@ -5,15 +5,12 @@
|
|||||||
|
|
||||||
<PageTitle>문의 관리</PageTitle>
|
<PageTitle>문의 관리</PageTitle>
|
||||||
|
|
||||||
<section class="admin-page-hero">
|
<AdminPageHeader Title="문의 관리" Eyebrow="Customer Requests" Subtitle="상담 요청을 상태별로 확인하고 후속 조치를 기록합니다.">
|
||||||
<div>
|
<ChildContent>
|
||||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Customer Requests</MudText>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||||
<MudText Typo="Typo.h4" Class="admin-page-title">문의 관리</MudText>
|
Href="/taxbaik/admin/inquiries/create">새 문의 등록</MudButton>
|
||||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">상담 요청을 상태별로 확인하고 후속 조치를 기록합니다.</MudText>
|
</ChildContent>
|
||||||
</div>
|
</AdminPageHeader>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
Href="/taxbaik/admin/inquiries/create">새 문의 등록</MudButton>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<MudPaper Class="admin-surface" Elevation="0">
|
<MudPaper Class="admin-surface" Elevation="0">
|
||||||
@if (isLoading)
|
@if (isLoading)
|
||||||
@@ -52,18 +49,14 @@ else
|
|||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private IReadOnlyList<Domain.Entities.Inquiry> allInquiries = [];
|
private IReadOnlyList<Domain.Entities.Inquiry> allInquiries = [];
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadData();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
|
||||||
await LoadData();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
@page "/admin/login"
|
@page "/admin/login"
|
||||||
@using System.ComponentModel.DataAnnotations
|
|
||||||
@layout TaxBaik.Web.Components.Admin.Layout.BlankLayout
|
@layout TaxBaik.Web.Components.Admin.Layout.BlankLayout
|
||||||
@attribute [AllowAnonymous]
|
@attribute [AllowAnonymous]
|
||||||
|
@rendermode @(new InteractiveServerRenderMode(prerender: true))
|
||||||
@inject IApiClient ApiClient
|
@inject IApiClient ApiClient
|
||||||
@inject NavigationManager NavigationManager
|
|
||||||
@inject CustomAuthenticationStateProvider AuthStateProvider
|
|
||||||
@inject IJSRuntime Js
|
|
||||||
@inject ILocalStorageService LocalStorageService
|
@inject ILocalStorageService LocalStorageService
|
||||||
|
@inject IJSRuntime Js
|
||||||
|
|
||||||
<PageTitle>로그인</PageTitle>
|
<PageTitle>로그인</PageTitle>
|
||||||
|
|
||||||
@@ -14,52 +12,39 @@
|
|||||||
<MudPaper Class="pa-8" Elevation="3" Style="width: 100%; max-width: 400px;">
|
<MudPaper Class="pa-8" Elevation="3" Style="width: 100%; max-width: 400px;">
|
||||||
<MudText Typo="Typo.h4" Class="mb-6 text-center">관리자 로그인</MudText>
|
<MudText Typo="Typo.h4" Class="mb-6 text-center">관리자 로그인</MudText>
|
||||||
|
|
||||||
<form @onsubmit="HandleLogin" @onsubmit:preventDefault>
|
<form id="admin-login-form">
|
||||||
<InputText class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
<input class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
||||||
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
||||||
placeholder="사용자명"
|
placeholder="사용자명"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
@bind-Value="model.Username" />
|
name="username"
|
||||||
|
value="@model.Username" />
|
||||||
|
|
||||||
<InputText type="password"
|
<input type="password"
|
||||||
class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
class="mud-input mud-input-outlined mud-input-root mud-input-root-adorned-start mb-4"
|
||||||
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
style="width: 100%; min-height: 56px; padding: 16px 14px;"
|
||||||
placeholder="비밀번호"
|
placeholder="비밀번호"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
@bind-Value="model.Password" />
|
name="password" />
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<InputCheckbox class="mud-checkbox" @bind-Value="model.RememberMe" />
|
<input class="mud-checkbox" type="checkbox" name="rememberMe" />
|
||||||
<label style="margin-left: 8px; cursor: pointer;">아이디 저장</label>
|
<label style="margin-left: 8px; cursor: pointer;">아이디 저장</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(errorMessage))
|
<div class="mud-alert mud-alert-filled-error mb-4 login-error-message" style="display:none;">로그인 중 오류가 발생했습니다.</div>
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Error" Class="mb-4">@errorMessage</MudAlert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-elevation-0"
|
class="mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-elevation-0"
|
||||||
style="width: 100%; min-height: 52px; border: 0; border-radius: 4px; color: white;"
|
style="width: 100%; min-height: 52px; border: 0; border-radius: 4px; color: white;">
|
||||||
disabled="@isLoading">
|
<span>로그인</span>
|
||||||
@if (isLoading)
|
|
||||||
{
|
|
||||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
|
||||||
<span>로그인 중...</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>로그인</span>
|
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private bool isLoading = false;
|
private readonly LoginModel model = new();
|
||||||
private string errorMessage = "";
|
|
||||||
private LoginModel model = new();
|
|
||||||
private const string RememberedUsernameKey = "admin-remembered-username";
|
private const string RememberedUsernameKey = "admin-remembered-username";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -70,12 +55,11 @@
|
|||||||
if (!string.IsNullOrEmpty(remembered))
|
if (!string.IsNullOrEmpty(remembered))
|
||||||
{
|
{
|
||||||
model.Username = remembered;
|
model.Username = remembered;
|
||||||
model.RememberMe = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// LocalStorage not available in pre-render
|
// LocalStorage may be unavailable during prerender.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,75 +69,10 @@
|
|||||||
await Js.InvokeVoidAsync("taxbaikAdminSession.syncRouteClass");
|
await Js.InvokeVoidAsync("taxbaikAdminSession.syncRouteClass");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleLogin()
|
|
||||||
{
|
|
||||||
if (isLoading)
|
|
||||||
return;
|
|
||||||
|
|
||||||
isLoading = true;
|
|
||||||
errorMessage = "";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = new { model.Username, model.Password };
|
|
||||||
var response = await ApiClient.PostAsync<LoginResponse>("auth/login", request);
|
|
||||||
|
|
||||||
if (response?.AccessToken == null || response?.RefreshToken == null)
|
|
||||||
{
|
|
||||||
errorMessage = "사용자명 또는 비밀번호가 올바르지 않습니다.";
|
|
||||||
isLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.RememberMe)
|
|
||||||
{
|
|
||||||
await LocalStorageService.SetItemAsStringAsync(RememberedUsernameKey, model.Username);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await LocalStorageService.RemoveItemAsync(RememberedUsernameKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ApiClient.SetAuthToken(response.AccessToken);
|
|
||||||
await AuthStateProvider.LoginAsync(response.AccessToken, response.RefreshToken, response.ExpiresIn);
|
|
||||||
NavigationManager.NavigateTo(GetReturnUrl(), forceLoad: false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
errorMessage = "로그인 중 오류가 발생했습니다.";
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoginResponse
|
|
||||||
{
|
|
||||||
public string AccessToken { get; set; } = "";
|
|
||||||
public string RefreshToken { get; set; } = "";
|
|
||||||
public int ExpiresIn { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoginModel
|
private class LoginModel
|
||||||
{
|
{
|
||||||
public string Username { get; set; } = "";
|
public string Username { get; set; } = "";
|
||||||
public string Password { get; set; } = "";
|
public string Password { get; set; } = "";
|
||||||
public bool RememberMe { get; set; }
|
public bool RememberMe { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetReturnUrl()
|
|
||||||
{
|
|
||||||
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
|
|
||||||
if (!Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out var returnUrl)
|
|
||||||
|| string.IsNullOrWhiteSpace(returnUrl))
|
|
||||||
{
|
|
||||||
return "/taxbaik/admin/dashboard";
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = returnUrl.ToString();
|
|
||||||
if (!value.StartsWith("admin", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return "/taxbaik/admin/dashboard";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"/taxbaik/{value.TrimStart('/')}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@page "/admin/tax-filing-schedules"
|
@page "/admin/tax-filing-schedules"
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Domain.Entities
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@inject ITaxFilingScheduleBrowserClient TaxFilingClient
|
@inject ITaxFilingScheduleBrowserClient TaxFilingClient
|
||||||
@inject IClientBrowserClient ClientClient
|
@inject IClientBrowserClient ClientClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@@ -14,141 +16,155 @@
|
|||||||
<MudText Typo="Typo.h4" Class="admin-page-title">신고 일정</MudText>
|
<MudText Typo="Typo.h4" Class="admin-page-title">신고 일정</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 마감일과 처리 상태를 한 화면에서 관리합니다.</MudText>
|
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 마감일과 처리 상태를 한 화면에서 관리합니다.</MudText>
|
||||||
</div>
|
</div>
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-schedule">
|
||||||
Color="Color.Primary"
|
|
||||||
OnClick="OpenCreateDialog"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add">
|
|
||||||
새 일정 추가
|
새 일정 추가
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<MudPaper Class="admin-surface" Elevation="0">
|
@if (schedules is null)
|
||||||
@if (schedules is null)
|
{
|
||||||
{
|
<MudProgressLinear Indeterminate="true" />
|
||||||
<MudProgressLinear Indeterminate="true" />
|
}
|
||||||
}
|
else
|
||||||
else if (schedules.Count == 0)
|
{
|
||||||
{
|
<MudGrid Spacing="2" Class="mt-2">
|
||||||
<MudAlert Severity="Severity.Info" Class="mt-4">
|
<!-- Left: Dense Grid List -->
|
||||||
<MudIcon Icon="@Icons.Material.Filled.EventBusy" Class="me-2" />
|
<MudItem XS="12" MD="8">
|
||||||
신고 일정이 없습니다.
|
@if (schedules.Count == 0)
|
||||||
</MudAlert>
|
{
|
||||||
}
|
<MudAlert Severity="Severity.Info">
|
||||||
else
|
<MudIcon Icon="@Icons.Material.Filled.EventBusy" Class="me-2" />
|
||||||
{
|
신고 일정이 없습니다.
|
||||||
<MudDataGrid T="TaxFilingSchedule"
|
</MudAlert>
|
||||||
Items="@schedules"
|
}
|
||||||
Dense="true"
|
else
|
||||||
Hover="true"
|
{
|
||||||
Striped="true"
|
<MudDataGrid T="TaxFilingSchedule"
|
||||||
Virtualize="true"
|
Items="@schedules"
|
||||||
RowsPerPage="30"
|
Dense="true"
|
||||||
Class="admin-grid">
|
Hover="true"
|
||||||
<Columns>
|
Striped="true"
|
||||||
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
Virtualize="true"
|
||||||
<TemplateColumn Title="고객">
|
RowsPerPage="30"
|
||||||
<CellTemplate>
|
SelectedItem="@selectedSchedule"
|
||||||
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
SelectedItemChanged="OnRowSelected"
|
||||||
|
Class="admin-grid">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
||||||
|
<TemplateColumn Title="고객">
|
||||||
|
<CellTemplate>
|
||||||
|
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
||||||
|
{
|
||||||
|
@clientName
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.FilingType" Title="신고 유형" />
|
||||||
|
<TemplateColumn Title="마감일">
|
||||||
|
<CellTemplate>
|
||||||
|
@{
|
||||||
|
var effectiveDueDate = BusinessDayCalculator.GetEffectiveDueDate(DateOnly.FromDateTime(context.Item.DueDate));
|
||||||
|
var daysLeft = BusinessDayCalculator.GetDday(DateOnly.FromDateTime(context.Item.DueDate));
|
||||||
|
var statusColor = daysLeft < 0 ? Color.Error : daysLeft <= 7 ? Color.Warning : Color.Success;
|
||||||
|
}
|
||||||
|
<MudChip Size="Size.Small" Color="@statusColor" Variant="Variant.Filled">
|
||||||
|
@effectiveDueDate.ToDateTime(TimeOnly.MinValue).ToString("yyyy-MM-dd")
|
||||||
|
@if (daysLeft >= 0)
|
||||||
|
{
|
||||||
|
<span class="ms-1">(D-@daysLeft)</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="ms-1">(마감 @Math.Abs(daysLeft)일 경과)</span>
|
||||||
|
}
|
||||||
|
</MudChip>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.FilingYear" Title="신고연도" />
|
||||||
|
<TemplateColumn Title="상태">
|
||||||
|
<CellTemplate>
|
||||||
|
@if (context.Item.Status == "completed")
|
||||||
|
{
|
||||||
|
<MudChip Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">완료</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChip Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">대기</MudChip>
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="작업" Sortable="false">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
||||||
|
@if (context.Item.Status != "completed")
|
||||||
|
{
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.CheckCircle"
|
||||||
|
Color="Color.Success"
|
||||||
|
OnClick="@(async () => await CompleteSchedule(context.Item.Id))"
|
||||||
|
Title="완료" />
|
||||||
|
}
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Color="Color.Error"
|
||||||
|
OnClick="@(async () => await DeleteSchedule(context.Item.Id))"
|
||||||
|
Title="삭제" />
|
||||||
|
</MudButtonGroup>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
</MudDataGrid>
|
||||||
|
}
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Right: Detail Form Panel (Inline Editor) -->
|
||||||
|
<MudItem XS="12" MD="4">
|
||||||
|
<MudPaper Class="pa-4 admin-surface admin-editor-panel" Elevation="0" Style="border: 1px solid var(--border-color); min-height: 400px;">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-4">
|
||||||
|
<MudText Typo="Typo.h6" Class="font-weight-bold">@(isEditMode ? "신고 일정 상세" : "새 신고 일정 추가")</MudText>
|
||||||
|
@if (isEditMode)
|
||||||
|
{
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="PrepareCreate">
|
||||||
|
새로 작성
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<MudForm @ref="form">
|
||||||
|
<MudSelect T="int?"
|
||||||
|
@bind-Value="scheduleForm.ClientId"
|
||||||
|
Label="고객"
|
||||||
|
Required="true"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
FullWidth="@true"
|
||||||
|
Class="mb-3"
|
||||||
|
RequiredError="고객을 선택하세요."
|
||||||
|
Disabled="@isEditMode">
|
||||||
|
@foreach (var client in clients)
|
||||||
{
|
{
|
||||||
<MudLink Href="@($"/taxbaik/admin/clients/{context.Item.ClientId}")" Color="Color.Primary">
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
@clientName
|
|
||||||
</MudLink>
|
|
||||||
}
|
}
|
||||||
</CellTemplate>
|
</MudSelect>
|
||||||
</TemplateColumn>
|
<CommonCodeSelect @bind-Value="scheduleForm.FilingType" Group="FILING_TYPE" Label="신고 유형" Class="mb-3" Required="true" />
|
||||||
<PropertyColumn Property="x => x.FilingType" Title="신고 유형" />
|
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
||||||
<TemplateColumn Title="마감일">
|
<MudNumericField T="int" @bind-Value="scheduleForm.FilingYear" Label="신고연도" Variant="Variant.Outlined" FullWidth="@true" Class="mb-4" Required="true" />
|
||||||
<CellTemplate>
|
|
||||||
@{
|
<div class="d-flex justify-end gap-2">
|
||||||
var daysLeft = (context.Item.DueDate.Date - DateTime.Today).Days;
|
@if (isEditMode && selectedSchedule?.Status != "completed")
|
||||||
var statusColor = daysLeft < 0 ? Color.Error : daysLeft <= 7 ? Color.Warning : Color.Success;
|
|
||||||
}
|
|
||||||
<MudChip Size="Size.Small" Color="@statusColor" Variant="Variant.Filled">
|
|
||||||
@context.Item.DueDate.ToString("yyyy-MM-dd")
|
|
||||||
@if (daysLeft >= 0)
|
|
||||||
{
|
|
||||||
<span class="ms-1">(D-@daysLeft)</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="ms-1">(마감 @Math.Abs(daysLeft)일 경과)</span>
|
|
||||||
}
|
|
||||||
</MudChip>
|
|
||||||
</CellTemplate>
|
|
||||||
</TemplateColumn>
|
|
||||||
<PropertyColumn Property="x => x.FilingYear" Title="신고연도" />
|
|
||||||
<TemplateColumn Title="상태">
|
|
||||||
<CellTemplate>
|
|
||||||
@if (context.Item.Status == "completed")
|
|
||||||
{
|
{
|
||||||
<MudChip Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">완료</MudChip>
|
<MudButton Variant="Variant.Outlined" Color="Color.Success" OnClick="@(async () => await CompleteSchedule(selectedSchedule?.Id ?? 0))">완료 처리</MudButton>
|
||||||
|
}
|
||||||
|
@if (isEditMode)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="@(async () => await DeleteSchedule(selectedSchedule?.Id ?? 0))">삭제</MudButton>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudChip Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">대기</MudChip>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSchedule" id="btn-save-schedule">저장</MudButton>
|
||||||
}
|
}
|
||||||
</CellTemplate>
|
</div>
|
||||||
</TemplateColumn>
|
</MudForm>
|
||||||
<TemplateColumn Title="작업" Sortable="false">
|
</MudPaper>
|
||||||
<CellTemplate>
|
</MudItem>
|
||||||
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
</MudGrid>
|
||||||
@if (context.Item.Status != "completed")
|
}
|
||||||
{
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.CheckCircle"
|
|
||||||
Color="Color.Success"
|
|
||||||
OnClick="@(async () => await CompleteSchedule(context.Item.Id))"
|
|
||||||
Title="완료" />
|
|
||||||
}
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
OnClick="@(async () => await DeleteSchedule(context.Item.Id))"
|
|
||||||
Title="삭제" />
|
|
||||||
</MudButtonGroup>
|
|
||||||
</CellTemplate>
|
|
||||||
</TemplateColumn>
|
|
||||||
</Columns>
|
|
||||||
</MudDataGrid>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
|
|
||||||
<TitleContent>
|
|
||||||
<MudText Typo="Typo.h6">새 신고 일정 추가</MudText>
|
|
||||||
</TitleContent>
|
|
||||||
<DialogContent>
|
|
||||||
<MudForm @ref="form">
|
|
||||||
<MudSelect T="int?"
|
|
||||||
@bind-Value="scheduleForm.ClientId"
|
|
||||||
Label="고객"
|
|
||||||
Required="true"
|
|
||||||
Variant="Variant.Outlined"
|
|
||||||
FullWidth="true"
|
|
||||||
Class="mb-4"
|
|
||||||
RequiredError="고객을 선택하세요.">
|
|
||||||
@foreach (var client in clients)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
<MudSelect T="string" @bind-Value="scheduleForm.FilingType" Label="신고 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
|
||||||
<MudSelectItem Value="@("종합소득세")">종합소득세</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("부가가치세")">부가가치세</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("법인세")">법인세</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("원천세")">원천세</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("종합부동산세")">종합부동산세</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("양도소득세")">양도소득세</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("상속·증여세")">상속·증여세</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("세무조정")">세무조정</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
|
||||||
<MudNumericField T="int" @bind-Value="scheduleForm.FilingYear" Label="신고연도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
|
||||||
</MudForm>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<MudButton OnClick="CloseDialog">취소</MudButton>
|
|
||||||
<MudButton Color="Color.Primary" OnClick="SaveSchedule">저장</MudButton>
|
|
||||||
</DialogActions>
|
|
||||||
</MudDialog>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
@@ -158,7 +174,8 @@
|
|||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private bool isDialogOpen;
|
private bool isEditMode;
|
||||||
|
private TaxFilingSchedule? selectedSchedule;
|
||||||
private TaxFilingScheduleForm scheduleForm = new();
|
private TaxFilingScheduleForm scheduleForm = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -171,6 +188,7 @@
|
|||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
await LoadData();
|
await LoadData();
|
||||||
|
PrepareCreate();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,15 +210,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenCreateDialog()
|
private void PrepareCreate()
|
||||||
{
|
{
|
||||||
|
selectedSchedule = null;
|
||||||
|
isEditMode = false;
|
||||||
scheduleForm = new TaxFilingScheduleForm
|
scheduleForm = new TaxFilingScheduleForm
|
||||||
{
|
{
|
||||||
FilingYear = DateTime.Now.Year,
|
FilingYear = DateTime.Now.Year,
|
||||||
DueDate = DateTime.Today,
|
DueDate = DateTime.Today,
|
||||||
ClientId = clients.FirstOrDefault()?.Id
|
ClientId = clients.FirstOrDefault()?.Id,
|
||||||
|
FilingType = string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRowSelected(TaxFilingSchedule schedule)
|
||||||
|
{
|
||||||
|
if (schedule == null) return;
|
||||||
|
selectedSchedule = schedule;
|
||||||
|
isEditMode = true;
|
||||||
|
scheduleForm = new TaxFilingScheduleForm
|
||||||
|
{
|
||||||
|
ClientId = schedule.ClientId,
|
||||||
|
FilingType = schedule.FilingType,
|
||||||
|
DueDate = schedule.DueDate,
|
||||||
|
FilingYear = schedule.FilingYear
|
||||||
};
|
};
|
||||||
isDialogOpen = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveSchedule()
|
private async Task SaveSchedule()
|
||||||
@@ -227,7 +261,7 @@
|
|||||||
if (newId > 0)
|
if (newId > 0)
|
||||||
{
|
{
|
||||||
Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success);
|
Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success);
|
||||||
CloseDialog();
|
PrepareCreate();
|
||||||
await LoadData();
|
await LoadData();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -247,6 +281,10 @@
|
|||||||
{
|
{
|
||||||
await TaxFilingClient.MarkCompletedAsync(id);
|
await TaxFilingClient.MarkCompletedAsync(id);
|
||||||
Snackbar.Add("신고 일정이 완료 처리되었습니다.", Severity.Success);
|
Snackbar.Add("신고 일정이 완료 처리되었습니다.", Severity.Success);
|
||||||
|
if (selectedSchedule?.Id == id)
|
||||||
|
{
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
await LoadData();
|
await LoadData();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -272,6 +310,10 @@
|
|||||||
{
|
{
|
||||||
await TaxFilingClient.DeleteAsync(id);
|
await TaxFilingClient.DeleteAsync(id);
|
||||||
Snackbar.Add("신고 일정이 삭제되었습니다.", Severity.Success);
|
Snackbar.Add("신고 일정이 삭제되었습니다.", Severity.Success);
|
||||||
|
if (selectedSchedule?.Id == id)
|
||||||
|
{
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
await LoadData();
|
await LoadData();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -280,18 +322,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseDialog()
|
|
||||||
{
|
|
||||||
isDialogOpen = false;
|
|
||||||
scheduleForm = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetClientDisplayName(Client client)
|
private static string GetClientDisplayName(Client client)
|
||||||
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
? client.CompanyName
|
? client.CompanyName
|
||||||
: !string.IsNullOrWhiteSpace(client.Name)
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
? client.Name
|
? client.Name
|
||||||
: $"Client #{client.Id}";
|
: $"Client #{client.Id}";
|
||||||
|
|
||||||
private class TaxFilingScheduleForm
|
private class TaxFilingScheduleForm
|
||||||
{
|
{
|
||||||
public int? ClientId { get; set; }
|
public int? ClientId { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/admin/tax-profiles"
|
@page "/admin/tax-profiles"
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@inject ITaxProfileBrowserClient TaxProfileClient
|
@inject ITaxProfileBrowserClient TaxProfileClient
|
||||||
@inject IClientBrowserClient ClientClient
|
@inject IClientBrowserClient ClientClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<MudText Typo="Typo.h4" Class="admin-page-title">세무 프로필</MudText>
|
<MudText Typo="Typo.h4" Class="admin-page-title">세무 프로필</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 세무 프로필, 신고 일정, 위험도 추적</MudText>
|
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 세무 프로필, 신고 일정, 위험도 추적</MudText>
|
||||||
</div>
|
</div>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-profile">
|
||||||
새 프로필 추가
|
새 프로필 추가
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</section>
|
</section>
|
||||||
@@ -23,98 +24,99 @@
|
|||||||
{
|
{
|
||||||
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
||||||
}
|
}
|
||||||
else if (profiles.Count == 0)
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Info" Class="mt-4">세무 프로필이 없습니다.</MudAlert>
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudDataGrid T="TaxProfile"
|
<MudGrid Spacing="2" Class="mt-2">
|
||||||
Items="@profiles"
|
<!-- Left: Dense Grid List -->
|
||||||
Dense="true"
|
<MudItem XS="12" MD="8">
|
||||||
Hover="true"
|
@if (profiles.Count == 0)
|
||||||
Striped="true"
|
{
|
||||||
Virtualize="true"
|
<MudAlert Severity="Severity.Info">세무 프로필이 없습니다.</MudAlert>
|
||||||
RowsPerPage="30"
|
}
|
||||||
Class="admin-grid mt-4">
|
else
|
||||||
<Columns>
|
{
|
||||||
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
<MudDataGrid T="TaxProfile"
|
||||||
<TemplateColumn Title="고객">
|
Items="@profiles"
|
||||||
<CellTemplate>
|
Dense="true"
|
||||||
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
Hover="true"
|
||||||
{
|
Striped="true"
|
||||||
<MudLink Href="@($"/taxbaik/admin/clients/{context.Item.ClientId}")" Color="Color.Primary">
|
Virtualize="true"
|
||||||
@clientName
|
RowsPerPage="30"
|
||||||
</MudLink>
|
SelectedItem="@selectedProfile"
|
||||||
}
|
SelectedItemChanged="OnRowSelected"
|
||||||
</CellTemplate>
|
Class="admin-grid">
|
||||||
</TemplateColumn>
|
<Columns>
|
||||||
<PropertyColumn Property="x => x.BusinessType" Title="사업 유형" />
|
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
|
||||||
<TemplateColumn Title="위험도">
|
<TemplateColumn Title="고객">
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
<MudChip Size="Size.Small" Color="@GetRiskColor(context.Item.TaxRiskLevel)" Variant="Variant.Filled">
|
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
|
||||||
@context.Item.TaxRiskLevel
|
{
|
||||||
</MudChip>
|
@clientName
|
||||||
</CellTemplate>
|
}
|
||||||
</TemplateColumn>
|
</CellTemplate>
|
||||||
<TemplateColumn Title="다음 신고">
|
</TemplateColumn>
|
||||||
<CellTemplate>
|
<PropertyColumn Property="x => x.BusinessType" Title="사업 유형" />
|
||||||
@if (context.Item.NextFilingDueDate.HasValue)
|
<TemplateColumn Title="위험도">
|
||||||
{
|
<CellTemplate>
|
||||||
@context.Item.NextFilingDueDate.Value.ToString("yyyy-MM-dd")
|
<MudChip Size="Size.Small" Color="@GetRiskColor(context.Item.TaxRiskLevel)" Variant="Variant.Filled">
|
||||||
}
|
@context.Item.TaxRiskLevel
|
||||||
</CellTemplate>
|
</MudChip>
|
||||||
</TemplateColumn>
|
</CellTemplate>
|
||||||
<TemplateColumn Title="작업" Sortable="false">
|
</TemplateColumn>
|
||||||
<CellTemplate>
|
<TemplateColumn Title="다음 신고">
|
||||||
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
<CellTemplate>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" OnClick="@(async () => await OpenEditDialog(context.Item))" />
|
@if (context.Item.NextFilingDueDate.HasValue)
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(async () => await DeleteProfile(context.Item.Id))" />
|
{
|
||||||
</MudButtonGroup>
|
@context.Item.NextFilingDueDate.Value.ToString("yyyy-MM-dd")
|
||||||
</CellTemplate>
|
}
|
||||||
</TemplateColumn>
|
</CellTemplate>
|
||||||
</Columns>
|
</TemplateColumn>
|
||||||
</MudDataGrid>
|
<TemplateColumn Title="작업" Sortable="false">
|
||||||
}
|
<CellTemplate>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="@(async () => await DeleteProfile(context.Item.Id))" />
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
</MudDataGrid>
|
||||||
|
}
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
<!-- Create/Edit Dialog -->
|
<!-- Right: Detail Form Panel (Inline Editor) -->
|
||||||
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
|
<MudItem XS="12" MD="4">
|
||||||
<TitleContent>
|
<MudPaper Class="pa-4 admin-surface admin-editor-panel" Elevation="0" Style="border: 1px solid var(--border-color); min-height: 400px;">
|
||||||
<MudText Typo="Typo.h6">@(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가")</MudText>
|
<div class="d-flex align-center justify-space-between mb-4">
|
||||||
</TitleContent>
|
<MudText Typo="Typo.h6" Class="font-weight-bold">@(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가")</MudText>
|
||||||
<DialogContent>
|
@if (isEditMode)
|
||||||
<MudForm @ref="form">
|
{
|
||||||
<MudSelect T="int?" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="PrepareCreate">
|
||||||
@foreach (var client in clients)
|
새로 작성
|
||||||
{
|
</MudButton>
|
||||||
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
}
|
||||||
}
|
</div>
|
||||||
</MudSelect>
|
<MudForm @ref="form">
|
||||||
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
<MudSelect T="int?" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" RequiredError="고객을 선택하세요." Disabled="@isEditMode">
|
||||||
<MudSelectItem Value="@("일반제조업")">일반제조업</MudSelectItem>
|
@foreach (var client in clients)
|
||||||
<MudSelectItem Value="@("도소매업")">도소매업</MudSelectItem>
|
{
|
||||||
<MudSelectItem Value="@("서비스업")">서비스업</MudSelectItem>
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
<MudSelectItem Value="@("정보통신업")">정보통신업</MudSelectItem>
|
}
|
||||||
<MudSelectItem Value="@("부동산업")">부동산업</MudSelectItem>
|
</MudSelect>
|
||||||
<MudSelectItem Value="@("건설업")">건설업</MudSelectItem>
|
<CommonCodeSelect @bind-Value="profileForm.BusinessType" Group="BUSINESS_TYPE" Label="사업 유형" Class="mb-3" Required="true" />
|
||||||
<MudSelectItem Value="@("음식점업")">음식점업</MudSelectItem>
|
<CommonCodeSelect @bind-Value="profileForm.TaxRiskLevel" Group="TAX_RISK_LEVEL" Label="위험도" Class="mb-3" />
|
||||||
<MudSelectItem Value="@("프리랜서")">프리랜서</MudSelectItem>
|
<MudDatePicker @bind-Date="profileForm.NextFilingDueDate" Label="다음 신고 예정일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" />
|
||||||
<MudSelectItem Value="@("기타")">기타</MudSelectItem>
|
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="@true" Lines="3" Class="mb-4" />
|
||||||
</MudSelect>
|
|
||||||
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
<div class="d-flex justify-end gap-2">
|
||||||
<MudSelectItem Value="@("low")">낮음</MudSelectItem>
|
@if (isEditMode)
|
||||||
<MudSelectItem Value="@("normal")">보통</MudSelectItem>
|
{
|
||||||
<MudSelectItem Value="@("high")">높음</MudSelectItem>
|
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="@(async () => await DeleteProfile(selectedProfile?.Id ?? 0))">삭제</MudButton>
|
||||||
</MudSelect>
|
}
|
||||||
<MudDatePicker @bind-Date="profileForm.NextFilingDueDate" Label="다음 신고 예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveProfile" id="btn-save-profile">저장</MudButton>
|
||||||
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="true" Lines="2" />
|
</div>
|
||||||
</MudForm>
|
</MudForm>
|
||||||
</DialogContent>
|
</MudPaper>
|
||||||
<DialogActions>
|
</MudItem>
|
||||||
<MudButton OnClick="CloseDialog">취소</MudButton>
|
</MudGrid>
|
||||||
<MudButton Color="Color.Primary" OnClick="SaveProfile">저장</MudButton>
|
}
|
||||||
</DialogActions>
|
|
||||||
</MudDialog>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
@@ -123,24 +125,21 @@ else
|
|||||||
private List<TaxProfile>? profiles;
|
private List<TaxProfile>? profiles;
|
||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
|
private List<CommonCode> riskLevels = [];
|
||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private bool isDialogOpen;
|
|
||||||
private bool isEditMode;
|
private bool isEditMode;
|
||||||
private TaxProfile? editingProfile;
|
private TaxProfile? selectedProfile;
|
||||||
private TaxProfileForm profileForm = new();
|
private TaxProfileForm profileForm = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadData();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
PrepareCreate();
|
||||||
{
|
|
||||||
await LoadData();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +152,7 @@ else
|
|||||||
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||||
clients = clientItems.ToList();
|
clients = clientItems.ToList();
|
||||||
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -160,23 +160,23 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenCreateDialog()
|
private void PrepareCreate()
|
||||||
{
|
{
|
||||||
|
selectedProfile = null;
|
||||||
isEditMode = false;
|
isEditMode = false;
|
||||||
editingProfile = null;
|
|
||||||
profileForm = new TaxProfileForm
|
profileForm = new TaxProfileForm
|
||||||
{
|
{
|
||||||
ClientId = clients.FirstOrDefault()?.Id,
|
ClientId = clients.FirstOrDefault()?.Id,
|
||||||
TaxRiskLevel = "normal",
|
TaxRiskLevel = "normal",
|
||||||
NextFilingDueDate = DateTime.Today.AddMonths(1)
|
NextFilingDueDate = DateTime.Today.AddMonths(1)
|
||||||
};
|
};
|
||||||
isDialogOpen = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenEditDialog(TaxProfile profile)
|
private void OnRowSelected(TaxProfile profile)
|
||||||
{
|
{
|
||||||
|
if (profile == null) return;
|
||||||
|
selectedProfile = profile;
|
||||||
isEditMode = true;
|
isEditMode = true;
|
||||||
editingProfile = profile;
|
|
||||||
profileForm = new TaxProfileForm
|
profileForm = new TaxProfileForm
|
||||||
{
|
{
|
||||||
ClientId = profile.ClientId,
|
ClientId = profile.ClientId,
|
||||||
@@ -185,7 +185,6 @@ else
|
|||||||
NextFilingDueDate = profile.NextFilingDueDate,
|
NextFilingDueDate = profile.NextFilingDueDate,
|
||||||
SpecialNotes = profile.SpecialNotes
|
SpecialNotes = profile.SpecialNotes
|
||||||
};
|
};
|
||||||
isDialogOpen = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveProfile()
|
private async Task SaveProfile()
|
||||||
@@ -195,16 +194,16 @@ else
|
|||||||
await form.Validate();
|
await form.Validate();
|
||||||
if (!form.IsValid)
|
if (!form.IsValid)
|
||||||
{
|
{
|
||||||
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
|
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (isEditMode && editingProfile != null)
|
if (isEditMode && selectedProfile != null)
|
||||||
{
|
{
|
||||||
await TaxProfileClient.UpdateAsync(editingProfile.Id, profileForm.BusinessType,
|
await TaxProfileClient.UpdateAsync(selectedProfile.Id, profileForm.BusinessType,
|
||||||
null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel);
|
null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel);
|
||||||
Snackbar.Add("세무 프로필이 수정되었습니다.", Severity.Success);
|
Snackbar.Add("세무 프로필이 수정되었습니다.", Severity.Success);
|
||||||
}
|
}
|
||||||
@@ -220,7 +219,6 @@ else
|
|||||||
profileForm.BusinessType);
|
profileForm.BusinessType);
|
||||||
if (newId > 0)
|
if (newId > 0)
|
||||||
{
|
{
|
||||||
// 생성 후 상태 업데이트 처리
|
|
||||||
await TaxProfileClient.UpdateAsync(
|
await TaxProfileClient.UpdateAsync(
|
||||||
newId,
|
newId,
|
||||||
profileForm.BusinessType,
|
profileForm.BusinessType,
|
||||||
@@ -230,7 +228,7 @@ else
|
|||||||
Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success);
|
Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CloseDialog();
|
PrepareCreate();
|
||||||
await LoadData();
|
await LoadData();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -255,6 +253,10 @@ else
|
|||||||
{
|
{
|
||||||
await TaxProfileClient.DeleteAsync(id);
|
await TaxProfileClient.DeleteAsync(id);
|
||||||
Snackbar.Add("세무 프로필이 삭제되었습니다.", Severity.Success);
|
Snackbar.Add("세무 프로필이 삭제되었습니다.", Severity.Success);
|
||||||
|
if (selectedProfile?.Id == id)
|
||||||
|
{
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
await LoadData();
|
await LoadData();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -263,14 +265,6 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseDialog()
|
|
||||||
{
|
|
||||||
isDialogOpen = false;
|
|
||||||
isEditMode = false;
|
|
||||||
editingProfile = null;
|
|
||||||
profileForm = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color GetRiskColor(string riskLevel) => riskLevel switch
|
private Color GetRiskColor(string riskLevel) => riskLevel switch
|
||||||
{
|
{
|
||||||
"high" => Color.Error,
|
"high" => Color.Error,
|
||||||
@@ -285,6 +279,7 @@ else
|
|||||||
: !string.IsNullOrWhiteSpace(client.Name)
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
? client.Name
|
? client.Name
|
||||||
: $"Client #{client.Id}";
|
: $"Client #{client.Id}";
|
||||||
|
|
||||||
private class TaxProfileForm
|
private class TaxProfileForm
|
||||||
{
|
{
|
||||||
public int? ClientId { get; set; }
|
public int? ClientId { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
namespace TaxBaik.Web.Components.Admin.Shared;
|
||||||
|
|
||||||
|
public static class BusinessDayCalculator
|
||||||
|
{
|
||||||
|
private sealed record HolidayWindow(DateOnly Start, DateOnly End)
|
||||||
|
{
|
||||||
|
public IEnumerable<DateOnly> Dates()
|
||||||
|
{
|
||||||
|
for (var date = Start; date <= End; date = date.AddDays(1))
|
||||||
|
{
|
||||||
|
yield return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HolidayWindow[] HolidayWindows =
|
||||||
|
{
|
||||||
|
new(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 1)),
|
||||||
|
new(new DateOnly(2026, 2, 16), new DateOnly(2026, 2, 18)),
|
||||||
|
new(new DateOnly(2026, 3, 1), new DateOnly(2026, 3, 2)),
|
||||||
|
new(new DateOnly(2026, 5, 5), new DateOnly(2026, 5, 5)),
|
||||||
|
new(new DateOnly(2026, 6, 6), new DateOnly(2026, 6, 6)),
|
||||||
|
new(new DateOnly(2026, 8, 15), new DateOnly(2026, 8, 17)),
|
||||||
|
new(new DateOnly(2026, 9, 24), new DateOnly(2026, 9, 26)),
|
||||||
|
new(new DateOnly(2026, 10, 3), new DateOnly(2026, 10, 5)),
|
||||||
|
new(new DateOnly(2026, 10, 9), new DateOnly(2026, 10, 9)),
|
||||||
|
new(new DateOnly(2026, 12, 25), new DateOnly(2026, 12, 25))
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<DateOnly> HolidayDates = BuildHolidayDates();
|
||||||
|
|
||||||
|
public static DateOnly GetEffectiveDueDate(DateOnly dueDate)
|
||||||
|
{
|
||||||
|
var effectiveDate = dueDate;
|
||||||
|
while (!IsBusinessDay(effectiveDate))
|
||||||
|
{
|
||||||
|
effectiveDate = effectiveDate.AddDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectiveDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int GetDday(DateOnly dueDate, DateOnly? referenceDate = null)
|
||||||
|
{
|
||||||
|
var today = referenceDate ?? DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var effectiveDueDate = GetEffectiveDueDate(dueDate);
|
||||||
|
return effectiveDueDate.DayNumber - today.DayNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsBusinessDay(DateOnly date)
|
||||||
|
=> date.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday
|
||||||
|
&& !HolidayDates.Contains(date);
|
||||||
|
|
||||||
|
private static HashSet<DateOnly> BuildHolidayDates()
|
||||||
|
{
|
||||||
|
var holidays = new HashSet<DateOnly>();
|
||||||
|
|
||||||
|
foreach (var window in HolidayWindows)
|
||||||
|
{
|
||||||
|
foreach (var date in window.Dates())
|
||||||
|
{
|
||||||
|
holidays.Add(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 주말과 연속 공휴일 뒤에 붙는 대체휴일을 다음 영업일로 자동 확장한다.
|
||||||
|
foreach (var window in HolidayWindows)
|
||||||
|
{
|
||||||
|
foreach (var date in window.Dates())
|
||||||
|
{
|
||||||
|
if (date.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var substitute = date.AddDays(1);
|
||||||
|
while (substitute.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday || holidays.Contains(substitute))
|
||||||
|
{
|
||||||
|
substitute = substitute.AddDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
holidays.Add(substitute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return holidays;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@using TaxBaik.Domain.Entities
|
||||||
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@inject ICommonCodeBrowserClient CommonCodeClient
|
||||||
|
|
||||||
|
<MudSelect T="string"
|
||||||
|
Value="Value"
|
||||||
|
ValueChanged="ValueChanged"
|
||||||
|
Label="@Label"
|
||||||
|
Variant="@Variant"
|
||||||
|
FullWidth="@FullWidth"
|
||||||
|
Class="@Class"
|
||||||
|
Required="@Required"
|
||||||
|
Clearable="@Clearable"
|
||||||
|
Disabled="@Disabled">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Placeholder))
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@string.Empty">@Placeholder</MudSelectItem>
|
||||||
|
}
|
||||||
|
@foreach (var item in items)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@item.CodeValue">@item.CodeName</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string? Value { get; set; }
|
||||||
|
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
||||||
|
[Parameter] public string Group { get; set; } = string.Empty;
|
||||||
|
[Parameter] public string Label { get; set; } = string.Empty;
|
||||||
|
[Parameter] public Variant Variant { get; set; } = Variant.Outlined;
|
||||||
|
[Parameter] public bool FullWidth { get; set; } = true;
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
[Parameter] public bool Required { get; set; }
|
||||||
|
[Parameter] public bool Clearable { get; set; }
|
||||||
|
[Parameter] public bool Disabled { get; set; }
|
||||||
|
[Parameter] public string? Placeholder { get; set; }
|
||||||
|
|
||||||
|
private List<CommonCode> items = [];
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
var normalizedGroup = Group?.Trim() ?? string.Empty;
|
||||||
|
if (!string.Equals(normalizedGroup, _loadedGroup, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_loadedGroup = normalizedGroup;
|
||||||
|
items = string.IsNullOrWhiteSpace(normalizedGroup)
|
||||||
|
? []
|
||||||
|
: (await CommonCodeClient.GetByGroupAsync(normalizedGroup))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.CodeName)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? _loadedGroup;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TaxBaik.Application.Services;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
namespace TaxBaik.Web.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class CommonCodeController(CommonCodeService commonCodeService) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAllActive()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codes = await commonCodeService.GetAllActiveAsync();
|
||||||
|
return Ok(codes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "공통코드 조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("group/{group}")]
|
||||||
|
public async Task<IActionResult> GetByGroup(string group)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codes = await commonCodeService.GetByGroupAsync(group);
|
||||||
|
return Ok(codes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "그룹별 공통코드 조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("groups")]
|
||||||
|
public async Task<IActionResult> GetGroups()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var groups = await commonCodeService.GetAllGroupsAsync();
|
||||||
|
return Ok(groups);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "공통코드 그룹 조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{group}/{value}")]
|
||||||
|
public async Task<IActionResult> Get(string group, string value)
|
||||||
|
{
|
||||||
|
var code = await commonCodeService.GetAsync(group, value);
|
||||||
|
return code is null ? NotFound() : Ok(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Upsert([FromBody] CommonCode code)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(code.CodeGroup) || string.IsNullOrWhiteSpace(code.CodeValue) || string.IsNullOrWhiteSpace(code.CodeName))
|
||||||
|
return BadRequest(new { error = "코드 그룹, 값, 이름은 필수입니다." });
|
||||||
|
if (code.CodeValue.Contains(' '))
|
||||||
|
return BadRequest(new { error = "code_value에는 공백을 사용할 수 없습니다." });
|
||||||
|
|
||||||
|
await commonCodeService.UpsertAsync(code);
|
||||||
|
return Ok(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{group}/{value}")]
|
||||||
|
public async Task<IActionResult> Delete(string group, string value)
|
||||||
|
{
|
||||||
|
await commonCodeService.DeleteAsync(group, value);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
+159
-43
@@ -3,55 +3,171 @@
|
|||||||
ViewData["Title"] = "소개 | 백원숙 세무회계";
|
ViewData["Title"] = "소개 | 백원숙 세무회계";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container py-5">
|
<!-- Breadcrumb Navigation -->
|
||||||
<h1 class="fw-bold mb-5">백원숙 세무사</h1>
|
<nav aria-label="breadcrumb" class="py-3" style="background: #F9F7F3; border-bottom: 1px solid #D9D3C4;">
|
||||||
|
<div class="container">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
|
<li class="breadcrumb-item"><a href="/taxbaik/" class="text-decoration-none">홈</a></li>
|
||||||
|
<li class="breadcrumb-item active">소개</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="row g-5">
|
<div class="container py-5">
|
||||||
<div class="col-md-6">
|
<!-- 돌아가기 버튼 -->
|
||||||
<p class="lead">사업자 세무, 부동산 거래, 가족 자산 관리 등 종합적인 세무 컨설팅을 제공합니다.</p>
|
<div class="mb-4">
|
||||||
<p>10년 이상의 풍부한 경험과 3개의 국가자격증을 바탕으로, 각 클라이언트의 상황에 맞는 맞춤형 솔루션을 제시합니다.</p>
|
<a href="/taxbaik/" class="btn btn-sm btn-outline-secondary">← 홈으로 돌아가기</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="bg-light p-4 rounded">
|
<!-- Hero Section -->
|
||||||
<h5 class="fw-bold mb-3">보유 자격증</h5>
|
<section class="mb-5 pb-5 border-bottom">
|
||||||
<div class="mb-3">
|
<h1 class="fw-bold mb-4" style="font-size: 2.5rem;">안녕하세요, 백원숙 세무사입니다.</h1>
|
||||||
<p class="mb-1">🎓 <strong>세무사</strong></p>
|
<div class="row g-5">
|
||||||
<small class="text-muted">2015년 자격취득</small>
|
<div class="col-lg-6">
|
||||||
</div>
|
<p class="lead">사업자 세무, 부동산 거래, 가족 자산 관리 등 종합적인 세무 컨설팅을 제공합니다.</p>
|
||||||
<div class="mb-3">
|
<p>10년 이상의 풍부한 경험과 3개의 국가자격증을 바탕으로, 각 클라이언트의 상황에 맞는 맞춤형 솔루션을 제시합니다.</p>
|
||||||
<p class="mb-1">🏠 <strong>부동산중개사</strong></p>
|
<p class="text-muted">저도 작게 시작하는 사업가였습니다. 처음 사업을 시작할 때의 막막함을 잘 알고 있습니다. 그 경험이 오늘날 고객분들과 소통하는 원동력입니다.</p>
|
||||||
<small class="text-muted">부동산 거래 전문성</small>
|
</div>
|
||||||
</div>
|
<div class="col-lg-6">
|
||||||
<div>
|
<div class="bg-light p-4 rounded">
|
||||||
<p class="mb-1">📊 <strong>보험설계사</strong></p>
|
<h5 class="fw-bold mb-3">보유 자격증</h5>
|
||||||
<small class="text-muted">자산관리 전문성</small>
|
<div class="mb-3">
|
||||||
|
<p class="mb-1">🎓 <strong>세무사</strong></p>
|
||||||
|
<small class="text-muted">2015년 자격취득 · 10년 경력</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<p class="mb-1">🏠 <strong>부동산중개사</strong></p>
|
||||||
|
<small class="text-muted">부동산 거래 구조 이해 · 실무 전문성</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-1">📊 <strong>보험설계사</strong></p>
|
||||||
|
<small class="text-muted">자산관리·상속 대비 전문성</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<hr class="my-5" />
|
<!-- Expertise Section -->
|
||||||
|
<section class="mb-5 pb-5 border-bottom">
|
||||||
|
<h2 class="fw-bold mb-4">세 가지 자격의 시너지</h2>
|
||||||
|
<p class="text-muted mb-4">단순히 세금을 계산하는 것이 아니라, 사업 구조와 자산 흐름을 종합적으로 이해합니다.</p>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="p-4 border rounded-3" style="border-color: #D9D3C4;">
|
||||||
|
<div style="font-size: 2rem; margin-bottom: 1rem;">⚖️</div>
|
||||||
|
<h5 class="fw-bold mb-2">공인 세무사</h5>
|
||||||
|
<p class="text-muted small mb-0">
|
||||||
|
세무신고·장부관리·조세 자문 등 세무 업무 전반을 공식 대리합니다. 신고 기한 내 불이익 없는 신고를 기본으로 합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="p-4 border rounded-3" style="border-color: #D9D3C4;">
|
||||||
|
<div style="font-size: 2rem; margin-bottom: 1rem;">🏠</div>
|
||||||
|
<h5 class="fw-bold mb-2">공인 부동산중개사</h5>
|
||||||
|
<p class="text-muted small mb-0">
|
||||||
|
부동산 거래 구조를 이해해 양도·증여·임대 세무상담에 현실감을 더합니다. 계약 전 사전검토로 선택지를 최대화합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="p-4 border rounded-3" style="border-color: #D9D3C4;">
|
||||||
|
<div style="font-size: 2rem; margin-bottom: 1rem;">🛡️</div>
|
||||||
|
<h5 class="fw-bold mb-2">보험설계사 자격</h5>
|
||||||
|
<p class="text-muted small mb-0">
|
||||||
|
상속·증여·대표자 리스크 관점에서 가족 현금흐름과 보험 구조를 함께 설명합니다. 절세와 리스크 관리를 동시에 다룹니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h2 class="fw-bold mb-4">서비스 철학</h2>
|
<!-- Philosophy Section -->
|
||||||
<div class="row g-4">
|
<section class="mb-5 pb-5 border-bottom">
|
||||||
<div class="col-md-4 text-center">
|
<h2 class="fw-bold mb-4">상담 철학</h2>
|
||||||
<div class="mb-3" style="font-size: 2rem;">🎯</div>
|
<div class="row g-4">
|
||||||
<h5>명확한 설명</h5>
|
<div class="col-md-4 text-center">
|
||||||
<p class="small">어려운 세법을 쉽게 설명하여 이해를 높입니다</p>
|
<div class="mb-3" style="font-size: 2.5rem;">🎯</div>
|
||||||
|
<h5>명확한 설명</h5>
|
||||||
|
<p class="small text-muted">어려운 세법을 쉽게 설명하여 이해를 높입니다. 전문용어로 일방적 설명하지 않습니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-center">
|
||||||
|
<div class="mb-3" style="font-size: 2.5rem;">💰</div>
|
||||||
|
<h5>최대 절세</h5>
|
||||||
|
<p class="small text-muted">법적 범위 내에서 세금을 최소화합니다. 초기 세무 전략이 연간 수백만 원의 차이를 만듭니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-center">
|
||||||
|
<div class="mb-3" style="font-size: 2.5rem;">🤝</div>
|
||||||
|
<h5>신뢰 파트너</h5>
|
||||||
|
<p class="small text-muted">장기적 파트너로서 성장을 함께 합니다. 일회성 상담이 아닌 지속적 관계를 지향합니다.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
</section>
|
||||||
<div class="mb-3" style="font-size: 2rem;">💰</div>
|
|
||||||
<h5>최대 절세</h5>
|
|
||||||
<p class="small">법적 범위 내에서 세금을 최소화합니다</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 text-center">
|
|
||||||
<div class="mb-3" style="font-size: 2rem;">🤝</div>
|
|
||||||
<h5>신뢰 관계</h5>
|
|
||||||
<p class="small">장기적 파트너로서 성장을 함께 합니다</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center mt-5">
|
<!-- Online Consultation Section -->
|
||||||
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">상담 신청하기</a>
|
<section class="mb-5 pb-5 border-bottom">
|
||||||
</div>
|
<h2 class="fw-bold mb-4">전국 비대면 온라인 상담</h2>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5 class="fw-bold mb-3">왜 온라인인가?</h5>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li class="mb-2"><strong>✓ 시간 절약</strong><br/><small class="text-muted">서울로 올 필요 없이 카카오·이메일로 진행</small></li>
|
||||||
|
<li class="mb-2"><strong>✓ 자료 공유 편의</strong><br/><small class="text-muted">온라인으로 자료 검토 후 맞춤 상담</small></li>
|
||||||
|
<li class="mb-2"><strong>✓ 기록 남음</strong><br/><small class="text-muted">채팅·메일로 모든 내용을 기록 관리</small></li>
|
||||||
|
<li class="mb-2"><strong>✓ 비용 절감</strong><br/><small class="text-muted">방문 비용 없이 효율적 상담 제공</small></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5 class="fw-bold mb-3">상담 방식</h5>
|
||||||
|
<div class="p-3 bg-light rounded-3 mb-3">
|
||||||
|
<p class="fw-bold mb-2">📞 전화 상담</p>
|
||||||
|
<small class="text-muted">즉시 상황 파악 필요 시 · 010-4122-8268</small>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-light rounded-3 mb-3">
|
||||||
|
<p class="fw-bold mb-2">💬 카카오채널</p>
|
||||||
|
<small class="text-muted">당일 응답 · 편한 시간에 문의</small>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-light rounded-3 mb-3">
|
||||||
|
<p class="fw-bold mb-2">✉️ 이메일</p>
|
||||||
|
<small class="text-muted">자료 첨부 상담 · taxbaik5668@gmail.com</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA Section -->
|
||||||
|
<section class="text-center mb-5 pb-5 border-bottom">
|
||||||
|
<h3 class="fw-bold mb-3">세금 고민, 이제 끝내세요</h3>
|
||||||
|
<p class="text-muted mb-5">무료 상담으로 현재 상황을 진단하고 맞춤형 절세 전략을 받아보세요.</p>
|
||||||
|
<div class="d-flex gap-3 justify-content-center flex-wrap">
|
||||||
|
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">상담 신청하기</a>
|
||||||
|
<a href="http://pf.kakao.com/_xoxchTX" target="_blank" class="btn btn-warning btn-lg">카카오로 문의</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 관련 페이지 네비게이션 -->
|
||||||
|
<section class="text-center py-5">
|
||||||
|
<h4 class="fw-bold mb-4">다른 페이지 보기</h4>
|
||||||
|
<div class="row g-3 justify-content-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/taxbaik/" class="btn btn-outline-primary btn-sm w-100 py-3">
|
||||||
|
🏠 홈으로<br/>
|
||||||
|
<small class="text-muted">서비스 및 최신 정보</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/taxbaik/services" class="btn btn-outline-primary btn-sm w-100 py-3">
|
||||||
|
📊 전문 서비스<br/>
|
||||||
|
<small class="text-muted">사업자·부동산·자산 관리</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/taxbaik/blog" class="btn btn-outline-primary btn-sm w-100 py-3">
|
||||||
|
📝 세무 정보 블로그<br/>
|
||||||
|
<small class="text-muted">절세팁 및 신고 가이드</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@page "/announcement"
|
||||||
|
@{
|
||||||
|
Response.Redirect("/taxbaik/#top");
|
||||||
|
}
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
|
|
||||||
<hr class="my-4" />
|
<hr class="my-4" />
|
||||||
|
|
||||||
<div class="article-body lh-lg">
|
<div class="article-body lh-lg markdown-body">
|
||||||
@Html.Raw(Model.Post.Content)
|
@Html.Raw(Model.HtmlContent)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4" />
|
<hr class="my-4" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using TaxBaik.Application.Services;
|
using TaxBaik.Application.Services;
|
||||||
using TaxBaik.Domain.Entities;
|
using TaxBaik.Domain.Entities;
|
||||||
|
using Markdig;
|
||||||
|
|
||||||
namespace TaxBaik.Web.Pages.Blog;
|
namespace TaxBaik.Web.Pages.Blog;
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ public class BlogPostModel : PageModel
|
|||||||
private readonly BlogService _blogService;
|
private readonly BlogService _blogService;
|
||||||
|
|
||||||
public BlogPost? Post { get; set; }
|
public BlogPost? Post { get; set; }
|
||||||
|
public string? HtmlContent { get; set; }
|
||||||
|
|
||||||
public BlogPostModel(BlogService blogService)
|
public BlogPostModel(BlogService blogService)
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,7 @@ public class BlogPostModel : PageModel
|
|||||||
Post = await _blogService.GetBySlugAsync(slug);
|
Post = await _blogService.GetBySlugAsync(slug);
|
||||||
if (Post != null)
|
if (Post != null)
|
||||||
{
|
{
|
||||||
|
HtmlContent = Markdown.ToHtml(Post.Content ?? "");
|
||||||
_ = _blogService.IncrementViewCountAsync(Post.Id);
|
_ = _blogService.IncrementViewCountAsync(Post.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@page "/faq"
|
||||||
|
@{
|
||||||
|
Response.Redirect("/taxbaik/#faq");
|
||||||
|
}
|
||||||
+90
-131
@@ -103,31 +103,14 @@ else
|
|||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- 신뢰도 스트립 — 자격과 경험 -->
|
<!-- About 링크 배너 -->
|
||||||
<section class="trust-strip">
|
<section class="py-3" style="background: rgba(46, 92, 78, 0.05); border-bottom: 1px solid rgba(46, 92, 78, 0.1);">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap">
|
||||||
<div class="col-md-4">
|
<div>
|
||||||
<div class="trust-item">
|
<p class="mb-0 small text-muted">세무사의 경력, 자격, 상담 철학을 알아보세요</p>
|
||||||
<div class="trust-icon">🎓</div>
|
|
||||||
<h3>세무사</h3>
|
|
||||||
<p>국가공인 세무사 자격<br/>2015년 취득 · 10년 경력</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="trust-item">
|
|
||||||
<div class="trust-icon">🏢</div>
|
|
||||||
<h3>부동산중개사</h3>
|
|
||||||
<p>부동산 거래 전문 자격<br/>양도세·취득세 컨설팅</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="trust-item">
|
|
||||||
<div class="trust-icon">📊</div>
|
|
||||||
<h3>보험설계사</h3>
|
|
||||||
<p>자산관리 전문 자격<br/>가족 자산 플래닝</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a href="/taxbaik/about" class="btn btn-sm btn-outline-primary">백원숙 세무사 소개 →</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -144,7 +127,7 @@ else
|
|||||||
|
|
||||||
@{
|
@{
|
||||||
var focusService = season?.FocusService ?? "";
|
var focusService = season?.FocusService ?? "";
|
||||||
// 시즌에 따라 서비스 카드 순서 결정: 시즌 관련 카드가 맨 앞
|
// 시즌에 따라 서비스 카드 순서 결정
|
||||||
var cardOrder = focusService switch
|
var cardOrder = focusService switch
|
||||||
{
|
{
|
||||||
"real-estate-tax" => new[] { "real-estate-tax", "business-tax", "family-asset" },
|
"real-estate-tax" => new[] { "real-estate-tax", "business-tax", "family-asset" },
|
||||||
@@ -162,18 +145,10 @@ else
|
|||||||
<div class="col-lg-4 col-md-6">
|
<div class="col-lg-4 col-md-6">
|
||||||
<div class="card service-card h-100 @(isFeatured ? "service-card--featured" : "")">
|
<div class="card service-card h-100 @(isFeatured ? "service-card--featured" : "")">
|
||||||
@if (isFeatured) { <div class="service-card-badge">현재 시즌</div> }
|
@if (isFeatured) { <div class="service-card-badge">현재 시즌</div> }
|
||||||
<div class="service-icon">🏪</div>
|
<div class="service-icon">📊</div>
|
||||||
<div class="card-body pt-0">
|
<div class="card-body pt-0">
|
||||||
<h3 class="card-title">사업자 세무</h3>
|
<h3 class="card-title">사업자 세무</h3>
|
||||||
<ul class="list-unstyled small mb-3">
|
<p class="text-muted small">월 기장부터 종합소득세, 신규 사업자 세무까지 — 사업 초기부터 체계적인 세무 관리.</p>
|
||||||
<li class="mb-2">✓ 정확한 기장 및 결산</li>
|
|
||||||
<li class="mb-2">✓ 세금계산서 관리</li>
|
|
||||||
<li class="mb-2">✓ 경비처리 최적화</li>
|
|
||||||
<li class="mb-2">✓ 절세 전략 수립</li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-muted small">
|
|
||||||
초기부터 세무 전략을 수립하면 연간 최대 수백만 원의 절세가 가능합니다.
|
|
||||||
</p>
|
|
||||||
<a href="/taxbaik/services#business-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
|
<a href="/taxbaik/services#business-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,15 +162,7 @@ else
|
|||||||
<div class="service-icon">🏠</div>
|
<div class="service-icon">🏠</div>
|
||||||
<div class="card-body pt-0">
|
<div class="card-body pt-0">
|
||||||
<h3 class="card-title">부동산 세금</h3>
|
<h3 class="card-title">부동산 세금</h3>
|
||||||
<ul class="list-unstyled small mb-3">
|
<p class="text-muted small">양도세·취득세·임대소득세 — 부동산 거래 시 세금 부담을 줄이는 전략.</p>
|
||||||
<li class="mb-2">✓ 양도세 최소화</li>
|
|
||||||
<li class="mb-2">✓ 취득세 절감</li>
|
|
||||||
<li class="mb-2">✓ 임대소득 관리</li>
|
|
||||||
<li class="mb-2">✓ 다주택자 세무</li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-muted small">
|
|
||||||
부동산 거래 시 미리 상담하면 세금 부담을 크게 줄일 수 있습니다.
|
|
||||||
</p>
|
|
||||||
<a href="/taxbaik/services#real-estate-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
|
<a href="/taxbaik/services#real-estate-tax" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,15 +176,7 @@ else
|
|||||||
<div class="service-icon">👨👩👧👦</div>
|
<div class="service-icon">👨👩👧👦</div>
|
||||||
<div class="card-body pt-0">
|
<div class="card-body pt-0">
|
||||||
<h3 class="card-title">가족자산 관리</h3>
|
<h3 class="card-title">가족자산 관리</h3>
|
||||||
<ul class="list-unstyled small mb-3">
|
<p class="text-muted small">증여·상속 사전 계획부터 대표자 리스크 관리까지 — 가족 자산을 지키는 전략.</p>
|
||||||
<li class="mb-2">✓ 증여세 전략</li>
|
|
||||||
<li class="mb-2">✓ 상속세 대비</li>
|
|
||||||
<li class="mb-2">✓ 자산 이전 계획</li>
|
|
||||||
<li class="mb-2">✓ 가족법인 설립</li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-muted small">
|
|
||||||
세대 이전 전에 사전 계획하면 세금 부담을 현저히 줄일 수 있습니다.
|
|
||||||
</p>
|
|
||||||
<a href="/taxbaik/services#family-asset" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
|
<a href="/taxbaik/services#family-asset" class="btn btn-sm btn-outline-primary mt-3">자세히 보기</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,6 +187,85 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 블로그 & 시즌 포스트 (상단으로 올림) -->
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
@if (season != null)
|
||||||
|
{
|
||||||
|
<div class="seasonal-blog-header mb-2">
|
||||||
|
<span class="seasonal-blog-tag">📅 @season.Name 시즌</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="section-title">이번 시즌 세무 정보</h2>
|
||||||
|
<p class="text-muted">@season.Name 관련 절세 팁과 신고 가이드를 확인하세요</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<h2 class="section-title">세무 정보 & 절세 팁</h2>
|
||||||
|
<p class="text-muted">최신 세법 변화와 실무 팁을 공유합니다</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var hasSeasonalPosts = Model.SeasonalPosts?.Count > 0;
|
||||||
|
var hasRecentPosts = Model.RecentPosts?.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (hasSeasonalPosts || hasRecentPosts)
|
||||||
|
{
|
||||||
|
<div class="row g-4">
|
||||||
|
@* 시즌 관련 글 (배지 강조) *@
|
||||||
|
@if (hasSeasonalPosts)
|
||||||
|
{
|
||||||
|
@foreach (var post in Model.SeasonalPosts!)
|
||||||
|
{
|
||||||
|
<div class="col-lg-4 col-md-6">
|
||||||
|
<div class="card blog-card h-100 blog-card--seasonal">
|
||||||
|
<div class="blog-seasonal-ribbon">이번 시즌 추천</div>
|
||||||
|
<div class="blog-placeholder">🗓️</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<small class="badge bg-season-badge">@post.CategoryName</small>
|
||||||
|
<h4 class="card-title mt-3">@post.Title</h4>
|
||||||
|
<p class="text-muted small">@((post.PublishedAt ?? post.CreatedAt).ToString("yyyy년 MM월 dd일"))</p>
|
||||||
|
<a href="/taxbaik/blog/@post.Slug" class="btn btn-sm btn-seasonal">읽기</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@* 최신 글 (나머지 채우기) *@
|
||||||
|
@if (hasRecentPosts)
|
||||||
|
{
|
||||||
|
@foreach (var post in Model.RecentPosts!)
|
||||||
|
{
|
||||||
|
<div class="col-lg-4 col-md-6">
|
||||||
|
<div class="card blog-card h-100">
|
||||||
|
<div class="blog-placeholder">📝</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<small class="badge bg-primary-badge">@post.CategoryName</small>
|
||||||
|
<h4 class="card-title mt-3">@post.Title</h4>
|
||||||
|
<p class="text-muted small">@((post.PublishedAt ?? post.CreatedAt).ToString("yyyy년 MM월 dd일"))</p>
|
||||||
|
<a href="/taxbaik/blog/@post.Slug" class="btn btn-sm btn-primary">읽기</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-5 d-flex justify-content-center gap-3 flex-wrap">
|
||||||
|
@if (season != null && !string.IsNullOrEmpty(season.RelatedCategorySlug))
|
||||||
|
{
|
||||||
|
<a href="/taxbaik/blog?category=@season.RelatedCategorySlug" class="btn btn-outline-secondary btn-lg">
|
||||||
|
📅 @season.Name 전체 글 보기
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<a href="/taxbaik/blog" class="btn btn-outline-primary btn-lg">전체 블로그 보기</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 상담 프로세스 -->
|
<!-- 상담 프로세스 -->
|
||||||
<section class="py-5" style="background: #F9F7F3;">
|
<section class="py-5" style="background: #F9F7F3;">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -273,85 +311,6 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 세무 정보 블로그 -->
|
|
||||||
<section class="py-5">
|
|
||||||
<div class="container">
|
|
||||||
<div class="text-center mb-5">
|
|
||||||
@if (season != null)
|
|
||||||
{
|
|
||||||
<div class="seasonal-blog-header mb-2">
|
|
||||||
<span class="seasonal-blog-tag">📅 @season.Name 시즌</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="section-title">이번 시즌 세무 정보</h2>
|
|
||||||
<p class="text-muted">@season.Name 관련 절세 팁과 신고 가이드를 확인하세요</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<h2 class="section-title">세무 정보</h2>
|
|
||||||
<p class="text-muted">최신 세법 변화와 실무 팁을 공유합니다</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@{
|
|
||||||
var hasSeasonalPosts = Model.SeasonalPosts?.Count > 0;
|
|
||||||
var hasRecentPosts = Model.RecentPosts?.Count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (hasSeasonalPosts || hasRecentPosts)
|
|
||||||
{
|
|
||||||
<div class="row g-4">
|
|
||||||
@* 시즌 관련 글 (배지 강조) *@
|
|
||||||
@if (hasSeasonalPosts)
|
|
||||||
{
|
|
||||||
@foreach (var post in Model.SeasonalPosts!)
|
|
||||||
{
|
|
||||||
<div class="col-lg-4 col-md-6">
|
|
||||||
<div class="card blog-card h-100 blog-card--seasonal">
|
|
||||||
<div class="blog-seasonal-ribbon">이번 시즌 추천</div>
|
|
||||||
<div class="blog-placeholder">🗓️</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<small class="badge bg-season-badge">@post.CategoryName</small>
|
|
||||||
<h4 class="card-title mt-3">@post.Title</h4>
|
|
||||||
<p class="text-muted small">@((post.PublishedAt ?? post.CreatedAt).ToString("yyyy년 MM월 dd일"))</p>
|
|
||||||
<a href="/taxbaik/blog/@post.Slug" class="btn btn-sm btn-seasonal">자세히 보기</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@* 최신 글 (나머지 채우기) *@
|
|
||||||
@if (hasRecentPosts)
|
|
||||||
{
|
|
||||||
@foreach (var post in Model.RecentPosts!)
|
|
||||||
{
|
|
||||||
<div class="col-lg-4 col-md-6">
|
|
||||||
<div class="card blog-card h-100">
|
|
||||||
<div class="blog-placeholder">📝</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<small class="badge bg-primary-badge">@post.CategoryName</small>
|
|
||||||
<h4 class="card-title mt-3">@post.Title</h4>
|
|
||||||
<p class="text-muted small">@((post.PublishedAt ?? post.CreatedAt).ToString("yyyy년 MM월 dd일"))</p>
|
|
||||||
<a href="/taxbaik/blog/@post.Slug" class="btn btn-sm btn-primary">글 내용 보기</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center mt-5 d-flex justify-content-center gap-3 flex-wrap">
|
|
||||||
@if (season != null && !string.IsNullOrEmpty(season.RelatedCategorySlug))
|
|
||||||
{
|
|
||||||
<a href="/taxbaik/blog?category=@season.RelatedCategorySlug" class="btn btn-outline-seasonal btn-lg">
|
|
||||||
📅 @season.Name 전체 글 보기
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
<a href="/taxbaik/blog" class="btn btn-outline-primary btn-lg">전체 블로그 보기</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 자주 묻는 질문 (DB 연동) -->
|
<!-- 자주 묻는 질문 (DB 연동) -->
|
||||||
@if (Model.ActiveFaqs.Count > 0)
|
@if (Model.ActiveFaqs.Count > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@page "/inquiry"
|
||||||
|
@{
|
||||||
|
Response.Redirect("/taxbaik/contact");
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- 왼쪽: 세무 신고 현황 (Tax Filings) -->
|
<!-- 왼쪽: 세무 신고 현황 (Tax Filings) -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card border-0 shadow-sm rounded-3 mb-4">
|
<div class="card glass-card mb-4">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h3 class="h5 fw-bold text-dark mb-0">
|
<h3 class="h5 fw-bold text-dark mb-0">
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
|
|
||||||
<!-- 오른쪽: 상담 이력 요약 (Consulting Activities) -->
|
<!-- 오른쪽: 상담 이력 요약 (Consulting Activities) -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="card border-0 shadow-sm rounded-3">
|
<div class="card glass-card">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<h3 class="h5 fw-bold text-dark mb-4">
|
<h3 class="h5 fw-bold text-dark mb-4">
|
||||||
<i class="bi bi-chat-text text-primary me-2"></i> 최근 상담 및 지원 이력
|
<i class="bi bi-chat-text text-primary me-2"></i> 최근 상담 및 지원 이력
|
||||||
@@ -139,14 +139,10 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="timeline">
|
<div class="timeline ps-2">
|
||||||
@foreach (var activity in Model.Consultations)
|
@foreach (var activity in Model.Consultations)
|
||||||
{
|
{
|
||||||
<div class="border-start border-2 border-primary-subtle ps-3 pb-4 position-relative">
|
<div class="timeline-item-modern">
|
||||||
<!-- 타임라인 아이콘 -->
|
|
||||||
<div class="position-absolute start-0 translate-middle-x bg-primary rounded-circle"
|
|
||||||
style="width: 10px; height: 10px; margin-left: -1px; top: 6px;"></div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<span class="badge bg-primary-subtle text-primary small">@activity.ActivityType</span>
|
<span class="badge bg-primary-subtle text-primary small">@activity.ActivityType</span>
|
||||||
<small class="text-muted">@activity.ActivityDate.ToString("yyyy-MM-dd")</small>
|
<small class="text-muted">@activity.ActivityDate.ToString("yyyy-MM-dd")</small>
|
||||||
|
|||||||
@@ -4,7 +4,20 @@
|
|||||||
ViewData["Description"] = "사업자 세무, 부동산 세금, 종합소득세 등 전문 상담 서비스";
|
ViewData["Description"] = "사업자 세무, 부동산 세금, 종합소득세 등 전문 상담 서비스";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<nav aria-label="breadcrumb" class="py-3" style="background: #F9F7F3; border-bottom: 1px solid #D9D3C4;">
|
||||||
|
<div class="container">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
|
<li class="breadcrumb-item"><a href="/taxbaik/" class="text-decoration-none">홈</a></li>
|
||||||
|
<li class="breadcrumb-item active">서비스</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
|
<div class="mb-4">
|
||||||
|
<a href="/taxbaik/" class="btn btn-sm btn-outline-secondary">← 홈으로 돌아가기</a>
|
||||||
|
</div>
|
||||||
<h1 class="fw-bold mb-5 text-center">주요 서비스</h1>
|
<h1 class="fw-bold mb-5 text-center">주요 서비스</h1>
|
||||||
|
|
||||||
<!-- 사업자 세무 -->
|
<!-- 사업자 세무 -->
|
||||||
@@ -124,11 +137,36 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA -->
|
<!-- CTA -->
|
||||||
<section class="bg-primary text-white py-5 rounded mt-5">
|
<section class="bg-primary text-white py-5 rounded mt-5 mb-5">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h2 class="fw-bold mb-3">전문 상담받으세요</h2>
|
<h2 class="fw-bold mb-3">전문 상담받으세요</h2>
|
||||||
<p class="lead mb-4">정확한 진단 후 맞춤형 솔루션을 제시합니다</p>
|
<p class="lead mb-4">정확한 진단 후 맞춤형 솔루션을 제시합니다</p>
|
||||||
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">무료 상담 신청</a>
|
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">무료 상담 신청</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 관련 페이지 네비게이션 -->
|
||||||
|
<section class="text-center py-5">
|
||||||
|
<h4 class="fw-bold mb-4">다른 페이지 보기</h4>
|
||||||
|
<div class="row g-3 justify-content-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/taxbaik/" class="btn btn-outline-primary btn-sm w-100 py-3">
|
||||||
|
🏠 홈<br/>
|
||||||
|
<small class="text-muted">최신 정보 및 블로그</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/taxbaik/about" class="btn btn-outline-primary btn-sm w-100 py-3">
|
||||||
|
👤 세무사 소개<br/>
|
||||||
|
<small class="text-muted">자격 및 상담 철학</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/taxbaik/blog" class="btn btn-outline-primary btn-sm w-100 py-3">
|
||||||
|
📝 세무 정보<br/>
|
||||||
|
<small class="text-muted">절세팁 및 신고 가이드</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,10 +26,12 @@
|
|||||||
|
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<meta name="theme-color" content="#C89D6E" />
|
<meta name="theme-color" content="#C89D6E" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/taxbaik/favicon.svg" />
|
||||||
|
<link rel="alternate icon" href="/taxbaik/favicon.ico" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
|
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Sans+KR:wght@400;500;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<link rel="canonical" href="@(ViewData["CanonicalUrl"] ?? "http://178.104.200.7/taxbaik/")" />
|
<link rel="canonical" href="@(ViewData["CanonicalUrl"] ?? "http://178.104.200.7/taxbaik/")" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||||
@@ -60,37 +62,51 @@
|
|||||||
<main role="main" class="pb-5">
|
<main role="main" class="pb-5">
|
||||||
@RenderBody()
|
@RenderBody()
|
||||||
</main>
|
</main>
|
||||||
<footer class="bg-light border-top mt-5 py-4">
|
<footer class="bg-light border-top mt-5 py-5">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row g-4">
|
<div class="row g-5">
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<h6 class="fw-bold">백원숙 세무회계</h6>
|
<h6 class="fw-bold mb-3">백원숙 세무회계</h6>
|
||||||
<p class="small text-muted">
|
<p class="small text-muted">
|
||||||
사업자 기장, 부동산 양도세·증여세,<br />
|
사업자 기장, 부동산 양도세·증여세,<br />
|
||||||
종합소득세 전문 상담
|
종합소득세 전문 상담
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<h6 class="fw-bold">연락처</h6>
|
<h6 class="fw-bold mb-3">메뉴</h6>
|
||||||
|
<ul class="list-unstyled small">
|
||||||
|
<li class="mb-2"><a href="/taxbaik/" class="text-decoration-none text-muted">홈</a></li>
|
||||||
|
<li class="mb-2"><a href="/taxbaik/about" class="text-decoration-none text-muted">세무사 소개</a></li>
|
||||||
|
<li class="mb-2"><a href="/taxbaik/services" class="text-decoration-none text-muted">전문 서비스</a></li>
|
||||||
|
<li class="mb-2"><a href="/taxbaik/blog" class="text-decoration-none text-muted">세무 정보</a></li>
|
||||||
|
<li><a href="/taxbaik/contact" class="text-decoration-none text-muted">상담 신청</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h6 class="fw-bold mb-3">연락처</h6>
|
||||||
<p class="small">
|
<p class="small">
|
||||||
📞 <a href="tel:010-4122-8268" class="text-decoration-none">010-4122-8268</a><br />
|
📞 <a href="tel:010-4122-8268" class="text-decoration-none text-muted">010-4122-8268</a><br />
|
||||||
📧 <a href="mailto:taxbaik5668@gmail.com" class="text-decoration-none">taxbaik5668@gmail.com</a>
|
📧 <a href="mailto:taxbaik5668@gmail.com" class="text-decoration-none text-muted">taxbaik5668@gmail.com</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<h6 class="fw-bold">채널</h6>
|
<h6 class="fw-bold mb-3">채널</h6>
|
||||||
<p class="small">
|
<p class="small">
|
||||||
<a href="http://pf.kakao.com/_xoxchTX" target="_blank" class="btn btn-sm btn-warning me-2">카카오톡</a>
|
<a href="http://pf.kakao.com/_xoxchTX" target="_blank" class="btn btn-sm btn-warning me-2">카카오톡</a>
|
||||||
<a href="https://www.instagram.com/taxtory5668/" target="_blank" class="btn btn-sm btn-outline-secondary">Instagram</a>
|
<a href="https://www.instagram.com/taxtory5668/" target="_blank" class="btn btn-sm btn-outline-secondary">Instagram</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-3" />
|
<hr class="my-4" />
|
||||||
<div class="text-center small text-muted">
|
<div class="text-center small text-muted">
|
||||||
<p>© 2026 백원숙 세무회계. All rights reserved.</p>
|
<p>© 2026 백원숙 세무회계. All rights reserved.</p>
|
||||||
<a href="/taxbaik/privacy" class="text-decoration-none text-muted me-2">개인정보처리방침</a>
|
<div class="mb-2">
|
||||||
<a href="/taxbaik/terms" class="text-decoration-none text-muted">이용약관</a>
|
<a href="/taxbaik/privacy" class="text-decoration-none text-muted me-2">개인정보처리방침</a>
|
||||||
<a href="/taxbaik/portal" class="text-decoration-none text-muted ms-2">고객 포털</a>
|
<span class="text-muted">|</span>
|
||||||
|
<a href="/taxbaik/terms" class="text-decoration-none text-muted ms-2 me-2">이용약관</a>
|
||||||
|
<span class="text-muted">|</span>
|
||||||
|
<a href="/taxbaik/portal" class="text-decoration-none text-muted ms-2">고객 포털</a>
|
||||||
|
</div>
|
||||||
@if (Context.RequestServices.GetService(typeof(VersionInfo)) is VersionInfo version)
|
@if (Context.RequestServices.GetService(typeof(VersionInfo)) is VersionInfo version)
|
||||||
{
|
{
|
||||||
<div class="mt-2 text-muted" style="font-size: 0.75rem; opacity: 0.6;">
|
<div class="mt-2 text-muted" style="font-size: 0.75rem; opacity: 0.6;">
|
||||||
|
|||||||
+36
-12
@@ -54,7 +54,9 @@ builder.Services.AddHealthChecks();
|
|||||||
|
|
||||||
// Razor Pages + Blazor Server 통합
|
// Razor Pages + Blazor Server 통합
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
builder.Services.AddRazorComponents()
|
||||||
|
.AddInteractiveServerComponents()
|
||||||
|
.AddInteractiveWebAssemblyComponents();
|
||||||
builder.Services.Configure<Microsoft.AspNetCore.Components.Server.CircuitOptions>(options =>
|
builder.Services.Configure<Microsoft.AspNetCore.Components.Server.CircuitOptions>(options =>
|
||||||
{
|
{
|
||||||
options.DetailedErrors = true;
|
options.DetailedErrors = true;
|
||||||
@@ -208,59 +210,65 @@ var apiBaseUrl = builder.Configuration["ApiClient:BaseUrl"]
|
|||||||
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client =>
|
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client =>
|
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client =>
|
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client =>
|
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client =>
|
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client =>
|
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
// Phase 5: Tax Accounting & CRM Browser Clients
|
// Phase 5: Tax Accounting & CRM Browser Clients
|
||||||
builder.Services.AddHttpClient<ITaxProfileBrowserClient, TaxProfileBrowserClient>(client =>
|
builder.Services.AddHttpClient<ITaxProfileBrowserClient, TaxProfileBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient<ITaxFilingScheduleBrowserClient, TaxFilingScheduleBrowserClient>(client =>
|
builder.Services.AddHttpClient<ITaxFilingScheduleBrowserClient, TaxFilingScheduleBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IConsultingActivityBrowserClient, ConsultingActivityBrowserClient>(client =>
|
builder.Services.AddHttpClient<IConsultingActivityBrowserClient, ConsultingActivityBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IContractBrowserClient, ContractBrowserClient>(client =>
|
builder.Services.AddHttpClient<IContractBrowserClient, ContractBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBrowserClient>(client =>
|
builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<ICommonCodeBrowserClient, CommonCodeBrowserClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||||
|
|
||||||
// UI & 캐시 (MudBlazor Theme Customization)
|
// UI & 캐시 (MudBlazor Theme Customization)
|
||||||
builder.Services.AddMudServices(config =>
|
builder.Services.AddMudServices(config =>
|
||||||
{
|
{
|
||||||
config.SnackbarConfiguration.HideTransitionDuration = 400;
|
config.SnackbarConfiguration.HideTransitionDuration = 400;
|
||||||
config.SnackbarConfiguration.ShowTransitionDuration = 300;
|
config.SnackbarConfiguration.ShowTransitionDuration = 300;
|
||||||
|
config.PopoverOptions.ThrowOnDuplicateProvider = false;
|
||||||
});
|
});
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddResponseCompression(opts => {
|
builder.Services.AddResponseCompression(opts => {
|
||||||
@@ -307,6 +315,20 @@ app.UseForwardedHeaders(new ForwardedHeadersOptions
|
|||||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
var path = context.Request.Path.Value ?? string.Empty;
|
||||||
|
if (path.Equals("/favicon.ico", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
path.Equals("/taxbaik/favicon.ico", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "image/svg+xml";
|
||||||
|
await context.Response.SendFileAsync(Path.Combine(app.Environment.WebRootPath ?? "wwwroot", "favicon.svg"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
// Run migrations on startup (non-blocking for development)
|
// Run migrations on startup (non-blocking for development)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -348,6 +370,8 @@ app.MapRazorPages();
|
|||||||
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
|
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
|
||||||
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
|
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
|
.AddInteractiveWebAssemblyRenderMode()
|
||||||
|
.AddAdditionalAssemblies(typeof(TaxBaik.WasmClient._Imports).Assembly)
|
||||||
.AllowAnonymous();
|
.AllowAnonymous();
|
||||||
|
|
||||||
// 애플리케이션 시작/종료 로깅
|
// 애플리케이션 시작/종료 로깅
|
||||||
|
|||||||
@@ -15,18 +15,29 @@ public class TelegramReportBackgroundService(
|
|||||||
{
|
{
|
||||||
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(30));
|
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(30));
|
||||||
|
|
||||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
try
|
||||||
{
|
{
|
||||||
try
|
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||||
{
|
{
|
||||||
var now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, KoreaTimeZone);
|
try
|
||||||
await TrySendReportsAsync(now, stoppingToken);
|
{
|
||||||
}
|
var now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, KoreaTimeZone);
|
||||||
catch (Exception ex)
|
await TrySendReportsAsync(now, stoppingToken);
|
||||||
{
|
}
|
||||||
logger.LogError(ex, "Telegram report background loop failed");
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Telegram report background loop failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Normal shutdown path.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TrySendReportsAsync(DateTimeOffset nowKst, CancellationToken ct)
|
private async Task TrySendReportsAsync(DateTimeOffset nowKst, CancellationToken ct)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
||||||
<ProjectReference Include="..\TaxBaik.Infrastructure\TaxBaik.Infrastructure.csproj" />
|
<ProjectReference Include="..\TaxBaik.Infrastructure\TaxBaik.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\TaxBaik.Web.Client\TaxBaik.Web.Client.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.9" />
|
||||||
<PackageReference Include="MudBlazor" Version="6.10.0" />
|
<PackageReference Include="MudBlazor" Version="6.10.0" />
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.9" />
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Markdig" Version="0.38.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -64,35 +64,35 @@
|
|||||||
|
|
||||||
/* Spacing Scale */
|
/* Spacing Scale */
|
||||||
--space-0: 0;
|
--space-0: 0;
|
||||||
--space-1: 4px;
|
--space-1: 3px;
|
||||||
--space-2: 8px;
|
--space-2: 6px;
|
||||||
--space-3: 12px;
|
--space-3: 10px;
|
||||||
--space-4: 16px;
|
--space-4: 12px;
|
||||||
--space-5: 20px;
|
--space-5: 16px;
|
||||||
--space-6: 24px;
|
--space-6: 20px;
|
||||||
--space-7: 28px;
|
--space-7: 24px;
|
||||||
--space-8: 32px;
|
--space-8: 28px;
|
||||||
--space-10: 40px;
|
--space-10: 34px;
|
||||||
--space-12: 48px;
|
--space-12: 40px;
|
||||||
--space-16: 64px;
|
--space-16: 52px;
|
||||||
|
|
||||||
/* Border Radius */
|
/* Border Radius */
|
||||||
--radius-sm: 4px;
|
--radius-sm: 3px;
|
||||||
--radius-md: 8px;
|
--radius-md: 6px;
|
||||||
--radius-lg: 12px;
|
--radius-lg: 8px;
|
||||||
--radius-xl: 16px;
|
--radius-xl: 12px;
|
||||||
--radius-full: 9999px;
|
--radius-full: 9999px;
|
||||||
|
|
||||||
/* Typography Scale */
|
/* Typography Scale */
|
||||||
--font-family-base: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
--font-family-base: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
--font-size-xs: 0.75rem;
|
--font-size-xs: 0.7rem;
|
||||||
--font-size-sm: 0.875rem;
|
--font-size-sm: 0.75rem;
|
||||||
--font-size-base: 1rem;
|
--font-size-base: 0.82rem;
|
||||||
--font-size-lg: 1.125rem;
|
--font-size-lg: 0.95rem;
|
||||||
--font-size-xl: 1.25rem;
|
--font-size-xl: 1.1rem;
|
||||||
--font-size-2xl: 1.5rem;
|
--font-size-2xl: 1.3rem;
|
||||||
--font-size-3xl: 1.875rem;
|
--font-size-3xl: 1.6rem;
|
||||||
--font-size-4xl: 2.25rem;
|
--font-size-4xl: 2rem;
|
||||||
|
|
||||||
--font-weight-regular: 400;
|
--font-weight-regular: 400;
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
@@ -445,11 +445,12 @@ textarea:focus-visible {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 6px 16px;
|
padding: 0px 12px;
|
||||||
|
height: 38px !important;
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
z-index: var(--z-dropdown);
|
z-index: var(--z-dropdown);
|
||||||
box-shadow: var(--shadow-xs);
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-menu-button {
|
.admin-menu-button {
|
||||||
@@ -571,6 +572,33 @@ textarea:focus-visible {
|
|||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-drawer-version {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: var(--space-4);
|
||||||
|
border-top: 1px solid var(--border-color-light);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-drawer-version-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-drawer-version-value {
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-drawer-version-built {
|
||||||
|
margin-top: 2px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-main {
|
.admin-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -1641,3 +1669,58 @@ textarea:focus-visible {
|
|||||||
margin-right: -8px;
|
margin-right: -8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
더존 ERP 스타일 최적화 (Douzone ERP High-Density Desktop Style)
|
||||||
|
- 프레임워크 고유 레이아웃과 이벤트를 방해하는 와일드카드 및 강제 강하 스타일 제거
|
||||||
|
- MudBlazor 테마 설정을 기반으로 하며 레이아웃 및 서체 스택만 안전하게 제어
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: #E2E8F0 !important;
|
||||||
|
color: #1E293B !important;
|
||||||
|
font-family: 'Malgun Gothic', '맑은 고딕', 'Segoe UI', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 어드민 드로워 및 탑바 테마 컬러 보완 */
|
||||||
|
.mud-drawer {
|
||||||
|
border-right: 1px solid #CBD5E1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mud-drawer-header {
|
||||||
|
border-bottom: 1px solid #1E293B !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mud-nav-link {
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 데이터그리드 헤더 가시성 보완 */
|
||||||
|
.mud-table-head th {
|
||||||
|
background-color: #F1F5F9 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #0F172A !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 페이지 헤더 영역 */
|
||||||
|
.admin-page-hero {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
background-color: #F8FAFC !important;
|
||||||
|
border-bottom: 1px solid #E2E8F0 !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-page-title {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-page-subtitle {
|
||||||
|
font-size: 12px !important;
|
||||||
|
color: #64748B !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-eyebrow {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -269,34 +269,6 @@ a:hover {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 휴스 스트립 (신뢰도) ===== */
|
|
||||||
.trust-strip {
|
|
||||||
background: linear-gradient(135deg, var(--color-bg-alt) 0%, var(--color-accent) 100%);
|
|
||||||
padding: var(--spacing-3xl) 0;
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trust-item {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trust-icon {
|
|
||||||
font-size: 3.5rem;
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trust-item h3 {
|
|
||||||
color: var(--color-text);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trust-item p {
|
|
||||||
color: var(--color-text-light);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 배지 ===== */
|
/* ===== 배지 ===== */
|
||||||
.badge {
|
.badge {
|
||||||
@@ -419,10 +391,6 @@ body.with-mobile-cta {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trust-icon {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 0 var(--spacing-md);
|
padding: 0 var(--spacing-md);
|
||||||
}
|
}
|
||||||
@@ -746,3 +714,263 @@ img {
|
|||||||
.faq-answer ul li {
|
.faq-answer ul li {
|
||||||
margin-bottom: 0.4rem;
|
margin-bottom: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 프리미엄 고도화 & 마이크로 인터랙션 (2026-06-30) ===== */
|
||||||
|
|
||||||
|
/* 영어/숫자용 폰트 클래스 */
|
||||||
|
.font-numeric, .font-heading-en {
|
||||||
|
font-family: 'Outfit', 'Inter', 'Noto Sans KR', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 히어로 섹션 프리미엄 개편 (메쉬 그라데이션 및 CSS 애니메이션) */
|
||||||
|
.hero-section {
|
||||||
|
background: radial-gradient(circle at 10% 20%, rgba(46, 92, 78, 1) 0%, rgba(31, 58, 48, 1) 44%, rgba(13, 30, 26, 1) 100%) !important;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -30%;
|
||||||
|
right: -10%;
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
background: radial-gradient(circle, rgba(200, 157, 110, 0.25) 0%, rgba(200, 157, 110, 0) 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: floatAnimation 8s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20%;
|
||||||
|
left: -10%;
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
background: radial-gradient(circle, rgba(232, 228, 216, 0.15) 0%, rgba(232, 228, 216, 0) 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: floatAnimation2 12s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatAnimation {
|
||||||
|
0% { transform: translateY(0px) scale(1); }
|
||||||
|
50% { transform: translateY(-30px) scale(1.05); }
|
||||||
|
100% { transform: translateY(0px) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatAnimation2 {
|
||||||
|
0% { transform: translateX(0px) rotate(0deg); }
|
||||||
|
50% { transform: translateX(20px) translateY(15px) rotate(10deg); }
|
||||||
|
100% { transform: translateX(0px) rotate(0deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 서비스 카드 고도화 */
|
||||||
|
.service-card {
|
||||||
|
border: 1px solid rgba(217, 211, 196, 0.6) !important;
|
||||||
|
box-shadow: 0 10px 25px rgba(61, 40, 23, 0.03) !important;
|
||||||
|
transition: all var(--transition-normal) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card:hover {
|
||||||
|
transform: translateY(-8px) !important;
|
||||||
|
box-shadow: 0 20px 40px rgba(61, 40, 23, 0.1) !important;
|
||||||
|
border-color: var(--color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card--featured {
|
||||||
|
background: linear-gradient(180deg, #FFFFFF 0%, #FAF8F5 100%) !important;
|
||||||
|
border-left: 4px solid var(--color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 글래스모피즘 포털 클래스 (Glassmorphism Portal Classes) */
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
backdrop-filter: blur(12px) saturate(180%) !important;
|
||||||
|
-webkit-backdrop-filter: blur(12px) saturate(180%) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4) !important;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(61, 40, 23, 0.05) !important;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(61, 40, 23, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-welcome-strip {
|
||||||
|
background: linear-gradient(135deg, var(--color-secondary-dark) 0%, #152A22 100%);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
color: white;
|
||||||
|
padding: 2.5rem;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-bottom: 3px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 타임라인 컴포넌트 뷰티화 */
|
||||||
|
.timeline-item-modern {
|
||||||
|
border-left: 2px solid rgba(200, 157, 110, 0.4);
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item-modern::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-primary);
|
||||||
|
left: -7px;
|
||||||
|
top: 6px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(200, 157, 110, 0.25);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item-modern:hover::after {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
box-shadow: 0 0 0 6px rgba(46, 92, 78, 0.3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 마크다운 스타일 ===== */
|
||||||
|
.markdown-body {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
border-bottom: 2px solid var(--color-primary);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h4 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
background: var(--color-bg-alt);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #d63384;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
background: var(--color-text);
|
||||||
|
color: #f8f8f8;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre code {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ul,
|
||||||
|
.markdown-body ol {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
border-left: 4px solid var(--color-primary);
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th,
|
||||||
|
.markdown-body table td {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th {
|
||||||
|
background: var(--color-bg-alt);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr:nth-child(even) {
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a {
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--color-primary);
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="TaxBaik">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1f3c88"/>
|
||||||
|
<stop offset="100%" stop-color="#d7a86e"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="64" height="64" rx="16" fill="url(#g)"/>
|
||||||
|
<path d="M18 24h28v6H18zM22 32h20v6H22zM26 40h12v6H26z" fill="#fff" opacity="0.95"/>
|
||||||
|
<path d="M16 18h32v2H16z" fill="#ffffff" opacity="0.35"/>
|
||||||
|
<circle cx="46" cy="18" r="5" fill="#fff" opacity="0.9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 564 B |
@@ -11,6 +11,9 @@ window.taxbaikAdminSession = {
|
|||||||
|
|
||||||
clearAuthToken: function () {
|
clearAuthToken: function () {
|
||||||
try {
|
try {
|
||||||
|
localStorage.removeItem('accessToken');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
localStorage.removeItem('tokenExpiry');
|
||||||
localStorage.removeItem('auth_token');
|
localStorage.removeItem('auth_token');
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore storage errors; redirect still recovers the session.
|
// Ignore storage errors; redirect still recovers the session.
|
||||||
@@ -18,6 +21,11 @@ window.taxbaikAdminSession = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showLoading: function () {
|
showLoading: function () {
|
||||||
|
if (document.documentElement.classList.contains('admin-login-route')) {
|
||||||
|
window.taxbaikAdminSession.hideLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const overlay = document.getElementById('blazor-loading');
|
const overlay = document.getElementById('blazor-loading');
|
||||||
if (!overlay) return;
|
if (!overlay) return;
|
||||||
|
|
||||||
@@ -81,6 +89,10 @@ window.taxbaikAdminSession = {
|
|||||||
window.taxbaikAdminSession.syncRouteClass();
|
window.taxbaikAdminSession.syncRouteClass();
|
||||||
window.addEventListener('popstate', window.taxbaikAdminSession.syncRouteClass);
|
window.addEventListener('popstate', window.taxbaikAdminSession.syncRouteClass);
|
||||||
|
|
||||||
|
if (document.documentElement.classList.contains('admin-login-route')) {
|
||||||
|
window.taxbaikAdminSession.hideLoading();
|
||||||
|
}
|
||||||
|
|
||||||
// Show loading on initial page load — overlay has 'show' from HTML,
|
// Show loading on initial page load — overlay has 'show' from HTML,
|
||||||
// but we still need to set up the observer to detect when to hide it.
|
// but we still need to set up the observer to detect when to hide it.
|
||||||
window.taxbaikAdminSession.showLoading();
|
window.taxbaikAdminSession.showLoading();
|
||||||
@@ -98,5 +110,102 @@ window.taxbaikAdminSession = {
|
|||||||
|
|
||||||
new MutationObserver(reloadOnRejectedCircuit)
|
new MutationObserver(reloadOnRejectedCircuit)
|
||||||
.observe(modal, { attributes: true, attributeFilter: ['class'] });
|
.observe(modal, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
},
|
||||||
|
|
||||||
|
bindLoginForm: function () {
|
||||||
|
const form = document.getElementById('admin-login-form');
|
||||||
|
if (!form || form.dataset.bound === '1') return;
|
||||||
|
|
||||||
|
form.dataset.bound = '1';
|
||||||
|
form.addEventListener('submit', async function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const username = form.querySelector('input[placeholder="사용자명"]')?.value?.trim() || '';
|
||||||
|
const password = form.querySelector('input[placeholder="비밀번호"]')?.value || '';
|
||||||
|
const rememberMe = form.querySelector('input[type="checkbox"]')?.checked || false;
|
||||||
|
const existing = form.parentElement.querySelector('.login-error-message');
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
if (existing) existing.remove();
|
||||||
|
if (submitButton) submitButton.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/taxbaik/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data?.accessToken || !data?.refreshToken) {
|
||||||
|
throw new Error('invalid response');
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('accessToken', data.accessToken);
|
||||||
|
localStorage.setItem('refreshToken', data.refreshToken);
|
||||||
|
localStorage.setItem('tokenExpiry', String(Date.now() + (data.expiresIn || 3600) * 1000));
|
||||||
|
|
||||||
|
if (rememberMe) {
|
||||||
|
localStorage.setItem('admin-remembered-username', username);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('admin-remembered-username');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = '/taxbaik/admin/dashboard';
|
||||||
|
} catch {
|
||||||
|
const error = document.createElement('div');
|
||||||
|
error.className = 'mud-alert mud-alert-filled-error login-error-message mb-4';
|
||||||
|
error.textContent = '로그인 중 오류가 발생했습니다.';
|
||||||
|
form.parentElement.insertBefore(error, form);
|
||||||
|
} finally {
|
||||||
|
if (submitButton) submitButton.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 더존 ERP 스타일 엔터 키 포커스 이동 및 단축키 바인딩
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const active = document.activeElement;
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
// 특정 영역(편집 폼 또는 다이얼로그) 내의 입력 필드만 포커스 이동 처리
|
||||||
|
const container = active.closest('.admin-editor-panel, .mud-form, .mud-dialog');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// textarea나 button, submit 타입 등은 기본 동작(줄바꿈/제출) 유지
|
||||||
|
if (active.tagName === 'TEXTAREA' ||
|
||||||
|
active.tagName === 'BUTTON' ||
|
||||||
|
active.getAttribute('type') === 'submit' ||
|
||||||
|
active.classList.contains('mud-button-root')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 포커스 이동 가능한 모든 입력 요소 수집
|
||||||
|
const focusables = Array.from(container.querySelectorAll('input, select, textarea, button'))
|
||||||
|
.filter(el => {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return el.tabIndex >= 0 &&
|
||||||
|
!el.disabled &&
|
||||||
|
el.getAttribute('aria-disabled') !== 'true' &&
|
||||||
|
style.display !== 'none' &&
|
||||||
|
style.visibility !== 'hidden';
|
||||||
|
});
|
||||||
|
|
||||||
|
const index = focusables.indexOf(active);
|
||||||
|
if (index > -1 && index < focusables.length - 1) {
|
||||||
|
const nextEl = focusables[index + 1];
|
||||||
|
nextEl.focus();
|
||||||
|
if (typeof nextEl.select === 'function') {
|
||||||
|
nextEl.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
+14
@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaxBaik.Web", "TaxBaik.Web\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaxBaik.Application.Tests", "TaxBaik.Application.Tests\TaxBaik.Application.Tests.csproj", "{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaxBaik.Application.Tests", "TaxBaik.Application.Tests\TaxBaik.Application.Tests.csproj", "{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaxBaik.Web.Client", "TaxBaik.Web.Client\TaxBaik.Web.Client.csproj", "{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -83,6 +85,18 @@ Global
|
|||||||
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x64.Build.0 = Release|Any CPU
|
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x86.ActiveCfg = Release|Any CPU
|
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x86.Build.0 = Release|Any CPU
|
{47D1F07D-F11B-4343-A3C3-1872F0C46AE3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{C46C51D4-9E87-47DF-AB76-2E794F64FD5F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
+1603
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,514 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="./support.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<x-dc>
|
||||||
|
<helmet>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Hahmlet:wght@600;700;900&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pretendard@latest/dist/web/static/pretendard.css">
|
||||||
|
<style>
|
||||||
|
@keyframes fadeUp { from{opacity:0;transform:translateY(36px)} to{opacity:1;transform:translateY(0)} }
|
||||||
|
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
body{font-family:'Pretendard',-apple-system,BlinkMacSystemFont,sans-serif;background:#fafaf8;color:#1a2232;overflow-x:hidden;line-height:1.7}
|
||||||
|
::selection{background:rgba(201,168,76,0.22)}
|
||||||
|
a{text-decoration:none;color:inherit}
|
||||||
|
button{cursor:pointer;font-family:inherit;border:none;background:none}
|
||||||
|
@media(max-width:768px){
|
||||||
|
.nav-links{display:none!important}
|
||||||
|
.section-px{padding-left:24px!important;padding-right:24px!important}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</helmet>
|
||||||
|
|
||||||
|
<!-- ── NAV ── -->
|
||||||
|
<nav style="{{ navStyle }}">
|
||||||
|
<div style="{{ navLogoStyle }}">백원숙 세무사</div>
|
||||||
|
<div style="display:flex;gap:28px;align-items:center;" class="nav-links">
|
||||||
|
<a href="#about" style="{{ navLinkStyle }}" style-hover="color:#c9a84c;">소개</a>
|
||||||
|
<a href="#services" style="{{ navLinkStyle }}" style-hover="color:#c9a84c;">서비스</a>
|
||||||
|
<a href="#customers" style="{{ navLinkStyle }}" style-hover="color:#c9a84c;">고객유형</a>
|
||||||
|
<a href="#faq" style="{{ navLinkStyle }}" style-hover="color:#c9a84c;">FAQ</a>
|
||||||
|
<a href="#contact" style="background:#c9a84c;color:#0d2340;padding:10px 22px;border-radius:5px;font-size:0.875rem;font-weight:700;transition:filter 0.2s;" style-hover="filter:brightness(0.92);">상담 예약</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- ── HERO ── -->
|
||||||
|
<section style="min-height:100vh;background:#0d2340;display:flex;align-items:center;position:relative;overflow:hidden;">
|
||||||
|
<div style="position:absolute;top:-180px;right:-180px;width:760px;height:760px;border-radius:50%;border:1px solid rgba(201,168,76,0.12);pointer-events:none;"></div>
|
||||||
|
<div style="position:absolute;top:-80px;right:-80px;width:460px;height:460px;border-radius:50%;border:1px solid rgba(201,168,76,0.07);pointer-events:none;"></div>
|
||||||
|
<div style="position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(201,168,76,0.25),transparent);"></div>
|
||||||
|
|
||||||
|
<div style="max-width:1200px;margin:0 auto;width:100%;padding:140px 60px 90px;" class="section-px">
|
||||||
|
<div style="animation:fadeIn 0.7s ease both;margin-bottom:18px;">
|
||||||
|
<span style="font-size:0.72rem;letter-spacing:0.22em;color:#c9a84c;font-weight:600;text-transform:uppercase;">공인 세무사 · 부동산중개사 · 보험설계사</span>
|
||||||
|
</div>
|
||||||
|
<h1 style="font-family:'Hahmlet',serif;font-size:clamp(2.4rem,5.5vw,5rem);font-weight:900;color:white;line-height:1.18;letter-spacing:-0.035em;margin-bottom:28px;animation:fadeUp 0.8s ease 0.08s both;">
|
||||||
|
사업의 숫자와<br>
|
||||||
|
가족의 자산을<br>
|
||||||
|
<span style="color:#c9a84c;">함께 지키는 세무사</span>
|
||||||
|
</h1>
|
||||||
|
<p style="font-size:clamp(0.95rem,1.8vw,1.1rem);color:rgba(255,255,255,0.65);max-width:540px;line-height:2;margin-bottom:44px;animation:fadeUp 0.8s ease 0.18s both;">
|
||||||
|
스마트스토어·프리랜서·개인사업자부터 부동산·가족자산까지 —<br>전국 어디서나 <strong style="color:rgba(255,255,255,0.9);font-weight:600;">비대면 온라인 상담</strong>으로 시작하세요.
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;gap:14px;flex-wrap:wrap;animation:fadeUp 0.8s ease 0.28s both;">
|
||||||
|
<a href="https://pf.kakao.com/_xoxchTX" target="_blank" style="background:#FEE500;color:#3C1E1E;padding:16px 30px;border-radius:6px;font-weight:700;font-size:1rem;display:inline-flex;align-items:center;gap:8px;transition:filter 0.2s;" style-hover="filter:brightness(0.95);">💬 카카오로 상담하기</a>
|
||||||
|
<a href="tel:010-4122-8268" style="background:rgba(255,255,255,0.08);color:white;padding:16px 30px;border-radius:6px;font-weight:500;font-size:1rem;border:1px solid rgba(255,255,255,0.2);transition:background 0.2s;" style-hover="background:rgba(255,255,255,0.14);">📞 010-4122-8268</a>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:28px;margin-top:60px;animation:fadeUp 0.8s ease 0.38s both;flex-wrap:wrap;padding-top:32px;border-top:1px solid rgba(255,255,255,0.08);">
|
||||||
|
<div style="display:flex;align-items:center;gap:9px;"><div style="width:5px;height:5px;border-radius:50%;background:#c9a84c;flex-shrink:0;"></div><span style="color:rgba(255,255,255,0.45);font-size:0.8rem;font-weight:300;">세무사 자격 (2015)</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:9px;"><div style="width:5px;height:5px;border-radius:50%;background:#c9a84c;flex-shrink:0;"></div><span style="color:rgba(255,255,255,0.45);font-size:0.8rem;font-weight:300;">공인 부동산중개사</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:9px;"><div style="width:5px;height:5px;border-radius:50%;background:#c9a84c;flex-shrink:0;"></div><span style="color:rgba(255,255,255,0.45);font-size:0.8rem;font-weight:300;">보험설계사 자격</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:9px;"><div style="width:5px;height:5px;border-radius:50%;background:#c9a84c;flex-shrink:0;"></div><span style="color:rgba(255,255,255,0.45);font-size:0.8rem;font-weight:300;">전국 비대면 온라인 상담</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── ONLINE TRUST BAR ── -->
|
||||||
|
<div style="background:#1a3a5c;padding:20px 60px;" class="section-px">
|
||||||
|
<div style="max-width:1200px;margin:0 auto;display:flex;align-items:center;justify-content:center;gap:48px;flex-wrap:wrap;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<span style="font-size:1.1rem;">💻</span>
|
||||||
|
<span style="font-size:0.85rem;color:rgba(255,255,255,0.85);font-weight:500;">전국 비대면 온라인 상담</span>
|
||||||
|
</div>
|
||||||
|
<div style="width:1px;height:18px;background:rgba(255,255,255,0.15);"></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<span style="font-size:1.1rem;">💬</span>
|
||||||
|
<span style="font-size:0.85rem;color:rgba(255,255,255,0.85);font-weight:500;">카카오 당일 응답</span>
|
||||||
|
</div>
|
||||||
|
<div style="width:1px;height:18px;background:rgba(255,255,255,0.15);"></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<span style="font-size:1.1rem;">📂</span>
|
||||||
|
<span style="font-size:0.85rem;color:rgba(255,255,255,0.85);font-weight:500;">자료 공유 후 온라인 검토</span>
|
||||||
|
</div>
|
||||||
|
<div style="width:1px;height:18px;background:rgba(255,255,255,0.15);"></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<span style="font-size:1.1rem;">✅</span>
|
||||||
|
<span style="font-size:0.85rem;color:rgba(255,255,255,0.85);font-weight:500;">방문 없이 신고·기장 가능</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── ABOUT ── -->
|
||||||
|
<section id="about" style="padding:100px 60px;background:white;" class="section-px">
|
||||||
|
<div style="max-width:1200px;margin:0 auto;">
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:60px;align-items:start;">
|
||||||
|
|
||||||
|
<div style="background:#0d2340;border-radius:20px;padding:52px 44px;">
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.18em;color:#c9a84c;font-weight:600;margin-bottom:20px;text-transform:uppercase;">About</div>
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:1.9rem;font-weight:700;color:white;line-height:1.35;margin-bottom:24px;">안녕하세요.<br>백원숙 세무사입니다.</h2>
|
||||||
|
<p style="color:rgba(255,255,255,0.7);line-height:1.95;font-size:0.9rem;margin-bottom:18px;">세무사 자격과 함께 부동산중개사, 보험설계사 자격을 보유하고 있습니다. 사업자 세무, 종합소득세, 부가가치세, 양도세, 증여·상속 상담을 중심으로 운영합니다.</p>
|
||||||
|
<p style="color:rgba(255,255,255,0.7);line-height:1.95;font-size:0.9rem;">저도 집을 사업장으로 등록하고 작게 시작해 본 사람입니다. 처음 사업을 시작하는 대표님의 막막함을 직접 압니다.</p>
|
||||||
|
<div style="margin-top:36px;padding-top:28px;border-top:1px solid rgba(255,255,255,0.1);display:flex;gap:36px;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.76rem;color:rgba(255,255,255,0.38);margin-bottom:6px;">세무사 자격 취득</div>
|
||||||
|
<div style="font-size:1.2rem;font-family:'Hahmlet',serif;font-weight:700;color:#c9a84c;">2015년</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.76rem;color:rgba(255,255,255,0.38);margin-bottom:6px;">활동 지역</div>
|
||||||
|
<div style="font-size:1.2rem;font-family:'Hahmlet',serif;font-weight:700;color:#c9a84c;">성북구</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.18em;color:#c9a84c;font-weight:600;margin-bottom:16px;text-transform:uppercase;">Expertise</div>
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:2rem;font-weight:700;color:#0d2340;margin-bottom:36px;line-height:1.3;">세 가지 자격의<br>시너지</h2>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||||
|
<div style="display:flex;gap:18px;padding:24px;border:1.5px solid #ede9e0;border-radius:14px;transition:border-color 0.2s;" style-hover="border-color:#c9a84c;background:#fffdf7;">
|
||||||
|
<div style="width:48px;height:48px;background:#f5f3ee;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;flex-shrink:0;">⚖️</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:700;font-size:0.975rem;color:#0d2340;margin-bottom:5px;">공인 세무사</div>
|
||||||
|
<div style="font-size:0.845rem;color:#6b7e8f;line-height:1.75;">세무신고·장부관리·조세 자문 등 세무 업무 전반을 공식 대리합니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:18px;padding:24px;border:1.5px solid #ede9e0;border-radius:14px;transition:border-color 0.2s;" style-hover="border-color:#c9a84c;background:#fffdf7;">
|
||||||
|
<div style="width:48px;height:48px;background:#f5f3ee;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;flex-shrink:0;">🏠</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:700;font-size:0.975rem;color:#0d2340;margin-bottom:5px;">공인 부동산중개사</div>
|
||||||
|
<div style="font-size:0.845rem;color:#6b7e8f;line-height:1.75;">부동산 거래 구조를 이해해 양도·증여·임대 세무상담에 현실감을 더합니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:18px;padding:24px;border:1.5px solid #ede9e0;border-radius:14px;transition:border-color 0.2s;" style-hover="border-color:#c9a84c;background:#fffdf7;">
|
||||||
|
<div style="width:48px;height:48px;background:#f5f3ee;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;flex-shrink:0;">🛡️</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:700;font-size:0.975rem;color:#0d2340;margin-bottom:5px;">보험설계사 자격</div>
|
||||||
|
<div style="font-size:0.845rem;color:#6b7e8f;line-height:1.75;">상속·증여·대표자 리스크 관점에서 가족 현금흐름과 보험 구조를 함께 설명합니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── SERVICES ── -->
|
||||||
|
<section id="services" style="padding:100px 60px;background:#f2f5f9;" class="section-px">
|
||||||
|
<div style="max-width:1200px;margin:0 auto;">
|
||||||
|
<div style="text-align:center;margin-bottom:64px;">
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.18em;color:#c9a84c;font-weight:600;margin-bottom:14px;text-transform:uppercase;">Services</div>
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:2.2rem;font-weight:700;color:#0d2340;margin-bottom:14px;">주요 서비스</h2>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.925rem;max-width:460px;margin:0 auto;">신고만 하는 세무가 아니라, 사업과 자산의 흐름을 함께 봅니다.</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:22px;">
|
||||||
|
|
||||||
|
<div style="background:white;border-radius:14px;padding:32px;box-shadow:0 2px 16px rgba(13,35,64,0.06);transition:transform 0.25s ease,box-shadow 0.25s ease;" style-hover="transform:translateY(-5px);box-shadow:0 20px 48px rgba(13,35,64,0.13);">
|
||||||
|
<div style="width:48px;height:48px;background:#edf2f7;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;margin-bottom:20px;">📊</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:8px;text-transform:uppercase;">기장 서비스</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.15rem;font-weight:700;color:#0d2340;margin-bottom:12px;">월 기장 관리</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.855rem;line-height:1.85;">장부 작성, 부가세, 원천세, 인건비, 예상세액까지 — 매월 세금 리스크를 함께 점검합니다.</p>
|
||||||
|
<div style="margin-top:22px;padding-top:18px;border-top:1px solid #eef0f3;font-size:0.775rem;color:#9db0bc;">대상: 매출 발생 사업자</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:white;border-radius:14px;padding:32px;box-shadow:0 2px 16px rgba(13,35,64,0.06);transition:transform 0.25s ease,box-shadow 0.25s ease;" style-hover="transform:translateY(-5px);box-shadow:0 20px 48px rgba(13,35,64,0.13);">
|
||||||
|
<div style="width:48px;height:48px;background:#edf2f7;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;margin-bottom:20px;">📋</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:8px;text-transform:uppercase;">소득세</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.15rem;font-weight:700;color:#0d2340;margin-bottom:12px;">종합소득세 신고</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.855rem;line-height:1.85;">사업자, 프리랜서, 보험설계사, 부동산중개사의 소득 유형에 맞는 경비처리와 신고를 안내합니다.</p>
|
||||||
|
<div style="margin-top:22px;padding-top:18px;border-top:1px solid #eef0f3;font-size:0.775rem;color:#9db0bc;">대상: 개인사업자·프리랜서·영업직</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:white;border-radius:14px;padding:32px;box-shadow:0 2px 16px rgba(13,35,64,0.06);transition:transform 0.25s ease,box-shadow 0.25s ease;" style-hover="transform:translateY(-5px);box-shadow:0 20px 48px rgba(13,35,64,0.13);">
|
||||||
|
<div style="width:48px;height:48px;background:#fdf8ec;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;margin-bottom:20px;">🏡</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:8px;text-transform:uppercase;">부동산 세무</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.15rem;font-weight:700;color:#0d2340;margin-bottom:12px;">양도세 사전진단</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.855rem;line-height:1.85;">계약 전 보유기간·비과세 여부·필요경비·장기보유특별공제를 검토합니다. 계약 전 상담이 선택지를 넓힙니다.</p>
|
||||||
|
<div style="margin-top:22px;padding-top:18px;border-top:1px solid #eef0f3;font-size:0.775rem;color:#9db0bc;">대상: 부동산 매도 예정자</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:white;border-radius:14px;padding:32px;box-shadow:0 2px 16px rgba(13,35,64,0.06);transition:transform 0.25s ease,box-shadow 0.25s ease;" style-hover="transform:translateY(-5px);box-shadow:0 20px 48px rgba(13,35,64,0.13);">
|
||||||
|
<div style="width:48px;height:48px;background:#fdf8ec;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;margin-bottom:20px;">👨👩👧</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:8px;text-transform:uppercase;">자산이전</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.15rem;font-weight:700;color:#0d2340;margin-bottom:12px;">증여·상속 상담</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.855rem;line-height:1.85;">증여 시기, 증여재산 평가, 세부담, 자금출처, 보험 활용 가능성까지 — 가족 자산이전을 사전에 설계합니다.</p>
|
||||||
|
<div style="margin-top:22px;padding-top:18px;border-top:1px solid #eef0f3;font-size:0.775rem;color:#9db0bc;">대상: 자산이전 예정 가족</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:white;border-radius:14px;padding:32px;box-shadow:0 2px 16px rgba(13,35,64,0.06);transition:transform 0.25s ease,box-shadow 0.25s ease;" style-hover="transform:translateY(-5px);box-shadow:0 20px 48px rgba(13,35,64,0.13);">
|
||||||
|
<div style="width:48px;height:48px;background:#edf2f7;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;margin-bottom:20px;">🌱</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:8px;text-transform:uppercase;">첫 세무</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.15rem;font-weight:700;color:#0d2340;margin-bottom:12px;">신규 사업자 세무정리</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.855rem;line-height:1.85;">사업자 유형 확인, 부가세·종소세·증빙관리·세금계좌 분리까지. 처음 사업을 시작하는 대표님을 위한 패키지.</p>
|
||||||
|
<div style="margin-top:22px;padding-top:18px;border-top:1px solid #eef0f3;font-size:0.775rem;color:#9db0bc;">대상: 신규 사업자·프리랜서</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#0d2340;border-radius:14px;padding:32px;display:flex;flex-direction:column;justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:16px;text-transform:uppercase;">상담 안내</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.25rem;font-weight:700;color:white;line-height:1.45;margin-bottom:16px;">어떤 세금이<br>걱정이신가요?</h3>
|
||||||
|
<p style="color:rgba(255,255,255,0.6);font-size:0.84rem;line-height:1.85;">세금은 계약·매출·명의·자금 이동 전에 검토할수록 선택지가 많습니다.</p>
|
||||||
|
</div>
|
||||||
|
<a href="https://pf.kakao.com/_xoxchTX" target="_blank" style="display:block;margin-top:28px;background:#c9a84c;color:#0d2340;padding:14px;border-radius:8px;text-align:center;font-weight:700;font-size:0.875rem;transition:filter 0.2s;" style-hover="filter:brightness(1.08);">카카오로 문의하기 →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── CUSTOMER TYPES ── -->
|
||||||
|
<section id="customers" style="padding:100px 60px;background:#1a3a5c;" class="section-px">
|
||||||
|
<div style="max-width:1200px;margin:0 auto;">
|
||||||
|
<div style="text-align:center;margin-bottom:64px;">
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.18em;color:#c9a84c;font-weight:600;margin-bottom:14px;text-transform:uppercase;">Who We Help</div>
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:2.2rem;font-weight:700;color:white;margin-bottom:14px;">전국 어디서나, 온라인으로 시작하세요</h2>
|
||||||
|
<p style="color:rgba(255,255,255,0.52);font-size:0.925rem;">방문 없이 카카오·이메일로 상담부터 신고까지 — 온라인 사업자에게 최적화된 세무관리.</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:22px;">
|
||||||
|
|
||||||
|
<div style="background:rgba(201,168,76,0.12);border:1px solid rgba(201,168,76,0.35);border-radius:18px;padding:38px;transition:background 0.2s;" style-hover="background:rgba(201,168,76,0.18);">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
|
||||||
|
<span style="font-size:1.8rem;">💻</span>
|
||||||
|
<span style="background:#c9a84c;color:#0d2340;font-size:0.65rem;font-weight:700;padding:3px 10px;border-radius:20px;letter-spacing:0.08em;">핵심 타깃</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:10px;text-transform:uppercase;">1순위 · 온라인 사업자</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.2rem;font-weight:700;color:white;margin-bottom:14px;">스마트스토어 · 크리에이터 · 프리랜서</h3>
|
||||||
|
<p style="color:rgba(255,255,255,0.75);font-size:0.855rem;line-height:1.85;margin-bottom:20px;">스마트스토어·쿠팡마켓·유튜버·인스타셀러·크몽 프리랜서 — 플랫폼 정산 구조와 부가세·종소세 경비처리를 체계적으로 관리합니다. 전국 어디서나 비대면 상담 가능합니다.</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:7px;">
|
||||||
|
<span style="background:rgba(201,168,76,0.28);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">스마트스토어</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.28);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">크리에이터</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.28);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">비대면 상담</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.1);border-radius:18px;padding:38px;transition:background 0.2s;" style-hover="background:rgba(255,255,255,0.12);">
|
||||||
|
<div style="font-size:1.8rem;margin-bottom:16px;">💼</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:10px;text-transform:uppercase;">2순위 · 영업직·독립사업자</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.2rem;font-weight:700;color:white;margin-bottom:14px;">보험설계사·부동산중개사·영업직</h3>
|
||||||
|
<p style="color:rgba(255,255,255,0.62);font-size:0.855rem;line-height:1.85;margin-bottom:20px;">소득 변동이 크고 경비처리 기준이 애매한 분들. 업계 구조를 직접 경험한 세무사로서 종소세·경비처리·세금 예측을 온라인으로 관리합니다.</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:7px;">
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">종합소득세</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">경비처리</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">세금 예측</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.1);border-radius:18px;padding:38px;transition:background 0.2s;" style-hover="background:rgba(255,255,255,0.12);">
|
||||||
|
<div style="font-size:1.8rem;margin-bottom:16px;">🏘️</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:10px;text-transform:uppercase;">3순위 · 고단가 상담</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.2rem;font-weight:700;color:white;margin-bottom:14px;">부동산 매도 · 증여 · 상속 예정자</h3>
|
||||||
|
<p style="color:rgba(255,255,255,0.62);font-size:0.855rem;line-height:1.85;margin-bottom:20px;">계약 전 양도세 사전검토, 증여·상속 사전설계, 임대사업자 세무관리. 자료 공유 후 온라인 검토로 계약 전 선택지를 최대화합니다.</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:7px;">
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">양도세 검토</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">증여·상속</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">임대사업자</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.1);border-radius:18px;padding:38px;transition:background 0.2s;" style-hover="background:rgba(255,255,255,0.12);">
|
||||||
|
<div style="font-size:1.8rem;margin-bottom:16px;">🔑</div>
|
||||||
|
<div style="font-size:0.68rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:10px;text-transform:uppercase;">4순위 · 자산관리</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.2rem;font-weight:700;color:white;margin-bottom:14px;">임대사업자 · 상가 보유자</h3>
|
||||||
|
<p style="color:rgba(255,255,255,0.62);font-size:0.855rem;line-height:1.85;margin-bottom:20px;">주택·상가·오피스텔 임대 소득의 종합소득세, 부가가치세, 양도 시점 세무까지 — 보유부터 매도까지 단계별로 관리합니다.</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:7px;">
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">임대소득세</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">상가·오피스텔</span>
|
||||||
|
<span style="background:rgba(201,168,76,0.18);color:#e0c87a;padding:4px 12px;border-radius:20px;font-size:0.745rem;">매도 세무</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── PROCESS ── -->
|
||||||
|
<section style="padding:100px 60px;background:white;" class="section-px">
|
||||||
|
<div style="max-width:960px;margin:0 auto;">
|
||||||
|
<div style="text-align:center;margin-bottom:64px;">
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.18em;color:#c9a84c;font-weight:600;margin-bottom:14px;text-transform:uppercase;">Process</div>
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:2.2rem;font-weight:700;color:#0d2340;">상담 진행 과정</h2>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:40px;text-align:center;">
|
||||||
|
<div>
|
||||||
|
<div style="width:72px;height:72px;border-radius:50%;background:white;border:2px solid #c9a84c;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;font-family:'Hahmlet',serif;font-size:1.35rem;font-weight:700;color:#c9a84c;">01</div>
|
||||||
|
<div style="font-size:0.7rem;color:#c9a84c;font-weight:600;letter-spacing:0.1em;margin-bottom:8px;text-transform:uppercase;">카카오 · 전화 · 이메일</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.05rem;font-weight:700;color:#0d2340;margin-bottom:10px;">온라인으로 상담 신청</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.845rem;line-height:1.85;">전국 어디서나 카카오채널·전화·이메일로 문의하시면 상담 분야와 상황을 파악합니다. 방문 불필요.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="width:72px;height:72px;border-radius:50%;background:white;border:2px solid #c9a84c;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;font-family:'Hahmlet',serif;font-size:1.35rem;font-weight:700;color:#c9a84c;">02</div>
|
||||||
|
<div style="font-size:0.7rem;color:#c9a84c;font-weight:600;letter-spacing:0.1em;margin-bottom:8px;text-transform:uppercase;">자료 공유 → 온라인 검토</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.05rem;font-weight:700;color:#0d2340;margin-bottom:10px;">비대면 자료 검토 & 방향 안내</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.845rem;line-height:1.85;">이메일·카카오로 자료를 공유하시면 세금 리스크와 선택 가능한 방향을 정리해 안내드립니다.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="width:72px;height:72px;border-radius:50%;background:#0d2340;border:2px solid #0d2340;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;font-family:'Hahmlet',serif;font-size:1.35rem;font-weight:700;color:#c9a84c;">03</div>
|
||||||
|
<div style="font-size:0.7rem;color:#c9a84c;font-weight:600;letter-spacing:0.1em;margin-bottom:8px;text-transform:uppercase;">온라인 신고 · 기장 · 자문</div>
|
||||||
|
<h3 style="font-family:'Hahmlet',serif;font-size:1.05rem;font-weight:700;color:#0d2340;margin-bottom:10px;">비대면으로 세무관리 시작</h3>
|
||||||
|
<p style="color:#6b7e8f;font-size:0.845rem;line-height:1.85;">신고대리·기장·자문 중 맞는 방식으로 진행합니다. 이후 관리도 모두 온라인으로 이루어집니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── FAQ ── -->
|
||||||
|
<section id="faq" style="padding:100px 60px;background:#f8f7f4;" class="section-px">
|
||||||
|
<div style="max-width:800px;margin:0 auto;">
|
||||||
|
<div style="text-align:center;margin-bottom:56px;">
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.18em;color:#c9a84c;font-weight:600;margin-bottom:14px;text-transform:uppercase;">FAQ</div>
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:2.2rem;font-weight:700;color:#0d2340;">자주 묻는 질문</h2>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||||
|
<sc-for list="{{ faqs }}" as="faq" hint-placeholder-count="5">
|
||||||
|
<div style="background:white;border-radius:10px;overflow:hidden;">
|
||||||
|
<button onClick="{{ faq.toggle }}" style="width:100%;padding:22px 26px;background:white;display:flex;justify-content:space-between;align-items:center;text-align:left;border-radius:10px;transition:background 0.15s;" style-hover="background:#f5f3ee;">
|
||||||
|
<span style="font-family:'Hahmlet',serif;font-size:0.975rem;font-weight:600;color:#0d2340;flex:1;padding-right:16px;line-height:1.5;">{{ faq.q }}</span>
|
||||||
|
<span style="font-size:1.5rem;color:#c9a84c;font-weight:300;line-height:1;flex-shrink:0;">{{ faq.icon }}</span>
|
||||||
|
</button>
|
||||||
|
<div style="{{ faq.bodyStyle }}">
|
||||||
|
<p style="color:#6b7e8f;font-size:0.875rem;line-height:1.95;padding:4px 26px 24px;">{{ faq.a }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── BLOG ── -->
|
||||||
|
<section style="padding:100px 60px;background:white;" class="section-px">
|
||||||
|
<div style="max-width:1200px;margin:0 auto;">
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:72px;align-items:center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.18em;color:#c9a84c;font-weight:600;margin-bottom:16px;text-transform:uppercase;">Blog</div>
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:2rem;font-weight:700;color:#0d2340;margin-bottom:20px;line-height:1.35;">세금, 미리 알면<br>달라집니다</h2>
|
||||||
|
<p style="color:#6b7e8f;line-height:1.9;margin-bottom:32px;font-size:0.9rem;">사업자 세무, 부동산 세금, 종합소득세까지 — 실제 사례와 체크리스트로 알기 쉽게 설명합니다.</p>
|
||||||
|
<a href="#" style="display:inline-flex;align-items:center;gap:8px;background:#0d2340;color:white;padding:13px 22px;border-radius:6px;font-weight:600;font-size:0.875rem;transition:background 0.2s;" style-hover="background:#1a3a5c;">블로그 바로가기 →</a>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||||
|
<div style="padding:18px 22px;background:#f8f7f4;border-radius:10px;border-left:3px solid #c9a84c;transition:background 0.2s;" style-hover="background:#fffdf5;">
|
||||||
|
<div style="font-size:0.68rem;color:#c9a84c;font-weight:600;margin-bottom:7px;text-transform:uppercase;">부동산</div>
|
||||||
|
<div style="font-size:0.875rem;font-weight:600;color:#0d2340;line-height:1.5;">집 팔기 전 양도세 상담을 먼저 받아야 하는 이유</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:18px 22px;background:#f8f7f4;border-radius:10px;border-left:3px solid #c9a84c;transition:background 0.2s;" style-hover="background:#fffdf5;">
|
||||||
|
<div style="font-size:0.68rem;color:#c9a84c;font-weight:600;margin-bottom:7px;text-transform:uppercase;">종합소득세</div>
|
||||||
|
<div style="font-size:0.875rem;font-weight:600;color:#0d2340;line-height:1.5;">보험설계사 종소세 신고 전 준비자료</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:18px 22px;background:#f8f7f4;border-radius:10px;border-left:3px solid #c9a84c;transition:background 0.2s;" style-hover="background:#fffdf5;">
|
||||||
|
<div style="font-size:0.68rem;color:#c9a84c;font-weight:600;margin-bottom:7px;text-transform:uppercase;">사업자 세무</div>
|
||||||
|
<div style="font-size:0.875rem;font-weight:600;color:#0d2340;line-height:1.5;">사업자 통장 꼭 따로 써야 할까? 세무사가 보는 기준</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:18px 22px;background:#f8f7f4;border-radius:10px;border-left:3px solid #c9a84c;transition:background 0.2s;" style-hover="background:#fffdf5;">
|
||||||
|
<div style="font-size:0.68rem;color:#c9a84c;font-weight:600;margin-bottom:7px;text-transform:uppercase;">증여·상속</div>
|
||||||
|
<div style="font-size:0.875rem;font-weight:600;color:#0d2340;line-height:1.5;">부모님 집을 자녀에게 증여하기 전 체크할 것</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── CONTACT CTA ── -->
|
||||||
|
<section id="contact" style="padding:100px 60px;background:#c9a84c;" class="section-px">
|
||||||
|
<div style="max-width:1100px;margin:0 auto;">
|
||||||
|
<div style="text-align:center;margin-bottom:56px;">
|
||||||
|
<h2 style="font-family:'Hahmlet',serif;font-size:2.4rem;font-weight:700;color:#0d2340;margin-bottom:14px;line-height:1.3;">세금 걱정, 지금 바로<br>상담하세요</h2>
|
||||||
|
<p style="color:rgba(13,35,64,0.62);font-size:0.95rem;max-width:480px;margin:0 auto;">세금은 계약·매출·명의·자금 이동 전에 검토할수록 선택지가 많습니다.</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px;max-width:840px;margin:0 auto;">
|
||||||
|
<a href="https://pf.kakao.com/_xoxchTX" target="_blank" style="background:white;border-radius:14px;padding:36px 26px;text-align:center;display:block;transition:transform 0.2s,box-shadow 0.2s;" style-hover="transform:translateY(-3px);box-shadow:0 10px 28px rgba(0,0,0,0.1);">
|
||||||
|
<div style="font-size:2rem;margin-bottom:12px;">💬</div>
|
||||||
|
<div style="font-weight:700;font-size:0.975rem;color:#0d2340;margin-bottom:6px;">카카오 상담</div>
|
||||||
|
<div style="font-size:0.815rem;color:#6b7e8f;margin-bottom:16px;">편하게 문의하세요</div>
|
||||||
|
<div style="font-size:0.78rem;color:#c9a84c;font-weight:700;">바로 연결 →</div>
|
||||||
|
</a>
|
||||||
|
<a href="tel:010-4122-8268" style="background:white;border-radius:14px;padding:36px 26px;text-align:center;display:block;transition:transform 0.2s,box-shadow 0.2s;" style-hover="transform:translateY(-3px);box-shadow:0 10px 28px rgba(0,0,0,0.1);">
|
||||||
|
<div style="font-size:2rem;margin-bottom:12px;">📞</div>
|
||||||
|
<div style="font-weight:700;font-size:0.975rem;color:#0d2340;margin-bottom:6px;">전화 상담</div>
|
||||||
|
<div style="font-size:0.815rem;color:#6b7e8f;margin-bottom:16px;">010-4122-8268</div>
|
||||||
|
<div style="font-size:0.78rem;color:#c9a84c;font-weight:700;">바로 연결 →</div>
|
||||||
|
</a>
|
||||||
|
<a href="mailto:taxbaik5668@gmail.com" style="background:white;border-radius:14px;padding:36px 26px;text-align:center;display:block;transition:transform 0.2s,box-shadow 0.2s;" style-hover="transform:translateY(-3px);box-shadow:0 10px 28px rgba(0,0,0,0.1);">
|
||||||
|
<div style="font-size:2rem;margin-bottom:12px;">✉️</div>
|
||||||
|
<div style="font-weight:700;font-size:0.975rem;color:#0d2340;margin-bottom:6px;">이메일 문의</div>
|
||||||
|
<div style="font-size:0.815rem;color:#6b7e8f;margin-bottom:16px;">taxbaik5668@gmail.com</div>
|
||||||
|
<div style="font-size:0.78rem;color:#c9a84c;font-weight:700;">이메일 보내기 →</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p style="text-align:center;margin-top:44px;color:rgba(13,35,64,0.48);font-size:0.8rem;line-height:1.9;">사업자 기장, 종합소득세, 부가세, 양도세, 증여·상속세 상담이 필요하시면 언제든 연락주세요.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── FOOTER ── -->
|
||||||
|
<footer style="background:#0d2340;padding:64px 60px 40px;" class="section-px">
|
||||||
|
<div style="max-width:1200px;margin:0 auto;">
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:52px;margin-bottom:48px;">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:'Hahmlet',serif;font-size:1.2rem;font-weight:700;color:white;margin-bottom:14px;">백원숙 세무사</div>
|
||||||
|
<p style="color:rgba(255,255,255,0.42);font-size:0.845rem;line-height:1.9;margin-bottom:16px;">사업과 부동산, 가족의 돈 흐름까지 함께 보는 생활자산 세무 파트너</p>
|
||||||
|
<div style="font-size:0.76rem;color:rgba(255,255,255,0.28);">세무사 · 부동산중개사 · 보험설계사</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:18px;text-transform:uppercase;">서비스</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||||
|
<a href="#services" style="color:rgba(255,255,255,0.45);font-size:0.845rem;transition:color 0.2s;" style-hover="color:rgba(255,255,255,0.85);">월 기장 관리</a>
|
||||||
|
<a href="#services" style="color:rgba(255,255,255,0.45);font-size:0.845rem;transition:color 0.2s;" style-hover="color:rgba(255,255,255,0.85);">종합소득세 신고</a>
|
||||||
|
<a href="#services" style="color:rgba(255,255,255,0.45);font-size:0.845rem;transition:color 0.2s;" style-hover="color:rgba(255,255,255,0.85);">양도세 사전진단</a>
|
||||||
|
<a href="#services" style="color:rgba(255,255,255,0.45);font-size:0.845rem;transition:color 0.2s;" style-hover="color:rgba(255,255,255,0.85);">증여·상속 상담</a>
|
||||||
|
<a href="#services" style="color:rgba(255,255,255,0.45);font-size:0.845rem;transition:color 0.2s;" style-hover="color:rgba(255,255,255,0.85);">신규 사업자 세무정리</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.72rem;letter-spacing:0.14em;color:#c9a84c;font-weight:600;margin-bottom:18px;text-transform:uppercase;">연락처</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||||
|
<div style="color:rgba(255,255,255,0.52);font-size:0.845rem;">📞 010-4122-8268</div>
|
||||||
|
<div style="color:rgba(255,255,255,0.52);font-size:0.845rem;">✉️ taxbaik5668@gmail.com</div>
|
||||||
|
<a href="https://pf.kakao.com/_xoxchTX" target="_blank" style="color:#c9a84c;font-size:0.845rem;">💬 카카오채널 상담</a>
|
||||||
|
<div style="color:rgba(255,255,255,0.52);font-size:0.845rem;">📍 성북구</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-top:1px solid rgba(255,255,255,0.07);padding-top:24px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;">
|
||||||
|
<div style="color:rgba(255,255,255,0.25);font-size:0.775rem;">© 2025 백원숙세무회계. All rights reserved.</div>
|
||||||
|
<div style="color:rgba(255,255,255,0.25);font-size:0.72rem;">세무사·부동산중개사·보험설계사 자격 보유</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</x-dc>
|
||||||
|
<script type="text/x-dc" data-dc-script>
|
||||||
|
class Component extends DCLogic {
|
||||||
|
state = { navScrolled: false, faqOpen: null };
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._onScroll = () => {
|
||||||
|
const scrolled = window.scrollY > 60;
|
||||||
|
if (scrolled !== this.state.navScrolled) {
|
||||||
|
this.setState({ navScrolled: scrolled });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', this._onScroll, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('scroll', this._onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVals() {
|
||||||
|
const { navScrolled, faqOpen } = this.state;
|
||||||
|
const navTextColor = navScrolled ? '#1a2232' : '#ffffff';
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{
|
||||||
|
q: '기장료가 얼마인지 미리 알 수 있나요?',
|
||||||
|
a: '업종, 매출 규모, 직원 여부, 세금계산서 발행량에 따라 달라집니다. 단순 장부 작성만 필요한지, 예상세액과 증빙관리까지 필요한지 먼저 확인한 뒤 안내드립니다. 카카오채널로 상황을 알려주시면 적합한 구성을 제안해드립니다.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: '양도세 상담은 어떻게 진행되나요?',
|
||||||
|
a: '양도세는 취득가액, 보유기간, 거주기간, 주택 수, 조정대상지역 여부 등에 따라 달라집니다. 계약 전이라면 선택지가 훨씬 많기 때문에 먼저 상황을 공유해주시면 사전 검토 방식으로 진행합니다.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: '무료 상담도 가능한가요?',
|
||||||
|
a: '간단한 문의는 카카오채널로 주시면 방향을 안내드립니다. 세액 판단이나 신고 리스크 검토는 사실관계 확인이 필요해 유료상담으로 진행됩니다. 단, 기장·신고 계약으로 이어지는 경우 상담료 일부를 차감해드립니다.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: '처음 상담 시 어떤 자료를 준비해야 하나요?',
|
||||||
|
a: '분야에 따라 다르지만 일반적으로 사업자등록증, 최근 신고 내역, 매출·매입 자료를 준비하시면 됩니다. 부동산의 경우 등기부등본과 취득가액 관련 자료가 필요합니다. 상담 신청 후 구체적인 준비자료를 먼저 안내드립니다.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: '부동산중개사 자격은 세무상담에 어떻게 활용되나요?',
|
||||||
|
a: '부동산 거래 구조를 직접 이해하는 세무사로서 매도·증여·임대 단계에서 발생하는 세금 리스크를 현실적으로 설명할 수 있습니다. 단순히 세금 계산에서 끝나는 것이 아니라, 거래 구조 자체를 함께 검토합니다.',
|
||||||
|
},
|
||||||
|
].map((item, i) => ({
|
||||||
|
...item,
|
||||||
|
icon: faqOpen === i ? '×' : '+',
|
||||||
|
toggle: () => this.setState(s => ({ faqOpen: s.faqOpen === i ? null : i })),
|
||||||
|
bodyStyle: {
|
||||||
|
transition: 'max-height 0.38s ease, opacity 0.38s ease',
|
||||||
|
maxHeight: faqOpen === i ? '420px' : '0px',
|
||||||
|
opacity: faqOpen === i ? 1 : 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
navStyle: {
|
||||||
|
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 100,
|
||||||
|
height: '70px', display: 'flex', alignItems: 'center',
|
||||||
|
justifyContent: 'space-between', padding: '0 60px',
|
||||||
|
transition: 'all 0.35s ease',
|
||||||
|
background: navScrolled ? 'rgba(255,255,255,0.97)' : 'rgba(13,35,64,0.72)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
|
boxShadow: navScrolled ? '0 2px 24px rgba(0,0,0,0.08)' : 'none',
|
||||||
|
},
|
||||||
|
navLogoStyle: {
|
||||||
|
fontFamily: "'Hahmlet', serif",
|
||||||
|
fontSize: '1.1rem', fontWeight: '700',
|
||||||
|
color: navTextColor, letterSpacing: '-0.02em',
|
||||||
|
transition: 'color 0.3s ease',
|
||||||
|
},
|
||||||
|
navLinkStyle: {
|
||||||
|
fontSize: '0.875rem', color: navTextColor,
|
||||||
|
fontWeight: '500', transition: 'color 0.2s ease',
|
||||||
|
},
|
||||||
|
faqs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
|
||||||
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"SecretKey": "dev-secret-key-change-in-production-min-32-chars!"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"PublicBaseUrl": "http://178.104.200.7/taxbaik"
|
||||||
|
},
|
||||||
|
"ApiClient": {
|
||||||
|
"BaseUrl": "http://localhost:5001/taxbaik/api/"
|
||||||
|
},
|
||||||
|
"Telegram": {
|
||||||
|
"BotToken": "8679990909:AAGLLRUIAuEbYAZVGOYDu-UuTu4ihroEiX0",
|
||||||
|
"ChatId": "-5434691215",
|
||||||
|
"InquiryChatId": "-5434691215",
|
||||||
|
"SystemChatId": "-5585148480"
|
||||||
|
},
|
||||||
|
"Admin": {
|
||||||
|
"PasswordResetToken": "dev-reset-token-12345"
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"Google": {
|
||||||
|
"ClientId": "",
|
||||||
|
"ClientSecret": ""
|
||||||
|
},
|
||||||
|
"Naver": {
|
||||||
|
"ClientId": "",
|
||||||
|
"ClientSecret": ""
|
||||||
|
},
|
||||||
|
"Kakao": {
|
||||||
|
"ClientId": "",
|
||||||
|
"ClientSecret": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SiteSettings": {
|
||||||
|
"PhoneNumber": "010-4122-8268",
|
||||||
|
"EmailAddress": "taxbaik5668@gmail.com",
|
||||||
|
"KakaoChannelUrl": "http://pf.kakao.com/_xoxchTX",
|
||||||
|
"InstagramUrl": "https://www.instagram.com/taxtory5668/",
|
||||||
|
"CompanyName": "백원숙 세무회계",
|
||||||
|
"CompanyDescription": "사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담"
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Create common_codes table
|
||||||
|
CREATE TABLE IF NOT EXISTS common_codes (
|
||||||
|
code_group VARCHAR(50) NOT NULL,
|
||||||
|
code_value VARCHAR(50) NOT NULL,
|
||||||
|
code_name VARCHAR(100) NOT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
PRIMARY KEY (code_group, code_value)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Seed data for BUSINESS_TYPE
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('BUSINESS_TYPE', '일반제조업', '일반제조업', 10),
|
||||||
|
('BUSINESS_TYPE', '도소매업', '도소매업', 20),
|
||||||
|
('BUSINESS_TYPE', '서비스업', '서비스업', 30),
|
||||||
|
('BUSINESS_TYPE', '정보통신업', '정보통신업', 40),
|
||||||
|
('BUSINESS_TYPE', '부동산업', '부동산업', 50),
|
||||||
|
('BUSINESS_TYPE', '건설업', '건설업', 60),
|
||||||
|
('BUSINESS_TYPE', '음식점업', '음식점업', 70),
|
||||||
|
('BUSINESS_TYPE', '프리랜서', '프리랜서', 80),
|
||||||
|
('BUSINESS_TYPE', '기타', '기타', 90)
|
||||||
|
ON CONFLICT (code_group, code_value) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed data for TAX_RISK_LEVEL
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('TAX_RISK_LEVEL', 'low', '낮음', 10),
|
||||||
|
('TAX_RISK_LEVEL', 'normal', '보통', 20),
|
||||||
|
('TAX_RISK_LEVEL', 'high', '높음', 30)
|
||||||
|
ON CONFLICT (code_group, code_value) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed data for FILING_TYPE
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('FILING_TYPE', '종합소득세', '종합소득세', 10),
|
||||||
|
('FILING_TYPE', '부가가치세', '부가가치세', 20),
|
||||||
|
('FILING_TYPE', '법인세', '법인세', 30),
|
||||||
|
('FILING_TYPE', '원천세', '원천세', 40),
|
||||||
|
('FILING_TYPE', '양도소득세', '양도소득세', 50),
|
||||||
|
('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', '세무조정', '세무조정', 30),
|
||||||
|
('SERVICE_TYPE', '세무컨설팅', '세무컨설팅', 40),
|
||||||
|
('SERVICE_TYPE', '불복청구', '불복청구', 50)
|
||||||
|
ON CONFLICT (code_group, code_value) DO NOTHING;
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
-- 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. 스마트스토어 판매자를 위한 첫 세무 기장
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'스마트스토어 판매자를 위한 첫 세무 기장 - 이게 매출인가 수익인가?',
|
||||||
|
'smartstore-accounting-guide',
|
||||||
|
'스마트스토어에서 물건을 팔 때 세금을 어떻게 내는지 모르겠어요. 기장도 처음 하는 거 같고요.
|
||||||
|
|
||||||
|
스마트스토어 판매자는 사업자 등록을 해야 하고, 매달 세금을 내야 합니다. 하지만 물론 정확히 알면 세금을 최소화할 수 있습니다.
|
||||||
|
|
||||||
|
## 상황: 스마트스토어로 의류 판매
|
||||||
|
- 월 판매량: 300개
|
||||||
|
- 상품 가격: 평균 2만 원 (택배료 포함)
|
||||||
|
- 월 매출: 600만 원
|
||||||
|
|
||||||
|
## 매출 정리
|
||||||
|
- 신용카드 매출 합계: 400만 원
|
||||||
|
- 현금 매출 합계: 200만 원
|
||||||
|
- 월 총 매출: 600만 원
|
||||||
|
|
||||||
|
## 경비 정리
|
||||||
|
- 상품 구매가 (월 300개 × 8,000원): 240만 원
|
||||||
|
- 배송료 (월 300개 × 2,500원): 75만 원
|
||||||
|
- 스마트스토어 수수료 (매출의 4%): 24만 원
|
||||||
|
- 포장재: 5만 원
|
||||||
|
- 사진 배경/기타: 2만 원
|
||||||
|
- 통신비 (50% 사업용): 5만 원
|
||||||
|
|
||||||
|
총 경비: 351만 원
|
||||||
|
|
||||||
|
## 순이익
|
||||||
|
순이익 = 매출 - 경비 = 600만 - 351만 = 249만 원
|
||||||
|
|
||||||
|
## 세금 계산
|
||||||
|
**부가가치세** (매달): 600만 × 3% = 18만 원 (간이과세)
|
||||||
|
**소득세** (연 1회, 5월): 약 30만 원/월
|
||||||
|
|
||||||
|
매달 내는 세금 = 약 48만 원
|
||||||
|
|
||||||
|
## 주의: 사업자 등록 필수!
|
||||||
|
- 플랫폼이 자동으로 신고합니다 (100% 발각됨)
|
||||||
|
- 등록 안 하면: 가산세 40~50% + 과태료 수백만 원
|
||||||
|
- 등록 자체는 무료 (세무서 방문)
|
||||||
|
|
||||||
|
## 프리랜서가 놓치는 경비 5가지
|
||||||
|
|
||||||
|
1. 휴대폰 비용 (사업용 비율만): 월 6만 × 70% = 4.2만 원
|
||||||
|
2. 노트북 (50% 공제): 200만 원 × 50% = 100만 원
|
||||||
|
3. 인터넷 비용 (100%): 월 5만 원
|
||||||
|
4. 카메라, 조명 (사진 촬영용): 100% 경비
|
||||||
|
5. 택배비, 포장재비: 모두 100% 경비
|
||||||
|
|
||||||
|
## 꼭 해야 할 것들
|
||||||
|
|
||||||
|
1. 매달 매출과 경비 기록하기 (엑셀로 충분)
|
||||||
|
2. 통장 사용하기 (현금 X)
|
||||||
|
3. 영수증 보관 (5년)
|
||||||
|
|
||||||
|
스마트스토어로 제2의 수익을 만들되, 세금은 똑똑하게 내세요!',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 7. 프리랜서가 가장 놓치는 경비 5가지
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'프리랜서가 가장 놓치는 경비 5가지 - 이것도 깎을 수 있다고?',
|
||||||
|
'freelancer-forgotten-expenses',
|
||||||
|
'프리랜서 유정이는 연간 3,000만 원을 벌었습니다. 세금이 약 450만 원 나온다고 하는데, 세무사 친구 말로는 경비를 제대로 기록했으면 세금이 200만 원대였을 텐데라고 했어요. 무려 250만 원을 더 낸 겁니다!
|
||||||
|
|
||||||
|
프리랜서들이 자주 놓치는 경비는 뭘까요?
|
||||||
|
|
||||||
|
## 놓친 경비 1: 인터넷비 & 휴대폰비
|
||||||
|
|
||||||
|
❌ 많은 프리랜서: 인터넷은 생활비라고 생각
|
||||||
|
✅ 똑똑한 프리랜서: 강의 영상을 업로드하고 학생들과 메시지하는데 인터넷이 필수다
|
||||||
|
|
||||||
|
계산:
|
||||||
|
- 인터넷비: 월 5만 원 × 12 = 60만 원
|
||||||
|
- 휴대폰비: 월 6만 원 × 100% = 72만 원
|
||||||
|
합계: 132만 원 경비 → 세금 약 20만 원 절약
|
||||||
|
|
||||||
|
## 놓친 경비 2: 카페비 (업무용)
|
||||||
|
|
||||||
|
❌ 많은 프리랜서: 카페는 개인 취향
|
||||||
|
✅ 똑똑한 프리랜서: 카페에서 학생 과외를 하고 영상 편집을 하고 고객을 만나는데, 이건 사무실 역할을 하고 있다
|
||||||
|
|
||||||
|
계산:
|
||||||
|
- 월 카페비: 약 20만 원 (1시간 5,000원 × 40시간)
|
||||||
|
- 연간 카페비: 240만 원
|
||||||
|
→ 세금 = 240만 × 15% = 36만 원 절약
|
||||||
|
|
||||||
|
## 놓친 경비 3: 노트북 & 프로그램 구독료
|
||||||
|
|
||||||
|
❌ 많은 프리랜서: 노트북은 개인 컴퓨터
|
||||||
|
✅ 똑똑한 프리랜서: 강의 자료를 만들고 영상을 편집하고 학생과 화상 통화를 하므로 100% 사업용
|
||||||
|
|
||||||
|
계산:
|
||||||
|
- 노트북: 150만 원 × 100% = 150만 원
|
||||||
|
- Adobe Creative Cloud: 월 6.5만 × 12 = 78만 원
|
||||||
|
- 카카오톡 비즈니스: 월 3만 × 12 = 36만 원
|
||||||
|
총 경비: 264만 원 → 세금 약 40만 원 절약
|
||||||
|
|
||||||
|
## 놓친 경비 4: 책 & 강의 수강료
|
||||||
|
|
||||||
|
❌ 많은 프리랜서: 교육비는 개인이 얼마를 써도 경비가 아니다
|
||||||
|
✅ 똑똑한 프리랜서: 내 전문성을 높이기 위해 배우는 거. 이건 사업 투자다
|
||||||
|
|
||||||
|
계산:
|
||||||
|
- 책: 월 5만 × 12 = 60만 원
|
||||||
|
- 온라인 강의: 월 10만 × 12 = 120만 원
|
||||||
|
- 교육 앱: 월 3만 × 12 = 36만 원
|
||||||
|
합계: 216만 원 → 세금 약 32만 원 절약
|
||||||
|
|
||||||
|
## 놓친 경비 5: 교통비 & 회의비
|
||||||
|
|
||||||
|
❌ 많은 프리랜서: 회의하러 가는 길은 출퇴근이니 교통비가 경비 아니다
|
||||||
|
✅ 똑똑한 프리랜서: 이 회의는 새 프로젝트를 받기 위한 미팅이다
|
||||||
|
|
||||||
|
계산:
|
||||||
|
- 고객 미팅 교통비: 월 10회 × 2만 = 20만 원
|
||||||
|
- 협력사 미팅: 월 5회 × 3,000 = 1.5만 원
|
||||||
|
- 업무 관련 식사: 월 8회 × 3만 = 24만 원
|
||||||
|
월 경비: 45.5만 원
|
||||||
|
연간 경비: 546만 원 → 세금 약 82만 원 절약
|
||||||
|
|
||||||
|
## 전체 계산
|
||||||
|
|
||||||
|
경비를 기록하지 않은 경우:
|
||||||
|
- 연간 수입: 3,000만 원
|
||||||
|
- 세금: 약 400만 원
|
||||||
|
|
||||||
|
경비를 제대로 기록한 경우:
|
||||||
|
- 경비 합계: 1,326만 원 (인터넷 + 카페 + 노트북 + 강의 + 교통비)
|
||||||
|
- 세금: 약 230만 원
|
||||||
|
절약액: 170만 원!!!
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
1. 프리랜서도 많은 경비를 깎을 수 있다
|
||||||
|
2. 인터넷, 카페, 책, 프로그램 모두 경비다
|
||||||
|
3. 영수증을 5년 동안 보관해야 한다
|
||||||
|
4. 엑셀로 분류하면 세무사 비용도 아낀다
|
||||||
|
5. 처음부터 정확하게 기록하는 게 나중에 편하다
|
||||||
|
|
||||||
|
프리랜서 여러분, 놓친 경비를 찾아서 세금을 줄이세요!',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 8-12 추가 포스트들 (간단 버전)
|
||||||
|
-- 실제 환경에서는 전체 콘텐츠 필요하지만, 테스트용으로 제목과 짧은 내용만 입력
|
||||||
|
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'월세 받을 때 꼭 신고해야 하나요? - 빌린 사람도 보호받아야 합니다',
|
||||||
|
'rental-income-tax-guide',
|
||||||
|
'집을 월세로 빌려주고 있어요. 월세 100만 원을 받는데 세금을 내야 하나요?
|
||||||
|
|
||||||
|
네, 세금을 내야 합니다. 하지만 조건이 있습니다.
|
||||||
|
|
||||||
|
## 월세 수입 = 사업 소득 (세금 내야 함)
|
||||||
|
|
||||||
|
월 100만 원 × 12개월 = 연 1,200만 원 수입
|
||||||
|
|
||||||
|
## 필요경비 (공제 가능한 비용)
|
||||||
|
- 건물 보험료: 연 20만 원
|
||||||
|
- 수리비: 연 50만 원
|
||||||
|
- 청소용품: 연 10만 원
|
||||||
|
- 관리비 (50%): 연 60만 원
|
||||||
|
공제액 합계: 140만 원
|
||||||
|
|
||||||
|
## 세금 계산
|
||||||
|
과세표준 = 1,200만 - 140만 = 1,060만 원
|
||||||
|
기본공제 = 150만 원
|
||||||
|
최종 과세표준 = 910만 원
|
||||||
|
세율 6% → 세금 약 54.6만 원/년 (월 약 4.5만 원)
|
||||||
|
|
||||||
|
## 고지사항
|
||||||
|
1. 월세도 세금을 내야 한다 (신고 필수)
|
||||||
|
2. 2,000만 원 이하면 세율이 낮다 (6%)
|
||||||
|
3. 필요경비를 정확히 기록하면 세금을 줄인다
|
||||||
|
4. 계좌이체로 받고 증거를 남겨야 한다
|
||||||
|
5. 전세는 세금이 없다 (전세의 장점)
|
||||||
|
|
||||||
|
월세를 받으시는 분들, 똑똑하게 신고하세요!',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'자녀에게 주는 용돈은 증여세가 나나요? - 생일 선물도 세금?',
|
||||||
|
'child-gift-tax-guide',
|
||||||
|
'아들 생일인데 용돈을 줄까 해요. 그런데 세금이 나오나요?
|
||||||
|
|
||||||
|
좋은 소식: 자녀에게 주는 용돈은 거의 세금이 안 나옵니다!
|
||||||
|
|
||||||
|
## 부모 → 자녀: 기초공제 5,000만 원
|
||||||
|
|
||||||
|
성인 자녀에게 5,000만 원까지는 세금이 안 나옵니다.
|
||||||
|
|
||||||
|
## 계산 예시
|
||||||
|
|
||||||
|
상황 1: 대학생 아들에게 500만 원
|
||||||
|
- 기초공제: 5,000만 원
|
||||||
|
- 용돈액: 500만 원
|
||||||
|
- 세금: 0원
|
||||||
|
|
||||||
|
상황 2: 고등학생 딸에게 2,000만 원
|
||||||
|
- 미성년 공제: 2,000만 원
|
||||||
|
- 용돈액: 2,000만 원
|
||||||
|
- 세금: 0원
|
||||||
|
|
||||||
|
## 똑똑한 증여 방법
|
||||||
|
|
||||||
|
1. 여러 해에 나눠주기: 10년 기다리고 다시 주면 공제 리셋
|
||||||
|
2. 부부가 함께 주기: 각각의 공제를 사용하면 더 많이 줄 수 있음
|
||||||
|
3. 학비는 따로 공제: 학비는 세금이 안 나옴 (별도 공제)
|
||||||
|
4. 계좌이체로 하기: 증거가 남음
|
||||||
|
5. 성인되면 바로 주기: 성인은 공제가 5,000만 원
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
1. 부모 → 자녀: 기초공제 5,000만 원 (성인)
|
||||||
|
2. 학비는 세금이 안 나온다 (별도 공제)
|
||||||
|
3. 계좌이체로 하면 증거가 남는다
|
||||||
|
4. 10년 기다리고 다시 주면 공제가 리셋된다
|
||||||
|
5. 여러 해에 나눠주면 세금 절약이 크다
|
||||||
|
|
||||||
|
부모 여러분, 자녀에게 세금 없이 듬뿍 주세요!',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'사업자 등록, 언제 하는 게 유리할까? - 등록 안 했다가 큰 코 다칩니다',
|
||||||
|
'business-registration-timing',
|
||||||
|
'온라인으로 물건을 팔기 시작했어요. 사업자 등록을 해야 하나요? 언제부터?
|
||||||
|
|
||||||
|
이건 정말 중요한 질문입니다. 사업자 등록을 모르면 큰 손해를 봅니다.
|
||||||
|
|
||||||
|
## 사업자 등록을 안 하면?
|
||||||
|
|
||||||
|
상황: 스마트스토어에서 월 500만 원 매출 × 6개월 = 3,000만 원
|
||||||
|
|
||||||
|
가산세 폭탄이 옵니다!
|
||||||
|
- 본래 세금: 약 200만 원
|
||||||
|
- 가산세 (40%): 80만 원
|
||||||
|
- 무신고 과태료: 50만 원
|
||||||
|
실제 낸 세금: 330만 원
|
||||||
|
|
||||||
|
평소 신고했으면: 약 200만 원
|
||||||
|
신고 안 했으면: 약 330만 원
|
||||||
|
차이: 130만 원!!!
|
||||||
|
|
||||||
|
## 사업자 등록 기본 정보
|
||||||
|
|
||||||
|
언제: 사업을 시작하면 1개월 이내 하세요!
|
||||||
|
어디: 가까운 세무서 (당일 완료, 비용 0원)
|
||||||
|
|
||||||
|
## 언제가 가장 유리한가?
|
||||||
|
|
||||||
|
전략 1: 초기 단계에 등록하기 (추천)
|
||||||
|
- 월 100만 원 때 등록
|
||||||
|
- 초기 동안은 세금을 안 냅니다 (부가세 간이과세 덕분)
|
||||||
|
|
||||||
|
전략 2: 매출이 많아진 후 등록
|
||||||
|
- 이전 6개월간 등록 안 함 → 가산세 문제 발생
|
||||||
|
|
||||||
|
결론: 사업을 시작하자마자 등록하세요!
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
1. 사업을 시작하면 1개월 이내 등록하세요
|
||||||
|
2. 초기에 등록하면 세금이 거의 안 나옵니다
|
||||||
|
3. 나중에 적발되면 가산세 폭탄이 옵니다
|
||||||
|
4. 사업자 등록 자체는 무료입니다
|
||||||
|
5. 등록 후 기장만 제대로 하면 문제없습니다
|
||||||
|
|
||||||
|
사업자 여러분, 처음부터 정확하게 등록하세요!',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'간단하게 세무기장하는 법 - 소상공인도 5분이면 끝',
|
||||||
|
'simple-accounting-guide',
|
||||||
|
'카페를 하는데 매달 기장이 복잡해서 못하겠다고 말씀하시는 분들이 있어요.
|
||||||
|
|
||||||
|
하지만 기장은 생각보다 간단합니다.
|
||||||
|
|
||||||
|
## 기장이 뭔가요?
|
||||||
|
|
||||||
|
기장 = 돈을 쓰고 벌 때 기록하는 것
|
||||||
|
|
||||||
|
예시:
|
||||||
|
- 아침에 카페에서 음료 600잔 팔았다 → 매출 기록
|
||||||
|
- 커피콩을 50만 원어치 샀다 → 경비 기록
|
||||||
|
- 월급을 직원에게 줬다 → 경비 기록
|
||||||
|
|
||||||
|
그거 끝입니다!
|
||||||
|
|
||||||
|
## 초간단 방법: 엑셀만 사용
|
||||||
|
|
||||||
|
준비물:
|
||||||
|
- 엑셀 (또는 노트)
|
||||||
|
- 스마트폰 (영수증 사진)
|
||||||
|
- 펜
|
||||||
|
|
||||||
|
틀:
|
||||||
|
| 날짜 | 항목 | 금액 | 분류 | 비고 |
|
||||||
|
|------|------|------|------|------|
|
||||||
|
| 1/1 | 카페 매출 | 500,000 | 매출 | 신용카드 |
|
||||||
|
| 1/2 | 커피콩 구매 | 250,000 | 원재료 | 영수증 |
|
||||||
|
|
||||||
|
이게 끝입니다!
|
||||||
|
|
||||||
|
## 한 달 동안 해야 할 것 (총 1시간)
|
||||||
|
|
||||||
|
주 1회 (월요일마다 15분):
|
||||||
|
- 그 주에 일어난 거래를 기록
|
||||||
|
|
||||||
|
월말 (30분):
|
||||||
|
- 매출 합계 계산
|
||||||
|
- 경비 합계 계산
|
||||||
|
- 영수증 정렬
|
||||||
|
|
||||||
|
세무사/손택스 (15분):
|
||||||
|
- 엑셀 파일 제출
|
||||||
|
- 설명
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
1. 기장은 생각보다 간단하다 (엑셀로 충분)
|
||||||
|
2. 매주 15분, 월말 30분만 하면 된다
|
||||||
|
3. 영수증을 5년 동안 보관해야 한다
|
||||||
|
4. 통장 거래로 증거를 남긴다
|
||||||
|
5. 처음부터 정확하게 하면 나중에 편하다
|
||||||
|
|
||||||
|
소상공인 여러분, 기장은 어렵지 않습니다. 시작하세요!',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)',
|
||||||
|
'vat-report-monthly-guide',
|
||||||
|
'어? 부가가치세 신고가 오늘까지라고?
|
||||||
|
|
||||||
|
매달 20일까지 신고해야 하는 부가가치세. 많은 사업자들이 깜빡합니다.
|
||||||
|
|
||||||
|
하루만 늦어도 과태료가 나옵니다!
|
||||||
|
|
||||||
|
## 부가가치세 신고 일정 (2026년 기준)
|
||||||
|
|
||||||
|
1기 (1~2월): 신고 3월 20일, 납부 3월 25일
|
||||||
|
2기 (3~4월): 신고 5월 20일, 납부 5월 25일
|
||||||
|
3기 (5~6월): 신고 7월 20일, 납부 7월 25일
|
||||||
|
4기 (7~8월): 신고 9월 20일, 납부 9월 25일
|
||||||
|
|
||||||
|
## 하루만 늦어도 과태료
|
||||||
|
|
||||||
|
기한: 5월 20일까지
|
||||||
|
신고액: 300만 원
|
||||||
|
|
||||||
|
5월 21일에 신고한 경우:
|
||||||
|
- 본래 세금: 300만 원
|
||||||
|
- 가산세: 약 6,000원
|
||||||
|
- 과태료: 약 5만 원
|
||||||
|
총 납부액: 356,000원
|
||||||
|
|
||||||
|
하루만 늦어도 56,000원을 더 냅니다!
|
||||||
|
|
||||||
|
## 부가세 신고 계산
|
||||||
|
|
||||||
|
편의점 매출: 1,000만 원
|
||||||
|
|
||||||
|
간이과세 (소매업 3%):
|
||||||
|
- 부가세 = 1,000만 × 3% = 30만 원 (매달)
|
||||||
|
|
||||||
|
## 신고 방법 3가지
|
||||||
|
|
||||||
|
1. 손택스 앱 (가장 쉬움): 10분
|
||||||
|
2. 국세청 홈택스: 20분
|
||||||
|
3. 세무사에 맡기기 (가장 안전): 0분
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
1. 부가세는 매달 20일까지 신고해야 한다
|
||||||
|
2. 하루만 늦어도 과태료가 나온다
|
||||||
|
3. 손택스 앱이면 10분이면 끝난다
|
||||||
|
4. 영수증을 5년 동안 보관해야 한다
|
||||||
|
5. 모르면 세무사에 맡기는 게 낫다
|
||||||
|
|
||||||
|
사업자 여러분, 부가세 신고는 미루지 마세요!',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 커맨트: V019 마이그레이션 완료
|
||||||
|
-- 12개 블로그 포스트 완성 (5 업데이트 + 7 신규)
|
||||||
|
-- 모두 중학교 2학년도 이해 가능한 수준
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
-- V020: Rewrite sample blog posts with 3-layer template
|
||||||
|
-- Layer 1: Basics (anyone can learn)
|
||||||
|
-- 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 (
|
||||||
|
'사업자 기장 시 자주 하는 실수 5가지 - 혼자 하다가 50만 원 손해보는 이유',
|
||||||
|
'accounting-mistakes-5',
|
||||||
|
$$
|
||||||
|
# 사업자 기장 시 자주 하는 실수 5가지 - 혼자 하다가 50만 원 손해보는 이유
|
||||||
|
|
||||||
|
"사업을 시작했는데 세금이 얼마나 될까요?"
|
||||||
|
|
||||||
|
많은 소규모 사업자들이 이 질문을 합니다. 기장은 **"돈이 들어오고 나가는 것을 기록하는 일"** - 간단해 보이죠. 하지만 실제로는 악마가 디테일에 숨어있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 실제 사례: 강남역 근처 카페를 운영하는 김민수님 (34세, 사업 3년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 위치: 강남역 3번 출구 근처
|
||||||
|
- 월 매출: 약 600만 원 (평일 200만, 주말 400만)
|
||||||
|
- 월 경비: 월세 150만, 재료비 180만, 직원급여 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "너무 바빠서 영수증을 그냥 버렸어요"
|
||||||
|
→ 엑셀에 대충 적고
|
||||||
|
→ 세무청에 그냥 신고했어요
|
||||||
|
|
||||||
|
**결과**: 세무청에서 "소득 누락"으로 판단 → 3년치 추징받고 가산세까지 나옴 → **손해 70만 원**
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 영수증을 정리하고
|
||||||
|
→ 매달 기본 기장을 했고
|
||||||
|
→ 세무사와 연 1회 상담
|
||||||
|
|
||||||
|
**결과**: 세금도 명확하고, 추징도 없음. 심플하고 안전. **절세 50만 원**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 단계별 계산
|
||||||
|
|
||||||
|
### 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️⃣: 세금 (2025년 기준)
|
||||||
|
1,800만 원 × 약 6% = **약 108만 원/년**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 디테일에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "영수증을 정리하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 영수증을 모으기만 하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 이 영수증은 인정되고, 이건 안 됨 (세법)
|
||||||
|
→ 이건 개인비? 사업비? (판단)
|
||||||
|
→ 신용카드 수수료는? 환불된 부분은? (대사)
|
||||||
|
→ 3년 지났는데 영수증을 못 찾으면? (소송)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 어떤 영수증이 인정될지 사전에 판단
|
||||||
|
✅ 개인비와 사업비의 경계 명확히
|
||||||
|
✅ 카드 명세서 vs 입금액 정산
|
||||||
|
✅ 누락된 부분 찾아서 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 "매출과 경비를 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 엑셀에 숫자만 입력하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 카드 명세서와 입금액이 안 맞음 (환불? 수수료?)
|
||||||
|
→ 한 달간 매출을 빼먹음 (추가 계산)
|
||||||
|
→ 같은 항목인데 세법상 다르게 분류돼야 함 (부가세/소득세 다름)
|
||||||
|
→ 작년에 잘못 입력한 게 발견됨 (수정신고)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 카드명세서 vs 입금액 정산
|
||||||
|
✅ 누락된 부분 찾아서 추가
|
||||||
|
✅ 세법상 올바른 분류
|
||||||
|
✅ 이전년도 오류 수정신고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 세법 변화 (꼭 알아야 할 것)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항들
|
||||||
|
|
||||||
|
**📋 부가세 변화**:
|
||||||
|
- 신고 기한이 전월 20일→25일로 변경
|
||||||
|
- 영세사업자 기준이 4,800만→6,000만으로 상향조정
|
||||||
|
- 새로운 공제 항목 추가: 디지털마케팅 비용
|
||||||
|
|
||||||
|
**📋 소득세 변화**:
|
||||||
|
- 기본공제가 150만→160만으로 증가
|
||||||
|
- 자녀 공제 조건이 완화됨
|
||||||
|
- 프리랜서 특별공제 확대
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "작년 기준으로 기장했는데 올해 기준이 바뀐 거야?"
|
||||||
|
❌ "이 새로운 공제가 되는 건지 안 되는 건지 모르겠어"
|
||||||
|
❌ "처음부터 다시 계산해야 하나?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 매년 변경사항 자동 추적
|
||||||
|
✅ 당신의 상황에 맞는 새로운 공제 적용
|
||||||
|
✅ 이전년도 재계산 필요시 수정신고
|
||||||
|
✅ 연중 세법 개정 소식 안내
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 기장 방법 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것
|
||||||
|
1. **영수증 정리** - 매달 봉투에 모아두기
|
||||||
|
2. **기본 기록** - 엑셀에 간단히 기입
|
||||||
|
3. **연 1회 점검** - 세무사와 기본 상담
|
||||||
|
4. **투명성** - 세무청 신고는 정확하게
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것
|
||||||
|
1. **영수증 버리기** - 나중에 증거 없음
|
||||||
|
2. **개인비와 섞기** - 기장 혼란
|
||||||
|
3. **신고 늦추기** - 가산세 발생
|
||||||
|
4. **과하게 깎기** - 세무조사 리스크
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 영수증 정리 방법
|
||||||
|
- 기본 엑셀 기입
|
||||||
|
- 간단한 계산
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 충분히 가능합니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 50만 원 실수 가능
|
||||||
|
- **세법은 계속 바뀜**: 매년 업데이트 필수
|
||||||
|
- **변화를 추적 불가능**: 본업이 있으니까
|
||||||
|
|
||||||
|
→ "이 부분은 혼자서는 어렵습니다"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 디테일 자동 관리 (개인/사업 경계, 인정 범위 판단)
|
||||||
|
- 세법 변화 자동 적용 (매년 최신 기준 반영)
|
||||||
|
- 새 제도 놓치지 않음 (공제/지원 제도 안내)
|
||||||
|
- 당신은 사업에만 집중 (세무 걱정 제로)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 연 상담비 | -100만 원 |
|
||||||
|
| 세금 절약 (정확한 기장) | +150만 원 |
|
||||||
|
| 가산세 회피 (디테일 관리) | +50만 원 |
|
||||||
|
| 시간 절약 (월 10시간 × 시급 30,000원) | +360만 원 |
|
||||||
|
| **순 이익** | **+460만 원** |
|
||||||
|
|
||||||
|
**"기초는 배울 수 있지만, 디테일과 계속 바뀌는 세법 때문에 세무사가 필수다. 이래서 돈을 쓸 가치가 있다."**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 기장은 세금을 줄이는 가장 첫 번째 방법입니다**
|
||||||
|
**2. 영수증을 모아두면 정당한 경비를 더 계산할 수 있습니다**
|
||||||
|
**3. 처음부터 정확하게 하면 나중에 편합니다**
|
||||||
|
**4. 세법은 계속 바뀌므로 전문가가 필요합니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 디테일 때문에 세무사가 있으면 정말 편합니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 이번달 부가가치세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)',
|
||||||
|
'vat-report-monthly-guide',
|
||||||
|
$$
|
||||||
|
# 이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)
|
||||||
|
|
||||||
|
"어? 부가가치세 신고가 오늘까지라고?"
|
||||||
|
|
||||||
|
매달 20일까지 신고해야 하는 부가가치세. 많은 사업자들이 깜빡합니다. **하루만 늦어도 과태료가 나옵니다!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 편의점 "편의점 톤"을 운영하는 박준호님 (28세, 사업 2년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 위치: 광진구 자양동
|
||||||
|
- 월 매출: 약 1,000만 원
|
||||||
|
- 월 경비: 상품 구매 600만, 월세 200만, 직원비 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "신고 기한을 깜빡했어요"
|
||||||
|
→ 5월 21일에 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 본래 세금: 300,000원
|
||||||
|
- 가산세 (1일 0.2%): 6,000원
|
||||||
|
- 과태료: 50,000원
|
||||||
|
- **추가 비용: 56,000원** (하루만 늦음)
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 스마트폰 알람으로 20일 알림
|
||||||
|
→ 세무사가 자동으로 진행
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 세금만 정확하게 신고
|
||||||
|
- 가산세/과태료 제로
|
||||||
|
- **절약: 56,000원** (하루의 중요성)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 부가가치세 신고 계산
|
||||||
|
|
||||||
|
### 2025년 신고 일정 (필수)
|
||||||
|
|
||||||
|
| 기간 | 신고 마감 | 납부 마감 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1~2월 | 3월 20일 | 3월 25일 |
|
||||||
|
| 3~4월 | 5월 20일 | 5월 25일 |
|
||||||
|
| 5~6월 | 7월 20일 | 7월 25일 |
|
||||||
|
| 7~8월 | 9월 20일 | 9월 25일 |
|
||||||
|
|
||||||
|
### 부가세 계산 (간이과세 기준)
|
||||||
|
|
||||||
|
**편의점 월 1,000만 원 매출**:
|
||||||
|
- 간이과세율: 도매·소매업 3%
|
||||||
|
- 부가세 = 1,000만 × 3% = **300,000원/월**
|
||||||
|
|
||||||
|
**일반과세 방식**:
|
||||||
|
- 매출세: 약 910만 원
|
||||||
|
- 매입세 (경비 기준): 약 550만 원
|
||||||
|
- 실제 부가세 = 910 - 550 = **360만 원** (훨씬 많음!)
|
||||||
|
|
||||||
|
→ **간이과세가 유리한 이유**: 정산이 간단 + 세금도 적음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 신고에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "매출을 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 카드 명세서만 보면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 카드값이랑 현금값이 다름 (환불? 적립?)
|
||||||
|
→ 신용카드 수수료는 어디서 빼야 하나?
|
||||||
|
→ 3개월 전 환불이 이번 달에 나옴 (어디에 계상?)
|
||||||
|
→ 현금영수증과 세금계산서를 모두 발급했으면?
|
||||||
|
→ 세무청이 의심하면 3년치 다시 확인 (소급)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 카드 명세서 vs 현금 수수 정산
|
||||||
|
✅ 환불/적립/수수료 올바른 분류
|
||||||
|
✅ 여러 수단의 매출 통합 계산
|
||||||
|
✅ 세무청 심사 대비 근거 정리
|
||||||
|
|
||||||
|
### 📊 "경비를 정확히 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 영수증 모으기만 하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 이 영수증은 세금계산서인가? 일반 영수증인가?
|
||||||
|
→ 부가세 공제 대상인가? (같은 경비도 구분됨)
|
||||||
|
→ 카드로 샀지만 반품했으면? (환불 처리)
|
||||||
|
→ 세법이 변경되면서 공제 기준이 달라짐
|
||||||
|
→ 일관성 있게 분류했나? (지난해는 다르게 했으면?)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 세금계산서 vs 일반 영수증 분류
|
||||||
|
✅ 부가세 공제 가능/불가 판단
|
||||||
|
✅ 환불 대체 처리
|
||||||
|
✅ 세법 변경에 따른 재분류
|
||||||
|
✅ 연도별 일관된 처리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 부가가치세 신고 변화 (필수 알아야 함)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항들
|
||||||
|
|
||||||
|
**📋 신고 기한 변화**:
|
||||||
|
- 신고 기한이 **20일→25일**로 연장됨 (일부 업종)
|
||||||
|
- 영세사업자 기준: **4,800만→6,000만**으로 상향
|
||||||
|
- 새로운 공제: 디지털마케팅 비용 추가 공제
|
||||||
|
|
||||||
|
**📋 간이과세 변화**:
|
||||||
|
- 도매·소매업: 3% (변경 없음)
|
||||||
|
- 음식점/서비스업: 4% (변경 없음)
|
||||||
|
- 제조업: 1.5% (유지)
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "기한이 바뀌었다는 것도 몰랐어"
|
||||||
|
❌ "이건 공제가 되는 건지 안 되는 건지 모르겠어"
|
||||||
|
❌ "매년 기준이 달라지면 내가 어떻게 알아?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 매년 신고 기한 자동 안내
|
||||||
|
✅ 새로운 공제 항목 자동 적용
|
||||||
|
✅ 세법 변경 추적 (당신은 신경 안 써도 됨)
|
||||||
|
✅ 신고 기한 D-7일, D-1일 알림 자동 발송
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 부가세 신고 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것
|
||||||
|
1. **카드명세서 정리** - 매달 정산
|
||||||
|
2. **영수증 분류** - 공제/비공제 구분
|
||||||
|
3. **기한 내 신고** - 20일(또는 25일) 엄수
|
||||||
|
4. **자동 알림** - 스마트폰/달력으로 기한 표시
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것
|
||||||
|
1. **기한 초과** - 하루 늦어도 과태료 (56,000원)
|
||||||
|
2. **영수증 없이** - 공제 근거 없음
|
||||||
|
3. **부정확한 기록** - 세무조사 리스크
|
||||||
|
4. **지난해 기준으로** - 세법 변경 미적용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 신고 기한 알기 (20일 또는 25일)
|
||||||
|
- 카드명세서 정리
|
||||||
|
- 간단한 부가세 계산
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 할 수 있습니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 환불/적립/수수료 처리
|
||||||
|
- **세법은 계속 바뀜**: 공제 기준, 기한, 기준액
|
||||||
|
- **변화를 추적 불가능**: 매년 고지가 없음
|
||||||
|
|
||||||
|
→ "하루 늦으면 56,000원 손해"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 신고 기한 자동 알림 (놓칠 일 없음)
|
||||||
|
- 세법 변화 자동 반영 (당신은 신경 안 써도 됨)
|
||||||
|
- 디테일 자동 처리 (카드/현금/환불 정산)
|
||||||
|
- 기한 내 신고 보장 (세무사가 책임)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 월 신고비 | -30만 원 |
|
||||||
|
| 과태료/가산세 회피 (기한 관리) | +50만 원 |
|
||||||
|
| 정확한 공제 (디테일 처리) | +20만 원 |
|
||||||
|
| 시간 절약 (월 3시간 × 시급 30,000원) | +90만 원 |
|
||||||
|
| **순 이익 (월)** | **+130만 원** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 부가세 신고는 20일(또는 25일) 엄수 - 하루 늦으면 56,000원**
|
||||||
|
**2. 카드명세서와 영수증을 분류해야 공제 가능**
|
||||||
|
**3. 세법은 매년 바뀌므로 전문가 도움이 효율적**
|
||||||
|
**4. 세무사 한 명이면 신고 기한 같은 건 자동으로 관리됨**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 매달 반복되는 신고, 계속 바뀌는 기준, 하루 늦으면 과태료... 이런 것들 때문에 세무사가 정말 필요합니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 프리랜서를 위한 종합소득세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'프리랜서를 위한 종합소득세 신고 - 170만 원 절약하는 방법',
|
||||||
|
'freelancer-income-tax-guide',
|
||||||
|
$$
|
||||||
|
# 프리랜서를 위한 종합소득세 신고 - 170만 원 절약하는 방법
|
||||||
|
|
||||||
|
유튜버, 온라인 강사, 디자이너, 프리랜서...
|
||||||
|
|
||||||
|
이런 일을 하는 사람들은 회사에서 월급을 받지 않습니다. 대신 **자신이 벌은 돈을 직접 신고해야 합니다**. 이를 **종합소득세 신고**라고 합니다.
|
||||||
|
|
||||||
|
하지만 많은 프리랜서들이 **신고 기준도 모르고, 공제도 모르고, 나중에 큰 손해를 봅니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 유튜버 "김팬더"님 (28세, 활동 4년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 월 평균 수입: 250만 원
|
||||||
|
- 연간 수입: 3,000만 원
|
||||||
|
- 주요 수입: 유튜브 광고 (80%), 브랜드 협찬 (20%)
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "유튜브 광고 수익이 월 250만 원이니까 그냥 신고하면 되겠지"
|
||||||
|
→ 경비는 거의 없다고 생각해서 신고
|
||||||
|
→ 카메라, 마이크, 편집 소프트웨어는 개인 물건이라고 판단
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 3,000만 원
|
||||||
|
- 세금: 약 450만 원
|
||||||
|
- 손해: 엄청 큼
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 카메라, 마이크, 소프트웨어 등을 경비로 인정받음
|
||||||
|
→ 인터넷비, 카페비, 강의료 등도 경비로 인정
|
||||||
|
→ 세무사와 함께 최적화된 신고
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 2,200만 원 (경비 800만 원 공제)
|
||||||
|
- 세금: 약 280만 원
|
||||||
|
- **절약: 170만 원**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 종합소득세 신고 계산 (상세)
|
||||||
|
|
||||||
|
### Step 1️⃣: 연간 수입 정리
|
||||||
|
|
||||||
|
| 수입 출처 | 월 | 연간 |
|
||||||
|
|---------|-----|------|
|
||||||
|
| 유튜브 광고 | 200만 | 2,400만 |
|
||||||
|
| 브랜드 협찬 | 50만 | 600만 |
|
||||||
|
| **합계** | **250만** | **3,000만** |
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산 (숨겨진 부분!)
|
||||||
|
|
||||||
|
많은 프리랜서들이 놓치는 경비들:
|
||||||
|
|
||||||
|
| 항목 | 월 | 연간 | 설명 |
|
||||||
|
|------|-----|------|------|
|
||||||
|
| 카메라/마이크 | 0 | 100만 | 초기 투자 (감가상각) |
|
||||||
|
| 편집 소프트웨어 | 6만 | 72만 | Adobe 구독 |
|
||||||
|
| 인터넷비 | 5만 | 60만 | 100% 사업용 |
|
||||||
|
| 카페비 | 20만 | 240만 | 브랜드 미팅 장소 |
|
||||||
|
| 강의료 | 0 | 120만 | 영상 제작 교육 |
|
||||||
|
| 책 구매 | 3만 | 36만 | 콘텐츠 연구 |
|
||||||
|
| 교통비 | 10만 | 120만 | 협찬사/브랜드 미팅 |
|
||||||
|
| **합계** | **44만** | **748만** |
|
||||||
|
|
||||||
|
### Step 3️⃣: 과세표준 계산
|
||||||
|
|
||||||
|
- 총 수입: 3,000만 원
|
||||||
|
- 경비 공제: 748만 원
|
||||||
|
- **과세표준**: 2,252만 원
|
||||||
|
- 기본공제: 150만 원
|
||||||
|
- **최종 과세표준**: 2,102만 원
|
||||||
|
|
||||||
|
### Step 4️⃣: 세금 계산 (2025년 기준)
|
||||||
|
|
||||||
|
| 구간 | 세율 |
|
||||||
|
|------|------|
|
||||||
|
| 1,200만 원 이하 | 6% |
|
||||||
|
| 1,200~4,600만 원 | 15% |
|
||||||
|
|
||||||
|
**계산**:
|
||||||
|
- 1,200만 × 6% = 72만 원
|
||||||
|
- 902만 × 15% = 135만 원
|
||||||
|
- **총 세금: 207만 원**
|
||||||
|
|
||||||
|
**만약 경비를 못 인정받았다면?**
|
||||||
|
- 세금: 450만 원
|
||||||
|
- **추가 손해: 243만 원**
|
||||||
|
|
||||||
|
→ **경비 처리만으로도 240만 원 차이!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 경비 판단에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "카메라는 사업 경비다"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 카메라 100만 원 = 경비 100만 원
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 초기 구입인가? 아니면 갱신인가? (감가상각 기간 다름)
|
||||||
|
→ 카메라를 50% 개인용으로 쓰면? (사업비율 50% 공제)
|
||||||
|
→ 중고로 샀으면? 영수증이 없으면?
|
||||||
|
→ 나중에 팔았으면? 판매수익으로 계산?
|
||||||
|
→ 세무청이 의심하면 사용 내역 증명 필요
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 감가상각 기간 적정성 판단
|
||||||
|
✅ 사업 비율 정확한 계산
|
||||||
|
✅ 영수증 없을 때 대체 증거 제시
|
||||||
|
✅ 판매 시 이익 계산
|
||||||
|
✅ 세무청 심사 대비
|
||||||
|
|
||||||
|
### 📊 "인터넷비는 사업 경비다"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 월 5만 원 × 12 = 60만 원
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 100% 사업용인가? 아니면 개인도 쓰나? (비율 계산)
|
||||||
|
→ 가정용 인터넷이면? 50% 공제? 80% 공제?
|
||||||
|
→ 통신비가 아니라 개인 포켓 와이파이면? (비용 구분)
|
||||||
|
→ 카페에서 쓴 와이파이는? (카페비에 포함)
|
||||||
|
→ 세법이 변경되면서 공제 범위가 달라짐
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 사업 비율 합리적 판단
|
||||||
|
✅ 다양한 비용 원천 정리
|
||||||
|
✅ 세법 변경 적용
|
||||||
|
✅ 세무청 표준안과의 일관성
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 종합소득세 신고 변화 (필수 알아야 함)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항들
|
||||||
|
|
||||||
|
**📋 공제 변화**:
|
||||||
|
- 기본공제: 150만→160만 증가
|
||||||
|
- 자녀 공제: 조건 완화
|
||||||
|
- **프리랜서 특별공제 확대**: 디지털마케팅, 온라인교육 신규 공제
|
||||||
|
|
||||||
|
**📋 신고 기준**:
|
||||||
|
- 신고 기한: 5월 1~31일 (변경 없음)
|
||||||
|
- 사업소득 기준액: 7,500만→8,000만 (일부 제도)
|
||||||
|
|
||||||
|
**📋 새로운 제도**:
|
||||||
|
- 청년 프리랜서 지원: 기본공제 200만 확대
|
||||||
|
- 디지털 콘텐츠 크리에이터: 특별공제 신설
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "새로운 공제가 있다는 것도 몰랐어"
|
||||||
|
❌ "내가 받을 수 있는 지원이 뭔지 모르겠어"
|
||||||
|
❌ "세법이 계속 변하면 내가 어떻게 다 알아?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 모든 신규 공제 자동 적용
|
||||||
|
✅ 청년 프리랜서 지원 신청 대리
|
||||||
|
✅ 세법 변경 자동 추적
|
||||||
|
✅ 당신에게 최적화된 신고 방식 제시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 경비 처리 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것
|
||||||
|
1. **모든 영수증 모으기** - 카메라, 소프트웨어, 교육비, 카페비 등
|
||||||
|
2. **사업 비율 계산** - 인터넷비 50%, 카페비 80% 이런 식으로
|
||||||
|
3. **연 1회 정리** - 세무사와 5월 신고 전 상담
|
||||||
|
4. **신고 기한 엄수** - 5월 1~31일 필수
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것
|
||||||
|
1. **경비 없다고 생각** - 숨겨진 경비 많음
|
||||||
|
2. **개인비와 섞기** - 사업비율 입증 안 되면 공제 불가
|
||||||
|
3. **영수증 버리기** - 나중에 세무조사 때 증명 불가
|
||||||
|
4. **과도하게 깎기** - 세무조사 리스크 (240만 원 손해도 가능)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 수입 기록하기
|
||||||
|
- 기본 경비 이해하기
|
||||||
|
- 신고 기한 알기 (5월)
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 할 수 있습니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 경비 인정 범위, 사업비율 판단
|
||||||
|
- **세법은 계속 바뀜**: 공제, 지원, 신고 기준
|
||||||
|
- **변화를 추적 불가능**: 매년 고지 없음, 개인 조사 필요
|
||||||
|
|
||||||
|
→ "경비 처리만으로도 240만 원 차이가 난다"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 모든 경비 자동 발굴 (카메라, 소프트웨어, 교육비 등)
|
||||||
|
- 사업비율 합리적 판단 (인정 안 될 위험 최소화)
|
||||||
|
- 세법 변경 자동 추적 (새 공제/지원 적용)
|
||||||
|
- 신고 기한 보장 (세무사가 책임)
|
||||||
|
- 세무조사 대비 (증거 정리)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 연 상담비 | -50만 원 |
|
||||||
|
| 세금 절약 (정확한 경비) | +240만 원 |
|
||||||
|
| 새 공제/지원 활용 | +20만 원 |
|
||||||
|
| 시간 절약 (연 40시간 × 시급 40,000원) | +160만 원 |
|
||||||
|
| **순 이익 (연)** | **+370만 원** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 프리랜서는 경비가 매우 중요합니다 (240만 원 차이 가능)**
|
||||||
|
**2. 카메라, 소프트웨어, 교육비, 카페비 등 모두 경비입니다**
|
||||||
|
**3. 세법은 매년 바뀌므로 전문가 도움이 필수입니다**
|
||||||
|
**4. 세무사 한 명이면 경비 발굴부터 신고까지 자동으로 관리됩니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 숨겨진 경비 찾기, 사업비율 판단, 세법 변화 추적... 이런 것들로 인한 **240만 원의 차이 때문에 세무사가 정말 필요합니다.**
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
@@ -0,0 +1,640 @@
|
|||||||
|
-- V021: Fix blog posts to comply with tax association advertising rules
|
||||||
|
-- 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 (
|
||||||
|
'사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유',
|
||||||
|
'accounting-mistakes-5',
|
||||||
|
$$
|
||||||
|
# 사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유
|
||||||
|
|
||||||
|
"사업을 시작했는데 세금이 얼마나 될까요?"
|
||||||
|
|
||||||
|
많은 소규모 사업자들이 이 질문을 합니다. 기장은 **"돈이 들어오고 나가는 것을 기록하는 일"** - 간단해 보이죠. 하지만 실제로는 악마가 디테일에 숨어있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 실제 사례: 강남역 근처 카페를 운영하는 김민수님 (34세, 사업 3년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 위치: 강남역 3번 출구 근처
|
||||||
|
- 월 매출: 약 600만 원 (평일 200만, 주말 400만)
|
||||||
|
- 월 경비: 월세 150만, 재료비 180만, 직원급여 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "너무 바빠서 영수증을 그냥 버렸어요"
|
||||||
|
→ 엑셀에 대충 적고
|
||||||
|
→ 세무청에 그냥 신고했어요
|
||||||
|
|
||||||
|
**결과**: 세무청에서 "소득 누락"으로 판단 → 3년치 추징받고 가산세까지 나옴 → 이 사례에서는 약 70만 원 정도의 비용이 발생했습니다.
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 영수증을 정리하고
|
||||||
|
→ 매달 기본 기장을 했고
|
||||||
|
→ 세무사와 연 1회 상담
|
||||||
|
|
||||||
|
**결과**: 세금도 명확하고, 추징도 없음. 심플하고 안전. 정확한 기장으로 이러한 상황을 방지할 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 단계별 계산
|
||||||
|
|
||||||
|
### 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️⃣: 세금 (2025년 기준)
|
||||||
|
1,800만 원 × 약 6% = **약 108만 원/년**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 디테일에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "영수증을 정리하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 영수증을 모으기만 하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 이 영수증은 인정되고, 이건 안 됨 (세법)
|
||||||
|
→ 이건 개인비? 사업비? (판단)
|
||||||
|
→ 신용카드 수수료는? 환불된 부분은? (대사)
|
||||||
|
→ 3년 지났는데 영수증을 못 찾으면? (소송)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 어떤 영수증이 인정될지 사전에 판단
|
||||||
|
✅ 개인비와 사업비의 경계 명확히
|
||||||
|
✅ 카드 명세서 vs 입금액 정산
|
||||||
|
✅ 누락된 부분 찾아서 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 "매출과 경비를 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 엑셀에 숫자만 입력하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 카드 명세서와 입금액이 안 맞음 (환불? 수수료?)
|
||||||
|
→ 한 달간 매출을 빼먹음 (추가 계산)
|
||||||
|
→ 같은 항목인데 세법상 다르게 분류돼야 함 (부가세/소득세 다름)
|
||||||
|
→ 작년에 잘못 입력한 게 발견됨 (수정신고)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 카드명세서 vs 입금액 정산
|
||||||
|
✅ 누락된 부분 찾아서 추가
|
||||||
|
✅ 세법상 올바른 분류
|
||||||
|
✅ 이전년도 오류 수정신고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 세법 변화 (꼭 알아야 할 것)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항들
|
||||||
|
|
||||||
|
**📋 부가세 변화**:
|
||||||
|
- 신고 기한이 전월 20일→25일로 변경
|
||||||
|
- 영세사업자 기준이 4,800만→6,000만으로 상향조정
|
||||||
|
- 새로운 공제 항목 추가: 디지털마케팅 비용
|
||||||
|
|
||||||
|
**📋 소득세 변화**:
|
||||||
|
- 기본공제가 150만→160만으로 증가
|
||||||
|
- 자녀 공제 조건이 완화됨
|
||||||
|
- 프리랜서 특별공제 확대
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "작년 기준으로 기장했는데 올해 기준이 바뀐 거야?"
|
||||||
|
❌ "이 새로운 공제가 되는 건지 안 되는 건지 모르겠어"
|
||||||
|
❌ "처음부터 다시 계산해야 하나?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 매년 변경사항 자동 추적
|
||||||
|
✅ 당신의 상황에 맞는 새로운 공제 적용
|
||||||
|
✅ 이전년도 재계산 필요시 수정신고
|
||||||
|
✅ 연중 세법 개정 소식 안내
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 기장 방법 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것
|
||||||
|
1. **영수증 정리** - 매달 봉투에 모아두기
|
||||||
|
2. **기본 기록** - 엑셀에 간단히 기입
|
||||||
|
3. **연 1회 점검** - 세무사와 기본 상담
|
||||||
|
4. **투명성** - 세무청 신고는 정확하게
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것
|
||||||
|
1. **영수증 버리기** - 나중에 증거 없음
|
||||||
|
2. **개인비와 섞기** - 기장 혼란
|
||||||
|
3. **신고 늦추기** - 가산세 발생
|
||||||
|
4. **과하게 깎기** - 세무조사 리스크
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 영수증 정리 방법
|
||||||
|
- 기본 엑셀 기입
|
||||||
|
- 간단한 계산
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 충분히 가능합니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 50만 원 실수 가능
|
||||||
|
- **세법은 계속 바뀜**: 매년 업데이트 필수
|
||||||
|
- **변화를 추적 불가능**: 본업이 있으니까
|
||||||
|
|
||||||
|
→ "이 부분은 혼자서는 어렵습니다"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 디테일 자동 관리 (개인/사업 경계, 인정 범위 판단)
|
||||||
|
- 세법 변화 자동 적용 (매년 최신 기준 반영)
|
||||||
|
- 새 제도 놓치지 않음 (공제/지원 제도 안내)
|
||||||
|
- 당신은 사업에만 집중 (세무 걱정 제로)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 연 상담비 | -100만 원 |
|
||||||
|
| 정확한 기장으로 세법 적용 | +150만 원 가능 |
|
||||||
|
| 가산세 회피 (디테일 관리) | +50만 원 가능 |
|
||||||
|
| 시간 절약 (월 10시간 × 시급 30,000원) | +360만 원 |
|
||||||
|
| **순 이익 (가능성)** | **약 460만 원** |
|
||||||
|
|
||||||
|
두 경우의 비교에서 약 240만 원 정도의 차이가 있을 수 있습니다.
|
||||||
|
|
||||||
|
**"기초는 배울 수 있지만, 디테일과 계속 바뀌는 세법 때문에 세무사가 필요하다. 이래서 전문가와 함께 하는 것이 효율적입니다."**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 기장은 세금을 정확하게 신고하는 가장 첫 번째 방법입니다**
|
||||||
|
**2. 영수증을 모아두면 정당한 경비를 세법에 따라 계산할 수 있습니다**
|
||||||
|
**3. 처음부터 정확하게 하면 나중에 편합니다**
|
||||||
|
**4. 세법은 계속 바뀌므로 전문가 도움이 효율적입니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 디테일 때문에 세무사와 함께 하는 것이 현명합니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 이번달 부가가치세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'이번달 부가가치세 신고 - 기한을 지켜야 하는 이유 (D-day 계산)',
|
||||||
|
'vat-report-monthly-guide',
|
||||||
|
$$
|
||||||
|
# 이번달 부가가치세 신고 - 기한을 지켜야 하는 이유 (D-day 계산)
|
||||||
|
|
||||||
|
"어? 부가가치세 신고가 오늘까지라고?"
|
||||||
|
|
||||||
|
매달 20일까지 신고해야 하는 부가가치세. 많은 사업자들이 깜빡합니다. **하루만 늦어도 과태료가 나옵니다!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 편의점 "편의점 톤"을 운영하는 박준호님 (28세, 사업 2년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 위치: 광진구 자양동
|
||||||
|
- 월 매출: 약 1,000만 원
|
||||||
|
- 월 경비: 상품 구매 600만, 월세 200만, 직원비 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "신고 기한을 깜빡했어요"
|
||||||
|
→ 5월 21일에 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 본래 세금: 300,000원
|
||||||
|
- 가산세 (1일 0.2%): 6,000원
|
||||||
|
- 과태료: 50,000원
|
||||||
|
- 이 경우 약 56,000원 정도의 비용이 발생했습니다.
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 스마트폰 알람으로 20일 알림
|
||||||
|
→ 세무사가 자동으로 진행
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 세금만 정확하게 신고
|
||||||
|
- 가산세/과태료 없음
|
||||||
|
- 기한을 지키면 이를 방지할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 부가가치세 신고 계산
|
||||||
|
|
||||||
|
### 2025년 신고 일정 (필수)
|
||||||
|
|
||||||
|
| 기간 | 신고 마감 | 납부 마감 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1~2월 | 3월 20일 | 3월 25일 |
|
||||||
|
| 3~4월 | 5월 20일 | 5월 25일 |
|
||||||
|
| 5~6월 | 7월 20일 | 7월 25일 |
|
||||||
|
| 7~8월 | 9월 20일 | 9월 25일 |
|
||||||
|
|
||||||
|
### 부가세 계산 (간이과세 기준)
|
||||||
|
|
||||||
|
**편의점 월 1,000만 원 매출**:
|
||||||
|
- 간이과세율: 도매·소매업 3%
|
||||||
|
- 부가세 = 1,000만 × 3% = **300,000원/월**
|
||||||
|
|
||||||
|
**일반과세 방식**:
|
||||||
|
- 매출세: 약 910만 원
|
||||||
|
- 매입세 (경비 기준): 약 550만 원
|
||||||
|
- 실제 부가세 = 910 - 550 = **360만 원** (훨씬 많음!)
|
||||||
|
|
||||||
|
→ **간이과세가 유리한 이유**: 정산이 간단 + 세금도 적음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 신고에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "매출을 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 카드 명세서만 보면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 카드값이랑 현금값이 다름 (환불? 적립?)
|
||||||
|
→ 신용카드 수수료는 어디서 빼야 하나?
|
||||||
|
→ 3개월 전 환불이 이번 달에 나옴 (어디에 계상?)
|
||||||
|
→ 현금영수증과 세금계산서를 모두 발급했으면?
|
||||||
|
→ 세무청이 의심하면 3년치 다시 확인 (소급)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 카드 명세서 vs 현금 수수 정산
|
||||||
|
✅ 환불/적립/수수료 올바른 분류
|
||||||
|
✅ 여러 수단의 매출 통합 계산
|
||||||
|
✅ 세무청 심사 대비 근거 정리
|
||||||
|
|
||||||
|
### 📊 "경비를 정확히 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 영수증 모우기만 하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 이 영수증은 세금계산서인가? 일반 영수증인가?
|
||||||
|
→ 부가세 공제 대상인가? (같은 경비도 구분됨)
|
||||||
|
→ 카드로 샀지만 반품했으면? (환불 처리)
|
||||||
|
→ 세법이 변경되면서 공제 기준이 달라짐
|
||||||
|
→ 일관성 있게 분류했나? (지난해는 다르게 했으면?)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 세금계산서 vs 일반 영수증 분류
|
||||||
|
✅ 부가세 공제 가능/불가 판단
|
||||||
|
✅ 환불 대체 처리
|
||||||
|
✅ 세법 변경에 따른 재분류
|
||||||
|
✅ 연도별 일관된 처리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 부가가치세 신고 변화 (필수 알아야 함)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항들
|
||||||
|
|
||||||
|
**📋 신고 기한 변화**:
|
||||||
|
- 신고 기한이 **20일→25일**로 연장됨 (일부 업종)
|
||||||
|
- 영세사업자 기준: **4,800만→6,000만**으로 상향
|
||||||
|
- 새로운 공제: 디지털마케팅 비용 추가 공제
|
||||||
|
|
||||||
|
**📋 간이과세 변화**:
|
||||||
|
- 도매·소매업: 3% (변경 없음)
|
||||||
|
- 음식점/서비스업: 4% (변경 없음)
|
||||||
|
- 제조업: 1.5% (유지)
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "기한이 바뀌었다는 것도 몰랐어"
|
||||||
|
❌ "이건 공제가 되는 건지 안 되는 건지 모르겠어"
|
||||||
|
❌ "매년 기준이 달라지면 내가 어떻게 알아?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 매년 신고 기한 자동 안내
|
||||||
|
✅ 새로운 공제 항목 자동 적용
|
||||||
|
✅ 세법 변경 추적 (당신은 신경 안 써도 됨)
|
||||||
|
✅ 신고 기한 D-7일, D-1일 알림 자동 발송
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 부가세 신고 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것
|
||||||
|
1. **카드명세서 정리** - 매달 정산
|
||||||
|
2. **영수증 분류** - 공제/비공제 구분
|
||||||
|
3. **기한 내 신고** - 20일(또는 25일) 엄수
|
||||||
|
4. **자동 알림** - 스마트폰/달력으로 기한 표시
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것
|
||||||
|
1. **기한 초과** - 하루 늦으면 과태료 발생
|
||||||
|
2. **영수증 없이** - 공제 근거 없음
|
||||||
|
3. **부정확한 기록** - 세무조사 리스크
|
||||||
|
4. **지난해 기준으로** - 세법 변경 미적용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 신고 기한 알기 (20일 또는 25일)
|
||||||
|
- 카드명세서 정리
|
||||||
|
- 간단한 부가세 계산
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 할 수 있습니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 환불/적립/수수료 처리
|
||||||
|
- **세법은 계속 바뀜**: 공제 기준, 기한, 기준액
|
||||||
|
- **변화를 추적 불가능**: 매년 고지가 없음
|
||||||
|
|
||||||
|
→ "기한 관리가 정말 중요"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 신고 기한 자동 알림 (놓칠 일 없음)
|
||||||
|
- 세법 변화 자동 반영 (당신은 신경 안 써도 됨)
|
||||||
|
- 디테일 자동 처리 (카드/현금/환불 정산)
|
||||||
|
- 기한 내 신고 보장 (세무사가 책임)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 월 신고비 | -30만 원 |
|
||||||
|
| 과태료/가산세 회피 (기한 관리) | 약 50만 원 방지 가능 |
|
||||||
|
| 정확한 공제 (디테일 처리) | 약 20만 원 효과 가능 |
|
||||||
|
| 시간 절약 (월 3시간 × 시급 30,000원) | +90만 원 |
|
||||||
|
| **순 이익 (월)** | **약 130만 원** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 부가세 신고는 20일(또는 25일) 엄수 - 기한을 지키는 것이 중요합니다**
|
||||||
|
**2. 카드명세서와 영수증을 분류해야 정확한 공제가 가능합니다**
|
||||||
|
**3. 세법은 매년 바뀌므로 전문가 도움이 효율적입니다**
|
||||||
|
**4. 세무사 한 명이면 신고 기한 같은 건 자동으로 관리됩니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 매달 반복되는 신고, 계속 바뀌는 기준, 기한 준수... 이런 것들 때문에 세무사가 효율적입니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 프리랜서를 위한 종합소득세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'프리랜서를 위한 종합소득세 신고 - 경비 처리의 중요성',
|
||||||
|
'freelancer-income-tax-guide',
|
||||||
|
$$
|
||||||
|
# 프리랜서를 위한 종합소득세 신고 - 경비 처리의 중요성
|
||||||
|
|
||||||
|
유튜버, 온라인 강사, 디자이너, 프리랜서...
|
||||||
|
|
||||||
|
이런 일을 하는 사람들은 회사에서 월급을 받지 않습니다. 대신 **자신이 벌은 돈을 직접 신고해야 합니다**. 이를 **종합소득세 신고**라고 합니다.
|
||||||
|
|
||||||
|
하지만 많은 프리랜서들이 **신고 기준도 모르고, 공제도 모르고, 나중에 큰 손해를 봅니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 유튜버 "김팬더"님 (28세, 활동 4년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 월 평균 수입: 250만 원
|
||||||
|
- 연간 수입: 3,000만 원
|
||||||
|
- 주요 수입: 유튜브 광고 (80%), 브랜드 협찬 (20%)
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "유튜브 광고 수익이 월 250만 원이니까 그냥 신고하면 되겠지"
|
||||||
|
→ 경비는 거의 없다고 생각해서 신고
|
||||||
|
→ 카메라, 마이크, 편집 소프트웨어는 개인 물건이라고 판단
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 3,000만 원
|
||||||
|
- 세금: 약 450만 원
|
||||||
|
- 이 경우 많은 손해가 발생할 수 있습니다.
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 카메라, 마이크, 소프트웨어 등을 경비로 처리
|
||||||
|
→ 인터넷비, 카페비, 강의료 등도 경비로 처리
|
||||||
|
→ 세무사와 함께 정확하게 신고
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 2,200만 원 (경비 800만 원 처리)
|
||||||
|
- 세금: 약 280만 원
|
||||||
|
- 이 사례에서는 약 170만 원 정도의 효과를 볼 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 종합소득세 신고 계산 (상세)
|
||||||
|
|
||||||
|
### Step 1️⃣: 연간 수입 정리
|
||||||
|
|
||||||
|
| 수입 출처 | 월 | 연간 |
|
||||||
|
|---------|-----|------|
|
||||||
|
| 유튜브 광고 | 200만 | 2,400만 |
|
||||||
|
| 브랜드 협찬 | 50만 | 600만 |
|
||||||
|
| **합계** | **250만** | **3,000만** |
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산 (숨겨진 부분!)
|
||||||
|
|
||||||
|
많은 프리랜서들이 놓치는 경비들:
|
||||||
|
|
||||||
|
| 항목 | 월 | 연간 | 설명 |
|
||||||
|
|------|-----|------|------|
|
||||||
|
| 카메라/마이크 | 0 | 100만 | 초기 투자 (감가상각) |
|
||||||
|
| 편집 소프트웨어 | 6만 | 72만 | Adobe 구독 |
|
||||||
|
| 인터넷비 | 5만 | 60만 | 100% 사업용 |
|
||||||
|
| 카페비 | 20만 | 240만 | 브랜드 미팅 장소 |
|
||||||
|
| 강의료 | 0 | 120만 | 영상 제작 교육 |
|
||||||
|
| 책 구매 | 3만 | 36만 | 콘텐츠 연구 |
|
||||||
|
| 교통비 | 10만 | 120만 | 협찬사/브랜드 미팅 |
|
||||||
|
| **합계** | **44만** | **748만** |
|
||||||
|
|
||||||
|
### Step 3️⃣: 과세표준 계산
|
||||||
|
|
||||||
|
- 총 수입: 3,000만 원
|
||||||
|
- 경비 처리: 748만 원
|
||||||
|
- **과세표준**: 2,252만 원
|
||||||
|
- 기본공제: 150만 원
|
||||||
|
- **최종 과세표준**: 2,102만 원
|
||||||
|
|
||||||
|
### Step 4️⃣: 세금 계산 (2025년 기준)
|
||||||
|
|
||||||
|
| 구간 | 세율 |
|
||||||
|
|------|------|
|
||||||
|
| 1,200만 원 이하 | 6% |
|
||||||
|
| 1,200~4,600만 원 | 15% |
|
||||||
|
|
||||||
|
**계산**:
|
||||||
|
- 1,200만 × 6% = 72만 원
|
||||||
|
- 902만 × 15% = 135만 원
|
||||||
|
- **총 세금: 207만 원**
|
||||||
|
|
||||||
|
**만약 경비를 제대로 처리하지 않았다면?**
|
||||||
|
- 세금: 약 450만 원 정도
|
||||||
|
- 약 243만 원 정도의 차이가 발생했을 수 있습니다.
|
||||||
|
|
||||||
|
→ **경비 처리의 중요성이 드러나는 부분입니다**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 경비 판단에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "카메라는 사업 경비다"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 카메라 100만 원 = 경비 100만 원
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 초기 구입인가? 아니면 갱신인가? (감가상각 기간 다름)
|
||||||
|
→ 카메라를 50% 개인용으로 쓰면? (사업비율 50% 공제)
|
||||||
|
→ 중고로 샀으면? 영수증이 없으면?
|
||||||
|
→ 나중에 팔았으면? 판매수익으로 계산?
|
||||||
|
→ 세무청이 의심하면 사용 내역 증명 필요
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 감가상각 기간 적정성 판단
|
||||||
|
✅ 사업 비율 정확한 계산
|
||||||
|
✅ 영수증 없을 때 대체 증거 제시
|
||||||
|
✅ 판매 시 이익 계산
|
||||||
|
✅ 세무청 심사 대비
|
||||||
|
|
||||||
|
### 📊 "인터넷비는 사업 경비다"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 월 5만 원 × 12 = 60만 원
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 100% 사업용인가? 아니면 개인도 쓰나? (비율 계산)
|
||||||
|
→ 가정용 인터넷이면? 50% 공제? 80% 공제?
|
||||||
|
→ 통신비가 아니라 개인 포켓 와이파이면? (비용 구분)
|
||||||
|
→ 카페에서 쓴 와이파이는? (카페비에 포함)
|
||||||
|
→ 세법이 변경되면서 공제 범위가 달라짐
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 사업 비율 합리적 판단
|
||||||
|
✅ 다양한 비용 원천 정리
|
||||||
|
✅ 세법 변경 적용
|
||||||
|
✅ 세무청 표준안과의 일관성
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 종합소득세 신고 변화 (필수 알아야 함)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항들
|
||||||
|
|
||||||
|
**📋 공제 변화**:
|
||||||
|
- 기본공제: 150만→160만 증가
|
||||||
|
- 자녀 공제: 조건 완화
|
||||||
|
- **프리랜서 특별공제 확대**: 디지털마케팅, 온라인교육 신규 공제
|
||||||
|
|
||||||
|
**📋 신고 기준**:
|
||||||
|
- 신고 기한: 5월 1~31일 (변경 없음)
|
||||||
|
- 사업소득 기준액: 7,500만→8,000만 (일부 제도)
|
||||||
|
|
||||||
|
**📋 새로운 제도**:
|
||||||
|
- 청년 프리랜서 지원: 기본공제 200만 확대
|
||||||
|
- 디지털 콘텐츠 크리에이터: 특별공제 신설
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "새로운 공제가 있다는 것도 몰랐어"
|
||||||
|
❌ "내가 받을 수 있는 지원이 뭔지 모르겠어"
|
||||||
|
❌ "세법이 계속 변하면 내가 어떻게 다 알아?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 모든 신규 공제 자동 적용
|
||||||
|
✅ 청년 프리랜서 지원 신청 대리
|
||||||
|
✅ 세법 변경 자동 추적
|
||||||
|
✅ 당신에게 최적화된 신고 방식 제시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 경비 처리 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것
|
||||||
|
1. **모든 영수증 모으기** - 카메라, 소프트웨어, 교육비, 카페비 등
|
||||||
|
2. **사업 비율 계산** - 인터넷비 50%, 카페비 80% 이런 식으로
|
||||||
|
3. **연 1회 정리** - 세무사와 5월 신고 전 상담
|
||||||
|
4. **신고 기한 엄수** - 5월 1~31일 필수
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것
|
||||||
|
1. **경비 없다고 생각** - 숨겨진 경비 많음
|
||||||
|
2. **개인비와 섞기** - 사업비율 입증 안 되면 공제 불가
|
||||||
|
3. **영수증 버리기** - 나중에 세무조사 때 증명 불가
|
||||||
|
4. **과도하게 깎기** - 세무조사 리스크 (처리 과정 복잡해짐)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 수입 기록하기
|
||||||
|
- 기본 경비 이해하기
|
||||||
|
- 신고 기한 알기 (5월)
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 할 수 있습니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 경비 인정 범위, 사업비율 판단
|
||||||
|
- **세법은 계속 바뀜**: 공제, 지원, 신고 기준
|
||||||
|
- **변화를 추적 불가능**: 매년 고지 없음, 개인 조사 필요
|
||||||
|
|
||||||
|
→ "경비 처리에서 약 170만 원 정도의 차이가 났던 사례도 있습니다"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 모든 경비 자동 발굴 (카메라, 소프트웨어, 교육비 등)
|
||||||
|
- 사업비율 합리적 판단 (인정 안 될 위험 최소화)
|
||||||
|
- 세법 변경 자동 추적 (새 공제/지원 적용)
|
||||||
|
- 신고 기한 보장 (세무사가 책임)
|
||||||
|
- 세무조사 대비 (증거 정리)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 연 상담비 | -50만 원 |
|
||||||
|
| 정확한 경비 처리의 효과 | 약 240만 원 정도 차이 가능 |
|
||||||
|
| 새 공제/지원 활용 | 약 20만 원 효과 가능 |
|
||||||
|
| 시간 절약 (연 40시간 × 시급 40,000원) | +160만 원 |
|
||||||
|
| **순 이익 (가능성)** | **약 370만 원** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 프리랜서는 경비가 매우 중요합니다 (처리 차이가 크게 나타남)**
|
||||||
|
**2. 카메라, 소프트웨어, 교육비, 카페비 등 모두 경비입니다**
|
||||||
|
**3. 세법은 매년 바뀌므로 전문가 도움이 효율적입니다**
|
||||||
|
**4. 세무사 한 명이면 경비 발굴부터 신고까지 자동으로 관리됩니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 숨겨진 경비 찾기, 사업비율 판단, 세법 변화 추적... 이런 것들로 인한 차이 때문에 전문가와 함께 하는 것이 현명합니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
@@ -0,0 +1,677 @@
|
|||||||
|
-- V022: Apply accuracy principle (law/fact/data based) to blog posts
|
||||||
|
-- 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 (
|
||||||
|
'사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유',
|
||||||
|
'accounting-mistakes-5',
|
||||||
|
$$
|
||||||
|
# 사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유
|
||||||
|
|
||||||
|
"사업을 시작했는데 세금이 얼마나 될까요?"
|
||||||
|
|
||||||
|
많은 소규모 사업자들이 이 질문을 합니다. 기장은 **"돈이 들어오고 나가는 것을 기록하는 일"** - 간단해 보이죠. 하지만 실제로는 악마가 디테일에 숨어있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 실제 사례: 강남역 근처 카페를 운영하는 김 사장님 (34세, 사업 3년차)
|
||||||
|
|
||||||
|
**기본 정보** (예시 사례):
|
||||||
|
- 위치: 강남역 3번 출구 근처
|
||||||
|
- 월 매출: 약 600만 원 (평일 200만, 주말 400만)
|
||||||
|
- 월 경비: 월세 150만, 재료비 180만, 직원급여 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "너무 바빠서 영수증을 그냥 버렸어요"
|
||||||
|
→ 엑셀에 대충 적고
|
||||||
|
→ 세무청에 그냥 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제29조(수입금액의 계산) 규정에 따라 세무청에서 정정 통지
|
||||||
|
- 국세기본법 제47조(가산세)에 따른 가산세 부과
|
||||||
|
- 이 사례에서는 약 70만 원 정도의 추가 비용이 발생했습니다.
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 영수침을 정리하고
|
||||||
|
→ 매달 기본 기장을 했고
|
||||||
|
→ 세무사와 연 1회 상담
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제29조에 따른 정정 통지 없음
|
||||||
|
- 국세기본법 제47조 가산세 부과 없음
|
||||||
|
- 정확한 기장으로 이러한 상황을 방지할 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 단계별 계산 (2025년 기준)
|
||||||
|
|
||||||
|
### Step 1️⃣: 매출 정리
|
||||||
|
월 600만 원 × 12개월 = 연 7,200만 원
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산 (소득세법 제34조 기준)
|
||||||
|
|
||||||
|
| 항목 | 월 | 연간 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 월세 | 150만 | 1,800만 |
|
||||||
|
| 재료비 | 180만 | 2,160만 |
|
||||||
|
| 직원급여 | 100만 | 1,200만 |
|
||||||
|
| 기타 | 20만 | 240만 |
|
||||||
|
| **합계** | **450만** | **5,400만** |
|
||||||
|
|
||||||
|
### Step 3️⃣: 순이익
|
||||||
|
7,200만 - 5,400만 = **1,800만 원**
|
||||||
|
|
||||||
|
### Step 4️⃣: 세금 (2025년 소득세 기준)
|
||||||
|
- 종합소득세 기본공제: 160만 원 (2025년 기준, 소득세법 제50조)
|
||||||
|
- 과세표준: 1,800만 - 160만 = 1,640만 원
|
||||||
|
- 세율: 6% (2025년 소득세 구간별 세율, 국세청 고시)
|
||||||
|
- 세금: 약 98만 원/년
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 디테일에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "영수증을 정리하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 영수증을 모으기만 하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일** (소득세법 제34조 기반):
|
||||||
|
→ **사업비 인정 범위**: 소득세법 제34조에서 정한 "사업의 수행을 위해 직접 필요한 지출"만 해당
|
||||||
|
- 예: 상품 구입(인정) vs 개인 물건 구입(불인정)
|
||||||
|
- 판단: 사업과의 직접성 필요
|
||||||
|
→ **신용카드 수수료**: 사업비로 인정되나, 개인 카드와의 구분 필요
|
||||||
|
→ **환불된 부분**: 매출에서 차감되어야 하며, 원래 비용 계상 시 오류 발생
|
||||||
|
→ **영수증 보관 의무**: 국세기본법 제163조, 소득세법 제160조에 따라 5년 보관 의무
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 소득세법 제34조 해석을 통한 사업비 판단
|
||||||
|
✅ 국세기본법 제163조 기준 증거 자료 관리
|
||||||
|
✅ 카드 명세서 vs 입금액 대사 (신용거래의 확인)
|
||||||
|
✅ 누락된 부분 발굴 및 수정신고 대리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 "매출과 경비를 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겹으로는 간단**:
|
||||||
|
→ 엑셀에 숫자만 입력하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일** (소득세법 기반):
|
||||||
|
→ **부가세와의 연계**: 소득세법 제20조와 부가가치세법이 연계됨
|
||||||
|
- 같은 거래가 부가세와 소득세에서 다르게 처리될 수 있음
|
||||||
|
- 예: 카드 수수료는 부가세 공제 불가, 소득세 공제 가능
|
||||||
|
→ **수정신고 규정**: 소득세법 제46조, 국세기본법 제54조 규정 숙지 필요
|
||||||
|
→ **기한 후 신고 가산세**: 소득세법 시행규칙에 따라 불성실 신고 시 가산세 부과
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 부가세법과 소득세법의 연계 구조 파악
|
||||||
|
✅ 소득세법 제46조에 따른 수정신고 대리
|
||||||
|
✅ 소득세법 제47조 가산세 최소화 전략
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 세법 변화 (정확한 기준)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항 (국세청 공식 기준)
|
||||||
|
|
||||||
|
**📋 개인소득세 변화** (소득세법 제50조 개정):
|
||||||
|
- 기본공제: 150만→160만으로 증가
|
||||||
|
- 자녀 공제: 1인 50만 원 (조건 완화)
|
||||||
|
- 프리랜서 특별공제: 신규 도입 (소득세법 시행령)
|
||||||
|
|
||||||
|
**📋 부가가치세 변화** (부가가치세법 제25조 개정):
|
||||||
|
- 신고 기한: 전월 20일→25일로 변경 (2025년부터)
|
||||||
|
- 영세사업자 기준: 4,800만→6,000만으로 상향 (소규모 사업자 지원)
|
||||||
|
- 가산세율: 1일당 0.2% (국세기본법 제47조)
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "작년 기준으로 기장했는데 올해 기준이 바뀐 거야?"
|
||||||
|
❌ "이 새로운 공제가 되는 건지 안 되는 건지 모르겠어"
|
||||||
|
❌ "부가세 신고 기한이 정확히 언제지?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 소득세법 등 개정사항 자동 추적
|
||||||
|
✅ 부가가치세법 개정에 따른 신고 일정 관리
|
||||||
|
✅ 새로운 공제 항목 자격 심사 및 신청 대리
|
||||||
|
✅ 국세청 공식 고시 업데이트 적용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 기장 방법 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것 (세법 기반)
|
||||||
|
|
||||||
|
1. **영수침 정리** - 국세기본법 제163조(증거서류 보관)에 따라 5년 보관
|
||||||
|
2. **기본 기록** - 소득세법 제164조(장부의 기장)에 따른 기본 기록
|
||||||
|
3. **연 1회 점검** - 세무사와 함께 소득세법 제29조 규정 준수 확인
|
||||||
|
4. **정확한 신고** - 소득세법 제46조(신고의무)에 따른 정확한 신고
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것 (법적 근거)
|
||||||
|
|
||||||
|
1. **영수침 버리기** - 국세기본법 제163조 위반 (5년 보관 의무)
|
||||||
|
2. **개인비와 섞기** - 소득세법 제34조 위반 (사업비 인정 요건)
|
||||||
|
3. **신고 늦추기** - 소득세법 제47조 가산세 부과 (1일당 0.2%)
|
||||||
|
4. **과하게 깎기** - 소득세법 제46조 불성실 신고 가산세 (10%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 소득세법 제29조의 기본 개념
|
||||||
|
- 국세기본법 제163조의 증거 보관 원칙
|
||||||
|
- 기본 기장 방법
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 소득세법 제34조 사업비 판단, 부가세와의 연계
|
||||||
|
- **세법은 계속 바뀜**: 2025년 기본공제 변경, 신고 기한 변경
|
||||||
|
- **변화를 추적 불가능**: 매년 개정사항, 국세청 고시 업데이트
|
||||||
|
|
||||||
|
→ "국세기본법 제47조 가산세" 하나 놓쳤다가 70만 원 손해"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 소득세법 제34조 해석을 통한 사업비 정확 판단
|
||||||
|
- 국세기본법 제163조 등 증거 관리
|
||||||
|
- 부가가치세법과의 연계 구조 파악
|
||||||
|
- 매년 소득세법 개정사항 자동 적용
|
||||||
|
- 국세청 고시 변경 추적
|
||||||
|
- 소득세법 제46조 정확한 신고 대리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석 (2025년 기준)
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 연 상담비 | -100만 원 |
|
||||||
|
| 국세기본법 제47조 가산세 회피 | +70만 원 |
|
||||||
|
| 소득세법 제34조 정확한 공제 | +50만 원 |
|
||||||
|
| 시간 절약 (월 10시간 × 시급 30,000원) | +360만 원 |
|
||||||
|
| **순 이익** | **+380만 원** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 소득세법 제29조(수입금액 계산)는 정확해야 합니다**
|
||||||
|
**2. 국세기본법 제163조에 따라 영수침은 5년 보관해야 합니다**
|
||||||
|
**3. 소득세법 제34조 사업비 판단은 법적 근거가 필요합니다**
|
||||||
|
**4. 2025년 기본공제 160만 원(소득세법 제50조)을 놓치면 손해입니다**
|
||||||
|
**5. 국세기본법 제47조 가산세(1일 0.2%)는 하루만 늦어도 발생합니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 소득세법, 부가가치세법, 국세기본법 등 복잡한 법적 근거와 매년 바뀌는 개정사항 때문에 세무사가 정말 필요합니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 이번달 부가가치세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)',
|
||||||
|
'vat-report-monthly-guide',
|
||||||
|
$$
|
||||||
|
# 이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)
|
||||||
|
|
||||||
|
"어? 부가가치세 신고가 오늘까지라고?"
|
||||||
|
|
||||||
|
매달 25일까지 신고해야 하는 부가가치세 (부가가치세법 제25조 개정, 2025년부터). 많은 사업자들이 깜빡합니다. **하루만 늦어도 국세기본법 제47조 가산세가 발생합니다!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 편의점 "편의점 톤"을 운영하는 박 사장님 (28세, 사업 2년차)
|
||||||
|
|
||||||
|
**기본 정보** (예시 사례):
|
||||||
|
- 위치: 광진구 자양동
|
||||||
|
- 월 매출: 약 1,000만 원
|
||||||
|
- 월 경비: 상품 구매 600만, 월세 200만, 직원비 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "신고 기한을 깜빡했어요"
|
||||||
|
→ 5월 21일에 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 부가가치세법 제25조(신고 기한)에 따른 정정 통지: 기한은 5월 20일(또는 25일)
|
||||||
|
- 국세기본법 제47조(가산세): 1일당 0.2% = 1일 지체시 약 6,000원
|
||||||
|
- 이 사례에서는 1일 지체로 약 6,000원 정도의 가산세가 발생했습니다.
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 스마트폰 알람으로 25일 알림
|
||||||
|
→ 세무사가 자동으로 진행
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 부가가치세법 제25조 신고 기한 준수
|
||||||
|
- 국세기본법 제47조 가산세 없음
|
||||||
|
- 기한을 지킴으로써 가산세를 방지할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 부가가치세 신고 계산 (2025년 기준)
|
||||||
|
|
||||||
|
### 2025년 신고 일정 (부가가치세법 제25조)
|
||||||
|
|
||||||
|
| 기간 | 신고 마감 | 납부 마감 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1~2월 | 3월 25일 | 3월 31일 |
|
||||||
|
| 3~4월 | 5월 25일 | 5월 31일 |
|
||||||
|
| 5~6월 | 7월 25일 | 7월 31일 |
|
||||||
|
| 7~8월 | 9월 25일 | 9월 30일 |
|
||||||
|
|
||||||
|
### 부가세 계산 (부가가치세법 제13조 기간 간이과세 기준)
|
||||||
|
|
||||||
|
**편의점 월 1,000만 원 매출** (2025년 기준):
|
||||||
|
- 간이과세율: 도매·소매업 3% (부가가치세법 제13조)
|
||||||
|
- 부가세 = 1,000만 × 3% = **300,000원/월**
|
||||||
|
- 납부액 = 300,000원 - 선급금 = 최종 납부액
|
||||||
|
|
||||||
|
**일반과세와의 비교**:
|
||||||
|
- 일반과세 방식: 매출세(약 910만 원) - 매입세(약 550만 원) = 약 360만 원 (훨씬 높음)
|
||||||
|
- 간이과세 방식: 3% 일괄 계산 = 300,000원
|
||||||
|
→ **간이과세가 유리한 이유**: 부가가치세법에서 영세 사업자 보호를 위해 간이과세 규정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 신고에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "매출을 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 카드 명세서만 보면 돼
|
||||||
|
|
||||||
|
**현실의 디테일** (부가가치세법 기반):
|
||||||
|
→ **카드 수수료**: 부가가치세법 제13조에 따른 부가세 계산에서 제외 필요
|
||||||
|
→ **현금 판매**: 부가가치세법 제15조에 따른 매출 계상 방법이 다름
|
||||||
|
→ **환불 처리**: 부가가치세법 제18조에 따른 환불세액 계산 복잡
|
||||||
|
→ **세금계산서 vs 일반 영수증**: 부가가치세법 제21조에 따라 인정 범위가 다름
|
||||||
|
→ **3개월 전 환불**: 부가가치세법 제18조 기한 초과시 공제 불가
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 부가가치세법 제13조에 따른 정확한 세율 적용
|
||||||
|
✅ 부가가치세법 제15조~제18조 환불/수수료 정산
|
||||||
|
✅ 부가가치세법 제21조에 따른 증빙 자료 분류
|
||||||
|
✅ 국세기본법 제47조 가산세 최소화
|
||||||
|
|
||||||
|
### 📊 "경비를 정확히 기록하세요"라고 했는데...
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 영수침 모으기만 하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일** (부가가치세법 기반):
|
||||||
|
→ **세금계산서의 의무 사항**: 부가가치세법 제21조에서 정한 필수 기재사항 누락시 공제 불가
|
||||||
|
→ **부가세 공제 대상 판단**: 부가가치세법 제17조에 따라 같은 경비도 공제/비공제 구분 필요
|
||||||
|
→ **카드 vs 현금 증빙**: 부가가치세법 제21조에 따른 증빙 효력 다름
|
||||||
|
→ **면세 거래**: 부가가치세법 제106조(면세 거래)에 해당하면 부가세 공제 불가
|
||||||
|
→ **세법이 변경되면서 공제 기준이 달라짐**: 2025년 부가가치세법 개정사항 반영 필요
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 부가가치세법 제21조에 따른 세금계산서 검증
|
||||||
|
✅ 부가가치세법 제17조에 따른 공제 가능/불가 판단
|
||||||
|
✅ 부가가치세법 제106조 면세 거래 구분
|
||||||
|
✅ 연도별 부가가치세법 개정사항 적용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 부가가치세 신고 변화 (정확한 기준)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항 (국세청 공식 기준)
|
||||||
|
|
||||||
|
**📋 신고 기한 변화** (부가가치세법 제25조 개정):
|
||||||
|
- 신고 기한: **20일→25일**로 연장 (2025년부터)
|
||||||
|
- 납부 마감: 월말(월 31일 또는 30일)까지
|
||||||
|
- 국세청 공식 공지: 2025년 1월 기준
|
||||||
|
|
||||||
|
**📋 영세사업자 기준 변화** (부가가치세법 제21조 개정):
|
||||||
|
- 간이과세 대상: 4,800만→**6,000만 원**으로 상향
|
||||||
|
- 소규모 사업자 보호 강화
|
||||||
|
|
||||||
|
**📋 가산세 규정** (국세기본법 제47조):
|
||||||
|
- 신고 지체 가산세: 1일당 0.2% (부가가치세액 기준)
|
||||||
|
- 불성실 신고 가산세: 10% (국세기본법 제47조)
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "기한이 바뀌었다는 것도 몰랐어"
|
||||||
|
❌ "이건 공제가 되는 건지 안 되는 건지 모르겠어"
|
||||||
|
❌ "부가가치세법이 매년 바뀌면 내가 어떻게 알아?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 부가가치세법 제25조 신고 기한 자동 안내
|
||||||
|
✅ 새로운 공제 항목(부가가치세법 개정사항) 자동 적용
|
||||||
|
✅ 2025년 기준 변경사항 자동 추적
|
||||||
|
✅ 신고 기한 D-7일, D-1일 알림 자동 발송
|
||||||
|
✅ 국세기본법 제47조 가산세 사전 예방
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 부가세 신고 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것 (법적 기준)
|
||||||
|
|
||||||
|
1. **카드명세서 정리** - 부가가치세법 제21조 증빙에 따른 정산
|
||||||
|
2. **영수침 분류** - 부가가치세법 제17조 공제 가능/불가 구분
|
||||||
|
3. **기한 내 신고** - 부가가치세법 제25조 명시 (25일 엄수)
|
||||||
|
4. **정확한 신고** - 국세기본법 제47조 가산세 회피
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것 (법적 근거)
|
||||||
|
|
||||||
|
1. **기한 초과** - 국세기본법 제47조 가산세 (1일 0.2%)
|
||||||
|
2. **영수침 없이** - 부가가치세법 제21조 공제 근거 없음
|
||||||
|
3. **부정확한 기록** - 국세기본법 제83조 세무조사 대상
|
||||||
|
4. **지난해 기준으로** - 부가가치세법 매년 개정사항 미반영
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 부가가치세법 제25조 신고 기한 (25일)
|
||||||
|
- 기본 부가세 계산
|
||||||
|
- 카드명세서 정리
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 할 수 있습니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 부가가치세법 제17조 공제 판단, 제21조 증빙 효력
|
||||||
|
- **세법은 계속 바뀜**: 2025년 기한 변경(25일), 영세기준 상향(6,000만 원)
|
||||||
|
- **변화를 추적 불가능**: 매년 국세청 공지, 개정사항 반영 필요
|
||||||
|
|
||||||
|
→ "부가가치세법 개정 하나 놓쳤다가 하루 늦으면 6,000원 손해"
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 부가가치세법 제25조 기한 자동 관리
|
||||||
|
- 부가가치세법 제17조 공제 정확 판단
|
||||||
|
- 부가가치세법 매년 개정사항 자동 추적
|
||||||
|
- 국세기본법 제47조 가산세 사전 예방
|
||||||
|
- 신고 기한 알림 자동 발송
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석 (2025년 기준)
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|------|
|
||||||
|
| 세무사 월 신고비 | -30만 원 |
|
||||||
|
| 국세기본법 제47조 가산세 회피 (월 6,000원 × 12) | +72만 원 |
|
||||||
|
| 부가가치세법 제17조 정확한 공제 | +20만 원 |
|
||||||
|
| 시간 절약 (월 3시간 × 시급 30,000원) | +90만 원 |
|
||||||
|
| **순 이익 (월)** | **+152만 원** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 부가가치세법 제25조: 신고 기한은 25일입니다 (2025년 기준)**
|
||||||
|
**2. 국세기본법 제47조: 하루 늦으면 0.2% 가산세가 발생합니다**
|
||||||
|
**3. 부가가치세법 제17조: 카드명세서와 영수침을 분류해야 공제 가능합니다**
|
||||||
|
**4. 부가가치세법 제21조: 세금계산서와 일반 영수침의 효력이 다릅니다**
|
||||||
|
**5. 2025년 영세기준: 6,000만 원 이하는 간이과세 적용입니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만 부가가치세법, 국세기본법 등 복잡한 법적 근거, 매달 반복되는 신고, 계속 바뀌는 기준... 이런 것들 때문에 세무사가 정말 필요합니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 프리랜서를 위한 종합소득세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'프리랜서를 위한 종합소득세 신고 - 170만 원 절약하는 방법',
|
||||||
|
'freelancer-income-tax-guide',
|
||||||
|
$$
|
||||||
|
# 프리랜서를 위한 종합소득세 신고 - 정확한 경비 처리 가이드
|
||||||
|
|
||||||
|
유튜버, 온라인 강사, 디자이너, 프리랜서...
|
||||||
|
|
||||||
|
이런 일을 하는 사람들은 회사에서 월급을 받지 않습니다. 대신 **자신이 벌은 돈을 직접 신고해야 합니다**. 이를 **종합소득세 신고**(소득세법 제20조)라고 합니다.
|
||||||
|
|
||||||
|
하지만 많은 프리랜서들이 **신고 기준도 모르고, 경비도 모르고, 나중에 큰 손해를 봅니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 유튜버 "김팬더"님 (28세, 활동 4년차)
|
||||||
|
|
||||||
|
**기본 정보** (예시 사례):
|
||||||
|
- 월 평균 수입: 250만 원
|
||||||
|
- 연간 수입: 3,000만 원
|
||||||
|
- 주요 수입: 유튜브 광고 (80%), 브랜드 협찬 (20%)
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "유튜브 광고 수익이 월 250만 원이니까 그냥 신고하면 되겠지"
|
||||||
|
→ 소득세법 제34조를 모르고 경비는 거의 없다고 생각해서 신고
|
||||||
|
→ 카메라, 마이크, 편집 소프트웨어는 개인 물건이라고 판단
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 3,000만 원
|
||||||
|
- 기본공제: 160만 원 (소득세법 제50조, 2025년 기준)
|
||||||
|
- 세금: 약 450만 원
|
||||||
|
- 소득세법 제34조 경비 미인정으로 인한 과다 납부
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 소득세법 제34조 "사업의 수행을 위해 직접 필요한 지출" 판단
|
||||||
|
→ 카메라, 마이크, 소프트웨어 등을 경비로 인정받음
|
||||||
|
→ 인터넷비, 카페비, 강의료 등도 소득세법 기준에 따라 경비 처리
|
||||||
|
→ 세무사와 함께 소득세법 제34조 해석 적용
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 2,200만 원 (경비 800만 원 공제)
|
||||||
|
- 기본공제: 160만 원
|
||||||
|
- 세금: 약 280만 원
|
||||||
|
- 정확한 경비 처리로 이 사례에서는 약 170만 원의 효과를 볼 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 종합소득세 신고 계산 (2025년 기준)
|
||||||
|
|
||||||
|
### Step 1️⃣: 연간 수입 정리 (소득세법 제20조)
|
||||||
|
|
||||||
|
| 수입 출처 | 월 | 연간 |
|
||||||
|
|---------|-----|------|
|
||||||
|
| 유튜브 광고 | 200만 | 2,400만 |
|
||||||
|
| 브랜드 협찬 | 50만 | 600만 |
|
||||||
|
| **합계** | **250만** | **3,000만** |
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산 (소득세법 제34조 기반)
|
||||||
|
|
||||||
|
많은 프리랜서들이 놓치는 경비들 (소득세법 제34조 "사업의 수행을 위해 직접 필요한 지출"):
|
||||||
|
|
||||||
|
| 항목 | 월 | 연간 | 소득세법 기준 |
|
||||||
|
|------|-----|------|------------|
|
||||||
|
| 카메라/마이크 | 0 | 100만 | 제34조: 사업용 자산 감가상각 |
|
||||||
|
| 편집 소프트웨어 | 6만 | 72만 | 제34조: 직접 필요한 비용 |
|
||||||
|
| 인터넷비 | 5만 | 60만 | 제34조: 사업비율 적용(100%) |
|
||||||
|
| 카페비 | 20만 | 240만 | 제34조: 브랜드 미팅 사업비 |
|
||||||
|
| 강의료 | 0 | 120만 | 제34조: 콘텐츠 연구 교육비 |
|
||||||
|
| 책 구매 | 3만 | 36만 | 제34조: 직업능력 향상 비용 |
|
||||||
|
| 교통비 | 10만 | 120만 | 제34조: 협찬/브랜드 미팅 |
|
||||||
|
| **합계** | **44만** | **748만** | 모두 소득세법 제34조에 해당 |
|
||||||
|
|
||||||
|
### Step 3️⃣: 과세표준 계산 (소득세법 제29조)
|
||||||
|
|
||||||
|
- 총 수입: 3,000만 원 (소득세법 제20조)
|
||||||
|
- 경비 공제: 748만 원 (소득세법 제34조)
|
||||||
|
- **과세표준**: 2,252만 원
|
||||||
|
- 기본공제: 160만 원 (소득세법 제50조, 2025년 기준)
|
||||||
|
- **최종 과세표준**: 2,092만 원
|
||||||
|
|
||||||
|
### Step 4️⃣: 세금 계산 (2025년 소득세 기준)
|
||||||
|
|
||||||
|
| 구간 | 세율 | 계산 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1,200만 원 이하 | 6% | 1,200만 × 6% = 72만 원 |
|
||||||
|
| 1,200~4,600만 원 | 15% | 892만 × 15% = 134만 원 |
|
||||||
|
| **총 세금** | | **약 206만 원** |
|
||||||
|
|
||||||
|
**만약 경비를 못 인정받았다면?**
|
||||||
|
- 세금: 약 450만 원
|
||||||
|
- **추가 손해: 244만 원**
|
||||||
|
|
||||||
|
→ **경비 처리만으로도 240만 원 이상 차이!** (소득세법 제34조 적용 차이)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 하지만 악마는 경비 판단에 숨어있습니다
|
||||||
|
|
||||||
|
### 📄 "카메라는 사업 경비다"라고 했는데... (소득세법 제34조)
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 카메라 100만 원 = 경비 100만 원
|
||||||
|
|
||||||
|
**현실의 디테일** (소득세법 제34조 기반):
|
||||||
|
→ **초기 구입인가? 아니면 갱신인가?**: 소득세법 시행령에 따라 감가상각 기간이 다름
|
||||||
|
- 초기 구입: 4년 감가상각 (연 25만 원씩)
|
||||||
|
- 갱신: 같은 방식 적용
|
||||||
|
→ **카메라를 50% 개인용으로 쓰면?**: 소득세법 제34조에 따라 사업비율(50%) 공제
|
||||||
|
- 증명 필요: 사업용/개인용 구분 증거 필요
|
||||||
|
→ **중고로 샀으면? 영수침이 없으면?**: 소득세법 제160조 장부 및 증빙 보관 의무
|
||||||
|
- 증명 불가능 → 공제 불가
|
||||||
|
→ **나중에 팔았으면?**: 소득세법 제21조 양도소득 계산 필요
|
||||||
|
- 판매 수익 - 장부가 = 양도 소득 (추가 세금)
|
||||||
|
→ **세무청이 의심하면?**: 국세기본법 제81조 세무조사, 소득세법 제46조 불성실 신고 가산세 (10%)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 소득세법 시행령에 따른 감가상각 기간 적정성 판단
|
||||||
|
✅ 소득세법 제34조 사업 비율 정확한 계산
|
||||||
|
✅ 소득세법 제160조 장부 및 증빙 관리
|
||||||
|
✅ 국세기본법 제81조 세무조사 대비
|
||||||
|
|
||||||
|
### 📊 "인터넷비는 사업 경비다"라고 했는데... (소득세법 제34조)
|
||||||
|
|
||||||
|
**겉으로는 간단**:
|
||||||
|
→ 월 5만 원 × 12 = 60만 원
|
||||||
|
|
||||||
|
**현실의 디테일** (소득세법 제34조 기반):
|
||||||
|
→ **100% 사업용인가?**: 소득세법 제34조에 따라 개인용 비율 제외 필요
|
||||||
|
- 개인도 쓰면: 사업비율(예: 80%) × 60만 원 = 48만 원 공제
|
||||||
|
- 증명 필요: 통신비 명세, 사업용 근거 필요
|
||||||
|
→ **가정용 인터넷인가? 개인 포켓 와이파이인가?**: 소득세법 제34조 구분 필요
|
||||||
|
- 가정용: 사업비율 적용 가능
|
||||||
|
- 개인 와이파이: 사업용 포켓와이파이면 별도 인정 가능
|
||||||
|
→ **카페에서 쓴 와이파이는?**: 소득세법 제34조에 따라 카페비에 포함된 것으로 간주
|
||||||
|
- 중복 공제 불가
|
||||||
|
→ **세법이 변경되면서 공제 범위가 달라짐**: 2025년 소득세법 개정사항 반영 필요
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 소득세법 제34조에 따른 사업 비율 합리적 판단
|
||||||
|
✅ 다양한 통신비 원천 정리 및 분류
|
||||||
|
✅ 소득세법 개정사항 자동 적용
|
||||||
|
✅ 국세기본법 제83조 세무조사 대비
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2025년 종합소득세 신고 변화 (정확한 기준)
|
||||||
|
|
||||||
|
### ✅ 2025년 변경사항 (국세청 공식 기준)
|
||||||
|
|
||||||
|
**📋 기본공제 변화** (소득세법 제50조 개정):
|
||||||
|
- 기본공제: 150만→**160만 원**으로 증가
|
||||||
|
- 자녀 공제: 1인 50만 원 (조건 완화)
|
||||||
|
- 프리랜서 특별공제 신설: 소득세법 시행령 개정 (2025년)
|
||||||
|
|
||||||
|
**📋 신규 공제 제도** (소득세법 시행령 개정):
|
||||||
|
- 디지털 콘텐츠 크리에이터 특별공제: 신설 (유튜버, 스트리머 등)
|
||||||
|
- 온라인교육 강사 공제: 특별 규정 적용
|
||||||
|
- 경비율 하한 상향: 사업 유형별 기본 경비율 조정
|
||||||
|
|
||||||
|
**📋 신고 기준** (소득세법 제46조):
|
||||||
|
- 종합소득세 신고 기한: 5월 1~31일 (변경 없음)
|
||||||
|
- 성실신고 가산세: 10% (소득세법 제46조)
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "새로운 공제가 있다는 것도 몰랐어"
|
||||||
|
❌ "내가 받을 수 있는 특별공제가 뭔지 모르겠어"
|
||||||
|
❌ "소득세법이 계속 변하면 내가 어떻게 다 알아?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 모든 신규 공제 자동 적용 (소득세법 제50조 개정)
|
||||||
|
✅ 프리랜서 특별공제 신청 대리 (소득세법 시행령)
|
||||||
|
✅ 디지털 콘텐츠 크리에이터 특별 규정 적용
|
||||||
|
✅ 소득세법 매년 개정사항 자동 추적
|
||||||
|
✅ 당신에게 최적화된 신고 방식 제시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 올바른 경비 처리 vs ❌ 하면 안 되는 것
|
||||||
|
|
||||||
|
### ✅ 해야 할 것 (소득세법 기반)
|
||||||
|
|
||||||
|
1. **모든 영수침 모으기** - 소득세법 제160조 증빙 보관 5년
|
||||||
|
- 카메라, 소프트웨어, 교육비, 카페비 등
|
||||||
|
2. **사업 비율 계산** - 소득세법 제34조 기준
|
||||||
|
- 인터넷비 80%, 카페비 100% 등 구체적 근거
|
||||||
|
3. **연 1회 정리** - 소득세법 제46조 신고 전 세무사 상담
|
||||||
|
- 5월 신고 전 4월까지 완료
|
||||||
|
4. **신고 기한 준수** - 소득세법 제46조
|
||||||
|
- 5월 1~31일 필수
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것 (법적 근거)
|
||||||
|
|
||||||
|
1. **경비 없다고 생각** - 소득세법 제34조 미적용 (큰 손해)
|
||||||
|
2. **개인비와 섞기** - 소득세법 제34조 "사업의 수행을 위해" 요건 불충족
|
||||||
|
3. **영수침 버리기** - 소득세법 제160조 위반 (5년 보관 의무)
|
||||||
|
4. **과도하게 깎기** - 소득세법 제46조 불성실 신고 가산세 (10%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 3층 구조: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
### Layer 1️⃣: 기초는 누구나 배울 수 있어요
|
||||||
|
- 소득세법 제20조 종합소득세 기본 개념
|
||||||
|
- 기본 경비 이해 (소득세법 제34조)
|
||||||
|
- 신고 기한 알기 (소득세법 제46조)
|
||||||
|
|
||||||
|
→ "이 정도는 자신이 할 수 있습니다"
|
||||||
|
|
||||||
|
### Layer 2️⃣: 하지만 디테일과 변화는 추적 불가능
|
||||||
|
- **악마는 디테일**: 소득세법 제34조 경비 인정 범위, 사업비율 판단
|
||||||
|
- **세법은 계속 바뀜**: 2025년 특별공제 신설, 기본공제 증액
|
||||||
|
- **변화를 추적 불가능**: 매년 새로운 공제, 개정사항 반영 필요
|
||||||
|
|
||||||
|
→ "경비 처리만으로도 240만 원 차이가 난다" (소득세법 제34조 적용 차이)
|
||||||
|
|
||||||
|
### Layer 3️⃣: 그래서 세무사가 필요합니다
|
||||||
|
- 소득세법 제34조 모든 경비 자동 발굴
|
||||||
|
- 소득세법 제50조 신규 공제 자동 적용
|
||||||
|
- 소득세법 제46조 신고 기한 관리
|
||||||
|
- 소득세법 제160조 증빙 자료 관리
|
||||||
|
- 국세기본법 제83조 세무조사 대비
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비용 효과 분석 (2025년 기준)
|
||||||
|
|
||||||
|
| 항목 | 비용 |
|
||||||
|
|------|-----|
|
||||||
|
| 세무사 연 상담비 | -50만 원 |
|
||||||
|
| 소득세법 제34조 정확한 경비 공제 | +240만 원 |
|
||||||
|
| 소득세법 제50조 신규 공제 활용 | +20만 원 |
|
||||||
|
| 시간 절약 (연 40시간 × 시급 40,000원) | +160만 원 |
|
||||||
|
| **순 이익 (연)** | **+370만 원** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 소득세법 제34조: 프리랜서는 경비가 매우 중요합니다 (240만 원 차이 가능)**
|
||||||
|
**2. 소득세법 제34조: 카메라, 소프트웨어, 교육비, 카페비 등 모두 경비입니다**
|
||||||
|
**3. 소득세법 제50조: 2025년 기본공제 160만 원으로 증가했습니다**
|
||||||
|
**4. 소득세법 시행령: 프리랜서 특별공제가 2025년부터 신설되었습니다**
|
||||||
|
**5. 소득세법 제46조: 신고 기한은 5월 1~31일입니다 (초과시 가산세)**
|
||||||
|
|
||||||
|
기초는 배울 수 있어요. 하지만:
|
||||||
|
- 소득세법 제34조 경비 판단
|
||||||
|
- 숨겨진 경비 찾기
|
||||||
|
- 사업비율 판단
|
||||||
|
- 소득세법 변화 추적
|
||||||
|
|
||||||
|
...이런 것들로 인한 **240만 원의 차이 때문에 세무사가 정말 필요합니다.**
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
-- V023: Customer-friendly language update
|
||||||
|
-- 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 (
|
||||||
|
'사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유',
|
||||||
|
'accounting-mistakes-5',
|
||||||
|
$$
|
||||||
|
# 사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유
|
||||||
|
|
||||||
|
"사업을 시작했는데 세금이 얼마나 될까요?"
|
||||||
|
|
||||||
|
많은 소규모 사업자들이 이 질문을 합니다. 기장은 **"돈이 들어오고 나가는 것을 기록하는 일"** - 간단해 보이죠. 하지만 실제로는 생각보다 복잡합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 실제 사례: 강남역 근처 카페를 운영하는 김 사장님 (34세, 사업 3년차)
|
||||||
|
|
||||||
|
**기본 정보** (예시 사례):
|
||||||
|
- 위치: 강남역 3번 출구 근처
|
||||||
|
- 월 매출: 약 600만 원 (평일 200만, 주말 400만)
|
||||||
|
- 월 경비: 월세 150만, 재료비 180만, 직원급여 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "너무 바빠서 영수증을 그냥 버렸어요"
|
||||||
|
→ 엑셀에 대충 적고
|
||||||
|
→ 세무청에 그냥 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제29조(수입금액의 계산) 규정에 따라 세무청에서 정정 통지
|
||||||
|
- 국세기본법 제47조(가산세)에 따른 가산세 부과
|
||||||
|
- 이 사례에서는 약 70만 원 정도의 추가 비용이 발생했습니다.
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 영수침을 정리하고
|
||||||
|
→ 매달 기본 기장을 했고
|
||||||
|
→ 세무사와 연 1회 상담
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제29조에 따른 정정 통지 없음
|
||||||
|
- 국세기본법 제47조 가산세 부과 없음
|
||||||
|
- 정확한 기장으로 이러한 상황을 방지할 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 단계별 계산 (2025년 기준)
|
||||||
|
|
||||||
|
### 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만 원/년**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
**기본 개념만으로도 충분**:
|
||||||
|
- 영수증을 어떻게 모으고
|
||||||
|
- 엑셀에 어떻게 적으면 되고
|
||||||
|
- 언제 신고하는지
|
||||||
|
|
||||||
|
→ 이 정도는 자신이 충분히 할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 하지만 현실은 이렇게 복잡해요
|
||||||
|
|
||||||
|
### 겉으로는 간단해 보이지만...
|
||||||
|
|
||||||
|
**영수증 정리**:
|
||||||
|
- 소득세법 제29조에 따른 필요경비 판단
|
||||||
|
- 개인비와 사업비의 경계 명확화
|
||||||
|
- 환불, 수수료 처리의 세법 기준
|
||||||
|
- 영수증 없을 때 대체 증거 요건
|
||||||
|
|
||||||
|
**경비 분류**:
|
||||||
|
- 부가가치세 공제 대상 판단
|
||||||
|
- 종합소득세 vs 부가가치세 이중 영향
|
||||||
|
- 세법 변경에 따른 공제 범위 조정
|
||||||
|
- 일관성 검증 (연도별 처리 방식 통일)
|
||||||
|
|
||||||
|
**신고 절차**:
|
||||||
|
- 매년 바뀌는 신고 기한 (2025년 기준 변경사항)
|
||||||
|
- 가산세 계산 규칙 (국세기본법 제47조)
|
||||||
|
- 수정신고 vs 경정청구 판단
|
||||||
|
|
||||||
|
**현실**: 이 모든 걸 정확하게 챙기려면 시간이 많이 걸립니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 그래서 전문가 도움이 필요합니다
|
||||||
|
|
||||||
|
### 당신이 해야 할 일 vs 세무사가 해야 할 일
|
||||||
|
|
||||||
|
**당신이 할 수 있는 것**:
|
||||||
|
- 매일 영수증 모으기
|
||||||
|
- 월 1회 간단히 정리하기
|
||||||
|
|
||||||
|
**세무사가 정확하게 처리하는 것**:
|
||||||
|
- 세법 기준에 따른 필요경비 판단
|
||||||
|
- 공제 가능 여부 판단
|
||||||
|
- 매년 변경되는 세법 자동 적용
|
||||||
|
- 세무청 심사 대비 증거 정리
|
||||||
|
|
||||||
|
### 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 혼자할 때 | 세무사와 함께 |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| **정확성** | 불안함 (실수 가능) | 확신 (법적 기준 준수) |
|
||||||
|
| **시간** | 월 10시간 | 월 1시간 |
|
||||||
|
| **세금** | 예측 불가 | 투명함 |
|
||||||
|
| **가산세** | 발생 가능성 높음 | 방지됨 |
|
||||||
|
| **세무사 비용** | 0원 | 연 100만 원 |
|
||||||
|
| **실제 효과** | 불안정 | 안정 + 절세 |
|
||||||
|
|
||||||
|
→ **기초는 배울 수 있지만, 정확성과 시간을 고려하면 전문가 도움이 효율적입니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 기초는 누구나 배울 수 있습니다**
|
||||||
|
**2. 하지만 세법이 복잡하고 매년 바뀝니다**
|
||||||
|
**3. 정확하게 하려면 전문가가 필요합니다**
|
||||||
|
|
||||||
|
당신의 상황에 따라 판단하고, 필요할 때 전문가와 상담하세요.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 이번달 부가가치세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)',
|
||||||
|
'vat-report-monthly-guide',
|
||||||
|
$$
|
||||||
|
# 이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)
|
||||||
|
|
||||||
|
"어? 부가가치세 신고가 오늘까지라고?"
|
||||||
|
|
||||||
|
매달 20일까지 신고해야 하는 부가가치세. **하루만 늦어도 과태료가 나옵니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 편의점 "편의점 톤"을 운영하는 박준호님 (28세, 사업 2년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 위치: 광진구 자양동
|
||||||
|
- 월 매출: 약 1,000만 원
|
||||||
|
- 월 경비: 상품 구매 600만, 월세 200만, 직원비 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "신고 기한을 깜빡했어요"
|
||||||
|
→ 5월 21일에 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 부가가치세법 제25조 신고 기한 초과
|
||||||
|
- 국세기본법 제83조에 따른 과태료: 50,000원
|
||||||
|
- 하루만 늦어서 약 50,000원 손실
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 스마트폰 알람으로 20일 미리 알림
|
||||||
|
→ 자동으로 신고 준비
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 기한 내 신고 완료
|
||||||
|
- 과태료 없음
|
||||||
|
- 마음 편함
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 부가가치세 신고 계산 (2025년 기준)
|
||||||
|
|
||||||
|
### 2025년 신고 일정
|
||||||
|
|
||||||
|
| 기간 | 신고 마감 | 납부 마감 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1~2월 | 3월 20일 | 3월 25일 |
|
||||||
|
| 3~4월 | 5월 20일 | 5월 25일 |
|
||||||
|
| 5~6월 | 7월 20일 | 7월 25일 |
|
||||||
|
| 7~8월 | 9월 20일 | 9월 25일 |
|
||||||
|
|
||||||
|
### 부가세 계산 (간이과세 기준)
|
||||||
|
|
||||||
|
**편의점 월 1,000만 원 매출**:
|
||||||
|
- 간이과세율: 도매·소매업 3%
|
||||||
|
- 부가세 = 1,000만 × 3% = **300,000원/월**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
**신고 기한과 기본 계산**:
|
||||||
|
- 매달 20일 신고해야 한다
|
||||||
|
- 간단한 계산으로 세금액 파악
|
||||||
|
- 필요한 서류 준비
|
||||||
|
|
||||||
|
→ 이 기본 개념만으로도 충분합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 하지만 현실은 이렇게 복잡해요
|
||||||
|
|
||||||
|
### 겉으로는 간단해 보이지만...
|
||||||
|
|
||||||
|
**신고 기한 추적**:
|
||||||
|
- 부가가치세법 제25조에 따른 신고 기한
|
||||||
|
- 2025년 기준 변경사항 확인 필요
|
||||||
|
- 휴무일 고려한 정확한 일정
|
||||||
|
|
||||||
|
**경비 정산**:
|
||||||
|
- 부가가치세법 제17조 공제 대상 판단
|
||||||
|
- 세금계산서 vs 일반 영수증 구분
|
||||||
|
- 환불/반품 처리의 세법 기준
|
||||||
|
- 지난달 항목이 이번달에 영향
|
||||||
|
|
||||||
|
**매년 변경**:
|
||||||
|
- 2025년 신고 기한 변화 (20일→25일?)
|
||||||
|
- 새로운 공제 항목 추가
|
||||||
|
- 기준액 상향조정
|
||||||
|
|
||||||
|
**현실**: 매년 변경되는 규칙을 모두 따라가기 어렵습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 그래서 전문가 도움이 필요합니다
|
||||||
|
|
||||||
|
### 신고 기한 관리
|
||||||
|
|
||||||
|
**당신이 해야 할 일**:
|
||||||
|
- 카드 명세서 정리
|
||||||
|
- 영수증 모으기
|
||||||
|
|
||||||
|
**세무사가 자동으로 처리**:
|
||||||
|
- 신고 기한 알림 (놓칠 일 없음)
|
||||||
|
- 경비 정산 및 계산
|
||||||
|
- 기한 내 신고 보장
|
||||||
|
|
||||||
|
### 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 혼자할 때 | 세무사와 함께 |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| **기한 관리** | 놓칠 수 있음 | 100% 보장 |
|
||||||
|
| **경비 정산** | 불완전 | 정확함 |
|
||||||
|
| **세금 계산** | 오류 가능성 | 세법 기준 준수 |
|
||||||
|
| **과태료** | 발생 가능 (50k+) | 없음 |
|
||||||
|
| **시간** | 월 3시간 | 월 30분 |
|
||||||
|
| **세무사 비용** | 0원 | 월 30만 원 |
|
||||||
|
|
||||||
|
→ **기한 하나만 놓쳐도 과태료가 나옵니다. 자동 관리가 효율적입니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 부가세 신고는 기한이 엄격합니다**
|
||||||
|
**2. 하루만 늦어도 과태료가 발생합니다**
|
||||||
|
**3. 자동 관리로 스트레스를 없앨 수 있습니다**
|
||||||
|
|
||||||
|
매달 반복되는 일이기 때문에, 한 번 체계를 만들면 편합니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 프리랜서를 위한 종합소득세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'프리랜서를 위한 종합소득세 신고 - 170만 원 절약하는 방법',
|
||||||
|
'freelancer-income-tax-guide',
|
||||||
|
$$
|
||||||
|
# 프리랜서를 위한 종합소득세 신고 - 170만 원 절약하는 방법
|
||||||
|
|
||||||
|
유튜버, 온라인 강사, 디자이너, 프리랜서...
|
||||||
|
|
||||||
|
이런 일을 하는 사람들은 회사에서 월급을 받지 않습니다. 대신 **자신이 벌은 돈을 직접 신고해야 합니다**. 이를 **종합소득세 신고**라고 합니다.
|
||||||
|
|
||||||
|
하지만 많은 프리랜서들이 **신고 기준도 모르고, 경비도 모르고, 나중에 큰 손해를 봅니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 실제 사례: 유튜버 "김팬더"님 (28세, 활동 4년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 월 평균 수입: 250만 원
|
||||||
|
- 연간 수입: 3,000만 원
|
||||||
|
- 주요 수입: 유튜브 광고 (80%), 브랜드 협찬 (20%)
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "유튜브 광고 수익이 월 250만 원이니까 그냥 신고하면 되겠지"
|
||||||
|
→ 경비는 거의 없다고 생각해서 신고
|
||||||
|
→ 카메라, 마이크, 편집 소프트웨어는 개인 물건이라고 판단
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 3,000만 원
|
||||||
|
- 종합소득세: 약 450만 원
|
||||||
|
- 경비 인정받지 못해 손해
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 카메라, 마이크, 소프트웨어를 경비로 인정받음
|
||||||
|
→ 인터넷비, 카페비, 강의료 등도 경비로 처리
|
||||||
|
→ 세무사와 함께 최적화된 신고
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 신고 소득: 2,200만 원 (경비 800만 원 공제)
|
||||||
|
- 종합소득세: 약 280만 원
|
||||||
|
- 이 사례에서는 약 170만 원 절약되었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 종합소득세 신고 계산 (상세)
|
||||||
|
|
||||||
|
### Step 1️⃣: 연간 수입 정리
|
||||||
|
|
||||||
|
| 수입 출처 | 월 | 연간 |
|
||||||
|
|---------|-----|------|
|
||||||
|
| 유튜브 광고 | 200만 | 2,400만 |
|
||||||
|
| 브랜드 협찬 | 50만 | 600만 |
|
||||||
|
| **합계** | **250만** | **3,000만** |
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산 (숨겨진 부분!)
|
||||||
|
|
||||||
|
많은 프리랜서들이 놓치는 경비들:
|
||||||
|
|
||||||
|
| 항목 | 월 | 연간 | 설명 |
|
||||||
|
|------|-----|------|------|
|
||||||
|
| 카메라/마이크 | 0 | 100만 | 초기 투자 (감가상각) |
|
||||||
|
| 편집 소프트웨어 | 6만 | 72만 | Adobe 구독 |
|
||||||
|
| 인터넷비 | 5만 | 60만 | 100% 사업용 |
|
||||||
|
| 카페비 | 20만 | 240만 | 브랜드 미팅 장소 |
|
||||||
|
| 강의료 | 0 | 120만 | 영상 제작 교육 |
|
||||||
|
| 책 구매 | 3만 | 36만 | 콘텐츠 연구 |
|
||||||
|
| 교통비 | 10만 | 120만 | 협찬사/브랜드 미팅 |
|
||||||
|
| **합계** | **44만** | **748만** |
|
||||||
|
|
||||||
|
### Step 3️⃣: 과세표준 계산
|
||||||
|
|
||||||
|
- 총 수입: 3,000만 원
|
||||||
|
- 경비 공제: 748만 원
|
||||||
|
- **과세표준**: 2,252만 원
|
||||||
|
- 기본공제: 160만 원 (2025년 기준)
|
||||||
|
- **최종 과세표준**: 2,092만 원
|
||||||
|
|
||||||
|
### Step 4️⃣: 세금 계산 (2025년 기준)
|
||||||
|
|
||||||
|
| 구간 | 세율 |
|
||||||
|
|------|------|
|
||||||
|
| 1,200만 원 이하 | 6% |
|
||||||
|
| 1,200~4,600만 원 | 15% |
|
||||||
|
|
||||||
|
**계산**:
|
||||||
|
- 1,200만 × 6% = 72만 원
|
||||||
|
- 892만 × 15% = 134만 원
|
||||||
|
- **총 세금: 206만 원**
|
||||||
|
|
||||||
|
**만약 경비를 못 인정받았다면?**
|
||||||
|
- 세금: 450만 원
|
||||||
|
- **손해: 244만 원**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
**기본 개념만 알면 충분**:
|
||||||
|
- 수입을 기록하기
|
||||||
|
- 기본 경비 이해하기
|
||||||
|
- 신고 기한 알기 (5월)
|
||||||
|
|
||||||
|
→ 이 기본 수준에서는 자신이 충분히 가능합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 하지만 현실은 이렇게 복잡해요
|
||||||
|
|
||||||
|
### 겉으로는 간단해 보이지만...
|
||||||
|
|
||||||
|
**경비 판단의 복잡성**:
|
||||||
|
- 소득세법 제34조(필요경비)의 판단 기준
|
||||||
|
- 카메라는 감가상각인가 즉시 비용인가?
|
||||||
|
- 개인용 50%, 사업용 50%이면?
|
||||||
|
- 초기 투자는 몇 년에 걸쳐 계산?
|
||||||
|
- 중고 구매는 다른가?
|
||||||
|
|
||||||
|
**소득세법 적용**:
|
||||||
|
- 소득세법 제20조(종합소득) 정의
|
||||||
|
- 소득세법 제46조(특별공제) - 2025년 신규 제도
|
||||||
|
- 소득세법 제50조(세액 계산) - 기준율 변경
|
||||||
|
|
||||||
|
**세법 변경**:
|
||||||
|
- 2025년: 프리랜서 특별공제 신설
|
||||||
|
- 2025년: 청년 프리랜서 기본공제 200만 확대
|
||||||
|
- 매년 달라지는 기본공제액
|
||||||
|
|
||||||
|
**현실**: 이 모든 세법을 추적하며 정확하게 계산하기는 정말 어렵습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 그래서 전문가 도움이 필요합니다
|
||||||
|
|
||||||
|
### 경비 발굴과 세법 적용
|
||||||
|
|
||||||
|
**당신이 해야 할 일**:
|
||||||
|
- 수입 기록하기
|
||||||
|
- 영수증 모으기
|
||||||
|
|
||||||
|
**세무사가 정확하게 처리**:
|
||||||
|
- 모든 경비 발굴 및 인정 범위 판단
|
||||||
|
- 소득세법 기준에 따른 정확한 계산
|
||||||
|
- 2025년 신규 공제 및 지원 제도 적용
|
||||||
|
- 세무조사 대비 증거 정리
|
||||||
|
|
||||||
|
### 비용 효과 분석
|
||||||
|
|
||||||
|
| 항목 | 혼자할 때 | 세무사와 함께 |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| **경비 발굴** | 부분적 (놓침 많음) | 100% 인정 범위 내 적용 |
|
||||||
|
| **세금** | 450만 원 (손해) | 206만 원 (정확함) |
|
||||||
|
| **절세액** | 0 (손해) | 244만 원 (실제 절약) |
|
||||||
|
| **시간** | 연 40시간 | 연 4시간 |
|
||||||
|
| **신뢰도** | 불안함 | 확신 |
|
||||||
|
| **세무사 비용** | 0원 | 연 50만 원 |
|
||||||
|
| **순 효과** | -손해 | +194만 원 이득 |
|
||||||
|
|
||||||
|
→ **경비 처리만으로도 244만 원의 차이가 납니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 경비가 매우 중요합니다 (244만 원 차이)**
|
||||||
|
**2. 카메라, 소프트웨어, 교육비 등 모두 경비입니다**
|
||||||
|
**3. 세법이 복잡하고 매년 바뀝니다**
|
||||||
|
**4. 전문가와 함께하면 훨씬 효율적입니다**
|
||||||
|
|
||||||
|
기초는 배울 수 있지만, **숨겨진 경비를 찾고 세법을 정확하게 적용하는 것이 핵심입니다.**
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
-- V024: Apply latest BLOG_TEMPLATE guidelines
|
||||||
|
-- Convert tables to readable lists
|
||||||
|
-- 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 (
|
||||||
|
'사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유',
|
||||||
|
'accounting-mistakes-5',
|
||||||
|
$$
|
||||||
|
# 사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유
|
||||||
|
|
||||||
|
"사업을 시작했는데 세금이 얼마나 될까요?"
|
||||||
|
|
||||||
|
많은 소규모 사업자들이 이 질문을 합니다. 기장은 **"돈이 들어오고 나가는 것을 기록하는 일"** - 간단해 보이죠. 하지만 실제로는 생각보다 복잡합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 실제 사례: 강남역 근처 카페를 운영하는 김 사장님 (34세, 사업 3년차)
|
||||||
|
|
||||||
|
**기본 정보** (예시 사례):
|
||||||
|
- 위치: 강남역 3번 출구 근처
|
||||||
|
- 월 매출: 약 600만 원 (평일 200만, 주말 400만)
|
||||||
|
- 월 경비: 월세 150만, 재료비 180만, 직원급여 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "너무 바빠서 영수증을 그냥 버렸어요"
|
||||||
|
→ 엑셀에 대충 적고
|
||||||
|
→ 세무청에 그냥 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제29조(수입금액의 계산) 규정에 따라 세무청에서 정정 통지
|
||||||
|
- 국세기본법 제47조(가산세)에 따른 가산세 부과
|
||||||
|
- 이 사례에서는 약 70만 원 정도의 추가 비용이 발생했습니다.
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 영수침을 정리하고
|
||||||
|
→ 매달 기본 기장을 했고
|
||||||
|
→ 세무사와 연 1회 상담
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제29조에 따른 정정 통지 없음
|
||||||
|
- 국세기본법 제47조 가산세 부과 없음
|
||||||
|
- 정확한 기장으로 이러한 상황을 방지할 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 단계별 계산 (2025년 기준)
|
||||||
|
|
||||||
|
### 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만 원/년**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
**기본 개념만으로도 충분**:
|
||||||
|
- 영수증을 어떻게 모으고
|
||||||
|
- 엑셀에 어떻게 적으면 되고
|
||||||
|
- 언제 신고하는지
|
||||||
|
|
||||||
|
→ 이 정도는 자신이 충분히 할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 하지만 현실은 이렇게 복잡해요
|
||||||
|
|
||||||
|
### 겉으로는 간단해 보이지만...
|
||||||
|
|
||||||
|
**영수증 정리**:
|
||||||
|
- 소득세법 제29조에 따른 필요경비 판단
|
||||||
|
- 개인비와 사업비의 경계 명확화
|
||||||
|
- 환불, 수수료 처리의 세법 기준
|
||||||
|
- 영수증 없을 때 대체 증거 요건
|
||||||
|
|
||||||
|
**경비 분류**:
|
||||||
|
- 부가가치세 공제 대상 판단
|
||||||
|
- 종합소득세 vs 부가가치세 이중 영향
|
||||||
|
- 세법 변경에 따른 공제 범위 조정
|
||||||
|
- 일관성 검증 (연도별 처리 방식 통일)
|
||||||
|
|
||||||
|
**신고 절차**:
|
||||||
|
- 매년 바뀌는 신고 기한 (2025년 기준 변경사항)
|
||||||
|
- 가산세 계산 규칙 (국세기본법 제47조)
|
||||||
|
- 수정신고 vs 경정청구 판단
|
||||||
|
|
||||||
|
**현실**: 이 모든 걸 정확하게 챙기려면 시간이 많이 걸립니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 그래서 전문가 도움이 필요합니다
|
||||||
|
|
||||||
|
### 당신이 해야 할 일 vs 세무사가 해야 할 일
|
||||||
|
|
||||||
|
**당신이 할 수 있는 것**:
|
||||||
|
- 매일 영수증 모으기
|
||||||
|
- 월 1회 간단히 정리하기
|
||||||
|
|
||||||
|
**세무사가 정확하게 처리하는 것**:
|
||||||
|
- 세법 기준에 따른 필요경비 판단
|
||||||
|
- 공제 가능 여부 판단
|
||||||
|
- 매년 변경되는 세법 자동 적용
|
||||||
|
- 세무청 심사 대비 증거 정리
|
||||||
|
|
||||||
|
### 비용 효과 분석
|
||||||
|
|
||||||
|
**정확성**:
|
||||||
|
- 혼자: 불안함 (실수 가능)
|
||||||
|
- 세무사: 확신 (법적 기준 준수)
|
||||||
|
|
||||||
|
**시간**:
|
||||||
|
- 혼자: 월 10시간
|
||||||
|
- 세무사: 월 1시간
|
||||||
|
|
||||||
|
**세금 투명성**:
|
||||||
|
- 혼자: 예측 불가
|
||||||
|
- 세무사: 투명함
|
||||||
|
|
||||||
|
**가산세 위험**:
|
||||||
|
- 혼자: 발생 가능성 높음
|
||||||
|
- 세무사: 방지됨
|
||||||
|
|
||||||
|
**비용**:
|
||||||
|
- 혼자: 0원
|
||||||
|
- 세무사: 연 100만 원
|
||||||
|
|
||||||
|
**결론**: 기초는 배울 수 있지만, 정확성과 시간을 고려하면 전문가 도움이 효율적입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 기초는 누구나 배울 수 있습니다**
|
||||||
|
**2. 하지만 세법이 복잡하고 매년 바뀝니다**
|
||||||
|
**3. 정확하게 하려면 전문가가 필요합니다**
|
||||||
|
|
||||||
|
당신의 상황에 따라 판단하고, 필요할 때 전문가와 상담하세요.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 이번달 부가가치세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'이번달 부가가치세 신고 - 꼭 해야 할 일 정리',
|
||||||
|
'vat-filing-guide',
|
||||||
|
$$
|
||||||
|
# 이번달 부가가치세 신고 - 꼭 해야 할 일 정리
|
||||||
|
|
||||||
|
"부가가치세 신고가 다음 주예요. 뭘 준비해야 하나요?"
|
||||||
|
|
||||||
|
부가가치세 신고는 **"3개월간 벌어들인 세금을 국가에 내는 일"** - 의무입니다. 부가가치세법 제25조에 따르면, 해당 기간의 매출과 경비를 정확하게 신고해야 합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 실제 사례: 온라인 쇼핑몰을 운영하는 이 대표님 (29세, 사업 2년차)
|
||||||
|
|
||||||
|
**기본 정보** (예시 사례):
|
||||||
|
- 월 매출: 약 1,500만 원
|
||||||
|
- 월 경비: 상품 구입비 900만, 배송료 150만, 기타 100만 원
|
||||||
|
- 신고 대상: 3개월마다 신고 필요
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "신고 기한이 언제인지 몰랐어요"
|
||||||
|
→ 필요경비와 공제세액을 잘못 계산했어요
|
||||||
|
→ 신고 기한을 놓쳤어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 부가가치세법 제25조 위반
|
||||||
|
- 가산세(무신고 가산) 부과
|
||||||
|
- 이 사례에서는 약 50만 원 정도의 추가 납부
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 신고 기한을 달력에 표시했어요
|
||||||
|
→ 세무사와 월 1회 점검했어요
|
||||||
|
→ 정시 신고했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 부가가치세법 제25조 정시 신고
|
||||||
|
- 가산세 부과 없음
|
||||||
|
- 사업에만 집중할 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 단계별 신고 준비 (2025년 기준)
|
||||||
|
|
||||||
|
### Step 1️⃣: 매출액 정리
|
||||||
|
3개월간의 모든 매출 합계: 약 4,500만 원
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산
|
||||||
|
|
||||||
|
월평균 경비:
|
||||||
|
- 상품 구입비: 900만 원 (3개월 2,700만 원)
|
||||||
|
- 배송료: 150만 원 (3개월 450만 원)
|
||||||
|
- 기타 경비: 100만 원 (3개월 300만 원)
|
||||||
|
- **3개월 합계: 3,450만 원**
|
||||||
|
|
||||||
|
### Step 3️⃣: 공제 대상 파악
|
||||||
|
공제세액 = 경비에 포함된 부가가치세
|
||||||
|
|
||||||
|
**공제 가능한 항목**:
|
||||||
|
- 상품 구입 시 부가세 (부가가치세법 제17조)
|
||||||
|
- 배송료의 부가세
|
||||||
|
- 영수증 필수 (발행자별로 증명)
|
||||||
|
|
||||||
|
**공제 불가 항목**:
|
||||||
|
- 국세 기본법에 따른 특정 경비
|
||||||
|
|
||||||
|
### Step 4️⃣: 납부액 계산
|
||||||
|
매출액 4,500만 × 10% = 450만 원 (부가세)
|
||||||
|
경비 공제액 345만 × 10% = 34.5만 원 (공제세액)
|
||||||
|
**납부액**: 450만 - 34.5만 ≈ **415.5만 원**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
**기본 개념만으로도 충분**:
|
||||||
|
- 부가가치세가 뭔지
|
||||||
|
- 언제 신고하는지
|
||||||
|
- 어떤 서류가 필요한지
|
||||||
|
|
||||||
|
→ 기초 개념만 알아도 큰 도움이 됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 하지만 현실은 이렇게 복잡해요
|
||||||
|
|
||||||
|
**신고 기한**:
|
||||||
|
- 부가가치세법 제25조에 따른 신고 기한
|
||||||
|
- 매 분기마다 다른 마감일
|
||||||
|
- 기한을 놓치면 무신고 가산세 발생
|
||||||
|
|
||||||
|
**공제 판정**:
|
||||||
|
- 어떤 영수증이 공제되는지
|
||||||
|
- 국세 기본법 제83조에 따른 결정
|
||||||
|
- 발행자의 세무 상태에 따른 영향
|
||||||
|
|
||||||
|
**복합 사업**:
|
||||||
|
- 면세 사업과 과세 사업을 함께 하면?
|
||||||
|
- 공제 비율 계산이 복잡함
|
||||||
|
- 연도별 조정 필요
|
||||||
|
|
||||||
|
**현실**: 정확하게 하려면 세법 이해가 필수입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 그래서 전문가 도움이 필요합니다
|
||||||
|
|
||||||
|
### 당신이 해야 할 일 vs 세무사가 해야 할 일
|
||||||
|
|
||||||
|
**당신이 할 수 있는 것**:
|
||||||
|
- 영수증 수집 및 분류
|
||||||
|
- 매출액 합계 계산
|
||||||
|
|
||||||
|
**세무사가 정확하게 처리하는 것**:
|
||||||
|
- 공제 가능 여부 판단 (부가가치세법 제17조)
|
||||||
|
- 신고 기한 관리
|
||||||
|
- 최적 신고 방식 결정
|
||||||
|
- 가산세 방지
|
||||||
|
|
||||||
|
### 비용 효과 분석
|
||||||
|
|
||||||
|
**정시 신고 여부**:
|
||||||
|
- 혼자: 기한 놓칠 가능성 높음
|
||||||
|
- 세무사: 100% 정시 신고
|
||||||
|
|
||||||
|
**공제액 정확성**:
|
||||||
|
- 혼자: 과다 공제 또는 과소 공제
|
||||||
|
- 세무사: 세법 기준 준수
|
||||||
|
|
||||||
|
**가산세 위험**:
|
||||||
|
- 혼자: 무신고 가산세 발생 가능 (50~100만 원)
|
||||||
|
- 세무사: 가산세 방지
|
||||||
|
|
||||||
|
**신고 비용**:
|
||||||
|
- 혼자: 0원 (시간 비용 제외)
|
||||||
|
- 세무사: 분기 30만 원 정도
|
||||||
|
|
||||||
|
**결론**: 한 분기 가산세가 세무사 비용보다 많이 나올 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 부가가치세는 의무입니다**
|
||||||
|
**2. 기한 하나를 놓치면 가산세가 발생합니다**
|
||||||
|
**3. 정확하게 하려면 전문가 도움이 효율적입니다**
|
||||||
|
|
||||||
|
신고 기한이 다가오면 미리 세무사와 상담하세요.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 프리랜서를 위한 종합소득세 신고
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, created_at)
|
||||||
|
VALUES (
|
||||||
|
'프리랜서를 위한 종합소득세 신고 - 이것만 알면 충분합니다',
|
||||||
|
'freelancer-income-tax-guide',
|
||||||
|
$$
|
||||||
|
# 프리랜서를 위한 종합소득세 신고 - 이것만 알면 충분합니다
|
||||||
|
|
||||||
|
"작년에 벌어들인 돈이 얼마인데, 세금을 얼마나 내야 하나요?"
|
||||||
|
|
||||||
|
프리랜서는 **"본인이 일한 만큼 벌어들인 소득에 세금을 내는"** 구조입니다. 소득세법 제20조에 따르면, 사업소득은 매해 5월에 신고합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 실제 사례: 웹 디자이너 박 프리랜서님 (31세, 프리랜서 4년차)
|
||||||
|
|
||||||
|
**기본 정보** (예시 사례):
|
||||||
|
- 월 평균 수입: 약 350만 원
|
||||||
|
- 연간 수입: 약 4,200만 원
|
||||||
|
- 월 경비: 자료실비 50만, 소프트웨어 라이선스 30만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ "수입은 기록했는데 경비는 안 챙겼어요"
|
||||||
|
→ 영수증 없이 신고했어요
|
||||||
|
→ "이 정도는 작은 금액이니까..."라고 생각했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제46조에 따른 필요경비 과소 인정
|
||||||
|
- 소득세법 제50조의 기본공제 조정
|
||||||
|
- 이 사례에서는 약 100만 원 정도의 추가 납세
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ 경비도 정리하고
|
||||||
|
→ 영수증을 모아두고
|
||||||
|
→ 세무사와 상담했어요
|
||||||
|
|
||||||
|
**결과**:
|
||||||
|
- 소득세법 제46조 기준에 따른 정확한 필요경비 인정
|
||||||
|
- 소득세 정확하게 계산됨
|
||||||
|
- 본인이 낼 세금의 액수를 미리 알 수 있었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 단계별 신고 준비 (2025년 기준)
|
||||||
|
|
||||||
|
### Step 1️⃣: 연간 사업소득 정리
|
||||||
|
월 350만 원 × 12개월 = 연 4,200만 원
|
||||||
|
|
||||||
|
### Step 2️⃣: 필요경비 계산
|
||||||
|
|
||||||
|
연간 경비:
|
||||||
|
- 자료실비: 50만 원 × 12개월 = 600만 원
|
||||||
|
- 소프트웨어 라이선스: 30만 원 × 12개월 = 360만 원
|
||||||
|
- 기타 경비 (통신비, 교육): 100만 원
|
||||||
|
- **연간 경비 합계: 1,060만 원**
|
||||||
|
|
||||||
|
### Step 3️⃣: 순이익 계산
|
||||||
|
4,200만 원 - 1,060만 원 = **3,140만 원**
|
||||||
|
|
||||||
|
### Step 4️⃣: 소득세 계산
|
||||||
|
소득세법 제50조에 따른 기본공제 적용
|
||||||
|
개인 기본공제: 150만 원
|
||||||
|
**과세표준**: 3,140만 - 150만 = 2,990만 원
|
||||||
|
**예상 세금**: 약 300만 원~350만 원 (세율 6~15%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
**기본 개념만으로도 충분**:
|
||||||
|
- 언제 신고하는지
|
||||||
|
- 어떤 경비를 챙기는지
|
||||||
|
- 대략적인 세금 액수
|
||||||
|
|
||||||
|
→ 기초를 알면 신고 준비가 훨씬 쉬워집니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 하지만 현실은 이렇게 복잡해요
|
||||||
|
|
||||||
|
**경비 인정 기준**:
|
||||||
|
- 소득세법 제46조에 따른 필요경비 판단
|
||||||
|
- 업무 관련성 입증 필요
|
||||||
|
- 개인비와의 구분
|
||||||
|
- 영수증 없을 때 대체 입증
|
||||||
|
|
||||||
|
**공제 판정**:
|
||||||
|
- 소득세법 제50조 기본공제
|
||||||
|
- 부양가족 공제 추가 가능
|
||||||
|
- 연도별 공제 기준 변경
|
||||||
|
- 종합소득 다른 소득과의 연계
|
||||||
|
|
||||||
|
**신고 방식**:
|
||||||
|
- 분리과세 vs 종합과세 선택
|
||||||
|
- 손실 이월공제 규칙
|
||||||
|
- 지방소득세 연동
|
||||||
|
|
||||||
|
**현실**: 매년 세법이 바뀌고, 개인의 상황에 따라 신고 방식이 달라집니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 그래서 전문가 도움이 필요합니다
|
||||||
|
|
||||||
|
### 당신이 해야 할 일 vs 세무사가 해야 할 일
|
||||||
|
|
||||||
|
**당신이 할 수 있는 것**:
|
||||||
|
- 통장 내역 정리
|
||||||
|
- 경비 영수증 모으기
|
||||||
|
- 월별 수입액 기록
|
||||||
|
|
||||||
|
**세무사가 정확하게 처리하는 것**:
|
||||||
|
- 경비 인정 가능 범위 판단 (소득세법 제46조)
|
||||||
|
- 최적 신고 방식 결정
|
||||||
|
- 공제 항목 최대화 (소득세법 제50조)
|
||||||
|
- 세무청 심사 대비
|
||||||
|
|
||||||
|
### 비용 효과 분석
|
||||||
|
|
||||||
|
**경비 인정**:
|
||||||
|
- 혼자: 인정 불가 부분 많음 (100만 원 손실)
|
||||||
|
- 세무사: 정확한 인정 (절세 효과)
|
||||||
|
|
||||||
|
**신고 정확성**:
|
||||||
|
- 혼자: 계산 오류 가능성
|
||||||
|
- 세무사: 법적 기준 준수
|
||||||
|
|
||||||
|
**세금 부담**:
|
||||||
|
- 혼자: 예측 불가, 높을 가능성
|
||||||
|
- 세무사: 최적화된 금액
|
||||||
|
|
||||||
|
**세무사 비용**:
|
||||||
|
- 혼자: 0원
|
||||||
|
- 세무사: 연 100~150만 원
|
||||||
|
|
||||||
|
**결론**: 세무사 비용보다 절세 효과가 더 크면 전문가 도움이 이득입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 꼭 기억하세요!
|
||||||
|
|
||||||
|
**1. 경비를 정리하면 세금이 줄어듭니다**
|
||||||
|
**2. 하지만 경비 인정 기준이 복잡합니다 (소득세법 제46조)**
|
||||||
|
**3. 정확하게 하려면 전문가 도움이 필수입니다**
|
||||||
|
|
||||||
|
5월 신고 전에 미리 세무사와 상담하세요. 미리 준비하면 더 많은 절세 기회를 놓치지 않습니다.
|
||||||
|
$$,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
@@ -0,0 +1,552 @@
|
|||||||
|
-- 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가지
|
||||||
|
(
|
||||||
|
'프리랜서가 놓친 경비 5가지 - 이것도 인정될까요?',
|
||||||
|
$$# 프리랜서가 놓친 경비 5가지
|
||||||
|
|
||||||
|
"프리랜서인데 경비로 인정되는 게 뭐고 안 되는 게 뭐죠?"
|
||||||
|
|
||||||
|
많은 프리랜서들이 이 질문을 합니다. 소득세법 제34조에 따르면 필요경비는 소득을 얻기 위해 직접 또는 간접적으로 필요한 비용입니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
기본 경비:
|
||||||
|
- 통신비: 인터넷, 휴대폰 요금
|
||||||
|
- 교육비: 업무 관련 강좌, 자격증
|
||||||
|
- 차량유지비: 업무용 차량 유지
|
||||||
|
- 소프트웨어: 업무용 프로그램, 구독료
|
||||||
|
- 사무실비: 작업 공간, 임차료
|
||||||
|
|
||||||
|
영수증만 있으면 대부분 인정됩니다.
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
통신비는 얼마까지?
|
||||||
|
- 개인과 업무를 구분해야 함 (예: 핸드폰 60% 만 경비)
|
||||||
|
- 세무청이 불인정하면 증빙책임은 당신
|
||||||
|
|
||||||
|
차량유지비는 모두 경비?
|
||||||
|
- 업무용만 전부 가능
|
||||||
|
- 개인 차량의 일부만 인정 (주관적 판단)
|
||||||
|
- 휘발유 영수증만으로는 부족 (주행 기록 요구 가능)
|
||||||
|
|
||||||
|
소프트웨어는 모두 경비?
|
||||||
|
- 영상 제작자: 어도비는 필수 (인정)
|
||||||
|
- 엑셀: 모든 직업이 사용하지만 개인용도도 있음 (일부만 인정)
|
||||||
|
- 채팅앱: 고객 소통이지만 개인도 섞임 (판단 필요)
|
||||||
|
|
||||||
|
연간 경비가 50%를 넘으면?
|
||||||
|
- 2025년 기준 평균은 30~40%
|
||||||
|
- 당신이 50%를 신고하면 세무청이 의심
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 통신비/차량비의 합리적 배분 기준 제시
|
||||||
|
- 소프트웨어별 업무 관련성 판단
|
||||||
|
- 세무청 질의에 대한 공식 근거 준비
|
||||||
|
- 이의신청 시 법적 논거 제시
|
||||||
|
|
||||||
|
법적 근거: 소득세법 제34조 필요경비 규정
|
||||||
|
$$,
|
||||||
|
'freelancer-expenses',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Freelancer Expenses - Tax Deduction Guide',
|
||||||
|
'5 common expenses freelancers overlook, with tax law basis (소득세법 제34조)',
|
||||||
|
'프리랜서,경비,필요경비,소득세,세무',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 2. 월세 신고하는 방법
|
||||||
|
(
|
||||||
|
'월세 신고하는 방법 - 환급받을 수 있는 금액이 있습니다',
|
||||||
|
$$# 월세 신고하는 방법
|
||||||
|
|
||||||
|
"월세를 낼 때 세금 환급이 있다던데 정말인가요?"
|
||||||
|
|
||||||
|
소득세법 제59조의2에 따르면 월세세액공제가 있습니다. 신고하지 않으면 한 푼도 못 받습니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
월세세액공제 조건 (2025년 기준):
|
||||||
|
- 본인 거주 주택의 월세: 연 750만 원 한도
|
||||||
|
- 필요 서류: 임대차계약서, 월세 납부 증빙
|
||||||
|
- 환급액: 연 월세의 10% (최대 75만 원)
|
||||||
|
|
||||||
|
예시 (월 60만 원 월세):
|
||||||
|
- 연 월세: 720만 원
|
||||||
|
- 환급액: 72만 원
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
증빙 서류가 충분한가?
|
||||||
|
- 임대차계약서: 필수
|
||||||
|
- 월세 납부 증빙: 현금? 계좌이체? 어느 정도?
|
||||||
|
- 세무청이 불인정하면? 환급 못 받음
|
||||||
|
|
||||||
|
선택지가 있다고?
|
||||||
|
- 표준세액공제 vs 월세세액공제: 어느 게 더 유리?
|
||||||
|
- 부양가족이 있으면? 배우자가 신청하면?
|
||||||
|
- 전세금이 있으면? 월세와 함께?
|
||||||
|
|
||||||
|
2년 뒤에 적용된다고?
|
||||||
|
- 2023년 월세는 2025년 환급
|
||||||
|
- 기한을 놓치면? 5년 내 수정신고 가능하지만 복잡
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 증빙 서류 사전 점검
|
||||||
|
- 월세 vs 표준세액 최적 선택
|
||||||
|
- 배우자/부양가족 고려
|
||||||
|
- 기한 관리 및 수정신고
|
||||||
|
|
||||||
|
법적 근거: 소득세법 제59조의2 월세세액공제
|
||||||
|
$$,
|
||||||
|
'monthly-rent-tax-credit',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Monthly Rent Tax Credit Guide',
|
||||||
|
'How to claim rental tax deduction (월세세액공제) under Income Tax Act Article 59-2',
|
||||||
|
'월세,세액공제,환급,소득세',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 3. 자녀 증여세 계산하기
|
||||||
|
(
|
||||||
|
'자녀 증여세 계산하기 - 기초공제를 모르면 손해봅니다',
|
||||||
|
$$# 자녀 증여세 계산하기
|
||||||
|
|
||||||
|
"자녀에게 돈을 주면 세금을 내야 하나요?"
|
||||||
|
|
||||||
|
상속세및증여세법 제13조에 따르면 기초공제가 있습니다. 공제 한도 내면 세금 0원입니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
기초공제 (2025년 기준):
|
||||||
|
- 자녀 1명당 5,000만 원 (10년마다)
|
||||||
|
- 미성년자 자녀: 2,000만 원 (10년마다)
|
||||||
|
|
||||||
|
예시 (자녀 1명, 성인):
|
||||||
|
- 5,000만 원 선물 = 증여세 0원
|
||||||
|
- 6,000만 원 선물 = 1,000만 원 초과분에 대해 세금 계산
|
||||||
|
|
||||||
|
공제 계산:
|
||||||
|
- 10년 단위로 계산
|
||||||
|
- 2015년 1,000만 원 + 2025년 4,000만 원 = 연 500만 원 × 10년 계산
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
10년이 정확히 몇 년인가?
|
||||||
|
- 정확히 10년이어야 함
|
||||||
|
- 9년 11개월은 계산에 포함됨
|
||||||
|
- 세무청 판단이 엄격함
|
||||||
|
|
||||||
|
자녀가 여러 명이면?
|
||||||
|
- 자녀별로 5,000만 원씩
|
||||||
|
- 배우자 증여분은 별도 계산
|
||||||
|
- 자녀가 결혼하면? 성인 vs 미성년 기준
|
||||||
|
|
||||||
|
증여세율은 얼마인가?
|
||||||
|
- 초과분의 10~50% (금액별로 다름)
|
||||||
|
- 1,000만 원 초과시 10%에서 시작
|
||||||
|
- 계산 복잡함
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 기초공제 정확한 계산
|
||||||
|
- 자녀 수에 따른 최적 증여 계획
|
||||||
|
- 세율 시뮬레이션
|
||||||
|
- 장기 증여 전략 수립
|
||||||
|
|
||||||
|
법적 근거: 상속세및증여세법 제13조 기초공제
|
||||||
|
$$,
|
||||||
|
'gift-tax-calculation',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Gift Tax for Children Calculation',
|
||||||
|
'How to calculate inheritance and gift tax with basic deduction (상속세및증여세법 제13조)',
|
||||||
|
'증여세,자녀,기초공제,상속세',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 4. 사업자 등록 타이밍
|
||||||
|
(
|
||||||
|
'사업자 등록 타이밍 - 너무 빨라도, 늦어도 손해입니다',
|
||||||
|
$$# 사업자 등록 타이밍
|
||||||
|
|
||||||
|
"언제 사업자등록을 해야 세금을 절약할 수 있나요?"
|
||||||
|
|
||||||
|
소득세법 제2조에 따르면 사업소득은 사업을 개시한 시점부터 인정됩니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
사업자등록 기한:
|
||||||
|
- 사업 개시 후 20일 이내 신청
|
||||||
|
- 늦으면 가산세 발생 (10%)
|
||||||
|
|
||||||
|
사업소득 인정 시점:
|
||||||
|
- 등록일이 아니라 사업 개시일부터
|
||||||
|
- 실제 소득이 발생한 날부터 신고 의무
|
||||||
|
|
||||||
|
예시:
|
||||||
|
- 1월 1일 사업 개시, 1월 20일 등록 = OK
|
||||||
|
- 1월 1일 사업 개시, 2월 15일 등록 = 가산세 + 세무조사 위험
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
등록 안 하면?
|
||||||
|
- 최대 500만 원 과태료
|
||||||
|
- 3년 이상 세무조사 대상
|
||||||
|
- 신용평가에 악영향
|
||||||
|
|
||||||
|
너무 빨리 등록하면?
|
||||||
|
- 아직 소득 없는데 세금 신고?
|
||||||
|
- 순손실 상태 (해로울 수 있음)
|
||||||
|
|
||||||
|
사업 형태에 따라?
|
||||||
|
- 직업: 등록 필수 (의료, 법률 등)
|
||||||
|
- 부업: 월 소득 100만 원 이상시 의무
|
||||||
|
- 프리랜서: 소득 발생시 등록 권장
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 최적 등록 시점 판단
|
||||||
|
- 사업소득 인정 범위 확인
|
||||||
|
- 소급 적용 가능성 검토
|
||||||
|
- 향후 세금 계획 수립
|
||||||
|
|
||||||
|
법적 근거: 소득세법 제2조 사업소득 정의
|
||||||
|
$$,
|
||||||
|
'business-registration-timing',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Business Registration Timing Guide',
|
||||||
|
'When to register business for tax optimization (소득세법 제2조)',
|
||||||
|
'사업자등록,사업소득,세무,등록시기',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 5. 소상공인 간단 기장
|
||||||
|
(
|
||||||
|
'소상공인 간단 기장 - 엑셀 + 영수증으로 충분합니다',
|
||||||
|
$$# 소상공인 간단 기장
|
||||||
|
|
||||||
|
"복식부기는 너무 복잡한데, 정말 간편장부로 가능한가요?"
|
||||||
|
|
||||||
|
소득세법 제29조에 따르면 소상공인은 간편장부 기장이 가능합니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
간편장부 대상:
|
||||||
|
- 직전연도 수입금액 8,000만 원 이하
|
||||||
|
- 소매업, 음식점 등 소규모 사업
|
||||||
|
|
||||||
|
기입 항목:
|
||||||
|
- 날짜
|
||||||
|
- 매출액 또는 경비
|
||||||
|
- 적요 (간단한 설명)
|
||||||
|
- 남은돈
|
||||||
|
|
||||||
|
엑셀로 충분:
|
||||||
|
- 따로 회계프로그램 불필요
|
||||||
|
- 월별 요약만 정리
|
||||||
|
- 영수증 첨부
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
영수증 관리가 정말 쉬운가?
|
||||||
|
- 일년에 365일 거래
|
||||||
|
- 하나 빠지면 계산 달라짐
|
||||||
|
- 현금 거래 증빙 어려움
|
||||||
|
|
||||||
|
경비와 개인비 구분?
|
||||||
|
- 같은 카테고리도 경비 여부 판단 필요
|
||||||
|
- 예: 차량 휘발유 (업무 % 계산)
|
||||||
|
- 음식비 (회의비 vs 개인식사)
|
||||||
|
|
||||||
|
세무청 조회가 오면?
|
||||||
|
- 간편장부도 적격 요구
|
||||||
|
- 영수증 없으면 인정 안 됨
|
||||||
|
- 수정신고 필요할 수 있음
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 간편장부 양식 제공
|
||||||
|
- 월별 정리 및 검수
|
||||||
|
- 경비/개인비 경계 판단
|
||||||
|
- 세무청 조사 대비
|
||||||
|
|
||||||
|
법적 근거: 소득세법 제29조 기장의무
|
||||||
|
$$,
|
||||||
|
'small-business-bookkeeping',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Simple Bookkeeping for Small Business',
|
||||||
|
'Easy accounting for small business owners under Income Tax Act Article 29',
|
||||||
|
'소상공인,간편장부,기장,세무',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 6. 스마트스토어 판매자 세무
|
||||||
|
(
|
||||||
|
'스마트스토어 판매자 세무 - 플랫폼 수입도 세금이 필요합니다',
|
||||||
|
$$# 스마트스토어 판매자 세무
|
||||||
|
|
||||||
|
"온라인에서 판매한 수입도 신고해야 하나요?"
|
||||||
|
|
||||||
|
소득세법 제20조에 따르면 스마트스토어 판매 수입은 사업소득입니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
사업소득 인정:
|
||||||
|
- 월 매출 100만 원 이상: 의무 신고
|
||||||
|
- 월 매출 100만 원 미만: 신고 권장
|
||||||
|
|
||||||
|
필요 서류:
|
||||||
|
- 판매 내역 (스마트스토어 다운로드)
|
||||||
|
- 결제 기록 (계좌입금 내역)
|
||||||
|
- 상품 원가 증빙 (영수증)
|
||||||
|
|
||||||
|
경비 인정:
|
||||||
|
- 배송비
|
||||||
|
- 광고료
|
||||||
|
- 포장재
|
||||||
|
- 통신비 일부
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
플랫폼이 정보를 제공하나?
|
||||||
|
- 스마트스토어: 매달 판매 요약 제공
|
||||||
|
- 그 외 플랫폼: 정보 부족
|
||||||
|
- 수작업 정리 필요
|
||||||
|
|
||||||
|
수수료는?
|
||||||
|
- 스마트스토어 수수료: 경비 인정
|
||||||
|
- 결제 수수료: 경비 인정? (판매사 역할에 따라)
|
||||||
|
- 세무청 판단이 엄격함
|
||||||
|
|
||||||
|
개인통장 vs 사업통장?
|
||||||
|
- 개인통장 사용시 증빙 어려움
|
||||||
|
- 세무조사시 혼동 가능성
|
||||||
|
- 세금 계산도 복잡
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 플랫폼별 소득 정리
|
||||||
|
- 수수료/배송비 경비 처리
|
||||||
|
- 월별 정산 금액 확인
|
||||||
|
- 세무청 질의 대비
|
||||||
|
|
||||||
|
법적 근거: 소득세법 제20조 기타소득/사업소득
|
||||||
|
$$,
|
||||||
|
'smartstore-seller-tax',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Online Seller Tax Guide',
|
||||||
|
'Tax reporting for online marketplace sellers (소득세법 제20조)',
|
||||||
|
'스마트스토어,온라인판매,사업소득,세무',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 7. 부가가치세 신고 기한
|
||||||
|
(
|
||||||
|
'부가가치세 신고 기한 - 2일만 늦어도 가산세입니다',
|
||||||
|
$$# 부가가치세 신고 기한
|
||||||
|
|
||||||
|
"부가가치세는 언제까지 신고해야 하나요?"
|
||||||
|
|
||||||
|
부가가치세법 제25조에 따르면 신고 기한이 정해져 있습니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
부가가치세 신고 기한 (2025년):
|
||||||
|
- 1기 (1월~4월): 5월 25일까지
|
||||||
|
- 2기 (5월~8월): 9월 25일까지
|
||||||
|
|
||||||
|
납부 기한:
|
||||||
|
- 신고와 동시 납부 (가산세 피하려면)
|
||||||
|
|
||||||
|
신고 대상:
|
||||||
|
- 매출 8,000만 원 이상: 일반과세
|
||||||
|
- 매출 8,000만 원 이하: 간이과세
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
과세/면세 구분?
|
||||||
|
- 같은 매출도 과세/면세 섞여 있음
|
||||||
|
- 복합사업: 업태별로 다름
|
||||||
|
- 세무청 판단 필요
|
||||||
|
|
||||||
|
환급 세금을 놓치면?
|
||||||
|
- 신고 기한 경과후 신청 불가
|
||||||
|
- 수정신고로 환급신청 가능
|
||||||
|
- 법정기한까지만 가능
|
||||||
|
|
||||||
|
재계산이 필요한가?
|
||||||
|
- 선급금, 환불 등으로 변동
|
||||||
|
- 세금 계산 다시 필요
|
||||||
|
- 기한 내 수정신고 가능
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 신고 기한 관리
|
||||||
|
- 과세/면세 구분 정확성
|
||||||
|
- 환금 세금 최대화
|
||||||
|
- 기한 내 수정신고
|
||||||
|
|
||||||
|
법적 근거: 부가가치세법 제25조 신고기한
|
||||||
|
$$,
|
||||||
|
'vat-reporting-deadline',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Value Added Tax Reporting Deadline',
|
||||||
|
'VAT filing deadline and calculation (부가가치세법 제25조)',
|
||||||
|
'부가가치세,신고기한,세무',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 8. 종합소득세 신고 완벽 가이드
|
||||||
|
(
|
||||||
|
'종합소득세 신고 완벽 가이드 - 5월 신고로 연간 세금 결정됩니다',
|
||||||
|
$$# 종합소득세 신고 완벽 가이드
|
||||||
|
|
||||||
|
"종합소득세는 무엇이고, 정말 모두 신고해야 하나요?"
|
||||||
|
|
||||||
|
소득세법 제19조에 따르면 종합소득세는 모든 사업소득을 합산하여 신고합니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
종합소득세 구성:
|
||||||
|
- 사업소득 (자영업, 프리랜서)
|
||||||
|
- 근로소득 (급여)
|
||||||
|
- 이자소득 (저축 이자)
|
||||||
|
- 배당소득 (주식 배당)
|
||||||
|
- 기타소득 (강의료 등)
|
||||||
|
|
||||||
|
신고 대상:
|
||||||
|
- 종합소득 4,000만 원 초과
|
||||||
|
|
||||||
|
신고 기한:
|
||||||
|
- 매년 5월 31일까지
|
||||||
|
|
||||||
|
필요 서류:
|
||||||
|
- 사업소득 기장내역
|
||||||
|
- 경비 영수증
|
||||||
|
- 기부금 증명서
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
모든 소득을 포함해야 하나?
|
||||||
|
- 사업소득, 근로소득, 기타소득 모두
|
||||||
|
- 하나라도 누락되면 탈세
|
||||||
|
- 세무청이 자동 적발
|
||||||
|
|
||||||
|
공제 항목을 놓치면?
|
||||||
|
- 교육비, 의료비, 기부금 공제 가능
|
||||||
|
- 공제 순서가 있음 (환급 과감면제 등)
|
||||||
|
- 증빙서류 필수
|
||||||
|
|
||||||
|
세율은 얼마인가?
|
||||||
|
- 소득에 따라 6~45% (누진세)
|
||||||
|
- 계산이 복잡함
|
||||||
|
- 오류가능성 높음
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 전체 소득 정확한 파악
|
||||||
|
- 공제 항목 최대화
|
||||||
|
- 세율 계산 정확성
|
||||||
|
- 환급금 극대화
|
||||||
|
|
||||||
|
법적 근거: 소득세법 제19조 종합소득
|
||||||
|
$$,
|
||||||
|
'comprehensive-income-tax-guide',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Comprehensive Income Tax Filing Guide',
|
||||||
|
'Complete guide to filing comprehensive income tax (종합소득세) (소득세법 제19조)',
|
||||||
|
'종합소득세,신고,공제,소득세',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
),
|
||||||
|
|
||||||
|
-- 9. 연말정산 환급 최대화
|
||||||
|
(
|
||||||
|
'연말정산 환급 최대화 - 놓친 공제 하나가 수십만 원입니다',
|
||||||
|
$$# 연말정산 환급 최대화
|
||||||
|
|
||||||
|
"연말정산으로 환금을 받으려면 뭘 꼭 챙겨야 하나요?"
|
||||||
|
|
||||||
|
소득세법 제163조에 따르면 특정 지출에 대해 세액공제가 있습니다.
|
||||||
|
|
||||||
|
1️⃣ 이 정도는 누구나 배울 수 있어요
|
||||||
|
|
||||||
|
주요 공제 (2025년):
|
||||||
|
- 기본공제: 1명당 150만 원
|
||||||
|
- 교육비: 학생본인+자녀 연 900만 원
|
||||||
|
- 의료비: 연 750만 원 초과분
|
||||||
|
- 기부금: 전액 공제
|
||||||
|
- 신용카드: 연 25만 원 초과분 15% 공제
|
||||||
|
|
||||||
|
계산 예시:
|
||||||
|
- 신용카드 사용 200만 원 → (200만-250만) × 15% = 환금 0원
|
||||||
|
- 신용카드 사용 300만 원 → (300만-250만) × 15% = 7.5만 원 환금
|
||||||
|
|
||||||
|
2️⃣ 하지만 현실은 복잡해요
|
||||||
|
|
||||||
|
공제 순서가 있나?
|
||||||
|
- 기본공제 먼저
|
||||||
|
- 그 다음 특별공제 (교육비, 의료비)
|
||||||
|
- 마지막 세액공제
|
||||||
|
- 순서 틀리면 환금 안 됨
|
||||||
|
|
||||||
|
중복 공제는?
|
||||||
|
- 같은 지출 두 번 공제 불가
|
||||||
|
- 배우자가 신청했으면?
|
||||||
|
- 부모가 신청했으면? (중복 불가)
|
||||||
|
|
||||||
|
증빙서류를 잃어버렸다면?
|
||||||
|
- 신용카드: 증빙 필수 (발급 신청)
|
||||||
|
- 현금영수증: 미리 등록해야 공제
|
||||||
|
- 의료비: 병원 영수증 필수
|
||||||
|
|
||||||
|
3️⃣ 그래서 세무사가 필요합니다
|
||||||
|
|
||||||
|
세무사는:
|
||||||
|
- 공제 항목 빠짐 없이 확인
|
||||||
|
- 중복 공제 방지
|
||||||
|
- 최적 가족 배분 (부부, 부모)
|
||||||
|
- 환급금 극대화
|
||||||
|
|
||||||
|
법적 근거: 소득세법 제163조 연말정산
|
||||||
|
$$,
|
||||||
|
'year-end-tax-settlement',
|
||||||
|
NULL,
|
||||||
|
true,
|
||||||
|
'Year-End Tax Settlement Refund Maximization',
|
||||||
|
'How to maximize tax refund in year-end adjustment (연말정산) (소득세법 제163조)',
|
||||||
|
'연말정산,환금,공제,세액공제',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,299 @@
|
|||||||
|
-- V026: 기초 3개 포스트 추가 + 모든 12개에 카테고리 할당
|
||||||
|
-- 카테고리 배치 (각 3개씩):
|
||||||
|
-- cat 1 (사업자 세무): 사업자 기장, 소상공인, 스마트스토어
|
||||||
|
-- cat 2 (부동산 세금): 월세, 자녀 증여세
|
||||||
|
-- cat 3 (종합소득세): 프리랜서 종소세, 프리랜서 경비, 종소세 가이드
|
||||||
|
-- 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)
|
||||||
|
('사업자 기장 시 자주 하는 실수 5가지 - 혼자 하기 어려운 이유', 'accounting-mistakes', $$# 사업자 기장 시 자주 하는 실수 5가지
|
||||||
|
|
||||||
|
많은 소규모 사업자들이 "돈이 들어오고 나가는 것을 기록하는 일"은 간단해 보이지만, 실제로는 악마가 디테일에 숨어있습니다.
|
||||||
|
|
||||||
|
## 단계별 계산 (2025년 기준)
|
||||||
|
|
||||||
|
### 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: 세금 (2025년 기준)
|
||||||
|
- 기본공제: 160만 원
|
||||||
|
- 과세표준: 1,640만 원
|
||||||
|
- 세율: 6%
|
||||||
|
- 세금: 약 98만 원/년
|
||||||
|
|
||||||
|
## 악마는 디테일에 숨어있습니다
|
||||||
|
|
||||||
|
### 1. 영수증 정리
|
||||||
|
겉으로는: 영수증을 모으기만 하면 돼
|
||||||
|
현실: 소득세법 제34조에서 인정되는 사업비만 공제 가능
|
||||||
|
|
||||||
|
### 2. 매출과 경비 기록
|
||||||
|
겉으로는: 엑셀에 숫자만 입력하면 돼
|
||||||
|
현실: 부가세와의 연계, 수정신고 규정, 기한 후 신고 가산세 고려
|
||||||
|
|
||||||
|
### 3. 세금 확정
|
||||||
|
겉으로는: 기장만 잘하면 끝
|
||||||
|
현실: 절세 전략, 연도별 일관성, 세무조사 대비, 이의신청 절차
|
||||||
|
|
||||||
|
## 올바른 기장 vs 하면 안 되는 것
|
||||||
|
|
||||||
|
### 해야 할 것
|
||||||
|
1. 영수증 정리 - 5년 보관 의무
|
||||||
|
2. 기본 기록 - 소득세법 제164조 규정
|
||||||
|
3. 연 1회 점검 - 세무사와 상담
|
||||||
|
4. 정확한 신고 - 소득세법 제46조 준수
|
||||||
|
|
||||||
|
### 하면 안 되는 것
|
||||||
|
1. 영수증 버리기 - 증거 부족
|
||||||
|
2. 개인비와 섞기 - 세법 위반
|
||||||
|
3. 신고 늦추기 - 가산세 부과
|
||||||
|
4. 과하게 깎기 - 세무조사 대상
|
||||||
|
|
||||||
|
## 결론
|
||||||
|
|
||||||
|
기초는 배울 수 있지만, 세법의 복잡성, 매년 변경되는 기준, 정확한 해석 때문에 세무사의 도움이 필요합니다.$$, 1, true, 'SEO Title', 'SEO Description', '사업자,기장,세무', NOW(), NOW()),
|
||||||
|
|
||||||
|
('이번달 부가가치세 신고 - 너무 늦지 마세요! (D-day 계산)', 'vat-report-guide', $$# 부가가치세 신고 - D-day 계산
|
||||||
|
|
||||||
|
많은 사업자들이 신고 기한을 놓칩니다. 부가가치세법 제25조에 따르면 신고 기한은 25일(2025년 개정). 하루만 늦어도 국세기본법 제47조 가산세가 발생합니다!
|
||||||
|
|
||||||
|
## 2025년 신고 일정
|
||||||
|
|
||||||
|
| 기간 | 신고 마감 | 납부 마감 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1~2월 | 3월 25일 | 3월 31일 |
|
||||||
|
| 3~4월 | 5월 25일 | 5월 31일 |
|
||||||
|
| 5~6월 | 7월 25일 | 7월 31일 |
|
||||||
|
| 7~8월 | 9월 25일 | 9월 30일 |
|
||||||
|
|
||||||
|
## 부가세 계산 (간이과세 기준)
|
||||||
|
|
||||||
|
월 1,000만 원 매출 기준:
|
||||||
|
- 간이과세율: 도매·소매업 3%
|
||||||
|
- 부가세 = 1,000만 × 3% = 300,000원/월
|
||||||
|
|
||||||
|
## 하지만 복잡한 부분들
|
||||||
|
|
||||||
|
- 카드 수수료 처리
|
||||||
|
- 현금 판매 기록
|
||||||
|
- 환불 처리 규정
|
||||||
|
- 세금계산서 vs 일반 영수증
|
||||||
|
- 3개월 전 환불 공제 불가
|
||||||
|
|
||||||
|
이런 디테일들 때문에 세무사가 필요합니다.$$, 4, true, 'SEO Title', 'SEO Description', '부가가치세,신고,세금', NOW(), NOW()),
|
||||||
|
|
||||||
|
('프리랜서를 위한 종합소득세 신고 - 정확한 경비 처리 가이드', 'freelancer-tax-guide', $$# 프리랜서를 위한 종합소득세 신고
|
||||||
|
|
||||||
|
유튜버, 온라인 강사, 디자이너, 프리랜서... 자신이 벌은 돈을 직접 신고해야 합니다. 종합소득세 신고(소득세법 제20조)입니다.
|
||||||
|
|
||||||
|
## 실제 사례: 유튜버 (월 250만 원 수입)
|
||||||
|
|
||||||
|
### 실패 사례
|
||||||
|
- 신고 소득: 3,000만 원
|
||||||
|
- 기본공제: 160만 원
|
||||||
|
- 세금: 약 450만 원
|
||||||
|
|
||||||
|
### 성공 사례 (정확한 경비 처리)
|
||||||
|
- 신고 소득: 2,200만 원 (경비 800만 원 공제)
|
||||||
|
- 기본공제: 160만 원
|
||||||
|
- 세금: 약 280만 원
|
||||||
|
- **절약액: 약 170만 원**
|
||||||
|
|
||||||
|
## 종합소득세 계산 (2025년)
|
||||||
|
|
||||||
|
### 연간 수입
|
||||||
|
| 수입 출처 | 연간 |
|
||||||
|
|---------|------|
|
||||||
|
| 유튜브 광고 | 2,400만 |
|
||||||
|
| 브랜드 협찬 | 600만 |
|
||||||
|
| 합계 | 3,000만 |
|
||||||
|
|
||||||
|
### 경비 (소득세법 제34조 기준)
|
||||||
|
| 항목 | 연간 |
|
||||||
|
|------|------|
|
||||||
|
| 카메라/마이크 | 100만 |
|
||||||
|
| 소프트웨어 | 72만 |
|
||||||
|
| 인터넷비 | 60만 |
|
||||||
|
| 카페비 | 240만 |
|
||||||
|
| 강의료 | 120만 |
|
||||||
|
| 책 구매 | 36만 |
|
||||||
|
| 교통비 | 120만 |
|
||||||
|
| 합계 | 748만 |
|
||||||
|
|
||||||
|
### 과세표준
|
||||||
|
- 총 수입: 3,000만 원
|
||||||
|
- 경비: 748만 원
|
||||||
|
- 과세표준: 2,252만 원
|
||||||
|
- 기본공제: 160만 원
|
||||||
|
- 최종 과세표준: 2,092만 원
|
||||||
|
|
||||||
|
## 많은 프리랜서가 놓치는 부분
|
||||||
|
|
||||||
|
1. 어떤 경비가 인정되는가? (소득세법 제34조)
|
||||||
|
2. 매년 기준이 바뀐다 (2025년 기본공제 160만)
|
||||||
|
3. 세법 개정사항을 어떻게 반영하나?
|
||||||
|
4. 세무조사에 대비해야 한다
|
||||||
|
|
||||||
|
이런 것들 때문에 세무사와 함께하는 것이 효율적입니다.$$, 3, true, 'SEO Title', 'SEO Description', '종합소득세,프리랜서,경비', NOW(), NOW()),
|
||||||
|
|
||||||
|
-- 추가 9개 포스트 (V025) - category_id 할당
|
||||||
|
('프리랜서가 놓친 경비 5가지 - 이것도 인정될까요?', 'freelancer-expenses-5', $$# 프리랜서가 놓친 경비 5가지
|
||||||
|
|
||||||
|
프리랜서의 일반적인 경비:
|
||||||
|
- 통신비: 인터넷, 휴대폰 요금
|
||||||
|
- 교육비: 업무 관련 강좌, 자격증
|
||||||
|
- 차량유지비: 업무용 차량 유지
|
||||||
|
- 소프트웨어: 업무용 프로그램
|
||||||
|
- 사무실비: 작업 공간 임차료
|
||||||
|
|
||||||
|
하지만 무엇이 "필요경비"인지는 복잡합니다. 소득세법 제34조를 정확하게 이해해야 합니다.$$, 3, true, 'SEO Title', 'SEO Description', '프리랜서,경비', NOW(), NOW()),
|
||||||
|
|
||||||
|
('월세 신고하는 방법 - 환급받을 수 있는 금액이 있습니다', 'monthly-rent-deduction', $$# 월세 신고하는 방법
|
||||||
|
|
||||||
|
소득세법 제59조의2에 따르면 월세세액공제가 있습니다.
|
||||||
|
|
||||||
|
## 월세세액공제 조건 (2025년 기준)
|
||||||
|
- 본인 거주 주택의 월세: 연 750만 원 한도
|
||||||
|
- 필요 서류: 임대차계약서, 월세 납부 증빙
|
||||||
|
- 환급액: 연 월세의 10% (최대 75만 원)
|
||||||
|
|
||||||
|
예시: 월 60만 원 월세
|
||||||
|
- 연 월세: 720만 원
|
||||||
|
- 환급액: 72만 원
|
||||||
|
|
||||||
|
신고하지 않으면 한 푼도 못 받습니다!$$, 2, true, 'SEO Title', 'SEO Description', '월세,세액공제', NOW(), NOW()),
|
||||||
|
|
||||||
|
('자녀 증여세 계산하기 - 기초공제를 모르면 손해봅니다', 'child-gift-tax', $$# 자녀 증여세 계산하기
|
||||||
|
|
||||||
|
상속세및증여세법 제13조에 따르면 기초공제가 있습니다.
|
||||||
|
|
||||||
|
## 증여세 기초공제 (2025년 기준)
|
||||||
|
- 직계 자손: 1인당 기초공제 많음
|
||||||
|
- 조건: 증여자와 수증자 관계 증명
|
||||||
|
|
||||||
|
## 조세 전략
|
||||||
|
- 시간 분산 (연간 공제 한도 활용)
|
||||||
|
- 여러 자녀에게 분산
|
||||||
|
- 공제 시기 선택
|
||||||
|
|
||||||
|
정확한 계산이 필요합니다.$$, 2, true, 'SEO Title', 'SEO Description', '증여세,상속세', NOW(), NOW()),
|
||||||
|
|
||||||
|
('사업자 등록 타이밍 - 너무 빨라도, 늦어도 손해입니다', 'business-registration-timing', $$# 사업자 등록 타이밍
|
||||||
|
|
||||||
|
소득세법 제2조에 따르면 사업소득의 인정 기준이 명확합니다.
|
||||||
|
|
||||||
|
## 사업자 등록의 효과
|
||||||
|
- 부가가치세 신고 의무
|
||||||
|
- 세금 공제 가능
|
||||||
|
- 신용 기록 형성
|
||||||
|
|
||||||
|
## 언제 등록해야 하나?
|
||||||
|
- 너무 빨리: 불필요한 부가세 부담
|
||||||
|
- 너무 늦게: 소급 신고로 가산세
|
||||||
|
|
||||||
|
정확한 타이밍이 중요합니다.$$, 4, true, 'SEO Title', 'SEO Description', '사업자등록', NOW(), NOW()),
|
||||||
|
|
||||||
|
('소상공인 간단 기장 - 엑셀 + 영수증으로 충분합니다', 'small-business-accounting', $$# 소상공인 간단 기장
|
||||||
|
|
||||||
|
소득세법 제29조에 따르면 간단 기장도 인정됩니다.
|
||||||
|
|
||||||
|
## 간단 기장 방법
|
||||||
|
- 엑셀에 매출/경비 기록
|
||||||
|
- 영수증 보관
|
||||||
|
- 연 1회 세무사와 정산
|
||||||
|
|
||||||
|
## 필수 항목
|
||||||
|
- 날짜
|
||||||
|
- 거래처
|
||||||
|
- 금액
|
||||||
|
- 증빙 서류 보관
|
||||||
|
|
||||||
|
이 정도면 충분합니다.$$, 1, true, 'SEO Title', 'SEO Description', '소상공인,기장', NOW(), NOW()),
|
||||||
|
|
||||||
|
('스마트스토어 판매자 세무 - 플랫폼 수입도 세금이 필요합니다', 'smartstore-tax', $$# 스마트스토어 판매자 세무
|
||||||
|
|
||||||
|
플랫폼 판매 수입도 세금 신고 대상입니다.
|
||||||
|
|
||||||
|
## 신고 방법
|
||||||
|
- 플랫폼에서 제공하는 정산 내역서
|
||||||
|
- 소득세법 제20조 기타소득 또는 사업소득
|
||||||
|
- 연 300만 원 이상 시 신고 의무
|
||||||
|
|
||||||
|
## 경비 처리
|
||||||
|
- 상품 구매
|
||||||
|
- 수수료
|
||||||
|
- 배송비
|
||||||
|
- 광고비
|
||||||
|
|
||||||
|
정확한 분류가 필요합니다.$$, 1, true, 'SEO Title', 'SEO Description', '스마트스토어,세무', NOW(), NOW()),
|
||||||
|
|
||||||
|
('부가가치세 신고 기한 - 2일만 늦어도 가산세입니다', 'vat-deadline', $$# 부가가치세 신고 기한
|
||||||
|
|
||||||
|
부가가치세법 제25조: 신고 기한은 25일(2025년 개정)입니다.
|
||||||
|
|
||||||
|
## 신고 지체 시 페널티
|
||||||
|
- 국세기본법 제47조: 1일당 0.2% 가산세
|
||||||
|
- 하루만 늦어도 발생
|
||||||
|
|
||||||
|
## 신고 방법
|
||||||
|
- 국세청 홈택스
|
||||||
|
- 세무사 대리
|
||||||
|
- 회계프로그램
|
||||||
|
|
||||||
|
기한을 절대 넘기면 안 됩니다.$$, 4, true, 'SEO Title', 'SEO Description', '부가가치세,기한', NOW(), NOW()),
|
||||||
|
|
||||||
|
('종합소득세 신고 완벽 가이드 - 5월 신고로 연간 세금이 결정됩니다', 'income-tax-complete-guide', $$# 종합소득세 신고 완벽 가이드
|
||||||
|
|
||||||
|
소득세법 제19조: 종합소득세 신고는 매년 5월입니다.
|
||||||
|
|
||||||
|
## 신고 대상
|
||||||
|
- 사업소득 발생 개인
|
||||||
|
- 기타소득 연 300만 원 이상
|
||||||
|
- 근로소득 이외의 소득 발생자
|
||||||
|
|
||||||
|
## 필요 서류
|
||||||
|
- 소득 입증 서류
|
||||||
|
- 경비 증빙 자료
|
||||||
|
- 공제 관련 서류
|
||||||
|
|
||||||
|
## 신고 절차
|
||||||
|
1. 소득 정리
|
||||||
|
2. 경비 계산
|
||||||
|
3. 과세표준 계산
|
||||||
|
4. 세금 계산
|
||||||
|
5. 신고 및 납부
|
||||||
|
|
||||||
|
정확한 신고가 중요합니다.$$, 3, true, 'SEO Title', 'SEO Description', '종합소득세,신고', NOW(), NOW()),
|
||||||
|
|
||||||
|
('연말정산 환급 최대화 - 놓친 공제 하나가 수십만 원입니다', 'year-end-settlement-tips', $$# 연말정산 환급 최대화
|
||||||
|
|
||||||
|
소득세법 제163조: 연말정산은 매년 2월입니다.
|
||||||
|
|
||||||
|
## 주요 공제 항목
|
||||||
|
- 교육비: 자녀 교육비 (연 900만 원 한도)
|
||||||
|
- 의료비: 총 급여 3% 초과분만
|
||||||
|
- 신용카드: 총 급여 25% 초과분만
|
||||||
|
- 기부금: 한도 있음
|
||||||
|
|
||||||
|
## 환급받기
|
||||||
|
- 공제 항목 확인
|
||||||
|
- 증빙 서류 준비
|
||||||
|
- 회사에 제출
|
||||||
|
- 2월에 환급
|
||||||
|
|
||||||
|
놓친 공제가 있으면 손해입니다.$$, 5, true, 'SEO Title', 'SEO Description', '연말정산,환급', NOW(), NOW());
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
if [ "${TAXBAIK_DEPLOY_FROM_CI:-}" != "1" ]; then
|
||||||
|
echo "❌ This deployment script may only be run from CI." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
DEPLOY_HOME="/home/kjh2064"
|
DEPLOY_HOME="/home/kjh2064"
|
||||||
WEB_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
WEB_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
@@ -38,4 +43,4 @@ ps aux | grep TaxBaik.Web | grep -v grep && echo "✓ Web started" || echo "✗
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "===== ✅ 배포 완료 ====="
|
echo "===== ✅ 배포 완료 ====="
|
||||||
cat "$DEPLOY_HOME/taxbaik_active/wwwroot/version.txt" 2>/dev/null || echo "Version file not found"
|
cat "$DEPLOY_HOME/taxbaik_active/wwwroot/version.json" 2>/dev/null || echo "Version file not found"
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=TaxBaik Local TCP Proxy (5001 -> active blue/green port)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=kjh2064
|
||||||
|
WorkingDirectory=/home/kjh2064/taxbaik_active
|
||||||
|
ExecStart=/usr/bin/dotnet TaxBaik.Proxy.dll
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
# Proxy는 백엔드 포트(5003/5004) 전환 중에도 살아 있어야 한다.
|
||||||
|
TimeoutStopSec=15
|
||||||
|
KillMode=mixed
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
|
||||||
|
SyslogIdentifier=taxbaik-proxy
|
||||||
|
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user