Compare commits
532 Commits
a6ca30eec8
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b7cb442937 | |||
| 6a44272f25 | |||
| 8799fe250b | |||
| 41b723e908 | |||
| 8b1127b270 | |||
| 0179c1d640 | |||
| 9e08c6e12c | |||
| 13b35c5a2d | |||
| 200a1213a2 | |||
| f820b33fc5 | |||
| 7002d50a4e | |||
| fd5178b118 | |||
| c26319b6d7 | |||
| 333089a6ea | |||
| c00d237a4d | |||
| dc86ccfe35 | |||
| 7cfae3a38c | |||
| 2871df54c4 | |||
| 1330ef7c1d | |||
| f283a78845 | |||
| 9abdd06662 | |||
| dcd4c283b8 | |||
| af238543c5 | |||
| eb24f22477 | |||
| 8bc8cea5ef | |||
| 6b0bf5b78e | |||
| d2b2c7522a | |||
| b578a9ba57 | |||
| 673c78be17 | |||
| c95e92529d | |||
| 41791cfcd1 | |||
| 8091bb8902 | |||
| e1f3fc5270 | |||
| 4e674b2bc9 | |||
| 21a2225df7 | |||
| 08b002de27 | |||
| bdc6b0c80e | |||
| 3063bc0ed5 | |||
| 573abe858b | |||
| d63d20058c | |||
| f5478dd388 | |||
| 833b85ef0d | |||
| 901d75972a | |||
| f32bb47be2 | |||
| df0fb16cbd | |||
| 041e22b0d8 | |||
| f8ef5cd88a | |||
| 99a62904e8 | |||
| 1534e7dd5b | |||
| 0005b7f583 | |||
| d213290ed6 | |||
| 878ffdd3bb | |||
| d08de4fa10 | |||
| ef3f8ffaf4 | |||
| d26436b8a3 | |||
| 6b81c4a00e | |||
| 0980e2c267 | |||
| b819e5c8ea | |||
| ddea10f310 | |||
| a9454e2e31 | |||
| 33aa2adaea | |||
| 1cc2f8f5d2 | |||
| ead92badc1 | |||
| 7546c36528 | |||
| 089baa72cb | |||
| 20647f6ecc | |||
| a44a5bbb83 | |||
| c960860b3a | |||
| 65027c7862 | |||
| ff3fa7d22a | |||
| 93cc4b0c45 | |||
| aff388df2d | |||
| 58bec88d7d | |||
| d59440efbc | |||
| 3bfb1bab7e | |||
| 231f7676d3 | |||
| 5431299498 | |||
| 3827e374ca | |||
| 47bb3a38e6 | |||
| ffffa2869f | |||
| 2c62ce8a6e | |||
| 0f40eba363 | |||
| 8c7df7a813 | |||
| 9ee812f563 | |||
| a554e1795a | |||
| 8591d93b88 | |||
| af8b21fdb8 | |||
| bc3bde75af | |||
| 98ebc89505 | |||
| 98bc15b1d6 | |||
| 9797b86e16 | |||
| 7f1fdb4c57 | |||
| 54367696dc | |||
| 64e462e57e | |||
| 4b68fb20b9 | |||
| 35ab77fd38 | |||
| a5359869a0 | |||
| ef484c41a4 | |||
| dd660ef4b3 | |||
| f0269826fe | |||
| b7baff18dc | |||
| 48e2dfaf38 | |||
| a7f9b94499 | |||
| 66d6ae88f1 | |||
| caf7e5cf9f | |||
| 11019c7e0b | |||
| 7659dfd5e0 | |||
| a9827315e3 | |||
| e7436bb0e7 | |||
| 837ae530c7 | |||
| 9ae701ff93 | |||
| aaa867ce02 | |||
| 72e47d2661 | |||
| e2587bad40 | |||
| c71d858cd2 | |||
| d93e3a3aeb | |||
| 5abf086652 | |||
| 714c137740 | |||
| a6068e184b | |||
| 69ec7913d0 | |||
| 063ec189ce | |||
| 2bbe2ef47f | |||
| c5a0a54ee9 | |||
| c8f69bbd92 | |||
| d31e18e88b | |||
| 6f125e485b | |||
| 6fdf233976 | |||
| 76334bedd2 | |||
| a97f31f89c | |||
| 052fa1e9d7 | |||
| b89f9161d2 | |||
| 474a7cc72f | |||
| c92118ab32 | |||
| 675ef64975 | |||
| 055bc48d1d | |||
| 5faa1fb116 | |||
| a0918f03f0 | |||
| 21a654bd04 | |||
| f4cb922aa0 | |||
| 6990dbc6c2 | |||
| a905b31100 | |||
| 8277f60d84 | |||
| 260c410c5b | |||
| f08efb1a6d | |||
| 76872dfb72 | |||
| 40617d16e6 | |||
| dd2aa5e94a | |||
| 2762f74d1e | |||
| 300971bc3c | |||
| 2797473c56 | |||
| 69eeaca937 | |||
| ad6a65324a | |||
| 47dc8c6c57 | |||
| 840528698c | |||
| b6e0add2ac | |||
| 48c1b69af9 | |||
| e24d683d52 | |||
| 6fb17df2c2 | |||
| 015ace6671 | |||
| d3b9a6047c | |||
| da6058fb61 | |||
| 40cffb3beb | |||
| 041d3cae96 | |||
| 29a633e5fc | |||
| dda600d4e1 | |||
| 32029bff92 | |||
| 3d0cf1132c | |||
| 7ff8689a72 | |||
| b2dd217017 | |||
| e044acea17 | |||
| 29910d4d1b | |||
| e9a6ca9797 | |||
| 8095251eba | |||
| 6508282732 | |||
| ea447495d3 | |||
| c00d002972 | |||
| 83c1254a3e | |||
| e5981769b9 | |||
| d015bb6c92 | |||
| f29910030e | |||
| 8db3c1d220 | |||
| 328cfc0772 | |||
| 9b7e6eda4c | |||
| 059109b064 | |||
| 58ab7f44fa | |||
| c54b01bdc8 | |||
| 5d1eeb8485 | |||
| 04a5e15435 | |||
| 5ca1fe8620 | |||
| 56a7d0475b | |||
| 07e6a2a4ef | |||
| 9d99ab9f33 | |||
| 4b7bdbaffb | |||
| 8f41148756 | |||
| 41e130d26a | |||
| e202faa431 | |||
| f519df3e37 | |||
| 9c5a091e5a | |||
| 54a57b2306 | |||
| cc1fff44c0 | |||
| f8d81d8af0 | |||
| 484ece7a92 | |||
| 8202c3278b | |||
| 76446ee0f0 | |||
| 84f2839d9b | |||
| 24e94436e2 | |||
| d246071835 | |||
| ba981e7332 | |||
| f0b77b0e3f | |||
| 527a8821d8 | |||
| 3821914cf5 | |||
| ece69d576a | |||
| d45dbbc06d | |||
| e65612def8 | |||
| bb11a1bb87 | |||
| ae9380ddb3 | |||
| d8c52583ba | |||
| 585f426f0b | |||
| c8cf654131 | |||
| ebdcb4fd22 | |||
| 0ffb149296 | |||
| 870b51ece4 | |||
| b1ac7129d9 | |||
| 500d163ebc | |||
| d780fecf8c | |||
| b1601b0305 | |||
| e6253fdc83 | |||
| c885c6b234 | |||
| 96c7ab5e54 | |||
| 3f486d9fe9 | |||
| f68c968aed | |||
| 984da933ca | |||
| 3dd1cbb6ce | |||
| a3d294b6ff | |||
| e2d3eb9195 | |||
| 77aaed814c | |||
| d7ca51b741 | |||
| bc210969e2 | |||
| 6642f3d6f1 | |||
| 67f2f4b5d6 | |||
| faf4273e6d | |||
| 15c261a49d | |||
| b06c0f99fb | |||
| ad55bd1884 | |||
| e0b8d4e370 | |||
| e65f01b196 | |||
| 124b3b4dfc | |||
| 3785bc7a70 | |||
| bd44ec7c5f | |||
| cb47349a25 | |||
| b3cab87539 | |||
| 1fc3b6c0a4 | |||
| 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 | |||
| 54c179b1eb | |||
| 488b8d11b7 | |||
| 65c5f19a2f | |||
| eaacbc8d7f | |||
| ac8a70a2ca | |||
| 203e674c3f | |||
| 0c014d0bdf | |||
| 904c0972ca | |||
| 7e75aeeec7 | |||
| b13eed7b7e | |||
| 4647b049b8 | |||
| 1a5ebb45bc | |||
| f197663101 | |||
| 70b57f1d4c | |||
| 428eeb6fd8 | |||
| dd68a237a1 | |||
| ef9fd523c6 | |||
| f2ab78dea2 | |||
| 1e0c0b7e1c | |||
| 1b173376ee | |||
| 1a7bc9e209 | |||
| 3be379431f | |||
| 682e2db3a3 | |||
| d9766cb5ef | |||
| 6bcb9effa8 | |||
| 186c6ef7a4 | |||
| c2e8e08f09 | |||
| 3f7cd7cd84 | |||
| 4b352df408 | |||
| a4b1234900 | |||
| a3c81c4f70 | |||
| 6e8b4e76ac | |||
| 5807e1b35e | |||
| 3e1097f585 | |||
| 917600a793 | |||
| 0d3615b44d | |||
| fc339ca9e7 | |||
| da1226994f | |||
| 6bc03ce3d9 | |||
| ecfbfc7cac | |||
| 46cb508bdf | |||
| ecabe8d9cc | |||
| 55c65810c1 | |||
| 7054d397e4 | |||
| 11fb596fc2 | |||
| a04592499c | |||
| ea9478f2f1 | |||
| f569211967 | |||
| c8306e2ac7 | |||
| bad2f47ffe | |||
| 943fe9c819 | |||
| 7b819f4ab0 | |||
| 6a5740ec68 | |||
| 3c8f30af6d | |||
| 7e3b4e2229 | |||
| 67bd5dc666 | |||
| 84161ee2d9 | |||
| 5aec36b155 | |||
| 3ab8971025 | |||
| db30e71e0a | |||
| e4c2758dea | |||
| 75661aa0ef | |||
| 3303ba2e96 | |||
| 43c2ff6ad9 | |||
| a7bb8d7149 | |||
| 791ce6d526 | |||
| 61083a5bb1 | |||
| 66fb86d23c | |||
| 16f7c6097c | |||
| 7232635ed0 | |||
| b42b98d560 | |||
| f216660afa | |||
| b31b43e30e | |||
| 86bd9ef8ff | |||
| 2fd9984a45 | |||
| 91330ec94c | |||
| 08102c8684 | |||
| e2472b7ea1 | |||
| 033883aac5 | |||
| d2cfcd90f0 | |||
| 42e73fa694 | |||
| f8f8f869fc | |||
| db7f903054 | |||
| 0d7a081f5a | |||
| 0bd36ae26f | |||
| 447a62c0fb | |||
| a16438dcc6 | |||
| ebd12b78a0 | |||
| 4b62d35266 | |||
| c38b97377a | |||
| 59f1509368 | |||
| c2955ad02f | |||
| ea40e5c002 | |||
| 7dd51a1169 | |||
| c65742a0c7 | |||
| 52f1790acb | |||
| cd3bc8357c | |||
| 53beb8a6e4 | |||
| d3b4d59f3c | |||
| 691e4406f3 | |||
| db2af15a07 | |||
| 2bde490e9e | |||
| e797da6140 | |||
| 0265d7ec8c | |||
| 09420dca0e | |||
| e3a0ea03f0 | |||
| ba2cb85fd2 | |||
| 74ee47a269 | |||
| 2af7050800 | |||
| fb9c77943f | |||
| 27f57ff925 | |||
| 79d99cfd7a | |||
| 1a761e8e15 | |||
| c01933e295 | |||
| 73da1859fe | |||
| 68588a8491 | |||
| 0b6a64fbad | |||
| 96df0dd9b1 | |||
| 351c7ac82c | |||
| ad48befb9a | |||
| 804725a785 | |||
| 41c8106a10 | |||
| 472431d45a | |||
| 33ea84fb2b | |||
| 73a564c307 | |||
| 223f365dfd | |||
| 61931ab8eb | |||
| 71d5d2cc1f | |||
| db81f94051 | |||
| 700cdaed4f | |||
| 65241c453c | |||
| b3baef012d | |||
| 0d07b2d26a | |||
| 65c2dce8fe | |||
| 4d94b9b4ff | |||
| 4358b189c8 | |||
| 80a16d8b20 | |||
| fbdbbc7a1f | |||
| 160afb7c7e | |||
| 8149680487 | |||
| 08e9e07458 | |||
| 58edbd9c8f | |||
| 0334a5f607 | |||
| 40c3877fb0 | |||
| 5053245575 | |||
| 126643665a | |||
| d09726c46a | |||
| 114ab22197 | |||
| 640ea96ae7 | |||
| ae7ca7e382 | |||
| 541b04cf3d | |||
| 821b73fe01 | |||
| fb04f73f46 | |||
| 58ec984f41 | |||
| 8760a0a931 | |||
| 1c831b1b30 | |||
| 41f569362d | |||
| 22070c1619 | |||
| 79492184d0 | |||
| 9c96f15f86 | |||
| ccba017e3e | |||
| b67002dcf5 | |||
| 12070b70f8 | |||
| 0e98e68532 | |||
| 624156361a | |||
| 278126fd92 | |||
| 77a5c44cb5 | |||
| 46951d871a | |||
| 1ad720afe6 | |||
| cc72a67355 | |||
| 6af9221fab | |||
| 6be8a91cb6 | |||
| 301efb32ff | |||
| 5df5b596c8 | |||
| aec65905d9 | |||
| 0c49e12fa0 | |||
| d58e524dfc | |||
| 661ffbbf2c | |||
| a58aa7efe0 | |||
| 9f7e01652d | |||
| 38e81a7514 | |||
| e0067c6f55 | |||
| 8f0cb690c4 | |||
| bfad47c2af | |||
| f29f2c3cff | |||
| 64b08831e8 | |||
| 1c8208f38f | |||
| e3f548f163 | |||
| 1438a9e30a | |||
| 832aa49e96 | |||
| 046a16c75b | |||
| 4f2d5b1777 | |||
| 620491fa9f | |||
| 5626f976fc | |||
| f54cab5562 | |||
| 3e8cfc386c | |||
| 640b2079b0 | |||
| 113140e685 | |||
| 1d9f3bac4c | |||
| 6b5ea85733 | |||
| c5af05c5dd | |||
| 0872b44253 | |||
| 04326e2488 | |||
| cbef949a5a | |||
| a3aee8a4c3 | |||
| 2e67e52391 | |||
| 928fc0de37 | |||
| 28060b71be |
@@ -0,0 +1,12 @@
|
|||||||
|
ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
ASPNETCORE_URLS=http://0.0.0.0:5001
|
||||||
|
ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=change-me
|
||||||
|
Jwt__SecretKey=dev-secret-key-change-in-production-min-32-chars!
|
||||||
|
Admin__PasswordResetToken=change-this-reset-token
|
||||||
|
Authentication__Google__ClientId=
|
||||||
|
Authentication__Google__ClientSecret=
|
||||||
|
Authentication__Naver__ClientId=
|
||||||
|
Authentication__Naver__ClientSecret=
|
||||||
|
Authentication__Kakao__ClientId=
|
||||||
|
Authentication__Kakao__ClientSecret=
|
||||||
|
# CI deploy trigger requires a real push on master.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
name: TaxBaik Browser E2E
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["TaxBaik CI/CD"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
browser-e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-playwright-
|
||||||
|
|
||||||
|
- name: Install Playwright dependencies
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
npm ci
|
||||||
|
npx playwright install chromium --with-deps
|
||||||
|
|
||||||
|
- name: Wait for deployment
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
EXPECTED_VERSION: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
# Extract short commit hash (first 7 characters)
|
||||||
|
SHORT_VERSION=$(echo "$EXPECTED_VERSION" | cut -c1-7)
|
||||||
|
echo "Expected short version: $SHORT_VERSION"
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
# Suppress stderr and allow failures to handle transition/down periods cleanly
|
||||||
|
VERSION_BODY="$(curl -fsS "https://www.taxbaik.com/taxbaik/version.json" 2>/dev/null || true)"
|
||||||
|
BLOG_STATUS="$(curl -s -o /dev/null -w '%{http_code}' "https://www.taxbaik.com/taxbaik/blog/accountant-mistakes-5" || true)"
|
||||||
|
LOGIN_STATUS="$(curl -s -o /dev/null -w '%{http_code}' "https://www.taxbaik.com/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)"
|
||||||
|
ROOT_URL="https://www.taxbaik.com/" \
|
||||||
|
ADMIN_URL="https://www.taxbaik.com/taxbaik/admin/login" \
|
||||||
|
PUBLIC_MARKER="백원숙 세무회계" \
|
||||||
|
PUBLIC_FORBIDDEN="관리자" \
|
||||||
|
ADMIN_MARKER="관리자 로그인" \
|
||||||
|
MAX_RETRIES=1 \
|
||||||
|
bash ./scripts/taxbaik-smoke.sh
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ $i -lt 20 ]; then
|
||||||
|
echo " Attempt $i/20: waiting for deployment... (blog=${BLOG_STATUS:-?}, login=${LOGIN_STATUS:-?}, version=${VERSION_BODY:0:30}...)"
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "✗ TIMEOUT: Deployment failed to publish ${SHORT_VERSION} within 60 seconds" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Browser Smoke verification
|
||||||
|
env:
|
||||||
|
# Green-Blue 배포 지원: Nginx를 통해 active 포트로 라우팅
|
||||||
|
E2E_BASE_URL: https://www.taxbaik.com/taxbaik
|
||||||
|
# E2E 테스트는 test_admin 테스트 계정 사용 (실 admin 계정과 분리)
|
||||||
|
E2E_ADMIN_USERNAME: test_admin
|
||||||
|
E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "Running smoke checks on Desktop Chrome (production verification)"
|
||||||
|
npm run test:e2e:public-smoke
|
||||||
|
npm run test:e2e:admin-smoke
|
||||||
|
|
||||||
|
- name: Browser E2E verification
|
||||||
|
env:
|
||||||
|
# Green-Blue 배포 지원: Nginx를 통해 active 포트로 라우팅
|
||||||
|
E2E_BASE_URL: https://www.taxbaik.com/taxbaik
|
||||||
|
# E2E 테스트는 test_admin 테스트 계정 사용 (실 admin 계정과 분리)
|
||||||
|
E2E_ADMIN_USERNAME: test_admin
|
||||||
|
E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "Running full E2E on Desktop Chrome (production verification)"
|
||||||
|
npx playwright test --project="Desktop Chrome" --reporter=html --reporter=list
|
||||||
|
|
||||||
|
- name: API smoke verification
|
||||||
|
env:
|
||||||
|
E2E_ADMIN_USERNAME: test_admin
|
||||||
|
E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
TOKEN="$(curl -s -X POST "https://www.taxbaik.com/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" "https://www.taxbaik.com/taxbaik/api/blog/admin?page=1&pageSize=1" >/dev/null
|
||||||
|
curl -fsS -H "Authorization: Bearer $TOKEN" "https://www.taxbaik.com/taxbaik/api/faq" >/dev/null
|
||||||
|
curl -fsS -H "Authorization: Bearer $TOKEN" "https://www.taxbaik.com/taxbaik/api/announcement" >/dev/null
|
||||||
|
curl -fsS -H "Authorization: Bearer $TOKEN" "https://www.taxbaik.com/taxbaik/api/inquiry?page=1&pageSize=1" >/dev/null
|
||||||
|
curl -fsS "https://www.taxbaik.com/taxbaik/favicon.svg" >/dev/null
|
||||||
|
curl -fsS "https://www.taxbaik.com/taxbaik/favicon.ico" >/dev/null
|
||||||
|
curl -fsS "https://www.taxbaik.com/taxbaik/robots.txt" >/dev/null
|
||||||
|
|
||||||
|
- name: Browser E2E summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "Executed tests:"
|
||||||
|
echo "- admin-login"
|
||||||
|
echo "- admin-smoke"
|
||||||
|
echo "- public-smoke"
|
||||||
|
echo "- blog-seo"
|
||||||
|
echo "- contact-submit"
|
||||||
|
echo "- inquiry-detail"
|
||||||
|
echo "- admin-password-change"
|
||||||
+348
-56
@@ -1,6 +1,7 @@
|
|||||||
name: TaxBaik CI/CD
|
name: TaxBaik CI/CD
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -19,73 +20,364 @@ jobs:
|
|||||||
dotnet-version: '10.0'
|
dotnet-version: '10.0'
|
||||||
|
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore TaxBaik.sln
|
run: dotnet restore src/TaxBaik.sln
|
||||||
|
|
||||||
- name: Build solution
|
- name: Build solution
|
||||||
run: |
|
run: dotnet build src/TaxBaik.sln -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
dotnet clean TaxBaik.sln -c Release
|
|
||||||
dotnet build TaxBaik.sln -c Release --no-restore
|
|
||||||
|
|
||||||
- name: Publish Web (통합 앱)
|
- name: Test solution
|
||||||
run: dotnet publish TaxBaik.Web/ -c Release -o ./publish --no-restore
|
run: dotnet test src/TaxBaik.sln -c Release --no-build
|
||||||
|
|
||||||
- name: Copy migrations to publish
|
- name: Publish Web (auto-includes WASM from referenced TaxBaik.Web.Client)
|
||||||
run: |
|
run: |
|
||||||
cp -r db/migrations ./publish/migrations || true
|
set -e
|
||||||
|
mkdir -p ./publish-logs
|
||||||
|
web_log="./publish-logs/publish-web.log"
|
||||||
|
start=$(date +%s)
|
||||||
|
# Web.Client needs a Release static-web-assets manifest for Web publish.
|
||||||
|
# Build it explicitly so publish can reuse the prepared outputs.
|
||||||
|
dotnet build src/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
# Build the Web host in Release as well so publish has the same inputs
|
||||||
|
# the server uses in production.
|
||||||
|
dotnet build src/TaxBaik.Web/TaxBaik.Web.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
echo "--- Web.Client Release artifacts ---"
|
||||||
|
ls -la src/TaxBaik.Web.Client/bin/Release/net10.0 || true
|
||||||
|
ls -la src/TaxBaik.Web.Client/obj/Release/net10.0 || true
|
||||||
|
if ! dotnet publish src/TaxBaik.Web/ \
|
||||||
|
-c Release \
|
||||||
|
-o ./publish \
|
||||||
|
--no-restore \
|
||||||
|
-p:SelfContained=false \
|
||||||
|
-p:PublishReadyToRun=false \
|
||||||
|
-p:PerformanceSummary=true \
|
||||||
|
-clp:Summary \
|
||||||
|
-bl:"./publish-logs/publish-web.binlog" >"$web_log" 2>&1; then
|
||||||
|
echo "=== Publish Web failed; tailing log ==="
|
||||||
|
tail -n 120 "$web_log" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
end=$(date +%s)
|
||||||
|
echo "✓ Publish Web elapsed: $((end - start))s"
|
||||||
|
ls -lh ./publish-logs/publish-web.binlog
|
||||||
|
|
||||||
|
- name: Publish Proxy
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
mkdir -p ./publish-logs
|
||||||
|
# Proxy is not part of the solution restore graph, so restore it once
|
||||||
|
# here before publishing to avoid NETSDK1004 in CI.
|
||||||
|
dotnet restore src/TaxBaik.Proxy/
|
||||||
|
dotnet build src/TaxBaik.Proxy/TaxBaik.Proxy.csproj -c Release --no-restore
|
||||||
|
start=$(date +%s)
|
||||||
|
dotnet publish src/TaxBaik.Proxy/ \
|
||||||
|
-c Release \
|
||||||
|
-o ./publish/proxy \
|
||||||
|
--no-restore \
|
||||||
|
--no-build \
|
||||||
|
-p:PublishReadyToRun=false \
|
||||||
|
-p:PerformanceSummary=true \
|
||||||
|
-clp:Summary \
|
||||||
|
-bl:./publish-logs/publish-proxy.binlog
|
||||||
|
end=$(date +%s)
|
||||||
|
echo "✓ Publish Proxy elapsed: $((end - start))s"
|
||||||
|
ls -lh ./publish-logs/publish-proxy.binlog
|
||||||
|
|
||||||
|
- name: Write production secrets
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
JWT_SECRET_KEY="${{ secrets.TAXBAIK_JWT_SECRET_KEY }}"
|
||||||
|
TELEGRAM_BOT_TOKEN="${{ secrets.TAXBAIK_TELEGRAM_BOT_TOKEN }}"
|
||||||
|
TELEGRAM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_CHAT_ID }}"
|
||||||
|
TELEGRAM_INQUIRY_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_INQUIRY_CHAT_ID }}"
|
||||||
|
TELEGRAM_SYSTEM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_SYSTEM_CHAT_ID }}"
|
||||||
|
[ -z "$JWT_SECRET_KEY" ] && { echo "Missing TAXBAIK_JWT_SECRET_KEY" >&2; exit 1; }
|
||||||
|
[ -z "$TELEGRAM_BOT_TOKEN" ] && { echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2; exit 1; }
|
||||||
|
[ -z "$TELEGRAM_CHAT_ID" ] && { echo "Missing TAXBAIK_TELEGRAM_CHAT_ID" >&2; exit 1; }
|
||||||
|
[ -z "$TELEGRAM_INQUIRY_CHAT_ID" ] && TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_CHAT_ID"
|
||||||
|
[ -z "$TELEGRAM_SYSTEM_CHAT_ID" ] && TELEGRAM_SYSTEM_CHAT_ID="-5585148480"
|
||||||
|
JWT_SECRET_KEY="$JWT_SECRET_KEY" \
|
||||||
|
TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \
|
||||||
|
TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" \
|
||||||
|
TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_INQUIRY_CHAT_ID" \
|
||||||
|
TELEGRAM_SYSTEM_CHAT_ID="$TELEGRAM_SYSTEM_CHAT_ID" \
|
||||||
|
python3 -c '
|
||||||
|
import json, os, pathlib
|
||||||
|
pathlib.Path("./publish/appsettings.Production.json").write_text(
|
||||||
|
json.dumps({
|
||||||
|
"Jwt": {"SecretKey": os.environ["JWT_SECRET_KEY"]},
|
||||||
|
"Telegram": {
|
||||||
|
"BotToken": os.environ["TELEGRAM_BOT_TOKEN"],
|
||||||
|
"ChatId": os.environ["TELEGRAM_CHAT_ID"],
|
||||||
|
"InquiryChatId": os.environ["TELEGRAM_INQUIRY_CHAT_ID"],
|
||||||
|
"SystemChatId": os.environ["TELEGRAM_SYSTEM_CHAT_ID"]
|
||||||
|
}
|
||||||
|
}, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8"
|
||||||
|
)'
|
||||||
|
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
|
||||||
|
run: mkdir -p ./publish/db && cp -r db/migrations ./publish/db/ || true
|
||||||
|
|
||||||
|
- name: Validate migration version uniqueness
|
||||||
|
run: bash scripts/validate_migrations.sh db/migrations
|
||||||
|
|
||||||
|
- name: Validate KST timestamps
|
||||||
|
run: bash scripts/validate_kst_timestamps.sh
|
||||||
|
|
||||||
- name: Generate build info
|
- name: Generate build info
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./publish/wwwroot
|
|
||||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||||
BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
BUILD_TIME=$(TZ=Asia/Seoul date +'%Y-%m-%d %H:%M:%S KST')
|
||||||
echo "Version: $COMMIT_HASH" > ./publish/wwwroot/version.txt
|
mkdir -p ./publish/wwwroot
|
||||||
echo "Built: $BUILD_TIME" >> ./publish/wwwroot/version.txt
|
printf '{\n "version": "%s",\n "built": "%s"\n}\n' "$COMMIT_HASH" "$BUILD_TIME" > ./publish/wwwroot/version.json
|
||||||
echo "✓ Version: $COMMIT_HASH"
|
echo "✓ Build: $COMMIT_HASH @ $BUILD_TIME"
|
||||||
|
|
||||||
- name: Deploy (CI only, 통합 Web)
|
- name: Setup SSH
|
||||||
run: |
|
run: |
|
||||||
set -e
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
DEPLOY_HOME="/home/kjh2064"
|
|
||||||
DEPLOY_DIR="$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
|
||||||
DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
|
|
||||||
DEPLOY_USER="${{ secrets.DEPLOY_USER }}"
|
|
||||||
|
|
||||||
echo "=== Deploying TaxBaik v$(git rev-parse --short HEAD) ==="
|
|
||||||
mkdir -p "$DEPLOY_DIR"
|
|
||||||
cp -r ./publish/* "$DEPLOY_DIR/"
|
|
||||||
ln -sfn "$DEPLOY_DIR" "$DEPLOY_HOME/taxbaik_active"
|
|
||||||
echo "✓ Deployed to $DEPLOY_DIR"
|
|
||||||
|
|
||||||
# 서버에서 systemd로 서비스를 재시작
|
|
||||||
echo "=== Restarting service on server ==="
|
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
printf '%s' "${{ secrets.DEPLOY_SSH_KEY_B64 }}" | base64 -d > ~/.ssh/id_ed25519
|
SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}"
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}"
|
||||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
if [ -n "$SSH_KEY_B64" ]; then
|
||||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "sudo systemctl restart taxbaik"
|
printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
||||||
sleep 5
|
elif [ -n "$SSH_KEY_RAW" ]; then
|
||||||
echo "✓ Deployment complete"
|
if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then
|
||||||
|
printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519
|
||||||
- name: Verify deployment
|
else
|
||||||
run: |
|
printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519
|
||||||
set -e
|
fi
|
||||||
DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
|
|
||||||
DEPLOY_USER="${{ secrets.DEPLOY_USER }}"
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
printf '%s' "${{ secrets.DEPLOY_SSH_KEY_B64 }}" | base64 -d > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
||||||
sleep 10
|
|
||||||
HOME_STATUS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/" || echo "000")
|
|
||||||
LOGIN_STATUS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login" || echo "000")
|
|
||||||
ADMIN_TEST_PASSWORD="${{ secrets.TAXBAIK_ADMIN_TEST_PASSWORD }}"
|
|
||||||
AUTH_BODY=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "python3 -c \"import json, urllib.request; req = urllib.request.Request('http://127.0.0.1:5001/taxbaik/api/auth/login', data=json.dumps({'username':'admin','password':'${ADMIN_TEST_PASSWORD}'}).encode(), headers={'Content-Type':'application/json'}, method='POST'); print(urllib.request.urlopen(req, timeout=20).read().decode())\"" || echo "")
|
|
||||||
echo "Home Status: $HOME_STATUS"
|
|
||||||
echo "Login Status: $LOGIN_STATUS"
|
|
||||||
echo "Auth Body: $AUTH_BODY"
|
|
||||||
if [ "$HOME_STATUS" = "200" ] && [ "$LOGIN_STATUS" = "200" ] && echo "$AUTH_BODY" | grep -q '"token"'; then
|
|
||||||
echo "✓ Service is running"
|
|
||||||
else
|
else
|
||||||
echo "⚠ Service may not be running (home: $HOME_STATUS, login: $LOGIN_STATUS, auth: $AUTH_BODY)"
|
echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY" >&2; exit 1
|
||||||
fi
|
fi
|
||||||
|
sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Package artifact
|
||||||
|
run: |
|
||||||
|
cp deploy_gb.sh ./publish/deploy_gb.sh
|
||||||
|
mkdir -p ./publish/scripts
|
||||||
|
cp scripts/validate_migrations.sh ./publish/scripts/validate_migrations.sh
|
||||||
|
chmod +x ./publish/scripts/validate_migrations.sh
|
||||||
|
tar -czf taxbaik_deploy.tgz -C ./publish .
|
||||||
|
echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)"
|
||||||
|
|
||||||
|
- name: Deploy & verify on server
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
export TAXBAIK_DEPLOY_FROM_CI=1
|
||||||
|
TIMESTAMP=$(TZ=Asia/Seoul date +%Y%m%d_%H%M%S)
|
||||||
|
COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}"
|
||||||
|
DEPLOY_USER="${{ secrets.DEPLOY_USER }}"
|
||||||
|
TELEGRAM_BOT_TOKEN="${{ secrets.TAXBAIK_TELEGRAM_BOT_TOKEN }}"
|
||||||
|
TELEGRAM_SYSTEM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_SYSTEM_CHAT_ID }}"
|
||||||
|
TELEGRAM_CHAT_ID="${TELEGRAM_SYSTEM_CHAT_ID:--5585148480}"
|
||||||
|
|
||||||
|
send_telegram() {
|
||||||
|
local text="$1"
|
||||||
|
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
|
||||||
|
echo "Skipping Telegram notification: missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
-d "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
|
--data-urlencode "text=${text}" \
|
||||||
|
-d "parse_mode=HTML" >/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_failure() {
|
||||||
|
local exit_code=$?
|
||||||
|
send_telegram "❌ <b>TaxBaik 배포 실패</b>
|
||||||
|
|
||||||
|
커밋: <code>${COMMIT}</code>
|
||||||
|
시간: <code>${TIMESTAMP}</code>
|
||||||
|
단계: CI/CD deploy"
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap notify_failure ERR
|
||||||
|
|
||||||
|
echo "=== Deploying TaxBaik $COMMIT ($TIMESTAMP) ==="
|
||||||
|
|
||||||
|
# 1. 아티팩트 업로드
|
||||||
|
scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||||
|
taxbaik_deploy.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_${TIMESTAMP}.tgz"
|
||||||
|
|
||||||
|
# 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리, Green-Blue 지원)
|
||||||
|
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||||
|
-o ServerAliveInterval=10 \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST" TAXBAIK_DEPLOY_FROM_CI=1 bash << REMOTE
|
||||||
|
set -e
|
||||||
|
DEPLOY_HOME="/home/kjh2064"
|
||||||
|
DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
||||||
|
TIMESTAMP="${TIMESTAMP}"
|
||||||
|
COMMIT="${COMMIT}"
|
||||||
|
|
||||||
|
echo "--- [1/5] 압축 해제 ---"
|
||||||
|
mkdir -p "\$DEPLOY_DIR"
|
||||||
|
tar -xzf "/tmp/taxbaik_\${TIMESTAMP}.tgz" -C "\$DEPLOY_DIR"
|
||||||
|
rm -f "/tmp/taxbaik_\${TIMESTAMP}.tgz"
|
||||||
|
|
||||||
|
echo "--- [2/5] 운영 설정 검증 ---"
|
||||||
|
test -s "\$DEPLOY_DIR/appsettings.Production.json" \
|
||||||
|
|| { 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] 마이그레이션 사전 검증 ---"
|
||||||
|
test -x "\$DEPLOY_DIR/scripts/validate_migrations.sh" \
|
||||||
|
|| { echo "FATAL: validate_migrations.sh 없음" >&2; exit 1; }
|
||||||
|
"\$DEPLOY_DIR/scripts/validate_migrations.sh" "\$DEPLOY_DIR/db/migrations" "postgresql://taxbaik:taxbaik123@localhost:5432/taxbaikdb"
|
||||||
|
|
||||||
|
echo "--- [4/5] Green-Blue 배포 실행 ---"
|
||||||
|
chmod +x "\$DEPLOY_DIR/deploy_gb.sh"
|
||||||
|
"\$DEPLOY_DIR/deploy_gb.sh" "\$DEPLOY_DIR"
|
||||||
|
|
||||||
|
echo "--- [4.5/5] Nginx 설정 검증 ---"
|
||||||
|
# 실제 로드되는 파일은 sites-enabled/의 심볼릭 링크 대상만이다.
|
||||||
|
# sites-available/에 다른 파일(예: default)이 있어도 sites-enabled에
|
||||||
|
# 링크되어 있지 않으면 nginx는 그 내용을 절대 읽지 않는다.
|
||||||
|
NGINX_CONF=""
|
||||||
|
for f in /etc/nginx/sites-enabled/*; do
|
||||||
|
if [ -e "\$f" ] && grep -q "location /taxbaik" "\$f" 2>/dev/null; then
|
||||||
|
NGINX_CONF=\$(readlink -f "\$f")
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "\$NGINX_CONF" ]; then
|
||||||
|
echo "❌ FATAL: sites-enabled/ 안에서 'location /taxbaik'를 정의한 파일을 찾을 수 없음" >&2
|
||||||
|
echo " sites-available/에 파일을 수정해도 sites-enabled에 심볼릭 링크되어 있지 않으면 반영되지 않는다." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "실제 로드되는 설정 파일: \$NGINX_CONF"
|
||||||
|
|
||||||
|
# 불변식: '/'와 '/taxbaik' location 모두 반드시 127.0.0.1:5001 (TaxBaik.Proxy)을
|
||||||
|
# 가리켜야 한다. 5003/5004를 직접 하드코딩하면 Green-Blue 포트 전환 시
|
||||||
|
# 죽은 포트를 가리키게 되어 502/404가 발생한다 (실제 발생했던 장애).
|
||||||
|
if grep -E "proxy_pass\s+http://127\.0\.0\.1:500[34]" "\$NGINX_CONF" > /dev/null 2>&1; then
|
||||||
|
echo "❌ FATAL: \$NGINX_CONF 가 포트 5003/5004를 직접 참조함 (Green-Blue 전환 시 502 발생)" >&2
|
||||||
|
echo " 수정: sudo sed -i 's|127.0.0.1:500[34]|127.0.0.1:5001|g' \$NGINX_CONF && sudo nginx -t && sudo systemctl reload nginx" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# proxy_pass에 URI(끝 슬래시)가 있으면 nginx가 요청 경로를 재작성하며,
|
||||||
|
# location 접두사와 슬래시 개수가 안 맞으면 백엔드로 이중 슬래시(//)가
|
||||||
|
# 전달되어 404가 발생한다 (실제 발생했던 장애). 접두사 location에서는
|
||||||
|
# proxy_pass에 URI를 붙이지 않는다.
|
||||||
|
if grep -E "location\s+/taxbaik\s*\{" -A 1 "\$NGINX_CONF" | grep -qE "proxy_pass\s+http://127\.0\.0\.1:5001/;"; then
|
||||||
|
echo "❌ FATAL: location /taxbaik 의 proxy_pass 에 불필요한 trailing slash가 있음 (이중 슬래시로 인한 404 위험)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Nginx 설정 검증 통과 (실제 로드 파일 확인 + 포트 5001 고정 + trailing slash 없음)"
|
||||||
|
|
||||||
|
echo "--- [5/5] 헬스 체크 (최대 60초) ---"
|
||||||
|
ATTEMPTS=20
|
||||||
|
for i in \$(seq 1 \$ATTEMPTS); do
|
||||||
|
STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/healthz 2>/dev/null || echo "000")
|
||||||
|
if [ "\$STATUS" = "200" ]; then
|
||||||
|
echo "✓ [1/6] 헬스 체크 완료"
|
||||||
|
|
||||||
|
# 검증 1: 메인 페이지 로드. curl -L + -w 는 리다이렉트 체인의 상태코드를
|
||||||
|
# 이어붙이므로, 첫 응답 코드만 받아 200/3xx를 허용한다.
|
||||||
|
MAIN_STATUS=\$(curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/ 2>/dev/null || echo "000")
|
||||||
|
if ! printf '%s' "\$MAIN_STATUS" | grep -Eq '^(200|301|302|307|308)$'; then
|
||||||
|
echo "❌ 메인 페이지 로드 실패 (상태: \$MAIN_STATUS)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ [2/6] 메인 페이지 로드 완료"
|
||||||
|
|
||||||
|
# 검증 2: CSS 파일 로드
|
||||||
|
CSS_STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/css/admin.css 2>/dev/null || echo "000")
|
||||||
|
if [ "\$CSS_STATUS" != "200" ]; then
|
||||||
|
echo "❌ CSS 파일 로드 실패 (상태: \$CSS_STATUS)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ [3/6] CSS 파일 로드 완료"
|
||||||
|
|
||||||
|
# 검증 3: 버전 정보. 파일 존재만 보면 5001이 잘못된 구 프로세스를
|
||||||
|
# 가리키는 장애를 놓치므로, HTTP 응답이 이번 커밋인지 확인한다.
|
||||||
|
if [ ! -s "\$DEPLOY_DIR/wwwroot/version.json" ]; then
|
||||||
|
echo "❌ version.json 누락" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION_JSON=\$(curl -fsS http://127.0.0.1:5001/taxbaik/version.json 2>/dev/null || true)
|
||||||
|
if ! printf '%s' "\$VERSION_JSON" | grep -q "\"version\": \"\$COMMIT\""; then
|
||||||
|
echo "❌ 5001 프록시가 이번 배포 버전을 제공하지 않음" >&2
|
||||||
|
echo " expected: \$COMMIT" >&2
|
||||||
|
echo " actual: \$VERSION_JSON" >&2
|
||||||
|
echo " 확인: 5001 포트가 TaxBaik.Proxy.dll인지, /home/kjh2064/taxbaik_port가 새 포트인지 점검" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ [4/6] 버전 정보 확인 완료"
|
||||||
|
|
||||||
|
# 검증 4: 5001 프록시 확인
|
||||||
|
if ! ss -tlnp | grep -q ':5001 '; then
|
||||||
|
echo "❌ 5001 프록시가 실행 중이 아님" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ [5/6] 5001 프록시 확인 완료"
|
||||||
|
|
||||||
|
# 검증 5: 관리자 로그인 페이지
|
||||||
|
LOGIN_STATUS=\$(curl -fsSL -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
|
||||||
|
echo "❌ 관리자 로그인 페이지 로드 실패 (상태: \$LOGIN_STATUS)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ [6/6] 관리자 페이지 로드 완료"
|
||||||
|
|
||||||
|
echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)"
|
||||||
|
# 구 배포 디렉토리 정리 (최근 5개 보존)
|
||||||
|
ls -1dt \$DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null \
|
||||||
|
| tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "\$i" -eq "\$ATTEMPTS" ]; then
|
||||||
|
echo "=== FATAL: 서비스가 \$ATTEMPTS회 시도 후에도 응답하지 않음 ===" >&2
|
||||||
|
echo "--- 5001 listener ---" >&2
|
||||||
|
ss -tlnp 2>/dev/null | grep ':5001 ' >&2 || true
|
||||||
|
echo "--- active port file ---" >&2
|
||||||
|
cat "\$DEPLOY_HOME/taxbaik_port" >&2 || true
|
||||||
|
echo "--- 신규 앱 로그 ---" >&2
|
||||||
|
ACTIVE_PORT=\$(cat "\$DEPLOY_HOME/taxbaik_port" 2>/dev/null | tr -d '[:space:]' || true)
|
||||||
|
if [ -n "\$ACTIVE_PORT" ] && [ -s "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" ]; then
|
||||||
|
tail -n 80 "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" >&2
|
||||||
|
else
|
||||||
|
ls -la "\$DEPLOY_DIR" >&2 || true
|
||||||
|
fi
|
||||||
|
echo "--- proxy 로그 ---" >&2
|
||||||
|
tail -n 80 "\$DEPLOY_HOME/taxbaik_proxy.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " 대기 중... (\$i/\$ATTEMPTS, HTTP \$STATUS)"
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
REMOTE
|
||||||
|
|
||||||
|
echo "✓ 배포 완료: taxbaik_${TIMESTAMP} @ $DEPLOY_HOST"
|
||||||
|
|
||||||
|
echo "--- 실제 공개 도메인 종단 간 검증 (Nginx/Cloudflare 경유, 최대 3회 재시도) ---"
|
||||||
|
ROOT_URL="https://www.taxbaik.com/" \
|
||||||
|
ADMIN_URL="https://www.taxbaik.com/taxbaik/admin/login" \
|
||||||
|
PUBLIC_MARKER="백원숙 세무회계" \
|
||||||
|
PUBLIC_FORBIDDEN="관리자" \
|
||||||
|
ADMIN_MARKER="관리자 로그인" \
|
||||||
|
MAX_RETRIES=3 \
|
||||||
|
RETRY_SLEEP_SECONDS=5 \
|
||||||
|
bash ./scripts/taxbaik-smoke.sh
|
||||||
|
echo "✓ 실제 공개 도메인 전체 정상"
|
||||||
|
|
||||||
|
send_telegram "✅ <b>TaxBaik 배포 완료</b>
|
||||||
|
|
||||||
|
커밋: <code>${COMMIT}</code>
|
||||||
|
시간: <code>${TIMESTAMP}</code>
|
||||||
|
대상: <code>${DEPLOY_HOST}</code>
|
||||||
|
채널: <code>${TELEGRAM_CHAT_ID}</code>"
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ artifacts/
|
|||||||
# Test results
|
# Test results
|
||||||
TestResults/
|
TestResults/
|
||||||
*.trx
|
*.trx
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
.playwright-cli/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
@@ -46,6 +49,9 @@ Thumbs.db
|
|||||||
packages/
|
packages/
|
||||||
.nuget/
|
.nuget/
|
||||||
|
|
||||||
|
# Node / Playwright
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
publish/
|
publish/
|
||||||
PublishProfiles/
|
PublishProfiles/
|
||||||
@@ -54,3 +60,6 @@ PublishProfiles/
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
appsettings.Development.json
|
appsettings.Development.json
|
||||||
|
|
||||||
|
# Scratch / temporary work - never commit, see docs/ENGINEERING_HARNESS.md
|
||||||
|
.scratch/
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY ./publish/admin/ .
|
|
||||||
|
|
||||||
EXPOSE 5002
|
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "TaxBaik.Admin.dll"]
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY ./publish/web/ .
|
|
||||||
|
|
||||||
EXPOSE 5001
|
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "TaxBaik.Web.dll"]
|
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
**온라인 세무 상담 플랫폼** | 블로그 SEO 최적화 | 전국 고객 확보
|
**온라인 세무 상담 플랫폼** | 블로그 SEO 최적화 | 전국 고객 확보
|
||||||
|
|
||||||
|
CI deploy trigger verification note.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 개요
|
## 개요
|
||||||
@@ -22,13 +24,13 @@ TaxBaik는 세무사 백원숙의 전문성을 온라인으로 표현하기 위
|
|||||||
|
|
||||||
| 계층 | 기술 |
|
| 계층 | 기술 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| **백엔드** | ASP.NET Core 8, C# |
|
| **백엔드** | ASP.NET Core 10, C# |
|
||||||
| **공개 사이트** | Razor Pages (SSR) |
|
| **공개 사이트** | Razor Pages (SSR) |
|
||||||
| **관리자** | Blazor Server + MudBlazor |
|
| **관리자** | Blazor Server + MudBlazor |
|
||||||
| **데이터베이스** | PostgreSQL 18.4 |
|
| **데이터베이스** | PostgreSQL 18.4 |
|
||||||
| **ORM** | Dapper |
|
| **ORM** | Dapper |
|
||||||
| **리버스 프록시** | Nginx |
|
| **리버스 프록시** | Nginx |
|
||||||
| **배포** | Gitea Actions CI/CD, systemd |
|
| **배포** | Gitea Actions CI/CD, systemd 단일 서비스 |
|
||||||
| **아키텍처** | DDD (Domain-Driven Design), Layered Architecture |
|
| **아키텍처** | DDD (Domain-Driven Design), Layered Architecture |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -103,7 +105,7 @@ TaxBaik/
|
|||||||
### 개발 환경 설정
|
### 개발 환경 설정
|
||||||
|
|
||||||
**필수 요구사항:**
|
**필수 요구사항:**
|
||||||
- .NET 8.0 SDK
|
- .NET 10.0 SDK
|
||||||
- PostgreSQL 18.4
|
- PostgreSQL 18.4
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ createdb taxbaikdb
|
|||||||
psql -d taxbaikdb -f db/migrations/V001__InitialSchema.sql
|
psql -d taxbaikdb -f db/migrations/V001__InitialSchema.sql
|
||||||
psql -d taxbaikdb -f db/migrations/V002__SeedData.sql
|
psql -d taxbaikdb -f db/migrations/V002__SeedData.sql
|
||||||
psql -d taxbaikdb -f db/migrations/V003__SeedAdminAndBlogPosts.sql
|
psql -d taxbaikdb -f db/migrations/V003__SeedAdminAndBlogPosts.sql
|
||||||
|
psql -d taxbaikdb -f db/migrations/V004__CreateSiteSettings.sql
|
||||||
|
|
||||||
# 3. 환경 변수 설정
|
# 3. 환경 변수 설정
|
||||||
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=postgres;Password=password"
|
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=postgres;Password=password"
|
||||||
@@ -147,20 +150,40 @@ dotnet run --project TaxBaik.Web
|
|||||||
|
|
||||||
배포는 **Gitea Actions CI/CD**만 사용합니다.
|
배포는 **Gitea Actions CI/CD**만 사용합니다.
|
||||||
|
|
||||||
master 브랜치에 푸시하면 자동으로:
|
master 브랜치에 푸시하면 파이프라인이 다음 단계를 수행합니다.
|
||||||
1. ✅ .NET 빌드 (Release)
|
1. .NET 빌드 (Release)
|
||||||
2. ✅ 단위 테스트 실행
|
2. 단위 테스트 실행
|
||||||
3. ✅ `TaxBaik.Web` 게시
|
3. Playwright 브라우저 검증 실행
|
||||||
4. ✅ 서버 반영 및 서비스 재시작
|
4. `TaxBaik.Web` 게시
|
||||||
5. ✅ `/taxbaik/`, `/taxbaik/admin/login`, `/taxbaik/api/auth/login` 헬스 체크
|
5. 원격 서버 배포 디렉토리 업로드 및 `taxbaik_active` 심링크 교체
|
||||||
|
6. systemd `taxbaik` 단일 서비스 재시작
|
||||||
|
7. `/taxbaik/`, `/taxbaik/admin/login`, `/taxbaik/blog/{slug}`, `/taxbaik/api/auth/login` 검증
|
||||||
|
|
||||||
|
배포 완료 판정은 위 단계가 모두 성공하고, 배포본 기준 Playwright E2E가 통과했을 때만 한다.
|
||||||
|
|
||||||
**필수 Gitea Secrets 설정:**
|
**필수 Gitea Secrets 설정:**
|
||||||
- `DEPLOY_USER`: kjh2064
|
- `DEPLOY_USER`: kjh2064
|
||||||
- `DEPLOY_HOST`: 178.104.200.7
|
- `DEPLOY_HOST`: 178.104.200.7
|
||||||
- `DEPLOY_SSH_KEY_B64`: base64로 인코딩한 SSH 개인키
|
- `DEPLOY_SSH_KEY_B64`: base64로 인코딩한 SSH 개인키
|
||||||
- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호
|
- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호
|
||||||
|
- `Admin__PasswordResetToken`: 관리자 비밀번호 재설정 API용 서버 비밀값
|
||||||
|
|
||||||
수동 배포는 사용하지 않습니다. 실패 시 [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)의 CI 점검 절차를 따릅니다.
|
배포는 Gitea Actions CI/CD로만 수행합니다. 수동 배포 경로는 CI 하네스로 차단되어 있으며, 실패 시 [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md)의 CI 점검 절차를 따릅니다.
|
||||||
|
|
||||||
|
## E2E / Smoke
|
||||||
|
|
||||||
|
공개/관리자 분리 검증은 아래 명령을 사용합니다.
|
||||||
|
|
||||||
|
로컬에서 실행할 때는 먼저 `npm run test:e2e`가 가리키는 대상 서버를 띄워둬야 합니다.
|
||||||
|
|
||||||
|
| 용도 | Bash | PowerShell |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Public smoke | `E2E_BASE_URL=https://www.taxbaik.com/taxbaik npm run test:e2e:public-smoke` | `$env:E2E_BASE_URL="https://www.taxbaik.com/taxbaik"; npm run test:e2e:public-smoke` |
|
||||||
|
| Admin smoke | `E2E_BASE_URL=https://www.taxbaik.com/taxbaik npm run test:e2e:admin-smoke` | `$env:E2E_BASE_URL="https://www.taxbaik.com/taxbaik"; npm run test:e2e:admin-smoke` |
|
||||||
|
|
||||||
|
직접 smoke 스크립트가 필요하면 이 한 줄만 쓰면 됩니다.
|
||||||
|
|
||||||
|
`ROOT_URL="https://www.taxbaik.com/" ADMIN_URL="https://www.taxbaik.com/taxbaik/admin/login" bash ./scripts/taxbaik-smoke.sh`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -262,7 +285,13 @@ echo $ConnectionStrings__Default
|
|||||||
|
|
||||||
## 문서
|
## 문서
|
||||||
|
|
||||||
- [CLAUDE.md](./CLAUDE.md) - LLM 개발 지침 (9개 섹션)
|
- [docs/INDEX.md](./docs/INDEX.md) - 현재 개발 기준 인덱스
|
||||||
|
- [docs/ENGINEERING_HARNESS.md](./docs/ENGINEERING_HARNESS.md) - 코드 품질, API-first, CI/CD 하네스
|
||||||
|
- [docs/DOUZONE_UX_GUIDE.md](./docs/DOUZONE_UX_GUIDE.md) - 더존식 어드민 UX 원칙과 템플릿 기준
|
||||||
|
- [docs/COMMON_CODE_POLICY.md](./docs/COMMON_CODE_POLICY.md) - 공통코드 저장값/컬럼 길이/하드코딩 금지 기준
|
||||||
|
- [docs/COMBO_POLICY.md](./docs/COMBO_POLICY.md) - 콤보/검색/선택 입력 정책
|
||||||
|
- [docs/ADMIN_PATTERN_CRITIQUE_WBS.md](./docs/ADMIN_PATTERN_CRITIQUE_WBS.md) - 어드민 패턴 비판 및 정량 WBS
|
||||||
|
- [CLAUDE.md](./CLAUDE.md) - 보조 LLM 개발 지침
|
||||||
- [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md) - 배포 완전 가이드
|
- [DEPLOYMENT_GUIDE.md](./DEPLOYMENT_GUIDE.md) - 배포 완전 가이드
|
||||||
- [SERVER_SETUP.sh](./SERVER_SETUP.sh) - 서버 자동 설치 스크립트
|
- [SERVER_SETUP.sh](./SERVER_SETUP.sh) - 서버 자동 설치 스크립트
|
||||||
|
|
||||||
@@ -330,6 +359,6 @@ echo $ConnectionStrings__Default
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**최종 상태**: ✅ **프로덕션 준비 완료**
|
**최종 상태**: 진행 중
|
||||||
|
|
||||||
모든 커밋이 한국어로 작성되었으며, Gitea에 업로드된 상태입니다.
|
완료 판정은 실제 빌드, 테스트, 배포 검증, 브라우저 E2E 통과로만 한다.
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# TaxBaik Server Setup Script
|
|
||||||
# Run on Ubuntu 26.04 server as root or with sudo
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "===== TaxBaik Server Setup ====="
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
DEPLOY_USER="kjh2064"
|
|
||||||
DB_NAME="taxbaikdb"
|
|
||||||
DB_USER="taxbaik"
|
|
||||||
DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 12)}" # Use env var or generate
|
|
||||||
DEPLOY_DIR="/home/$DEPLOY_USER"
|
|
||||||
|
|
||||||
echo -e "${BLUE}1. Installing .NET 8 Runtime${NC}"
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y dotnet-runtime-8.0 aspnetcore-runtime-8.0
|
|
||||||
|
|
||||||
echo -e "${BLUE}2. Installing PostgreSQL 18${NC}"
|
|
||||||
sudo apt-get install -y postgresql postgresql-contrib
|
|
||||||
|
|
||||||
echo -e "${BLUE}3. Creating database and user${NC}"
|
|
||||||
sudo -u postgres psql << EOF
|
|
||||||
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
|
|
||||||
CREATE DATABASE $DB_NAME OWNER $DB_USER;
|
|
||||||
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo -e "${BLUE}4. Creating deployment directories${NC}"
|
|
||||||
sudo -u $DEPLOY_USER mkdir -p $DEPLOY_DIR/deployments
|
|
||||||
sudo -u $DEPLOY_USER mkdir -p $DEPLOY_DIR/taxbaik_active
|
|
||||||
sudo -u $DEPLOY_USER mkdir -p $DEPLOY_DIR/taxbaik_admin_active
|
|
||||||
|
|
||||||
echo -e "${BLUE}5. Installing systemd service files${NC}"
|
|
||||||
sudo cp deploy/taxbaik.service /etc/systemd/system/
|
|
||||||
sudo cp deploy/taxbaik-admin.service /etc/systemd/system/
|
|
||||||
|
|
||||||
# Update environment variables in service files
|
|
||||||
sudo sed -i "s/YOUR_SECURE_PASSWORD_HERE/$DB_PASSWORD/g" /etc/systemd/system/taxbaik.service
|
|
||||||
sudo sed -i "s/YOUR_SECURE_PASSWORD_HERE/$DB_PASSWORD/g" /etc/systemd/system/taxbaik-admin.service
|
|
||||||
|
|
||||||
echo -e "${BLUE}6. Configuring Nginx${NC}"
|
|
||||||
sudo mkdir -p /etc/nginx/conf.d
|
|
||||||
sudo cp deploy/nginx-taxbaik-locations.conf /etc/nginx/conf.d/taxbaik.conf
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
|
|
||||||
echo -e "${BLUE}7. Enabling services${NC}"
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable taxbaik taxbaik-admin
|
|
||||||
sudo systemctl enable postgresql
|
|
||||||
|
|
||||||
echo -e "${GREEN}===== Setup Complete ====="
|
|
||||||
echo ""
|
|
||||||
echo "Database credentials:"
|
|
||||||
echo " Host: localhost"
|
|
||||||
echo " Database: $DB_NAME"
|
|
||||||
echo " User: $DB_USER"
|
|
||||||
echo " Password: $DB_PASSWORD"
|
|
||||||
echo ""
|
|
||||||
echo "Next steps:"
|
|
||||||
echo " 1. Copy the first deployment to ~/deployments/taxbaik_TIMESTAMP/"
|
|
||||||
echo " 2. Create symlinks:"
|
|
||||||
echo " ln -s ~/deployments/taxbaik_TIMESTAMP ~/taxbaik_active"
|
|
||||||
echo " ln -s ~/deployments/taxbaik_admin_TIMESTAMP ~/taxbaik_admin_active"
|
|
||||||
echo " 3. Start services:"
|
|
||||||
echo " sudo systemctl start taxbaik taxbaik-admin"
|
|
||||||
echo " 4. Verify:"
|
|
||||||
echo " sudo systemctl status taxbaik taxbaik-admin"
|
|
||||||
echo " curl http://127.0.0.1:5001/taxbaik"
|
|
||||||
echo " curl http://127.0.0.1:5002/taxbaik/admin/login"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace TaxBaik.Application.DTOs;
|
|
||||||
|
|
||||||
public class CreateBlogPostDto
|
|
||||||
{
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
public int? CategoryId { get; set; }
|
|
||||||
public string Tags { get; set; }
|
|
||||||
public string SeoTitle { get; set; }
|
|
||||||
public string SeoDescription { get; set; }
|
|
||||||
public string ThumbnailUrl { get; set; }
|
|
||||||
public bool IsPublished { get; set; }
|
|
||||||
public int AuthorId { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace TaxBaik.Application;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using TaxBaik.Application.Services;
|
|
||||||
|
|
||||||
public static class DependencyInjection
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddScoped<BlogService>();
|
|
||||||
services.AddScoped<InquiryService>();
|
|
||||||
services.AddScoped<CategoryService>();
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
namespace TaxBaik.Application.Services;
|
|
||||||
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using TaxBaik.Application.DTOs;
|
|
||||||
using TaxBaik.Domain.Entities;
|
|
||||||
using TaxBaik.Domain.Interfaces;
|
|
||||||
|
|
||||||
public class BlogService(IBlogPostRepository repository)
|
|
||||||
{
|
|
||||||
public async Task<BlogPost?> GetBySlugAsync(string slug, CancellationToken ct = default) =>
|
|
||||||
await repository.GetBySlugAsync(slug, ct);
|
|
||||||
|
|
||||||
public async Task<(IEnumerable<BlogPost>, int)> GetPublishedPagedAsync(
|
|
||||||
int page, int pageSize, int? categoryId = null, CancellationToken ct = default) =>
|
|
||||||
await repository.GetPublishedPagedAsync(page, pageSize, categoryId, ct);
|
|
||||||
|
|
||||||
public async Task<IEnumerable<BlogPost>> GetAllAsync(CancellationToken ct = default) =>
|
|
||||||
await repository.GetAllForAdminAsync(ct);
|
|
||||||
|
|
||||||
public async Task<IEnumerable<BlogPost>> GetAllForAdminAsync(CancellationToken ct = default) =>
|
|
||||||
await repository.GetAllForAdminAsync(ct);
|
|
||||||
|
|
||||||
public async Task<int> CreateAsync(BlogPost post, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
post.Slug = GenerateSlug(post.Title);
|
|
||||||
post.IsPublished = false;
|
|
||||||
return await repository.CreateAsync(post, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BlogPost> CreateAsync(CreateBlogPostDto dto, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var post = new BlogPost
|
|
||||||
{
|
|
||||||
Title = dto.Title,
|
|
||||||
Content = dto.Content,
|
|
||||||
CategoryId = dto.CategoryId,
|
|
||||||
Tags = dto.Tags,
|
|
||||||
SeoTitle = dto.SeoTitle,
|
|
||||||
SeoDescription = dto.SeoDescription,
|
|
||||||
ThumbnailUrl = dto.ThumbnailUrl,
|
|
||||||
IsPublished = dto.IsPublished,
|
|
||||||
AuthorId = dto.AuthorId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
var id = await CreateAsync(post, ct);
|
|
||||||
post.Id = id;
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateAsync(BlogPost post, CancellationToken ct = default) =>
|
|
||||||
await repository.UpdateAsync(post, ct);
|
|
||||||
|
|
||||||
public async Task<BlogPost?> UpdateAsync(int id, CreateBlogPostDto dto, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var post = await repository.GetByIdAsync(id, ct);
|
|
||||||
if (post == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
post.Title = dto.Title;
|
|
||||||
post.Content = dto.Content;
|
|
||||||
post.CategoryId = dto.CategoryId;
|
|
||||||
post.Tags = dto.Tags;
|
|
||||||
post.SeoTitle = dto.SeoTitle;
|
|
||||||
post.SeoDescription = dto.SeoDescription;
|
|
||||||
post.ThumbnailUrl = dto.ThumbnailUrl;
|
|
||||||
post.IsPublished = dto.IsPublished;
|
|
||||||
|
|
||||||
await UpdateAsync(post, ct);
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
|
|
||||||
await repository.DeleteAsync(id, ct);
|
|
||||||
|
|
||||||
public async Task IncrementViewCountAsync(int id, CancellationToken ct = default) =>
|
|
||||||
await repository.IncrementViewCountAsync(id, ct);
|
|
||||||
|
|
||||||
private static string GenerateSlug(string title)
|
|
||||||
{
|
|
||||||
var slug = Regex.Replace(title.ToLowerInvariant(), @"[^\w\s-]", "");
|
|
||||||
slug = Regex.Replace(slug, @"\s+", "-");
|
|
||||||
slug = Regex.Replace(slug, @"-+", "-").Trim('-');
|
|
||||||
return slug.Length > 100 ? slug[..100] : slug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
namespace TaxBaik.Application.Services;
|
|
||||||
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using TaxBaik.Domain.Entities;
|
|
||||||
using TaxBaik.Domain.Interfaces;
|
|
||||||
|
|
||||||
public class InquiryService(IInquiryRepository repository)
|
|
||||||
{
|
|
||||||
private static readonly Regex PhoneRegex = new(@"^01[0-9]-\d{3,4}-\d{4}$");
|
|
||||||
|
|
||||||
public async Task<int> SubmitAsync(
|
|
||||||
string name, string phone, string serviceType, string message,
|
|
||||||
string? ipAddress = null, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
throw new ValidationException("이름을 입력하세요.");
|
|
||||||
|
|
||||||
if (!PhoneRegex.IsMatch(phone))
|
|
||||||
throw new ValidationException("올바른 전화번호를 입력하세요. (예: 010-1234-5678)");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(message))
|
|
||||||
throw new ValidationException("문의 내용을 입력하세요.");
|
|
||||||
|
|
||||||
var inquiry = new Inquiry
|
|
||||||
{
|
|
||||||
Name = name.Trim(),
|
|
||||||
Phone = phone.Trim(),
|
|
||||||
ServiceType = serviceType ?? "기타",
|
|
||||||
Message = message.Trim(),
|
|
||||||
IpAddress = ipAddress,
|
|
||||||
Status = "new",
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
return await repository.CreateAsync(inquiry, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Inquiry?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
|
||||||
await repository.GetByIdAsync(id, ct);
|
|
||||||
|
|
||||||
public async Task<(IEnumerable<Inquiry>, int)> GetPagedAsync(
|
|
||||||
int page, int pageSize, string? status = null, CancellationToken ct = default) =>
|
|
||||||
await repository.GetPagedAsync(page, pageSize, status, ct);
|
|
||||||
|
|
||||||
public async Task UpdateStatusAsync(int id, string status, CancellationToken ct = default) =>
|
|
||||||
await repository.UpdateStatusAsync(id, status, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ValidationException : Exception
|
|
||||||
{
|
|
||||||
public ValidationException(string message) : base(message) { }
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace TaxBaik.Domain.Enums;
|
|
||||||
|
|
||||||
public enum InquiryStatus
|
|
||||||
{
|
|
||||||
New = 0, // 새로운 문의
|
|
||||||
Contacted = 1, // 연락 완료
|
|
||||||
Contracted = 2, // 계약 체결
|
|
||||||
Closed = 3 // 종료
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace TaxBaik.Domain.Interfaces;
|
|
||||||
|
|
||||||
using TaxBaik.Domain.Entities;
|
|
||||||
|
|
||||||
public interface IInquiryRepository
|
|
||||||
{
|
|
||||||
Task<int> CreateAsync(Inquiry inquiry, CancellationToken cancellationToken = default);
|
|
||||||
Task<Inquiry?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
|
||||||
Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
|
|
||||||
int page, int pageSize, string? status = null, CancellationToken cancellationToken = default);
|
|
||||||
Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
namespace TaxBaik.Infrastructure;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using TaxBaik.Domain.Interfaces;
|
|
||||||
using TaxBaik.Infrastructure.Data;
|
|
||||||
using TaxBaik.Infrastructure.Repositories;
|
|
||||||
|
|
||||||
public static class DependencyInjection
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddSingleton<IDbConnectionFactory, DbConnectionFactory>();
|
|
||||||
services.AddScoped<IAdminUserRepository, AdminUserRepository>();
|
|
||||||
services.AddScoped<ICategoryRepository, CategoryRepository>();
|
|
||||||
services.AddScoped<IBlogPostRepository, BlogPostRepository>();
|
|
||||||
services.AddScoped<IInquiryRepository, InquiryRepository>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
namespace TaxBaik.Infrastructure.Repositories;
|
|
||||||
|
|
||||||
using Dapper;
|
|
||||||
using TaxBaik.Domain.Entities;
|
|
||||||
using TaxBaik.Domain.Interfaces;
|
|
||||||
|
|
||||||
public class InquiryRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), IInquiryRepository
|
|
||||||
{
|
|
||||||
public async Task<int> CreateAsync(Inquiry inquiry, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
using var conn = Conn();
|
|
||||||
return await conn.QueryFirstAsync<int>(
|
|
||||||
@"INSERT INTO inquiries (name, phone, email, service_type, message, status, ip_address, created_at)
|
|
||||||
VALUES (@Name, @Phone, @Email, @ServiceType, @Message, @Status, @IpAddress, NOW())
|
|
||||||
RETURNING id",
|
|
||||||
inquiry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Inquiry?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
using var conn = Conn();
|
|
||||||
return await conn.QueryFirstOrDefaultAsync<Inquiry>(
|
|
||||||
"SELECT id, name, phone, email, service_type, message, status, ip_address, created_at FROM inquiries WHERE id = @Id",
|
|
||||||
new { Id = id });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
|
|
||||||
int page, int pageSize, string? status = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
using var conn = Conn();
|
|
||||||
var offset = (page - 1) * pageSize;
|
|
||||||
|
|
||||||
using var reader = await conn.QueryMultipleAsync(
|
|
||||||
@"SELECT id, name, phone, email, service_type, message, status, ip_address, created_at
|
|
||||||
FROM inquiries
|
|
||||||
WHERE @Status::text IS NULL OR status = @Status
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT @PageSize OFFSET @Offset;
|
|
||||||
|
|
||||||
SELECT COUNT(*) FROM inquiries
|
|
||||||
WHERE @Status::text IS NULL OR status = @Status;",
|
|
||||||
new { Status = status, PageSize = pageSize, Offset = offset });
|
|
||||||
|
|
||||||
var items = (await reader.ReadAsync<Inquiry>()).ToList();
|
|
||||||
var total = await reader.ReadFirstAsync<int>();
|
|
||||||
|
|
||||||
return (items, total);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
using var conn = Conn();
|
|
||||||
await conn.ExecuteAsync("UPDATE inquiries SET status = @Status WHERE id = @Id", new { Id = id, Status = status });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>백원숙 세무회계 - 관리자</title>
|
|
||||||
<base href="/taxbaik/" />
|
|
||||||
<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="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/taxbaik/css/admin.css" />
|
|
||||||
<component type="typeof(HeadOutlet)" render-mode="InteractiveServer" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<Routes @rendermode="RenderMode.InteractiveServer" />
|
|
||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
|
||||||
<script src="_framework/blazor.web.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
@using TaxBaik.Domain.Interfaces
|
|
||||||
@inject IInquiryRepository InquiryRepository
|
|
||||||
|
|
||||||
<MudSimpleTable Striped="true" Dense="true" Class="mt-4">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>이름</th>
|
|
||||||
<th>전화</th>
|
|
||||||
<th>분야</th>
|
|
||||||
<th>메시지</th>
|
|
||||||
<th>날짜</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var inquiry in filteredInquiries)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@inquiry.Name</td>
|
|
||||||
<td>@inquiry.Phone</td>
|
|
||||||
<td>@inquiry.ServiceType</td>
|
|
||||||
<td>@inquiry.Message.Substring(0, Math.Min(30, inquiry.Message.Length))...</td>
|
|
||||||
<td>@inquiry.CreatedAt.ToString("yyyy-MM-dd")</td>
|
|
||||||
<td>
|
|
||||||
<MudButton Size="Size.Small" Variant="Variant.Text"
|
|
||||||
Href="@($"/taxbaik/admin/inquiries/{inquiry.Id}")">보기</MudButton>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</MudSimpleTable>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter]
|
|
||||||
public string Status { get; set; } = "";
|
|
||||||
|
|
||||||
private List<Domain.Entities.Inquiry> inquiries = [];
|
|
||||||
private List<Domain.Entities.Inquiry> filteredInquiries = [];
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
var (items, _) = await InquiryRepository.GetPagedAsync(1, 1000);
|
|
||||||
inquiries = items.ToList();
|
|
||||||
FilterInquiries();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FilterInquiries()
|
|
||||||
{
|
|
||||||
filteredInquiries = string.IsNullOrEmpty(Status)
|
|
||||||
? inquiries
|
|
||||||
: inquiries.Where(x => x.Status == Status).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
|
||||||
{
|
|
||||||
FilterInquiries();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Authorization
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<AuthorizeView>
|
|
||||||
<Authorized>
|
|
||||||
<MudThemeProvider />
|
|
||||||
<MudDialogProvider />
|
|
||||||
<MudSnackbarProvider />
|
|
||||||
|
|
||||||
<MudLayout>
|
|
||||||
<MudAppBar Elevation="1">
|
|
||||||
<MudText Typo="Typo.h6" Class="ml-3">백원숙 세무회계 관리자</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
<MudButton Color="Color.Inherit" Href="/taxbaik">공개 사이트</MudButton>
|
|
||||||
<MudButton Href="/taxbaik/admin/logout">로그아웃</MudButton>
|
|
||||||
</MudAppBar>
|
|
||||||
|
|
||||||
<MudDrawer @bind-open="@drawerOpen" Elevation="1">
|
|
||||||
<MudNavMenu>
|
|
||||||
<MudNavLink Href="/taxbaik/admin/" Match="NavLinkMatch.All">📊 대시보드</MudNavLink>
|
|
||||||
<MudNavLink Href="/taxbaik/admin/blog">📝 블로그 관리</MudNavLink>
|
|
||||||
<MudNavLink Href="/taxbaik/admin/inquiries">💬 문의 관리</MudNavLink>
|
|
||||||
<MudNavLink Href="/taxbaik/admin/settings">⚙️ 설정</MudNavLink>
|
|
||||||
</MudNavMenu>
|
|
||||||
</MudDrawer>
|
|
||||||
|
|
||||||
<MudMainContent>
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="my-4">
|
|
||||||
@Body
|
|
||||||
</MudContainer>
|
|
||||||
</MudMainContent>
|
|
||||||
</MudLayout>
|
|
||||||
</Authorized>
|
|
||||||
<NotAuthorized>
|
|
||||||
@Body
|
|
||||||
</NotAuthorized>
|
|
||||||
</AuthorizeView>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private bool drawerOpen = true;
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
@page "/admin/blog/create"
|
|
||||||
@using TaxBaik.Application.Services
|
|
||||||
@using TaxBaik.Domain.Interfaces
|
|
||||||
@attribute [Authorize]
|
|
||||||
@inject BlogService BlogService
|
|
||||||
@inject ICategoryRepository CategoryRepository
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject Snackbar Snackbar
|
|
||||||
|
|
||||||
<PageTitle>새 포스트 작성</PageTitle>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">📝 새 포스트</MudText>
|
|
||||||
|
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
|
||||||
<MudForm @ref="form">
|
|
||||||
<MudTextField @bind-Value="model.Title" Label="제목"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4" Required="true" />
|
|
||||||
|
|
||||||
<MudSelect @bind-Value="model.CategoryId" Label="카테고리"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4">
|
|
||||||
@foreach (var category in categories)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@category.Id">@category.Name</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.Content" Label="본문"
|
|
||||||
Variant="Variant.Outlined" Lines="10" Class="mb-4" Required="true" />
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.Tags" Label="태그 (쉼표로 구분)"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.SeoTitle" Label="SEO 제목"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="model.SeoDescription" Label="SEO 설명"
|
|
||||||
Variant="Variant.Outlined" Lines="3" Class="mb-4" />
|
|
||||||
|
|
||||||
<MudCheckBox @bind-Checked="model.IsPublished" Label="즉시 발행" Class="mb-4" />
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
|
||||||
@onclick="SavePost">저장</MudButton>
|
|
||||||
<MudButton Variant="Variant.Outlined" @onclick="@(() => Navigation.NavigateTo("/taxbaik/admin/blog"))">
|
|
||||||
취소
|
|
||||||
</MudButton>
|
|
||||||
</div>
|
|
||||||
</MudForm>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private MudForm form;
|
|
||||||
private List<Domain.Entities.Category> categories = [];
|
|
||||||
private CreatePostModel model = new();
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
categories = (await CategoryRepository.GetAllAsync()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SavePost()
|
|
||||||
{
|
|
||||||
// TODO: Implement BlogService.CreateAsync
|
|
||||||
Navigation.NavigateTo("/taxbaik/admin/blog");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CreatePostModel
|
|
||||||
{
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
public int CategoryId { get; set; }
|
|
||||||
public string Tags { get; set; }
|
|
||||||
public string SeoTitle { get; set; }
|
|
||||||
public string SeoDescription { get; set; }
|
|
||||||
public bool IsPublished { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
@page "/admin/blog"
|
|
||||||
@attribute [Authorize]
|
|
||||||
@inject IApiClient ApiClient
|
|
||||||
@inject DialogService DialogService
|
|
||||||
@inject Snackbar Snackbar
|
|
||||||
|
|
||||||
<PageTitle>블로그 관리</PageTitle>
|
|
||||||
|
|
||||||
<div class="mb-4 d-flex justify-content-between align-items-center">
|
|
||||||
<MudText Typo="Typo.h5">📝 블로그 관리</MudText>
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
|
||||||
Href="/taxbaik/admin/blog/create">새 포스트</MudButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MudDataGrid Items="@posts" Striped="true" Hoverable="true" Loading="@isLoading">
|
|
||||||
<Columns>
|
|
||||||
<PropertyColumn Property="x => x.Title" Title="제목" />
|
|
||||||
<PropertyColumn Property="x => x.IsPublished" Title="발행">
|
|
||||||
<CellTemplate Context="cell">
|
|
||||||
<MudCheckBox @bind-Checked="@cell.Item.IsPublished" />
|
|
||||||
</CellTemplate>
|
|
||||||
</PropertyColumn>
|
|
||||||
<PropertyColumn Property="x => x.ViewCount" Title="조회수" />
|
|
||||||
<PropertyColumn Property="x => x.CreatedAt" Title="작성일" Format="yyyy-MM-dd" />
|
|
||||||
<TemplateColumn>
|
|
||||||
<CellTemplate Context="cell">
|
|
||||||
<MudButton Variant="Variant.Text" Color="Color.Primary"
|
|
||||||
Href="@($"/taxbaik/admin/blog/{cell.Item.Id}/edit")">수정</MudButton>
|
|
||||||
<MudButton Variant="Variant.Text" Color="Color.Error"
|
|
||||||
@onclick="@(async () => await DeletePost(cell.Item.Id))">삭제</MudButton>
|
|
||||||
</CellTemplate>
|
|
||||||
</TemplateColumn>
|
|
||||||
</Columns>
|
|
||||||
</MudDataGrid>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private List<TaxBaik.Domain.Entities.BlogPost> posts = [];
|
|
||||||
private bool isLoading = true;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await LoadPosts();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadPosts()
|
|
||||||
{
|
|
||||||
isLoading = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var items = await ApiClient.GetAsync<List<TaxBaik.Domain.Entities.BlogPost>>("blog/admin/all");
|
|
||||||
posts = items ?? [];
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TogglePublish(int postId, bool isPublished)
|
|
||||||
{
|
|
||||||
// Publish status update via API
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeletePost(int postId)
|
|
||||||
{
|
|
||||||
await ApiClient.DeleteAsync($"blog/{postId}");
|
|
||||||
await LoadPosts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
@page "/admin/dashboard"
|
|
||||||
@using TaxBaik.Application.Services
|
|
||||||
@using TaxBaik.Domain.Interfaces
|
|
||||||
@attribute [Authorize]
|
|
||||||
@inject IInquiryRepository InquiryRepository
|
|
||||||
@inject BlogService BlogService
|
|
||||||
|
|
||||||
<PageTitle>대시보드</PageTitle>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">📊 대시보드</MudText>
|
|
||||||
|
|
||||||
<MudGrid>
|
|
||||||
<MudItem xs="12" sm="6" md="3">
|
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
|
||||||
<MudText Typo="Typo.subtitle2">이번달 문의</MudText>
|
|
||||||
<MudText Typo="Typo.h4">@thisMonthInquiries</MudText>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12" sm="6" md="3">
|
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
|
||||||
<MudText Typo="Typo.subtitle2">신규 문의</MudText>
|
|
||||||
<MudText Typo="Typo.h4">@newInquiries</MudText>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12" sm="6" md="3">
|
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
|
||||||
<MudText Typo="Typo.subtitle2">전체 포스트</MudText>
|
|
||||||
<MudText Typo="Typo.h4">@totalPosts</MudText>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12" sm="6" md="3">
|
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
|
||||||
<MudText Typo="Typo.subtitle2">발행된 포스트</MudText>
|
|
||||||
<MudText Typo="Typo.h4">@publishedPosts</MudText>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
|
||||||
<MudText Typo="Typo.h6" Class="mb-3">최근 문의</MudText>
|
|
||||||
<MudSimpleTable Striped="true" Dense="true">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>이름</th>
|
|
||||||
<th>전화</th>
|
|
||||||
<th>분야</th>
|
|
||||||
<th>상태</th>
|
|
||||||
<th>날짜</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var inquiry in recentInquiries)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@inquiry.Name</td>
|
|
||||||
<td>@inquiry.Phone</td>
|
|
||||||
<td>@inquiry.ServiceType</td>
|
|
||||||
<td>
|
|
||||||
<MudChip Size="Size.Small"
|
|
||||||
Color="@(inquiry.Status == "new" ? Color.Warning : inquiry.Status == "contacted" ? Color.Info : Color.Success)">
|
|
||||||
@inquiry.Status
|
|
||||||
</MudChip>
|
|
||||||
</td>
|
|
||||||
<td>@inquiry.CreatedAt.ToString("yyyy-MM-dd")</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</MudSimpleTable>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private int thisMonthInquiries = 0;
|
|
||||||
private int newInquiries = 0;
|
|
||||||
private int totalPosts = 0;
|
|
||||||
private int publishedPosts = 0;
|
|
||||||
private List<Domain.Entities.Inquiry> recentInquiries = [];
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
var (inquiries, total) = await InquiryRepository.GetPagedAsync(1, 100);
|
|
||||||
recentInquiries = inquiries.OrderByDescending(x => x.CreatedAt).Take(5).ToList();
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
thisMonthInquiries = inquiries.Count(x => x.CreatedAt.Year == now.Year && x.CreatedAt.Month == now.Month);
|
|
||||||
newInquiries = inquiries.Count(x => x.Status == "new");
|
|
||||||
totalPosts = 0; // TODO: get from blog service
|
|
||||||
publishedPosts = 0; // TODO: get from blog service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
@page "/admin/inquiries/{InquiryId:int}"
|
|
||||||
@using TaxBaik.Domain.Interfaces
|
|
||||||
@attribute [Authorize]
|
|
||||||
@inject IInquiryRepository InquiryRepository
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
<PageTitle>문의 상세</PageTitle>
|
|
||||||
|
|
||||||
@if (inquiry != null)
|
|
||||||
{
|
|
||||||
<MudButton Variant="Variant.Text" @onclick="@(() => Navigation.NavigateTo("/taxbaik/admin/inquiries"))">
|
|
||||||
← 돌아가기
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
|
||||||
<MudGrid>
|
|
||||||
<MudItem xs="12" md="6">
|
|
||||||
<MudText Typo="Typo.subtitle1">이름</MudText>
|
|
||||||
<MudText>@inquiry.Name</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="6">
|
|
||||||
<MudText Typo="Typo.subtitle1">연락처</MudText>
|
|
||||||
<MudText>@inquiry.Phone</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="6">
|
|
||||||
<MudText Typo="Typo.subtitle1">이메일</MudText>
|
|
||||||
<MudText>@inquiry.Email</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="6">
|
|
||||||
<MudText Typo="Typo.subtitle1">분야</MudText>
|
|
||||||
<MudText>@inquiry.ServiceType</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudText Typo="Typo.subtitle1">메시지</MudText>
|
|
||||||
<MudText>@inquiry.Message</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudText Typo="Typo.subtitle1">상태</MudText>
|
|
||||||
<MudSelect @bind-Value="inquiry.Status" Label="상태 변경">
|
|
||||||
<MudSelectItem Value="@("new")">신규</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("contacted")">연락함</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("completed")">완료</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudText>문의를 찾을 수 없습니다.</MudText>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter]
|
|
||||||
public int InquiryId { get; set; }
|
|
||||||
|
|
||||||
private Domain.Entities.Inquiry inquiry;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
var (inquiries, _) = await InquiryRepository.GetPagedAsync(1, 1000);
|
|
||||||
inquiry = inquiries.FirstOrDefault(x => x.Id == InquiryId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
@page "/admin/inquiries"
|
|
||||||
@using TaxBaik.Domain.Interfaces
|
|
||||||
@attribute [Authorize]
|
|
||||||
@inject IInquiryRepository InquiryRepository
|
|
||||||
|
|
||||||
<PageTitle>문의 관리</PageTitle>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">💬 문의 관리</MudText>
|
|
||||||
|
|
||||||
<MudTabs>
|
|
||||||
<MudTabPanel Text="전체">
|
|
||||||
<InquiryTable Status="" />
|
|
||||||
</MudTabPanel>
|
|
||||||
<MudTabPanel Text="신규">
|
|
||||||
<InquiryTable Status="new" />
|
|
||||||
</MudTabPanel>
|
|
||||||
<MudTabPanel Text="연락함">
|
|
||||||
<InquiryTable Status="contacted" />
|
|
||||||
</MudTabPanel>
|
|
||||||
<MudTabPanel Text="완료">
|
|
||||||
<InquiryTable Status="completed" />
|
|
||||||
</MudTabPanel>
|
|
||||||
</MudTabs>
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
@page "/admin/login"
|
|
||||||
@using System.ComponentModel.DataAnnotations
|
|
||||||
@layout TaxBaik.Web.Components.Admin.Layout.BlankLayout
|
|
||||||
@attribute [AllowAnonymous]
|
|
||||||
@inject IApiClient ApiClient
|
|
||||||
@inject NavigationManager NavigationManager
|
|
||||||
@inject CustomAuthenticationStateProvider AuthStateProvider
|
|
||||||
|
|
||||||
<PageTitle>로그인</PageTitle>
|
|
||||||
|
|
||||||
<MudThemeProvider />
|
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Small" Class="d-flex align-center justify-center" Style="min-height: 100vh;">
|
|
||||||
<MudPaper Class="pa-8" Elevation="3" Style="width: 100%; max-width: 400px;">
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-6 text-center">관리자 로그인</MudText>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<InputText 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;"
|
|
||||||
placeholder="사용자명"
|
|
||||||
autocomplete="username"
|
|
||||||
@bind-Value="model.Username" />
|
|
||||||
|
|
||||||
<InputText type="password"
|
|
||||||
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;"
|
|
||||||
placeholder="비밀번호"
|
|
||||||
autocomplete="current-password"
|
|
||||||
@bind-Value="model.Password" />
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(errorMessage))
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Error" Class="mb-4">@errorMessage</MudAlert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button type="button"
|
|
||||||
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;"
|
|
||||||
@onclick="HandleLogin"
|
|
||||||
disabled="@isLoading">
|
|
||||||
@if (isLoading)
|
|
||||||
{
|
|
||||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
|
||||||
<span>로그인 중...</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>로그인</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
</MudContainer>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private bool isLoading = false;
|
|
||||||
private string errorMessage = "";
|
|
||||||
|
|
||||||
private LoginModel model = new();
|
|
||||||
|
|
||||||
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?.Token == null)
|
|
||||||
{
|
|
||||||
errorMessage = "사용자명 또는 비밀번호가 올바르지 않습니다.";
|
|
||||||
isLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await AuthStateProvider.LoginAsync(response.Token);
|
|
||||||
NavigationManager.NavigateTo("/taxbaik/admin/dashboard", forceLoad: false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
errorMessage = "로그인 중 오류가 발생했습니다.";
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoginResponse
|
|
||||||
{
|
|
||||||
public string Token { get; set; } = "";
|
|
||||||
public int ExpiresIn { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoginModel
|
|
||||||
{
|
|
||||||
public string Username { get; set; } = "";
|
|
||||||
public string Password { get; set; } = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
@page "/admin/settings"
|
|
||||||
@using TaxBaik.Domain.Interfaces
|
|
||||||
@attribute [Authorize]
|
|
||||||
@inject Snackbar Snackbar
|
|
||||||
|
|
||||||
<PageTitle>설정</PageTitle>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">⚙️ 사이트 설정</MudText>
|
|
||||||
|
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
|
||||||
<MudForm>
|
|
||||||
<MudTextField @bind-Value="phone" Label="전화번호"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="email" Label="이메일"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="kakaoUrl" Label="카카오 채널 URL"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="instagramUrl" Label="인스타그램"
|
|
||||||
Variant="Variant.Outlined" Class="mb-4" />
|
|
||||||
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
|
||||||
@onclick="SaveSettings">저장</MudButton>
|
|
||||||
</MudForm>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private string phone = "010-4122-8268";
|
|
||||||
private string email = "taxbaik5668@gmail.com";
|
|
||||||
private string kakaoUrl = "http://pf.kakao.com/_xoxchTX";
|
|
||||||
private string instagramUrl = "https://www.instagram.com/taxtory5668/";
|
|
||||||
|
|
||||||
private async Task SaveSettings()
|
|
||||||
{
|
|
||||||
// TODO: Save settings to database
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Routing
|
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
|
||||||
|
|
||||||
<CascadingAuthenticationState>
|
|
||||||
<Router AppAssembly="typeof(Program).Assembly">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)" />
|
|
||||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
|
||||||
</Found>
|
|
||||||
<NotFound>
|
|
||||||
<PageTitle>찾을 수 없음</PageTitle>
|
|
||||||
<LayoutView Layout="typeof(TaxBaik.Web.Components.Admin.Layout.MainLayout)">
|
|
||||||
<p>요청한 페이지를 찾을 수 없습니다.</p>
|
|
||||||
</LayoutView>
|
|
||||||
</NotFound>
|
|
||||||
</Router>
|
|
||||||
</CascadingAuthenticationState>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using TaxBaik.Web.Services;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class AuthController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly AuthService _authService;
|
|
||||||
|
|
||||||
public AuthController(AuthService authService)
|
|
||||||
{
|
|
||||||
_authService = authService;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("login")]
|
|
||||||
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
|
||||||
return BadRequest(new { message = "Username and password are required" });
|
|
||||||
|
|
||||||
var token = await _authService.AuthenticateAndGenerateTokenAsync(request.Username, request.Password);
|
|
||||||
if (token == null)
|
|
||||||
return Unauthorized(new { message = "Invalid username or password" });
|
|
||||||
|
|
||||||
return Ok(new { token, expiresIn = 28800 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoginRequest
|
|
||||||
{
|
|
||||||
public string Username { get; set; } = string.Empty;
|
|
||||||
public string Password { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using TaxBaik.Application.Services;
|
|
||||||
using TaxBaik.Application.DTOs;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class BlogController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly BlogService _blogService;
|
|
||||||
|
|
||||||
public BlogController(BlogService blogService)
|
|
||||||
{
|
|
||||||
_blogService = blogService;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetPublished([FromQuery] int page = 1, [FromQuery] int pageSize = 12)
|
|
||||||
{
|
|
||||||
var (items, total) = await _blogService.GetPublishedPagedAsync(page, pageSize);
|
|
||||||
return Ok(new { data = items, total, page, pageSize });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{slug}")]
|
|
||||||
public async Task<IActionResult> GetBySlug(string slug)
|
|
||||||
{
|
|
||||||
var post = await _blogService.GetBySlugAsync(slug);
|
|
||||||
if (post == null)
|
|
||||||
return NotFound(new { message = "Post not found" });
|
|
||||||
return Ok(post);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("admin/all")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> GetAll()
|
|
||||||
{
|
|
||||||
var posts = await _blogService.GetAllAsync();
|
|
||||||
return Ok(posts);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> Create([FromBody] CreateBlogPostDto dto)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(dto.Title) || string.IsNullOrWhiteSpace(dto.Content))
|
|
||||||
return BadRequest(new { message = "Title and content are required" });
|
|
||||||
|
|
||||||
var result = await _blogService.CreateAsync(dto);
|
|
||||||
return CreatedAtAction(nameof(GetBySlug), new { slug = result.Slug }, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> Update(int id, [FromBody] CreateBlogPostDto dto)
|
|
||||||
{
|
|
||||||
var result = await _blogService.UpdateAsync(id, dto);
|
|
||||||
if (result == null)
|
|
||||||
return NotFound(new { message = "Post not found" });
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> Delete(int id)
|
|
||||||
{
|
|
||||||
await _blogService.DeleteAsync(id);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using TaxBaik.Application.Services;
|
|
||||||
using TaxBaik.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class InquiryController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly InquiryService _inquiryService;
|
|
||||||
private readonly IInquiryRepository _inquiryRepository;
|
|
||||||
|
|
||||||
public InquiryController(InquiryService inquiryService, IInquiryRepository inquiryRepository)
|
|
||||||
{
|
|
||||||
_inquiryService = inquiryService;
|
|
||||||
_inquiryRepository = inquiryRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Submit([FromBody] SubmitInquiryRequest request)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Phone))
|
|
||||||
return BadRequest(new { message = "Name and phone are required" });
|
|
||||||
|
|
||||||
await _inquiryService.SubmitAsync(request.Name, request.Phone, request.ServiceType, request.Message);
|
|
||||||
return Ok(new { message = "Inquiry submitted successfully" });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> GetPaged([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
|
||||||
{
|
|
||||||
var (inquiries, total) = await _inquiryRepository.GetPagedAsync(page, pageSize);
|
|
||||||
return Ok(new { data = inquiries, total, page, pageSize });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> GetById(int id)
|
|
||||||
{
|
|
||||||
var inquiry = await _inquiryRepository.GetByIdAsync(id);
|
|
||||||
if (inquiry == null)
|
|
||||||
return NotFound(new { message = "Inquiry not found" });
|
|
||||||
return Ok(inquiry);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}/status")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> UpdateStatus(int id, [FromBody] UpdateStatusRequest request)
|
|
||||||
{
|
|
||||||
var inquiry = await _inquiryRepository.GetByIdAsync(id);
|
|
||||||
if (inquiry == null)
|
|
||||||
return NotFound(new { message = "Inquiry not found" });
|
|
||||||
|
|
||||||
await _inquiryRepository.UpdateStatusAsync(id, request.Status);
|
|
||||||
return Ok(new { message = "Status updated" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SubmitInquiryRequest
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string Phone { get; set; } = string.Empty;
|
|
||||||
public string? Email { get; set; }
|
|
||||||
public string ServiceType { get; set; } = string.Empty;
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UpdateStatusRequest
|
|
||||||
{
|
|
||||||
public string Status { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
@page
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "소개 | 백원숙 세무회계";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container py-5">
|
|
||||||
<h1 class="fw-bold mb-5">백원숙 세무사</h1>
|
|
||||||
|
|
||||||
<div class="row g-5">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<p class="lead">사업자 세무, 부동산 거래, 가족 자산 관리 등 종합적인 세무 컨설팅을 제공합니다.</p>
|
|
||||||
<p>10년 이상의 풍부한 경험과 3개의 국가자격증을 바탕으로, 각 클라이언트의 상황에 맞는 맞춤형 솔루션을 제시합니다.</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="bg-light p-4 rounded">
|
|
||||||
<h5 class="fw-bold mb-3">보유 자격증</h5>
|
|
||||||
<div class="mb-3">
|
|
||||||
<p class="mb-1">🎓 <strong>세무사</strong></p>
|
|
||||||
<small class="text-muted">2015년 자격취득</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>
|
|
||||||
|
|
||||||
<hr class="my-5" />
|
|
||||||
|
|
||||||
<h2 class="fw-bold mb-4">서비스 철학</h2>
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-md-4 text-center">
|
|
||||||
<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 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">
|
|
||||||
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">상담 신청하기</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
@page
|
|
||||||
@model TaxBaik.Web.Pages.ContactModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "상담 신청 | 백원숙 세무회계";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container py-5" style="max-width: 600px;">
|
|
||||||
<h1 class="fw-bold mb-5">상담 신청</h1>
|
|
||||||
|
|
||||||
@if (TempData["Success"] != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
@TempData["Success"]
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">이름 <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="name" name="Name" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="phone" class="form-label">전화번호 <span class="text-danger">*</span></label>
|
|
||||||
<input type="tel" class="form-control" id="phone" name="Phone" placeholder="010-0000-0000" required />
|
|
||||||
<small class="form-text text-muted">형식: 010-0000-0000</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email" class="form-label">이메일</label>
|
|
||||||
<input type="email" class="form-control" id="email" name="Email" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="service" class="form-label">상담분야</label>
|
|
||||||
<select class="form-select" id="service" name="ServiceType">
|
|
||||||
<option value="기장">사업자 기장</option>
|
|
||||||
<option value="양도세">부동산 양도세</option>
|
|
||||||
<option value="종소세">종합소득세</option>
|
|
||||||
<option value="증여상속">증여·상속세</option>
|
|
||||||
<option value="기타">기타</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="message" class="form-label">문의내용 <span class="text-danger">*</span></label>
|
|
||||||
<textarea class="form-control" id="message" name="Message" rows="5" required></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="agree" name="Agree" required />
|
|
||||||
<label class="form-check-label" for="agree">
|
|
||||||
개인정보 수집·이용에 동의합니다
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg w-100">상담신청</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr class="my-5" />
|
|
||||||
|
|
||||||
<h5 class="fw-bold mb-3">빠른 상담을 원하시나요?</h5>
|
|
||||||
<p>카카오톡 채널을 통해 더 빠르게 상담받을 수 있습니다.</p>
|
|
||||||
<div class="gap-2 d-flex flex-wrap">
|
|
||||||
<a href="http://pf.kakao.com/_xoxchTX" target="_blank" class="btn btn-warning">카카오톡 채널 문의</a>
|
|
||||||
<a href="tel:010-4122-8268" class="btn btn-outline-primary">전화 상담</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using TaxBaik.Web.Services;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Pages;
|
|
||||||
|
|
||||||
public class ContactModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly IApiClient _apiClient;
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public string Phone { get; set; } = "";
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public string? Email { get; set; }
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public string ServiceType { get; set; } = "기타";
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public string Message { get; set; } = "";
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public bool Agree { get; set; }
|
|
||||||
|
|
||||||
public ContactModel(IApiClient apiClient)
|
|
||||||
{
|
|
||||||
_apiClient = apiClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostAsync()
|
|
||||||
{
|
|
||||||
if (!ModelState.IsValid || !Agree)
|
|
||||||
return Page();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var inquiry = new
|
|
||||||
{
|
|
||||||
Name,
|
|
||||||
Phone,
|
|
||||||
Email,
|
|
||||||
ServiceType,
|
|
||||||
Message
|
|
||||||
};
|
|
||||||
|
|
||||||
await _apiClient.PostAsync<object>("inquiry", inquiry);
|
|
||||||
TempData["Success"] = "상담 신청이 접수되었습니다. 빠른 시간 내에 연락드리겠습니다.";
|
|
||||||
return RedirectToPage();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("", "시스템 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
@page
|
|
||||||
@model TaxBaik.Web.Pages.IndexModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "백원숙 세무회계 | 사업자·부동산·증여 세무 상담";
|
|
||||||
ViewData["Description"] = "사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 온라인 맞춤 상담 제공.";
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Hero Section — 강임팩트 -->
|
|
||||||
<section class="hero-section text-white pt-5 pb-4">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row align-items-center py-4">
|
|
||||||
<div class="col-lg-7">
|
|
||||||
<span class="badge bg-primary-badge mb-3">경험 있는 세무사의 맞춤 전략</span>
|
|
||||||
<h1 class="mb-3">
|
|
||||||
세금과 자산<br/>
|
|
||||||
<span style="color: #E8E4D8;">한 번에 해결하는</span>
|
|
||||||
</h1>
|
|
||||||
<p class="fs-5 mb-4" style="line-height: 1.8; opacity: 0.95;">
|
|
||||||
사업자 세무, 부동산 거래, 가족자산 관리를 위한<br/>
|
|
||||||
통합 솔루션을 제공합니다.
|
|
||||||
</p>
|
|
||||||
<div class="d-flex gap-3 flex-wrap">
|
|
||||||
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">무료 상담 신청</a>
|
|
||||||
<a href="javascript:void(0);" class="btn btn-outline-primary btn-lg" onclick="openKakao()" style="border-color: white; color: white;">
|
|
||||||
💬 카카오 채널 문의
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-5 d-none d-lg-block text-center">
|
|
||||||
<div style="font-size: 120px; opacity: 0.15;">📋</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 신뢰도 스트립 — 자격과 경험 -->
|
|
||||||
<section class="trust-strip">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="trust-item">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 서비스 영역 — 전문성 강조 -->
|
|
||||||
<section class="py-5">
|
|
||||||
<div class="container">
|
|
||||||
<div class="text-center mb-5">
|
|
||||||
<h2 class="section-title">전문 서비스</h2>
|
|
||||||
<p class="fs-6 text-muted" style="max-width: 600px; margin: 0 auto;">
|
|
||||||
각 분야의 복잡한 세무 이슈를 경험과 노하우로 해결합니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-4">
|
|
||||||
<!-- 사업자 세무 -->
|
|
||||||
<div class="col-lg-4 col-md-6">
|
|
||||||
<div class="card service-card h-100">
|
|
||||||
<div class="service-icon">🏪</div>
|
|
||||||
<div class="card-body pt-0">
|
|
||||||
<h3 class="card-title">사업자 세무</h3>
|
|
||||||
<ul class="list-unstyled small mb-3">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 부동산 세금 -->
|
|
||||||
<div class="col-lg-4 col-md-6">
|
|
||||||
<div class="card service-card h-100">
|
|
||||||
<div class="service-icon">🏠</div>
|
|
||||||
<div class="card-body pt-0">
|
|
||||||
<h3 class="card-title">부동산 세금</h3>
|
|
||||||
<ul class="list-unstyled small mb-3">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 가족자산 & 증여 -->
|
|
||||||
<div class="col-lg-4 col-md-6">
|
|
||||||
<div class="card service-card h-100">
|
|
||||||
<div class="service-icon">👨👩👧👦</div>
|
|
||||||
<div class="card-body pt-0">
|
|
||||||
<h3 class="card-title">가족자산 관리</h3>
|
|
||||||
<ul class="list-unstyled small mb-3">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 상담 프로세스 -->
|
|
||||||
<section class="py-5" style="background: #F9F7F3;">
|
|
||||||
<div class="container">
|
|
||||||
<div class="text-center mb-5">
|
|
||||||
<h2 class="section-title">상담 과정</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-md-3 text-center mb-4 mb-md-0">
|
|
||||||
<div style="width: 80px; height: 80px; margin: 0 auto 1rem; background: linear-gradient(135deg, #C89D6E 0%, #A67C52 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 32px;">
|
|
||||||
📞
|
|
||||||
</div>
|
|
||||||
<h4>1단계: 무료 상담</h4>
|
|
||||||
<p class="text-muted small">상황 파악 및<br/>현재 문제점 확인</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 text-center mb-4 mb-md-0">
|
|
||||||
<div style="width: 80px; height: 80px; margin: 0 auto 1rem; background: linear-gradient(135deg, #C89D6E 0%, #A67C52 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 32px;">
|
|
||||||
📋
|
|
||||||
</div>
|
|
||||||
<h4>2단계: 세무진단</h4>
|
|
||||||
<p class="text-muted small">자료 분석 및<br/>최적 방안 도출</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 text-center mb-4 mb-md-0">
|
|
||||||
<div style="width: 80px; height: 80px; margin: 0 auto 1rem; background: linear-gradient(135deg, #C89D6E 0%, #A67C52 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 32px;">
|
|
||||||
💡
|
|
||||||
</div>
|
|
||||||
<h4>3단계: 맞춤제안</h4>
|
|
||||||
<p class="text-muted small">절세 전략 및<br/>실행 계획 제시</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 text-center">
|
|
||||||
<div style="width: 80px; height: 80px; margin: 0 auto 1rem; background: linear-gradient(135deg, #C89D6E 0%, #A67C52 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 32px;">
|
|
||||||
✅
|
|
||||||
</div>
|
|
||||||
<h4>4단계: 실행지원</h4>
|
|
||||||
<p class="text-muted small">지속적 관리 및<br/>추가 상담 제공</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center mt-5">
|
|
||||||
<p class="text-muted mb-3">상담은 온라인 또는 오프라인으로 진행되며, 완전히 비밀이 보장됩니다.</p>
|
|
||||||
<a href="/taxbaik/contact" class="btn btn-primary btn-lg">상담 신청하기</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 최근 블로그 -->
|
|
||||||
<section class="py-5">
|
|
||||||
<div class="container">
|
|
||||||
<div class="text-center mb-5">
|
|
||||||
<h2 class="section-title">세무 정보</h2>
|
|
||||||
<p class="text-muted">최신 세법 변화와 실무 팁을 공유합니다</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (Model.RecentPosts?.Count > 0)
|
|
||||||
{
|
|
||||||
<div class="row g-4">
|
|
||||||
@foreach (var post in Model.RecentPosts.Take(3))
|
|
||||||
{
|
|
||||||
<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.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">
|
|
||||||
<a href="/taxbaik/blog" class="btn btn-outline-primary btn-lg">전체 블로그 보기</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 최종 CTA — 강렬한 다크 배경 -->
|
|
||||||
<section class="py-5" style="background: linear-gradient(135deg, #2E5C4E 0%, #1F3A30 100%); color: white;">
|
|
||||||
<div class="container text-center">
|
|
||||||
<h2 class="mb-3 fw-bold" style="font-size: 2.5rem;">세금 고민은 이제 끝!</h2>
|
|
||||||
<p class="fs-5 mb-5" style="opacity: 0.95; max-width: 500px; margin-left: auto; margin-right: auto;">
|
|
||||||
무료 상담으로 현재 상황을 진단하고<br/>
|
|
||||||
맞춤형 절세 전략을 받아보세요.
|
|
||||||
</p>
|
|
||||||
<div class="d-flex gap-3 justify-content-center flex-wrap">
|
|
||||||
<a href="/taxbaik/contact" class="btn btn-warning btn-lg">상담 신청하기</a>
|
|
||||||
<a href="javascript:void(0);" onclick="openKakao()" class="btn btn-light btn-lg">카카오로 문의</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using TaxBaik.Domain.Entities;
|
|
||||||
using TaxBaik.Web.Services;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Pages;
|
|
||||||
|
|
||||||
public class IndexModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly IApiClient _apiClient;
|
|
||||||
|
|
||||||
public List<BlogPost> RecentPosts { get; set; } = [];
|
|
||||||
|
|
||||||
public IndexModel(IApiClient apiClient)
|
|
||||||
{
|
|
||||||
_apiClient = apiClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnGetAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await _apiClient.GetAsync<BlogApiResponse>("blog?page=1&pageSize=3");
|
|
||||||
if (response?.Data != null)
|
|
||||||
RecentPosts = response.Data.ToList();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
RecentPosts = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BlogApiResponse
|
|
||||||
{
|
|
||||||
public List<BlogPost> Data { get; set; } = [];
|
|
||||||
public int Total { get; set; }
|
|
||||||
public int Page { get; set; }
|
|
||||||
public int PageSize { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using TaxBaik.Application.Services;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Pages;
|
|
||||||
|
|
||||||
public class SitemapModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly BlogService _blogService;
|
|
||||||
public List<string> Urls { get; set; } = [];
|
|
||||||
|
|
||||||
public SitemapModel(BlogService blogService)
|
|
||||||
{
|
|
||||||
_blogService = blogService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnGetAsync()
|
|
||||||
{
|
|
||||||
var baseUrl = "http://178.104.200.7/taxbaik";
|
|
||||||
Urls.AddRange(new[]
|
|
||||||
{
|
|
||||||
$"{baseUrl}",
|
|
||||||
$"{baseUrl}/about",
|
|
||||||
$"{baseUrl}/services",
|
|
||||||
$"{baseUrl}/contact",
|
|
||||||
$"{baseUrl}/blog"
|
|
||||||
});
|
|
||||||
|
|
||||||
var (posts, _) = await _blogService.GetPublishedPagedAsync(1, 1000);
|
|
||||||
foreach (var post in posts)
|
|
||||||
{
|
|
||||||
Urls.Add($"{baseUrl}/blog/{post.Slug}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>@(ViewData["Title"] ?? "백원숙 세무회계")</title>
|
|
||||||
<meta name="description" content="@(ViewData["Description"] ?? "사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담.")" />
|
|
||||||
<meta property="og:title" content="@ViewData["Title"]" />
|
|
||||||
<meta property="og:description" content="@ViewData["Description"]" />
|
|
||||||
<meta property="og:image" content="@ViewData["OgImage"]" />
|
|
||||||
<meta property="og:url" content="@ViewData["OgUrl"]" />
|
|
||||||
<meta name="robots" content="index, follow" />
|
|
||||||
<meta name="theme-color" content="#C89D6E" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<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 rel="canonical" href="@ViewData["CanonicalUrl"]" />
|
|
||||||
<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" />
|
|
||||||
</head>
|
|
||||||
<body class="with-mobile-cta">
|
|
||||||
<partial name="_Header" />
|
|
||||||
<main role="main" class="pb-5">
|
|
||||||
@RenderBody()
|
|
||||||
</main>
|
|
||||||
<footer class="bg-light border-top mt-5 py-4">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h6 class="fw-bold">백원숙 세무회계</h6>
|
|
||||||
<p class="small text-muted">
|
|
||||||
사업자 기장, 부동산 양도세·증여세,<br />
|
|
||||||
종합소득세 전문 상담
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h6 class="fw-bold">연락처</h6>
|
|
||||||
<p class="small">
|
|
||||||
📞 <a href="tel:010-4122-8268" class="text-decoration-none">010-4122-8268</a><br />
|
|
||||||
📧 <a href="mailto:taxbaik5668@gmail.com" class="text-decoration-none">taxbaik5668@gmail.com</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h6 class="fw-bold">채널</h6>
|
|
||||||
<p class="small">
|
|
||||||
<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>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class="my-3" />
|
|
||||||
<div class="text-center small text-muted">
|
|
||||||
<p>© 2026 백원숙 세무회계. All rights reserved.</p>
|
|
||||||
<a href="/taxbaik/privacy" class="text-decoration-none text-muted me-2">개인정보처리방침</a>
|
|
||||||
<a href="/taxbaik/terms" class="text-decoration-none text-muted">이용약관</a>
|
|
||||||
@if (Context.RequestServices.GetService(typeof(VersionInfo)) is VersionInfo version)
|
|
||||||
{
|
|
||||||
<div class="mt-2 text-muted" style="font-size: 0.75rem; opacity: 0.6;">
|
|
||||||
v@(version.Version) · @version.Built
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<!-- Mobile Fixed CTA -->
|
|
||||||
<div class="mobile-cta-bar d-lg-none">
|
|
||||||
<a href="http://pf.kakao.com/_xoxchTX" target="_blank" class="btn-kakao-mobile">
|
|
||||||
💬 카카오 상담하기
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" defer></script>
|
|
||||||
<script src="~/js/site.js" asp-append-version="true" defer></script>
|
|
||||||
@await RenderSectionAsync("Scripts", required: false)
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Text.Unicode;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
using Microsoft.AspNetCore.ResponseCompression;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using MudBlazor.Services;
|
|
||||||
using TaxBaik.Application;
|
|
||||||
using TaxBaik.Infrastructure;
|
|
||||||
using TaxBaik.Web.Services;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
// Controllers (API)
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
|
|
||||||
// Razor Pages + Blazor Server 통합
|
|
||||||
builder.Services.AddRazorPages();
|
|
||||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
|
||||||
|
|
||||||
// JWT 인증
|
|
||||||
var jwtKey = builder.Configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing JWT SecretKey");
|
|
||||||
var key = Encoding.ASCII.GetBytes(jwtKey);
|
|
||||||
|
|
||||||
builder.Services.AddAuthentication(opts =>
|
|
||||||
{
|
|
||||||
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
})
|
|
||||||
.AddJwtBearer(opts =>
|
|
||||||
{
|
|
||||||
opts.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
|
||||||
ValidateIssuer = false,
|
|
||||||
ValidateAudience = false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Blazor 인증
|
|
||||||
builder.Services.AddScoped<AuthService>();
|
|
||||||
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
|
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CustomAuthenticationStateProvider>());
|
|
||||||
builder.Services.AddScoped<ILocalStorageService, LocalStorageService>();
|
|
||||||
builder.Services.AddCascadingAuthenticationState();
|
|
||||||
builder.Services.AddAuthorizationCore();
|
|
||||||
|
|
||||||
// HTTP Client for API
|
|
||||||
builder.Services.AddHttpClient<IApiClient, ApiClient>();
|
|
||||||
|
|
||||||
// UI & 캐시
|
|
||||||
builder.Services.AddMudServices();
|
|
||||||
builder.Services.AddMemoryCache();
|
|
||||||
builder.Services.AddResponseCompression(opts => {
|
|
||||||
opts.Providers.Add<GzipCompressionProvider>();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 한글 포함 다국어 문자를 유니코드 엔티티로 변환하지 않도록 설정
|
|
||||||
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
|
|
||||||
|
|
||||||
builder.Services.AddInfrastructure();
|
|
||||||
builder.Services.AddApplication();
|
|
||||||
|
|
||||||
// Register version info
|
|
||||||
var versionInfo = new VersionInfo();
|
|
||||||
var versionFilePath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "version.txt");
|
|
||||||
if (File.Exists(versionFilePath))
|
|
||||||
{
|
|
||||||
var lines = File.ReadAllLines(versionFilePath);
|
|
||||||
foreach (var line in lines)
|
|
||||||
{
|
|
||||||
if (line.StartsWith("Version:"))
|
|
||||||
versionInfo.Version = line.Substring("Version:".Length).Trim();
|
|
||||||
else if (line.StartsWith("Built:"))
|
|
||||||
versionInfo.Built = line.Substring("Built:".Length).Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.Services.AddSingleton(versionInfo);
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// Run migrations on startup (non-blocking for development)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var scope = app.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var connectionFactory = scope.ServiceProvider.GetRequiredService<TaxBaik.Domain.Interfaces.IDbConnectionFactory>();
|
|
||||||
var cs = builder.Configuration.GetConnectionString("Default")
|
|
||||||
?? throw new InvalidOperationException("Missing connection string");
|
|
||||||
var migrationRunner = new TaxBaik.Infrastructure.Data.MigrationRunner(cs, connectionFactory);
|
|
||||||
await migrationRunner.RunAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"⚠️ Migration warning (non-blocking): {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UsePathBase("/taxbaik");
|
|
||||||
app.UseResponseCompression();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
app.UseRouting();
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
app.UseAntiforgery();
|
|
||||||
|
|
||||||
if (!app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseExceptionHandler("/Error");
|
|
||||||
app.UseHsts();
|
|
||||||
}
|
|
||||||
|
|
||||||
// API + Razor Pages + Blazor 매핑
|
|
||||||
app.MapControllers();
|
|
||||||
app.MapRazorPages();
|
|
||||||
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>().AddInteractiveServerRenderMode();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using BCrypt.Net;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using TaxBaik.Domain.Entities;
|
|
||||||
using TaxBaik.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Services;
|
|
||||||
|
|
||||||
public class AuthService
|
|
||||||
{
|
|
||||||
private readonly IAdminUserRepository _adminUserRepository;
|
|
||||||
private readonly ILogger<AuthService> _logger;
|
|
||||||
private readonly string _jwtSecretKey;
|
|
||||||
private readonly int _tokenExpirationMinutes = 480; // 8시간
|
|
||||||
|
|
||||||
public AuthService(IAdminUserRepository adminUserRepository, ILogger<AuthService> logger, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
_adminUserRepository = adminUserRepository;
|
|
||||||
_logger = logger;
|
|
||||||
_jwtSecretKey = configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing 'Jwt:SecretKey' configuration.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> AuthenticateAndGenerateTokenAsync(string username, string password)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _adminUserRepository.GetByUsernameAsync(username);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("로그인 시도: 존재하지 않는 사용자 '{Username}'", username);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(user.PasswordHash))
|
|
||||||
{
|
|
||||||
_logger.LogError("로그인 실패: 사용자 '{Username}'의 PasswordHash가 비어 있습니다.", username);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("로그인 성공: {Username}", username);
|
|
||||||
return GenerateJwtToken(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GenerateJwtToken(AdminUser user)
|
|
||||||
{
|
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey));
|
|
||||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
||||||
|
|
||||||
var claims = new[]
|
|
||||||
{
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
|
||||||
new Claim(ClaimTypes.Name, user.Username),
|
|
||||||
new Claim("username", user.Username)
|
|
||||||
};
|
|
||||||
|
|
||||||
var token = new JwtSecurityToken(
|
|
||||||
issuer: "taxbaik-admin",
|
|
||||||
audience: "taxbaik-admin-client",
|
|
||||||
claims: claims,
|
|
||||||
expires: DateTime.UtcNow.AddMinutes(_tokenExpirationMinutes),
|
|
||||||
signingCredentials: creds);
|
|
||||||
|
|
||||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClaimsPrincipal? ValidateToken(string token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey));
|
|
||||||
var handler = new JwtSecurityTokenHandler();
|
|
||||||
var principal = handler.ValidateToken(token, new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKey = key,
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidIssuer = "taxbaik-admin",
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidAudience = "taxbaik-admin-client",
|
|
||||||
ValidateLifetime = true,
|
|
||||||
ClockSkew = TimeSpan.Zero
|
|
||||||
}, out SecurityToken validatedToken);
|
|
||||||
|
|
||||||
return principal;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Services;
|
|
||||||
|
|
||||||
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|
||||||
{
|
|
||||||
private readonly ILocalStorageService _localStorage;
|
|
||||||
private readonly AuthService _authService;
|
|
||||||
private readonly ILogger<CustomAuthenticationStateProvider> _logger;
|
|
||||||
|
|
||||||
public CustomAuthenticationStateProvider(ILocalStorageService localStorage, AuthService authService, ILogger<CustomAuthenticationStateProvider> logger)
|
|
||||||
{
|
|
||||||
_localStorage = localStorage;
|
|
||||||
_authService = authService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var token = await _localStorage.GetItemAsStringAsync("auth_token");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsTokenExpired(token))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("토큰 만료됨");
|
|
||||||
await _localStorage.RemoveItemAsync("auth_token");
|
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
||||||
}
|
|
||||||
|
|
||||||
var principal = _authService.ValidateToken(token);
|
|
||||||
if (principal == null)
|
|
||||||
{
|
|
||||||
await _localStorage.RemoveItemAsync("auth_token");
|
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AuthenticationState(principal);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "인증 상태 조회 중 오류 발생");
|
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoginAsync(string token)
|
|
||||||
{
|
|
||||||
await _localStorage.SetItemAsStringAsync("auth_token", token);
|
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogoutAsync()
|
|
||||||
{
|
|
||||||
await _localStorage.RemoveItemAsync("auth_token");
|
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsTokenExpired(string token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var handler = new JwtSecurityTokenHandler();
|
|
||||||
var jwtToken = handler.ReadJwtToken(token);
|
|
||||||
return jwtToken.ValidTo < DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
|
||||||
<ProjectReference Include="..\TaxBaik.Infrastructure\TaxBaik.Infrastructure.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="MudBlazor" Version="6.9.4" />
|
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
|
||||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
/* MudBlazor 초기화 전 기본 스타일 */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login 페이지 기본 스타일 */
|
|
||||||
.d-flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pa-8 {
|
|
||||||
padding: 32px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-4 {
|
|
||||||
margin-bottom: 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-6 {
|
|
||||||
margin-bottom: 24px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-3 {
|
|
||||||
margin-left: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MudContainer */
|
|
||||||
.mud-container {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-container-maxwidth-small {
|
|
||||||
max-width: 600px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MudPaper */
|
|
||||||
.mud-paper {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-paper.elevation-3 {
|
|
||||||
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MudText */
|
|
||||||
.mud-typography {
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-typography--h4 {
|
|
||||||
font-size: 2.125rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-typography--body1 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Elements */
|
|
||||||
input[type="text"],
|
|
||||||
input[type="password"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]:focus,
|
|
||||||
input[type="password"]:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #1976d2;
|
|
||||||
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #555;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MudButton */
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 24px;
|
|
||||||
margin-top: 12px;
|
|
||||||
background-color: #1976d2;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background-color: #bdbdbd;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MudAlert */
|
|
||||||
.mud-alert {
|
|
||||||
padding: 12px 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #ffebee;
|
|
||||||
border-left: 4px solid #c62828;
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-alert--error {
|
|
||||||
background-color: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-alert--success {
|
|
||||||
background-color: #e8f5e9;
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-alert--info {
|
|
||||||
background-color: #e3f2fd;
|
|
||||||
color: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Progress Circle */
|
|
||||||
.mud-progress-circular {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading state */
|
|
||||||
.loading {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.mud-container-maxwidth-small {
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mud-typography--h4 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -4,6 +4,12 @@ INSERT INTO admin_users (username, password_hash, created_at)
|
|||||||
VALUES ('admin', '$2a$11$N9qo8uLOickgx2ZMRZoMye6IjfQTp5emXyqhT3jrDZWCqYIxJkAOq', NOW())
|
VALUES ('admin', '$2a$11$N9qo8uLOickgx2ZMRZoMye6IjfQTp5emXyqhT3jrDZWCqYIxJkAOq', NOW())
|
||||||
ON CONFLICT (username) DO NOTHING;
|
ON CONFLICT (username) DO NOTHING;
|
||||||
|
|
||||||
|
-- 테스트 계정 (비밀번호: test123456 - 개발/테스트 전용)
|
||||||
|
-- bcrypt hash for 'test123456': $2a$11$...
|
||||||
|
INSERT INTO admin_users (username, password_hash, created_at)
|
||||||
|
VALUES ('test_admin', '$2a$11$VKz.3zR0QFGZxJZQJ/M6w.3XjfQTp5emXyqhT3jrDZWCqYIxJkAOq', NOW())
|
||||||
|
ON CONFLICT (username) DO NOTHING;
|
||||||
|
|
||||||
-- 초기 블로그 포스트 5개
|
-- 초기 블로그 포스트 5개
|
||||||
INSERT INTO blog_posts (title, content, slug, category_id, tags, author_id, published_at, is_published, seo_title, seo_description, created_at, updated_at)
|
INSERT INTO blog_posts (title, content, slug, category_id, tags, author_id, published_at, is_published, seo_title, seo_description, created_at, updated_at)
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS site_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO site_settings (key, value, updated_at)
|
||||||
|
VALUES
|
||||||
|
('PhoneNumber', '010-4122-8268', NOW()),
|
||||||
|
('EmailAddress', 'taxbaik5668@gmail.com', NOW()),
|
||||||
|
('KakaoChannelUrl', 'http://pf.kakao.com/_xoxchTX', NOW()),
|
||||||
|
('InstagramUrl', 'https://www.instagram.com/taxtory5668/', NOW())
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS announcements (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
display_type VARCHAR(20) NOT NULL DEFAULT 'info',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
starts_at TIMESTAMPTZ,
|
||||||
|
ends_at TIMESTAMPTZ,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN announcements.display_type IS 'banner | info | urgent';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- 고객 카드 (Client CRM)
|
||||||
|
CREATE TABLE IF NOT EXISTS clients (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
company_name VARCHAR(200),
|
||||||
|
phone VARCHAR(30),
|
||||||
|
email VARCHAR(200),
|
||||||
|
service_type VARCHAR(50), -- 기장, 부동산, 증여상속, 종합소득세, 기타
|
||||||
|
tax_type VARCHAR(30), -- 개인사업자, 법인사업자, 면세사업자
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, inactive
|
||||||
|
source VARCHAR(50), -- 홈페이지문의, 소개, 직접방문, 카카오채널, 블로그, 기타
|
||||||
|
memo TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clients_status ON clients (status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clients_name ON clients (name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clients_phone ON clients (phone);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- FAQ 관리 테이블
|
||||||
|
CREATE TABLE IF NOT EXISTS faqs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
question VARCHAR(300) NOT NULL,
|
||||||
|
answer TEXT NOT NULL,
|
||||||
|
category VARCHAR(50), -- 기장세금신고, 부동산, 증여상속, 기타
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_faqs_active_order ON faqs (is_active, sort_order);
|
||||||
|
|
||||||
|
-- 기본 FAQ 시드 데이터 (하드코딩 대체)
|
||||||
|
INSERT INTO faqs (question, answer, category, sort_order, is_active) VALUES
|
||||||
|
(
|
||||||
|
'기장료가 얼마인지 미리 알 수 있나요?',
|
||||||
|
'업종과 매출 규모에 따라 다르지만, 무료 상담 후 정확한 견적을 안내드립니다. 일반적으로 소규모 사업자는 월 10만 원대부터 시작하며, 부가가치세·소득세 신고 시기에는 별도 수수료 없이 포함 처리합니다. 먼저 상담해 보시면 구체적인 금액을 바로 말씀드릴 수 있습니다.',
|
||||||
|
'기장세금신고', 10, TRUE
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'양도세 상담은 어떻게 진행되나요?',
|
||||||
|
'등기부등본, 취득·양도 계약서, 보유 기간 확인 서류 등을 카카오채널 또는 문의폼으로 전달해 주시면 예상 세액과 절세 방법을 검토해 드립니다. 매도 전에 상담하시면 취득세·비과세 요건 등을 사전에 확인할 수 있어 훨씬 유리합니다.',
|
||||||
|
'부동산', 20, TRUE
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'무료 상담도 가능한가요?',
|
||||||
|
'네, 초기 현황 파악과 방향성 검토까지는 무료로 진행합니다. 카카오채널 또는 문의폼으로 연락 주시면 빠르게 확인해 드립니다. 실질적인 세무 처리·신고 대행이 시작되는 시점부터 수수료가 발생합니다.',
|
||||||
|
'기타', 30, TRUE
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'처음 상담 시 어떤 자료를 준비해야 하나요?',
|
||||||
|
'상담 목적에 따라 다르지만 아래 자료가 있으면 더 정확한 안내가 가능합니다. 사업자 세무: 사업자등록증, 최근 3개월 매출·매입 자료 / 부동산: 등기부등본, 취득·매도 계약서, 보유 기간 확인 자료 / 증여상속: 재산 목록, 증여 예정 자산 내역. 자료가 없어도 상담은 가능합니다. 먼저 연락 주세요.',
|
||||||
|
'증여상속', 40, TRUE
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- 상담 이력 테이블
|
||||||
|
CREATE TABLE IF NOT EXISTS consultations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
consultation_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||||
|
service_type VARCHAR(50),
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
result VARCHAR(30), -- consulting, contracted, rejected, pending, completed
|
||||||
|
fee NUMERIC(12,0),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consultations_client ON consultations (client_id);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- 문의 → 고객 연결 (문의에서 고객 카드 생성 시 연결)
|
||||||
|
ALTER TABLE inquiries ADD COLUMN IF NOT EXISTS client_id INT REFERENCES clients(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_inquiries_client ON inquiries (client_id);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- 고객별 세금 신고 일정
|
||||||
|
CREATE TABLE IF NOT EXISTS tax_filings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
filing_type VARCHAR(60) NOT NULL, -- 부가가치세, 종합소득세, 법인세, 원천징수, 종합부동산세, 기타
|
||||||
|
due_date DATE NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, filed, overdue
|
||||||
|
memo TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filings_client ON tax_filings (client_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filings_due_date ON tax_filings (due_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filings_status ON tax_filings (status);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 문의 상태 5단계로 확장 + 처리 메모 컬럼 추가
|
||||||
|
-- 기존: new, contacted, completed
|
||||||
|
-- 신규: new(신규), consulting(상담중), contracted(계약완료), rejected(거절), closed(종결)
|
||||||
|
|
||||||
|
UPDATE inquiries SET status = 'consulting' WHERE status = 'contacted';
|
||||||
|
UPDATE inquiries SET status = 'closed' WHERE status = 'completed';
|
||||||
|
|
||||||
|
ALTER TABLE inquiries ADD COLUMN IF NOT EXISTS admin_memo TEXT;
|
||||||
|
ALTER TABLE inquiries ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- 테스트 계정 추가 (E2E Playwright 자동 테스트용)
|
||||||
|
-- 비밀번호: TestAdmin@123456
|
||||||
|
-- API /api/auth/reset-password로 설정 (마이그레이션에서는 초기 해시만 설정)
|
||||||
|
|
||||||
|
INSERT INTO admin_users (username, password_hash, created_at)
|
||||||
|
VALUES ('test_admin', '$2a$11$VKz.3zR0QFGZxJZQJ/M6w.3XjfQTp5emXyqhT3jrDZWCqYIxJkAOq', NOW())
|
||||||
|
ON CONFLICT (username) DO UPDATE SET
|
||||||
|
password_hash = EXCLUDED.password_hash;
|
||||||
|
|
||||||
|
-- 검증
|
||||||
|
SELECT username, created_at FROM admin_users WHERE username IN ('admin', 'test_admin') ORDER BY username;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- 관리자 계정 확실히 하기 (멱등성 보장)
|
||||||
|
-- admin: 마이그레이션 V003에서 생성
|
||||||
|
-- test_admin: 마이그레이션 V012에서 생성, API reset-password로 최종 설정
|
||||||
|
|
||||||
|
-- V003에서 이미 생성된 admin 계정이 없으면 추가
|
||||||
|
INSERT INTO admin_users (username, password_hash, created_at)
|
||||||
|
VALUES ('admin', '$2a$11$N9qo8uLOickgx2ZMRZoMye6IjfQTp5emXyqhT3jrDZWCqYIxJkAOq', NOW())
|
||||||
|
ON CONFLICT (username) DO NOTHING;
|
||||||
|
|
||||||
|
-- V012에서 추가 시도한 test_admin 확인 후 수정
|
||||||
|
-- 만약 존재하지 않으면 생성
|
||||||
|
INSERT INTO admin_users (username, password_hash, created_at)
|
||||||
|
VALUES ('test_admin', '$2a$11$N9qo8uLOickgx2ZMRZoMye6IjfQTp5emXyqhT3jrDZWCqYIxJkAOq', NOW())
|
||||||
|
ON CONFLICT (username) DO NOTHING;
|
||||||
|
|
||||||
|
-- 검증: 두 계정 모두 admin123 비밀번호로 설정됨
|
||||||
|
SELECT id, username, created_at FROM admin_users ORDER BY username;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-- Create Companies table for multi-tenant support
|
||||||
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
company_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
company_name VARCHAR(200) NOT NULL,
|
||||||
|
contact_person VARCHAR(100),
|
||||||
|
phone VARCHAR(20),
|
||||||
|
email VARCHAR(200),
|
||||||
|
memo TEXT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add company_id to admin_users (nullable for backward compatibility)
|
||||||
|
ALTER TABLE admin_users ADD COLUMN IF NOT EXISTS company_id INT REFERENCES companies(id) ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
-- Create index for company lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_companies_code ON companies(company_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_admin_users_company ON admin_users(company_id);
|
||||||
|
|
||||||
|
-- Insert default company for existing admin users
|
||||||
|
INSERT INTO companies (company_code, company_name, is_active)
|
||||||
|
VALUES ('DEFAULT', '기본 회사', TRUE)
|
||||||
|
ON CONFLICT (company_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- Assign existing admin users to default company if not assigned
|
||||||
|
UPDATE admin_users
|
||||||
|
SET company_id = (SELECT id FROM companies WHERE company_code = 'DEFAULT')
|
||||||
|
WHERE company_id IS NULL;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
-- Extend clients table with tax-specific fields
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS business_registration_number VARCHAR(20);
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS business_type VARCHAR(50);
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS establishment_date DATE;
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS annual_revenue_range VARCHAR(50);
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS employee_count INT;
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS last_tax_filing_date DATE;
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS tax_risk_level VARCHAR(20) DEFAULT 'normal';
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS next_filing_due_date DATE;
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS contact_person VARCHAR(100);
|
||||||
|
ALTER TABLE clients ADD COLUMN IF NOT EXISTS company_id INT REFERENCES companies(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Create tax_profiles table for detailed tax information
|
||||||
|
CREATE TABLE IF NOT EXISTS tax_profiles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL UNIQUE REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
business_registration VARCHAR(20),
|
||||||
|
business_type VARCHAR(50),
|
||||||
|
establishment_date DATE,
|
||||||
|
annual_revenue_range VARCHAR(50),
|
||||||
|
employee_count INT,
|
||||||
|
accounting_method VARCHAR(50),
|
||||||
|
fiscal_year_end VARCHAR(10),
|
||||||
|
last_filing_date DATE,
|
||||||
|
next_filing_due_date DATE,
|
||||||
|
tax_risk_level VARCHAR(20) DEFAULT 'normal',
|
||||||
|
previous_audit_history BOOLEAN DEFAULT FALSE,
|
||||||
|
special_notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create tax_filing_schedules table for tracking schedules
|
||||||
|
CREATE TABLE IF NOT EXISTS tax_filing_schedules (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
filing_type VARCHAR(100) NOT NULL,
|
||||||
|
due_date DATE NOT NULL,
|
||||||
|
filing_year INT NOT NULL,
|
||||||
|
status VARCHAR(50) DEFAULT 'pending',
|
||||||
|
assigned_to INT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||||
|
completed_date DATE,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create consulting_activities table for CRM (상담 활동 추적)
|
||||||
|
CREATE TABLE IF NOT EXISTS consulting_activities (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
activity_type VARCHAR(50) NOT NULL,
|
||||||
|
activity_date DATE NOT NULL,
|
||||||
|
activity_time TIME,
|
||||||
|
assigned_consultant INT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
outcome VARCHAR(100),
|
||||||
|
next_followup_date DATE,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create contracts table (계약 관리)
|
||||||
|
CREATE TABLE IF NOT EXISTS contracts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
contract_number VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
service_type VARCHAR(100) NOT NULL,
|
||||||
|
contract_date DATE NOT NULL,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE,
|
||||||
|
monthly_fee NUMERIC(10, 2),
|
||||||
|
total_amount NUMERIC(10, 2),
|
||||||
|
payment_status VARCHAR(50) DEFAULT 'pending',
|
||||||
|
status VARCHAR(50) DEFAULT 'active',
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create revenue_tracking table (매출 추적)
|
||||||
|
CREATE TABLE IF NOT EXISTS revenue_tracking (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
invoice_number VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
invoice_date DATE NOT NULL,
|
||||||
|
service_type VARCHAR(100),
|
||||||
|
amount NUMERIC(10, 2) NOT NULL,
|
||||||
|
payment_status VARCHAR(50) DEFAULT 'pending',
|
||||||
|
payment_date DATE,
|
||||||
|
due_date DATE,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clients_company ON clients(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_profiles_client ON tax_profiles(client_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filing_schedules_client ON tax_filing_schedules(client_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filing_schedules_due_date ON tax_filing_schedules(due_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consulting_activities_client ON consulting_activities(client_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contracts_client ON contracts(client_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_revenue_tracking_client ON revenue_tracking(client_id);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS portal_users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NULL REFERENCES clients(id) ON DELETE SET NULL,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
phone VARCHAR(50),
|
||||||
|
provider VARCHAR(30) NOT NULL DEFAULT 'local',
|
||||||
|
provider_id VARCHAR(200),
|
||||||
|
password_hash TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_portal_users_provider
|
||||||
|
ON portal_users(provider, provider_id);
|
||||||
@@ -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,417 @@
|
|||||||
|
-- V019: Fix blog posts migration (V018 had quote escaping issues)
|
||||||
|
-- Complete rewrite using $$ quote style to avoid escaping problems
|
||||||
|
|
||||||
|
-- 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,637 @@
|
|||||||
|
-- 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)
|
||||||
|
|
||||||
|
-- 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,641 @@
|
|||||||
|
-- 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
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
)
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
)
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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,678 @@
|
|||||||
|
-- V022: Apply accuracy principle (law/fact/data based) to blog posts
|
||||||
|
-- Add tax law citations, 2025 standards, data sources
|
||||||
|
-- Remove speculation, assumptions, opinions
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
)
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
)
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
)
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
-- V023: Customer-friendly language update
|
||||||
|
-- Remove internal jargon (Layer 1-3, "3층 구조", etc.)
|
||||||
|
-- Replace with customer perspective: "할 수 있어요" → "복잡하네" → "세무사가 필요하네"
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
) ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
) ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
) ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
-- V024: Apply latest BLOG_TEMPLATE guidelines
|
||||||
|
-- Convert tables to readable lists
|
||||||
|
-- Simplify emojis (remove section headers like 📊, 🧮)
|
||||||
|
-- Keep customer-friendly language (1️⃣ 2️⃣ 3️⃣)
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
) ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
) ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
-- 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()
|
||||||
|
) ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
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 (가족자산): 연말정산 환급
|
||||||
|
|
||||||
|
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())
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
ALTER TABLE blog_posts
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_blog_slug;
|
||||||
|
ALTER TABLE blog_posts DROP CONSTRAINT IF EXISTS blog_posts_slug_key;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_blog_posts_slug_active
|
||||||
|
ON blog_posts (slug)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_blog_slug_active
|
||||||
|
ON blog_posts (slug)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_blog_published_active
|
||||||
|
ON blog_posts (is_published, published_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_blog_category_active
|
||||||
|
ON blog_posts (category_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
-- Seed and normalize admin common codes.
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('INQUIRY_SERVICE_TYPE', '사업자세무', '사업자세무', 10),
|
||||||
|
('INQUIRY_SERVICE_TYPE', '부동산세금', '부동산세금', 20),
|
||||||
|
('INQUIRY_SERVICE_TYPE', '가족자산', '가족자산', 30),
|
||||||
|
('INQUIRY_SERVICE_TYPE', '기타', '기타', 40),
|
||||||
|
|
||||||
|
('INQUIRY_STATUS', 'new', '신규', 10),
|
||||||
|
('INQUIRY_STATUS', 'consulting', '상담중', 20),
|
||||||
|
('INQUIRY_STATUS', 'contracted', '계약완료', 30),
|
||||||
|
('INQUIRY_STATUS', 'rejected', '거절', 40),
|
||||||
|
('INQUIRY_STATUS', 'closed', '종결', 50),
|
||||||
|
|
||||||
|
('CLIENT_STATUS', 'active', '활성', 10),
|
||||||
|
('CLIENT_STATUS', 'inactive', '비활성', 20),
|
||||||
|
|
||||||
|
('CLIENT_SERVICE_TYPE', '기장', '기장', 10),
|
||||||
|
('CLIENT_SERVICE_TYPE', '부동산', '부동산', 20),
|
||||||
|
('CLIENT_SERVICE_TYPE', '증여상속', '증여·상속', 30),
|
||||||
|
('CLIENT_SERVICE_TYPE', '종합소득세', '종합소득세', 40),
|
||||||
|
('CLIENT_SERVICE_TYPE', '법인세', '법인세', 50),
|
||||||
|
('CLIENT_SERVICE_TYPE', '부가가치세', '부가가치세', 60),
|
||||||
|
('CLIENT_SERVICE_TYPE', '기타', '기타', 70),
|
||||||
|
|
||||||
|
('CLIENT_TAX_TYPE', '개인사업자', '개인사업자', 10),
|
||||||
|
('CLIENT_TAX_TYPE', '법인사업자', '법인사업자', 20),
|
||||||
|
('CLIENT_TAX_TYPE', '면세사업자', '면세사업자', 30),
|
||||||
|
('CLIENT_TAX_TYPE', '근로소득자', '근로소득자', 40),
|
||||||
|
('CLIENT_TAX_TYPE', '기타', '기타', 50),
|
||||||
|
|
||||||
|
('CLIENT_SOURCE', '홈페이지문의', '홈페이지 문의', 10),
|
||||||
|
('CLIENT_SOURCE', '소개', '소개', 20),
|
||||||
|
('CLIENT_SOURCE', '직접방문', '직접 방문', 30),
|
||||||
|
('CLIENT_SOURCE', '카카오채널', '카카오 채널', 40),
|
||||||
|
('CLIENT_SOURCE', '블로그', '블로그', 50),
|
||||||
|
('CLIENT_SOURCE', '기타', '기타', 60),
|
||||||
|
|
||||||
|
('CONTRACT_SERVICE_TYPE', '개인기장대리', '개인 기장대리', 10),
|
||||||
|
('CONTRACT_SERVICE_TYPE', '법인기장대리', '법인 기장대리', 20),
|
||||||
|
('CONTRACT_SERVICE_TYPE', '세무조정', '세무조정', 30),
|
||||||
|
('CONTRACT_SERVICE_TYPE', '세무컨설팅', '세무컨설팅', 40),
|
||||||
|
('CONTRACT_SERVICE_TYPE', '불복청구', '불복청구', 50),
|
||||||
|
|
||||||
|
('REVENUE_SERVICE_TYPE', '기장수수료', '기장 수수료', 10),
|
||||||
|
('REVENUE_SERVICE_TYPE', '세무조정료', '세무조정료', 20),
|
||||||
|
('REVENUE_SERVICE_TYPE', '세무상담료', '세무상담료', 30),
|
||||||
|
('REVENUE_SERVICE_TYPE', '신고대행료', '신고 대행료', 40),
|
||||||
|
('REVENUE_SERVICE_TYPE', '자문수수료', '자문 수수료', 50),
|
||||||
|
|
||||||
|
('FILING_TYPE', '종합소득세', '종합소득세', 10),
|
||||||
|
('FILING_TYPE', '부가가치세', '부가가치세', 20),
|
||||||
|
('FILING_TYPE', '법인세', '법인세', 30),
|
||||||
|
('FILING_TYPE', '원천세', '원천세', 40),
|
||||||
|
('FILING_TYPE', '양도소득세', '양도소득세', 50),
|
||||||
|
('FILING_TYPE', '상속증여세', '상속·증여세', 60),
|
||||||
|
('FILING_TYPE', '세무조정', '세무조정', 70),
|
||||||
|
|
||||||
|
('TAX_RISK_LEVEL', 'low', '낮음', 10),
|
||||||
|
('TAX_RISK_LEVEL', 'normal', '보통', 20),
|
||||||
|
('TAX_RISK_LEVEL', 'high', '높음', 30)
|
||||||
|
ON CONFLICT (code_group, code_value) DO UPDATE
|
||||||
|
SET code_name = EXCLUDED.code_name,
|
||||||
|
sort_order = EXCLUDED.sort_order,
|
||||||
|
is_active = TRUE;
|
||||||
|
|
||||||
|
-- Normalize storage keys and migrate existing rows.
|
||||||
|
UPDATE common_codes
|
||||||
|
SET code_value = CASE
|
||||||
|
WHEN code_group = 'CLIENT_SERVICE_TYPE' AND code_value = '증여·상속' THEN '증여상속'
|
||||||
|
WHEN code_group = 'CLIENT_SOURCE' AND code_value = '홈페이지 문의' THEN '홈페이지문의'
|
||||||
|
WHEN code_group = 'CLIENT_SOURCE' AND code_value = '직접 방문' THEN '직접방문'
|
||||||
|
WHEN code_group = 'CLIENT_SOURCE' AND code_value = '카카오 채널' THEN '카카오채널'
|
||||||
|
WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '개인 기장대리' THEN '개인기장대리'
|
||||||
|
WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '법인 기장대리' THEN '법인기장대리'
|
||||||
|
WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '기장 수수료' THEN '기장수수료'
|
||||||
|
WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '신고 대행료' THEN '신고대행료'
|
||||||
|
WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '자문 수수료' THEN '자문수수료'
|
||||||
|
WHEN code_group = 'FILING_TYPE' AND code_value = '상속·증여세' THEN '상속증여세'
|
||||||
|
ELSE code_value
|
||||||
|
END,
|
||||||
|
code_name = CASE
|
||||||
|
WHEN code_group = 'CLIENT_SERVICE_TYPE' AND code_value = '증여·상속' THEN '증여·상속'
|
||||||
|
WHEN code_group = 'CLIENT_SOURCE' AND code_value = '홈페이지 문의' THEN '홈페이지 문의'
|
||||||
|
WHEN code_group = 'CLIENT_SOURCE' AND code_value = '직접 방문' THEN '직접 방문'
|
||||||
|
WHEN code_group = 'CLIENT_SOURCE' AND code_value = '카카오 채널' THEN '카카오 채널'
|
||||||
|
WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '개인 기장대리' THEN '개인 기장대리'
|
||||||
|
WHEN code_group = 'CONTRACT_SERVICE_TYPE' AND code_value = '법인 기장대리' THEN '법인 기장대리'
|
||||||
|
WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '기장 수수료' THEN '기장 수수료'
|
||||||
|
WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '신고 대행료' THEN '신고 대행료'
|
||||||
|
WHEN code_group = 'REVENUE_SERVICE_TYPE' AND code_value = '자문 수수료' THEN '자문 수수료'
|
||||||
|
WHEN code_group = 'FILING_TYPE' AND code_value = '상속·증여세' THEN '상속·증여세'
|
||||||
|
ELSE code_name
|
||||||
|
END
|
||||||
|
WHERE (code_group, code_value) IN (
|
||||||
|
('CLIENT_SERVICE_TYPE', '증여·상속'),
|
||||||
|
('CLIENT_SOURCE', '홈페이지 문의'),
|
||||||
|
('CLIENT_SOURCE', '직접 방문'),
|
||||||
|
('CLIENT_SOURCE', '카카오 채널'),
|
||||||
|
('CONTRACT_SERVICE_TYPE', '개인 기장대리'),
|
||||||
|
('CONTRACT_SERVICE_TYPE', '법인 기장대리'),
|
||||||
|
('REVENUE_SERVICE_TYPE', '기장 수수료'),
|
||||||
|
('REVENUE_SERVICE_TYPE', '신고 대행료'),
|
||||||
|
('REVENUE_SERVICE_TYPE', '자문 수수료'),
|
||||||
|
('FILING_TYPE', '상속·증여세')
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE clients
|
||||||
|
SET
|
||||||
|
service_type = CASE WHEN service_type = '증여·상속' THEN '증여상속' ELSE service_type END,
|
||||||
|
source = CASE
|
||||||
|
WHEN source = '홈페이지 문의' THEN '홈페이지문의'
|
||||||
|
WHEN source = '직접 방문' THEN '직접방문'
|
||||||
|
WHEN source = '카카오 채널' THEN '카카오채널'
|
||||||
|
ELSE source
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE contracts
|
||||||
|
SET service_type = REPLACE(REPLACE(service_type, ' ', ''), '·', '')
|
||||||
|
WHERE service_type IS NOT NULL;
|
||||||
|
|
||||||
|
UPDATE revenue_tracking
|
||||||
|
SET service_type = REPLACE(REPLACE(service_type, ' ', ''), '·', '')
|
||||||
|
WHERE service_type IS NOT NULL;
|
||||||
|
|
||||||
|
UPDATE tax_filings
|
||||||
|
SET filing_type = '상속증여세'
|
||||||
|
WHERE filing_type = '상속·증여세';
|
||||||
|
|
||||||
|
UPDATE tax_filing_schedules
|
||||||
|
SET filing_type = '상속증여세'
|
||||||
|
WHERE filing_type = '상속·증여세';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- Allow Korean code values and future growth without truncation risk.
|
||||||
|
ALTER TABLE common_codes
|
||||||
|
ALTER COLUMN code_group TYPE VARCHAR(80),
|
||||||
|
ALTER COLUMN code_value TYPE VARCHAR(120),
|
||||||
|
ALTER COLUMN code_name TYPE VARCHAR(200);
|
||||||
|
|
||||||
|
ALTER TABLE clients
|
||||||
|
ALTER COLUMN service_type TYPE VARCHAR(100),
|
||||||
|
ALTER COLUMN tax_type TYPE VARCHAR(60),
|
||||||
|
ALTER COLUMN source TYPE VARCHAR(100);
|
||||||
|
|
||||||
|
ALTER TABLE contracts
|
||||||
|
ALTER COLUMN service_type TYPE VARCHAR(120);
|
||||||
|
|
||||||
|
ALTER TABLE revenue_tracking
|
||||||
|
ALTER COLUMN service_type TYPE VARCHAR(120);
|
||||||
|
|
||||||
|
ALTER TABLE tax_filings
|
||||||
|
ALTER COLUMN filing_type TYPE VARCHAR(120);
|
||||||
|
|
||||||
|
ALTER TABLE tax_filing_schedules
|
||||||
|
ALTER COLUMN filing_type TYPE VARCHAR(120);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Additional common codes for admin combo policy normalization.
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('CONSULTING_ACTIVITY_TYPE', '방문상담', '방문 상담', 10),
|
||||||
|
('CONSULTING_ACTIVITY_TYPE', '전화상담', '전화 상담', 20),
|
||||||
|
('CONSULTING_ACTIVITY_TYPE', '세무조사대응미팅', '세무조사 대응 미팅', 30),
|
||||||
|
('CONSULTING_ACTIVITY_TYPE', '카카오톡상담', '카카오톡 상담', 40),
|
||||||
|
('CONSULTING_ACTIVITY_TYPE', '이메일자료접수', '이메일 자료 접수', 50),
|
||||||
|
('CONSULTING_ACTIVITY_TYPE', '기타', '기타', 60),
|
||||||
|
|
||||||
|
('ANNOUNCEMENT_DISPLAY_TYPE', 'info', '일반', 10),
|
||||||
|
('ANNOUNCEMENT_DISPLAY_TYPE', 'banner', '배너', 20),
|
||||||
|
('ANNOUNCEMENT_DISPLAY_TYPE', 'urgent', '긴급', 30)
|
||||||
|
ON CONFLICT (code_group, code_value) DO UPDATE
|
||||||
|
SET code_name = EXCLUDED.code_name,
|
||||||
|
sort_order = EXCLUDED.sort_order,
|
||||||
|
is_active = TRUE;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE blog_posts
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_blog_posts_deleted_at
|
||||||
|
ON blog_posts (deleted_at)
|
||||||
|
WHERE deleted_at IS NOT NULL;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order)
|
||||||
|
SELECT v.code_group, v.code_value, v.code_name, v.sort_order
|
||||||
|
FROM (
|
||||||
|
VALUES
|
||||||
|
('FAQ_CATEGORY', '기장세금신고', '기장세금신고', 10),
|
||||||
|
('FAQ_CATEGORY', '부동산', '부동산', 20),
|
||||||
|
('FAQ_CATEGORY', '증여상속', '증여상속', 30),
|
||||||
|
('FAQ_CATEGORY', '기타', '기타', 40)
|
||||||
|
) AS v(code_group, code_value, code_name, sort_order)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM common_codes cc
|
||||||
|
WHERE cc.code_group = v.code_group
|
||||||
|
AND cc.code_value = v.code_value
|
||||||
|
);
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
DEPLOY_HOME="/home/kjh2064"
|
|
||||||
WEB_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
|
|
||||||
echo "===== 🚀 TaxBaik 배포 스크립트 ====="
|
|
||||||
echo "Web Timestamp: $WEB_TIMESTAMP"
|
|
||||||
|
|
||||||
# Web 배포
|
|
||||||
echo "=== Deploying Web ==="
|
|
||||||
WEB_DEPLOY_DIR="$DEPLOY_HOME/deployments/taxbaik_${WEB_TIMESTAMP}"
|
|
||||||
mkdir -p "$WEB_DEPLOY_DIR"
|
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Error: Publish directory required"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 첫 번째 인자는 publish 경로
|
|
||||||
cp -r "$1/web" "$WEB_DEPLOY_DIR/"
|
|
||||||
ln -sfn "$WEB_DEPLOY_DIR/web" "$DEPLOY_HOME/taxbaik_active"
|
|
||||||
echo "✓ Web symlink updated: $WEB_DEPLOY_DIR/web"
|
|
||||||
|
|
||||||
# 프로세스 재시작
|
|
||||||
echo "=== Restarting processes ==="
|
|
||||||
pkill -9 -f "TaxBaik.Web" || true
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "=== Starting Web ==="
|
|
||||||
cd "$DEPLOY_HOME/taxbaik_active"
|
|
||||||
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
|
|
||||||
export ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
export ASPNETCORE_URLS=http://127.0.0.1:5001
|
|
||||||
nohup /usr/local/dotnet/dotnet TaxBaik.Web.dll > web.log 2>&1 &
|
|
||||||
sleep 2
|
|
||||||
ps aux | grep TaxBaik.Web | grep -v grep && echo "✓ Web started" || echo "✗ Web failed"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "===== ✅ 배포 완료 ====="
|
|
||||||
cat "$DEPLOY_HOME/taxbaik_active/wwwroot/version.txt" 2>/dev/null || echo "Version file not found"
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# 1. TaxBaik 홈페이지 (taxbaik.com, www.taxbaik.com)
|
||||||
|
server {
|
||||||
|
server_name taxbaik.com www.taxbaik.com;
|
||||||
|
client_max_body_size 512M;
|
||||||
|
|
||||||
|
|
||||||
|
# /admin 은 관리자 진입용 경로로만 사용하고, 실제 앱은 /taxbaik/admin 에서 서빙한다.
|
||||||
|
# 공개 루트(/)는 공용 SSR 홈페이지를 반환해야 하므로, 관리 UI와 절대 섞지 않는다.
|
||||||
|
location /admin {
|
||||||
|
return 301 $scheme://$host/taxbaik$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 루트 경로는 공용 SSR 홈페이지.
|
||||||
|
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/ 하위는 공개 사이트의 정식 base path.
|
||||||
|
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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=TaxBaik Public Website (.NET 8)
|
Description=TaxBaik Backend App (.NET 10)
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -17,7 +17,7 @@ KillSignal=SIGTERM
|
|||||||
|
|
||||||
SyslogIdentifier=taxbaik
|
SyslogIdentifier=taxbaik
|
||||||
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=DOTNET_PRINT_TELEMETRY_MESSAGE=false
|
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
|
||||||
# 아래 줄은 서버에서 직접 편집 (git에 커밋하지 않음)
|
# 아래 줄은 서버에서 직접 편집 (git에 커밋하지 않음)
|
||||||
# Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=CHANGE_ME
|
# Environment=ConnectionStrings__Default=Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=CHANGE_ME
|
||||||
|
|||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEPLOY_HOME="/home/kjh2064"
|
||||||
|
PORT_FILE="$DEPLOY_HOME/taxbaik_port"
|
||||||
|
TIMESTAMP=$(TZ=Asia/Seoul date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
echo "===== 🚀 TaxBaik Green/Blue Deployment Script ====="
|
||||||
|
|
||||||
|
if [ "${TAXBAIK_DEPLOY_FROM_CI:-}" != "1" ]; then
|
||||||
|
echo "❌ This deployment script may only be run from CI." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Determine active port
|
||||||
|
ACTIVE_PORT=5003
|
||||||
|
if [ -f "$PORT_FILE" ]; then
|
||||||
|
ACTIVE_PORT=$(cat "$PORT_FILE" | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Determine target port
|
||||||
|
TARGET_PORT=5003
|
||||||
|
if [ "$ACTIVE_PORT" -eq 5003 ]; then
|
||||||
|
TARGET_PORT=5004
|
||||||
|
else
|
||||||
|
TARGET_PORT=5003
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Active Port: $ACTIVE_PORT"
|
||||||
|
echo "Target Port: $TARGET_PORT"
|
||||||
|
|
||||||
|
# 3. New deploy dir is passed as first argument
|
||||||
|
DEPLOY_DIR="$1"
|
||||||
|
if [ -z "$DEPLOY_DIR" ]; then
|
||||||
|
echo "Error: Deployment directory argument required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploy Directory: $DEPLOY_DIR"
|
||||||
|
|
||||||
|
if [ ! -s "$DEPLOY_DIR/appsettings.Production.json" ]; then
|
||||||
|
echo "❌ Missing production settings: $DEPLOY_DIR/appsettings.Production.json" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s "$DEPLOY_DIR/proxy/TaxBaik.Proxy.dll" ]; then
|
||||||
|
echo "❌ Missing proxy artifact: $DEPLOY_DIR/proxy/TaxBaik.Proxy.dll" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
is_taxbaik_proxy_on_5001() {
|
||||||
|
local pids
|
||||||
|
pids=$(ss -tlnp 2>/dev/null | grep ':5001 ' | grep -oP 'pid=\K\d+' | sort -u || true)
|
||||||
|
[ -n "$pids" ] || return 1
|
||||||
|
|
||||||
|
for pid in $pids; do
|
||||||
|
if tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q 'TaxBaik.Proxy.dll'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 0. Ensure the local TCP proxy exists and is running.
|
||||||
|
# Nginx and external traffic always enter through 127.0.0.1:5001.
|
||||||
|
if ss -tln | grep -q ':5001 ' && ! is_taxbaik_proxy_on_5001; then
|
||||||
|
echo "⚠️ Port 5001 is occupied by a non-proxy process. Attempting to stop legacy taxbaik.service..."
|
||||||
|
echo " Current listener:" >&2
|
||||||
|
ss -tlnp 2>/dev/null | grep ':5001 ' >&2 || true
|
||||||
|
|
||||||
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet taxbaik 2>/dev/null; then
|
||||||
|
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
|
||||||
|
sudo -n systemctl stop taxbaik || true
|
||||||
|
sudo -n systemctl disable taxbaik || true
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
echo " sudo -n is unavailable; cannot stop legacy taxbaik.service automatically." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ss -tln | grep -q ':5001 ' && ! is_taxbaik_proxy_on_5001; then
|
||||||
|
echo "❌ Port 5001 is still occupied by a non-proxy process. Abort deploy to avoid routing traffic to the wrong app." >&2
|
||||||
|
echo " Expected: TaxBaik.Proxy.dll on 127.0.0.1:5001" >&2
|
||||||
|
echo " Manual fix: sudo systemctl stop taxbaik && sudo systemctl disable taxbaik" >&2
|
||||||
|
ss -tlnp 2>/dev/null | grep ':5001 ' >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_taxbaik_proxy_on_5001; then
|
||||||
|
echo "=== Starting proxy on 127.0.0.1:5001 ==="
|
||||||
|
cd "$DEPLOY_DIR/proxy"
|
||||||
|
nohup /usr/bin/dotnet TaxBaik.Proxy.dll > "$DEPLOY_HOME/taxbaik_proxy.log" 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_taxbaik_proxy_on_5001; then
|
||||||
|
echo "❌ Proxy on 127.0.0.1:5001 is not running. Abort deploy." >&2
|
||||||
|
ss -tlnp 2>/dev/null | grep ':5001 ' >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Start the new app on the target port
|
||||||
|
echo "=== Starting New App on Port $TARGET_PORT ==="
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
export ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
export ASPNETCORE_URLS="http://127.0.0.1:$TARGET_PORT"
|
||||||
|
export ConnectionStrings__Default="Host=localhost;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
|
||||||
|
export ApiClient__BaseUrl="http://127.0.0.1:$TARGET_PORT/taxbaik/api/"
|
||||||
|
export DOTNET_PRINT_TELEMETRY_MESSAGE=false
|
||||||
|
|
||||||
|
# Run dotnet process
|
||||||
|
nohup /usr/bin/dotnet TaxBaik.Web.dll > "web_${TARGET_PORT}.log" 2>&1 &
|
||||||
|
NEW_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Verify process is running
|
||||||
|
if ! ps -p $NEW_PID > /dev/null; then
|
||||||
|
echo "❌ Failed to start dotnet process on port $TARGET_PORT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Health Check Loop
|
||||||
|
echo "=== Health Checking Port $TARGET_PORT ==="
|
||||||
|
ATTEMPTS=20
|
||||||
|
SUCCESS=false
|
||||||
|
for i in $(seq 1 $ATTEMPTS); do
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w '%{http_code}' "http://127.0.0.1:${TARGET_PORT}/taxbaik/healthz" 2>/dev/null || echo "000")
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
|
echo "✓ Health check passed on port $TARGET_PORT (Attempt $i/$ATTEMPTS)"
|
||||||
|
SUCCESS=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo " Waiting for health check... ($i/$ATTEMPTS, Status: $STATUS)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SUCCESS" = "false" ]; then
|
||||||
|
echo "❌ Health check failed. Rolling back..."
|
||||||
|
kill -9 $NEW_PID || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Switch Traffic
|
||||||
|
# Nginx never needs per-deploy changes: it always proxies to the persistent
|
||||||
|
# TaxBaik.Proxy on 127.0.0.1:5001, which reads this same PORT_FILE and
|
||||||
|
# forwards to whichever port is currently active. See CLAUDE.md section 6.
|
||||||
|
echo "=== Switching Traffic to Port $TARGET_PORT ==="
|
||||||
|
echo "$TARGET_PORT" > "$PORT_FILE"
|
||||||
|
echo "✓ Traffic routed to $TARGET_PORT (via TaxBaik.Proxy on 5001)"
|
||||||
|
|
||||||
|
# 7. Terminate Old App
|
||||||
|
echo "=== Stopping Old App on Port $ACTIVE_PORT ==="
|
||||||
|
# Find PID listening on ACTIVE_PORT
|
||||||
|
OLD_PID=$(ss -tlnp | grep ":$ACTIVE_PORT " | grep -oP 'pid=\K\d+' | head -n1)
|
||||||
|
if [ -n "$OLD_PID" ]; then
|
||||||
|
echo "Killing old process PID: $OLD_PID"
|
||||||
|
kill -15 $OLD_PID || kill -9 $OLD_PID
|
||||||
|
echo "✓ Old process terminated"
|
||||||
|
else
|
||||||
|
echo "No old process found on port $ACTIVE_PORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 8. Cleanup old deployment directories (Keep last 5)
|
||||||
|
echo "=== Cleaning Up Old Deployments ==="
|
||||||
|
ls -1dt $DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||||
|
echo "✓ Cleanup completed"
|
||||||
|
|
||||||
|
echo "===== ✅ Green/Blue Deployment Completed Successfully ====="
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:18-alpine
|
|
||||||
container_name: taxbaik-db
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: taxbaikdb
|
|
||||||
POSTGRES_USER: taxbaik
|
|
||||||
POSTGRES_PASSWORD: taxbaik123
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U taxbaik -d taxbaikdb"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
taxbaik-web:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.web
|
|
||||||
container_name: taxbaik-web
|
|
||||||
environment:
|
|
||||||
ASPNETCORE_ENVIRONMENT: Development
|
|
||||||
ASPNETCORE_URLS: http://0.0.0.0:5001
|
|
||||||
ConnectionStrings__Default: "Host=postgres;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
|
|
||||||
ports:
|
|
||||||
- "5001:5001"
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- ./publish/web:/app
|
|
||||||
|
|
||||||
taxbaik-admin:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.admin
|
|
||||||
container_name: taxbaik-admin
|
|
||||||
environment:
|
|
||||||
ASPNETCORE_ENVIRONMENT: Development
|
|
||||||
ASPNETCORE_URLS: http://0.0.0.0:5002
|
|
||||||
ConnectionStrings__Default: "Host=postgres;Database=taxbaikdb;Username=taxbaik;Password=taxbaik123"
|
|
||||||
ports:
|
|
||||||
- "5002:5002"
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- ./publish/admin:/app
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Admin Pattern Critique And WBS
|
||||||
|
|
||||||
|
대상은 어드민 Blog, 문의사항, 등록/수정 페이지 전반이다. 이 문서는 비판, 개선 방향, 정량 완료 기준을 한 곳에 둔다.
|
||||||
|
|
||||||
|
## Brutal Critique
|
||||||
|
|
||||||
|
| 영역 | 현재 문제 | 왜 위험한가 | 개선 기준 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| API-first 위반 | 어드민 Razor 컴포넌트가 `BlogService`, `InquiryService`, repository를 직접 주입 | 어드민을 클라이언트 사이드 Blazor WebAssembly로 운용할 때 구조가 깨지고 API 계약 테스트가 우회된다 | 모든 어드민 화면은 BrowserClient를 통해 `/api/*` 호출 |
|
||||||
|
| Blog 등록/수정 중복 | `BlogCreate.razor`와 `BlogEdit.razor`가 필드, JS 편집기, 저장 로직을 반복 | 한쪽만 수정되는 파편화가 생긴다 | `BlogForm.razor` + `BlogEditorJsModule` 패턴 |
|
||||||
|
| JS 과다/전역 상태 | `window.easyMDEInstance` 단일 전역 인스턴스 사용 | 페이지 이동/다중 편집/재렌더에서 내용 섞임 위험, Blazor 책임 경계가 흐려진다 | JS 제거 우선 검토, 불가피하면 JS module + element별 instance map + dispose |
|
||||||
|
| 문의 수정 착시 | `InquiryEdit`가 이름/전화/이메일/내용 수정 UI를 보여주지만 실제 저장은 상태/메모 중심 | 운영자가 저장 성공을 믿어도 핵심 데이터가 DB에 반영되지 않을 수 있다 | 전체 수정 API를 만들거나 해당 필드를 read-only 처리 |
|
||||||
|
| 문자열 상태 난립 | 문의 상태, 서비스 유형이 UI 문자열/API 문자열/DB 값으로 분산 | 오타 하나가 통계와 필터를 깨뜨린다 | enum/공통코드/상태 mapper 단일화 |
|
||||||
|
| 삭제 위험 | Blog/Inquiry 삭제가 즉시 hard delete | 운영 감사, 상담 이력, SEO URL 보존에 취약 | soft delete 또는 archive 정책 |
|
||||||
|
| 정합성 부족 | Blog slug 생성이 전체 목록 조회 기반 | 동시 생성 충돌에 약하고 데이터가 늘면 느려진다 | DB unique index + 충돌 재시도 |
|
||||||
|
| 템플릿 부재 | CRUD 페이지마다 버튼, 오류, 로딩, 페이징 패턴이 다름 | 바이브코딩식 흔들림이 반복된다 | List/Form/Detail/PageState 템플릿화 |
|
||||||
|
| 배포 완료 착시 | 문서상 완료 항목과 운영 검증 항목이 섞임 | 체크박스가 실제 성공을 대체한다 | WBS는 수치, 로그, CI URL로만 완료 |
|
||||||
|
|
||||||
|
## Target Admin Pattern
|
||||||
|
|
||||||
|
```text
|
||||||
|
Razor Page/Form
|
||||||
|
-> BrowserClient with JWT
|
||||||
|
-> Controller DTO
|
||||||
|
-> Application Service
|
||||||
|
-> Repository
|
||||||
|
-> DB constraints/indexes
|
||||||
|
```
|
||||||
|
|
||||||
|
어드민은 클라이언트 사이드 Blazor WebAssembly 기준이다. 예외는 명시해야 한다. 서버 전용 컴포넌트가 Application Service를 직접 호출해야 한다면 `ENGINEERING_HARNESS.md`의 API-first 기준에 대한 사유와 제거 예정 WBS를 남긴다.
|
||||||
|
|
||||||
|
## Quantitative Success Metrics
|
||||||
|
|
||||||
|
| 지표 | 기준값 | 측정 방법 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Admin direct service injection | 0건 | `rg "@inject .*Service|@inject I.*Repository" src/TaxBaik.Web.Client/Components/Admin` |
|
||||||
|
| Blog create/edit duplicate fields | 0개 중복 폼 | `BlogForm.razor` 단일 사용 여부 |
|
||||||
|
| Admin JavaScript surface | 필수 module만 허용 | `window.*` 전역 admin JS 0건, JS interop 사유 문서화 |
|
||||||
|
| Inquiry visible-but-unsaved fields | 0개 | E2E로 수정 후 API 재조회 |
|
||||||
|
| Protected admin API anonymous access | 0개 | API smoke에서 401/403 확인 |
|
||||||
|
| CI required gates | 6/6 통과 | build, unit, publish, deploy, browser e2e, api smoke |
|
||||||
|
| Playwright admin flows | 8개 이상 통과 | login, blog CRUD, inquiry CRUD/status, responsive, password, smoke |
|
||||||
|
| DB integrity constraints | 핵심 테이블 100% | PK, FK, unique/check/index 리뷰 |
|
||||||
|
| WBS evidence coverage | 100% | 각 완료 항목에 command/log/test 파일 기재 |
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
| Phase | 목적 | 종료 조건 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| P0 Harness | 기준 고정과 문서 최소화 | 이 문서와 Engineering Harness가 README에서 참조됨 |
|
||||||
|
| P1 Stabilize | Blog/Inquiry 착시와 중복 제거 | API-first 전환, 공통 폼, 정합성 테스트 통과 |
|
||||||
|
| P2 Harden | DB 제약, 충돌 방지, 삭제 정책 | migration + 회귀 테스트 + E2E 통과 |
|
||||||
|
| P3 Standardize | CRUD 템플릿화와 반복 패턴 제거 | 신규 CRUD 생성 시 템플릿만 사용 |
|
||||||
|
| P4 Integrate | 더존 UX 정신 내재화 | 고밀도 화면, 표준 동선, 빠른 입력, 상태 가시성, 회귀 최소화 검증 |
|
||||||
|
| P5 Operate | CI/CD와 운영 지표 고도화 | 배포본 기준 smoke/E2E/로그 알림 안정화 |
|
||||||
|
|
||||||
|
## Detailed WBS
|
||||||
|
|
||||||
|
| ID | 작업 | 산출물 | 정량 완료 기준 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| P0-01 | 문서 기준점 정리 | `docs/INDEX.md`, `ENGINEERING_HARNESS.md` | canonical 문서 3개 이하, README 링크 1곳 |
|
||||||
|
| P0-02 | 기존 장문 문서 역할 축소 | README 문서 섹션 정리 | `CLAUDE.md`를 보조자료로 표시 |
|
||||||
|
| P1-01 | Blog API client 도입 | `IBlogBrowserClient`, `BlogBrowserClient` | Blog admin page direct service/repository injection 0건 |
|
||||||
|
| P1-02 | Blog 공통 폼 도입 | `BlogForm.razor` | create/edit 필드 중복 0건, 저장 E2E 2개 통과 |
|
||||||
|
| P1-03 | Markdown editor JS 최소화/격리 | Blazor 대체 또는 JS module | 전역 `window.easyMDEInstance` 사용 0건, JS interop 사유 명시 |
|
||||||
|
| P1-04 | Inquiry 수정 계약 확정 | `UpdateInquiryRequest` 또는 read-only UI | 화면 표시 editable 필드와 저장 필드 불일치 0건 |
|
||||||
|
| P1-05 | Inquiry API client 도입 | `IInquiryBrowserClient` 정비 | Inquiry admin direct service injection 0건 |
|
||||||
|
| P1-06 | 상태/서비스 유형 단일화 | enum/common code/mapper | 상태 문자열 하드코딩 UI 위치 0건 또는 공통 상수 참조 |
|
||||||
|
| P2-01 | Blog slug 충돌 방지 | unique index + retry | 동시 생성 테스트 1개 통과 |
|
||||||
|
| P2-02 | 삭제 정책 정리 | soft delete migration 또는 archive 정책 | hard delete 운영 엔티티 0건 또는 예외 문서화 |
|
||||||
|
| P2-03 | DB index 점검 | migration | 목록/검색/상태 필터 explain 기준 seq scan 위험 제거 |
|
||||||
|
| P2-04 | 낙관적 충돌 방지 | `updatedAt` 조건부 update | stale update API 테스트 1개 이상 통과 |
|
||||||
|
| P3-01 | CRUD 템플릿 작성 | page/form/client/test skeleton | 신규 admin CRUD 생성 시간 30% 감소 |
|
||||||
|
| P3-02 | 공통 PageState/Error 처리 | reusable component/service | admin page 중복 try/catch/snackbar 패턴 50% 감소 |
|
||||||
|
| P3-03 | 메뉴/라우팅 표준화 | route registry 또는 constants | admin route 문자열 중복 50% 감소 |
|
||||||
|
| P4-01 | 더존 UX 패턴 캡슐화 | 고밀도 grid/form/template 규칙 | 신규 어드민 화면이 템플릿을 따르지 않는 경우 0건 |
|
||||||
|
| P4-02 | UX 회귀 검증 | responsive, keyboard flow, density, state visibility test | 핵심 CRUD 화면 E2E 100% 통과 |
|
||||||
|
| P5-01 | CI gate 명문화 | workflow 체크 목록 | 6개 gate 모두 required |
|
||||||
|
| P5-02 | 배포본 API smoke 확장 | workflow curl 추가 | Blog/Inquiry create-read-update test 2xx |
|
||||||
|
| P5-03 | 운영 회귀 대시보드 | test report/version endpoint | 배포 커밋과 E2E 결과 추적 가능 |
|
||||||
|
| P4-03 | 기존 20개+ 어드민 화면을 SmartAdmin 5.5 참조(`legacy/smartadmin/`, `DOUZONE_UX_GUIDE.md`)로 재단장 (2026-07-03 시점 미착수, 향후 별도 진행) | 각 화면의 색상/카드/타이포그래피 갱신 | SmartAdmin 매핑 표 기준 적용 화면 수 / 전체 화면 수 100% |
|
||||||
|
|
||||||
|
## Immediate Refactor Order
|
||||||
|
|
||||||
|
1. `InquiryEdit` 착시 제거: 전체 수정 API를 추가하거나 저장 안 되는 필드를 read-only로 바꾼다.
|
||||||
|
2. `BlogForm.razor`를 만들고 create/edit 중복을 제거한다.
|
||||||
|
3. Blog/Inquiry 어드민 페이지를 BrowserClient 경유로 바꾼다.
|
||||||
|
4. 상태/서비스 유형 문자열을 단일 source로 모은다.
|
||||||
|
5. DB 제약과 삭제 정책을 migration으로 고정한다.
|
||||||
|
|
||||||
|
## Completion Rule
|
||||||
|
|
||||||
|
WBS 항목은 다음 네 가지가 모두 있어야 완료다.
|
||||||
|
|
||||||
|
- 관련 코드 또는 문서 diff
|
||||||
|
- 로컬 검증 명령과 결과
|
||||||
|
- CI/CD workflow 성공
|
||||||
|
- 배포본 기준 API 또는 Browser E2E 증거
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Combo Policy
|
||||||
|
|
||||||
|
이 문서는 TaxBaik 어드민의 콤보 정책을 정한다. 여기서 콤보는 `MudSelect`, `MudAutocomplete`, `MudChip`, 상태 필터, 코드 선택 입력을 포함한다.
|
||||||
|
|
||||||
|
## Policy
|
||||||
|
|
||||||
|
- 닫힌 집합은 `MudSelect`를 쓴다.
|
||||||
|
- 열린 집합 또는 검색이 필요한 집합은 `MudAutocomplete`를 쓴다.
|
||||||
|
- 상태/유형/등급처럼 값이 고정된 항목은 문자열 직접 입력을 금지한다.
|
||||||
|
- 선택한 값은 저장 값과 표시 값을 분리한다.
|
||||||
|
- 표시 값은 사람이 읽는 라벨, 저장 값은 코드값이어야 한다.
|
||||||
|
- `null` 허용 여부는 UI에서 명시한다.
|
||||||
|
- `전체`, `선택 안 함`, `기타`는 서로 다른 의미로 취급한다.
|
||||||
|
- 다중 선택이 필요하면 단일 선택 콤보를 억지로 재사용하지 않는다.
|
||||||
|
|
||||||
|
## Closed Set
|
||||||
|
|
||||||
|
다음 경우 `MudSelect`를 기본으로 사용한다.
|
||||||
|
|
||||||
|
- 상태
|
||||||
|
- 세금 유형
|
||||||
|
- 신고 유형
|
||||||
|
- 위험도
|
||||||
|
- 고정 서비스 유형
|
||||||
|
- 공지 유형
|
||||||
|
|
||||||
|
규칙:
|
||||||
|
|
||||||
|
- 값은 상수, enum, 공통코드 중 하나에서만 가져온다.
|
||||||
|
- `MudSelectItem`의 라벨과 값은 일치하는 쌍으로 관리한다.
|
||||||
|
- 운영자가 값의 의미를 추측해야 하는 항목은 콤보로 두지 않는다.
|
||||||
|
|
||||||
|
## Search Set
|
||||||
|
|
||||||
|
다음 경우 `MudAutocomplete`를 기본으로 사용한다.
|
||||||
|
|
||||||
|
- 고객 선택
|
||||||
|
- 회사 선택
|
||||||
|
- 데이터가 많아 스크롤 선택이 비효율적인 경우
|
||||||
|
|
||||||
|
규칙:
|
||||||
|
|
||||||
|
- 검색어 입력 후 서버 또는 클라이언트 필터 결과를 보여준다.
|
||||||
|
- 결과가 적을 때는 `MudSelect`보다 `MudAutocomplete`를 우선하지 않는다.
|
||||||
|
- 선택 후 보여주는 텍스트와 저장되는 id를 분리한다.
|
||||||
|
|
||||||
|
## Display Rules
|
||||||
|
|
||||||
|
- 목록에서는 상태를 칩으로 보여준다.
|
||||||
|
- 폼에서는 텍스트보다 구조화된 값으로 저장한다.
|
||||||
|
- 필터에서는 현재 선택값이 명확히 보이게 한다.
|
||||||
|
- `Clearable`은 의미가 명확한 경우에만 켠다.
|
||||||
|
|
||||||
|
## Standard Sources
|
||||||
|
|
||||||
|
- 상태 값은 `InquiryStatusMapper` 또는 전용 enum을 사용한다.
|
||||||
|
- 공지/신고/세무 정보는 각 도메인별 공통코드 소스를 둔다.
|
||||||
|
- 고객/회사 선택은 검색형 콤보로 통일한다.
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
- 같은 화면에 `MudSelect`와 자유 텍스트 입력을 섞어 같은 의미를 표현
|
||||||
|
- 코드값과 표시값을 뒤섞어서 저장
|
||||||
|
- 콤보 옵션을 화면마다 하드코딩
|
||||||
|
- `기타`를 예외 처리처럼 쓰고 실제 저장 값은 제각각 두는 것
|
||||||
|
- `전체`를 저장 값으로 사용
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- 신규 어드민 화면은 이 문서의 `Closed Set`/`Search Set` 중 하나를 명시해야 한다.
|
||||||
|
- 상태/유형/등급 입력이 있는 화면은 콤보 정책 위반이 없어야 한다.
|
||||||
|
- 고객/회사처럼 데이터가 많은 항목은 검색형 선택으로 통일한다.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Common Code Policy
|
||||||
|
|
||||||
|
이 문서는 어드민 콤보, 상태, 유형, 출처 값의 단일 기준이다. 값은 DB `common_codes`를 우선 사용하고, 화면은 표시명만 바꾼다.
|
||||||
|
|
||||||
|
## Canonical Rules
|
||||||
|
|
||||||
|
- `code_value`는 저장 키다.
|
||||||
|
- `code_name`은 화면 표시값이다.
|
||||||
|
- `code_value`는 공백을 넣지 않는다.
|
||||||
|
- `code_group`도 공백 없이 대문자/숫자/언더스코어 중심의 안정된 키를 쓴다.
|
||||||
|
- 새 콤보를 추가할 때는 먼저 `common_codes`에 그룹을 추가한다.
|
||||||
|
- 화면 하드코딩 배열은 금지한다. 불가피하면 임시 폴백으로만 두고 제거 계획을 함께 적는다.
|
||||||
|
- 같은 의미의 값이 테이블마다 다르면 저장값을 먼저 통일하고 마이그레이션으로 이관한다.
|
||||||
|
|
||||||
|
## Grouping Rules
|
||||||
|
|
||||||
|
- 상태값: `*_STATUS`
|
||||||
|
- 유형값: `*_TYPE`
|
||||||
|
- 출처값: `*_SOURCE`
|
||||||
|
- 위험도/스코어: `*_LEVEL`
|
||||||
|
|
||||||
|
## Standard Groups
|
||||||
|
|
||||||
|
- `INQUIRY_SERVICE_TYPE`
|
||||||
|
- `INQUIRY_STATUS`
|
||||||
|
- `CONSULTING_ACTIVITY_TYPE`
|
||||||
|
- `ANNOUNCEMENT_DISPLAY_TYPE`
|
||||||
|
- `CLIENT_STATUS`
|
||||||
|
- `CLIENT_SERVICE_TYPE`
|
||||||
|
- `CLIENT_TAX_TYPE`
|
||||||
|
- `CLIENT_SOURCE`
|
||||||
|
- `CONTRACT_SERVICE_TYPE`
|
||||||
|
- `REVENUE_SERVICE_TYPE`
|
||||||
|
- `FILING_TYPE`
|
||||||
|
- `TAX_RISK_LEVEL`
|
||||||
|
- `BUSINESS_TYPE`
|
||||||
|
- `FAQ_CATEGORY`
|
||||||
|
|
||||||
|
## Data Rules
|
||||||
|
|
||||||
|
- DB seed와 운영 데이터의 저장값이 다르면 UI를 먼저 맞추지 말고 저장값을 먼저 정규화한다.
|
||||||
|
- 한글 코드값을 사용하더라도 컬럼 길이를 먼저 검토하고, 업무 테이블과 마스터 테이블을 함께 조정한다.
|
||||||
|
- 한글 `code_value`가 필요하면 DB 컬럼 길이와 인덱스 길이를 먼저 확인하고, 초과 가능성이 있으면 표시값과 저장값을 분리한다.
|
||||||
|
- 표시용 문구가 길면 `code_name`에 둔다.
|
||||||
|
|
||||||
|
## UI Rules
|
||||||
|
|
||||||
|
- `MudSelect`는 `code_value`를 바인딩하고 `code_name`을 보여준다.
|
||||||
|
- 검색형이면 `MudAutocomplete`를 쓰고, 선택형이면 `MudSelect`를 쓴다.
|
||||||
|
- 자유 입력을 허용하지 않을 값은 텍스트 필드로 만들지 않는다.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- 신규 콤보 추가 시 DB 마이그레이션이 먼저 존재해야 한다.
|
||||||
|
- 화면에 하드코딩된 선택값이 없어야 한다.
|
||||||
|
- 기존 저장값과 신규 저장값의 불일치가 없어야 한다.
|
||||||
|
|
||||||
|
## Audit
|
||||||
|
|
||||||
|
- 점검 SQL은 [docs/ops/COMMON_CODE_AUDIT.sql](./ops/COMMON_CODE_AUDIT.sql)를 사용한다.
|
||||||
|
- 그룹 공백, 값 공백, 길이 초과, 테이블 매핑 불일치는 이 SQL에서 먼저 잡는다.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# DOUZONE UX Guide
|
||||||
|
|
||||||
|
이 문서는 TaxBaik 어드민 UX의 기준선이다. 목표는 더존 세무회계프로그램류의 고밀도 운영 화면을 구현하되, TaxBaik의 도메인과 검증 규칙을 유지하는 것이다.
|
||||||
|
|
||||||
|
## UX Principles
|
||||||
|
|
||||||
|
- 고밀도 우선: 한 화면에서 상태, 입력, 결과, 작업을 함께 본다.
|
||||||
|
- 표준 동선 우선: 목록 -> 상세 -> 수정 -> 저장 흐름을 기본으로 둔다.
|
||||||
|
- 빠른 입력 우선: 마우스 최소, 키보드/단축 동선 최대, 기본값 명확화.
|
||||||
|
- 상태 가시성 우선: 진행중/성공/실패/비활성/삭제됨을 즉시 구분 가능하게 한다.
|
||||||
|
- 회귀 최소화 우선: 같은 화면 패턴은 같은 컴포넌트와 같은 구조를 사용한다.
|
||||||
|
- 추측 금지: 의미가 불명확한 텍스트, 상태, 버튼, 색상은 새로 만들지 않는다.
|
||||||
|
|
||||||
|
## Layout Template
|
||||||
|
|
||||||
|
어드민 화면은 기본적으로 아래 구조를 따른다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
PageHeader
|
||||||
|
FilterBar or ActionBar
|
||||||
|
ContentSurface
|
||||||
|
-> DenseGrid or DetailPanel
|
||||||
|
-> EmptyState when empty
|
||||||
|
-> Paging/Footer when needed
|
||||||
|
```
|
||||||
|
|
||||||
|
권장 규칙:
|
||||||
|
|
||||||
|
- 페이지 제목은 1개만 둔다.
|
||||||
|
- 보조 설명은 1줄만 둔다.
|
||||||
|
- 주요 액션은 우측 상단 또는 헤더 우측에 둔다.
|
||||||
|
- 목록은 `Dense`를 기본으로 한다.
|
||||||
|
- 상세/수정은 좌우 2열 또는 상단 요약 + 하단 폼 패턴을 우선한다.
|
||||||
|
|
||||||
|
## Component Template
|
||||||
|
|
||||||
|
### Page Header
|
||||||
|
|
||||||
|
- 구성: `Eyebrow`, `Title`, `Subtitle`, `Primary Action`
|
||||||
|
- 역할: 화면 맥락 고정, 다음 행동 제시
|
||||||
|
- 금지: 동일 화면에 헤더가 2개 이상 존재
|
||||||
|
|
||||||
|
### Dense Grid
|
||||||
|
|
||||||
|
- 행 간격은 좁게 유지한다.
|
||||||
|
- 컬럼은 우선순위 순으로 배치한다.
|
||||||
|
- 상태는 텍스트 대신 칩/색상/아이콘으로 함께 보여준다.
|
||||||
|
- 작업 버튼은 `보기`, `수정`, `삭제`처럼 짧고 일관되게 둔다.
|
||||||
|
|
||||||
|
### Form
|
||||||
|
|
||||||
|
- 기본값은 채워진 상태로 시작한다.
|
||||||
|
- 저장 전 필수 검증은 화면에서 즉시 보인다.
|
||||||
|
- 저장되지 않는 필드는 read-only로 바꾼다.
|
||||||
|
- 입력이 많은 폼은 섹션으로 나누되, 섹션 수는 최소화한다.
|
||||||
|
|
||||||
|
### Empty State
|
||||||
|
|
||||||
|
- 데이터 없음, 필터 결과 없음, 로드 실패를 구분한다.
|
||||||
|
- 단순 문구보다 다음 행동 버튼을 함께 둔다.
|
||||||
|
|
||||||
|
### Status Chip
|
||||||
|
|
||||||
|
- 상태는 문자열 그대로 노출하지 말고 칩으로 시각화한다.
|
||||||
|
- 색상은 의미를 유지한다.
|
||||||
|
- 동일 상태는 동일 색을 사용한다.
|
||||||
|
|
||||||
|
## SmartAdmin 5.5 Design Reference (2026-07-03, 신규 화면부터 적용)
|
||||||
|
|
||||||
|
이 섹션은 어드민의 **시각적 스킨**(색상, 카드 크롬, 로그인 화면 스타일, 셸 레이아웃) 기준이다. 위 UX Principles(고밀도, 표준 동선, 더존 정신)는 그대로 유지하고, SmartAdmin 5.5는 그 위에 입히는 룩앤필만 담당한다.
|
||||||
|
|
||||||
|
- 소스: `legacy/smartadmin/`(로컬에 이미 포함된 v5.5 HTML/CSS 데모 패키지, Bootstrap 5 기반). 정확한 색상/여백 값이 필요하면 이 디렉터리를 직접 참조한다(추측 금지).
|
||||||
|
- 적용 범위: **향후 신규 어드민 화면부터**. 기존 20개+ 화면(Dashboard, Blog, Inquiry, Client 등)은 이번엔 재단장하지 않는다. 기존 화면을 다른 이유로 수정할 때 자연스럽게 이 기준으로 수렴시킨다.
|
||||||
|
|
||||||
|
### 매핑 표
|
||||||
|
|
||||||
|
| SmartAdmin 5.5 참조 | 파일 | TaxBaik MudBlazor 대응 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 상단 `<header>` 툴바 | `dashboard-control-center.html` | `AdminShell`의 `MudAppBar` |
|
||||||
|
| `<aside class="app-sidebar">` (로고 + 필터 입력 + 메뉴) | `dashboard-control-center.html` | `AdminShell`의 `MudDrawer` (검색/필터 입력 포함) |
|
||||||
|
| 로그인 카드 (`rounded-4`, 반투명 다크 글래스, `bg-dark bg-opacity-50`) | `auth-login.html` | `AdminLoginForm.razor`의 `MudPaper` 카드 — 반투명/블러 배경 톤 참고 |
|
||||||
|
| 색상 팔레트 | `colorpalette.html`, `css/smartapp.min.css` | `App.razor`의 `MudTheme.Palette` (Primary/Secondary/Tertiary 등) |
|
||||||
|
| 카드형 위젯 | `dashboard-*.html` | `AdminMetricCard`, `MudPaper` 기반 카드 |
|
||||||
|
|
||||||
|
### 적용 규칙
|
||||||
|
|
||||||
|
- 새 어드민 화면을 만들 때: 레이아웃/동선/밀도는 `DOUZONE_UX_GUIDE.md` 상단 원칙을 따르고, 색상·카드 모서리·그림자·로그인류 화면의 톤은 `legacy/smartadmin/`을 참조해 `MudTheme`/CSS 변수로 반영한다.
|
||||||
|
- SmartAdmin 원본은 jQuery/Bootstrap 5 기반이므로 JS/DOM 구조를 그대로 이식하지 않는다. **시각적 토큰(색, 반경, 여백, 타이포그래피)만** 가져오고, 동작은 MudBlazor 컴포넌트로 구현한다.
|
||||||
|
- 기존 화면을 SmartAdmin 스타일로 일괄 재단장하는 작업은 별도 WBS로 `docs/ADMIN_PATTERN_CRITIQUE_WBS.md`에 등록한 뒤 진행한다(이번 범위 아님).
|
||||||
|
|
||||||
|
## Text And Labels
|
||||||
|
|
||||||
|
- 라벨은 짧게 쓴다.
|
||||||
|
- 같은 개념은 같은 단어를 쓴다.
|
||||||
|
- 약어는 화면 전체에서 통일한다.
|
||||||
|
- 운영자가 오해할 수 있는 추상적인 표현은 금지한다.
|
||||||
|
|
||||||
|
## Serving Rules
|
||||||
|
|
||||||
|
- 공개 사이트는 SSR, 어드민은 Blazor WebAssembly 기준으로 본다.
|
||||||
|
- 어드민 화면은 API-first 경유를 기본으로 한다.
|
||||||
|
- JS는 불가피할 때만 사용하고, 모듈로 격리한다.
|
||||||
|
- 상태/메뉴/라우트/버튼은 문자열 흩뿌리기를 금지하고 공통 상수 또는 템플릿으로 묶는다.
|
||||||
|
|
||||||
|
## Reference Rules
|
||||||
|
|
||||||
|
- 이 문서를 어드민 UX의 1차 기준으로 사용한다.
|
||||||
|
- 세부 코드 규칙은 [ENGINEERING_HARNESS.md](./ENGINEERING_HARNESS.md)를 따른다.
|
||||||
|
- 콤보/선택/검색 규칙은 [COMBO_POLICY.md](./COMBO_POLICY.md)를 따른다.
|
||||||
|
- 공통코드/저장값 규칙은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)를 따른다.
|
||||||
|
- 패턴 비판과 WBS는 [ADMIN_PATTERN_CRITIQUE_WBS.md](./ADMIN_PATTERN_CRITIQUE_WBS.md)를 따른다.
|
||||||
|
- 문서 인덱스는 [INDEX.md](./INDEX.md)를 따른다.
|
||||||
|
|
||||||
|
## Prohibited Patterns
|
||||||
|
|
||||||
|
- 목록마다 서로 다른 헤더 구조
|
||||||
|
- 버튼 색과 의미의 중복/충돌
|
||||||
|
- 저장 안 되는 필드를 편집 가능한 척 보여주기
|
||||||
|
- 전역 JS 상태에 의존하는 편집기
|
||||||
|
- 같은 CRUD 화면의 개별 구현체마다 다른 DOM/행 높이/행동 패턴
|
||||||
|
- 불필요한 중첩 컴포넌트와 과한 추상화
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- 신규 어드민 화면은 이 문서의 레이아웃/컴포넌트 규칙 중 최소 80%를 따른다.
|
||||||
|
- 기존 화면은 새로 건드릴 때 이 문서로 수렴한다.
|
||||||
|
- 화면 추가 시 `PageHeader`, `EmptyState`, `DenseGrid`, `Form` 패턴 중 하나 이상을 재사용한다.
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Engineering Harness
|
||||||
|
|
||||||
|
이 문서는 TaxBaik 코드가 매번 흔들리지 않도록 막는 최소 하네스다. 여기에 없는 내용은 추측하지 않고 코드, 테스트, 운영 로그, DB 스키마 중 하나로 확인한다.
|
||||||
|
|
||||||
|
## Quick Guardrails
|
||||||
|
|
||||||
|
- 라우팅 루프 금지
|
||||||
|
- 공개/관리자 분리 검증
|
||||||
|
- smoke 재사용
|
||||||
|
- lint는 있으면 앞단 실패, 없으면 강제 생성 금지
|
||||||
|
|
||||||
|
## Non-Negotiables
|
||||||
|
|
||||||
|
| 항목 | 기준 | 실패 판정 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Runtime | ASP.NET Core `net10.0` 기준 유지 | 프로젝트별 TargetFramework 불일치 |
|
||||||
|
| Public UI | 홈페이지/공개 페이지는 서버 사이드 렌더링 기준 | 공개 페이지가 불필요하게 WASM 번들에 의존 |
|
||||||
|
| Admin UI | 어드민은 클라이언트 사이드 Blazor WebAssembly + MudBlazor + API-first | 어드민 컴포넌트가 Application/Repository를 직접 주입 |
|
||||||
|
| API | 모든 운영 기능은 `/api/*` DTO 경유 | UI 전용 서비스 호출만 존재 |
|
||||||
|
| Auth | JWT 인증, 관리자 API는 `[Authorize]` | 익명으로 관리자 데이터 접근 가능 |
|
||||||
|
| Deploy | Gitea Actions CI/CD만 배포 경로 | 수동 SSH/복사로 운영 반영 |
|
||||||
|
| Evidence | 빌드, 테스트, E2E, API smoke 로그 | "확인함", "될 것" 같은 진술 |
|
||||||
|
| Admin Render | Router/Routes에는 전역 `@rendermode`를 두지 않고 페이지별로 지정한다. 로그인 페이지만 `prerender: true`로 최초 HTML에 폼을 포함시키고, 나머지 `[Authorize]` 페이지는 `prerender: false`를 유지한다 | Router/Routes에 전역 렌더모드가 다시 생기거나, 로그인 폼이 최초 HTML에 없다 |
|
||||||
|
| KST Timestamp | CI/배포/백업 폴더명과 추적 일시는 `TZ=Asia/Seoul` | `date`가 기본 UTC 또는 서버 로캘에 종속 |
|
||||||
|
| Repo Root | 소스는 `src/`, 문서는 `docs/`, 테스트는 `tests/`, 스크립트는 `scripts/`, 마이그레이션은 `db/`, 배포 참조 설정은 `deploy/`에 둔다. 루트에는 진입점 설정(`CLAUDE.md`, `README.md`, `.gitignore`, `package.json` 등)만 남긴다 | 루트에 스크린샷/로그/1회성 디버그 스크립트/빌드 산출물이 커밋된다 |
|
||||||
|
|
||||||
|
## Architecture Guardrails
|
||||||
|
|
||||||
|
- Domain은 엔티티, enum, repository interface만 가진다.
|
||||||
|
- Application은 use case와 검증 규칙을 가진다. HTTP, JS, MudBlazor, DB 연결 세부를 모른다.
|
||||||
|
- Infrastructure는 Dapper SQL과 외부 시스템 구현을 가진다.
|
||||||
|
- Web은 Controller, 공개 Razor Pages SSR, Blazor host, 인증/서빙 설정을 가진다.
|
||||||
|
- Web.Client/Admin UI는 클라이언트 사이드 Blazor WebAssembly로 본다. 서버 DI 서비스에 의존하지 않고 API client만 호출한다.
|
||||||
|
- 관리자 호스트가 prerender를 사용하더라도 데이터 접근 원칙은 WASM + API-first다. prerender는 초기 마크업용이며 비즈니스 로직의 근거가 아니다.
|
||||||
|
- 어드민 기본 렌더는 WASM이다. 다만 초기 흰 화면 방지 목적의 셸 프리렌더와 로그인 화면의 서버 프리렌더는 허용한다. 비즈니스 로직은 여전히 API-first다.
|
||||||
|
- 로그인 화면은 예외적으로 “먼저 보여야 하는 화면”이다. JS 바인딩/텔레메트리/하이드레이션이 실패해도 로그인 폼 자체는 화면에 남아 있어야 하며, 실패 시 흰 화면이나 빈 본문을 허용하지 않는다.
|
||||||
|
- 로그인 화면은 공통 추적보다 가시성을 우선한다. 추적은 보조이며, 로그인 폼 렌더를 가로막는 코드는 금지한다.
|
||||||
|
- 로그인 화면의 JS는 `try/catch`로 감싸고, 실패해도 사용자 입력과 화면 표시를 막지 않아야 한다.
|
||||||
|
- JavaScript는 최소화한다. 브라우저 API, 인증 토큰 저장, 서드파티 편집기처럼 Blazor/MudBlazor만으로 해결하기 부적절한 경우에만 JS module로 격리한다.
|
||||||
|
- 상속은 프레임워크 요구 또는 명확한 다형성 모델에만 사용한다. 폼/테이블/CRUD 재사용은 기본적으로 컴포넌트 합성과 작은 service/client로 처리한다.
|
||||||
|
- 과유불급을 지킨다. 실제 재사용이 2곳 미만이면 새 추상화를 만들지 말고 기존 컴포넌트를 직접 조합한다.
|
||||||
|
- CI, 배포 폴더명, 백업명, 버전 추적에 쓰는 시간 문자열은 `TZ=Asia/Seoul`을 기본으로 한다.
|
||||||
|
- 클라이언트 오류 수집은 서버/브라우저를 보호하는 목적의 제한형 수집으로만 운영한다. 건당 비동기 전송, 중복 억제, 분당 상한, 서버 rate limit, 실패 시 조용히 폐기, 재시도 폭주 금지.
|
||||||
|
- 브라우저에서 발생한 JS 오류는 운영 장애 탐지를 위한 샘플 데이터로만 취급하고, 전체 이벤트 스트림을 보존하려는 설계는 금지한다.
|
||||||
|
- 텔레그램 알림은 운영자의 주의 채널이지 이벤트 버스가 아니다. 같은 원인/같은 기간의 중복 알림은 억제하고, 리포트/오류/문의/시작 장애는 종류별 시간창을 분리한다.
|
||||||
|
- 오류 알림에는 재현성 6요소를 포함한다: 화면, 기능, 액션, 단계, 데이터 식별자, 현재 라우트. 이 정보가 없으면 운영 대응이 끝나지 않은 것으로 본다.
|
||||||
|
- 루트에 새 파일을 직접 추가하지 않는다. 소스는 `src/`, 문서는 `docs/`, 테스트는 `tests/`, 스크립트는 `scripts/`, 마이그레이션은 `db/`, 배포 참조 설정은 `deploy/`에 둔다.
|
||||||
|
- 임시/스크래치 작업(스크린샷, 1회성 디버그 스크립트, 로그)은 저장소 밖(OS/세션 임시 폴더)에서 하고 절대 커밋하지 않는다. 저장소 안에서 꼭 필요하면 `.gitignore`에 등록된 `.scratch/`만 사용한다.
|
||||||
|
- 커밋 전 `git status`로 루트에 낯선 파일이 생기지 않았는지 확인한다. 빌드 산출물(runtimeconfig.json, deps.json, wwwroot 산출물 등)이 루트나 프로젝트 폴더 밖에 커밋되면 안 된다.
|
||||||
|
- 재현 맥락은 페이지별 수동 JS 호출이 아니라 `AdminTelemetryContext` 같은 공통 컴포넌트가 담당한다. 새 어드민 화면은 레이아웃 경유 기본값을 자동 상속해야 하며, 예외만 명시적으로 덮어쓴다.
|
||||||
|
|
||||||
|
## Code Quality Harness
|
||||||
|
|
||||||
|
| 원칙 | 적용 방식 |
|
||||||
|
| --- | --- |
|
||||||
|
| SOLID | 페이지는 orchestration만, 검증은 Application, 저장은 Repository, HTTP 계약은 DTO |
|
||||||
|
| 유지보수 | Blog/Inquiry 같은 CRUD는 `List`, `Form`, `Client`, `Dto`, `Validator` 패턴으로 고정 |
|
||||||
|
| 리팩토링 | 동작 보존 테스트를 먼저 추가하고 작은 단위로 이동 |
|
||||||
|
| 일관성 | 오류 응답은 ProblemDetails, 페이징은 `{ data, total, page, pageSize }` |
|
||||||
|
| 파편화 방지 | 같은 필드/상태/서비스유형 문자열은 enum/상수/공통 코드 중 하나로 단일화 |
|
||||||
|
| 과유불급 | 추상화는 2개 이상 실제 사용처가 생긴 뒤 도입 |
|
||||||
|
| 정규화 | 고객, 문의, 상담, 계약, 세금신고는 원천 테이블을 분리 |
|
||||||
|
| 역정규화 | 대시보드/검색/운영 요약용 스냅샷만 허용하고 원천 id와 갱신 시점을 저장 |
|
||||||
|
| 충돌방지 | 수정 API는 가능하면 `updatedAt` 또는 row version 기반 충돌 감지를 둔다 |
|
||||||
|
| 더존 UX 정신 | 더존 세무회계프로그램처럼 고밀도, 표준 동선, 빠른 입력, 상태 가시성, 회귀 최소화를 기본 UX 원칙으로 삼는다 |
|
||||||
|
| 추측금지 | 세법, 세율, 더존 필드, 운영 계정, 배포 결과는 공식 자료/코드/DB/로그 없이는 단정하지 않는다 |
|
||||||
|
| JS 최소화 | Blazor/MudBlazor 우선, 불가피한 JS는 module + dispose + 테스트 가능한 얇은 wrapper |
|
||||||
|
| 공통코드 | 상태/유형/출처/위험도는 `common_codes`를 우선 소스로 사용하고 화면 하드코딩을 금지 |
|
||||||
|
|
||||||
|
## Data Integrity Harness
|
||||||
|
|
||||||
|
- DB 제약 조건이 1차 방어선이다: NOT NULL, UNIQUE, FK, CHECK, index.
|
||||||
|
- Application validation은 사용자 메시지와 use case 규칙을 담당한다.
|
||||||
|
- UI validation은 빠른 피드백일 뿐이며 유일한 검증으로 보지 않는다.
|
||||||
|
- 관리자 수정 화면에 노출한 필드는 실제 저장되어야 한다. 저장하지 않는 필드는 read-only로 표시한다.
|
||||||
|
- 상태 전이는 허용 목록을 둔다. 임의 문자열 저장을 금지한다.
|
||||||
|
- 삭제는 운영 데이터 손실 위험이 있으면 soft delete 또는 archive를 우선 검토한다.
|
||||||
|
- 콤보 값은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)를 1차 기준으로 삼는다.
|
||||||
|
- 로그인 화면은 배포 전 브라우저 실증이 필수다. `dotnet build`만으로 로그인 화면 정상 표시를 완료로 선언하지 않는다.
|
||||||
|
- 로그인 화면 실증 기준은 최소 1회 실제 브라우저 응답, 로그인 폼 렌더, 입력 포커스 가능 여부 확인이다.
|
||||||
|
- 공개 루트와 관리자 루트는 반드시 분리 검증한다. `https://www.taxbaik.com/` 은 공용 홈페이지 제목을 가져야 하고, `https://www.taxbaik.com/taxbaik/admin/login` 은 관리자 제목을 가져야 한다.
|
||||||
|
- 배포 후 smoke 기준은 `scripts/taxbaik-smoke.sh`를 1차 기준으로 사용한다. CI와 브라우저 E2E는 이 스크립트를 재사용해야 한다.
|
||||||
|
- `UsePathBase("/taxbaik")`가 있는 Web 앱에서 `MapGet("/")` 같은 루트 리다이렉트를 추가할 때는, 반드시 루프 여부를 `curl`로 먼저 확인하고 나서만 반영한다. `/`와 `/taxbaik/`가 서로를 다시 가리키면 안 된다.
|
||||||
|
- nginx의 `location /`와 `location /taxbaik`를 건드린 뒤에는, 변경 직후 `curl -I https://www.taxbaik.com/`, `curl -I https://www.taxbaik.com/taxbaik/`, `curl -I https://www.taxbaik.com/taxbaik/admin/login` 순서로 확인하고 결과를 기록한다.
|
||||||
|
- 클라이언트 로그와 장애 진단 로그는 운영 데이터가 아니라 관측 데이터로 본다. 저장 실패는 사용자 흐름을 막지 않으며, 수집 실패 자체를 재시도 루프로 증폭하지 않는다.
|
||||||
|
- 동일 오류의 텔레그램 재알림은 일정 기간 1회로 제한하고, 재전송 목적의 루프는 금지한다.
|
||||||
|
- 데이터가 오류 재현에 필요하면 `entity`, `entityId`, `dataKey` 같은 최소 식별자만 남기고, 원문 데이터 전체를 로그에 싣지 않는다.
|
||||||
|
|
||||||
|
## API-First Admin Pattern
|
||||||
|
|
||||||
|
새 어드민 기능은 클라이언트 사이드 Blazor WebAssembly를 기준으로 아래 구조를 기본 템플릿으로 따른다.
|
||||||
|
|
||||||
|
| Layer | Naming | 책임 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| DTO | `CreateXRequest`, `UpdateXRequest`, `XResponse` | HTTP 계약 |
|
||||||
|
| Controller | `XController` | 인증, 라우팅, status code, ProblemDetails |
|
||||||
|
| Client | `IXBrowserClient`, `XBrowserClient` | JWT 포함 HTTP 호출 |
|
||||||
|
| Page | `XList.razor`, `XCreate.razor`, `XEdit.razor` | 화면 상태와 navigation |
|
||||||
|
| Form | `XForm.razor` | 입력 컴포넌트와 UI validation |
|
||||||
|
| Tests | unit + Playwright/API smoke | 회귀 방지 |
|
||||||
|
|
||||||
|
## FastEndpoints Framework
|
||||||
|
|
||||||
|
새 API 엔드포인트는 Controllers 대신 **FastEndpoints**로 구현한다.
|
||||||
|
|
||||||
|
| 규칙 | 실행 |
|
||||||
|
| --- | --- |
|
||||||
|
| Library | `FastEndpoints` v5.30.0 이상 |
|
||||||
|
| Naming | `Create[Entity]Endpoint`, `Get[Entity]Endpoint`, `List[Entity]Endpoint` 등 |
|
||||||
|
| Location | `src/TaxBaik.Web/Features/[DomainName]/` |
|
||||||
|
| Pattern | Request DTO → Endpoint class → Response DTO |
|
||||||
|
| Validation | FluentValidation (FastEndpoints 내장) |
|
||||||
|
| Registration | `builder.Services.AddFastEndpoints()` + `app.MapFastEndpoints()` |
|
||||||
|
| Coexistence | Controllers와 FastEndpoints는 PathBase 내에서 병행 가능 (URL 충돌만 피함) |
|
||||||
|
|
||||||
|
**주의**: 기존 Controllers에서 FastEndpoints로 마이그레이션 시, 기존 엔드포인트 URL(`/api/*`)이 변경되지 않도록 명시적 라우팅을 지정한다.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class GetBlogEndpoint : Endpoint<GetBlogRequest, GetBlogResponse>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Get("/api/blog/{id}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(GetBlogRequest req, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Logic here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rendering Boundary
|
||||||
|
|
||||||
|
| 영역 | 렌더링 | 데이터 접근 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Public Home/Blog/Contact | 서버 사이드 렌더링 | 서버 Application Service 직접 사용 가능 |
|
||||||
|
| Admin | 클라이언트 사이드 Blazor WebAssembly | JWT 포함 HTTP API만 사용 |
|
||||||
|
| Shared DTO | 서버/클라이언트 공유 가능 | UI 전용 상태와 DB 엔티티를 섞지 않음 |
|
||||||
|
|
||||||
|
공개 페이지의 SEO와 초기 로딩은 SSR로 최적화한다. 어드민은 앱처럼 동작해야 하므로 WebAssembly와 API 계약을 기준으로 설계한다.
|
||||||
|
|
||||||
|
## CI Harness
|
||||||
|
|
||||||
|
완료는 로컬 성공이 아니라 CI와 배포본 성공이다.
|
||||||
|
|
||||||
|
| Gate | Command/Check | Target |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Build | `dotnet build src/TaxBaik.sln -c Release --no-restore` | error 0 |
|
||||||
|
| Unit | `dotnet test src/TaxBaik.sln -c Release --no-build` | failed 0 |
|
||||||
|
| Browser | `npx playwright test --project="Desktop Chrome"` | failed 0 |
|
||||||
|
| Lint | 존재하는 경우 `dotnet format --verify-no-changes`, ESLint/Stylelint 등 프로젝트 기본 lint | 경고/오류 0 |
|
||||||
|
| API Smoke | login + protected admin API curl | HTTP 2xx |
|
||||||
|
| Deploy | `.gitea/workflows/deploy.yml` | success |
|
||||||
|
| Post Deploy | `.gitea/workflows/browser-e2e.yml` | success |
|
||||||
|
|
||||||
|
### Gitea Auth Harness
|
||||||
|
|
||||||
|
- Gitea API와 workflow dispatch에는 `GITEA_TOKEN_TAXBAIK`만 사용한다.
|
||||||
|
- `GITEA_TOKEN`은 쓰지 않는다.
|
||||||
|
- 인증 헤더는 `Authorization: token <GITEA_TOKEN_TAXBAIK>`를 기본으로 한다.
|
||||||
|
- 토큰 검증은 먼저 `GET /api/v1/user`로 확인하고, 그 다음 `workflow_dispatch`를 실행한다.
|
||||||
|
- `401 invalid username, password or token`이 나오면 토큰 이름, 공백, 환경 변수 scope를 먼저 확인한다.
|
||||||
|
|
||||||
|
## Stop Conditions
|
||||||
|
|
||||||
|
- 동일 개념이 3곳 이상 다른 이름/계약으로 구현되면 기능 추가를 중단하고 정리한다.
|
||||||
|
- UI가 저장한다고 보이는 필드를 API/Application이 저장하지 않으면 릴리스하지 않는다.
|
||||||
|
- 운영 배포 검증이 CI 밖에서만 가능하면 완료로 보지 않는다.
|
||||||
|
- `UsePathBase` 아래에서 루트 기본값을 만들려면 리다이렉트보다 실제 최종 콘텐츠가 나오는지 먼저 확인한다. 루트가 `/taxbaik/`를 다시 반환하면 즉시 중단한다.
|
||||||
|
- lint가 필요하면 build보다 먼저, 그리고 배포보다 훨씬 앞단에서 실패시키고, lint가 없는 프로젝트에 억지로 새 lint 체계를 만들지 않는다.
|
||||||
|
- 데이터 모델을 추측해서 세무 규칙이나 더존 UX 관습을 왜곡해 구현하지 않는다.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# TaxBaik Engineering Index
|
||||||
|
|
||||||
|
이 디렉터리의 문서만 현재 개발 기준의 기준점으로 사용한다. 다른 문서는 보조 자료로만 본다.
|
||||||
|
|
||||||
|
## Canonical Documents
|
||||||
|
|
||||||
|
| 문서 | 용도 | 변경 조건 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [ENGINEERING_HARNESS.md](./ENGINEERING_HARNESS.md) | 아키텍처, 코드 품질, 배포, 데이터 정합성 하네스 | 방향성 변경 또는 반복 위반 발견 |
|
||||||
|
| [DOUZONE_UX_GUIDE.md](./DOUZONE_UX_GUIDE.md) | 더존식 어드민 UX 원칙, 템플릿, 컴포넌트, 서빙 규칙 | 화면 패턴 변경 또는 신규 템플릿 추가 |
|
||||||
|
| [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md) | 공통코드, 저장값, 컬럼 길이, 하드코딩 금지 규칙 | 공통코드 또는 콤보 추가/수정 |
|
||||||
|
| [COMBO_POLICY.md](./COMBO_POLICY.md) | 콤보/선택/검색 입력 정책과 저장값 규칙 | 상태/유형/선택 입력 정책 변경 |
|
||||||
|
| [ADMIN_PATTERN_CRITIQUE_WBS.md](./ADMIN_PATTERN_CRITIQUE_WBS.md) | 어드민 Blog/문의 등록 패턴 비판, 개선 로드맵, 정량 WBS | WBS 상태 또는 성공 지표 변경 |
|
||||||
|
| `scripts/taxbaik-smoke.sh` | 공개/관리자 분리 검증용 배포 스모크 | 루트/관리자 응답 기준 변경 |
|
||||||
|
|
||||||
|
## Route And Serving Map
|
||||||
|
|
||||||
|
| 영역 | 라우트/파일 | 기준 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Public Home/Blog/Contact | `/taxbaik/`, `/taxbaik/blog`, `/taxbaik/contact` | 서버 사이드 렌더링, SEO 우선, WASM 의존 금지 |
|
||||||
|
| Admin Blog | `/taxbaik/admin/blog`, `/taxbaik/admin/blog/create`, `/taxbaik/admin/blog/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, API-first 클라이언트 경유, JS 최소화 |
|
||||||
|
| Admin Inquiry | `/taxbaik/admin/inquiries`, `/taxbaik/admin/inquiries/create`, `/taxbaik/admin/inquiries/{id}/edit` | 클라이언트 사이드 Blazor WebAssembly, 공개 접수/관리자 등록/상태 변경 분리 |
|
||||||
|
| Public API | `/taxbaik/api/*` | JWT 인증, ProblemDetails 오류, DTO 입출력 |
|
||||||
|
| CI/CD | `.gitea/workflows/deploy.yml`, `.gitea/workflows/browser-e2e.yml` | 수동 배포 금지, 배포본 E2E 통과 후 완료 |
|
||||||
|
| Smoke | `scripts/taxbaik-smoke.sh` | 루트는 공용 홈, `/taxbaik/admin/login` 은 관리자 화면이어야 함 |
|
||||||
|
|
||||||
|
## Shared Component Map
|
||||||
|
|
||||||
|
| 컴포넌트 | 용도 | 대표 사용처 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AdminShell` | 관리자 상단바/드로워/버전/알림 공통 shell | `Components/Admin/Layout/MainLayout.razor` |
|
||||||
|
| `AdminLoginForm` | 관리자 로그인 입력/제출 UI | `Components/Admin/Pages/Login.razor` |
|
||||||
|
| `AdminPageHeader` | 페이지 타이틀/보조설명/주요 액션 | Blog, Inquiry, Client, FAQ 목록 |
|
||||||
|
| `AdminDataPanel` | 목록/표면/로딩 스켈레톤 공통 래퍼 | Blog, Inquiry, CommonCode, Dashboard |
|
||||||
|
| `AdminEditorPanel` | 편집형 스켈레톤 래퍼 | BlogEdit, InquiryEdit, ClientEdit, CompanyEdit, FAQEdit |
|
||||||
|
| `AdminSkeletonRows` | 반복 로딩 골격 | AdminDataPanel, AdminEditorPanel, Dashboard |
|
||||||
|
| `AdminMetricCard` | 대시보드 KPI 카드 | `Components/Admin/Pages/Dashboard.razor` |
|
||||||
|
| `AdminEmptyState` | empty/empty-filter 상태 | ClientList 등 목록 화면 |
|
||||||
|
| `AdminFormSection` | 폼 입력 섹션 구획 | BlogForm, InquiryForm |
|
||||||
|
| `AdminFormActions` | 제출/취소 버튼 묶음 | BlogForm, InquiryForm |
|
||||||
|
| `AdminDetailSection` | 상세 정보 카드 | InquiryDetail |
|
||||||
|
| `AdminCrudPageShell` | create/edit 페이지 공통 헤더+취소+편집 래퍼 | BlogCreate/Edit, InquiryCreate/Edit |
|
||||||
|
| `CommonCodeGroupPanel` | 공통코드 그룹 선택/추가 패널 | CommonCodes |
|
||||||
|
| `CommonCodeListPanel` | 공통코드 목록/편집 패널 | CommonCodes |
|
||||||
|
|
||||||
|
## Document Rules
|
||||||
|
|
||||||
|
- 문서는 짧게 유지한다. 새 문서를 만들기 전에 이 인덱스에 추가할 가치가 있는지 판단한다.
|
||||||
|
- 동일한 기준을 여러 문서에 중복 작성하지 않는다.
|
||||||
|
- 아키텍처/UX/콤보/공통코드 기준은 `ENGINEERING_HARNESS.md`, `DOUZONE_UX_GUIDE.md`, `COMBO_POLICY.md`, `COMMON_CODE_POLICY.md`만 본다.
|
||||||
|
- WBS 완료 여부는 체크박스가 아니라 수치와 실행 로그로 판단한다.
|
||||||
|
- 코드 변경 시 관련 WBS ID를 커밋/PR 설명 또는 작업 메모에 남긴다.
|
||||||
|
- 공통코드 관련 규칙은 [COMMON_CODE_POLICY.md](./COMMON_CODE_POLICY.md)만 1차 기준으로 사용한다.
|
||||||
|
- 공유 컴포넌트는 `INDEX.md`의 Shared Component Map을 먼저 확인한다.
|
||||||
@@ -0,0 +1,777 @@
|
|||||||
|
# 블로그 포스트 작성 템플릿
|
||||||
|
|
||||||
|
## 정확성 원칙 (법적 책임 수반)
|
||||||
|
|
||||||
|
블로그는 **사실 기반, 세법 기반, 데이터 기반**이어야 합니다. 추측이나 예상은 법적 문제를 일으킬 수 있습니다.
|
||||||
|
|
||||||
|
### 절대 금지 표현
|
||||||
|
|
||||||
|
- "아마도", "할 것 같다", "추측된다" (추측)
|
||||||
|
- "대략", "정도일 거다", "보통" (예상)
|
||||||
|
- "좋을 것 같다", "나쁠 것 같다" (의견)
|
||||||
|
- 증거 없는 "모두", "항상", "누구나" (일반화)
|
||||||
|
- 출처 없는 통계 ("80% 고객이", "평균 X만 원")
|
||||||
|
|
||||||
|
### 필수 요소
|
||||||
|
|
||||||
|
**1. 세법 기반**:
|
||||||
|
- 모든 주장에 세법/시행령/고시 인용
|
||||||
|
- 조항 명시: "소득세법 제XX조에 따르면"
|
||||||
|
- 최신 기준 명시: "2025년 기준"
|
||||||
|
- 변경사항 반영: "전년도와 다르게..."
|
||||||
|
|
||||||
|
**2. 사실 기반**:
|
||||||
|
- 실제 일어난 고객 사례만 사용
|
||||||
|
- 가정일 경우 명시: "예를 들어, 만약 이렇다면"
|
||||||
|
- 가상 사례는 "예시 사례"라고 명확히
|
||||||
|
- 개인정보는 익명화 (이름, 나이는 일반적인 표현)
|
||||||
|
|
||||||
|
**3. 데이터 기반**:
|
||||||
|
- 객관적 수치만 사용 (국세청 통계, 협회 자료)
|
||||||
|
- 출처 명시: "2025년 세무청 통계에 따르면"
|
||||||
|
- 구체적 금액: "약 50만 원" (범위 표현)
|
||||||
|
- 비교 데이터: "작년 대비 X% 증가"
|
||||||
|
|
||||||
|
**4. 사례 제시 시 확인 사항**:
|
||||||
|
```
|
||||||
|
✅ 실제 고객인가? (공개 가능한 정보만)
|
||||||
|
✅ 세법을 정확하게 적용했는가?
|
||||||
|
✅ 금액 계산이 정확한가?
|
||||||
|
✅ 이 사례가 대표적인가? (극단적 사례면 명시)
|
||||||
|
✅ 다른 고객에게도 적용 가능한가?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 카테고리 필수 규칙
|
||||||
|
|
||||||
|
**모든 블로그 포스트는 반드시 하나의 카테고리에 할당되어야 합니다. (NOT NULL)**
|
||||||
|
|
||||||
|
### 카테고리별 포스트 배치
|
||||||
|
|
||||||
|
| 카테고리 | 최소 포스트 | 주제 범위 |
|
||||||
|
|---------|-----------|---------|
|
||||||
|
| 사업자 세무 | 3개 | 기장, 세무신고, 부가세, 종합소득세 |
|
||||||
|
| 부동산 세금 | 3개 | 월세, 양도세, 상속세(부동산) |
|
||||||
|
| 종합소득세 | 3개 | 프리랜서, 부업, 경비 처리 |
|
||||||
|
| 부가가치세 | 3개 | 신고, 기한, 간이과세 vs 일반과세 |
|
||||||
|
| 가족자산·증여 | 3개 | 자녀 증여, 상속, 자산 이전 |
|
||||||
|
|
||||||
|
### 카테고리 할당 규칙
|
||||||
|
|
||||||
|
1. **명확한 주제 분류**: 포스트 내용이 카테고리 범위에 명확하게 해당
|
||||||
|
2. **중복 금지**: 한 포스트는 정확히 하나의 카테고리에만 할당
|
||||||
|
3. **균형 배치**: 각 카테고리당 최소 3개씩 (고객 검색 UX)
|
||||||
|
4. **검색 최적화**: 고객이 카테고리로 찾을 때 관련 포스트 3개 이상 노출
|
||||||
|
|
||||||
|
### 카테고리 미할당 시 (오류)
|
||||||
|
- ❌ category_id = NULL (데이터베이스 제약 위반)
|
||||||
|
- ❌ SQL 실행 실패 (NOT NULL 제약)
|
||||||
|
- ❌ 블로그 페이지 노출 불가
|
||||||
|
|
||||||
|
**이 규칙은 모든 포스트 생성/수정 시 필수 준수사항입니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 핵심 철학: 고객이 느끼는 여정
|
||||||
|
|
||||||
|
### 1️⃣ 기초: "이 정도는 할 수 있어요"
|
||||||
|
- 고객이 배울 수 있는 기본 개념
|
||||||
|
- 실제 사례로 구체화
|
||||||
|
- 단계별 설명
|
||||||
|
|
||||||
|
### 2️⃣ 현실: "하지만 복잡하네요"
|
||||||
|
- 겹겹이 쌓인 세부사항들
|
||||||
|
- 매년 바뀌는 세법
|
||||||
|
- "이거 일일이 다 챙기기 어렵다"는 느낌
|
||||||
|
|
||||||
|
### 3️⃣ 해결: "세무사와 함께면 괜찮아요"
|
||||||
|
- 디테일 자동 관리
|
||||||
|
- 세법 변화 자동 반영
|
||||||
|
- 고객은 사업에만 집중
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**고객이 글을 읽은 후 느끼는 것**:
|
||||||
|
|
||||||
|
1️⃣ 읽고 나서: "아, 이 정도는 내가 할 수 있겠네"
|
||||||
|
2️⃣ 생각해보니: "근데 이 모든 걸 매년 챙기기는... 힘들겠는데?"
|
||||||
|
3️⃣ 결론: "그럼 전문가 도움을 받는 게 낫겠다"
|
||||||
|
|
||||||
|
→ 자연스럽게 세무사의 필요성을 깨달음 (강요 아님)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 템플릿 (복사해서 사용)
|
||||||
|
|
||||||
|
### Step 1: 도입부 (공감)
|
||||||
|
```markdown
|
||||||
|
# [제목]
|
||||||
|
|
||||||
|
"[구체적 상황]?"
|
||||||
|
"많은 [직업]들이 이 상황을 겪습니다."
|
||||||
|
|
||||||
|
→ 독자가 자신의 상황을 발견하도록
|
||||||
|
```
|
||||||
|
|
||||||
|
**예시**:
|
||||||
|
```markdown
|
||||||
|
# 동네 카페 월세 낼 때 세금이 안 나와요 - 정말 그럴까?
|
||||||
|
|
||||||
|
"사업을 시작했는데 세금을 낸 적이 없어요"
|
||||||
|
"많은 소규모 사업자들이 이렇게 생각합니다."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: 실제 사례 (구체적 페르소나)
|
||||||
|
|
||||||
|
**필수 정보**:
|
||||||
|
- 이름, 나이, 직업, 사업 경력
|
||||||
|
- 월/연간 매출 (현실적 수치)
|
||||||
|
- 실제 겪은 문제/성공 사례
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### 상황: [지역] [카테고리]를 운영하는 [이름]님 ([나이]세, 사업 [년]차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 위치: [구체적 위치]
|
||||||
|
- 월 매출: [금액]
|
||||||
|
- 월 경비: [주요 항목들]
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요 (실패 사례)
|
||||||
|
→ [실제 실수 1]
|
||||||
|
→ [실제 실수 2]
|
||||||
|
→ **결과**: 세금을 [X만 원] 더 내게 됨 (또는 세무조사 대상)
|
||||||
|
|
||||||
|
### 바뀐 후 (성공 사례)
|
||||||
|
→ [해결책 1]
|
||||||
|
→ [해결책 2]
|
||||||
|
→ **결과**: 세금을 [X만 원] 절약함 (또는 안정적인 운영)
|
||||||
|
```
|
||||||
|
|
||||||
|
**예시**:
|
||||||
|
```markdown
|
||||||
|
### 상황: 강남 역삼동에서 카페를 운영하는 김민수님 (34세, 사업 3년차)
|
||||||
|
|
||||||
|
**기본 정보**:
|
||||||
|
- 위치: 강남역 3번 출구 근처
|
||||||
|
- 월 매출: 약 600만 원 (평일 200만, 주말 400만)
|
||||||
|
- 월 경비: 월세 150만, 재료비 180만, 직원급여 100만 원
|
||||||
|
|
||||||
|
### 원래는 이렇게 했어요
|
||||||
|
→ "세금은 큰 회사나 내는 거라고 생각했어요"
|
||||||
|
→ 영수증도 대충 정리하고
|
||||||
|
→ **결과**: 세무청에서 3년치를 추징받고 가산세까지...손해 70만 원
|
||||||
|
|
||||||
|
### 바뀐 후
|
||||||
|
→ 매달 영수증을 정리해서
|
||||||
|
→ 세무사와 년 1회 기장 상담
|
||||||
|
→ **결과**: 세금도 명확하고, 추징도 없음. 심플하고 안전
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: 계산 & 설명
|
||||||
|
|
||||||
|
**구조**:
|
||||||
|
1. **기본 정보 확인** (위에서 제시한 사례 요약)
|
||||||
|
2. **단계별 계산** (Step 1, Step 2, ... 명확히)
|
||||||
|
3. **표로 시각화**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 계산 방법
|
||||||
|
|
||||||
|
### Step 1️⃣: 매출 정리
|
||||||
|
월 600만 원 × 12개월 = 연 7,200만 원
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산
|
||||||
|
|
||||||
|
월 경비 구성:
|
||||||
|
- 월세: 150만 원 (연 1,800만 원)
|
||||||
|
- 재료비: 180만 원 (연 2,160만 원)
|
||||||
|
- 직원급여: 100만 원 (연 1,200만 원)
|
||||||
|
- 기타: 20만 원 (연 240만 원)
|
||||||
|
- **월 합계: 450만 원**
|
||||||
|
- **연 합계: 5,400만 원**
|
||||||
|
|
||||||
|
### Step 3️⃣: 순이익
|
||||||
|
7,200만 - 5,400만 = **1,800만 원**
|
||||||
|
|
||||||
|
### Step 4️⃣: 세금
|
||||||
|
1,800만 원 × 약 6% = **약 108만 원/년**
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎭 Step 3.5: 악마는 디테일이다 (선택사항이지만 강력함)
|
||||||
|
|
||||||
|
**구조**: "간단해 보이지만, 실제로는..."
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 겉으로는 간단해 보여요... 하지만
|
||||||
|
|
||||||
|
### 📄 "영수증을 정리하세요"라고 했는데
|
||||||
|
|
||||||
|
**겉으로는**:
|
||||||
|
→ 영수증을 모으기만 하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 이 영수증은 인정되고, 이건 안 됨 (세법)
|
||||||
|
→ 이건 개인비? 사업비? (판단)
|
||||||
|
→ 카드값이랑 현금값이랑 다르면? (대사)
|
||||||
|
→ 3년 지났는데 영수증을 못 찾으면? (소송)
|
||||||
|
→ 세무청이 불인정하면? (항의 절차)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 어떤 영수증이 인정될지 사전에 판단
|
||||||
|
✅ 개인비와 사업비의 경계 명확히
|
||||||
|
✅ 세법 변경사항 적용
|
||||||
|
✅ 세무청 부인시 대응 준비
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 "매출과 경비를 기록하세요"라고 했는데
|
||||||
|
|
||||||
|
**겉으로는**:
|
||||||
|
→ 엑셀에 숫자만 입력하면 돼
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 카드 명세서와 입금액이 안 맞음 (환불? 수수료?)
|
||||||
|
→ 한 달간 매출을 빼먹음 (추가 계산)
|
||||||
|
→ 같은 카테고리인데 세법상 다르게 분류돼야 함 (부가세/소득세 다름)
|
||||||
|
→ 작년에 잘못 입력한 게 발견됨 (수정신고)
|
||||||
|
→ 월별로 편차가 커서 세무청이 의심함 (설명 준비)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 카드명세서 vs 입금액 정산
|
||||||
|
✅ 누락된 부분 찾아서 추가
|
||||||
|
✅ 세법상 올바른 분류
|
||||||
|
✅ 이전년도 오류 수정신고
|
||||||
|
✅ 세무청 질의에 대한 근거 제시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ "정확하게 기장하면 세금이 확정돼요"라고 했는데
|
||||||
|
|
||||||
|
**겉으로는**:
|
||||||
|
→ 기장만 잘하면 세금 끝
|
||||||
|
|
||||||
|
**현실의 디테일**:
|
||||||
|
→ 같은 사업도 절세 방법이 다양함 (어떤 게 맞나?)
|
||||||
|
→ 올해는 이렇게, 내년은 저렇게? (일관성)
|
||||||
|
→ 부가세와 소득세 둘 다 고려해야 함 (연계 계산)
|
||||||
|
→ 세무조사가 오면 3년치를 모두 봄 (소급 처리)
|
||||||
|
→ 이의신청/항소하려면? (법적 절차)
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 최적의 절세 전략 제시
|
||||||
|
✅ 연도별 일관된 기장 방식 유지
|
||||||
|
✅ 부가세/소득세 동시 최적화
|
||||||
|
✅ 세무조사 대비 사전 정리
|
||||||
|
✅ 이의신청/항소 등 법적 대응
|
||||||
|
```
|
||||||
|
|
||||||
|
**💡 핵심**:
|
||||||
|
- 기초는 누구나 배울 수 있어요
|
||||||
|
- **하지만 디테일을 모두 처리하려면?**
|
||||||
|
- **그 디테일들이 바로 세무사가 하는 일**
|
||||||
|
- **디테일 하나 놓쳤다가 가산세 50만 원... 이래서 세무사 비용이 아깝지 않음**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔄 Step 3.6: 세법은 계속 바뀐다 (매년 업데이트)
|
||||||
|
|
||||||
|
**구조**: "올해의 세법 변화"를 포스트 작성 시점에 맞춰 갱신
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 그런데 세법은 해마다 바뀝니다
|
||||||
|
|
||||||
|
### 📋 [연도] 변경사항들 (꼭 알아야 할 것들)
|
||||||
|
|
||||||
|
**✅ 2025년 부가세 변화**:
|
||||||
|
- 신고 기한이 [날짜]로 변경됨
|
||||||
|
- 영세사업자 기준이 [금액]로 상향조정됨
|
||||||
|
- 새로운 공제 항목이 추가됨: [항목들]
|
||||||
|
|
||||||
|
**✅ 2025년 소득세 변화**:
|
||||||
|
- 기본공제가 [금액]에서 [금액]로 증가
|
||||||
|
- 자녀 공제 조건이 변경됨
|
||||||
|
- 월급 원천징수 기준이 조정됨
|
||||||
|
|
||||||
|
**✅ 2025년 새로운 제도**:
|
||||||
|
- 소상공인 세금 감면 확대
|
||||||
|
- 청년사업자 지원 강화
|
||||||
|
- 부가가치세 간편신청 범위 확대
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**혼자서 할 때의 문제**:
|
||||||
|
❌ "작년 기준으로 기장했는데 올해 기준이 바뀐 거야?"
|
||||||
|
❌ "이 공제가 되는 건지 안 되는 건지 모르겠어"
|
||||||
|
❌ "새로운 제도가 나왔다는 것도 몰랐어"
|
||||||
|
❌ "처음 다시 계산해야 하나?"
|
||||||
|
|
||||||
|
**세무사가 처리하는 것**:
|
||||||
|
✅ 매년 변경사항 자동 추적
|
||||||
|
✅ 당신의 상황에 맞는 새로운 공제 적용
|
||||||
|
✅ 이전년도 재계산 필요시 수정신고
|
||||||
|
✅ 연중 세법 개정 소식 안내
|
||||||
|
✅ 새로운 지원 정책 놓치지 않게 관리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 결과 비교: 혼자 할 때 vs 세무사와 함께
|
||||||
|
|
||||||
|
**세법 변화 추적**
|
||||||
|
- 혼자: "어? 규칙이 바뀌었네?"
|
||||||
|
- 세무사: 자동으로 적용됨
|
||||||
|
|
||||||
|
**새로운 공제**
|
||||||
|
- 혼자: 놓치기 쉬움
|
||||||
|
- 세무사: 모두 적용됨
|
||||||
|
|
||||||
|
**매년 재계산**
|
||||||
|
- 혼자: 직접 해야 함
|
||||||
|
- 세무사: 자동 갱신
|
||||||
|
|
||||||
|
**마음 편함**
|
||||||
|
- 혼자: 불안감 ("맞나?")
|
||||||
|
- 세무사: 확신 ("전문가가 관리")
|
||||||
|
|
||||||
|
**투자 시간**
|
||||||
|
- 혼자: 당신의 시간
|
||||||
|
- 세무사: 포함 (전문가 비용)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 요약: 왜 세무사가 필요한가
|
||||||
|
|
||||||
|
**기초는 배울 수 있지만**:
|
||||||
|
- 세법은 매년 바뀌고
|
||||||
|
- 당신은 본업이 있어서 추적이 어렵고
|
||||||
|
- 실수 하나가 가산세 50만 원...
|
||||||
|
|
||||||
|
**그래서 세무사가 있으면**:
|
||||||
|
- 변화를 자동으로 적용해주고
|
||||||
|
- 새 제도도 놓치지 않아주고
|
||||||
|
- 당신은 사업에만 집중
|
||||||
|
|
||||||
|
→ **결국 시간, 돈, 스트레스 모두 절약**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💡 Step 4: 실무 팁 (3~5개)
|
||||||
|
|
||||||
|
**구조**: ✅ 이렇게 하세요 / ❌ 이렇게 하면 안 돼요
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 이렇게 하면 세금이 명확해요
|
||||||
|
|
||||||
|
### ✅ 해야 할 것
|
||||||
|
1. **영수증 정리** - 매달 봉투에 모아두기
|
||||||
|
2. **기본 기록** - 엑셀에 간단히 기입
|
||||||
|
3. **연 1회 점검** - 세무사와 기본 상담
|
||||||
|
4. **투명성** - 세무청 신고는 정확하게
|
||||||
|
|
||||||
|
### ❌ 하면 안 되는 것
|
||||||
|
1. **영수증 버리기** - 나중에 증거 없음
|
||||||
|
2. **개인비와 섞기** - 기장 혼란
|
||||||
|
3. **신고 늦추기** - 가산세 발생
|
||||||
|
4. **과하게 깎기** - 세무조사 리스크
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📝 Step 5: 결론
|
||||||
|
|
||||||
|
고객이 읽은 후 자연스럽게 결론을 내리도록:
|
||||||
|
|
||||||
|
**구조**:
|
||||||
|
1. 기초는 할 수 있다 (긍정)
|
||||||
|
2. 근데 복잡하네요 (현실 직시)
|
||||||
|
3. 그래서 세무사가 필요하구나 (자연스러운 깨달음)
|
||||||
|
|
||||||
|
**고객이 느끼는 여정**:
|
||||||
|
- 처음: "아, 이 정도는 내가 할 수 있겠네"
|
||||||
|
- 중간: "근데 이 모든 걸 매년 챙기기는..."
|
||||||
|
- 결론: "전문가 도움이 낫겠다"
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 기초는 누구나 할 수 있어요
|
||||||
|
|
||||||
|
**이 정도면 자신이 충분히 가능합니다**:
|
||||||
|
- 소규모 사업 (월 500만~1,000만 원)
|
||||||
|
- 단순 경비 (재료, 임차료 등)
|
||||||
|
- 월 1회 정도 기본 정리
|
||||||
|
|
||||||
|
→ 영수증 정리 + 기본 엑셀 기입면 충분
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 하지만 이렇게 복잡하면 전문가 도움이 효율적입니다
|
||||||
|
|
||||||
|
**세무사 상담을 권하는 경우**:
|
||||||
|
- 📊 월 매출이 2,000만 원을 넘어갈 때
|
||||||
|
- 💼 여러 사업을 동시에 운영할 때
|
||||||
|
- 🏠 부동산 등 추가 수입이 있을 때
|
||||||
|
- 📈 직원을 여러 명 두고 있을 때
|
||||||
|
- 🌍 해외 거래나 수입이 있을 때
|
||||||
|
|
||||||
|
### 실제 효과: 숫자로 본 세무사의 가치
|
||||||
|
|
||||||
|
**절세액**
|
||||||
|
- 혼자: X만 원
|
||||||
|
- 세무사: X + 200만 원
|
||||||
|
- 차이: +200만 원 절약
|
||||||
|
|
||||||
|
**세무조사 스트레스**
|
||||||
|
- 혼자: 매년 불안
|
||||||
|
- 세무사: 안정적 대응
|
||||||
|
- 차이: 심리적 안정
|
||||||
|
|
||||||
|
**시간 투자**
|
||||||
|
- 혼자: 월 10시간
|
||||||
|
- 세무사: 월 1시간
|
||||||
|
- 차이: 월 9시간 자유
|
||||||
|
|
||||||
|
**세무사 비용**
|
||||||
|
- 혼자: 0원
|
||||||
|
- 세무사: 약 100만 원/년
|
||||||
|
- 차이: -100만 원
|
||||||
|
|
||||||
|
**실제 이익**
|
||||||
|
- 혼자: 순이익
|
||||||
|
- 세무사: 순이익 + 100만 원
|
||||||
|
- 차이: +100만 원 순이익
|
||||||
|
|
||||||
|
**돈을 쓰는 이유**:
|
||||||
|
- 세금 절약: 절세 200만 원 - 비용 100만 원 = 순 100만 원 이득
|
||||||
|
- 시간 절약: 월 9시간(연 108시간) = 사업에 집중
|
||||||
|
- 스트레스 감소: 세무조사 불안 제거
|
||||||
|
- 리스크 관리: 실수로 인한 가산세 방지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
**기본 개념을 아는 것만으로도**:
|
||||||
|
- 실수를 줄이고
|
||||||
|
- 세금을 절약하고
|
||||||
|
- 세무사와의 상담이 훨씬 효율적
|
||||||
|
|
||||||
|
당신의 상황이 어느 정도인지 판단하고,
|
||||||
|
필요할 때 전문가와 함께 하세요.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 작성 체크리스트
|
||||||
|
|
||||||
|
### 내용
|
||||||
|
- [ ] **실제 사례**: 동네 카페/편의점/학원 같은 주변 상황
|
||||||
|
- [ ] **구체적 페르소나**: 이름, 나이, 직업, 사업 경력
|
||||||
|
- [ ] **실제 금액**: 매출, 경비, 세금 (현실적 수치)
|
||||||
|
- [ ] **Before/After**: 실패 사례 → 성공 사례
|
||||||
|
- [ ] **절세 효과**: "X만 원 절약" 또는 "손해를 막음"
|
||||||
|
- [ ] **계산**: Step별로 명확, 표 포함
|
||||||
|
- [ ] **악마는 디테일**: "겉으로는 간단해 보이지만 실제로는..." (세무사가 처리하는 디테일들)
|
||||||
|
- [ ] **세법 변화**: 해당 연도의 세법 변경사항과 그 영향 설명
|
||||||
|
|
||||||
|
### 톤
|
||||||
|
- [ ] **교육적**: 개념을 이해하도록
|
||||||
|
- [ ] **격려적**: 경고/협박 없음
|
||||||
|
- [ ] **현실적**: 복잡할 수 있다는 인정
|
||||||
|
- [ ] **세무사 자연스러운 유도**: "필요할 때 도움되는 선택"
|
||||||
|
- [ ] **임파워먼트**: "기초는 누구나 할 수 있어요"
|
||||||
|
|
||||||
|
### 표현
|
||||||
|
- [ ] **중학교 수준**: 어려운 용어는 () 설명
|
||||||
|
- [ ] **이모지**: 🏠💰✅❌📊 등으로 시각화
|
||||||
|
- [ ] **짧은 문장**: 한 문장에 한 개념
|
||||||
|
- [ ] **표와 리스트**: 수치는 표로, 항목은 리스트로
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 피해야 할 표현 (한국세무사협회 광고 규칙 준수)
|
||||||
|
|
||||||
|
### ❌ **절대 금지 표현** (법적 위반 위험)
|
||||||
|
|
||||||
|
**1. 과도한 절세 약속 & 절대 표현**:
|
||||||
|
- ❌ "50만 원 절약 가능"
|
||||||
|
- ❌ "최대한 경비를 깎아줍니다"
|
||||||
|
- ❌ "세금을 반으로 줄여드립니다"
|
||||||
|
- ❌ "세금을 덜 냅니다" (보장으로 해석)
|
||||||
|
- ❌ "가장 많이 절세해드립니다"
|
||||||
|
- ✅ "이 사례에서는 약 50만 원 절약되었습니다" (과거 사례만)
|
||||||
|
- ✅ "정확한 경비 처리로 세법에 따른 정당한 공제를 받을 수 있습니다" (법적 근거)
|
||||||
|
- ✅ "경비를 빠짐없이 처리합니다" (객관적 프로세스)
|
||||||
|
|
||||||
|
**2. 보장 표현 (불가능한 결과 약속)**:
|
||||||
|
- ❌ "반드시 세금을 줄입니다"
|
||||||
|
- ❌ "세무조사 안 받게 해드립니다"
|
||||||
|
- ❌ "100% 절세를 보장합니다"
|
||||||
|
- ❌ "세금을 보장합니다"
|
||||||
|
- ✅ "정확한 신고로 세무조사 리스크를 최소화합니다"
|
||||||
|
- ✅ "세법에 따른 정당한 공제를 받을 수 있습니다"
|
||||||
|
|
||||||
|
**3. 무료 & 가격 표현**:
|
||||||
|
- ❌ "무료로 세금 절약해드립니다"
|
||||||
|
- ❌ "최저가 신고료"
|
||||||
|
- ❌ "가장 저렴한 가격"
|
||||||
|
- ✅ "합리적인 비용으로 전문 서비스를 제공합니다"
|
||||||
|
|
||||||
|
**4. 절대/최상급 표현**:
|
||||||
|
- ❌ "반드시", "무조건", "반듯이", "항상", "절대"
|
||||||
|
- ❌ "최고", "최우수", "1등", "유일"
|
||||||
|
- ❌ "모든", "완벽하게"
|
||||||
|
- ✅ "일반적으로", "대부분의 경우", "보통"
|
||||||
|
|
||||||
|
**5. 과도한 단순화 표현**:
|
||||||
|
- ❌ "매우 편합니다", "너무 쉽습니다"
|
||||||
|
- ❌ "아무도 실수할 수 없습니다"
|
||||||
|
- ❌ "5분이면 끝납니다"
|
||||||
|
- ✅ "기초 개념을 배울 수 있습니다"
|
||||||
|
- ✅ "복잡한 부분은 전문가가 관리합니다"
|
||||||
|
|
||||||
|
**6. 객관적 증거 없는 수치**:
|
||||||
|
- ❌ "평균 170만 원 절약" (근거 없으면)
|
||||||
|
- ❌ "고객의 80%가 만족" (통계 없으면)
|
||||||
|
- ❌ "보통 2배의 환급" (데이터 없으면)
|
||||||
|
- ✅ "이 사례에서는 약 170만 원 절약되었습니다"
|
||||||
|
- ✅ "많은 고객들이 정확한 기장의 필요성을 느낍니다"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ **안전한 표현 (권장)**
|
||||||
|
|
||||||
|
| 대신 이렇게 | 이유 |
|
||||||
|
|----------|------|
|
||||||
|
| "정확한 기장으로 세법에 따른 공제를 받을 수 있습니다" | 법적 근거 (보장 아님) |
|
||||||
|
| "경비를 빠짐없이 처리합니다" | 객관적 프로세스 |
|
||||||
|
| "이 사례에서는 약 50만 원 절약되었습니다" | 과거 사례 (보장 아님) |
|
||||||
|
| "경비를 빠짐없이 처리합니다" | 객관적 프로세스 |
|
||||||
|
| "세무조사 대비 근거를 정리합니다" | 예방적 표현 |
|
||||||
|
| "당신의 상황에 맞는 최선의 방법을 제시합니다" | 개별 맞춤형 |
|
||||||
|
| "세법이 자주 바뀌므로 전문가 도움이 효율적입니다" | 필요성 설명 |
|
||||||
|
| "이 정도는 자신이 충분히 가능합니다" | 존중과 임파워먼트 |
|
||||||
|
| "복잡한 경우는 전문가와 상담하세요" | 선택지 제시 |
|
||||||
|
| "정확하게 하면 나중에 편합니다" | 미래 가치 (현재 보장 아님) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📋 블로그 작성 시 광고 규칙 체크리스트
|
||||||
|
|
||||||
|
- [ ] **절세 약속 제거**: "최대한", "반드시", "보장", "무조건" 단어 없음
|
||||||
|
- [ ] **보장 표현 제거**: "세무조사 안 받게", "100% 절세", "확실" 제거
|
||||||
|
- [ ] **무료/가격 표현 제거**: "무료", "최저가", "가장 저렴" 제거
|
||||||
|
- [ ] **절대 표현 제거**: "항상", "절대", "모두", "완벽" 제거
|
||||||
|
- [ ] **최상급 제거**: "최고", "최우수", "1등" (객관적 증거 있으면 가능)
|
||||||
|
- [ ] **과도한 단순화 제거**: "매우 쉽습니다", "아무도 실수할 수 없음" 제거
|
||||||
|
- [ ] **수치는 사례로**: "절약 가능" → "이 사례에서는 약 X만 원 절약"
|
||||||
|
- [ ] **객관성 유지**: 구체적 사례 + 과거형 표현 사용
|
||||||
|
- [ ] **필요성 설명**: "왜 필요한가" → 이해와 선택 유도
|
||||||
|
- [ ] **세무사협회 규정 준수**: 법적 문제 없음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 시즌별 주제 예시
|
||||||
|
|
||||||
|
| 월 | 추천 주제 | 톤 |
|
||||||
|
|----|---------|-----|
|
||||||
|
| 1월 | 부가세 2기 신고 기한 | "이 정도면 자신이 가능" |
|
||||||
|
| 5월 | 종소세 신고 방법 | "핵심 개념 + 전문가 도움 타이밍" |
|
||||||
|
| 7월 | 부가세 1기 신고 | "기초 정리 방법" |
|
||||||
|
| 11월 | 다음해 준비 | "계획하면 편해요" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 실수 방지 체크리스트 (과거 오류 기록)
|
||||||
|
|
||||||
|
**이전에 반복된 실수들을 기록하여, 같은 실수를 하지 않도록 합니다.**
|
||||||
|
|
||||||
|
### 1️⃣ 카테고리 할당 실수 ❌
|
||||||
|
|
||||||
|
**과거 오류**: 포스트를 만들 때 category_id를 NULL로 두었음
|
||||||
|
|
||||||
|
**문제점**:
|
||||||
|
- DB NOT NULL 제약 위반
|
||||||
|
- 블로그 페이지에 노출 안 됨
|
||||||
|
- 고객이 카테고리로 검색 불가
|
||||||
|
|
||||||
|
**예방책**:
|
||||||
|
- ✅ **SQL INSERT 시 반드시 category_id 명시**
|
||||||
|
- ✅ **포스트 작성 전에 카테고리 결정**
|
||||||
|
- ✅ **DB 적용 후 category_id NOT NULL 확인**
|
||||||
|
- ✅ **각 카테고리별 최소 3개 이상 포스트 유지**
|
||||||
|
|
||||||
|
**SQL 예시** (권장):
|
||||||
|
```sql
|
||||||
|
INSERT INTO blog_posts (title, slug, content, category_id, is_published, ...)
|
||||||
|
VALUES ('제목', 'slug', $$본문$$, 1, true, ...);
|
||||||
|
-- category_id 절대 생략 금지!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ 내용 길이 부족 ❌
|
||||||
|
|
||||||
|
**과거 오류**: 에이전트가 지침(1,500~2,500자)을 무시하고 간단한 버전(500자)으로 생성
|
||||||
|
|
||||||
|
**문제점**:
|
||||||
|
- 고객 설득력 부족
|
||||||
|
- 계산 예시 없음
|
||||||
|
- 3단계 구조 불완전
|
||||||
|
- 세법 인용 부족
|
||||||
|
|
||||||
|
**예방책**:
|
||||||
|
- ✅ **각 포스트 최소 1,500자 이상 (추천 2,000~2,500자)**
|
||||||
|
- ✅ **포스트 작성 후 글자 수 확인: `LENGTH(content) >= 1500`**
|
||||||
|
- ✅ **항상 실제 사례 포함** (이름, 나이, 직업, 구체적 상황)
|
||||||
|
- ✅ **항상 계산 과정 포함** (절세액 수치화)
|
||||||
|
- ✅ **3단계 구조 필수** (1️⃣ 기초 → 2️⃣ 현실 → 3️⃣ 해결책)
|
||||||
|
|
||||||
|
**확인 쿼리**:
|
||||||
|
```sql
|
||||||
|
SELECT id, title, LENGTH(content) as length FROM blog_posts
|
||||||
|
WHERE LENGTH(content) < 1500; -- 부족한 포스트 검출
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ 테이블 사용 금지 ❌
|
||||||
|
|
||||||
|
**과거 오류**: 마크다운 테이블(`| |---|---|`) 사용
|
||||||
|
|
||||||
|
**문제점**:
|
||||||
|
- 지침 위반 (리스트만 사용)
|
||||||
|
- 모바일에서 가독성 저하
|
||||||
|
- 유지보수 어려움
|
||||||
|
|
||||||
|
**예방책**:
|
||||||
|
- ✅ **테이블 금지, 리스트만 사용** (- 또는 숫자 목록)
|
||||||
|
- ✅ **작성 후 `| |` 패턴 검색으로 테이블 확인**
|
||||||
|
- ✅ **수치/계산은 리스트 형식**:
|
||||||
|
|
||||||
|
**❌ 금지 (테이블)**:
|
||||||
|
```markdown
|
||||||
|
| 항목 | 월 | 연간 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 월세 | 150만 | 1,800만 |
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 권장 (리스트)**:
|
||||||
|
```markdown
|
||||||
|
월 경비 구성:
|
||||||
|
- 월세: 150만 원 (연 1,800만 원)
|
||||||
|
- 재료비: 180만 원 (연 2,160만 원)
|
||||||
|
- 직원급여: 100만 원 (연 1,200만 원)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ 계산 예시 누락 ❌
|
||||||
|
|
||||||
|
**과거 오류**: 포스트에 개념만 있고 실제 계산 예시 부족
|
||||||
|
|
||||||
|
**문제점**:
|
||||||
|
- 고객이 "내 상황에 얼마나 해당하나" 판단 어려움
|
||||||
|
- 추상적 설명으로 설득력 감소
|
||||||
|
- 세무사 필요성 전달 미흡
|
||||||
|
|
||||||
|
**예방책**:
|
||||||
|
- ✅ **모든 포스트에 구체적 계산 예시 필수**
|
||||||
|
- ✅ **절세액을 수치로 제시** ("약 50만 원 절약")
|
||||||
|
- ✅ **단계별 계산 과정 포함** (Step 1️⃣, 2️⃣, 3️⃣, 4️⃣)
|
||||||
|
- ✅ **실제 사례로 숫자 구체화**:
|
||||||
|
|
||||||
|
**예시**:
|
||||||
|
```markdown
|
||||||
|
### Step 1️⃣: 매출 정리
|
||||||
|
월 600만 원 × 12개월 = 연 7,200만 원
|
||||||
|
|
||||||
|
### Step 2️⃣: 경비 계산
|
||||||
|
- 월세: 150만 원 → 연 1,800만 원
|
||||||
|
- 재료비: 180만 원 → 연 2,160만 원
|
||||||
|
합계: 5,400만 원
|
||||||
|
|
||||||
|
### Step 3️⃣: 순이익
|
||||||
|
7,200만 - 5,400만 = 1,800만 원
|
||||||
|
|
||||||
|
### Step 4️⃣: 세금
|
||||||
|
1,800만 원 × 약 6% = **약 108만 원/년**
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5️⃣ 카테고리 주제 불일치 ❌
|
||||||
|
|
||||||
|
**과거 오류**: 포스트 주제와 카테고리가 맞지 않음
|
||||||
|
|
||||||
|
**문제점**:
|
||||||
|
- 고객이 원하는 정보 검색 불가
|
||||||
|
- 카테고리 신뢰도 저하
|
||||||
|
- UX 혼란
|
||||||
|
|
||||||
|
**예방책**:
|
||||||
|
- ✅ **포스트 작성 전 카테고리 명확히 결정**
|
||||||
|
- ✅ **포스트 주제와 카테고리 일관성 검증**:
|
||||||
|
|
||||||
|
| 포스트 | 카테고리 | 확인 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 프리랜서 경비 | 종합소득세 (3) | ✅ 맞음 |
|
||||||
|
| 월세 신고 | 부동산 세금 (2) | ✅ 맞음 |
|
||||||
|
| 자녀 증여세 | 가족자산·증여 (5) | ✅ 맞음 |
|
||||||
|
| 사업자 기장 | 사업자 세무 (1) | ✅ 맞음 |
|
||||||
|
| 부가세 신고 | 부가가치세 (4) | ✅ 맞음 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6️⃣ 정확한 세법 인용 누락 ❌
|
||||||
|
|
||||||
|
**과거 오류**: 일부 포스트에서 법조 명시 부족
|
||||||
|
|
||||||
|
**문제점**:
|
||||||
|
- 정확성 원칙 위반
|
||||||
|
- 법적 책임 불명확
|
||||||
|
- 고객 신뢰도 저하
|
||||||
|
|
||||||
|
**예방책**:
|
||||||
|
- ✅ **모든 주요 내용에 세법 조항 인용 필수**
|
||||||
|
- ✅ **형식**: "소득세법 제XX조에 따르면"
|
||||||
|
- ✅ **연도 기준 명시**: "2025년 기준"
|
||||||
|
- ✅ **포스트 끝에 "법적 근거" 섹션 필수**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**법적 근거**:
|
||||||
|
- 소득세법 제29조 (수입금액의 계산)
|
||||||
|
- 국세기본법 제47조 (가산세)
|
||||||
|
- 소득세법 제160조 (증빙 보관)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 포스트 최종 체크리스트
|
||||||
|
|
||||||
|
모든 포스트를 DB에 등록하기 전에 다음을 확인하세요:
|
||||||
|
|
||||||
|
- [ ] **카테고리 할당**: `category_id NOT NULL` (필수)
|
||||||
|
- [ ] **내용 길이**: `LENGTH(content) >= 1500` (최소 1,500자)
|
||||||
|
- [ ] **테이블 확인**: `| |` 패턴 없음 (리스트만)
|
||||||
|
- [ ] **계산 예시**: Step 1️⃣~4️⃣ 포함 (절세액 수치)
|
||||||
|
- [ ] **세법 인용**: 모든 주요 내용에 법조 명시
|
||||||
|
- [ ] **카테고리 일치**: 포스트 주제 ↔ 카테고리 일관성
|
||||||
|
- [ ] **3단계 구조**: 1️⃣ 기초 → 2️⃣ 현실 → 3️⃣ 해결책
|
||||||
|
- [ ] **광고 규칙**: 금지 표현(보장, 최저가, 무료) 없음
|
||||||
|
- [ ] **사례 포함**: 실제 상황 + 이름/나이/직업 구체화
|
||||||
|
- [ ] **정확성**: 추측/예상/의견 표현 없음
|
||||||
|
|
||||||
|
**체크 쿼리**:
|
||||||
|
```sql
|
||||||
|
-- DB 적용 후 확인
|
||||||
|
SELECT id, title, LENGTH(content), category_id
|
||||||
|
FROM blog_posts
|
||||||
|
WHERE LENGTH(content) < 1500 OR category_id IS NULL
|
||||||
|
ORDER BY id;
|
||||||
|
-- 결과 없음이 정상!
|
||||||
|
```
|
||||||
@@ -17,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 |
|
||||||
@@ -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
|
||||||
```
|
```
|
||||||
@@ -58,13 +72,14 @@ sudo systemctl reload nginx
|
|||||||
- `DEPLOY_HOST`: `178.104.200.7`
|
- `DEPLOY_HOST`: `178.104.200.7`
|
||||||
- `DEPLOY_SSH_KEY_B64`: base64로 인코딩한 SSH 개인키
|
- `DEPLOY_SSH_KEY_B64`: base64로 인코딩한 SSH 개인키
|
||||||
- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호
|
- `TAXBAIK_ADMIN_TEST_PASSWORD`: 배포 검증용 관리자 비밀번호
|
||||||
|
- `Admin__PasswordResetToken`: 관리자 비밀번호 재설정 API용 서버 비밀값
|
||||||
|
|
||||||
2. 배포 워크플로우는 자동으로 실행:
|
2. 배포 워크플로우는 자동으로 실행:
|
||||||
```
|
```
|
||||||
master 브랜치 push → build → publish → restart
|
master 브랜치 push → build → test → publish → restart → health check → Playwright
|
||||||
```
|
```
|
||||||
|
|
||||||
수동 배포는 사용하지 않습니다. 배포 이슈는 Gitea Actions 로그로 해결합니다.
|
수동 배포는 사용하지 않습니다. `deploy_gb.sh`는 `TAXBAIK_DEPLOY_FROM_CI=1`이 없으면 즉시 종료하므로, 배포는 반드시 Gitea Actions에서만 실행됩니다.
|
||||||
|
|
||||||
## 마이그레이션 자동 실행
|
## 마이그레이션 자동 실행
|
||||||
|
|
||||||
@@ -95,14 +110,15 @@ curl -X POST http://178.104.200.7/taxbaik/api/auth/login \
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"username\":\"admin\",\"password\":\"<TAXBAIK_ADMIN_TEST_PASSWORD>\"}"
|
-d "{\"username\":\"admin\",\"password\":\"<TAXBAIK_ADMIN_TEST_PASSWORD>\"}"
|
||||||
|
|
||||||
# 문의 폼 제출 테스트
|
# Playwright 브라우저 검증
|
||||||
curl -X POST http://178.104.200.7/taxbaik/contact \
|
npm run test:e2e
|
||||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
||||||
-d "name=테스트&phone=010-1234-5678&service_type=사업자세무&message=테스트"
|
|
||||||
|
|
||||||
# DB에서 확인
|
# 필요한 경우 개별 테스트 실행
|
||||||
ssh kjh2064@178.104.200.7
|
npx playwright test tests/e2e/admin-login.spec.ts
|
||||||
psql -U taxbaik -d taxbaikdb -c "SELECT * FROM inquiries ORDER BY created_at DESC LIMIT 1;"
|
npx playwright test tests/e2e/admin-smoke.spec.ts
|
||||||
|
npx playwright test tests/e2e/public-smoke.spec.ts
|
||||||
|
npx playwright test tests/e2e/blog-seo.spec.ts
|
||||||
|
npx playwright test tests/e2e/contact-submit.spec.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
### 블로그 포스트 확인
|
### 블로그 포스트 확인
|
||||||
@@ -111,7 +127,7 @@ psql -U taxbaik -d taxbaikdb -c "SELECT * FROM inquiries ORDER BY created_at DES
|
|||||||
# 초기 5개 포스트 확인
|
# 초기 5개 포스트 확인
|
||||||
curl http://178.104.200.7/taxbaik/blog
|
curl http://178.104.200.7/taxbaik/blog
|
||||||
|
|
||||||
# 첫 번째 포스트 상세 (slug: accountant-mistakes-5)
|
# 첫 번째 포스트 상세
|
||||||
curl http://178.104.200.7/taxbaik/blog/accountant-mistakes-5
|
curl http://178.104.200.7/taxbaik/blog/accountant-mistakes-5
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -126,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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -137,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
|
||||||
@@ -163,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'
|
||||||
|
```
|
||||||
|
|
||||||
## 초기 데이터
|
## 초기 데이터
|
||||||
|
|
||||||
### 관리자 계정
|
### 관리자 계정
|
||||||
@@ -187,7 +222,7 @@ V003 마이그레이션에서 5개 포스트 자동 생성:
|
|||||||
|
|
||||||
- [ ] SSL 인증서 적용 (Let's Encrypt)
|
- [ ] SSL 인증서 적용 (Let's Encrypt)
|
||||||
- [ ] 도메인 연결 (현재는 IP 기반)
|
- [ ] 도메인 연결 (현재는 IP 기반)
|
||||||
- [ ] 관리자 인증 로직 구현 (현재는 플레이스홀더)
|
- [ ] 관리자 인증 보안 고도화 (rate limit, 비밀번호 교체 절차)
|
||||||
- [ ] 블로그 포스트 CRUD 기능 완성
|
- [ ] 블로그 포스트 수정 화면 완성
|
||||||
- [ ] Naver/Google Search Console 등록
|
- [ ] Naver/Google Search Console 등록
|
||||||
- [ ] 운영 관리자 비밀번호를 초기 시드값에서 교체하고 `TAXBAIK_ADMIN_TEST_PASSWORD` 갱신
|
- [ ] 운영 관리자 비밀번호를 초기 시드값에서 교체하고 `TAXBAIK_ADMIN_TEST_PASSWORD` 갱신
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
# TaxBaik 배포 완료 보고서
|
# TaxBaik 배포 요약
|
||||||
|
|
||||||
## 📊 최종 완성 현황
|
> 이 문서는 현재 WBS 기준의 검증 문서가 아니라, 과거 배포 요약의 기록이다.
|
||||||
|
> 최신 상태는 `ROADMAP_WBS.md`와 CI 로그를 기준으로 판단한다.
|
||||||
|
|
||||||
### ✅ W0-W6 모든 단계 완료
|
## 📊 과거 기록 현황
|
||||||
|
|
||||||
|
### ⚠️ 과거 기준 기록
|
||||||
|
|
||||||
| 단계 | 항목 | 상태 |
|
| 단계 | 항목 | 상태 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| W0 | 프로젝트 기반 구축 | ✅ 완료 |
|
| W0 | 프로젝트 기반 구축 | 과거 기록 |
|
||||||
| W1 | LLM 개발 지침 (CLAUDE.md) | ✅ 완료 |
|
| W1 | LLM 개발 지침 (CLAUDE.md) | 과거 기록 |
|
||||||
| W2 | 도메인/인프라/서비스 레이어 | ✅ 완료 |
|
| W2 | 도메인/인프라/서비스 레이어 | 과거 기록 |
|
||||||
| **W3** | **공개 홈페이지 (Razor Pages SSR)** | ✅ **배포됨** |
|
| **W3** | **공개 홈페이지 (Razor Pages SSR)** | 과거 기록 |
|
||||||
| **W4** | **관리자 백오피스 (Blazor Server)** | ✅ **배포됨** |
|
| **W4** | **관리자 백오피스 (Blazor Server)** | 과거 기록 |
|
||||||
| **W5** | **스타일링 및 모바일 UX** | ✅ **완성됨** |
|
| **W5** | **스타일링 및 모바일 UX** | 과거 기록 |
|
||||||
| **W6** | **출시 준비 (E2E 테스트)** | ✅ **검증됨** |
|
| **W6** | **출시 준비 (E2E 테스트)** | 과거 기록 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 배포된 엔드포인트 (모두 HTTP 200)
|
## 🚀 과거 배포 엔드포인트 기록
|
||||||
|
|
||||||
### 공개 사이트
|
### 공개 사이트
|
||||||
- 🏠 **홈페이지**: http://178.104.200.7/taxbaik
|
- 🏠 **홈페이지**: http://178.104.200.7/taxbaik
|
||||||
@@ -28,11 +31,11 @@
|
|||||||
### 관리자
|
### 관리자
|
||||||
- 🔐 **로그인**: http://178.104.200.7/taxbaik/admin/login
|
- 🔐 **로그인**: http://178.104.200.7/taxbaik/admin/login
|
||||||
- 📊 **대시보드**: http://178.104.200.7/taxbaik/admin/dashboard
|
- 📊 **대시보드**: http://178.104.200.7/taxbaik/admin/dashboard
|
||||||
- 👤 **기본 계정**: admin / admin123
|
- 계정 정보는 문서에 기록하지 않고 Gitea Secrets 또는 서버 환경변수로만 관리한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 기술 구현
|
## 📁 과거 기술 구성 기록
|
||||||
|
|
||||||
### 공개 사이트
|
### 공개 사이트
|
||||||
- **기술**: ASP.NET Core 10 Razor Pages (SSR)
|
- **기술**: ASP.NET Core 10 Razor Pages (SSR)
|
||||||
@@ -55,16 +58,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 데이터베이스
|
## 📊 과거 데이터베이스 기록
|
||||||
|
|
||||||
### 초기 데이터
|
### 초기 데이터
|
||||||
- ✅ **5개 카테고리**: 사업자세무, 부동산세금, 종합소득세, 부가가치세, 가족자산증여
|
- 5개 카테고리: 사업자세무, 부동산세금, 종합소득세, 부가가치세, 가족자산증여
|
||||||
- ✅ **5개 블로그 포스트**: 초기 콘텐츠 포함
|
- 5개 블로그 포스트: 초기 콘텐츠 포함
|
||||||
- ✅ **1개 관리자 계정**: admin/admin123
|
- 관리자 계정: 비밀번호는 문서화하지 않는다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 배포 절차
|
## 🔧 과거 배포 절차 기록
|
||||||
|
|
||||||
1. **로컬 빌드**
|
1. **로컬 빌드**
|
||||||
```bash
|
```bash
|
||||||
@@ -98,18 +101,18 @@ e7e01d0 마이그레이션 및 보안 수정
|
|||||||
|
|
||||||
## ✨ 주요 특징
|
## ✨ 주요 특징
|
||||||
|
|
||||||
- ✅ SEO 최적화 (Server-Side Rendering)
|
- SEO 항목 (Server-Side Rendering)
|
||||||
- ✅ 무중단 배포 (Shadow Copy)
|
- 심링크 기반 배포
|
||||||
- ✅ 반응형 모바일 UI
|
- 반응형 모바일 UI
|
||||||
- ✅ 한국어 완전 지원
|
- 한국어 UI
|
||||||
- ✅ 자동 마이그레이션
|
- 자동 마이그레이션
|
||||||
- ✅ 안전한 인증 (쿠키 + 인증)
|
- 인증 항목
|
||||||
- ✅ 체계적인 레이어 구조
|
- 레이어 구조
|
||||||
- ✅ 프로덕션 준비 완료
|
- 기록용 요약일 뿐, 현재 완료 판정 기준은 아니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 다음 단계 (향후 개선)
|
## 🎯 향후 개선 후보
|
||||||
|
|
||||||
1. BCrypt 실제 인증 개선
|
1. BCrypt 실제 인증 개선
|
||||||
2. Blog CRUD 관리자 기능 완성
|
2. Blog CRUD 관리자 기능 완성
|
||||||
@@ -120,5 +123,5 @@ e7e01d0 마이그레이션 및 보안 수정
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**배포 완료**: 2026-06-26
|
**기록일**: 2026-06-26
|
||||||
**상태**: ✅ 운영 중
|
**상태**: 기록용 요약
|
||||||
@@ -1,34 +1,34 @@
|
|||||||
# TaxBaik 최종 완성 보고서
|
# TaxBaik 과거 완료 요약 기록
|
||||||
|
|
||||||
**프로젝트**: 세무사 백원숙 전문성 표현 홈페이지
|
**프로젝트**: 세무사 백원숙 전문성 표현 홈페이지
|
||||||
**완성일**: 2026-06-26
|
**기록일**: 2026-06-26
|
||||||
**상태**: ✅ **프로덕션 준비 완료**
|
**상태**: 과거 기록. 현재 완료 판정은 `ROADMAP_WBS.md`와 CI/Playwright 로그를 기준으로 한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 프로젝트 개요
|
## 📌 프로젝트 개요
|
||||||
|
|
||||||
### 비즈니스 목표
|
### 비즈니스 목표 기록
|
||||||
- ✅ 온라인 전문성 표현
|
- 온라인 전문성 표현
|
||||||
- ✅ 블로그 SEO 유입
|
- 블로그 SEO 유입
|
||||||
- ✅ 전국 고객 확보
|
- 전국 고객 확보
|
||||||
|
|
||||||
### 핵심 포지셔닝
|
### 핵심 포지셔닝
|
||||||
> "사업자 세금 + 부동산 + 가족자산 = 맞춤형 세무 파트너"
|
> "사업자 세금 + 부동산 + 가족자산 = 맞춤형 세무 파트너"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 완료된 작업 (W0~W6)
|
## 🎯 과거 기준 작업 기록 (W0~W6)
|
||||||
|
|
||||||
| 단계 | 작업 | 상태 | 커밋 수 |
|
| 단계 | 작업 | 상태 | 커밋 수 |
|
||||||
|------|------|------|--------|
|
|------|------|------|--------|
|
||||||
| **W0** | 프로젝트 기반 구축 | ✅ | 3 |
|
| **W0** | 프로젝트 기반 구축 | 과거 기록 | 3 |
|
||||||
| **W1** | LLM 개발 지침 작성 | ✅ | 1 |
|
| **W1** | LLM 개발 지침 작성 | 과거 기록 | 1 |
|
||||||
| **W2** | Domain/Infrastructure/Application | ✅ | 2 |
|
| **W2** | Domain/Infrastructure/Application | 과거 기록 | 2 |
|
||||||
| **W3** | 공개 홈페이지 (Razor Pages) | ✅ | 4 |
|
| **W3** | 공개 홈페이지 (Razor Pages) | 과거 기록 | 4 |
|
||||||
| **W4** | 관리자 백오피스 (Blazor) | ✅ | 3 |
|
| **W4** | 관리자 백오피스 (Blazor) | 과거 기록 | 3 |
|
||||||
| **W5** | 스타일링 & 성능 최적화 | ✅ | 1 |
|
| **W5** | 스타일링 & 성능 최적화 | 과거 기록 | 1 |
|
||||||
| **W6** | 배포 준비 & CI/CD | ✅ | 5 |
|
| **W6** | 배포 준비 & CI/CD | 과거 기록 | 5 |
|
||||||
|
|
||||||
**총 커밋**: 19개 (모두 한국어)
|
**총 커밋**: 19개 (모두 한국어)
|
||||||
|
|
||||||
@@ -95,24 +95,23 @@ TaxBaik.Admin/ 95 KB (Blazor Server)
|
|||||||
## ✨ 주요 기능
|
## ✨ 주요 기능
|
||||||
|
|
||||||
### 공개 사이트
|
### 공개 사이트
|
||||||
- ✅ SEO 최적화 블로그 (5개 카테고리)
|
- SEO 블로그
|
||||||
- ✅ 온라인 상담 신청 폼
|
- 온라인 상담 신청 폼
|
||||||
- ✅ 반응형 디자인 (모바일 375px+)
|
- 반응형 디자인
|
||||||
- ✅ 성능 최적화 (gzip, lazy load)
|
- 성능 최적화 항목
|
||||||
|
|
||||||
### 관리자 백오피스
|
### 관리자 백오피스
|
||||||
- ✅ 대시보드 (KPI 카드)
|
- 대시보드
|
||||||
- ✅ 블로그 CRUD
|
- 블로그 관리
|
||||||
- ✅ 문의 관리 (상태 변경)
|
- 문의 관리
|
||||||
- ✅ 사이트 설정
|
- 사이트 설정
|
||||||
|
|
||||||
### 보안 & 성능
|
### 보안 & 성능
|
||||||
- ✅ SQL Injection 방지 (파라미터화 쿼리)
|
- SQL Injection 방지 항목
|
||||||
- ✅ CSRF 보호 ([ValidateAntiForgeryToken])
|
- 인증/인가 항목
|
||||||
- ✅ Cookie 기반 인증 (8시간 세션)
|
- gzip 응답 압축
|
||||||
- ✅ gzip 응답 압축
|
- 이미지 lazy load
|
||||||
- ✅ 이미지 lazy load
|
- 폰트 preconnect
|
||||||
- ✅ 폰트 preconnect
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -130,7 +129,7 @@ Gitea Actions 트리거
|
|||||||
4. 심링크 스왑
|
4. 심링크 스왑
|
||||||
5. systemctl restart
|
5. systemctl restart
|
||||||
↓
|
↓
|
||||||
배포 완료 (무중단)
|
배포 기록 생성
|
||||||
```
|
```
|
||||||
|
|
||||||
### 자동 마이그레이션
|
### 자동 마이그레이션
|
||||||
@@ -143,53 +142,53 @@ schema_migrations 테이블 확인
|
|||||||
↓
|
↓
|
||||||
미실행 마이그레이션 자동 실행
|
미실행 마이그레이션 자동 실행
|
||||||
↓
|
↓
|
||||||
DB 준비 완료
|
DB 준비 기록 생성
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 코드 품질
|
## 📊 과거 코드 품질 기록
|
||||||
|
|
||||||
| 항목 | 상태 | 세부 |
|
| 항목 | 상태 | 세부 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **빌드** | ✅ | 0 errors, 12 warnings (NuGet 보안 정보) |
|
| **빌드** | 과거 기록 | 최신 상태는 CI 로그 기준 |
|
||||||
| **보안** | ✅ | SQL injection 방지, CSRF 보호, 인증 |
|
| **보안** | 과거 기록 | 최신 상태는 코드 리뷰와 테스트 기준 |
|
||||||
| **성능** | ✅ | gzip, lazy load, 메모리 캐시 |
|
| **성능** | 과거 기록 | 최신 상태는 WBS 검증 기준 |
|
||||||
| **SEO** | ✅ | 메타 태그, sitemap, robots.txt |
|
| **SEO** | 과거 기록 | 최신 상태는 `blog-seo` Playwright 기준 |
|
||||||
| **테스트** | ✅ | 구조적 검증 완료 |
|
| **테스트** | 과거 기록 | 최신 상태는 Playwright/CI 기준 |
|
||||||
| **문서** | ✅ | 1,500+ 라인 (개발 + 배포 가이드) |
|
| **문서** | 과거 기록 | 최신 상태는 `ROADMAP_WBS.md` 기준 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 수락 기준
|
## 🎯 과거 수락 기준 기록
|
||||||
|
|
||||||
### 기술적 요구사항
|
### 기술적 요구사항
|
||||||
- [x] ASP.NET Core 8 + C#11 기반
|
- ASP.NET Core 기반
|
||||||
- [x] Dapper + PostgreSQL 사용
|
- Dapper + PostgreSQL 사용
|
||||||
- [x] Razor Pages SSR (공개 사이트)
|
- Razor Pages SSR (공개 사이트)
|
||||||
- [x] Blazor Server (관리자)
|
- Blazor Server (관리자)
|
||||||
- [x] 계층화된 아키텍처 (Domain → Infrastructure → Application → Web/Admin)
|
- 계층화된 아키텍처
|
||||||
- [x] 모든 UI 문자열 한국어
|
- UI 문자열 한국어
|
||||||
|
|
||||||
### 기능 요구사항
|
### 기능 요구사항
|
||||||
- [x] 블로그 (5개 카테고리, SEO 최적화)
|
- 블로그
|
||||||
- [x] 온라인 문의 폼
|
- 온라인 문의 폼
|
||||||
- [x] 관리자 백오피스 (블로그 + 문의 관리)
|
- 관리자 백오피스
|
||||||
- [x] 반응형 디자인
|
- 반응형 디자인
|
||||||
- [x] 성능 최적화
|
- 성능 최적화
|
||||||
|
|
||||||
### 배포 요구사항
|
### 배포 요구사항
|
||||||
- [x] CI/CD 파이프라인 (Gitea Actions)
|
- CI/CD 파이프라인
|
||||||
- [x] 자동 마이그레이션
|
- 자동 마이그레이션
|
||||||
- [x] 무중단 배포 (심링크 스왑)
|
- 심링크 배포
|
||||||
- [x] systemd 서비스 파일
|
- systemd 서비스 파일
|
||||||
- [x] Nginx 리버스 프록시 설정
|
- Nginx 리버스 프록시 설정
|
||||||
|
|
||||||
### 문서 요구사항
|
### 문서 요구사항
|
||||||
- [x] CLAUDE.md (개발 지침)
|
- CLAUDE.md
|
||||||
- [x] DEPLOYMENT_GUIDE.md (배포 가이드)
|
- DEPLOYMENT_GUIDE.md
|
||||||
- [x] README.md (프로젝트 개요)
|
- README.md
|
||||||
- [x] 서버 설치 스크립트
|
- 서버 설치 스크립트
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -229,54 +228,41 @@ b300cd7 완성: 빌드 성공 및 최종 통합 (W0~W6 완료)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎊 최종 체크리스트
|
## 과거 체크리스트 기록
|
||||||
|
|
||||||
### 개발 완료
|
### 개발 기록
|
||||||
- [x] 코드 작성
|
- 코드 작성 기록
|
||||||
- [x] 로컬 빌드 성공
|
- 로컬 빌드 기록
|
||||||
- [x] Git 커밋/푸시
|
- Git 커밋/푸시 기록
|
||||||
|
|
||||||
### 검증 완료
|
### 검증 기록
|
||||||
- [x] 아키텍처 검증
|
- 아키텍처 검토 기록
|
||||||
- [x] 코드 구조 검증
|
- 코드 구조 검토 기록
|
||||||
- [x] 보안 검증
|
- 보안 검토 기록
|
||||||
- [x] 성능 검증
|
- 성능 검토 기록
|
||||||
- [x] SEO 검증
|
- SEO 검토 기록
|
||||||
|
|
||||||
### 배포 준비
|
### 배포 준비
|
||||||
- [x] CI/CD 파이프라인
|
- CI/CD 파이프라인
|
||||||
- [x] 자동 마이그레이션
|
- 자동 마이그레이션
|
||||||
- [x] 배포 스크립트
|
- 배포 스크립트
|
||||||
- [x] 배포 가이드
|
- 배포 가이드
|
||||||
- [x] 모니터링 설정
|
- 모니터링 설정
|
||||||
|
|
||||||
### 문서 완성
|
### 문서 기록
|
||||||
- [x] README.md
|
- README.md
|
||||||
- [x] CLAUDE.md
|
- CLAUDE.md
|
||||||
- [x] DEPLOYMENT_GUIDE.md
|
- DEPLOYMENT_GUIDE.md
|
||||||
- [x] PRODUCTION_CHECKLIST.md
|
- PRODUCTION_CHECKLIST.md
|
||||||
- [x] SERVER_SETUP.sh
|
- SERVER_SETUP.sh
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 다음 단계
|
## 현재 후속 기준
|
||||||
|
|
||||||
### 즉시 실행 (서버에서)
|
1. `ROADMAP_WBS.md`의 미완료 항목을 기준으로 작업한다.
|
||||||
```bash
|
2. 완료 판정은 CI 배포, 배포 검증, Playwright E2E 통과 후에만 한다.
|
||||||
bash SERVER_SETUP.sh # 자동 설치
|
3. 서버 수동 변경은 비상 롤백을 제외하고 금지한다.
|
||||||
sudo systemctl start taxbaik # 서비스 시작
|
|
||||||
curl http://localhost:5001 # 접근 확인
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gitea Actions 활성화
|
|
||||||
1. Secrets 추가: DEPLOY_USER, DEPLOY_HOST, DEPLOY_SSH_KEY
|
|
||||||
2. master 브랜치 푸시 → 자동 배포 트리거
|
|
||||||
|
|
||||||
### 운영 단계
|
|
||||||
1. 초기 로그인 (admin/admin123)
|
|
||||||
2. 블로그 포스트 작성
|
|
||||||
3. SEO 최적화
|
|
||||||
4. 모니터링 시작
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -289,8 +275,6 @@ curl http://localhost:5001 # 접근 확인
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**프로젝트 상태**: ✅ **완성 (COMPLETE)**
|
**프로젝트 상태**: 진행 중
|
||||||
|
|
||||||
모든 제안된 작업이 우선순위 순서대로 완료되었습니다.
|
이 문서는 과거 완료 요약으로 남기고, 현재 진행 상태는 `ROADMAP_WBS.md`를 따른다.
|
||||||
|
|
||||||
배포 준비가 완료되었으므로, 서버에서 `SERVER_SETUP.sh`를 실행하면 즉시 운영을 시작할 수 있습니다.
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# TaxBaik 프로덕션 배포 체크리스트
|
# TaxBaik 프로덕션 배포 체크리스트
|
||||||
|
|
||||||
**작성일**: 2026-06-26
|
**작성일**: 2026-06-26
|
||||||
**상태**: 배포 준비 완료
|
**상태**: 운영 기준 정비 중
|
||||||
**대상**: 178.104.200.7 (Ubuntu 26.04)
|
**대상**: 178.104.200.7 (Ubuntu 26.04)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
### 1. 코드 검증
|
### 1. 코드 검증
|
||||||
- [ ] `dotnet build TaxBaik.sln -c Release` 성공
|
- [ ] `dotnet build TaxBaik.sln -c Release` 성공
|
||||||
- [ ] 모든 컴파일 오류 0개
|
- [ ] 모든 컴파일 오류 0개
|
||||||
- [ ] 경고 무시 (NuGet 보안 정보만)
|
- [ ] 경고 0개 유지
|
||||||
|
|
||||||
### 2. Git 상태 확인
|
### 2. Git 상태 확인
|
||||||
- [ ] 모든 변경사항 커밋됨
|
- [ ] 모든 변경사항 커밋됨
|
||||||
@@ -20,12 +20,10 @@
|
|||||||
|
|
||||||
### 3. 발행 검증
|
### 3. 발행 검증
|
||||||
```bash
|
```bash
|
||||||
dotnet publish TaxBaik.Web -c Release -o ./publish/web
|
dotnet publish TaxBaik.Web -c Release -o ./publish
|
||||||
dotnet publish TaxBaik.Admin -c Release -o ./publish/admin
|
|
||||||
|
|
||||||
# 확인
|
# 확인
|
||||||
ls -lh ./publish/web/TaxBaik.Web.dll
|
ls -lh ./publish/TaxBaik.Web.dll
|
||||||
ls -lh ./publish/admin/TaxBaik.Admin.dll
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -47,36 +45,10 @@ ssh kjh2064@178.104.200.7 'bash ~/SERVER_SETUP.sh'
|
|||||||
# 3. 배포 디렉토리 생성 (자동으로 진행됨)
|
# 3. 배포 디렉토리 생성 (자동으로 진행됨)
|
||||||
# ~/deployments/
|
# ~/deployments/
|
||||||
# ~/taxbaik_active
|
# ~/taxbaik_active
|
||||||
# ~/taxbaik_admin_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/web/ \
|
|
||||||
$DEPLOY_USER@$DEPLOY_HOST:~/deployments/taxbaik_${TIMESTAMP}/
|
|
||||||
|
|
||||||
rsync -avz --delete ./publish/admin/ \
|
|
||||||
$DEPLOY_USER@$DEPLOY_HOST:~/deployments/taxbaik_admin_${TIMESTAMP}/
|
|
||||||
|
|
||||||
# 심링크 변경 및 시작
|
|
||||||
ssh $DEPLOY_USER@$DEPLOY_HOST << EOF
|
|
||||||
ln -sfn ~/deployments/taxbaik_${TIMESTAMP} ~/taxbaik_active
|
|
||||||
ln -sfn ~/deployments/taxbaik_admin_${TIMESTAMP} ~/taxbaik_admin_active
|
|
||||||
sudo systemctl start taxbaik taxbaik-admin
|
|
||||||
sudo systemctl status taxbaik taxbaik-admin
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3단계: Gitea Actions 설정 (선택)
|
|
||||||
|
|
||||||
**Gitea 저장소 Settings → Secrets 추가**:
|
**Gitea 저장소 Settings → Secrets 추가**:
|
||||||
- `DEPLOY_USER`: `kjh2064`
|
- `DEPLOY_USER`: `kjh2064`
|
||||||
@@ -95,13 +67,13 @@ EOF
|
|||||||
ssh kjh2064@178.104.200.7
|
ssh kjh2064@178.104.200.7
|
||||||
|
|
||||||
# 서비스 상태
|
# 서비스 상태
|
||||||
sudo systemctl status taxbaik taxbaik-admin
|
sudo systemctl status taxbaik
|
||||||
|
|
||||||
# 프로세스 확인
|
# 프로세스 확인
|
||||||
ps aux | grep TaxBaik
|
ps aux | grep TaxBaik
|
||||||
|
|
||||||
# 포트 확인
|
# 포트 확인
|
||||||
netstat -tlnp | grep -E '5001|5002'
|
netstat -tlnp | grep -E '5001'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 엔드포인트 테스트
|
### 2. 엔드포인트 테스트
|
||||||
@@ -145,9 +117,6 @@ SELECT COUNT(*) FROM categories;
|
|||||||
# 웹 서비스
|
# 웹 서비스
|
||||||
journalctl -u taxbaik -n 50
|
journalctl -u taxbaik -n 50
|
||||||
|
|
||||||
# 관리자 서비스
|
|
||||||
journalctl -u taxbaik-admin -n 50
|
|
||||||
|
|
||||||
# Nginx
|
# Nginx
|
||||||
sudo tail -f /var/log/nginx/access.log | grep taxbaik
|
sudo tail -f /var/log/nginx/access.log | grep taxbaik
|
||||||
```
|
```
|
||||||
@@ -197,7 +166,7 @@ curl -I -H "Accept-Encoding: gzip" http://178.104.200.7/taxbaik/ | grep -i encod
|
|||||||
- [ ] 로그인 폼 표시
|
- [ ] 로그인 폼 표시
|
||||||
- [ ] 초기 계정 로그인
|
- [ ] 초기 계정 로그인
|
||||||
- username: `admin`
|
- username: `admin`
|
||||||
- password: `admin123`
|
- password: `<TAXBAIK_ADMIN_TEST_PASSWORD>`
|
||||||
|
|
||||||
#### 대시보드
|
#### 대시보드
|
||||||
- [ ] 로그인 후 대시보드 로드
|
- [ ] 로그인 후 대시보드 로드
|
||||||
@@ -226,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;` 후 재시작 |
|
||||||
@@ -239,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-admin -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'
|
||||||
@@ -255,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:5002/taxbaik/admin || systemctl restart taxbaik-admin
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -277,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/web
|
|
||||||
rsync -avz ./publish/web/ 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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 롤백 절차
|
### 롤백 절차
|
||||||
@@ -293,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
|
||||||
```
|
```
|
||||||
@@ -306,7 +265,7 @@ EOF
|
|||||||
- [ ] 모든 엔드포인트 HTTP 200 응답
|
- [ ] 모든 엔드포인트 HTTP 200 응답
|
||||||
- [ ] 데이터베이스 마이그레이션 완료 (schema_migrations 테이블 확인)
|
- [ ] 데이터베이스 마이그레이션 완료 (schema_migrations 테이블 확인)
|
||||||
- [ ] 초기 5개 블로그 포스트 DB에 존재
|
- [ ] 초기 5개 블로그 포스트 DB에 존재
|
||||||
- [ ] 로그인 기능 정상 (admin/admin123)
|
- [ ] 로그인 기능 정상 (admin/<TAXBAIK_ADMIN_TEST_PASSWORD>)
|
||||||
- [ ] 문의 폼 제출 → DB 저장 확인
|
- [ ] 문의 폼 제출 → DB 저장 확인
|
||||||
- [ ] Nginx 프록시 정상 작동
|
- [ ] Nginx 프록시 정상 작동
|
||||||
- [ ] 응답 gzip 압축 확인
|
- [ ] 응답 gzip 압축 확인
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
# TaxBaik 개선 로드맵 WBS
|
||||||
|
|
||||||
|
이 문서는 "완료 보고"가 아니라 검증 가능한 작업 목록이다. 각 WBS는 성공 기준을 통과해야 완료로 본다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 완료 판정 원칙
|
||||||
|
|
||||||
|
- 코드 변경만으로 완료 처리하지 않는다.
|
||||||
|
- 서버 배포 대상 기능은 CI/CD 성공과 실제 동작 확인을 요구한다.
|
||||||
|
- API 기능은 단위 테스트 또는 통합 테스트와 함께 실제 HTTP 호출 결과를 확인한다.
|
||||||
|
- DB 변경은 마이그레이션과 롤백 위험을 문서화한다.
|
||||||
|
- 비밀값은 Gitea Secrets 또는 서버 환경변수로만 관리한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 홈페이지 · SEO · UX ───────────────────────────
|
||||||
|
|
||||||
|
## WBS-UX-01 공개 홈페이지 UX/SEO 검증
|
||||||
|
|
||||||
|
목표: 공개 홈페이지가 검색 유입과 상담 전환에 맞는 구조인지 검증한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 홈/블로그 목록/블로그 상세/상담 문의 페이지 200
|
||||||
|
- 주요 페이지 title/description 존재
|
||||||
|
- 모바일 viewport에서 주요 CTA가 보인다.
|
||||||
|
- 상담 문의 제출 Playwright E2E가 통과한다.
|
||||||
|
- 블로그 상세 SEO 메타 검증이 배포본 기준으로 통과한다.
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] 공개 페이지 Playwright smoke E2E 추가
|
||||||
|
- [x] 상담 문의 제출 E2E 추가
|
||||||
|
- [x] 블로그 상세 SEO 메타 검증 추가
|
||||||
|
|
||||||
|
검증 파일:
|
||||||
|
- `tests/e2e/public-smoke.spec.ts`
|
||||||
|
- `tests/e2e/blog-seo.spec.ts`
|
||||||
|
- `tests/e2e/contact-submit.spec.ts`
|
||||||
|
- `tests/e2e/inquiry-detail.spec.ts`
|
||||||
|
|
||||||
|
## WBS-UX-02 홈페이지 FAQ 섹션 (정적)
|
||||||
|
|
||||||
|
목표: 방문자가 상담 전 자주 묻는 질문에서 직접 답을 얻고 전환율을 높인다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 홈페이지에 4개 FAQ 아코디언 표시 (기장료, 양도세 상담, 무료 상담, 첫 상담 준비물)
|
||||||
|
- 아코디언 열림/닫힘 동작
|
||||||
|
- 모바일에서 가독성 확인
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] Index.cshtml에 FAQ 아코디언 섹션 추가 (최종 CTA 앞)
|
||||||
|
- [x] site.css faq-accordion / faq-item / faq-question / faq-answer 스타일
|
||||||
|
- [x] 배포 완료 (`12070b7`)
|
||||||
|
- [ ] 배포 후 브라우저 아코디언 동작 확인
|
||||||
|
|
||||||
|
## WBS-UX-04 개인정보처리방침·이용약관 페이지
|
||||||
|
|
||||||
|
목표: 법적 의무를 충족하고 방문자 신뢰를 높이는 정책 페이지를 제공한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- `/taxbaik/privacy` 개인정보처리방침 페이지 정상 렌더링 (200)
|
||||||
|
- `/taxbaik/terms` 이용약관 페이지 정상 렌더링 (200)
|
||||||
|
- 푸터에 두 페이지 링크 표시
|
||||||
|
- 개인정보처리방침: 수집 항목, 이용 목적, 보유 기간, 파기 방법, 책임자 정보 포함
|
||||||
|
- 이용약관: 목적, 서비스 범위, 면책 조항, 저작권, 준거법 포함
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] Privacy.cshtml + Privacy.cshtml.cs (Razor Page)
|
||||||
|
- [x] Terms.cshtml + Terms.cshtml.cs (Razor Page)
|
||||||
|
- [x] _Footer.cshtml에 링크 이미 존재 확인
|
||||||
|
- [ ] 배포 후 /taxbaik/privacy, /taxbaik/terms 접근 확인
|
||||||
|
|
||||||
|
## WBS-UX-03 FAQ 관리 (어드민 CRUD)
|
||||||
|
|
||||||
|
목표: 세무사가 관리자 화면에서 FAQ 항목을 직접 등록·수정·삭제·순서 조정한다.
|
||||||
|
홈페이지 FAQ가 하드코딩에서 DB 기반으로 전환되어, 코드 수정 없이 운영 가능해진다.
|
||||||
|
|
||||||
|
설계 방향:
|
||||||
|
- FAQ 항목: 질문(question), 답변(answer), 정렬 순서(sort_order), 활성화 여부(is_active)
|
||||||
|
- 홈페이지는 is_active=TRUE 항목을 sort_order 오름차순으로 표시
|
||||||
|
- 카테고리 태그(선택): "기장·세금신고", "부동산", "증여·상속", "기타" — 홈페이지에서 탭 필터 가능
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 관리자 `/taxbaik/admin/faqs` 목록/생성/수정/삭제/순서변경 동작
|
||||||
|
- 홈페이지 FAQ 섹션이 DB에서 로드 (하드코딩 제거)
|
||||||
|
- 비활성 항목은 홈페이지 미표시
|
||||||
|
- sort_order 기준 정렬
|
||||||
|
|
||||||
|
DB 스키마:
|
||||||
|
- `faqs` 테이블 (V007 마이그레이션)
|
||||||
|
- id SERIAL PK
|
||||||
|
- question VARCHAR(300) NOT NULL
|
||||||
|
- answer TEXT NOT NULL
|
||||||
|
- category VARCHAR(50) — 기장·세금신고, 부동산, 증여·상속, 기타
|
||||||
|
- sort_order INT DEFAULT 0
|
||||||
|
- is_active BOOLEAN DEFAULT TRUE
|
||||||
|
- created_at TIMESTAMPTZ
|
||||||
|
- updated_at TIMESTAMPTZ
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] V007__CreateFaqs.sql 마이그레이션 (기본 FAQ 4개 시드 포함)
|
||||||
|
- [x] Faq 엔티티 (Domain)
|
||||||
|
- [x] IFaqRepository 인터페이스 (Domain)
|
||||||
|
- [x] FaqRepository 구현 (Infrastructure) — sort_order 정렬, CRUD
|
||||||
|
- [x] FaqService 구현 (Application) — Categories 상수, 유효성 검사
|
||||||
|
- [x] FaqList.razor 관리자 목록 (활성/비활성 상태 칩, 삭제 확인)
|
||||||
|
- [x] FaqEdit.razor 관리자 등록/수정 (질문/답변/카테고리/순서/활성 토글)
|
||||||
|
- [x] Index.cshtml FAQ 섹션 하드코딩 → DB 루프로 교체 (빈 DB에도 안전)
|
||||||
|
- [x] IndexModel FaqService 주입, Task.WhenAll 병렬 로드
|
||||||
|
- [x] MainLayout.razor FAQ 관리 메뉴 추가 (홈페이지 그룹 하위)
|
||||||
|
- [ ] 배포 후 관리자에서 FAQ 추가 → 홈페이지 반영 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 시즌별 마케팅 ───────────────────────────────
|
||||||
|
|
||||||
|
## WBS-MKT-01 시즌별 홈페이지 자동 전환
|
||||||
|
|
||||||
|
목표: 세무 신고 시즌마다 홈페이지 Hero·CTA·서비스 카드 순서가 자동 변경된다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 7개 시즌(vat-2nd, year-end-settlement, corporate-tax, income-tax, vat-1st, comprehensive-real-estate-tax, year-end-gift) 날짜 판정 정확
|
||||||
|
- 시즌 중 Hero에 UrgencyBadge 표시
|
||||||
|
- D-7일 이내 긴박감 메시지 표시
|
||||||
|
- FocusService 기준 서비스 카드 순서 자동 정렬
|
||||||
|
- 최종 CTA 시즌 문구 전환
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] TaxSeason / TaxSeasonCalendar 정의
|
||||||
|
- [x] CurrentSeasonDto / SeasonalMarketingService 구현
|
||||||
|
- [x] Index.cshtml Hero 시즌 분기 렌더링
|
||||||
|
- [x] Index.cshtml 서비스 카드 cardOrder 정렬 로직
|
||||||
|
- [x] Index.cshtml 최종 CTA 시즌 전환
|
||||||
|
- [x] CLAUDE.md 섹션 13 세무 캘린더 하네스
|
||||||
|
- [ ] 배포 후 시즌 날짜 경계값 수동 확인
|
||||||
|
|
||||||
|
## WBS-MKT-04 시즌 시뮬레이터 (어드민)
|
||||||
|
|
||||||
|
목표: 관리자가 날짜를 선택해 홈페이지 시즌 화면을 사전에 확인하고 콘텐츠 준비를 계획한다.
|
||||||
|
|
||||||
|
배경: 7개 시즌이 자동 전환되므로, 실제 날짜가 되기 전 미리 Hero 화면을 확인하는 도구가 필요하다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 관리자 `/taxbaik/admin/season-simulator` 접근 가능
|
||||||
|
- 날짜 선택 시 해당 날짜의 Hero 섹션 미리보기 렌더링
|
||||||
|
- 각 시즌 버튼 클릭으로 해당 시즌 첫날로 즉시 이동
|
||||||
|
- 비시즌 날짜 선택 시 기본 Hero 미리보기 표시
|
||||||
|
- 연간 시즌 타임라인 테이블 표시
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] SeasonSimulator.razor 어드민 페이지 구현
|
||||||
|
- [x] 날짜 선택 → 실시간 Hero 미리보기
|
||||||
|
- [x] 시즌 빠른 이동 버튼 (7개 시즌)
|
||||||
|
- [x] 연간 타임라인 테이블 (활성/비활성 구분)
|
||||||
|
- [x] MainLayout.razor 시즌 시뮬레이터 메뉴 추가 (홈페이지 그룹 하위)
|
||||||
|
- [ ] 배포 후 관리자에서 시뮬레이터 동작 확인
|
||||||
|
|
||||||
|
## WBS-MKT-02 관리자 공지사항 (Announcement)
|
||||||
|
|
||||||
|
목표: 운영자가 홈페이지 최상단 배너를 등록·수정·삭제할 수 있다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 관리자 `/taxbaik/admin/announcements` 목록/생성/수정/삭제 동작
|
||||||
|
- is_active=TRUE + 기간 조건(starts_at~ends_at)에 해당하는 공지만 홈페이지에 노출
|
||||||
|
- 유형(info/banner/urgent) 별 색상 배지 표시
|
||||||
|
- 홈페이지 최상단 announcement-bar 노출
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] V005__CreateAnnouncements.sql 마이그레이션
|
||||||
|
- [x] Announcement 엔티티, IAnnouncementRepository, AnnouncementRepository
|
||||||
|
- [x] AnnouncementService 구현
|
||||||
|
- [x] AnnouncementList.razor, AnnouncementEdit.razor 관리자 화면
|
||||||
|
- [x] Index.cshtml 공지사항 배너 렌더링
|
||||||
|
- [x] MainLayout.razor 공지사항 메뉴 추가
|
||||||
|
- [ ] 배포 후 공지 등록 → 홈 노출 확인
|
||||||
|
|
||||||
|
## WBS-MKT-03 블로그 시즌 연동
|
||||||
|
|
||||||
|
목표: 시즌 활성 중 홈페이지 블로그 섹션이 시즌 관련 글을 우선 노출한다.
|
||||||
|
|
||||||
|
배경: 세무 시즌에 맞는 콘텐츠를 전면에 배치해 상담 전환율과 SEO 체류시간을 높인다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 시즌 중: 해당 카테고리 글 최대 2개(이번 시즌 추천 배지) + 최신 글로 3개 채움
|
||||||
|
- 평상시: 최신 글 3개 (기존 동작)
|
||||||
|
- 시즌별 전체 글 보기 버튼 (`/taxbaik/blog?category=<slug>`)
|
||||||
|
- 배너 헤더가 시즌명 표시
|
||||||
|
|
||||||
|
카테고리 → 시즌 슬러그 매핑:
|
||||||
|
- `vat-2nd` / `vat-1st` → `vat`
|
||||||
|
- `income-tax` → `income-tax`
|
||||||
|
- `year-end-settlement` / `corporate-tax` → `business-tax`
|
||||||
|
- `comprehensive-real-estate-tax` → `real-estate-tax`
|
||||||
|
- `year-end-gift` → `family-asset`
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] TaxSeason.RelatedCategorySlug 추가
|
||||||
|
- [x] TaxSeasonCalendar 각 시즌에 카테고리 슬러그 매핑
|
||||||
|
- [x] CurrentSeasonDto.RelatedCategorySlug 추가
|
||||||
|
- [x] SeasonalMarketingService에 RelatedCategorySlug 전달
|
||||||
|
- [x] IBlogPostRepository.GetByCategorySlugAsync 추가
|
||||||
|
- [x] BlogPostRepository.GetByCategorySlugAsync 구현
|
||||||
|
- [x] BlogService.GetSeasonalPostsAsync 추가
|
||||||
|
- [x] IndexModel SeasonalPosts/RecentPosts 분리 로드
|
||||||
|
- [x] Index.cshtml 블로그 섹션 시즌 분기 렌더링
|
||||||
|
- [x] site.css 블로그 시즌 강조 스타일 추가
|
||||||
|
- [ ] 배포 후 시즌 활성 날짜에 블로그 카드 "이번 시즌 추천" 배지 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 운영 인프라 ─────────────────────────────────
|
||||||
|
|
||||||
|
## WBS-OPS-01 배포 검증 게이트 고도화
|
||||||
|
|
||||||
|
목표: curl/API만이 아니라 실제 브라우저 검증까지 통과해야 배포를 성공으로 본다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- `dotnet build TaxBaik.sln -c Release` 경고 0, 오류 0
|
||||||
|
- `dotnet test TaxBaik.sln -c Release --no-build` 전체 통과
|
||||||
|
- CI 배포 후 Playwright가 `/taxbaik/admin/login`에서 실제 로그인 수행
|
||||||
|
- 로그인 후 `/taxbaik/admin/dashboard` 도달
|
||||||
|
- 브라우저 console error 및 page error 0개
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] Playwright Test 프로젝트 추가
|
||||||
|
- [x] 관리자 로그인 E2E 추가
|
||||||
|
- [x] CI 배포 후 Playwright 실행 단계 추가
|
||||||
|
- [x] Playwright가 발견한 Blazor DI 결함 수정
|
||||||
|
- [ ] CI run에서 Playwright 전체 통과 확인
|
||||||
|
- [ ] 배포 검증에 블로그 상세/문의/비밀번호 변경 성공 기준 반영 확인
|
||||||
|
|
||||||
|
## WBS-OPS-02 배포 502 / Nginx 유지보수 페이지
|
||||||
|
|
||||||
|
목표: CI 배포 중 502 Bad Gateway 대신 한국어 유지보수 페이지를 제공한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- Nginx error_page 502/503 → maintenance.html 직접 서빙
|
||||||
|
- 배포 중 방문자는 유지보수 페이지(15초 자동 새로고침)를 본다.
|
||||||
|
- 배포 완료 후 정상 서비스 복구
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] maintenance.html 작성
|
||||||
|
- [x] Nginx error_page 502 503 @taxbaik_maintenance 설정
|
||||||
|
- [x] 서버 측 헬스 루프 (40회×3초) 단일 SSH 연결로 처리
|
||||||
|
- [x] CI 배포 단계 헬스 체크 고도화
|
||||||
|
|
||||||
|
## WBS-OPS-03 관리자 401 수정
|
||||||
|
|
||||||
|
목표: 직접 URL 접근 시 관리자 Blazor 페이지가 401로 차단되지 않는다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- `/taxbaik/admin/announcements` 등 직접 접근 시 Blazor Shell 200 응답
|
||||||
|
- 미인증 사용자는 로그인 페이지로 리다이렉트
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] MapRazorComponents().AllowAnonymous() 적용
|
||||||
|
- [x] AuthorizeRouteView → RedirectToLogin 인증 흐름 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 인증 · 관리자 ─────────────────────────────────
|
||||||
|
|
||||||
|
## WBS-AUTH-01 인증/비밀번호 운영 안정화
|
||||||
|
|
||||||
|
목표: DB 직접 수정 대신 API로 관리자 인증 운영 작업을 수행한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 비밀번호 변경 API가 현재 비밀번호를 요구한다.
|
||||||
|
- 비밀번호 재설정 API는 운영 secret 없이는 동작하지 않는다.
|
||||||
|
- 실패 응답은 민감 정보를 노출하지 않는다.
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] 로그인 API 검증
|
||||||
|
- [x] 비밀번호 변경 API 추가
|
||||||
|
- [x] 재설정 API 추가
|
||||||
|
- [x] 관리자 UI에 비밀번호 변경 화면 추가
|
||||||
|
- [x] 비밀번호 변경 Playwright E2E 추가
|
||||||
|
|
||||||
|
## WBS-ADMIN-01 관리자 Blazor 안정화
|
||||||
|
|
||||||
|
목표: 관리자 화면을 일반 웹페이지처럼 명시적 사용자 액션에만 갱신하고, circuit 예외를 배포 전 차단한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 관리자 주요 메뉴 대시보드/블로그/문의/설정/공지사항 circuit error 0개
|
||||||
|
- 저장/삭제/상태 변경 액션은 성공/실패 메시지를 표시한다.
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] 중복 `/admin` 라우트 제거
|
||||||
|
- [x] MudBlazor DI 타입 오류 수정
|
||||||
|
- [x] 관리자 메뉴 smoke E2E 추가
|
||||||
|
- [x] 설정 저장 TODO를 실제 DB 기반 기능으로 전환
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 고객지원 백오피스 (CRM) ──────────────────────
|
||||||
|
|
||||||
|
> **배경**: 세무사 사무실에서 고객 정보와 상담 이력이 파편화(메모장·카톡·기억)되면 마감 누락, 서비스 연속성 단절, 재계약 기회 손실이 발생한다.
|
||||||
|
> 30년 경력 세무사가 혼자 또는 소수 인원으로 운영할 때 가장 먼저 필요한 것은 고객 카드와 상담 이력이다.
|
||||||
|
|
||||||
|
## WBS-CRM-01 고객 카드 (Client Card) — Phase 1
|
||||||
|
|
||||||
|
목표: 고객별 기본 정보·서비스 유형·상태를 한 화면에서 관리한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 관리자 `/taxbaik/admin/clients` 목록/검색/생성/수정/삭제 동작
|
||||||
|
- 고객 카드: 이름, 회사명, 연락처, 이메일, 서비스 유형, 세금 유형, 상태, 유입 경로, 메모
|
||||||
|
- 상태 필터(활성/비활성)로 목록 조회
|
||||||
|
- 고객 저장 시 updated_at 자동 갱신
|
||||||
|
|
||||||
|
DB 스키마:
|
||||||
|
- `clients` 테이블 (V006 마이그레이션)
|
||||||
|
- 컬럼: id, name, company_name, phone, email, service_type, tax_type, status, source, memo, created_at, updated_at
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] V006__CreateClients.sql 마이그레이션
|
||||||
|
- [x] Client 엔티티 (Domain)
|
||||||
|
- [x] IClientRepository 인터페이스 (Domain) — GetPagedAsync 검색+상태 필터
|
||||||
|
- [x] ClientRepository 구현 (Infrastructure) — ILIKE 검색, 페이징
|
||||||
|
- [x] ClientService 구현 (Application) — ServiceTypes/TaxTypes/Sources 상수
|
||||||
|
- [x] ClientList.razor 관리자 목록 화면 — 검색바, 상태 필터, 페이징
|
||||||
|
- [x] ClientEdit.razor 관리자 등록/수정 화면 — 기본/세무/관리 섹션
|
||||||
|
- [x] MainLayout.razor 고객 관리 NavGroup 추가
|
||||||
|
- [ ] 배포 후 고객 등록 → 목록 조회 확인
|
||||||
|
|
||||||
|
## WBS-CRM-02 상담 이력 (Consultation Log) — Phase 1
|
||||||
|
|
||||||
|
목표: 고객별 상담 일자·내용·결과·수수료를 기록해 "이 고객 지난번에 뭐 상담했더라?"를 해결한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 고객 상세에서 상담 이력 목록/추가/삭제 동작
|
||||||
|
- 상담 이력 필드: 날짜, 서비스 유형, 상담 요약, 결과(계약/보류/거절/완료), 수수료
|
||||||
|
- 이력 없는 고객은 빈 목록 표시
|
||||||
|
|
||||||
|
DB 스키마:
|
||||||
|
- `consultations` 테이블 (V008 마이그레이션)
|
||||||
|
- 컬럼: id, client_id(FK), consultation_date, service_type, summary, result, fee, created_at
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] V008__CreateConsultations.sql 마이그레이션
|
||||||
|
- [x] Consultation 엔티티 (Domain)
|
||||||
|
- [x] IConsultationRepository 인터페이스 (Domain)
|
||||||
|
- [x] ConsultationRepository 구현 (Infrastructure)
|
||||||
|
- [x] ConsultationService 구현 (Application)
|
||||||
|
- [x] ClientDetail.razor (고객 상세 + 상담 이력 추가/삭제)
|
||||||
|
- [x] DI 등록 (Infrastructure + Application)
|
||||||
|
- [ ] 배포 후 고객 상세에서 상담 이력 추가 확인
|
||||||
|
|
||||||
|
## WBS-CRM-03 문의 → 고객 전환 — Phase 1
|
||||||
|
|
||||||
|
목표: 홈페이지 문의 접수 건을 클릭 한 번으로 고객 카드로 등록한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 문의 상세에 "고객으로 등록" 버튼 표시
|
||||||
|
- 버튼 클릭 시 고객 카드 자동 생성 후 연결
|
||||||
|
- 이미 연결된 고객이 있으면 버튼 대신 고객 카드 링크 표시
|
||||||
|
- inquiries 테이블에 client_id, admin_memo, updated_at 컬럼 추가
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] V009__AddClientIdToInquiries.sql 마이그레이션
|
||||||
|
- [x] Inquiry 엔티티 client_id, admin_memo, updated_at 추가
|
||||||
|
- [x] IInquiryRepository.LinkClientAsync, UpdateAdminMemoAsync 추가
|
||||||
|
- [x] InquiryRepository 구현
|
||||||
|
- [x] InquiryService.LinkClientAsync, UpdateAdminMemoAsync 추가
|
||||||
|
- [x] ClientService.CreateFromInquiryAsync 추가
|
||||||
|
- [x] InquiryDetail.razor "고객으로 등록" 버튼 + 담당자 메모 추가
|
||||||
|
- [ ] 배포 후 문의 → 고객 전환 흐름 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 고객지원 백오피스 Phase 2 ──────────────────────
|
||||||
|
|
||||||
|
## WBS-CRM-04 신고 일정 캘린더 — Phase 2
|
||||||
|
|
||||||
|
목표: 고객별 신고 예정일과 마감일을 추적해 가산세 리스크를 방지한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 관리자에서 고객별 세금 신고 일정 등록/수정/완료 처리
|
||||||
|
- D-Day 표시 (D-7일 이내 강조)
|
||||||
|
- 이번 달 마감 목록을 대시보드 위젯으로 표시
|
||||||
|
|
||||||
|
DB 스키마:
|
||||||
|
- `tax_filings` 테이블 (V010 마이그레이션)
|
||||||
|
- 컬럼: id, client_id(FK), filing_type, due_date, status(pending/filed/overdue), memo
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] V010__CreateTaxFilings.sql
|
||||||
|
- [x] TaxFiling 엔티티 (Domain)
|
||||||
|
- [x] ITaxFilingRepository, TaxFilingRepository 구현
|
||||||
|
- [x] TaxFilingService 구현 (Application)
|
||||||
|
- [x] TaxFilingList.razor (관리자 신고 일정 화면 + 상태별 탭)
|
||||||
|
- [x] FilingTable.razor (D-Day 강조, 완료 처리, 삭제)
|
||||||
|
- [x] Dashboard.razor에 30일 이내 마감 위젯 추가
|
||||||
|
- [x] MainLayout.razor 신고 일정 메뉴 추가
|
||||||
|
- [x] DI 등록
|
||||||
|
- [ ] 배포 후 신고 일정 등록 → D-Day 표시 확인
|
||||||
|
|
||||||
|
## WBS-CRM-05 문의 접수 현황 강화 — Phase 2
|
||||||
|
|
||||||
|
목표: 문의 상태를 세분화하고 담당자 메모를 기록해 처리 흐름을 추적한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 문의 상태: 신규/상담중/계약완료/거절/종결 5단계
|
||||||
|
- 목록에서 상태 탭 필터로 빠른 분류
|
||||||
|
- 상태 변경 시 updated_at 자동 기록
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] V011__ExtendInquiryStatus.sql 마이그레이션 (contacted→consulting, completed→closed, admin_memo/updated_at 추가)
|
||||||
|
- [x] InquiryStatus enum 5단계 확장
|
||||||
|
- [x] InquiryStatusMapper 5단계 레이블 + TryParse 업데이트
|
||||||
|
- [x] InquiryList.razor 5단계 탭 (신규/상담중/계약완료/거절/종결)
|
||||||
|
- [x] InquiryDetail.razor 5단계 상태 버튼 + 색상 구분
|
||||||
|
- [x] Dashboard.razor 상태 레이블 5단계 반영
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 고객지원 백오피스 Phase 3 ──────────────────────
|
||||||
|
|
||||||
|
## WBS-CRM-06 텔레그램 자동 리포트 — Phase 3
|
||||||
|
|
||||||
|
목표: 세무사에게 일/주 단위 신규 문의·처리 현황·마감 임박 건을 텔레그램으로 전송한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 매일 오전 9시 신규 문의 수, 처리 대기 수 자동 전송
|
||||||
|
- 매주 월요일 주간 리포트 (신규 고객, 이번 주 마감 신고 건)
|
||||||
|
- 텔레그램 전송 실패 시 로그만 남기고 앱 정상 운영 유지
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] BackgroundService 또는 Hangfire 기반 스케줄러 추가
|
||||||
|
- [x] 일간/주간 리포트 메시지 템플릿
|
||||||
|
- [x] TelegramNotificationService에 리포트 메서드 추가
|
||||||
|
|
||||||
|
## WBS-CRM-07 고객 포털 (읽기 전용) — Phase 3
|
||||||
|
|
||||||
|
목표: 기장 고객이 본인 신고 현황과 중요 알림을 직접 확인한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 고객 전용 URL + 인증(소셜 로그인 또는 링크 토큰)
|
||||||
|
- 본인 신고 일정, 상담 요약(세무사 허용 항목만) 조회
|
||||||
|
- 개인정보 열람 범위는 세무사가 허용한 항목만
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] 고객 포털 설계 (인증 방식 결정 — WBS-CRM-08 선행)
|
||||||
|
- [x] 고객 전용 Razor Pages 추가
|
||||||
|
- [x] 세무사 허용 권한 설정 UI
|
||||||
|
|
||||||
|
## WBS-CRM-08 고객 회원가입 · 소셜 로그인 — Phase 3
|
||||||
|
|
||||||
|
목표: 고객 포털 접근을 위한 회원가입과 소셜 로그인을 제공한다.
|
||||||
|
가입 마찰을 최소화해 상담 접수 → 고객 포털 전환율을 높인다.
|
||||||
|
|
||||||
|
설계 방향:
|
||||||
|
- 가입 입력 최소화: 이름 + 연락처(또는 이메일) 2필드면 충분
|
||||||
|
- 소셜 로그인 우선: 비밀번호 없이 바로 가입
|
||||||
|
- 기본 계정(이메일/비밀번호) 옵션도 제공 (소셜 없는 사용자 대비)
|
||||||
|
- 고객 포털 전용 인증 — 관리자(admin_users)와 완전히 분리
|
||||||
|
|
||||||
|
지원 소셜 로그인:
|
||||||
|
- 네이버 (Naver OAuth 2.0) — 국내 주요 채널
|
||||||
|
- 카카오 (Kakao Login) — 기존 카카오 채널 연계
|
||||||
|
- 구글 (Google OAuth 2.0) — 해외·젊은 고객층
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 소셜 로그인 3종 모두 동작 (네이버·카카오·구글)
|
||||||
|
- 이메일/비밀번호 기본 계정 가입 + 로그인 동작
|
||||||
|
- 가입 폼: 이름·연락처 2필드만 요구 (소셜 프로필에서 자동 채우기)
|
||||||
|
- 로그인 후 고객 포털 (`/taxbaik/portal`) 접근
|
||||||
|
- 고객 계정이 백오피스 clients 테이블 레코드와 연결
|
||||||
|
- 회원 계정 미인증 상태에서 포털 접근 시 로그인 페이지 리다이렉트
|
||||||
|
|
||||||
|
DB 스키마:
|
||||||
|
- `portal_users` 테이블 (V011 마이그레이션)
|
||||||
|
- id, client_id(FK, nullable), email, name, phone, provider(naver/kakao/google/local), provider_id, password_hash(nullable), created_at
|
||||||
|
- 소셜 로그인 provider_id는 각 플랫폼 식별자
|
||||||
|
|
||||||
|
기술 결정:
|
||||||
|
- ASP.NET Core OAuth Middleware (Microsoft.AspNetCore.Authentication.OAuth)
|
||||||
|
- 네이버: 커스텀 OAuth handler (공식 패키지 없음, 직접 구현)
|
||||||
|
- 카카오: AspNet.Security.OAuth.Kakao 패키지
|
||||||
|
- 구글: Microsoft.AspNetCore.Authentication.Google 패키지
|
||||||
|
- 고객 포털 세션: HttpOnly Cookie 기반 (JWT localStorage와 분리)
|
||||||
|
|
||||||
|
환경 변수 필요 (Gitea Secrets 추가):
|
||||||
|
- `NAVER_CLIENT_ID` / `NAVER_CLIENT_SECRET`
|
||||||
|
- `KAKAO_CLIENT_ID` / `KAKAO_CLIENT_SECRET`
|
||||||
|
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] WBS-CRM-07 고객 포털 기본 구조 완성 (선행)
|
||||||
|
- [x] OAuth 앱 등록 (네이버·카카오·구글 개발자 콘솔)
|
||||||
|
- [x] V011__CreatePortalUsers.sql 마이그레이션 (실제 V016__CreatePortalUsers.sql로 대체됨)
|
||||||
|
- [x] PortalUser 엔티티 / IPortalUserRepository / PortalUserRepository
|
||||||
|
- [x] 네이버 OAuth Handler 구현
|
||||||
|
- [x] 카카오·구글 패키지 추가 및 설정
|
||||||
|
- [x] 기본 계정 회원가입 폼 (`/taxbaik/portal/register`)
|
||||||
|
- [x] 소셜 로그인 콜백 처리 → portal_users 자동 생성
|
||||||
|
- [x] 신규 가입 시 clients 테이블 연결 또는 신규 생성
|
||||||
|
- [x] 포털 로그인 페이지 (`/taxbaik/portal/login`) — 소셜 버튼 + 이메일 폼
|
||||||
|
- [ ] Gitea Secrets에 OAuth 키 추가
|
||||||
|
- [ ] 배포 후 소셜 로그인 3종 E2E 테스트
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 유지보수성 ─────────────────────────────────
|
||||||
|
|
||||||
|
## WBS-MAINT-01 유지보수성/파편화 축소
|
||||||
|
|
||||||
|
목표: 문서와 실제 구조의 불일치를 줄이고 단일 앱 운영 기준을 유지한다.
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] README 테스트/배포 섹션 갱신
|
||||||
|
- [x] CLAUDE.md E2E 기준 갱신
|
||||||
|
- [x] 오래된 최종 보고 문서의 허위 완료 표현 정정
|
||||||
|
- [x] CLAUDE.md 섹션 13 시즌별 마케팅 하네스 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 현재 검증 메모
|
||||||
|
|
||||||
|
- `dotnet build TaxBaik.sln` 성공 (2026-06-27 기준, 경고 0 오류 0)
|
||||||
|
- 최종 배포 커밋: `9c96f15` (FAQ 관리 기능)
|
||||||
|
- WBS-MKT-01/02/03/04 구현 완료, 배포 후 시각 검증 필요
|
||||||
|
- WBS-UX-03/04 구현 완료
|
||||||
|
- WBS-CRM-01/02/03/04/05 구현 완료 (배포 후 검증 필요)
|
||||||
|
- WBS-CRM-06/07/08 (텔레그램·포털·소셜 로그인) Phase 3 미착수
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ── 홈페이지 · 어드민 · 포털 프리미엄 UX/UI 개편 (2026-06-30) ──────────────────
|
||||||
|
|
||||||
|
## WBS-UX-05 홈페이지 프리미엄 UI 및 마이크로 인터랙션
|
||||||
|
|
||||||
|
목표: 홈페이지 디자인을 극도로 모던하고 신뢰성 있는 프리미엄 스타일로 전면 개편한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- Hero 섹션에 유려한 배경 그라데이션 및 부드러운 CSS 애니메이션 효과 적용
|
||||||
|
- 서비스 카드에 섀도우 및 보더 트랜지션, 골드/그린 그라데이션 호버 이펙트 추가
|
||||||
|
- 신뢰도 스트립 카드에 입체감 및 돋보이는 레이아웃 설계
|
||||||
|
- Noto Sans KR 외에 Outfit/Inter 등의 보조 영문 폰트 결합으로 타이포그래피 고급화
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] `site.css` 내 Hero 섹션 그라데이션 및 CSS 애니메이션 보강
|
||||||
|
- [x] 서비스 카드 및 신뢰도 스트립 컴포넌트 프리미엄 스타일로 개편
|
||||||
|
- [x] 홈페이지 폰트 스택 확장 및 메인 레이아웃 적용
|
||||||
|
|
||||||
|
## WBS-PORTAL-01 고객 포털 UI/UX 고도화 및 글래스모피즘
|
||||||
|
|
||||||
|
목표: 고객 마이 포털 화면을 미려하고 현대적인 글래스모피즘 디자인으로 개편하여 이용 가치를 극대화한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 포털 메인 대시보드 카드를 Glassmorphism 스타일(blur, semi-transparent border)로 변경
|
||||||
|
- 세무 신고 현황 테이블 및 상담 이력 타임라인 컴포넌트의 모던 디자인화
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] `site.css` 내 포털 전용 모던 글래스모피즘 클래스군 추가
|
||||||
|
- [x] `Portal/Index.cshtml` 레이아웃 및 컴포넌트 UI 고도화
|
||||||
|
|
||||||
|
## WBS-MAINT-02 코드 품질 및 경고 결함 차단
|
||||||
|
|
||||||
|
목표: 빌드 컴파일 타임 경고(Warnings)를 0으로 유지하여 미래 코드 결함을 방지한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- `dotnet build` 수행 시 경고 0개 달성
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] `CustomAuthenticationStateProvider.cs` Nullable 경고 수정
|
||||||
|
- [x] `Dashboard.razor` 미사용 변수 제거 및 UI 연계 바인딩 처리
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- Common code audit checks
|
||||||
|
SELECT code_group, code_value
|
||||||
|
FROM common_codes
|
||||||
|
WHERE code_group LIKE '% %';
|
||||||
|
|
||||||
|
SELECT code_group, code_value
|
||||||
|
FROM common_codes
|
||||||
|
WHERE code_value LIKE '% %';
|
||||||
|
|
||||||
|
SELECT code_group, code_value, LEN(code_group) AS code_group_len, LEN(code_value) AS code_value_len
|
||||||
|
FROM common_codes
|
||||||
|
WHERE LEN(code_group) > 80
|
||||||
|
OR LEN(code_value) > 120
|
||||||
|
OR LEN(code_name) > 200;
|
||||||
|
|
||||||
|
SELECT code_group, COUNT(*)
|
||||||
|
FROM common_codes
|
||||||
|
GROUP BY code_group
|
||||||
|
ORDER BY code_group;
|
||||||
|
|
||||||
|
SELECT DISTINCT c.service_type
|
||||||
|
FROM clients c
|
||||||
|
LEFT JOIN common_codes cc
|
||||||
|
ON cc.code_group = 'CLIENT_SERVICE_TYPE'
|
||||||
|
AND cc.code_value = c.service_type
|
||||||
|
WHERE c.service_type IS NOT NULL
|
||||||
|
AND cc.code_value IS NULL;
|
||||||
|
|
||||||
|
SELECT c.status, COUNT(*) AS cnt
|
||||||
|
FROM clients c
|
||||||
|
LEFT JOIN common_codes cc
|
||||||
|
ON cc.code_group = 'CLIENT_STATUS'
|
||||||
|
AND cc.code_value = c.status
|
||||||
|
WHERE c.status IS NOT NULL
|
||||||
|
AND cc.code_value IS NULL
|
||||||
|
GROUP BY c.status;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Restore archived blog posts that were hidden by soft delete.
|
||||||
|
-- Use only when the goal is to bring back previously archived posts.
|
||||||
|
|
||||||
|
UPDATE blog_posts
|
||||||
|
SET deleted_at = NULL,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE deleted_at IS NOT NULL;
|
||||||
+937
@@ -0,0 +1,937 @@
|
|||||||
|
2026-07-04T12:49:21.1083596Z hz-prod-runner-3(version:v0.6.1) received task 1550 of job build-and-deploy, be triggered by event: push
|
||||||
|
2026-07-04T12:49:21.1123461Z workflow prepared
|
||||||
|
2026-07-04T12:49:21.1124291Z evaluating expression 'success()'
|
||||||
|
2026-07-04T12:49:21.1125104Z expression 'success()' evaluated to 'true'
|
||||||
|
2026-07-04T12:49:21.1125339Z 🚀 Start image=docker.gitea.com/runner-images:ubuntu-latest
|
||||||
|
2026-07-04T12:49:21.1227263Z 🐳 docker pull image=docker.gitea.com/runner-images:ubuntu-latest platform= username= forcePull=false
|
||||||
|
2026-07-04T12:49:21.1227465Z 🐳 docker pull docker.gitea.com/runner-images:ubuntu-latest
|
||||||
|
2026-07-04T12:49:21.1502476Z Image exists? true
|
||||||
|
2026-07-04T12:49:21.2087073Z 🐳 docker create image=docker.gitea.com/runner-images:ubuntu-latest platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_default"
|
||||||
|
2026-07-04T12:49:21.3084981Z Created container name=GITEA-ACTIONS-TASK-1550-WORKFLOW-TaxBaik-CI-CD-JOB-build-and-de-d1b621a7a13ad682fe29533e6a28b6835590d0898313b1b9fc5be41b5870455c id=7fdc784e6b95f4a2f3a7139414bcbaeff9754c0efd147950d7f0357c1b99e6c6 from image docker.gitea.com/runner-images:ubuntu-latest (platform: )
|
||||||
|
2026-07-04T12:49:21.3085729Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
|
||||||
|
2026-07-04T12:49:21.3085886Z 🐳 docker run image=docker.gitea.com/runner-images:ubuntu-latest platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_default"
|
||||||
|
2026-07-04T12:49:21.3086611Z Starting container: 7fdc784e6b95f4a2f3a7139414bcbaeff9754c0efd147950d7f0357c1b99e6c6
|
||||||
|
2026-07-04T12:49:21.4654984Z Started container: 7fdc784e6b95f4a2f3a7139414bcbaeff9754c0efd147950d7f0357c1b99e6c6
|
||||||
|
2026-07-04T12:49:21.5734362Z Writing entry to tarball workflow/event.json len:5621
|
||||||
|
2026-07-04T12:49:21.5735278Z Writing entry to tarball workflow/envs.txt len:0
|
||||||
|
2026-07-04T12:49:21.5735491Z Extracting content to '/var/run/act/'
|
||||||
|
2026-07-04T12:49:21.5964245Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
|
||||||
|
2026-07-04T12:49:21.5964651Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
|
||||||
|
2026-07-04T12:49:22.5232989Z Unable to pull refs/heads/v4: non-fast-forward update
|
||||||
|
2026-07-04T12:49:22.5233439Z Cloned https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
|
||||||
|
2026-07-04T12:49:22.5369401Z Checked out v4
|
||||||
|
2026-07-04T12:49:22.5461867Z ☁ git clone 'https://github.com/actions/setup-dotnet' # ref=v4
|
||||||
|
2026-07-04T12:49:22.5462260Z cloning https://github.com/actions/setup-dotnet to /root/.cache/act/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336
|
||||||
|
2026-07-04T12:49:23.1436838Z Unable to pull refs/heads/v4: worktree contains unstaged changes
|
||||||
|
2026-07-04T12:49:23.1437339Z Cloned https://github.com/actions/setup-dotnet to /root/.cache/act/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336
|
||||||
|
2026-07-04T12:49:23.1569954Z Checked out v4
|
||||||
|
2026-07-04T12:49:23.1781704Z evaluating expression ''
|
||||||
|
2026-07-04T12:49:23.1782188Z expression '' evaluated to 'true'
|
||||||
|
2026-07-04T12:49:23.1782304Z ⭐ Run Main Checkout code
|
||||||
|
2026-07-04T12:49:23.1782488Z Writing entry to tarball workflow/outputcmd.txt len:0
|
||||||
|
2026-07-04T12:49:23.1782639Z Writing entry to tarball workflow/statecmd.txt len:0
|
||||||
|
2026-07-04T12:49:23.1782757Z Writing entry to tarball workflow/pathcmd.txt len:0
|
||||||
|
2026-07-04T12:49:23.1782863Z Writing entry to tarball workflow/envs.txt len:0
|
||||||
|
2026-07-04T12:49:23.1782956Z Writing entry to tarball workflow/SUMMARY.md len:0
|
||||||
|
2026-07-04T12:49:23.1783048Z Extracting content to '/var/run/act'
|
||||||
|
2026-07-04T12:49:23.1829051Z ::group::Run Checkout code
|
||||||
|
2026-07-04T12:49:23.6519171Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
|
||||||
|
2026-07-04T12:49:23.6522622Z Syncing repository: ***/taxbaik
|
||||||
|
2026-07-04T12:49:23.6526573Z ::group::Getting Git version info
|
||||||
|
2026-07-04T12:49:23.6527380Z Working directory is '/workspace/***/taxbaik'
|
||||||
|
2026-07-04T12:49:23.6565904Z [command]/usr/bin/git version
|
||||||
|
2026-07-04T12:49:23.6603817Z git version 2.54.0
|
||||||
|
2026-07-04T12:49:23.6627189Z ::endgroup::
|
||||||
|
2026-07-04T12:49:23.6642333Z Temporarily overriding HOME='/tmp/fee36d59-c0cf-4401-bc9a-d41af1fdbe1b' before making global git config changes
|
||||||
|
2026-07-04T12:49:23.6644026Z Adding repository directory to the temporary git global config as a safe directory
|
||||||
|
2026-07-04T12:49:23.6648194Z [command]/usr/bin/git config --global --add safe.directory /workspace/***/taxbaik
|
||||||
|
2026-07-04T12:49:23.6681379Z Deleting the contents of '/workspace/***/taxbaik'
|
||||||
|
2026-07-04T12:49:23.6686213Z ::group::Initializing the repository
|
||||||
|
2026-07-04T12:49:23.6689751Z [command]/usr/bin/git init /workspace/***/taxbaik
|
||||||
|
2026-07-04T12:49:23.6772415Z hint: Using 'master' as the name for the initial branch. This default branch name
|
||||||
|
2026-07-04T12:49:23.6772830Z hint: will change to "main" in Git 3.0. To configure the initial branch name
|
||||||
|
2026-07-04T12:49:23.6773287Z hint: to use in all of your new repositories, which will suppress this warning,
|
||||||
|
2026-07-04T12:49:23.6773438Z hint: call:
|
||||||
|
2026-07-04T12:49:23.6773518Z hint:
|
||||||
|
2026-07-04T12:49:23.6773598Z hint: git config --global init.defaultBranch <name>
|
||||||
|
2026-07-04T12:49:23.6773696Z hint:
|
||||||
|
2026-07-04T12:49:23.6777666Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
|
||||||
|
2026-07-04T12:49:23.6777914Z hint: 'development'. The just-created branch can be renamed via this command:
|
||||||
|
2026-07-04T12:49:23.6778021Z hint:
|
||||||
|
2026-07-04T12:49:23.6778115Z hint: git branch -m <name>
|
||||||
|
2026-07-04T12:49:23.6778197Z hint:
|
||||||
|
2026-07-04T12:49:23.6778269Z hint: Disable this message with "git config set advice.defaultBranchName false"
|
||||||
|
2026-07-04T12:49:23.6779297Z Initialized empty Git repository in /workspace/***/taxbaik/.git/
|
||||||
|
2026-07-04T12:49:23.6794841Z [command]/usr/bin/git remote add origin http://gitea:3000/***/taxbaik
|
||||||
|
2026-07-04T12:49:23.6829189Z ::endgroup::
|
||||||
|
2026-07-04T12:49:23.6829674Z ::group::Disabling automatic garbage collection
|
||||||
|
2026-07-04T12:49:23.6835147Z [command]/usr/bin/git config --local gc.auto 0
|
||||||
|
2026-07-04T12:49:23.6860980Z ::endgroup::
|
||||||
|
2026-07-04T12:49:23.6861272Z ::group::Setting up auth
|
||||||
|
2026-07-04T12:49:23.6870622Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
|
||||||
|
2026-07-04T12:49:23.6913960Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
|
||||||
|
2026-07-04T12:49:23.7142806Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/gitea\:3000\/\.extraheader
|
||||||
|
2026-07-04T12:49:23.7169599Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/gitea\:3000\/\.extraheader' && git config --local --unset-all 'http.http://gitea:3000/.extraheader' || :"
|
||||||
|
2026-07-04T12:49:23.7403942Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
|
||||||
|
2026-07-04T12:49:23.7444452Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
|
||||||
|
2026-07-04T12:49:23.7675767Z [command]/usr/bin/git config --local http.http://gitea:3000/.extraheader AUTHORIZATION: basic ***
|
||||||
|
2026-07-04T12:49:23.7709656Z ::endgroup::
|
||||||
|
2026-07-04T12:49:23.7709961Z ::group::Fetching the repository
|
||||||
|
2026-07-04T12:49:23.7717271Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +dc86ccfe35d48fec886729841aaf77d2323ffb81:refs/remotes/origin/master
|
||||||
|
2026-07-04T12:49:25.3253215Z From http://gitea:3000/***/taxbaik
|
||||||
|
2026-07-04T12:49:25.3253890Z * [new ref] dc86ccfe35d48fec886729841aaf77d2323ffb81 -> origin/master
|
||||||
|
2026-07-04T12:49:25.3282693Z ::endgroup::
|
||||||
|
2026-07-04T12:49:25.3282925Z ::group::Determining the checkout info
|
||||||
|
2026-07-04T12:49:25.3286679Z ::endgroup::
|
||||||
|
2026-07-04T12:49:25.3292353Z [command]/usr/bin/git sparse-checkout disable
|
||||||
|
2026-07-04T12:49:25.3338153Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
|
||||||
|
2026-07-04T12:49:25.3366870Z ::group::Checking out the ref
|
||||||
|
2026-07-04T12:49:25.3370429Z [command]/usr/bin/git checkout --progress --force -B master refs/remotes/origin/master
|
||||||
|
2026-07-04T12:49:25.8277722Z Reset branch 'master'
|
||||||
|
2026-07-04T12:49:25.8278332Z branch 'master' set up to track 'origin/master'.
|
||||||
|
2026-07-04T12:49:25.8290141Z ::endgroup::
|
||||||
|
2026-07-04T12:49:25.8321715Z [command]/usr/bin/git log -1 --format=%H
|
||||||
|
2026-07-04T12:49:25.8341618Z dc86ccfe35d48fec886729841aaf77d2323ffb81
|
||||||
|
2026-07-04T12:49:25.8359179Z ::remove-matcher owner=checkout-git::
|
||||||
|
2026-07-04T12:49:25.8473361Z ::endgroup::
|
||||||
|
2026-07-04T12:49:25.8925201Z ::group::Run Setup .NET
|
||||||
|
2026-07-04T12:49:25.8925719Z with:
|
||||||
|
2026-07-04T12:49:25.8925850Z dotnet-version: 10.0
|
||||||
|
2026-07-04T12:49:26.4364417Z (node:142) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
||||||
|
2026-07-04T12:49:26.4365135Z (Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
2026-07-04T12:49:26.4415455Z [command]/run/act/actions/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336/externals/install-dotnet.sh --skip-non-versioned-files --runtime dotnet --channel LTS
|
||||||
|
2026-07-04T12:49:26.7591141Z dotnet-install: Attempting to download using aka.ms link https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.9/dotnet-runtime-10.0.9-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:49:26.9823877Z dotnet-install: Remote file https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.9/dotnet-runtime-10.0.9-linux-x64.tar.gz size is 36606251 bytes.
|
||||||
|
2026-07-04T12:49:26.9830977Z dotnet-install: Extracting archive from https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.9/dotnet-runtime-10.0.9-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:49:27.7850437Z dotnet-install: Downloaded file size is 36606251 bytes.
|
||||||
|
2026-07-04T12:49:27.7851130Z dotnet-install: The remote and local file sizes are equal.
|
||||||
|
2026-07-04T12:49:27.8443797Z dotnet-install: Installed version is 10.0.9
|
||||||
|
2026-07-04T12:49:27.8494627Z dotnet-install: Adding to current process PATH: `/usr/share/dotnet`. Note: This change will be visible only when sourcing script.
|
||||||
|
2026-07-04T12:49:27.8495003Z dotnet-install: Note that the script does not resolve dependencies during installation.
|
||||||
|
2026-07-04T12:49:27.8495163Z dotnet-install: To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the "Dependencies" section.
|
||||||
|
2026-07-04T12:49:27.8495299Z dotnet-install: Installation finished successfully.
|
||||||
|
2026-07-04T12:49:27.8526885Z [command]/run/act/actions/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336/externals/install-dotnet.sh --skip-non-versioned-files --channel 10.0
|
||||||
|
2026-07-04T12:49:28.1718954Z dotnet-install: Attempting to download using aka.ms link https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.301/dotnet-sdk-10.0.301-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:49:28.9847774Z dotnet-install: Remote file https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.301/dotnet-sdk-10.0.301-linux-x64.tar.gz size is 235086718 bytes.
|
||||||
|
2026-07-04T12:49:28.9854648Z dotnet-install: Extracting archive from https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.301/dotnet-sdk-10.0.301-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:49:34.5345102Z dotnet-install: Downloaded file size is 235086718 bytes.
|
||||||
|
2026-07-04T12:49:34.5347250Z dotnet-install: The remote and local file sizes are equal.
|
||||||
|
2026-07-04T12:49:34.7378875Z dotnet-install: Installed version is 10.0.301
|
||||||
|
2026-07-04T12:49:34.7438030Z dotnet-install: Adding to current process PATH: `/usr/share/dotnet`. Note: This change will be visible only when sourcing script.
|
||||||
|
2026-07-04T12:49:34.7441902Z dotnet-install: Note that the script does not resolve dependencies during installation.
|
||||||
|
2026-07-04T12:49:34.7442410Z dotnet-install: To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the "Dependencies" section.
|
||||||
|
2026-07-04T12:49:34.7442569Z dotnet-install: Installation finished successfully.
|
||||||
|
2026-07-04T12:49:34.7473572Z ##[add-matcher]/run/act/actions/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336/.github/csc.json
|
||||||
|
2026-07-04T12:49:34.7575673Z ::endgroup::
|
||||||
|
2026-07-04T12:49:34.8690239Z ::group::Run dotnet restore src/TaxBaik.sln
|
||||||
|
2026-07-04T12:49:34.8690735Z dotnet restore src/TaxBaik.sln
|
||||||
|
2026-07-04T12:49:34.8691061Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:49:34.8691248Z ::endgroup::
|
||||||
|
2026-07-04T12:49:35.0741950Z
|
||||||
|
2026-07-04T12:49:35.0749035Z Welcome to .NET 10.0!
|
||||||
|
2026-07-04T12:49:35.0749790Z ---------------------
|
||||||
|
2026-07-04T12:49:35.0750163Z SDK Version: 10.0.301
|
||||||
|
2026-07-04T12:49:35.0750267Z
|
||||||
|
2026-07-04T12:49:35.0750367Z Telemetry
|
||||||
|
2026-07-04T12:49:35.0750443Z ---------
|
||||||
|
2026-07-04T12:49:35.0750707Z The .NET tools collect usage data in order to help us improve your experience. It is collected by Microsoft and shared with the community. You can opt-out of telemetry by setting the DOTNET_CLI_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell.
|
||||||
|
2026-07-04T12:49:35.0751163Z
|
||||||
|
2026-07-04T12:49:35.0751291Z Read more about .NET CLI Tools telemetry: https://aka.ms/dotnet-cli-telemetry
|
||||||
|
2026-07-04T12:49:35.6068528Z
|
||||||
|
2026-07-04T12:49:35.6070150Z ----------------
|
||||||
|
2026-07-04T12:49:35.6070372Z Installed an ASP.NET Core HTTPS development certificate.
|
||||||
|
2026-07-04T12:49:35.6072186Z To trust the certificate, run 'dotnet dev-certs https --trust'
|
||||||
|
2026-07-04T12:49:35.6076372Z Learn about HTTPS: https://aka.ms/dotnet-https
|
||||||
|
2026-07-04T12:49:35.6076628Z
|
||||||
|
2026-07-04T12:49:35.6076758Z ----------------
|
||||||
|
2026-07-04T12:49:35.6076846Z Write your first app: https://aka.ms/dotnet-hello-world
|
||||||
|
2026-07-04T12:49:35.6076954Z Find out what's new: https://aka.ms/dotnet-whats-new
|
||||||
|
2026-07-04T12:49:35.6077049Z Explore documentation: https://aka.ms/dotnet-docs
|
||||||
|
2026-07-04T12:49:35.6077169Z Report issues and find source on GitHub: https://github.com/dotnet/core
|
||||||
|
2026-07-04T12:49:35.6077278Z Use 'dotnet --help' to see available commands or visit: https://aka.ms/dotnet-cli
|
||||||
|
2026-07-04T12:49:35.6077474Z --------------------------------------------------------------------------------------
|
||||||
|
2026-07-04T12:49:36.5161335Z Determining projects to restore...
|
||||||
|
2026-07-04T12:49:37.2639927Z Restored /workspace/***/taxbaik/src/TaxBaik.Domain/TaxBaik.Domain.csproj (in 114 ms).
|
||||||
|
2026-07-04T12:49:38.6108539Z Restored /workspace/***/taxbaik/src/TaxBaik.Application/TaxBaik.Application.csproj (in 1.5 sec).
|
||||||
|
2026-07-04T12:49:39.8969495Z Restored /workspace/***/taxbaik/src/TaxBaik.Web/TaxBaik.Web.csproj (in 1.27 sec).
|
||||||
|
2026-07-04T12:49:41.1437758Z Restored /workspace/***/taxbaik/src/TaxBaik.Application.Tests/TaxBaik.Application.Tests.csproj (in 3.86 sec).
|
||||||
|
2026-07-04T12:49:41.2503876Z Restored /workspace/***/taxbaik/src/TaxBaik.Infrastructure/TaxBaik.Infrastructure.csproj (in 30 ms).
|
||||||
|
2026-07-04T12:49:41.8107150Z Restored /workspace/***/taxbaik/src/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj (in 1.74 sec).
|
||||||
|
2026-07-04T12:49:41.9707509Z ::group::Run dotnet build src/TaxBaik.sln -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:49:41.9707880Z dotnet build src/TaxBaik.sln -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:49:41.9708017Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:49:41.9708125Z ::endgroup::
|
||||||
|
2026-07-04T12:49:45.3296980Z TaxBaik.Domain -> /workspace/***/taxbaik/src/TaxBaik.Domain/bin/Release/net10.0/TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:49:47.7710543Z TaxBaik.Application -> /workspace/***/taxbaik/src/TaxBaik.Application/bin/Release/net10.0/TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:49:48.6747737Z TaxBaik.Infrastructure -> /workspace/***/taxbaik/src/TaxBaik.Infrastructure/bin/Release/net10.0/TaxBaik.Infrastructure.dll
|
||||||
|
2026-07-04T12:50:08.6007977Z TaxBaik.Web.Client -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Debug/net10.0/TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:50:08.6107202Z TaxBaik.Web.Client (Blazor output) -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Debug/net10.0/wwwroot
|
||||||
|
2026-07-04T12:50:19.3402859Z TaxBaik.Web -> /workspace/***/taxbaik/src/TaxBaik.Web/bin/Release/net10.0/TaxBaik.Web.dll
|
||||||
|
2026-07-04T12:50:20.2847876Z TaxBaik.Application.Tests -> /workspace/***/taxbaik/src/TaxBaik.Application.Tests/bin/Release/net10.0/TaxBaik.Application.Tests.dll
|
||||||
|
2026-07-04T12:50:20.3269626Z
|
||||||
|
2026-07-04T12:50:20.3279927Z Build succeeded.
|
||||||
|
2026-07-04T12:50:20.3283223Z 0 Warning(s)
|
||||||
|
2026-07-04T12:50:20.3284820Z 0 Error(s)
|
||||||
|
2026-07-04T12:50:20.3287249Z
|
||||||
|
2026-07-04T12:50:20.3288831Z Time Elapsed 00:00:38.09
|
||||||
|
2026-07-04T12:50:20.4838413Z ::group::Run dotnet test src/TaxBaik.sln -c Release --no-build
|
||||||
|
2026-07-04T12:50:20.4839066Z dotnet test src/TaxBaik.sln -c Release --no-build
|
||||||
|
2026-07-04T12:50:20.4839241Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:50:20.4839359Z ::endgroup::
|
||||||
|
2026-07-04T12:50:23.9713693Z Test run for /workspace/***/taxbaik/src/TaxBaik.Application.Tests/bin/Release/net10.0/TaxBaik.Application.Tests.dll (.NETCoreApp,Version=v10.0)
|
||||||
|
2026-07-04T12:50:24.7096642Z A total of 1 test files matched the specified pattern.
|
||||||
|
2026-07-04T12:50:30.0285340Z
|
||||||
|
2026-07-04T12:50:30.0898162Z Passed! - Failed: 0, Passed: 26, Skipped: 0, Total: 26, Duration: 990 ms - TaxBaik.Application.Tests.dll (net10.0)
|
||||||
|
2026-07-04T12:50:30.3809977Z ::group::Run set -e
|
||||||
|
2026-07-04T12:50:30.3810322Z set -e
|
||||||
|
2026-07-04T12:50:30.3810444Z mkdir -p ./publish-logs
|
||||||
|
2026-07-04T12:50:30.3810532Z web_log="./publish-logs/publish-web.log"
|
||||||
|
2026-07-04T12:50:30.3810628Z start=$(date +%s)
|
||||||
|
2026-07-04T12:50:30.3810710Z # Web.Client needs a Release static-web-assets manifest for Web publish.
|
||||||
|
2026-07-04T12:50:30.3810972Z # Build it explicitly so publish can reuse the prepared outputs.
|
||||||
|
2026-07-04T12:50:30.3811068Z dotnet build src/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:50:30.3811171Z # Build the Web host in Release as well so publish has the same inputs
|
||||||
|
2026-07-04T12:50:30.3811261Z # the server uses in production.
|
||||||
|
2026-07-04T12:50:30.3811343Z dotnet build src/TaxBaik.Web/TaxBaik.Web.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:50:30.3811442Z echo "--- Web.Client Release artifacts ---"
|
||||||
|
2026-07-04T12:50:30.3811529Z ls -la src/TaxBaik.Web.Client/bin/Release/net10.0 || true
|
||||||
|
2026-07-04T12:50:30.3811622Z ls -la src/TaxBaik.Web.Client/obj/Release/net10.0 || true
|
||||||
|
2026-07-04T12:50:30.3811701Z if ! dotnet publish src/TaxBaik.Web/ \
|
||||||
|
2026-07-04T12:50:30.3811809Z -c Release \
|
||||||
|
2026-07-04T12:50:30.3811886Z -o ./publish \
|
||||||
|
2026-07-04T12:50:30.3811968Z --no-restore \
|
||||||
|
2026-07-04T12:50:30.3812042Z -p:SelfContained=false \
|
||||||
|
2026-07-04T12:50:30.3812116Z -p:PublishReadyToRun=false \
|
||||||
|
2026-07-04T12:50:30.3812197Z -p:PerformanceSummary=true \
|
||||||
|
2026-07-04T12:50:30.3812269Z -clp:Summary \
|
||||||
|
2026-07-04T12:50:30.3812338Z -bl:"./publish-logs/publish-web.binlog" >"$web_log" 2>&1; then
|
||||||
|
2026-07-04T12:50:30.3812451Z echo "=== Publish Web failed; tailing log ==="
|
||||||
|
2026-07-04T12:50:30.3812603Z tail -n 120 "$web_log" || true
|
||||||
|
2026-07-04T12:50:30.3812747Z exit 1
|
||||||
|
2026-07-04T12:50:30.3812836Z fi
|
||||||
|
2026-07-04T12:50:30.3812917Z end=$(date +%s)
|
||||||
|
2026-07-04T12:50:30.3813009Z echo "✓ Publish Web elapsed: $((end - start))s"
|
||||||
|
2026-07-04T12:50:30.3813102Z ls -lh ./publish-logs/publish-web.binlog
|
||||||
|
2026-07-04T12:50:30.3813186Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:50:30.3813283Z ::endgroup::
|
||||||
|
2026-07-04T12:50:32.0488223Z TaxBaik.Domain -> /workspace/***/taxbaik/src/TaxBaik.Domain/bin/Release/net10.0/TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:50:32.3508048Z TaxBaik.Application -> /workspace/***/taxbaik/src/TaxBaik.Application/bin/Release/net10.0/TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:50:44.4958268Z TaxBaik.Web.Client -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:50:44.4978125Z TaxBaik.Web.Client (Blazor output) -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/wwwroot
|
||||||
|
2026-07-04T12:50:44.5536984Z
|
||||||
|
2026-07-04T12:50:44.5540603Z Build succeeded.
|
||||||
|
2026-07-04T12:50:44.5541781Z 0 Warning(s)
|
||||||
|
2026-07-04T12:50:44.5557450Z 0 Error(s)
|
||||||
|
2026-07-04T12:50:44.5559019Z
|
||||||
|
2026-07-04T12:50:44.5567098Z Time Elapsed 00:00:13.64
|
||||||
|
2026-07-04T12:50:46.1657100Z TaxBaik.Domain -> /workspace/***/taxbaik/src/TaxBaik.Domain/bin/Release/net10.0/TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:50:46.2447793Z TaxBaik.Infrastructure -> /workspace/***/taxbaik/src/TaxBaik.Infrastructure/bin/Release/net10.0/TaxBaik.Infrastructure.dll
|
||||||
|
2026-07-04T12:50:46.5259148Z TaxBaik.Application -> /workspace/***/taxbaik/src/TaxBaik.Application/bin/Release/net10.0/TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:50:48.5388949Z TaxBaik.Web.Client -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:50:48.5397206Z TaxBaik.Web.Client (Blazor output) -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/wwwroot
|
||||||
|
2026-07-04T12:50:51.6865226Z TaxBaik.Web -> /workspace/***/taxbaik/src/TaxBaik.Web/bin/Release/net10.0/TaxBaik.Web.dll
|
||||||
|
2026-07-04T12:50:51.7325224Z
|
||||||
|
2026-07-04T12:50:51.7335278Z Build succeeded.
|
||||||
|
2026-07-04T12:50:51.7356860Z 0 Warning(s)
|
||||||
|
2026-07-04T12:50:51.7360102Z 0 Error(s)
|
||||||
|
2026-07-04T12:50:51.7364579Z
|
||||||
|
2026-07-04T12:50:51.7376644Z Time Elapsed 00:00:06.73
|
||||||
|
2026-07-04T12:50:51.7670469Z --- Web.Client Release artifacts ---
|
||||||
|
2026-07-04T12:50:51.7699768Z total 42056
|
||||||
|
2026-07-04T12:50:51.7700066Z drwxr-xr-x 3 root root 20480 Jul 4 12:50 .
|
||||||
|
2026-07-04T12:50:51.7700191Z drwxr-xr-x 3 root root 4096 Jul 4 12:50 ..
|
||||||
|
2026-07-04T12:50:51.7700291Z -rwxr--r-- 1 root root 55632 May 20 20:19 Microsoft.AspNetCore.Authorization.dll
|
||||||
|
2026-07-04T12:50:51.7700420Z -rwxr--r-- 1 root root 34128 May 20 20:19 Microsoft.AspNetCore.Components.Authorization.dll
|
||||||
|
2026-07-04T12:50:51.7700590Z -rwxr--r-- 1 root root 47952 May 20 20:19 Microsoft.AspNetCore.Components.Forms.dll
|
||||||
|
2026-07-04T12:50:51.7700682Z -rwxr--r-- 1 root root 189264 May 20 20:19 Microsoft.AspNetCore.Components.Web.dll
|
||||||
|
2026-07-04T12:50:51.7700950Z -rwxr--r-- 1 root root 166736 May 20 20:20 Microsoft.AspNetCore.Components.WebAssembly.dll
|
||||||
|
2026-07-04T12:50:51.7701048Z -rwxr--r-- 1 root root 399184 May 20 20:19 Microsoft.AspNetCore.Components.dll
|
||||||
|
2026-07-04T12:50:51.7701131Z -rwxr--r-- 1 root root 16208 May 20 20:18 Microsoft.AspNetCore.Metadata.dll
|
||||||
|
2026-07-04T12:50:51.7701220Z -rwxr--r-- 1 root root 19248 Dec 12 2025 Microsoft.Bcl.Cryptography.dll
|
||||||
|
2026-07-04T12:50:51.7701312Z -rwxr--r-- 1 root root 311632 May 20 18:30 Microsoft.CSharp.dll
|
||||||
|
2026-07-04T12:50:51.7701416Z -rwxr--r-- 1 root root 38192 Oct 24 2025 Microsoft.Extensions.Caching.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7701508Z -rwxr--r-- 1 root root 28496 May 20 19:29 Microsoft.Extensions.Configuration.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7701618Z -rwxr--r-- 1 root root 43344 May 20 19:32 Microsoft.Extensions.Configuration.Binder.dll
|
||||||
|
2026-07-04T12:50:51.7701708Z -rwxr--r-- 1 root root 28496 May 20 19:34 Microsoft.Extensions.Configuration.FileExtensions.dll
|
||||||
|
2026-07-04T12:50:51.7701813Z -rwxr--r-- 1 root root 27984 May 20 19:36 Microsoft.Extensions.Configuration.Json.dll
|
||||||
|
2026-07-04T12:50:51.7701918Z -rwxr--r-- 1 root root 44880 May 20 19:29 Microsoft.Extensions.Configuration.dll
|
||||||
|
2026-07-04T12:50:51.7702004Z -rwxr--r-- 1 root root 65872 May 20 19:29 Microsoft.Extensions.DependencyInjection.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7702097Z -rwxr--r-- 1 root root 95568 May 20 19:30 Microsoft.Extensions.DependencyInjection.dll
|
||||||
|
2026-07-04T12:50:51.7702194Z -rwxr--r-- 1 root root 31056 May 20 19:32 Microsoft.Extensions.Diagnostics.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7702288Z -rwxr--r-- 1 root root 36176 May 20 19:34 Microsoft.Extensions.Diagnostics.dll
|
||||||
|
2026-07-04T12:50:51.7702374Z -rwxr--r-- 1 root root 23376 May 20 19:29 Microsoft.Extensions.FileProviders.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7702458Z -rwxr--r-- 1 root root 45392 May 20 19:33 Microsoft.Extensions.FileProviders.Physical.dll
|
||||||
|
2026-07-04T12:50:51.7702562Z -rwxr--r-- 1 root root 47952 May 20 19:30 Microsoft.Extensions.FileSystemGlobbing.dll
|
||||||
|
2026-07-04T12:50:51.7702655Z -rwxr--r-- 1 root root 93008 May 20 19:37 Microsoft.Extensions.Http.dll
|
||||||
|
2026-07-04T12:50:51.7702760Z -rwxr--r-- 1 root root 19576 Mar 25 2023 Microsoft.Extensions.Localization.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7702853Z -rwxr--r-- 1 root root 31872 Mar 25 2023 Microsoft.Extensions.Localization.dll
|
||||||
|
2026-07-04T12:50:51.7702942Z -rwxr--r-- 1 root root 66896 May 20 19:29 Microsoft.Extensions.Logging.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7703029Z -rwxr--r-- 1 root root 51536 May 20 19:33 Microsoft.Extensions.Logging.dll
|
||||||
|
2026-07-04T12:50:51.7703111Z -rwxr--r-- 1 root root 21840 May 20 19:33 Microsoft.Extensions.Options.ConfigurationExtensions.dll
|
||||||
|
2026-07-04T12:50:51.7703206Z -rwxr--r-- 1 root root 65360 May 20 19:30 Microsoft.Extensions.Options.dll
|
||||||
|
2026-07-04T12:50:51.7703314Z -rwxr--r-- 1 root root 44880 May 20 19:29 Microsoft.Extensions.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7703426Z -rwxr--r-- 1 root root 43344 May 20 20:19 Microsoft.Extensions.Validation.dll
|
||||||
|
2026-07-04T12:50:51.7703510Z -rwxr--r-- 1 root root 19296 Jun 2 20:51 Microsoft.IdentityModel.Abstractions.dll
|
||||||
|
2026-07-04T12:50:51.7703597Z -rwxr--r-- 1 root root 172856 Jun 2 20:52 Microsoft.IdentityModel.JsonWebTokens.dll
|
||||||
|
2026-07-04T12:50:51.7703683Z -rwxr--r-- 1 root root 38200 Jun 2 20:51 Microsoft.IdentityModel.Logging.dll
|
||||||
|
2026-07-04T12:50:51.7703764Z -rwxr--r-- 1 root root 407352 Jun 2 20:51 Microsoft.IdentityModel.Tokens.dll
|
||||||
|
2026-07-04T12:50:51.7703844Z -rwxr--r-- 1 root root 24912 May 20 20:20 Microsoft.JSInterop.WebAssembly.dll
|
||||||
|
2026-07-04T12:50:51.7703924Z -rwxr--r-- 1 root root 75088 May 20 20:19 Microsoft.JSInterop.dll
|
||||||
|
2026-07-04T12:50:51.7704012Z -rwxr--r-- 1 root root 428880 May 20 18:30 Microsoft.VisualBasic.Core.dll
|
||||||
|
2026-07-04T12:50:51.7704101Z -rwxr--r-- 1 root root 17232 May 20 18:31 Microsoft.VisualBasic.dll
|
||||||
|
2026-07-04T12:50:51.7704182Z -rwxr--r-- 1 root root 15696 May 20 18:29 Microsoft.Win32.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7704272Z -rwxr--r-- 1 root root 33104 May 20 18:29 Microsoft.Win32.Registry.dll
|
||||||
|
2026-07-04T12:50:51.7704425Z -rwxr--r-- 1 root root 9108480 Sep 14 2023 MudBlazor.dll
|
||||||
|
2026-07-04T12:50:51.7704514Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.AppContext.dll
|
||||||
|
2026-07-04T12:50:51.7704608Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Buffers.dll
|
||||||
|
2026-07-04T12:50:51.7704698Z -rwxr--r-- 1 root root 88912 May 20 18:29 System.Collections.Concurrent.dll
|
||||||
|
2026-07-04T12:50:51.7704789Z -rwxr--r-- 1 root root 251216 May 20 18:29 System.Collections.Immutable.dll
|
||||||
|
2026-07-04T12:50:51.7704875Z -rwxr--r-- 1 root root 47952 May 20 18:29 System.Collections.NonGeneric.dll
|
||||||
|
2026-07-04T12:50:51.7704966Z -rwxr--r-- 1 root root 47952 May 20 18:29 System.Collections.Specialized.dll
|
||||||
|
2026-07-04T12:50:51.7707333Z -rwxr--r-- 1 root root 112976 May 20 18:28 System.Collections.dll
|
||||||
|
2026-07-04T12:50:51.7707687Z -rwxr--r-- 1 root root 102736 May 20 18:31 System.ComponentModel.Annotations.dll
|
||||||
|
2026-07-04T12:50:51.7708035Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.ComponentModel.DataAnnotations.dll
|
||||||
|
2026-07-04T12:50:51.7708335Z -rwxr--r-- 1 root root 26448 May 20 18:29 System.ComponentModel.EventBasedAsync.dll
|
||||||
|
2026-07-04T12:50:51.7708514Z -rwxr--r-- 1 root root 42320 May 20 18:29 System.ComponentModel.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7708690Z -rwxr--r-- 1 root root 316752 May 20 18:31 System.ComponentModel.TypeConverter.dll
|
||||||
|
2026-07-04T12:50:51.7708912Z -rwxr--r-- 1 root root 16208 May 20 18:29 System.ComponentModel.dll
|
||||||
|
2026-07-04T12:50:51.7709087Z -rwxr--r-- 1 root root 19280 May 20 18:31 System.Configuration.dll
|
||||||
|
2026-07-04T12:50:51.7709258Z -rwxr--r-- 1 root root 54096 May 20 18:31 System.Console.dll
|
||||||
|
2026-07-04T12:50:51.7709460Z -rwxr--r-- 1 root root 23376 May 20 18:31 System.Core.dll
|
||||||
|
2026-07-04T12:50:51.7709630Z -rwxr--r-- 1 root root 1018192 May 20 18:31 System.Data.Common.dll
|
||||||
|
2026-07-04T12:50:51.7709804Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Data.DataSetExtensions.dll
|
||||||
|
2026-07-04T12:50:51.7709982Z -rwxr--r-- 1 root root 25424 May 20 18:31 System.Data.dll
|
||||||
|
2026-07-04T12:50:51.7710183Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Diagnostics.Contracts.dll
|
||||||
|
2026-07-04T12:50:51.7710361Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Diagnostics.Debug.dll
|
||||||
|
2026-07-04T12:50:51.7710524Z -rwxr--r-- 1 root root 202576 May 20 18:29 System.Diagnostics.DiagnosticSource.dll
|
||||||
|
2026-07-04T12:50:51.7710895Z -rwxr--r-- 1 root root 22864 May 20 18:29 System.Diagnostics.FileVersionInfo.dll
|
||||||
|
2026-07-04T12:50:51.7711109Z -rwxr--r-- 1 root root 56656 May 20 18:29 System.Diagnostics.Process.dll
|
||||||
|
2026-07-04T12:50:51.7711275Z -rwxr--r-- 1 root root 25936 May 20 18:29 System.Diagnostics.StackTrace.dll
|
||||||
|
2026-07-04T12:50:51.7711461Z -rwxr--r-- 1 root root 31568 May 20 18:31 System.Diagnostics.TextWriterTraceListener.dll
|
||||||
|
2026-07-04T12:50:51.7711697Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Diagnostics.Tools.dll
|
||||||
|
2026-07-04T12:50:51.7711883Z -rwxr--r-- 1 root root 58704 May 20 18:29 System.Diagnostics.TraceSource.dll
|
||||||
|
2026-07-04T12:50:51.7712056Z -rwxr--r-- 1 root root 16208 May 20 18:28 System.Diagnostics.Tracing.dll
|
||||||
|
2026-07-04T12:50:51.7712283Z -rwxr--r-- 1 root root 64848 May 20 18:29 System.Drawing.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7712465Z -rwxr--r-- 1 root root 20304 May 20 18:31 System.Drawing.dll
|
||||||
|
2026-07-04T12:50:51.7712648Z -rwxr--r-- 1 root root 16208 May 20 18:30 System.Dynamic.Runtime.dll
|
||||||
|
2026-07-04T12:50:51.7712825Z -rwxr--r-- 1 root root 97104 May 20 18:29 System.Formats.Asn1.dll
|
||||||
|
2026-07-04T12:50:51.7713000Z -rwxr--r-- 1 root root 38736 May 20 18:29 System.Formats.Tar.dll
|
||||||
|
2026-07-04T12:50:51.7713087Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Globalization.Calendars.dll
|
||||||
|
2026-07-04T12:50:51.7713176Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Globalization.Extensions.dll
|
||||||
|
2026-07-04T12:50:51.7713291Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Globalization.dll
|
||||||
|
2026-07-04T12:50:51.7713387Z -rwxr--r-- 1 root root 28496 May 20 18:29 System.IO.Compression.Brotli.dll
|
||||||
|
2026-07-04T12:50:51.7713486Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.IO.Compression.FileSystem.dll
|
||||||
|
2026-07-04T12:50:51.7713573Z -rwxr--r-- 1 root root 53584 May 20 18:29 System.IO.Compression.ZipFile.dll
|
||||||
|
2026-07-04T12:50:51.7713654Z -rwxr--r-- 1 root root 167760 May 20 18:31 System.IO.Compression.dll
|
||||||
|
2026-07-04T12:50:51.7713736Z -rwxr--r-- 1 root root 32080 May 20 18:29 System.IO.FileSystem.AccessControl.dll
|
||||||
|
2026-07-04T12:50:51.7713827Z -rwxr--r-- 1 root root 23888 May 20 18:29 System.IO.FileSystem.DriveInfo.dll
|
||||||
|
2026-07-04T12:50:51.7713926Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.IO.FileSystem.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7714013Z -rwxr--r-- 1 root root 33104 May 20 18:29 System.IO.FileSystem.Watcher.dll
|
||||||
|
2026-07-04T12:50:51.7714096Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.IO.FileSystem.dll
|
||||||
|
2026-07-04T12:50:51.7714178Z -rwxr--r-- 1 root root 35152 May 20 18:30 System.IO.IsolatedStorage.dll
|
||||||
|
2026-07-04T12:50:51.7714259Z -rwxr--r-- 1 root root 50000 May 20 18:31 System.IO.MemoryMappedFiles.dll
|
||||||
|
2026-07-04T12:50:51.7714344Z -rwxr--r-- 1 root root 78160 May 20 18:29 System.IO.Pipelines.dll
|
||||||
|
2026-07-04T12:50:51.7714423Z -rwxr--r-- 1 root root 23376 May 20 18:29 System.IO.Pipes.AccessControl.dll
|
||||||
|
2026-07-04T12:50:51.7714513Z -rwxr--r-- 1 root root 42320 May 20 18:29 System.IO.Pipes.dll
|
||||||
|
2026-07-04T12:50:51.7714598Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.IO.UnmanagedMemoryStream.dll
|
||||||
|
2026-07-04T12:50:51.7714682Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.IO.dll
|
||||||
|
2026-07-04T12:50:51.7714764Z -rwxr--r-- 1 root root 92000 Jun 2 20:51 System.IdentityModel.Tokens.Jwt.dll
|
||||||
|
2026-07-04T12:50:51.7714848Z -rwxr--r-- 1 root root 456528 May 20 18:29 System.Linq.AsyncEnumerable.dll
|
||||||
|
2026-07-04T12:50:51.7714934Z -rwxr--r-- 1 root root 575312 May 20 18:29 System.Linq.Expressions.dll
|
||||||
|
2026-07-04T12:50:51.7715013Z -rwxr--r-- 1 root root 223056 May 20 18:31 System.Linq.Parallel.dll
|
||||||
|
2026-07-04T12:50:51.7715099Z -rwxr--r-- 1 root root 78672 May 20 18:31 System.Linq.Queryable.dll
|
||||||
|
2026-07-04T12:50:51.7715179Z -rwxr--r-- 1 root root 201040 May 20 18:29 System.Linq.dll
|
||||||
|
2026-07-04T12:50:51.7715280Z -rwxr--r-- 1 root root 55632 May 20 18:28 System.Memory.dll
|
||||||
|
2026-07-04T12:50:51.7715363Z -rwxr--r-- 1 root root 56144 May 20 18:30 System.Net.Http.Json.dll
|
||||||
|
2026-07-04T12:50:51.7715443Z -rwxr--r-- 1 root root 296272 May 20 18:31 System.Net.Http.dll
|
||||||
|
2026-07-04T12:50:51.7724789Z -rwxr--r-- 1 root root 56144 May 20 18:30 System.Net.HttpListener.dll
|
||||||
|
2026-07-04T12:50:51.7725003Z -rwxr--r-- 1 root root 105296 May 20 18:31 System.Net.Mail.dll
|
||||||
|
2026-07-04T12:50:51.7725118Z -rwxr--r-- 1 root root 24400 May 20 18:31 System.Net.NameResolution.dll
|
||||||
|
2026-07-04T12:50:51.7725378Z -rwxr--r-- 1 root root 42320 May 20 18:29 System.Net.NetworkInformation.dll
|
||||||
|
2026-07-04T12:50:51.7725485Z -rwxr--r-- 1 root root 27984 May 20 18:29 System.Net.Ping.dll
|
||||||
|
2026-07-04T12:50:51.7725575Z -rwxr--r-- 1 root root 107344 May 20 18:31 System.Net.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7725659Z -rwxr--r-- 1 root root 39248 May 20 18:30 System.Net.Quic.dll
|
||||||
|
2026-07-04T12:50:51.7725741Z -rwxr--r-- 1 root root 65872 May 20 18:30 System.Net.Requests.dll
|
||||||
|
2026-07-04T12:50:51.7725821Z -rwxr--r-- 1 root root 114512 May 20 18:30 System.Net.Security.dll
|
||||||
|
2026-07-04T12:50:51.7725899Z -rwxr--r-- 1 root root 40784 May 20 18:29 System.Net.ServerSentEvents.dll
|
||||||
|
2026-07-04T12:50:51.7725997Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Net.ServicePoint.dll
|
||||||
|
2026-07-04T12:50:51.7726207Z -rwxr--r-- 1 root root 74576 May 20 18:29 System.Net.Sockets.dll
|
||||||
|
2026-07-04T12:50:51.7726297Z -rwxr--r-- 1 root root 56144 May 20 18:31 System.Net.WebClient.dll
|
||||||
|
2026-07-04T12:50:51.7726382Z -rwxr--r-- 1 root root 33104 May 20 18:29 System.Net.WebHeaderCollection.dll
|
||||||
|
2026-07-04T12:50:51.7726473Z -rwxr--r-- 1 root root 21840 May 20 18:31 System.Net.WebProxy.dll
|
||||||
|
2026-07-04T12:50:51.7726565Z -rwxr--r-- 1 root root 52560 May 20 18:31 System.Net.WebSockets.Client.dll
|
||||||
|
2026-07-04T12:50:51.7726651Z -rwxr--r-- 1 root root 108880 May 20 18:31 System.Net.WebSockets.dll
|
||||||
|
2026-07-04T12:50:51.7726745Z -rwxr--r-- 1 root root 17232 May 20 18:31 System.Net.dll
|
||||||
|
2026-07-04T12:50:51.7726828Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Numerics.Vectors.dll
|
||||||
|
2026-07-04T12:50:51.7726973Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Numerics.dll
|
||||||
|
2026-07-04T12:50:51.7727145Z -rwxr--r-- 1 root root 41296 May 20 18:29 System.ObjectModel.dll
|
||||||
|
2026-07-04T12:50:51.7727300Z -rwxr--r-- 1 root root 4880208 May 20 18:19 System.Private.CoreLib.dll
|
||||||
|
2026-07-04T12:50:51.7727457Z -rwxr--r-- 1 root root 859472 May 20 18:31 System.Private.DataContractSerialization.dll
|
||||||
|
2026-07-04T12:50:51.7727613Z -rwxr--r-- 1 root root 105808 May 20 18:28 System.Private.Uri.dll
|
||||||
|
2026-07-04T12:50:51.7727769Z -rwxr--r-- 1 root root 153936 May 20 18:30 System.Private.Xml.Linq.dll
|
||||||
|
2026-07-04T12:50:51.7727929Z -rwxr--r-- 1 root root 3106128 May 20 18:30 System.Private.Xml.dll
|
||||||
|
2026-07-04T12:50:51.7728079Z -rwxr--r-- 1 root root 38224 May 20 18:31 System.Reflection.DispatchProxy.dll
|
||||||
|
2026-07-04T12:50:51.7728248Z -rwxr--r-- 1 root root 15696 May 20 18:29 System.Reflection.Emit.ILGeneration.dll
|
||||||
|
2026-07-04T12:50:51.7728406Z -rwxr--r-- 1 root root 15696 May 20 18:29 System.Reflection.Emit.Lightweight.dll
|
||||||
|
2026-07-04T12:50:51.7728565Z -rwxr--r-- 1 root root 133456 May 20 18:29 System.Reflection.Emit.dll
|
||||||
|
2026-07-04T12:50:51.7728728Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Reflection.Extensions.dll
|
||||||
|
2026-07-04T12:50:51.7728907Z -rwxr--r-- 1 root root 503632 May 20 18:29 System.Reflection.Metadata.dll
|
||||||
|
2026-07-04T12:50:51.7729052Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Reflection.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7729143Z -rwxr--r-- 1 root root 24400 May 20 18:31 System.Reflection.TypeExtensions.dll
|
||||||
|
2026-07-04T12:50:51.7729244Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Reflection.dll
|
||||||
|
2026-07-04T12:50:51.7729337Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Resources.Reader.dll
|
||||||
|
2026-07-04T12:50:51.7729450Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Resources.ResourceManager.dll
|
||||||
|
2026-07-04T12:50:51.7729588Z -rwxr--r-- 1 root root 26960 May 20 18:31 System.Resources.Writer.dll
|
||||||
|
2026-07-04T12:50:51.7729711Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Runtime.CompilerServices.Unsafe.dll
|
||||||
|
2026-07-04T12:50:51.7729802Z -rwxr--r-- 1 root root 17232 May 20 18:30 System.Runtime.CompilerServices.VisualC.dll
|
||||||
|
2026-07-04T12:50:51.7729888Z -rwxr--r-- 1 root root 17744 May 20 18:30 System.Runtime.Extensions.dll
|
||||||
|
2026-07-04T12:50:51.7729981Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Runtime.Handles.dll
|
||||||
|
2026-07-04T12:50:51.7730089Z -rwxr--r-- 1 root root 89936 May 20 18:31 System.Runtime.InteropServices.JavaScript.dll
|
||||||
|
2026-07-04T12:50:51.7730186Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Runtime.InteropServices.RuntimeInformation.dll
|
||||||
|
2026-07-04T12:50:51.7730278Z -rwxr--r-- 1 root root 64848 May 20 18:29 System.Runtime.InteropServices.dll
|
||||||
|
2026-07-04T12:50:51.7730393Z -rwxr--r-- 1 root root 17232 May 20 18:29 System.Runtime.Intrinsics.dll
|
||||||
|
2026-07-04T12:50:51.7730536Z -rwxr--r-- 1 root root 15696 May 20 18:29 System.Runtime.Loader.dll
|
||||||
|
2026-07-04T12:50:51.7730680Z -rwxr--r-- 1 root root 145232 May 20 18:29 System.Runtime.Numerics.dll
|
||||||
|
2026-07-04T12:50:51.7730916Z -rwxr--r-- 1 root root 65872 May 20 18:29 System.Runtime.Serialization.Formatters.dll
|
||||||
|
2026-07-04T12:50:51.7731009Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Runtime.Serialization.Json.dll
|
||||||
|
2026-07-04T12:50:51.7731092Z -rwxr--r-- 1 root root 23376 May 20 18:30 System.Runtime.Serialization.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7731180Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Runtime.Serialization.Xml.dll
|
||||||
|
2026-07-04T12:50:51.7731381Z -rwxr--r-- 1 root root 17232 May 20 18:31 System.Runtime.Serialization.dll
|
||||||
|
2026-07-04T12:50:51.7731472Z -rwxr--r-- 1 root root 44880 May 20 18:28 System.Runtime.dll
|
||||||
|
2026-07-04T12:50:51.7731568Z -rwxr--r-- 1 root root 58192 May 20 18:29 System.Security.AccessControl.dll
|
||||||
|
2026-07-04T12:50:51.7731659Z -rwxr--r-- 1 root root 55120 May 20 18:29 System.Security.Claims.dll
|
||||||
|
2026-07-04T12:50:51.7731742Z -rwxr--r-- 1 root root 17232 May 20 18:31 System.Security.Cryptography.Algorithms.dll
|
||||||
|
2026-07-04T12:50:51.7731846Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Security.Cryptography.Cng.dll
|
||||||
|
2026-07-04T12:50:51.7731929Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Security.Cryptography.Csp.dll
|
||||||
|
2026-07-04T12:50:51.7732017Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Security.Cryptography.Encoding.dll
|
||||||
|
2026-07-04T12:50:51.7732102Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Security.Cryptography.OpenSsl.dll
|
||||||
|
2026-07-04T12:50:51.7732185Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Security.Cryptography.Primitives.dll
|
||||||
|
2026-07-04T12:50:51.7732267Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Security.Cryptography.X509Certificates.dll
|
||||||
|
2026-07-04T12:50:51.7732350Z -rwxr--r-- 1 root root 654160 May 20 18:31 System.Security.Cryptography.dll
|
||||||
|
2026-07-04T12:50:51.7732431Z -rwxr--r-- 1 root root 37712 May 20 18:29 System.Security.Principal.Windows.dll
|
||||||
|
2026-07-04T12:50:51.7732517Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Security.Principal.dll
|
||||||
|
2026-07-04T12:50:51.7732604Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Security.SecureString.dll
|
||||||
|
2026-07-04T12:50:51.7732693Z -rwxr--r-- 1 root root 18256 May 20 18:31 System.Security.dll
|
||||||
|
2026-07-04T12:50:51.7732776Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.ServiceModel.Web.dll
|
||||||
|
2026-07-04T12:50:51.7732919Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.ServiceProcess.dll
|
||||||
|
2026-07-04T12:50:51.7733005Z -rwxr--r-- 1 root root 742736 May 20 18:29 System.Text.Encoding.CodePages.dll
|
||||||
|
2026-07-04T12:50:51.7733087Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Text.Encoding.Extensions.dll
|
||||||
|
2026-07-04T12:50:51.7733169Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Text.Encoding.dll
|
||||||
|
2026-07-04T12:50:51.7733257Z -rwxr--r-- 1 root root 65872 May 20 18:32 System.Text.Encodings.Web.dll
|
||||||
|
2026-07-04T12:50:51.7733337Z -rwxr--r-- 1 root root 649040 May 20 18:30 System.Text.Json.dll
|
||||||
|
2026-07-04T12:50:51.7733484Z -rwxr--r-- 1 root root 384848 May 20 18:30 System.Text.RegularExpressions.dll
|
||||||
|
2026-07-04T12:50:51.7733633Z -rwxr--r-- 1 root root 33616 May 20 18:29 System.Threading.AccessControl.dll
|
||||||
|
2026-07-04T12:50:51.7733786Z -rwxr--r-- 1 root root 66384 May 20 18:29 System.Threading.Channels.dll
|
||||||
|
2026-07-04T12:50:51.7733944Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Threading.Overlapped.dll
|
||||||
|
2026-07-04T12:50:51.7734092Z -rwxr--r-- 1 root root 185680 May 20 18:29 System.Threading.Tasks.Dataflow.dll
|
||||||
|
2026-07-04T12:50:51.7734257Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Threading.Tasks.Extensions.dll
|
||||||
|
2026-07-04T12:50:51.7734393Z -rwxr--r-- 1 root root 61264 May 20 18:31 System.Threading.Tasks.Parallel.dll
|
||||||
|
2026-07-04T12:50:51.7734482Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Threading.Tasks.dll
|
||||||
|
2026-07-04T12:50:51.7734567Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Threading.Thread.dll
|
||||||
|
2026-07-04T12:50:51.7734649Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Threading.ThreadPool.dll
|
||||||
|
2026-07-04T12:50:51.7734728Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Threading.Timer.dll
|
||||||
|
2026-07-04T12:50:51.7734807Z -rwxr--r-- 1 root root 44880 May 20 18:28 System.Threading.dll
|
||||||
|
2026-07-04T12:50:51.7734908Z -rwxr--r-- 1 root root 175952 May 20 18:30 System.Transactions.Local.dll
|
||||||
|
2026-07-04T12:50:51.7734993Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Transactions.dll
|
||||||
|
2026-07-04T12:50:51.7735073Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.ValueTuple.dll
|
||||||
|
2026-07-04T12:50:51.7735153Z -rwxr--r-- 1 root root 30032 May 20 18:31 System.Web.HttpUtility.dll
|
||||||
|
2026-07-04T12:50:51.7735268Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Web.dll
|
||||||
|
2026-07-04T12:50:51.7735365Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Windows.dll
|
||||||
|
2026-07-04T12:50:51.7735446Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.Linq.dll
|
||||||
|
2026-07-04T12:50:51.7735534Z -rwxr--r-- 1 root root 21840 May 20 18:30 System.Xml.ReaderWriter.dll
|
||||||
|
2026-07-04T12:50:51.7735652Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Xml.Serialization.dll
|
||||||
|
2026-07-04T12:50:51.7735784Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.XDocument.dll
|
||||||
|
2026-07-04T12:50:51.7735871Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.XPath.XDocument.dll
|
||||||
|
2026-07-04T12:50:51.7735952Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Xml.XPath.dll
|
||||||
|
2026-07-04T12:50:51.7736041Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.XmlDocument.dll
|
||||||
|
2026-07-04T12:50:51.7736341Z -rwxr--r-- 1 root root 17744 May 20 18:31 System.Xml.XmlSerializer.dll
|
||||||
|
2026-07-04T12:50:51.7736447Z -rwxr--r-- 1 root root 23376 May 20 18:31 System.Xml.dll
|
||||||
|
2026-07-04T12:50:51.7736537Z -rwxr--r-- 1 root root 50000 May 20 18:32 System.dll
|
||||||
|
2026-07-04T12:50:51.7736621Z -rw-r--r-- 1 root root 156672 Jul 4 12:49 TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:50:51.7736707Z -rw-r--r-- 1 root root 38468 Jul 4 12:49 TaxBaik.Application.pdb
|
||||||
|
2026-07-04T12:50:51.7736794Z -rw-r--r-- 1 root root 37888 Jul 4 12:49 TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:50:51.7736873Z -rw-r--r-- 1 root root 24100 Jul 4 12:49 TaxBaik.Domain.pdb
|
||||||
|
2026-07-04T12:50:51.7736955Z -rw-r--r-- 1 root root 713216 Jul 4 12:50 TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:50:51.7737039Z -rw-r--r-- 1 root root 373136 Jul 4 12:50 TaxBaik.Web.Client.pdb
|
||||||
|
2026-07-04T12:50:51.7737139Z -rw-r--r-- 1 root root 2546 Jul 4 12:50 TaxBaik.Web.Client.runtimeconfig.json
|
||||||
|
2026-07-04T12:50:51.7737338Z -rw-r--r-- 1 root root 1007340 Jul 4 12:50 TaxBaik.Web.Client.staticwebassets.endpoints.json
|
||||||
|
2026-07-04T12:50:51.7737432Z -rw-r--r-- 1 root root 78064 Jul 4 12:50 TaxBaik.Web.Client.staticwebassets.runtime.json
|
||||||
|
2026-07-04T12:50:51.7737525Z -rwxr--r-- 1 root root 16208 May 20 18:31 WindowsBase.dll
|
||||||
|
2026-07-04T12:50:51.7737609Z -rwxr--r-- 1 root root 37898 May 20 18:42 dotnet.js
|
||||||
|
2026-07-04T12:50:51.7737687Z -rwxr--r-- 1 root root 51818 May 20 18:42 dotnet.js.map
|
||||||
|
2026-07-04T12:50:51.7737778Z -rwxr--r-- 1 root root 145050 May 20 18:43 dotnet.native.js
|
||||||
|
2026-07-04T12:50:51.7737860Z -rwxr--r-- 1 root root 3002101 May 20 18:43 dotnet.native.wasm
|
||||||
|
2026-07-04T12:50:51.7737942Z -rwxr--r-- 1 root root 198479 May 20 18:42 dotnet.runtime.js
|
||||||
|
2026-07-04T12:50:51.7738024Z -rwxr--r-- 1 root root 276757 May 20 18:42 dotnet.runtime.js.map
|
||||||
|
2026-07-04T12:50:51.7738104Z -rwxr--r-- 1 root root 956416 Apr 2 19:04 icudt_CJK.dat
|
||||||
|
2026-07-04T12:50:51.7738182Z -rwxr--r-- 1 root root 550832 Apr 2 19:04 icudt_EFIGS.dat
|
||||||
|
2026-07-04T12:50:51.7738259Z -rwxr--r-- 1 root root 1107168 Apr 2 19:04 icudt_no_CJK.dat
|
||||||
|
2026-07-04T12:50:51.7738341Z -rwxr--r-- 1 root root 59728 May 20 18:31 mscorlib.dll
|
||||||
|
2026-07-04T12:50:51.7738417Z -rwxr--r-- 1 root root 100688 May 20 18:32 netstandard.dll
|
||||||
|
2026-07-04T12:50:51.7738496Z drwxr-xr-x 3 root root 4096 Jul 4 12:50 wwwroot
|
||||||
|
2026-07-04T12:50:51.7740611Z total 4352
|
||||||
|
2026-07-04T12:50:51.7740967Z drwxr-xr-x 8 root root 4096 Jul 4 12:50 .
|
||||||
|
2026-07-04T12:50:51.7741083Z drwxr-xr-x 3 root root 4096 Jul 4 12:50 ..
|
||||||
|
2026-07-04T12:50:51.7741169Z -rw-r--r-- 1 root root 196 Jul 4 12:50 .NETCoreApp,Version=v10.0.AssemblyAttributes.cs
|
||||||
|
2026-07-04T12:50:51.7741346Z -rw-r--r-- 1 root root 137 Jul 4 12:50 EmbeddedAttribute.cs
|
||||||
|
2026-07-04T12:50:51.7741437Z -rw-r--r-- 1 root root 0 Jul 4 12:50 TaxBaik..C36EE7CA.Up2Date
|
||||||
|
2026-07-04T12:50:51.7741531Z -rw-r--r-- 1 root root 1008 Jul 4 12:50 TaxBaik.Web.Client.AssemblyInfo.cs
|
||||||
|
2026-07-04T12:50:51.7741630Z -rw-r--r-- 1 root root 65 Jul 4 12:50 TaxBaik.Web.Client.AssemblyInfoInputs.cache
|
||||||
|
2026-07-04T12:50:51.7741744Z -rw-r--r-- 1 root root 16355 Jul 4 12:50 TaxBaik.Web.Client.GeneratedMSBuildEditorConfig.editorconfig
|
||||||
|
2026-07-04T12:50:51.7741848Z -rw-r--r-- 1 root root 433 Jul 4 12:50 TaxBaik.Web.Client.GlobalUsings.g.cs
|
||||||
|
2026-07-04T12:50:51.7741978Z -rw-r--r-- 1 root root 0 Jul 4 12:50 TaxBaik.Web.Client.MvcApplicationPartsAssemblyInfo.cache
|
||||||
|
2026-07-04T12:50:51.7742164Z -rw-r--r-- 1 root root 27329 Jul 4 12:50 TaxBaik.Web.Client.assets.cache
|
||||||
|
2026-07-04T12:50:51.7742340Z -rw-r--r-- 1 root root 20456 Jul 4 12:50 TaxBaik.Web.Client.csproj.AssemblyReference.cache
|
||||||
|
2026-07-04T12:50:51.7742497Z -rw-r--r-- 1 root root 65 Jul 4 12:50 TaxBaik.Web.Client.csproj.CoreCompileInputs.cache
|
||||||
|
2026-07-04T12:50:51.7742654Z -rw-r--r-- 1 root root 136894 Jul 4 12:50 TaxBaik.Web.Client.csproj.FileListAbsolute.txt
|
||||||
|
2026-07-04T12:50:51.7742893Z -rw-r--r-- 1 root root 713216 Jul 4 12:50 TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:50:51.7743120Z -rw-r--r-- 1 root root 65 Jul 4 12:50 TaxBaik.Web.Client.genruntimeconfig.cache
|
||||||
|
2026-07-04T12:50:51.7743334Z -rw-r--r-- 1 root root 373136 Jul 4 12:50 TaxBaik.Web.Client.pdb
|
||||||
|
2026-07-04T12:50:51.7743742Z -rw-r--r-- 1 root root 276 Jul 4 12:50 ValidatableTypeAttribute.cs
|
||||||
|
2026-07-04T12:50:51.7743930Z -rw-r--r-- 1 root root 3 Jul 4 12:50 blazor.build.boot-extension.json
|
||||||
|
2026-07-04T12:50:51.7744151Z drwxr-xr-x 2 root root 20480 Jul 4 12:50 compressed
|
||||||
|
2026-07-04T12:50:51.7744319Z -rw-r--r-- 1 root root 93392 Jul 4 12:50 dotnet.js
|
||||||
|
2026-07-04T12:50:51.7744459Z -rw-r--r-- 1 root root 276706 Jul 4 12:50 rbcswa.dswa.cache.json
|
||||||
|
2026-07-04T12:50:51.7744599Z drwxr-xr-x 2 root root 4096 Jul 4 12:50 ref
|
||||||
|
2026-07-04T12:50:51.7744731Z drwxr-xr-x 2 root root 4096 Jul 4 12:50 refint
|
||||||
|
2026-07-04T12:50:51.7744901Z -rw-r--r-- 1 root root 322 Jul 4 12:50 rjimswa.dswa.cache.json
|
||||||
|
2026-07-04T12:50:51.7745034Z -rw-r--r-- 1 root root 3404 Jul 4 12:50 rjsmcshtml.dswa.cache.json
|
||||||
|
2026-07-04T12:50:51.7745177Z -rw-r--r-- 1 root root 3404 Jul 4 12:50 rjsmrazor.dswa.cache.json
|
||||||
|
2026-07-04T12:50:51.7745357Z -rw-r--r-- 1 root root 4173 Jul 4 12:50 rpswa.dswa.cache.json
|
||||||
|
2026-07-04T12:50:51.7745511Z drwxr-xr-x 2 root root 4096 Jul 4 12:50 staticwebassets
|
||||||
|
2026-07-04T12:50:51.7745658Z -rw-r--r-- 1 root root 1007340 Jul 4 12:50 staticwebassets.build.endpoints.json
|
||||||
|
2026-07-04T12:50:51.7745816Z -rw-r--r-- 1 root root 1564906 Jul 4 12:50 staticwebassets.build.json
|
||||||
|
2026-07-04T12:50:51.7746022Z -rw-r--r-- 1 root root 44 Jul 4 12:50 staticwebassets.build.json.cache
|
||||||
|
2026-07-04T12:50:51.7804929Z -rw-r--r-- 1 root root 78064 Jul 4 12:50 staticwebassets.development.json
|
||||||
|
2026-07-04T12:50:51.7805097Z -rw-r--r-- 1 root root 0 Jul 4 12:50 swae.build.ex.cache
|
||||||
|
2026-07-04T12:50:51.7805385Z drwxr-xr-x 2 root root 4096 Jul 4 12:50 tmp-webcil
|
||||||
|
2026-07-04T12:50:51.7805479Z drwxr-xr-x 2 root root 20480 Jul 4 12:50 webcil
|
||||||
|
2026-07-04T12:51:56.0836642Z ✓ Publish Web elapsed: 86s
|
||||||
|
2026-07-04T12:51:56.0856804Z -rw-r--r-- 1 root root 2.2M Jul 4 12:51 ./publish-logs/publish-web.binlog
|
||||||
|
2026-07-04T12:51:56.1993997Z ::group::Run set -e
|
||||||
|
2026-07-04T12:51:56.1994446Z set -e
|
||||||
|
2026-07-04T12:51:56.1994570Z mkdir -p ./publish-logs
|
||||||
|
2026-07-04T12:51:56.1994674Z # Proxy is not part of the solution restore graph, so restore it once
|
||||||
|
2026-07-04T12:51:56.1994784Z # here before publishing to avoid NETSDK1004 in CI.
|
||||||
|
2026-07-04T12:51:56.1994877Z dotnet restore src/TaxBaik.Proxy/
|
||||||
|
2026-07-04T12:51:56.1994953Z dotnet build src/TaxBaik.Proxy/TaxBaik.Proxy.csproj -c Release --no-restore
|
||||||
|
2026-07-04T12:51:56.1995040Z start=$(date +%s)
|
||||||
|
2026-07-04T12:51:56.1995115Z dotnet publish src/TaxBaik.Proxy/ \
|
||||||
|
2026-07-04T12:51:56.1995200Z -c Release \
|
||||||
|
2026-07-04T12:51:56.1995275Z -o ./publish/proxy \
|
||||||
|
2026-07-04T12:51:56.1995345Z --no-restore \
|
||||||
|
2026-07-04T12:51:56.1995433Z --no-build \
|
||||||
|
2026-07-04T12:51:56.1995766Z -p:PublishReadyToRun=false \
|
||||||
|
2026-07-04T12:51:56.1995969Z -p:PerformanceSummary=true \
|
||||||
|
2026-07-04T12:51:56.1996302Z -clp:Summary \
|
||||||
|
2026-07-04T12:51:56.1996496Z -bl:./publish-logs/publish-proxy.binlog
|
||||||
|
2026-07-04T12:51:56.1996597Z end=$(date +%s)
|
||||||
|
2026-07-04T12:51:56.1996672Z echo "✓ Publish Proxy elapsed: $((end - start))s"
|
||||||
|
2026-07-04T12:51:56.1996955Z ls -lh ./publish-logs/publish-proxy.binlog
|
||||||
|
2026-07-04T12:51:56.1997163Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:51:56.1997314Z ::endgroup::
|
||||||
|
2026-07-04T12:51:57.1407533Z Determining projects to restore...
|
||||||
|
2026-07-04T12:51:57.6632131Z Restored /workspace/***/taxbaik/src/TaxBaik.Proxy/TaxBaik.Proxy.csproj (in 125 ms).
|
||||||
|
2026-07-04T12:51:58.9747864Z TaxBaik.Proxy -> /workspace/***/taxbaik/src/TaxBaik.Proxy/bin/Release/net10.0/TaxBaik.Proxy.dll
|
||||||
|
2026-07-04T12:51:58.9938437Z
|
||||||
|
2026-07-04T12:51:58.9947013Z Build succeeded.
|
||||||
|
2026-07-04T12:51:58.9949917Z 0 Warning(s)
|
||||||
|
2026-07-04T12:51:58.9951624Z 0 Error(s)
|
||||||
|
2026-07-04T12:51:58.9954373Z
|
||||||
|
2026-07-04T12:51:58.9954554Z Time Elapsed 00:00:01.05
|
||||||
|
2026-07-04T12:52:00.0446907Z TaxBaik.Proxy -> /workspace/***/taxbaik/publish/proxy/
|
||||||
|
2026-07-04T12:52:00.0565606Z
|
||||||
|
2026-07-04T12:52:00.0583909Z Build succeeded.
|
||||||
|
2026-07-04T12:52:00.0584436Z 0 Warning(s)
|
||||||
|
2026-07-04T12:52:00.0584681Z 0 Error(s)
|
||||||
|
2026-07-04T12:52:00.0584764Z
|
||||||
|
2026-07-04T12:52:00.0584841Z Time Elapsed 00:00:00.64
|
||||||
|
2026-07-04T12:52:00.0903373Z ✓ Publish Proxy elapsed: 1s
|
||||||
|
2026-07-04T12:52:00.0915796Z -rw-r--r-- 1 root root 335K Jul 4 12:52 ./publish-logs/publish-proxy.binlog
|
||||||
|
2026-07-04T12:52:00.2477651Z ::group::Run set -e
|
||||||
|
2026-07-04T12:52:00.2478168Z set -e
|
||||||
|
2026-07-04T12:52:00.2478625Z JWT_SECRET_KEY="***"
|
||||||
|
2026-07-04T12:52:00.2478852Z TELEGRAM_BOT_TOKEN="***"
|
||||||
|
2026-07-04T12:52:00.2478963Z TELEGRAM_CHAT_ID="***"
|
||||||
|
2026-07-04T12:52:00.2479117Z TELEGRAM_INQUIRY_CHAT_ID=""
|
||||||
|
2026-07-04T12:52:00.2479461Z TELEGRAM_SYSTEM_CHAT_ID=""
|
||||||
|
2026-07-04T12:52:00.2479762Z [ -z "$JWT_SECRET_KEY" ] && { echo "Missing TAXBAIK_JWT_SECRET_KEY" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:00.2479916Z [ -z "$TELEGRAM_BOT_TOKEN" ] && { echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:00.2480078Z [ -z "$TELEGRAM_CHAT_ID" ] && { echo "Missing TAXBAIK_TELEGRAM_CHAT_ID" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:00.2480551Z [ -z "$TELEGRAM_INQUIRY_CHAT_ID" ] && TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_CHAT_ID"
|
||||||
|
2026-07-04T12:52:00.2480669Z [ -z "$TELEGRAM_SYSTEM_CHAT_ID" ] && TELEGRAM_SYSTEM_CHAT_ID="-5585148480"
|
||||||
|
2026-07-04T12:52:00.2481140Z JWT_SECRET_KEY="$JWT_SECRET_KEY" \
|
||||||
|
2026-07-04T12:52:00.2481420Z TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \
|
||||||
|
2026-07-04T12:52:00.2481513Z TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" \
|
||||||
|
2026-07-04T12:52:00.2481588Z TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_INQUIRY_CHAT_ID" \
|
||||||
|
2026-07-04T12:52:00.2482377Z TELEGRAM_SYSTEM_CHAT_ID="$TELEGRAM_SYSTEM_CHAT_ID" \
|
||||||
|
2026-07-04T12:52:00.2482488Z python3 -c '
|
||||||
|
2026-07-04T12:52:00.2482577Z import json, os, pathlib
|
||||||
|
2026-07-04T12:52:00.2482665Z pathlib.Path("./publish/appsettings.Production.json").write_text(
|
||||||
|
2026-07-04T12:52:00.2483035Z json.dumps({
|
||||||
|
2026-07-04T12:52:00.2483310Z "Jwt": {"SecretKey": os.environ["JWT_SECRET_KEY"]},
|
||||||
|
2026-07-04T12:52:00.2483405Z "Telegram": {
|
||||||
|
2026-07-04T12:52:00.2483477Z "BotToken": os.environ["TELEGRAM_BOT_TOKEN"],
|
||||||
|
2026-07-04T12:52:00.2483559Z "ChatId": os.environ["TELEGRAM_CHAT_ID"],
|
||||||
|
2026-07-04T12:52:00.2483634Z "InquiryChatId": os.environ["TELEGRAM_INQUIRY_CHAT_ID"],
|
||||||
|
2026-07-04T12:52:00.2483960Z "SystemChatId": os.environ["TELEGRAM_SYSTEM_CHAT_ID"]
|
||||||
|
2026-07-04T12:52:00.2484230Z }
|
||||||
|
2026-07-04T12:52:00.2484311Z }, ensure_ascii=False, indent=2),
|
||||||
|
2026-07-04T12:52:00.2484386Z encoding="utf-8"
|
||||||
|
2026-07-04T12:52:00.2484471Z )'
|
||||||
|
2026-07-04T12:52:00.2484543Z test -s ./publish/appsettings.Production.json || { echo "appsettings.Production.json is empty" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:00.2484866Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:00.2485155Z ::endgroup::
|
||||||
|
2026-07-04T12:52:00.5025133Z ::group::Run test -s ./publish/proxy/TaxBaik.Proxy.dll || { echo "TaxBaik.Proxy.dll missing" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:00.5025722Z test -s ./publish/proxy/TaxBaik.Proxy.dll || { echo "TaxBaik.Proxy.dll missing" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:00.5025922Z test -s ./publish/proxy/TaxBaik.Proxy.runtimeconfig.json || { echo "TaxBaik.Proxy.runtimeconfig.json missing" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:00.5026227Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:00.5026398Z ::endgroup::
|
||||||
|
2026-07-04T12:52:00.6764369Z ::group::Run mkdir -p ./publish/db && cp -r db/migrations ./publish/db/ || true
|
||||||
|
2026-07-04T12:52:00.6764765Z mkdir -p ./publish/db && cp -r db/migrations ./publish/db/ || true
|
||||||
|
2026-07-04T12:52:00.6764895Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:00.6765004Z ::endgroup::
|
||||||
|
2026-07-04T12:52:00.8728261Z ::group::Run bash scripts/validate_migrations.sh db/migrations
|
||||||
|
2026-07-04T12:52:00.8728660Z bash scripts/validate_migrations.sh db/migrations
|
||||||
|
2026-07-04T12:52:00.8728905Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:00.8729177Z ::endgroup::
|
||||||
|
2026-07-04T12:52:00.9538985Z Duplicate version check passed.
|
||||||
|
2026-07-04T12:52:01.0855583Z ::group::Run bash scripts/validate_kst_timestamps.sh
|
||||||
|
2026-07-04T12:52:01.0856340Z bash scripts/validate_kst_timestamps.sh
|
||||||
|
2026-07-04T12:52:01.0856747Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:01.0856924Z ::endgroup::
|
||||||
|
2026-07-04T12:52:01.1537519Z KST timestamp harness passed.
|
||||||
|
2026-07-04T12:52:01.3082211Z ::group::Run COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||||
|
2026-07-04T12:52:01.3082595Z COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||||
|
2026-07-04T12:52:01.3082713Z BUILD_TIME=$(TZ=Asia/Seoul date +'%Y-%m-%d %H:%M:%S KST')
|
||||||
|
2026-07-04T12:52:01.3082807Z mkdir -p ./publish/wwwroot
|
||||||
|
2026-07-04T12:52:01.3082952Z printf '{\n "version": "%s",\n "built": "%s"\n}\n' "$COMMIT_HASH" "$BUILD_TIME" > ./publish/wwwroot/version.json
|
||||||
|
2026-07-04T12:52:01.3083148Z echo "✓ Build: $COMMIT_HASH @ $BUILD_TIME"
|
||||||
|
2026-07-04T12:52:01.3083277Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:01.3083386Z ::endgroup::
|
||||||
|
2026-07-04T12:52:01.3712948Z ✓ Build: dc86ccf @ 2026-07-04 21:52:01 KST
|
||||||
|
2026-07-04T12:52:01.4848269Z ::group::Run mkdir -p ~/.ssh
|
||||||
|
2026-07-04T12:52:01.4849031Z mkdir -p ~/.ssh
|
||||||
|
2026-07-04T12:52:01.4849211Z SSH_KEY_B64="***"
|
||||||
|
2026-07-04T12:52:01.4849414Z SSH_KEY_RAW="***"
|
||||||
|
2026-07-04T12:52:01.4849576Z if [ -n "$SSH_KEY_B64" ]; then
|
||||||
|
2026-07-04T12:52:01.4850063Z printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T12:52:01.4850225Z elif [ -n "$SSH_KEY_RAW" ]; then
|
||||||
|
2026-07-04T12:52:01.4850362Z if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then
|
||||||
|
2026-07-04T12:52:01.4850943Z printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T12:52:01.4851190Z else
|
||||||
|
2026-07-04T12:52:01.4851357Z printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T12:52:01.4851809Z fi
|
||||||
|
2026-07-04T12:52:01.4851949Z else
|
||||||
|
2026-07-04T12:52:01.4852141Z echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY" >&2; exit 1
|
||||||
|
2026-07-04T12:52:01.4852495Z fi
|
||||||
|
2026-07-04T12:52:01.4852617Z sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T12:52:01.4852755Z chmod 600 ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T12:52:01.4852886Z ssh-keyscan -H "***" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
|
2026-07-04T12:52:01.4853093Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:01.4853480Z ::endgroup::
|
||||||
|
2026-07-04T12:52:01.9503887Z ::group::Run cp deploy_gb.sh ./publish/deploy_gb.sh
|
||||||
|
2026-07-04T12:52:01.9504389Z cp deploy_gb.sh ./publish/deploy_gb.sh
|
||||||
|
2026-07-04T12:52:01.9504506Z mkdir -p ./publish/scripts
|
||||||
|
2026-07-04T12:52:01.9504591Z cp scripts/validate_migrations.sh ./publish/scripts/validate_migrations.sh
|
||||||
|
2026-07-04T12:52:01.9504690Z chmod +x ./publish/scripts/validate_migrations.sh
|
||||||
|
2026-07-04T12:52:01.9504769Z tar -czf taxbaik_deploy.tgz -C ./publish .
|
||||||
|
2026-07-04T12:52:01.9504850Z echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)"
|
||||||
|
2026-07-04T12:52:01.9504949Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:01.9505044Z ::endgroup::
|
||||||
|
2026-07-04T12:52:04.4650255Z ✓ Package: 23M
|
||||||
|
2026-07-04T12:52:04.5749245Z ::group::Run set -e
|
||||||
|
2026-07-04T12:52:04.5749613Z set -e
|
||||||
|
2026-07-04T12:52:04.5749749Z export TAXBAIK_DEPLOY_FROM_CI=1
|
||||||
|
2026-07-04T12:52:04.5749842Z TIMESTAMP=$(TZ=Asia/Seoul date +%Y%m%d_%H%M%S)
|
||||||
|
2026-07-04T12:52:04.5749941Z COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
2026-07-04T12:52:04.5750025Z DEPLOY_HOST="***"
|
||||||
|
2026-07-04T12:52:04.5750112Z DEPLOY_USER="***"
|
||||||
|
2026-07-04T12:52:04.5750193Z TELEGRAM_BOT_TOKEN="***"
|
||||||
|
2026-07-04T12:52:04.5750278Z TELEGRAM_SYSTEM_CHAT_ID=""
|
||||||
|
2026-07-04T12:52:04.5750351Z TELEGRAM_CHAT_ID="${TELEGRAM_SYSTEM_CHAT_ID:--5585148480}"
|
||||||
|
2026-07-04T12:52:04.5750434Z
|
||||||
|
2026-07-04T12:52:04.5750499Z send_telegram() {
|
||||||
|
2026-07-04T12:52:04.5750573Z local text="$1"
|
||||||
|
2026-07-04T12:52:04.5750651Z if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
|
||||||
|
2026-07-04T12:52:04.5750957Z echo "Skipping Telegram notification: missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2
|
||||||
|
2026-07-04T12:52:04.5751063Z return 0
|
||||||
|
2026-07-04T12:52:04.5751137Z fi
|
||||||
|
2026-07-04T12:52:04.5751205Z
|
||||||
|
2026-07-04T12:52:04.5751268Z curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
2026-07-04T12:52:04.5751359Z -d "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
|
2026-07-04T12:52:04.5751470Z --data-urlencode "text=${text}" \
|
||||||
|
2026-07-04T12:52:04.5751545Z -d "parse_mode=HTML" >/dev/null || true
|
||||||
|
2026-07-04T12:52:04.5751619Z }
|
||||||
|
2026-07-04T12:52:04.5751687Z
|
||||||
|
2026-07-04T12:52:04.5751744Z notify_failure() {
|
||||||
|
2026-07-04T12:52:04.5751817Z local exit_code=$?
|
||||||
|
2026-07-04T12:52:04.5751888Z send_telegram "❌ <b>TaxBaik 배포 실패</b>
|
||||||
|
2026-07-04T12:52:04.5751985Z
|
||||||
|
2026-07-04T12:52:04.5752046Z 커밋: <code>${COMMIT}</code>
|
||||||
|
2026-07-04T12:52:04.5752122Z 시간: <code>${TIMESTAMP}</code>
|
||||||
|
2026-07-04T12:52:04.5752192Z 단계: CI/CD deploy"
|
||||||
|
2026-07-04T12:52:04.5752262Z exit "$exit_code"
|
||||||
|
2026-07-04T12:52:04.5752330Z }
|
||||||
|
2026-07-04T12:52:04.5752395Z
|
||||||
|
2026-07-04T12:52:04.5752476Z trap notify_failure ERR
|
||||||
|
2026-07-04T12:52:04.5752547Z
|
||||||
|
2026-07-04T12:52:04.5752607Z echo "=== Deploying TaxBaik $COMMIT ($TIMESTAMP) ==="
|
||||||
|
2026-07-04T12:52:04.5752682Z
|
||||||
|
2026-07-04T12:52:04.5752745Z # 1. 아티팩트 업로드
|
||||||
|
2026-07-04T12:52:04.5752819Z scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||||
|
2026-07-04T12:52:04.5752898Z taxbaik_deploy.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_${TIMESTAMP}.tgz"
|
||||||
|
2026-07-04T12:52:04.5752988Z
|
||||||
|
2026-07-04T12:52:04.5753051Z # 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리, Green-Blue 지원)
|
||||||
|
2026-07-04T12:52:04.5753136Z ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||||
|
2026-07-04T12:52:04.5753543Z -o ServerAliveInterval=10 \
|
||||||
|
2026-07-04T12:52:04.5753625Z "$DEPLOY_USER@$DEPLOY_HOST" TAXBAIK_DEPLOY_FROM_CI=1 bash << REMOTE
|
||||||
|
2026-07-04T12:52:04.5753709Z set -e
|
||||||
|
2026-07-04T12:52:04.5753788Z DEPLOY_HOME="/home/***"
|
||||||
|
2026-07-04T12:52:04.5753873Z DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
||||||
|
2026-07-04T12:52:04.5753953Z TIMESTAMP="${TIMESTAMP}"
|
||||||
|
2026-07-04T12:52:04.5754024Z COMMIT="${COMMIT}"
|
||||||
|
2026-07-04T12:52:04.5754093Z
|
||||||
|
2026-07-04T12:52:04.5754169Z echo "--- [1/5] 압축 해제 ---"
|
||||||
|
2026-07-04T12:52:04.5754243Z mkdir -p "\$DEPLOY_DIR"
|
||||||
|
2026-07-04T12:52:04.5754318Z tar -xzf "/tmp/taxbaik_\${TIMESTAMP}.tgz" -C "\$DEPLOY_DIR"
|
||||||
|
2026-07-04T12:52:04.5754407Z rm -f "/tmp/taxbaik_\${TIMESTAMP}.tgz"
|
||||||
|
2026-07-04T12:52:04.5754475Z
|
||||||
|
2026-07-04T12:52:04.5754537Z echo "--- [2/5] 운영 설정 검증 ---"
|
||||||
|
2026-07-04T12:52:04.5754611Z test -s "\$DEPLOY_DIR/appsettings.Production.json" \
|
||||||
|
2026-07-04T12:52:04.5754687Z || { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:04.5754769Z test -s "\$DEPLOY_DIR/proxy/TaxBaik.Proxy.dll" \
|
||||||
|
2026-07-04T12:52:04.5754845Z || { echo "FATAL: TaxBaik.Proxy.dll 없음" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:04.5754928Z
|
||||||
|
2026-07-04T12:52:04.5754989Z echo "--- [3/5] 마이그레이션 사전 검증 ---"
|
||||||
|
2026-07-04T12:52:04.5755073Z test -x "\$DEPLOY_DIR/scripts/validate_migrations.sh" \
|
||||||
|
2026-07-04T12:52:04.5755143Z || { echo "FATAL: validate_migrations.sh 없음" >&2; exit 1; }
|
||||||
|
2026-07-04T12:52:04.5755220Z "\$DEPLOY_DIR/scripts/validate_migrations.sh" "\$DEPLOY_DIR/db/migrations" "postgresql://taxbaik:taxbaik123@localhost:5432/taxbaikdb"
|
||||||
|
2026-07-04T12:52:04.5755316Z
|
||||||
|
2026-07-04T12:52:04.5755383Z echo "--- [4/5] Green-Blue 배포 실행 ---"
|
||||||
|
2026-07-04T12:52:04.5755479Z chmod +x "\$DEPLOY_DIR/deploy_gb.sh"
|
||||||
|
2026-07-04T12:52:04.5755550Z "\$DEPLOY_DIR/deploy_gb.sh" "\$DEPLOY_DIR"
|
||||||
|
2026-07-04T12:52:04.5755622Z
|
||||||
|
2026-07-04T12:52:04.5755686Z echo "--- [4.5/5] Nginx 설정 검증 ---"
|
||||||
|
2026-07-04T12:52:04.5755768Z # 실제 로드되는 파일은 sites-enabled/의 심볼릭 링크 대상만이다.
|
||||||
|
2026-07-04T12:52:04.5755850Z # sites-available/에 다른 파일(예: default)이 있어도 sites-enabled에
|
||||||
|
2026-07-04T12:52:04.5755945Z # 링크되어 있지 않으면 nginx는 그 내용을 절대 읽지 않는다.
|
||||||
|
2026-07-04T12:52:04.5756036Z NGINX_CONF=""
|
||||||
|
2026-07-04T12:52:04.5756277Z for f in /etc/nginx/sites-enabled/*; do
|
||||||
|
2026-07-04T12:52:04.5756361Z if [ -e "\$f" ] && grep -q "location /taxbaik" "\$f" 2>/dev/null; then
|
||||||
|
2026-07-04T12:52:04.5756482Z NGINX_CONF=\$(readlink -f "\$f")
|
||||||
|
2026-07-04T12:52:04.5756667Z break
|
||||||
|
2026-07-04T12:52:04.5756846Z fi
|
||||||
|
2026-07-04T12:52:04.5756964Z done
|
||||||
|
2026-07-04T12:52:04.5757108Z
|
||||||
|
2026-07-04T12:52:04.5757224Z if [ -z "\$NGINX_CONF" ]; then
|
||||||
|
2026-07-04T12:52:04.5757368Z echo "❌ FATAL: sites-enabled/ 안에서 'location /taxbaik'를 정의한 파일을 찾을 수 없음" >&2
|
||||||
|
2026-07-04T12:52:04.5757523Z echo " sites-available/에 파일을 수정해도 sites-enabled에 심볼릭 링크되어 있지 않으면 반영되지 않는다." >&2
|
||||||
|
2026-07-04T12:52:04.5757636Z exit 1
|
||||||
|
2026-07-04T12:52:04.5757715Z fi
|
||||||
|
2026-07-04T12:52:04.5757781Z echo "실제 로드되는 설정 파일: \$NGINX_CONF"
|
||||||
|
2026-07-04T12:52:04.5757891Z
|
||||||
|
2026-07-04T12:52:04.5758224Z # 불변식: '/'와 '/taxbaik' location 모두 반드시 127.0.0.1:5001 (TaxBaik.Proxy)을
|
||||||
|
2026-07-04T12:52:04.5758411Z # 가리켜야 한다. 5003/5004를 직접 하드코딩하면 Green-Blue 포트 전환 시
|
||||||
|
2026-07-04T12:52:04.5758502Z # 죽은 포트를 가리키게 되어 502/404가 발생한다 (실제 발생했던 장애).
|
||||||
|
2026-07-04T12:52:04.5758585Z if grep -E "proxy_pass\s+http://127\.0\.0\.1:500[34]" "\$NGINX_CONF" > /dev/null 2>&1; then
|
||||||
|
2026-07-04T12:52:04.5758688Z echo "❌ FATAL: \$NGINX_CONF 가 포트 5003/5004를 직접 참조함 (Green-Blue 전환 시 502 발생)" >&2
|
||||||
|
2026-07-04T12:52:04.5758790Z echo " 수정: sudo sed -i 's|127.0.0.1:500[34]|127.0.0.1:5001|g' \$NGINX_CONF && sudo nginx -t && sudo systemctl reload nginx" >&2
|
||||||
|
2026-07-04T12:52:04.5758903Z exit 1
|
||||||
|
2026-07-04T12:52:04.5758978Z fi
|
||||||
|
2026-07-04T12:52:04.5759041Z
|
||||||
|
2026-07-04T12:52:04.5759104Z # proxy_pass에 URI(끝 슬래시)가 있으면 nginx가 요청 경로를 재작성하며,
|
||||||
|
2026-07-04T12:52:04.5759451Z # location 접두사와 슬래시 개수가 안 맞으면 백엔드로 이중 슬래시(//)가
|
||||||
|
2026-07-04T12:52:04.5759539Z # 전달되어 404가 발생한다 (실제 발생했던 장애). 접두사 location에서는
|
||||||
|
2026-07-04T12:52:04.5759631Z # proxy_pass에 URI를 붙이지 않는다.
|
||||||
|
2026-07-04T12:52:04.5759730Z if grep -E "location\s+/taxbaik\s*\{" -A 1 "\$NGINX_CONF" | grep -qE "proxy_pass\s+http://127\.0\.0\.1:5001/;"; then
|
||||||
|
2026-07-04T12:52:04.5759822Z echo "❌ FATAL: location /taxbaik 의 proxy_pass 에 불필요한 trailing slash가 있음 (이중 슬래시로 인한 404 위험)" >&2
|
||||||
|
2026-07-04T12:52:04.5759913Z exit 1
|
||||||
|
2026-07-04T12:52:04.5759981Z fi
|
||||||
|
2026-07-04T12:52:04.5760047Z
|
||||||
|
2026-07-04T12:52:04.5760121Z echo "✓ Nginx 설정 검증 통과 (실제 로드 파일 확인 + 포트 5001 고정 + trailing slash 없음)"
|
||||||
|
2026-07-04T12:52:04.5760214Z
|
||||||
|
2026-07-04T12:52:04.5760285Z echo "--- [5/5] 헬스 체크 (최대 60초) ---"
|
||||||
|
2026-07-04T12:52:04.5760366Z ATTEMPTS=20
|
||||||
|
2026-07-04T12:52:04.5760433Z for i in \$(seq 1 \$ATTEMPTS); do
|
||||||
|
2026-07-04T12:52:04.5760506Z STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/healthz 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T12:52:04.5760596Z if [ "\$STATUS" = "200" ]; then
|
||||||
|
2026-07-04T12:52:04.5760678Z echo "✓ [1/6] 헬스 체크 완료"
|
||||||
|
2026-07-04T12:52:04.5760917Z
|
||||||
|
2026-07-04T12:52:04.5760986Z # 검증 1: 메인 페이지 로드. curl -L + -w 는 리다이렉트 체인의 상태코드를
|
||||||
|
2026-07-04T12:52:04.5761079Z # 이어붙이므로, 첫 응답 코드만 받아 200/3xx를 허용한다.
|
||||||
|
2026-07-04T12:52:04.5761159Z MAIN_STATUS=\$(curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/ 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T12:52:04.5761249Z if ! printf '%s' "\$MAIN_STATUS" | grep -Eq '^(200|301|302|307|308)$'; then
|
||||||
|
2026-07-04T12:52:04.5761333Z echo "❌ 메인 페이지 로드 실패 (상태: \$MAIN_STATUS)" >&2
|
||||||
|
2026-07-04T12:52:04.5761415Z exit 1
|
||||||
|
2026-07-04T12:52:04.5761482Z fi
|
||||||
|
2026-07-04T12:52:04.5761561Z echo "✓ [2/6] 메인 페이지 로드 완료"
|
||||||
|
2026-07-04T12:52:04.5761647Z
|
||||||
|
2026-07-04T12:52:04.5761724Z # 검증 2: CSS 파일 로드
|
||||||
|
2026-07-04T12:52:04.5761804Z CSS_STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/css/admin.css 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T12:52:04.5761955Z if [ "\$CSS_STATUS" != "200" ]; then
|
||||||
|
2026-07-04T12:52:04.5762120Z echo "❌ CSS 파일 로드 실패 (상태: \$CSS_STATUS)" >&2
|
||||||
|
2026-07-04T12:52:04.5762317Z exit 1
|
||||||
|
2026-07-04T12:52:04.5762451Z fi
|
||||||
|
2026-07-04T12:52:04.5762570Z echo "✓ [3/6] CSS 파일 로드 완료"
|
||||||
|
2026-07-04T12:52:04.5762691Z
|
||||||
|
2026-07-04T12:52:04.5762793Z # 검증 3: 버전 정보. 파일 존재만 보면 5001이 잘못된 구 프로세스를
|
||||||
|
2026-07-04T12:52:04.5762940Z # 가리키는 장애를 놓치므로, HTTP 응답이 이번 커밋인지 확인한다.
|
||||||
|
2026-07-04T12:52:04.5763096Z if [ ! -s "\$DEPLOY_DIR/wwwroot/version.json" ]; then
|
||||||
|
2026-07-04T12:52:04.5763233Z echo "❌ version.json 누락" >&2
|
||||||
|
2026-07-04T12:52:04.5763398Z exit 1
|
||||||
|
2026-07-04T12:52:04.5763528Z fi
|
||||||
|
2026-07-04T12:52:04.5763654Z VERSION_JSON=\$(curl -fsS http://127.0.0.1:5001/taxbaik/version.json 2>/dev/null || true)
|
||||||
|
2026-07-04T12:52:04.5763823Z if ! printf '%s' "\$VERSION_JSON" | grep -q "\"version\": \"\$COMMIT\""; then
|
||||||
|
2026-07-04T12:52:04.5763977Z echo "❌ 5001 프록시가 이번 배포 버전을 제공하지 않음" >&2
|
||||||
|
2026-07-04T12:52:04.5764122Z echo " expected: \$COMMIT" >&2
|
||||||
|
2026-07-04T12:52:04.5764261Z echo " actual: \$VERSION_JSON" >&2
|
||||||
|
2026-07-04T12:52:04.5764401Z echo " 확인: 5001 포트가 TaxBaik.Proxy.dll인지, /home/***/taxbaik_port가 새 포트인지 점검" >&2
|
||||||
|
2026-07-04T12:52:04.5764575Z exit 1
|
||||||
|
2026-07-04T12:52:04.5764708Z fi
|
||||||
|
2026-07-04T12:52:04.5764827Z echo "✓ [4/6] 버전 정보 확인 완료"
|
||||||
|
2026-07-04T12:52:04.5764961Z
|
||||||
|
2026-07-04T12:52:04.5765061Z # 검증 4: 5001 프록시 확인
|
||||||
|
2026-07-04T12:52:04.5765196Z if ! ss -tlnp | grep -q ':5001 '; then
|
||||||
|
2026-07-04T12:52:04.5765343Z echo "❌ 5001 프록시가 실행 중이 아님" >&2
|
||||||
|
2026-07-04T12:52:04.5765486Z exit 1
|
||||||
|
2026-07-04T12:52:04.5765610Z fi
|
||||||
|
2026-07-04T12:52:04.5765730Z echo "✓ [5/6] 5001 프록시 확인 완료"
|
||||||
|
2026-07-04T12:52:04.5765870Z
|
||||||
|
2026-07-04T12:52:04.5765986Z # 검증 5: 관리자 로그인 페이지
|
||||||
|
2026-07-04T12:52:04.5766315Z LOGIN_STATUS=\$(curl -fsSL -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T12:52:04.5766510Z if [ "\$LOGIN_STATUS" != "200" ]; then
|
||||||
|
2026-07-04T12:52:04.5766652Z echo "❌ 관리자 로그인 페이지 로드 실패 (상태: \$LOGIN_STATUS)" >&2
|
||||||
|
2026-07-04T12:52:04.5766809Z exit 1
|
||||||
|
2026-07-04T12:52:04.5766928Z fi
|
||||||
|
2026-07-04T12:52:04.5767049Z echo "✓ [6/6] 관리자 페이지 로드 완료"
|
||||||
|
2026-07-04T12:52:04.5767172Z
|
||||||
|
2026-07-04T12:52:04.5767291Z echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)"
|
||||||
|
2026-07-04T12:52:04.5767439Z # 구 배포 디렉토리 정리 (최근 5개 보존)
|
||||||
|
2026-07-04T12:52:04.5767591Z ls -1dt \$DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null \
|
||||||
|
2026-07-04T12:52:04.5767740Z | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||||
|
2026-07-04T12:52:04.5767880Z exit 0
|
||||||
|
2026-07-04T12:52:04.5768010Z fi
|
||||||
|
2026-07-04T12:52:04.5768129Z if [ "\$i" -eq "\$ATTEMPTS" ]; then
|
||||||
|
2026-07-04T12:52:04.5768263Z echo "=== FATAL: 서비스가 \$ATTEMPTS회 시도 후에도 응답하지 않음 ===" >&2
|
||||||
|
2026-07-04T12:52:04.5768450Z echo "--- 5001 listener ---" >&2
|
||||||
|
2026-07-04T12:52:04.5768587Z ss -tlnp 2>/dev/null | grep ':5001 ' >&2 || true
|
||||||
|
2026-07-04T12:52:04.5768666Z echo "--- active port file ---" >&2
|
||||||
|
2026-07-04T12:52:04.5768786Z cat "\$DEPLOY_HOME/taxbaik_port" >&2 || true
|
||||||
|
2026-07-04T12:52:04.5768919Z echo "--- 신규 앱 로그 ---" >&2
|
||||||
|
2026-07-04T12:52:04.5769051Z ACTIVE_PORT=\$(cat "\$DEPLOY_HOME/taxbaik_port" 2>/dev/null | tr -d '[:space:]' || true)
|
||||||
|
2026-07-04T12:52:04.5769205Z if [ -n "\$ACTIVE_PORT" ] && [ -s "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" ]; then
|
||||||
|
2026-07-04T12:52:04.5769305Z tail -n 80 "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" >&2
|
||||||
|
2026-07-04T12:52:04.5769387Z else
|
||||||
|
2026-07-04T12:52:04.5769507Z ls -la "\$DEPLOY_DIR" >&2 || true
|
||||||
|
2026-07-04T12:52:04.5769640Z fi
|
||||||
|
2026-07-04T12:52:04.5769755Z echo "--- proxy 로그 ---" >&2
|
||||||
|
2026-07-04T12:52:04.5769889Z tail -n 80 "\$DEPLOY_HOME/taxbaik_proxy.log" >&2 || true
|
||||||
|
2026-07-04T12:52:04.5770030Z exit 1
|
||||||
|
2026-07-04T12:52:04.5770436Z fi
|
||||||
|
2026-07-04T12:52:04.5770573Z echo " 대기 중... (\$i/\$ATTEMPTS, HTTP \$STATUS)"
|
||||||
|
2026-07-04T12:52:04.5770874Z sleep 3
|
||||||
|
2026-07-04T12:52:04.5771010Z done
|
||||||
|
2026-07-04T12:52:04.5771127Z REMOTE
|
||||||
|
2026-07-04T12:52:04.5771246Z
|
||||||
|
2026-07-04T12:52:04.5771356Z echo "✓ 배포 완료: taxbaik_${TIMESTAMP} @ $DEPLOY_HOST"
|
||||||
|
2026-07-04T12:52:04.5771479Z
|
||||||
|
2026-07-04T12:52:04.5771545Z echo "--- 실제 공개 도메인 종단 간 검증 (Nginx/Cloudflare 경유, 최대 3회 재시도) ---"
|
||||||
|
2026-07-04T12:52:04.5771637Z ROOT_URL="https://www.taxbaik.com/" \
|
||||||
|
2026-07-04T12:52:04.5771759Z ADMIN_URL="https://www.taxbaik.com/taxbaik/admin/login" \
|
||||||
|
2026-07-04T12:52:04.5771883Z PUBLIC_MARKER="백원숙 세무회계" \
|
||||||
|
2026-07-04T12:52:04.5771959Z PUBLIC_FORBIDDEN="관리자" \
|
||||||
|
2026-07-04T12:52:04.5772036Z ADMIN_MARKER="관리자 로그인" \
|
||||||
|
2026-07-04T12:52:04.5772120Z MAX_RETRIES=3 \
|
||||||
|
2026-07-04T12:52:04.5772189Z RETRY_SLEEP_SECONDS=5 \
|
||||||
|
2026-07-04T12:52:04.5772309Z bash ./scripts/taxbaik-smoke.sh
|
||||||
|
2026-07-04T12:52:04.5772408Z echo "✓ 실제 공개 도메인 전체 정상"
|
||||||
|
2026-07-04T12:52:04.5772486Z
|
||||||
|
2026-07-04T12:52:04.5772551Z send_telegram "✅ <b>TaxBaik 배포 완료</b>
|
||||||
|
2026-07-04T12:52:04.5772628Z
|
||||||
|
2026-07-04T12:52:04.5772713Z 커밋: <code>${COMMIT}</code>
|
||||||
|
2026-07-04T12:52:04.5772791Z 시간: <code>${TIMESTAMP}</code>
|
||||||
|
2026-07-04T12:52:04.5772868Z 대상: <code>${DEPLOY_HOST}</code>
|
||||||
|
2026-07-04T12:52:04.5772944Z 채널: <code>${TELEGRAM_CHAT_ID}</code>"
|
||||||
|
2026-07-04T12:52:04.5773021Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:52:04.5773130Z ::endgroup::
|
||||||
|
2026-07-04T12:52:04.6332316Z === Deploying TaxBaik dc86ccf (20260704_215204) ===
|
||||||
|
2026-07-04T12:52:05.3820913Z --- [1/5] 압축 해제 ---
|
||||||
|
2026-07-04T12:52:05.6284914Z --- [2/5] 운영 설정 검증 ---
|
||||||
|
2026-07-04T12:52:05.6285418Z --- [3/5] 마이그레이션 사전 검증 ---
|
||||||
|
2026-07-04T12:52:05.8433051Z Migration dry-run validation passed.
|
||||||
|
2026-07-04T12:52:05.8438859Z --- [4/5] Green-Blue 배포 실행 ---
|
||||||
|
2026-07-04T12:52:05.8509623Z ===== 🚀 TaxBaik Green/Blue Deployment Script =====
|
||||||
|
2026-07-04T12:52:05.8541302Z Active Port: 5003
|
||||||
|
2026-07-04T12:52:05.8541658Z Target Port: 5004
|
||||||
|
2026-07-04T12:52:05.8542047Z Deploy Directory: /home/***/deployments/taxbaik_20260704_215204
|
||||||
|
2026-07-04T12:52:05.9049695Z === Starting New App on Port 5004 ===
|
||||||
|
2026-07-04T12:52:07.9185792Z === Health Checking Port 5004 ===
|
||||||
|
2026-07-04T12:52:08.2035208Z ✓ Health check passed on port 5004 (Attempt 1/20)
|
||||||
|
2026-07-04T12:52:08.2035939Z === Switching Traffic to Port 5004 ===
|
||||||
|
2026-07-04T12:52:08.2039084Z ✓ Traffic routed to 5004 (via TaxBaik.Proxy on 5001)
|
||||||
|
2026-07-04T12:52:08.2041337Z === Stopping Old App on Port 5003 ===
|
||||||
|
2026-07-04T12:52:08.2181936Z Killing old process PID: 4117859
|
||||||
|
2026-07-04T12:52:08.2184795Z ✓ Old process terminated
|
||||||
|
2026-07-04T12:52:08.2192076Z === Cleaning Up Old Deployments ===
|
||||||
|
2026-07-04T12:52:08.2379074Z ✓ Cleanup completed
|
||||||
|
2026-07-04T12:52:08.2379676Z ===== ✅ Green/Blue Deployment Completed Successfully =====
|
||||||
|
2026-07-04T12:52:08.2385094Z --- [4.5/5] Nginx 설정 검증 ---
|
||||||
|
2026-07-04T12:52:08.2436700Z 실제 로드되는 설정 파일: /etc/nginx/sites-available/taxbaik-domains.conf
|
||||||
|
2026-07-04T12:52:08.2509737Z ✓ Nginx 설정 검증 통과 (실제 로드 파일 확인 + 포트 5001 고정 + trailing slash 없음)
|
||||||
|
2026-07-04T12:52:08.2510207Z --- [5/5] 헬스 체크 (최대 60초) ---
|
||||||
|
2026-07-04T12:52:08.2933338Z ✓ [1/6] 헬스 체크 완료
|
||||||
|
2026-07-04T12:52:08.3102145Z ✓ [2/6] 메인 페이지 로드 완료
|
||||||
|
2026-07-04T12:52:08.3324117Z ✓ [3/6] CSS 파일 로드 완료
|
||||||
|
2026-07-04T12:52:08.3445763Z ✓ [4/6] 버전 정보 확인 완료
|
||||||
|
2026-07-04T12:52:08.3544241Z ✓ [5/6] 5001 프록시 확인 완료
|
||||||
|
2026-07-04T12:52:08.3650602Z ✓ [6/6] 관리자 페이지 로드 완료
|
||||||
|
2026-07-04T12:52:08.3651465Z ✓ 서비스 정상 (시도 1/20)
|
||||||
|
2026-07-04T12:52:08.3716219Z ✓ 배포 완료: taxbaik_20260704_215204 @ ***
|
||||||
|
2026-07-04T12:52:08.3716849Z --- 실제 공개 도메인 종단 간 검증 (Nginx/Cloudflare 경유, 최대 3회 재시도) ---
|
||||||
|
2026-07-04T12:52:10.5585268Z retrying... (1/3)
|
||||||
|
2026-07-04T12:52:10.5585809Z ✗ https://www.taxbaik.com/ -> body missing required marker: 백원숙 세무회계
|
||||||
|
2026-07-04T12:52:17.6149854Z ✗ https://www.taxbaik.com/ -> body missing required marker: 백원숙 세무회계
|
||||||
|
2026-07-04T12:52:17.6152469Z retrying... (2/3)
|
||||||
|
2026-07-04T12:52:24.5668073Z ✗ https://www.taxbaik.com/ -> body missing required marker: 백원숙 세무회계
|
||||||
|
2026-07-04T12:52:24.5668594Z ✗ smoke verification failed
|
||||||
|
2026-07-04T12:52:24.8198071Z ❌ Failure - Main Deploy & verify on server
|
||||||
|
2026-07-04T12:52:24.8336834Z exitcode '1': failure
|
||||||
|
2026-07-04T12:52:24.8630234Z evaluating expression 'success()'
|
||||||
|
2026-07-04T12:52:24.8631375Z expression 'success()' evaluated to 'false'
|
||||||
|
2026-07-04T12:52:24.8631585Z Skipping step 'Setup .NET' due to 'success()'
|
||||||
|
2026-07-04T12:52:24.8839000Z evaluating expression 'always()'
|
||||||
|
2026-07-04T12:52:24.8839624Z expression 'always()' evaluated to 'true'
|
||||||
|
2026-07-04T12:52:24.8839784Z ⭐ Run Post Checkout code
|
||||||
|
2026-07-04T12:52:24.8839977Z Writing entry to tarball workflow/outputcmd.txt len:0
|
||||||
|
2026-07-04T12:52:24.8840143Z Writing entry to tarball workflow/statecmd.txt len:0
|
||||||
|
2026-07-04T12:52:24.8840244Z Writing entry to tarball workflow/pathcmd.txt len:0
|
||||||
|
2026-07-04T12:52:24.8840333Z Writing entry to tarball workflow/envs.txt len:0
|
||||||
|
2026-07-04T12:52:24.8840411Z Writing entry to tarball workflow/SUMMARY.md len:0
|
||||||
|
2026-07-04T12:52:24.8840523Z Extracting content to '/var/run/act'
|
||||||
|
2026-07-04T12:52:24.8865590Z run post step for 'Checkout code'
|
||||||
|
2026-07-04T12:52:24.8866457Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
|
||||||
|
2026-07-04T12:52:24.9112212Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
|
||||||
|
2026-07-04T12:52:24.9112610Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
|
||||||
|
2026-07-04T12:52:24.9113018Z Working directory '/workspace/***/taxbaik'
|
||||||
|
2026-07-04T12:52:25.1573902Z [command]/usr/bin/git version
|
||||||
|
2026-07-04T12:52:25.1613809Z git version 2.54.0
|
||||||
|
2026-07-04T12:52:25.1650128Z ***
|
||||||
|
2026-07-04T12:52:25.1677928Z Temporarily overriding HOME='/tmp/daa4130a-b951-44ca-9bf6-2a832ad8d592' before making global git config changes
|
||||||
|
2026-07-04T12:52:25.1678247Z Adding repository directory to the temporary git global config as a safe directory
|
||||||
|
2026-07-04T12:52:25.1680429Z [command]/usr/bin/git config --global --add safe.directory /workspace/***/taxbaik
|
||||||
|
2026-07-04T12:52:25.1715854Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
|
||||||
|
2026-07-04T12:52:25.1760614Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
|
||||||
|
2026-07-04T12:52:25.1997273Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/gitea\:3000\/\.extraheader
|
||||||
|
2026-07-04T12:52:25.2020534Z http.http://gitea:3000/.extraheader
|
||||||
|
2026-07-04T12:52:25.2034486Z [command]/usr/bin/git config --local --unset-all http.http://gitea:3000/.extraheader
|
||||||
|
2026-07-04T12:52:25.2067074Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/gitea\:3000\/\.extraheader' && git config --local --unset-all 'http.http://gitea:3000/.extraheader' || :"
|
||||||
|
2026-07-04T12:52:25.2313893Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
|
||||||
|
2026-07-04T12:52:25.2344461Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
|
||||||
|
2026-07-04T12:52:25.2706764Z ✅ Success - Post Checkout code
|
||||||
|
2026-07-04T12:52:25.2810331Z Cleaning up container for job build-and-deploy
|
||||||
|
2026-07-04T12:52:25.6754704Z Removed container: 7fdc784e6b95f4a2f3a7139414bcbaeff9754c0efd147950d7f0357c1b99e6c6
|
||||||
|
2026-07-04T12:52:25.6765166Z 🐳 docker volume rm GITEA-ACTIONS-TASK-1550-WORKFLOW-TaxBaik-CI-CD-JOB-build-and-de-d1b621a7a13ad682fe29533e6a28b6835590d0898313b1b9fc5be41b5870455c
|
||||||
|
2026-07-04T12:52:25.7686731Z 🐳 docker volume rm GITEA-ACTIONS-TASK-1550-WORKFLOW-TaxBaik-CI-CD-JOB-build-and-de-d1b621a7a13ad682fe29533e6a28b6835590d0898313b1b9fc5be41b5870455c-env
|
||||||
|
2026-07-04T12:52:25.8187706Z 🏁 Job failed
|
||||||
|
2026-07-04T12:52:25.8314043Z Job 'build-and-deploy' failed
|
||||||
+944
@@ -0,0 +1,944 @@
|
|||||||
|
2026-07-04T12:55:56.4615511Z hz-prod-runner-2(version:v0.6.1) received task 1552 of job build-and-deploy, be triggered by event: push
|
||||||
|
2026-07-04T12:55:56.4637712Z workflow prepared
|
||||||
|
2026-07-04T12:55:56.4638531Z evaluating expression 'success()'
|
||||||
|
2026-07-04T12:55:56.4639250Z expression 'success()' evaluated to 'true'
|
||||||
|
2026-07-04T12:55:56.4639410Z 🚀 Start image=docker.gitea.com/runner-images:ubuntu-latest
|
||||||
|
2026-07-04T12:55:56.4717320Z 🐳 docker pull image=docker.gitea.com/runner-images:ubuntu-latest platform= username= forcePull=false
|
||||||
|
2026-07-04T12:55:56.4717578Z 🐳 docker pull docker.gitea.com/runner-images:ubuntu-latest
|
||||||
|
2026-07-04T12:55:56.4972676Z Image exists? true
|
||||||
|
2026-07-04T12:55:56.5450519Z 🐳 docker create image=docker.gitea.com/runner-images:ubuntu-latest platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_default"
|
||||||
|
2026-07-04T12:55:56.6404647Z Created container name=GITEA-ACTIONS-TASK-1552-WORKFLOW-TaxBaik-CI-CD-JOB-build-and-de-ab2d1e70739a3063f448d1e9d12be855fbd02428b801ec092546b895bdf5c88d id=545d203ee9994ebf9eca9357f948b418a29c0a80f13c2d69e6b64ca3d86d4b5b from image docker.gitea.com/runner-images:ubuntu-latest (platform: )
|
||||||
|
2026-07-04T12:55:56.6405073Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
|
||||||
|
2026-07-04T12:55:56.6405231Z 🐳 docker run image=docker.gitea.com/runner-images:ubuntu-latest platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_default"
|
||||||
|
2026-07-04T12:55:56.6405352Z Starting container: 545d203ee9994ebf9eca9357f948b418a29c0a80f13c2d69e6b64ca3d86d4b5b
|
||||||
|
2026-07-04T12:55:56.8160318Z Started container: 545d203ee9994ebf9eca9357f948b418a29c0a80f13c2d69e6b64ca3d86d4b5b
|
||||||
|
2026-07-04T12:55:56.9278346Z Writing entry to tarball workflow/event.json len:4744
|
||||||
|
2026-07-04T12:55:56.9278912Z Writing entry to tarball workflow/envs.txt len:0
|
||||||
|
2026-07-04T12:55:56.9279089Z Extracting content to '/var/run/act/'
|
||||||
|
2026-07-04T12:55:56.9554266Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
|
||||||
|
2026-07-04T12:55:56.9554640Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
|
||||||
|
2026-07-04T12:55:57.7778122Z Unable to pull refs/heads/v4: non-fast-forward update
|
||||||
|
2026-07-04T12:55:57.7778594Z Cloned https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
|
||||||
|
2026-07-04T12:55:57.7935665Z Checked out v4
|
||||||
|
2026-07-04T12:55:57.8047363Z ☁ git clone 'https://github.com/actions/setup-dotnet' # ref=v4
|
||||||
|
2026-07-04T12:55:57.8048149Z cloning https://github.com/actions/setup-dotnet to /root/.cache/act/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336
|
||||||
|
2026-07-04T12:55:58.3258730Z Unable to pull refs/heads/v4: worktree contains unstaged changes
|
||||||
|
2026-07-04T12:55:58.3259204Z Cloned https://github.com/actions/setup-dotnet to /root/.cache/act/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336
|
||||||
|
2026-07-04T12:55:58.3442763Z Checked out v4
|
||||||
|
2026-07-04T12:55:58.3705741Z evaluating expression ''
|
||||||
|
2026-07-04T12:55:58.3706586Z expression '' evaluated to 'true'
|
||||||
|
2026-07-04T12:55:58.3706713Z ⭐ Run Main Checkout code
|
||||||
|
2026-07-04T12:55:58.3706901Z Writing entry to tarball workflow/outputcmd.txt len:0
|
||||||
|
2026-07-04T12:55:58.3707051Z Writing entry to tarball workflow/statecmd.txt len:0
|
||||||
|
2026-07-04T12:55:58.3707153Z Writing entry to tarball workflow/pathcmd.txt len:0
|
||||||
|
2026-07-04T12:55:58.3707264Z Writing entry to tarball workflow/envs.txt len:0
|
||||||
|
2026-07-04T12:55:58.3707351Z Writing entry to tarball workflow/SUMMARY.md len:0
|
||||||
|
2026-07-04T12:55:58.3707451Z Extracting content to '/var/run/act'
|
||||||
|
2026-07-04T12:55:58.3748068Z ::group::Run Checkout code
|
||||||
|
2026-07-04T12:55:58.9258868Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
|
||||||
|
2026-07-04T12:55:58.9264682Z Syncing repository: ***/taxbaik
|
||||||
|
2026-07-04T12:55:58.9272267Z ::group::Getting Git version info
|
||||||
|
2026-07-04T12:55:58.9272441Z Working directory is '/workspace/***/taxbaik'
|
||||||
|
2026-07-04T12:55:58.9311321Z [command]/usr/bin/git version
|
||||||
|
2026-07-04T12:55:58.9387081Z git version 2.54.0
|
||||||
|
2026-07-04T12:55:58.9458330Z ::endgroup::
|
||||||
|
2026-07-04T12:55:58.9480820Z Temporarily overriding HOME='/tmp/2e7b254c-3300-4ed7-b3ac-5e0628723e83' before making global git config changes
|
||||||
|
2026-07-04T12:55:58.9482460Z Adding repository directory to the temporary git global config as a safe directory
|
||||||
|
2026-07-04T12:55:58.9497103Z [command]/usr/bin/git config --global --add safe.directory /workspace/***/taxbaik
|
||||||
|
2026-07-04T12:55:58.9544269Z Deleting the contents of '/workspace/***/taxbaik'
|
||||||
|
2026-07-04T12:55:58.9576838Z ::group::Initializing the repository
|
||||||
|
2026-07-04T12:55:58.9588388Z [command]/usr/bin/git init /workspace/***/taxbaik
|
||||||
|
2026-07-04T12:55:58.9715154Z hint: Using 'master' as the name for the initial branch. This default branch name
|
||||||
|
2026-07-04T12:55:58.9728629Z hint: will change to "main" in Git 3.0. To configure the initial branch name
|
||||||
|
2026-07-04T12:55:58.9729225Z hint: to use in all of your new repositories, which will suppress this warning,
|
||||||
|
2026-07-04T12:55:58.9729399Z hint: call:
|
||||||
|
2026-07-04T12:55:58.9729772Z hint:
|
||||||
|
2026-07-04T12:55:58.9729859Z hint: git config --global init.defaultBranch <name>
|
||||||
|
2026-07-04T12:55:58.9729946Z hint:
|
||||||
|
2026-07-04T12:55:58.9730097Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
|
||||||
|
2026-07-04T12:55:58.9730184Z hint: 'development'. The just-created branch can be renamed via this command:
|
||||||
|
2026-07-04T12:55:58.9730499Z hint:
|
||||||
|
2026-07-04T12:55:58.9730665Z hint: git branch -m <name>
|
||||||
|
2026-07-04T12:55:58.9730770Z hint:
|
||||||
|
2026-07-04T12:55:58.9730838Z hint: Disable this message with "git config set advice.defaultBranchName false"
|
||||||
|
2026-07-04T12:55:58.9730925Z Initialized empty Git repository in /workspace/***/taxbaik/.git/
|
||||||
|
2026-07-04T12:55:58.9834190Z [command]/usr/bin/git remote add origin http://gitea:3000/***/taxbaik
|
||||||
|
2026-07-04T12:55:58.9916667Z ::endgroup::
|
||||||
|
2026-07-04T12:55:58.9917174Z ::group::Disabling automatic garbage collection
|
||||||
|
2026-07-04T12:55:58.9928398Z [command]/usr/bin/git config --local gc.auto 0
|
||||||
|
2026-07-04T12:55:58.9992137Z ::endgroup::
|
||||||
|
2026-07-04T12:55:58.9992625Z ::group::Setting up auth
|
||||||
|
2026-07-04T12:55:58.9992804Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
|
||||||
|
2026-07-04T12:55:59.0031077Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
|
||||||
|
2026-07-04T12:55:59.0510927Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/gitea\:3000\/\.extraheader
|
||||||
|
2026-07-04T12:55:59.0554440Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/gitea\:3000\/\.extraheader' && git config --local --unset-all 'http.http://gitea:3000/.extraheader' || :"
|
||||||
|
2026-07-04T12:55:59.0907354Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
|
||||||
|
2026-07-04T12:55:59.0920983Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
|
||||||
|
2026-07-04T12:55:59.1162021Z [command]/usr/bin/git config --local http.http://gitea:3000/.extraheader AUTHORIZATION: basic ***
|
||||||
|
2026-07-04T12:55:59.1196020Z ::endgroup::
|
||||||
|
2026-07-04T12:55:59.1196454Z ::group::Fetching the repository
|
||||||
|
2026-07-04T12:55:59.1210231Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +c00d237a4d2716bc22cf06f053a807cd7a45f200:refs/remotes/origin/master
|
||||||
|
2026-07-04T12:56:01.5767577Z From http://gitea:3000/***/taxbaik
|
||||||
|
2026-07-04T12:56:01.5768075Z * [new ref] c00d237a4d2716bc22cf06f053a807cd7a45f200 -> origin/master
|
||||||
|
2026-07-04T12:56:01.5798239Z ::endgroup::
|
||||||
|
2026-07-04T12:56:01.5798671Z ::group::Determining the checkout info
|
||||||
|
2026-07-04T12:56:01.5801467Z ::endgroup::
|
||||||
|
2026-07-04T12:56:01.5808449Z [command]/usr/bin/git sparse-checkout disable
|
||||||
|
2026-07-04T12:56:01.5862591Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
|
||||||
|
2026-07-04T12:56:01.5900860Z ::group::Checking out the ref
|
||||||
|
2026-07-04T12:56:01.5907120Z [command]/usr/bin/git checkout --progress --force -B master refs/remotes/origin/master
|
||||||
|
2026-07-04T12:56:02.4699566Z Reset branch 'master'
|
||||||
|
2026-07-04T12:56:02.4847933Z branch 'master' set up to track 'origin/master'.
|
||||||
|
2026-07-04T12:56:02.4849782Z ::endgroup::
|
||||||
|
2026-07-04T12:56:02.4886004Z [command]/usr/bin/git log -1 --format=%H
|
||||||
|
2026-07-04T12:56:02.4952462Z c00d237a4d2716bc22cf06f053a807cd7a45f200
|
||||||
|
2026-07-04T12:56:02.4994673Z ::remove-matcher owner=checkout-git::
|
||||||
|
2026-07-04T12:56:02.5182273Z ::endgroup::
|
||||||
|
2026-07-04T12:56:02.5668549Z ::group::Run Setup .NET
|
||||||
|
2026-07-04T12:56:02.5669115Z with:
|
||||||
|
2026-07-04T12:56:02.5669224Z dotnet-version: 10.0
|
||||||
|
2026-07-04T12:56:03.6548348Z (node:141) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
|
||||||
|
2026-07-04T12:56:03.6548980Z (Use `node --trace-deprecation ...` to show where the warning was created)
|
||||||
|
2026-07-04T12:56:03.6690381Z [command]/run/act/actions/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336/externals/install-dotnet.sh --skip-non-versioned-files --runtime dotnet --channel LTS
|
||||||
|
2026-07-04T12:56:04.3566016Z dotnet-install: Attempting to download using aka.ms link https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.9/dotnet-runtime-10.0.9-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:56:04.7466768Z dotnet-install: Remote file https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.9/dotnet-runtime-10.0.9-linux-x64.tar.gz size is 36606251 bytes.
|
||||||
|
2026-07-04T12:56:04.7467352Z dotnet-install: Extracting archive from https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.9/dotnet-runtime-10.0.9-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:56:06.0871997Z dotnet-install: Downloaded file size is 36606251 bytes.
|
||||||
|
2026-07-04T12:56:06.0872663Z dotnet-install: The remote and local file sizes are equal.
|
||||||
|
2026-07-04T12:56:06.1077477Z dotnet-install: Installed version is 10.0.9
|
||||||
|
2026-07-04T12:56:06.1177302Z dotnet-install: Adding to current process PATH: `/usr/share/dotnet`. Note: This change will be visible only when sourcing script.
|
||||||
|
2026-07-04T12:56:06.1177905Z dotnet-install: Note that the script does not resolve dependencies during installation.
|
||||||
|
2026-07-04T12:56:06.1178042Z dotnet-install: To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the "Dependencies" section.
|
||||||
|
2026-07-04T12:56:06.1178186Z dotnet-install: Installation finished successfully.
|
||||||
|
2026-07-04T12:56:06.1212465Z [command]/run/act/actions/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336/externals/install-dotnet.sh --skip-non-versioned-files --channel 10.0
|
||||||
|
2026-07-04T12:56:06.5612026Z dotnet-install: Attempting to download using aka.ms link https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.301/dotnet-sdk-10.0.301-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:56:07.8777550Z dotnet-install: Remote file https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.301/dotnet-sdk-10.0.301-linux-x64.tar.gz size is 235086718 bytes.
|
||||||
|
2026-07-04T12:56:07.8778105Z dotnet-install: Extracting archive from https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.301/dotnet-sdk-10.0.301-linux-x64.tar.gz
|
||||||
|
2026-07-04T12:56:15.9574962Z dotnet-install: Downloaded file size is 235086718 bytes.
|
||||||
|
2026-07-04T12:56:15.9575450Z dotnet-install: The remote and local file sizes are equal.
|
||||||
|
2026-07-04T12:56:16.4216427Z dotnet-install: Installed version is 10.0.301
|
||||||
|
2026-07-04T12:56:16.4608895Z dotnet-install: Adding to current process PATH: `/usr/share/dotnet`. Note: This change will be visible only when sourcing script.
|
||||||
|
2026-07-04T12:56:16.4609480Z dotnet-install: Note that the script does not resolve dependencies during installation.
|
||||||
|
2026-07-04T12:56:16.4609633Z dotnet-install: To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the "Dependencies" section.
|
||||||
|
2026-07-04T12:56:16.4609773Z dotnet-install: Installation finished successfully.
|
||||||
|
2026-07-04T12:56:16.4610111Z ##[add-matcher]/run/act/actions/2d637816dd86ec9ff59dad9ec3547bf90b88133b3029538a91ec96ac7f316336/.github/csc.json
|
||||||
|
2026-07-04T12:56:16.4838195Z ::endgroup::
|
||||||
|
2026-07-04T12:56:16.6836471Z ::group::Run dotnet restore src/TaxBaik.sln
|
||||||
|
2026-07-04T12:56:16.6836809Z dotnet restore src/TaxBaik.sln
|
||||||
|
2026-07-04T12:56:16.6836915Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:56:16.6837057Z ::endgroup::
|
||||||
|
2026-07-04T12:56:17.1062566Z
|
||||||
|
2026-07-04T12:56:17.1063187Z Welcome to .NET 10.0!
|
||||||
|
2026-07-04T12:56:17.1063335Z ---------------------
|
||||||
|
2026-07-04T12:56:17.1063423Z SDK Version: 10.0.301
|
||||||
|
2026-07-04T12:56:17.1063509Z
|
||||||
|
2026-07-04T12:56:17.1063586Z Telemetry
|
||||||
|
2026-07-04T12:56:17.1063723Z ---------
|
||||||
|
2026-07-04T12:56:17.1063814Z The .NET tools collect usage data in order to help us improve your experience. It is collected by Microsoft and shared with the community. You can opt-out of telemetry by setting the DOTNET_CLI_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell.
|
||||||
|
2026-07-04T12:56:17.1063988Z
|
||||||
|
2026-07-04T12:56:17.1064082Z Read more about .NET CLI Tools telemetry: https://aka.ms/dotnet-cli-telemetry
|
||||||
|
2026-07-04T12:56:17.7956740Z
|
||||||
|
2026-07-04T12:56:17.7957774Z ----------------
|
||||||
|
2026-07-04T12:56:17.7958124Z Installed an ASP.NET Core HTTPS development certificate.
|
||||||
|
2026-07-04T12:56:17.7958509Z To trust the certificate, run 'dotnet dev-certs https --trust'
|
||||||
|
2026-07-04T12:56:17.7958698Z Learn about HTTPS: https://aka.ms/dotnet-https
|
||||||
|
2026-07-04T12:56:17.7958830Z
|
||||||
|
2026-07-04T12:56:17.7959148Z ----------------
|
||||||
|
2026-07-04T12:56:17.7959321Z Write your first app: https://aka.ms/dotnet-hello-world
|
||||||
|
2026-07-04T12:56:17.7959481Z Find out what's new: https://aka.ms/dotnet-whats-new
|
||||||
|
2026-07-04T12:56:17.7959628Z Explore documentation: https://aka.ms/dotnet-docs
|
||||||
|
2026-07-04T12:56:17.7959926Z Report issues and find source on GitHub: https://github.com/dotnet/core
|
||||||
|
2026-07-04T12:56:17.7960053Z Use 'dotnet --help' to see available commands or visit: https://aka.ms/dotnet-cli
|
||||||
|
2026-07-04T12:56:17.7962047Z --------------------------------------------------------------------------------------
|
||||||
|
2026-07-04T12:56:20.9087685Z Determining projects to restore...
|
||||||
|
2026-07-04T12:56:30.8926957Z Restored /workspace/***/taxbaik/src/TaxBaik.Infrastructure/TaxBaik.Infrastructure.csproj (in 6.9 sec).
|
||||||
|
2026-07-04T12:56:30.9025803Z Restored /workspace/***/taxbaik/src/TaxBaik.Web/TaxBaik.Web.csproj (in 6.89 sec).
|
||||||
|
2026-07-04T12:56:30.9698454Z Restored /workspace/***/taxbaik/src/TaxBaik.Domain/TaxBaik.Domain.csproj (in 2 ms).
|
||||||
|
2026-07-04T12:56:31.4466553Z Restored /workspace/***/taxbaik/src/TaxBaik.Application/TaxBaik.Application.csproj (in 444 ms).
|
||||||
|
2026-07-04T12:56:37.8960870Z Restored /workspace/***/taxbaik/src/TaxBaik.Application.Tests/TaxBaik.Application.Tests.csproj (in 6.43 sec).
|
||||||
|
2026-07-04T12:56:38.1253187Z Restored /workspace/***/taxbaik/src/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj (in 7.21 sec).
|
||||||
|
2026-07-04T12:56:38.3047389Z ::group::Run dotnet build src/TaxBaik.sln -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:56:38.3047747Z dotnet build src/TaxBaik.sln -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:56:38.3047878Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:56:38.3047984Z ::endgroup::
|
||||||
|
2026-07-04T12:56:47.5523499Z TaxBaik.Domain -> /workspace/***/taxbaik/src/TaxBaik.Domain/bin/Release/net10.0/TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:56:53.8139393Z TaxBaik.Application -> /workspace/***/taxbaik/src/TaxBaik.Application/bin/Release/net10.0/TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:56:56.3263776Z TaxBaik.Infrastructure -> /workspace/***/taxbaik/src/TaxBaik.Infrastructure/bin/Release/net10.0/TaxBaik.Infrastructure.dll
|
||||||
|
2026-07-04T12:57:41.2956856Z TaxBaik.Web.Client -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Debug/net10.0/TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:57:41.2957970Z TaxBaik.Web.Client (Blazor output) -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Debug/net10.0/wwwroot
|
||||||
|
2026-07-04T12:57:54.4727116Z TaxBaik.Web -> /workspace/***/taxbaik/src/TaxBaik.Web/bin/Release/net10.0/TaxBaik.Web.dll
|
||||||
|
2026-07-04T12:57:56.1020240Z TaxBaik.Application.Tests -> /workspace/***/taxbaik/src/TaxBaik.Application.Tests/bin/Release/net10.0/TaxBaik.Application.Tests.dll
|
||||||
|
2026-07-04T12:57:56.1185508Z
|
||||||
|
2026-07-04T12:57:56.1186502Z Build succeeded.
|
||||||
|
2026-07-04T12:57:56.1187397Z 0 Warning(s)
|
||||||
|
2026-07-04T12:57:56.1187565Z 0 Error(s)
|
||||||
|
2026-07-04T12:57:56.1187859Z
|
||||||
|
2026-07-04T12:57:56.1187992Z Time Elapsed 00:01:17.28
|
||||||
|
2026-07-04T12:57:56.3442702Z ::group::Run dotnet test src/TaxBaik.sln -c Release --no-build
|
||||||
|
2026-07-04T12:57:56.3443052Z dotnet test src/TaxBaik.sln -c Release --no-build
|
||||||
|
2026-07-04T12:57:56.3443177Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:57:56.3443278Z ::endgroup::
|
||||||
|
2026-07-04T12:58:00.2708385Z Test run for /workspace/***/taxbaik/src/TaxBaik.Application.Tests/bin/Release/net10.0/TaxBaik.Application.Tests.dll (.NETCoreApp,Version=v10.0)
|
||||||
|
2026-07-04T12:58:01.4967596Z A total of 1 test files matched the specified pattern.
|
||||||
|
2026-07-04T12:58:08.9229118Z
|
||||||
|
2026-07-04T12:58:08.9419846Z Passed! - Failed: 0, Passed: 26, Skipped: 0, Total: 26, Duration: 547 ms - TaxBaik.Application.Tests.dll (net10.0)
|
||||||
|
2026-07-04T12:58:09.4829427Z ::group::Run set -e
|
||||||
|
2026-07-04T12:58:09.4829769Z set -e
|
||||||
|
2026-07-04T12:58:09.4829932Z mkdir -p ./publish-logs
|
||||||
|
2026-07-04T12:58:09.4830025Z web_log="./publish-logs/publish-web.log"
|
||||||
|
2026-07-04T12:58:09.4830116Z start=$(date +%s)
|
||||||
|
2026-07-04T12:58:09.4830216Z # Web.Client needs a Release static-web-assets manifest for Web publish.
|
||||||
|
2026-07-04T12:58:09.4830322Z # Build it explicitly so publish can reuse the prepared outputs.
|
||||||
|
2026-07-04T12:58:09.4830419Z dotnet build src/TaxBaik.Web.Client/TaxBaik.Web.Client.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:58:09.4830524Z # Build the Web host in Release as well so publish has the same inputs
|
||||||
|
2026-07-04T12:58:09.4830643Z # the server uses in production.
|
||||||
|
2026-07-04T12:58:09.4830726Z dotnet build src/TaxBaik.Web/TaxBaik.Web.csproj -c Release --no-restore -p:ContinuousIntegrationBuild=true
|
||||||
|
2026-07-04T12:58:09.4830819Z echo "--- Web.Client Release artifacts ---"
|
||||||
|
2026-07-04T12:58:09.4830932Z ls -la src/TaxBaik.Web.Client/bin/Release/net10.0 || true
|
||||||
|
2026-07-04T12:58:09.4831022Z ls -la src/TaxBaik.Web.Client/obj/Release/net10.0 || true
|
||||||
|
2026-07-04T12:58:09.4831103Z if ! dotnet publish src/TaxBaik.Web/ \
|
||||||
|
2026-07-04T12:58:09.4831188Z -c Release \
|
||||||
|
2026-07-04T12:58:09.4831285Z -o ./publish \
|
||||||
|
2026-07-04T12:58:09.4831373Z --no-restore \
|
||||||
|
2026-07-04T12:58:09.4831449Z -p:SelfContained=false \
|
||||||
|
2026-07-04T12:58:09.4831732Z -p:PublishReadyToRun=false \
|
||||||
|
2026-07-04T12:58:09.4831866Z -p:PerformanceSummary=true \
|
||||||
|
2026-07-04T12:58:09.4831988Z -clp:Summary \
|
||||||
|
2026-07-04T12:58:09.4832113Z -bl:"./publish-logs/publish-web.binlog" >"$web_log" 2>&1; then
|
||||||
|
2026-07-04T12:58:09.4832289Z echo "=== Publish Web failed; tailing log ==="
|
||||||
|
2026-07-04T12:58:09.4832414Z tail -n 120 "$web_log" || true
|
||||||
|
2026-07-04T12:58:09.4832535Z exit 1
|
||||||
|
2026-07-04T12:58:09.4832795Z fi
|
||||||
|
2026-07-04T12:58:09.4832909Z end=$(date +%s)
|
||||||
|
2026-07-04T12:58:09.4833051Z echo "✓ Publish Web elapsed: $((end - start))s"
|
||||||
|
2026-07-04T12:58:09.4833195Z ls -lh ./publish-logs/publish-web.binlog
|
||||||
|
2026-07-04T12:58:09.4833380Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T12:58:09.4833512Z ::endgroup::
|
||||||
|
2026-07-04T12:58:15.0626337Z TaxBaik.Domain -> /workspace/***/taxbaik/src/TaxBaik.Domain/bin/Release/net10.0/TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:58:16.0646936Z TaxBaik.Application -> /workspace/***/taxbaik/src/TaxBaik.Application/bin/Release/net10.0/TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:58:43.1637915Z TaxBaik.Web.Client -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:58:43.1638555Z TaxBaik.Web.Client (Blazor output) -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/wwwroot
|
||||||
|
2026-07-04T12:58:43.2125386Z
|
||||||
|
2026-07-04T12:58:43.2137113Z Build succeeded.
|
||||||
|
2026-07-04T12:58:43.2137638Z 0 Warning(s)
|
||||||
|
2026-07-04T12:58:43.2137752Z 0 Error(s)
|
||||||
|
2026-07-04T12:58:43.2137837Z
|
||||||
|
2026-07-04T12:58:43.2137937Z Time Elapsed 00:00:31.72
|
||||||
|
2026-07-04T12:58:46.1821154Z TaxBaik.Domain -> /workspace/***/taxbaik/src/TaxBaik.Domain/bin/Release/net10.0/TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:58:46.3588108Z TaxBaik.Infrastructure -> /workspace/***/taxbaik/src/TaxBaik.Infrastructure/bin/Release/net10.0/TaxBaik.Infrastructure.dll
|
||||||
|
2026-07-04T12:58:47.0759678Z TaxBaik.Application -> /workspace/***/taxbaik/src/TaxBaik.Application/bin/Release/net10.0/TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:58:51.7209180Z TaxBaik.Web.Client -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:58:51.7249991Z TaxBaik.Web.Client (Blazor output) -> /workspace/***/taxbaik/src/TaxBaik.Web.Client/bin/Release/net10.0/wwwroot
|
||||||
|
2026-07-04T12:59:06.8811243Z TaxBaik.Web -> /workspace/***/taxbaik/src/TaxBaik.Web/bin/Release/net10.0/TaxBaik.Web.dll
|
||||||
|
2026-07-04T12:59:06.9584686Z
|
||||||
|
2026-07-04T12:59:06.9595915Z Build succeeded.
|
||||||
|
2026-07-04T12:59:06.9663358Z 0 Warning(s)
|
||||||
|
2026-07-04T12:59:06.9664940Z 0 Error(s)
|
||||||
|
2026-07-04T12:59:06.9667639Z
|
||||||
|
2026-07-04T12:59:06.9727168Z Time Elapsed 00:00:23.15
|
||||||
|
2026-07-04T12:59:07.2928672Z --- Web.Client Release artifacts ---
|
||||||
|
2026-07-04T12:59:07.2995244Z total 42056
|
||||||
|
2026-07-04T12:59:07.2995608Z drwxr-xr-x 3 root root 20480 Jul 4 12:58 .
|
||||||
|
2026-07-04T12:59:07.2995883Z drwxr-xr-x 3 root root 4096 Jul 4 12:58 ..
|
||||||
|
2026-07-04T12:59:07.2996012Z -rwxr--r-- 1 root root 55632 May 20 20:19 Microsoft.AspNetCore.Authorization.dll
|
||||||
|
2026-07-04T12:59:07.2996353Z -rwxr--r-- 1 root root 34128 May 20 20:19 Microsoft.AspNetCore.Components.Authorization.dll
|
||||||
|
2026-07-04T12:59:07.2996466Z -rwxr--r-- 1 root root 47952 May 20 20:19 Microsoft.AspNetCore.Components.Forms.dll
|
||||||
|
2026-07-04T12:59:07.2996556Z -rwxr--r-- 1 root root 189264 May 20 20:19 Microsoft.AspNetCore.Components.Web.dll
|
||||||
|
2026-07-04T12:59:07.2996643Z -rwxr--r-- 1 root root 166736 May 20 20:20 Microsoft.AspNetCore.Components.WebAssembly.dll
|
||||||
|
2026-07-04T12:59:07.2996769Z -rwxr--r-- 1 root root 399184 May 20 20:19 Microsoft.AspNetCore.Components.dll
|
||||||
|
2026-07-04T12:59:07.2996860Z -rwxr--r-- 1 root root 16208 May 20 20:18 Microsoft.AspNetCore.Metadata.dll
|
||||||
|
2026-07-04T12:59:07.2997106Z -rwxr--r-- 1 root root 19248 Dec 12 2025 Microsoft.Bcl.Cryptography.dll
|
||||||
|
2026-07-04T12:59:07.2997201Z -rwxr--r-- 1 root root 311632 May 20 18:30 Microsoft.CSharp.dll
|
||||||
|
2026-07-04T12:59:07.2997298Z -rwxr--r-- 1 root root 38192 Oct 24 2025 Microsoft.Extensions.Caching.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.2997390Z -rwxr--r-- 1 root root 28496 May 20 19:29 Microsoft.Extensions.Configuration.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.2997500Z -rwxr--r-- 1 root root 43344 May 20 19:32 Microsoft.Extensions.Configuration.Binder.dll
|
||||||
|
2026-07-04T12:59:07.2997593Z -rwxr--r-- 1 root root 28496 May 20 19:34 Microsoft.Extensions.Configuration.FileExtensions.dll
|
||||||
|
2026-07-04T12:59:07.2997681Z -rwxr--r-- 1 root root 27984 May 20 19:36 Microsoft.Extensions.Configuration.Json.dll
|
||||||
|
2026-07-04T12:59:07.2997903Z -rwxr--r-- 1 root root 44880 May 20 19:29 Microsoft.Extensions.Configuration.dll
|
||||||
|
2026-07-04T12:59:07.2997993Z -rwxr--r-- 1 root root 65872 May 20 19:29 Microsoft.Extensions.DependencyInjection.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.2998083Z -rwxr--r-- 1 root root 95568 May 20 19:30 Microsoft.Extensions.DependencyInjection.dll
|
||||||
|
2026-07-04T12:59:07.2998186Z -rwxr--r-- 1 root root 31056 May 20 19:32 Microsoft.Extensions.Diagnostics.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.2998272Z -rwxr--r-- 1 root root 36176 May 20 19:34 Microsoft.Extensions.Diagnostics.dll
|
||||||
|
2026-07-04T12:59:07.2998461Z -rwxr--r-- 1 root root 23376 May 20 19:29 Microsoft.Extensions.FileProviders.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.2998731Z -rwxr--r-- 1 root root 45392 May 20 19:33 Microsoft.Extensions.FileProviders.Physical.dll
|
||||||
|
2026-07-04T12:59:07.2998823Z -rwxr--r-- 1 root root 47952 May 20 19:30 Microsoft.Extensions.FileSystemGlobbing.dll
|
||||||
|
2026-07-04T12:59:07.2998932Z -rwxr--r-- 1 root root 93008 May 20 19:37 Microsoft.Extensions.Http.dll
|
||||||
|
2026-07-04T12:59:07.2999016Z -rwxr--r-- 1 root root 19576 Mar 25 2023 Microsoft.Extensions.Localization.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.2999099Z -rwxr--r-- 1 root root 31872 Mar 25 2023 Microsoft.Extensions.Localization.dll
|
||||||
|
2026-07-04T12:59:07.2999184Z -rwxr--r-- 1 root root 66896 May 20 19:29 Microsoft.Extensions.Logging.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.2999292Z -rwxr--r-- 1 root root 51536 May 20 19:33 Microsoft.Extensions.Logging.dll
|
||||||
|
2026-07-04T12:59:07.2999529Z -rwxr--r-- 1 root root 21840 May 20 19:33 Microsoft.Extensions.Options.ConfigurationExtensions.dll
|
||||||
|
2026-07-04T12:59:07.2999620Z -rwxr--r-- 1 root root 65360 May 20 19:30 Microsoft.Extensions.Options.dll
|
||||||
|
2026-07-04T12:59:07.2999737Z -rwxr--r-- 1 root root 44880 May 20 19:29 Microsoft.Extensions.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.2999839Z -rwxr--r-- 1 root root 43344 May 20 20:19 Microsoft.Extensions.Validation.dll
|
||||||
|
2026-07-04T12:59:07.2999920Z -rwxr--r-- 1 root root 19296 Jun 2 20:51 Microsoft.IdentityModel.Abstractions.dll
|
||||||
|
2026-07-04T12:59:07.3000007Z -rwxr--r-- 1 root root 172856 Jun 2 20:52 Microsoft.IdentityModel.JsonWebTokens.dll
|
||||||
|
2026-07-04T12:59:07.3000449Z -rwxr--r-- 1 root root 38200 Jun 2 20:51 Microsoft.IdentityModel.Logging.dll
|
||||||
|
2026-07-04T12:59:07.3000602Z -rwxr--r-- 1 root root 407352 Jun 2 20:51 Microsoft.IdentityModel.Tokens.dll
|
||||||
|
2026-07-04T12:59:07.3000692Z -rwxr--r-- 1 root root 24912 May 20 20:20 Microsoft.JSInterop.WebAssembly.dll
|
||||||
|
2026-07-04T12:59:07.3000807Z -rwxr--r-- 1 root root 75088 May 20 20:19 Microsoft.JSInterop.dll
|
||||||
|
2026-07-04T12:59:07.3000896Z -rwxr--r-- 1 root root 428880 May 20 18:30 Microsoft.VisualBasic.Core.dll
|
||||||
|
2026-07-04T12:59:07.3000979Z -rwxr--r-- 1 root root 17232 May 20 18:31 Microsoft.VisualBasic.dll
|
||||||
|
2026-07-04T12:59:07.3001230Z -rwxr--r-- 1 root root 15696 May 20 18:29 Microsoft.Win32.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3001364Z -rwxr--r-- 1 root root 33104 May 20 18:29 Microsoft.Win32.Registry.dll
|
||||||
|
2026-07-04T12:59:07.3001452Z -rwxr--r-- 1 root root 9108480 Sep 14 2023 MudBlazor.dll
|
||||||
|
2026-07-04T12:59:07.3001536Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.AppContext.dll
|
||||||
|
2026-07-04T12:59:07.3001639Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Buffers.dll
|
||||||
|
2026-07-04T12:59:07.3001876Z -rwxr--r-- 1 root root 88912 May 20 18:29 System.Collections.Concurrent.dll
|
||||||
|
2026-07-04T12:59:07.3002141Z -rwxr--r-- 1 root root 251216 May 20 18:29 System.Collections.Immutable.dll
|
||||||
|
2026-07-04T12:59:07.3002230Z -rwxr--r-- 1 root root 47952 May 20 18:29 System.Collections.NonGeneric.dll
|
||||||
|
2026-07-04T12:59:07.3002356Z -rwxr--r-- 1 root root 47952 May 20 18:29 System.Collections.Specialized.dll
|
||||||
|
2026-07-04T12:59:07.3011962Z -rwxr--r-- 1 root root 112976 May 20 18:28 System.Collections.dll
|
||||||
|
2026-07-04T12:59:07.3012179Z -rwxr--r-- 1 root root 102736 May 20 18:31 System.ComponentModel.Annotations.dll
|
||||||
|
2026-07-04T12:59:07.3012338Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.ComponentModel.DataAnnotations.dll
|
||||||
|
2026-07-04T12:59:07.3012446Z -rwxr--r-- 1 root root 26448 May 20 18:29 System.ComponentModel.EventBasedAsync.dll
|
||||||
|
2026-07-04T12:59:07.3012536Z -rwxr--r-- 1 root root 42320 May 20 18:29 System.ComponentModel.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3012797Z -rwxr--r-- 1 root root 316752 May 20 18:31 System.ComponentModel.TypeConverter.dll
|
||||||
|
2026-07-04T12:59:07.3012938Z -rwxr--r-- 1 root root 16208 May 20 18:29 System.ComponentModel.dll
|
||||||
|
2026-07-04T12:59:07.3013028Z -rwxr--r-- 1 root root 19280 May 20 18:31 System.Configuration.dll
|
||||||
|
2026-07-04T12:59:07.3013114Z -rwxr--r-- 1 root root 54096 May 20 18:31 System.Console.dll
|
||||||
|
2026-07-04T12:59:07.3013209Z -rwxr--r-- 1 root root 23376 May 20 18:31 System.Core.dll
|
||||||
|
2026-07-04T12:59:07.3013292Z -rwxr--r-- 1 root root 1018192 May 20 18:31 System.Data.Common.dll
|
||||||
|
2026-07-04T12:59:07.3013385Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Data.DataSetExtensions.dll
|
||||||
|
2026-07-04T12:59:07.3013632Z -rwxr--r-- 1 root root 25424 May 20 18:31 System.Data.dll
|
||||||
|
2026-07-04T12:59:07.3013742Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Diagnostics.Contracts.dll
|
||||||
|
2026-07-04T12:59:07.3013844Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Diagnostics.Debug.dll
|
||||||
|
2026-07-04T12:59:07.3013934Z -rwxr--r-- 1 root root 202576 May 20 18:29 System.Diagnostics.DiagnosticSource.dll
|
||||||
|
2026-07-04T12:59:07.3014028Z -rwxr--r-- 1 root root 22864 May 20 18:29 System.Diagnostics.FileVersionInfo.dll
|
||||||
|
2026-07-04T12:59:07.3014112Z -rwxr--r-- 1 root root 56656 May 20 18:29 System.Diagnostics.Process.dll
|
||||||
|
2026-07-04T12:59:07.3014195Z -rwxr--r-- 1 root root 25936 May 20 18:29 System.Diagnostics.StackTrace.dll
|
||||||
|
2026-07-04T12:59:07.3014435Z -rwxr--r-- 1 root root 31568 May 20 18:31 System.Diagnostics.TextWriterTraceListener.dll
|
||||||
|
2026-07-04T12:59:07.3014564Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Diagnostics.Tools.dll
|
||||||
|
2026-07-04T12:59:07.3014656Z -rwxr--r-- 1 root root 58704 May 20 18:29 System.Diagnostics.TraceSource.dll
|
||||||
|
2026-07-04T12:59:07.3014763Z -rwxr--r-- 1 root root 16208 May 20 18:28 System.Diagnostics.Tracing.dll
|
||||||
|
2026-07-04T12:59:07.3014864Z -rwxr--r-- 1 root root 64848 May 20 18:29 System.Drawing.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3014949Z -rwxr--r-- 1 root root 20304 May 20 18:31 System.Drawing.dll
|
||||||
|
2026-07-04T12:59:07.3015044Z -rwxr--r-- 1 root root 16208 May 20 18:30 System.Dynamic.Runtime.dll
|
||||||
|
2026-07-04T12:59:07.3015280Z -rwxr--r-- 1 root root 97104 May 20 18:29 System.Formats.Asn1.dll
|
||||||
|
2026-07-04T12:59:07.3015397Z -rwxr--r-- 1 root root 38736 May 20 18:29 System.Formats.Tar.dll
|
||||||
|
2026-07-04T12:59:07.3015481Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Globalization.Calendars.dll
|
||||||
|
2026-07-04T12:59:07.3015567Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Globalization.Extensions.dll
|
||||||
|
2026-07-04T12:59:07.3015663Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Globalization.dll
|
||||||
|
2026-07-04T12:59:07.3015747Z -rwxr--r-- 1 root root 28496 May 20 18:29 System.IO.Compression.Brotli.dll
|
||||||
|
2026-07-04T12:59:07.3015831Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.IO.Compression.FileSystem.dll
|
||||||
|
2026-07-04T12:59:07.3016040Z -rwxr--r-- 1 root root 53584 May 20 18:29 System.IO.Compression.ZipFile.dll
|
||||||
|
2026-07-04T12:59:07.3016293Z -rwxr--r-- 1 root root 167760 May 20 18:31 System.IO.Compression.dll
|
||||||
|
2026-07-04T12:59:07.3016382Z -rwxr--r-- 1 root root 32080 May 20 18:29 System.IO.FileSystem.AccessControl.dll
|
||||||
|
2026-07-04T12:59:07.3016467Z -rwxr--r-- 1 root root 23888 May 20 18:29 System.IO.FileSystem.DriveInfo.dll
|
||||||
|
2026-07-04T12:59:07.3016562Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.IO.FileSystem.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3016652Z -rwxr--r-- 1 root root 33104 May 20 18:29 System.IO.FileSystem.Watcher.dll
|
||||||
|
2026-07-04T12:59:07.3016895Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.IO.FileSystem.dll
|
||||||
|
2026-07-04T12:59:07.3016989Z -rwxr--r-- 1 root root 35152 May 20 18:30 System.IO.IsolatedStorage.dll
|
||||||
|
2026-07-04T12:59:07.3017098Z -rwxr--r-- 1 root root 50000 May 20 18:31 System.IO.MemoryMappedFiles.dll
|
||||||
|
2026-07-04T12:59:07.3017197Z -rwxr--r-- 1 root root 78160 May 20 18:29 System.IO.Pipelines.dll
|
||||||
|
2026-07-04T12:59:07.3017283Z -rwxr--r-- 1 root root 23376 May 20 18:29 System.IO.Pipes.AccessControl.dll
|
||||||
|
2026-07-04T12:59:07.3017385Z -rwxr--r-- 1 root root 42320 May 20 18:29 System.IO.Pipes.dll
|
||||||
|
2026-07-04T12:59:07.3017475Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.IO.UnmanagedMemoryStream.dll
|
||||||
|
2026-07-04T12:59:07.3017708Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.IO.dll
|
||||||
|
2026-07-04T12:59:07.3017795Z -rwxr--r-- 1 root root 92000 Jun 2 20:51 System.IdentityModel.Tokens.Jwt.dll
|
||||||
|
2026-07-04T12:59:07.3017911Z -rwxr--r-- 1 root root 456528 May 20 18:29 System.Linq.AsyncEnumerable.dll
|
||||||
|
2026-07-04T12:59:07.3018007Z -rwxr--r-- 1 root root 575312 May 20 18:29 System.Linq.Expressions.dll
|
||||||
|
2026-07-04T12:59:07.3018088Z -rwxr--r-- 1 root root 223056 May 20 18:31 System.Linq.Parallel.dll
|
||||||
|
2026-07-04T12:59:07.3018173Z -rwxr--r-- 1 root root 78672 May 20 18:31 System.Linq.Queryable.dll
|
||||||
|
2026-07-04T12:59:07.3018253Z -rwxr--r-- 1 root root 201040 May 20 18:29 System.Linq.dll
|
||||||
|
2026-07-04T12:59:07.3018473Z -rwxr--r-- 1 root root 55632 May 20 18:28 System.Memory.dll
|
||||||
|
2026-07-04T12:59:07.3018562Z -rwxr--r-- 1 root root 56144 May 20 18:30 System.Net.Http.Json.dll
|
||||||
|
2026-07-04T12:59:07.3018718Z -rwxr--r-- 1 root root 296272 May 20 18:31 System.Net.Http.dll
|
||||||
|
2026-07-04T12:59:07.3018879Z -rwxr--r-- 1 root root 56144 May 20 18:30 System.Net.HttpListener.dll
|
||||||
|
2026-07-04T12:59:07.3019013Z -rwxr--r-- 1 root root 105296 May 20 18:31 System.Net.Mail.dll
|
||||||
|
2026-07-04T12:59:07.3019285Z -rwxr--r-- 1 root root 24400 May 20 18:31 System.Net.NameResolution.dll
|
||||||
|
2026-07-04T12:59:07.3019384Z -rwxr--r-- 1 root root 42320 May 20 18:29 System.Net.NetworkInformation.dll
|
||||||
|
2026-07-04T12:59:07.3019468Z -rwxr--r-- 1 root root 27984 May 20 18:29 System.Net.Ping.dll
|
||||||
|
2026-07-04T12:59:07.3019549Z -rwxr--r-- 1 root root 107344 May 20 18:31 System.Net.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3019646Z -rwxr--r-- 1 root root 39248 May 20 18:30 System.Net.Quic.dll
|
||||||
|
2026-07-04T12:59:07.3019725Z -rwxr--r-- 1 root root 65872 May 20 18:30 System.Net.Requests.dll
|
||||||
|
2026-07-04T12:59:07.3019808Z -rwxr--r-- 1 root root 114512 May 20 18:30 System.Net.Security.dll
|
||||||
|
2026-07-04T12:59:07.3019892Z -rwxr--r-- 1 root root 40784 May 20 18:29 System.Net.ServerSentEvents.dll
|
||||||
|
2026-07-04T12:59:07.3020121Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Net.ServicePoint.dll
|
||||||
|
2026-07-04T12:59:07.3020212Z -rwxr--r-- 1 root root 74576 May 20 18:29 System.Net.Sockets.dll
|
||||||
|
2026-07-04T12:59:07.3020295Z -rwxr--r-- 1 root root 56144 May 20 18:31 System.Net.WebClient.dll
|
||||||
|
2026-07-04T12:59:07.3020488Z -rwxr--r-- 1 root root 33104 May 20 18:29 System.Net.WebHeaderCollection.dll
|
||||||
|
2026-07-04T12:59:07.3020573Z -rwxr--r-- 1 root root 21840 May 20 18:31 System.Net.WebProxy.dll
|
||||||
|
2026-07-04T12:59:07.3020657Z -rwxr--r-- 1 root root 52560 May 20 18:31 System.Net.WebSockets.Client.dll
|
||||||
|
2026-07-04T12:59:07.3020744Z -rwxr--r-- 1 root root 108880 May 20 18:31 System.Net.WebSockets.dll
|
||||||
|
2026-07-04T12:59:07.3020970Z -rwxr--r-- 1 root root 17232 May 20 18:31 System.Net.dll
|
||||||
|
2026-07-04T12:59:07.3021057Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Numerics.Vectors.dll
|
||||||
|
2026-07-04T12:59:07.3021143Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Numerics.dll
|
||||||
|
2026-07-04T12:59:07.3021232Z -rwxr--r-- 1 root root 41296 May 20 18:29 System.ObjectModel.dll
|
||||||
|
2026-07-04T12:59:07.3021315Z -rwxr--r-- 1 root root 4880208 May 20 18:19 System.Private.CoreLib.dll
|
||||||
|
2026-07-04T12:59:07.3021397Z -rwxr--r-- 1 root root 859472 May 20 18:31 System.Private.DataContractSerialization.dll
|
||||||
|
2026-07-04T12:59:07.3021490Z -rwxr--r-- 1 root root 105808 May 20 18:28 System.Private.Uri.dll
|
||||||
|
2026-07-04T12:59:07.3021829Z -rwxr--r-- 1 root root 153936 May 20 18:30 System.Private.Xml.Linq.dll
|
||||||
|
2026-07-04T12:59:07.3021924Z -rwxr--r-- 1 root root 3106128 May 20 18:30 System.Private.Xml.dll
|
||||||
|
2026-07-04T12:59:07.3022020Z -rwxr--r-- 1 root root 38224 May 20 18:31 System.Reflection.DispatchProxy.dll
|
||||||
|
2026-07-04T12:59:07.3022121Z -rwxr--r-- 1 root root 15696 May 20 18:29 System.Reflection.Emit.ILGeneration.dll
|
||||||
|
2026-07-04T12:59:07.3022214Z -rwxr--r-- 1 root root 15696 May 20 18:29 System.Reflection.Emit.Lightweight.dll
|
||||||
|
2026-07-04T12:59:07.3022300Z -rwxr--r-- 1 root root 133456 May 20 18:29 System.Reflection.Emit.dll
|
||||||
|
2026-07-04T12:59:07.3022534Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Reflection.Extensions.dll
|
||||||
|
2026-07-04T12:59:07.3022623Z -rwxr--r-- 1 root root 503632 May 20 18:29 System.Reflection.Metadata.dll
|
||||||
|
2026-07-04T12:59:07.3022707Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Reflection.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3022794Z -rwxr--r-- 1 root root 24400 May 20 18:31 System.Reflection.TypeExtensions.dll
|
||||||
|
2026-07-04T12:59:07.3022902Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Reflection.dll
|
||||||
|
2026-07-04T12:59:07.3022986Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Resources.Reader.dll
|
||||||
|
2026-07-04T12:59:07.3023069Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Resources.ResourceManager.dll
|
||||||
|
2026-07-04T12:59:07.3023153Z -rwxr--r-- 1 root root 26960 May 20 18:31 System.Resources.Writer.dll
|
||||||
|
2026-07-04T12:59:07.3023377Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Runtime.CompilerServices.Unsafe.dll
|
||||||
|
2026-07-04T12:59:07.3023472Z -rwxr--r-- 1 root root 17232 May 20 18:30 System.Runtime.CompilerServices.VisualC.dll
|
||||||
|
2026-07-04T12:59:07.3023557Z -rwxr--r-- 1 root root 17744 May 20 18:30 System.Runtime.Extensions.dll
|
||||||
|
2026-07-04T12:59:07.3023656Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Runtime.Handles.dll
|
||||||
|
2026-07-04T12:59:07.3023754Z -rwxr--r-- 1 root root 89936 May 20 18:31 System.Runtime.InteropServices.JavaScript.dll
|
||||||
|
2026-07-04T12:59:07.3023845Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Runtime.InteropServices.RuntimeInformation.dll
|
||||||
|
2026-07-04T12:59:07.3023932Z -rwxr--r-- 1 root root 64848 May 20 18:29 System.Runtime.InteropServices.dll
|
||||||
|
2026-07-04T12:59:07.3024221Z -rwxr--r-- 1 root root 17232 May 20 18:29 System.Runtime.Intrinsics.dll
|
||||||
|
2026-07-04T12:59:07.3024393Z -rwxr--r-- 1 root root 15696 May 20 18:29 System.Runtime.Loader.dll
|
||||||
|
2026-07-04T12:59:07.3024484Z -rwxr--r-- 1 root root 145232 May 20 18:29 System.Runtime.Numerics.dll
|
||||||
|
2026-07-04T12:59:07.3024579Z -rwxr--r-- 1 root root 65872 May 20 18:29 System.Runtime.Serialization.Formatters.dll
|
||||||
|
2026-07-04T12:59:07.3024674Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Runtime.Serialization.Json.dll
|
||||||
|
2026-07-04T12:59:07.3024767Z -rwxr--r-- 1 root root 23376 May 20 18:30 System.Runtime.Serialization.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3025038Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Runtime.Serialization.Xml.dll
|
||||||
|
2026-07-04T12:59:07.3025127Z -rwxr--r-- 1 root root 17232 May 20 18:31 System.Runtime.Serialization.dll
|
||||||
|
2026-07-04T12:59:07.3025213Z -rwxr--r-- 1 root root 44880 May 20 18:28 System.Runtime.dll
|
||||||
|
2026-07-04T12:59:07.3025296Z -rwxr--r-- 1 root root 58192 May 20 18:29 System.Security.AccessControl.dll
|
||||||
|
2026-07-04T12:59:07.3025393Z -rwxr--r-- 1 root root 55120 May 20 18:29 System.Security.Claims.dll
|
||||||
|
2026-07-04T12:59:07.3025477Z -rwxr--r-- 1 root root 17232 May 20 18:31 System.Security.Cryptography.Algorithms.dll
|
||||||
|
2026-07-04T12:59:07.3025565Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Security.Cryptography.Cng.dll
|
||||||
|
2026-07-04T12:59:07.3025922Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Security.Cryptography.Csp.dll
|
||||||
|
2026-07-04T12:59:07.3026035Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Security.Cryptography.Encoding.dll
|
||||||
|
2026-07-04T12:59:07.3026508Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Security.Cryptography.OpenSsl.dll
|
||||||
|
2026-07-04T12:59:07.3026604Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Security.Cryptography.Primitives.dll
|
||||||
|
2026-07-04T12:59:07.3026700Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Security.Cryptography.X509Certificates.dll
|
||||||
|
2026-07-04T12:59:07.3026796Z -rwxr--r-- 1 root root 654160 May 20 18:31 System.Security.Cryptography.dll
|
||||||
|
2026-07-04T12:59:07.3026889Z -rwxr--r-- 1 root root 37712 May 20 18:29 System.Security.Principal.Windows.dll
|
||||||
|
2026-07-04T12:59:07.3026988Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Security.Principal.dll
|
||||||
|
2026-07-04T12:59:07.3027225Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Security.SecureString.dll
|
||||||
|
2026-07-04T12:59:07.3027312Z -rwxr--r-- 1 root root 18256 May 20 18:31 System.Security.dll
|
||||||
|
2026-07-04T12:59:07.3027399Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.ServiceModel.Web.dll
|
||||||
|
2026-07-04T12:59:07.3027496Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.ServiceProcess.dll
|
||||||
|
2026-07-04T12:59:07.3027578Z -rwxr--r-- 1 root root 742736 May 20 18:29 System.Text.Encoding.CodePages.dll
|
||||||
|
2026-07-04T12:59:07.3027663Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Text.Encoding.Extensions.dll
|
||||||
|
2026-07-04T12:59:07.3027753Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Text.Encoding.dll
|
||||||
|
2026-07-04T12:59:07.3027971Z -rwxr--r-- 1 root root 65872 May 20 18:32 System.Text.Encodings.Web.dll
|
||||||
|
2026-07-04T12:59:07.3028064Z -rwxr--r-- 1 root root 649040 May 20 18:30 System.Text.Json.dll
|
||||||
|
2026-07-04T12:59:07.3028143Z -rwxr--r-- 1 root root 384848 May 20 18:30 System.Text.RegularExpressions.dll
|
||||||
|
2026-07-04T12:59:07.3028244Z -rwxr--r-- 1 root root 33616 May 20 18:29 System.Threading.AccessControl.dll
|
||||||
|
2026-07-04T12:59:07.3028326Z -rwxr--r-- 1 root root 66384 May 20 18:29 System.Threading.Channels.dll
|
||||||
|
2026-07-04T12:59:07.3028411Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Threading.Overlapped.dll
|
||||||
|
2026-07-04T12:59:07.3028493Z -rwxr--r-- 1 root root 185680 May 20 18:29 System.Threading.Tasks.Dataflow.dll
|
||||||
|
2026-07-04T12:59:07.3028587Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Threading.Tasks.Extensions.dll
|
||||||
|
2026-07-04T12:59:07.3028808Z -rwxr--r-- 1 root root 61264 May 20 18:31 System.Threading.Tasks.Parallel.dll
|
||||||
|
2026-07-04T12:59:07.3028902Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Threading.Tasks.dll
|
||||||
|
2026-07-04T12:59:07.3028994Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Threading.Thread.dll
|
||||||
|
2026-07-04T12:59:07.3029076Z -rwxr--r-- 1 root root 15696 May 20 18:28 System.Threading.ThreadPool.dll
|
||||||
|
2026-07-04T12:59:07.3029160Z -rwxr--r-- 1 root root 15184 May 20 18:30 System.Threading.Timer.dll
|
||||||
|
2026-07-04T12:59:07.3029262Z -rwxr--r-- 1 root root 44880 May 20 18:28 System.Threading.dll
|
||||||
|
2026-07-04T12:59:07.3029345Z -rwxr--r-- 1 root root 175952 May 20 18:30 System.Transactions.Local.dll
|
||||||
|
2026-07-04T12:59:07.3029560Z -rwxr--r-- 1 root root 16720 May 20 18:31 System.Transactions.dll
|
||||||
|
2026-07-04T12:59:07.3029651Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.ValueTuple.dll
|
||||||
|
2026-07-04T12:59:07.3029740Z -rwxr--r-- 1 root root 30032 May 20 18:31 System.Web.HttpUtility.dll
|
||||||
|
2026-07-04T12:59:07.3029838Z -rwxr--r-- 1 root root 15184 May 20 18:31 System.Web.dll
|
||||||
|
2026-07-04T12:59:07.3029919Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Windows.dll
|
||||||
|
2026-07-04T12:59:07.3030002Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.Linq.dll
|
||||||
|
2026-07-04T12:59:07.3030083Z -rwxr--r-- 1 root root 21840 May 20 18:30 System.Xml.ReaderWriter.dll
|
||||||
|
2026-07-04T12:59:07.3030164Z -rwxr--r-- 1 root root 16208 May 20 18:31 System.Xml.Serialization.dll
|
||||||
|
2026-07-04T12:59:07.3030381Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.XDocument.dll
|
||||||
|
2026-07-04T12:59:07.3030479Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.XPath.XDocument.dll
|
||||||
|
2026-07-04T12:59:07.3030561Z -rwxr--r-- 1 root root 15696 May 20 18:30 System.Xml.XPath.dll
|
||||||
|
2026-07-04T12:59:07.3030650Z -rwxr--r-- 1 root root 15696 May 20 18:31 System.Xml.XmlDocument.dll
|
||||||
|
2026-07-04T12:59:07.3030739Z -rwxr--r-- 1 root root 17744 May 20 18:31 System.Xml.XmlSerializer.dll
|
||||||
|
2026-07-04T12:59:07.3030822Z -rwxr--r-- 1 root root 23376 May 20 18:31 System.Xml.dll
|
||||||
|
2026-07-04T12:59:07.3030913Z -rwxr--r-- 1 root root 50000 May 20 18:32 System.dll
|
||||||
|
2026-07-04T12:59:07.3030995Z -rw-r--r-- 1 root root 156672 Jul 4 12:56 TaxBaik.Application.dll
|
||||||
|
2026-07-04T12:59:07.3031231Z -rw-r--r-- 1 root root 38468 Jul 4 12:56 TaxBaik.Application.pdb
|
||||||
|
2026-07-04T12:59:07.3031318Z -rw-r--r-- 1 root root 37888 Jul 4 12:56 TaxBaik.Domain.dll
|
||||||
|
2026-07-04T12:59:07.3031411Z -rw-r--r-- 1 root root 24100 Jul 4 12:56 TaxBaik.Domain.pdb
|
||||||
|
2026-07-04T12:59:07.3031494Z -rw-r--r-- 1 root root 713216 Jul 4 12:58 TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:59:07.3031583Z -rw-r--r-- 1 root root 373136 Jul 4 12:58 TaxBaik.Web.Client.pdb
|
||||||
|
2026-07-04T12:59:07.3031768Z -rw-r--r-- 1 root root 2546 Jul 4 12:58 TaxBaik.Web.Client.runtimeconfig.json
|
||||||
|
2026-07-04T12:59:07.3032037Z -rw-r--r-- 1 root root 1007340 Jul 4 12:58 TaxBaik.Web.Client.staticwebassets.endpoints.json
|
||||||
|
2026-07-04T12:59:07.3032142Z -rw-r--r-- 1 root root 78064 Jul 4 12:58 TaxBaik.Web.Client.staticwebassets.runtime.json
|
||||||
|
2026-07-04T12:59:07.3032230Z -rwxr--r-- 1 root root 16208 May 20 18:31 WindowsBase.dll
|
||||||
|
2026-07-04T12:59:07.3032313Z -rwxr--r-- 1 root root 37898 May 20 18:42 dotnet.js
|
||||||
|
2026-07-04T12:59:07.3032406Z -rwxr--r-- 1 root root 51818 May 20 18:42 dotnet.js.map
|
||||||
|
2026-07-04T12:59:07.3032485Z -rwxr--r-- 1 root root 145050 May 20 18:43 dotnet.native.js
|
||||||
|
2026-07-04T12:59:07.3032565Z -rwxr--r-- 1 root root 3002101 May 20 18:43 dotnet.native.wasm
|
||||||
|
2026-07-04T12:59:07.3032781Z -rwxr--r-- 1 root root 198479 May 20 18:42 dotnet.runtime.js
|
||||||
|
2026-07-04T12:59:07.3032873Z -rwxr--r-- 1 root root 276757 May 20 18:42 dotnet.runtime.js.map
|
||||||
|
2026-07-04T12:59:07.3032955Z -rwxr--r-- 1 root root 956416 Apr 2 19:04 icudt_CJK.dat
|
||||||
|
2026-07-04T12:59:07.3033032Z -rwxr--r-- 1 root root 550832 Apr 2 19:04 icudt_EFIGS.dat
|
||||||
|
2026-07-04T12:59:07.3033111Z -rwxr--r-- 1 root root 1107168 Apr 2 19:04 icudt_no_CJK.dat
|
||||||
|
2026-07-04T12:59:07.3033190Z -rwxr--r-- 1 root root 59728 May 20 18:31 mscorlib.dll
|
||||||
|
2026-07-04T12:59:07.3033269Z -rwxr--r-- 1 root root 100688 May 20 18:32 netstandard.dll
|
||||||
|
2026-07-04T12:59:07.3033348Z drwxr-xr-x 3 root root 4096 Jul 4 12:58 wwwroot
|
||||||
|
2026-07-04T12:59:07.3071176Z total 4352
|
||||||
|
2026-07-04T12:59:07.3071587Z drwxr-xr-x 8 root root 4096 Jul 4 12:58 .
|
||||||
|
2026-07-04T12:59:07.3071872Z drwxr-xr-x 3 root root 4096 Jul 4 12:58 ..
|
||||||
|
2026-07-04T12:59:07.3071979Z -rw-r--r-- 1 root root 196 Jul 4 12:58 .NETCoreApp,Version=v10.0.AssemblyAttributes.cs
|
||||||
|
2026-07-04T12:59:07.3072092Z -rw-r--r-- 1 root root 137 Jul 4 12:58 EmbeddedAttribute.cs
|
||||||
|
2026-07-04T12:59:07.3072373Z -rw-r--r-- 1 root root 0 Jul 4 12:58 TaxBaik..C36EE7CA.Up2Date
|
||||||
|
2026-07-04T12:59:07.3072471Z -rw-r--r-- 1 root root 1008 Jul 4 12:58 TaxBaik.Web.Client.AssemblyInfo.cs
|
||||||
|
2026-07-04T12:59:07.3072582Z -rw-r--r-- 1 root root 65 Jul 4 12:58 TaxBaik.Web.Client.AssemblyInfoInputs.cache
|
||||||
|
2026-07-04T12:59:07.3072703Z -rw-r--r-- 1 root root 16355 Jul 4 12:58 TaxBaik.Web.Client.GeneratedMSBuildEditorConfig.editorconfig
|
||||||
|
2026-07-04T12:59:07.3072810Z -rw-r--r-- 1 root root 433 Jul 4 12:58 TaxBaik.Web.Client.GlobalUsings.g.cs
|
||||||
|
2026-07-04T12:59:07.3072898Z -rw-r--r-- 1 root root 0 Jul 4 12:58 TaxBaik.Web.Client.MvcApplicationPartsAssemblyInfo.cache
|
||||||
|
2026-07-04T12:59:07.3073143Z -rw-r--r-- 1 root root 27329 Jul 4 12:58 TaxBaik.Web.Client.assets.cache
|
||||||
|
2026-07-04T12:59:07.3073238Z -rw-r--r-- 1 root root 20456 Jul 4 12:58 TaxBaik.Web.Client.csproj.AssemblyReference.cache
|
||||||
|
2026-07-04T12:59:07.3073326Z -rw-r--r-- 1 root root 65 Jul 4 12:58 TaxBaik.Web.Client.csproj.CoreCompileInputs.cache
|
||||||
|
2026-07-04T12:59:07.3073433Z -rw-r--r-- 1 root root 136894 Jul 4 12:58 TaxBaik.Web.Client.csproj.FileListAbsolute.txt
|
||||||
|
2026-07-04T12:59:07.3073537Z -rw-r--r-- 1 root root 713216 Jul 4 12:58 TaxBaik.Web.Client.dll
|
||||||
|
2026-07-04T12:59:07.3073640Z -rw-r--r-- 1 root root 65 Jul 4 12:58 TaxBaik.Web.Client.genruntimeconfig.cache
|
||||||
|
2026-07-04T12:59:07.3073730Z -rw-r--r-- 1 root root 373136 Jul 4 12:58 TaxBaik.Web.Client.pdb
|
||||||
|
2026-07-04T12:59:07.3073973Z -rw-r--r-- 1 root root 276 Jul 4 12:58 ValidatableTypeAttribute.cs
|
||||||
|
2026-07-04T12:59:07.3074072Z -rw-r--r-- 1 root root 3 Jul 4 12:58 blazor.build.boot-extension.json
|
||||||
|
2026-07-04T12:59:07.3074158Z drwxr-xr-x 2 root root 20480 Jul 4 12:58 compressed
|
||||||
|
2026-07-04T12:59:07.3074258Z -rw-r--r-- 1 root root 93392 Jul 4 12:58 dotnet.js
|
||||||
|
2026-07-04T12:59:07.3074339Z -rw-r--r-- 1 root root 276577 Jul 4 12:58 rbcswa.dswa.cache.json
|
||||||
|
2026-07-04T12:59:07.3074420Z drwxr-xr-x 2 root root 4096 Jul 4 12:58 ref
|
||||||
|
2026-07-04T12:59:07.3074500Z drwxr-xr-x 2 root root 4096 Jul 4 12:58 refint
|
||||||
|
2026-07-04T12:59:07.3074734Z -rw-r--r-- 1 root root 327 Jul 4 12:58 rjimswa.dswa.cache.json
|
||||||
|
2026-07-04T12:59:07.3074829Z -rw-r--r-- 1 root root 3434 Jul 4 12:58 rjsmcshtml.dswa.cache.json
|
||||||
|
2026-07-04T12:59:07.3074912Z -rw-r--r-- 1 root root 3434 Jul 4 12:58 rjsmrazor.dswa.cache.json
|
||||||
|
2026-07-04T12:59:07.3074999Z -rw-r--r-- 1 root root 4208 Jul 4 12:58 rpswa.dswa.cache.json
|
||||||
|
2026-07-04T12:59:07.3075082Z drwxr-xr-x 2 root root 4096 Jul 4 12:58 staticwebassets
|
||||||
|
2026-07-04T12:59:07.3075161Z -rw-r--r-- 1 root root 1007340 Jul 4 12:58 staticwebassets.build.endpoints.json
|
||||||
|
2026-07-04T12:59:07.3075247Z -rw-r--r-- 1 root root 1564906 Jul 4 12:58 staticwebassets.build.json
|
||||||
|
2026-07-04T12:59:07.3075331Z -rw-r--r-- 1 root root 44 Jul 4 12:58 staticwebassets.build.json.cache
|
||||||
|
2026-07-04T12:59:07.3075530Z -rw-r--r-- 1 root root 78064 Jul 4 12:58 staticwebassets.development.json
|
||||||
|
2026-07-04T12:59:07.3075619Z -rw-r--r-- 1 root root 0 Jul 4 12:58 swae.build.ex.cache
|
||||||
|
2026-07-04T12:59:07.3075707Z drwxr-xr-x 2 root root 4096 Jul 4 12:58 tmp-webcil
|
||||||
|
2026-07-04T12:59:07.3075785Z drwxr-xr-x 2 root root 20480 Jul 4 12:58 webcil
|
||||||
|
2026-07-04T13:01:22.5627758Z ✓ Publish Web elapsed: 193s
|
||||||
|
2026-07-04T13:01:22.5646529Z -rw-r--r-- 1 root root 2.2M Jul 4 13:01 ./publish-logs/publish-web.binlog
|
||||||
|
2026-07-04T13:01:22.6880698Z ::group::Run set -e
|
||||||
|
2026-07-04T13:01:22.6881027Z set -e
|
||||||
|
2026-07-04T13:01:22.6881197Z mkdir -p ./publish-logs
|
||||||
|
2026-07-04T13:01:22.6881283Z # Proxy is not part of the solution restore graph, so restore it once
|
||||||
|
2026-07-04T13:01:22.6881372Z # here before publishing to avoid NETSDK1004 in CI.
|
||||||
|
2026-07-04T13:01:22.6881469Z dotnet restore src/TaxBaik.Proxy/
|
||||||
|
2026-07-04T13:01:22.6881543Z dotnet build src/TaxBaik.Proxy/TaxBaik.Proxy.csproj -c Release --no-restore
|
||||||
|
2026-07-04T13:01:22.6881631Z start=$(date +%s)
|
||||||
|
2026-07-04T13:01:22.6881698Z dotnet publish src/TaxBaik.Proxy/ \
|
||||||
|
2026-07-04T13:01:22.6881795Z -c Release \
|
||||||
|
2026-07-04T13:01:22.6882017Z -o ./publish/proxy \
|
||||||
|
2026-07-04T13:01:22.6882108Z --no-restore \
|
||||||
|
2026-07-04T13:01:22.6882193Z --no-build \
|
||||||
|
2026-07-04T13:01:22.6882265Z -p:PublishReadyToRun=false \
|
||||||
|
2026-07-04T13:01:22.6882334Z -p:PerformanceSummary=true \
|
||||||
|
2026-07-04T13:01:22.6882404Z -clp:Summary \
|
||||||
|
2026-07-04T13:01:22.6882495Z -bl:./publish-logs/publish-proxy.binlog
|
||||||
|
2026-07-04T13:01:22.6882564Z end=$(date +%s)
|
||||||
|
2026-07-04T13:01:22.6882635Z echo "✓ Publish Proxy elapsed: $((end - start))s"
|
||||||
|
2026-07-04T13:01:22.6882738Z ls -lh ./publish-logs/publish-proxy.binlog
|
||||||
|
2026-07-04T13:01:22.6882814Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:22.6882907Z ::endgroup::
|
||||||
|
2026-07-04T13:01:24.3061500Z Determining projects to restore...
|
||||||
|
2026-07-04T13:01:24.9616018Z Restored /workspace/***/taxbaik/src/TaxBaik.Proxy/TaxBaik.Proxy.csproj (in 156 ms).
|
||||||
|
2026-07-04T13:01:26.9924481Z TaxBaik.Proxy -> /workspace/***/taxbaik/src/TaxBaik.Proxy/bin/Release/net10.0/TaxBaik.Proxy.dll
|
||||||
|
2026-07-04T13:01:27.0997744Z
|
||||||
|
2026-07-04T13:01:27.1028989Z Build succeeded.
|
||||||
|
2026-07-04T13:01:27.1047131Z 0 Warning(s)
|
||||||
|
2026-07-04T13:01:27.1047398Z 0 Error(s)
|
||||||
|
2026-07-04T13:01:27.1047541Z
|
||||||
|
2026-07-04T13:01:27.1047621Z Time Elapsed 00:00:01.55
|
||||||
|
2026-07-04T13:01:28.9796929Z TaxBaik.Proxy -> /workspace/***/taxbaik/publish/proxy/
|
||||||
|
2026-07-04T13:01:29.0117388Z
|
||||||
|
2026-07-04T13:01:29.0137929Z Build succeeded.
|
||||||
|
2026-07-04T13:01:29.0142336Z 0 Warning(s)
|
||||||
|
2026-07-04T13:01:29.0144041Z 0 Error(s)
|
||||||
|
2026-07-04T13:01:29.0147048Z
|
||||||
|
2026-07-04T13:01:29.0149487Z Time Elapsed 00:00:01.04
|
||||||
|
2026-07-04T13:01:29.0760476Z ✓ Publish Proxy elapsed: 2s
|
||||||
|
2026-07-04T13:01:29.0761097Z -rw-r--r-- 1 root root 335K Jul 4 13:01 ./publish-logs/publish-proxy.binlog
|
||||||
|
2026-07-04T13:01:29.2472841Z ::group::Run set -e
|
||||||
|
2026-07-04T13:01:29.2473270Z set -e
|
||||||
|
2026-07-04T13:01:29.2473414Z JWT_SECRET_KEY="***"
|
||||||
|
2026-07-04T13:01:29.2473519Z TELEGRAM_BOT_TOKEN="***"
|
||||||
|
2026-07-04T13:01:29.2473612Z TELEGRAM_CHAT_ID="***"
|
||||||
|
2026-07-04T13:01:29.2473691Z TELEGRAM_INQUIRY_CHAT_ID=""
|
||||||
|
2026-07-04T13:01:29.2473786Z TELEGRAM_SYSTEM_CHAT_ID=""
|
||||||
|
2026-07-04T13:01:29.2473869Z [ -z "$JWT_SECRET_KEY" ] && { echo "Missing TAXBAIK_JWT_SECRET_KEY" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:29.2473974Z [ -z "$TELEGRAM_BOT_TOKEN" ] && { echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:29.2474063Z [ -z "$TELEGRAM_CHAT_ID" ] && { echo "Missing TAXBAIK_TELEGRAM_CHAT_ID" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:29.2474147Z [ -z "$TELEGRAM_INQUIRY_CHAT_ID" ] && TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_CHAT_ID"
|
||||||
|
2026-07-04T13:01:29.2474240Z [ -z "$TELEGRAM_SYSTEM_CHAT_ID" ] && TELEGRAM_SYSTEM_CHAT_ID="-5585148480"
|
||||||
|
2026-07-04T13:01:29.2474325Z JWT_SECRET_KEY="$JWT_SECRET_KEY" \
|
||||||
|
2026-07-04T13:01:29.2474401Z TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \
|
||||||
|
2026-07-04T13:01:29.2474541Z TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" \
|
||||||
|
2026-07-04T13:01:29.2474616Z TELEGRAM_INQUIRY_CHAT_ID="$TELEGRAM_INQUIRY_CHAT_ID" \
|
||||||
|
2026-07-04T13:01:29.2474692Z TELEGRAM_SYSTEM_CHAT_ID="$TELEGRAM_SYSTEM_CHAT_ID" \
|
||||||
|
2026-07-04T13:01:29.2474783Z python3 -c '
|
||||||
|
2026-07-04T13:01:29.2474854Z import json, os, pathlib
|
||||||
|
2026-07-04T13:01:29.2474933Z pathlib.Path("./publish/appsettings.Production.json").write_text(
|
||||||
|
2026-07-04T13:01:29.2475012Z json.dumps({
|
||||||
|
2026-07-04T13:01:29.2475107Z "Jwt": {"SecretKey": os.environ["JWT_SECRET_KEY"]},
|
||||||
|
2026-07-04T13:01:29.2475203Z "Telegram": {
|
||||||
|
2026-07-04T13:01:29.2475275Z "BotToken": os.environ["TELEGRAM_BOT_TOKEN"],
|
||||||
|
2026-07-04T13:01:29.2475367Z "ChatId": os.environ["TELEGRAM_CHAT_ID"],
|
||||||
|
2026-07-04T13:01:29.2475442Z "InquiryChatId": os.environ["TELEGRAM_INQUIRY_CHAT_ID"],
|
||||||
|
2026-07-04T13:01:29.2475513Z "SystemChatId": os.environ["TELEGRAM_SYSTEM_CHAT_ID"]
|
||||||
|
2026-07-04T13:01:29.2475584Z }
|
||||||
|
2026-07-04T13:01:29.2475671Z }, ensure_ascii=False, indent=2),
|
||||||
|
2026-07-04T13:01:29.2475739Z encoding="utf-8"
|
||||||
|
2026-07-04T13:01:29.2475812Z )'
|
||||||
|
2026-07-04T13:01:29.2475895Z test -s ./publish/appsettings.Production.json || { echo "appsettings.Production.json is empty" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:29.2475995Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:29.2476277Z ::endgroup::
|
||||||
|
2026-07-04T13:01:29.5445729Z ::group::Run test -s ./publish/proxy/TaxBaik.Proxy.dll || { echo "TaxBaik.Proxy.dll missing" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:29.5446479Z test -s ./publish/proxy/TaxBaik.Proxy.dll || { echo "TaxBaik.Proxy.dll missing" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:29.5446656Z test -s ./publish/proxy/TaxBaik.Proxy.runtimeconfig.json || { echo "TaxBaik.Proxy.runtimeconfig.json missing" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:29.5446772Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:29.5446901Z ::endgroup::
|
||||||
|
2026-07-04T13:01:29.7396976Z ::group::Run mkdir -p ./publish/db && cp -r db/migrations ./publish/db/ || true
|
||||||
|
2026-07-04T13:01:29.7397352Z mkdir -p ./publish/db && cp -r db/migrations ./publish/db/ || true
|
||||||
|
2026-07-04T13:01:29.7397484Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:29.7397589Z ::endgroup::
|
||||||
|
2026-07-04T13:01:30.0169769Z ::group::Run bash scripts/validate_migrations.sh db/migrations
|
||||||
|
2026-07-04T13:01:30.0170284Z bash scripts/validate_migrations.sh db/migrations
|
||||||
|
2026-07-04T13:01:30.0170495Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:30.0170623Z ::endgroup::
|
||||||
|
2026-07-04T13:01:30.1873738Z Duplicate version check passed.
|
||||||
|
2026-07-04T13:01:30.3350394Z ::group::Run bash scripts/validate_kst_timestamps.sh
|
||||||
|
2026-07-04T13:01:30.3350747Z bash scripts/validate_kst_timestamps.sh
|
||||||
|
2026-07-04T13:01:30.3350854Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:30.3350983Z ::endgroup::
|
||||||
|
2026-07-04T13:01:30.4075762Z KST timestamp harness passed.
|
||||||
|
2026-07-04T13:01:30.5339955Z ::group::Run COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||||
|
2026-07-04T13:01:30.5340420Z COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||||
|
2026-07-04T13:01:30.5340542Z BUILD_TIME=$(TZ=Asia/Seoul date +'%Y-%m-%d %H:%M:%S KST')
|
||||||
|
2026-07-04T13:01:30.5340635Z mkdir -p ./publish/wwwroot
|
||||||
|
2026-07-04T13:01:30.5340751Z printf '{\n "version": "%s",\n "built": "%s"\n}\n' "$COMMIT_HASH" "$BUILD_TIME" > ./publish/wwwroot/version.json
|
||||||
|
2026-07-04T13:01:30.5340864Z echo "✓ Build: $COMMIT_HASH @ $BUILD_TIME"
|
||||||
|
2026-07-04T13:01:30.5340985Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:30.5341090Z ::endgroup::
|
||||||
|
2026-07-04T13:01:30.5999292Z ✓ Build: c00d237 @ 2026-07-04 22:01:30 KST
|
||||||
|
2026-07-04T13:01:30.7264039Z ::group::Run mkdir -p ~/.ssh
|
||||||
|
2026-07-04T13:01:30.7264431Z mkdir -p ~/.ssh
|
||||||
|
2026-07-04T13:01:30.7264604Z SSH_KEY_B64="***"
|
||||||
|
2026-07-04T13:01:30.7264799Z SSH_KEY_RAW="***"
|
||||||
|
2026-07-04T13:01:30.7265065Z if [ -n "$SSH_KEY_B64" ]; then
|
||||||
|
2026-07-04T13:01:30.7265231Z printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T13:01:30.7265382Z elif [ -n "$SSH_KEY_RAW" ]; then
|
||||||
|
2026-07-04T13:01:30.7265513Z if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then
|
||||||
|
2026-07-04T13:01:30.7265665Z printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T13:01:30.7265813Z else
|
||||||
|
2026-07-04T13:01:30.7265955Z printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T13:01:30.7266448Z fi
|
||||||
|
2026-07-04T13:01:30.7266624Z else
|
||||||
|
2026-07-04T13:01:30.7266747Z echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY" >&2; exit 1
|
||||||
|
2026-07-04T13:01:30.7266892Z fi
|
||||||
|
2026-07-04T13:01:30.7267009Z sed -i 's/\r$//' ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T13:01:30.7267138Z chmod 600 ~/.ssh/id_ed25519
|
||||||
|
2026-07-04T13:01:30.7267222Z ssh-keyscan -H "***" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
|
2026-07-04T13:01:30.7267363Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:30.7267559Z ::endgroup::
|
||||||
|
2026-07-04T13:01:31.3915758Z ::group::Run cp deploy_gb.sh ./publish/deploy_gb.sh
|
||||||
|
2026-07-04T13:01:31.3916326Z cp deploy_gb.sh ./publish/deploy_gb.sh
|
||||||
|
2026-07-04T13:01:31.3916448Z mkdir -p ./publish/scripts
|
||||||
|
2026-07-04T13:01:31.3916532Z cp scripts/validate_migrations.sh ./publish/scripts/validate_migrations.sh
|
||||||
|
2026-07-04T13:01:31.3916629Z chmod +x ./publish/scripts/validate_migrations.sh
|
||||||
|
2026-07-04T13:01:31.3916706Z tar -czf taxbaik_deploy.tgz -C ./publish .
|
||||||
|
2026-07-04T13:01:31.3916791Z echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)"
|
||||||
|
2026-07-04T13:01:31.3916894Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:31.3916989Z ::endgroup::
|
||||||
|
2026-07-04T13:01:34.5550762Z ✓ Package: 23M
|
||||||
|
2026-07-04T13:01:34.6596310Z ::group::Run set -e
|
||||||
|
2026-07-04T13:01:34.6596644Z set -e
|
||||||
|
2026-07-04T13:01:34.6596755Z export TAXBAIK_DEPLOY_FROM_CI=1
|
||||||
|
2026-07-04T13:01:34.6596854Z TIMESTAMP=$(TZ=Asia/Seoul date +%Y%m%d_%H%M%S)
|
||||||
|
2026-07-04T13:01:34.6596935Z COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
2026-07-04T13:01:34.6597016Z DEPLOY_HOST="***"
|
||||||
|
2026-07-04T13:01:34.6597103Z DEPLOY_USER="***"
|
||||||
|
2026-07-04T13:01:34.6597300Z TELEGRAM_BOT_TOKEN="***"
|
||||||
|
2026-07-04T13:01:34.6597384Z TELEGRAM_SYSTEM_CHAT_ID=""
|
||||||
|
2026-07-04T13:01:34.6597460Z TELEGRAM_CHAT_ID="${TELEGRAM_SYSTEM_CHAT_ID:--5585148480}"
|
||||||
|
2026-07-04T13:01:34.6597556Z
|
||||||
|
2026-07-04T13:01:34.6597626Z send_telegram() {
|
||||||
|
2026-07-04T13:01:34.6597703Z local text="$1"
|
||||||
|
2026-07-04T13:01:34.6597779Z if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
|
||||||
|
2026-07-04T13:01:34.6597884Z echo "Skipping Telegram notification: missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2
|
||||||
|
2026-07-04T13:01:34.6597983Z return 0
|
||||||
|
2026-07-04T13:01:34.6598050Z fi
|
||||||
|
2026-07-04T13:01:34.6598129Z
|
||||||
|
2026-07-04T13:01:34.6598193Z curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
2026-07-04T13:01:34.6598278Z -d "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
|
2026-07-04T13:01:34.6598353Z --data-urlencode "text=${text}" \
|
||||||
|
2026-07-04T13:01:34.6598442Z -d "parse_mode=HTML" >/dev/null || true
|
||||||
|
2026-07-04T13:01:34.6598515Z }
|
||||||
|
2026-07-04T13:01:34.6598583Z
|
||||||
|
2026-07-04T13:01:34.6598655Z notify_failure() {
|
||||||
|
2026-07-04T13:01:34.6598728Z local exit_code=$?
|
||||||
|
2026-07-04T13:01:34.6598798Z send_telegram "❌ <b>TaxBaik 배포 실패</b>
|
||||||
|
2026-07-04T13:01:34.6598890Z
|
||||||
|
2026-07-04T13:01:34.6598968Z 커밋: <code>${COMMIT}</code>
|
||||||
|
2026-07-04T13:01:34.6599054Z 시간: <code>${TIMESTAMP}</code>
|
||||||
|
2026-07-04T13:01:34.6599132Z 단계: CI/CD deploy"
|
||||||
|
2026-07-04T13:01:34.6599222Z exit "$exit_code"
|
||||||
|
2026-07-04T13:01:34.6599293Z }
|
||||||
|
2026-07-04T13:01:34.6599355Z
|
||||||
|
2026-07-04T13:01:34.6599452Z trap notify_failure ERR
|
||||||
|
2026-07-04T13:01:34.6599539Z
|
||||||
|
2026-07-04T13:01:34.6599604Z echo "=== Deploying TaxBaik $COMMIT ($TIMESTAMP) ==="
|
||||||
|
2026-07-04T13:01:34.6599681Z
|
||||||
|
2026-07-04T13:01:34.6599753Z # 1. 아티팩트 업로드
|
||||||
|
2026-07-04T13:01:34.6599828Z scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||||
|
2026-07-04T13:01:34.6599908Z taxbaik_deploy.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_${TIMESTAMP}.tgz"
|
||||||
|
2026-07-04T13:01:34.6599993Z
|
||||||
|
2026-07-04T13:01:34.6600070Z # 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리, Green-Blue 지원)
|
||||||
|
2026-07-04T13:01:34.6600159Z ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \
|
||||||
|
2026-07-04T13:01:34.6600240Z -o ServerAliveInterval=10 \
|
||||||
|
2026-07-04T13:01:34.6600323Z "$DEPLOY_USER@$DEPLOY_HOST" TAXBAIK_DEPLOY_FROM_CI=1 bash << REMOTE
|
||||||
|
2026-07-04T13:01:34.6600403Z set -e
|
||||||
|
2026-07-04T13:01:34.6600473Z DEPLOY_HOME="/home/***"
|
||||||
|
2026-07-04T13:01:34.6600555Z DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}"
|
||||||
|
2026-07-04T13:01:34.6600671Z TIMESTAMP="${TIMESTAMP}"
|
||||||
|
2026-07-04T13:01:34.6600741Z COMMIT="${COMMIT}"
|
||||||
|
2026-07-04T13:01:34.6600810Z
|
||||||
|
2026-07-04T13:01:34.6600871Z echo "--- [1/5] 압축 해제 ---"
|
||||||
|
2026-07-04T13:01:34.6600946Z mkdir -p "\$DEPLOY_DIR"
|
||||||
|
2026-07-04T13:01:34.6601017Z tar -xzf "/tmp/taxbaik_\${TIMESTAMP}.tgz" -C "\$DEPLOY_DIR"
|
||||||
|
2026-07-04T13:01:34.6601091Z rm -f "/tmp/taxbaik_\${TIMESTAMP}.tgz"
|
||||||
|
2026-07-04T13:01:34.6601165Z
|
||||||
|
2026-07-04T13:01:34.6601224Z echo "--- [2/5] 운영 설정 검증 ---"
|
||||||
|
2026-07-04T13:01:34.6601302Z test -s "\$DEPLOY_DIR/appsettings.Production.json" \
|
||||||
|
2026-07-04T13:01:34.6601375Z || { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:34.6601452Z test -s "\$DEPLOY_DIR/proxy/TaxBaik.Proxy.dll" \
|
||||||
|
2026-07-04T13:01:34.6601525Z || { echo "FATAL: TaxBaik.Proxy.dll 없음" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:34.6601608Z
|
||||||
|
2026-07-04T13:01:34.6601676Z echo "--- [3/5] 마이그레이션 사전 검증 ---"
|
||||||
|
2026-07-04T13:01:34.6601750Z test -x "\$DEPLOY_DIR/scripts/validate_migrations.sh" \
|
||||||
|
2026-07-04T13:01:34.6601824Z || { echo "FATAL: validate_migrations.sh 없음" >&2; exit 1; }
|
||||||
|
2026-07-04T13:01:34.6602022Z "\$DEPLOY_DIR/scripts/validate_migrations.sh" "\$DEPLOY_DIR/db/migrations" "postgresql://taxbaik:taxbaik123@localhost:5432/taxbaikdb"
|
||||||
|
2026-07-04T13:01:34.6602131Z
|
||||||
|
2026-07-04T13:01:34.6602198Z echo "--- [4/5] Green-Blue 배포 실행 ---"
|
||||||
|
2026-07-04T13:01:34.6602279Z chmod +x "\$DEPLOY_DIR/deploy_gb.sh"
|
||||||
|
2026-07-04T13:01:34.6602356Z "\$DEPLOY_DIR/deploy_gb.sh" "\$DEPLOY_DIR"
|
||||||
|
2026-07-04T13:01:34.6602427Z
|
||||||
|
2026-07-04T13:01:34.6602489Z echo "--- [4.5/5] Nginx 설정 검증 ---"
|
||||||
|
2026-07-04T13:01:34.6602561Z # 실제 로드되는 파일은 sites-enabled/의 심볼릭 링크 대상만이다.
|
||||||
|
2026-07-04T13:01:34.6602644Z # sites-available/에 다른 파일(예: default)이 있어도 sites-enabled에
|
||||||
|
2026-07-04T13:01:34.6602727Z # 링크되어 있지 않으면 nginx는 그 내용을 절대 읽지 않는다.
|
||||||
|
2026-07-04T13:01:34.6602804Z NGINX_CONF=""
|
||||||
|
2026-07-04T13:01:34.6602878Z for f in /etc/nginx/sites-enabled/*; do
|
||||||
|
2026-07-04T13:01:34.6602947Z if [ -e "\$f" ] && grep -q "location /taxbaik" "\$f" 2>/dev/null; then
|
||||||
|
2026-07-04T13:01:34.6603031Z NGINX_CONF=\$(readlink -f "\$f")
|
||||||
|
2026-07-04T13:01:34.6603100Z break
|
||||||
|
2026-07-04T13:01:34.6603168Z fi
|
||||||
|
2026-07-04T13:01:34.6603237Z done
|
||||||
|
2026-07-04T13:01:34.6603297Z
|
||||||
|
2026-07-04T13:01:34.6603360Z if [ -z "\$NGINX_CONF" ]; then
|
||||||
|
2026-07-04T13:01:34.6603428Z echo "❌ FATAL: sites-enabled/ 안에서 'location /taxbaik'를 정의한 파일을 찾을 수 없음" >&2
|
||||||
|
2026-07-04T13:01:34.6603520Z echo " sites-available/에 파일을 수정해도 sites-enabled에 심볼릭 링크되어 있지 않으면 반영되지 않는다." >&2
|
||||||
|
2026-07-04T13:01:34.6603616Z exit 1
|
||||||
|
2026-07-04T13:01:34.6603681Z fi
|
||||||
|
2026-07-04T13:01:34.6603749Z echo "실제 로드되는 설정 파일: \$NGINX_CONF"
|
||||||
|
2026-07-04T13:01:34.6603826Z
|
||||||
|
2026-07-04T13:01:34.6603893Z # 불변식: '/'와 '/taxbaik' location 모두 반드시 127.0.0.1:5001 (TaxBaik.Proxy)을
|
||||||
|
2026-07-04T13:01:34.6603979Z # 가리켜야 한다. 5003/5004를 직접 하드코딩하면 Green-Blue 포트 전환 시
|
||||||
|
2026-07-04T13:01:34.6604062Z # 죽은 포트를 가리키게 되어 502/404가 발생한다 (실제 발생했던 장애).
|
||||||
|
2026-07-04T13:01:34.6604145Z if grep -E "proxy_pass\s+http://127\.0\.0\.1:500[34]" "\$NGINX_CONF" > /dev/null 2>&1; then
|
||||||
|
2026-07-04T13:01:34.6604235Z echo "❌ FATAL: \$NGINX_CONF 가 포트 5003/5004를 직접 참조함 (Green-Blue 전환 시 502 발생)" >&2
|
||||||
|
2026-07-04T13:01:34.6604332Z echo " 수정: sudo sed -i 's|127.0.0.1:500[34]|127.0.0.1:5001|g' \$NGINX_CONF && sudo nginx -t && sudo systemctl reload nginx" >&2
|
||||||
|
2026-07-04T13:01:34.6604421Z exit 1
|
||||||
|
2026-07-04T13:01:34.6604491Z fi
|
||||||
|
2026-07-04T13:01:34.6604558Z
|
||||||
|
2026-07-04T13:01:34.6604619Z # proxy_pass에 URI(끝 슬래시)가 있으면 nginx가 요청 경로를 재작성하며,
|
||||||
|
2026-07-04T13:01:34.6607916Z # location 접두사와 슬래시 개수가 안 맞으면 백엔드로 이중 슬래시(//)가
|
||||||
|
2026-07-04T13:01:34.6608040Z # 전달되어 404가 발생한다 (실제 발생했던 장애). 접두사 location에서는
|
||||||
|
2026-07-04T13:01:34.6608133Z # proxy_pass에 URI를 붙이지 않는다.
|
||||||
|
2026-07-04T13:01:34.6608233Z if grep -E "location\s+/taxbaik\s*\{" -A 1 "\$NGINX_CONF" | grep -qE "proxy_pass\s+http://127\.0\.0\.1:5001/;"; then
|
||||||
|
2026-07-04T13:01:34.6608347Z echo "❌ FATAL: location /taxbaik 의 proxy_pass 에 불필요한 trailing slash가 있음 (이중 슬래시로 인한 404 위험)" >&2
|
||||||
|
2026-07-04T13:01:34.6608443Z exit 1
|
||||||
|
2026-07-04T13:01:34.6608513Z fi
|
||||||
|
2026-07-04T13:01:34.6608578Z
|
||||||
|
2026-07-04T13:01:34.6608647Z echo "✓ Nginx 설정 검증 통과 (실제 로드 파일 확인 + 포트 5001 고정 + trailing slash 없음)"
|
||||||
|
2026-07-04T13:01:34.6608736Z
|
||||||
|
2026-07-04T13:01:34.6608799Z echo "--- [5/5] 헬스 체크 (최대 60초) ---"
|
||||||
|
2026-07-04T13:01:34.6608888Z ATTEMPTS=20
|
||||||
|
2026-07-04T13:01:34.6608968Z for i in \$(seq 1 \$ATTEMPTS); do
|
||||||
|
2026-07-04T13:01:34.6609044Z STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/healthz 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T13:01:34.6609143Z if [ "\$STATUS" = "200" ]; then
|
||||||
|
2026-07-04T13:01:34.6609218Z echo "✓ [1/6] 헬스 체크 완료"
|
||||||
|
2026-07-04T13:01:34.6609291Z
|
||||||
|
2026-07-04T13:01:34.6609353Z # 검증 1: 메인 페이지 로드. curl -L + -w 는 리다이렉트 체인의 상태코드를
|
||||||
|
2026-07-04T13:01:34.6609448Z # 이어붙이므로, 첫 응답 코드만 받아 200/3xx를 허용한다.
|
||||||
|
2026-07-04T13:01:34.6609531Z MAIN_STATUS=\$(curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/ 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T13:01:34.6609626Z if ! printf '%s' "\$MAIN_STATUS" | grep -Eq '^(200|301|302|307|308)$'; then
|
||||||
|
2026-07-04T13:01:34.6609715Z echo "❌ 메인 페이지 로드 실패 (상태: \$MAIN_STATUS)" >&2
|
||||||
|
2026-07-04T13:01:34.6609801Z exit 1
|
||||||
|
2026-07-04T13:01:34.6609869Z fi
|
||||||
|
2026-07-04T13:01:34.6609935Z echo "✓ [2/6] 메인 페이지 로드 완료"
|
||||||
|
2026-07-04T13:01:34.6610016Z
|
||||||
|
2026-07-04T13:01:34.6610077Z # 검증 2: CSS 파일 로드
|
||||||
|
2026-07-04T13:01:34.6610147Z CSS_STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/css/admin.css 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T13:01:34.6610236Z if [ "\$CSS_STATUS" != "200" ]; then
|
||||||
|
2026-07-04T13:01:34.6610312Z echo "❌ CSS 파일 로드 실패 (상태: \$CSS_STATUS)" >&2
|
||||||
|
2026-07-04T13:01:34.6610402Z exit 1
|
||||||
|
2026-07-04T13:01:34.6610470Z fi
|
||||||
|
2026-07-04T13:01:34.6610544Z echo "✓ [3/6] CSS 파일 로드 완료"
|
||||||
|
2026-07-04T13:01:34.6610621Z
|
||||||
|
2026-07-04T13:01:34.6610685Z # 검증 3: 버전 정보. 파일 존재만 보면 5001이 잘못된 구 프로세스를
|
||||||
|
2026-07-04T13:01:34.6610769Z # 가리키는 장애를 놓치므로, HTTP 응답이 이번 커밋인지 확인한다.
|
||||||
|
2026-07-04T13:01:34.6610858Z if [ ! -s "\$DEPLOY_DIR/wwwroot/version.json" ]; then
|
||||||
|
2026-07-04T13:01:34.6610939Z echo "❌ version.json 누락" >&2
|
||||||
|
2026-07-04T13:01:34.6611026Z exit 1
|
||||||
|
2026-07-04T13:01:34.6611104Z fi
|
||||||
|
2026-07-04T13:01:34.6611170Z VERSION_JSON=\$(curl -fsS http://127.0.0.1:5001/taxbaik/version.json 2>/dev/null || true)
|
||||||
|
2026-07-04T13:01:34.6611253Z if ! printf '%s' "\$VERSION_JSON" | grep -q "\"version\": \"\$COMMIT\""; then
|
||||||
|
2026-07-04T13:01:34.6611332Z echo "❌ 5001 프록시가 이번 배포 버전을 제공하지 않음" >&2
|
||||||
|
2026-07-04T13:01:34.6611415Z echo " expected: \$COMMIT" >&2
|
||||||
|
2026-07-04T13:01:34.6611490Z echo " actual: \$VERSION_JSON" >&2
|
||||||
|
2026-07-04T13:01:34.6611563Z echo " 확인: 5001 포트가 TaxBaik.Proxy.dll인지, /home/***/taxbaik_port가 새 포트인지 점검" >&2
|
||||||
|
2026-07-04T13:01:34.6611663Z exit 1
|
||||||
|
2026-07-04T13:01:34.6611729Z fi
|
||||||
|
2026-07-04T13:01:34.6611794Z echo "✓ [4/6] 버전 정보 확인 완료"
|
||||||
|
2026-07-04T13:01:34.6612039Z
|
||||||
|
2026-07-04T13:01:34.6612103Z # 검증 4: 5001 프록시 확인
|
||||||
|
2026-07-04T13:01:34.6612177Z if ! ss -tlnp | grep -q ':5001 '; then
|
||||||
|
2026-07-04T13:01:34.6612250Z echo "❌ 5001 프록시가 실행 중이 아님" >&2
|
||||||
|
2026-07-04T13:01:34.6612338Z exit 1
|
||||||
|
2026-07-04T13:01:34.6612405Z fi
|
||||||
|
2026-07-04T13:01:34.6612476Z echo "✓ [5/6] 5001 프록시 확인 완료"
|
||||||
|
2026-07-04T13:01:34.6612558Z
|
||||||
|
2026-07-04T13:01:34.6612622Z # 검증 5: 관리자 로그인 페이지
|
||||||
|
2026-07-04T13:01:34.6612698Z LOGIN_STATUS=\$(curl -fsSL -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login 2>/dev/null || echo "000")
|
||||||
|
2026-07-04T13:01:34.6612790Z if [ "\$LOGIN_STATUS" != "200" ]; then
|
||||||
|
2026-07-04T13:01:34.6612871Z echo "❌ 관리자 로그인 페이지 로드 실패 (상태: \$LOGIN_STATUS)" >&2
|
||||||
|
2026-07-04T13:01:34.6612964Z exit 1
|
||||||
|
2026-07-04T13:01:34.6613037Z fi
|
||||||
|
2026-07-04T13:01:34.6613099Z echo "✓ [6/6] 관리자 페이지 로드 완료"
|
||||||
|
2026-07-04T13:01:34.6613182Z
|
||||||
|
2026-07-04T13:01:34.6613243Z echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)"
|
||||||
|
2026-07-04T13:01:34.6613322Z # 구 배포 디렉토리 정리 (최근 5개 보존)
|
||||||
|
2026-07-04T13:01:34.6613401Z ls -1dt \$DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null \
|
||||||
|
2026-07-04T13:01:34.6613481Z | tail -n +6 | xargs rm -rf 2>/dev/null || true
|
||||||
|
2026-07-04T13:01:34.6613557Z exit 0
|
||||||
|
2026-07-04T13:01:34.6613622Z fi
|
||||||
|
2026-07-04T13:01:34.6613692Z if [ "\$i" -eq "\$ATTEMPTS" ]; then
|
||||||
|
2026-07-04T13:01:34.6613767Z echo "=== FATAL: 서비스가 \$ATTEMPTS회 시도 후에도 응답하지 않음 ===" >&2
|
||||||
|
2026-07-04T13:01:34.6613858Z echo "--- 5001 listener ---" >&2
|
||||||
|
2026-07-04T13:01:34.6613947Z ss -tlnp 2>/dev/null | grep ':5001 ' >&2 || true
|
||||||
|
2026-07-04T13:01:34.6614022Z echo "--- active port file ---" >&2
|
||||||
|
2026-07-04T13:01:34.6614102Z cat "\$DEPLOY_HOME/taxbaik_port" >&2 || true
|
||||||
|
2026-07-04T13:01:34.6614174Z echo "--- 신규 앱 로그 ---" >&2
|
||||||
|
2026-07-04T13:01:34.6614245Z ACTIVE_PORT=\$(cat "\$DEPLOY_HOME/taxbaik_port" 2>/dev/null | tr -d '[:space:]' || true)
|
||||||
|
2026-07-04T13:01:34.6614329Z if [ -n "\$ACTIVE_PORT" ] && [ -s "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" ]; then
|
||||||
|
2026-07-04T13:01:34.6614409Z tail -n 80 "\$DEPLOY_DIR/web_\${ACTIVE_PORT}.log" >&2
|
||||||
|
2026-07-04T13:01:34.6614489Z else
|
||||||
|
2026-07-04T13:01:34.6614554Z ls -la "\$DEPLOY_DIR" >&2 || true
|
||||||
|
2026-07-04T13:01:34.6614626Z fi
|
||||||
|
2026-07-04T13:01:34.6614690Z echo "--- proxy 로그 ---" >&2
|
||||||
|
2026-07-04T13:01:34.6614764Z tail -n 80 "\$DEPLOY_HOME/taxbaik_proxy.log" >&2 || true
|
||||||
|
2026-07-04T13:01:34.6614848Z exit 1
|
||||||
|
2026-07-04T13:01:34.6614917Z fi
|
||||||
|
2026-07-04T13:01:34.6614986Z echo " 대기 중... (\$i/\$ATTEMPTS, HTTP \$STATUS)"
|
||||||
|
2026-07-04T13:01:34.6615062Z sleep 3
|
||||||
|
2026-07-04T13:01:34.6615131Z done
|
||||||
|
2026-07-04T13:01:34.6615194Z REMOTE
|
||||||
|
2026-07-04T13:01:34.6615256Z
|
||||||
|
2026-07-04T13:01:34.6615315Z echo "✓ 배포 완료: taxbaik_${TIMESTAMP} @ $DEPLOY_HOST"
|
||||||
|
2026-07-04T13:01:34.6615389Z
|
||||||
|
2026-07-04T13:01:34.6615454Z echo "--- 실제 공개 도메인 종단 간 검증 (Nginx/Cloudflare 경유, 최대 3회 재시도) ---"
|
||||||
|
2026-07-04T13:01:34.6615540Z ROOT_URL="https://www.taxbaik.com/" \
|
||||||
|
2026-07-04T13:01:34.6615611Z ADMIN_URL="https://www.taxbaik.com/taxbaik/admin/login" \
|
||||||
|
2026-07-04T13:01:34.6615686Z PUBLIC_MARKER="백원숙 세무회계" \
|
||||||
|
2026-07-04T13:01:34.6615758Z PUBLIC_FORBIDDEN="관리자" \
|
||||||
|
2026-07-04T13:01:34.6615829Z ADMIN_MARKER="관리자 로그인" \
|
||||||
|
2026-07-04T13:01:34.6615900Z MAX_RETRIES=3 \
|
||||||
|
2026-07-04T13:01:34.6615976Z RETRY_SLEEP_SECONDS=5 \
|
||||||
|
2026-07-04T13:01:34.6616044Z bash ./scripts/taxbaik-smoke.sh
|
||||||
|
2026-07-04T13:01:34.6616322Z echo "✓ 실제 공개 도메인 전체 정상"
|
||||||
|
2026-07-04T13:01:34.6616404Z
|
||||||
|
2026-07-04T13:01:34.6616463Z send_telegram "✅ <b>TaxBaik 배포 완료</b>
|
||||||
|
2026-07-04T13:01:34.6616542Z
|
||||||
|
2026-07-04T13:01:34.6616609Z 커밋: <code>${COMMIT}</code>
|
||||||
|
2026-07-04T13:01:34.6616695Z 시간: <code>${TIMESTAMP}</code>
|
||||||
|
2026-07-04T13:01:34.6616774Z 대상: <code>${DEPLOY_HOST}</code>
|
||||||
|
2026-07-04T13:01:34.6616853Z 채널: <code>${TELEGRAM_CHAT_ID}</code>"
|
||||||
|
2026-07-04T13:01:34.6616931Z shell: bash --noprofile --norc -e -o pipefail {0}
|
||||||
|
2026-07-04T13:01:34.6617030Z ::endgroup::
|
||||||
|
2026-07-04T13:01:34.7483080Z === Deploying TaxBaik c00d237 (20260704_220134) ===
|
||||||
|
2026-07-04T13:01:36.0560253Z --- [1/5] 압축 해제 ---
|
||||||
|
2026-07-04T13:01:36.3575256Z --- [2/5] 운영 설정 검증 ---
|
||||||
|
2026-07-04T13:01:36.3575950Z --- [3/5] 마이그레이션 사전 검증 ---
|
||||||
|
2026-07-04T13:01:36.9509044Z Migration dry-run validation passed.
|
||||||
|
2026-07-04T13:01:36.9509614Z --- [4/5] Green-Blue 배포 실행 ---
|
||||||
|
2026-07-04T13:01:36.9593876Z ===== 🚀 TaxBaik Green/Blue Deployment Script =====
|
||||||
|
2026-07-04T13:01:37.1157726Z Active Port: 5004
|
||||||
|
2026-07-04T13:01:37.1158178Z Target Port: 5003
|
||||||
|
2026-07-04T13:01:37.1158370Z Deploy Directory: /home/***/deployments/taxbaik_20260704_220134
|
||||||
|
2026-07-04T13:01:37.4066841Z === Starting New App on Port 5003 ===
|
||||||
|
2026-07-04T13:01:39.4311374Z === Health Checking Port 5003 ===
|
||||||
|
2026-07-04T13:01:39.5507815Z Waiting for health check... (1/20, Status: 000000)
|
||||||
|
2026-07-04T13:01:42.1278558Z ✓ Health check passed on port 5003 (Attempt 2/20)
|
||||||
|
2026-07-04T13:01:42.1279137Z === Switching Traffic to Port 5003 ===
|
||||||
|
2026-07-04T13:01:42.1312702Z ✓ Traffic routed to 5003 (via TaxBaik.Proxy on 5001)
|
||||||
|
2026-07-04T13:01:42.1313191Z === Stopping Old App on Port 5004 ===
|
||||||
|
2026-07-04T13:01:42.2456799Z Killing old process PID: 4150110
|
||||||
|
2026-07-04T13:01:42.2457355Z ✓ Old process terminated
|
||||||
|
2026-07-04T13:01:42.2457639Z === Cleaning Up Old Deployments ===
|
||||||
|
2026-07-04T13:01:42.2824667Z ✓ Cleanup completed
|
||||||
|
2026-07-04T13:01:42.2825372Z ===== ✅ Green/Blue Deployment Completed Successfully =====
|
||||||
|
2026-07-04T13:01:42.2827207Z --- [4.5/5] Nginx 설정 검증 ---
|
||||||
|
2026-07-04T13:01:42.2917525Z 실제 로드되는 설정 파일: /etc/nginx/sites-available/taxbaik-domains.conf
|
||||||
|
2026-07-04T13:01:42.2977470Z ✓ Nginx 설정 검증 통과 (실제 로드 파일 확인 + 포트 5001 고정 + trailing slash 없음)
|
||||||
|
2026-07-04T13:01:42.2978026Z --- [5/5] 헬스 체크 (최대 60초) ---
|
||||||
|
2026-07-04T13:01:42.3668351Z ✓ [1/6] 헬스 체크 완료
|
||||||
|
2026-07-04T13:01:42.8378177Z ✓ [2/6] 메인 페이지 로드 완료
|
||||||
|
2026-07-04T13:01:42.8839270Z ✓ [3/6] CSS 파일 로드 완료
|
||||||
|
2026-07-04T13:01:42.8929193Z ✓ [4/6] 버전 정보 확인 완료
|
||||||
|
2026-07-04T13:01:42.9068857Z ✓ [5/6] 5001 프록시 확인 완료
|
||||||
|
2026-07-04T13:01:42.9149449Z ✓ [6/6] 관리자 페이지 로드 완료
|
||||||
|
2026-07-04T13:01:42.9150100Z ✓ 서비스 정상 (시도 1/20)
|
||||||
|
2026-07-04T13:01:42.9248052Z ✓ 배포 완료: taxbaik_20260704_220134 @ ***
|
||||||
|
2026-07-04T13:01:42.9248637Z --- 실제 공개 도메인 종단 간 검증 (Nginx/Cloudflare 경유, 최대 3회 재시도) ---
|
||||||
|
2026-07-04T13:01:43.3724216Z ✓ https://www.taxbaik.com/ -> HTTP 200
|
||||||
|
2026-07-04T13:01:43.6877712Z ✓ https://www.taxbaik.com/taxbaik/ -> HTTP 200
|
||||||
|
2026-07-04T13:01:44.0126645Z retrying... (1/3)
|
||||||
|
2026-07-04T13:01:44.0128053Z ✗ https://www.taxbaik.com/taxbaik/admin/login -> body missing required marker: 관리자 로그인
|
||||||
|
2026-07-04T13:01:49.3854327Z ✓ https://www.taxbaik.com/ -> HTTP 200
|
||||||
|
2026-07-04T13:01:49.6485192Z ✓ https://www.taxbaik.com/taxbaik/ -> HTTP 200
|
||||||
|
2026-07-04T13:01:50.0153135Z ✗ https://www.taxbaik.com/taxbaik/admin/login -> body missing required marker: 관리자 로그인
|
||||||
|
2026-07-04T13:01:50.0153696Z retrying... (2/3)
|
||||||
|
2026-07-04T13:01:55.2722916Z ✓ https://www.taxbaik.com/ -> HTTP 200
|
||||||
|
2026-07-04T13:01:55.6800476Z ✓ https://www.taxbaik.com/taxbaik/ -> HTTP 200
|
||||||
|
2026-07-04T13:01:55.9334729Z ✗ https://www.taxbaik.com/taxbaik/admin/login -> body missing required marker: 관리자 로그인
|
||||||
|
2026-07-04T13:01:55.9335523Z ✗ smoke verification failed
|
||||||
|
2026-07-04T13:01:56.2238319Z ❌ Failure - Main Deploy & verify on server
|
||||||
|
2026-07-04T13:01:56.2411008Z exitcode '1': failure
|
||||||
|
2026-07-04T13:01:56.2699164Z evaluating expression 'success()'
|
||||||
|
2026-07-04T13:01:56.2699697Z expression 'success()' evaluated to 'false'
|
||||||
|
2026-07-04T13:01:56.2699842Z Skipping step 'Setup .NET' due to 'success()'
|
||||||
|
2026-07-04T13:01:56.2953445Z evaluating expression 'always()'
|
||||||
|
2026-07-04T13:01:56.2953918Z expression 'always()' evaluated to 'true'
|
||||||
|
2026-07-04T13:01:56.2954039Z ⭐ Run Post Checkout code
|
||||||
|
2026-07-04T13:01:56.2954206Z Writing entry to tarball workflow/outputcmd.txt len:0
|
||||||
|
2026-07-04T13:01:56.2954356Z Writing entry to tarball workflow/statecmd.txt len:0
|
||||||
|
2026-07-04T13:01:56.2954454Z Writing entry to tarball workflow/pathcmd.txt len:0
|
||||||
|
2026-07-04T13:01:56.2954553Z Writing entry to tarball workflow/envs.txt len:0
|
||||||
|
2026-07-04T13:01:56.2954636Z Writing entry to tarball workflow/SUMMARY.md len:0
|
||||||
|
2026-07-04T13:01:56.2954724Z Extracting content to '/var/run/act'
|
||||||
|
2026-07-04T13:01:56.2989360Z run post step for 'Checkout code'
|
||||||
|
2026-07-04T13:01:56.2989963Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
|
||||||
|
2026-07-04T13:01:56.3354078Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
|
||||||
|
2026-07-04T13:01:56.3354506Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
|
||||||
|
2026-07-04T13:01:56.3354997Z Working directory '/workspace/***/taxbaik'
|
||||||
|
2026-07-04T13:01:56.5634391Z [command]/usr/bin/git version
|
||||||
|
2026-07-04T13:01:56.5708822Z git version 2.54.0
|
||||||
|
2026-07-04T13:01:56.5752988Z ***
|
||||||
|
2026-07-04T13:01:56.5772894Z Temporarily overriding HOME='/tmp/29afdc3d-d383-4267-8baa-f87edca3a0e3' before making global git config changes
|
||||||
|
2026-07-04T13:01:56.5782158Z Adding repository directory to the temporary git global config as a safe directory
|
||||||
|
2026-07-04T13:01:56.5785763Z [command]/usr/bin/git config --global --add safe.directory /workspace/***/taxbaik
|
||||||
|
2026-07-04T13:01:56.5856624Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
|
||||||
|
2026-07-04T13:01:56.5933870Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
|
||||||
|
2026-07-04T13:01:56.6622902Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/gitea\:3000\/\.extraheader
|
||||||
|
2026-07-04T13:01:56.6697660Z http.http://gitea:3000/.extraheader
|
||||||
|
2026-07-04T13:01:56.6698844Z [command]/usr/bin/git config --local --unset-all http.http://gitea:3000/.extraheader
|
||||||
|
2026-07-04T13:01:56.6735534Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/gitea\:3000\/\.extraheader' && git config --local --unset-all 'http.http://gitea:3000/.extraheader' || :"
|
||||||
|
2026-07-04T13:01:56.6986910Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
|
||||||
|
2026-07-04T13:01:56.7046662Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
|
||||||
|
2026-07-04T13:01:56.8590734Z ✅ Success - Post Checkout code
|
||||||
|
2026-07-04T13:01:56.8750550Z Cleaning up container for job build-and-deploy
|
||||||
|
2026-07-04T13:01:57.5600546Z Removed container: 545d203ee9994ebf9eca9357f948b418a29c0a80f13c2d69e6b64ca3d86d4b5b
|
||||||
|
2026-07-04T13:01:57.5657268Z 🐳 docker volume rm GITEA-ACTIONS-TASK-1552-WORKFLOW-TaxBaik-CI-CD-JOB-build-and-de-ab2d1e70739a3063f448d1e9d12be855fbd02428b801ec092546b895bdf5c88d
|
||||||
|
2026-07-04T13:01:57.6930032Z 🐳 docker volume rm GITEA-ACTIONS-TASK-1552-WORKFLOW-TaxBaik-CI-CD-JOB-build-and-de-ab2d1e70739a3063f448d1e9d12be855fbd02428b801ec092546b895bdf5c88d-env
|
||||||
|
2026-07-04T13:01:57.9092324Z 🏁 Job failed
|
||||||
|
2026-07-04T13:01:57.9175317Z Job 'build-and-deploy' failed
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user