diff --git a/.dockerignore b/.dockerignore index be60bda..ad2c509 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,7 +15,7 @@ node_modules/ solution/frontend/ solution/site/ solution/shared/ -admin/src/ +admin/frontend/ docs/ nginx/ postgres-init/ diff --git a/admin/backend/app.py b/admin/backend/app.py index 0de81ee..93089ed 100644 --- a/admin/backend/app.py +++ b/admin/backend/app.py @@ -1,23 +1,9 @@ -"""내부 운영 API. 사장님 API(:9800)와 **프로세스와 포트가 갈린다.** +"""내부 운영 API. 사장님 API(:9800)와 프로세스·포트가 갈린다. -★ 이 파일은 `admin/` 안에 있지만 도메인 코드는 `solution/backend` 것을 그대로 쓴다. - PYTHONPATH 에 `solution/backend` 가 들어 있어서 `common.*` `router.*` `services.*` 가 - 그대로 import 된다(compose 의 api-admin 서비스 · Dockerfile 참조). - 왜 복제하지 않는지는 아래 첫 번째 ★ 를 볼 것. - -★ 왜 라우터를 새로 쓰지 않고 같은 것을 다시 마운트하나 - admin 화면이 부르는 API 는 전부 place·fact 라우터에 이미 있다(세어봤다: admin 전용 - 라우터는 0개다). 여기서 엔드포인트를 새로 쓰면 같은 DB 의 같은 테이블을 두 벌 - 구현하게 된다. 그래서 **같은 router 객체를 그대로 include 하고, 앱 단위로 권한만 덧건다.** - -★ 왜 경로 접두어(/v1/admin/...)가 아니라 포트를 가르나 - 접두어는 같은 프로세스 안에 있다 — 사장님이 닿는 서버에 내부 엔드포인트가 **존재한다.** - 포트를 가르면 내부 API 는 사장님이 닿는 네트워크에 아예 없다. 가드보다 강하다. - (compose 에서 이 포트는 127.0.0.1 에만 연다. 0.0.0.0 으로 열면 가른 의미가 없다.) - -★ 여기 붙는 모든 엔드포인트는 role >= DEVELOPER 다. - OWNER 는 자기 회사 최상위일 뿐 남의 회사를 볼 권한이 아니라서 막힌다 - (RequireDeveloper 주석 참조). 예외를 한 곳이라도 두면 그 예외가 기본값이 된다. +도메인 코드는 solution/backend 것을 PYTHONPATH 로 쓴다 — admin 전용 라우터가 0개라 +(전부 place·fact) 새로 쓰면 같은 테이블을 두 벌 구현하는 것뿐이다. +경로 접두어가 아니라 포트를 가른 이유: 접두어는 같은 프로세스라 사장님이 닿는 서버에 +내부 엔드포인트가 존재한다. 포트를 가르면 아예 없다. """ import time @@ -45,8 +31,7 @@ API_SERVER_START_TIME = GTime.UTCStr() @asynccontextmanager async def lifespan(app: FastAPI): - # ★ 스케줄러를 여기서 기동하지 않는다. 크론은 :9800 컨테이너 담당이고, - # 두 프로세스가 같이 돌면 같은 시각에 중복 실행된다. + # 크론은 :9800 담당. 여기서도 돌리면 같은 시각에 중복 실행된다. yield await DB_SESSION_MNG.dispose_all() @@ -89,11 +74,10 @@ async def healthz(): return API_SERVER_START_TIME -# ★ auth 만 게이트 밖이다 — 로그인 자체를 막으면 아무도 들어올 수 없다. -# (로그인은 되지만 role 이 낮으면 아래 라우터가 전부 403 이다.) +# auth 만 게이트 밖 — 로그인까지 막으면 아무도 못 들어온다. app.include_router(router.v1.auth.account.router) -# 그 밖의 전부: 앱 단위 DEVELOPER 게이트. +# 나머지 전부 role >= DEVELOPER. _gate = [Depends(RequireDeveloper)] app.include_router(router.v1.place.place.router, dependencies=_gate) app.include_router(router.v1.fact.fact.router, dependencies=_gate) diff --git a/admin/backend/main.py b/admin/backend/main.py index 47591f7..d8fb0bf 100644 --- a/admin/backend/main.py +++ b/admin/backend/main.py @@ -1,11 +1,5 @@ -# 내부 운영 API 서버. 사장님 API(solution/backend/web_main.py, :9800)와 -# **다른 프로세스·다른 포트**다. -# +# 내부 운영 API 서버 (:9801). 근거는 app.py 주석. # PYTHONPATH=../../solution/backend python main.py -# -# ★ 코드는 나누지 않는다. services/crud/models 를 web_main 과 그대로 공유하고 -# 진입점만 둘이다 — 도메인을 두 번 구현하지 않으면서 프로세스·네트워크만 가른다. -# 왜 그래야 하는지는 app.py 주석에 있다. import os diff --git a/admin/.env.example b/admin/frontend/.env.example similarity index 100% rename from admin/.env.example rename to admin/frontend/.env.example diff --git a/admin/eslint.config.js b/admin/frontend/eslint.config.js similarity index 100% rename from admin/eslint.config.js rename to admin/frontend/eslint.config.js diff --git a/admin/index.html b/admin/frontend/index.html similarity index 100% rename from admin/index.html rename to admin/frontend/index.html diff --git a/admin/package.json b/admin/frontend/package.json similarity index 100% rename from admin/package.json rename to admin/frontend/package.json diff --git a/admin/src/app/main.tsx b/admin/frontend/src/app/main.tsx similarity index 100% rename from admin/src/app/main.tsx rename to admin/frontend/src/app/main.tsx diff --git a/admin/src/app/provider.tsx b/admin/frontend/src/app/provider.tsx similarity index 100% rename from admin/src/app/provider.tsx rename to admin/frontend/src/app/provider.tsx diff --git a/admin/src/app/router.tsx b/admin/frontend/src/app/router.tsx similarity index 100% rename from admin/src/app/router.tsx rename to admin/frontend/src/app/router.tsx diff --git a/admin/frontend/src/lib/solutionUrl.ts b/admin/frontend/src/lib/solutionUrl.ts new file mode 100644 index 0000000..3dc1c86 --- /dev/null +++ b/admin/frontend/src/lib/solutionUrl.ts @@ -0,0 +1,10 @@ +/** + * 사장님 앱 오리진. 빌더는 다른 오리진이라(:3000 vs :3002) react-router Link 로 두면 + * admin 안에서 404 다. 절대 URL + 새 탭으로 연다. + */ +const ORIGIN = import.meta.env.VITE_SOLUTION_URL ?? 'http://localhost:3000'; + +export function builderUrl(params?: {placeId?: string; isNew?: boolean}): string { + if (params?.placeId) return `${ORIGIN}/builder?placeId=${encodeURIComponent(params.placeId)}`; + return `${ORIGIN}/builder?new=1`; +} diff --git a/admin/src/pages/LocalContentPage.tsx b/admin/frontend/src/pages/LocalContentPage.tsx similarity index 100% rename from admin/src/pages/LocalContentPage.tsx rename to admin/frontend/src/pages/LocalContentPage.tsx diff --git a/admin/src/pages/PlaceDetailPage.tsx b/admin/frontend/src/pages/PlaceDetailPage.tsx similarity index 100% rename from admin/src/pages/PlaceDetailPage.tsx rename to admin/frontend/src/pages/PlaceDetailPage.tsx diff --git a/admin/src/pages/PlaceListPage.tsx b/admin/frontend/src/pages/PlaceListPage.tsx similarity index 100% rename from admin/src/pages/PlaceListPage.tsx rename to admin/frontend/src/pages/PlaceListPage.tsx diff --git a/admin/src/pages/SeoAuditPage.tsx b/admin/frontend/src/pages/SeoAuditPage.tsx similarity index 100% rename from admin/src/pages/SeoAuditPage.tsx rename to admin/frontend/src/pages/SeoAuditPage.tsx diff --git a/admin/frontend/tsconfig.json b/admin/frontend/tsconfig.json new file mode 100644 index 0000000..e9429a9 --- /dev/null +++ b/admin/frontend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["vite/client", "node"], + "paths": { + "@admin/*": ["./src/*"], + "@/*": ["../../solution/frontend/src/*"], + "@o2o/shared": ["../../solution/shared/src/index.ts"], + "@o2o/shared/*": ["../../solution/shared/src/*"] + } + }, + "include": ["src", "vite.config.ts"] +} diff --git a/admin/frontend/vite.config.ts b/admin/frontend/vite.config.ts new file mode 100644 index 0000000..20a2c29 --- /dev/null +++ b/admin/frontend/vite.config.ts @@ -0,0 +1,28 @@ +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import {defineConfig} from 'vite'; + +/** + * 내부 운영 화면. 사장님 앱(solution/frontend)과 번들이 갈린다. + * + * `@` 를 이 앱이 아니라 사장님 앱 src 로 겨눈다 — 내부 화면이 쓰는 API·UI·수집 배선이 + * 거기 한 벌만 있고 그 파일들끼리도 `@/...` 로 서로를 부른다(자기 src 로 잡으면 TS2307 14건). + * 이 앱 고유 파일은 `@admin`. 의존 방향은 admin → solution 한 쪽뿐이다. + */ +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@admin': path.resolve(__dirname, 'src'), + '@': path.resolve(__dirname, '../../solution/frontend/src'), + // css 별칭이 먼저 와야 한다 — 접두어가 먼저 걸리면 없는 경로로 떨어진다. + '@o2o/shared/tokens.css': path.resolve(__dirname, '../../solution/shared/src/styles/tokens.css'), + '@o2o/shared/base.css': path.resolve(__dirname, '../../solution/shared/src/styles/base.css'), + '@o2o/shared': path.resolve(__dirname, '../../solution/shared/src'), + }, + }, + server: { + watch: {usePolling: true, interval: 300}, + }, +}); diff --git a/admin/src/lib/solutionUrl.ts b/admin/src/lib/solutionUrl.ts deleted file mode 100644 index f320220..0000000 --- a/admin/src/lib/solutionUrl.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 사장님 앱(`solution/frontend`)의 오리진. - * - * ★ 앱을 가른 뒤로 빌더는 **다른 오리진**이다(로컬 :3000 / 내부 :3002). - * react-router `` 로 두면 admin 안에서 라우트를 찾다 404 로 떨어진다. - * 그래서 절대 URL 로 나가고 새 탭으로 연다 — 돌아오는 길이 탭 닫기 하나로 끝난다. - * (사장님 앱에서 admin 으로 돌아오는 링크는 만들지 않는다. 사장님 앱은 admin 을 몰라야 한다.) - */ -const ORIGIN = import.meta.env.VITE_SOLUTION_URL ?? 'http://localhost:3000'; - -export function builderUrl(params?: {placeId?: string; isNew?: boolean}): string { - if (params?.placeId) return `${ORIGIN}/builder?placeId=${encodeURIComponent(params.placeId)}`; - return `${ORIGIN}/builder?new=1`; -} diff --git a/admin/tsconfig.json b/admin/tsconfig.json deleted file mode 100644 index 34c2b66..0000000 --- a/admin/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "types": ["vite/client", "node"], - "paths": { - "@admin/*": ["./src/*"], - "@/*": ["../solution/frontend/src/*"], - "@o2o/shared": ["../solution/shared/src/index.ts"], - "@o2o/shared/*": ["../solution/shared/src/*"] - } - }, - "include": ["src", "vite.config.ts"] -} diff --git a/admin/vite.config.ts b/admin/vite.config.ts deleted file mode 100644 index 72f76de..0000000 --- a/admin/vite.config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import tailwindcss from '@tailwindcss/vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; -import {defineConfig} from 'vite'; - -/** - * 내부 운영 화면(우리 회사용). 사장님 앱(`solution/frontend`)과 **번들이 갈린다** — - * 이 앱의 라우트 이름과 코드는 사장님 브라우저에 내려가지 않는다. 그게 앱을 가른 이유다. - * - * ★ `@` 를 이 앱이 아니라 **사장님 앱의 src** 로 겨눈다. - * 내부 화면이 쓰는 API 클라이언트·UI 프리미티브·수집 배선이 전부 거기 한 벌만 있고, - * 그 파일들도 자기들끼리 `@/...` 로 서로를 부르기 때문이다. 여기서 `@` 를 admin/src 로 - * 잡으면 그 내부 참조가 전부 깨진다(실측: TS2307 14건). - * 복제해서 푸는 방법도 있지만 RecollectPanel 주석이 그걸 금지한다 — - * "수집 경로를 두 벌 만들면 확정 게이트"가 갈라진다. - * - * ★ 이 앱 고유의 파일은 `@admin` 이다. 방향은 admin → solution 한 쪽뿐이고, - * 반대가 생기면 번들을 가른 의미가 사라진다. - */ -export default defineConfig({ - plugins: [react(), tailwindcss()], - resolve: { - alias: { - '@admin': path.resolve(__dirname, 'src'), - '@': path.resolve(__dirname, '../solution/frontend/src'), - // css 는 더 구체적인 별칭이 먼저 와야 한다 — '@o2o/shared' 접두어가 먼저 걸리면 - // 없는 경로(shared/src/tokens.css)로 떨어진다. - '@o2o/shared/tokens.css': path.resolve(__dirname, '../solution/shared/src/styles/tokens.css'), - '@o2o/shared/base.css': path.resolve(__dirname, '../solution/shared/src/styles/base.css'), - '@o2o/shared': path.resolve(__dirname, '../solution/shared/src'), - }, - }, - server: { - watch: {usePolling: true, interval: 300}, - }, -}); diff --git a/docker-compose.yml b/docker-compose.yml index afa0818..80195bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -134,7 +134,7 @@ services: [ -x node_modules/.bin/vite ] || npm install node solution/site/scripts/serve-sites.mjs & node solution/site/scripts/watch-payloads.mjs & - exec npm run dev -w @o2o/front + exec npm run dev -w @o2o/frontend environment: PORT: 3001 INDEXNOW_KEY: ${INDEXNOW_KEY:-} @@ -152,7 +152,7 @@ services: - /app/node_modules - /app/solution/site/node_modules - /app/solution/frontend/node_modules - - /app/admin/node_modules + - /app/admin/frontend/node_modules # ★ 산출물은 named volume. 호스트 경로면 재배포로 코드를 갈아엎는 순간 전 사이트가 404 다. - site-out:/app/solution/site/out ports: @@ -186,7 +186,7 @@ services: - /app/node_modules - /app/solution/site/node_modules - /app/solution/frontend/node_modules - - /app/admin/node_modules + - /app/admin/frontend/node_modules ports: # ★ 운영에서는 내부망에만 연다. 0.0.0.0 으로 열면 앱을 가른 의미가 없다. - "${ADMIN_BIND:-127.0.0.1}:3002:3002" diff --git a/package-lock.json b/package-lock.json index 4f89e18..032438f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,46 @@ "solution/shared", "solution/frontend", "solution/site", - "admin" + "admin/frontend" ], "engines": { "node": ">=20" } }, "admin": { + "name": "@o2o/admin", + "version": "0.0.0", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@o2o/shared": "*", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.62.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.546.0", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "react-router": "^7.17.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.0.1", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^7.1.1", + "tailwindcss": "^4.1.14", + "typescript": "~5.8.2", + "typescript-eslint": "^8.45.0", + "vite": "^6.2.3" + } + }, + "admin/frontend": { "name": "@o2o/admin", "version": "0.0.0", "dependencies": { @@ -1450,10 +1483,10 @@ } }, "node_modules/@o2o/admin": { - "resolved": "admin", + "resolved": "admin/frontend", "link": true }, - "node_modules/@o2o/front": { + "node_modules/@o2o/frontend": { "resolved": "solution/frontend", "link": true }, @@ -8750,7 +8783,7 @@ } }, "solution/frontend": { - "name": "@o2o/front", + "name": "@o2o/frontend", "version": "0.0.0", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index c42d09b..212f836 100644 --- a/package.json +++ b/package.json @@ -7,21 +7,21 @@ "solution/shared", "solution/frontend", "solution/site", - "admin" + "admin/frontend" ], "scripts": { - "dev": "npm run dev -w @o2o/front", - "dev:front": "npm run dev -w @o2o/front", + "dev": "npm run dev -w @o2o/frontend", + "dev:frontend": "npm run dev -w @o2o/frontend", "dev:admin": "npm run dev -w @o2o/admin", "dev:site": "npm run dev -w @o2o/site", - "build": "npm run build -w @o2o/front && npm run build -w @o2o/admin && npm run build -w @o2o/site", - "build:front": "npm run build -w @o2o/front", + "build": "npm run build -w @o2o/frontend && npm run build -w @o2o/admin && npm run build -w @o2o/site", + "build:frontend": "npm run build -w @o2o/frontend", "build:admin": "npm run build -w @o2o/admin", "build:site": "npm run build -w @o2o/site", "prerender": "npm run prerender -w @o2o/site", - "lint": "npm run lint -w @o2o/front && npm run lint -w @o2o/admin && npm run lint -w @o2o/site", - "orval": "npm run orval -w @o2o/front", - "clean": "rm -rf solution/frontend/dist admin/dist solution/site/dist solution/site/.ssr-dist node_modules/.vite" + "lint": "npm run lint -w @o2o/frontend && npm run lint -w @o2o/admin && npm run lint -w @o2o/site", + "orval": "npm run orval -w @o2o/frontend", + "clean": "rm -rf solution/frontend/dist admin/frontend/dist solution/site/dist solution/site/.ssr-dist node_modules/.vite" }, "engines": { "node": ">=20" diff --git a/solution/frontend/package.json b/solution/frontend/package.json index 00ef620..0ffa08b 100644 --- a/solution/frontend/package.json +++ b/solution/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "@o2o/front", + "name": "@o2o/frontend", "private": true, "version": "0.0.0", "type": "module",