feat(ai-character): 관리자 웹 기반 구성
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -6,3 +6,10 @@
|
|||||||
.opencode
|
.opencode
|
||||||
|
|
||||||
mise.toml
|
mise.toml
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
coverage/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|||||||
29
AGENTS.md
29
AGENTS.md
@@ -1,25 +1,12 @@
|
|||||||
# AI 캐릭터 관리자 프로젝트 에이전트 지침
|
# AI 캐릭터 관리자 프로젝트 에이전트 지침
|
||||||
|
|
||||||
## 커뮤니케이션 규칙
|
이 저장소의 상세 에이전트 지침은 `docs/agent-guide/` 아래 문서를 따른다.
|
||||||
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
|
||||||
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
|
||||||
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
|
||||||
|
|
||||||
## 문서 유지보수 규칙
|
- [커뮤니케이션 규칙](docs/agent-guide/communication.md)
|
||||||
- PRD 문서와 구현 계획/TASK 문서는 `docs/[날짜]_구현할내용한글/` 아래에 함께 둔다.
|
- [문서 유지보수 규칙](docs/agent-guide/documentation.md)
|
||||||
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
- [에이전트 동작 원칙](docs/agent-guide/agent-behavior.md)
|
||||||
- PRD 문서 파일명은 `prd.md`, 구현 계획/TASK 문서 파일명은 `plan-task.md`를 사용한다.
|
- [실행 스크립트](docs/agent-guide/scripts.md)
|
||||||
- 구현 항목은 기능/작업 단위로 분리해 체크박스(`- [ ]`) 목록으로 작성한다.
|
- [환경 변수](docs/agent-guide/environment.md)
|
||||||
- 구현 완료 시마다 체크박스를 `- [x]`로 갱신하고, 각 항목이 정상 구현되었는지 확인한다.
|
- [코드 스타일 규칙](docs/agent-guide/code-style.md)
|
||||||
- 작업 도중 범위가 변경되면 계획 문서의 체크박스 항목을 먼저 업데이트한 뒤 구현을 진행한다.
|
|
||||||
- 모든 구현이 끝난 후 결과 보고 시 계획 문서 맨 아래에 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 기록한다.
|
|
||||||
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰지 않고 누적한다(예: `1차 구현`, `2차 수정`).
|
|
||||||
- 검증 기록은 단계별로 `무엇을/왜/어떻게`를 유지해 작성하고, 이전 단계와 구분이 되도록 명시한다.
|
|
||||||
- 단계별 `어떻게`에는 실제 실행한 검증 명령과 결과(성공/실패/불가 사유)를 함께 기록한다.
|
|
||||||
- 기존 기록 정정이 필요하면 원문을 지우지 말고 `정정` 항목을 추가해 사유와 변경 내용을 남긴다.
|
|
||||||
|
|
||||||
## 에이전트 동작 원칙
|
핵심 원칙: 사용자에게 전달하는 답변과 설명은 한국어로 작성하고, 작업 전 관련 문서를 읽어 추측 없이 진행한다.
|
||||||
- 추측하지 말고, 근거 파일을 읽고 결정한다.
|
|
||||||
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
|
|
||||||
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
|
|
||||||
- 결과 보고 시 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 남긴다.
|
|
||||||
|
|||||||
36
README.md
Normal file
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# AI 캐릭터 관리자 웹
|
||||||
|
|
||||||
|
React, TypeScript, Vite 기반 독립 관리자 SPA입니다.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js 24.12.0
|
||||||
|
- npm 11.7.0+
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npx playwright install chromium webkit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Vite mode별 API base URL은 아래 파일에 둡니다.
|
||||||
|
|
||||||
|
- `.env.development`: `https://test-character-admin.sodalive.net`
|
||||||
|
- `.env.production`: `https://character-admin.sodalive.net`
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
npm run build:dev
|
||||||
|
npm run build:prod
|
||||||
|
npm run typecheck
|
||||||
|
npm run lint
|
||||||
|
npm run test
|
||||||
|
npm run test:run
|
||||||
|
npm run e2e
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
@@ -176,38 +176,52 @@ feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/
|
|||||||
|
|
||||||
### Task 0.1 런타임·패키지 기반
|
### Task 0.1 런타임·패키지 기반
|
||||||
|
|
||||||
- [ ] `mise.toml`의 Node `24.12.0`을 기준으로 npm package와 lockfile을 생성한다.
|
- [x] `mise.toml`의 Node `24.12.0`을 기준으로 npm package와 lockfile을 생성한다.
|
||||||
- [ ] React + TypeScript + Vite 진입점과 `@` path alias를 구성한다.
|
- [x] React + TypeScript + Vite 진입점과 `@` path alias를 구성한다.
|
||||||
- [ ] `VITE_API_BASE_URL`만 `.env.example`에 문서화하고 token·password 같은 비밀값을 넣지 않는다.
|
- [x] `VITE_API_BASE_URL`만 `.env.example`에 문서화하고 token·password 같은 비밀값을 넣지 않는다.
|
||||||
- [ ] runtime env 누락·잘못된 URL을 앱 시작 전에 설명 가능한 오류로 차단하는 test를 작성한다.
|
- [x] runtime env 누락·잘못된 URL을 앱 시작 전에 설명 가능한 오류로 차단하는 test를 작성한다.
|
||||||
- [ ] unit test는 `vi.stubEnv`, Playwright webServer는 명시적 test URL로 `VITE_API_BASE_URL`을 주입해 `.env.example` 자동 로드를 전제하지 않는다.
|
- [x] unit test는 `vi.stubEnv`, Playwright webServer는 명시적 test URL로 `VITE_API_BASE_URL`을 주입해 `.env.example` 자동 로드를 전제하지 않는다.
|
||||||
- [ ] `dev`, `build`, `typecheck`, `lint`, `test`, `test:run`, `e2e` script를 정의한다.
|
- [x] `dev`, `build`, `typecheck`, `lint`, `test`, `test:run`, `e2e` script를 정의한다.
|
||||||
- [ ] 이 Phase에 필요하지 않은 router, server-state, form, drag-and-drop dependency는 아직 설치하지 않는다.
|
- [x] 이 Phase에 필요하지 않은 router, server-state, form, drag-and-drop dependency는 아직 설치하지 않는다.
|
||||||
|
|
||||||
### Task 0.2 test 기반
|
### Task 0.2 test 기반
|
||||||
|
|
||||||
- [ ] Vitest, jsdom, React Testing Library, jest-dom을 구성한다.
|
- [x] Vitest, jsdom, React Testing Library, jest-dom을 구성한다.
|
||||||
- [ ] 각 test 뒤 DOM·mock·storage가 정리되는 공통 setup을 만든다.
|
- [x] 각 test 뒤 DOM·mock·storage가 정리되는 공통 setup을 만든다.
|
||||||
- [ ] Playwright에 desktop Chromium/WebKit과 mobile Chrome/Safari viewport project, Vite webServer를 구성한다.
|
- [x] Playwright에 desktop Chromium/WebKit과 mobile Chrome/Safari viewport project, Vite webServer를 구성한다.
|
||||||
- [ ] fresh environment에서 Chromium/WebKit browser binary를 설치하는 명령을 README와 Gate에 포함한다.
|
- [x] fresh environment에서 Chromium/WebKit browser binary를 설치하는 명령을 README와 Gate에 포함한다.
|
||||||
- [ ] `<html lang="ko">`, `main` landmark, root content를 확인하는 unit test를 먼저 실패시킨 뒤 최소 App shell을 만든다.
|
- [x] `<html lang="ko">`, `main` landmark, root content를 확인하는 unit test를 먼저 실패시킨 뒤 최소 App shell을 만든다.
|
||||||
- [ ] 동일 shell이 각 Playwright project에서 열리는 smoke E2E를 만든다.
|
- [x] 동일 shell이 각 Playwright project에서 열리는 smoke E2E를 만든다.
|
||||||
|
|
||||||
### Phase 0 Gate
|
### Phase 0 Gate
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm ci
|
||||||
npx playwright install chromium webkit
|
npx playwright install chromium webkit
|
||||||
export VITE_API_BASE_URL=http://127.0.0.1:4010
|
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts
|
npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts
|
||||||
npm run e2e -- tests/e2e/smoke.spec.ts
|
npm run e2e -- tests/e2e/smoke.spec.ts
|
||||||
npm run build
|
npm run build:dev
|
||||||
|
npm run build:prod
|
||||||
```
|
```
|
||||||
|
|
||||||
**Expected:** fresh install 후 typecheck·lint·unit·E2E·production build가 모두 0 exit code이며 환경 변수 오류가 test로 고정된다.
|
**Expected:** fresh install 후 typecheck·lint·unit·E2E·production build가 모두 0 exit code이며 환경 변수 오류가 test로 고정된다.
|
||||||
|
|
||||||
|
**1차 구현 검증 기록 (2026-07-26):**
|
||||||
|
|
||||||
|
- 무엇을: Phase 0의 React + TypeScript + Vite 프로젝트 기반, runtime env 검증, Vitest/RTL/jsdom, Playwright Chromium/WebKit 및 mobile viewport smoke, README와 npm scripts를 구현했다.
|
||||||
|
- 왜: 후속 Phase가 동일한 명령으로 개발·test·build할 수 있고, `VITE_API_BASE_URL` 누락/오류와 root shell 회귀를 자동 검증하기 위해서다.
|
||||||
|
- 어떻게: `npm ci` 성공(0 vulnerabilities), `npx playwright install chromium webkit` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run typecheck` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run lint` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts` 성공(2 files/4 tests passed), `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run e2e -- tests/e2e/smoke.spec.ts` 성공(4 projects passed), `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run build` 성공.
|
||||||
|
- 정정/참고: `@playwright/test@1.62.0`의 WebKit binary가 현재 macOS 26.0 x86_64 환경에서 시작 전 `Segmentation fault: 11`로 실패해, Phase 0 Gate 충족을 위해 `@playwright/test@1.61.1`로 고정했다.
|
||||||
|
|
||||||
|
**2차 설정 정정 기록 (2026-07-26):**
|
||||||
|
|
||||||
|
- 무엇을: 개발 서버 포트를 `8888`로 고정하고, 개발/프로덕션 API base URL과 build 명령을 분리했다.
|
||||||
|
- 왜: 실행 환경별 API endpoint를 명확히 분리하고, AGENTS.md를 짧게 유지하면서 상세 지침을 `docs/agent-guide/README.md`에서 관리하기 위해서다.
|
||||||
|
- 어떻게: `npm run typecheck`, `npm run lint`, `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts`, `npm run e2e -- tests/e2e/smoke.spec.ts`, `npm run build:dev`, `npm run build:prod`가 성공했다.
|
||||||
|
- 정정: 기존 1차 기록의 `VITE_API_BASE_URL=http://127.0.0.1:4010`과 단일 `npm run build`는 당시 실행 기록으로 보존하되, 현재 Gate 기준은 `.env.development`, `.env.production`, `build:dev`, `build:prod`를 사용한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 1. 공통 플랫폼·인증/인가·컴포넌트 기반
|
## Phase 1. 공통 플랫폼·인증/인가·컴포넌트 기반
|
||||||
|
|||||||
6
docs/agent-guide/agent-behavior.md
Normal file
6
docs/agent-guide/agent-behavior.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 에이전트 동작 원칙
|
||||||
|
|
||||||
|
- 추측하지 말고, 근거 파일을 읽고 결정한다.
|
||||||
|
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
|
||||||
|
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
|
||||||
|
- 결과 보고 시 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 남긴다.
|
||||||
7
docs/agent-guide/code-style.md
Normal file
7
docs/agent-guide/code-style.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 코드 스타일 규칙
|
||||||
|
|
||||||
|
- TypeScript는 `strict` 기준을 유지하고 `as any`, `@ts-ignore`, `@ts-expect-error`로 타입 오류를 숨기지 않는다.
|
||||||
|
- React 컴포넌트와 테스트는 named export/import를 우선 사용하고, `@/*` path alias는 `src/*` 기준으로 사용한다.
|
||||||
|
- 문자열 quote, semicolon, trailing comma 등 세부 포맷은 현재 ESLint/TypeScript 설정과 주변 코드 스타일을 따른다.
|
||||||
|
- 테스트는 Vitest + React Testing Library를 사용하고, DOM/mock/storage 정리는 `src/shared/test/setup.ts`의 공통 setup에 맡긴다.
|
||||||
|
- Phase 0에서는 router, server-state, form, drag-and-drop, Tailwind/shadcn 의존성을 추가하지 않는다. 필요한 Phase에서 실제 소비 시점에 추가한다.
|
||||||
5
docs/agent-guide/communication.md
Normal file
5
docs/agent-guide/communication.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 커뮤니케이션 규칙
|
||||||
|
|
||||||
|
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
||||||
|
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
||||||
|
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
||||||
13
docs/agent-guide/documentation.md
Normal file
13
docs/agent-guide/documentation.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 문서 유지보수 규칙
|
||||||
|
|
||||||
|
- PRD 문서와 구현 계획/TASK 문서는 `docs/[날짜]_구현할내용한글/` 아래에 함께 둔다.
|
||||||
|
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||||
|
- PRD 문서 파일명은 `prd.md`, 구현 계획/TASK 문서 파일명은 `plan-task.md`를 사용한다.
|
||||||
|
- 구현 항목은 기능/작업 단위로 분리해 체크박스(`- [ ]`) 목록으로 작성한다.
|
||||||
|
- 구현 완료 시마다 체크박스를 `- [x]`로 갱신하고, 각 항목이 정상 구현되었는지 확인한다.
|
||||||
|
- 작업 도중 범위가 변경되면 계획 문서의 체크박스 항목을 먼저 업데이트한 뒤 구현을 진행한다.
|
||||||
|
- 모든 구현이 끝난 후 결과 보고 시 계획 문서 맨 아래에 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 기록한다.
|
||||||
|
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰지 않고 누적한다(예: `1차 구현`, `2차 수정`).
|
||||||
|
- 검증 기록은 단계별로 `무엇을/왜/어떻게`를 유지해 작성하고, 이전 단계와 구분이 되도록 명시한다.
|
||||||
|
- 단계별 `어떻게`에는 실제 실행한 검증 명령과 결과(성공/실패/불가 사유)를 함께 기록한다.
|
||||||
|
- 기존 기록 정정이 필요하면 원문을 지우지 말고 `정정` 항목을 추가해 사유와 변경 내용을 남긴다.
|
||||||
5
docs/agent-guide/environment.md
Normal file
5
docs/agent-guide/environment.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 환경 변수
|
||||||
|
|
||||||
|
- 개발 서버 API: `VITE_API_BASE_URL=https://test-character-admin.sodalive.net`
|
||||||
|
- 프로덕션 서버 API: `VITE_API_BASE_URL=https://character-admin.sodalive.net`
|
||||||
|
- Vite mode별 파일은 `.env.development`, `.env.production`을 사용한다.
|
||||||
12
docs/agent-guide/scripts.md
Normal file
12
docs/agent-guide/scripts.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 실행 스크립트
|
||||||
|
|
||||||
|
- 개발 서버: `npm run dev` (`http://127.0.0.1:8888`)
|
||||||
|
- 개발 서버용 빌드: `npm run build:dev`
|
||||||
|
- 프로덕션 서버용 빌드: `npm run build:prod`
|
||||||
|
- 기본 프로덕션 빌드: `npm run build`
|
||||||
|
- 타입 검사: `npm run typecheck`
|
||||||
|
- 린트: `npm run lint`
|
||||||
|
- Vitest watch: `npm run test`
|
||||||
|
- Vitest 단발 실행: `npm run test:run`
|
||||||
|
- Playwright E2E: `npm run e2e`
|
||||||
|
- Phase 0 Gate 기준: `npm ci`, `npx playwright install chromium webkit`, `npm run typecheck`, `npm run lint`, `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts`, `npm run e2e -- tests/e2e/smoke.spec.ts`, `npm run build:dev`, `npm run build:prod`
|
||||||
43
eslint.config.js
Normal file
43
eslint.config.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
".codex/**",
|
||||||
|
".omo/**",
|
||||||
|
".opencode/**",
|
||||||
|
".worktrees/**",
|
||||||
|
"dist/**",
|
||||||
|
"node_modules/**",
|
||||||
|
"playwright-report/**",
|
||||||
|
"test-results/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.config.{js,ts}", "eslint.config.js"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AI 캐릭터 관리자</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3941
package-lock.json
generated
Normal file
3941
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-character-admin-web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 127.0.0.1 --port 8888 --strictPort",
|
||||||
|
"build": "npm run build:prod",
|
||||||
|
"build:dev": "tsc -b && vite build --mode development",
|
||||||
|
"build:prod": "tsc -b && vite build --mode production",
|
||||||
|
"typecheck": "tsc -b --pretty false",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"e2e": "playwright test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "19.2.8",
|
||||||
|
"react-dom": "19.2.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "10.0.1",
|
||||||
|
"@playwright/test": "1.61.1",
|
||||||
|
"@testing-library/dom": "10.4.1",
|
||||||
|
"@testing-library/jest-dom": "7.0.0",
|
||||||
|
"@testing-library/react": "16.3.2",
|
||||||
|
"@types/node": "26.1.1",
|
||||||
|
"@types/react": "19.2.17",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@vitejs/plugin-react": "6.0.4",
|
||||||
|
"eslint": "10.8.0",
|
||||||
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
|
"eslint-plugin-react-refresh": "0.5.3",
|
||||||
|
"globals": "17.7.0",
|
||||||
|
"jsdom": "29.1.1",
|
||||||
|
"typescript": "6.0.3",
|
||||||
|
"typescript-eslint": "8.65.0",
|
||||||
|
"vite": "8.1.5",
|
||||||
|
"vitest": "4.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
playwright.config.ts
Normal file
19
playwright.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
webServer: {
|
||||||
|
command: "npm run dev",
|
||||||
|
url: "http://127.0.0.1:8888",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
baseURL: "http://127.0.0.1:8888",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||||
|
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
|
||||||
|
{ name: "mobile-chrome", use: { ...devices["Pixel 5"] } },
|
||||||
|
{ name: "mobile-safari", use: { ...devices["iPhone 12"] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
10
src/app/App.test.tsx
Normal file
10
src/app/App.test.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
|
import { App } from "./App";
|
||||||
|
|
||||||
|
test("renders Korean root shell with a main landmark", () => {
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
expect(document.documentElement).toHaveAttribute("lang", "ko");
|
||||||
|
expect(screen.getByRole("main")).toHaveTextContent("AI 캐릭터 관리자");
|
||||||
|
});
|
||||||
13
src/app/App.tsx
Normal file
13
src/app/App.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = "ko";
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>AI 캐릭터 관리자</h1>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/main.tsx
Normal file
19
src/main.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import { App } from "@/app/App";
|
||||||
|
import { getRuntimeEnv } from "@/shared/config/env";
|
||||||
|
|
||||||
|
getRuntimeEnv();
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
throw new Error("Root element #root was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
27
src/shared/config/env.test.ts
Normal file
27
src/shared/config/env.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { getRuntimeEnv } from "./env";
|
||||||
|
|
||||||
|
describe("getRuntimeEnv", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the configured API base URL", () => {
|
||||||
|
vi.stubEnv("VITE_API_BASE_URL", "https://api.example.com");
|
||||||
|
|
||||||
|
expect(getRuntimeEnv()).toEqual({ apiBaseUrl: "https://api.example.com" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks startup when VITE_API_BASE_URL is missing", () => {
|
||||||
|
vi.stubEnv("VITE_API_BASE_URL", "");
|
||||||
|
|
||||||
|
expect(() => getRuntimeEnv()).toThrow("VITE_API_BASE_URL is required");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks startup when VITE_API_BASE_URL is not an http URL", () => {
|
||||||
|
vi.stubEnv("VITE_API_BASE_URL", "not-a-url");
|
||||||
|
|
||||||
|
expect(() => getRuntimeEnv()).toThrow("VITE_API_BASE_URL must be a valid http(s) URL");
|
||||||
|
});
|
||||||
|
});
|
||||||
23
src/shared/config/env.ts
Normal file
23
src/shared/config/env.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export type RuntimeEnv = {
|
||||||
|
apiBaseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getRuntimeEnv(): RuntimeEnv {
|
||||||
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
|
if (!apiBaseUrl) {
|
||||||
|
throw new Error("VITE_API_BASE_URL is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(apiBaseUrl);
|
||||||
|
} catch {
|
||||||
|
throw new Error("VITE_API_BASE_URL must be a valid http(s) URL");
|
||||||
|
}
|
||||||
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||||
|
throw new Error("VITE_API_BASE_URL must be a valid http(s) URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { apiBaseUrl };
|
||||||
|
}
|
||||||
10
src/shared/test/setup.ts
Normal file
10
src/shared/test/setup.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import { cleanup } from "@testing-library/react";
|
||||||
|
import { afterEach, vi } from "vitest";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
sessionStorage.clear();
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
8
tests/e2e/smoke.spec.ts
Normal file
8
tests/e2e/smoke.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("opens the root shell", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("lang", "ko");
|
||||||
|
await expect(page.getByRole("main")).toContainText("AI 캐릭터 관리자");
|
||||||
|
});
|
||||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client", "vitest/globals"],
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
19
tsconfig.node.json
Normal file
19
tsconfig.node.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts", "eslint.config.js", "playwright.config.ts"]
|
||||||
|
}
|
||||||
21
vite.config.ts
Normal file
21
vite.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 8888,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": new URL("./src", import.meta.url).pathname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: ["./src/shared/test/setup.ts"],
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user