구조: admin 을 backend/frontend 로 가른다
admin/ 이 프론트 파일만 널려 있는 폴더였다. :9801 을 띄우는 코드도 solution/backend 안에 얹혀 있어서, 폴더만 봐서는 admin 에 백엔드가 있다는 걸 몰랐다. admin/backend/ main.py · app.py (:9801 진입점. 도메인 코드는 PYTHONPATH 로 solution/backend) admin/frontend/ 운영 화면 패키지 이름도 폴더에 맞췄다: @o2o/front → @o2o/frontend. 이미지 빌드 컨텍스트를 레포 루트로 올렸다 — 진입점(admin/)과 도메인 코드(solution/)가 한 이미지에 들어와야 한다. 루트 .dockerignore 로 프론트·문서·시크릿을 잘라냈다. ★ 실측으로 잡은 것: 패키지명을 바꾸면서 compose 의 `-w @o2o/front` 를 안 고쳐 web 컨테이너가 `No workspaces found` 로 재기동 루프에 빠져 있었다. 주석은 짧게 줄였다. 검증: lint·build 6개 전부 0. 컨테이너 5개 엔드포인트(9800·9801·3000·3002·80) 전부 200. 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
cabcdeaacc
commit
374a3a4a6e
@ -15,7 +15,7 @@ node_modules/
|
||||
solution/frontend/
|
||||
solution/site/
|
||||
solution/shared/
|
||||
admin/src/
|
||||
admin/frontend/
|
||||
docs/
|
||||
nginx/
|
||||
postgres-init/
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
10
admin/frontend/src/lib/solutionUrl.ts
Normal file
10
admin/frontend/src/lib/solutionUrl.ts
Normal file
@ -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`;
|
||||
}
|
||||
13
admin/frontend/tsconfig.json
Normal file
13
admin/frontend/tsconfig.json
Normal file
@ -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"]
|
||||
}
|
||||
28
admin/frontend/vite.config.ts
Normal file
28
admin/frontend/vite.config.ts
Normal file
@ -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},
|
||||
},
|
||||
});
|
||||
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 사장님 앱(`solution/frontend`)의 오리진.
|
||||
*
|
||||
* ★ 앱을 가른 뒤로 빌더는 **다른 오리진**이다(로컬 :3000 / 내부 :3002).
|
||||
* react-router `<Link to="/builder">` 로 두면 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`;
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
@ -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},
|
||||
},
|
||||
});
|
||||
@ -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"
|
||||
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@ -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",
|
||||
|
||||
16
package.json
16
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"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@o2o/front",
|
||||
"name": "@o2o/frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user