@simplysm/sd-cli 14.0.97 → 14.0.99

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 (69) hide show
  1. package/dist/commands/init/generators/client.d.ts.map +1 -1
  2. package/dist/commands/init/generators/client.js +2 -1
  3. package/dist/commands/init/generators/client.js.map +1 -1
  4. package/dist/commands/init/generators/root.d.ts.map +1 -1
  5. package/dist/commands/init/generators/root.js +0 -1
  6. package/dist/commands/init/generators/root.js.map +1 -1
  7. package/dist/commands/init/init-client.d.ts +5 -0
  8. package/dist/commands/init/init-client.d.ts.map +1 -0
  9. package/dist/commands/init/init-client.js +117 -0
  10. package/dist/commands/init/init-client.js.map +1 -0
  11. package/dist/commands/init/init.d.ts.map +1 -1
  12. package/dist/commands/init/init.js +1 -2
  13. package/dist/commands/init/init.js.map +1 -1
  14. package/dist/commands/init/normalize.d.ts.map +1 -1
  15. package/dist/commands/init/normalize.js +3 -1
  16. package/dist/commands/init/normalize.js.map +1 -1
  17. package/dist/commands/init/patch.d.ts +16 -0
  18. package/dist/commands/init/patch.d.ts.map +1 -0
  19. package/dist/commands/init/patch.js +247 -0
  20. package/dist/commands/init/patch.js.map +1 -0
  21. package/dist/commands/init/prompts.d.ts +6 -1
  22. package/dist/commands/init/prompts.d.ts.map +1 -1
  23. package/dist/commands/init/prompts.js +46 -31
  24. package/dist/commands/init/prompts.js.map +1 -1
  25. package/dist/commands/init/recover.d.ts +12 -0
  26. package/dist/commands/init/recover.d.ts.map +1 -0
  27. package/dist/commands/init/recover.js +183 -0
  28. package/dist/commands/init/recover.js.map +1 -0
  29. package/dist/commands/init/ts-ast.d.ts +8 -0
  30. package/dist/commands/init/ts-ast.d.ts.map +1 -0
  31. package/dist/commands/init/ts-ast.js +39 -0
  32. package/dist/commands/init/ts-ast.js.map +1 -0
  33. package/dist/commands/init/types.d.ts +2 -0
  34. package/dist/commands/init/types.d.ts.map +1 -1
  35. package/dist/commands/init/validate.d.ts +3 -1
  36. package/dist/commands/init/validate.d.ts.map +1 -1
  37. package/dist/commands/init/validate.js +15 -0
  38. package/dist/commands/init/validate.js.map +1 -1
  39. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  40. package/dist/esbuild/esbuild-client-config.js +17 -0
  41. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  42. package/dist/sd-cli-entry.d.ts.map +1 -1
  43. package/dist/sd-cli-entry.js +15 -2
  44. package/dist/sd-cli-entry.js.map +1 -1
  45. package/package.json +4 -4
  46. package/src/commands/init/generators/client.ts +6 -1
  47. package/src/commands/init/generators/root.ts +0 -1
  48. package/src/commands/init/init-client.ts +155 -0
  49. package/src/commands/init/init.ts +63 -64
  50. package/src/commands/init/normalize.ts +3 -1
  51. package/src/commands/init/patch.ts +306 -0
  52. package/src/commands/init/prompts.ts +56 -34
  53. package/src/commands/init/recover.ts +236 -0
  54. package/src/commands/init/templates/client/public/robots.txt.hbs +2 -0
  55. package/src/commands/init/templates/client/src/app/home/master/role-permission/role.list.ts.hbs +12 -14
  56. package/src/commands/init/templates/client/src/app/home/master/user.list.ts.hbs +25 -27
  57. package/src/commands/init/templates/client/src/app/home/system/data-log/data-log.list.ts.hbs +29 -31
  58. package/src/commands/init/templates/client/src/app/home/system/system-log/system-log.list.ts.hbs +46 -48
  59. package/src/commands/init/templates/workspace-root/gitignore +9 -1
  60. package/src/commands/init/ts-ast.ts +48 -0
  61. package/src/commands/init/types.ts +2 -0
  62. package/src/commands/init/validate.ts +24 -1
  63. package/src/esbuild/esbuild-client-config.ts +17 -0
  64. package/src/sd-cli-entry.ts +18 -5
  65. package/tests/init/patch.spec.ts +222 -0
  66. package/tests/init/recover.spec.ts +236 -0
  67. package/tests/init/render.spec.ts +19 -0
  68. package/src/commands/init/templates/client/public/robots.txt +0 -1
  69. package/src/commands/init/templates/workspace-root/.prettierignore +0 -1
