diff --git a/README.md b/README.md
index cc13894..83b881d 100755
--- a/README.md
+++ b/README.md
@@ -1,24 +1,10 @@
-# ๐ Web Portal โ Kubernetes + GitOps ํ๋ฉ ํ๋ก์ ํธ
+# Web Portal - Kubernetes + GitOps ๋ฐฐํฌ ๊ฐ์ด๋
-> ์กฐ์ง ๋ด๋ถ ์นํ์ด์ง๋ฅผ ํตํฉ ๊ด๋ฆฌํ๋ ํฌํธ ์์คํ
์ **์จํ๋ ๋ฏธ์ค Kubernetes ํ๊ฒฝ**์์ ์ง์ ์ค๊ณยท๊ตฌ์ถยท์ด์ํ ํ๋ฉ ํ๋ก์ ํธ์
๋๋ค.
+## ๐ ํ๋ก์ ํธ ๊ฐ์
-[](https://kubernetes.io)
-[](https://argoproj.github.io/cd)
-[](https://fastapi.tiangolo.com)
-[](https://www.postgresql.org)
-[](https://www.docker.com)
-[](https://letsencrypt.org)
-
----
-
-## ๐ ํ๋ก์ ํธ ๋ชฉ์
-
-๋จ์ํ ๋ฐ๋ผํ๋ ํํ ๋ฆฌ์ผ์ด ์๋, **์ค์ ์ด์ ํ๊ฒฝ์์ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค์ ์ง์ ๋ง๋ฅ๋จ๋ฆฌ๊ณ ํด๊ฒฐ**ํ๋ ๊ฒ์ ๋ชฉํ๋ก ๊ตฌ์ถํ์ต๋๋ค.
-
-- ์ค์ ๋๋ฉ์ธ(`cyanburu.com`) + HTTPS ์ ์ฉ์ผ๋ก ์ธ๋ถ ์ธํฐ๋ท์์ ์ ๊ทผ ๊ฐ๋ฅํ ์๋น์ค ์ด์
-- GitOps ๋ฐฉ์์ผ๋ก ์ฝ๋ ๋ณ๊ฒฝ ์ ์๋ ๋ฐฐํฌ๋๋ CI/CD ํ์ดํ๋ผ์ธ ๊ตฌ์ฑ
-- Pod ์ด์ ๊ฐ์ง, ์ธ์ฆ์ ๋ง๋ฃ ์๋ฐ ์ DiscordยทGmail ์๋ ์๋ฆผ ๊ตฌํ
-- ๋ฐ์ํ ์ฅ์ 14๊ฑด์ ๋ชจ๋ ๋ฌธ์ํํ์ฌ ์ฌํยทํด๊ฒฐ ๊ณผ์ ๊ธฐ๋ก
+์กฐ์ง ๋ด๋ถ ์นํ์ด์ง๋ฅผ ํตํฉ ๊ด๋ฆฌํ๋ ํฌํธ ์์คํ
์
๋๋ค.
+๋ก๊ทธ์ธ ํ ๋ณธ์ธ์๊ฒ ํ ๋น๋ ์นํ์ด์ง ๋ชฉ๋ก์ ํ์ธํ๊ณ ์ ์ํ ์ ์์ผ๋ฉฐ,
+๊ด๋ฆฌ์๋ ํ์ด์ง ๋ฐ ์ฌ์ฉ์ ๊ถํ์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
---
@@ -26,207 +12,53 @@
```
์ฌ์ฉ์ (์ธ๋ถ ์ธํฐ๋ท)
- โโโ https://cyanburu.com โ Web Portal
- โโโ https://gitea.cyanburu.com โ Gitea (Self-hosted Git)
- โโโ https://argo.cyanburu.com โ ArgoCD (GitOps)
- โ
- MSI ๋ผ์ฐํฐ (ํฌํธํฌ์๋ฉ 80/443)
- โ
- Nginx Ingress Controller โ TLS ์ข
๋ฃ, ๋๋ฉ์ธ๋ณ ๋ผ์ฐํ
- โ
- cert-manager โ Let's Encrypt ์ธ์ฆ์ ์๋ ๋ฐ๊ธ/๊ฐฑ์
- โ
- Kubernetes ๋ค์์คํ์ด์ค
- โโโ web-portal
- โ โโโ Nginx Frontend (SPA)
- โ โโโ FastAPI Backend (REST API)
- โ โโโ PostgreSQL DB (PVC ์๊ตฌ ์ ์ฅ)
- โโโ gitea โ Self-hosted Git + Container Registry
- โโโ argocd โ GitOps ์๋ ๋ฐฐํฌ ์์ง
- โ
- ๊ฐ๋ฐ์ git push โ Gitea โ ArgoCD ์๋ ๊ฐ์ง & ๋ฐฐํฌ
+ โโโ https://cyanburu.com โ Hub ํํ์ด์ง (ํฌํธํด๋ฆฌ์ค/์๋น์ค ํ๋ธ)
+ โโโ https://cyanburu.com/portal โ Web Portal
+ โโโ https://cyanburu.com/kingscup โ King's Cup ๊ฒ์ (๊ฐ๋ฐ ์์ )
+ โโโ https://gitea.cyanburu.com โ Gitea
+ โโโ https://argo.cyanburu.com โ ArgoCD
+ โ
+MSI ๋ผ์ฐํฐ (ํฌํธํฌ์๋ฉ 80/443)
+ โ
+Nginx Ingress Controller โ TLS ์ข
๋ฃ, ๋๋ฉ์ธ/๊ฒฝ๋ก๋ณ ๋ผ์ฐํ
+ โ
+cert-manager โ Let's Encrypt ์ธ์ฆ์ ์๋ ๋ฐ๊ธ/๊ฐฑ์
+ โ
+Kubernetes ๋ค์์คํ์ด์ค๋ณ ์๋น์ค
+ โโโ hub
+ โ โโโ Nginx (์ ์ HTML) โ cyanburu.com/ ํ๋ธ ํํ์ด์ง
+ โโโ web-portal
+ โ โโโ Nginx Frontend (ClusterIP: 80)
+ โ โโโ FastAPI Backend (ClusterIP: 8000)
+ โ โโโ PostgreSQL DB (ClusterIP: 5432)
+ โโโ gitea
+ โ โโโ Gitea (ClusterIP: 3000)
+ โโโ argocd
+ โโโ ArgoCD Server (ClusterIP: 443)
+ โ
+๊ฐ๋ฐ์ (git push)
+ โ
+Gitea โ ArgoCD ์๋ ๊ฐ์ง & ๋ฐฐํฌ
```
---
## ๐ ๏ธ ๊ธฐ์ ์คํ
-| ๊ตฌ๋ถ | ๊ธฐ์ | ์ ํ ์ด์ |
-|------|------|-----------|
-| **Container Orchestration** | Kubernetes (Docker Desktop ๋ด์ฅ) | ์จํ๋ ๋ฏธ์ค ํ๊ฒฝ์์ ํ๋ก๋์
๊ณผ ๋์ผํ ๊ตฌ์กฐ ๊ตฌํ |
-| **GitOps** | Gitea + ArgoCD | Git์ ๋จ์ผ ์ง์ค ์์ค๋ก, ์ ์ธ์ ๋ฐฐํฌ ์๋ํ |
-| **Image Registry** | Gitea Container Registry | ์ธ๋ถ ์์กด ์์ด ์ฌ๋ด ๋ ์ง์คํธ๋ฆฌ ์์ฒด ์ด์ |
-| **Ingress** | Nginx Ingress Controller | ๋๋ฉ์ธ ๊ธฐ๋ฐ ๋ผ์ฐํ
, TLS ์ข
๋ฃ ์ฒ๋ฆฌ |
-| **TLS** | cert-manager + Let's Encrypt | ์ธ์ฆ์ ๋ฐ๊ธยท๊ฐฑ์ ์์ ์๋ํ |
-| **Backend** | Python FastAPI | ๋น๋๊ธฐ REST API, JWT ์ธ์ฆ |
-| **Database** | PostgreSQL + PVC | ์ปจํ
์ด๋ ์ฌ์์ ํ์๋ ๋ฐ์ดํฐ ์์ |
-| **Monitoring** | APScheduler (์ปค์คํ
) | Pod ์ํ 1๋ถ ์ฃผ๊ธฐ, ์ธ์ฆ์ ๋ง๋ฃ 24์๊ฐ ์ฃผ๊ธฐ ์ฒดํฌ |
-| **Alerting** | Discord Webhook + Gmail | ์ฅ์ ๋ฐ์ ์ ์ฆ์ ์๋ฆผ |
-
----
-
-## โจ ๊ตฌํ ๊ธฐ๋ฅ
-
-### ๋ณด์ (Security)
-- JWT ๊ธฐ๋ฐ ์ธ์ฆ / ์ธ์
๊ด๋ฆฌ
-- **๋น๋ฐ๋ฒํธ 5ํ ์ค๋ฅ ์ ๊ณ์ ์๋ ์ ๊ธ** โ ๊ด๋ฆฌ์ ํด์
-- **์ต์ด ๋ก๊ทธ์ธ ๊ฐ์ ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ** (์์ ๋น๋ฐ๋ฒํธ ๋ฐ๊ธ ํฌํจ)
-- **Nginx Rate Limiting** โ ๋ก๊ทธ์ธ API ๋ถ๋น 5ํ ์ ํ (Brute Force ๋ฐฉ์ด)
-- HTTPS ์๋ ๋ฆฌ๋ค์ด๋ ํธ (`ssl-redirect: "true"`)
-
-### ๋ชจ๋ํฐ๋ง & ์๋ฆผ (Observability)
-- Pod ์ํ 1๋ถ๋ง๋ค ์๋ ์ฒดํฌ โ ์ด์/๋ณต๊ตฌ ์ **Discord + Gmail ์๋ฆผ**
-- ์ธ์ฆ์ ๋ง๋ฃ 24์๊ฐ ์ฃผ๊ธฐ ์ฒดํฌ โ ๋ง๋ฃ ์๋ฐ ์ Gmail ์๋ฆผ
-- **์น UI์์ ์๋ฆผ ์ฑ๋ ๊ด๋ฆฌ** (Discord/Gmail/ํผํฉ ์ฑ๋ ๋ค์ค ๋ฑ๋ก)
-- DB ๊ธฐ๋ฐ ์๋ฆผ ์ค์ โ Pod ์ฌ์์ ํ์๋ ์ค์ ์ ์ง
-
-### ๊ด๋ฆฌ์ ๊ธฐ๋ฅ
-- ์ฌ์ฉ์ ๊ณ์ ์์ฑ/์ญ์ , ์ ๊ทผ ๊ถํ ์ค์ (์ฒดํฌ๋ฐ์ค UI)
-- ์์ ๋น๋ฐ๋ฒํธ ์๋ ์์ฑ ๋ฐ ๋ฐ๊ธ
-- ๊ณต์ง์ฌํญ / ๊ด๋ฆฌ์ ์์ฒญ ๊ฒ์ํ (๋๊ธยท๋ต๊ธ ํฌํจ)
-- ์ฌ์ฉ์ ์ํ ํ๊ทธ: `์ ์` / `๐์ ๊น` / `์ด๊ธฐPW` / `๋ณ๊ฒฝ์์ฒญ`
-
----
-
-## ๐
๊ตฌ์ถ ์ด๋ ฅ
-
-| ๋ ์ง | ์ฃผ์ ์์
|
-|------|-----------|
-| 2026-04-06 | K8s ์ด๊ธฐ ๊ตฌ์ถ, FastAPI + Nginx + PostgreSQL ๋ฐฐํฌ, Gitea + ArgoCD GitOps ๊ตฌ์ฑ |
-| 2026-04-10 | ๋๋ฉ์ธ ์ฐ๊ฒฐ (HTTPS), cert-manager, CoreDNS ํค์ดํ NAT ์ฐํ, ๋ณด์ ๊ธฐ๋ฅ ๊ฐํ |
-| 2026-04-27 | Discord/Gmail ์๋ฆผ ์ถ๊ฐ, APScheduler ๋ชจ๋ํฐ๋ง, ์๋ฆผ ์ฑ๋ ๊ด๋ฆฌ UI |
-
----
-
-## ๐ฅ ํธ๋ฌ๋ธ์ํ
(14๊ฑด ํด๊ฒฐ)
-
-์ค์ ๊ตฌ์ถ ๊ณผ์ ์์ ๊ฒช์ ์ฅ์ ์ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๊ธฐ๋กํฉ๋๋ค.
-
-
-1. NodePort ํฌํธ ์ถฉ๋
-
-**์ฆ์** `provided port is already allocated`
-**์์ธ** ํด๋น NodePort๊ฐ ๋ค๋ฅธ ์๋น์ค์์ ์ด๋ฏธ ์ฌ์ฉ ์ค
-**ํด๊ฒฐ** `k8s/05-frontend.yaml`์์ NodePort ๋ฒํธ ๋ณ๊ฒฝ ํ ์๋น์ค ์ฌ์ ์ฉ
-
-
-
-
-2. Backend CrashLoopBackOff โ Liveness Probe ์คํจ
-
-**์ฆ์** `Liveness probe failed: HTTP probe failed with statuscode: 404`
-**์์ธ** DB ์ฐ๊ฒฐ ๋๊ธฐ ์ค liveness probe๊ฐ ๋จผ์ ์คํจํด K8s๊ฐ ๊ฐ์ ์ฌ์์
-**ํด๊ฒฐ** `initialDelaySeconds: 60`, `failureThreshold: 5`๋ก ๋๊ธฐ ์๊ฐ ์ฆ๊ฐ
-
-
-
-
-3. Nginx API ํ๋ก์ ์คํจ โ JSON ํ์ฑ ์ค๋ฅ
-
-**์ฆ์** ๋ก๊ทธ์ธ ์ `Unexpected token '<', "..." is not valid JSON`
-**์์ธ** Nginx๊ฐ `/api/` ์์ฒญ์ ๋ฐฑ์๋๋ก ์ ๋ฌํ์ง ๋ชปํ๊ณ HTML ๋ฐํ
-**ํด๊ฒฐ** `proxy_pass`๋ฅผ K8s ๋ด๋ถ FQDN(`backend-service.web-portal.svc.cluster.local`)์ผ๋ก ๋ณ๊ฒฝ
-
-
-
-
-4. ArgoCD โ authentication required: Unauthorized
-
-**์ฆ์** `failed to list refs: authentication required`
-**์์ธ** ArgoCD๊ฐ Gitea ์ ์ฅ์ ์ธ์ฆ ์ ๋ณด ์์
-**ํด๊ฒฐ** `argocd.argoproj.io/secret-type=repository` ๋ ์ด๋ธ์ด ์๋ Secret ์ง์ ์์ฑ
-
-
-
-
-5. RepeatedResourceWarning โ ๋ฆฌ์์ค ์ค๋ณต
-
-**์ฆ์** `Resource appeared 2 times among application resources`
-**์์ธ** `k8s/` ํด๋ ๋ด ๋์ผ ๋ฆฌ์์ค๋ฅผ ์ ์ํ๋ YAML ์ค๋ณต ์กด์ฌ
-**ํด๊ฒฐ** ์ค๋ณต ํ์ผ ์ญ์ ํ push
-
-
-
-
-6. ImagePullBackOff โ Gitea Registry HTTP ์ ๊ทผ ์ค๋ฅ
-
-**์ฆ์** `server gave HTTP response to HTTPS client`
-**์์ธ** Gitea Registry๊ฐ HTTP์ธ๋ฐ Docker๊ฐ HTTPS๋ก ์ ๊ทผ ์๋
-**ํด๊ฒฐ** Docker Desktop `insecure-registries` ์ค์ ์ถ๊ฐ
-
-
-
-
-7. Gitea Container Registry ๋ก๊ทธ์ธ ์คํจ (context deadline exceeded)
-
-**์ฆ์** `Get "http://...:30000/v2/": context deadline exceeded`
-**์์ธ** Gitea ROOT_URL์ด `localhost`๋ก ์ค์ ๋์ด token ์์ฒญ์ด ์ธ๋ถ๋ก ๋๊ฐ์ง ๋ชปํจ
-**ํด๊ฒฐ** Helm upgrade๋ก `ROOT_URL`, `DOMAIN`, Packages ํ์ฑํ ์ค์ ๋ณ๊ฒฝ
-
-
-
-
-8. K8s ๋ด๋ถ Gitea Registry ์ด๋ฏธ์ง Pull ์คํจ
-
-**์ฆ์** ์ธ๋ถ์์๋ push ์ฑ๊ณต, Pod๋ `ImagePullBackOff`
-**์์ธ** K8s Pod โ ์ธ๋ถ IP ๊ฒฝ๋ก ๋ถ์์
-**ํด๊ฒฐ** image ์ฃผ์๋ฅผ K8s ๋ด๋ถ ์๋น์ค๋ช
(`gitea-http.gitea.svc.cluster.local:3000`)์ผ๋ก ๋ณ๊ฒฝ
-
-
-
-
-9. ๋ก๊ทธ์ธ ์คํจ โ ๋น๋ฐ๋ฒํธ ํด์ ์ค์ผ
-
-**์ฆ์** ์ฌ๋ฐ๋ฅธ ๊ณ์ ์ผ๋ก๋ ๋ก๊ทธ์ธ ์คํจ
-**์์ธ** ํฐ๋ฏธ๋ ์์ ์ฝ๋(`\x1B[0m`)๊ฐ bcrypt ํด์์ ์์ฌ DB์ ์ ์ฅ๋จ
-**ํด๊ฒฐ** Backend Pod์์ Python์ผ๋ก ํด์ ์ฌ์์ฑ ํ DB ์ง์ ์
๋ฐ์ดํธ
-
-
-
-
-10. git push ๊ฑฐ์ โ non-fast-forward
-
-**์ฆ์** `Updates were rejected because the tip of your current branch is behind`
-**์์ธ** Gitea UI์์ ์ง์ ํ์ผ ์์ ์ผ๋ก ๋ก์ปฌ-์๊ฒฉ ๋ธ๋์น diverge
-**ํด๊ฒฐ** `git pull origin main --rebase` ํ push
-
-
-
-
-11. cert-manager HTTP01 Challenge Pending โ ํค์ดํ NAT
-
-**์ฆ์** `propagation check failed: context deadline exceeded`
-**์์ธ** cert-manager๊ฐ K8s ๋ด๋ถ์์ ์ธ๋ถ ๋๋ฉ์ธ์ผ๋ก self-check ์ ํค์ดํ NAT ๋ฏธ์ง์์ผ๋ก ํ์์์
-**ํด๊ฒฐ** CoreDNS์ ๋ด๋ถ ๋๋ฉ์ธ์ ์ง์ ๋ฑ๋ก โ K8s ๋ด๋ถ์์ ๋๋ฉ์ธ์ ๋ด๋ถ IP๋ก ํด์
-
-
-
-
-12. Ingress Controller EXTERNAL-IP๊ฐ localhost๋ก ํ์
-
-**์ฆ์** `kubectl get svc -n ingress-nginx` โ EXTERNAL-IP: `localhost`
-**์์ธ** Docker Desktop ํ๊ฒฝ์ ์ ์ ๋์ (`localhost` = ์ค์ PC)
-**ํด๊ฒฐ** ํฌํธํฌ์๋ฉ์ NodePort๊ฐ ์๋ **80/443 โ PC ๋ด๋ถ IP:80/443**์ผ๋ก ์ง์ ์ค์
-
-
-
-
-13. git commit ์คํจ โ Author identity unknown
-
-**์ฆ์** `fatal: unable to auto-detect email address`
-**์์ธ** Git ์ฌ์ฉ์ ์ ๋ณด ๋ฏธ์ค์
-**ํด๊ฒฐ** `git config --global user.email`, `user.name` ์ค์
-
-
-
-
-14. Gitea Registry ImagePullBackOff โ ํ ํฐ ์ธ์ฆ ํค์ดํ NAT
-
-**์ฆ์** `Get "http://gitea-http...svc.cluster.local:3000/v2/token?...": context deadline exceeded`
-**์์ธ** Docker ๋ฐ๋ชฌ์ด K8s ๋ด๋ถ DNS๋ฅผ ํด์ํ์ง ๋ชปํด ํ ํฐ ์ธ์ฆ ์คํจ
-**ํด๊ฒฐ** image ์ฃผ์์ Registry Secret์ ์ธ๋ถ IP(`192.168.10.101:30000`)๋ก ํต์ผ
-
-
+| ๊ตฌ๋ถ | ๊ธฐ์ |
+|------|------|
+| Frontend | Nginx + HTML/CSS/JS (SPA) |
+| Backend | Python FastAPI |
+| Database | PostgreSQL |
+| Cache | Redis (King's Cup ์ธ์
) |
+| Container | Docker Desktop |
+| Orchestration | Kubernetes (Docker Desktop ๋ด์ฅ) |
+| GitOps | Gitea + ArgoCD |
+| Image Registry | Gitea Container Registry |
+| Ingress | Nginx Ingress Controller |
+| TLS | cert-manager + Let's Encrypt |
+| Domain | cyanburu.com (ํ์ด์ฆ) |
+| ์๋ธ๋๋ฉ์ธ | gitea.cyanburu.com, argo.cyanburu.com |
---
@@ -234,92 +66,890 @@
```
nginx-portal/
+โโโ .gitea/
+โ โโโ workflows/
+โ โโโ build-and-push.yaml # Gitea Actions CI (์ ํ์ฌํญ)
โโโ backend/
-โ โโโ main.py # FastAPI ์ ์ฒด API ๋ก์ง (์ธ์ฆ, ์ฌ์ฉ์ ๊ด๋ฆฌ, ๊ฒ์ํ)
-โ โโโ notifier.py # Discord/Gmail ์๋ฆผ ๋ชจ๋ (DB ๊ธฐ๋ฐ ๋ค์ค ์ฑ๋)
-โ โโโ monitor.py # APScheduler ๋ชจ๋ํฐ๋ง (Pod ์ํ, ์ธ์ฆ์ ๋ง๋ฃ)
+โ โโโ main.py # FastAPI ์ ์ฒด API ๋ก์ง
โ โโโ requirements.txt
โ โโโ Dockerfile
โโโ frontend/
-โ โโโ index.html # ์ฑ๊ธ ํ์ด์ง ์ฑ (SPA)
-โ โโโ nginx.conf # Nginx ์ค์ + Rate Limiting + /api/* ํ๋ก์
+โ โโโ index.html # ์ฑ๊ธ ํ์ด์ง ์ฑ (SPA)
+โ โโโ nginx.conf # Nginx ์ค์ + /api/* ํ๋ก์ (/portal subpath ๋์)
+โ โโโ Dockerfile
+โโโ hub/ # ํ๋ธ ํํ์ด์ง (cyanburu.com/)
+โ โโโ index.html # ํฌํธํด๋ฆฌ์ค/์๋น์ค ํ๋ธ ์ ์ ํ์ด์ง
โ โโโ Dockerfile
โโโ k8s/
-โ โโโ 01-namespace.yaml
-โ โโโ 02-postgres.yaml # PostgreSQL + PVC
-โ โโโ 03-secrets.yaml # DB / JWT ์ํฌ๋ฆฟ
-โ โโโ 04-backend.yaml # FastAPI Deployment
-โ โโโ 05-frontend.yaml # Nginx Deployment
-โ โโโ 07-clusterissuer.yaml # Let's Encrypt ClusterIssuer
-โ โโโ 08-ingress.yaml # cyanburu.com
-โ โโโ 09-ingress-gitea.yaml
-โ โโโ 10-ingress-argocd.yaml
-โโโ 06-argocd-app.yaml # ArgoCD Application (k8s/ ํด๋ ๋ฐ โ ์ํ ์ฐธ์กฐ ๋ฐฉ์ง)
+โ โโโ 00-hub.yaml # hub ๋ค์์คํ์ด์ค + Deployment + Service
+โ โโโ 01-namespace.yaml # web-portal ๋ค์์คํ์ด์ค
+โ โโโ 02-postgres.yaml # PostgreSQL + PVC + Service
+โ โโโ 03-secrets.yaml # DB/JWT ์ํฌ๋ฆฟ
+โ โโโ 04-backend.yaml # FastAPI Deployment + Service
+โ โโโ 05-frontend.yaml # Nginx Deployment + NodePort(30090)
+โ โโโ 07-clusterissuer.yaml # Let's Encrypt ClusterIssuer
+โ โโโ 08-ingress.yaml # Web Portal Ingress (cyanburu.com/portal)
+โ โโโ 09-ingress-gitea.yaml # Gitea Ingress (gitea.cyanburu.com)
+โ โโโ 10-ingress-argocd.yaml # ArgoCD Ingress (argo.cyanburu.com)
+โ โโโ 11-notify-secrets.yaml # Discord Webhook / Gmail Secret
+โ โโโ 12-???.yaml # ๊ธฐ์กด ํ์ผ
+โ โโโ 13-ingress-hub.yaml # Hub Ingress (cyanburu.com/)
+โโโ 06-argocd-app.yaml # ArgoCD Application ์ ์ (k8s ํด๋ ๋ฐ์ ์์น)
โโโ README.md
```
+> โ ๏ธ `06-argocd-app.yaml` ์ ๋ฐ๋์ `k8s/` ํด๋ **๋ฐ**์ ์์นํด์ผ ํฉ๋๋ค.
+> ArgoCD๊ฐ `k8s/` ํด๋๋ฅผ ๊ฐ์ํ๋ฏ๋ก ํด๋น ํ์ผ์ด ์์ ์์ผ๋ฉด ์ํ ์ฐธ์กฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
+
---
-## ๐ ๋ฐฐํฌ ๋ฐฉ๋ฒ
+## ๐ ๊ธฐ๋ณธ ๊ณ์
-### ์ฌ์ ์๊ตฌ ์ฌํญ
-- Docker Desktop (Kubernetes ํ์ฑํ)
-- kubectl, helm ์ค์น
-- ๊ณต์ธ ๋๋ฉ์ธ (A ๋ ์ฝ๋ โ ๊ณต์ธ IP ์ฐ๊ฒฐ)
-- ๋ผ์ฐํฐ ํฌํธํฌ์๋ฉ ์ค์ (80, 443)
+| ๊ตฌ๋ถ | ID | Password |
+|------|-----|----------|
+| ๊ด๋ฆฌ์ | `admin` | `admin1234` |
+| ์ผ๋ฐ์ฌ์ฉ์ | `user1` | `user1234` |
-### ํต์ฌ ๋ฐฐํฌ ์์
+---
+## โจ ๊ธฐ๋ฅ ์ค๋ช
+
+### ์ผ๋ฐ ์ฌ์ฉ์
+- ๋ก๊ทธ์ธ ํ **MY Page List** ์์ ๋ณธ์ธ์๊ฒ ํ ๋น๋ ์นํ์ด์ง๋ฅผ ์นด๋ ํํ๋ก ํ์ธ
+- ์นด๋์ Favicon ์๋ ํ์ (์์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์์ด์ฝ)
+- ์นด๋ ํด๋ฆญ ์ **์ ํญ์์ ํด๋น URL๋ก ์ด๋**
+- **๊ณต์ง์ฌํญ** ํญ์์ ๊ด๋ฆฌ์๊ฐ ๋ฑ๋กํ ๊ณต์ง ํ์ธ ๋ฐ ๋๊ธ ์์ฑ
+- **๊ด๋ฆฌ์ ์์ฒญ** ํญ์์ ๊ฒ์๊ธ ์์ฑ ๋ฐ ๋ต๊ธ ์์ฑ
+- ๋ก๊ทธ์ธ ๋น๋ฐ๋ฒํธ ํ์/์จ๊น ํ ๊ธ ๋ฒํผ
+- ๋ก๊ทธ์ธ ์คํจ ์ ์์ด๋ ์ ์ง (๋น๋ฐ๋ฒํธ๋ง ์ด๊ธฐํ)
+- **๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ** ๋ฉ๋ด (ํค๋์์ ์ธ์ ๋ ๋ณ๊ฒฝ ๊ฐ๋ฅ)
+- **์ต์ด ๋ก๊ทธ์ธ ์ ๋น๋ฐ๋ฒํธ ๊ฐ์ ๋ณ๊ฒฝ** (๋ณ๊ฒฝ ์ ๊น์ง ์๋น์ค ์ด์ฉ ๋ถ๊ฐ)
+- **๋น๋ฐ๋ฒํธ 5ํ ์ค๋ฅ ์ ๊ณ์ ์๋ ์ ๊ธ** โ ๊ด๋ฆฌ์์๊ฒ ์ ๊ธ ํด์ ์์ฒญ ํ์
+
+### ๊ด๋ฆฌ์
+์ผ๋ฐ ์ฌ์ฉ์ ๊ธฐ๋ฅ + ์ถ๊ฐ:
+- **ํ์ด์ง ๊ด๋ฆฌ**: ์นํ์ด์ง ์ถ๊ฐ / ์์ / ์ญ์
+- **์ฌ์ฉ์ ๊ด๋ฆฌ**: ๊ณ์ ์์ฑ / ์ญ์
+- **๊ถํ ์ค์ **: ์ฌ์ฉ์๋ณ ์ ๊ทผ ๊ฐ๋ฅ ํ์ด์ง๋ฅผ ์ฒดํฌ๋ฐ์ค๋ก ์ง์
+- **๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ**: ํน์ ์ฌ์ฉ์์ ๋น๋ฐ๋ฒํธ ์ง์ ๋ณ๊ฒฝ (๋ณ๊ฒฝ ํ ํด๋น ์ฌ์ฉ์ ๊ฐ์ ๋ณ๊ฒฝ ์ ์ฉ)
+- **์์ ๋น๋ฐ๋ฒํธ ๋ฐ๊ธ**: ๋๋ค ์์ ๋น๋ฐ๋ฒํธ ์๋ ์์ฑ ํ ํ๋ฉด์ ํ์
+- **๊ณ์ ์ ๊ธ ํด์ **: ์ ๊ธด ๊ณ์ ์ ๋ฒํผ ํ๋๋ก ํด์
+- **์ฌ์ฉ์ ์ํ ํ์ธ**: ์ ์ / ๐์ ๊น / ์ด๊ธฐPW / ๋ณ๊ฒฝ์์ฒญ ํ๊ทธ๋ก ํ๋์ ํ์ธ
+- **๊ณต์ง์ฌํญ ์์ฑ**: ๊ณต์ง ํญ์์ ์ ์ฒด ์ฌ์ฉ์์๊ฒ ๊ณต์ง ๋ฑ๋ก / ์ญ์
+- **๐ ํ ์นด๋ ๊ด๋ฆฌ**: `cyanburu.com` ๋ฉ์ธ ํ๋ธ ํํ์ด์ง์ ํ์๋ ์นด๋ ์ถ๊ฐ / ์์ / ์ญ์ / ์์ ๋ณ๊ฒฝ
+
+### ๊ฒ์ํ (๊ณต์ง / ๊ด๋ฆฌ์ ์์ฒญ)
+
+| ๊ตฌ๋ถ | ๊ณต์ง | ๊ด๋ฆฌ์ ์์ฒญ |
+|------|------|------------|
+| ๊ธ ์์ฑ | ๊ด๋ฆฌ์๋ง ๊ฐ๋ฅ | ๋ชจ๋ ์ฌ์ฉ์ ๊ฐ๋ฅ |
+| ๋๊ธ/๋ต๊ธ | ๋ชจ๋ ์ฌ์ฉ์ ๊ฐ๋ฅ | ๋ชจ๋ ์ฌ์ฉ์ ๊ฐ๋ฅ |
+| ๊ธ ์ญ์ | ๊ด๋ฆฌ์๋ง ๊ฐ๋ฅ | ๋ณธ์ธ ๋๋ ๊ด๋ฆฌ์ |
+| ๋๊ธ ์ญ์ | ๋ณธ์ธ ๋๋ ๊ด๋ฆฌ์ | ๋ณธ์ธ ๋๋ ๊ด๋ฆฌ์ |
+| ๋ชฉ๋ก ํญ๋ชฉ | ๋ฒํธ, ์ ๋ชฉ, ์์ฑ์, ์์ฑ์ผ(์๊ฐํฌํจ) | ๋ฒํธ, ์ ๋ชฉ, ์์ฑ์, ์์ฑ์ผ(์๊ฐํฌํจ) |
+
+---
+
+## ๐ ์ต์ด ๋ฐฐํฌ ์์
+
+### 1๋จ๊ณ. Docker Desktop insecure-registry ์ค์
+Gitea Registry๊ฐ HTTP์ด๋ฏ๋ก Docker Desktop์์ ํ์ฉ ์ค์ ํ์.
+
+Docker Desktop โ Settings โ Docker Engine:
+```json
+{
+ "builder": {
+ "gc": {
+ "defaultKeepStorage": "20GB",
+ "enabled": true
+ }
+ },
+ "experimental": false,
+ "insecure-registries": ["192.168.10.101:30000"]
+}
+```
+**Apply & Restart** ํด๋ฆญ
+
+### 2๋จ๊ณ. Gitea Registry ๋ก๊ทธ์ธ ๋ฐ ์ด๋ฏธ์ง ๋น๋ & Push
```bash
-# 1. Nginx Ingress Controller
-kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/cloud/deploy.yaml
+# Registry ๋ก๊ทธ์ธ
+docker login 192.168.10.101:30000 -u
-# 2. cert-manager
-kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml
+# ๋ฐฑ์๋ ์ด๋ฏธ์ง ๋น๋ & Push
+docker build -t 192.168.10.101:30000/<๊ณ์ >/portal-backend:latest ./backend/
+docker push 192.168.10.101:30000/<๊ณ์ >/portal-backend:latest
-# 3. CoreDNS ํค์ดํ NAT ์ฐํ ์ค์
-kubectl patch configmap coredns -n kube-system --patch-file coredns-patch.yaml
-kubectl rollout restart deployment/coredns -n kube-system
+# ํ๋ก ํธ์๋ ์ด๋ฏธ์ง ๋น๋ & Push
+docker build -t 192.168.10.101:30000/<๊ณ์ >/portal-frontend:latest ./frontend/
+docker push 192.168.10.101:30000/<๊ณ์ >/portal-frontend:latest
+```
-# 4. ์ด๋ฏธ์ง ๋น๋ & Push
-docker build -t /portal-backend:latest ./backend/
-docker build -t /portal-frontend:latest ./frontend/
-docker push /portal-backend:latest
-docker push /portal-frontend:latest
+### 3๋จ๊ณ. K8s Registry Secret ์์ฑ
+```bash
+kubectl create namespace web-portal
-# 5. K8s ๋ฆฌ์์ค ๋ฐฐํฌ
-kubectl apply -f k8s/
+kubectl create secret docker-registry gitea-registry-secret \
+ --namespace=web-portal \
+ --docker-server=gitea-http.gitea.svc.cluster.local:3000 \
+ --docker-username= \
+ --docker-password=
+```
-# 6. ArgoCD Application ๋ฑ๋ก
+### 4๋จ๊ณ. ArgoCD์ Gitea ์ ์ฅ์ ์ธ์ฆ ๋ฑ๋ก
+```bash
+kubectl create secret generic gitea-repo-secret \
+ --namespace=argocd \
+ --from-literal=type=git \
+ --from-literal=url=http://192.168.10.101:30000/<๊ณ์ >/nginx-portal.git \
+ --from-literal=username= \
+ --from-literal=password=
+
+kubectl label secret gitea-repo-secret \
+ -n argocd \
+ argocd.argoproj.io/secret-type=repository
+```
+
+### 5๋จ๊ณ. ArgoCD Application ๋ฑ๋ก
+```bash
kubectl apply -f 06-argocd-app.yaml
```
-์์ธํ ๋ฐฐํฌ ๊ฐ์ด๋๋ [๋ฐฐํฌ ๋ฌธ์](docs/deployment.md)๋ฅผ ์ฐธ๊ณ ํ์ธ์.
+### 6๋จ๊ณ. Nginx Ingress Controller ์ค์น (์ต์ด 1ํ)
+```bash
+kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/cloud/deploy.yaml
+kubectl get pods -n ingress-nginx
+```
+
+### 7๋จ๊ณ. cert-manager ์ค์น (์ต์ด 1ํ)
+```bash
+kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml
+kubectl get pods -n cert-manager
+```
+
+### 8๋จ๊ณ. CoreDNS ๋ด๋ถ ๋๋ฉ์ธ ๋ฑ๋ก (ํค์ดํ NAT ์ฐํ)
+```bash
+kubectl patch configmap coredns -n kube-system --patch-file coredns-patch.yaml
+kubectl rollout restart deployment/coredns -n kube-system
+```
+
+> `coredns-patch.yaml` ๋ด์ฉ:
+> ```yaml
+> data:
+> Corefile: |
+> cyanburu.com {
+> hosts {
+> 192.168.10.101 cyanburu.com
+> fallthrough
+> }
+> cache 30
+> }
+> .:53 {
+> ... (๊ธฐ์กด ๋ด์ฉ ์ ์ง)
+> }
+> ```
+
+### 9๋จ๊ณ. ๋ผ์ฐํฐ ํฌํธํฌ์๋ฉ ์ค์
+MSI ๋ผ์ฐํฐ์์ ์ค์ :
+
+| ๊ณต์ฉ ํฌํธ | ๋ด๋ถ IP | ๋น๊ณต๊ฐ ํฌํธ |
+|-----------|---------|-------------|
+| 80 | 192.168.10.101 | 80 |
+| 443 | 192.168.10.101 | 443 |
+
+### 10๋จ๊ณ. Ingress + ClusterIssuer ๋ฐฐํฌ
+```bash
+git add k8s/07-clusterissuer.yaml k8s/08-ingress.yaml k8s/05-frontend.yaml
+git commit -m "feat: Ingress + cert-manager HTTPS ์ค์ "
+git push origin main
+# ArgoCD๊ฐ ์๋ ๋ฐฐํฌ
+```
+
+### 11๋จ๊ณ. ์ธ์ฆ์ ๋ฐ๊ธ ํ์ธ
+```bash
+kubectl get certificate -n web-portal
+# READY: True ํ์ธ
+```
+
+### 12๋จ๊ณ. ์๋ธ๋๋ฉ์ธ Ingress ์ ์ฉ (Gitea, ArgoCD)
+```bash
+kubectl apply -f 09-ingress-gitea.yaml
+kubectl apply -f 10-ingress-argocd.yaml
+
+# ์ธ์ฆ์ ๋ฐ๊ธ ํ์ธ
+kubectl get certificate -n gitea
+kubectl get certificate -n argocd
+```
+
+### 13๋จ๊ณ. ์ ์ ํ์ธ
+
+| ์๋น์ค | URL |
+|--------|-----|
+| Hub ํํ์ด์ง | `https://cyanburu.com` |
+| Web Portal | `https://cyanburu.com/portal` |
+| Gitea | `https://gitea.cyanburu.com` |
+| ArgoCD | `https://argo.cyanburu.com` |
+
+### 14๋จ๊ณ. Hub ํํ์ด์ง ๋ฐฐํฌ (์ต์ด 1ํ)
+```bash
+# hub ๋ค์์คํ์ด์ค ์์ฑ
+kubectl create namespace hub
+
+# Registry Secret ์์ฑ
+kubectl create secret docker-registry gitea-registry-secret \
+ --namespace=hub \
+ --docker-server=192.168.10.101:30000 \
+ --docker-username=<๊ณ์ > \
+ --docker-password=<ํจ์ค์๋>
+
+# ์ด๋ฏธ์ง ๋น๋ & Push
+docker build -t 192.168.10.101:30000/<๊ณ์ >/hub:latest ./hub/
+docker push 192.168.10.101:30000/<๊ณ์ >/hub:latest
+
+# ๋ฐฐํฌ
+kubectl apply -f k8s/00-hub.yaml
+kubectl apply -f k8s/13-ingress-hub.yaml
+```
---
-## ๐ ๋ฐฐ์ด ์ / ํต์ฌ ์ธ์ฌ์ดํธ
+## ๐ ์ดํ ๋ฐฐํฌ ๋ฐฉ๋ฒ (์ฝ๋ ์์ ์)
-| ๋ฌธ์ ์ ํ | ๋ฐฐ์ด ์ |
-|-----------|---------|
-| **ํค์ดํ NAT** | K8s ๋ด๋ถ์์ ์ธ๋ถ ๋๋ฉ์ธ์ผ๋ก self-check ์ ๋ฐ์ํ๋ ๋คํธ์ํฌ ๋ฃจํ โ CoreDNS ๋ด๋ถ ์ค๋ฒ๋ผ์ด๋๋ก ํด๊ฒฐ |
-| **Docker ๋ฐ๋ชฌ vs kubelet DNS** | kubelet์ K8s ๋ด๋ถ DNS ์ฌ์ฉ ๊ฐ๋ฅ, Docker ๋ฐ๋ชฌ์ ํธ์คํธ DNS๋ง ์ฌ์ฉ โ ๋ ์ง์คํธ๋ฆฌ ์ฃผ์ ํต์ผ ํ์ |
-| **GitOps ์ํ ์ฐธ์กฐ** | ArgoCD Application YAML์ ๊ฐ์ ๋์ ํด๋ ์์ ๋ฃ์ผ๋ฉด ๋ฌดํ ๋ฃจํ โ ํด๋ ๋ฐ ๋ณ๋ ๊ด๋ฆฌ |
-| **์ปจํ
์ด๋ ์์ ์์** | DB ์ค๋น ์ ๋ฐฑ์๋ probe ์คํ โ `initialDelaySeconds`๋ก ์์กด์ฑ ์์ ์ ์ด |
-| **bcrypt ํด์ ์ค์ผ** | ํฐ๋ฏธ๋ ANSI ์์ ์ฝ๋๊ฐ ํด์์ ์์ด๋ ์ฃ์ง ์ผ์ด์ค โ ์ถ๋ ฅ๊ฐ ์ง์ ๊ฒ์ฆ ํ์ |
+### yaml๋ง ๋ณ๊ฒฝํ ๊ฒฝ์ฐ (์ค์ ๋ณ๊ฒฝ)
+```bash
+git add .
+git commit -m "fix: ๋ณ๊ฒฝ๋ด์ฉ"
+git push origin main
+# โ ArgoCD๊ฐ ์๋์ผ๋ก ๊ฐ์งํด์ ์ฌ๋ฐฐํฌ
+```
+
+### ์ด๋ฏธ์ง๋ ๋ณ๊ฒฝํ ๊ฒฝ์ฐ (์ฝ๋ ๋ณ๊ฒฝ)
+```bash
+# ์ด๋ฏธ์ง ์ฌ๋น๋ & Push
+docker build -t 192.168.10.101:30000/<๊ณ์ >/portal-backend:latest ./backend/
+docker push 192.168.10.101:30000/<๊ณ์ >/portal-backend:latest
+
+# Pod ์ฌ์์
+kubectl rollout restart deployment/backend -n web-portal
+
+# yaml๋ ๋ณ๊ฒฝํ๋ค๋ฉด
+git add .
+git commit -m "feat: ๋ณ๊ฒฝ๋ด์ฉ"
+git push origin main
+```
---
-## ๐ ๊ฐ์ ์์ (Next Steps)
+## ๐ง ์ด์ ๋ช
๋ น์ด
-- [ ] GitHub Actions CI/CD ํ์ดํ๋ผ์ธ ๊ตฌ์ฑ (์๋ ๋น๋ โ Push โ ArgoCD sync)
-- [ ] Helm Chart๋ก ํจํค์ง (values.yaml ํ๊ฒฝ๋ณ ๋ถ๋ฆฌ)
-- [ ] Prometheus + Grafana ๋ชจ๋ํฐ๋ง ์คํ ๋์
-- [ ] Terraform์ผ๋ก Azure ์ธํ๋ผ IaC ๊ด๋ฆฌ
-- [ ] Trivy ์ด๋ฏธ์ง ์ทจ์ฝ์ ์ค์บ โ GitHub Actions ํตํฉ
+```bash
+# ์ ์ฒด ๋ฆฌ์์ค ์ํ ํ์ธ
+kubectl get all -n web-portal
+
+# ๋ฐฑ์๋ ๋ก๊ทธ ํ์ธ
+kubectl logs -n web-portal deployment/backend -f
+
+# ํ๋ก ํธ์๋ ๋ก๊ทธ ํ์ธ
+kubectl logs -n web-portal deployment/frontend -f
+
+# Pod ์ฌ์์
+kubectl rollout restart deployment/backend -n web-portal
+kubectl rollout restart deployment/frontend -n web-portal
+
+# ์ ์ฒด ์ญ์
+kubectl delete namespace web-portal
+```
---
-## ๐ ๋ผ์ด์ ์ค
+## ๐ ์ด์ ์ ๋ณด์ ์ค์
-MIT License
+### K8s Secret ๋ณ๊ฒฝ
+`k8s/03-secrets.yaml` ์์ ๋ฐ๋์ ๋ณ๊ฒฝ:
+```yaml
+stringData:
+ db-password: "๊ฐ๋ ฅํํจ์ค์๋๋ก๋ณ๊ฒฝ"
+ jwt-secret: "64์์ด์์๋๋ค๋ฌธ์์ด๋ก๋ณ๊ฒฝ"
+```
+
+### Nginx Rate Limiting (Brute Force ๋ฐฉ์ด)
+`frontend/nginx.conf` ์๋จ์ ์ถ๊ฐ:
+```nginx
+limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
+```
+๋ก๊ทธ์ธ location์ ์ ์ฉ:
+```nginx
+location /api/auth/login {
+ limit_req zone=login_limit burst=3 nodelay;
+ limit_req_status 429;
+ proxy_pass http://backend-service.web-portal.svc.cluster.local:8000/api/auth/login;
+ ...
+}
+```
+๊ฐ์ IP์์ ๋ถ๋น 5ํ ์ด๊ณผ ์ 429 ์๋ต ๋ฐํ. ๊ณ์ ์ ๊ธ(5ํ ์คํจ)๊ณผ ํจ๊ป ์ด์ค์ผ๋ก Brute Force๋ฅผ ๋ฐฉ์ดํฉ๋๋ค.
+
+### ์ถ๊ฐ ๋ณด์ ๊ถ๊ณ ์ฌํญ
+- **Fail2ban**: Nginx ๋ก๊ทธ ๊ฐ์ ํ ๋ฐ๋ณต ์คํจ IP๋ฅผ ๋ฐฉํ๋ฒฝ์ผ๋ก ์๋ ์ฐจ๋จ (์จํ๋ ๋ฏธ์ค ํ๊ฒฝ ๊ถ์ฅ)
+- **CAPTCHA**: ๋ก๊ทธ์ธ 3ํ ์คํจ ์ Google reCAPTCHA ํ์ (์ถ๊ฐ ๊ฐ๋ฐ ํ์)
+- **JWT ๋ง๋ฃ ์๊ฐ ๋จ์ถ**: `main.py` ์์ `timedelta(hours=8)` โ `timedelta(hours=2)` ๋ณ๊ฒฝ ๊ฐ๋ฅ
+
+### HTTPS / TLS
+- cert-manager๊ฐ Let's Encrypt ์ธ์ฆ์๋ฅผ **์๋์ผ๋ก ๊ฐฑ์ ** (๋ง๋ฃ 30์ผ ์ )
+- HTTP ์ ์ ์ ์๋์ผ๋ก HTTPS๋ก ๋ฆฌ๋ค์ด๋ ํธ (`ssl-redirect: "true"`)
+- ์ธ์ฆ์ ์ํ ํ์ธ:
+```bash
+kubectl get certificate -n web-portal # cyanburu.com
+kubectl get certificate -n gitea # gitea.cyanburu.com
+kubectl get certificate -n argocd # argo.cyanburu.com
+```
+
+---
+
+## โ ํธ๋ฌ๋ธ์ํ
+
+### 1. NodePort ํฌํธ ์ถฉ๋
+**์ฆ์**
+```
+Service "frontend-service" is invalid: spec.ports[0].nodePort:
+Invalid value: 30080: provided port is already allocated
+```
+**์์ธ** ํด๋น NodePort๊ฐ ๋ค๋ฅธ ์๋น์ค์์ ์ด๋ฏธ ์ฌ์ฉ ์ค.
+
+**ํด๊ฒฐ**
+`k8s/05-frontend.yaml` ์์ nodePort๋ฅผ ๋ค๋ฅธ ๋ฒํธ๋ก ๋ณ๊ฒฝ (30000~32767 ๋ฒ์):
+```yaml
+nodePort: 30090
+```
+๋ณ๊ฒฝ ํ ๊ธฐ์กด ์๋น์ค ์ญ์ ๋ฐ ์ฌ์ ์ฉ:
+```bash
+kubectl delete service frontend-service -n web-portal
+kubectl apply -f k8s/05-frontend.yaml
+```
+
+---
+
+### 2. Backend Liveness Probe ์คํจ๋ก ์ธํ CrashLoopBackOff
+**์ฆ์**
+```
+Liveness probe failed: HTTP probe failed with statuscode: 404
+Back-off restarting failed container
+```
+**์์ธ** Backend๊ฐ DB ์ฐ๊ฒฐ์ ๊ธฐ๋ค๋ฆฌ๋ ๋์ liveness probe๊ฐ ๋จผ์ ์คํจํด์ K8s๊ฐ ๊ฐ์ ์ฌ์์.
+
+**ํด๊ฒฐ**
+`k8s/04-backend.yaml` ์ probe ๋๊ธฐ ์๊ฐ ์ฆ๊ฐ:
+```yaml
+readinessProbe:
+ initialDelaySeconds: 20
+ periodSeconds: 5
+ failureThreshold: 6
+livenessProbe:
+ initialDelaySeconds: 60
+ periodSeconds: 15
+ failureThreshold: 5
+```
+
+---
+
+### 3. Nginx๊ฐ API ์์ฒญ์ ๋ฐฑ์๋๋ก ํ๋ก์ ๋ชปํจ
+**์ฆ์** ๋ก๊ทธ์ธ ์ `Unexpected token '<', "..." is not valid JSON` ์๋ฌ.
+
+**์์ธ** Nginx๊ฐ `/api/` ์์ฒญ์ ๋ฐฑ์๋๋ก ์ ๋ฌํ์ง ๋ชปํ๊ณ HTML์ ๋ฐํ.
+
+**ํด๊ฒฐ**
+`frontend/nginx.conf` ์์ ๋ฐฑ์๋ ์ฃผ์๋ฅผ FQDN์ผ๋ก ๋ณ๊ฒฝ:
+```nginx
+location /api/ {
+ proxy_pass http://backend-service.web-portal.svc.cluster.local:8000/api/;
+}
+```
+
+---
+
+### 4. ArgoCD - authentication required: Unauthorized
+**์ฆ์**
+```
+failed to list refs: authentication required: Unauthorized
+```
+**์์ธ** ArgoCD๊ฐ Gitea ์ ์ฅ์์ ์ ๊ทผํ ์ธ์ฆ ์ ๋ณด๊ฐ ์์.
+
+**ํด๊ฒฐ**
+kubectl๋ก ์ง์ ์ธ์ฆ Secret ์์ฑ:
+```bash
+kubectl create secret generic gitea-repo-secret \
+ --namespace=argocd \
+ --from-literal=type=git \
+ --from-literal=url=http://192.168.10.101:30000/<๊ณ์ >/nginx-portal.git \
+ --from-literal=username=<๊ณ์ > \
+ --from-literal=password=<ํจ์ค์๋>
+
+kubectl label secret gitea-repo-secret \
+ -n argocd \
+ argocd.argoproj.io/secret-type=repository
+```
+
+---
+
+### 5. RepeatedResourceWarning - ๋ฆฌ์์ค ์ค๋ณต
+**์ฆ์**
+```
+Resource apps/Deployment/web-portal/backend appeared 2 times among application resources
+```
+**์์ธ** `k8s/` ํด๋ ์์ ๋์ผํ ๋ฆฌ์์ค๋ฅผ ์ ์ํ๋ yaml ํ์ผ์ด ์ค๋ณต ์กด์ฌ (`portal.yaml` ๋ฑ).
+
+**ํด๊ฒฐ**
+์ค๋ณต ํ์ผ ์ญ์ ํ push:
+```bash
+rm k8s/portal.yaml
+git add .
+git commit -m "fix: ์ค๋ณต yaml ํ์ผ ์ ๊ฑฐ"
+git push origin main
+```
+
+---
+
+### 6. ImagePullBackOff - Gitea Registry HTTP ์ ๊ทผ ์ค๋ฅ
+**์ฆ์**
+```
+Failed to pull image: server gave HTTP response to HTTPS client
+```
+**์์ธ** Gitea Registry๊ฐ HTTP์ธ๋ฐ Docker๊ฐ HTTPS๋ก ์ ๊ทผ ์๋.
+
+**ํด๊ฒฐ**
+Docker Desktop โ Settings โ Docker Engine์ insecure-registry ์ถ๊ฐ:
+```json
+{
+ "insecure-registries": ["192.168.10.101:30000"]
+}
+```
+Apply & Restart ํ ์ด๋ฏธ์ง ์ฌ๋น๋ & Push.
+
+---
+
+### 7. Gitea Container Registry ๋ก๊ทธ์ธ ์คํจ (context deadline exceeded)
+**์ฆ์**
+```
+Error response from daemon: Get "http://192.168.10.101:30000/v2/":
+context deadline exceeded
+```
+**์์ธ** Gitea์ ROOT_URL์ด `localhost` ๋ก ์ค์ ๋์ด ์์ด token ์์ฒญ์ด ์ธ๋ถ๋ก ๋๊ฐ์ง ๋ชปํจ.
+๋ํ Packages(Container Registry) ๊ธฐ๋ฅ์ด ๋นํ์ฑํ๋ ์ํ.
+
+**ํด๊ฒฐ**
+Helm upgrade๋ก Gitea ์ค์ ์๊ตฌ ๋ณ๊ฒฝ:
+```bash
+helm repo add gitea https://dl.gitea.com/charts/
+helm repo update
+
+helm upgrade gitea gitea/gitea -n gitea \
+ --set gitea.config.server.DOMAIN=192.168.10.101 \
+ --set gitea.config.server.ROOT_URL=http://192.168.10.101:30000 \
+ --set gitea.config.server.HTTP_PORT=3000 \
+ --set gitea.config.packages.ENABLED=true \
+ --set service.http.type=NodePort \
+ --set service.http.nodePort=30000 \
+ --reuse-values
+```
+
+---
+
+### 8. K8s ๋ด๋ถ์์ Gitea Registry ์ด๋ฏธ์ง Pull ์คํจ
+**์ฆ์** Pod๊ฐ `ImagePullBackOff` ์ํ. ์ธ๋ถ์์๋ push๊ฐ ๋์ง๋ง K8s Pod๋ ์ด๋ฏธ์ง๋ฅผ ๋ชป ๊ฐ์ ธ์ด.
+
+**์์ธ** K8s Pod๋ ์ธ๋ถ IP(`192.168.10.101:30000`)๋ก ์ ๊ทผ์ด ๋ถ์์ ํ๋ฏ๋ก
+๋ด๋ถ ์๋น์ค๋ช
์ผ๋ก ์ ๊ทผํด์ผ ํจ.
+
+**ํด๊ฒฐ**
+yaml์ image ์ฃผ์๋ฅผ K8s ๋ด๋ถ ์๋น์ค๋ช
์ผ๋ก ๋ณ๊ฒฝ:
+```bash
+sed -i "s|192.168.10.101:30000|gitea-http.gitea.svc.cluster.local:3000|g" k8s/04-backend.yaml
+sed -i "s|192.168.10.101:30000|gitea-http.gitea.svc.cluster.local:3000|g" k8s/05-frontend.yaml
+```
+Registry Secret๋ ๋ด๋ถ ์ฃผ์๋ก ์ฌ์์ฑ:
+```bash
+kubectl delete secret gitea-registry-secret -n web-portal
+
+kubectl create secret docker-registry gitea-registry-secret \
+ --namespace=web-portal \
+ --docker-server=gitea-http.gitea.svc.cluster.local:3000 \
+ --docker-username=<๊ณ์ > \
+ --docker-password=<ํจ์ค์๋>
+```
+
+---
+
+### 9. ๋ก๊ทธ์ธ ์คํจ - ๋น๋ฐ๋ฒํธ ํด์ ์ค์ผ
+**์ฆ์** ์ฌ๋ฐ๋ฅธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํด๋ `์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค` ์ถ๋ ฅ.
+
+**์์ธ** ํฐ๋ฏธ๋ ์์ ์ฝ๋(`\x1B[0m` ๋ฑ)๊ฐ ๋น๋ฐ๋ฒํธ ํด์์ ์์ฌ DB์ ์ ์ฅ๋จ.
+
+**ํด๊ฒฐ**
+Backend Pod์์ ์ง์ Python์ผ๋ก ํด์ ์ฌ์์ฑ ํ DB ์
๋ฐ์ดํธ:
+```bash
+kubectl exec -n web-portal deployment/backend -- python3 -c "
+import bcrypt, psycopg2
+conn = psycopg2.connect(host='postgres-service', database='portaldb', user='portaluser', password='portalpass')
+cur = conn.cursor()
+h1 = bcrypt.hashpw('admin1234'.encode(), bcrypt.gensalt()).decode()
+h2 = bcrypt.hashpw('user1234'.encode(), bcrypt.gensalt()).decode()
+cur.execute('UPDATE users SET password_hash=%s WHERE username=%s', (h1, 'admin'))
+cur.execute('UPDATE users SET password_hash=%s WHERE username=%s', (h2, 'user1'))
+conn.commit()
+print('์๋ฃ')
+"
+```
+
+---
+
+### 10. git push ๊ฑฐ์ (non-fast-forward)
+**์ฆ์**
+```
+error: failed to push some refs
+hint: Updates were rejected because the tip of your current branch is behind
+```
+**์์ธ** Gitea UI์์ ์ง์ ํ์ผ์ ์์ ํด์ ๋ก์ปฌ๊ณผ ์๊ฒฉ ๋ธ๋์น๊ฐ diverge๋ ์ํ.
+
+**ํด๊ฒฐ**
+```bash
+git pull origin main --rebase
+git push origin main
+```
+
+---
+
+### 11. cert-manager HTTP01 Challenge pending (ํค์ดํ NAT)
+**์ฆ์**
+```
+propagation check failed: failed to perform self check GET request
+context deadline exceeded (Client.Timeout exceeded while awaiting headers)
+```
+**์์ธ** cert-manager๊ฐ K8s ๋ด๋ถ์์ ์ธ๋ถ ๋๋ฉ์ธ(`cyanburu.com`)์ผ๋ก self-check ์์ฒญ์ ๋ณด๋ผ ๋,
+๊ณต์ธ IP โ ๋ผ์ฐํฐ โ ๋ด๋ถ PC๋ก ๋์์ค๋ ํค์ดํ NAT์ด ์ง์๋์ง ์์ ํ์์์ ๋ฐ์.
+
+**ํด๊ฒฐ**
+CoreDNS์ ๋ด๋ถ ๋๋ฉ์ธ์ ์ง์ ๋ฑ๋กํด์ K8s ๋ด๋ถ์์ ๋๋ฉ์ธ์ ๋ด๋ถ IP๋ก ํด์ํ๊ฒ ์ค์ :
+```bash
+kubectl patch configmap coredns -n kube-system --patch-file coredns-patch.yaml
+kubectl rollout restart deployment/coredns -n kube-system
+```
+
+---
+
+### 12. Ingress Controller EXTERNAL-IP๊ฐ localhost๋ก ํ์
+**์ฆ์** `kubectl get svc -n ingress-nginx` ์์ EXTERNAL-IP๊ฐ `localhost` ๋ก ํ์๋จ.
+
+**์์ธ** Docker Desktop ํ๊ฒฝ์ ์ ์์ ์ธ ๋์. `localhost` = ์ค์ PC๋ฅผ ์๋ฏธ.
+
+**ํด๊ฒฐ** ํฌํธํฌ์๋ฉ์ NodePort(30118, 30963)๊ฐ ์๋ **80, 443 โ PC๋ด๋ถIP:80, 443** ์ผ๋ก ์ค์ .
+Docker Desktop์ด 80/443์ ๋ฐ์์ Ingress Controller๋ก ์๋ ์ ๋ฌ.
+
+---
+
+### 13. git commit ์ Author identity unknown
+**์ฆ์**
+```
+Author identity unknown
+fatal: unable to auto-detect email address
+```
+**์์ธ** Git ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์ค์ ๋์ง ์์ ์ํ.
+
+**ํด๊ฒฐ**
+```bash
+git config --global user.email "๊ณ์ @gitea.com"
+git config --global user.name "๊ณ์ ๋ช
"
+```
+
+---
+
+### 14. Gitea Registry ImagePullBackOff โ ํ ํฐ ์ธ์ฆ ํค์ดํ NAT ๋ฌธ์
+**์ฆ์**
+```
+Failed to pull image: Error response from daemon:
+Get "http://gitea-http.gitea.svc.cluster.local:3000/v2/token?...": context deadline exceeded
+```
+**์์ธ** Docker Desktop ํ๊ฒฝ์์ kubelet์ด ์ด๋ฏธ์ง๋ฅผ Pullํ ๋ Docker ๋ฐ๋ชฌ์ ํตํด ์ํํ๋๋ฐ,
+Docker ๋ฐ๋ชฌ์ K8s ๋ด๋ถ DNS(`gitea-http.gitea.svc.cluster.local`)๋ฅผ ํด์ํ์ง ๋ชปํจ.
+Gitea Registry๊ฐ ํ ํฐ ์ธ์ฆ์ ๋ด๋ถ ์๋น์ค๋ช
์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธํ๋ฉด Docker ๋ฐ๋ชฌ์ด ์ ๊ทผ ๋ถ๊ฐ โ timeout ๋ฐ์.
+
+**ํด๊ฒฐ**
+์ด๋ฏธ์ง ์ฃผ์์ Registry Secret์ ์ธ๋ถ IP๋ก ํต์ผ:
+```bash
+# yaml ์ด๋ฏธ์ง ์ฃผ์ ๋ณ๊ฒฝ
+sed -i "s|gitea-http.gitea.svc.cluster.local:3000|192.168.10.101:30000|g" k8s/04-backend.yaml
+sed -i "s|gitea-http.gitea.svc.cluster.local:3000|192.168.10.101:30000|g" k8s/05-frontend.yaml
+
+# Registry Secret ์ฌ์์ฑ (์ธ๋ถ IP ๊ธฐ์ค)
+kubectl delete secret gitea-registry-secret -n web-portal
+kubectl create secret docker-registry gitea-registry-secret \
+ --namespace=web-portal \
+ --docker-server=192.168.10.101:30000 \
+ --docker-username=<๊ณ์ > \
+ --docker-password=<ํจ์ค์๋>
+
+# ์ ์ฉ
+kubectl apply -f k8s/04-backend.yaml
+kubectl apply -f k8s/05-frontend.yaml
+kubectl rollout restart deployment/backend -n web-portal
+kubectl rollout restart deployment/frontend -n web-portal
+```
+
+> โ ๏ธ Gitea ROOT_URL์ ๋ด๋ถ ์๋น์ค๋ช
์ผ๋ก ๋ณ๊ฒฝํด๋ Docker ๋ฐ๋ชฌ์ K8s ๋ด๋ถ DNS๋ฅผ ์ฌ์ฉํ ์ ์์ผ๋ฏ๋ก
+> Docker Desktop ํ๊ฒฝ์์๋ ๋ฐ๋์ ์ธ๋ถ IP(`192.168.10.101:30000`)๋ฅผ ์ฌ์ฉํด์ผ ํจ.
+
+---
+
+### 15. ๋ชจ๋ํฐ๋ง ์๋ฆผ ํ๊ฒฝ๋ณ์ ๋ฏธ์ ์ฉ (notifier skipping)
+**์ฆ์**
+```
+[NOTIFIER] Discord webhook URL not set, skipping
+[NOTIFIER] Gmail config not set, skipping
+```
+**์์ธ** `kubectl set image` ๋ช
๋ น์ด๋ก ์ด๋ฏธ์ง๋ฅผ ๋ณ๊ฒฝํ๋ฉด deployment์ ํ๊ฒฝ๋ณ์ ์ค์ ์ด ์ด๊ธฐํ๋จ.
+๋๋ Secret ์ด๋ฆ์ด yaml๊ณผ ๋ค๋ฅด๊ฒ ์์ฑ๋ ๊ฒฝ์ฐ.
+
+**ํด๊ฒฐ**
+yaml์ Secret ์ด๋ฆ(`notify-secrets`)๊ณผ key ์ด๋ฆ์ ํ์ธ ํ ๋์ผํ๊ฒ Secret ์ฌ์์ฑ:
+```bash
+kubectl delete secret notify-secrets -n web-portal
+kubectl create secret generic notify-secrets \
+ --namespace=web-portal \
+ --from-literal=discord-webhook-url="https://discord.com/api/webhooks/..." \
+ --from-literal=gmail-user="๋ฐ์ก๊ณ์ @gmail.com" \
+ --from-literal=gmail-app-password="xxxx xxxx xxxx xxxx" \
+ --from-literal=alert-email-to="์์ ๊ณ์ @gmail.com"
+
+kubectl rollout restart deployment/backend -n web-portal
+```
+yaml ๋ณ๊ฒฝ ํ์๋ ๋ฐ๋์ `kubectl apply -f k8s/04-backend.yaml` ๋ก ์ฌ์ ์ฉํ ๊ฒ.
+
+---
+
+## ๐
๋ณ๊ฒฝ ์ด๋ ฅ
+
+### 2026-04-06 (์ด๊ธฐ ๊ตฌ์ถ)
+- Kubernetes ํ๊ฒฝ ๊ตฌ์ฑ (Docker Desktop)
+- FastAPI ๋ฐฑ์๋ + Nginx ํ๋ก ํธ์๋ + PostgreSQL ๋ฐฐํฌ
+- Gitea + ArgoCD GitOps ํ์ดํ๋ผ์ธ ๊ตฌ์ฑ
+- Gitea Container Registry ์ฐ๋
+
+### 2026-04-10 (๊ธฐ๋ฅ ์ถ๊ฐ + ๋๋ฉ์ธ ์ฐ๊ฒฐ)
+#### ๊ธฐ๋ฅ ์ถ๊ฐ
+- **MY Page**: ํญ๋ช
/๋ชฉ๋ก ์ ๋ชฉ ์๋ฌธ ๋ณ๊ฒฝ, URL ๋ฏธํ๊ธฐ, Favicon ์๋ ํ์
+- **๋น๋ฐ๋ฒํธ ๋ณด์ ๊ฐํ**
+ - ๋ก๊ทธ์ธ ๋น๋ฐ๋ฒํธ ํ์/์จ๊น ํ ๊ธ ๋ฒํผ
+ - ๋ก๊ทธ์ธ ์คํจ ์ ์์ด๋ ์ ์ง (๋น๋ฐ๋ฒํธ๋ง ์ด๊ธฐํ)
+ - ์ต์ด ๋ก๊ทธ์ธ ์ ๋น๋ฐ๋ฒํธ ๊ฐ์ ๋ณ๊ฒฝ
+ - ๋น๋ฐ๋ฒํธ 5ํ ์ค๋ฅ ์ ๊ณ์ ์๋ ์ ๊ธ
+ - ๊ด๋ฆฌ์์ ์ฌ์ฉ์ ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ / ์์ ๋น๋ฐ๋ฒํธ ๋ฐ๊ธ / ์ ๊ธ ํด์
+- **๊ณต์ง์ฌํญ ํญ**: ๊ด๋ฆฌ์ ์์ฑ ์ ์ฉ, ๋ชจ๋ ์ฌ์ฉ์ ๋๊ธ ๊ฐ๋ฅ
+- **๊ด๋ฆฌ์ ์์ฒญ ํญ**: ๊ฒ์ํ ํํ, ๋ชจ๋ ์ฌ์ฉ์ ์์ฑ/๋ต๊ธ ๊ฐ๋ฅ
+- **Nginx Rate Limiting**: ๋ก๊ทธ์ธ API ๋ถ๋น 5ํ ์ ํ (Brute Force ๋ฐฉ์ด)
+
+#### ๋๋ฉ์ธ ์ฐ๊ฒฐ (HTTPS)
+- **Nginx Ingress Controller** ์ค์น ๋ฐ ๊ตฌ์ฑ
+- **cert-manager** ์ค์น + Let's Encrypt ์ธ์ฆ์ ์๋ ๋ฐ๊ธ
+- **cyanburu.com** ๋๋ฉ์ธ ์ฐ๊ฒฐ (ํ์ด์ฆ)
+- **MSI ๋ผ์ฐํฐ** ํฌํธํฌ์๋ฉ ์ค์ (80/443)
+- **CoreDNS** ๋ด๋ถ ๋๋ฉ์ธ ๋ฑ๋ก (ํค์ดํ NAT ์ฐํ)
+- **HTTPS ์๋ ๋ฆฌ๋ค์ด๋ ํธ** ์ ์ฉ
+- ์ต์ข
์ ์ URL: `https://cyanburu.com`
+
+#### ์๋ธ๋๋ฉ์ธ ์ฐ๊ฒฐ
+- **gitea.cyanburu.com** โ Gitea (Let's Encrypt ์ธ์ฆ์ ์๋ ๋ฐ๊ธ)
+- **argo.cyanburu.com** โ ArgoCD (Let's Encrypt ์ธ์ฆ์ ์๋ ๋ฐ๊ธ)
+- CoreDNS์ ์๋ธ๋๋ฉ์ธ ๋ด๋ถ IP ๋ฑ๋ก (ํค์ดํ NAT ์ฐํ)
+
+### 2026-04-27 (๋ชจ๋ํฐ๋ง ์๋ฆผ ์ถ๊ฐ + ์ฅ์ ๋ณต๊ตฌ)
+
+#### ๊ธฐ๋ฅ ์ถ๊ฐ
+- **Discord ์๋ฆผ**: Pod ์ด์/๋ณต๊ตฌ, ๊ณ์ ์ ๊ธ, ์์ ๋น๋ฐ๋ฒํธ ๋ฐ๊ธ ์ Discord Webhook ์๋ฆผ
+- **Gmail ์๋ฆผ**: Pod ์ด์/๋ณต๊ตฌ, ์ธ์ฆ์ ๋ง๋ฃ ์๋ฐ ์ ์ด๋ฉ์ผ ์๋ฆผ
+- **์๋ ๋ชจ๋ํฐ๋ง ์ค์ผ์ค๋ฌ**: Pod ์ํ 1๋ถ๋ง๋ค ์ฒดํฌ, ์ธ์ฆ์ ๋ง๋ฃ 24์๊ฐ๋ง๋ค ์ฒดํฌ
+- `notifier.py` โ Discord/Gmail ์๋ฆผ ๋ชจ๋ ์ถ๊ฐ
+- `monitor.py` โ APScheduler ๊ธฐ๋ฐ ๋ชจ๋ํฐ๋ง ๋ชจ๋ ์ถ๊ฐ
+- `main.py` โ `asyncio.create_task()` โ `await` ๋ฐฉ์์ผ๋ก ์์ (๋๊ธฐํจ์ ๋ด ๋น๋๊ธฐ ํธ์ถ ์ค๋ฅ ์์ )
+
+#### ์๋ฆผ ์ค์ ๋ฐฉ๋ฒ
+**1. Discord Webhook URL ๋ฐ๊ธ**
+Discord ์๋ฒ โ ์๋ฆผ๋ฐ์ ์ฑ๋ โ โ๏ธ ์ฑ๋ ์ค์ โ ์ฐ๋ โ ์นํํฌ โ ์ ์นํํฌ ์์ฑ โ URL ๋ณต์ฌ
+
+**2. Gmail ์ฑ ๋น๋ฐ๋ฒํธ ๋ฐ๊ธ**
+- Google ๊ณ์ โ ๋ณด์ โ 2๋จ๊ณ ์ธ์ฆ ํ์ฑํ (ํ์)
+- `https://myaccount.google.com/apppasswords` โ ์ฑ ์ด๋ฆ ์
๋ ฅ โ 16์๋ฆฌ ๋น๋ฐ๋ฒํธ ๋ณต์ฌ
+
+**3. K8s Secret ๋ฑ๋ก**
+```bash
+kubectl create secret generic notify-secrets \
+ --namespace=web-portal \
+ --from-literal=discord-webhook-url="https://discord.com/api/webhooks/..." \
+ --from-literal=gmail-user="๋ฐ์ก๊ณ์ @gmail.com" \
+ --from-literal=gmail-app-password="xxxx xxxx xxxx xxxx" \
+ --from-literal=alert-email-to="์์ ๊ณ์ @gmail.com"
+```
+
+**4. ์๋ฆผ ํ
์คํธ**
+๋ธ๋ผ์ฐ์ ์ฝ์(F12)์์ ์คํ:
+```javascript
+fetch('/api/admin/notify-test', {
+ headers: { 'Authorization': 'Bearer ' + localStorage.getItem('portal_token') }
+}).then(r => r.json()).then(console.log)
+```
+
+#### ์ฅ์ ๋ณต๊ตฌ ๋ด์ฉ
+- **backend ImagePullBackOff** โ Gitea Registry ํ ํฐ ์ธ์ฆ ๋ฌธ์ ๋ก ๋ฐ์, ์ธ๋ถ IP ๋ฐฉ์์ผ๋ก ํด๊ฒฐ
+- **backend/frontend ์ด๋ฏธ์ง ์ฃผ์** โ ๋ด๋ถ ์๋น์ค๋ช
(`gitea-http.gitea.svc.cluster.local:3000`) โ ์ธ๋ถ IP(`192.168.10.101:30000`)๋ก ๋ณ๊ฒฝ
+- **notifier.py, monitor.py ๋๋ฝ** โ Dockerfile์ ํ์ผ์ด ์์์ผ๋ `--no-cache` ์ฌ๋น๋๋ก ํด๊ฒฐ
+- **notify-secrets** โ ๊ธฐ์กด Secret์ด ์๋ชป๋ ๊ฐ์ผ๋ก ๋ฑ๋ก๋์ด ์์ด ์ฌ์์ฑ
+
+### 2026-04-27 (์๋ฆผ ์ฑ๋ ๊ด๋ฆฌ UI ์ถ๊ฐ)
+
+#### ๊ธฐ๋ฅ ์ถ๊ฐ
+- **๐ ์๋ฆผ ์ฑ๋ ๊ด๋ฆฌ ํ์ด์ง** โ ๊ด๋ฆฌ์ ํญ์ ์ถ๊ฐ, ์น์์ ์ง์ ์๋ฆผ ์ฑ๋ ์ถ๊ฐ/์์ /์ญ์ ๊ฐ๋ฅ
+- **๋ค์ค ์ฑ๋ ์ง์** โ Discord ์ ์ฉ / Gmail ์ ์ฉ / Discord+Gmail ํผํฉ ์ฑ๋์ ๋ณต์๋ก ๋ฑ๋ก ๊ฐ๋ฅ
+- **์ฑ๋ ์ ํ๋ณ ๋ฐ์ก (B๋ฐฉ์)** โ ์ฑ๋ ์ ํ๊ณผ ์๋ฆผ ์ข
๋ฅ์ ๊ต์งํฉ์ผ๋ก ๋ฐ์ก ๋์ ์๋ ๊ฒฐ์
+- **DB ๊ธฐ๋ฐ ์๋ฆผ ์ค์ ** โ `notify_channels` ํ
์ด๋ธ์ ์ ์ฅ, Pod ์ฌ์์ ํ์๋ ์ค์ ์ ์ง
+- **ํ๊ฒฝ๋ณ์ fallback** โ DB ์ฑ๋ ๋ฏธ๋ฑ๋ก ์ K8s Secret ํ๊ฒฝ๋ณ์๋ก ์๋ ๋์ฒด
+- **notifier.py ์ ๋ฉด ๊ฐ์ ** โ ๋งค ๋ฐ์ก ์ DB์์ ํ์ฑ ์ฑ๋ ๋ชฉ๋ก ์ง์ ์กฐํ ํ ๋ฐ์ก
+
+#### ์ฑ๋ ์ ํ๋ณ ๋ฐ์ก ๊ท์น
+
+| ์ฑ๋ ์ ํ | notify_both (Pod ์ด์/๋ณต๊ตฌ) | notify_discord_only (๊ณ์ ์ ๊ธ/์์PW) | notify_email_only (์ธ์ฆ์๋ง๋ฃ) |
+|---------|--------------------------|--------------------------------------|-------------------------------|
+| Discord+Gmail | Discord + Gmail | Discord๋ง | Gmail๋ง |
+| Discord ์ ์ฉ | Discord๋ง | Discord๋ง | ๋ฐ์ก ์ ํจ |
+| Gmail ์ ์ฉ | Gmail๋ง | ๋ฐ์ก ์ ํจ | Gmail๋ง |
+
+#### ์๋ฆผ ์ฑ๋ ๋ฑ๋ก ๋ฐฉ๋ฒ (์น UI)
+1. ๊ด๋ฆฌ์ ๋ก๊ทธ์ธ โ **๐ ์๋ฆผ ์ค์ ** ํญ
+2. ์ข์ธก ํผ์์ ์ฑ๋ ์ด๋ฆ, ์ ํ, Webhook URL / Gmail ์ ๋ณด ์
๋ ฅ
+3. **๐พ ์ ์ฅ** ํด๋ฆญ โ ์ฐ์ธก ๋ชฉ๋ก์ ์ถ๊ฐ๋จ
+4. ๋ชฉ๋ก์์ โ๏ธ ํด๋ฆญ โ ํผ์ ๊ฐ ์๋ ์
๋ ฅ ํ ์์ /์ ์ฅ
+5. ๐๏ธ ํด๋ฆญ โ ์ฑ๋ ์ญ์
+6. **๐จ ํ
์คํธ ๋ฐ์ก** ๋ฒํผ์ผ๋ก ๋ฑ๋ก๋ ๋ชจ๋ ์ฑ๋์ ํ
์คํธ ์๋ฆผ ๋ฐ์ก
+
+#### DB ํ
์ด๋ธ ์๋ ์์ฑ (์ ๊ท ๋ฐฐํฌ ์ ํ์ ์)
+```bash
+kubectl exec -n web-portal deployment/backend -- python3 -c "
+import psycopg2
+conn = psycopg2.connect(host='postgres-service', database='portaldb', user='portaluser', password='portalpass')
+cur = conn.cursor()
+cur.execute('''CREATE TABLE IF NOT EXISTS notify_channels (
+ id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, type VARCHAR(20) NOT NULL,
+ discord_webhook_url TEXT DEFAULT chr(0), gmail_user VARCHAR(200) DEFAULT chr(0),
+ gmail_app_password TEXT DEFAULT chr(0), alert_email_to VARCHAR(200) DEFAULT chr(0),
+ enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW()
+)''')
+conn.commit()
+print('์๋ฃ')
+"
+```
+
+> โน๏ธ ์ `main.py` ๋ฐฐํฌ ํ์๋ startup ์ ์๋์ผ๋ก ํ
์ด๋ธ์ด ์์ฑ๋๋ฏ๋ก ์๋ ์์ฑ ๋ถํ์.
+
+#### ํ๋ก์ ํธ ๊ตฌ์กฐ ๋ณ๊ฒฝ
+```
+backend/
+โโโ main.py โ ์๋ฆผ ์ฑ๋ CRUD API ์ถ๊ฐ, notify_channels ํ
์ด๋ธ ์๋ ์์ฑ
+โโโ notifier.py โ DB ๊ธฐ๋ฐ ๋ค์ค ์ฑ๋ ๋ฐ์ก์ผ๋ก ์ ๋ฉด ์ฌ์์ฑ
+โโโ monitor.py โ ๋ณ๊ฒฝ ์์ (notifier ์ธํฐํ์ด์ค ๋์ผ)
+
+frontend/
+โโโ index.html โ ๐ ์๋ฆผ ์ค์ ํญ ์ถ๊ฐ (์ข์ธก ํผ + ์ฐ์ธก ๋ฆฌ์คํธ UI)
+```
+
+---
+
+### 2026-05-19 (ํ๋ธ ํํ์ด์ง ๊ตฌ์ถ + URL ๊ตฌ์กฐ ๊ฐํธ)
+
+#### URL ๊ตฌ์กฐ ๋ณ๊ฒฝ
+
+| ๋ณ๊ฒฝ ์ | ๋ณ๊ฒฝ ํ | ๋ด์ฉ |
+|---------|---------|------|
+| `cyanburu.com/` | `cyanburu.com/portal` | ๊ธฐ์กด ์น ํฌํธ ๊ฒฝ๋ก ๋ณ๊ฒฝ |
+| โ | `cyanburu.com/` | ์ ๊ท ํ๋ธ ํํ์ด์ง (๋ฃจํธ) |
+| โ | `cyanburu.com/kingscup` | ํน์ปต ๊ฒ์ (๊ฐ๋ฐ ์์ ) |
+
+#### ๊ธฐ๋ฅ ์ถ๊ฐ
+
+**ํ๋ธ ํํ์ด์ง (`cyanburu.com/`)**
+- ํฌํธํด๋ฆฌ์ค ๊ฒธ ์๋น์ค ํ๋ธ ํ์ด์ง ์ ๊ท ๊ตฌ์ถ
+- ์๋ํ ๋ฆฌ์ผ ๋งค๊ฑฐ์ง + ์ฌ์ด๋ฒํํฌ ๋ฏน์ค ๋์์ธ
+ - `Cormorant Garamond` ์ธ๋ฆฌํ ํฐํธ + ํฌ๋ฆผ ๋ฐฐ๊ฒฝ (์๋ํ ๋ฆฌ์ผ)
+ - ๋ฐฐ๊ฒฝ ๊ฒฉ์ ๊ทธ๋ฆฌ๋, ๋ฏผํธ ๋ค์จ ํธ๋ฒ ํจ๊ณผ (์ฌ์ด๋ฒํํฌ)
+ - ํฐ๋ฏธ๋ ์คํ์ผ ํด๋ฌ์คํฐ ์ํ ํ์ + ์ค์๊ฐ ์
ํ์ ์นด์ดํฐ
+ - ์ปค์คํ
์ปค์ (๋ฏผํธ ์ + ๋ง)
+- DB ๊ธฐ๋ฐ ๋์ ์นด๋ ๋ก๋ฉ โ `/portal/api/homepage/cards` API ํธ์ถ
+- API ์คํจ ์ ์ ์ ์นด๋๋ก ์๋ ํด๋ฐฑ
+- `hub` ๋ค์์คํ์ด์ค์ ๋
๋ฆฝ ๋ฐฐํฌ (nginx ์ ์ ์๋น)
+
+**ํ ์นด๋ ๊ด๋ฆฌ (๊ด๋ฆฌ์)**
+- `cyanburu.com/portal` ๊ด๋ฆฌ์ ํญ์ `๐ ํ ์นด๋ ๊ด๋ฆฌ` ์ถ๊ฐ
+- ์นด๋ ์ถ๊ฐ / ์์ / ์ญ์ / ์์ ๋ณ๊ฒฝ / ๊ณต๊ฐ ์ฌ๋ถ ์ค์
+- ๋ณ๊ฒฝ ์ฌํญ์ด ํ๋ธ ํํ์ด์ง์ ์ฆ์ ๋ฐ์ (์ฌ๋ฐฐํฌ ๋ถํ์)
+- `homepage_cards` ํ
์ด๋ธ (PostgreSQL) ์ ๊ท ์ถ๊ฐ
+
+**web-portal ๊ฒฝ๋ก ๋ณ๊ฒฝ (`/` โ `/portal`)**
+- Nginx Ingress `rewrite-target` ์ผ๋ก `/portal` prefix strip ์ฒ๋ฆฌ
+- `frontend/nginx.conf` subpath ๋์ ์์
+- `frontend/index.html` API ๊ฒฝ๋ก `/api` โ `/portal/api` ์์
+- `backend/main.py` `root_path="/portal"` ์ถ๊ฐ
+
+#### ์ ๊ท k8s ํ์ผ
+
+| ํ์ผ | ๋ด์ฉ |
+|------|------|
+| `k8s/00-hub.yaml` | hub ๋ค์์คํ์ด์ค + Deployment + Service |
+| `k8s/13-ingress-hub.yaml` | Hub Ingress (`cyanburu.com/`) |
+
+#### DB ํ
์ด๋ธ ์ถ๊ฐ
+```sql
+CREATE TABLE IF NOT EXISTS homepage_cards (
+ id SERIAL PRIMARY KEY,
+ title VARCHAR(100) NOT NULL,
+ subtitle VARCHAR(200),
+ description TEXT,
+ url VARCHAR(500) NOT NULL,
+ tag VARCHAR(20) DEFAULT 'LIVE',
+ sort_order INTEGER DEFAULT 0,
+ visible BOOLEAN DEFAULT TRUE,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+```
+
+> โน๏ธ ์ ๊ท ๋ฐฐํฌ ์ `main.py` startup ์ด๋ฒคํธ์์ ์๋ ์์ฑ. ์คํจ ์ psql์์ ์ง์ ์คํ.
+
+#### ํ๋ก์ ํธ ๊ตฌ์กฐ ๋ณ๊ฒฝ
+```
+hub/ โ ์ ๊ท ์ถ๊ฐ
+โโโ index.html โ ํ๋ธ ํํ์ด์ง (๋์ ์นด๋ ๋ก๋ฉ)
+โโโ Dockerfile โ nginx:alpine ๊ธฐ๋ฐ ์ ์ ์๋น
+
+backend/
+โโโ main.py โ homepage_cards CRUD API ์ถ๊ฐ, root_path="/portal" ์ค์
+
+frontend/
+โโโ index.html โ API ๊ฒฝ๋ก /portal/api ๋ก ์์ , ๐ ํ ์นด๋ ๊ด๋ฆฌ ํญ ์ถ๊ฐ
+โโโ nginx.conf โ /portal subpath ๋์ ์์
+
+k8s/
+โโโ 00-hub.yaml โ ์ ๊ท ์ถ๊ฐ
+โโโ 08-ingress.yaml โ /portal ๊ฒฝ๋ก๋ก ๋ณ๊ฒฝ
+โโโ 13-ingress-hub.yaml โ ์ ๊ท ์ถ๊ฐ (cyanburu.com/ โ hub)
+```
+
+#### ํธ๋ฌ๋ธ์ํ
(5์)
+
+**hub namespace not found**
+- Registry Secret ์์ฑ ์ ๋ค์์คํ์ด์ค๊ฐ ์์ด์ ๋ฐ์
+- ํด๊ฒฐ: `kubectl create namespace hub` ๋จผ์ ์คํ ํ Secret ์์ฑ
+
+**Ingress host+path ์ถฉ๋ (BadRequest)**
+- ๊ธฐ์กด web-portal-ingress๊ฐ `cyanburu.com /` ๋ฅผ ์ ์ ํ๊ณ ์์ด์ hub-ingress ์์ฑ ๋ถ๊ฐ
+- ํด๊ฒฐ: `08-ingress.yaml`์ `/portal` ๊ฒฝ๋ก๋ก ๋จผ์ apply ํ `13-ingress-hub.yaml` apply
+
+**ํ ์นด๋ ๊ด๋ฆฌ ํญ ๋ด์ฉ ๋ฏธํ์**
+- `page-admin-hub-cards` div๊ฐ `` ๋ฐ์ ์์นํด์ JS๊ฐ null ๋ฐํ
+- ํด๊ฒฐ: sed ๋ช
๋ น์ผ๋ก div๋ฅผ `` ์์ผ๋ก ์ด๋
+
+**hub ํํ์ด์ง ์นด๋ ๋ฏธ๋ฐ์**
+- `hub/index.html`์ ๋์ ๋ก๋ฉ ์ฝ๋๊ฐ ์๋ ๊ตฌ๋ฒ์ ํ์ผ์ด ๋ฐฐํฌ๋จ
+- ํด๊ฒฐ: ๋์ ์นด๋ ๋ก๋ฉ ๋ฒ์ ์ผ๋ก `hub/index.html` ๊ต์ฒด ํ ์ฌ๋น๋