sh-ui-cli 0.76.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/changelog/versions.json +26 -0
- package/data/registry/react/components/switch/index.tailwind.tsx +1 -1
- package/data/registry/react/components/switch/styles.css +6 -0
- package/data/registry/react/components/switch/styles.module.css +6 -0
- package/data/registry/react/tokens-used.json +3 -1
- package/package.json +1 -1
- package/src/create/architectures/index.js +2 -1
- package/src/create/architectures/mes.js +53 -0
- package/templates/monorepo/packages/eslint-config/mes.js +82 -0
- package/templates/monorepo/packages/eslint-config/package.json +2 -1
- package/templates/nextjs-app/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-app/_arch/mes/app/layout.tsx +16 -0
- package/templates/nextjs-app/_arch/mes/app/sign-in/page.tsx +1 -0
- package/templates/nextjs-app/_arch/mes/eslint.config.js +10 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-app/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/index.tsx +1 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
- package/templates/nextjs-app/_arch/mes/src/hooks/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/hooks/useAppMutation.ts +59 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/error.ts +12 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/http.ts +13 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/observability.ts +20 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-app/_arch/mes/src/lib/config/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/index.ts +2 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/formatDate.ts +26 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/api.ts +3 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/index.tsx +14 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/schema.ts +2 -0
- package/templates/nextjs-app/_arch/mes/tsconfig.json +24 -0
- package/templates/nextjs-standalone/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-standalone/_arch/mes/app/globals.css +49 -0
- package/templates/nextjs-standalone/_arch/mes/app/layout.tsx +16 -0
- package/templates/nextjs-standalone/_arch/mes/app/sign-in/page.tsx +1 -0
- package/templates/nextjs-standalone/_arch/mes/eslint.config.js +137 -0
- package/templates/nextjs-standalone/_arch/mes/sh-ui.config.json +22 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
- package/templates/nextjs-standalone/_arch/mes/src/hooks/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/hooks/useAppMutation.ts +59 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/error.ts +12 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/http.ts +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/observability.ts +20 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/config/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/styles/tokens.css +170 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/index.ts +2 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatDate.ts +26 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/utils.ts +6 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/api.ts +3 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/index.tsx +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/schema.ts +2 -0
- package/templates/nextjs-standalone/_arch/mes/tsconfig.json +39 -0
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.77.0",
|
|
7
|
+
"date": "2026-05-11",
|
|
8
|
+
"title": "minor — `--arch mes` 프리셋 — 백오피스용 페이지 격리 구조",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`sh-ui-cli create ... --arch mes` 신규 지원** — 스마트팩토리 MES·ERP·관리자 도구처럼 페이지 간 상호작용이 적은 CRUD-heavy 앱을 위한 페이지 격리 프리셋. FSD 의 6 레이어가 과한 백오피스 시나리오 전용.",
|
|
12
|
+
"**페이지 격리 구조** — 각 `src/pages/<name>/` 가 자기완결 슬롯 (index.tsx + components/ + api.ts + hooks.ts + schema.ts + columns.ts). `app/<route>/page.tsx` 는 `export { default } from '@/pages/<name>'` 한 줄 위임. 페이지 끼리는 import 금지 (eslint boundaries 강제).",
|
|
13
|
+
"**`src/pages/sign-in/` 스텁 동봉** — 슬롯 레이아웃을 보여주는 빈 페이지 (동작 X, 폴더 모양만). 새 페이지 추가 = 이 폴더 복사 → 이름 교체.",
|
|
14
|
+
"**tsconfig** — catch-all `@/*` → `./src/*`. 모든 import 가 `@/pages/...`, `@/components/...`, `@/lib/...` 처럼 짧고 일관.",
|
|
15
|
+
"monorepo (`apps/<name>` + `packages/ui/...`) 에서도 `--arch mes` 동작. nextjs-standalone / nextjs-app 두 템플릿 모두 mes overlay 포함."
|
|
16
|
+
],
|
|
17
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.77.0"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.76.1",
|
|
21
|
+
"date": "2026-05-11",
|
|
22
|
+
"title": "patch — Switch ON 상태 thumb 가시성 (다크모드)",
|
|
23
|
+
"type": "patch",
|
|
24
|
+
"highlights": [
|
|
25
|
+
"**Switch — checked thumb 가 `--primary-foreground` 로 전환** — 다크모드는 `--primary` 가 거의 흰색(`#FAFAFA`)인데 thumb 가 하드코딩 `white` 라 ON 상태에서 thumb 와 트랙이 같은 색으로 묻혀 위치를 알 수 없던 문제 수정.",
|
|
26
|
+
"변종 4종 동기화 — `styles.css` / `styles.module.css` / `index.tailwind.tsx` / docs 복사본.",
|
|
27
|
+
"라이트/다크 양쪽 모두 트랙과 thumb 가 항상 반전 대비를 유지."
|
|
28
|
+
],
|
|
29
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.76.1"
|
|
30
|
+
},
|
|
5
31
|
{
|
|
6
32
|
"version": "0.76.0",
|
|
7
33
|
"date": "2026-05-11",
|
|
@@ -18,7 +18,7 @@ const switchRoot = cva(
|
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
const switchThumb = cva(
|
|
21
|
-
"block rounded-full bg-white shadow-[var(--shadow-sm)] transition-transform duration-150 ease-out motion-reduce:transition-none forced-colors:[background:ButtonText] forced-colors:data-[checked]:[background:HighlightText] forced-colors:data-[disabled]:[background:GrayText]",
|
|
21
|
+
"block rounded-full bg-white data-[checked]:bg-primary-foreground shadow-[var(--shadow-sm)] transition-transform duration-150 ease-out motion-reduce:transition-none forced-colors:[background:ButtonText] forced-colors:data-[checked]:[background:HighlightText] forced-colors:data-[disabled]:[background:GrayText]",
|
|
22
22
|
{
|
|
23
23
|
variants: {
|
|
24
24
|
size: {
|
|
@@ -74,6 +74,12 @@
|
|
|
74
74
|
transform: translateX(1.125rem);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/* checked 상태에서 thumb 와 트랙(--primary)이 같은 색으로 보이지 않도록
|
|
78
|
+
--primary-foreground 로 전환 (다크모드: primary=near-white → thumb=dark). */
|
|
79
|
+
.sh-ui-switch[data-checked] .sh-ui-switch__thumb {
|
|
80
|
+
background: var(--primary-foreground);
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
/* reduced motion */
|
|
78
84
|
@media (prefers-reduced-motion: reduce) {
|
|
79
85
|
.sh-ui-switch,
|
|
@@ -74,6 +74,12 @@
|
|
|
74
74
|
transform: translateX(1.125rem);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/* checked 상태에서 thumb 와 트랙(--primary)이 같은 색으로 보이지 않도록
|
|
78
|
+
--primary-foreground 로 전환 (다크모드: primary=near-white → thumb=dark). */
|
|
79
|
+
.switch[data-checked] .switch__thumb {
|
|
80
|
+
background: var(--primary-foreground);
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
/* reduced motion */
|
|
78
84
|
@media (prefers-reduced-motion: reduce) {
|
|
79
85
|
.switch,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$description": "컴포넌트별 토큰 의존성 (var(--*) 추출). build-registry-tokens.mjs 가 자동 생성.",
|
|
3
|
-
"$generated": "2026-05-
|
|
3
|
+
"$generated": "2026-05-11T07:02:18.245Z",
|
|
4
4
|
"components": {
|
|
5
5
|
"button": {
|
|
6
6
|
"plain": [
|
|
@@ -1556,6 +1556,7 @@
|
|
|
1556
1556
|
"--border-width-strong",
|
|
1557
1557
|
"--opacity-disabled",
|
|
1558
1558
|
"--primary",
|
|
1559
|
+
"--primary-foreground",
|
|
1559
1560
|
"--primary-hover",
|
|
1560
1561
|
"--ring",
|
|
1561
1562
|
"--shadow-sm"
|
|
@@ -1571,6 +1572,7 @@
|
|
|
1571
1572
|
"--border-width-strong",
|
|
1572
1573
|
"--opacity-disabled",
|
|
1573
1574
|
"--primary",
|
|
1575
|
+
"--primary-foreground",
|
|
1574
1576
|
"--primary-hover",
|
|
1575
1577
|
"--ring",
|
|
1576
1578
|
"--shadow-sm"
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { fsdArch } from './fsd.js';
|
|
2
2
|
import { flatArch } from './flat.js';
|
|
3
|
+
import { mesArch } from './mes.js';
|
|
3
4
|
import { validateArchitectures } from './archSchema.js';
|
|
4
5
|
|
|
5
|
-
export const allArchitectures = [fsdArch, flatArch];
|
|
6
|
+
export const allArchitectures = [fsdArch, flatArch, mesArch];
|
|
6
7
|
|
|
7
8
|
// 모듈 로드 시점에 모든 arch 디스크립터를 schema 로 검증.
|
|
8
9
|
// 누락된 키, 잘못된 형태가 있으면 즉시 에러.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MES (Backoffice) 아키텍처 디스크립터.
|
|
3
|
+
*
|
|
4
|
+
* 스마트팩토리 MES·ERP·관리자 도구처럼 **페이지 간 상호작용이 거의 없고**
|
|
5
|
+
* 페이지마다 자기 컬럼/스키마/다이얼로그를 다시 정의하는 CRUD-heavy 앱을 위한
|
|
6
|
+
* 페이지 격리 구조.
|
|
7
|
+
*
|
|
8
|
+
* 핵심 컨벤션:
|
|
9
|
+
* - `app/<route>/page.tsx` 는 한 줄짜리 위임 (`export { default } from "@/pages/<name>"`)
|
|
10
|
+
* - 페이지 본체는 `src/pages/<name>/` 에 자기완결로 거주 — index.tsx, components/,
|
|
11
|
+
* api.ts, hooks.ts, schema.ts, columns.ts.
|
|
12
|
+
* - 두 페이지 이상에서 같은 코드가 보이기 시작하면 그때 `src/components/` `src/hooks/`
|
|
13
|
+
* `src/lib/` 로 승격. **두 번째 쓰임이 나타나기 전엔 공용 만들지 않기.**
|
|
14
|
+
*
|
|
15
|
+
* tsconfig 의 `paths` 는 FSD 처럼 catch-all `@/*` 를 쓰되 매핑 대상이 `./src/*` —
|
|
16
|
+
* 즉 모든 import 가 `@/pages/...`, `@/components/...`, `@/lib/...` 처럼 `src/` 루트
|
|
17
|
+
* 기준으로 짧게 정리된다.
|
|
18
|
+
*/
|
|
19
|
+
export const mesArch = {
|
|
20
|
+
name: 'mes',
|
|
21
|
+
label: 'MES (Backoffice)',
|
|
22
|
+
description:
|
|
23
|
+
'페이지 격리 구조 (src/pages/<name>/ 자기완결). 페이지 간 상호작용이 적은 CRUD-heavy 관리자 도구·MES 류에 적합.',
|
|
24
|
+
platforms: ['next'],
|
|
25
|
+
|
|
26
|
+
paths: {
|
|
27
|
+
layouts: 'src/components/layouts',
|
|
28
|
+
providers: 'src/components/providers',
|
|
29
|
+
api: 'src/lib/api',
|
|
30
|
+
config: 'src/lib/config',
|
|
31
|
+
hooks: 'src/hooks',
|
|
32
|
+
utils: 'src/lib/utils',
|
|
33
|
+
ui: 'src/components/common',
|
|
34
|
+
test: 'src/lib/test',
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
aliases: {
|
|
38
|
+
layouts: '@/components/layouts',
|
|
39
|
+
providers: '@/components/providers',
|
|
40
|
+
api: '@/lib/api',
|
|
41
|
+
config: '@/lib/config',
|
|
42
|
+
hooks: '@/hooks',
|
|
43
|
+
utils: '@/lib/utils',
|
|
44
|
+
ui: '@/components/common',
|
|
45
|
+
test: '@/lib/test',
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Catch-all `@/*` → `./src/*`. FSD 와 같은 결의 명명이지만 src/ 루트가 바뀜.
|
|
49
|
+
// `@/pages/customers`, `@/components/...`, `@/lib/api/...` 모두 자연스럽게 풀린다.
|
|
50
|
+
tsconfigPaths: {
|
|
51
|
+
'@/*': ['./src/*'],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import boundaries from "eslint-plugin-boundaries"
|
|
2
|
+
import checkFile from "eslint-plugin-check-file"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MES (Backoffice) ESLint configuration.
|
|
6
|
+
*
|
|
7
|
+
* 페이지 격리 + 단방향 의존을 강제:
|
|
8
|
+
*
|
|
9
|
+
* - `src/lib/*` — lib 끼리만 (UI/페이지 모름)
|
|
10
|
+
* - `src/hooks/*` — hooks / lib 만
|
|
11
|
+
* - `src/components/*` — components / hooks / lib 만
|
|
12
|
+
* - `src/pages/*` — components / hooks / lib 만. **다른 페이지 import 금지** (격리)
|
|
13
|
+
* - `app/` — pages / components / hooks / lib 모두 OK (한 줄 위임)
|
|
14
|
+
*
|
|
15
|
+
* @type {import("eslint").Linter.Config[]}
|
|
16
|
+
*/
|
|
17
|
+
export const mesConfig = [
|
|
18
|
+
// ── boundaries ──
|
|
19
|
+
{
|
|
20
|
+
plugins: { boundaries },
|
|
21
|
+
settings: {
|
|
22
|
+
"import/resolver": {
|
|
23
|
+
typescript: { alwaysTryTypes: true },
|
|
24
|
+
},
|
|
25
|
+
"boundaries/elements": [
|
|
26
|
+
{ type: "lib", pattern: ["src/lib/*"], mode: "folder" },
|
|
27
|
+
{ type: "hooks", pattern: ["src/hooks"], mode: "folder" },
|
|
28
|
+
{ type: "components", pattern: ["src/components/*"], mode: "folder" },
|
|
29
|
+
{ type: "pages", pattern: ["src/pages/*"], mode: "folder" },
|
|
30
|
+
{ type: "app", pattern: ["app"], mode: "folder" },
|
|
31
|
+
],
|
|
32
|
+
"boundaries/ignore": ["**/*.test.*", "**/*.spec.*"],
|
|
33
|
+
},
|
|
34
|
+
rules: {
|
|
35
|
+
"boundaries/element-types": [
|
|
36
|
+
"warn",
|
|
37
|
+
{
|
|
38
|
+
default: "disallow",
|
|
39
|
+
rules: [
|
|
40
|
+
{ from: "app", allow: ["pages", "components", "hooks", "lib"] },
|
|
41
|
+
// pages 끼리는 import 금지 — 페이지 격리 원칙
|
|
42
|
+
{ from: "pages", allow: ["components", "hooks", "lib"] },
|
|
43
|
+
{ from: "components", allow: ["components", "hooks", "lib"] },
|
|
44
|
+
{ from: "hooks", allow: ["hooks", "lib"] },
|
|
45
|
+
{ from: "lib", allow: ["lib"] },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// ── check-file (MES 는 .tsx PASCAL, .ts CAMEL) ──
|
|
53
|
+
{
|
|
54
|
+
plugins: { "check-file": checkFile },
|
|
55
|
+
rules: {
|
|
56
|
+
"check-file/filename-naming-convention": [
|
|
57
|
+
"error",
|
|
58
|
+
{
|
|
59
|
+
"**/components/**/*.tsx": "PASCAL_CASE",
|
|
60
|
+
"**/pages/**/components/**/*.tsx": "PASCAL_CASE",
|
|
61
|
+
"**/lib/**/*.ts": "CAMEL_CASE",
|
|
62
|
+
"**/hooks/**/*.ts": "CAMEL_CASE",
|
|
63
|
+
},
|
|
64
|
+
{ ignoreMiddleExtensions: true },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
files: [
|
|
70
|
+
"**/index.tsx", "**/index.ts",
|
|
71
|
+
"**/layout.tsx", "**/page.tsx",
|
|
72
|
+
"**/error.tsx", "**/not-found.tsx",
|
|
73
|
+
"**/routing.ts", "**/navigation.ts", "**/request.ts",
|
|
74
|
+
// MES 페이지 concern 파일들
|
|
75
|
+
"**/pages/**/api.ts", "**/pages/**/schema.ts",
|
|
76
|
+
"**/pages/**/columns.ts", "**/pages/**/hooks.ts",
|
|
77
|
+
],
|
|
78
|
+
rules: {
|
|
79
|
+
"check-file/filename-naming-convention": "off",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
captureApiError,
|
|
6
|
+
logApiError,
|
|
7
|
+
} from '@/lib/api/observability';
|
|
8
|
+
|
|
9
|
+
const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
|
|
10
|
+
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
11
|
+
const LOCALE_COOKIE = 'NEXT_LOCALE';
|
|
12
|
+
|
|
13
|
+
const proxyRequest = async (
|
|
14
|
+
request: NextRequest,
|
|
15
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
16
|
+
method: string,
|
|
17
|
+
) => {
|
|
18
|
+
const { path } = await ctx.params;
|
|
19
|
+
const apiPath = path.join('/');
|
|
20
|
+
const url = new URL(`${API_URL}/${apiPath}`);
|
|
21
|
+
|
|
22
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
23
|
+
url.searchParams.set(key, value);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const cookieStore = await cookies();
|
|
27
|
+
const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
28
|
+
const locale =
|
|
29
|
+
cookieStore.get(LOCALE_COOKIE)?.value ??
|
|
30
|
+
request.headers.get('Accept-Language') ??
|
|
31
|
+
undefined;
|
|
32
|
+
|
|
33
|
+
const headers: Record<string, string> = {};
|
|
34
|
+
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
35
|
+
if (locale) headers['Accept-Language'] = locale;
|
|
36
|
+
|
|
37
|
+
let body: BodyInit | undefined;
|
|
38
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
39
|
+
const contentType = request.headers.get('Content-Type');
|
|
40
|
+
if (contentType?.includes('multipart/form-data')) {
|
|
41
|
+
body = await request.formData();
|
|
42
|
+
} else {
|
|
43
|
+
headers['Content-Type'] = 'application/json';
|
|
44
|
+
body = await request.text();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let response: Response;
|
|
49
|
+
try {
|
|
50
|
+
response = await fetch(url.toString(), { method, headers, body });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`[PROXY] ${method} ${url.toString()} —`, error);
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{
|
|
55
|
+
result: 'ERROR',
|
|
56
|
+
data: null,
|
|
57
|
+
error: {
|
|
58
|
+
code: 'NETWORK_ERROR',
|
|
59
|
+
message: 'Failed to reach upstream server.',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{ status: 502 },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
logApiError('PROXY', {
|
|
70
|
+
url: url.toString(),
|
|
71
|
+
method,
|
|
72
|
+
status: response.status,
|
|
73
|
+
requestBody: typeof body === 'string' ? body : undefined,
|
|
74
|
+
responseBody: data,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
captureApiError({
|
|
78
|
+
url: url.toString(),
|
|
79
|
+
apiPath,
|
|
80
|
+
method,
|
|
81
|
+
status: response.status,
|
|
82
|
+
responseBody: data,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return NextResponse.json(data, { status: response.status });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const GET = (
|
|
90
|
+
req: NextRequest,
|
|
91
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
92
|
+
) => proxyRequest(req, ctx, 'GET');
|
|
93
|
+
|
|
94
|
+
export const POST = (
|
|
95
|
+
req: NextRequest,
|
|
96
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
97
|
+
) => proxyRequest(req, ctx, 'POST');
|
|
98
|
+
|
|
99
|
+
export const PUT = (
|
|
100
|
+
req: NextRequest,
|
|
101
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
102
|
+
) => proxyRequest(req, ctx, 'PUT');
|
|
103
|
+
|
|
104
|
+
export const PATCH = (
|
|
105
|
+
req: NextRequest,
|
|
106
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
107
|
+
) => proxyRequest(req, ctx, 'PATCH');
|
|
108
|
+
|
|
109
|
+
export const DELETE = (
|
|
110
|
+
req: NextRequest,
|
|
111
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
112
|
+
) => proxyRequest(req, ctx, 'DELETE');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import '@workspace/ui-app-name/globals.css';
|
|
3
|
+
import { RootLayout } from '@/components/layouts/RootLayout';
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: 'sh-ui app',
|
|
7
|
+
description: 'sh-ui 기반 앱 — metadata 를 변경하세요.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function Layout({
|
|
11
|
+
children,
|
|
12
|
+
}: Readonly<{
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}>) {
|
|
15
|
+
return <RootLayout>{children}</RootLayout>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@/pages/sign-in';
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Component,
|
|
5
|
+
type ComponentType,
|
|
6
|
+
type ErrorInfo,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
Suspense,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { QueryErrorResetBoundary } from '@tanstack/react-query';
|
|
11
|
+
|
|
12
|
+
export type ErrorFallbackProps = {
|
|
13
|
+
error: Error | null;
|
|
14
|
+
resetErrorBoundary: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ErrorBoundaryProps = {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
fallback?: ComponentType<ErrorFallbackProps>;
|
|
20
|
+
onReset: () => void;
|
|
21
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ErrorBoundaryState = {
|
|
25
|
+
hasError: boolean;
|
|
26
|
+
error: Error | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
30
|
+
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
31
|
+
|
|
32
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
33
|
+
return { hasError: true, error };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
37
|
+
this.props.onError?.(error, info);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
resetErrorBoundary = () => {
|
|
41
|
+
this.props.onReset();
|
|
42
|
+
this.setState({ hasError: false, error: null });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
render() {
|
|
46
|
+
const { hasError, error } = this.state;
|
|
47
|
+
const { children, fallback: Fallback } = this.props;
|
|
48
|
+
|
|
49
|
+
if (hasError && Fallback) {
|
|
50
|
+
return (
|
|
51
|
+
<Fallback error={error} resetErrorBoundary={this.resetErrorBoundary} />
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return children;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type FallbackBoundaryProps = {
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
errorFallback?: ComponentType<ErrorFallbackProps>;
|
|
62
|
+
suspenseFallback?: ReactNode;
|
|
63
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Suspense + ErrorBoundary 합성. React Query 의 reset 신호에 맞춰
|
|
68
|
+
* `errorFallback` 의 `resetErrorBoundary` 가 쿼리까지 함께 리셋한다.
|
|
69
|
+
*/
|
|
70
|
+
export function FallbackBoundary({
|
|
71
|
+
children,
|
|
72
|
+
errorFallback,
|
|
73
|
+
suspenseFallback,
|
|
74
|
+
onError,
|
|
75
|
+
}: FallbackBoundaryProps) {
|
|
76
|
+
return (
|
|
77
|
+
<QueryErrorResetBoundary>
|
|
78
|
+
{({ reset }) => (
|
|
79
|
+
<ErrorBoundary
|
|
80
|
+
onReset={reset}
|
|
81
|
+
fallback={errorFallback}
|
|
82
|
+
onError={onError}
|
|
83
|
+
>
|
|
84
|
+
<Suspense fallback={suspenseFallback}>{children}</Suspense>
|
|
85
|
+
</ErrorBoundary>
|
|
86
|
+
)}
|
|
87
|
+
</QueryErrorResetBoundary>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
dehydrate,
|
|
5
|
+
type FetchQueryOptions,
|
|
6
|
+
HydrationBoundary,
|
|
7
|
+
} from '@tanstack/react-query';
|
|
8
|
+
|
|
9
|
+
import { getQueryClient } from '@/lib/utils/getQueryClient';
|
|
10
|
+
|
|
11
|
+
export type FetchOptions = Pick<FetchQueryOptions, 'queryKey' | 'queryFn'>;
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
fetchOptions?: FetchOptions[] | FetchOptions | null;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RSC 에서 prefetch 를 끝낸 뒤 dehydrated state 로 클라이언트에 hydrate.
|
|
20
|
+
* 단일/배열 둘 다 받는다.
|
|
21
|
+
*/
|
|
22
|
+
export async function PrefetchBoundary({ fetchOptions, children }: Props) {
|
|
23
|
+
const queryClient = getQueryClient();
|
|
24
|
+
|
|
25
|
+
if (fetchOptions) {
|
|
26
|
+
const list = Array.isArray(fetchOptions) ? fetchOptions : [fetchOptions];
|
|
27
|
+
await Promise.all(list.map((opt) => queryClient.prefetchQuery(opt)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
32
|
+
{children}
|
|
33
|
+
</HydrationBoundary>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GlobalProvider } from '@/components/providers';
|
|
2
|
+
|
|
3
|
+
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
// `lang` 은 앱의 주 언어. 영어 등 다른 언어 우선이면 'en' 으로 바꾸거나
|
|
5
|
+
// next-intl 플러그인을 활성화해 라우트 기반 자동 분기로 전환하세요.
|
|
6
|
+
return (
|
|
7
|
+
<html lang='ko' suppressHydrationWarning>
|
|
8
|
+
<body>
|
|
9
|
+
<GlobalProvider>{children}</GlobalProvider>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Toaster } from 'sonner';
|
|
3
|
+
|
|
4
|
+
import { QueryClientProvider } from '../tanstack/QueryClientProvider';
|
|
5
|
+
import { TanstackDevtoolsProvider } from '../tanstack/TanstackDevtoolsProvider';
|
|
6
|
+
import { ThemeProvider } from '../theme/ThemeProvider';
|
|
7
|
+
|
|
8
|
+
interface GlobalProviderProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function GlobalProvider({ children }: GlobalProviderProps) {
|
|
13
|
+
return (
|
|
14
|
+
<ThemeProvider>
|
|
15
|
+
<QueryClientProvider>
|
|
16
|
+
<TanstackDevtoolsProvider>
|
|
17
|
+
<Toaster />
|
|
18
|
+
{children}
|
|
19
|
+
</TanstackDevtoolsProvider>
|
|
20
|
+
</QueryClientProvider>
|
|
21
|
+
</ThemeProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GlobalProvider } from './GlobalProvider';
|
package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import { getBrowserQueryClient } from '@/lib/api/queryClient';
|
|
7
|
+
|
|
8
|
+
export function QueryClientProvider({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<TanstackQueryClientProvider client={getBrowserQueryClient()}>
|
|
11
|
+
{children}
|
|
12
|
+
</TanstackQueryClientProvider>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export function TanstackDevtoolsProvider({ children }: { children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
{children}
|
|
10
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 다크/라이트 테마 — next-themes ThemeProvider 를 wrap.
|
|
8
|
+
*
|
|
9
|
+
* - `attribute='class'` — `<html class="dark">` 토글 (Tailwind dark variant 와 호환)
|
|
10
|
+
* - `defaultTheme='system'` + `enableSystem` — OS 설정에 자동 동기화. light/dark 만
|
|
11
|
+
* 노출하려면 `enableSystem` 을 false 로
|
|
12
|
+
* - `disableTransitionOnChange` — 토글 순간 transition 깜빡임 차단
|
|
13
|
+
*
|
|
14
|
+
* useTheme 는 next-themes 에서 직접 import: `import { useTheme } from 'next-themes'`
|
|
15
|
+
*/
|
|
16
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
17
|
+
return (
|
|
18
|
+
<NextThemesProvider
|
|
19
|
+
attribute='class'
|
|
20
|
+
defaultTheme='system'
|
|
21
|
+
enableSystem
|
|
22
|
+
disableTransitionOnChange
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</NextThemesProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
File without changes
|