@@ -49,7 +49,7 @@ export async function promptInit(workspaceNameDefault: string): Promise<InitInpu
49
49
  });
50
50
 
51
51
  hasAuth = await confirm({
52
- message: "사용자 인증을 사용할까요? (사용자/역할/권한/로그 테이블 부트스트랩)",
52
+ message: "사용자 인증을 사용할까요? (사용자/역할/권한)",
53
53
  default: true,
54
54
  });
55
55
  if (hasAuth) {
@@ -92,43 +92,12 @@ export async function promptInit(workspaceNameDefault: string): Promise<InitInpu
92
92
  : await confirm({ message: "다른 client 를 더 추가할까요?", default: false });
93
93
  if (!shouldAdd) break;
94
94
 
95
- const clientName = await input({
96
- message: "client 이름 (예: admin → 자동으로 client-admin):",
97
- validate: (v) => KEBAB_CASE_RE.test(v) || "영문 kebab-case 만 허용됩니다.",
98
- });
99
- const clientType = await select<ClientType>({
100
- message: "client 타입:",
101
- choices: [
102
- { name: "일반 웹", value: "web" },
103
- { name: "모바일 (capacitor)", value: "mobile" },
104
- ],
105
- });
106
-
107
- let hasRouter = false;
108
- if (clientType !== "mobile") {
109
- hasRouter = await confirm({
110
- message: `${clientName}: 라우팅 (@angular/router) 을 쓸까요?`,
111
- default: true,
112
- });
113
- }
114
-
115
- let useSsg = false;
116
- if (clientType === "web" && hasRouter) {
117
- useSsg = await confirm({
118
- message: `${clientName}: SSG (SEO 용 빌드 타임 프리렌더) 를 쓸까요?`,
119
- default: false,
120
- });
121
- }
122
-
123
- clients.push({ name: clientName, type: clientType, hasRouter, useSsg });
95
+ clients.push(await promptClientSpec());
124
96
  }
125
97
 
126
98
  let mobileAppId: string | undefined;
127
99
  if (clients.some((c) => c.type === "mobile")) {
128
- mobileAppId = await input({
129
- message: "capacitor appId (reverse-DNS, 예: kr.co.example.app):",
130
- validate: (v) => APPID_RE.test(v) || "reverse-DNS 형식이어야 합니다.",
131
- });
100
+ mobileAppId = await promptMobileAppId();
132
101
  }
133
102
 
134
103
  return {
@@ -146,3 +115,56 @@ export async function promptInit(workspaceNameDefault: string): Promise<InitInpu
146
115
  serverPort,
147
116
  };
148
117
  }
