문서: 앱 경계를 negosium 규약(프로젝트별 최상단, 안에서 f/b)으로 다시 쓴다
ARCHITECTURE.md 4절이 권고하던 `frontend/{console,admin,site,shared}` 는 사내 다른
레포(o2o-negosium)의 규약과 어긋났다. 이 레포만 다르게 갈 이유가 없어 최상단을
프로젝트 단위로 평평하게 두는 쪽으로 바꾼다 — solution/{backend,front,site} + admin/.
admin 에 백엔드를 두지 않는 근거를 실측으로 적었다: 내부 4장이 부르는 훅이 전부
router/v1/{place,fact,local,validator} 에 이미 있어 새로 만들 게 없고, 자체 백엔드를
두면 같은 DB 에 대고 도메인을 두 번 구현하게 된다. negosium 의 lps-admin 이
프론트 전용 최상단 폴더의 선례다.
shared/ 를 없애는 근거도 적었다 — 그것이 지키던 결합(슬러그 규칙, SitePayload)의
양쪽이 둘 다 solution/ 안에 들어가므로 공용 워크스페이스가 필요 없어진다.
"admin/ 이 두 앱을 반씩 겸한다"는 서술도 고쳤다. 실제로는 사장님 110파일 대
내부 814줄이라, 사실상 사장님 앱에 내부 화면 4장이 얹힌 모양이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
This commit is contained in:
parent
08a95e2bd7
commit
a186842b54
@ -102,63 +102,102 @@ frontend/
|
|||||||
|
|
||||||
**`admin/` 이 성격이 반대인 두 앱을 겸하고 있다.** ([PRODUCT.md 4절](PRODUCT.md) 표)
|
**`admin/` 이 성격이 반대인 두 앱을 겸하고 있다.** ([PRODUCT.md 4절](PRODUCT.md) 표)
|
||||||
|
|
||||||
|
크기는 이렇게 갈린다 (손으로 쓴 코드 152개 기준 — `api/generated` 296개는 orval 산출물이라 뺐다):
|
||||||
|
|
||||||
|
| | 규모 |
|
||||||
|
|---|---|
|
||||||
|
| 사장님 — `features/builder`(79) `onboarding`(21) `publish`(10) | **110 파일** |
|
||||||
|
| 내부 운영 — `pages/` 4장 | **814 줄** |
|
||||||
|
|
||||||
|
즉 `admin/` 은 두 앱을 반씩 겸하는 게 아니라, **사실상 사장님 앱인데 내부 화면 4장이 얹혀 있다.**
|
||||||
|
|
||||||
### 나눠야 하는 이유 — 취향이 아니라 셋 다 실제 문제다
|
### 나눠야 하는 이유 — 취향이 아니라 셋 다 실제 문제다
|
||||||
|
|
||||||
1. **내부 기능이 사장님 번들에 실려 나간다.** 한 앱이면 `/local-content`, `/places/:id/seo`
|
1. **내부 기능이 사장님 번들에 실려 나간다.** 한 앱이면 `/local-content`, `/places/:id/seo`
|
||||||
같은 내부 라우트 이름과 화면 코드가 사장님 브라우저에 그대로 내려간다.
|
같은 내부 라우트 이름과 화면 코드가 사장님 브라우저에 그대로 내려간다.
|
||||||
`UserRole.DEVELOPER` 는 코드 주석에 **"고객사에 존재를 노출하지 않는다"** 고 적혀 있는데,
|
`UserRole.DEVELOPER` 는 코드 주석에 **"고객사에 존재를 노출하지 않는다"** 고 적혀 있는데,
|
||||||
번들이 그 약속을 깨고 있다. 라우트 가드는 화면을 가리지 **번들은 못 가린다**.
|
번들이 그 약속을 깨고 있다. 라우트 가드는 화면을 가리지 **번들은 못 가린다**.
|
||||||
2. **인증 모델이 정반대다.** 빌더는 일부러 로그인을 안 세운다("만들어 보기도 전에 막힌다").
|
★ 이 문제는 **코드 크기와 무관하다.** 내부가 814줄뿐이어도 사장님 브라우저에 내려가는 건 같다.
|
||||||
운영 화면은 엄격해야 한다. 한 앱 안에서 두 정책을 유지하면 실수는 항상 **느슨한 쪽으로** 난다.
|
2. **인증 모델이 갈라진다.** 지금 빌더는 일부러 로그인을 안 세운다("만들어 보기도 전에 막힌다")
|
||||||
|
— `stores/auth.ts` 를 빌더 쪽 어느 파일도 import 하지 않는 것이 그 증거다.
|
||||||
|
그런데 사장님에게 **"내 사이트 관리"** 가 붙는 순간 그쪽도 로그인 뒤로 들어간다.
|
||||||
|
같은 로그인이 아니라 **role 이 다른 로그인**(OWNER vs DEVELOPER)이다.
|
||||||
|
한 앱에서 두 정책을 유지하면 실수는 항상 **느슨한 쪽으로** 난다.
|
||||||
3. **배포 리듬이 다르다.** 사장님 화면은 조심스럽게, 내부 화면은 매일 고쳐도 된다.
|
3. **배포 리듬이 다르다.** 사장님 화면은 조심스럽게, 내부 화면은 매일 고쳐도 된다.
|
||||||
한 번들이면 내부 화면 수정 때문에 사장님 화면을 재배포한다.
|
한 번들이면 내부 화면 수정 때문에 사장님 화면을 재배포한다.
|
||||||
|
|
||||||
### 권고 구조 — 레포는 하나, 배포물은 셋
|
### 권고 구조 — o2o-negosium 과 같은 규약
|
||||||
|
|
||||||
|
**2026-08-31 결정.** 최상단은 **프로젝트 단위**로 평평하게 두고, 프로젝트 안에서 backend/front 를
|
||||||
|
가른다. `frontend/` `backend/` 를 최상단 묶음 폴더로 쓰지 않는다 —
|
||||||
|
**사내 다른 레포(`o2o-negosium`)가 이미 그 규약이고, 이 레포만 다르게 갈 이유가 없다.**
|
||||||
|
|
||||||
```
|
```
|
||||||
o2o-web4ai/ ← 레포 하나 (모노레포). 쪼개지 않는다
|
o2o-web4ai/ ← 레포 하나. 쪼개지 않는다
|
||||||
├─ backend/ FastAPI 모듈러 모놀리스 + 워커
|
├─ solution/ 사장님 — 사이트 만들기·관리
|
||||||
├─ frontend/
|
│ ├─ backend/ FastAPI + 워커 · 이 레포의 유일한 백엔드
|
||||||
│ ├─ console/ (신설) 사장님용. 현 admin 의 /builder 계열
|
│ ├─ front/ builder · onboarding · publish
|
||||||
│ ├─ admin/ (남김) 내부 운영용. 현 /places, /local-content, /seo
|
│ └─ site/ 발행 정적 사이트 (SSR 엔트리 + 프리렌더)
|
||||||
│ ├─ site/ (그대로) 발행 정적 사이트
|
│
|
||||||
│ └─ shared/ (그대로) 세 앱이 공유하는 타입·slug·토큰
|
├─ admin/ 우리 — 전체 사이트 운영. **프론트 전용, 백엔드 없음**
|
||||||
└─ docs/
|
│ └─ src/ package.json vite.config.ts
|
||||||
|
│
|
||||||
|
├─ docs/ nginx/ postgres-init/ docker-compose.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
**핵심: 소스는 합치고, 배포 단위만 쪼갠다.** 2026 기준 이 형태(모노레포 + 다중 배포물)가
|
**negosium 대응:** `negodata/{backend, front}` 가 프로젝트 안에서 f/b 를 가르는 선례이고,
|
||||||
이 규모에서 기본값이다.
|
`lps-admin/` 이 **백엔드 없이 프론트만 가진 최상단 폴더**의 선례다. `admin/` 이 후자다.
|
||||||
|
|
||||||
**왜 레포를 안 쪼개나 — 이 프로젝트에는 특히 강한 이유가 있다.**
|
**`admin/` 에 백엔드를 두지 않는 이유.** 내부 4장이 부르는 것이 전부 지금 백엔드에 이미 있다 —
|
||||||
슬러그 규칙이 `site_payload.publish_slug()` 와 `shared/lib/slug.ts` **두 곳에 있고 같아야 한다.**
|
`useGetPlace` `useListPlaces` `useListFacts` `useListLinks` `useGetSchema` `useConfirmLink`
|
||||||
`SitePayload` 타입도 백엔드 출력과 프론트 입력이 짝이다. 한 레포에서는 이게 어긋나면
|
`useTransitionFact` → `router/v1/{place, fact, local, validator}`. 새로 만들 게 없고,
|
||||||
**타입 에러·테스트 실패**로 잡힌다. 레포를 쪼개면 같은 실수가 **운영 404** 로 나타난다.
|
자체 백엔드를 두면 `place`·`fact`·`link` 도메인을 **같은 DB 에 대고 두 번** 구현하게 된다.
|
||||||
게다가 지금 만드는 건 세 개의 제품이 아니라 **한 파이프라인의 세 창구**다.
|
→ 대가: `solution/backend` 가 죽으면 admin 도 멈춘다. **내부 도구라 감수한다.**
|
||||||
|
|
||||||
|
**`shared/` 는 없앤다.** 지금 `shared/` 가 지키던 진짜 결합은 슬러그 규칙과 `SitePayload` 이고,
|
||||||
|
그 양쪽(`backend` ↔ `site`)이 **둘 다 `solution/` 안에 있다.** 결합이 프로젝트 하나 안에서 닫히므로
|
||||||
|
공용 워크스페이스가 필요 없다. `admin/` 이 쓰는 것은 열거형 몇 개(`PlaceCategory` `PlaceStatus`
|
||||||
|
`FactStatus` `isPublishableFact`)뿐이라 자기 것으로 갖는다.
|
||||||
|
|
||||||
|
**왜 레포를 안 쪼개나.** 슬러그 규칙이 `site_payload.publish_slug()` 와 `site` 쪽 `publishUrl()`
|
||||||
|
**두 곳에 있고 같아야 한다.** `SitePayload`(252줄) 도 백엔드 출력과 프론트 입력이 짝이다.
|
||||||
|
한 레포에서는 어긋나면 **타입 에러·테스트 실패**로 잡히고, 레포를 쪼개면 같은 실수가
|
||||||
|
**운영 404** 로 나타난다 — 배포 시점이 달라 언제 깨졌는지도 모른다.
|
||||||
|
지금 만드는 건 세 개의 제품이 아니라 **한 파이프라인의 세 창구**다.
|
||||||
|
|
||||||
**백엔드는 쪼개지 않는다.** 마이크로서비스로 가르자는 제안은 지금 근거가 없다 —
|
**백엔드는 쪼개지 않는다.** 마이크로서비스로 가르자는 제안은 지금 근거가 없다 —
|
||||||
팀 규모·트래픽 어느 쪽도 그 비용을 정당화하지 못한다. 대신 **라우터 표면을 청중별로 가른다**:
|
팀 규모·트래픽 어느 쪽도 그 비용을 정당화하지 못한다. 그리고 `router/v1/` 이 이미 도메인별로
|
||||||
|
갈려 있어 **나중에 진짜 나눠야 할 때 그 선 따라 떨어진다** — 미룬다고 나중이 더 어려워지지 않는다.
|
||||||
|
지금 할 일은 **라우터 표면을 청중별로 가르는 것**뿐이다:
|
||||||
|
|
||||||
```
|
```
|
||||||
/api/v1/... 사장님 (console) — 자기 리소스만
|
/api/v1/... 사장님 (solution/front) — 자기 리소스만
|
||||||
/api/v1/admin/... 내부 (admin) — 라우터 레벨에서 role >= DEVELOPER 강제
|
/api/v1/admin/... 내부 (admin) — 라우터 레벨에서 role >= DEVELOPER 강제
|
||||||
```
|
```
|
||||||
|
|
||||||
엔드포인트마다 `if role >= ...` 를 흩뿌리지 않고 **의존성 하나로 라우터에 건다.**
|
엔드포인트마다 `if role >= ...` 를 흩뿌리지 않고 **의존성 하나로 라우터에 건다.**
|
||||||
지금 `common/authz.py is_owner_or_admin()` 이 그 단일 출처 역할을 하고 있으니, 그걸 라우터
|
지금 `common/authz.py is_owner_or_admin()` 이 그 단일 출처 역할을 하고 있으니, 그걸 라우터
|
||||||
의존성으로 올리면 된다.
|
의존성으로 올리면 된다.
|
||||||
|
⚠️ 그 함수의 이름에서 **admin 은 `UserRole.OWNER`(고객사 최상위)** 를 뜻한다.
|
||||||
|
최상단 폴더 `admin/`(우리 내부)과 **반대 뜻**이므로 읽을 때 헷갈리지 않는다.
|
||||||
|
|
||||||
### 마이그레이션 — 하루짜리, 되돌리기 쉬운 순서
|
### 마이그레이션 — 되돌리기 쉬운 순서
|
||||||
|
|
||||||
1. `frontend/console/` 생성 — `admin/` 의 `vite.config.ts`·`tsconfig.json`·`index.html` 복제
|
1. `solution/` 생성 → `backend/` 를 통째로 `git mv`. 경로만 바뀌고 내용은 그대로다.
|
||||||
2. `frontend/package.json` workspaces 에 `console` 추가, `dev:console`·`build:console` 스크립트
|
2. `frontend/site` → `solution/site`, `frontend/admin` → `solution/front`
|
||||||
3. `pages/BuilderPage.tsx` + `features/builder`, `features/onboarding`, `features/publish` →
|
3. `frontend/shared` 해체 — 타입·slug·토큰을 `solution/` 안으로 흡수
|
||||||
`console/` 로 이동. `components/ui`·`lib`·`stores` 중 **양쪽이 쓰는 것은 `shared/` 로 올린다**
|
4. `solution/front` 에서 내부 4장(`PlaceList` `PlaceDetail` `SeoAudit` `LocalContent`)과
|
||||||
4. `admin/src/app/router.tsx` 에서 `/builder` 라우트 제거, `/places` 를 기본 진입으로
|
`components/layout/AppShell`·`RequireAuth` 를 떼어 `admin/` 으로. 라우터에서도 제거
|
||||||
5. `docker-compose.yml` 에 console 서비스 추가 (또는 nginx 에서 경로/서브도메인 분기)
|
5. `docker-compose.yml` 경로 전부 갱신 + admin Vite 서비스 추가.
|
||||||
|
진입점 `exec npm run dev -w admin` 이 `solution/front` 를 가리키도록 바꾼다
|
||||||
6. 백엔드 `/api/v1/admin/*` 라우터 분리 + role 의존성
|
6. 백엔드 `/api/v1/admin/*` 라우터 분리 + role 의존성
|
||||||
|
|
||||||
★ 3번이 유일하게 시간이 드는 단계다. 공용으로 올릴 것과 한쪽 전용인 것을 가르는 작업이고,
|
★ **4번의 유일한 얽힘**: `PlaceDetailPage.tsx:18` 이 `@/features/onboarding` 의
|
||||||
여기서 대충 하면 `shared/` 가 쓰레기통이 된다.
|
`PasteFactsPanel`·`RecollectPanel` 을 쓴다. 내부 페이지가 사장님 쪽 feature 를 참조하는
|
||||||
|
**단 하나의 지점**이고, 이 둘만 복제하거나 옮기면 4장은 그냥 떨어진다.
|
||||||
|
|
||||||
|
★ **먼저 정리할 것**: 메인 체크아웃에 미커밋으로 남은 `frontend/admin/src/stores/builder.ts`·
|
||||||
|
`orval.config.ts` 가 이 마이그레이션이 옮길 파일이다. 커밋하든 버리든 **먼저 비우고** 시작한다.
|
||||||
|
|
||||||
**아직 실행하지 않았다.** 지금 구조로도 동작하고, 위 3가지 문제는 실사용 고객이 붙기 전까지는
|
**아직 실행하지 않았다.** 지금 구조로도 동작하고, 위 3가지 문제는 실사용 고객이 붙기 전까지는
|
||||||
터지지 않는다. 다만 **사장님에게 계정을 열어주기 전에는 반드시 끝내야 한다** — 1번(번들 노출)이
|
터지지 않는다. 다만 **사장님에게 계정을 열어주기 전에는 반드시 끝내야 한다** — 1번(번들 노출)이
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user