diff --git a/CLAUDE.md b/CLAUDE.md index 7776340..5abc428 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,8 @@ AI DEV — 사내 개발 포털. 직원이 한 곳에서 **사내 개발 사이 - **Express 4 + EJS** — 서버 사이드 렌더링, 빌드 없음. - **Postgres** (`pg`) — OpenEverest `appdb`, schema `portal`. - **Keycloak OIDC** (`openid-client` v5) — Authorization Code. -- 정적 CSS 한 장: `public/style.css`. 로고: `public/logo.svg`. +- 정적 CSS 한 장: `public/style.css`(라이트/다크 CSS 변수). 가벼운 바닐라 JS: `public/app.js`. 로고: `public/logo.svg`. +- 프레임워크/번들러 없이 **점진적 향상**: 폼 POST 기본 동작 + JS 있으면 fetch 로 즉시 반영. ## 명령 - `npm run dev` — `node --watch`, http://localhost:3000 @@ -30,16 +31,20 @@ AI DEV — 사내 개발 포털. 직원이 한 곳에서 **사내 개발 사이 - `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` +- `src/db.js` — Postgres 풀 + 쿼리 함수 (`hearts`❤️/`stars`⭐/`comments`/`projects`) +- `src/sites.js` — 도구 카탈로그(개발/인프라) + `toolsForUser(isAdmin)` role 필터 +- `src/helpers.js` — 뷰 헬퍼: `icon`(Octicon SVG)·`identicon`(아바타 SVG)·`langColor`·`langFromTags` (res.locals 노출) +- `views/` — `_header`, `_project_detail`(모달/단독 공용 상세), `dashboard`, `login`, `project` (EJS) +- `public/` — `style.css`(CSS 변수 라이트/다크 단일 파일), `app.js`(테마·모달·검색/정렬·반응 fetch·이스터에그), `logo.svg` - `schema.sql` — DB 스키마(멱등), `scripts/init-db.js` 가 적용 - `deploy/portal-route.yaml` — Gateway HTTPRoute (portal.bokdev.in) +- `design-refs/` — 디자인 레퍼런스(.dc.html). 화면 변경 시 시각 기준. - `.claude/` — Claude Code 훅·설정 ## 인증 모델 -- **사번 = Keycloak `preferred_username`.** 세션에 `req.session.user = { sabun, name, email }`. +- **사번 = Keycloak `preferred_username`.** 세션에 `req.session.user = { sabun, name, email, roles, isAdmin }`. +- **관리자 = `realm_access.roles` 에 `ADMIN_ROLE`(기본 `admin`) 포함.** 인프라 도구(OpenEverest/MinIO/ + Keycloak/LiteLLM)는 관리자에게만 렌더(`sites.js` 의 `adminOnly` + `toolsForUser`). - 미인증으로 보호 페이지 진입 시 중간 화면 없이 곧장 Keycloak 로그인으로 리다이렉트 (`requireAuth` → `loginRedirect`). OIDC discovery 실패 시에만 `login.ejs` 폴백. - ⚠️ **알려진 문제**: `/logout` 은 로컬 세션만 파기 → Keycloak SSO 세션이 남아 재진입 시 자동 @@ -51,7 +56,7 @@ AI DEV — 사내 개발 포털. 직원이 한 곳에서 **사내 개발 사이 - `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`. +- Kubero Env: `BASE_URL`, `DATABASE_URL`, `OIDC_*`, `SESSION_SECRET`, `PORTAL_BRAND`, `ADMIN_ROLE`, (선택) 도구 `*_URL`. ## 컨벤션 - 라우트는 `src/server.js` 에, DB 접근은 `src/db.js` 의 함수로만 (인라인 SQL 산재 금지). diff --git a/PLANS.md b/PLANS.md index 5b2905c..18eaf38 100644 --- a/PLANS.md +++ b/PLANS.md @@ -74,12 +74,14 @@ - [x] `views/project.ejs`(단독) · `views/login.ejs` 새 디자인으로 재작성. - [x] 세 뷰 모의 렌더 검증 통과(저장소 3개·공유모달·body 신호·백링크·알림). -## Phase 6 — 문서 · 마무리 · 점검 -- [ ] `ai-dev-portal-prd.md` 보강: 반응 2종(heart/star), 공개/비공개, lang, 댓글 parent_id, 추가 라우트, 다크모드/검색/토스트 등 디자인 확정 사항 반영. -- [ ] `CLAUDE.md` 구조 맵 갱신(`public/app.js`, `_icons.ejs`, hearts 테이블 등) 필요 시. -- [ ] Pretendard 폰트: 시스템 폰트 폴백 확정. (선택) `public/` 에 woff2 로컬 호스팅 태스크 별도. -- [ ] 로컬 구동 점검: `npm run dev` → 로그인/도구/내프로젝트/피드/모달/공유/반응/댓글/대댓글/삭제/테마/검색 수동 확인. -- [ ] 미인증·관리자/일반 분기·비공개 노출 규칙 점검. +## Phase 6 — 문서 · 마무리 · 점검 ✅ (실DB 점검만 사용자 몫) +- [x] `ai-dev-portal-prd.md` 보강: 반응 2종(heart/star), 공개/비공개, lang, 댓글 parent_id, 추가 라우트(heart/star/visibility/delete/comments-delete), 다크모드/검색/토스트/이스터에그, role 확정. +- [x] `CLAUDE.md` 구조 맵 갱신(`public/app.js`, `src/helpers.js`, `_project_detail.ejs`, hearts 테이블, role 인증 모델, design-refs). +- [x] Pretendard 폰트: 시스템 폰트 폴백 확정(CSS font-family + PRD 명시). (선택) woff2 로컬 호스팅은 추후. +- [x] 부팅 스모크 테스트: 더미 OIDC 로 기동 → `/healthz` `{ok:true}` 응답 확인(OIDC 실패는 우아하게 폴백). +- [x] 권한 규칙 점검(코드): `requireAuth`(미인증→Keycloak), `toolsForUser`(관리자 도구 숨김), `listProjects`(남의 비공개 비노출), `getProject`(비공개 본인만). +- [ ] **실 DB 기능 점검(사용자 몫)**: `.project-env` 채우고 `npm run db:init` → `npm run dev` 로 + 로그인/도구/내프로젝트/피드/모달/공유/❤️·⭐/댓글·대댓글/삭제/테마/검색 수동 확인. --- diff --git a/ai-dev-portal-prd.md b/ai-dev-portal-prd.md index ac1a08e..2daf8fd 100644 --- a/ai-dev-portal-prd.md +++ b/ai-dev-portal-prd.md @@ -152,18 +152,28 @@ projects ( description text, repo_url text, -- Gitea 등 app_url text, -- 배포 URL (바로가기 대상) - tags text[] default '{}', + tags text, -- 콤마 구분(실제 구현은 text). 표시 시 split. + is_public boolean default true, -- 공개(피드 노출) / 비공개(나만) + lang text, -- 대표 언어(태그에서 추론, 카드 언어 점) created_at timestamptz default now(), updated_at timestamptz default now() ) -likes ( +-- 반응은 2종(디자인 확정): ❤️ 좋아요(hearts) + ⭐ 즐겨찾기/Star(stars). 각 1인 1회. +hearts ( 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좋아요 ) +stars ( + 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, @@ -177,8 +187,11 @@ comments ( ``` 조회 쿼리 포인트: -- 피드 카드의 ❤️ 수 / 💬 수는 `count` 서브쿼리 또는 집계 뷰로. -- "내 좋아요 여부"는 `likes`에 `(project_id, 현재 사용자)` 존재 여부. +- 피드 카드의 ❤️ 수 / ⭐ 수 / 💬 수는 `count` 서브쿼리로(`db.js` 의 `PROJECT_SELECT`). +- "내 좋아요/즐겨찾기 여부"는 `hearts`/`stars` 에 `(project_id, 현재 사용자)` 존재 여부(`EXISTS`). +- 목록은 **공개 프로젝트 + 본인 비공개**만 반환(남의 비공개 비노출). +- ⚠️ 실제 스키마는 사번(`sabun`)을 PK/FK 로 쓰고 `users.is_admin` 은 두지 않는다(관리자 여부는 + 매 로그인 시 Keycloak role 로 판별). 컬럼·타입의 단일 진실은 [`schema.sql`](./schema.sql). --- @@ -186,44 +199,57 @@ comments ( | 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 | `/` | 대시보드 (도구 + 내 프로젝트 + 피드). `?open=:id`/`?toast=`/`?egg=` 신호 | 로그인 | +| GET | `/projects/:id` | 프로젝트 상세(단독 페이지·모달 폴백). 비공개는 본인만 | 로그인 | +| POST | `/projects` | 프로젝트 공유 생성 (visibility·lang 포함) | 로그인 | +| POST | `/projects/:id/heart` | ❤️ 좋아요 토글 (AJAX 면 `{on,count}`) | 로그인 | +| POST | `/projects/:id/star` | ⭐ 즐겨찾기 토글 (AJAX 면 `{on,count}`) | 로그인 | +| POST | `/projects/:id/visibility` | 공개/비공개 전환 | 본인 | +| POST | `/projects/:id/delete` | 내 프로젝트 삭제 (form POST) | 본인 | +| POST | `/projects/:id/comments` | 댓글/대댓글 작성 (`parent_id` 옵션, 1뎁스 검증) | 로그인 | +| POST | `/comments/:id/delete` | 내 댓글 삭제 (form POST) | 본인 | | GET | `/auth/callback` | OIDC 콜백 | - | | GET | `/logout` | 로그아웃 | - | +> 반응/댓글은 **점진적 향상**: JS 없으면 폼 POST→리다이렉트, JS 있으면 fetch 로 즉시 반영. +> 삭제는 메서드 제약(HTML 폼) 때문에 `POST .../delete` 로 처리(논리적으로는 DELETE). + --- ## 9. 비기능 / 운영 - **배포**: Kubero, NodeJS buildpack (`npm install` → `node index.js`). Dockerfile 불필요. -- **Env**: `BASE_URL`, `DATABASE_URL`, `OIDC_*`, `SESSION_SECRET`, `PORTAL_BRAND`, 각 도구 URL. +- **Env**: `BASE_URL`, `DATABASE_URL`, `OIDC_*`, `SESSION_SECRET`, `PORTAL_BRAND`, `ADMIN_ROLE`, 각 도구 URL. - **DB**: OpenEverest Postgres의 `appdb`, schema `portal`, role `portal_app`. - **Issuer**: `https://keycloak.bokdev.in/realms/bokdev`. - **브랜드명**: `PORTAL_BRAND` 환경변수로 교체 가능 (`AI DEV` 기본). -- 폐쇄망: 외부 폰트/CDN 의존 최소화 (Pretendard는 로컬 호스팅 권장). +- 폐쇄망: 외부 폰트/CDN 의존 최소화. 폰트는 **시스템 폰트 폴백**(Pretendard → Apple SD Gothic/Malgun + Gothic)으로 동작. CDN 불가 환경에선 `public/` 에 Pretendard woff2 로컬 호스팅 권장(선택). + 아이콘은 인라인 SVG(Octicon)·이모지로 외부 의존 없음. +- **클라이언트 부가기능(프런트 전용, 서버 무관)**: 라이트/다크 테마 토글(localStorage), 헤더 검색, + 토스트 알림, 이스터에그(로고 흔들기·👍🚀✨ 날리기·LGTM 스탬프·콘솔 아트). --- ## 10. MVP 범위 / 나중 -**MVP (지금)** -- 3페이지, 역할 분기(관리자 도구 숨김), 프로젝트 공유, 좋아요, 댓글+대댓글(1뎁스), SSO 링크, 바로가기(직접 입력 URL). +**MVP (지금 — 구현 완료)** +- 3화면(대시보드+상세 모달/단독 페이지), 역할 분기(관리자 도구 숨김), 프로젝트 공유, + 공개/비공개, ❤️ 좋아요 + ⭐ 즐겨찾기, 댓글+대댓글(1뎁스), SSO 링크, 바로가기(직접 입력 URL), + 검색·정렬(최신/인기)·즐겨찾기 필터(클라이언트), 라이트/다크 테마. **나중에 (선택)** - Kubero API로 앱 목록 자동 불러오기 (URL 입력 편의) -- 프로젝트 검색/태그 필터, 정렬(인기순) +- 서버측 검색/태그 필터·정렬(현재는 클라이언트 처리, 새로고침 시 초기화) - 댓글 알림, 멘션 - 프로젝트 썸네일/스크린샷 +- 내 프로젝트 **수정(edit)** (현재는 삭제·가시성 전환만) --- -## 11. 열린 질문 +## 11. 열린 질문 / 결정 -1. 관리자/일반 구분을 Keycloak **role**로 줄지 **group**으로 줄지 — 운영 편한 쪽으로. -2. "내 프로젝트"에서 **수정(edit)** 도 MVP에 넣을지? (삭제만으로 시작해도 됨) -3. 좋아요/댓글에 **익명성**은 없음(사번 기반) — 사내 문화상 OK인지 확인. +1. ~~관리자/일반 구분을 role 로 줄지 group 으로 줄지~~ → **확정: Keycloak realm role** + (`realm_access.roles` 에 `ADMIN_ROLE`(기본 `admin`) 포함 여부). 운영 중 group 으로 바꾸려면 `auth.js` 수정. +2. "내 프로젝트" **수정(edit)** → MVP 제외(삭제·가시성 전환만). 추후 과제. +3. 좋아요/댓글에 **익명성**은 없음(사번 기반) — 사내 문화상 OK 가정.