118
+
119
+ /** init client — 신규 클라이언트 질문 (워크스페이스에 capacitor appId 가 없고 mobile 이면 appId 질문 포함) */
120
+ export async function promptInitClient(
121
+ hasExistingMobileAppId: boolean,
122
+ ): Promise<{ client: ClientInputSpec; mobileAppId?: string }> {
123
+ const client = await promptClientSpec();
124
+
125
+ let mobileAppId: string | undefined;
126
+ if (client.type === "mobile" && !hasExistingMobileAppId) {
127
+ mobileAppId = await promptMobileAppId();
128
+ }
129
+
130
+ return { client, mobileAppId };
131
+ }
132
+
133
+ async function promptClientSpec(): Promise<ClientInputSpec> {
134
+ const clientName = await input({
135
+ message: "client 이름 (예: admin → 자동으로 client-admin):",
136
+ validate: (v) => KEBAB_CASE_RE.test(v) || "영문 kebab-case 만 허용됩니다.",
137
+ });
138
+ const clientType = await select<ClientType>({
139
+ message: "client 타입:",
140
+ choices: [
141
+ { name: "일반 웹", value: "web" },
142
+ { name: "모바일 (capacitor)", value: "mobile" },
143
+ ],
144
+ });
145
+
146
+ let hasRouter = false;
147
+ if (clientType !== "mobile") {
148
+ hasRouter = await confirm({
149
+ message: `${clientName}: 라우팅 (@angular/router) 을 쓸까요?`,
150
+ default: true,
151
+ });
152
+ }
153
+
154
+ let useSsg = false;
155
+ if (clientType === "web" && hasRouter) {
156
+ useSsg = await confirm({
157
+ message: `${clientName}: SSG (SEO 용 빌드 타임 프리렌더) 를 쓸까요?`,
158
+ default: false,
159
+ });
160
+ }
161
+
162
+ return { name: clientName, type: clientType, hasRouter, useSsg };
163
+ }
164
+
165
+ async function promptMobileAppId(): Promise<string> {
166
+ return input({
167
+ message: "capacitor appId (reverse-DNS, 예: kr.co.example.app):",
168
+ validate: (v) => APPID_RE.test(v) || "reverse-DNS 형식이어야 합니다.",
169
+ });
170
+ }
@@ -0,0 +1,236 @@
1
+ import path from "path";
2
+ import ts from "typescript";
3
+ import { str } from "@simplysm/core-common";
4
+ import { fsx } from "@simplysm/core-node";
5
+ import { findPackagesObject, getObjectProp, getProp, getPropertyName, getStringProp } from "./ts-ast";
6
+ import type { ClientInputSpec, DbDialect, InitInput } from "./types";
7
+
8
+ /** 기존 워크스페이스 파일에서 복원한 init 입력 + 부가 정보 */
9
+ export interface RecoveredWorkspace {
10
+ /** 기존 워크스페이스를 표현하는 init 입력 (clients = 기존 클라이언트 목록) */
11
+ input: InitInput;
12
+ /** packages/client-common 패키지 실재 여부 */
13
+ hasClientCommonPkg: boolean;
14
+ /** common/src/app-structure.ts 의 실제 export 식별자 목록 (선언 순서) */
15
+ appStructureExportNames: string[];
16
+ }
17
+
18
+ export async function recoverWorkspace(cwd: string): Promise<RecoveredWorkspace> {
19
+ const rootPkg = await readRootPackageJson(cwd);
20
+ const sdConfig = await parseSdConfig(cwd);
21
+
22
+ const clients: ClientInputSpec[] = [];
23
+ for (const c of sdConfig.clients) {
24
+ clients.push({
25
+ name: c.name,
26
+ type: c.isMobile ? "mobile" : "web",
27
+ hasRouter: await fsx.exists(path.resolve(cwd, "packages", c.name, "src/routes.ts")),
28
+ useSsg: c.useSsg,
29
+ });
30
+ }
31
+
32
+ const commonSrc = path.resolve(cwd, "packages/common/src");
33
+ const appStructurePath = path.join(commonSrc, "app-structure.ts");
34
+ const hasAuth = await fsx.exists(appStructurePath);
35
+
36
+ let dbContextName: string | undefined;
37
+ let userEntityName: string | undefined;
38
+ let userEntityLabel: string | undefined;
39
+ let appStructureExportNames: string[] = [];
40
+
41
+ if (sdConfig.hasDb) {
42
+ const dbFolderName = await findDbFolderName(commonSrc);
43
+ dbContextName = str.toCamelCase(dbFolderName.slice("db-".length));
44
+
45
+ if (hasAuth) {
46
+ userEntityName = await findUserEntityName(path.join(commonSrc, dbFolderName));
47
+ }
48
+ }
49
+
50
+ if (hasAuth) {
51
+ const appStructureSource = await fsx.read(appStructurePath);
52
+ const sf = ts.createSourceFile(
53
+ "app-structure.ts",
54
+ appStructureSource,
55
+ ts.ScriptTarget.Latest,
56
+ true,
57
+ );
58
+ appStructureExportNames = findExportedVariableNames(sf);
59
+ if (userEntityName != null) {
60
+ userEntityLabel = findMenuTitleByCode(sf, userEntityName);
61
+ if (userEntityLabel == null) {
62
+ throw new Error(
63
+ `common/src/app-structure.ts 에서 사용자 엔티티("${userEntityName}") 메뉴 정의를 찾을 수 없습니다.`,
64
+ );
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ input: {
71
+ workspaceName: rootPkg.workspaceName,
72
+ description: rootPkg.description,
73
+ hasServer: sdConfig.hasServer,
74
+ clients,
75
+ hasDb: sdConfig.hasDb,
76
+ dbDialect: sdConfig.dbDialect,
77
+ dbContextName,
78
+ hasAuth,
79
+ userEntityName,
80
+ userEntityLabel,
81
+ mobileAppId: sdConfig.mobileAppId,
82
+ serverPort: sdConfig.serverPort,
83
+ },
84
+ hasClientCommonPkg: await fsx.exists(path.resolve(cwd, "packages/client-common")),
85
+ appStructureExportNames,
86
+ };
87
+ }
88
+
89
+ //-- 루트 package.json
90
+
91
+ async function readRootPackageJson(
92
+ cwd: string,
93
+ ): Promise<{ workspaceName: string; description: string }> {
94
+ const pkgPath = path.resolve(cwd, "package.json");
95
+ if (!(await fsx.exists(pkgPath))) {
96
+ throw new Error(`루트 package.json 을 찾을 수 없습니다: ${pkgPath}`);
97
+ }
98
+ const pkg = JSON.parse(await fsx.read(pkgPath)) as { name?: string; description?: string };
99
+ if (pkg.name == null) {
100
+ throw new Error("루트 package.json 에 name 이 없습니다.");
101
+ }
102
+ return { workspaceName: pkg.name, description: pkg.description ?? "" };
103
+ }
104
+
105
+ //-- sd.config.ts
106
+
107
+ interface SdConfigInfo {
108
+ hasServer: boolean;
109
+ serverPort?: number;
110
+ hasDb: boolean;
111
+ dbDialect?: DbDialect;
112
+ mobileAppId?: string;
113
+ clients: { name: string; isMobile: boolean; useSsg: boolean }[];
114
+ }
115
+
116
+ async function parseSdConfig(cwd: string): Promise<SdConfigInfo> {
117
+ const configPath = path.resolve(cwd, "sd.config.ts");
118
+ if (!(await fsx.exists(configPath))) {
119
+ throw new Error(`sd.config.ts 를 찾을 수 없습니다: ${configPath}`);
120
+ }
121
+ const source = await fsx.read(configPath);
122
+ const sf = ts.createSourceFile("sd.config.ts", source, ts.ScriptTarget.Latest, true);
123
+
124
+ const packagesObj = findPackagesObject(sf);
125
+ if (packagesObj == null) {
126
+ throw new Error("sd.config.ts 에서 packages 정의를 찾을 수 없습니다.");
127
+ }
128
+
129
+ const info: SdConfigInfo = { hasServer: false, hasDb: false, clients: [] };
130
+
131
+ for (const prop of packagesObj.properties) {
132
+ if (!ts.isPropertyAssignment(prop) || !ts.isObjectLiteralExpression(prop.initializer)) {
133
+ continue;
134
+ }
135
+ const pkgName = getPropertyName(prop.name);
136
+ if (pkgName == null) continue;
137
+ const pkgObj = prop.initializer;
138
+ const target = getStringProp(pkgObj, "target");
139
+
140
+ if (target === "server") {
141
+ info.hasServer = true;
142
+
143
+ const envObj = getObjectProp(pkgObj, "env");
144
+ const portStr = envObj != null ? getStringProp(envObj, "SERVER_PORT") : undefined;
145
+ if (portStr != null) info.serverPort = Number(portStr);
146
+
147
+ const configsObj = getObjectProp(pkgObj, "configs");
148
+ const ormObj = configsObj != null ? getObjectProp(configsObj, "orm") : undefined;
149
+ if (ormObj != null) {
150
+ info.hasDb = true;
151
+ const firstConn = ormObj.properties.find(
152
+ (p): p is ts.PropertyAssignment =>
153
+ ts.isPropertyAssignment(p) && ts.isObjectLiteralExpression(p.initializer),
154
+ );
155
+ if (firstConn != null) {
156
+ info.dbDialect = getStringProp(
157
+ firstConn.initializer as ts.ObjectLiteralExpression,
158
+ "dialect",
159
+ ) as DbDialect | undefined;
160
+ }
161
+ }
162
+ } else if (target === "client") {
163
+ const capacitorObj = getObjectProp(pkgObj, "capacitor");
164
+ if (capacitorObj != null && info.mobileAppId == null) {
165
+ info.mobileAppId = getStringProp(capacitorObj, "appId");
166
+ }
167
+ info.clients.push({
168
+ name: pkgName,
169
+ isMobile: capacitorObj != null,
170
+ useSsg: getProp(pkgObj, "prerender") != null,
171
+ });
172
+ }
173
+ }
174
+
175
+ return info;
176
+ }
177
+
178
+ //-- common 패키지 구조
179
+
180
+ async function findDbFolderName(commonSrc: string): Promise<string> {
181
+ if (!(await fsx.exists(commonSrc))) {
182
+ throw new Error(`packages/common/src 를 찾을 수 없습니다: ${commonSrc}`);
183
+ }
184
+ const children = await fsx.readdir(commonSrc);
185
+ const dbFolderName = children.find((c) => c.startsWith("db-"));
186
+ if (dbFolderName == null) {
187
+ throw new Error("packages/common/src 에서 DB context 폴더(db-*)를 찾을 수 없습니다.");
188
+ }
189
+ return dbFolderName;
190
+ }
191
+
192
+ async function findUserEntityName(dbFolder: string): Promise<string> {
193
+ const masterDir = path.join(dbFolder, "tables/master");
194
+ if (!(await fsx.exists(masterDir))) {
195
+ throw new Error(`사용자 엔티티 테이블 폴더를 찾을 수 없습니다: ${masterDir}`);
196
+ }
197
+ const fileNames = await fsx.readdir(masterDir);
198
+ for (const fileName of fileNames) {
199
+ if (!fileName.endsWith(".ts") || fileName.endsWith("-config.ts")) continue;
200
+ const base = fileName.slice(0, -".ts".length);
201
+ if (fileNames.includes(`${base}-config.ts`)) return base;
202
+ }
203
+ throw new Error(
204
+ `사용자 엔티티 테이블 쌍(<엔티티>.ts + <엔티티>-config.ts)을 찾을 수 없습니다: ${masterDir}`,
205
+ );
206
+ }
207
+
208
+ //-- app-structure.ts
209
+
210
+ function findExportedVariableNames(sf: ts.SourceFile): string[] {
211
+ const exportNames: string[] = [];
212
+ for (const stmt of sf.statements) {
213
+ if (!ts.isVariableStatement(stmt)) continue;
214
+ const isExported = stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
215
+ if (isExported !== true) continue;
216
+ for (const decl of stmt.declarationList.declarations) {
217
+ if (ts.isIdentifier(decl.name)) exportNames.push(decl.name.text);
218
+ }
219
+ }
220
+ return exportNames;
221
+ }
222
+
223
+ function findMenuTitleByCode(sf: ts.SourceFile, code: string): string | undefined {
224
+ let title: string | undefined;
225
+ const visit = (node: ts.Node): void => {
226
+ if (title != null) return;
227
+ if (ts.isObjectLiteralExpression(node) && getStringProp(node, "code") === code) {
228
+ title = getStringProp(node, "title");
229
+ if (title != null) return;
230
+ }
231
+ ts.forEachChild(node, visit);
232
+ };
233
+ visit(sf);
234
+ return title;
235
+ }
236
+
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ {{client.robotsDirective}}
@@ -86,20 +86,18 @@ const ITEMS_PER_PAGE = 50;
86
86
  (restore)="onRestore($event)"
87
87
  >
88
88
  <ng-template #filterTpl>
89
- <div class="form-box-inline">
90
- <div>
91
- <label>검색어</label>
92
- <sd-textfield
93
- [type]="'text'"
94
- [(value)]="filter().searchText"
95
- (valueChange)="mark(filter)"
96
- />
97
- </div>
98
- <div class="form-box-item">
99
- <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
100
- 삭제항목 포함
101
- </sd-checkbox>
102
- </div>
89
+ <div>
90
+ <label>검색어</label>
91
+ <sd-textfield
92
+ [type]="'text'"
93
+ [(value)]="filter().searchText"
94
+ (valueChange)="mark(filter)"
95
+ />
96
+ </div>
97
+ <div>
98
+ <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
99
+ 삭제항목 포함
100
+ </sd-checkbox>
103
101
  </div>
104
102
  </ng-template>
105
103
 
@@ -87,33 +87,31 @@ const ITEMS_PER_PAGE = 50;
87
87
  (restore)="onRestore($event)"
88
88
  >
89
89
  <ng-template #filterTpl>
90
- <div class="form-box-inline">
91
- <div>
92
- <label>검색어</label>
93
- <sd-textfield
94
- [type]="'text'"
95
- [(value)]="filter().searchText"
96
- (valueChange)="mark(filter)"
97
- />
98
- </div>
99
- <div>
100
- <label>역할</label>
101
- <sd-shared-data-select
102
- [selectMode]="'multi'"
103
- [items]="sharedRoles.items()"
104
- [(value)]="filter().roleIds"
105
- (valueChange)="mark(filter)"
106
- >
107
- <ng-template [itemOf]="sharedRoles.items()" let-item="item">
108
- \{{ item.name }}
109
- </ng-template>
110
- </sd-shared-data-select>
111
- </div>
112
- <div class="form-box-item">
113
- <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
114
- 삭제항목 포함
115
- </sd-checkbox>
116
- </div>
90
+ <div>
91
+ <label>검색어</label>
92
+ <sd-textfield
93
+ [type]="'text'"
94
+ [(value)]="filter().searchText"
95
+ (valueChange)="mark(filter)"
96
+ />
97
+ </div>
98
+ <div>
99
+ <label>역할</label>
100
+ <sd-shared-data-select
101
+ [selectMode]="'multi'"
102
+ [items]="sharedRoles.items()"
103
+ [(value)]="filter().roleIds"
104
+ (valueChange)="mark(filter)"
105
+ >
106
+ <ng-template [itemOf]="sharedRoles.items()" let-item="item">
107
+ \{{ item.name }}
108
+ </ng-template>
109
+ </sd-shared-data-select>
110
+ </div>
111
+ <div>
112
+ <sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
113
+ 삭제항목 포함
114
+ </sd-checkbox>
117
115
  </div>
118
116
  </ng-template>
119
117
 
@@ -74,37 +74,35 @@ const ITEMS_PER_PAGE = 50;
74
74
  (filterSubmit)="onFilterSubmit()"
75
75
  >
76
76
  <ng-template #filterTpl>
77
- <div class="form-box-inline">
78
- <div>
79
- <label>기간</label>
80
- <sd-date-range-picker
81
- [(from)]="filter().fromDate"
82
- (fromChange)="mark(filter)"
83
- [(to)]="filter().toDate"
84
- (toChange)="mark(filter)"
85
- />
86
- </div>
87
- <div>
88
- <label>검색어</label>
89
- <sd-textfield
90
- [type]="'text'"
91
- [(value)]="filter().searchText"
92
- (valueChange)="mark(filter)"
93
- />
94
- </div>
95
- <div>
96
- <label>수행자</label>
97
- <sd-shared-data-select
98
- [selectMode]="'multi'"
99
- [items]="shared{{userEntityPascal}}s.items()"
100
- [(value)]="filter().{{userEntityCamel}}Ids"
101
- (valueChange)="mark(filter)"
102
- >
103
- <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
104
- \{{ item.name }}
105
- </ng-template>
106
- </sd-shared-data-select>
107
- </div>
77
+ <div>
78
+ <label>기간</label>
79
+ <sd-date-range-picker
80
+ [(from)]="filter().fromDate"
81
+ (fromChange)="mark(filter)"
82
+ [(to)]="filter().toDate"
83
+ (toChange)="mark(filter)"
84
+ />
85
+ </div>
86
+ <div>
87
+ <label>검색어</label>
88
+ <sd-textfield
89
+ [type]="'text'"
90
+ [(value)]="filter().searchText"
91
+ (valueChange)="mark(filter)"
92
+ />
93
+ </div>
94
+ <div>
95
+ <label>수행자</label>
96
+ <sd-shared-data-select
97
+ [selectMode]="'multi'"
98
+ [items]="shared{{userEntityPascal}}s.items()"
99
+ [(value)]="filter().{{userEntityCamel}}Ids"
100
+ (valueChange)="mark(filter)"
101
+ >
102
+ <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
103
+ \{{ item.name }}
104
+ </ng-template>
105
+ </sd-shared-data-select>
108
106
  </div>
109
107
  </ng-template>
110
108
 
@@ -82,54 +82,52 @@ type TSeverity = "error" | "warn" | "log";
82
82
  (filterSubmit)="onFilterSubmit()"
83
83
  >
84
84
  <ng-template #filterTpl>
85
- <div class="form-box-inline">
86
- <div>
87
- <label>기간</label>
88
- <sd-date-range-picker
89
- [(from)]="filter().fromDate"
90
- (fromChange)="mark(filter)"
91
- [(to)]="filter().toDate"
92
- (toChange)="mark(filter)"
93
- />
94
- </div>
95
- <div>
96
- <label>심각도</label>
97
- <sd-select [(value)]="filter().severity" (valueChange)="mark(filter)">
98
- <sd-select-item [value]="undefined">
99
- <span class="tx-theme-gray-default">전체</span>
100
- </sd-select-item>
101
- <sd-select-item [value]="'error'">
102
- <sd-label [theme]="'danger'">error</sd-label>
103
- </sd-select-item>
104
- <sd-select-item [value]="'warn'">
105
- <sd-label [theme]="'warning'">warn</sd-label>
106
- </sd-select-item>
107
- <sd-select-item [value]="'log'">
108
- <sd-label [theme]="'gray'">log</sd-label>
109
- </sd-select-item>
110
- </sd-select>
111
- </div>
112
- <div>
113
- <label>검색어</label>
114
- <sd-textfield
115
- [type]="'text'"
116
- [(value)]="filter().searchText"
117
- (valueChange)="mark(filter)"
118
- />
119
- </div>
120
- <div>
121
- <label>사용자</label>
122
- <sd-shared-data-select
123
- [selectMode]="'multi'"
124
- [items]="shared{{userEntityPascal}}s.items()"
125
- [(value)]="filter().{{userEntityCamel}}Ids"
126
- (valueChange)="mark(filter)"
127
- >
128
- <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
129
- \{{ item.name }}
130
- </ng-template>
131
- </sd-shared-data-select>
132
- </div>
85
+ <div>
86
+ <label>기간</label>
87
+ <sd-date-range-picker
88
+ [(from)]="filter().fromDate"
89
+ (fromChange)="mark(filter)"
90
+ [(to)]="filter().toDate"
91
+ (toChange)="mark(filter)"
92
+ />
93
+ </div>
94
+ <div>
95
+ <label>심각도</label>
96
+ <sd-select [(value)]="filter().severity" (valueChange)="mark(filter)">
97
+ <sd-select-item [value]="undefined">
98
+ <span class="tx-theme-gray-default">전체</span>
99
+ </sd-select-item>
100
+ <sd-select-item [value]="'error'">
101
+ <sd-label [theme]="'danger'">error</sd-label>
102
+ </sd-select-item>
103
+ <sd-select-item [value]="'warn'">
104
+ <sd-label [theme]="'warning'">warn</sd-label>
105
+ </sd-select-item>
106
+ <sd-select-item [value]="'log'">
107
+ <sd-label [theme]="'gray'">log</sd-label>
108
+ </sd-select-item>
109
+ </sd-select>
110
+ </div>
111
+ <div>
112
+ <label>검색어</label>
113
+ <sd-textfield
114
+ [type]="'text'"
115
+ [(value)]="filter().searchText"
116
+ (valueChange)="mark(filter)"
117
+ />
118
+ </div>
119
+ <div>
120
+ <label>사용자</label>
121
+ <sd-shared-data-select
122
+ [selectMode]="'multi'"
123
+ [items]="shared{{userEntityPascal}}s.items()"
124
+ [(value)]="filter().{{userEntityCamel}}Ids"
125
+ (valueChange)="mark(filter)"
126
+ >
127
+ <ng-template [itemOf]="shared{{userEntityPascal}}s.items()" let-item="item">
128
+ \{{ item.name }}
129
+ </ng-template>
130
+ </sd-shared-data-select>
133
131
  </div>
134
132
  </ng-template>
135
133
 
@@ -1,4 +1,12 @@
1
- .claude
1
+ .claude/sd-*
2
+ .claude/references/sd-*
3
+ .claude/rules/sd-*
4
+ .claude/scripts/sd_*
5
+ .claude/skills/sd-*
6
+ .claude/skills/playwright-cli
7
+ .claude/skills/angular-developer
8
+ .claude/settings.json
9
+ .claude/simplysm.json
2
10
 
3
11
  .playwright-cli
4
12
  __pycache__