@simplysm/sd-cli 13.0.70 → 13.0.72

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.
Files changed (103) hide show
  1. package/dist/commands/init.d.ts +4 -5
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +26 -8
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/sd-cli-entry.d.ts.map +1 -1
  6. package/dist/sd-cli-entry.js +0 -20
  7. package/dist/sd-cli-entry.js.map +1 -1
  8. package/package.json +4 -4
  9. package/src/commands/init.ts +40 -21
  10. package/src/sd-cli-entry.ts +0 -24
  11. package/templates/init/{.prettierrc.yaml.hbs → .prettierrc.yaml} +1 -1
  12. package/templates/init/eslint.config.ts +15 -0
  13. package/templates/init/mise.toml +3 -0
  14. package/templates/init/package.json.hbs +8 -7
  15. package/templates/init/packages/client-admin/index.html.hbs +144 -0
  16. package/templates/init/packages/client-admin/package.json.hbs +26 -0
  17. package/templates/init/packages/client-admin/public/assets/logo-landscape.png +0 -0
  18. package/templates/init/packages/client-admin/public/assets/logo.png +0 -0
  19. package/templates/init/packages/client-admin/src/App.tsx +42 -0
  20. package/templates/init/packages/client-admin/src/dev/DevDialog.tsx +34 -0
  21. package/templates/{add-client/__CLIENT__/src/main.css.hbs → init/packages/client-admin/src/main.css} +1 -1
  22. package/templates/init/packages/client-admin/src/main.tsx.hbs +146 -0
  23. package/templates/init/packages/client-admin/src/providers/AppServiceProvider.tsx.hbs +103 -0
  24. package/templates/init/packages/client-admin/src/providers/AppStructureProvider.tsx +84 -0
  25. package/templates/init/packages/client-admin/src/providers/AuthProvider.tsx.hbs +71 -0
  26. package/templates/init/packages/client-admin/src/providers/configureSharedData.ts.hbs +67 -0
  27. package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +132 -0
  28. package/templates/init/packages/client-admin/src/views/home/HomeView.tsx +108 -0
  29. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeDetail.tsx.hbs +262 -0
  30. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +271 -0
  31. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleDetail.tsx.hbs +154 -0
  32. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionDetail.tsx.hbs +123 -0
  33. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionView.tsx +52 -0
  34. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +125 -0
  35. package/templates/init/packages/client-admin/src/views/home/main/MainView.tsx.hbs +13 -0
  36. package/templates/init/packages/client-admin/src/views/home/my-info/MyInfoDetail.tsx.hbs +248 -0
  37. package/templates/init/packages/client-admin/src/views/home/system/system-log/SystemLogSheet.tsx.hbs +169 -0
  38. package/templates/init/packages/client-admin/src/views/not-found/NotFoundView.tsx +15 -0
  39. package/templates/init/packages/client-admin/tailwind.config.ts +10 -0
  40. package/templates/init/packages/db-main/package.json.hbs +13 -0
  41. package/templates/init/packages/db-main/src/MainDbContext.ts +20 -0
  42. package/templates/init/packages/db-main/src/dataLogExt.ts +127 -0
  43. package/templates/init/packages/db-main/src/index.ts +10 -0
  44. package/templates/init/packages/db-main/src/tables/Employee.ts +24 -0
  45. package/templates/init/packages/db-main/src/tables/EmployeeConfig.ts +13 -0
  46. package/templates/init/packages/db-main/src/tables/Role.ts +9 -0
  47. package/templates/init/packages/db-main/src/tables/RolePermission.ts +13 -0
  48. package/templates/init/packages/db-main/src/tables/_DataLog.ts +19 -0
  49. package/templates/init/packages/db-main/src/tables/_Log.ts +16 -0
  50. package/templates/init/packages/server/package.json.hbs +20 -0
  51. package/templates/init/packages/server/public-dev/dev//354/264/210/352/270/260/355/231/224.xlsx +0 -0
  52. package/templates/init/packages/server/src/index.ts +4 -0
  53. package/templates/init/packages/server/src/main.ts.hbs +34 -0
  54. package/templates/init/packages/server/src/services/AuthService.ts.hbs +171 -0
  55. package/templates/init/packages/server/src/services/DevService.ts.hbs +94 -0
  56. package/templates/init/packages/server/src/services/EmployeeService.ts.hbs +122 -0
  57. package/templates/init/packages/server/src/services/RoleService.ts.hbs +59 -0
  58. package/templates/init/{pnpm-workspace.yaml.hbs → pnpm-workspace.yaml} +3 -1
  59. package/templates/init/sd.config.ts.hbs +30 -1
  60. package/templates/init/tests/e2e/package.json.hbs +16 -0
  61. package/templates/init/tests/e2e/src/e2e.spec.ts +36 -0
  62. package/templates/init/tests/e2e/src/employee-crud.ts +204 -0
  63. package/templates/init/tests/e2e/src/login.ts +61 -0
  64. package/templates/init/tests/e2e/vitest.setup.ts.hbs +220 -0
  65. package/templates/init/tsconfig.json.hbs +0 -11
  66. package/templates/init/{vitest.config.ts.hbs → vitest.config.ts} +16 -12
  67. package/tests/infra/WorkerManager.spec.ts +1 -1
  68. package/tests/replace-deps.spec.ts +2 -2
  69. package/tests/run-lint.spec.ts +6 -6
  70. package/tests/run-typecheck.spec.ts +3 -3
  71. package/tests/sd-cli.spec.ts +1 -1
  72. package/dist/commands/add-client.d.ts +0 -18
  73. package/dist/commands/add-client.d.ts.map +0 -1
  74. package/dist/commands/add-client.js +0 -79
  75. package/dist/commands/add-client.js.map +0 -6
  76. package/dist/commands/add-server.d.ts +0 -18
  77. package/dist/commands/add-server.d.ts.map +0 -1
  78. package/dist/commands/add-server.js +0 -83
  79. package/dist/commands/add-server.js.map +0 -6
  80. package/dist/utils/config-editor.d.ts +0 -17
  81. package/dist/utils/config-editor.d.ts.map +0 -1
  82. package/dist/utils/config-editor.js +0 -79
  83. package/dist/utils/config-editor.js.map +0 -6
  84. package/src/commands/add-client.ts +0 -126
  85. package/src/commands/add-server.ts +0 -138
  86. package/src/utils/config-editor.ts +0 -141
  87. package/templates/add-client/__CLIENT__/index.html.hbs +0 -13
  88. package/templates/add-client/__CLIENT__/package.json.hbs +0 -16
  89. package/templates/add-client/__CLIENT__/src/App.tsx.hbs +0 -65
  90. package/templates/add-client/__CLIENT__/src/appStructure.ts.hbs +0 -20
  91. package/templates/add-client/__CLIENT__/src/main.tsx.hbs +0 -24
  92. package/templates/add-client/__CLIENT__/src/pages/HomePage.tsx.hbs +0 -9
  93. package/templates/add-client/__CLIENT__/tailwind.config.ts.hbs +0 -15
  94. package/templates/add-server/__SERVER__/package.json.hbs +0 -10
  95. package/templates/add-server/__SERVER__/src/main.ts.hbs +0 -14
  96. package/templates/init/.gitignore.hbs +0 -26
  97. package/templates/init/.npmrc.hbs +0 -1
  98. package/templates/init/eslint.config.ts.hbs +0 -5
  99. package/templates/init/mise.toml.hbs +0 -3
  100. package/tests/config-editor.spec.ts +0 -160
  101. /package/templates/init/{.prettierignore.hbs → .prettierignore} +0 -0
  102. /package/templates/{add-client/__CLIENT__ → init/packages/client-admin}/public/favicon.ico +0 -0
  103. /package/templates/init/{stylelint.config.ts.hbs → stylelint.config.ts} +0 -0
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import type { Page } from "playwright";
3
+
4
+ export function employeeCrudTests(ctx: { page: Page; baseUrl: string }) {
5
+ describe("EmployeeSheet CRUD", () => {
6
+ const sheetSelector = '[data-sheet="employee-page-sheet"]';
7
+
8
+ function sheetRows() {
9
+ return ctx.page.locator(`${sheetSelector} tbody tr`);
10
+ }
11
+
12
+ function dialogLocator() {
13
+ return ctx.page.locator("[data-modal-dialog]").last();
14
+ }
15
+
16
+ async function waitForSheetLoaded() {
17
+ await sheetRows().first().waitFor({ timeout: 1000 });
18
+ }
19
+
20
+ async function waitForBusyDone() {
21
+ await ctx.page.waitForTimeout(50);
22
+ await ctx.page.waitForFunction(() => !document.querySelector(".animate-spin"));
23
+ }
24
+
25
+ async function waitForDialogClosed() {
26
+ await ctx.page.waitForFunction(
27
+ () =>
28
+ document.querySelectorAll("[data-modal-dialog]").length === 0 &&
29
+ document.querySelectorAll("[data-modal-backdrop]").length === 0,
30
+ );
31
+ }
32
+
33
+ async function assertNotification(text: string) {
34
+ const alert = ctx.page
35
+ .locator("[data-notification-banner]")
36
+ .filter({ hasText: text })
37
+ .first();
38
+ await alert.waitFor();
39
+ const closeButtons = ctx.page.locator(
40
+ '[data-notification-banner] button[aria-label="알림 닫기"]',
41
+ );
42
+ const count = await closeButtons.count();
43
+ for (let i = count - 1; i >= 0; i--) {
44
+ await closeButtons
45
+ .nth(i)
46
+ .click()
47
+ .catch(() => {});
48
+ }
49
+ await ctx.page.waitForFunction(
50
+ () => document.querySelectorAll("[data-notification-banner]").length === 0,
51
+ );
52
+ }
53
+
54
+ async function clickSearch() {
55
+ await ctx.page.getByRole("button", { name: "조회" }).click();
56
+ await waitForBusyDone();
57
+ }
58
+
59
+ async function clickAdd() {
60
+ await ctx.page.getByRole("button", { name: /등록/ }).click();
61
+ await dialogLocator().waitFor();
62
+ }
63
+
64
+ async function clickEditRow(rowIndex: number) {
65
+ await sheetRows().nth(rowIndex).locator("a").first().click();
66
+ await dialogLocator().waitFor();
67
+ await waitForBusyDone();
68
+ }
69
+
70
+ async function fillDialogField(label: string, value: string) {
71
+ const dlg = dialogLocator();
72
+ const th = dlg.locator("th").filter({ hasText: label });
73
+ const td = th.locator("xpath=following-sibling::td[1]");
74
+ const field = td.locator("input:not([aria-hidden])").first();
75
+ await field.fill(value);
76
+ }
77
+
78
+ async function clickDialogSubmit() {
79
+ await dialogLocator().getByRole("button", { name: "확인" }).click();
80
+ }
81
+
82
+ async function closeDialog() {
83
+ await ctx.page.keyboard.press("Escape");
84
+ await waitForDialogClosed();
85
+ }
86
+
87
+ beforeAll(async () => {
88
+ await ctx.page.goto(`${ctx.baseUrl}/#/home/base/employee`);
89
+ await waitForSheetLoaded();
90
+ });
91
+
92
+ describe("Create", () => {
93
+ it("새 직원 등록", async () => {
94
+ await clickAdd();
95
+
96
+ await fillDialogField("이름", "E2E테스트유저");
97
+ await fillDialogField("이메일", "e2e@test.com");
98
+
99
+ await clickDialogSubmit();
100
+ await assertNotification("저장되었습니다");
101
+ await waitForDialogClosed();
102
+ await clickSearch();
103
+
104
+ const firstCell = sheetRows().first().locator("td").nth(1);
105
+ await expect(firstCell.textContent()).resolves.toContain("E2E테스트유저");
106
+ });
107
+ });
108
+
109
+ describe("Read / Filter", () => {
110
+ it("검색어 필터링", async () => {
111
+ const searchInput = ctx.page.locator("form").first().locator("input").first();
112
+ await searchInput.fill("E2E테스트");
113
+ await clickSearch();
114
+
115
+ const rowCount = await sheetRows().count();
116
+ expect(rowCount).toBe(1);
117
+ await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
118
+ "E2E테스트유저",
119
+ );
120
+
121
+ await searchInput.fill("");
122
+ await clickSearch();
123
+ const allRowCount = await sheetRows().count();
124
+ expect(allRowCount).toBe(2);
125
+ });
126
+ });
127
+
128
+ describe("Update", () => {
129
+ it("기존 유저 이름 수정", async () => {
130
+ await clickEditRow(0);
131
+
132
+ await fillDialogField("이름", "E2E수정유저");
133
+
134
+ await clickDialogSubmit();
135
+ await assertNotification("저장되었습니다");
136
+ await waitForDialogClosed();
137
+ await clickSearch();
138
+
139
+ await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
140
+ "E2E수정유저",
141
+ );
142
+ });
143
+ });
144
+
145
+ describe("Delete", () => {
146
+ it("유저 soft delete", async () => {
147
+ await clickEditRow(0);
148
+
149
+ await dialogLocator().getByRole("button", { name: "삭제" }).click();
150
+ await assertNotification("삭제되었습니다");
151
+ await waitForDialogClosed();
152
+ await clickSearch();
153
+
154
+ const rowCount = await sheetRows().count();
155
+ expect(rowCount).toBe(1);
156
+ await expect(sheetRows().first().locator("td").nth(1).textContent()).resolves.toContain(
157
+ "테스트",
158
+ );
159
+ });
160
+
161
+ it("삭제항목 포함 필터로 삭제된 유저 확인", async () => {
162
+ await ctx.page.getByText("삭제항목 포함").click();
163
+ await clickSearch();
164
+
165
+ const rowCount = await sheetRows().count();
166
+ expect(rowCount).toBe(2);
167
+
168
+ await ctx.page.getByText("삭제항목 포함").click();
169
+ await clickSearch();
170
+ });
171
+ });
172
+
173
+ describe("Error cases", () => {
174
+ it("이름 중복 에러", async () => {
175
+ await clickAdd();
176
+ await fillDialogField("이름", "테스트");
177
+ await clickDialogSubmit();
178
+ await assertNotification("동일한 이름이 이미 등록되어 있습니다");
179
+
180
+ await closeDialog();
181
+ });
182
+
183
+ it("이메일 중복 에러", async () => {
184
+ await clickAdd();
185
+ await fillDialogField("이름", "고유이름");
186
+ await fillDialogField("이메일", "admin@test.com");
187
+ await clickDialogSubmit();
188
+ await assertNotification("동일한 이메일이 이미 등록되어 있습니다");
189
+
190
+ await closeDialog();
191
+ });
192
+
193
+ it("자기 자신 삭제 불가", async () => {
194
+ await clickEditRow(0);
195
+ await waitForBusyDone();
196
+
197
+ const deleteBtn = dialogLocator().getByRole("button", { name: "삭제" });
198
+ expect(await deleteBtn.count()).toBe(0);
199
+
200
+ await closeDialog();
201
+ });
202
+ });
203
+ });
204
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Page } from "playwright";
3
+
4
+ export function loginTests(ctx: { page: Page; baseUrl: string }) {
5
+ describe("Login", () => {
6
+ it("/ 접속 시 /login으로 리다이렉트", async () => {
7
+ await ctx.page.goto(`${ctx.baseUrl}/`, { timeout: 5000 });
8
+ await ctx.page.waitForURL(`${ctx.baseUrl}/#/login`);
9
+ await expect(
10
+ ctx.page.getByRole("button", { name: "로그인" }).textContent(),
11
+ ).resolves.toContain("로그인");
12
+ });
13
+
14
+ it("토큰 없이 /home/main 접근 시 /login으로 리다이렉트", async () => {
15
+ await ctx.page.goto(`${ctx.baseUrl}/#/home/main`);
16
+ await ctx.page.waitForURL(`${ctx.baseUrl}/#/login`);
17
+ await expect(
18
+ ctx.page.getByRole("button", { name: "로그인" }).textContent(),
19
+ ).resolves.toContain("로그인");
20
+ });
21
+
22
+ it("틀린 비밀번호 시 에러 메시지", async () => {
23
+ await ctx.page.getByPlaceholder("이메일을 입력하세요").fill("admin@test.com");
24
+ await ctx.page.getByPlaceholder("비밀번호를 입력하세요").fill("wrongpassword");
25
+ await ctx.page.getByRole("button", { name: "로그인" }).click();
26
+
27
+ const notification = ctx.page.getByRole("alert");
28
+ await notification.waitFor({ timeout: 1000 });
29
+ await expect(notification.textContent()).resolves.toContain(
30
+ "이메일 또는 비밀번호가 올바르지 않습니다",
31
+ );
32
+ });
33
+
34
+ it("올바른 자격 증명으로 로그인 성공", async () => {
35
+ await ctx.page.goto(`${ctx.baseUrl}/#/login`);
36
+ await ctx.page.getByRole("button", { name: "로그인" }).waitFor({ timeout: 2000 });
37
+
38
+ await ctx.page.getByPlaceholder("이메일을 입력하세요").fill("admin@test.com");
39
+ await ctx.page.getByPlaceholder("비밀번호를 입력하세요").fill("test1234");
40
+ await ctx.page.getByRole("button", { name: "로그인" }).click({ timeout: 2000 });
41
+
42
+ await ctx.page.waitForURL(`${ctx.baseUrl}/#/home/main`, { timeout: 2000 });
43
+ const mainContent = ctx.page.locator("main").last();
44
+ await expect(mainContent.locator("h1").textContent()).resolves.toContain("테스트");
45
+ });
46
+
47
+ it("마지막 로그인 이메일 기억", async () => {
48
+ const lastLoginEmail = await ctx.page.evaluate(() =>
49
+ JSON.parse(localStorage.getItem("client-admin.last-login-email") ?? "null"),
50
+ );
51
+ expect(lastLoginEmail).toBe("admin@test.com");
52
+ });
53
+
54
+ it("페이지 새로고침 시 자동 로그인", async () => {
55
+ await ctx.page.reload();
56
+ await ctx.page.waitForURL(`${ctx.baseUrl}/#/home/main`, { timeout: 2000 });
57
+ const mainContent = ctx.page.locator("main").last();
58
+ await expect(mainContent.locator("h1").textContent()).resolves.toContain("테스트");
59
+ });
60
+ });
61
+ }
@@ -0,0 +1,220 @@
1
+ import { type ChildProcess, spawn, execSync } from "child_process";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { createDbConn, createOrm } from "@simplysm/orm-node";
5
+ import { MainDbContext } from "@{{projectName}}/db-main";
6
+ import bcrypt from "bcrypt";
7
+ import { chromium } from "playwright";
8
+ import type { TestProject } from "vitest/node";
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const rootDir = path.resolve(__dirname, "../..");
12
+
13
+ const DB_CONFIG = {
14
+ dialect: "postgresql" as const,
15
+ host: "localhost",
16
+ port: 5432,
17
+ username: "postgres",
18
+ password: "1234",
19
+ database: "{{projectName}}-test",
20
+ };
21
+
22
+ const TEST_USER = {
23
+ name: "테스트",
24
+ email: "admin@test.com",
25
+ password: "test1234",
26
+ };
27
+
28
+ let devServerProcess: ChildProcess | undefined;
29
+
30
+ function killProcessTree(proc: ChildProcess): void {
31
+ if (proc.pid == null) return;
32
+ try {
33
+ if (process.platform === "win32") {
34
+ execSync(`taskkill /T /F /PID ${proc.pid}`, { stdio: "ignore" });
35
+ } else {
36
+ process.kill(-proc.pid, "SIGTERM");
37
+ }
38
+ } catch {
39
+ /* 무시 */
40
+ }
41
+ }
42
+
43
+ function stripAnsi(str: string): string {
44
+ return str.replace(/\x1B\[[0-9;]*m/g, "");
45
+ }
46
+
47
+ function waitForServerUrl(process: ChildProcess, timeout: number): Promise<string> {
48
+ return new Promise((resolve, reject) => {
49
+ let settled = false;
50
+ let output = "";
51
+
52
+ const timer = setTimeout(() => {
53
+ if (!settled) {
54
+ settled = true;
55
+ reject(
56
+ new Error(`dev 서버 URL 대기 시간 초과 (${timeout}ms)\n출력:\n${output.slice(-2000)}`),
57
+ );
58
+ }
59
+ }, timeout);
60
+
61
+ function onData(data: Buffer) {
62
+ const text = stripAnsi(data.toString());
63
+ console.log(text);
64
+ output += text;
65
+
66
+ const urlMatch = text.match(/http:\/\/localhost:\d+\/client-admin\/?/);
67
+ if (urlMatch && !settled) {
68
+ settled = true;
69
+ clearTimeout(timer);
70
+ resolve(urlMatch[0].endsWith("/") ? urlMatch[0].slice(0, -1) : urlMatch[0]);
71
+ }
72
+ }
73
+
74
+ process.stdout?.on("data", onData);
75
+ process.stderr?.on("data", onData);
76
+
77
+ process.on("exit", (code) => {
78
+ if (!settled) {
79
+ settled = true;
80
+ clearTimeout(timer);
81
+ reject(
82
+ new Error(`dev 서버가 비정상 종료됨 (code: ${code})\n출력:\n${output.slice(-2000)}`),
83
+ );
84
+ }
85
+ });
86
+ });
87
+ }
88
+
89
+ async function warmup(baseUrl: string, timeout: number): Promise<void> {
90
+ const browser = await chromium.launch({ headless: true });
91
+ try {
92
+ const page = await browser.newPage();
93
+ await page.goto(baseUrl, { timeout });
94
+
95
+ const result = await Promise.race([
96
+ page
97
+ .locator(".app-loading")
98
+ .waitFor({ state: "detached", timeout })
99
+ .then(() => "ready" as const),
100
+ page
101
+ .locator("vite-error-overlay")
102
+ .waitFor({ state: "attached", timeout })
103
+ .then(async () => {
104
+ const errorText = await page
105
+ .locator("vite-error-overlay")
106
+ .evaluate((el) => el.shadowRoot?.textContent ?? "");
107
+ return `error:${errorText}` as const;
108
+ }),
109
+ ]);
110
+
111
+ if (result.startsWith("error:")) {
112
+ throw new Error(`Vite 빌드 에러:\n${result.slice(6).slice(0, 2000)}`);
113
+ }
114
+ } finally {
115
+ await browser.close();
116
+ }
117
+ }
118
+
119
+ export async function setup(project: TestProject) {
120
+ // 1. 테스트 DB 생성 (없으면)
121
+ console.log("[e2e] 테스트 DB 확인...");
122
+ const adminConn = await createDbConn({ ...DB_CONFIG, database: "postgres" });
123
+ await adminConn.connect();
124
+ try {
125
+ const result = await adminConn.executeParametrized(
126
+ `SELECT 1 FROM pg_database WHERE datname = $1`,
127
+ [DB_CONFIG.database],
128
+ );
129
+ if (result[0].length === 0) {
130
+ await adminConn.execute([`CREATE DATABASE "${DB_CONFIG.database}"`]);
131
+ console.log(`[e2e] DB "${DB_CONFIG.database}" 생성됨.`);
132
+ } else {
133
+ console.log(`[e2e] DB "${DB_CONFIG.database}" 이미 존재.`);
134
+ }
135
+ } finally {
136
+ await adminConn.close();
137
+ }
138
+
139
+ // 2. DB 초기화 및 시딩
140
+ const orm = createOrm(MainDbContext, DB_CONFIG);
141
+ await orm.connectWithoutTransaction(async (db) => {
142
+ await db.initialize({ force: true });
143
+
144
+ // 권한그룹 생성
145
+ await db.role().insert([{ name: "관리자" }]);
146
+ const role = (await db.role().first())!;
147
+
148
+ // 전체 권한 부여
149
+ await db.rolePermission().insert([
150
+ { roleId: role.id, code: "/home/base/employee/use", valueJson: "true" },
151
+ { roleId: role.id, code: "/home/base/employee/edit", valueJson: "true" },
152
+ { roleId: role.id, code: "/home/base/employee/auth/use", valueJson: "true" },
153
+ { roleId: role.id, code: "/home/base/employee/auth/edit", valueJson: "true" },
154
+ { roleId: role.id, code: "/home/base/employee/personal/use", valueJson: "true" },
155
+ { roleId: role.id, code: "/home/base/employee/personal/edit", valueJson: "true" },
156
+ { roleId: role.id, code: "/home/base/employee/payroll/use", valueJson: "true" },
157
+ { roleId: role.id, code: "/home/base/employee/payroll/edit", valueJson: "true" },
158
+ ]);
159
+
160
+ // 테스트 유저 생성 (roleId 포함)
161
+ const encryptedPassword = await bcrypt.hash(TEST_USER.password, 10);
162
+ await db.employee().insert([
163
+ {
164
+ name: TEST_USER.name,
165
+ email: TEST_USER.email,
166
+ encryptedPassword,
167
+ roleId: role.id,
168
+ isDeleted: false,
169
+ },
170
+ ]);
171
+ });
172
+ console.log("[e2e] DB 초기화 및 시딩 완료.");
173
+
174
+ // 3. dev 서버 시작
175
+ console.log("[e2e] dev 서버 시작...");
176
+ devServerProcess = spawn("node", ["node_modules/@simplysm/sd-cli/dist/sd-cli.js", "dev"], {
177
+ cwd: rootDir,
178
+ stdio: "pipe",
179
+ ...(process.platform !== "win32" ? { detached: true } : {}),
180
+ env: {
181
+ ...process.env,
182
+ DB_DATABASE: DB_CONFIG.database,
183
+ DB_PORT: String(DB_CONFIG.port),
184
+ NODE_ENV: undefined, // consola가 콘솔을 강제로 warn이상으로 설정하지 못하도록 하기 위함
185
+ TEST: undefined, // consola가 콘솔을 강제로 warn이상으로 설정하지 못하도록 하기 위함
186
+ },
187
+ });
188
+
189
+ let baseUrl: string;
190
+ try {
191
+ // 4. stdout에서 URL 파싱 (90초 타임아웃)
192
+ baseUrl = await waitForServerUrl(devServerProcess, 90_000);
193
+ console.log(`[e2e] dev 서버 URL 감지: ${baseUrl}`);
194
+
195
+ // 5. Playwright warmup — Vite 빌드 트리거 및 에러 감지 (60초 타임아웃)
196
+ console.log("[e2e] Vite 빌드 warmup...");
197
+ await warmup(baseUrl, 60_000);
198
+ console.log("[e2e] warmup 완료. 테스트 시작 준비됨.");
199
+
200
+ project.provide("baseUrl", baseUrl);
201
+ } catch (err) {
202
+ killProcessTree(devServerProcess);
203
+ devServerProcess = undefined;
204
+ throw err;
205
+ }
206
+ }
207
+
208
+ export function teardown() {
209
+ if (devServerProcess != null) {
210
+ console.log("[e2e] dev 서버 종료...");
211
+ killProcessTree(devServerProcess);
212
+ devServerProcess = undefined;
213
+ }
214
+ }
215
+
216
+ declare module "vitest" {
217
+ export interface ProvidedContext {
218
+ baseUrl: string;
219
+ }
220
+ }
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "compilerOptions": {
3
- // 출력
4
3
  "target": "ESNext",
5
4
  "module": "ESNext",
6
5
  "lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"],
7
6
  "moduleResolution": "bundler",
8
-
9
- // JSX (SolidJS)
10
7
  "jsx": "preserve",
11
8
  "jsxImportSource": "solid-js",
12
-
13
- // Strict
14
9
  "strict": true,
15
10
  "noImplicitOverride": true,
16
11
  "noImplicitReturns": true,
@@ -20,19 +15,13 @@
20
15
  "noPropertyAccessFromIndexSignature": true,
21
16
  "forceConsistentCasingInFileNames": true,
22
17
  "useDefineForClassFields": true,
23
-
24
- // 모듈
25
18
  "esModuleInterop": true,
26
19
  "verbatimModuleSyntax": true,
27
-
28
- // 빌드
29
20
  "declaration": true,
30
21
  "declarationMap": true,
31
22
  "sourceMap": true,
32
23
  "skipLibCheck": true,
33
24
  "importHelpers": true,
34
-
35
- // 경로
36
25
  "baseUrl": ".",
37
26
  "paths": {
38
27
  "@{{projectName}}/*": ["packages/*/src/index.ts"]
@@ -1,6 +1,10 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
1
3
  import { defineConfig } from "vitest/config";
2
4
  import tsconfigPaths from "vite-tsconfig-paths";
3
5
 
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
4
8
  export default defineConfig({
5
9
  plugins: [
6
10
  tsconfigPaths({
@@ -24,22 +28,22 @@ export default defineConfig({
24
28
  {
25
29
  extends: true,
26
30
  test: {
27
- name: "unit",
28
- include: ["packages/*/tests/**/*.spec.{ts,tsx}"],
31
+ name: "node",
32
+ include: ["packages/*/tests/**/*.spec.{ts,tsx,js}"],
29
33
  testTimeout: 30000,
30
34
  },
31
35
  },
32
36
  // E2E 테스트 (Docker + dev 서버 필요)
33
- // {
34
- // extends: true,
35
- // test: {
36
- // name: "e2e",
37
- // include: ["tests/e2e/src/**/*.spec.ts"],
38
- // globalSetup: "./tests/e2e/vitest.setup.ts",
39
- // fileParallelism: false,
40
- // testTimeout: 30000,
41
- // },
42
- // },
37
+ {
38
+ extends: true,
39
+ test: {
40
+ name: "e2e",
41
+ include: ["tests/e2e/src/**/*.spec.ts"],
42
+ globalSetup: "./tests/e2e/vitest.setup.ts",
43
+ fileParallelism: false,
44
+ testTimeout: 30000,
45
+ },
46
+ },
43
47
  ],
44
48
  },
45
49
  });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { WorkerManager } from "../../src/infra/WorkerManager";
3
3
 
4
- // Worker 모킹
4
+ // Mock Worker
5
5
  vi.mock("@simplysm/core-node", () => ({
6
6
  Worker: {
7
7
  create: vi.fn(() => ({
@@ -258,12 +258,12 @@ describe("watchReplaceDeps", () => {
258
258
  beforeEach(async () => {
259
259
  tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "sd-watch-replace-"));
260
260
 
261
- // 소스 패키지 (simplysm/packages/solid)
261
+ // Source package (simplysm/packages/solid)
262
262
  const sourceDir = path.join(tmpDir, "simplysm", "packages", "solid");
263
263
  await fs.promises.mkdir(sourceDir, { recursive: true });
264
264
  await fs.promises.writeFile(path.join(sourceDir, "index.js"), "export default 1;");
265
265
 
266
- // 대상 프로젝트 (.pnpm 스토어 방식)
266
+ // Target project (.pnpm store style)
267
267
  const appRoot = path.join(tmpDir, "app");
268
268
  const pnpmStoreTarget = path.join(
269
269
  appRoot,
@@ -11,7 +11,7 @@ const { mockState, mockJitiImportFn } = vi.hoisted(() => ({
11
11
  mockJitiImportFn: vi.fn(),
12
12
  }));
13
13
 
14
- // 외부 의존성 모킹
14
+ // Mock external dependencies
15
15
 
16
16
  vi.mock("eslint", () => {
17
17
  class MockESLint {
@@ -94,7 +94,7 @@ describe("runLint", () => {
94
94
  originalCwd = process.cwd;
95
95
  process.cwd = vi.fn().mockReturnValue("/project");
96
96
 
97
- // 상태 초기화
97
+ // Reset state
98
98
  mockState.lintResults = [];
99
99
  mockState.lintedFiles = [];
100
100
  mockState.outputFixesCalled = false;
@@ -237,7 +237,7 @@ describe("runLint", () => {
237
237
 
238
238
  await runLint({ targets: [], fix: false, timing: false });
239
239
 
240
- // ESLint 호출되지 않으므로 lintedFiles 배열 유지
240
+ // ESLint is not called, so lintedFiles stays empty
241
241
  expect(mockState.lintedFiles).toHaveLength(0);
242
242
  expect(process.exitCode).toBeUndefined();
243
243
  });
@@ -293,7 +293,7 @@ describe("runLint", () => {
293
293
 
294
294
  await runLint({ targets: [], fix: false, timing: true });
295
295
 
296
- // TIMING 설정되었는지 확인 (함수 내에서 설정됨)
296
+ // Verify TIMING is set (set inside the function)
297
297
  expect(process.env["TIMING"]).toBe("1");
298
298
 
299
299
  // cleanup
@@ -350,12 +350,12 @@ describe("executeLint", () => {
350
350
  originalCwd = process.cwd;
351
351
  process.cwd = vi.fn().mockReturnValue("/project");
352
352
 
353
- // 상태 초기화
353
+ // Reset state
354
354
  mockState.lintResults = [];
355
355
  mockState.lintedFiles = [];
356
356
  mockState.outputFixesCalled = false;
357
357
 
358
- // 기본 ESLint 설정 mock
358
+ // Default ESLint config mock
359
359
  const cwd = "/project";
360
360
  vi.mocked(fsExists).mockImplementation((filePath: string) => {
361
361
  return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
2
  import "@simplysm/core-common"; // Import to use Map.getOrCreate extension method
3
3
 
4
- // 외부 의존성 모킹
4
+ // Mock external dependencies
5
5
  vi.mock("typescript", () => {
6
6
  const DiagnosticCategory = {
7
7
  Error: 1,
@@ -194,7 +194,7 @@ describe("runTypecheck", () => {
194
194
  options: [],
195
195
  });
196
196
 
197
- // Worker 통해 실행되므로 exitCode 설정되지 않아야
197
+ // Runs via Worker, so exitCode should not be set
198
198
  expect(process.exitCode).toBeUndefined();
199
199
  });
200
200
 
@@ -393,7 +393,7 @@ describe("runTypecheck", () => {
393
393
  vi.mocked(fsExists).mockResolvedValue(false);
394
394
  vi.mocked(fsReadJson).mockResolvedValue({ devDependencies: {} });
395
395
 
396
- // Worker 에러 결과를 반환하도록 모킹
396
+ // Mock Worker to return error results
397
397
  const { Worker } = await import("@simplysm/core-node");
398
398
  vi.mocked(Worker.create).mockReturnValue({
399
399
  build: vi.fn(() =>
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
2
  import { consola, LogLevels } from "consola";
3
3
 
4
- // runLint, runTypecheck, runWatch, runCheck를 모킹
4
+ // Mock runLint, runTypecheck, runWatch, runCheck
5
5
  vi.mock("../src/commands/lint", () => ({
6
6
  runLint: vi.fn(),
7
7
  }));