diff --git a/.gitignore b/.gitignore index 3b6f819..69edd36 100644 --- a/.gitignore +++ b/.gitignore @@ -20,13 +20,26 @@ venv/ # 생성물(FastAPI OpenAPI 스펙). 필요 시 재생성한다. backend/openapi.json -# 발행 payload 는 frontend/site/payloads/.gitignore 가 단일 출처다 — 여기에 겹쳐 쓰지 않는다. +# 발행 payload 는 solution/site/payloads/.gitignore 가 단일 출처다 — 여기에 겹쳐 쓰지 않는다. # 정적 산출물 — 운영에서는 Docker named volume(site-out)에 있고, 로컬에서 직접 구우면 # 여기 생긴다. 어느 쪽이든 payload 로 다시 굽는 재생성물이라 git 이 관리할 대상이 아니다. -frontend/site/out/ -frontend/site/dist/ -frontend/admin/dist/ +solution/site/out/ +solution/site/dist/ +admin/dist/ + +# nginx 설정: 서버마다 다르므로 실제 파일은 커밋하지 않는다. 템플릿만 커밋한다. +# ★ compose(docker-compose.yml:224)가 ./nginx/site.conf 를 bind mount 한다 — +# 파일이 없으면 Docker 가 그 자리에 **디렉토리를 만들어** nginx 가 설정 없이 뜬다. +# 클론 직후 반드시: cp nginx/site.conf.example nginx/site.conf +nginx/site.conf +!nginx/site.conf.example + +# ── npm 워크스페이스(루트가 solution/{shared,front,site} + admin 을 묶는다) ── +node_modules/ +dist/ +.ssr-dist/ +*.tsbuildinfo # OS .DS_Store diff --git a/AGENTS.md b/AGENTS.md index d128964..3fad9b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ - **로컬 `out/assets` 는 빌드마다 통째로 갈린다** (`prerender.ts:573` `rmSync`). 옛 해시 파일이 사라지므로 새 번들로 일부 사이트만 구우면 나머지는 CSS 가 404 다. → 프리렌더 기동 시 전체 재굽기가 이 구멍을 메운다. -- **★ 프론트(`frontend/site`)를 배포하면 반드시 전체 재굽기 + 전체 재업로드.** +- **★ 프론트(`solution/site`)를 배포하면 반드시 전체 재굽기 + 전체 재업로드.** `azure_static.publish(slug)` 는 공용 자산 + `s/` 만 올린다 — **렌더러를 고쳐도 다른 사이트에는 반영되지 않는다.** → `docker compose restart web` 후 `python scripts/republish_all.py` @@ -43,7 +43,7 @@ (프리렌더는 서브패스 마운트를 지원한다 — `basePath` 가 `/sites/s/joy` 면 자산은 `/sites/assets`.) - **`AZURE_STORAGE_CONTAINER=$web`** — 셸에서 export 할 땐 반드시 작은따옴표(`'$web'`). - **슬러그 규칙은 두 곳에 있고 같아야 한다**: `site_payload.publish_slug()` ↔ - `frontend/shared/src/lib/slug.ts publishUrl`. 어긋나면 발행은 성공하고 주소만 404 다. + `solution/shared/src/lib/slug.ts publishUrl`. 어긋나면 발행은 성공하고 주소만 404 다. - **디렉토리 요청 → `index.html`.** `/s/` 가 **끝 슬래시 없이** 열려야 한다. 정적 서버를 바꾸든 nginx 설정을 만지든 이 규칙부터 확인한다. @@ -58,19 +58,51 @@ 코드를 읽으면 알 수 있는 "무엇"은 쓰지 않는다. - 문서는 코드와 **같은 커밋**에서 고친다. 동작을 바꿨는데 문서를 안 고쳤으면 미완이다. +## 레포 구조 + +``` +solution/ 사장님 — backend(FastAPI+워커) · front(빌더) · site(발행물) · shared(계약) +admin/ 우리 — 전체 사이트 운영. **프론트 전용, 백엔드 없음** +``` + +최상단은 **프로젝트 단위**다(`o2o-negosium` 과 같은 규약). `frontend/` `backend/` 를 최상단 +묶음 폴더로 쓰지 않는다. 근거와 경계는 [ARCHITECTURE.md 4절](docs/ARCHITECTURE.md). + +★ **의존 방향은 `admin` → `solution` 한 쪽뿐이다.** admin 이 수집 배선·UI 를 재수출로 가져다 쓴다 +(`admin/src/**` 의 얇은 파일들). 반대 방향이 생기면 번들을 가른 의미가 사라진다. + ## 실행 ```bash -docker compose up -d # API :9800 · 워커 · web :3000 · nginx :80 +docker compose up -d # API :9800 · 워커 · web :3000 · admin :3002 · nginx :80 docker compose logs -f worker ``` -- 발행 사이트: `http://localhost:3000/s/` (admin Vite 가 :3001 정적서버로 프록시) -- DB 는 compose 밖이다 (호스트 PostgreSQL, `host.docker.internal`). `.env` 없으면 `cp .env.example .env` -- 백엔드 스크립트는 `backend/` 에서 `.venv/bin/python scripts/.py` -- 테스트: `backend/` 에서 `.venv/bin/pytest`. **`APP_ENV=test` 면 `.env` 를 읽지 않는다** — +- 사장님 앱: `http://localhost:3000` · 내부 운영: `http://localhost:3002` +- 발행 사이트: `http://localhost:3000/s/` (front Vite 가 :3001 정적서버로 프록시) +- **클론 직후 1회**: `cp .env.example .env` · `cp nginx/site.conf.example nginx/site.conf` + (후자를 빼먹으면 Docker 가 그 자리에 디렉토리를 만들어 nginx 가 설정 없이 뜬다) +- DB 는 compose 밖이다 (호스트 PostgreSQL, `host.docker.internal`). + 스키마는 `postgres-init/init-data/init.sql` **한 벌**이다 — 누적 ALTER 파일은 없다 +- npm 워크스페이스 루트는 **레포 루트**다. `npm install` 은 루트에서 한 번. + `npm run dev:front` / `dev:admin` / `dev:site` +- 백엔드 스크립트는 `solution/backend/` 에서 `.venv/bin/python scripts/.py` +- 테스트: `solution/backend/` 에서 `.venv/bin/pytest`. **`APP_ENV=test` 면 `.env` 를 읽지 않는다** — 실키가 테스트로 새어 외부 API 요금이 나가는 경로를 막아 뒀다 +## .env 는 어디에 두나 + +**루트 `.env` 가 단일 출처다.** compose 가 이 파일만 읽고(`${...}` 치환 + `env_file`), +Vite 앱은 구조상 **자기 디렉토리의 `.env` 만** 읽는다 — 그래서 나뉘어 있는 것이지 취향이 아니다. + +| 파일 | 담는 것 | +|---|---| +| `.env` | DB · JWT · 외부 API 키 · `SITE_PUBLIC_HOST` · `INDEXNOW_KEY` | +| `solution/front/.env` · `admin/.env` | 그 앱에만 있는 `VITE_*` | + +★ **두 곳에 같은 값을 적지 않는다.** 발행 호스트는 compose 가 루트의 `SITE_PUBLIC_HOST` 를 +`VITE_PUBLISH_HOST` 로 흘려보낸다. 각자 적으면 canonical 과 화면 주소가 조용히 갈라진다. + ## 커밋 - 한 커밋 = 한 가지 변경. 문서와 그 문서가 설명하는 코드는 같은 커밋에 둔다. diff --git a/README.md b/README.md index bb65e5f..a0eb940 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,20 @@ docker compose logs -f worker | API 문서 | http://localhost:9800/docs | DB 는 compose 밖이다 (호스트 PostgreSQL, `host.docker.internal`). -백엔드 스크립트는 `backend/` 에서 `.venv/bin/python scripts/.py`. +백엔드 스크립트는 `solution/backend/` 에서 `.venv/bin/python scripts/.py`. ## 레포 구조 ``` -backend/ FastAPI + 워커. HTML 을 만들지 않는다 — payload JSON 만 떨어뜨린다 -frontend/ - admin/ 빌더(사장님) + 내부 운영 화면 — ★ 분리 예정, ARCHITECTURE.md 4절 +solution/ 사장님 — 사이트 만들기·관리 + backend/ FastAPI + 워커. HTML 을 만들지 않는다 — payload JSON 만 떨어뜨린다 + front/ 빌더(위저드 + 에디터 + 발행 게이트) site/ 발행 사이트. SSR 엔트리 + 프리렌더 + 정적 서버 - shared/ 두 앱과 백엔드 계약이 만나는 타입·규칙 (SitePayload, slug) + shared/ front·site·백엔드 계약이 만나는 타입·규칙 (SitePayload, slug) +admin/ 우리 — 전체 사이트 운영. **프론트 전용, 백엔드 없음** docs/ 아래 표 -postgres-init/ 스키마 DDL +nginx/ 발행 사이트 정적 서빙 (site.conf 는 .example 만 커밋한다) +postgres-init/ 스키마 DDL (init.sql 한 벌) ``` ## 문서 지도 diff --git a/admin/.env.example b/admin/.env.example new file mode 100644 index 0000000..b923695 --- /dev/null +++ b/admin/.env.example @@ -0,0 +1,4 @@ +# 내부 운영 앱. Vite 는 .env 를 **자기 디렉토리에서만** 읽으므로 여기 둔다. +# ★ 사장님 앱과 겹치는 값(발행 호스트 등)은 여기 적지 않는다 — 루트 .env 가 단일 출처이고 +# compose 가 주입한다. 두 곳에 적으면 언젠가 갈라진다. +VITE_API_BASE_URL=http://localhost:9800 diff --git a/frontend/admin/eslint.config.js b/admin/eslint.config.js similarity index 100% rename from frontend/admin/eslint.config.js rename to admin/eslint.config.js diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..dbef4f3 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + Web4Ai 운영 + + +
+ + + diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 0000000..9fbe3b0 --- /dev/null +++ b/admin/package.json @@ -0,0 +1,39 @@ +{ + "name": "@o2o/admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3002 --host=0.0.0.0", + "build": "tsc --noEmit && eslint src && vite build", + "preview": "vite preview", + "clean": "rm -rf dist", + "lint": "tsc --noEmit && eslint src" + }, + "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" + } +} diff --git a/admin/src/app/main.tsx b/admin/src/app/main.tsx new file mode 100644 index 0000000..1ac81f1 --- /dev/null +++ b/admin/src/app/main.tsx @@ -0,0 +1,15 @@ +import {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import {RouterProvider} from 'react-router'; +import {Providers} from './provider'; +import {router} from './router'; +// 스타일은 사장님 앱과 같은 것을 쓴다 — 같은 회사 화면이라 두 벌로 갈릴 이유가 없다. +import '@/index.css'; + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/admin/src/app/provider.tsx b/admin/src/app/provider.tsx new file mode 100644 index 0000000..cbdd365 --- /dev/null +++ b/admin/src/app/provider.tsx @@ -0,0 +1,57 @@ +import {QueryClientProvider} from '@tanstack/react-query'; +import {useEffect, type ReactNode} from 'react'; +import {Toaster} from 'sonner'; +import {getAccessToken, me} from '@/api'; +import {queryClient} from '@/lib/query-client'; +import {toAuthUser, useAuthStore} from '@/stores/auth'; + +/** + * 저장된 액세스 토큰으로 세션을 복구한다. + * + * ★ 사장님 앱과 결정적으로 다른 점: **여기서는 실패가 곧 차단이다.** + * 빌더는 로그인 없이도 돌아야 해서 인증 실패를 삼키지만(solution/front/app/provider.tsx), + * 내부 운영 화면은 전부 RequireAuth 뒤에 있다. 두 정책을 한 앱에 두면 실수가 늘 + * 느슨한 쪽으로 나기 때문에 앱을 갈랐다. + */ +function useRestoreSession() { + const setUser = useAuthStore((s) => s.setUser); + const finishRestore = useAuthStore((s) => s.finishRestore); + + useEffect(() => { + let alive = true; + if (!getAccessToken()) { + finishRestore(); + return; + } + void me() + .then((res) => { + if (!alive || res.result?.success === false) return; + // RemoveNoneResponse 라 신원 필드가 통째로 빠져 올 수 있다 — 반쪽짜리 사용자를 세우지 않는다. + if (!res.user_id || !res.id) return; + setUser(toAuthUser(res)); + }) + .catch(() => { + /* 토큰이 죽었으면 비로그인 상태로 떨어뜨린다. RequireAuth 가 로그인으로 보낸다. */ + }) + .finally(() => { + if (alive) finishRestore(); + }); + return () => { + alive = false; + }; + }, [setUser, finishRestore]); +} + +function SessionGate({children}: {children: ReactNode}) { + useRestoreSession(); + return <>{children}; +} + +export function Providers({children}: {children: ReactNode}) { + return ( + + {children} + + + ); +} diff --git a/admin/src/app/router.tsx b/admin/src/app/router.tsx new file mode 100644 index 0000000..9185007 --- /dev/null +++ b/admin/src/app/router.tsx @@ -0,0 +1,47 @@ +import {Building2, CalendarDays} from 'lucide-react'; +import {createBrowserRouter, Navigate, Outlet} from 'react-router'; +import {AppShell, type NavItem} from '@/components/layout/AppShell'; +import {RequireAuth} from '@/components/layout/RequireAuth'; +import {LocalContentPage} from '@admin/pages/LocalContentPage'; +import {LoginPage} from '@/pages/LoginPage'; +import {NotFoundPage} from '@/pages/NotFoundPage'; +import {PlaceDetailPage} from '@admin/pages/PlaceDetailPage'; +import {PlaceListPage} from '@admin/pages/PlaceListPage'; +import {SeoAuditPage} from '@admin/pages/SeoAuditPage'; + +/** + * ★ 내부 메뉴는 여기 있다. AppShell(사장님 앱 소유)에 두면 이 경로 이름들이 + * 사장님 번들에 문자열로 남는다 — 앱을 가른 이유가 사라진다. + */ +const ADMIN_NAV: NavItem[] = [ + {to: '/places', match: '/places', label: '사업장', icon: Building2}, + {to: '/local-content', match: '/local-content', label: '지역 콘텐츠', icon: CalendarDays}, +]; + +/** + * 내부 운영 화면. **전부 RequireAuth 뒤에 둔다** — 예외를 하나 두는 순간 + * 그 예외가 기본값이 된다. 사장님 앱과 앱을 가른 이유가 이 규칙을 지키기 위해서다. + */ +export const router = createBrowserRouter([ + {path: '/login', element: }, + + {path: '/', element: }, + + { + element: ( + + + + + + ), + children: [ + {path: '/places', element: }, + {path: '/places/:placeId', element: }, + {path: '/places/:placeId/seo', element: }, + {path: '/local-content', element: }, + ], + }, + + {path: '*', element: }, +]); diff --git a/admin/src/lib/solutionUrl.ts b/admin/src/lib/solutionUrl.ts new file mode 100644 index 0000000..a6f03a8 --- /dev/null +++ b/admin/src/lib/solutionUrl.ts @@ -0,0 +1,14 @@ +/** + * 사장님 앱(`solution/front`)의 오리진. + * + * ★ 앱을 가른 뒤로 빌더는 **다른 오리진**이다(로컬 :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/frontend/admin/src/pages/LocalContentPage.tsx b/admin/src/pages/LocalContentPage.tsx similarity index 100% rename from frontend/admin/src/pages/LocalContentPage.tsx rename to admin/src/pages/LocalContentPage.tsx diff --git a/frontend/admin/src/pages/PlaceDetailPage.tsx b/admin/src/pages/PlaceDetailPage.tsx similarity index 100% rename from frontend/admin/src/pages/PlaceDetailPage.tsx rename to admin/src/pages/PlaceDetailPage.tsx diff --git a/frontend/admin/src/pages/PlaceListPage.tsx b/admin/src/pages/PlaceListPage.tsx similarity index 92% rename from frontend/admin/src/pages/PlaceListPage.tsx rename to admin/src/pages/PlaceListPage.tsx index 06f5c31..9c5608a 100644 --- a/frontend/admin/src/pages/PlaceListPage.tsx +++ b/admin/src/pages/PlaceListPage.tsx @@ -14,6 +14,7 @@ import { import {PlaceCategory, PlaceStatus} from '@o2o/shared'; import {deletePlace, useListPlaces} from '@/api'; import {EmptyState, PageContainer} from '@/components/layout/AppShell'; +import {builderUrl} from '@admin/lib/solutionUrl'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Input} from '@/components/ui/input'; @@ -87,13 +88,15 @@ export function PlaceListPage() { className="w-56" /> {/* 새로 만드는 길은 위저드다 — `?new=1` 이 지난번 편집 상태를 비우고 1단계부터 연다. */} - 새 사이트 만들기 - + } > @@ -116,13 +119,15 @@ export function PlaceListPage() { title="아직 만든 사이트가 없습니다" description="상호명 하나만 있으면 시작할 수 있습니다. 주소·좌표는 동일 업소 검증이 채웁니다." action={ - 새 사이트 만들기 - + } /> ) : ( @@ -130,8 +135,10 @@ export function PlaceListPage() { {places.map((place) => (
  • {/* 줄 전체가 에디터로 가는 링크다 — 목록에 온 용건이 편집이라서다. */} -
    @@ -159,16 +166,18 @@ export function PlaceListPage() { )}
    - +
    - 사이트 편집 - + {/* 발행 게이트에 걸렸을 때 가는 곳 — 수집된 값을 확인·수정한다. */} ` 로 접속을 시도하다 실패한다 — 반드시 주입해야 한다. - # 기본값은 로컬 dev(negosium-db 컨테이너, postgres/password). 배포는 .env 나 셸 env 로 덮는다. - DB_HOST: ${DB_HOST:-host.docker.internal} # 컨테이너→호스트 DB (toml 의 127.0.0.1 override) + PYTHONUNBUFFERED: "1" + # 이미지 안 config.local.toml 은 플레이스홀더다 — 아래 값이 없으면 접속·서명이 실패한다. + DB_HOST: ${DB_HOST:-host.docker.internal} DB_PORT: ${DB_PORT:-5432} DB_USER: ${DB_USER:-postgres} DB_PASSWORD: ${DB_PASSWORD:-password} DB_NAME: ${DB_NAME:-web4ai_db} - # ── JWT 서명 키 ── - # ★ DB 와 같은 이유로 반드시 주입해야 한다. 이미지의 config.local.toml 은 플레이스홀더라 - # 주입하지 않으면 "" 이라는 공개된 문자열이 서명 키가 된다(부팅 시 경고 로그). - # 운영 배포에서는 .env 나 배포 시크릿으로 반드시 채운다. 두 컨테이너가 같은 키를 봐야 한다. JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:-} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-} - # ── 발행 호스트 ── - # 커스텀 도메인이 없는 사이트의 공개 주소(`https://<이 값>/s/`). - # ★ API 와 워커가 **같은 값**을 봐야 한다. API 가 화면에 보여준 주소와 워커가 구운 - # canonical·사이트맵이 갈리면, 사장님 화면은 멀쩡한데 검색엔진만 엉뚱한 주소를 받는다. + # ★ 프론트(VITE_PUBLISH_HOST)와 같은 값이어야 한다. canonical·og:url·sitemap 이 전부 이걸 쓴다. SITE_PUBLIC_HOST: ${SITE_PUBLIC_HOST:-w4ai.o2o.kr} - # ── 발행 산출물 ── - # 빌드 잡이 발행마다 payload JSON 을 여기 떨어뜨린다. 호스트의 frontend/site/payloads 로 - # 바인드돼 있어서, o2o-web4ai-web 의 감시 프로세스가 **바뀐 payload 만** 골라 굽는다 — - # docker cp 로 옮기는 수동 단계가 없다. - # - # ★ 이 디렉토리는 양방향이다. 프리렌더가 사이트마다 결과를 - # `.status/.json` 에 써 주고, 백엔드가 그걸 읽어 렌더 상태를 사이트 조회 API 로 - # 내보낸다(services/render_report). 백엔드에 마운트된 유일한 디렉토리라 여기를 쓴다 — - # 이게 없으면 프리렌더가 깨져도 DB 는 "발행됨"이라 답하고 아무도 모른다. SITE_PAYLOAD_DIR: /app/out/payloads SITE_OUTPUT_DIR: /app/out/sites - # ── 색인 통보 ── - # 발행 즉시 네이버·Bing 에 알린다(구글은 IndexNow 를 지원하지 않는다 — Search Console 사이트맵 제출이 따로다). - # ★ 프리렌더(o2o-web4ai-web)와 백엔드가 **같은 값**을 봐야 한다. 프리렌더는 이 키로 루트에 - # `.txt` 를 굽고, 검색엔진은 통보를 받으면 그 파일을 열어 대조한다. 어긋나면 403 이다. + # ★ 프리렌더와 같은 값이어야 한다. 어긋나면 색인 통보가 403 이다. INDEXNOW_KEY: ${INDEXNOW_KEY:-} services: - # ── API 서버 ──────────────────────────────────────────────────────────── - # 요청 접수/조회만 한다. 수집·비전분석·빌드는 잡으로 큐에 넣고 즉시 응답하며, - # 클라이언트는 GET /v1/job/{id} 를 폴링한다. api: build: - context: ./backend + context: ./solution/backend dockerfile: Dockerfile - image: o2o-web4ai-backend # 워커와 공유하는 이미지 태그(한 번만 빌드된다) + image: o2o-web4ai-backend container_name: o2o-web4ai-api command: ["python", "web_main.py"] env_file: - - .env # 외부 API 키. 없으면 cp .env.example .env + - .env environment: <<: *common-env - # ★ APScheduler 크론(지역정보 갱신 등)은 **이 컨테이너에서만** 돈다. - # 크론은 '시각'으로 발화하므로 프로세스가 여럿이면 같은 시각에 중복 실행된다 — - # 그래서 단일 컨테이너로 고정한다. 아래 워커의 잡 큐와는 성격이 다르다: - # 잡 큐는 DB 가 원자적으로 한 명에게만 배분(FOR UPDATE SKIP LOCKED)하므로 몇 개를 띄워도 안전하다. + # ★ 크론은 이 컨테이너에서만 돈다. 프로세스가 여럿이면 같은 시각에 중복 실행된다. SCHEDULER_ENABLED: "1" volumes: - # ★ 컨테이너 안에만 두면 컨테이너를 지울 때 발행 산출물이 같이 사라진다. - # SSG 를 돌리는 쪽(호스트의 frontend/site)이 직접 읽어야 하므로 밖으로 뺀다. - - ./frontend/site/payloads:/app/out/payloads + - ./solution/site/payloads:/app/out/payloads ports: - - "${API_BIND:-0.0.0.0}:9800:9800" # prod 는 API_BIND=127.0.0.1 로 내부만 개방(리버스프록시 뒤) + - "${API_BIND:-0.0.0.0}:9800:9800" extra_hosts: - "host.docker.internal:host-gateway" restart: unless-stopped @@ -100,132 +49,106 @@ services: driver: json-file options: { max-size: "10m", max-file: "5" } - # ── 작업 큐 워커 ──────────────────────────────────────────────────────── - # 수집 파이프라인 · 사진 비전 분석 · 사이트 빌드를 처리한다. 한 건에 3~10분 걸리므로 - # API 요청 안에서 처리하지 않는다. - # - # ★ 스케일 안전: docker compose up -d --scale o2o-web4ai-worker=3 - # 큐가 `UPDATE ... WHERE job_id = (SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING` 단일 문장으로 - # 할당하므로 워커가 몇 개든 같은 잡이 두 번 돌지 않는다. 컨테이너가 죽어도 lease(JOB_LEASE_SEC)가 - # 만료되면 reaper 가 회수해 재큐한다 — 잡이 증발하지 않는다. - # (스케일하려면 container_name 을 두면 안 된다 — 이름이 충돌한다. 그래서 여기엔 없다. - # 프로젝트 이름이 붙어 o2o-web4ai-worker-1 로 뜬다.) worker: build: - context: ./backend + context: ./solution/backend dockerfile: Dockerfile - image: o2o-web4ai-backend # api 와 동일 이미지 — 두 번 빌드되지 않는다 + image: o2o-web4ai-backend command: ["python", "worker_main.py"] env_file: - .env environment: <<: *common-env - SCHEDULER_ENABLED: "0" # 크론은 API 컨테이너 담당. 워커는 잡 큐만 돈다 - WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-1} # 한 프로세스가 동시에 처리할 잡 수 - JOB_DEADLINE_SEC: ${JOB_DEADLINE_SEC:-900} # 잡 1건 처리 상한. Perplexity(10~30s)+크롤링+Vision(사진 20~50장) 감안 - JOB_LEASE_SEC: ${JOB_LEASE_SEC:-120} # 소유권 임대. heartbeat 가 1/3 주기로 갱신, 워커 사망 시 이만큼 뒤 회수 - # ★ 이미지의 HEALTHCHECK 는 API 용(HTTP :9800)이다. 워커는 포트가 없어서 그대로 두면 - # 멀쩡히 잡을 돌면서도 계속 unhealthy 로 뜬다 — 진짜 고장과 구분이 안 되므로 끈다. - # (Dockerfile 주석의 "워커는 이 헬스체크를 쓰지 않는다"를 compose 에서 실제로 반영하는 자리.) - # 워커용 하트비트 기반 체크는 필요해질 때 추가한다. + SCHEDULER_ENABLED: "0" + WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-1} + JOB_DEADLINE_SEC: ${JOB_DEADLINE_SEC:-900} + JOB_LEASE_SEC: ${JOB_LEASE_SEC:-120} + # 이미지의 HEALTHCHECK 는 API 용(HTTP :9800)이다. 워커는 포트가 없어 그대로 두면 늘 unhealthy 다. healthcheck: disable: true volumes: - # ★ 컨테이너 안에만 두면 컨테이너를 지울 때 발행 산출물이 같이 사라진다. - # SSG 를 돌리는 쪽(호스트의 frontend/site)이 직접 읽어야 하므로 밖으로 뺀다. - - ./frontend/site/payloads:/app/out/payloads - # 발행 검수 통과 후 워커가 이 산출물을 Azure Blob에 업로드한다. + - ./solution/site/payloads:/app/out/payloads - site-out:/app/out/sites:ro extra_hosts: - "host.docker.internal:host-gateway" - # graceful 종료: SIGTERM → 새 잡 claim 중단 → 하던 잡 마무리 → 종료. - # 이 시간을 넘기면 Docker 가 SIGKILL 하지만, 그래도 안전하다 — lease 가 만료되면 - # reaper 가 그 잡을 재큐한다(작업이 유실되지 않고 다음 워커가 이어받는다). - # 값은 '배포 속도 vs 하던 일 마무리' 트레이드오프다. JOB_DEADLINE_SEC(900)까지 올리면 - # 어떤 잡이든 끝까지 기다리지만 docker compose down 이 최대 15분 매달린다. stop_grace_period: 300s depends_on: - - api # 이미지 빌드/기동 순서만 맞춘다(런타임 의존은 DB 뿐) + - api restart: unless-stopped logging: driver: json-file options: { max-size: "10m", max-file: "5" } - # ── 발행 사이트 빌더 + 정적 서버 ──────────────────────────────────────── - # 발행 잡이 payload JSON 을 떨어뜨리면(SITE_PAYLOAD_DIR) 이 컨테이너가 그걸 보고 - # 정적 HTML 을 굽고, 같은 결과물을 그대로 서빙한다. - # - # ★ 왜 백엔드가 직접 굽지 않나: 굽는 데 Node 와 프론트 의존성이 필요하다. 파이썬 이미지에 - # 그걸 넣으면 백엔드가 프론트 빌드 도구를 떠안는다. payload 디렉토리를 사이에 두고 - # 백엔드는 파일만 쓰고, 여기는 파일만 읽는다 — 두 쪽이 서로를 모른다. + # 사장님 앱(:3000) + 발행 사이트 프리렌더·정적서버(:3001, `/s/*` 프록시) web: image: node:24-alpine container_name: o2o-web4ai-web - working_dir: /app/frontend/site - # 의존성이 없으면 설치부터 한다(최초 1회). 그 뒤 감시와 서버를 동시에 띄운다. - # ★ `&` 는 그 앞의 명령 전체를 배경으로 돌린다 — 앞에 붙인 `cd` 까지 함께 묶여 나가서 - # 뒤 명령이 엉뚱한 디렉토리에서 돈다(실측: /app/frontend 에서 스크립트를 찾다 실패). - # 그래서 cd 를 먼저 끝내고, 배경으로 보내는 것은 서버 하나뿐이다. + working_dir: /app command: - sh - -c - | - cd /app/frontend - # ★ `-d node_modules` 로 판단하면 안 된다. 익명 볼륨은 **빈 디렉토리로 이미 존재**해서 - # 설치를 건너뛰고 `vite: not found`(exit 127)로 죽는다. 실행 파일이 있는지를 본다. + cd /app + # ★ `-d node_modules` 로 판단하면 안 된다. 익명 볼륨은 빈 디렉토리로 이미 존재해서 + # 설치를 건너뛰고 `vite: not found`(exit 127)로 죽는다. [ -x node_modules/.bin/vite ] || npm install - # 외부 진입점은 admin Vite(:3000) 하나다. `/s/*`만 같은 컨테이너의 - # 정적 서버(:3001)로 프록시하고, payload 감시는 백그라운드에서 계속 돈다. - node site/scripts/serve-sites.mjs & - # 감시 프로세스가 기동 때 번들을 한 번 만들고, 그 뒤로는 바뀐 payload 만 굽는다. - # (예전에는 발행 한 건마다 vite 번들 + 전체 사이트를 다시 구웠다 — 사이트가 늘면 못 쓴다.) - node site/scripts/watch-payloads.mjs & - exec npm run dev -w admin + node solution/site/scripts/serve-sites.mjs & + node solution/site/scripts/watch-payloads.mjs & + exec npm run dev -w @o2o/front environment: PORT: 3001 - # 프리렌더가 루트에 `.txt` 를 굽는다. 백엔드와 같은 값이어야 한다. INDEXNOW_KEY: ${INDEXNOW_KEY:-} + # ★ 발행 호스트를 프론트 .env 에 따로 적지 않는다 — 루트 .env 의 SITE_PUBLIC_HOST 를 + # 그대로 흘려보낸다. 두 곳에 적으면 canonical 과 화면 주소가 조용히 갈라진다. + VITE_PUBLISH_HOST: ${SITE_PUBLIC_HOST:-w4ai.o2o.kr} volumes: - # 소스와 산출물을 통째로 마운트한다 — 발행 payload 가 호스트에 그대로 보여야 - # 개발 중 무슨 일이 일어났는지 파일로 확인할 수 있다. - - ./frontend:/app/frontend - # ★ node_modules 만은 컨테이너 것을 쓴다(익명 볼륨으로 마운트를 덮는다). - # 호스트가 macOS(arm64-darwin)라 그 안의 rollup·esbuild 네이티브 바이너리는 - # 리눅스 컨테이너에서 못 쓴다 — 실측: `Cannot find module '@rollup/rollup-linux-arm64-musl'` - # 로 프리렌더가 통째로 실패했고, 발행해도 사이트가 안 구워졌다. - - /app/frontend/node_modules - - /app/frontend/site/node_modules - - /app/frontend/admin/node_modules - # ★ 산출물만 named volume 으로 뺀다(./frontend 마운트 위에 덮인다). - # 호스트 경로에 두면 재배포로 코드를 갈아엎는 순간 out/ 이 비어 전 사이트가 404 다. - # nginx 가 같은 볼륨을 읽는다. - - site-out:/app/frontend/site/out + - ./package.json:/app/package.json + - ./package-lock.json:/app/package-lock.json + - ./tsconfig.base.json:/app/tsconfig.base.json + - ./solution:/app/solution + - ./admin:/app/admin + # ★ node_modules 는 컨테이너 것을 쓴다. 호스트가 macOS(arm64-darwin)라 그 안의 + # rollup·esbuild 네이티브 바이너리를 리눅스 컨테이너가 못 쓴다. + - /app/node_modules + - /app/solution/site/node_modules + - /app/solution/front/node_modules + - /app/admin/node_modules + # ★ 산출물은 named volume. 호스트 경로면 재배포로 코드를 갈아엎는 순간 전 사이트가 404 다. + - site-out:/app/solution/site/out ports: - # admin(:3000)이 공개 진입점. `/s/*`는 Vite proxy가 내부 :3001로 전달한다. - "3000:3000" restart: unless-stopped logging: driver: json-file options: { max-size: "10m", max-file: "5" } - # ── 발행 사이트 정적 서빙 ─────────────────────────────────────────────── - # - # ★ 굽는 쪽(o2o-web4ai-web)과 서빙하는 쪽을 분리한다. 프리렌더는 파일만 쓰고 여기는 파일만 - # 읽는다 — 볼륨 하나를 사이에 두고 서로를 모른다. - # - # ★ Azure Blob 으로 프록시하지 않는다. 파일이 이미 이 볼륨에 있는데 클라우드로 보냈다가 - # 되받아 오면 요청마다 왕복이 하나 더 붙고, Blob 은 커스텀 도메인 TLS 도 못 붙인다. - # Blob 업로드(azure_static)는 배달 백업으로 남겨 둔다. - nginx: - image: nginx:alpine - container_name: o2o-web4ai-nginx + # 내부 운영 앱(:3002). 사장님 번들과 갈라 두는 것이 이 서비스의 존재 이유다. + admin: + image: node:24-alpine + container_name: o2o-web4ai-admin + working_dir: /app + command: + - sh + - -c + - | + cd /app + [ -x node_modules/.bin/vite ] || npm install + exec npm run dev -w @o2o/admin + environment: + VITE_API_BASE_URL: ${ADMIN_API_BASE_URL:-http://localhost:9800} volumes: - - site-out:/srv/sites:ro - - ./nginx/site.conf:/etc/nginx/conf.d/default.conf:ro + - ./package.json:/app/package.json + - ./package-lock.json:/app/package-lock.json + - ./tsconfig.base.json:/app/tsconfig.base.json + - ./solution:/app/solution + - ./admin:/app/admin + - /app/node_modules + - /app/solution/site/node_modules + - /app/solution/front/node_modules + - /app/admin/node_modules ports: - - "80:80" - # TLS 를 붙이면 여기에 443 과 인증서 볼륨을 추가한다(certbot 또는 발급받은 인증서). - # - "443:443" + # ★ 운영에서는 내부망에만 연다. 0.0.0.0 으로 열면 앱을 가른 의미가 없다. + - "${ADMIN_BIND:-127.0.0.1}:3002:3002" depends_on: - web restart: unless-stopped @@ -233,24 +156,24 @@ services: driver: json-file options: { max-size: "10m", max-file: "5" } - # ── (예정) 크롤링 전용 워커 ────────────────────────────────────────────── - # Phase 1 은 collector 가 MockAdapter 만 등록하므로 브라우저가 필요 없다. - # 크롤링 법무 검토(docs/DECISIONS.md 1-1)가 끝나 HeadlessAdapter 를 붙이면 여기에 서비스를 하나 더 만든다. - # 선례는 o2o-negosium 의 lps-worker — 그대로 따라가면 된다: - # - # o2o-web4ai-crawler: - # build: { context: ./backend, dockerfile: Dockerfile.worker } # Chrome + Xvfb + 한글폰트 (~1.5GB) - # platform: linux/amd64 # google-chrome-stable(Linux)은 amd64 전용. arm64 맥에선 명시 없으면 빌드 실패 - # shm_size: "1gb" # Chrome 는 /dev/shm 을 많이 씀 — 부족하면 탭 크래시 - # environment: { DISPLAY: ":99" } - # - # API 이미지(~200MB)에 Chrome 을 넣으면 배포마다 1.5GB 를 밀게 되므로 반드시 이미지를 갈라야 한다. + # 발행 사이트 정적 서빙. 굽는 쪽(web)과 분리 — 볼륨 하나를 사이에 두고 서로를 모른다. + nginx: + image: nginx:alpine + container_name: o2o-web4ai-nginx + volumes: + - site-out:/srv/sites:ro + # ★ 클론 직후: cp nginx/site.conf.example nginx/site.conf + # 파일이 없으면 Docker 가 그 자리에 디렉토리를 만들어 nginx 가 설정 없이 뜬다. + - ./nginx/site.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "80:80" + depends_on: + - web + restart: unless-stopped + logging: + driver: json-file + options: { max-size: "10m", max-file: "5" } -# ── 볼륨 ────────────────────────────────────────────────────────────────── volumes: - # 발행 산출물. 프리렌더가 쓰고, 워커(색인 통보·Azure 업로드)와 nginx 가 읽는다. - # - # ★ `docker compose down` 으로는 지워지지 않는다. `down -v` 만 지운다. - # ★ 재생성물이라 백업 대상이 아니다 — 날아가도 payload 로 다시 굽는다(DEPLOY.md 1절). - # 지켜야 할 것은 DB 와 out/payloads 뿐이고, 그 둘은 여전히 호스트에 bind mount 다. + # ★ `down -v` 만 지운다. 재생성물이라 백업 대상이 아니다 — 날아가도 payload 로 다시 굽는다. site-out: diff --git a/docs/API_USAGE.md b/docs/API_USAGE.md index ccf1e3c..3e9f303 100644 --- a/docs/API_USAGE.md +++ b/docs/API_USAGE.md @@ -1,7 +1,7 @@ # API 사용 이력 · 비용 기록 **집계 시각: 2026-08-27 08:59 KST** -집계 대상: o2o-web4ai 프로젝트 전체 (backend · frontend/admin · frontend/site) +집계 대상: o2o-web4ai 프로젝트 전체 (backend · solution/front · solution/site) --- @@ -103,7 +103,7 @@ |---|---|---|---|---|---|---| | `4db7aca5` | 메인 (backend 주력) | 08-26 16:23 ~ 08-27 08:55 | 157 | 266,385 | 35,748,750 | **$32.65** | | `99be2a6c` | 메인 | 08-26 17:24 ~ 08-27 08:58 | 92 | 124,641 | 16,660,246 | **$19.79** | -| `19be092e` | 메인 (frontend/admin) | 08-27 08:42 ~ 08:58 | 29 | 25,903 | 2,476,298 | $3.37 | +| `19be092e` | 메인 (solution/front) | 08-27 08:42 ~ 08:58 | 29 | 25,903 | 2,476,298 | $3.37 | | `agent-a58…` | 서브에이전트 | 08-27 08:51 ~ 08:57 | 13 | 6,173 | 5,741,359 | $3.21 | | `agent-afa…` | 서브에이전트 | 08-27 08:52 ~ 08:55 | 14 | 423 | 6,132,381 | $3.21 | | `6052d29a` | 메인 (이 기록 작성 세션) | 08-27 08:52 ~ 08:58 | 21 | 23,936 | 1,678,581 | $2.92 | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ccdcbbd..903aaf7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -19,7 +19,7 @@ backend (Python) ──쓴다──▶ out/payloads/.json ◀──읽 **둘을 직접 붙이자는 제안은 이 문서를 근거로 거절한다** — 붙이는 순간 파이썬 프로세스가 React 를 렌더해야 하고, 그때부터 디자인 수정에 백엔드 배포가 필요해진다. -계약의 타입은 `frontend/shared/src/types/site-payload.ts` 하나다. +계약의 타입은 `solution/shared/src/types/site-payload.ts` 하나다. ## 2. 발행 파이프라인 @@ -87,82 +87,99 @@ nginx 는 `try_files $uri $uri/index.html =404` 로 같은 규칙을 맞춘다 **전 사이트가 동시에 내려간다.** 지금은 테스트 단계라 감수하는 리스크이고, **실사용 고객이 붙는 시점**이 위 표의 오른쪽으로 넘어가는 트리거다. -## 4. 앱 경계 — 지금 구조와 나눌 지점 +## 4. 앱 경계 — 두 앱, 한 백엔드 -### 지금 +**2026-08-31 실행 완료.** 아래는 "나눌 계획"이 아니라 지금 구조다. ``` -frontend/ - admin/ Vite CSR SPA ── /builder 사장님 위저드·에디터 (로그인 안 걸림) - │ └ /places, /places/:id/seo, /local-content - │ 내부 운영 화면 (RequireAuth) - site/ SSR 엔트리 + 프리렌더 → 정적 HTML 발행 사이트 - shared/ 타입·slug·디자인 토큰 (npm workspace) +o2o-web4ai/ +├─ solution/ 사장님 — 사이트 만들기·관리 +│ ├─ backend/ FastAPI + 워커 · 이 레포의 유일한 백엔드 +│ ├─ front/ 빌더 (위저드 + 에디터 + 발행 게이트) +│ ├─ site/ 발행 정적 사이트 (SSR 엔트리 + 프리렌더) +│ └─ shared/ front·site·백엔드 계약 (SitePayload · slug · 토큰) +│ +├─ admin/ 우리 — 전체 사이트 운영. **프론트 전용, 백엔드 없음** +│ +├─ docs/ nginx/ postgres-init/ docker-compose.yml package.json(워크스페이스 루트) ``` -**`admin/` 이 성격이 반대인 두 앱을 겸하고 있다.** ([PRODUCT.md 4절](PRODUCT.md) 표) +최상단은 **프로젝트 단위**로 평평하다. `frontend/` `backend/` 를 최상단 묶음 폴더로 쓰지 않는다 — +사내 다른 레포(`o2o-negosium`)가 이미 그 규약이고, 이 레포만 다르게 갈 이유가 없다. +negosium 대응: `negodata/{backend, front}` 가 프로젝트 안에서 f/b 를 가르는 선례, +`lps-admin/` 이 **백엔드 없이 프론트만 가진 최상단 폴더**의 선례다 — `admin/` 이 후자다. -### 나눠야 하는 이유 — 취향이 아니라 셋 다 실제 문제다 +### 왜 갈랐나 — 취향이 아니라 셋 다 실제 문제였다 -1. **내부 기능이 사장님 번들에 실려 나간다.** 한 앱이면 `/local-content`, `/places/:id/seo` +1. **내부 기능이 사장님 번들에 실려 나갔다.** 한 앱이면 `/local-content`, `/places/:id/seo` 같은 내부 라우트 이름과 화면 코드가 사장님 브라우저에 그대로 내려간다. - `UserRole.DEVELOPER` 는 코드 주석에 **"고객사에 존재를 노출하지 않는다"** 고 적혀 있는데, - 번들이 그 약속을 깨고 있다. 라우트 가드는 화면을 가리지 **번들은 못 가린다**. -2. **인증 모델이 정반대다.** 빌더는 일부러 로그인을 안 세운다("만들어 보기도 전에 막힌다"). - 운영 화면은 엄격해야 한다. 한 앱 안에서 두 정책을 유지하면 실수는 항상 **느슨한 쪽으로** 난다. -3. **배포 리듬이 다르다.** 사장님 화면은 조심스럽게, 내부 화면은 매일 고쳐도 된다. - 한 번들이면 내부 화면 수정 때문에 사장님 화면을 재배포한다. + `UserRole.DEVELOPER` 주석의 **"고객사에 존재를 노출하지 않는다"** 를 번들이 깨고 있었다. + 라우트 가드는 화면을 가리지 **번들은 못 가린다.** + ★ 이 문제는 **코드 크기와 무관하다.** 내부 화면이 814줄뿐이어도 내려가는 건 같다. +2. **인증 모델이 갈라진다.** 빌더는 일부러 로그인을 안 세운다("만들어 보기도 전에 막힌다"). + 내부 화면은 전부 `RequireAuth` 뒤다. 한 앱에서 두 정책을 유지하면 실수는 늘 **느슨한 쪽으로** 난다. + → 지금은 두 `provider.tsx` 가 그 차이를 각자 명시한다(사장님: 인증 실패를 삼킨다 / + 내부: 실패가 곧 차단). +3. **배포 리듬이 다르다.** 내부 화면을 고치려고 사장님 화면을 재배포하지 않는다. -### 권고 구조 — 레포는 하나, 배포물은 셋 +### admin 에 백엔드를 두지 않은 이유 + +내부 화면이 부르는 것이 전부 지금 백엔드에 이미 있다 — `useGetPlace` `useListPlaces` +`useListFacts` `useListLinks` `useGetSchema` `useConfirmLink` `useTransitionFact` +→ `router/v1/{place, fact, local, validator}`. 새로 만들 게 없고, 자체 백엔드를 두면 +`place`·`fact`·`link` 도메인을 **같은 DB 에 대고 두 번** 구현하게 된다. + +→ 대가: `solution/backend` 가 죽으면 admin 도 멈춘다. **내부 도구라 감수한다.** + +### 두 앱이 코드를 나눠 갖는 방식 — `@` 가 solution 을 가리킨다 + +`admin/vite.config.ts` 와 `tsconfig.json` 에서 **`@` 는 `solution/front/src`** 다. +admin 자기 파일만 `@admin` 이다. + +왜 복제하지 않았나: 내부 화면이 쓰는 API 클라이언트·UI 프리미티브·수집 배선이 solution 에 +한 벌만 있고, **그 파일들끼리도 `@/...` 로 서로를 부른다.** admin 에서 `@` 를 자기 src 로 +잡으면 그 내부 참조가 전부 깨진다(실측: `TS2307` 14건). 그리고 수집 배선은 +`RecollectPanel` 주석이 복제를 명시적으로 금지한다 — *"수집 경로를 두 벌 만들면 확정 게이트"* 가 +갈라진다. + +★ **의존 방향은 `admin` → `solution` 한 쪽뿐이다.** 이 방향이라 사장님 번들에는 내부 코드가 +섞이지 않는다. 반대 방향이 하나라도 생기면 앱을 가른 의미가 사라진다. + +### 백엔드는 쪼개지 않는다 + +마이크로서비스로 가르자는 제안은 지금 근거가 없다 — 팀 규모·트래픽 어느 쪽도 그 비용을 +정당화하지 못한다. 그리고 `router/v1/` 이 이미 도메인별로 갈려 있어 **나중에 진짜 나눠야 할 때 +그 선 따라 떨어진다** — 미룬다고 나중이 더 어려워지지 않는다. + +남은 일은 **라우터 표면을 청중별로 가르는 것**뿐이다: ``` -o2o-web4ai/ ← 레포 하나 (모노레포). 쪼개지 않는다 -├─ backend/ FastAPI 모듈러 모놀리스 + 워커 -├─ frontend/ -│ ├─ console/ (신설) 사장님용. 현 admin 의 /builder 계열 -│ ├─ admin/ (남김) 내부 운영용. 현 /places, /local-content, /seo -│ ├─ site/ (그대로) 발행 정적 사이트 -│ └─ shared/ (그대로) 세 앱이 공유하는 타입·slug·토큰 -└─ docs/ -``` - -**핵심: 소스는 합치고, 배포 단위만 쪼갠다.** 2026 기준 이 형태(모노레포 + 다중 배포물)가 -이 규모에서 기본값이다. - -**왜 레포를 안 쪼개나 — 이 프로젝트에는 특히 강한 이유가 있다.** -슬러그 규칙이 `site_payload.publish_slug()` 와 `shared/lib/slug.ts` **두 곳에 있고 같아야 한다.** -`SitePayload` 타입도 백엔드 출력과 프론트 입력이 짝이다. 한 레포에서는 이게 어긋나면 -**타입 에러·테스트 실패**로 잡힌다. 레포를 쪼개면 같은 실수가 **운영 404** 로 나타난다. -게다가 지금 만드는 건 세 개의 제품이 아니라 **한 파이프라인의 세 창구**다. - -**백엔드는 쪼개지 않는다.** 마이크로서비스로 가르자는 제안은 지금 근거가 없다 — -팀 규모·트래픽 어느 쪽도 그 비용을 정당화하지 못한다. 대신 **라우터 표면을 청중별로 가른다**: - -``` -/api/v1/... 사장님 (console) — 자기 리소스만 -/api/v1/admin/... 내부 (admin) — 라우터 레벨에서 role >= DEVELOPER 강제 +/api/v1/... 사장님 (solution/front) — 자기 리소스만 +/api/v1/admin/... 내부 (admin) — 라우터 레벨에서 role >= DEVELOPER 강제 ``` 엔드포인트마다 `if role >= ...` 를 흩뿌리지 않고 **의존성 하나로 라우터에 건다.** -지금 `common/authz.py is_owner_or_admin()` 이 그 단일 출처 역할을 하고 있으니, 그걸 라우터 -의존성으로 올리면 된다. +`common/authz.py is_owner_or_admin()` 이 그 단일 출처 역할을 하고 있으니 라우터 의존성으로 올린다. -### 마이그레이션 — 하루짜리, 되돌리기 쉬운 순서 +⚠️ 그 함수 이름의 **admin 은 `UserRole.OWNER`(고객사 최상위)** 를 뜻한다. 최상단 폴더 +`admin/`(우리 내부)과 **반대 뜻**이므로 읽을 때 헷갈리지 않는다. 폴더 이름을 admin 으로 정할 때 +알고 정한 충돌이다. -1. `frontend/console/` 생성 — `admin/` 의 `vite.config.ts`·`tsconfig.json`·`index.html` 복제 -2. `frontend/package.json` workspaces 에 `console` 추가, `dev:console`·`build:console` 스크립트 -3. `pages/BuilderPage.tsx` + `features/builder`, `features/onboarding`, `features/publish` → - `console/` 로 이동. `components/ui`·`lib`·`stores` 중 **양쪽이 쓰는 것은 `shared/` 로 올린다** -4. `admin/src/app/router.tsx` 에서 `/builder` 라우트 제거, `/places` 를 기본 진입으로 -5. `docker-compose.yml` 에 console 서비스 추가 (또는 nginx 에서 경로/서브도메인 분기) -6. 백엔드 `/api/v1/admin/*` 라우터 분리 + role 의존성 +### 왜 레포는 안 쪼개나 -★ 3번이 유일하게 시간이 드는 단계다. 공용으로 올릴 것과 한쪽 전용인 것을 가르는 작업이고, - 여기서 대충 하면 `shared/` 가 쓰레기통이 된다. +슬러그 규칙이 `site_payload.publish_slug()` 와 `solution/shared/src/lib/slug.ts` +**두 곳에 있고 같아야 한다.** `SitePayload`(252줄) 도 백엔드 출력과 프론트 입력이 짝이다. +한 레포에서는 어긋나면 **타입 에러·테스트 실패**로 잡히고, 레포를 쪼개면 같은 실수가 +**운영 404** 로 나타난다 — 배포 시점이 달라 언제 깨졌는지도 모른다. +지금 만드는 건 세 개의 제품이 아니라 **한 파이프라인의 세 창구**다. -**아직 실행하지 않았다.** 지금 구조로도 동작하고, 위 3가지 문제는 실사용 고객이 붙기 전까지는 -터지지 않는다. 다만 **사장님에게 계정을 열어주기 전에는 반드시 끝내야 한다** — 1번(번들 노출)이 -그때부터 실제 유출이 되기 때문이다. +### 아직 안 한 것 + +- `/api/v1/admin/*` 라우터 분리 + role 의존성 (위) +- 사장님 **"내 사이트 관리"** 화면. 이게 붙으면 빌더도 로그인 뒤로 들어간다 — + 그때 `solution/front` 의 인증 정책을 다시 본다. +- 운영 배포에서 `admin`(:3002)을 내부망에만 여는 것. compose 는 `ADMIN_BIND` 기본값을 + `127.0.0.1` 로 두었다. **0.0.0.0 으로 열면 앱을 가른 의미가 없다.** ## 5. 산출물 — 사이트 하나 = 한 장 diff --git a/docs/COLLECTION_SEO_AEO_FLOW.md b/docs/COLLECTION_SEO_AEO_FLOW.md index f79e814..98e7215 100644 --- a/docs/COLLECTION_SEO_AEO_FLOW.md +++ b/docs/COLLECTION_SEO_AEO_FLOW.md @@ -60,8 +60,8 @@ Perplexity 응답의 영업시간, 가격 같은 본문 정보는 사용하지 - `backend/services/external/perplexity.py` - `backend/services/prompts/channel_discovery.py` -- `frontend/admin/src/features/onboarding/useCollectFlow.ts` -- `frontend/admin/src/features/onboarding/ChannelConfirmPanel.tsx` +- `solution/front/src/features/onboarding/useCollectFlow.ts` +- `solution/front/src/features/onboarding/ChannelConfirmPanel.tsx` ## 4. 크롤링 @@ -170,7 +170,7 @@ Perplexity 응답의 영업시간, 가격 같은 본문 정보는 사용하지 - `backend/services/snapshot.py` - `backend/services/site_payload.py` - `backend/services/publish_gate.py` -- `frontend/site/` +- `solution/site/` ## 8. SEO 구현 @@ -190,10 +190,10 @@ Perplexity 응답의 영업시간, 가격 같은 본문 정보는 사용하지 관련 파일: -- `frontend/site/src/seo/head.ts` -- `frontend/site/src/seo/jsonld.ts` -- `frontend/site/src/seo/robots.ts` -- `frontend/site/src/seo/sitemap.ts` +- `solution/site/src/seo/head.ts` +- `solution/site/src/seo/jsonld.ts` +- `solution/site/src/seo/robots.ts` +- `solution/site/src/seo/sitemap.ts` ## 9. AEO 구현 @@ -212,9 +212,9 @@ JSON-LD 값은 화면에도 동일하게 존재해야 한다. 렌더러가 양 관련 파일: -- `frontend/site/src/seo/llms.ts` -- `frontend/site/src/seo/verify.ts` -- `frontend/site/src/sections/AnswerBlock.tsx` +- `solution/site/src/seo/llms.ts` +- `solution/site/src/seo/verify.ts` +- `solution/site/src/sections/AnswerBlock.tsx` - `backend/services/publish_gate.py` ## 10. SEO·AEO 진단 점수 @@ -248,7 +248,7 @@ AEO 항목: - `backend/services/seo_audit.py` - `backend/router/v1/site/site.py` -- `frontend/admin/src/pages/SeoAuditPage.tsx` +- `solution/front/src/pages/SeoAuditPage.tsx` API: diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 54659ce..34e590f 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -70,7 +70,7 @@ | 레이어 구조 | 원본 그대로 — `router` → `service` → `crud`, 람다 DB 실행, `Req_*`/`Res_*` 프로토콜, `RemoveNoneResponse` | "기존 컨벤션을 그대로 따른다" | | 포트 | **9800** | negosium 9300 / negodata 9400 / agent 9500 / lps 9600 / anchoring 9700 다음 번호 | | DB | `web4ai_db` (테스트 `web4ai_test_db`), 기존 로컬 postgres(`negosium-db` 컨테이너, 5432) 안의 **별도 database** | 원본과 같은 인스턴스·다른 DB. 스키마 네임스페이스 컨벤션 유지 | -| 마이그레이션 | Alembic 안 씀. `postgres-init/init-data/init.sql`(전체 DDL, 재실행 안전) + `postgres-init/alters/YYYY-MM-DD-<주제>.sql`(누적 ALTER) | 원본 방식 그대로 | +| 마이그레이션 | Alembic 안 씀. `postgres-init/init-data/init.sql` **한 벌**(전체 DDL, 재실행 안전) | 2026-08-31: 누적 ALTER 파일(`alters/`)을 없앴다. 아직 git·서버 어디에도 안 올라가 **보정할 기존 DB 가 없다** — init.sql 에 이미 전부 반영돼 있어 두 벌을 유지할 이유가 없었다. 운영 DB 가 생기는 순간 다시 필요해진다 | | 남긴 것 | config 로더 · 로거 · 싱글톤 · DB 세션 매니저(R/W 분리) · gmodel · gtime · authz · JWT/bcrypt dependencies · `company.companies`/`company.users` · auth 라우터 · 스케줄러 껍데기 · conftest(테스트 DB 자동 생성/삭제) | 전 모듈이 공통으로 쓰는 인프라. 인증은 places·facts·sites 전부가 `IsValidAccessToken` 에 의존한다 | | 뺀 것 | quotation · supplier · item · card · dashboard · statistics · learning · renegotiation · landing · admin · notification · LPS 연동 · anchoring · 초청메일(ACS/SMTP) · Azure Blob 클라이언트 | negodata 고유 도메인. Blob 클라이언트만 1-2 결론 후 media 모듈과 함께 재이식 예정 | | `companies` 테이블 유지 | 유지 | 보일러플레이트의 멀티테넌트 스코프 키(`UserInfo.company_id`)가 전 계층에 박혀 있다. 대행사/운영사 단위로 그대로 쓴다 | @@ -153,7 +153,7 @@ | 승인 | 후보 → 노출값, 옛 값 EXPIRED | `PUBLISHED_REPLACED` | | 수정(사람 직접) | 즉시 노출값 교체 | `PUBLISHED_REPLACED` | -마이그레이션: `postgres-init/alters/2026-08-27-fact-candidate-model.sql` +스키마: `postgres-init/init-data/init.sql` (`fact.facts` 활성 유니크 + 후보 상태) ### 5-2. 그 밖의 결정 diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 460a5fe..446e4d4 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -74,7 +74,7 @@ site-out/ 하면 정합이 맞는다. 하지만 Azure 는 발행 잡이 도는 사이트 하나씩만 올린다 — 그 짝을 맞추는 게 `backend/scripts/republish_all.py` 다. -**규칙: `frontend/site` 를 배포하면 반드시 전체 재굽기 + 전체 재업로드.** +**규칙: `solution/site` 를 배포하면 반드시 전체 재굽기 + 전체 재업로드.** ```bash docker compose restart web # 기동하며 전체 재굽기 diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 9870407..8d62ef0 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -33,7 +33,7 @@ | 원칙 | 왜 | 어디에 박혀 있나 | |---|---|---| -| **발행물은 정적 HTML** — 서버도 JS 실행도 없이 읽힌다 | 크롤러가 읽어야 존재하는 것이다 | `frontend/site` 프리렌더 | +| **발행물은 정적 HTML** — 서버도 JS 실행도 없이 읽힌다 | 크롤러가 읽어야 존재하는 것이다 | `solution/site` 프리렌더 | | **확인된 값만 발행한다** | 틀린 정보를 1차 출처로 만들면 제품이 해를 끼친다 | `publish_gate.py` 규칙 1 | | **고유 콘텐츠 0건이면 발행 거부** | 같은 템플릿 대량 생성은 검색엔진의 스팸 판정 대상 | `publish_gate.py` 규칙 2 | | **JSON-LD 값 = 화면 값** | 어긋나면 구조화 데이터 조작이다. 색인에서 통째로 불신당한다 | `publish_gate.py` 규칙 3, 빌드 실패 | diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 9cdd441..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -node_modules/ -dist/ -.ssr-dist/ -*.tsbuildinfo - -# 발행 산출물 — 빌드 잡이 매번 새로 굽는다. 커밋하지 않는다. -out/ - -# 환경변수: 실제 값은 커밋하지 않는다(.env.example 만 커밋). -.env -.env.* -!.env.example - -.DS_Store diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 3e64bc5..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "o2o-web4ai-frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "workspaces": [ - "shared", - "admin", - "site" - ], - "scripts": { - "dev": "npm run dev -w admin", - "dev:admin": "npm run dev -w admin", - "dev:site": "npm run dev -w site", - "build": "npm run build -w admin && npm run build -w site", - "build:admin": "npm run build -w admin", - "build:site": "npm run build -w site", - "prerender": "npm run prerender -w site", - "lint": "npm run lint -w admin && npm run lint -w site", - "orval": "npm run orval -w admin", - "clean": "rm -rf admin/dist site/dist site/.ssr-dist node_modules/.vite" - }, - "engines": { - "node": ">=20" - } -} diff --git a/nginx/site.conf.example b/nginx/site.conf.example new file mode 100644 index 0000000..9f086c1 --- /dev/null +++ b/nginx/site.conf.example @@ -0,0 +1,79 @@ +# 발행 사이트 정적 서빙. +# +# ★ 산출물은 named volume(site-out)으로 들어온다. 프리렌더 컨테이너가 쓰고 여기서 읽기만 한다 — +# 호스트 경로가 등장하지 않으므로 재배포로 코드를 갈아엎어도 사이트가 죽지 않는다. +# +# ★ 규칙은 하나뿐이다: 디렉토리 요청 → index.html. +# `/s/butter` (끝 슬래시 없음)가 사장님이 주소창에 치는 형태다. 이게 404 면 +# "발행했는데 안 나온다"가 된다. + +server { + listen 80; + listen [::]:80; + server_name _; + + # ★ nginx 이미지의 기본 문서 루트(/usr/share/nginx/html)를 쓰지 않는다. + # named volume 을 거기 마운트하면 Docker 가 **이미지에 들어 있던 index.html 을 + # 빈 볼륨으로 복사한다** — 그러면 오리진 루트가 "Welcome to nginx!" 를 띄우고, + # 그게 크롤러에 잡힌다. 빈 경로에 마운트하면 복사될 것이 없다. + root /srv/sites; + + # index 지시를 끈다. 오리진 루트에는 페이지가 없다(사이트는 /s/ 아래에 있다). + index index.html; + + charset utf-8; + server_tokens off; + + # 텍스트 산출물은 압축이 크게 먹는다(HTML 55KB → 10KB 안팎). + gzip on; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_vary on; + gzip_types + text/plain text/css text/xml + application/javascript application/json application/xml + image/svg+xml; + + # ── 공용 번들 ────────────────────────────────────────────── + # 파일명에 해시가 박혀 있다. 내용이 바뀌면 이름이 바뀌므로 영구 캐시가 안전하다. + # ★ expires 와 add_header 를 함께 쓰면 Cache-Control 헤더가 두 줄로 나간다. + # 합쳐서 읽히긴 하지만 의도가 흐려지므로 add_header 하나로 통일한다. + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + access_log off; + try_files $uri =404; + } + + location /fonts/ { + add_header Cache-Control "public, max-age=604800"; + access_log off; + try_files $uri =404; + } + + # ── 크롤러가 읽는 파일 ───────────────────────────────────── + # 발행하면 곧바로 반영되어야 한다. 길게 캐시하면 새 사업장이 사이트맵에 들어가도 + # 크롤러가 옛 파일을 계속 본다. + location = /robots.txt { + add_header Cache-Control "public, max-age=300, must-revalidate"; + } + + location = /sitemap.xml { + add_header Cache-Control "public, max-age=300, must-revalidate"; + } + + # ── 발행 사이트 ──────────────────────────────────────────── + location / { + # $uri/ 를 거치면 nginx 가 끝 슬래시로 301 을 내보낸다. 크롤러가 리다이렉트를 + # 한 번 더 타야 하므로 index.html 을 바로 준다. + try_files $uri $uri/index.html =404; + add_header Cache-Control "public, max-age=300, must-revalidate"; + } + + # 발행되지 않은 주소. 사장님이 오타를 냈을 때 흰 화면 대신 이유를 보여준다. + error_page 404 /404.html; + location = /404.html { + internal; + return 404 '페이지를 찾을 수 없습니다

    페이지를 찾을 수 없습니다

    주소를 다시 확인해 주세요.

    '; + add_header Content-Type "text/html; charset=utf-8"; + } +} diff --git a/frontend/package-lock.json b/package-lock.json similarity index 99% rename from frontend/package-lock.json rename to package-lock.json index e766b4e..a42872d 100644 --- a/frontend/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { - "name": "o2o-web4ai-frontend", + "name": "o2o-web4ai", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "o2o-web4ai-frontend", + "name": "o2o-web4ai", "version": "0.0.0", "workspaces": [ - "shared", - "admin", - "site" + "solution/shared", + "solution/front", + "solution/site", + "admin" ], "engines": { "node": ">=20" @@ -20,26 +21,18 @@ "name": "@o2o/admin", "version": "0.0.0", "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@hookform/resolvers": "^5.4.0", "@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", - "motion": "^12.23.24", "react": "^19.0.1", "react-dom": "^19.0.1", - "react-hook-form": "^7.79.0", "react-router": "^7.17.0", "sonner": "^2.0.7", - "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0", - "zod": "^4.4.3", - "zustand": "^5.0.14" + "tailwind-merge": "^3.0.1", + "zustand": "^5.0.2" }, "devDependencies": { "@types/node": "^22.14.0", @@ -48,7 +41,6 @@ "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^7.1.1", - "orval": "^7.3.0", "tailwindcss": "^4.1.14", "typescript": "~5.8.2", "typescript-eslint": "^8.45.0", @@ -1461,12 +1453,16 @@ "resolved": "admin", "link": true }, + "node_modules/@o2o/front": { + "resolved": "solution/front", + "link": true + }, "node_modules/@o2o/shared": { - "resolved": "shared", + "resolved": "solution/shared", "link": true }, "node_modules/@o2o/site": { - "resolved": "site", + "resolved": "solution/site", "link": true }, "node_modules/@orval/angular": { @@ -8756,12 +8752,89 @@ "shared": { "name": "@o2o/shared", "version": "0.0.0", + "extraneous": true, "dependencies": { "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" } }, "site": { + "name": "@o2o/site", + "version": "0.0.0", + "extraneous": true, + "dependencies": { + "@o2o/shared": "*", + "@tailwindcss/vite": "^4.1.14", + "clsx": "^2.1.1", + "embla-carousel-react": "^8.6.0", + "lucide-react": "^0.546.0", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "react-router": "^7.17.0", + "tailwind-merge": "^3.6.0" + }, + "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", + "vitest": "^4.1.11" + } + }, + "solution/front": { + "name": "@o2o/front", + "version": "0.0.0", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^5.4.0", + "@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", + "motion": "^12.23.24", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "react-hook-form": "^7.79.0", + "react-router": "^7.17.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" + }, + "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", + "orval": "^7.3.0", + "tailwindcss": "^4.1.14", + "typescript": "~5.8.2", + "typescript-eslint": "^8.45.0", + "vite": "^6.2.3" + } + }, + "solution/shared": { + "name": "@o2o/shared", + "version": "0.0.0", + "dependencies": { + "clsx": "^2.1.1", + "tailwind-merge": "^3.6.0" + } + }, + "solution/site": { "name": "@o2o/site", "version": "0.0.0", "dependencies": { diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa5ac99 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "o2o-web4ai", + "private": true, + "version": "0.0.0", + "type": "module", + "workspaces": [ + "solution/shared", + "solution/front", + "solution/site", + "admin" + ], + "scripts": { + "dev": "npm run dev -w @o2o/front", + "dev:front": "npm run dev -w @o2o/front", + "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: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/front/dist admin/dist solution/site/dist solution/site/.ssr-dist node_modules/.vite" + }, + "engines": { + "node": ">=20" + } +} diff --git a/postgres-init/alters/2026-08-27-external-place-id.sql b/postgres-init/alters/2026-08-27-external-place-id.sql deleted file mode 100644 index a7acd9a..0000000 --- a/postgres-init/alters/2026-08-27-external-place-id.sql +++ /dev/null @@ -1,41 +0,0 @@ --- 사업장의 외부 장소 식별자를 카카오 전용에서 **소스 중립**으로 바꾼다. --- 신규 DB 는 init-data/init.sql 에 반영돼 있어 이 파일이 필요 없다(멱등이라 실행해도 무해). --- 적용: psql -h -p -U -f postgres-init/alters/2026-08-27-external-place-id.sql --- --- 왜 바꾸나 --- 카카오 REST 키가 없어 네이버 지역검색으로 동일 업소를 검증하는데, **네이버에는 카카오의 --- kakao_place_id 같은 안정적 고유 키가 없다**(응답의 link 는 업체 홈페이지지 플레이스 URL 이 아니다). --- 그대로 두면 네이버로 검증한 사업장은 external id 가 NULL 이라 중복 등록 방지 유니크가 걸리지 않고, --- 같은 가게를 두 번 등록해도 막히지 않는다. --- --- → 식별자 컬럼을 소스 중립으로 바꾸고(external_source + external_place_id), --- 고유 키가 없는 소스를 위해 **(회사, 상호명, 도로명주소)** 대체 유니크를 둔다. --- 도로명주소만으로는 안 된다 — 한 건물에 여러 가게가 있다. - -\connect web4ai_db - -ALTER TABLE place.places - RENAME COLUMN kakao_place_id TO external_place_id; - -ALTER TABLE place.places - ADD COLUMN IF NOT EXISTS external_source SMALLINT NULL; -- ExternalPlaceSource: 1=kakao 2=naver - --- 기존 값은 전부 카카오에서 온 것이다. -UPDATE place.places SET external_source = 1 - WHERE external_place_id IS NOT NULL AND external_source IS NULL; - -DROP INDEX IF EXISTS place.uq_places_company_kakao; - --- 고유 키가 있는 소스(카카오): 소스 + 외부 id 로 중복을 막는다. -CREATE UNIQUE INDEX IF NOT EXISTS uq_places_company_external - ON place.places (company_id, external_source, external_place_id) - WHERE deleted = FALSE AND external_place_id IS NOT NULL; - --- 고유 키가 없는 소스(네이버): 상호명 + 도로명주소로 막는다. --- ★ 도로명주소만 쓰면 안 된다 — 한 건물에 카페와 식당이 같이 있다. -CREATE UNIQUE INDEX IF NOT EXISTS uq_places_company_name_address - ON place.places (company_id, name, road_address) - WHERE deleted = FALSE AND road_address IS NOT NULL AND external_place_id IS NULL; - -CREATE INDEX IF NOT EXISTS idx_places_external ON place.places (external_source, external_place_id) - WHERE deleted = FALSE AND external_place_id IS NOT NULL; diff --git a/postgres-init/alters/2026-08-27-fact-candidate-model.sql b/postgres-init/alters/2026-08-27-fact-candidate-model.sql deleted file mode 100644 index 6aa9a06..0000000 --- a/postgres-init/alters/2026-08-27-fact-candidate-model.sql +++ /dev/null @@ -1,33 +0,0 @@ --- fact 를 '노출값 1건 + 후보 N건' 모델로 바꾼다 — 재수집(업데이트) 프로세스를 담기 위함. --- 신규 DB 는 init-data/init.sql 에 이미 반영돼 있어 이 파일이 필요 없다(멱등이라 실행해도 무해). --- 적용: psql -h -p -U -f postgres-init/alters/2026-08-27-fact-candidate-model.sql --- --- 왜 바꾸나 --- 기존: 활성 유니크가 status IN (1,2,3,4) 라 (사업장,단위,key) 당 살아있는 fact 가 1건뿐이었다. --- → 재수집이 오면 확인된 노출값을 반드시 밀어내야 했고, 그 순간 사이트에서 사실이 사라졌다. --- → 값이 그대로여도 검증(VERIFIED)이 초기화됐다. --- 변경: 유니크를 **노출 상태(3=VERIFIED, 4=CORRECTED)에만** 건다. --- → 사이트에 나가는 값은 여전히 1건(두 값으로 갈라지지 않는다) --- → 후보(1=UNVERIFIED, 2=PENDING_OWNER)는 여러 건 공존 가능 --- → 재수집은 노출값을 건드리지 않고 후보로 쌓이고, 사람이 승인할 때 교체된다 - -\connect web4ai_db - -DROP INDEX IF EXISTS fact.uq_facts_place_key; -DROP INDEX IF EXISTS fact.uq_facts_unit_key; - --- 노출값은 (사업장, 단위, key) 당 1건. 후보·이력은 제외한다. -CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_published_place_key ON fact.facts (place_id, key) - WHERE deleted = FALSE AND unit_id IS NULL AND status IN (3, 4); -CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_published_unit_key ON fact.facts (place_id, unit_id, key) - WHERE deleted = FALSE AND unit_id IS NOT NULL AND status IN (3, 4); - --- 후보 조회 경로(사람 확인 큐) — 재수집이 올려놓은 대기 항목을 훑는다. -CREATE INDEX IF NOT EXISTS idx_facts_candidate ON fact.facts (place_id, key, source_type) - WHERE deleted = FALSE AND status IN (1, 2); - --- 노출값이 실제로 바뀐 시각. ★ 개별 재빌드 대상 판별용 — --- site_versions.built_at < places.content_updated_at 인 사이트만 다시 빌드하면 된다. --- (사이트 1,000개에서 전체 재빌드는 못 쓴다.) -ALTER TABLE place.places - ADD COLUMN IF NOT EXISTS content_updated_at TIMESTAMPTZ NULL; diff --git a/postgres-init/alters/2026-08-27-local-content-publishing.sql b/postgres-init/alters/2026-08-27-local-content-publishing.sql deleted file mode 100644 index af6bc4d..0000000 --- a/postgres-init/alters/2026-08-27-local-content-publishing.sql +++ /dev/null @@ -1,10 +0,0 @@ -ALTER TABLE local.local_contents - ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 1, - ADD COLUMN IF NOT EXISTS published_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS published_by uuid NULL, - ADD COLUMN IF NOT EXISTS display_start_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS display_end_at TIMESTAMPTZ NULL; - -CREATE INDEX IF NOT EXISTS idx_local_contents_status - ON local.local_contents (status, collected_at DESC) - WHERE deleted = false; diff --git a/postgres-init/alters/2026-08-27-site-template.sql b/postgres-init/alters/2026-08-27-site-template.sql deleted file mode 100644 index 46d422f..0000000 --- a/postgres-init/alters/2026-08-27-site-template.sql +++ /dev/null @@ -1,12 +0,0 @@ --- 사이트 템플릿 저장. --- --- 위저드 4단계에서 사장님이 고른 템플릿이 브라우저 상태로만 남아 있었다. --- 그래서 발행 잡(services/site_payload)이 그 값을 읽을 곳이 없어 업종 기본 템플릿으로 --- 굽고 있었고, 사장님이 고른 디자인과 실제 발행본이 달랐다. --- --- template_id 는 프론트의 배리에이션 레지스트리 키다(예: 'stay-o2o-editorial'). --- 서버는 값을 해석하지 않고 그대로 보관·반환만 한다 — 템플릿 목록은 프론트가 소유한다. -ALTER TABLE site.sites ADD COLUMN IF NOT EXISTS template_id varchar(100); - -COMMENT ON COLUMN site.sites.template_id IS - '사장님이 고른 템플릿 키. NULL 이면 업종 기본 템플릿으로 굽는다.'; diff --git a/postgres-init/alters/2026-08-28-allow-duplicate-places.sql b/postgres-init/alters/2026-08-28-allow-duplicate-places.sql deleted file mode 100644 index 3b11f7d..0000000 --- a/postgres-init/alters/2026-08-28-allow-duplicate-places.sql +++ /dev/null @@ -1,3 +0,0 @@ --- 한 사용자가 같은 실제 업장을 여러 사업장 프로젝트로 등록할 수 있게 한다. -DROP INDEX IF EXISTS place.uq_places_company_external; -DROP INDEX IF EXISTS place.uq_places_company_name_address; diff --git a/postgres-init/alters/2026-08-28-site-theme.sql b/postgres-init/alters/2026-08-28-site-theme.sql deleted file mode 100644 index bdf39f1..0000000 --- a/postgres-init/alters/2026-08-28-site-theme.sql +++ /dev/null @@ -1,23 +0,0 @@ --- 사이트 테마(색·서체·섹션) 저장. --- --- 관리자 에디터에서 사장님이 바꿀 수 있는 디자인은 6가지다: --- 템플릿 · 섹션 on/off · 섹션 순서 · 섹션별 배리에이션 · 색 · 서체 --- 그중 저장되는 자리가 있던 건 template_id 하나뿐이었다. 나머지 다섯은 브라우저 메모리에만 있다가 --- 새로고침하면 사라졌고, 발행 잡(services/site_payload)은 읽을 곳이 없어 업종 기본 표를 그대로 구웠다. --- 그래서 사장님이 섹션을 끄고 순서를 바꿔도 발행본은 언제나 업종 기본 모양으로 나갔다 — --- template_id 컬럼이 생긴 것과 정확히 같은 이유이고, 남은 다섯을 담을 자리가 이 컬럼이다. --- --- ★ 왜 컬럼 5개가 아니라 jsonb 1개인가 --- 섹션 목록·배리에이션 키·색 토큰 이름은 전부 프론트(배리에이션 레지스트리)가 소유한다. --- 관계형 컬럼으로 펼치면 프론트가 섹션이나 색 토큰을 하나 늘릴 때마다 마이그레이션이 따라와야 한다. --- 서버는 이 값을 해석하지 않고 보관·반환만 하므로 통째로 담는 것이 맞다. --- --- ★ templateId 는 여기 넣지 않는다. --- 이미 sites.template_id 컬럼이 있다. 두 곳에 두면 어느 쪽이 진짜인지 갈리고, --- uq/조회가 걸린 쪽(컬럼)과 payload 가 읽는 쪽(jsonb)이 어긋나는 순간 화면과 발행본이 달라진다. -ALTER TABLE site.sites ADD COLUMN IF NOT EXISTS theme jsonb NULL; - -COMMENT ON COLUMN site.sites.theme IS - '에디터가 정한 디자인. {"colors":{...},"fontStyle":"...","sections":[{"id","name","enabled","locked","variantId"}]} ' - '— 서버는 해석하지 않고 그대로 보관·반환한다(목록은 프론트가 소유). ' - 'NULL 이면 발행 잡이 업종 기본 색·서체·섹션으로 굽는다. templateId 는 sites.template_id 가 소유한다.'; diff --git a/frontend/README.md b/solution/README.md similarity index 90% rename from frontend/README.md rename to solution/README.md index 8c1c72c..71e03b7 100644 --- a/frontend/README.md +++ b/solution/README.md @@ -1,4 +1,4 @@ -# o2o-web4ai frontend +# solution — 사장님 앱 (backend · front · site) 소상공인 홈페이지 자동 생성 솔루션의 프론트엔드. `o2o-negosium/negodata/front` 보일러플레이트를 이식했다 — 빌드 도구·구조·프로토콜 규약은 원본과 같다. @@ -6,7 +6,7 @@ **목표는 예쁜 사이트가 아니라 AI 검색이 이 가게를 공식 홈페이지 기준으로 설명하게 만드는 것**이다 (백엔드 README 와 같은 문장). 그래서 두 앱의 요구사항이 정반대다. -| | `admin/` (빌더) | `site/` (발행 사이트) | +| | `front/` (빌더) | `site/` (발행 사이트) | |---|---|---| | 사용자 | 사장님 · 운영자 | 손님 · **검색/AI 크롤러** | | 렌더링 | CSR SPA | **SSG (정적 HTML)** | @@ -14,14 +14,17 @@ | 런타임 | Vite dev / 정적 호스팅 | **서버 없음.** 파일만 | | 데이터 | 편집 중 상태(미확인 값 포함) | `SitePayload` — **확인된 값만** | -한 레포에 두 앱을 두되 빌드를 나눈 이유가 이 표다. 보일러플레이트는 순수 CSR 이라 +한 프로젝트에 두 앱을 두되 빌드를 나눈 이유가 이 표다. 보일러플레이트는 순수 CSR 이라 발행 사이트에 그대로 쓰면 크롤러가 `
    ` 만 읽고 떠난다. ``` -frontend/ -├── shared/ 두 앱이 공유 — 도메인 enum · SitePayload 계약 · 디자인 토큰 · fact 필터 -├── admin/ 빌더 (위저드 4단계 + 실시간 에디터 + 발행 게이트) +solution/ +├── backend/ FastAPI + 워커. payload JSON 만 떨어뜨린다 +├── shared/ front·site 가 공유 — 도메인 enum · SitePayload 계약 · 디자인 토큰 · fact 필터 +├── front/ 빌더 (위저드 4단계 + 실시간 에디터 + 발행 게이트) └── site/ 발행 사이트 렌더러 + 프리렌더 스크립트 + +내부 운영 화면은 여기 없다 — 최상단 `admin/` 이다(ARCHITECTURE.md 4절). ``` ## 실행 @@ -34,8 +37,8 @@ docker compose up -d - 발행 사이트: `http://localhost:3000/s/` - API/Swagger: `http://localhost:9800/docs` -- 정적 사이트 저장 위치: `frontend/site/out/s//` -- 프리렌더 입력 payload: `frontend/site/payloads/.json` +- 정적 사이트 저장 위치: `solution/site/out/s//` +- 프리렌더 입력 payload: `solution/site/payloads/.json` - 컨테이너 내부 정적 서버는 3001을 사용하지만, Docker가 호스트 3000으로만 공개한다. 예: `http://localhost:3000/s/grazz` @@ -118,7 +121,7 @@ API 클라이언트는 **전부 orval 생성물**이다(React Query 훅). 화면 npm run orval -w admin # 백엔드가 떠 있을 때 cd ../backend && python scripts/export_openapi.py # 서버 없이 — 스펙을 먼저 뽑고 -cd ../frontend && ORVAL_INPUT=../../backend/openapi.json npm run orval -w admin +cd .. && ORVAL_INPUT=solution/backend/openapi.json npm run orval ``` 열려 있는 도메인은 `auth` / `place` / `fact` / `job` / `site` 전부다. diff --git a/backend/.dockerignore b/solution/backend/.dockerignore similarity index 100% rename from backend/.dockerignore rename to solution/backend/.dockerignore diff --git a/backend/.env.example b/solution/backend/.env.example similarity index 97% rename from backend/.env.example rename to solution/backend/.env.example index 7f87406..a954f97 100644 --- a/backend/.env.example +++ b/solution/backend/.env.example @@ -24,7 +24,7 @@ COLLECT_USE_PERPLEXITY=0 # ── 발행 산출물 ──────────────────────────────────────────── # BUILD 잡이 발행 payload JSON 을 떨어뜨릴 디렉토리. 없으면 만든다. -# 이 JSON 하나가 정적 렌더러(frontend/site)의 유일한 입력이다 — +# 이 JSON 하나가 정적 렌더러(solution/site)의 유일한 입력이다 — # npm run prerender -- --payload=<이 디렉토리 또는 그 안의 파일> → out//index.html # 컨테이너 밖(볼륨·오브젝트 스토리지)으로 빼기 쉬우라고 코드가 아니라 env 로 둔다. # ★ 쓰기에 실패해도 발행은 진행된다(경고 로그만). 발행 기록은 DB 가 진실이다. diff --git a/backend/Dockerfile b/solution/backend/Dockerfile similarity index 100% rename from backend/Dockerfile rename to solution/backend/Dockerfile diff --git a/backend/README.md b/solution/backend/README.md similarity index 99% rename from backend/README.md rename to solution/backend/README.md index d0b922f..9ceef45 100644 --- a/backend/README.md +++ b/solution/backend/README.md @@ -7,7 +7,7 @@ ## 현재 상태 -**수집 → 생성 → 빌드 → 발행 파이프라인이 API 로 다 열렸다.** 프론트(`frontend/admin`)가 이 API 를 붙여 쓴다. +**수집 → 생성 → 빌드 → 발행 파이프라인이 API 로 다 열렸다.** 프론트(`solution/front`)가 이 API 를 붙여 쓴다. | 모듈 | 상태 | 역할 | |---|---|---| diff --git a/backend/common/authz.py b/solution/backend/common/authz.py similarity index 100% rename from backend/common/authz.py rename to solution/backend/common/authz.py diff --git a/backend/common/category_schema/__init__.py b/solution/backend/common/category_schema/__init__.py similarity index 100% rename from backend/common/category_schema/__init__.py rename to solution/backend/common/category_schema/__init__.py diff --git a/backend/common/category_schema/loader.py b/solution/backend/common/category_schema/loader.py similarity index 100% rename from backend/common/category_schema/loader.py rename to solution/backend/common/category_schema/loader.py diff --git a/backend/common/category_schema/resources/cafe.json b/solution/backend/common/category_schema/resources/cafe.json similarity index 100% rename from backend/common/category_schema/resources/cafe.json rename to solution/backend/common/category_schema/resources/cafe.json diff --git a/backend/common/category_schema/resources/lodging.json b/solution/backend/common/category_schema/resources/lodging.json similarity index 100% rename from backend/common/category_schema/resources/lodging.json rename to solution/backend/common/category_schema/resources/lodging.json diff --git a/backend/common/category_schema/resources/restaurant.json b/solution/backend/common/category_schema/resources/restaurant.json similarity index 100% rename from backend/common/category_schema/resources/restaurant.json rename to solution/backend/common/category_schema/resources/restaurant.json diff --git a/backend/common/category_schema/resources/tour_activity.json b/solution/backend/common/category_schema/resources/tour_activity.json similarity index 100% rename from backend/common/category_schema/resources/tour_activity.json rename to solution/backend/common/category_schema/resources/tour_activity.json diff --git a/backend/common/cost.py b/solution/backend/common/cost.py similarity index 100% rename from backend/common/cost.py rename to solution/backend/common/cost.py diff --git a/backend/common/database/db_session_manager.py b/solution/backend/common/database/db_session_manager.py similarity index 100% rename from backend/common/database/db_session_manager.py rename to solution/backend/common/database/db_session_manager.py diff --git a/backend/common/database/model/models.py b/solution/backend/common/database/model/models.py similarity index 100% rename from backend/common/database/model/models.py rename to solution/backend/common/database/model/models.py diff --git a/backend/common/enums.py b/solution/backend/common/enums.py similarity index 100% rename from backend/common/enums.py rename to solution/backend/common/enums.py diff --git a/backend/common/logger.py b/solution/backend/common/logger.py similarity index 100% rename from backend/common/logger.py rename to solution/backend/common/logger.py diff --git a/backend/common/models/gmodel.py b/solution/backend/common/models/gmodel.py similarity index 100% rename from backend/common/models/gmodel.py rename to solution/backend/common/models/gmodel.py diff --git a/backend/common/singleton.py b/solution/backend/common/singleton.py similarity index 100% rename from backend/common/singleton.py rename to solution/backend/common/singleton.py diff --git a/backend/common/utils/gtime.py b/solution/backend/common/utils/gtime.py similarity index 100% rename from backend/common/utils/gtime.py rename to solution/backend/common/utils/gtime.py diff --git a/backend/config/config.local.toml.example b/solution/backend/config/config.local.toml.example similarity index 100% rename from backend/config/config.local.toml.example rename to solution/backend/config/config.local.toml.example diff --git a/solution/backend/config/config.test.toml.example b/solution/backend/config/config.test.toml.example new file mode 100644 index 0000000..7c3d7d5 --- /dev/null +++ b/solution/backend/config/config.test.toml.example @@ -0,0 +1,51 @@ +# 복사해서 사용: cp config.test.toml.example config.test.toml +# 실제 config.test.toml 은 커밋하지 않는다(.gitignore: *.toml). +# +# ★ 이 파일이 없으면 `.venv/bin/pytest` 가 conftest import 단계에서 죽는다 +# (server_configs 가 config..toml 을 읽는다). 그래서 템플릿을 둔다. +# ★ DB 이름을 dev 와 절대 같게 두지 말 것 — 픽스처가 TRUNCATE 를 돌린다. +# conftest 의 안전가드가 이름을 확인하지만, 여기서부터 갈라 두는 게 먼저다. +[WebServerConfig] +server_name = "O2oSiteServerTest" +port = 9800 +process_count = 1 +is_ssl = false +is_test = true +client_url = "http://localhost:3000" +landing_url = "" + +[LogConfig] +print_console = false +log_level = "warning" + +[MainDBConfig] +db_type = "postgresql" +name = "web4ai_test_db" # ★ dev(web4ai_db)와 다른 DB. 픽스처가 매번 지웠다 만든다 +write_host = "127.0.0.1" +write_port = 5432 +write_id = "postgres" +write_pw = "password" +read_host = "127.0.0.1" +read_port = 5432 +read_id = "postgres" +read_pw = "password" +show_log = false +pool_size = 5 +max_overflow = 10 +sslmode = "" + +[JwtToken] +access_key = "test-access-key-not-a-secret" +refresh_key = "test-refresh-key-not-a-secret" +access_expire_min = 30 +refresh_expire_day = 7 + +# ★ 전부 빈값으로 둔다. APP_ENV=test 는 .env 를 읽지 않는데(실키가 테스트로 새는 경로 차단), +# 여기에 실키를 적으면 그 차단을 우회해 외부 API 요금이 나간다. +[ExternalApiConfig] +perplexity_api_key = "" +kakao_rest_api_key = "" +naver_client_id = "" +naver_client_secret = "" +gemini_api_key = "" +tour_api_key = "" diff --git a/backend/config/config_loader.py b/solution/backend/config/config_loader.py similarity index 100% rename from backend/config/config_loader.py rename to solution/backend/config/config_loader.py diff --git a/backend/config/config_models.py b/solution/backend/config/config_models.py similarity index 100% rename from backend/config/config_models.py rename to solution/backend/config/config_models.py diff --git a/backend/config/server_configs.py b/solution/backend/config/server_configs.py similarity index 100% rename from backend/config/server_configs.py rename to solution/backend/config/server_configs.py diff --git a/backend/conftest.py b/solution/backend/conftest.py similarity index 99% rename from backend/conftest.py rename to solution/backend/conftest.py index 3e86764..d980cfa 100644 --- a/backend/conftest.py +++ b/solution/backend/conftest.py @@ -173,7 +173,7 @@ async def auth_headers(db_engine, client, company_id): # ── 렌더러 스텁 ────────────────────────────────────────────────────────────── @pytest.fixture(autouse=True) def fake_renderer(monkeypatch, tmp_path_factory): - """정적 렌더러(frontend/site) 대역. + """정적 렌더러(solution/site) 대역. ★ 왜 필요한가 발행 게이트는 이제 **실제로 나갈 HTML** 을 보고 판정한다. 그 HTML 은 Node 렌더러가 @@ -182,7 +182,7 @@ def fake_renderer(monkeypatch, tmp_path_factory): 대역을 끼운다 — 여기서 검사하려는 건 **백엔드가 보고서를 어떻게 처리하는가** 다. ★ 구조화 데이터 ↔ 화면 값 대조 자체는 렌더러 쪽 테스트가 본다 - (frontend/site/src/seo/verify.test.ts). 그 규칙을 여기서 다시 구현하지 않는다 — + (solution/site/src/seo/verify.test.ts). 그 규칙을 여기서 다시 구현하지 않는다 — 두 벌로 두면 어긋나고, 어긋난 걸 아무도 모르는 게 원래 문제였다. """ import json diff --git a/backend/crud/fact_crud.py b/solution/backend/crud/fact_crud.py similarity index 100% rename from backend/crud/fact_crud.py rename to solution/backend/crud/fact_crud.py diff --git a/backend/crud/faq_crud.py b/solution/backend/crud/faq_crud.py similarity index 100% rename from backend/crud/faq_crud.py rename to solution/backend/crud/faq_crud.py diff --git a/backend/crud/job_crud.py b/solution/backend/crud/job_crud.py similarity index 100% rename from backend/crud/job_crud.py rename to solution/backend/crud/job_crud.py diff --git a/backend/crud/local_content_crud.py b/solution/backend/crud/local_content_crud.py similarity index 100% rename from backend/crud/local_content_crud.py rename to solution/backend/crud/local_content_crud.py diff --git a/backend/crud/media_crud.py b/solution/backend/crud/media_crud.py similarity index 100% rename from backend/crud/media_crud.py rename to solution/backend/crud/media_crud.py diff --git a/backend/crud/place_crud.py b/solution/backend/crud/place_crud.py similarity index 100% rename from backend/crud/place_crud.py rename to solution/backend/crud/place_crud.py diff --git a/backend/crud/site_crud.py b/solution/backend/crud/site_crud.py similarity index 100% rename from backend/crud/site_crud.py rename to solution/backend/crud/site_crud.py diff --git a/backend/crud/user_crud.py b/solution/backend/crud/user_crud.py similarity index 100% rename from backend/crud/user_crud.py rename to solution/backend/crud/user_crud.py diff --git a/backend/demo_site.html b/solution/backend/demo_site.html similarity index 100% rename from backend/demo_site.html rename to solution/backend/demo_site.html diff --git a/backend/pytest.ini b/solution/backend/pytest.ini similarity index 100% rename from backend/pytest.ini rename to solution/backend/pytest.ini diff --git a/backend/requirements.txt b/solution/backend/requirements.txt similarity index 100% rename from backend/requirements.txt rename to solution/backend/requirements.txt diff --git a/backend/router/router.py b/solution/backend/router/router.py similarity index 100% rename from backend/router/router.py rename to solution/backend/router/router.py diff --git a/backend/router/v1/auth/account.py b/solution/backend/router/v1/auth/account.py similarity index 100% rename from backend/router/v1/auth/account.py rename to solution/backend/router/v1/auth/account.py diff --git a/backend/router/v1/auth/protocol.py b/solution/backend/router/v1/auth/protocol.py similarity index 100% rename from backend/router/v1/auth/protocol.py rename to solution/backend/router/v1/auth/protocol.py diff --git a/backend/router/v1/fact/fact.py b/solution/backend/router/v1/fact/fact.py similarity index 100% rename from backend/router/v1/fact/fact.py rename to solution/backend/router/v1/fact/fact.py diff --git a/backend/router/v1/fact/protocol.py b/solution/backend/router/v1/fact/protocol.py similarity index 100% rename from backend/router/v1/fact/protocol.py rename to solution/backend/router/v1/fact/protocol.py diff --git a/backend/router/v1/faq/faq.py b/solution/backend/router/v1/faq/faq.py similarity index 100% rename from backend/router/v1/faq/faq.py rename to solution/backend/router/v1/faq/faq.py diff --git a/backend/router/v1/faq/protocol.py b/solution/backend/router/v1/faq/protocol.py similarity index 100% rename from backend/router/v1/faq/protocol.py rename to solution/backend/router/v1/faq/protocol.py diff --git a/backend/router/v1/job/job.py b/solution/backend/router/v1/job/job.py similarity index 100% rename from backend/router/v1/job/job.py rename to solution/backend/router/v1/job/job.py diff --git a/backend/router/v1/job/protocol.py b/solution/backend/router/v1/job/protocol.py similarity index 100% rename from backend/router/v1/job/protocol.py rename to solution/backend/router/v1/job/protocol.py diff --git a/backend/router/v1/local/__init__.py b/solution/backend/router/v1/local/__init__.py similarity index 100% rename from backend/router/v1/local/__init__.py rename to solution/backend/router/v1/local/__init__.py diff --git a/backend/router/v1/local/local.py b/solution/backend/router/v1/local/local.py similarity index 100% rename from backend/router/v1/local/local.py rename to solution/backend/router/v1/local/local.py diff --git a/backend/router/v1/local/protocol.py b/solution/backend/router/v1/local/protocol.py similarity index 100% rename from backend/router/v1/local/protocol.py rename to solution/backend/router/v1/local/protocol.py diff --git a/backend/router/v1/media/media.py b/solution/backend/router/v1/media/media.py similarity index 100% rename from backend/router/v1/media/media.py rename to solution/backend/router/v1/media/media.py diff --git a/backend/router/v1/media/protocol.py b/solution/backend/router/v1/media/protocol.py similarity index 100% rename from backend/router/v1/media/protocol.py rename to solution/backend/router/v1/media/protocol.py diff --git a/backend/router/v1/place/place.py b/solution/backend/router/v1/place/place.py similarity index 100% rename from backend/router/v1/place/place.py rename to solution/backend/router/v1/place/place.py diff --git a/backend/router/v1/place/protocol.py b/solution/backend/router/v1/place/protocol.py similarity index 100% rename from backend/router/v1/place/protocol.py rename to solution/backend/router/v1/place/protocol.py diff --git a/backend/router/v1/site/protocol.py b/solution/backend/router/v1/site/protocol.py similarity index 100% rename from backend/router/v1/site/protocol.py rename to solution/backend/router/v1/site/protocol.py diff --git a/backend/router/v1/site/site.py b/solution/backend/router/v1/site/site.py similarity index 100% rename from backend/router/v1/site/site.py rename to solution/backend/router/v1/site/site.py diff --git a/backend/router/v1/validator/dependencies.py b/solution/backend/router/v1/validator/dependencies.py similarity index 100% rename from backend/router/v1/validator/dependencies.py rename to solution/backend/router/v1/validator/dependencies.py diff --git a/backend/scheduler/__init__.py b/solution/backend/scheduler/__init__.py similarity index 100% rename from backend/scheduler/__init__.py rename to solution/backend/scheduler/__init__.py diff --git a/backend/scheduler/jobs.py b/solution/backend/scheduler/jobs.py similarity index 100% rename from backend/scheduler/jobs.py rename to solution/backend/scheduler/jobs.py diff --git a/backend/scripts/check_search_ready.py b/solution/backend/scripts/check_search_ready.py similarity index 100% rename from backend/scripts/check_search_ready.py rename to solution/backend/scripts/check_search_ready.py diff --git a/backend/scripts/demo_build.py b/solution/backend/scripts/demo_build.py similarity index 98% rename from backend/scripts/demo_build.py rename to solution/backend/scripts/demo_build.py index d81267a..9a3104c 100644 --- a/backend/scripts/demo_build.py +++ b/solution/backend/scripts/demo_build.py @@ -125,10 +125,10 @@ async def main(): return # ★ HTML 은 백엔드가 만들지 않는다. payload JSON 을 쓰는 것까지가 백엔드의 일이고, - # 그걸 정적 페이지로 굽는 것은 frontend/site 의 렌더러다(그게 방문자가 보는 유일한 페이지). + # 그걸 정적 페이지로 굽는 것은 solution/site 의 렌더러다(그게 방문자가 보는 유일한 페이지). print(f"\npayload: {r.get('payload_path')}") print(f"페이지: {r.get('routes')}개 · JSON-LD 노드 {len(r.get('mismatches') or []) == 0 and '검증 통과' or '불일치'}") - print("정적 파일: frontend/site/out/s// (렌더러가 굽는다)") + print("정적 파일: solution/site/out/s// (렌더러가 굽는다)") await engine.dispose() asyncio.run(main()) diff --git a/backend/scripts/demo_pipeline.py b/solution/backend/scripts/demo_pipeline.py similarity index 100% rename from backend/scripts/demo_pipeline.py rename to solution/backend/scripts/demo_pipeline.py diff --git a/backend/scripts/export_openapi.py b/solution/backend/scripts/export_openapi.py similarity index 100% rename from backend/scripts/export_openapi.py rename to solution/backend/scripts/export_openapi.py diff --git a/backend/scripts/republish_all.py b/solution/backend/scripts/republish_all.py similarity index 98% rename from backend/scripts/republish_all.py rename to solution/backend/scripts/republish_all.py index 2674e99..bf65ec3 100644 --- a/backend/scripts/republish_all.py +++ b/solution/backend/scripts/republish_all.py @@ -3,7 +3,7 @@ python scripts/republish_all.py (backend/ 에서 실행) python scripts/republish_all.py --dry-run (올리지 않고 목록만 본다) -★ 왜 필요한가 — 렌더러(frontend/site)를 고쳐 배포하면 번들 파일명이 바뀐다 +★ 왜 필요한가 — 렌더러(solution/site)를 고쳐 배포하면 번들 파일명이 바뀐다 (`assets/index-DvNTmLhy.css` → `assets/index-<새해시>.css`). 그런데 평소 업로드 경로 (services/azure_static.publish)는 **방금 발행한 사이트 하나**만 올린다. 그래서 나머지 사이트의 HTML 은 Blob 에 옛 해시를 가리킨 채로 남는다. 옛 자산 블롭은 지워지지 diff --git a/backend/services/auth_service.py b/solution/backend/services/auth_service.py similarity index 100% rename from backend/services/auth_service.py rename to solution/backend/services/auth_service.py diff --git a/backend/services/azure_static.py b/solution/backend/services/azure_static.py similarity index 100% rename from backend/services/azure_static.py rename to solution/backend/services/azure_static.py diff --git a/backend/services/build_service.py b/solution/backend/services/build_service.py similarity index 99% rename from backend/services/build_service.py rename to solution/backend/services/build_service.py index b626ce4..b72ecef 100644 --- a/backend/services/build_service.py +++ b/solution/backend/services/build_service.py @@ -170,7 +170,7 @@ async def run_build(job: dict) -> dict: return await _fail(f"{facts_gate.reason.name}: {facts_gate.as_log()}", facts_gate) # ---- 렌더러에 넘긴다 ---- - # ★ 여기가 "발행 기록"과 "실제 페이지"를 잇는 자리다. 렌더러(frontend/site)의 유일한 입력이 + # ★ 여기가 "발행 기록"과 "실제 페이지"를 잇는 자리다. 렌더러(solution/site)의 유일한 입력이 # 이 payload JSON 이고, 그게 굽는 HTML 이 방문자와 크롤러가 보는 유일한 페이지다. # ★ payload 에 실릴 발행 상태를 미리 맞춘다. 렌더러는 이 값으로 datePublished 를 굽는데, # 발행 뒤에 payload 를 쓰던 예전 순서에서는 그게 채워져 있었다. 순서가 바뀌었다고 diff --git a/backend/services/collect_service.py b/solution/backend/services/collect_service.py similarity index 100% rename from backend/services/collect_service.py rename to solution/backend/services/collect_service.py diff --git a/backend/services/collector/__init__.py b/solution/backend/services/collector/__init__.py similarity index 100% rename from backend/services/collector/__init__.py rename to solution/backend/services/collector/__init__.py diff --git a/backend/services/collector/base.py b/solution/backend/services/collector/base.py similarity index 100% rename from backend/services/collector/base.py rename to solution/backend/services/collector/base.py diff --git a/backend/services/collector/mock_adapter.py b/solution/backend/services/collector/mock_adapter.py similarity index 100% rename from backend/services/collector/mock_adapter.py rename to solution/backend/services/collector/mock_adapter.py diff --git a/backend/services/collector/naver_place_adapter.py b/solution/backend/services/collector/naver_place_adapter.py similarity index 100% rename from backend/services/collector/naver_place_adapter.py rename to solution/backend/services/collector/naver_place_adapter.py diff --git a/backend/services/collector/registry.py b/solution/backend/services/collector/registry.py similarity index 100% rename from backend/services/collector/registry.py rename to solution/backend/services/collector/registry.py diff --git a/backend/services/collector/static_html_adapter.py b/solution/backend/services/collector/static_html_adapter.py similarity index 100% rename from backend/services/collector/static_html_adapter.py rename to solution/backend/services/collector/static_html_adapter.py diff --git a/backend/services/collector/tour_api_adapter.py b/solution/backend/services/collector/tour_api_adapter.py similarity index 100% rename from backend/services/collector/tour_api_adapter.py rename to solution/backend/services/collector/tour_api_adapter.py diff --git a/backend/services/copy_service.py b/solution/backend/services/copy_service.py similarity index 100% rename from backend/services/copy_service.py rename to solution/backend/services/copy_service.py diff --git a/backend/services/external/gemini.py b/solution/backend/services/external/gemini.py similarity index 100% rename from backend/services/external/gemini.py rename to solution/backend/services/external/gemini.py diff --git a/backend/services/external/gemini_extract.py b/solution/backend/services/external/gemini_extract.py similarity index 100% rename from backend/services/external/gemini_extract.py rename to solution/backend/services/external/gemini_extract.py diff --git a/backend/services/external/gemini_text.py b/solution/backend/services/external/gemini_text.py similarity index 100% rename from backend/services/external/gemini_text.py rename to solution/backend/services/external/gemini_text.py diff --git a/backend/services/external/kakao.py b/solution/backend/services/external/kakao.py similarity index 100% rename from backend/services/external/kakao.py rename to solution/backend/services/external/kakao.py diff --git a/backend/services/external/naver.py b/solution/backend/services/external/naver.py similarity index 100% rename from backend/services/external/naver.py rename to solution/backend/services/external/naver.py diff --git a/backend/services/external/naver_place_lookup.py b/solution/backend/services/external/naver_place_lookup.py similarity index 100% rename from backend/services/external/naver_place_lookup.py rename to solution/backend/services/external/naver_place_lookup.py diff --git a/backend/services/external/open_meteo.py b/solution/backend/services/external/open_meteo.py similarity index 100% rename from backend/services/external/open_meteo.py rename to solution/backend/services/external/open_meteo.py diff --git a/backend/services/external/perplexity.py b/solution/backend/services/external/perplexity.py similarity index 100% rename from backend/services/external/perplexity.py rename to solution/backend/services/external/perplexity.py diff --git a/backend/services/external/tour_api.py b/solution/backend/services/external/tour_api.py similarity index 100% rename from backend/services/external/tour_api.py rename to solution/backend/services/external/tour_api.py diff --git a/backend/services/external/tour_lookup.py b/solution/backend/services/external/tour_lookup.py similarity index 100% rename from backend/services/external/tour_lookup.py rename to solution/backend/services/external/tour_lookup.py diff --git a/backend/services/fact_service.py b/solution/backend/services/fact_service.py similarity index 100% rename from backend/services/fact_service.py rename to solution/backend/services/fact_service.py diff --git a/backend/services/faq_service.py b/solution/backend/services/faq_service.py similarity index 100% rename from backend/services/faq_service.py rename to solution/backend/services/faq_service.py diff --git a/backend/services/grounding/__init__.py b/solution/backend/services/grounding/__init__.py similarity index 100% rename from backend/services/grounding/__init__.py rename to solution/backend/services/grounding/__init__.py diff --git a/backend/services/grounding/channels.py b/solution/backend/services/grounding/channels.py similarity index 100% rename from backend/services/grounding/channels.py rename to solution/backend/services/grounding/channels.py diff --git a/backend/services/grounding/copy.py b/solution/backend/services/grounding/copy.py similarity index 100% rename from backend/services/grounding/copy.py rename to solution/backend/services/grounding/copy.py diff --git a/backend/services/grounding/extract.py b/solution/backend/services/grounding/extract.py similarity index 100% rename from backend/services/grounding/extract.py rename to solution/backend/services/grounding/extract.py diff --git a/backend/services/indexnow.py b/solution/backend/services/indexnow.py similarity index 100% rename from backend/services/indexnow.py rename to solution/backend/services/indexnow.py diff --git a/backend/services/job_service.py b/solution/backend/services/job_service.py similarity index 100% rename from backend/services/job_service.py rename to solution/backend/services/job_service.py diff --git a/backend/services/llm/__init__.py b/solution/backend/services/llm/__init__.py similarity index 100% rename from backend/services/llm/__init__.py rename to solution/backend/services/llm/__init__.py diff --git a/backend/services/llm/gemini.py b/solution/backend/services/llm/gemini.py similarity index 100% rename from backend/services/llm/gemini.py rename to solution/backend/services/llm/gemini.py diff --git a/backend/services/llm/perplexity.py b/solution/backend/services/llm/perplexity.py similarity index 100% rename from backend/services/llm/perplexity.py rename to solution/backend/services/llm/perplexity.py diff --git a/backend/services/local_content_service.py b/solution/backend/services/local_content_service.py similarity index 100% rename from backend/services/local_content_service.py rename to solution/backend/services/local_content_service.py diff --git a/backend/services/media_service.py b/solution/backend/services/media_service.py similarity index 100% rename from backend/services/media_service.py rename to solution/backend/services/media_service.py diff --git a/backend/services/place_service.py b/solution/backend/services/place_service.py similarity index 100% rename from backend/services/place_service.py rename to solution/backend/services/place_service.py diff --git a/backend/services/prompts/__init__.py b/solution/backend/services/prompts/__init__.py similarity index 100% rename from backend/services/prompts/__init__.py rename to solution/backend/services/prompts/__init__.py diff --git a/backend/services/prompts/channel_discovery.py b/solution/backend/services/prompts/channel_discovery.py similarity index 100% rename from backend/services/prompts/channel_discovery.py rename to solution/backend/services/prompts/channel_discovery.py diff --git a/backend/services/prompts/copy.py b/solution/backend/services/prompts/copy.py similarity index 100% rename from backend/services/prompts/copy.py rename to solution/backend/services/prompts/copy.py diff --git a/backend/services/prompts/extract.py b/solution/backend/services/prompts/extract.py similarity index 100% rename from backend/services/prompts/extract.py rename to solution/backend/services/prompts/extract.py diff --git a/backend/services/prompts/vision.py b/solution/backend/services/prompts/vision.py similarity index 100% rename from backend/services/prompts/vision.py rename to solution/backend/services/prompts/vision.py diff --git a/backend/services/publish_gate.py b/solution/backend/services/publish_gate.py similarity index 100% rename from backend/services/publish_gate.py rename to solution/backend/services/publish_gate.py diff --git a/backend/services/render_report.py b/solution/backend/services/render_report.py similarity index 100% rename from backend/services/render_report.py rename to solution/backend/services/render_report.py diff --git a/backend/services/seo_audit.py b/solution/backend/services/seo_audit.py similarity index 100% rename from backend/services/seo_audit.py rename to solution/backend/services/seo_audit.py diff --git a/backend/services/site_payload.py b/solution/backend/services/site_payload.py similarity index 99% rename from backend/services/site_payload.py rename to solution/backend/services/site_payload.py index 7773eb0..150513b 100644 --- a/backend/services/site_payload.py +++ b/solution/backend/services/site_payload.py @@ -1,11 +1,11 @@ -"""발행 payload — 정적 렌더러(frontend/site)가 먹는 유일한 입력 JSON. +"""발행 payload — 정적 렌더러(solution/site)가 먹는 유일한 입력 JSON. ★ 왜 필요한가 빌드 잡은 지금까지 HTML 을 굽고 그 **길이만** 재고 버렸다(발행 기록은 남는데 페이지가 없었다). - 실제로 방문자에게 보여줄 페이지는 frontend/site 의 SSG 가 굽는다. 그 렌더러의 유일한 입력이 + 실제로 방문자에게 보여줄 페이지는 solution/site 의 SSG 가 굽는다. 그 렌더러의 유일한 입력이 이 payload 이므로, 빌드 잡이 이 JSON 만 파일로 떨어뜨리면 발행이 실제 페이지로 이어진다. -★ 스키마는 frontend/shared/src/types/site-payload.ts 의 `SitePayload` 다. +★ 스키마는 solution/shared/src/types/site-payload.ts 의 `SitePayload` 다. 필드명이 camelCase 인 이유는 그쪽이 원본이기 때문이다 — 여기서 스네이크로 바꾸면 렌더러가 못 읽는다. 스키마가 바뀌면 schemaVersion 을 올린다(렌더러는 모르는 버전을 조용히 반쪽 렌더하지 않고 실패한다). @@ -152,7 +152,7 @@ def _iso(value) -> str | None: def slugify(text: str) -> str: - """frontend/shared 의 toSlug 와 같은 규칙(로마자 음차하지 않는다). + """solution/shared 의 toSlug 와 같은 규칙(로마자 음차하지 않는다). ★ 한글을 음차하면 같은 가게가 두 주소를 갖는다. 한글은 그대로 두고 퍼센트 인코딩에 맡긴다. NFC 로 정규화하는 이유: macOS 가 준 NFD 문자열이 그대로 디렉토리명이 되면 @@ -200,7 +200,7 @@ def _parse_address_parts(*addresses: str | None) -> dict: ★ **원문에 있는 조각을 그대로만 쓴다.** "경기" 를 "경기도" 로 펴지 않는다. addressRegion·addressLocality 는 JSON-LD 로 나가고, JSON-LD 는 화면 텍스트와 - 대조된다(frontend/site/src/seo/verify.ts). 화면에는 원문 주소가 그대로 찍히므로 + 대조된다(solution/site/src/seo/verify.ts). 화면에는 원문 주소가 그대로 찍히므로 정식명으로 펴는 순간 '화면에 없는 값'이 되어 발행 게이트가 막는다. 정규화가 필요한 곳은 ISO 코드 하나뿐이고, 그건 화면 대조 대상이 아니다. @@ -373,7 +373,7 @@ def _sections(saved_sections, default_spec) -> list: return out -# WMO weather code → 한 줄 날씨. ★ frontend/site/src/lib/use-live-weather.ts 의 condition() 과 +# WMO weather code → 한 줄 날씨. ★ solution/site/src/lib/use-live-weather.ts 의 condition() 과 # **같은 구간**이어야 한다. 렌더러는 프리렌더된 이 값으로 그리다가 하이드레이션 뒤 최신 캐시로 덮어쓰는데, # 두 곳이 다른 표를 쓰면 같은 날씨인데 화면 문구가 바뀐다(사장님 눈에는 버그로 보인다). def _weather_condition(code) -> str: diff --git a/backend/services/site_service.py b/solution/backend/services/site_service.py similarity index 100% rename from backend/services/site_service.py rename to solution/backend/services/site_service.py diff --git a/backend/services/site_slug.py b/solution/backend/services/site_slug.py similarity index 100% rename from backend/services/site_slug.py rename to solution/backend/services/site_slug.py diff --git a/backend/services/snapshot.py b/solution/backend/services/snapshot.py similarity index 99% rename from backend/services/snapshot.py rename to solution/backend/services/snapshot.py index baa02bd..5cf5022 100644 --- a/backend/services/snapshot.py +++ b/solution/backend/services/snapshot.py @@ -176,7 +176,7 @@ async def _local_contents(place) -> dict: ★ expires_at 은 보지 않는다. 모델 주석대로 그건 '갱신 대상'이라는 표시지 '못 쓰는 값'이 아니다 (외부 API 가 죽어도 직전 값을 유지하는 게 이 캐시의 규약이다). 게다가 날씨는 렌더러가 - 하이드레이션 뒤 최신값으로 덮어쓴다(frontend/site/src/lib/use-live-weather.ts). + 하이드레이션 뒤 최신값으로 덮어쓴다(solution/site/src/lib/use-live-weather.ts). """ # ★ getattr 로 읽는다 — 이 함수는 ORM 행뿐 아니라 테스트의 가짜 place 객체도 받는다. region_code = str(getattr(place, "region_code", None) or "").strip() diff --git a/backend/services/vision_service.py b/solution/backend/services/vision_service.py similarity index 100% rename from backend/services/vision_service.py rename to solution/backend/services/vision_service.py diff --git a/backend/tests/test_auth.py b/solution/backend/tests/test_auth.py similarity index 100% rename from backend/tests/test_auth.py rename to solution/backend/tests/test_auth.py diff --git a/backend/tests/test_azure_static.py b/solution/backend/tests/test_azure_static.py similarity index 100% rename from backend/tests/test_azure_static.py rename to solution/backend/tests/test_azure_static.py diff --git a/backend/tests/test_build_publish.py b/solution/backend/tests/test_build_publish.py similarity index 99% rename from backend/tests/test_build_publish.py rename to solution/backend/tests/test_build_publish.py index 4453272..c8e6207 100644 --- a/backend/tests/test_build_publish.py +++ b/solution/backend/tests/test_build_publish.py @@ -9,7 +9,7 @@ ★ 게이트는 이제 **렌더러가 실제로 구운 결과**를 보고 판정한다. 여기서는 그 보고서를 백엔드가 어떻게 처리하는지를 본다 — 구조화 데이터 ↔ 화면 값 대조 자체는 - 렌더러 쪽 테스트가 본다(frontend/site/src/seo/verify.test.ts). + 렌더러 쪽 테스트가 본다(solution/site/src/seo/verify.test.ts). """ import uuid diff --git a/backend/tests/test_category_schema.py b/solution/backend/tests/test_category_schema.py similarity index 100% rename from backend/tests/test_category_schema.py rename to solution/backend/tests/test_category_schema.py diff --git a/backend/tests/test_collect_api.py b/solution/backend/tests/test_collect_api.py similarity index 100% rename from backend/tests/test_collect_api.py rename to solution/backend/tests/test_collect_api.py diff --git a/backend/tests/test_collect_pipeline.py b/solution/backend/tests/test_collect_pipeline.py similarity index 100% rename from backend/tests/test_collect_pipeline.py rename to solution/backend/tests/test_collect_pipeline.py diff --git a/backend/tests/test_collector.py b/solution/backend/tests/test_collector.py similarity index 100% rename from backend/tests/test_collector.py rename to solution/backend/tests/test_collector.py diff --git a/backend/tests/test_config.py b/solution/backend/tests/test_config.py similarity index 100% rename from backend/tests/test_config.py rename to solution/backend/tests/test_config.py diff --git a/backend/tests/test_copy_api.py b/solution/backend/tests/test_copy_api.py similarity index 100% rename from backend/tests/test_copy_api.py rename to solution/backend/tests/test_copy_api.py diff --git a/backend/tests/test_cost.py b/solution/backend/tests/test_cost.py similarity index 100% rename from backend/tests/test_cost.py rename to solution/backend/tests/test_cost.py diff --git a/backend/tests/test_extract_grounding.py b/solution/backend/tests/test_extract_grounding.py similarity index 100% rename from backend/tests/test_extract_grounding.py rename to solution/backend/tests/test_extract_grounding.py diff --git a/backend/tests/test_fact_api.py b/solution/backend/tests/test_fact_api.py similarity index 100% rename from backend/tests/test_fact_api.py rename to solution/backend/tests/test_fact_api.py diff --git a/backend/tests/test_fact_schema.py b/solution/backend/tests/test_fact_schema.py similarity index 100% rename from backend/tests/test_fact_schema.py rename to solution/backend/tests/test_fact_schema.py diff --git a/backend/tests/test_faq_api.py b/solution/backend/tests/test_faq_api.py similarity index 100% rename from backend/tests/test_faq_api.py rename to solution/backend/tests/test_faq_api.py diff --git a/backend/tests/test_gemini.py b/solution/backend/tests/test_gemini.py similarity index 100% rename from backend/tests/test_gemini.py rename to solution/backend/tests/test_gemini.py diff --git a/backend/tests/test_gemini_text.py b/solution/backend/tests/test_gemini_text.py similarity index 100% rename from backend/tests/test_gemini_text.py rename to solution/backend/tests/test_gemini_text.py diff --git a/backend/tests/test_healthz.py b/solution/backend/tests/test_healthz.py similarity index 100% rename from backend/tests/test_healthz.py rename to solution/backend/tests/test_healthz.py diff --git a/backend/tests/test_indexnow.py b/solution/backend/tests/test_indexnow.py similarity index 100% rename from backend/tests/test_indexnow.py rename to solution/backend/tests/test_indexnow.py diff --git a/backend/tests/test_job_queue.py b/solution/backend/tests/test_job_queue.py similarity index 100% rename from backend/tests/test_job_queue.py rename to solution/backend/tests/test_job_queue.py diff --git a/backend/tests/test_kakao.py b/solution/backend/tests/test_kakao.py similarity index 100% rename from backend/tests/test_kakao.py rename to solution/backend/tests/test_kakao.py diff --git a/backend/tests/test_naver.py b/solution/backend/tests/test_naver.py similarity index 100% rename from backend/tests/test_naver.py rename to solution/backend/tests/test_naver.py diff --git a/backend/tests/test_naver_place_lookup.py b/solution/backend/tests/test_naver_place_lookup.py similarity index 100% rename from backend/tests/test_naver_place_lookup.py rename to solution/backend/tests/test_naver_place_lookup.py diff --git a/backend/tests/test_perplexity.py b/solution/backend/tests/test_perplexity.py similarity index 100% rename from backend/tests/test_perplexity.py rename to solution/backend/tests/test_perplexity.py diff --git a/backend/tests/test_place.py b/solution/backend/tests/test_place.py similarity index 100% rename from backend/tests/test_place.py rename to solution/backend/tests/test_place.py diff --git a/backend/tests/test_publish_gate.py b/solution/backend/tests/test_publish_gate.py similarity index 100% rename from backend/tests/test_publish_gate.py rename to solution/backend/tests/test_publish_gate.py diff --git a/backend/tests/test_render_report.py b/solution/backend/tests/test_render_report.py similarity index 100% rename from backend/tests/test_render_report.py rename to solution/backend/tests/test_render_report.py diff --git a/backend/tests/test_schema_ddl.py b/solution/backend/tests/test_schema_ddl.py similarity index 93% rename from backend/tests/test_schema_ddl.py rename to solution/backend/tests/test_schema_ddl.py index 31ced49..9707077 100644 --- a/backend/tests/test_schema_ddl.py +++ b/solution/backend/tests/test_schema_ddl.py @@ -8,7 +8,9 @@ from pathlib import Path from common.database.model.models import MAIN_BASE -_INIT_SQL = Path(__file__).resolve().parents[2] / "postgres-init" / "init-data" / "init.sql" +# parents[3] = 레포 루트 (tests → backend → solution → 루트). +# ★ 백엔드가 solution/ 안으로 들어가면서 한 칸 깊어졌다 — 폴더를 옮기면 여기부터 깨진다. +_INIT_SQL = Path(__file__).resolve().parents[3] / "postgres-init" / "init-data" / "init.sql" _CREATE_TABLE_RE = re.compile( r"CREATE TABLE IF NOT EXISTS\s+(?P\w+)\.(?P\w+)\s*\((?P.*?)\n\);", diff --git a/backend/tests/test_seo_audit.py b/solution/backend/tests/test_seo_audit.py similarity index 100% rename from backend/tests/test_seo_audit.py rename to solution/backend/tests/test_seo_audit.py diff --git a/backend/tests/test_site_slug.py b/solution/backend/tests/test_site_slug.py similarity index 100% rename from backend/tests/test_site_slug.py rename to solution/backend/tests/test_site_slug.py diff --git a/backend/tests/test_site_template.py b/solution/backend/tests/test_site_template.py similarity index 100% rename from backend/tests/test_site_template.py rename to solution/backend/tests/test_site_template.py diff --git a/backend/tests/test_site_theme.py b/solution/backend/tests/test_site_theme.py similarity index 99% rename from backend/tests/test_site_theme.py rename to solution/backend/tests/test_site_theme.py index e7bd6f3..4cbf587 100644 --- a/backend/tests/test_site_theme.py +++ b/solution/backend/tests/test_site_theme.py @@ -255,7 +255,7 @@ def test_saved_theme_does_not_override_chosen_template(): # 여기에 없는 섹션은 사장님이 에디터에서 아무리 봐도 사이트에 나오지 않는다. # 실측으로 날씨·실시간 예약·대관 문의·관람 안내가 그렇게 빠져 있었다. _ADMIN_SECTIONS_TS = ( - pathlib.Path(__file__).resolve().parents[2] / "frontend/admin/src/data/industryData.ts" + pathlib.Path(__file__).resolve().parents[2] / "front/src/data/industryData.ts" # parents[2] = solution/ ) # 에디터 업종 키 → PlaceCategory. 이름이 다른 건 두 층의 어휘가 달라서다(tour vs TOUR_ACTIVITY). diff --git a/backend/tests/test_snapshot.py b/solution/backend/tests/test_snapshot.py similarity index 100% rename from backend/tests/test_snapshot.py rename to solution/backend/tests/test_snapshot.py diff --git a/backend/tests/test_tour_api_adapter.py b/solution/backend/tests/test_tour_api_adapter.py similarity index 100% rename from backend/tests/test_tour_api_adapter.py rename to solution/backend/tests/test_tour_api_adapter.py diff --git a/backend/tests/test_verify_candidates.py b/solution/backend/tests/test_verify_candidates.py similarity index 100% rename from backend/tests/test_verify_candidates.py rename to solution/backend/tests/test_verify_candidates.py diff --git a/backend/tests/test_vision_api.py b/solution/backend/tests/test_vision_api.py similarity index 100% rename from backend/tests/test_vision_api.py rename to solution/backend/tests/test_vision_api.py diff --git a/backend/tests/test_weather_api.py b/solution/backend/tests/test_weather_api.py similarity index 100% rename from backend/tests/test_weather_api.py rename to solution/backend/tests/test_weather_api.py diff --git a/backend/web_main.py b/solution/backend/web_main.py similarity index 100% rename from backend/web_main.py rename to solution/backend/web_main.py diff --git a/backend/worker/handlers.py b/solution/backend/worker/handlers.py similarity index 100% rename from backend/worker/handlers.py rename to solution/backend/worker/handlers.py diff --git a/backend/worker/notify.py b/solution/backend/worker/notify.py similarity index 100% rename from backend/worker/notify.py rename to solution/backend/worker/notify.py diff --git a/backend/worker/runner.py b/solution/backend/worker/runner.py similarity index 100% rename from backend/worker/runner.py rename to solution/backend/worker/runner.py diff --git a/backend/worker_main.py b/solution/backend/worker_main.py similarity index 100% rename from backend/worker_main.py rename to solution/backend/worker_main.py diff --git a/frontend/admin/.env.example b/solution/front/.env.example similarity index 80% rename from frontend/admin/.env.example rename to solution/front/.env.example index ec8f129..98c392a 100644 --- a/frontend/admin/.env.example +++ b/solution/front/.env.example @@ -3,6 +3,8 @@ VITE_API_BASE_URL=http://localhost:9800 # 발행 사이트의 기본 호스트. 발행 모달이 보여줄 주소를 이걸로 만든다. # 커스텀 도메인이 붙기 전까지 .<이 값> 형태로 나간다. +# ★ 로컬에서 compose 없이 띄울 때만 쓴다. compose 로 띄우면 루트 .env 의 SITE_PUBLIC_HOST 가 +# 주입돼 이 값을 덮는다 — 발행 호스트의 단일 출처는 루트 .env 다. VITE_PUBLISH_HOST=w4ai.o2o.kr # 발행 사이트 렌더러 개발 서버. 에디터 상단 "발행본 사이트 열기" 가 이 주소를 연다. diff --git a/frontend/admin/components.json b/solution/front/components.json similarity index 100% rename from frontend/admin/components.json rename to solution/front/components.json diff --git a/solution/front/eslint.config.js b/solution/front/eslint.config.js new file mode 100644 index 0000000..b669fb4 --- /dev/null +++ b/solution/front/eslint.config.js @@ -0,0 +1,31 @@ +// 최소 게이트 — 전체 스타일 린트가 아니라, tsc 가 못 잡는 런타임 크래시 버그만 막는다. +// (negodata 보일러플레이트에서 이식. 도입 계기는 로컬 const 가 동명 import 를 가려 +// zustand 셀렉터가 TDZ 참조 → 프로덕션 크래시. 그 케이스는 tsc --noEmit 도 통과했다.) +import tseslint from 'typescript-eslint'; +import reactHooks from 'eslint-plugin-react-hooks'; + +export default tseslint.config({ + files: ['src/**/*.{ts,tsx}'], + ignores: ['src/api/generated/**'], + languageOptions: { + parser: tseslint.parser, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + 'react-hooks': reactHooks, + }, + rules: { + // 훅 호출 순서 위반은 런타임 크래시라 error, deps 누락은 기존 코드가 많아 warn. + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + // 선언보다 위에서 변수를 쓰는 것(TDZ) 차단. 함수/타입 호이스팅은 안전하므로 허용. + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': [ + 'error', + {functions: false, classes: false, typedefs: false, enums: false, variables: true}, + ], + // 상위 스코프(import 포함) 이름을 가리는 재선언 차단. + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + }, +}); diff --git a/frontend/admin/index.html b/solution/front/index.html similarity index 100% rename from frontend/admin/index.html rename to solution/front/index.html diff --git a/frontend/admin/orval.config.ts b/solution/front/orval.config.ts similarity index 97% rename from frontend/admin/orval.config.ts rename to solution/front/orval.config.ts index 133eae9..c615bf9 100644 --- a/frontend/admin/orval.config.ts +++ b/solution/front/orval.config.ts @@ -10,7 +10,7 @@ import {defineConfig} from 'orval'; * 실행: `npm run orval` * * 서버를 안 띄우고 생성하려면 백엔드가 뽑아둔 파일을 지정한다: - * ORVAL_INPUT=../../backend/openapi.json npm run orval + * ORVAL_INPUT=../backend/openapi.json npm run orval */ export default defineConfig({ web4ai: { diff --git a/frontend/admin/package.json b/solution/front/package.json similarity index 98% rename from frontend/admin/package.json rename to solution/front/package.json index 98acb29..00ef620 100644 --- a/frontend/admin/package.json +++ b/solution/front/package.json @@ -1,5 +1,5 @@ { - "name": "@o2o/admin", + "name": "@o2o/front", "private": true, "version": "0.0.0", "type": "module", diff --git a/frontend/admin/public/apple-touch-icon.png b/solution/front/public/apple-touch-icon.png similarity index 100% rename from frontend/admin/public/apple-touch-icon.png rename to solution/front/public/apple-touch-icon.png diff --git a/frontend/admin/public/brand/ai-for-web-logo.png b/solution/front/public/brand/ai-for-web-logo.png similarity index 100% rename from frontend/admin/public/brand/ai-for-web-logo.png rename to solution/front/public/brand/ai-for-web-logo.png diff --git a/frontend/admin/public/brand/ai-for-web-mark-512.png b/solution/front/public/brand/ai-for-web-mark-512.png similarity index 100% rename from frontend/admin/public/brand/ai-for-web-mark-512.png rename to solution/front/public/brand/ai-for-web-mark-512.png diff --git a/frontend/admin/public/brand/ai-for-web-wordmark.svg b/solution/front/public/brand/ai-for-web-wordmark.svg similarity index 100% rename from frontend/admin/public/brand/ai-for-web-wordmark.svg rename to solution/front/public/brand/ai-for-web-wordmark.svg diff --git a/frontend/admin/public/brand/favicon-afw.svg b/solution/front/public/brand/favicon-afw.svg similarity index 100% rename from frontend/admin/public/brand/favicon-afw.svg rename to solution/front/public/brand/favicon-afw.svg diff --git a/frontend/admin/public/brand/favicon-cursor.svg b/solution/front/public/brand/favicon-cursor.svg similarity index 100% rename from frontend/admin/public/brand/favicon-cursor.svg rename to solution/front/public/brand/favicon-cursor.svg diff --git a/frontend/admin/public/brand/favicon-magic-wand.svg b/solution/front/public/brand/favicon-magic-wand.svg similarity index 100% rename from frontend/admin/public/brand/favicon-magic-wand.svg rename to solution/front/public/brand/favicon-magic-wand.svg diff --git a/frontend/admin/public/brand/favicon-top-hat-cursor.svg b/solution/front/public/brand/favicon-top-hat-cursor.svg similarity index 100% rename from frontend/admin/public/brand/favicon-top-hat-cursor.svg rename to solution/front/public/brand/favicon-top-hat-cursor.svg diff --git a/frontend/admin/public/brand/favicon-top-hat.svg b/solution/front/public/brand/favicon-top-hat.svg similarity index 100% rename from frontend/admin/public/brand/favicon-top-hat.svg rename to solution/front/public/brand/favicon-top-hat.svg diff --git a/frontend/admin/public/brand/favicon-w4a.svg b/solution/front/public/brand/favicon-w4a.svg similarity index 100% rename from frontend/admin/public/brand/favicon-w4a.svg rename to solution/front/public/brand/favicon-w4a.svg diff --git a/frontend/admin/public/brand/web4ai-wordmark.svg b/solution/front/public/brand/web4ai-wordmark.svg similarity index 100% rename from frontend/admin/public/brand/web4ai-wordmark.svg rename to solution/front/public/brand/web4ai-wordmark.svg diff --git a/frontend/admin/public/favicon-32.png b/solution/front/public/favicon-32.png similarity index 100% rename from frontend/admin/public/favicon-32.png rename to solution/front/public/favicon-32.png diff --git a/frontend/admin/public/favicon.svg b/solution/front/public/favicon.svg similarity index 100% rename from frontend/admin/public/favicon.svg rename to solution/front/public/favicon.svg diff --git a/frontend/admin/public/fonts/README.md b/solution/front/public/fonts/README.md similarity index 100% rename from frontend/admin/public/fonts/README.md rename to solution/front/public/fonts/README.md diff --git a/frontend/admin/src/api/README.md b/solution/front/src/api/README.md similarity index 100% rename from frontend/admin/src/api/README.md rename to solution/front/src/api/README.md diff --git a/frontend/admin/src/api/generated/.gitkeep b/solution/front/src/api/generated/.gitkeep similarity index 100% rename from frontend/admin/src/api/generated/.gitkeep rename to solution/front/src/api/generated/.gitkeep diff --git a/frontend/admin/src/api/generated/auth/auth.ts b/solution/front/src/api/generated/auth/auth.ts similarity index 100% rename from frontend/admin/src/api/generated/auth/auth.ts rename to solution/front/src/api/generated/auth/auth.ts diff --git a/frontend/admin/src/api/generated/default/default.ts b/solution/front/src/api/generated/default/default.ts similarity index 100% rename from frontend/admin/src/api/generated/default/default.ts rename to solution/front/src/api/generated/default/default.ts diff --git a/frontend/admin/src/api/generated/fact/fact.ts b/solution/front/src/api/generated/fact/fact.ts similarity index 100% rename from frontend/admin/src/api/generated/fact/fact.ts rename to solution/front/src/api/generated/fact/fact.ts diff --git a/frontend/admin/src/api/generated/faq/faq.ts b/solution/front/src/api/generated/faq/faq.ts similarity index 100% rename from frontend/admin/src/api/generated/faq/faq.ts rename to solution/front/src/api/generated/faq/faq.ts diff --git a/frontend/admin/src/api/generated/job/job.ts b/solution/front/src/api/generated/job/job.ts similarity index 100% rename from frontend/admin/src/api/generated/job/job.ts rename to solution/front/src/api/generated/job/job.ts diff --git a/frontend/admin/src/api/generated/local-content/local-content.ts b/solution/front/src/api/generated/local-content/local-content.ts similarity index 100% rename from frontend/admin/src/api/generated/local-content/local-content.ts rename to solution/front/src/api/generated/local-content/local-content.ts diff --git a/frontend/admin/src/api/generated/media/media.ts b/solution/front/src/api/generated/media/media.ts similarity index 100% rename from frontend/admin/src/api/generated/media/media.ts rename to solution/front/src/api/generated/media/media.ts diff --git a/frontend/admin/src/api/generated/model/auditCheckData.ts b/solution/front/src/api/generated/model/auditCheckData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/auditCheckData.ts rename to solution/front/src/api/generated/model/auditCheckData.ts diff --git a/frontend/admin/src/api/generated/model/auditCheckDataRecommendation.ts b/solution/front/src/api/generated/model/auditCheckDataRecommendation.ts similarity index 100% rename from frontend/admin/src/api/generated/model/auditCheckDataRecommendation.ts rename to solution/front/src/api/generated/model/auditCheckDataRecommendation.ts diff --git a/frontend/admin/src/api/generated/model/buildStatus.ts b/solution/front/src/api/generated/model/buildStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/buildStatus.ts rename to solution/front/src/api/generated/model/buildStatus.ts diff --git a/frontend/admin/src/api/generated/model/checkSlugParams.ts b/solution/front/src/api/generated/model/checkSlugParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/checkSlugParams.ts rename to solution/front/src/api/generated/model/checkSlugParams.ts diff --git a/frontend/admin/src/api/generated/model/companyData.ts b/solution/front/src/api/generated/model/companyData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/companyData.ts rename to solution/front/src/api/generated/model/companyData.ts diff --git a/frontend/admin/src/api/generated/model/errorInfo.ts b/solution/front/src/api/generated/model/errorInfo.ts similarity index 100% rename from frontend/admin/src/api/generated/model/errorInfo.ts rename to solution/front/src/api/generated/model/errorInfo.ts diff --git a/frontend/admin/src/api/generated/model/errorInfoCode.ts b/solution/front/src/api/generated/model/errorInfoCode.ts similarity index 100% rename from frontend/admin/src/api/generated/model/errorInfoCode.ts rename to solution/front/src/api/generated/model/errorInfoCode.ts diff --git a/frontend/admin/src/api/generated/model/errorInfoDesc.ts b/solution/front/src/api/generated/model/errorInfoDesc.ts similarity index 100% rename from frontend/admin/src/api/generated/model/errorInfoDesc.ts rename to solution/front/src/api/generated/model/errorInfoDesc.ts diff --git a/frontend/admin/src/api/generated/model/errorInfoSuccess.ts b/solution/front/src/api/generated/model/errorInfoSuccess.ts similarity index 100% rename from frontend/admin/src/api/generated/model/errorInfoSuccess.ts rename to solution/front/src/api/generated/model/errorInfoSuccess.ts diff --git a/frontend/admin/src/api/generated/model/externalPlaceSource.ts b/solution/front/src/api/generated/model/externalPlaceSource.ts similarity index 100% rename from frontend/admin/src/api/generated/model/externalPlaceSource.ts rename to solution/front/src/api/generated/model/externalPlaceSource.ts diff --git a/frontend/admin/src/api/generated/model/factData.ts b/solution/front/src/api/generated/model/factData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factData.ts rename to solution/front/src/api/generated/model/factData.ts diff --git a/frontend/admin/src/api/generated/model/factDataCollectedAt.ts b/solution/front/src/api/generated/model/factDataCollectedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataCollectedAt.ts rename to solution/front/src/api/generated/model/factDataCollectedAt.ts diff --git a/frontend/admin/src/api/generated/model/factDataExpiresAt.ts b/solution/front/src/api/generated/model/factDataExpiresAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataExpiresAt.ts rename to solution/front/src/api/generated/model/factDataExpiresAt.ts diff --git a/frontend/admin/src/api/generated/model/factDataSourceUrl.ts b/solution/front/src/api/generated/model/factDataSourceUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataSourceUrl.ts rename to solution/front/src/api/generated/model/factDataSourceUrl.ts diff --git a/frontend/admin/src/api/generated/model/factDataUnit.ts b/solution/front/src/api/generated/model/factDataUnit.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataUnit.ts rename to solution/front/src/api/generated/model/factDataUnit.ts diff --git a/frontend/admin/src/api/generated/model/factDataUnitId.ts b/solution/front/src/api/generated/model/factDataUnitId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataUnitId.ts rename to solution/front/src/api/generated/model/factDataUnitId.ts diff --git a/frontend/admin/src/api/generated/model/factDataValue.ts b/solution/front/src/api/generated/model/factDataValue.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataValue.ts rename to solution/front/src/api/generated/model/factDataValue.ts diff --git a/frontend/admin/src/api/generated/model/factDataVerifiedAt.ts b/solution/front/src/api/generated/model/factDataVerifiedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataVerifiedAt.ts rename to solution/front/src/api/generated/model/factDataVerifiedAt.ts diff --git a/frontend/admin/src/api/generated/model/factDataVerifiedBy.ts b/solution/front/src/api/generated/model/factDataVerifiedBy.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factDataVerifiedBy.ts rename to solution/front/src/api/generated/model/factDataVerifiedBy.ts diff --git a/frontend/admin/src/api/generated/model/factStatus.ts b/solution/front/src/api/generated/model/factStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factStatus.ts rename to solution/front/src/api/generated/model/factStatus.ts diff --git a/frontend/admin/src/api/generated/model/factWriteOutcome.ts b/solution/front/src/api/generated/model/factWriteOutcome.ts similarity index 100% rename from frontend/admin/src/api/generated/model/factWriteOutcome.ts rename to solution/front/src/api/generated/model/factWriteOutcome.ts diff --git a/frontend/admin/src/api/generated/model/faqData.ts b/solution/front/src/api/generated/model/faqData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/faqData.ts rename to solution/front/src/api/generated/model/faqData.ts diff --git a/frontend/admin/src/api/generated/model/faqDataCreatedAt.ts b/solution/front/src/api/generated/model/faqDataCreatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/faqDataCreatedAt.ts rename to solution/front/src/api/generated/model/faqDataCreatedAt.ts diff --git a/frontend/admin/src/api/generated/model/faqDataSourceFactIds.ts b/solution/front/src/api/generated/model/faqDataSourceFactIds.ts similarity index 100% rename from frontend/admin/src/api/generated/model/faqDataSourceFactIds.ts rename to solution/front/src/api/generated/model/faqDataSourceFactIds.ts diff --git a/frontend/admin/src/api/generated/model/faqDataUpdatedAt.ts b/solution/front/src/api/generated/model/faqDataUpdatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/faqDataUpdatedAt.ts rename to solution/front/src/api/generated/model/faqDataUpdatedAt.ts diff --git a/frontend/admin/src/api/generated/model/fieldSpecData.ts b/solution/front/src/api/generated/model/fieldSpecData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/fieldSpecData.ts rename to solution/front/src/api/generated/model/fieldSpecData.ts diff --git a/frontend/admin/src/api/generated/model/fieldSpecDataUnit.ts b/solution/front/src/api/generated/model/fieldSpecDataUnit.ts similarity index 100% rename from frontend/admin/src/api/generated/model/fieldSpecDataUnit.ts rename to solution/front/src/api/generated/model/fieldSpecDataUnit.ts diff --git a/frontend/admin/src/api/generated/model/getWeatherParams.ts b/solution/front/src/api/generated/model/getWeatherParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/getWeatherParams.ts rename to solution/front/src/api/generated/model/getWeatherParams.ts diff --git a/frontend/admin/src/api/generated/model/hTTPValidationError.ts b/solution/front/src/api/generated/model/hTTPValidationError.ts similarity index 100% rename from frontend/admin/src/api/generated/model/hTTPValidationError.ts rename to solution/front/src/api/generated/model/hTTPValidationError.ts diff --git a/frontend/admin/src/api/generated/model/index.ts b/solution/front/src/api/generated/model/index.ts similarity index 100% rename from frontend/admin/src/api/generated/model/index.ts rename to solution/front/src/api/generated/model/index.ts diff --git a/frontend/admin/src/api/generated/model/jobData.ts b/solution/front/src/api/generated/model/jobData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobData.ts rename to solution/front/src/api/generated/model/jobData.ts diff --git a/frontend/admin/src/api/generated/model/jobDataCreatedAt.ts b/solution/front/src/api/generated/model/jobDataCreatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobDataCreatedAt.ts rename to solution/front/src/api/generated/model/jobDataCreatedAt.ts diff --git a/frontend/admin/src/api/generated/model/jobDataLastError.ts b/solution/front/src/api/generated/model/jobDataLastError.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobDataLastError.ts rename to solution/front/src/api/generated/model/jobDataLastError.ts diff --git a/frontend/admin/src/api/generated/model/jobDataResult.ts b/solution/front/src/api/generated/model/jobDataResult.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobDataResult.ts rename to solution/front/src/api/generated/model/jobDataResult.ts diff --git a/frontend/admin/src/api/generated/model/jobDataRunAfter.ts b/solution/front/src/api/generated/model/jobDataRunAfter.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobDataRunAfter.ts rename to solution/front/src/api/generated/model/jobDataRunAfter.ts diff --git a/frontend/admin/src/api/generated/model/jobDataRunStartedAt.ts b/solution/front/src/api/generated/model/jobDataRunStartedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobDataRunStartedAt.ts rename to solution/front/src/api/generated/model/jobDataRunStartedAt.ts diff --git a/frontend/admin/src/api/generated/model/jobDataUpdatedAt.ts b/solution/front/src/api/generated/model/jobDataUpdatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobDataUpdatedAt.ts rename to solution/front/src/api/generated/model/jobDataUpdatedAt.ts diff --git a/frontend/admin/src/api/generated/model/jobStatus.ts b/solution/front/src/api/generated/model/jobStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobStatus.ts rename to solution/front/src/api/generated/model/jobStatus.ts diff --git a/frontend/admin/src/api/generated/model/jobType.ts b/solution/front/src/api/generated/model/jobType.ts similarity index 100% rename from frontend/admin/src/api/generated/model/jobType.ts rename to solution/front/src/api/generated/model/jobType.ts diff --git a/frontend/admin/src/api/generated/model/linkChannel.ts b/solution/front/src/api/generated/model/linkChannel.ts similarity index 100% rename from frontend/admin/src/api/generated/model/linkChannel.ts rename to solution/front/src/api/generated/model/linkChannel.ts diff --git a/frontend/admin/src/api/generated/model/linkData.ts b/solution/front/src/api/generated/model/linkData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/linkData.ts rename to solution/front/src/api/generated/model/linkData.ts diff --git a/frontend/admin/src/api/generated/model/linkDataConfirmedAt.ts b/solution/front/src/api/generated/model/linkDataConfirmedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/linkDataConfirmedAt.ts rename to solution/front/src/api/generated/model/linkDataConfirmedAt.ts diff --git a/frontend/admin/src/api/generated/model/linkDataDiscoveredAt.ts b/solution/front/src/api/generated/model/linkDataDiscoveredAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/linkDataDiscoveredAt.ts rename to solution/front/src/api/generated/model/linkDataDiscoveredAt.ts diff --git a/frontend/admin/src/api/generated/model/linkDataTitle.ts b/solution/front/src/api/generated/model/linkDataTitle.ts similarity index 100% rename from frontend/admin/src/api/generated/model/linkDataTitle.ts rename to solution/front/src/api/generated/model/linkDataTitle.ts diff --git a/frontend/admin/src/api/generated/model/listContentsParams.ts b/solution/front/src/api/generated/model/listContentsParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/listContentsParams.ts rename to solution/front/src/api/generated/model/listContentsParams.ts diff --git a/frontend/admin/src/api/generated/model/listFactsParams.ts b/solution/front/src/api/generated/model/listFactsParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/listFactsParams.ts rename to solution/front/src/api/generated/model/listFactsParams.ts diff --git a/frontend/admin/src/api/generated/model/listFaqsParams.ts b/solution/front/src/api/generated/model/listFaqsParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/listFaqsParams.ts rename to solution/front/src/api/generated/model/listFaqsParams.ts diff --git a/frontend/admin/src/api/generated/model/listLinksParams.ts b/solution/front/src/api/generated/model/listLinksParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/listLinksParams.ts rename to solution/front/src/api/generated/model/listLinksParams.ts diff --git a/frontend/admin/src/api/generated/model/listMediaParams.ts b/solution/front/src/api/generated/model/listMediaParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/listMediaParams.ts rename to solution/front/src/api/generated/model/listMediaParams.ts diff --git a/frontend/admin/src/api/generated/model/listPlacesParams.ts b/solution/front/src/api/generated/model/listPlacesParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/listPlacesParams.ts rename to solution/front/src/api/generated/model/listPlacesParams.ts diff --git a/frontend/admin/src/api/generated/model/localContentData.ts b/solution/front/src/api/generated/model/localContentData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentData.ts rename to solution/front/src/api/generated/model/localContentData.ts diff --git a/frontend/admin/src/api/generated/model/localContentDataBody.ts b/solution/front/src/api/generated/model/localContentDataBody.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentDataBody.ts rename to solution/front/src/api/generated/model/localContentDataBody.ts diff --git a/frontend/admin/src/api/generated/model/localContentDataCollectedAt.ts b/solution/front/src/api/generated/model/localContentDataCollectedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentDataCollectedAt.ts rename to solution/front/src/api/generated/model/localContentDataCollectedAt.ts diff --git a/frontend/admin/src/api/generated/model/localContentDataDisplayEndAt.ts b/solution/front/src/api/generated/model/localContentDataDisplayEndAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentDataDisplayEndAt.ts rename to solution/front/src/api/generated/model/localContentDataDisplayEndAt.ts diff --git a/frontend/admin/src/api/generated/model/localContentDataDisplayStartAt.ts b/solution/front/src/api/generated/model/localContentDataDisplayStartAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentDataDisplayStartAt.ts rename to solution/front/src/api/generated/model/localContentDataDisplayStartAt.ts diff --git a/frontend/admin/src/api/generated/model/localContentDataExternalId.ts b/solution/front/src/api/generated/model/localContentDataExternalId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentDataExternalId.ts rename to solution/front/src/api/generated/model/localContentDataExternalId.ts diff --git a/frontend/admin/src/api/generated/model/localContentDataPublishedAt.ts b/solution/front/src/api/generated/model/localContentDataPublishedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentDataPublishedAt.ts rename to solution/front/src/api/generated/model/localContentDataPublishedAt.ts diff --git a/frontend/admin/src/api/generated/model/localContentDataTitle.ts b/solution/front/src/api/generated/model/localContentDataTitle.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentDataTitle.ts rename to solution/front/src/api/generated/model/localContentDataTitle.ts diff --git a/frontend/admin/src/api/generated/model/localContentStatus.ts b/solution/front/src/api/generated/model/localContentStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentStatus.ts rename to solution/front/src/api/generated/model/localContentStatus.ts diff --git a/frontend/admin/src/api/generated/model/localContentType.ts b/solution/front/src/api/generated/model/localContentType.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localContentType.ts rename to solution/front/src/api/generated/model/localContentType.ts diff --git a/frontend/admin/src/api/generated/model/localSource.ts b/solution/front/src/api/generated/model/localSource.ts similarity index 100% rename from frontend/admin/src/api/generated/model/localSource.ts rename to solution/front/src/api/generated/model/localSource.ts diff --git a/frontend/admin/src/api/generated/model/mediaData.ts b/solution/front/src/api/generated/model/mediaData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaData.ts rename to solution/front/src/api/generated/model/mediaData.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataAltText.ts b/solution/front/src/api/generated/model/mediaDataAltText.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataAltText.ts rename to solution/front/src/api/generated/model/mediaDataAltText.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataCreatedAt.ts b/solution/front/src/api/generated/model/mediaDataCreatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataCreatedAt.ts rename to solution/front/src/api/generated/model/mediaDataCreatedAt.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataHeight.ts b/solution/front/src/api/generated/model/mediaDataHeight.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataHeight.ts rename to solution/front/src/api/generated/model/mediaDataHeight.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataLabel.ts b/solution/front/src/api/generated/model/mediaDataLabel.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataLabel.ts rename to solution/front/src/api/generated/model/mediaDataLabel.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataOriginUrl.ts b/solution/front/src/api/generated/model/mediaDataOriginUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataOriginUrl.ts rename to solution/front/src/api/generated/model/mediaDataOriginUrl.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataSourceUrl.ts b/solution/front/src/api/generated/model/mediaDataSourceUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataSourceUrl.ts rename to solution/front/src/api/generated/model/mediaDataSourceUrl.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataUnitId.ts b/solution/front/src/api/generated/model/mediaDataUnitId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataUnitId.ts rename to solution/front/src/api/generated/model/mediaDataUnitId.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataVisionConfidence.ts b/solution/front/src/api/generated/model/mediaDataVisionConfidence.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataVisionConfidence.ts rename to solution/front/src/api/generated/model/mediaDataVisionConfidence.ts diff --git a/frontend/admin/src/api/generated/model/mediaDataWidth.ts b/solution/front/src/api/generated/model/mediaDataWidth.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaDataWidth.ts rename to solution/front/src/api/generated/model/mediaDataWidth.ts diff --git a/frontend/admin/src/api/generated/model/mediaStatus.ts b/solution/front/src/api/generated/model/mediaStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/mediaStatus.ts rename to solution/front/src/api/generated/model/mediaStatus.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidate.ts b/solution/front/src/api/generated/model/placeCandidate.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidate.ts rename to solution/front/src/api/generated/model/placeCandidate.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidateAddress.ts b/solution/front/src/api/generated/model/placeCandidateAddress.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidateAddress.ts rename to solution/front/src/api/generated/model/placeCandidateAddress.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidateCategoryName.ts b/solution/front/src/api/generated/model/placeCandidateCategoryName.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidateCategoryName.ts rename to solution/front/src/api/generated/model/placeCandidateCategoryName.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidateExternalPlaceId.ts b/solution/front/src/api/generated/model/placeCandidateExternalPlaceId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidateExternalPlaceId.ts rename to solution/front/src/api/generated/model/placeCandidateExternalPlaceId.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidateLatitude.ts b/solution/front/src/api/generated/model/placeCandidateLatitude.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidateLatitude.ts rename to solution/front/src/api/generated/model/placeCandidateLatitude.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidateLongitude.ts b/solution/front/src/api/generated/model/placeCandidateLongitude.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidateLongitude.ts rename to solution/front/src/api/generated/model/placeCandidateLongitude.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidateNaverPlaceUrl.ts b/solution/front/src/api/generated/model/placeCandidateNaverPlaceUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidateNaverPlaceUrl.ts rename to solution/front/src/api/generated/model/placeCandidateNaverPlaceUrl.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidatePhone.ts b/solution/front/src/api/generated/model/placeCandidatePhone.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidatePhone.ts rename to solution/front/src/api/generated/model/placeCandidatePhone.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidatePlaceUrl.ts b/solution/front/src/api/generated/model/placeCandidatePlaceUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidatePlaceUrl.ts rename to solution/front/src/api/generated/model/placeCandidatePlaceUrl.ts diff --git a/frontend/admin/src/api/generated/model/placeCandidateRoadAddress.ts b/solution/front/src/api/generated/model/placeCandidateRoadAddress.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCandidateRoadAddress.ts rename to solution/front/src/api/generated/model/placeCandidateRoadAddress.ts diff --git a/frontend/admin/src/api/generated/model/placeCategory.ts b/solution/front/src/api/generated/model/placeCategory.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeCategory.ts rename to solution/front/src/api/generated/model/placeCategory.ts diff --git a/frontend/admin/src/api/generated/model/placeData.ts b/solution/front/src/api/generated/model/placeData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeData.ts rename to solution/front/src/api/generated/model/placeData.ts diff --git a/frontend/admin/src/api/generated/model/placeDataAddress.ts b/solution/front/src/api/generated/model/placeDataAddress.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataAddress.ts rename to solution/front/src/api/generated/model/placeDataAddress.ts diff --git a/frontend/admin/src/api/generated/model/placeDataContentUpdatedAt.ts b/solution/front/src/api/generated/model/placeDataContentUpdatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataContentUpdatedAt.ts rename to solution/front/src/api/generated/model/placeDataContentUpdatedAt.ts diff --git a/frontend/admin/src/api/generated/model/placeDataCreatedAt.ts b/solution/front/src/api/generated/model/placeDataCreatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataCreatedAt.ts rename to solution/front/src/api/generated/model/placeDataCreatedAt.ts diff --git a/frontend/admin/src/api/generated/model/placeDataExternalPlaceId.ts b/solution/front/src/api/generated/model/placeDataExternalPlaceId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataExternalPlaceId.ts rename to solution/front/src/api/generated/model/placeDataExternalPlaceId.ts diff --git a/frontend/admin/src/api/generated/model/placeDataExternalSource.ts b/solution/front/src/api/generated/model/placeDataExternalSource.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataExternalSource.ts rename to solution/front/src/api/generated/model/placeDataExternalSource.ts diff --git a/frontend/admin/src/api/generated/model/placeDataLatitude.ts b/solution/front/src/api/generated/model/placeDataLatitude.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataLatitude.ts rename to solution/front/src/api/generated/model/placeDataLatitude.ts diff --git a/frontend/admin/src/api/generated/model/placeDataLongitude.ts b/solution/front/src/api/generated/model/placeDataLongitude.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataLongitude.ts rename to solution/front/src/api/generated/model/placeDataLongitude.ts diff --git a/frontend/admin/src/api/generated/model/placeDataOwnerUserId.ts b/solution/front/src/api/generated/model/placeDataOwnerUserId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataOwnerUserId.ts rename to solution/front/src/api/generated/model/placeDataOwnerUserId.ts diff --git a/frontend/admin/src/api/generated/model/placeDataPhone.ts b/solution/front/src/api/generated/model/placeDataPhone.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataPhone.ts rename to solution/front/src/api/generated/model/placeDataPhone.ts diff --git a/frontend/admin/src/api/generated/model/placeDataRegionCode.ts b/solution/front/src/api/generated/model/placeDataRegionCode.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataRegionCode.ts rename to solution/front/src/api/generated/model/placeDataRegionCode.ts diff --git a/frontend/admin/src/api/generated/model/placeDataRoadAddress.ts b/solution/front/src/api/generated/model/placeDataRoadAddress.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataRoadAddress.ts rename to solution/front/src/api/generated/model/placeDataRoadAddress.ts diff --git a/frontend/admin/src/api/generated/model/placeDataVerifiedAt.ts b/solution/front/src/api/generated/model/placeDataVerifiedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeDataVerifiedAt.ts rename to solution/front/src/api/generated/model/placeDataVerifiedAt.ts diff --git a/frontend/admin/src/api/generated/model/placeStatus.ts b/solution/front/src/api/generated/model/placeStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/placeStatus.ts rename to solution/front/src/api/generated/model/placeStatus.ts diff --git a/frontend/admin/src/api/generated/model/publishAction.ts b/solution/front/src/api/generated/model/publishAction.ts similarity index 100% rename from frontend/admin/src/api/generated/model/publishAction.ts rename to solution/front/src/api/generated/model/publishAction.ts diff --git a/frontend/admin/src/api/generated/model/publishLogData.ts b/solution/front/src/api/generated/model/publishLogData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/publishLogData.ts rename to solution/front/src/api/generated/model/publishLogData.ts diff --git a/frontend/admin/src/api/generated/model/publishLogDataCreatedAt.ts b/solution/front/src/api/generated/model/publishLogDataCreatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/publishLogDataCreatedAt.ts rename to solution/front/src/api/generated/model/publishLogDataCreatedAt.ts diff --git a/frontend/admin/src/api/generated/model/publishLogDataDetail.ts b/solution/front/src/api/generated/model/publishLogDataDetail.ts similarity index 100% rename from frontend/admin/src/api/generated/model/publishLogDataDetail.ts rename to solution/front/src/api/generated/model/publishLogDataDetail.ts diff --git a/frontend/admin/src/api/generated/model/publishLogDataRejectReason.ts b/solution/front/src/api/generated/model/publishLogDataRejectReason.ts similarity index 100% rename from frontend/admin/src/api/generated/model/publishLogDataRejectReason.ts rename to solution/front/src/api/generated/model/publishLogDataRejectReason.ts diff --git a/frontend/admin/src/api/generated/model/publishRejectReason.ts b/solution/front/src/api/generated/model/publishRejectReason.ts similarity index 100% rename from frontend/admin/src/api/generated/model/publishRejectReason.ts rename to solution/front/src/api/generated/model/publishRejectReason.ts diff --git a/frontend/admin/src/api/generated/model/publishResult.ts b/solution/front/src/api/generated/model/publishResult.ts similarity index 100% rename from frontend/admin/src/api/generated/model/publishResult.ts rename to solution/front/src/api/generated/model/publishResult.ts diff --git a/frontend/admin/src/api/generated/model/renderStatusData.ts b/solution/front/src/api/generated/model/renderStatusData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/renderStatusData.ts rename to solution/front/src/api/generated/model/renderStatusData.ts diff --git a/frontend/admin/src/api/generated/model/renderStatusDataError.ts b/solution/front/src/api/generated/model/renderStatusDataError.ts similarity index 100% rename from frontend/admin/src/api/generated/model/renderStatusDataError.ts rename to solution/front/src/api/generated/model/renderStatusDataError.ts diff --git a/frontend/admin/src/api/generated/model/renderStatusDataRenderedAt.ts b/solution/front/src/api/generated/model/renderStatusDataRenderedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/renderStatusDataRenderedAt.ts rename to solution/front/src/api/generated/model/renderStatusDataRenderedAt.ts diff --git a/frontend/admin/src/api/generated/model/renderStatusDataRenderedVersion.ts b/solution/front/src/api/generated/model/renderStatusDataRenderedVersion.ts similarity index 100% rename from frontend/admin/src/api/generated/model/renderStatusDataRenderedVersion.ts rename to solution/front/src/api/generated/model/renderStatusDataRenderedVersion.ts diff --git a/frontend/admin/src/api/generated/model/reqCreateFaq.ts b/solution/front/src/api/generated/model/reqCreateFaq.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqCreateFaq.ts rename to solution/front/src/api/generated/model/reqCreateFaq.ts diff --git a/frontend/admin/src/api/generated/model/reqCreateFaqSortOrder.ts b/solution/front/src/api/generated/model/reqCreateFaqSortOrder.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqCreateFaqSortOrder.ts rename to solution/front/src/api/generated/model/reqCreateFaqSortOrder.ts diff --git a/frontend/admin/src/api/generated/model/reqCreateLink.ts b/solution/front/src/api/generated/model/reqCreateLink.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqCreateLink.ts rename to solution/front/src/api/generated/model/reqCreateLink.ts diff --git a/frontend/admin/src/api/generated/model/reqCreateLinkTitle.ts b/solution/front/src/api/generated/model/reqCreateLinkTitle.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqCreateLinkTitle.ts rename to solution/front/src/api/generated/model/reqCreateLinkTitle.ts diff --git a/frontend/admin/src/api/generated/model/reqCreatePlace.ts b/solution/front/src/api/generated/model/reqCreatePlace.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqCreatePlace.ts rename to solution/front/src/api/generated/model/reqCreatePlace.ts diff --git a/frontend/admin/src/api/generated/model/reqCreatePlaceOwnerUserId.ts b/solution/front/src/api/generated/model/reqCreatePlaceOwnerUserId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqCreatePlaceOwnerUserId.ts rename to solution/front/src/api/generated/model/reqCreatePlaceOwnerUserId.ts diff --git a/frontend/admin/src/api/generated/model/reqCreateUnit.ts b/solution/front/src/api/generated/model/reqCreateUnit.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqCreateUnit.ts rename to solution/front/src/api/generated/model/reqCreateUnit.ts diff --git a/frontend/admin/src/api/generated/model/reqExtractFacts.ts b/solution/front/src/api/generated/model/reqExtractFacts.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqExtractFacts.ts rename to solution/front/src/api/generated/model/reqExtractFacts.ts diff --git a/frontend/admin/src/api/generated/model/reqLogin.ts b/solution/front/src/api/generated/model/reqLogin.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqLogin.ts rename to solution/front/src/api/generated/model/reqLogin.ts diff --git a/frontend/admin/src/api/generated/model/reqPublishLocalContent.ts b/solution/front/src/api/generated/model/reqPublishLocalContent.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqPublishLocalContent.ts rename to solution/front/src/api/generated/model/reqPublishLocalContent.ts diff --git a/frontend/admin/src/api/generated/model/reqSiteSlug.ts b/solution/front/src/api/generated/model/reqSiteSlug.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSiteSlug.ts rename to solution/front/src/api/generated/model/reqSiteSlug.ts diff --git a/frontend/admin/src/api/generated/model/reqSiteStatus.ts b/solution/front/src/api/generated/model/reqSiteStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSiteStatus.ts rename to solution/front/src/api/generated/model/reqSiteStatus.ts diff --git a/frontend/admin/src/api/generated/model/reqSiteTemplate.ts b/solution/front/src/api/generated/model/reqSiteTemplate.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSiteTemplate.ts rename to solution/front/src/api/generated/model/reqSiteTemplate.ts diff --git a/frontend/admin/src/api/generated/model/reqSiteTheme.ts b/solution/front/src/api/generated/model/reqSiteTheme.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSiteTheme.ts rename to solution/front/src/api/generated/model/reqSiteTheme.ts diff --git a/frontend/admin/src/api/generated/model/reqSiteThemeTheme.ts b/solution/front/src/api/generated/model/reqSiteThemeTheme.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSiteThemeTheme.ts rename to solution/front/src/api/generated/model/reqSiteThemeTheme.ts diff --git a/frontend/admin/src/api/generated/model/reqStartBuild.ts b/solution/front/src/api/generated/model/reqStartBuild.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqStartBuild.ts rename to solution/front/src/api/generated/model/reqStartBuild.ts diff --git a/frontend/admin/src/api/generated/model/reqStartCollect.ts b/solution/front/src/api/generated/model/reqStartCollect.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqStartCollect.ts rename to solution/front/src/api/generated/model/reqStartCollect.ts diff --git a/frontend/admin/src/api/generated/model/reqStartCopy.ts b/solution/front/src/api/generated/model/reqStartCopy.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqStartCopy.ts rename to solution/front/src/api/generated/model/reqStartCopy.ts diff --git a/frontend/admin/src/api/generated/model/reqStartVision.ts b/solution/front/src/api/generated/model/reqStartVision.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqStartVision.ts rename to solution/front/src/api/generated/model/reqStartVision.ts diff --git a/frontend/admin/src/api/generated/model/reqSyncFestivals.ts b/solution/front/src/api/generated/model/reqSyncFestivals.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSyncFestivals.ts rename to solution/front/src/api/generated/model/reqSyncFestivals.ts diff --git a/frontend/admin/src/api/generated/model/reqSyncFestivalsAreaCode.ts b/solution/front/src/api/generated/model/reqSyncFestivalsAreaCode.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSyncFestivalsAreaCode.ts rename to solution/front/src/api/generated/model/reqSyncFestivalsAreaCode.ts diff --git a/frontend/admin/src/api/generated/model/reqSyncFestivalsStartDate.ts b/solution/front/src/api/generated/model/reqSyncFestivalsStartDate.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqSyncFestivalsStartDate.ts rename to solution/front/src/api/generated/model/reqSyncFestivalsStartDate.ts diff --git a/frontend/admin/src/api/generated/model/reqTransitionFact.ts b/solution/front/src/api/generated/model/reqTransitionFact.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqTransitionFact.ts rename to solution/front/src/api/generated/model/reqTransitionFact.ts diff --git a/frontend/admin/src/api/generated/model/reqTransitionFactValue.ts b/solution/front/src/api/generated/model/reqTransitionFactValue.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqTransitionFactValue.ts rename to solution/front/src/api/generated/model/reqTransitionFactValue.ts diff --git a/frontend/admin/src/api/generated/model/reqTransitionFaq.ts b/solution/front/src/api/generated/model/reqTransitionFaq.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqTransitionFaq.ts rename to solution/front/src/api/generated/model/reqTransitionFaq.ts diff --git a/frontend/admin/src/api/generated/model/reqTransitionFaqAnswer.ts b/solution/front/src/api/generated/model/reqTransitionFaqAnswer.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqTransitionFaqAnswer.ts rename to solution/front/src/api/generated/model/reqTransitionFaqAnswer.ts diff --git a/frontend/admin/src/api/generated/model/reqTransitionFaqQuestion.ts b/solution/front/src/api/generated/model/reqTransitionFaqQuestion.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqTransitionFaqQuestion.ts rename to solution/front/src/api/generated/model/reqTransitionFaqQuestion.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateLocalContent.ts b/solution/front/src/api/generated/model/reqUpdateLocalContent.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateLocalContent.ts rename to solution/front/src/api/generated/model/reqUpdateLocalContent.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateLocalContentBody.ts b/solution/front/src/api/generated/model/reqUpdateLocalContentBody.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateLocalContentBody.ts rename to solution/front/src/api/generated/model/reqUpdateLocalContentBody.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateLocalContentBodyAnyOf.ts b/solution/front/src/api/generated/model/reqUpdateLocalContentBodyAnyOf.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateLocalContentBodyAnyOf.ts rename to solution/front/src/api/generated/model/reqUpdateLocalContentBodyAnyOf.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateLocalContentDisplayEndAt.ts b/solution/front/src/api/generated/model/reqUpdateLocalContentDisplayEndAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateLocalContentDisplayEndAt.ts rename to solution/front/src/api/generated/model/reqUpdateLocalContentDisplayEndAt.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateLocalContentDisplayStartAt.ts b/solution/front/src/api/generated/model/reqUpdateLocalContentDisplayStartAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateLocalContentDisplayStartAt.ts rename to solution/front/src/api/generated/model/reqUpdateLocalContentDisplayStartAt.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateLocalContentTitle.ts b/solution/front/src/api/generated/model/reqUpdateLocalContentTitle.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateLocalContentTitle.ts rename to solution/front/src/api/generated/model/reqUpdateLocalContentTitle.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateMe.ts b/solution/front/src/api/generated/model/reqUpdateMe.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateMe.ts rename to solution/front/src/api/generated/model/reqUpdateMe.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateMeContactNumber.ts b/solution/front/src/api/generated/model/reqUpdateMeContactNumber.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateMeContactNumber.ts rename to solution/front/src/api/generated/model/reqUpdateMeContactNumber.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateMeEmail.ts b/solution/front/src/api/generated/model/reqUpdateMeEmail.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateMeEmail.ts rename to solution/front/src/api/generated/model/reqUpdateMeEmail.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateMeName.ts b/solution/front/src/api/generated/model/reqUpdateMeName.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateMeName.ts rename to solution/front/src/api/generated/model/reqUpdateMeName.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdateMePassword.ts b/solution/front/src/api/generated/model/reqUpdateMePassword.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdateMePassword.ts rename to solution/front/src/api/generated/model/reqUpdateMePassword.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdatePlace.ts b/solution/front/src/api/generated/model/reqUpdatePlace.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdatePlace.ts rename to solution/front/src/api/generated/model/reqUpdatePlace.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdatePlaceName.ts b/solution/front/src/api/generated/model/reqUpdatePlaceName.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdatePlaceName.ts rename to solution/front/src/api/generated/model/reqUpdatePlaceName.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdatePlaceOwnerUserId.ts b/solution/front/src/api/generated/model/reqUpdatePlaceOwnerUserId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdatePlaceOwnerUserId.ts rename to solution/front/src/api/generated/model/reqUpdatePlaceOwnerUserId.ts diff --git a/frontend/admin/src/api/generated/model/reqUpdatePlaceStatus.ts b/solution/front/src/api/generated/model/reqUpdatePlaceStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpdatePlaceStatus.ts rename to solution/front/src/api/generated/model/reqUpdatePlaceStatus.ts diff --git a/frontend/admin/src/api/generated/model/reqUpsertFact.ts b/solution/front/src/api/generated/model/reqUpsertFact.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpsertFact.ts rename to solution/front/src/api/generated/model/reqUpsertFact.ts diff --git a/frontend/admin/src/api/generated/model/reqUpsertFactExpiresAt.ts b/solution/front/src/api/generated/model/reqUpsertFactExpiresAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpsertFactExpiresAt.ts rename to solution/front/src/api/generated/model/reqUpsertFactExpiresAt.ts diff --git a/frontend/admin/src/api/generated/model/reqUpsertFactSourceUrl.ts b/solution/front/src/api/generated/model/reqUpsertFactSourceUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpsertFactSourceUrl.ts rename to solution/front/src/api/generated/model/reqUpsertFactSourceUrl.ts diff --git a/frontend/admin/src/api/generated/model/reqUpsertFactUnitId.ts b/solution/front/src/api/generated/model/reqUpsertFactUnitId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpsertFactUnitId.ts rename to solution/front/src/api/generated/model/reqUpsertFactUnitId.ts diff --git a/frontend/admin/src/api/generated/model/reqUpsertFactValue.ts b/solution/front/src/api/generated/model/reqUpsertFactValue.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqUpsertFactValue.ts rename to solution/front/src/api/generated/model/reqUpsertFactValue.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlace.ts b/solution/front/src/api/generated/model/reqVerifyPlace.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlace.ts rename to solution/front/src/api/generated/model/reqVerifyPlace.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlaceAddress.ts b/solution/front/src/api/generated/model/reqVerifyPlaceAddress.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlaceAddress.ts rename to solution/front/src/api/generated/model/reqVerifyPlaceAddress.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlaceByUrl.ts b/solution/front/src/api/generated/model/reqVerifyPlaceByUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlaceByUrl.ts rename to solution/front/src/api/generated/model/reqVerifyPlaceByUrl.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlaceLatitude.ts b/solution/front/src/api/generated/model/reqVerifyPlaceLatitude.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlaceLatitude.ts rename to solution/front/src/api/generated/model/reqVerifyPlaceLatitude.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlaceLongitude.ts b/solution/front/src/api/generated/model/reqVerifyPlaceLongitude.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlaceLongitude.ts rename to solution/front/src/api/generated/model/reqVerifyPlaceLongitude.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlacePhone.ts b/solution/front/src/api/generated/model/reqVerifyPlacePhone.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlacePhone.ts rename to solution/front/src/api/generated/model/reqVerifyPlacePhone.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlacePlaceUrl.ts b/solution/front/src/api/generated/model/reqVerifyPlacePlaceUrl.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlacePlaceUrl.ts rename to solution/front/src/api/generated/model/reqVerifyPlacePlaceUrl.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlaceRegionCode.ts b/solution/front/src/api/generated/model/reqVerifyPlaceRegionCode.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlaceRegionCode.ts rename to solution/front/src/api/generated/model/reqVerifyPlaceRegionCode.ts diff --git a/frontend/admin/src/api/generated/model/reqVerifyPlaceRoadAddress.ts b/solution/front/src/api/generated/model/reqVerifyPlaceRoadAddress.ts similarity index 100% rename from frontend/admin/src/api/generated/model/reqVerifyPlaceRoadAddress.ts rename to solution/front/src/api/generated/model/reqVerifyPlaceRoadAddress.ts diff --git a/frontend/admin/src/api/generated/model/resCategorySchema.ts b/solution/front/src/api/generated/model/resCategorySchema.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resCategorySchema.ts rename to solution/front/src/api/generated/model/resCategorySchema.ts diff --git a/frontend/admin/src/api/generated/model/resCategorySchemaCategory.ts b/solution/front/src/api/generated/model/resCategorySchemaCategory.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resCategorySchemaCategory.ts rename to solution/front/src/api/generated/model/resCategorySchemaCategory.ts diff --git a/frontend/admin/src/api/generated/model/resCategorySchemaLabel.ts b/solution/front/src/api/generated/model/resCategorySchemaLabel.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resCategorySchemaLabel.ts rename to solution/front/src/api/generated/model/resCategorySchemaLabel.ts diff --git a/frontend/admin/src/api/generated/model/resCategorySchemaMsg.ts b/solution/front/src/api/generated/model/resCategorySchemaMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resCategorySchemaMsg.ts rename to solution/front/src/api/generated/model/resCategorySchemaMsg.ts diff --git a/frontend/admin/src/api/generated/model/resExtractFacts.ts b/solution/front/src/api/generated/model/resExtractFacts.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resExtractFacts.ts rename to solution/front/src/api/generated/model/resExtractFacts.ts diff --git a/frontend/admin/src/api/generated/model/resExtractFactsMsg.ts b/solution/front/src/api/generated/model/resExtractFactsMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resExtractFactsMsg.ts rename to solution/front/src/api/generated/model/resExtractFactsMsg.ts diff --git a/frontend/admin/src/api/generated/model/resExtractedFact.ts b/solution/front/src/api/generated/model/resExtractedFact.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resExtractedFact.ts rename to solution/front/src/api/generated/model/resExtractedFact.ts diff --git a/frontend/admin/src/api/generated/model/resExtractedFactReason.ts b/solution/front/src/api/generated/model/resExtractedFactReason.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resExtractedFactReason.ts rename to solution/front/src/api/generated/model/resExtractedFactReason.ts diff --git a/frontend/admin/src/api/generated/model/resExtractedFactUnitName.ts b/solution/front/src/api/generated/model/resExtractedFactUnitName.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resExtractedFactUnitName.ts rename to solution/front/src/api/generated/model/resExtractedFactUnitName.ts diff --git a/frontend/admin/src/api/generated/model/resFact.ts b/solution/front/src/api/generated/model/resFact.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFact.ts rename to solution/front/src/api/generated/model/resFact.ts diff --git a/frontend/admin/src/api/generated/model/resFactFact.ts b/solution/front/src/api/generated/model/resFactFact.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFactFact.ts rename to solution/front/src/api/generated/model/resFactFact.ts diff --git a/frontend/admin/src/api/generated/model/resFactList.ts b/solution/front/src/api/generated/model/resFactList.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFactList.ts rename to solution/front/src/api/generated/model/resFactList.ts diff --git a/frontend/admin/src/api/generated/model/resFactListMsg.ts b/solution/front/src/api/generated/model/resFactListMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFactListMsg.ts rename to solution/front/src/api/generated/model/resFactListMsg.ts diff --git a/frontend/admin/src/api/generated/model/resFactMsg.ts b/solution/front/src/api/generated/model/resFactMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFactMsg.ts rename to solution/front/src/api/generated/model/resFactMsg.ts diff --git a/frontend/admin/src/api/generated/model/resFactOutcome.ts b/solution/front/src/api/generated/model/resFactOutcome.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFactOutcome.ts rename to solution/front/src/api/generated/model/resFactOutcome.ts diff --git a/frontend/admin/src/api/generated/model/resFaq.ts b/solution/front/src/api/generated/model/resFaq.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFaq.ts rename to solution/front/src/api/generated/model/resFaq.ts diff --git a/frontend/admin/src/api/generated/model/resFaqFaq.ts b/solution/front/src/api/generated/model/resFaqFaq.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFaqFaq.ts rename to solution/front/src/api/generated/model/resFaqFaq.ts diff --git a/frontend/admin/src/api/generated/model/resFaqList.ts b/solution/front/src/api/generated/model/resFaqList.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFaqList.ts rename to solution/front/src/api/generated/model/resFaqList.ts diff --git a/frontend/admin/src/api/generated/model/resFaqListMsg.ts b/solution/front/src/api/generated/model/resFaqListMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFaqListMsg.ts rename to solution/front/src/api/generated/model/resFaqListMsg.ts diff --git a/frontend/admin/src/api/generated/model/resFaqMsg.ts b/solution/front/src/api/generated/model/resFaqMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resFaqMsg.ts rename to solution/front/src/api/generated/model/resFaqMsg.ts diff --git a/frontend/admin/src/api/generated/model/resJob.ts b/solution/front/src/api/generated/model/resJob.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resJob.ts rename to solution/front/src/api/generated/model/resJob.ts diff --git a/frontend/admin/src/api/generated/model/resJobJob.ts b/solution/front/src/api/generated/model/resJobJob.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resJobJob.ts rename to solution/front/src/api/generated/model/resJobJob.ts diff --git a/frontend/admin/src/api/generated/model/resJobMsg.ts b/solution/front/src/api/generated/model/resJobMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resJobMsg.ts rename to solution/front/src/api/generated/model/resJobMsg.ts diff --git a/frontend/admin/src/api/generated/model/resJobOps.ts b/solution/front/src/api/generated/model/resJobOps.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resJobOps.ts rename to solution/front/src/api/generated/model/resJobOps.ts diff --git a/frontend/admin/src/api/generated/model/resJobOpsMsg.ts b/solution/front/src/api/generated/model/resJobOpsMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resJobOpsMsg.ts rename to solution/front/src/api/generated/model/resJobOpsMsg.ts diff --git a/frontend/admin/src/api/generated/model/resLink.ts b/solution/front/src/api/generated/model/resLink.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLink.ts rename to solution/front/src/api/generated/model/resLink.ts diff --git a/frontend/admin/src/api/generated/model/resLinkLink.ts b/solution/front/src/api/generated/model/resLinkLink.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLinkLink.ts rename to solution/front/src/api/generated/model/resLinkLink.ts diff --git a/frontend/admin/src/api/generated/model/resLinkList.ts b/solution/front/src/api/generated/model/resLinkList.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLinkList.ts rename to solution/front/src/api/generated/model/resLinkList.ts diff --git a/frontend/admin/src/api/generated/model/resLinkListMsg.ts b/solution/front/src/api/generated/model/resLinkListMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLinkListMsg.ts rename to solution/front/src/api/generated/model/resLinkListMsg.ts diff --git a/frontend/admin/src/api/generated/model/resLinkMsg.ts b/solution/front/src/api/generated/model/resLinkMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLinkMsg.ts rename to solution/front/src/api/generated/model/resLinkMsg.ts diff --git a/frontend/admin/src/api/generated/model/resLocalContentList.ts b/solution/front/src/api/generated/model/resLocalContentList.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLocalContentList.ts rename to solution/front/src/api/generated/model/resLocalContentList.ts diff --git a/frontend/admin/src/api/generated/model/resLocalContentListMsg.ts b/solution/front/src/api/generated/model/resLocalContentListMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLocalContentListMsg.ts rename to solution/front/src/api/generated/model/resLocalContentListMsg.ts diff --git a/frontend/admin/src/api/generated/model/resLogin.ts b/solution/front/src/api/generated/model/resLogin.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLogin.ts rename to solution/front/src/api/generated/model/resLogin.ts diff --git a/frontend/admin/src/api/generated/model/resLoginMsg.ts b/solution/front/src/api/generated/model/resLoginMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resLoginMsg.ts rename to solution/front/src/api/generated/model/resLoginMsg.ts diff --git a/frontend/admin/src/api/generated/model/resMe.ts b/solution/front/src/api/generated/model/resMe.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMe.ts rename to solution/front/src/api/generated/model/resMe.ts diff --git a/frontend/admin/src/api/generated/model/resMeCompany.ts b/solution/front/src/api/generated/model/resMeCompany.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMeCompany.ts rename to solution/front/src/api/generated/model/resMeCompany.ts diff --git a/frontend/admin/src/api/generated/model/resMeContactNumber.ts b/solution/front/src/api/generated/model/resMeContactNumber.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMeContactNumber.ts rename to solution/front/src/api/generated/model/resMeContactNumber.ts diff --git a/frontend/admin/src/api/generated/model/resMeEmail.ts b/solution/front/src/api/generated/model/resMeEmail.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMeEmail.ts rename to solution/front/src/api/generated/model/resMeEmail.ts diff --git a/frontend/admin/src/api/generated/model/resMeMsg.ts b/solution/front/src/api/generated/model/resMeMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMeMsg.ts rename to solution/front/src/api/generated/model/resMeMsg.ts diff --git a/frontend/admin/src/api/generated/model/resMeName.ts b/solution/front/src/api/generated/model/resMeName.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMeName.ts rename to solution/front/src/api/generated/model/resMeName.ts diff --git a/frontend/admin/src/api/generated/model/resMediaList.ts b/solution/front/src/api/generated/model/resMediaList.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMediaList.ts rename to solution/front/src/api/generated/model/resMediaList.ts diff --git a/frontend/admin/src/api/generated/model/resMediaListMsg.ts b/solution/front/src/api/generated/model/resMediaListMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resMediaListMsg.ts rename to solution/front/src/api/generated/model/resMediaListMsg.ts diff --git a/frontend/admin/src/api/generated/model/resPlace.ts b/solution/front/src/api/generated/model/resPlace.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resPlace.ts rename to solution/front/src/api/generated/model/resPlace.ts diff --git a/frontend/admin/src/api/generated/model/resPlaceList.ts b/solution/front/src/api/generated/model/resPlaceList.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resPlaceList.ts rename to solution/front/src/api/generated/model/resPlaceList.ts diff --git a/frontend/admin/src/api/generated/model/resPlaceListMsg.ts b/solution/front/src/api/generated/model/resPlaceListMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resPlaceListMsg.ts rename to solution/front/src/api/generated/model/resPlaceListMsg.ts diff --git a/frontend/admin/src/api/generated/model/resPlaceMsg.ts b/solution/front/src/api/generated/model/resPlaceMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resPlaceMsg.ts rename to solution/front/src/api/generated/model/resPlaceMsg.ts diff --git a/frontend/admin/src/api/generated/model/resPlacePlace.ts b/solution/front/src/api/generated/model/resPlacePlace.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resPlacePlace.ts rename to solution/front/src/api/generated/model/resPlacePlace.ts diff --git a/frontend/admin/src/api/generated/model/resPublishLogs.ts b/solution/front/src/api/generated/model/resPublishLogs.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resPublishLogs.ts rename to solution/front/src/api/generated/model/resPublishLogs.ts diff --git a/frontend/admin/src/api/generated/model/resPublishLogsMsg.ts b/solution/front/src/api/generated/model/resPublishLogsMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resPublishLogsMsg.ts rename to solution/front/src/api/generated/model/resPublishLogsMsg.ts diff --git a/frontend/admin/src/api/generated/model/resRefreshToken.ts b/solution/front/src/api/generated/model/resRefreshToken.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resRefreshToken.ts rename to solution/front/src/api/generated/model/resRefreshToken.ts diff --git a/frontend/admin/src/api/generated/model/resRefreshTokenMsg.ts b/solution/front/src/api/generated/model/resRefreshTokenMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resRefreshTokenMsg.ts rename to solution/front/src/api/generated/model/resRefreshTokenMsg.ts diff --git a/frontend/admin/src/api/generated/model/resSeoAudit.ts b/solution/front/src/api/generated/model/resSeoAudit.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSeoAudit.ts rename to solution/front/src/api/generated/model/resSeoAudit.ts diff --git a/frontend/admin/src/api/generated/model/resSeoAuditMsg.ts b/solution/front/src/api/generated/model/resSeoAuditMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSeoAuditMsg.ts rename to solution/front/src/api/generated/model/resSeoAuditMsg.ts diff --git a/frontend/admin/src/api/generated/model/resSeoAuditSummary.ts b/solution/front/src/api/generated/model/resSeoAuditSummary.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSeoAuditSummary.ts rename to solution/front/src/api/generated/model/resSeoAuditSummary.ts diff --git a/frontend/admin/src/api/generated/model/resSeoAuditVisibility.ts b/solution/front/src/api/generated/model/resSeoAuditVisibility.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSeoAuditVisibility.ts rename to solution/front/src/api/generated/model/resSeoAuditVisibility.ts diff --git a/frontend/admin/src/api/generated/model/resSite.ts b/solution/front/src/api/generated/model/resSite.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSite.ts rename to solution/front/src/api/generated/model/resSite.ts diff --git a/frontend/admin/src/api/generated/model/resSiteCurrentVersion.ts b/solution/front/src/api/generated/model/resSiteCurrentVersion.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteCurrentVersion.ts rename to solution/front/src/api/generated/model/resSiteCurrentVersion.ts diff --git a/frontend/admin/src/api/generated/model/resSiteMsg.ts b/solution/front/src/api/generated/model/resSiteMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteMsg.ts rename to solution/front/src/api/generated/model/resSiteMsg.ts diff --git a/frontend/admin/src/api/generated/model/resSiteSite.ts b/solution/front/src/api/generated/model/resSiteSite.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteSite.ts rename to solution/front/src/api/generated/model/resSiteSite.ts diff --git a/frontend/admin/src/api/generated/model/resSiteSlug.ts b/solution/front/src/api/generated/model/resSiteSlug.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteSlug.ts rename to solution/front/src/api/generated/model/resSiteSlug.ts diff --git a/frontend/admin/src/api/generated/model/resSiteSlugMsg.ts b/solution/front/src/api/generated/model/resSiteSlugMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteSlugMsg.ts rename to solution/front/src/api/generated/model/resSiteSlugMsg.ts diff --git a/frontend/admin/src/api/generated/model/resSiteSlugReason.ts b/solution/front/src/api/generated/model/resSiteSlugReason.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteSlugReason.ts rename to solution/front/src/api/generated/model/resSiteSlugReason.ts diff --git a/frontend/admin/src/api/generated/model/resSiteSlugSite.ts b/solution/front/src/api/generated/model/resSiteSlugSite.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteSlugSite.ts rename to solution/front/src/api/generated/model/resSiteSlugSite.ts diff --git a/frontend/admin/src/api/generated/model/resSiteSlugSuggestion.ts b/solution/front/src/api/generated/model/resSiteSlugSuggestion.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteSlugSuggestion.ts rename to solution/front/src/api/generated/model/resSiteSlugSuggestion.ts diff --git a/frontend/admin/src/api/generated/model/resSiteVersions.ts b/solution/front/src/api/generated/model/resSiteVersions.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteVersions.ts rename to solution/front/src/api/generated/model/resSiteVersions.ts diff --git a/frontend/admin/src/api/generated/model/resSiteVersionsMsg.ts b/solution/front/src/api/generated/model/resSiteVersionsMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSiteVersionsMsg.ts rename to solution/front/src/api/generated/model/resSiteVersionsMsg.ts diff --git a/frontend/admin/src/api/generated/model/resSlugCheck.ts b/solution/front/src/api/generated/model/resSlugCheck.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSlugCheck.ts rename to solution/front/src/api/generated/model/resSlugCheck.ts diff --git a/frontend/admin/src/api/generated/model/resSlugCheckMsg.ts b/solution/front/src/api/generated/model/resSlugCheckMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSlugCheckMsg.ts rename to solution/front/src/api/generated/model/resSlugCheckMsg.ts diff --git a/frontend/admin/src/api/generated/model/resSlugCheckReason.ts b/solution/front/src/api/generated/model/resSlugCheckReason.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSlugCheckReason.ts rename to solution/front/src/api/generated/model/resSlugCheckReason.ts diff --git a/frontend/admin/src/api/generated/model/resSlugCheckSuggestion.ts b/solution/front/src/api/generated/model/resSlugCheckSuggestion.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSlugCheckSuggestion.ts rename to solution/front/src/api/generated/model/resSlugCheckSuggestion.ts diff --git a/frontend/admin/src/api/generated/model/resStartBuild.ts b/solution/front/src/api/generated/model/resStartBuild.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartBuild.ts rename to solution/front/src/api/generated/model/resStartBuild.ts diff --git a/frontend/admin/src/api/generated/model/resStartBuildJobId.ts b/solution/front/src/api/generated/model/resStartBuildJobId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartBuildJobId.ts rename to solution/front/src/api/generated/model/resStartBuildJobId.ts diff --git a/frontend/admin/src/api/generated/model/resStartBuildMsg.ts b/solution/front/src/api/generated/model/resStartBuildMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartBuildMsg.ts rename to solution/front/src/api/generated/model/resStartBuildMsg.ts diff --git a/frontend/admin/src/api/generated/model/resStartBuildStatus.ts b/solution/front/src/api/generated/model/resStartBuildStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartBuildStatus.ts rename to solution/front/src/api/generated/model/resStartBuildStatus.ts diff --git a/frontend/admin/src/api/generated/model/resStartCollect.ts b/solution/front/src/api/generated/model/resStartCollect.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCollect.ts rename to solution/front/src/api/generated/model/resStartCollect.ts diff --git a/frontend/admin/src/api/generated/model/resStartCollectJobId.ts b/solution/front/src/api/generated/model/resStartCollectJobId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCollectJobId.ts rename to solution/front/src/api/generated/model/resStartCollectJobId.ts diff --git a/frontend/admin/src/api/generated/model/resStartCollectMsg.ts b/solution/front/src/api/generated/model/resStartCollectMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCollectMsg.ts rename to solution/front/src/api/generated/model/resStartCollectMsg.ts diff --git a/frontend/admin/src/api/generated/model/resStartCollectStatus.ts b/solution/front/src/api/generated/model/resStartCollectStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCollectStatus.ts rename to solution/front/src/api/generated/model/resStartCollectStatus.ts diff --git a/frontend/admin/src/api/generated/model/resStartCopy.ts b/solution/front/src/api/generated/model/resStartCopy.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCopy.ts rename to solution/front/src/api/generated/model/resStartCopy.ts diff --git a/frontend/admin/src/api/generated/model/resStartCopyJobId.ts b/solution/front/src/api/generated/model/resStartCopyJobId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCopyJobId.ts rename to solution/front/src/api/generated/model/resStartCopyJobId.ts diff --git a/frontend/admin/src/api/generated/model/resStartCopyMsg.ts b/solution/front/src/api/generated/model/resStartCopyMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCopyMsg.ts rename to solution/front/src/api/generated/model/resStartCopyMsg.ts diff --git a/frontend/admin/src/api/generated/model/resStartCopyStatus.ts b/solution/front/src/api/generated/model/resStartCopyStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartCopyStatus.ts rename to solution/front/src/api/generated/model/resStartCopyStatus.ts diff --git a/frontend/admin/src/api/generated/model/resStartVision.ts b/solution/front/src/api/generated/model/resStartVision.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartVision.ts rename to solution/front/src/api/generated/model/resStartVision.ts diff --git a/frontend/admin/src/api/generated/model/resStartVisionJobId.ts b/solution/front/src/api/generated/model/resStartVisionJobId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartVisionJobId.ts rename to solution/front/src/api/generated/model/resStartVisionJobId.ts diff --git a/frontend/admin/src/api/generated/model/resStartVisionMsg.ts b/solution/front/src/api/generated/model/resStartVisionMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartVisionMsg.ts rename to solution/front/src/api/generated/model/resStartVisionMsg.ts diff --git a/frontend/admin/src/api/generated/model/resStartVisionStatus.ts b/solution/front/src/api/generated/model/resStartVisionStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resStartVisionStatus.ts rename to solution/front/src/api/generated/model/resStartVisionStatus.ts diff --git a/frontend/admin/src/api/generated/model/resSyncFestivals.ts b/solution/front/src/api/generated/model/resSyncFestivals.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSyncFestivals.ts rename to solution/front/src/api/generated/model/resSyncFestivals.ts diff --git a/frontend/admin/src/api/generated/model/resSyncFestivalsMsg.ts b/solution/front/src/api/generated/model/resSyncFestivalsMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resSyncFestivalsMsg.ts rename to solution/front/src/api/generated/model/resSyncFestivalsMsg.ts diff --git a/frontend/admin/src/api/generated/model/resUnit.ts b/solution/front/src/api/generated/model/resUnit.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resUnit.ts rename to solution/front/src/api/generated/model/resUnit.ts diff --git a/frontend/admin/src/api/generated/model/resUnitList.ts b/solution/front/src/api/generated/model/resUnitList.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resUnitList.ts rename to solution/front/src/api/generated/model/resUnitList.ts diff --git a/frontend/admin/src/api/generated/model/resUnitListMsg.ts b/solution/front/src/api/generated/model/resUnitListMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resUnitListMsg.ts rename to solution/front/src/api/generated/model/resUnitListMsg.ts diff --git a/frontend/admin/src/api/generated/model/resUnitMsg.ts b/solution/front/src/api/generated/model/resUnitMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resUnitMsg.ts rename to solution/front/src/api/generated/model/resUnitMsg.ts diff --git a/frontend/admin/src/api/generated/model/resUnitUnit.ts b/solution/front/src/api/generated/model/resUnitUnit.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resUnitUnit.ts rename to solution/front/src/api/generated/model/resUnitUnit.ts diff --git a/frontend/admin/src/api/generated/model/resVerifyCandidates.ts b/solution/front/src/api/generated/model/resVerifyCandidates.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resVerifyCandidates.ts rename to solution/front/src/api/generated/model/resVerifyCandidates.ts diff --git a/frontend/admin/src/api/generated/model/resVerifyCandidatesMsg.ts b/solution/front/src/api/generated/model/resVerifyCandidatesMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resVerifyCandidatesMsg.ts rename to solution/front/src/api/generated/model/resVerifyCandidatesMsg.ts diff --git a/frontend/admin/src/api/generated/model/resVerifyCandidatesReason.ts b/solution/front/src/api/generated/model/resVerifyCandidatesReason.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resVerifyCandidatesReason.ts rename to solution/front/src/api/generated/model/resVerifyCandidatesReason.ts diff --git a/frontend/admin/src/api/generated/model/resVerifyCandidatesSource.ts b/solution/front/src/api/generated/model/resVerifyCandidatesSource.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resVerifyCandidatesSource.ts rename to solution/front/src/api/generated/model/resVerifyCandidatesSource.ts diff --git a/frontend/admin/src/api/generated/model/resWeather.ts b/solution/front/src/api/generated/model/resWeather.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resWeather.ts rename to solution/front/src/api/generated/model/resWeather.ts diff --git a/frontend/admin/src/api/generated/model/resWeatherMsg.ts b/solution/front/src/api/generated/model/resWeatherMsg.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resWeatherMsg.ts rename to solution/front/src/api/generated/model/resWeatherMsg.ts diff --git a/frontend/admin/src/api/generated/model/resWeatherWeather.ts b/solution/front/src/api/generated/model/resWeatherWeather.ts similarity index 100% rename from frontend/admin/src/api/generated/model/resWeatherWeather.ts rename to solution/front/src/api/generated/model/resWeatherWeather.ts diff --git a/frontend/admin/src/api/generated/model/siteData.ts b/solution/front/src/api/generated/model/siteData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteData.ts rename to solution/front/src/api/generated/model/siteData.ts diff --git a/frontend/admin/src/api/generated/model/siteDataCurrentVersionId.ts b/solution/front/src/api/generated/model/siteDataCurrentVersionId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteDataCurrentVersionId.ts rename to solution/front/src/api/generated/model/siteDataCurrentVersionId.ts diff --git a/frontend/admin/src/api/generated/model/siteDataDomain.ts b/solution/front/src/api/generated/model/siteDataDomain.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteDataDomain.ts rename to solution/front/src/api/generated/model/siteDataDomain.ts diff --git a/frontend/admin/src/api/generated/model/siteDataPublishedAt.ts b/solution/front/src/api/generated/model/siteDataPublishedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteDataPublishedAt.ts rename to solution/front/src/api/generated/model/siteDataPublishedAt.ts diff --git a/frontend/admin/src/api/generated/model/siteDataTemplateId.ts b/solution/front/src/api/generated/model/siteDataTemplateId.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteDataTemplateId.ts rename to solution/front/src/api/generated/model/siteDataTemplateId.ts diff --git a/frontend/admin/src/api/generated/model/siteDataTheme.ts b/solution/front/src/api/generated/model/siteDataTheme.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteDataTheme.ts rename to solution/front/src/api/generated/model/siteDataTheme.ts diff --git a/frontend/admin/src/api/generated/model/siteDataThemeAnyOf.ts b/solution/front/src/api/generated/model/siteDataThemeAnyOf.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteDataThemeAnyOf.ts rename to solution/front/src/api/generated/model/siteDataThemeAnyOf.ts diff --git a/frontend/admin/src/api/generated/model/siteStatus.ts b/solution/front/src/api/generated/model/siteStatus.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteStatus.ts rename to solution/front/src/api/generated/model/siteStatus.ts diff --git a/frontend/admin/src/api/generated/model/siteVersionData.ts b/solution/front/src/api/generated/model/siteVersionData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteVersionData.ts rename to solution/front/src/api/generated/model/siteVersionData.ts diff --git a/frontend/admin/src/api/generated/model/siteVersionDataBuildError.ts b/solution/front/src/api/generated/model/siteVersionDataBuildError.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteVersionDataBuildError.ts rename to solution/front/src/api/generated/model/siteVersionDataBuildError.ts diff --git a/frontend/admin/src/api/generated/model/siteVersionDataBuiltAt.ts b/solution/front/src/api/generated/model/siteVersionDataBuiltAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteVersionDataBuiltAt.ts rename to solution/front/src/api/generated/model/siteVersionDataBuiltAt.ts diff --git a/frontend/admin/src/api/generated/model/siteVersionDataCreatedAt.ts b/solution/front/src/api/generated/model/siteVersionDataCreatedAt.ts similarity index 100% rename from frontend/admin/src/api/generated/model/siteVersionDataCreatedAt.ts rename to solution/front/src/api/generated/model/siteVersionDataCreatedAt.ts diff --git a/frontend/admin/src/api/generated/model/sourceType.ts b/solution/front/src/api/generated/model/sourceType.ts similarity index 100% rename from frontend/admin/src/api/generated/model/sourceType.ts rename to solution/front/src/api/generated/model/sourceType.ts diff --git a/frontend/admin/src/api/generated/model/unitData.ts b/solution/front/src/api/generated/model/unitData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/unitData.ts rename to solution/front/src/api/generated/model/unitData.ts diff --git a/frontend/admin/src/api/generated/model/userRole.ts b/solution/front/src/api/generated/model/userRole.ts similarity index 100% rename from frontend/admin/src/api/generated/model/userRole.ts rename to solution/front/src/api/generated/model/userRole.ts diff --git a/frontend/admin/src/api/generated/model/validationError.ts b/solution/front/src/api/generated/model/validationError.ts similarity index 100% rename from frontend/admin/src/api/generated/model/validationError.ts rename to solution/front/src/api/generated/model/validationError.ts diff --git a/frontend/admin/src/api/generated/model/validationErrorCtx.ts b/solution/front/src/api/generated/model/validationErrorCtx.ts similarity index 100% rename from frontend/admin/src/api/generated/model/validationErrorCtx.ts rename to solution/front/src/api/generated/model/validationErrorCtx.ts diff --git a/frontend/admin/src/api/generated/model/validationErrorLocItem.ts b/solution/front/src/api/generated/model/validationErrorLocItem.ts similarity index 100% rename from frontend/admin/src/api/generated/model/validationErrorLocItem.ts rename to solution/front/src/api/generated/model/validationErrorLocItem.ts diff --git a/frontend/admin/src/api/generated/model/verifyCandidatesParams.ts b/solution/front/src/api/generated/model/verifyCandidatesParams.ts similarity index 100% rename from frontend/admin/src/api/generated/model/verifyCandidatesParams.ts rename to solution/front/src/api/generated/model/verifyCandidatesParams.ts diff --git a/frontend/admin/src/api/generated/model/weatherData.ts b/solution/front/src/api/generated/model/weatherData.ts similarity index 100% rename from frontend/admin/src/api/generated/model/weatherData.ts rename to solution/front/src/api/generated/model/weatherData.ts diff --git a/frontend/admin/src/api/generated/model/weatherDataTimezone.ts b/solution/front/src/api/generated/model/weatherDataTimezone.ts similarity index 100% rename from frontend/admin/src/api/generated/model/weatherDataTimezone.ts rename to solution/front/src/api/generated/model/weatherDataTimezone.ts diff --git a/frontend/admin/src/api/generated/model/weatherDataWindSpeed.ts b/solution/front/src/api/generated/model/weatherDataWindSpeed.ts similarity index 100% rename from frontend/admin/src/api/generated/model/weatherDataWindSpeed.ts rename to solution/front/src/api/generated/model/weatherDataWindSpeed.ts diff --git a/frontend/admin/src/api/generated/place/place.ts b/solution/front/src/api/generated/place/place.ts similarity index 100% rename from frontend/admin/src/api/generated/place/place.ts rename to solution/front/src/api/generated/place/place.ts diff --git a/frontend/admin/src/api/generated/site/site.ts b/solution/front/src/api/generated/site/site.ts similarity index 100% rename from frontend/admin/src/api/generated/site/site.ts rename to solution/front/src/api/generated/site/site.ts diff --git a/frontend/admin/src/api/index.ts b/solution/front/src/api/index.ts similarity index 100% rename from frontend/admin/src/api/index.ts rename to solution/front/src/api/index.ts diff --git a/frontend/admin/src/api/mutator/custom-fetch.ts b/solution/front/src/api/mutator/custom-fetch.ts similarity index 100% rename from frontend/admin/src/api/mutator/custom-fetch.ts rename to solution/front/src/api/mutator/custom-fetch.ts diff --git a/frontend/admin/src/api/pollJob.ts b/solution/front/src/api/pollJob.ts similarity index 100% rename from frontend/admin/src/api/pollJob.ts rename to solution/front/src/api/pollJob.ts diff --git a/frontend/admin/src/app/main.tsx b/solution/front/src/app/main.tsx similarity index 100% rename from frontend/admin/src/app/main.tsx rename to solution/front/src/app/main.tsx diff --git a/frontend/admin/src/app/provider.tsx b/solution/front/src/app/provider.tsx similarity index 100% rename from frontend/admin/src/app/provider.tsx rename to solution/front/src/app/provider.tsx diff --git a/frontend/admin/src/app/router.tsx b/solution/front/src/app/router.tsx similarity index 67% rename from frontend/admin/src/app/router.tsx rename to solution/front/src/app/router.tsx index 123e822..1f6d35d 100644 --- a/frontend/admin/src/app/router.tsx +++ b/solution/front/src/app/router.tsx @@ -1,14 +1,8 @@ -import {createBrowserRouter, Navigate, Outlet} from 'react-router'; -import {AppShell} from '@/components/layout/AppShell'; -import {RequireAuth} from '@/components/layout/RequireAuth'; +import {createBrowserRouter, Navigate} from 'react-router'; import {BuilderPage} from '@/pages/BuilderPage'; import {DevShowcasePage} from '@/pages/DevShowcasePage'; import {LoginPage} from '@/pages/LoginPage'; import {NotFoundPage} from '@/pages/NotFoundPage'; -import {PlaceDetailPage} from '@/pages/PlaceDetailPage'; -import {PlaceListPage} from '@/pages/PlaceListPage'; -import {SeoAuditPage} from '@/pages/SeoAuditPage'; -import {LocalContentPage} from '@/pages/LocalContentPage'; export const router = createBrowserRouter([ {path: '/login', element: }, @@ -31,21 +25,13 @@ export const router = createBrowserRouter([ */ {path: '/builder', element: }, - { - element: ( - - - - - - ), - children: [ - {path: '/places', element: }, - {path: '/places/:placeId', element: }, - {path: '/places/:placeId/seo', element: }, - {path: '/local-content', element: }, - ], - }, + /** + * ★ 내부 운영 화면(/places, /local-content, /seo)은 여기 없다 — 최상단 `admin/` 앱으로 나갔다. + * 라우트 가드는 화면을 가리지 **번들은 못 가린다**: 한 앱이면 내부 라우트 이름과 코드가 + * 사장님 브라우저에 그대로 내려간다. `UserRole.DEVELOPER` 주석의 + * "고객사에 존재를 노출하지 않는다"를 지키려면 번들 자체가 갈라져야 한다. + * (ARCHITECTURE.md 4절) + */ // 개발 빌드에서만 열리는 컴포넌트 쇼케이스. 운영 번들에서는 라우트 자체가 없다. ...(import.meta.env.DEV ? [{path: '/dev/showcase', element: }] : []), diff --git a/frontend/admin/src/components/layout/AppShell.tsx b/solution/front/src/components/layout/AppShell.tsx similarity index 77% rename from frontend/admin/src/components/layout/AppShell.tsx rename to solution/front/src/components/layout/AppShell.tsx index 1765d7a..b78b597 100644 --- a/frontend/admin/src/components/layout/AppShell.tsx +++ b/solution/front/src/components/layout/AppShell.tsx @@ -1,24 +1,32 @@ -import type {ReactNode} from 'react'; +import type {ComponentType, ReactNode} from 'react'; import {Link, NavLink, useLocation} from 'react-router'; -import {Building2, CalendarDays, LayoutGrid, LogOut, Search, Wand2} from 'lucide-react'; +import {LayoutGrid, LogOut, Search, Wand2} from 'lucide-react'; import {cn} from '@/lib/utils'; import {useAuthStore} from '@/stores/auth'; +export type NavItem = { + to: string; + /** 활성 표시용 경로. 쿼리스트링이 붙은 `to` 로는 pathname 을 비교할 수 없다. */ + match: string; + label: string; + icon: ComponentType<{className?: string}>; +}; + /** - * 순서가 곧 흐름이다 — 사업장 목록이 허브이고, 빌더는 **새로 만드는 입구**다. + * ★ 메뉴를 이 파일에 하드코딩하지 않는다 — 앱마다 다르고, **섞이면 새어 나간다.** + * 내부 메뉴(`/places`, `/local-content`)를 여기 두면 그 경로 이름이 사장님 번들에 + * 문자열로 남는다(실측: 앱을 가른 뒤에도 dist 에서 `local-content` 가 나왔다). + * 앱을 가른 이유가 그거라 메뉴도 앱이 들고 온다. 내부 메뉴는 `admin/src/app/router.tsx`. * * ★ 빌더는 `?new=1` 로 간다. 그냥 `/builder` 로 보내면 저장된 위저드 상태가 복원돼 * 지난번에 편집하던 가게의 에디터가 뜬다(BuilderPage 주석) — 새로 만들러 누른 사람에게는 - * 남의 화면이다. 이미 만든 가게를 고치는 길은 사업장 목록의 [사이트 편집] 하나뿐이다. - * ★ match 는 활성 표시용 경로다 — 쿼리스트링이 붙은 to 로는 pathname 을 비교할 수 없다. + * 남의 화면이다. */ -const NAV = [ - {to: '/places', match: '/places', label: '사업장', icon: Building2}, +const OWNER_NAV: NavItem[] = [ {to: '/builder?new=1', match: '/builder', label: '새 사이트', icon: Wand2}, - {to: '/local-content', match: '/local-content', label: '지역 콘텐츠', icon: CalendarDays}, ]; -export function AppShell({children}: {children: ReactNode}) { +export function AppShell({children, nav = OWNER_NAV}: {children: ReactNode; nav?: NavItem[]}) { const user = useAuthStore((s) => s.user); const signOut = useAuthStore((s) => s.signOut); const location = useLocation(); @@ -26,12 +34,12 @@ export function AppShell({children}: {children: ReactNode}) { return (