From d889a8516f5446b3f615264c483f3ecd4a1adfac Mon Sep 17 00:00:00 2001 From: Hyemin Lee Date: Mon, 29 Jun 2026 17:11:32 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EA=B8=B0=ED=9A=8D=EC=84=9C(PRD)?= =?UTF-8?q?=EC=99=80=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EC=A7=80=EC=B9=A8(CLAUDE.md)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 61 ++++++++++++ ai-dev-portal-prd.md | 229 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 CLAUDE.md create mode 100644 ai-dev-portal-prd.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7776340 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# CLAUDE.md + +AI DEV — 사내 개발 포털. 직원이 한 곳에서 **사내 개발 사이트로 이동**하고, **만든 프로젝트를 +공유·코멘트**하는 곳. Keycloak SSO(사번 로그인) + Node(Express+EJS) + Postgres. + +## 이 저장소를 다룰 때 (먼저 읽기) +- **사용자는 개발 전문가가 아닌 사내 IT 기획자/직원**이다. 변경은 **단순하게** 유지한다. + - 빌드 단계·프런트엔드 프레임워크(React 등)·번들러 도입 **금지**. 서버 렌더링(EJS) + 단일 CSS 유지. + - 새 npm 의존성은 꼭 필요할 때만, 이유와 함께 추가한다. +- **UI 문구와 코드 주석은 한국어**로 쓴다. +- **비밀값은 코드에 넣지 않는다.** 환경변수(`.project-env` 로컬 / Kubero Env 배포)로만 다룬다. +- 할 일·작업 단위는 [`PLANS.md`](./PLANS.md), 디자인 방향은 [`design-refs/`](./design-refs/) 참고. + +## 스택 +- **Node 22 ESM** (`"type": "module"`) — import 경로에 `.js` 확장자 **필수**. +- **Express 4 + EJS** — 서버 사이드 렌더링, 빌드 없음. +- **Postgres** (`pg`) — OpenEverest `appdb`, schema `portal`. +- **Keycloak OIDC** (`openid-client` v5) — Authorization Code. +- 정적 CSS 한 장: `public/style.css`. 로고: `public/logo.svg`. + +## 명령 +- `npm run dev` — `node --watch`, http://localhost:3000 +- `npm start` — 프로덕션 (`node index.js`) +- `npm run db:init` — `schema.sql` 적용 (멱등, 최초 1회) +- 로컬 환경변수 로드: `cp .project-env.example .project-env` 로 값을 채운 뒤 + `set -a; . ./.project-env; set +a` + +## 구조 맵 +- `index.js` → `src/server.js` 진입 (Kubero buildpack run = `node index.js`) +- `src/server.js` — Express 앱, 라우트, 부팅 +- `src/config.js` — 환경변수 설정 +- `src/auth.js` — Keycloak OIDC (로그인 리다이렉트 / 콜백 / `requireAuth` 가드) +- `src/db.js` — Postgres 풀 + 쿼리 함수 +- `src/sites.js` — 대시보드 "사이트 바로가기" 카탈로그 +- `views/` — `_header`, `dashboard`, `login`, `project` (EJS) +- `public/` — `style.css`, `logo.svg` +- `schema.sql` — DB 스키마(멱등), `scripts/init-db.js` 가 적용 +- `deploy/portal-route.yaml` — Gateway HTTPRoute (portal.bokdev.in) +- `.claude/` — Claude Code 훅·설정 + +## 인증 모델 +- **사번 = Keycloak `preferred_username`.** 세션에 `req.session.user = { sabun, name, email }`. +- 미인증으로 보호 페이지 진입 시 중간 화면 없이 곧장 Keycloak 로그인으로 리다이렉트 + (`requireAuth` → `loginRedirect`). OIDC discovery 실패 시에만 `login.ejs` 폴백. +- ⚠️ **알려진 문제**: `/logout` 은 로컬 세션만 파기 → Keycloak SSO 세션이 남아 재진입 시 자동 + 재로그인된다(= 로그아웃 안 됨). 세션이 인메모리라 재시작/다중 replica 시 로그인이 풀린다. + 자세한 내용과 해결 작업은 [`PLANS.md`](./PLANS.md) 참고. + +## 배포 +- **Kubero NodeJS buildpack** (`npm install` → `node index.js`). 평소 Dockerfile 불필요. +- `Dockerfile` 은 Kubero buildpack 의 harbor push 실패를 우회하는 **수동 push 용**. +- 도메인 `portal.bokdev.in` (`deploy/portal-route.yaml`). 도메인 추가 시 Keycloak `portal` + client redirect 에 `https://<도메인>/auth/callback` 등록 필요. +- Kubero Env: `BASE_URL`, `DATABASE_URL`, `OIDC_*`, `SESSION_SECRET`, `PORTAL_BRAND`. + +## 컨벤션 +- 라우트는 `src/server.js` 에, DB 접근은 `src/db.js` 의 함수로만 (인라인 SQL 산재 금지). +- 새 화면을 추가할 때는 `views/_header` 를 include 하고 기존 `.box` / `.btn` / `.tag` 등 + CSS 클래스를 재사용한다. 새 스타일은 `public/style.css` 한 곳에. +- DB 스키마 변경은 `schema.sql` 에 멱등(`IF NOT EXISTS`)으로 추가하고 `npm run db:init` 재실행. +- 브랜드명은 하드코딩하지 말고 `PORTAL_BRAND`(= `res.locals.brand`)를 사용한다. diff --git a/ai-dev-portal-prd.md b/ai-dev-portal-prd.md new file mode 100644 index 0000000..ac1a08e --- /dev/null +++ b/ai-dev-portal-prd.md @@ -0,0 +1,229 @@ +# AI DEV 포털 — 기획서 (PRD) + +> 사내 개발의 시작점. 한 곳에서 **개발 도구로 이동**하고, **만든 프로젝트를 공유·소통**하는 경량 포털. +> Backstage를 걷어내고 직접 만든 가벼운 사이트로 노선 변경. +> Stack: Node(Express + EJS) + Postgres + Keycloak SSO. Kubero로 배포. + +--- + +## 1. 한 줄 정의 + +직원이 사번 계정 **한 번의 로그인(SSO)** 으로 사내 개발 도구(Coder/Gitea/Kubero 등)에 이동하고, +자기가 만든 프로젝트를 동료에게 **공유 → 좋아요 → 댓글**로 소통하는 내부 개발 포털. + +--- + +## 2. 왜 Backstage를 버리는가 + +| 항목 | Backstage | 직접 만든 경량 포털 | +|---|---|---| +| 무게 | 무겁고 과함 (플러그인/카탈로그/엔티티 모델) | 필요한 화면 2~3개 | +| 학습/운영 비용 | 높음 | 낮음 (Express+EJS) | +| 커스터마이징 | 프레임워크에 종속 | 100% 자유 | +| 우리가 실제 쓰는 기능 | 링크 모음 + 공유 피드 | 정확히 그 두 가지 | + +→ **우리가 원하는 건 "예쁜 링크 허브 + 가벼운 소셜 피드"**. Backstage는 오버킬. + +--- + +## 3. 화면 구조 (3 페이지) + +### 3.1 로그인 (`/login`) +- 미인증 진입 시 **중간 화면 없이 곧장 Keycloak 로그인 폼**으로 리다이렉트 (이미 구현됨). +- OIDC discovery 실패 시에만 안내 페이지 폴백. + +### 3.2 대시보드 (`/`) — 메인 +로그인 후 보게 되는 단일 페이지. 위→아래로 **3개 영역**. + +``` +┌─────────────────────────────────────────────┐ +│ [헤더] AI DEV ····· 사번님 ▾ (로그아웃) │ +├─────────────────────────────────────────────┤ +│ ① 개발 도구 (Tools) │ +│ - 모두에게: Coder / Gitea / Kubero / Harbor│ +│ - 관리자만: OpenEverest / MinIO / │ +│ Keycloak / LiteLLM │ +│ 클릭 시 SSO 자동로그인 URL로 새 탭 │ +├─────────────────────────────────────────────┤ +│ ② 내 프로젝트 (My Projects) │ +│ - 내가 공유한 것만. 공유 피드와 시각적으로 │ +│ 확연히 구분 (배경/테두리/섹션 헤더 다름) │ +│ - [+ 새 프로젝트 공유] 버튼 │ +├─────────────────────────────────────────────┤ +│ ③ 공유 프로젝트 피드 (Shared Projects) │ +│ - 나 + 남이 공유한 전체 (최신순/인기순) │ +│ - 카드: 제목·설명·태그·작성자·❤️ 수·💬 수 │ +│ - 카드 클릭 → 프로젝트 상세 (3.3) │ +│ - 카드의 "바로가기" → 배포 URL 새 탭 │ +└─────────────────────────────────────────────┘ +``` + +**핵심 UX 결정: ②와 ③의 구분** +- ②는 "내 작업 공간"의 느낌 — 좌측 액센트 바, 옅은 배경 톤, "내 프로젝트" 섹션 라벨. +- ③은 "공개 피드"의 느낌 — 카드 그리드, 작성자 아바타 노출, 좋아요/댓글 카운트 전면. +- 같은 카드 컴포넌트를 쓰되 **컨테이너 스타일과 배지로 분리**. (관리자가 아니어도 둘은 항상 분리.) + +### 3.3 프로젝트 상세 (`/projects/:id`) +- 제목 / 설명 / 태그 / 작성자 / 작성일 +- **바로가기 버튼** (배포 URL — 새 탭) +- **좋아요** 토글 (❤️ + 카운트) +- **댓글** + **대댓글(1뎁스까지)** + - 댓글: 작성자, 시각, 본문, [답글] 버튼 + - 대댓글: 부모 댓글 아래 들여쓰기. **대댓글에는 [답글] 버튼 없음** (1뎁스 고정) + - 본인 댓글은 삭제 가능 + +--- + +## 4. 권한 모델 (Role) + +Keycloak에서 내려주는 role(또는 group)로 분기. `preferred_username` = 사번. + +| 역할 | 볼 수 있는 도구 | 비고 | +|---|---|---| +| **일반 사용자** | Coder, Gitea, Kubero, Harbor | 개발 핵심 | +| **관리자(admin)** | 위 + OpenEverest, MinIO, Keycloak, LiteLLM | 인프라/운영 도구까지 | + +- 구현: 세션의 `roles`에 `admin` 포함 여부로 `sites.js`의 각 항목 `adminOnly: true` 필터링. +- 일반 사용자에게 관리자 전용 카드는 **렌더링 자체를 안 함** (숨김, disabled 아님). +- 프로젝트 공유/좋아요/댓글은 역할 무관 모두 가능. + +```js +// sites.js 형태 (예시) +export const sites = [ + { key:'coder', name:'Coder', desc:'클라우드 IDE', url: SSO_START.coder, icon:'coder', group:'dev' }, + { key:'gitea', name:'Gitea', desc:'Git 저장소', url: SSO_START.gitea, icon:'gitea', group:'dev' }, + { key:'kubero', name:'Kubero', desc:'배포(PaaS)', url: KUBERO_URL, icon:'kubero', group:'dev' }, + { key:'harbor', name:'Harbor', desc:'컨테이너 레지스트리', url: HARBOR_URL, icon:'harbor', group:'dev' }, + { key:'openeverest', name:'OpenEverest', desc:'DB 콘솔', url: OE_URL, icon:'db', group:'infra', adminOnly:true }, + { key:'minio', name:'MinIO', desc:'오브젝트 스토리지', url: MINIO_URL, icon:'minio', group:'infra', adminOnly:true }, + { key:'keycloak', name:'Keycloak', desc:'계정/SSO 관리', url: KC_URL, icon:'key', group:'infra', adminOnly:true }, + { key:'litellm', name:'LiteLLM', desc:'LLM 게이트웨이', url: LITELLM_URL, icon:'ai', group:'infra', adminOnly:true }, +]; +``` + +--- + +## 5. 배포 URL 연동 — Kubero에서 어떻게 받아오나 + +> 네 질문: "공유된 프로젝트 링크를 새 탭으로 열려면 Kubero에서 URL을 받아와야 하나?" + +**결론: Kubero API를 실시간으로 조회하지 말고, 공유 시점에 URL을 직접 저장한다.** + +### 왜 실시간 조회가 아닌가 +- Kubero는 빌드 후 **설정된 도메인(configured domain)** 으로 앱을 노출함. 앱마다 도메인은 이미 정해져 있음 (`.apps.bokdev.in` 같은 패턴). +- Kubero v3에 API가 있지만, 포털이 매 렌더링마다 Kubero API를 때리면: ① 토큰/권한 관리 부담 ② Kubero 다운 시 포털 피드도 깨짐 ③ 폐쇄망에서 불필요한 결합도 증가. +- 공유 프로젝트의 URL은 **거의 안 바뀜**. 실시간성이 필요 없는 데이터. + +### 권장 방식 (단순) +프로젝트 공유 폼에서 사용자가 **URL을 직접 입력**(또는 선택): +- `repo_url` — Gitea 저장소 주소 +- `app_url` — 배포된 앱 주소 (예: `https://myapp.apps.bokdev.in`) — **이게 "바로가기"가 여는 링크** +- 둘 다 선택 입력. `app_url`이 없으면 "바로가기" 버튼은 숨김. + +### (선택) 편의 기능 — 나중에 +공유 폼에서 "내 Kubero 앱 목록 불러오기" 버튼 → Kubero API로 사용자의 파이프라인/앱과 그 도메인을 가져와 드롭다운 제공. URL 오타 방지용. **MVP에는 불필요**, 직접 입력으로 충분. + +--- + +## 6. SSO 자동로그인 (도구 카드 클릭 시) + +이미 적용된 패턴 유지: +- Coder/Gitea처럼 OIDC를 지원하는 도구는 **OIDC 시작 URL**(자동로그인 진입점)로 링크 → 사용자가 도구에서 다시 로그인 안 함. +- 카드에 "SSO" 배지로 자동로그인 가능함을 표시. +- 모든 도구 링크는 `target="_blank"` + `rel="noopener"` 로 새 탭. + +--- + +## 7. 데이터 모델 (Postgres, schema `portal`) + +```sql +-- 사용자: Keycloak가 진실의 원천. 포털은 표시에 필요한 최소만 캐시. +users ( + id text primary key, -- preferred_username (사번) + display_name text, + is_admin boolean default false, + created_at timestamptz default now() +) + +projects ( + id bigserial primary key, + owner_id text not null references users(id), + title text not null, + description text, + repo_url text, -- Gitea 등 + app_url text, -- 배포 URL (바로가기 대상) + tags text[] default '{}', + created_at timestamptz default now(), + updated_at timestamptz default now() +) + +likes ( + project_id bigint references projects(id) on delete cascade, + user_id text references users(id), + created_at timestamptz default now(), + primary key (project_id, user_id) -- 1인 1좋아요 +) + +comments ( + id bigserial primary key, + project_id bigint not null references projects(id) on delete cascade, + user_id text not null references users(id), + parent_id bigint references comments(id) on delete cascade, -- null이면 최상위 + body text not null, + created_at timestamptz default now() +) +-- 대댓글 1뎁스 제약: parent_id가 가리키는 댓글의 parent_id는 반드시 null +-- (앱 레벨에서 검증. 대댓글에 또 답글 달기 차단) +``` + +조회 쿼리 포인트: +- 피드 카드의 ❤️ 수 / 💬 수는 `count` 서브쿼리 또는 집계 뷰로. +- "내 좋아요 여부"는 `likes`에 `(project_id, 현재 사용자)` 존재 여부. + +--- + +## 8. 라우트 (API) + +| Method | Path | 설명 | 권한 | +|---|---|---|---| +| GET | `/` | 대시보드 (도구 + 내 프로젝트 + 피드) | 로그인 | +| GET | `/projects/:id` | 프로젝트 상세 | 로그인 | +| POST | `/projects` | 프로젝트 공유 생성 | 로그인 | +| POST | `/projects/:id/like` | 좋아요 토글 | 로그인 | +| DELETE | `/projects/:id` | 내 프로젝트 삭제 | 본인 | +| POST | `/projects/:id/comments` | 댓글/대댓글 작성 (`parent_id` 옵션) | 로그인 | +| DELETE | `/comments/:id` | 내 댓글 삭제 | 본인 | +| GET | `/auth/callback` | OIDC 콜백 | - | +| GET | `/logout` | 로그아웃 | - | + +--- + +## 9. 비기능 / 운영 + +- **배포**: Kubero, NodeJS buildpack (`npm install` → `node index.js`). Dockerfile 불필요. +- **Env**: `BASE_URL`, `DATABASE_URL`, `OIDC_*`, `SESSION_SECRET`, `PORTAL_BRAND`, 각 도구 URL. +- **DB**: OpenEverest Postgres의 `appdb`, schema `portal`, role `portal_app`. +- **Issuer**: `https://keycloak.bokdev.in/realms/bokdev`. +- **브랜드명**: `PORTAL_BRAND` 환경변수로 교체 가능 (`AI DEV` 기본). +- 폐쇄망: 외부 폰트/CDN 의존 최소화 (Pretendard는 로컬 호스팅 권장). + +--- + +## 10. MVP 범위 / 나중 + +**MVP (지금)** +- 3페이지, 역할 분기(관리자 도구 숨김), 프로젝트 공유, 좋아요, 댓글+대댓글(1뎁스), SSO 링크, 바로가기(직접 입력 URL). + +**나중에 (선택)** +- Kubero API로 앱 목록 자동 불러오기 (URL 입력 편의) +- 프로젝트 검색/태그 필터, 정렬(인기순) +- 댓글 알림, 멘션 +- 프로젝트 썸네일/스크린샷 + +--- + +## 11. 열린 질문 + +1. 관리자/일반 구분을 Keycloak **role**로 줄지 **group**으로 줄지 — 운영 편한 쪽으로. +2. "내 프로젝트"에서 **수정(edit)** 도 MVP에 넣을지? (삭제만으로 시작해도 됨) +3. 좋아요/댓글에 **익명성**은 없음(사번 기반) — 사내 문화상 OK인지 확인.